Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
585 lines
18 KiB
JavaScript
Executable file
585 lines
18 KiB
JavaScript
Executable file
/* global _, debug, Toolkit, Formatting, Common */
|
|
/* exported ChartUtils */
|
|
'use strict';
|
|
|
|
var ChartUtils = (function() {
|
|
var DEVICE_PIXEL_RATIO = window.devicePixelRatio || 1;
|
|
var DAY = 24 * 60 * 60 * 1000;
|
|
var NEVER_PERIOD = 30 * DAY;
|
|
var NEVER_ANCHOR = 21 * DAY;
|
|
var CHART_BG_RATIO = 0.87;
|
|
var FONT_SIZE = toDevicePixels(13);
|
|
var FONT_WEIGHT = '600';
|
|
var FONT_WEIGHT_AXIS = '400';
|
|
var WIFI_CHART_STROKE = '#8b9052';
|
|
var WIFI_CHART_FILL = '#cbd936';
|
|
var MOBILE_CHART_STROKE = '#762d4a';
|
|
var MOBILE_CHART_FILL = 'rgba(147, 21, 98, 0.7)';
|
|
|
|
function toDevicePixels(value) {
|
|
return value * DEVICE_PIXEL_RATIO;
|
|
}
|
|
|
|
function makeCSSFontString(fontSize, fontWeight) {
|
|
return fontWeight + ' ' + fontSize + 'px sans-serif';
|
|
}
|
|
|
|
function formatChartDate(date) {
|
|
return _('verbose-chart-date-format', {
|
|
'monthday-number': date.getDate(),
|
|
'em-month': _('month-' + date.getMonth() + '-short')
|
|
});
|
|
}
|
|
|
|
function getLimitInBytes(settings) {
|
|
var multiplier = 1000000; // MB
|
|
if (settings.dataLimitUnit === 'GB') {
|
|
multiplier = 1000000000; // GB
|
|
}
|
|
|
|
var value = settings.dataLimitValue;
|
|
return (value && value !== 0) ? value * multiplier : null;
|
|
}
|
|
|
|
function calculateUpperDate(settings) {
|
|
var today = new Date();
|
|
var trackingPeriod = settings.trackingPeriod;
|
|
var nextReset = settings.nextReset;
|
|
if (trackingPeriod !== 'never' && nextReset) {
|
|
return new Date(nextReset.getTime() - DAY);
|
|
}
|
|
|
|
var lastReset = settings.lastCompleteDataReset || today;
|
|
var offset = today.getTime() - lastReset.getTime();
|
|
var upperDate = new Date(lastReset.getTime() + NEVER_PERIOD);
|
|
if (offset >= NEVER_ANCHOR) {
|
|
upperDate = new Date(today.getTime() + (NEVER_PERIOD - NEVER_ANCHOR));
|
|
}
|
|
|
|
debug('Upper date:', upperDate);
|
|
return upperDate;
|
|
}
|
|
|
|
function calculateLowerDate(settings) {
|
|
var today = new Date();
|
|
var lowerDate = Toolkit.toMidnight(new Date());
|
|
var nextReset = settings.nextReset || lowerDate;
|
|
var trackingPeriod = settings.trackingPeriod;
|
|
|
|
if (trackingPeriod === 'weekly') {
|
|
lowerDate.setTime(nextReset.getTime() - (7 * DAY));
|
|
} else if (trackingPeriod === 'monthly') {
|
|
var monthDate = settings.resetTime;
|
|
var isFirstDayOfPeriod = today.getDate() == monthDate;
|
|
var isAfterFirstDayOfPeriod = today.getDate() > monthDate;
|
|
if (isAfterFirstDayOfPeriod) {
|
|
// lowerDate is in the current month
|
|
lowerDate.setDate(monthDate);
|
|
} else if (!isFirstDayOfPeriod) {
|
|
// lowerDate is on the previous month
|
|
var LAST_DAY_OF_PREVIOUS_MONTH = 0;
|
|
var newMonth = today.getMonth() - 1;
|
|
var newYear = today.getFullYear();
|
|
if (newMonth < 0) {
|
|
newMonth = 11;
|
|
newYear--;
|
|
}
|
|
|
|
lowerDate = Toolkit.toMidnight(new Date(newYear, newMonth, monthDate));
|
|
// The day of the month of lowerDate is different to settings.resetTime
|
|
// value when the resetTime day doesn't exists this month, (eg. 30th
|
|
// February), on this case, the lowerDate must be the last day of the
|
|
// previous month
|
|
if (lowerDate.getDate() != monthDate) {
|
|
lowerDate = Toolkit.toMidnight(new Date());
|
|
lowerDate.setDate(LAST_DAY_OF_PREVIOUS_MONTH);
|
|
}
|
|
}
|
|
} else {
|
|
var lastReset = settings.lastCompleteDataReset || lowerDate;
|
|
lowerDate = lastReset;
|
|
var offset = today.getTime() - lastReset.getTime();
|
|
if (offset >= NEVER_ANCHOR) {
|
|
lowerDate = new Date(today.getTime() - NEVER_ANCHOR);
|
|
}
|
|
}
|
|
|
|
debug('Lower date:', lowerDate);
|
|
return lowerDate;
|
|
}
|
|
|
|
// Expand the model with some computed values
|
|
function expandModel(base) {
|
|
|
|
// Update today
|
|
var today = Toolkit.toMidnight(new Date());
|
|
|
|
// Graphic settings
|
|
base.originY = Math.floor(base.height * CHART_BG_RATIO);
|
|
|
|
// Today value
|
|
base.axis.X.today = today;
|
|
|
|
// Normalize today
|
|
Toolkit.toMidnight(base.axis.X.today);
|
|
Toolkit.toMidnight(base.axis.X.lower);
|
|
Toolkit.toMidnight(base.axis.X.upper);
|
|
|
|
// X axis projection function to convert a value into a pixel value
|
|
var xLowerBound = base.axis.X.lower.getTime();
|
|
var xSize = base.axis.X.upper.getTime() - xLowerBound;
|
|
var realWidth = base.endX - base.originX;
|
|
base.axis.X.len = realWidth;
|
|
base.axis.X.get = function cc_dataToXPx(value) {
|
|
var projection = (value.getTime() - xLowerBound) / xSize;
|
|
return projection * realWidth + base.originX;
|
|
};
|
|
|
|
// Y max value
|
|
var limitEnabled = true; // XXX: model.limits.enabled;
|
|
base.axis.Y.maxValue = Math.max(limitEnabled ? base.limits.value : 0,
|
|
base.data.mobile.total,
|
|
base.data.wifi.total);
|
|
|
|
// Y axis projection function and automatic values
|
|
base.axis.Y.range = base.axis.Y.upper - base.axis.Y.lower;
|
|
base.axis.Y.upper = (1 + base.axis.Y.margin) * base.axis.Y.maxValue;
|
|
var yLowerBound = base.axis.Y.lower;
|
|
var ySize = base.axis.Y.upper - yLowerBound;
|
|
base.axis.Y.get = function cc_dataToYPx(value) {
|
|
var projection = (value - yLowerBound) / ySize;
|
|
return base.originY * (1 - projection);
|
|
};
|
|
base.axis.Y.step = base.axis.Y.get(base.axis.Y.maxValue);
|
|
|
|
// Limits
|
|
base.limits.warning = Common.DATA_USAGE_WARNING;
|
|
base.limits.warningValue = base.limits.value * Common.DATA_USAGE_WARNING;
|
|
}
|
|
|
|
// Check if the segment of the graph is inside chart area. If so, draw it
|
|
function clipAndDrawSegment(ctx, model, x0, y0, x1, y1) {
|
|
if (x0 >= model.originX && x1 <= model.endX) {
|
|
var x0Fixed = Math.floor(x0) - 0.5;
|
|
var x1Fixed = Math.floor(x1) + 0.5;
|
|
|
|
// Fill
|
|
ctx.globalCompositeOperation = 'destination-over';
|
|
ctx.beginPath();
|
|
ctx.lineTo(x1Fixed, y1);
|
|
ctx.lineTo(x1Fixed, model.originY);
|
|
ctx.lineTo(x0Fixed, model.originY);
|
|
ctx.lineTo(x0Fixed, y0);
|
|
ctx.moveTo(x1Fixed, y0);
|
|
ctx.fill();
|
|
|
|
// Stroke
|
|
ctx.globalCompositeOperation = 'source-over';
|
|
ctx.beginPath();
|
|
// It's necessary add 1 to the X coordinate to make up for the offset
|
|
// produced with the drawing of the previous line.
|
|
ctx.moveTo(x0Fixed + 1, y0);
|
|
ctx.lineTo(x1Fixed, y1);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
function drawTodayMark(ctx, x, y, color) {
|
|
ctx.save();
|
|
var radius = toDevicePixels(4);
|
|
ctx.fillStyle = color;
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, radius, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
ctx.restore();
|
|
}
|
|
|
|
function drawBackgroundLayer(canvas, model, showMobile) {
|
|
canvas.height = model.height;
|
|
var width = canvas.width = model.width;
|
|
var ctx = canvas.getContext('2d');
|
|
|
|
ctx.save();
|
|
|
|
// White bg
|
|
ctx.fillStyle = 'white';
|
|
ctx.fillRect(0, 0, width, model.originY);
|
|
|
|
// Horizontal lines every step
|
|
var step = model.axis.Y.step;
|
|
var limitY = model.axis.Y.get(model.limits.value);
|
|
ctx.strokeStyle = '#e0e0e0';
|
|
ctx.lineWidth = toDevicePixels(1);
|
|
var displayLimit = model.limits.enabled && showMobile;
|
|
for (var y = model.originY - step; y > step; y -= step) {
|
|
if (displayLimit && same(y, limitY, 0.1)) {
|
|
continue;
|
|
}
|
|
var drawY = Math.floor(y) - 0.5;
|
|
ctx.beginPath();
|
|
ctx.moveTo(model.originX, drawY);
|
|
ctx.lineTo(model.endX, drawY);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// Vertical lines every day
|
|
var days = (model.axis.X.upper - model.axis.X.lower) / DAY;
|
|
step = model.axis.X.len / days;
|
|
ctx.strokeStyle = '#eeeeee';
|
|
ctx.lineWidth = toDevicePixels(1);
|
|
for (var x = model.originX; x <= model.endX; x += step) {
|
|
var drawX = Math.floor(x) + 0.5;
|
|
ctx.beginPath();
|
|
ctx.moveTo(drawX, model.originY);
|
|
ctx.lineTo(drawX, 0);
|
|
ctx.stroke();
|
|
// Ensure draw the last vertical line
|
|
if (((x + step) > model.endX) && (x !== model.endX)) {
|
|
// the 0.5 offset is needed to avoid drawing a double line.
|
|
drawX = model.endX - 0.5;
|
|
ctx.beginPath();
|
|
ctx.moveTo(drawX, model.originY);
|
|
ctx.lineTo(drawX, 0);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
ctx.restore();
|
|
|
|
// Return true if a and b not differ more than threshold
|
|
function same(a, b, threshold) {
|
|
threshold = threshold || 0;
|
|
return Math.abs(a - b) <= threshold;
|
|
}
|
|
}
|
|
|
|
function drawTodayLayer(canvas, model) {
|
|
canvas.height = model.height;
|
|
canvas.width = model.width;
|
|
var ctx = canvas.getContext('2d');
|
|
|
|
// Compute the X offset
|
|
var offsetX = model.axis.X.get(model.axis.X.today);
|
|
|
|
// Configure Centered today text
|
|
var marginTop = toDevicePixels(10);
|
|
|
|
var todayTag = formatChartDate(model.axis.X.today);
|
|
|
|
// Render the text
|
|
ctx.font = makeCSSFontString(FONT_SIZE, FONT_WEIGHT);
|
|
ctx.textAlign = 'center';
|
|
ctx.textBaseline = 'top';
|
|
|
|
// Update measues of today label
|
|
var metrics = ctx.measureText(todayTag);
|
|
model.todayLabel.x0 = offsetX - metrics.width / 2;
|
|
model.todayLabel.x1 = offsetX + metrics.width / 2;
|
|
if (model.todayLabel.x1 > model.endX) {
|
|
ctx.textAlign = 'right';
|
|
}
|
|
|
|
ctx.fillStyle = 'black';
|
|
ctx.fillText(todayTag, offsetX, model.originY + marginTop);
|
|
|
|
setAccessibilityAttributes(canvas, 'today-layer', { today: todayTag });
|
|
}
|
|
|
|
function drawAxisLayer(canvas, model, showMobile) {
|
|
canvas.height = model.height;
|
|
canvas.width = model.width;
|
|
var ctx = canvas.getContext('2d');
|
|
|
|
// Start drawing Y axis
|
|
var step = model.axis.Y.step;
|
|
var dataStep = model.axis.Y.upper - model.axis.Y.maxValue;
|
|
var marginRight = 4;
|
|
var offsetX = model.originX - marginRight;
|
|
ctx.font = makeCSSFontString(FONT_SIZE, FONT_WEIGHT_AXIS);
|
|
ctx.textAlign = 'right';
|
|
var displayLimit = showMobile && model.limits.enabled;
|
|
var lastUnit;
|
|
for (var y = 0.5 + model.originY, value = 0;
|
|
y > step; y -= step, value += dataStep) {
|
|
|
|
// First X label for 0 is aligned with the bottom
|
|
if (value === 0) {
|
|
lastUnit = Formatting.smartRound(dataStep, 0)[1];
|
|
ctx.textBaseline = 'bottom';
|
|
ctx.fillStyle = '#6a6a6a';
|
|
ctx.fillText(Formatting.formatData([0, lastUnit]), offsetX, y - 2.5);
|
|
continue;
|
|
}
|
|
|
|
// Rest of labels are aligned with the middle
|
|
var rounded = Formatting.smartRound(value, -1);
|
|
var v = rounded[0];
|
|
var u = rounded[1];
|
|
var label = v;
|
|
if (lastUnit !== u) {
|
|
label = Formatting.formatData([v, u]);
|
|
}
|
|
|
|
lastUnit = u;
|
|
ctx.textBaseline = 'middle';
|
|
ctx.fillStyle = displayLimit &&
|
|
(value === model.limits.value) &&
|
|
model.limits.enabled ?
|
|
'#b50202' : '#6a6a6a';
|
|
ctx.fillText(label, offsetX, y);
|
|
}
|
|
|
|
// Now the X axis
|
|
ctx.fillStyle = '#6a6a6a';
|
|
var marginTop = toDevicePixels(10);
|
|
|
|
// Left tag
|
|
var leftTag = formatChartDate(model.axis.X.lower);
|
|
ctx.font = makeCSSFontString(FONT_SIZE, FONT_WEIGHT);
|
|
ctx.textBaseline = 'top';
|
|
ctx.textAlign = 'left';
|
|
|
|
var isBelowToday = model.todayLabel.x0 <=
|
|
model.originX + ctx.measureText(leftTag).width;
|
|
if (!isBelowToday) {
|
|
ctx.fillText(leftTag, model.originX, model.originY + marginTop);
|
|
}
|
|
|
|
// Right tag
|
|
var rightTag = formatChartDate(model.axis.X.upper);
|
|
ctx.textAlign = 'right';
|
|
|
|
isBelowToday = model.todayLabel.x1 >=
|
|
model.endX - ctx.measureText(rightTag).width;
|
|
if (!isBelowToday) {
|
|
ctx.fillText(rightTag, model.endX, model.originY + marginTop);
|
|
}
|
|
|
|
setAccessibilityAttributes(canvas, 'axis-layer',
|
|
{ from: leftTag, to: rightTag });
|
|
}
|
|
|
|
function setAccessibilityAttributes(element, identifier, data) {
|
|
if (identifier) {
|
|
element.setAttribute('data-l10n-id', identifier);
|
|
if (data) {
|
|
element.setAttribute('data-l10n-args', JSON.stringify(data));
|
|
}
|
|
} else {
|
|
element.removeAttribute('data-l10n-id');
|
|
element.removeAttribute('data-l10n-args');
|
|
element.removeAttribute('aria-label');
|
|
}
|
|
}
|
|
|
|
function drawLimits(canvas, model, showMobile) {
|
|
var set = model.limits.value;
|
|
var color = '#b50202';
|
|
|
|
canvas.height = model.height;
|
|
canvas.width = model.width;
|
|
var ctx = canvas.getContext('2d');
|
|
|
|
var displayLimit = showMobile && model.limits.enabled;
|
|
if (!displayLimit) {
|
|
return;
|
|
}
|
|
|
|
ctx.save();
|
|
|
|
var marginTop = 1;
|
|
var offsetY = set ? model.axis.Y.get(model.limits.value) :
|
|
FONT_SIZE + 2 * marginTop;
|
|
|
|
ctx.font = makeCSSFontString(FONT_SIZE, FONT_WEIGHT);
|
|
|
|
// The dashed limit line
|
|
var lineLength = toDevicePixels(15);
|
|
var gapLength = toDevicePixels(7);
|
|
ctx.strokeStyle = color;
|
|
ctx.lineWidth = toDevicePixels(1);
|
|
ctx.beginPath();
|
|
for (var x = model.originX, drawY = Math.floor(offsetY) - 0.5;
|
|
x < model.endX; x += gapLength) {
|
|
ctx.moveTo(x, drawY);
|
|
ctx.lineTo(Math.min(x += lineLength, model.endX), drawY);
|
|
}
|
|
ctx.stroke();
|
|
|
|
ctx.restore();
|
|
|
|
setAccessibilityAttributes(canvas,
|
|
'limit-layer-' + model.limits.dataLimitUnit,
|
|
{ value: model.limits.dataLimitValue });
|
|
}
|
|
|
|
function drawDataLayer(canvas, model, sampleType, style) {
|
|
var samples = (model.data[sampleType] || {}).samples;
|
|
if (!samples) {
|
|
return;
|
|
}
|
|
|
|
canvas.height = model.height;
|
|
canvas.width = model.width;
|
|
var ctx = canvas.getContext('2d');
|
|
|
|
// Style
|
|
ctx.fillStyle = style.fill;
|
|
ctx.strokeStyle = style.stroke;
|
|
ctx.lineWidth = toDevicePixels(2);
|
|
ctx.lineJoin = 'round';
|
|
ctx.moveTo(model.originX, model.originY);
|
|
|
|
var today = Toolkit.toMidnight(new Date());
|
|
// offset in milliseconds
|
|
var offset = today.getTimezoneOffset() * 60 * 1000;
|
|
|
|
var sum = 0; var x, y = model.originY;
|
|
var lastX = model.originX, lastY = model.axis.Y.get(sum);
|
|
// Only dealing with the use cases of negative UTC offset because, due to
|
|
// the database granularity, the returned samples are given in UTC days, and
|
|
// we don't know what part of the traffic sample values correspond to the
|
|
// current localtime day. The query for the samples is generated on the
|
|
// costcontrol module, and the case into which the returned samples only
|
|
// have one record with the data for the localtime day of yesterday is not
|
|
// possible.
|
|
|
|
for (var i = 0, len = samples.length; i < len; i++) {
|
|
var sample = samples[i];
|
|
var sampleLocalTime = sample.date.getTime() + offset;
|
|
var sampleUTCDate = Toolkit.toMidnight(new Date(sampleLocalTime));
|
|
|
|
var isToday = (today.getTime() === sampleUTCDate.getTime());
|
|
var isTomorrow = (today.getTime() + DAY === sampleUTCDate.getTime());
|
|
var thereIsATomorrowSample = (isToday && (i + 2 === len));
|
|
// Depends on the hour of the day and the offset, it is possible the
|
|
// networkStats API returns the current data mobile in the tomorrow
|
|
// sample, because on the UTC hour is another day.
|
|
if (thereIsATomorrowSample) {
|
|
// Join the value of the samples for today and tomorrow
|
|
var tomorrowSample = samples[i+1];
|
|
if (typeof sample.value === 'undefined') {
|
|
sample.value = tomorrowSample.value;
|
|
} else if (typeof tomorrowSample.value !== 'undefined') {
|
|
sample.value += tomorrowSample.value;
|
|
}
|
|
|
|
if (i === 0) {
|
|
lastX = model.axis.X.get(sample.date);
|
|
}
|
|
i++;
|
|
}
|
|
|
|
if (typeof sample.value === 'undefined') {
|
|
lastX = x = model.axis.X.get(sample.date);
|
|
ctx.moveTo(x, y);
|
|
|
|
} else {
|
|
if (i === 0) {
|
|
lastX = model.axis.X.get(sample.date);
|
|
}
|
|
|
|
sum += sample.value;
|
|
x = model.axis.X.get(sample.date);
|
|
y = model.axis.Y.get(sum);
|
|
|
|
clipAndDrawSegment(ctx, model, lastX, lastY, x, y);
|
|
|
|
lastX = x;
|
|
lastY = y;
|
|
}
|
|
|
|
var onlyExistTomorrowSample = (i===0 && isTomorrow);
|
|
var isXInsideTheGraph = (x >= model.originX);
|
|
if ((isToday || onlyExistTomorrowSample) && isXInsideTheGraph) {
|
|
drawTodayMark(ctx, x, y, style.stroke);
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!style.pattern) {
|
|
return;
|
|
}
|
|
|
|
var pattern = ctx.createPattern(style.pattern, 'repeat');
|
|
ctx.globalCompositeOperation = 'source-atop';
|
|
ctx.fillStyle = pattern;
|
|
ctx.fillRect(0, 0, model.width, model.originY);
|
|
}
|
|
|
|
function drawWarningLayer(canvas, model) {
|
|
canvas.height = model.height;
|
|
canvas.width = model.width;
|
|
var ctx = canvas.getContext('2d');
|
|
|
|
if (!model.limits.enabled || model.limits.value === null) {
|
|
setAccessibilityAttributes(canvas);
|
|
return;
|
|
}
|
|
|
|
// No problem here
|
|
var mobileUsage = model.data.mobile.total;
|
|
var mobileUsageDifference = mobileUsage - model.limits.value;
|
|
if (mobileUsage <= model.limits.warningValue) {
|
|
setAccessibilityAttributes(canvas);
|
|
return;
|
|
}
|
|
|
|
// Warning mode
|
|
if (mobileUsage <= model.limits.value) {
|
|
var limitValue = Math.floor(model.axis.Y.get(model.limits.value));
|
|
var warningValue =
|
|
Math.round(model.axis.Y.get(model.limits.warningValue));
|
|
ctx.beginPath();
|
|
ctx.fillStyle = 'rgba(255, 112, 0, 0.5)';
|
|
ctx.fillRect(
|
|
model.originX, limitValue,
|
|
model.axis.X.len + 0.5, warningValue - limitValue
|
|
);
|
|
|
|
setAccessibilityAttributes(canvas, 'warning-layer', {
|
|
value: Formatting.formatData(Formatting.roundData(
|
|
-mobileUsageDifference))
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
// Limit exceeded
|
|
var limitValueExceeded = model.axis.Y.get(model.limits.value);
|
|
ctx.beginPath();
|
|
ctx.fillStyle = 'rgba(255, 0, 0, 0.3)';
|
|
ctx.fillRect(
|
|
model.originX, 0,
|
|
model.axis.X.len + 0.5, limitValueExceeded
|
|
);
|
|
setAccessibilityAttributes(canvas, 'limit-exceeded-layer', {
|
|
value: Formatting.formatData(Formatting.roundData(mobileUsageDifference))
|
|
});
|
|
}
|
|
|
|
return {
|
|
// Constants
|
|
WIFI_CHART_STROKE: WIFI_CHART_STROKE,
|
|
WIFI_CHART_FILL: WIFI_CHART_FILL,
|
|
MOBILE_CHART_STROKE: MOBILE_CHART_STROKE,
|
|
MOBILE_CHART_FILL: MOBILE_CHART_FILL,
|
|
|
|
// Functions
|
|
toDevicePixels: toDevicePixels,
|
|
makeCSSFontString: makeCSSFontString,
|
|
formatChartDate: formatChartDate,
|
|
getLimitInBytes: getLimitInBytes,
|
|
calculateUpperDate: calculateUpperDate,
|
|
calculateLowerDate: calculateLowerDate,
|
|
expandModel: expandModel,
|
|
clipAndDrawSegment: clipAndDrawSegment,
|
|
drawTodayMark: drawTodayMark,
|
|
drawBackgroundLayer: drawBackgroundLayer,
|
|
drawTodayLayer: drawTodayLayer,
|
|
drawAxisLayer: drawAxisLayer,
|
|
drawLimits: drawLimits,
|
|
drawDataLayer: drawDataLayer,
|
|
drawWarningLayer: drawWarningLayer
|
|
};
|
|
}());
|