UNPKG

transcend-charts

Version:

Transcend is a charting and graph library for NUVI

1,037 lines (929 loc) 44 kB
'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;