UNPKG

transcend-charts

Version:

Transcend is a charting and graph library for NUVI

972 lines (904 loc) 43.1 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _lodash = require('lodash'); var _lodash2 = _interopRequireDefault(_lodash); var _Charts = require('../helpers/Charts'); var _Charts2 = _interopRequireDefault(_Charts); var _Numbers = require('../helpers/Numbers'); var _Numbers2 = _interopRequireDefault(_Numbers); 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 }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var DEBUG = false; var BubbleStream = function () { function BubbleStream(htmlContainer, graphData, graphOptions) { _classCallCheck(this, BubbleStream); this.needsRender = true; this.el = htmlContainer; this.lastFrame = new Date().getTime(); this.plotVars = {}; this.graphArea = {}; this.hits = {}; this.isDestroyed = false; this.autoBind(); this.setOptions(graphOptions); this.setData(graphData); this.dateRange = null; this.setDateRange(); this.build(); this.attach(); this.dateRangeChangedCallback = null; this.hoverChangedCallback = null; this.onAnimateFrame(); window.setTimeout(this.fillParent.call(this), 0); } _createClass(BubbleStream, [{ key: 'autoBind', value: function autoBind() { this.onAnimateFrame = this.onAnimateFrame.bind(this); this.onFullscreenChange = this.onFullscreenChange.bind(this); this.onWindowResize = this.onWindowResize.bind(this); this.onMouseOut = this.onMouseOut.bind(this); this.onMouseMove = this.onMouseMove.bind(this); this.onMouseDown = this.onMouseDown.bind(this); this.onMouseUp = this.onMouseUp.bind(this); } }, { key: 'build', value: function build() { this.htmlCanvas = document.createElement('CANVAS'); this.el.appendChild(this.htmlCanvas); this.ctx = this.htmlCanvas.getContext('2d'); } }, { key: 'attach', value: function attach() { window.addEventListener('webkitfullscreenchange', this.onFullscreenChange); window.addEventListener('fullscreenchange', this.onFullscreenChange); window.addEventListener('mozfullscreenchange', this.onFullscreenChange); window.addEventListener('msfullscreenchange', this.onFullscreenChange); window.addEventListener('resize', this.onWindowResize); this.htmlCanvas.addEventListener('mousemove', this.onMouseMove); this.htmlCanvas.addEventListener('mouseout', this.onMouseOut); this.htmlCanvas.addEventListener('mousedown', this.onMouseDown); this.htmlCanvas.addEventListener('mouseup', this.onMouseUp); } }, { key: 'destroy', value: function destroy() { this.detach(); this.el.removeChild(this.htmlCanvas); this.isDestroyed = true; } }, { key: 'detach', value: function detach() { window.removeEventListener('webkitfullscreenchange', this.onFullscreenChange); window.removeEventListener('fullscreenchange', this.onFullscreenChange); window.removeEventListener('mozfullscreenchange', this.onFullscreenChange); window.removeEventListener('msfullscreenchange', this.onFullscreenChange); window.removeEventListener('resize', this.onWindowResize); this.htmlCanvas.removeEventListener('mousemove', this.onMouseMove); this.htmlCanvas.removeEventListener('mouseout', this.onMouseOut); this.htmlCanvas.removeEventListener('click', this.onClick); } }, { key: 'setData', value: function setData() { var _this = this; var someData = arguments.length <= 0 || arguments[0] === undefined ? [] : arguments[0]; // let's create a variable for each bar so we can track animation this.data = _lodash2.default.map(someData, function (bubble) { var dateItem = bubble.date; var timestamp = Date.parse(dateItem); if (!isNaN(timestamp)) { var date = new Date(timestamp); bubble.dateX = date.getTime(); } if (!bubble.value) { bubble.value = _lodash2.default.sum(bubble.segments, function (segment) { return segment.value; }); } var barData = Object.assign({}, bubble); barData.segments = _this.processSegmentData(barData.segments, barData); return barData; }); this.data.sort(function (a, b) { return b.value - a.value; }); } }, { key: 'processSegmentData', value: function processSegmentData(segments, bubble) { return _lodash2.default.map(segments, function (segmentData) { var segment = Object.assign({}, segmentData || {}); var parts = _Numbers2.default.separateNumberUnits(segment.value); if (segment.prefix === undefined) { segment.prefix = parts.prefix; } if (segment.suffix === undefined) { segment.suffix = parts.suffix; } segment.value = parts.value; if (!bubble.prefix) { bubble.prefix = segment.prefix; } if (!bubble.suffix) { bubble.suffix = segment.suffix; } if (segment.barFillColor) { segment.barFillColor = new _Color2.default.Color(segment.barFillColor); } if (segment.barBorderColor) { segment.barBorderColor = new _Color2.default.Color(segment.barBorderColor); } if (segment.barStripeColor) { segment.barStripeColor = new _Color2.default.Color(segment.barStripeColor); } return segment; }); } }, { key: 'fillParent', value: function fillParent() { if (this.htmlCanvas && this.htmlCanvas.parentNode) { this.needsRender = true; var pxRatio = window.devicePixelRatio || 1; var style = window.getComputedStyle(this.htmlCanvas.parentNode); var width = this.htmlCanvas.parentNode.offsetWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight); var height = this.htmlCanvas.parentNode.offsetHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom); // upscale this thang if the device pixel ratio is higher than 1 this.htmlCanvas.width = width * pxRatio; this.htmlCanvas.height = height * pxRatio; this.htmlCanvas.style.width = width + 'px'; this.htmlCanvas.style.height = height + 'px'; } } }, { key: 'setOptions', value: function setOptions(options) { this.options = Object.assign({}, { alternatingBackgroundColor: new _Color2.default.Color('transparent'), autoFit: false, backgroundColor: new _Color2.default.Color('transparent'), dateInterval: 'day', dateRange: null, fillColor: new _Color2.default.Color('54c7dd'), gridLineColor: new _Color2.default.Color('#444444'), gridLineWidth: 1, highlightedDateRangeColor: new _Color2.default.Color('rgba(0,0,0,0.05)'), labelFontFamily: 'Arial', labelFontSize: 10, labelFontColor: new _Color2.default.Color('#444444'), legendBackgroundColor: 'rgba(255,255,255,0.3)', legendFontColor: '#ffffff', legendFontFamily: 'Arial', legendFontSize: 12, legendFontWeight: '400', legendLabelValueSpacing: 10, legendOutlineColor: 'transparent', legendOutlineCornerSize: 8, // the width of the little corner notches around the legend legendOutlineWidth: 1, // the width of the stroke around the legend maxRadius: 25, minRadius: 5, padding: 0, segmentPadding: 5, segmentWidth: 15, showLabels: true, showLegend: true, showLiveAnimation: true, liveTimeRange: 1000 * 60 * 60 * 24, legendPadding: 12 }, options); this.timeRangeOption(); this.dateRangeOption(); this.colorizeOptions(); this.percentageOptions(); this.pixelOptions(); } }, { key: 'timeRangeOption', value: function timeRangeOption() { // this option only matters if it is a live chart if (this.options.showLiveAnimation) { if (typeof this.options.liveTimeRange == "string") { this.options.liveTimeRange = parseInt(this.options.liveTimeRange); } else if (typeof this.options.liveTimeRange != "number") { this.options.liveTimeRange = 1000 * 60 * 60 * 24; } } } }, { key: 'dateRangeOption', value: function dateRangeOption() { // this option only matters if it's not a live chart if (!this.options.showLiveAnimation) { if (this.options.dateRange == null) { return; // this is okay. we'll let the data dictate what date range we should show } if (_typeof(this.options.dateRange) != "object" || !this.options.dateRange.start || !this.options.dateRange.end) { throw "Invalid dateRange option provided. Must be an object with the shape { start: [timestamp], end: [timestamp] }"; } else { var timestamp = Date.parse(this.options.dateRange.start); if (isNaN(timestamp)) { throw "Invalid start date provided for dateRange option. Must be a parsable Date string"; } timestamp = Date.parse(this.options.dateRange.end); if (isNaN(timestamp)) { throw "Invalid end date provided for dateRange option. Must be a parsable Date string"; } } } } }, { key: 'percentageOptions', value: function percentageOptions() { var _this2 = this; ['labelFontSize', 'maxRadius', 'minRadius'].forEach(function (key) { var val = _this2.options[key]; if (_this2.isPercentage(val)) { _this2.options[key + 'AsPct'] = parseFloat(val.substr(0, val.length - 1)) / 100; } else { _this2.options[key] = parseFloat(val); _this2.options[key + 'AsPct'] = false; } }); } }, { key: 'isPercentage', value: function isPercentage(x) { var str = String(x); return str.substr(-1) === '%'; } }, { key: 'colorizeOptions', value: function colorizeOptions() { var _this3 = this; var keys = ['alternatingBackgroundColor', 'backgroundColor', 'fillColor', 'gridLineColor', 'labelFontFamily', 'legendBackgroundColor', 'legendOutlineColor', 'legendFontColor']; keys.forEach(function (key) { var color = _this3.options[key]; if (color) { _this3.options[key] = new _Color2.default.Color(color); } }); } }, { key: 'pixelOptions', value: function pixelOptions() { var _this4 = this; var keys = ['gridLineWidth', 'labelFontSize', 'legendFontSize', 'legendOutlineCornerSize', 'legendOutlineWidth', 'maxRadius', 'minRadius', 'padding', 'segmentPadding', 'segmentWidth', 'legendPadding']; keys.forEach(function (key) { _this4.options[key] = parseFloat(_this4.options[key]); }); } }, { key: 'setDateRange', value: function setDateRange() { if (this.options.showLiveAnimation) { this.dateRange = { end: thisFrame, start: this.dateRange.end - this.options.liveTimeRange }; } else { if (this.options.dateRange) { this.dateRange = { start: Date.parse(this.options.dateRange.start), end: Date.parse(this.options.dateRange.end) }; } else { var minDateX = _lodash2.default.min(this.data, function (dataPoint) { return dataPoint.dateX; }).dateX; var maxDateX = _lodash2.default.max(this.data, function (dataPoint) { return dataPoint.dateX; }).dateX; this.dateRange = { start: _Charts2.default.roundStartDateByInterval(minDateX, this.options.dateInterval), end: _Charts2.default.roundEndDateByInterval(maxDateX, this.options.dateInterval) }; } } this.plotVars.minX = this.dateRange.start; this.plotVars.maxX = this.dateRange.end; } }, { key: 'onWindowResize', value: function onWindowResize() { this.fillParent(); } }, { key: 'onAnimateFrame', value: function onAnimateFrame() { var _this5 = this; var thisFrame = new Date().getTime(); var elapsed = thisFrame - this.lastFrame; // elapsed time since last render this.fps = 1000 / elapsed; if (!this.isDestroyed) { if (this.data && this.data.length) { if (this.options.showLiveAnimation) { dateRange.end = thisFrame; dateRange.start = dateRange.end - this.options.liveTimeRange; this.plotVars.minX = this.dateRange.start; this.plotVars.maxX = this.dateRange.end; // filter out the data that's fallen off the edge of the time range this.data = _lodash2.default.filter(this.data, function (d) { return d.dateX > _this5.dateRange.start && d.dateX < _this5.dateRange.end; }); } var pxRatio = window.devicePixelRatio || 1; var backingStoreRatio = this.ctx.webkitBackingStorePixelRatio || this.ctx.mozBackingStorePixelRatio || this.ctx.msBackingStorePixelRatio || this.ctx.oBackingStorePixelRatio || this.ctx.backingStorePixelRatio || 1; var canvasWidth = this.htmlCanvas.width / (pxRatio / backingStoreRatio); var canvasHeight = this.htmlCanvas.height / (pxRatio / backingStoreRatio); // define the plot area of the canvas that will have the actual graph on it (no labels or padding or anything) this.graphArea = { top: this.options.padding, right: this.options.padding + this.options.maxRadius, bottom: this.options.padding + this.options.labelFontSize, left: this.options.padding + this.options.maxRadius }; this.graphArea.width = canvasWidth - this.graphArea.left - this.graphArea.right; this.graphArea.height = canvasHeight - this.graphArea.bottom - this.graphArea.top; // get the min & max value in all data series this.plotVars.maxY = _lodash2.default.max(this.data, function (dataPoint) { return dataPoint.value; }).value; this.plotVars.minY = _lodash2.default.min(this.data, function (dataPoint) { return dataPoint.value; }).value; // animation effects this.data.forEach(function (dataPoint) { dataPoint.oldX = dataPoint.x; var x = _this5.plotVars.maxX - _this5.plotVars.minX === 0 ? _this5.graphArea.left + _this5.graphArea.width / 2 : _this5.graphArea.left + (dataPoint.dateX - _this5.plotVars.minX) / (_this5.plotVars.maxX - _this5.plotVars.minX) * _this5.graphArea.width; var y = _this5.graphArea.top + _this5.graphArea.height / 2; var radius = _this5.plotVars.maxY - _this5.plotVars.minY == 0 || _this5.options.maxRadius - _this5.options.minRadius == 0 ? _this5.options.maxRadius : _this5.options.minRadius + (dataPoint.value - _this5.plotVars.minY) / (_this5.plotVars.maxY - _this5.plotVars.minY) * (_this5.options.maxRadius - _this5.options.minRadius); dataPoint.radius = radius; _this5.findRoomForCircle(dataPoint, x, _this5.graphArea.top + _this5.graphArea.height / 2); }); this.reCenterCircles(); for (var i = 0; i < this.data.length; i++) { if (this.data[i].x !== this.data[i].oldX) { this.needsRender = true; break; } }; } if (this.needsRender) { this.render(); this.needsRender = false; } this.lastFrame = thisFrame; if (window.requestAnimationFrame) { window.requestAnimationFrame(this.onAnimateFrame); } else if (window.webkitRequestAnimationFrame) { window.webkitRequestAnimationFrame(this.onAnimateFrame); } else if (window.mozRequestAnimationFrame) { window.mozRequestAnimationFrame(this.onAnimateFrame); } else if (window.oRequestAnimationFrame) { window.oRequestAnimationFrame(this.onAnimateFrame); } } } }, { key: 'onFullscreenChange', value: function onFullscreenChange() { 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] === this.htmlCanvas) { canvases[c].width = 1; canvases[c].height = 1; canvases[c].style.width = '1px'; canvases[c].style.height = '1px'; break; } } } } }, { key: 'onMouseDown', value: function onMouseDown(event) { var rect = this.htmlCanvas.getBoundingClientRect(); var pt = { x: event.clientX - rect.left, y: event.clientY - rect.top }; var timestamp = this.pointToDate(pt); this.plotVars.highlightedXStart = timestamp; this.plotVars.highlightedXEnd = timestamp; this.isMouseDown = true; if (this.dateRangeChangedCallback) { this.dateRangeChangedCallback(this.plotVars.highlightedXStart, this.plotVars.highlightedXEnd); } } }, { key: 'onMouseUp', value: function onMouseUp(event) { var rect = this.htmlCanvas.getBoundingClientRect(); var pt = { x: event.clientX - rect.left, y: event.clientY - rect.top }; this.plotVars.highlightedXEnd = this.pointToDate(pt); this.isMouseDown = false; if (this.dateRangeChangedCallback) { this.dateRangeChangedCallback(this.plotVars.highlightedXStart, this.plotVars.highlightedXEnd); } } }, { key: 'onMouseMove', value: function onMouseMove(event) { if (!this.options.showLegend) { return; } var rect = this.htmlCanvas.getBoundingClientRect(); var pt = { x: event.clientX - rect.left, y: event.clientY - rect.top }; this.plotVars.highlightedX = undefined; var closestDate = this.findClosestDateToPt(pt); if (closestDate && closestDate != this.plotVars.highlightedX) { this.plotVars.highlightedX = closestDate; if (this.hoverChangedCallback) { this.hoverChangedCallback(closestDate); } this.needsRender = true; } if (this.isMouseDown) { var timestamp = this.pointToDate(pt); this.plotVars.highlightedXEnd = timestamp; if (this.dateRangeChangedCallback) { this.dateRangeChangedCallback(this.plotVars.highlightedXStart, this.plotVars.highlightedXEnd); } this.needsRender = true; } var bubble = this.hitTest(pt); var highlightedBubble = this.data.find(function (dataPoint) { return dataPoint.isHighlighted; }); if (bubble != highlightedBubble) { this.needsRender = true; } if (bubble && !bubble.isHighlighted) { this.unhighlightAll(); bubble.isHighlighted = true; } else if (!bubble) { this.unhighlightAll(); } } }, { key: 'onMouseOut', value: function onMouseOut() { this.unhighlightAll(); this.isMouseDown = false; if (this.hoverChangedCallback) { this.hoverChangedCallback(null); } } }, { key: 'hoverDate', value: function hoverDate(highlightedTimestamp) { this.plotVars.highlightedX = highlightedTimestamp; this.needsRender = true; } }, { key: 'highlightDateRange', value: function highlightDateRange(startTimestamp, endTimestamp) { if (!startTimestamp || !endTimestamp) { this.plotVars.highlightedXStart = undefined; this.plotVars.highlightedXEnd = undefined; } else { this.plotVars.highlightedXStart = startTimestamp; this.plotVars.highlightedXEnd = endTimestamp; } this.needsRender = true; } }, { key: 'setDateRangeChangedCallback', value: function setDateRangeChangedCallback(callback) { this.dateRangeChangedCallback = callback; } }, { key: 'setHoverChangedCallback', value: function setHoverChangedCallback(callback) { this.hoverChangedCallback = callback; } }, { key: 'unhighlightAll', value: function unhighlightAll() { this.data.forEach(function (dataPoint) { dataPoint.isHighlighted = false; }); } }, { key: 'hitTest', value: function hitTest(pt) { return this.data.find(function (dataPoint) { var sqd = (dataPoint.x - pt.x) * (dataPoint.x - pt.x) + (dataPoint.y - pt.y) * (dataPoint.y - pt.y); return sqd <= dataPoint.radius * dataPoint.radius; }); } }, { key: 'pointToDate', value: function pointToDate(pt) { // convert point to date var pct = (pt.x - this.graphArea.left) / this.graphArea.width; var timestamp = this.plotVars.maxX - this.plotVars.minX == 0 ? this.plotVars.minX : Math.floor(this.plotVars.minX + pct * (this.plotVars.maxX - this.plotVars.minX)); return timestamp; } }, { key: 'findClosestDateToPt', value: function findClosestDateToPt(pt) { var timestamp = this.pointToDate(pt); var down = _Charts2.default.roundStartDateByInterval(timestamp, this.options.dateInterval); var up = _Charts2.default.roundEndDateByInterval(timestamp, this.options.dateInterval); return Math.abs(down - timestamp) < Math.abs(up - timestamp) ? down : up; } }, { key: 'render', value: function render() { var _this6 = this; // If there's no HTML canvas made something went horribly wrong. Abort! if (!this.htmlCanvas || !this.ctx) { return; } // upscale this thang if the device pixel ratio is higher than 1 var pxRatio = window.devicePixelRatio || 1; var backingStoreRatio = this.ctx.webkitBackingStorePixelRatio || this.ctx.mozBackingStorePixelRatio || this.ctx.msBackingStorePixelRatio || this.ctx.oBackingStorePixelRatio || this.ctx.backingStorePixelRatio || 1; this.ctx.save(); if (pxRatio > 1) { this.ctx.scale(pxRatio / backingStoreRatio, pxRatio / backingStoreRatio); } var canvasWidth = this.htmlCanvas.width / (pxRatio / backingStoreRatio); var canvasHeight = this.htmlCanvas.height / (pxRatio / backingStoreRatio); // clear the background, ready to re-render this.ctx.clearRect(0, 0, canvasWidth, canvasHeight); if (this.options.backgroundColor.toString() !== 'transparent') { this.ctx.fillStyle = this.options.backgroundColor.toString(); this.ctx.fillRect(0, 0, canvasWidth, canvasHeight); } this.ctx.strokeStyle = this.options.gridLineColor.toString(); this.ctx.lineWidth = this.options.gridLineWidth; this.ctx.beginPath(); this.ctx.moveTo(this.options.padding, this.graphArea.top + this.graphArea.height / 2); this.ctx.lineTo(canvasWidth - this.options.padding, this.graphArea.top + this.graphArea.height / 2); this.ctx.stroke(); // smash all x labels into one sorted array // var minDate = _.min(this.data, (dataPoint) => dataPoint.dateX).dateX; // var maxDate = _.max(this.data, (dataPoint) => dataPoint.dateX).dateX; // var allLabels = Charts.createDateLabels(minDate, maxDate, this.options.dateInterval); var allLabels = _Charts2.default.createDateLabels(this.dateRange.start, this.dateRange.end, this.options.dateInterval); // draw alternating background colors (if any) if (this.options.alternatingBackgroundColor.toString() !== this.options.backgroundColor.toString()) { var backgroundWidth = this.graphArea.width; this.ctx.fillStyle = this.options.alternatingBackgroundColor.toString(); var preferredSegmentWidth = 0.15; // 15% of chart width var preferredWidthOfInterval = preferredSegmentWidth * backgroundWidth; var actualWidthOfInterval = backgroundWidth / (allLabels.length - 1); var alternatingColorWidth = actualWidthOfInterval; if (preferredWidthOfInterval > actualWidthOfInterval) { alternatingColorWidth = actualWidthOfInterval * Math.round(preferredWidthOfInterval / actualWidthOfInterval); } var intervals = Math.ceil(backgroundWidth / alternatingColorWidth); for (var _i = 0; _i < intervals; _i += 2) { var awidth = alternatingColorWidth; var ax = this.graphArea.left + _i * alternatingColorWidth; if (ax + awidth > this.graphArea.left + backgroundWidth) { awidth = this.graphArea.left + backgroundWidth - ax; } this.ctx.fillRect(ax, this.graphArea.top, awidth, this.graphArea.height); } } if (this.options.showLabels) { this.ctx.fillStyle = this.options.labelFontColor; this.ctx.fontSize = this.options.labelFontSize; this.ctx.fontFamily = this.options.labelFontFamily; this.ctx.textAlign = 'center'; this.ctx.textBaseline = 'top'; // 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 = this.graphArea.width / Math.floor(allLabels.length / everyNthLabel) * 0.8; willFit = true; for (var i = 0; i < allLabels.length; i += everyNthLabel) { if (this.ctx.measureText(allLabels[i].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 i = 0; i < allLabels.length; i += everyNthLabel) { var labelX = this.plotVars.maxX - this.plotVars.minX === 0 ? this.graphArea.left + this.graphArea.width / 2 : this.graphArea.left + (allLabels[i].date - this.plotVars.minX) / (this.plotVars.maxX - this.plotVars.minX) * this.graphArea.width; var labelY = this.graphArea.top + this.graphArea.height + 2; this.ctx.fillText(allLabels[i].datestr, labelX, labelY); } } // render date range highlight (if any) if (this.plotVars.highlightedXStart && this.plotVars.highlightedXEnd) { var startTime = _lodash2.default.min([this.plotVars.highlightedXStart, this.plotVars.highlightedXEnd]); var endTime = _lodash2.default.max([this.plotVars.highlightedXStart, this.plotVars.highlightedXEnd]); this.ctx.fillStyle = this.options.highlightedDateRangeColor.toString(); var startX = this.plotVars.maxX - this.plotVars.minX == 0 ? this.graphArea.left + this.graphArea.width / 2 : this.graphArea.left + (startTime - this.plotVars.minX) / (this.plotVars.maxX - this.plotVars.minX) * this.graphArea.width; var endX = this.plotVars.maxX - this.plotVars.minX == 0 ? this.graphArea.left + this.graphArea.width / 2 : this.graphArea.left + (endTime - this.plotVars.minX) / (this.plotVars.maxX - this.plotVars.minX) * this.graphArea.width; var width = endX - startX; this.ctx.fillRect(startX, this.graphArea.top, width, this.graphArea.height); } var highlightBarWidth = 4; var highlightedBubble = this.data.find(function (dataPoint) { return dataPoint.isHighlighted; }); if (highlightedBubble) { this.ctx.lineWidth = highlightBarWidth; this.ctx.strokeStyle = this.options.legendBackgroundColor.toString(); this.ctx.beginPath(); this.ctx.moveTo(highlightedBubble.x, this.graphArea.top); this.ctx.lineTo(highlightedBubble.x, this.graphArea.top + this.graphArea.height); this.ctx.stroke(); } // render highlight of mouseover (if any). This is intentionally placed below the series lines so it's more of an 'underlight' than a highlight else if (this.plotVars.highlightedX) { this.ctx.lineWidth = highlightBarWidth; this.ctx.strokeStyle = this.options.legendBackgroundColor.toString(); this.ctx.beginPath(); var line_x = this.plotVars.maxX - this.plotVars.minX == 0 ? this.graphArea.left + this.graphArea.width / 2 : this.graphArea.left + (this.plotVars.highlightedX - this.plotVars.minX) / (this.plotVars.maxX - this.plotVars.minX) * this.graphArea.width; this.ctx.moveTo(line_x, this.graphArea.top); this.ctx.lineTo(line_x, this.graphArea.top + this.graphArea.height); this.ctx.stroke(); } // render the bubbles this.data.forEach(function (bubble) { _this6.ctx.beginPath(); var bubbleColor = new _Color2.default.Color(_this6.options.fillColor); if (bubble.fillColor) { bubbleColor = new _Color2.default.Color(bubble.fillColor); } if (highlightedBubble && !bubble.isHighlighted) { bubbleColor.fade(0.5); } _this6.ctx.fillStyle = bubbleColor.toString(); _this6.ctx.arc(bubble.x, bubble.y, bubble.radius, 0, Math.PI * 2); _this6.ctx.fill(); }); var legendDatePadding = 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 // render highlighted bubble stuff if (highlightedBubble) { (function () { var movingRadian = 0; highlightedBubble.segments.forEach(function (segment) { var segmentInRadians = segment.value / highlightedBubble.value * Math.PI * 2; _this6.ctx.fillStyle = segment.fillColor ? segment.fillColor.toString() : _this6.options.fillColor.toString(); _this6.ctx.beginPath(); var tmpx = Math.cos(movingRadian); var tmpy = Math.sin(movingRadian); _this6.ctx.arc(highlightedBubble.x, highlightedBubble.y, highlightedBubble.radius + _this6.options.segmentPadding + _this6.options.segmentWidth, movingRadian, movingRadian + segmentInRadians); _this6.ctx.arc(highlightedBubble.x, highlightedBubble.y, highlightedBubble.radius + _this6.options.segmentPadding, movingRadian + segmentInRadians, movingRadian, true); _this6.ctx.lineTo(highlightedBubble.x + tmpx * (highlightedBubble.radius + _this6.options.segmentPadding + _this6.options.segmentWidth), highlightedBubble.y + tmpy * (highlightedBubble.radius + _this6.options.segmentPadding + _this6.options.segmentWidth)); _this6.ctx.fill(); movingRadian += segmentInRadians; }); // render legend/tooltip for highlight of mouseover (if any) // build array of points with this x value (will determine height of legend) var seriesPts = []; for (var s = 0; s < highlightedBubble.segments.length; s++) { seriesPts.push({ name: highlightedBubble.segments[s].name, color: highlightedBubble.segments[s].fillColor, value: highlightedBubble.segments[s].value, prefix: highlightedBubble.segments[s].prefix, suffix: highlightedBubble.segments[s].suffix }); } // determine width of tooltip/legend _this6.ctx.font = _this6.options.legendFontSize + 'px ' + _this6.options.legendFontFamily; _this6.ctx.textBaseline = 'top'; _this6.ctx.textAlign = 'left'; var colorbarWidth = _this6.options.legendFontSize; var widestValue = 0; for (var _i2 = 0; _i2 < seriesPts.length; _i2++) { var w = _this6.ctx.measureText(seriesPts[_i2].name + ': ' + seriesPts[_i2].prefix + _Numbers2.default.formatNumber(seriesPts[_i2].value) + seriesPts[_i2].suffix).width; if (w > widestValue) { widestValue = w; } } var highlightedDateStr = (0, _moment2.default)(highlightedBubble.dateX).format('MMM D, YYYY h:mma'); var dateTextWidth = _this6.ctx.measureText(highlightedDateStr).width; var legendWidth = colorbarWidth + colorbarSpacing + widestValue + _this6.options.legendPadding * 2; if (dateTextWidth > colorbarWidth + colorbarSpacing + widestValue) { legendWidth = dateTextWidth + _this6.options.legendPadding * 2; } // determine height of legend/tooltip var legendHeight = (seriesPts.length + 1) * (_this6.options.legendFontSize * 1.25) + _this6.options.legendPadding * 2 + legendDatePadding; // determine where the legend/tooltip will fit var hx = _this6.plotVars.maxX - _this6.plotVars.minX == 0 ? _this6.graphArea.left + _this6.graphArea.width / 2 : _this6.graphArea.left + (highlightedBubble.dateX - _this6.plotVars.minX) / (_this6.plotVars.maxX - _this6.plotVars.minX) * _this6.graphArea.width; var x = hx + highlightBarWidth; if (hx + legendWidth > _this6.graphArea.left + _this6.graphArea.width) { x = hx - highlightBarWidth - legendWidth; } var y = _this6.graphArea.top + 5; // draw legend/tooltip box _this6.ctx.fillStyle = _this6.options.legendBackgroundColor.toString(); _this6.ctx.fillRect(x, y, legendWidth, legendHeight); // draw four corners _this6.ctx.strokeStyle = _this6.options.legendOutlineColor.toString(); _this6.ctx.lineWidth = legendOutlineWidth; _this6.ctx.beginPath(); _this6.ctx.moveTo(x + legendOutlineWidth, y + legendOutlineCornerSize); _this6.ctx.lineTo(x + legendOutlineWidth, y + legendOutlineWidth); _this6.ctx.lineTo(x + legendOutlineCornerSize, y + legendOutlineWidth); _this6.ctx.moveTo(x + legendWidth - legendOutlineCornerSize, y + legendOutlineWidth); _this6.ctx.lineTo(x + legendWidth - legendOutlineWidth, y + legendOutlineWidth); _this6.ctx.lineTo(x + legendWidth - legendOutlineWidth, y + legendOutlineCornerSize); _this6.ctx.moveTo(x + legendWidth - legendOutlineWidth, y + legendHeight - legendOutlineCornerSize); _this6.ctx.lineTo(x + legendWidth - legendOutlineWidth, y + legendHeight - legendOutlineWidth); _this6.ctx.lineTo(x + legendWidth - legendOutlineCornerSize, y + legendHeight - legendOutlineWidth); _this6.ctx.moveTo(x + legendOutlineCornerSize, y + legendHeight - legendOutlineWidth); _this6.ctx.lineTo(x + legendOutlineWidth, y + legendHeight - legendOutlineWidth); _this6.ctx.lineTo(x + legendOutlineWidth, y + legendHeight - legendOutlineCornerSize); _this6.ctx.stroke(); // draw values for each series (one per line) _this6.ctx.fillStyle = new _Color2.default.Color(_this6.options.legendFontColor).fade(0.3).toString(); _this6.ctx.fillText(highlightedDateStr, x + _this6.options.legendPadding, y + _this6.options.legendPadding); for (var _i3 = 0; _i3 < seriesPts.length; _i3++) { // draw a color bar for reference _this6.ctx.fillStyle = seriesPts[_i3].color; _this6.ctx.fillRect(x + _this6.options.legendPadding, y + legendDatePadding + _this6.options.legendPadding + (_i3 + 1) * _this6.options.legendFontSize * 1.25, colorbarWidth, _this6.options.legendFontSize); var serieslabelw = _this6.ctx.measureText(seriesPts[_i3].name + ': ').width; // draw the number/value in the series color _this6.ctx.fillText(seriesPts[_i3].prefix + _Numbers2.default.formatNumber(seriesPts[_i3].value) + seriesPts[_i3].suffix, x + _this6.options.legendPadding + colorbarWidth + colorbarSpacing + serieslabelw, y + legendDatePadding + _this6.options.legendPadding + (_i3 + 1) * _this6.options.legendFontSize * 1.25); // draw the series label in a plain label color _this6.ctx.fillStyle = _this6.options.legendFontColor.toString(); _this6.ctx.fillText(seriesPts[_i3].name + ': ', x + _this6.options.legendPadding + colorbarWidth + colorbarSpacing, y + legendDatePadding + _this6.options.legendPadding + (_i3 + 1) * _this6.options.legendFontSize * 1.25); } })(); } // draw fps if (DEBUG) { this.ctx.fillStyle = '#666666'; this.ctx.fillRect(canvasWidth - 40, canvasHeight - 15, 40, 15); this.ctx.textAlign = 'right'; this.ctx.textBaseline = 'bottom'; this.ctx.fillStyle = '#000000'; this.ctx.font = '11px sans-serif'; this.ctx.fillText(String(Math.round(this.fps)) + ' fps', canvasWidth - 5, canvasHeight - 1); } this.ctx.restore(); } }, { key: 'findRoomForCircle', value: function findRoomForCircle(dataPoint, x, y) { // if the circle can fit at x, y then let's do it var doesOverlap = false; for (var i = 0; i < this.data.length; i++) { if (dataPoint == this.data[i]) { break; } if (this.data[i].x && this.data[i].y && (x - this.data[i].x) * (x - this.data[i].x) + (y - this.data[i].y) * (y - this.data[i].y) < (dataPoint.radius + this.data[i].radius) * (dataPoint.radius + this.data[i].radius)) { doesOverlap = true; break; } } if (!doesOverlap) { dataPoint.y = y; dataPoint.x = x; return; } var conflictingCircles = []; for (var i = 0; i < this.data.length; i++) { var conflictCircle = this.data[i]; if (conflictCircle == dataPoint) { break; } if (conflictCircle.x && conflictCircle.y && Math.abs(x - conflictCircle.x) < dataPoint.radius + conflictCircle.radius) { conflictingCircles.push(conflictCircle); } } // get the minY required to fit circle without overlap var suggestedPoints = []; var circleSafePoints = conflictingCircles.map(function (circle) { // calculate y required to fit above and below var combinedRadii = dataPoint.radius + circle.radius; var theta = Math.acos((x - circle.x) / combinedRadii); var y_delta = Math.sin(theta) * combinedRadii; suggestedPoints.push(circle.y - y_delta); suggestedPoints.push(circle.y + y_delta); return { min: circle.y - y_delta, max: circle.y + y_delta }; }); // sort the safe suggestedPoints array by closest to center suggestedPoints.sort(function (a, b) { return Math.abs(a - y) - Math.abs(b - y); }); // iterate over the suggestedPoints and find one that is not between a min and max (overlapping a circle) for (var s = 0; s < suggestedPoints.length; s++) { var isValid = true; for (var p = 0; p < circleSafePoints.length; p++) { if (suggestedPoints[s] > circleSafePoints[p].min && suggestedPoints[s] < circleSafePoints[p].max) { isValid = false; break; } } if (isValid) { dataPoint.y = suggestedPoints[s]; break; } } dataPoint.x = x; } }, { key: 'reCenterCircles', value: function reCenterCircles() { if (this.options.autoFit) { this.autoFitCircles(); } var maxAllowableOffset = 3; var minCircle = _lodash2.default.min(this.data, function (dataPoint) { return dataPoint.y - dataPoint.radius; }); var minY = minCircle.y - minCircle.radius; var maxCircle = _lodash2.default.max(this.data, function (dataPoint) { return dataPoint.y + dataPoint.radius; }); var maxY = maxCircle.y + maxCircle.radius; var avg = (maxY + minY) / 2; var graphCenter = this.graphArea.top + this.graphArea.height / 2; var diff = graphCenter - avg; if (Math.abs(diff) > maxAllowableOffset) { for (var i = 0; i < this.data.length; i++) { this.data[i].y += diff; } this.needsRender = true; } } }, { key: 'autoFitCircles', value: function autoFitCircles() { var maxAllowableScaleDifference = 0.01; var minCircle = _lodash2.default.min(this.data, function (dataPoint) { return dataPoint.y - dataPoint.radius; }); var minY = minCircle.y - minCircle.radius; var maxCircle = _lodash2.default.max(this.data, function (dataPoint) { return dataPoint.y + dataPoint.radius; }); var maxY = maxCircle.y + maxCircle.radius; if (maxY - minY == 0) { return; } // console.log(maxY, minY, this.graphArea.height); if (maxY - minY < this.graphArea.height) { return; } var graphCenter = this.graphArea.top + this.graphArea.height / 2; var scale = this.graphArea.height / (maxY - minY); if (Math.abs(1 - scale) > maxAllowableScaleDifference) { this.data.forEach(function (dataPoint) { dataPoint.radius *= scale; dataPoint.y = graphCenter + (dataPoint.y - graphCenter) * scale; }); this.needsRender = true; } } }]); return BubbleStream; }(); exports.default = BubbleStream;