Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
585 lines
18 KiB
JavaScript
585 lines
18 KiB
JavaScript
/* 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
|
|
};
|
|
}());
|