transcend-charts
Version:
Transcend is a charting and graph library for NUVI
972 lines (904 loc) • 43.1 kB
JavaScript
'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;