transcend-charts
Version:
Transcend is a charting and graph library for NUVI
1,037 lines (929 loc) • 44 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _lodash = require('lodash');
var _lodash2 = _interopRequireDefault(_lodash);
var _Numbers = require('../helpers/Numbers');
var _Numbers2 = _interopRequireDefault(_Numbers);
var _Charts = require('../helpers/Charts');
var _Charts2 = _interopRequireDefault(_Charts);
var _Color = require('./Color');
var _Color2 = _interopRequireDefault(_Color);
var _moment = require('moment');
var _moment2 = _interopRequireDefault(_moment);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var LineGraph = function LineGraph(htmlContainer, graphData, graphOptions) {
var htmlCanvas = null;
var lineSeriesArray = [];
var pxRatio = window.devicePixelRatio || 1;
var DEBUG = false;
var fullScreenChangeListener = void 0;
var webkitFullScreenChangeListener = void 0;
var mozFullScreenChangeListener = void 0;
var msFullScreenChangeListener = void 0;
var resizeListener = void 0;
var isDestroyed = false;
var resizeTimer = null;
var needsRender = false;
var dateRangeChangedCallback = null;
var hoverChangedCallback = null;
var lastFrame = new Date().getTime(); // the timestamp of the last time the frame was last rendered
var fps = 0;
// define variables used to calculate things
var labelHeight = 1;
var gridLineWidth = 1; // the width of each grid line
var notchWidth = 5; // the width of the little notch on the y-axis next to the labels
var notchPadding = 3; // the space between the notch and the label on the y axis
var dotRadius = 3; // the radius in px of circles drawn on data points in the line graph
var tooltipPadding = 12; // the padding for the tooltip callout used on mouseover
var tooltipDatePadding = 6; // the padding between the date in the tooltip and the legend
var colorbarSpacing = 5; // the pixel spacing between a color swatch on the legend and a label
var legendOutlineWidth = 1; // the width of the stroke around the legend
var legendOutlineCornerSize = 8; // the width of the little corner notches around the legend
var plotVars = {
graphArea: { top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0 }, // the size of the plot area on the graph relative to the bounds of the canvas (e.g. bottom is the number of px from the bottom of the canvas)
minX: 0, // the minimum x value of all lineSeriesArray
maxX: 0, // the maximum x value of all lineSeriesArray
minY: 0, // the minimum y value of all lineSeriesArray
maxY: 0, // the maximum y value of all lineSeriesArray (usually rounded up for nice labeling)
chartMinY: 0, // the minimum y value on the graph (usually 0)
chartMaxY: 0, // the maximum y value on the graph (usually the nice, round number just above the maxY)
highlightedX: null, // which vertical of data is highlighted (if any) due to mouseover
highlightedXStart: null, // the start date of a highlighted range of data
highlightedXEnd: null }; // this const is updated regularly in the render function to ensure it always contains correct data about the size and state of the graph
this.render = function (data) {
if (hoverChangedCallback) {
htmlCanvas.style.cursor = 'col-resize';
} else if (graphOptions.cursor === 'pointer') {
htmlCanvas.style.cursor = 'pointer';
} else {
htmlCanvas.style.cursor = 'default';
}
if (data) {
this.setupData(data);
}
// If there's no HTML canvas made something went horribly wrong. Abort!
if (!htmlCanvas) {
return;
}
// Get the context of the canvas
var ctx = htmlCanvas.getContext('2d');
// upscale this thang if the device pixel ratio is higher than 1
var backingStoreRatio = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1;
ctx.save();
if (pxRatio > 1) {
ctx.scale(pxRatio / backingStoreRatio, pxRatio / backingStoreRatio);
}
var canvasWidth = htmlCanvas.width / (pxRatio / backingStoreRatio);
var canvasHeight = htmlCanvas.height / (pxRatio / backingStoreRatio);
// clear the background, ready to re-render
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
if (this.options.backgroundColor.toString() !== 'transparent') {
ctx.fillStyle = this.options.backgroundColor.toString();
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
}
// Let's not go any further if there's no data to graph
if (!lineSeriesArray || !lineSeriesArray.length) {
return;
}
// define the plot area of the canvas that will have the actual graph on it (no labels or padding or anything)
plotVars.graphArea = {
top: this.options.padding,
right: this.options.padding,
bottom: this.options.padding,
left: this.options.padding
};
if (this.options.showDataPoints) {
plotVars.graphArea.right += dotRadius;
plotVars.graphArea.left += dotRadius;
}
if (this.options.showLabels) {
plotVars.graphArea.top += this.options.labelFontSize / 2;
plotVars.graphArea.bottom += labelHeight + this.options.labelFontSize / 2;
}
plotVars.graphArea.width = canvasWidth - plotVars.graphArea.left - plotVars.graphArea.right;
plotVars.graphArea.height = canvasHeight - plotVars.graphArea.bottom - plotVars.graphArea.top;
// get the max value in all data series
plotVars.maxY = undefined;
plotVars.minY = undefined;
for (var d = 0; d < lineSeriesArray.length; d++) {
var seriesmax = _lodash2.default.max(lineSeriesArray[d].data, function (dataPoint) {
return dataPoint.value;
}).value;
var seriesmin = _lodash2.default.min(lineSeriesArray[d].data, function (dataPoint) {
return dataPoint.value;
}).value;
if (plotVars.maxY === undefined || seriesmax > plotVars.maxY) {
plotVars.maxY = seriesmax;
}
if (plotVars.minY === undefined || seriesmin < plotVars.minY) {
plotVars.minY = seriesmin;
}
}
plotVars.chartMinY = Math.min(0, plotVars.minY); // someday we'll give the option of having a relative chart where the minimum isn't hard set at 0
var _HelpersCharts$yAxis = _Charts2.default.yAxis(plotVars.graphArea.height, plotVars.maxY * 1.1, plotVars.chartMinY, this.options.labelFontSize);
var numberOfLabels = _HelpersCharts$yAxis.numberOfLabels;
var valueBetweenLabels = _HelpersCharts$yAxis.valueBetweenLabels;
plotVars.chartMaxY = numberOfLabels * valueBetweenLabels + plotVars.chartMinY;
var interval = plotVars.graphArea.height / numberOfLabels;
var labelWidth = 0;
// calculate the width of the y-value labels and adjust the graphArea to accommodate the longest one
if (this.options.showLabels) {
ctx.font = this.options.labelFontSize + 'px ' + this.options.labelFontFamily;
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
for (var i = 0; i <= numberOfLabels; i++) {
var number = i * valueBetweenLabels + plotVars.chartMinY;
var lw = ctx.measureText(_Numbers2.default.formatNumber(number)).width;
if (lw > labelWidth) {
labelWidth = lw;
}
}
plotVars.graphArea.left = this.options.padding + labelWidth + notchWidth + notchPadding;
plotVars.graphArea.width = canvasWidth - plotVars.graphArea.left - plotVars.graphArea.right;
}
// calculate the x min and max
plotVars.minX = undefined;
plotVars.maxX = undefined;
for (var _d = 0; _d < lineSeriesArray.length; _d++) {
var seriesminX = _lodash2.default.min(lineSeriesArray[_d].data, function (dataPoint) {
return dataPoint.x;
}).x;
var seriesmaxX = _lodash2.default.max(lineSeriesArray[_d].data, function (dataPoint) {
return dataPoint.x;
}).x;
if (plotVars.maxX === undefined || seriesmaxX > plotVars.maxX) {
plotVars.maxX = seriesmaxX;
}
if (plotVars.minX === undefined || seriesminX < plotVars.minX) {
plotVars.minX = seriesminX;
}
}
// smash all x labels into one sorted array
var allLabels = lineSeriesArray.map(function (lineSeries) {
return lineSeries.data.map(function (datum) {
return { date: datum.x, datestr: datum.x };
});
});
allLabels = _lodash2.default.flatten(allLabels);
allLabels = _lodash2.default.uniq(allLabels, function (label) {
return label.date;
});
// draw alternating background colors (if any)
if (this.options.alternatingBackgroundColor.toString() !== this.options.backgroundColor.toString()) {
ctx.fillStyle = this.options.alternatingBackgroundColor.toString();
var preferredSegmentWidth = 0.15; // 15% of chart width
var preferredWidthOfInterval = preferredSegmentWidth * plotVars.graphArea.width;
var actualWidthOfInterval = plotVars.graphArea.width / (allLabels.length - 1);
var alternatingColorWidth = actualWidthOfInterval;
if (preferredWidthOfInterval > actualWidthOfInterval) {
alternatingColorWidth = actualWidthOfInterval * Math.round(preferredWidthOfInterval / actualWidthOfInterval);
}
var intervals = Math.ceil(plotVars.graphArea.width / alternatingColorWidth);
for (var _i = 0; _i < intervals; _i += 2) {
var awidth = alternatingColorWidth;
var ax = plotVars.graphArea.left + _i * alternatingColorWidth;
if (ax + awidth > plotVars.graphArea.left + plotVars.graphArea.width) {
awidth = plotVars.graphArea.left + plotVars.graphArea.width - ax;
}
ctx.fillRect(ax, plotVars.graphArea.top, awidth, plotVars.graphArea.height);
}
}
// draw the y value grid and labels
ctx.lineWidth = gridLineWidth;
ctx.strokeStyle = this.options.gridLineColor.toString();
ctx.fillStyle = this.options.labelFontColor.toString();
// y-value lines
for (var _i2 = 0; _i2 <= numberOfLabels; _i2++) {
ctx.beginPath();
ctx.moveTo(plotVars.graphArea.left - notchWidth, plotVars.graphArea.top + plotVars.graphArea.height - _i2 * interval);
ctx.lineTo(plotVars.graphArea.left + plotVars.graphArea.width, plotVars.graphArea.top + plotVars.graphArea.height - _i2 * interval);
ctx.stroke();
// label text
if (this.options.showLabels) {
ctx.fillText(_Numbers2.default.formatNumber(_i2 * valueBetweenLabels + plotVars.chartMinY), plotVars.graphArea.left - notchWidth - notchPadding, plotVars.graphArea.top + plotVars.graphArea.height - _i2 * interval);
}
}
// draw the x axis
ctx.beginPath();
ctx.moveTo(plotVars.graphArea.left, plotVars.graphArea.top);
ctx.lineTo(plotVars.graphArea.left, plotVars.graphArea.top + plotVars.graphArea.height);
ctx.stroke();
// labels for the x axis
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
if (this.options.showLabels) {
// iterate the array and create the date string for the label
for (var _i3 = 0; _i3 < allLabels.length; _i3++) {
var dt = new Date(allLabels[_i3].date);
allLabels[_i3].datestr = dt.getMonth() + 1 + '/' + dt.getDate();
if (this.options.dateInterval === 'hour') {
var hr = dt.getHours();
var ampm = 'am';
if (hr === 12) {
ampm = 'pm';
}
if (hr > 12) {
hr -= 12;
ampm = 'pm';
}
allLabels[_i3].datestr = hr + ':00' + ampm;
} else if (this.options.dateInterval === 'minute') {
allLabels[_i3].datestr = ':' + dt.getMinutes();
} else if (this.options.dateInterval === 'month') {
allLabels[_i3].datestr = dt.getMonth() + 1;
} else if (this.options.dateInterval === 'year') {
allLabels[_i3].datestr = dt.getFullYear();
}
}
// iterate the array until we find a number of labels that actually fits on the graph
var everyNthLabel = 1; // start with every label, then every other, then every 3rd, then every 4th...
var willFit = false;
while (!willFit) {
var allowableLabelWidth = plotVars.graphArea.width / Math.floor(allLabels.length / everyNthLabel) * 0.8;
willFit = true;
for (var _i4 = 0; _i4 < allLabels.length; _i4 += everyNthLabel) {
if (ctx.measureText(allLabels[_i4].datestr).width > allowableLabelWidth) {
willFit = false;
everyNthLabel++;
break;
}
}
if (everyNthLabel > allLabels.length) {
break; // something went horribly wrong because none of the labels fit at all
}
}
// iterate the array of labels and render the ones that will fit
for (var _i5 = 0; _i5 < allLabels.length; _i5 += everyNthLabel) {
if (allLabels[_i5].date === plotVars.minX && allLabels.length > 1) {
ctx.textAlign = 'left';
} else if (allLabels[_i5].date === plotVars.maxX && allLabels.length > 1) {
ctx.textAlign = 'right';
var _allowableLabelWidth = plotVars.graphArea.width / Math.floor(allLabels.length / everyNthLabel);
if (ctx.measureText(allLabels[_i5].datestr).width > _allowableLabelWidth / 2) {
continue;
}
} else {
ctx.textAlign = 'center';
}
var labelX = plotVars.maxX - plotVars.minX === 0 ? plotVars.graphArea.left + plotVars.graphArea.width / 2 : plotVars.graphArea.left + (allLabels[_i5].date - plotVars.minX) / (plotVars.maxX - plotVars.minX) * plotVars.graphArea.width;
var labelY = plotVars.graphArea.top + plotVars.graphArea.height + 2;
ctx.fillText(allLabels[_i5].datestr, labelX, labelY);
}
}
// render date range highlight (if any)
if (plotVars.highlightedXStart && plotVars.highlightedXEnd) {
var startTime = _lodash2.default.min([plotVars.highlightedXStart, plotVars.highlightedXEnd]);
var endTime = _lodash2.default.max([plotVars.highlightedXStart, plotVars.highlightedXEnd]);
ctx.fillStyle = this.options.highlightedDateRangeColor.toString();
var startX = plotVars.graphArea.left + (startTime - plotVars.minX) / (plotVars.maxX - plotVars.minX) * plotVars.graphArea.width;
var endX = plotVars.graphArea.left + (endTime - plotVars.minX) / (plotVars.maxX - plotVars.minX) * plotVars.graphArea.width;
var width = endX - startX;
ctx.fillRect(startX, plotVars.graphArea.top, width, plotVars.graphArea.height);
}
// render highlight of mouseover (if any). This is intentionally placed below the series lines so it's more of an 'underlight' than a highlight
if (plotVars.highlightedX) {
ctx.lineWidth = dotRadius * 2;
ctx.strokeStyle = this.options.legendBackgroundColor.toString();
ctx.beginPath();
ctx.moveTo(plotVars.graphArea.left + (plotVars.highlightedX - plotVars.minX) / (plotVars.maxX - plotVars.minX) * plotVars.graphArea.width, plotVars.graphArea.top);
ctx.lineTo(plotVars.graphArea.left + (plotVars.highlightedX - plotVars.minX) / (plotVars.maxX - plotVars.minX) * plotVars.graphArea.width, plotVars.graphArea.top + plotVars.graphArea.height);
ctx.stroke();
}
// render the series lines
ctx.lineWidth = this.options.lineWidth;
for (var _d2 = 0; _d2 < lineSeriesArray.length; _d2++) {
// the line
ctx.beginPath();
ctx.strokeStyle = lineSeriesArray[_d2].color.toString();
// determine the fillStyle
var fillStyle = undefined;
if (lineSeriesArray[_d2].fillColor) {
fillStyle = new _Color2.default.Color(lineSeriesArray[_d2].fillColor).toString();
} else if (this.options.fillStyle === 'gradient') {
var color = new _Color2.default.Color(lineSeriesArray[_d2].color);
color.fade(0.1);
var seriesMaxY = _lodash2.default.max(lineSeriesArray[_d2].data, function (obj) {
return obj.value;
}).value;
var ySize = plotVars.chartMaxY - plotVars.chartMinY;
var maxYInPixels = ySize === 0 ? 0 : (seriesMaxY - plotVars.chartMinY) / ySize * plotVars.graphArea.height;
var grd = ctx.createLinearGradient(0, plotVars.graphArea.top + plotVars.graphArea.height - maxYInPixels, 0, plotVars.graphArea.top + plotVars.graphArea.height);
grd.addColorStop(0, color.toString());
grd.addColorStop(1, 'transparent');
fillStyle = grd;
} else if (this.options.fillStyle === 'solid') {
fillStyle = lineSeriesArray[_d2].color;
} else if (this.options.fillStyle === 'translucent') {
var _color = new _Color2.default.Color(lineSeriesArray[_d2].color);
_color.fade(0.5);
fillStyle = _color.toString();
}
// draw the line
if (lineSeriesArray[_d2].data.length > 1) {
for (var _i6 = 0; _i6 < lineSeriesArray[_d2].data.length; _i6++) {
var x = plotVars.maxX - plotVars.minX === 0 ? plotVars.graphArea.left + plotVars.graphArea.width / 2 : (lineSeriesArray[_d2].data[_i6].x - plotVars.minX) / (plotVars.maxX - plotVars.minX) * plotVars.graphArea.width;
var y = (lineSeriesArray[_d2].data[_i6].value - plotVars.chartMinY) / (plotVars.chartMaxY - plotVars.chartMinY) * plotVars.graphArea.height;
if (_i6 === 0) {
ctx.moveTo(plotVars.graphArea.left + x, plotVars.graphArea.top + plotVars.graphArea.height - y);
} else {
if (this.options.useBezierLines) {
var prevX = (lineSeriesArray[_d2].data[_i6 - 1].x - plotVars.minX) / (plotVars.maxX - plotVars.minX) * plotVars.graphArea.width;
var prevY = (lineSeriesArray[_d2].data[_i6 - 1].value - plotVars.chartMinY) / (plotVars.chartMaxY - plotVars.chartMinY) * plotVars.graphArea.height;
var histogramWidth = plotVars.graphArea.width / allLabels.length;
ctx.bezierCurveTo(plotVars.graphArea.left + prevX + histogramWidth * 0.5, plotVars.graphArea.top + plotVars.graphArea.height - prevY, // control point 1
plotVars.graphArea.left + x - histogramWidth * 0.5, plotVars.graphArea.top + plotVars.graphArea.height - y, // control point 2
plotVars.graphArea.left + x, plotVars.graphArea.top + plotVars.graphArea.height - y); // end
} else {
ctx.lineTo(plotVars.graphArea.left + x, plotVars.graphArea.top + plotVars.graphArea.height - y);
}
}
}
ctx.stroke();
}
if (fillStyle) {
ctx.fillStyle = fillStyle;
ctx.lineTo(plotVars.graphArea.left + plotVars.graphArea.width, plotVars.graphArea.top + plotVars.graphArea.height);
ctx.lineTo(plotVars.graphArea.left, plotVars.graphArea.top + plotVars.graphArea.height);
ctx.closePath();
ctx.fill();
}
// dots at each data point
if (this.options.showDataPoints || lineSeriesArray[_d2].data.length === 1) {
ctx.fillStyle = lineSeriesArray[_d2].color.toString();
for (var _i7 = 0; _i7 < lineSeriesArray[_d2].data.length; _i7++) {
var _x = plotVars.maxX - plotVars.minX === 0 ? plotVars.graphArea.left + plotVars.graphArea.width / 2 : plotVars.graphArea.left + (lineSeriesArray[_d2].data[_i7].x - plotVars.minX) / (plotVars.maxX - plotVars.minX) * plotVars.graphArea.width;
var _y = (lineSeriesArray[_d2].data[_i7].value - plotVars.chartMinY) / (plotVars.chartMaxY - plotVars.chartMinY) * plotVars.graphArea.height;
ctx.beginPath();
ctx.arc(_x, plotVars.graphArea.top + plotVars.graphArea.height - _y, dotRadius, 0, Math.PI * 2);
ctx.fill();
}
}
}
// render legend/tooltip for highlight of mouseover (if any)
if (plotVars.highlightedX) {
// build array of points with this x value (will determine height of legend)
var seriesPts = [];
for (var _d3 = 0; _d3 < lineSeriesArray.length; _d3++) {
for (var _i8 = 0; _i8 < lineSeriesArray[_d3].data.length; _i8++) {
if (lineSeriesArray[_d3].data[_i8].x === plotVars.highlightedX) {
seriesPts.push({
name: lineSeriesArray[_d3].name,
color: lineSeriesArray[_d3].color,
value: lineSeriesArray[_d3].data[_i8].value,
prefix: lineSeriesArray[_d3].data[_i8].prefix,
suffix: lineSeriesArray[_d3].data[_i8].suffix
});
}
}
}
// determine width of tooltip/legend
ctx.font = this.options.legendFontSize + 'px ' + this.options.legendFontFamily;
ctx.textBaseline = 'baseline';
ctx.textAlign = 'left';
var colorbarWidth = this.options.legendFontSize;
var widestValue = 0;
for (var _i9 = 0; _i9 < seriesPts.length; _i9++) {
var w = ctx.measureText(seriesPts[_i9].name + ': ' + seriesPts[_i9].prefix + _Numbers2.default.formatNumber(seriesPts[_i9].value) + seriesPts[_i9].suffix).width;
if (w > widestValue) {
widestValue = w;
}
}
var legendWidth = colorbarWidth + colorbarSpacing + widestValue + tooltipPadding * 2;
// determine height of legend/tooltip
var legendHeight = (seriesPts.length + 1) * (this.options.legendFontSize * 1.25) + tooltipPadding * 2 + tooltipDatePadding;
// determine where the legend/tooltip will fit
var dist = plotVars.highlightedX - plotVars.minX;
var mxDist = plotVars.maxX - plotVars.minX;
var hx = plotVars.maxX - plotVars.minX == 0 ? plotVars.graphArea.left + plotVars.graphArea.width : plotVars.graphArea.left + dist / mxDist * plotVars.graphArea.width;
var _x2 = hx + dotRadius * 2;
if (hx + legendWidth > plotVars.graphArea.left + plotVars.graphArea.width) {
_x2 = hx - dotRadius * 2 - legendWidth;
}
var _y2 = plotVars.graphArea.top + 5;
// draw legend/tooltip box
ctx.fillStyle = this.options.legendBackgroundColor.toString();
ctx.fillRect(_x2, _y2, legendWidth, legendHeight);
// draw four corners
ctx.strokeStyle = this.options.legendOutlineColor.toString();
ctx.lineWidth = legendOutlineWidth;
ctx.beginPath();
ctx.moveTo(_x2 + legendOutlineWidth, _y2 + legendOutlineCornerSize);
ctx.lineTo(_x2 + legendOutlineWidth, _y2 + legendOutlineWidth);
ctx.lineTo(_x2 + legendOutlineCornerSize, _y2 + legendOutlineWidth);
ctx.moveTo(_x2 + legendWidth - legendOutlineCornerSize, _y2 + legendOutlineWidth);
ctx.lineTo(_x2 + legendWidth - legendOutlineWidth, _y2 + legendOutlineWidth);
ctx.lineTo(_x2 + legendWidth - legendOutlineWidth, _y2 + legendOutlineCornerSize);
ctx.moveTo(_x2 + legendWidth - legendOutlineWidth, _y2 + legendHeight - legendOutlineCornerSize);
ctx.lineTo(_x2 + legendWidth - legendOutlineWidth, _y2 + legendHeight - legendOutlineWidth);
ctx.lineTo(_x2 + legendWidth - legendOutlineCornerSize, _y2 + legendHeight - legendOutlineWidth);
ctx.moveTo(_x2 + legendOutlineCornerSize, _y2 + legendHeight - legendOutlineWidth);
ctx.lineTo(_x2 + legendOutlineWidth, _y2 + legendHeight - legendOutlineWidth);
ctx.lineTo(_x2 + legendOutlineWidth, _y2 + legendHeight - legendOutlineCornerSize);
ctx.stroke();
// draw values for each series (one per line)
ctx.fillStyle = new _Color2.default.Color(this.options.legendFontColor).fade(0.3).toString();
var highlightedDateStr = '';
switch (this.options.dateInterval) {
case 'hour':
highlightedDateStr = (0, _moment2.default)(plotVars.highlightedX).format('MMM D, YYYY h:mma');
break;
case 'month':
highlightedDateStr = (0, _moment2.default)(plotVars.highlightedX).format('MMM YYYY');
break;
case 'year':
highlightedDateStr = (0, _moment2.default)(plotVars.highlightedX).format('YYYY');
break;
case 'day':
default:
highlightedDateStr = (0, _moment2.default)(plotVars.highlightedX).format('MMM D, YYYY');
break;
}
ctx.fillText(highlightedDateStr, _x2 + tooltipPadding, _y2 + tooltipPadding);
for (var _i10 = 0; _i10 < seriesPts.length; _i10++) {
// draw a color bar for reference
ctx.fillStyle = seriesPts[_i10].color;
ctx.fillRect(_x2 + tooltipPadding, _y2 + tooltipDatePadding + tooltipPadding + (_i10 + 1) * this.options.legendFontSize * 1.25, colorbarWidth, this.options.legendFontSize);
var serieslabelw = ctx.measureText(seriesPts[_i10].name + ': ').width;
// draw the number/value in the series color
ctx.fillText(seriesPts[_i10].prefix + _Numbers2.default.formatNumber(seriesPts[_i10].value) + seriesPts[_i10].suffix, _x2 + tooltipPadding + colorbarWidth + colorbarSpacing + serieslabelw, _y2 + tooltipDatePadding + tooltipPadding + (_i10 + 1) * this.options.legendFontSize * 1.25);
// draw the series label in a plain label color
ctx.fillStyle = this.options.legendFontColor.toString();
ctx.fillText(seriesPts[_i10].name + ': ', _x2 + tooltipPadding + colorbarWidth + colorbarSpacing, _y2 + tooltipDatePadding + tooltipPadding + (_i10 + 1) * this.options.legendFontSize * 1.25);
}
}
// draw fps
if (DEBUG) {
ctx.fillStyle = '#666666';
ctx.fillRect(canvasWidth - 40, canvasHeight - 15, 40, 15);
ctx.textAlign = 'right';
ctx.textBaseline = 'bottom';
ctx.fillStyle = '#000000';
ctx.font = '11px sans-serif';
ctx.fillText(String(Math.round(fps)) + ' fps', canvasWidth - 5, canvasHeight - 1);
}
ctx.restore();
};
function pointToDate(pt) {
// convert point to date
var pct = (pt.x - plotVars.graphArea.left) / plotVars.graphArea.width;
var timestamp = Math.floor(plotVars.minX + pct * (plotVars.maxX - plotVars.minX));
return timestamp;
}
function findClosestDateToPt(pt) {
var closestX = void 0;
var closestDate = void 0;
// only highlight something if the mouse is actually within the plot area of the canvas
if (pt.x >= plotVars.graphArea.left && pt.x < plotVars.graphArea.left + plotVars.graphArea.width && pt.y >= plotVars.graphArea.top && pt.y < plotVars.graphArea.top + plotVars.graphArea.height) {
for (var d = 0; d < lineSeriesArray.length; d++) {
for (var i = 0; i < lineSeriesArray[d].data.length; i++) {
var x = plotVars.graphArea.left + (lineSeriesArray[d].data[i].x - plotVars.minX) / (plotVars.maxX - plotVars.minX) * plotVars.graphArea.width;
if (closestX === undefined || Math.abs(x - pt.x) < Math.abs(closestX - pt.x)) {
closestX = x;
closestDate = lineSeriesArray[d].data[i].x;
}
}
}
}
return closestDate;
}
/**
* The animateFrame method is called many times per second to calculate any
* movement needed in the graph. It then calls render() to update the display.
* Even if there is no animation in the graph this method ensures the view is updated frequently in case
* mouse events change the view
**/
this.animateFrame = function () {
var thisFrame = new Date().getTime();
var elapsed = thisFrame - lastFrame; // elapsed time since last render
fps = 1000 / elapsed;
if (needsRender) {
this.render();
needsRender = false;
}
lastFrame = thisFrame;
if (!isDestroyed) {
if (window.requestAnimationFrame) {
window.requestAnimationFrame(this.animateFrame.bind(this));
} else if (window.webkitRequestAnimationFrame) {
window.webkitRequestAnimationFrame(this.animateFrame.bind(this));
} else if (window.mozRequestAnimationFrame) {
window.mozRequestAnimationFrame(this.animateFrame.bind(this));
} else if (window.oRequestAnimationFrame) {
window.oRequestAnimationFrame(this.animateFrame.bind(this));
}
}
};
/**
* Handles events related to mouse movement
* This method sets the appropriate variables to update the view.
* It does not call this.render() for every time the mouse moves,
* instead it relies on the next natural render cycle to update the view
**/
var isMouseDown = false;
this.handleMouseMove = function (pt) {
if (!this.options.showLegend) {
return;
}
plotVars.highlightedX = undefined;
var closestDate = findClosestDateToPt(pt);
if (closestDate) {
plotVars.highlightedX = closestDate;
if (hoverChangedCallback) {
hoverChangedCallback(closestDate);
}
}
if (isMouseDown) {
var timestamp = pointToDate(pt);
plotVars.highlightedXEnd = timestamp;
if (dateRangeChangedCallback) {
dateRangeChangedCallback(plotVars.highlightedXStart, plotVars.highlightedXEnd);
}
}
needsRender = true;
};
this.handleMouseDown = function (pt) {
var timestamp = pointToDate(pt);
plotVars.highlightedXStart = timestamp;
plotVars.highlightedXEnd = timestamp;
isMouseDown = true;
if (dateRangeChangedCallback) {
dateRangeChangedCallback(plotVars.highlightedXStart, plotVars.highlightedXEnd);
}
};
this.handleMouseUp = function (pt) {
plotVars.highlightedXEnd = pointToDate(pt);
isMouseDown = false;
if (dateRangeChangedCallback) {
dateRangeChangedCallback(plotVars.highlightedXStart, plotVars.highlightedXEnd);
}
};
/**
* Handles the mouseout event
**/
this.handleMouseOut = function () {
// make sure no data points are highlighted if the mouse is gone
plotVars.highlightedX = undefined;
needsRender = true;
if (hoverChangedCallback) {
hoverChangedCallback(null);
}
isMouseDown = false;
};
this.hoverDate = function (highlightedTimestamp) {
plotVars.highlightedX = highlightedTimestamp;
needsRender = true;
};
this.highlightDateRange = function (startTimestamp, endTimestamp) {
if (!startTimestamp || !endTimestamp) {
plotVars.highlightedXStart = undefined;
plotVars.highlightedXEnd = undefined;
} else {
plotVars.highlightedXStart = startTimestamp;
plotVars.highlightedXEnd = endTimestamp;
}
needsRender = true;
};
this.setDateRangeChangedCallback = function (callback) {
dateRangeChangedCallback = callback;
};
this.setHoverChangedCallback = function (callback) {
hoverChangedCallback = callback;
};
/**
* Handles the window.resize event so the canvas can be made to fill the parent automatically
**/
this.fillParent = function () {
if (htmlCanvas && htmlCanvas.parentNode && htmlContainer.parentNode) {
var style = window.getComputedStyle(htmlCanvas.parentNode);
var width = htmlCanvas.parentNode.offsetWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
var height = htmlCanvas.parentNode.offsetHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom);
pxRatio = window.devicePixelRatio || 1;
// upscale this thang if the device pixel ratio is higher than 1
htmlCanvas.width = width * pxRatio;
htmlCanvas.height = height * pxRatio;
htmlCanvas.style.width = width + 'px';
htmlCanvas.style.height = height + 'px';
}
needsRender = true;
};
// initialize some variables and create an html canvas to draw on
function _init() {
htmlCanvas = document.createElement('CANVAS');
htmlContainer.appendChild(htmlCanvas);
this.setOptions(graphOptions);
this.setupData(graphData);
this.setupListeners();
this.fillParent();
if (window.devicePixelRatio > 1) {
window.setTimeout(this.fillParent.bind(this), 100);
window.setTimeout(this.fillParent.bind(this), 400);
}
}
this.setOptions = function () {
var newOptions = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
this.options = Object.assign(this.options || {}, newOptions);
// background and padding
this.options.dateKey = this.options.dateKey || 'date';
this.options.valueKey = this.options.valueKey || 'value';
if (!this.options.backgroundColor) {
this.options.backgroundColor = new _Color2.default.Color('transparent');
} else {
this.options.backgroundColor = new _Color2.default.Color(this.options.backgroundColor);
}
if (!this.options.padding) {
this.options.padding = 0;
} else {
this.options.padding = parseFloat(this.options.padding);
}
// grid this.options
if (!this.options.gridLineColor) {
this.options.gridLineColor = new _Color2.default.Color('#444444');
} else {
this.options.gridLineColor = new _Color2.default.Color(this.options.gridLineColor);
}
if (!this.options.alternatingBackgroundColor) {
this.options.alternatingBackgroundColor = this.options.backgroundColor;
} else {
this.options.alternatingBackgroundColor = new _Color2.default.Color(this.options.alternatingBackgroundColor);
}
// series line this.options
if (!this.options.lineWidth) {
this.options.lineWidth = 2;
} else {
this.options.lineWidth = parseFloat(this.options.lineWidth);
}
// label this.options
if (!this.options.labelFontColor) {
this.options.labelFontColor = this.options.gridLineColor;
} else {
this.options.labelFontColor = new _Color2.default.Color(this.options.labelFontColor);
}
if (!this.options.labelFontFamily) {
this.options.labelFontFamily = 'Arial';
}
if (!this.options.labelFontSize) {
this.options.labelFontSize = 10;
} else {
this.options.labelFontSize = parseFloat(this.options.labelFontSize);
}
if (this.options.showLabels === undefined || this.options.showLabels === null || this.options.showLabels === true) {
this.options.showLabels = true;
} else {
this.options.showLabels = false;
}
// date interval for x axis
if (!this.options.dateInterval || ['minute', 'hour', 'day', 'month', 'year'].indexOf(this.options.dateInterval.toLowerCase()) === -1) {
this.options.dateInterval = 'day';
}
this.options.dateInterval = this.options.dateInterval.toLowerCase();
// legend
if (!this.options.legendFontColor) {
this.options.legendFontColor = new _Color2.default.Color('#ffffff');
} else {
this.options.legendFontColor = new _Color2.default.Color(this.options.legendFontColor);
}
if (!this.options.legendFontSize) {
this.options.legendFontSize = this.options.labelFontSize;
} else {
this.options.legendFontSize = parseFloat(this.options.legendFontSize);
}
if (!this.options.legendFontFamily) {
this.options.legendFontFamily = this.options.labelFontFamily;
}
if (!this.options.legendBackgroundColor) {
this.options.legendBackgroundColor = new _Color2.default.Color('rgba(0,0,0,0.05)');
} else {
this.options.legendBackgroundColor = new _Color2.default.Color(this.options.legendBackgroundColor);
}
if (!this.options.legendOutlineColor) {
this.options.legendOutlineColor = new _Color2.default.Color('transparent');
} else {
this.options.legendOutlineColor = new _Color2.default.Color(this.options.legendOutlineColor);
}
if (this.options.showLegend === undefined || this.options.showLegend === null || this.options.showLegend === true) {
this.options.showLegend = true;
} else {
this.options.showLegend = false;
}
// highlight
if (!this.options.highlightedDateRangeColor) {
this.options.highlightedDateRangeColor = new _Color2.default.Color('rgba(255, 255, 255, 0.05)');
}
// line styles
if (this.options.useBezierLines !== true) {
this.options.useBezierLines = false;
}
if (!this.options.fillStyle || this.options.fillStyle !== 'gradient' && this.options.fillStyle !== 'solid' && this.options.fillStyle !== 'translucent') {
this.options.fillStyle = 'none';
}
if (this.options.showDataPoints === undefined || this.options.showDataPoints === null || this.options.showDataPoints === true) {
this.options.showDataPoints = true;
} else {
this.options.showDataPoints = false;
}
labelHeight = this.options.labelFontSize; // the font size of labels on the graph (x and y axes)
needsRender = true;
};
this.dateKeyFormatForInterval = function () {
return {
minute: 'YYYY-MM-DD HH:mm',
hour: 'YYYY-MM-DD HH',
day: 'YYYY-MM-DD',
month: 'YYYY-MM',
year: 'YYYY'
}[this.options.dateInterval];
};
this.normalizeLineData = function () {
var _this = this;
var lineData = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
var lineDefinition = Object.assign({}, lineData);
if (lineData.color) {
lineDefinition.color = new _Color2.default.Color(lineData.color);
}
var datesHash = {};
var formatKey = this.dateKeyFormatForInterval();
var valueKey = lineData.valueKey || this.options.valueKey;
var dateKey = lineData.dateKey || this.options.dateKey;
var orderedData = _lodash2.default.chain(lineData.data).map(function (lineDatum) {
var datum = Object.assign({}, lineDatum);
var parts = _Numbers2.default.separateNumberUnits(datum[valueKey] || 0);
if (datum.prefix === undefined) {
datum.prefix = parts.prefix;
}
if (datum.suffix === undefined) {
datum.suffix = parts.suffix;
}
datum.value = parts.value || 0;
// discover if the series x is a date or number and normalize it
var dateItem = datum[dateKey];
if (!_lodash2.default.isDate(dateItem)) {
var dateFormat = datum.dateFormat || _this.options.dateFormat;
if (dateFormat) {
dateItem = (0, _moment2.default)(dateItem, dateFormat).toDate();
} else {
var timestamp = Date.parse(dateItem) || parseInt(dateItem, 10);
if (!isNaN(timestamp)) {
dateItem = new Date(timestamp);
}
}
}
if (_this.options.utc) {
dateItem.setTime(dateItem.getTime() + dateItem.getTimezoneOffset() * 60 * 1000);
}
var mom = (0, _moment2.default)(dateItem).startOf(_this.options.dateInterval);
datum.x = mom.toDate().getTime();
datum.originalDate = datum[lineData.dateKey || _this.options.dateKey];
datum.date = mom.toDate();
var key = mom.format(formatKey);
datesHash[key] = datum;
return datum;
}).sortBy(function (datum) {
return datum.date;
}).value();
var data = [];
var lastY = 0;
if (orderedData.length) {
var startDate = this.options.startDate || _lodash2.default.first(orderedData).date;
var endDate = this.options.endDate || _lodash2.default.last(orderedData).date;
var carryValues = this.options.carryValues;
if (startDate && endDate) {
var currentMoment = (0, _moment2.default)(startDate).startOf(this.options.dateInterval);
while (!currentMoment.isAfter(endDate)) {
var key = currentMoment.format(formatKey);
var foundData = datesHash[key];
if (!foundData) {
foundData = {
padding: true,
x: currentMoment.clone().toDate().getTime(),
date: currentMoment.clone().toDate(),
value: carryValues ? lastY : 0,
prefix: '',
suffix: ''
};
} else {
lastY = foundData.value;
}
data.push(foundData);
currentMoment = currentMoment.add(1, this.options.dateInterval);
}
}
}
lineDefinition.data = data;
return lineDefinition;
};
this.setupData = function setupData() {
var data = arguments.length <= 0 || arguments[0] === undefined ? [] : arguments[0];
lineSeriesArray = [];
for (var i = 0; i < data.length; i++) {
lineSeriesArray.push(this.normalizeLineData(data[i]));
}
needsRender = true;
};
this.fixCanvasSizing = function (canvas) {
var _this2 = this;
canvas.width = 1;
canvas.height = 1;
canvas.style.width = '1px';
canvas.style.height = '1px';
setTimeout(function () {
_this2.fillParent(_this2);
}, 10);
};
this.fullscreenChange = function (event) {
var fullscreenElement = document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement;
if (!fullscreenElement) {
// exiting full screen
var targetElement = event.target;
// if this element contains a canvas, make it tiny so we don't mess up the size of its parent
// don't worry, the window.resize event that fires next will make it fill its parent just fine
var canvases = targetElement.getElementsByTagName('CANVAS');
for (var c = 0; c < canvases.length; c++) {
if (canvases[c] === htmlCanvas) {
this.fixCanvasSizing(canvases[c]);
break;
}
}
}
};
this.setupListeners = function () {
var _this3 = this;
// Set up some event handling, please
// we want the canvas to always fill its parent
resizeListener = window.addEventListener('resize', function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () {
_this3.fillParent();
}, 50);
});
fullScreenChangeListener = window.addEventListener('webkitfullscreenchange', this.fullscreenChange.bind(this));
webkitFullScreenChangeListener = window.addEventListener('fullscreenchange', this.fullscreenChange.bind(this));
mozFullScreenChangeListener = window.addEventListener('mozfullscreenchange', this.fullscreenChange.bind(this));
msFullScreenChangeListener = window.addEventListener('msfullscreenchange', this.fullscreenChange.bind(this));
// mousemove event
htmlCanvas.addEventListener('mousemove', function (event) {
var rect = htmlCanvas.getBoundingClientRect();
var pt = {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
_this3.handleMouseMove(pt);
});
// mousedown event
htmlCanvas.addEventListener('mousedown', function (event) {
var rect = htmlCanvas.getBoundingClientRect();
var pt = {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
_this3.handleMouseDown(pt);
});
// mouseup event
htmlCanvas.addEventListener('mouseup', function (event) {
var rect = htmlCanvas.getBoundingClientRect();
var pt = {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
_this3.handleMouseUp(pt);
});
// mouseout event
htmlCanvas.addEventListener('mouseout', function () {
_this3.handleMouseOut();
});
if (window.requestAnimationFrame) {
window.requestAnimationFrame(this.animateFrame.bind(this));
} else if (window.webkitRequestAnimationFrame) {
window.webkitRequestAnimationFrame(this.animateFrame.bind(this));
} else if (window.mozRequestAnimationFrame) {
window.mozRequestAnimationFrame(this.animateFrame.bind(this));
} else if (window.oRequestAnimationFrame) {
window.oRequestAnimationFrame(this.animateFrame.bind(this));
}
};
this.destroy = function () {
if (fullScreenChangeListener) {
window.removeEventListener('fullscreenchange', fullScreenChangeListener);
}
if (webkitFullScreenChangeListener) {
window.removeEventListener('webkitfullscreenchange', webkitFullScreenChangeListener);
}
if (mozFullScreenChangeListener) {
window.removeEventListener('mozfullscreenchange', mozFullScreenChangeListener);
}
if (msFullScreenChangeListener) {
window.removeEventListener('msfullscreenchange', msFullScreenChangeListener);
}
if (resizeListener) {
window.removeEventListener('resize', resizeListener);
}
if (htmlCanvas && htmlCanvas.parentNode) {
htmlCanvas.parentNode.removeChild(htmlCanvas);
}
isDestroyed = true;
};
// Initialize
_init.call(this);
};
exports.default = LineGraph;