transcend-charts
Version:
Transcend is a charting and graph library for NUVI
629 lines (547 loc) • 22.2 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
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 _ChartDictionary = require('./ChartDictionary');
var _ChartDictionary2 = _interopRequireDefault(_ChartDictionary);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var NuviBiDirectionalBarGraph = function NuviBiDirectionalBarGraph(htmlContainer, graphData, graphOptions) {
var self = this;
var htmlCanvas = null;
var data = null;
var plotVars = {
graphArea: { top: 0, right: 0, bottom: 0, left: 0, width: 0, height: 0 } }; // this var is updated regularly in the render function to ensure it always contains correct data about the size and state of the graph
var pxRatio = window.devicePixelRatio || 1;
var backingStoreRatio = 1;
var canvasWidth = 0;
var canvasHeight = 0;
var allLabels = [];
var labelWidth = 0;
var allowedBarHeight = 0;
var barHeight = 0;
var maxBarWidth = 0;
var gridLabelSpacing = 5;
var valuePadding = 10;
var labelBarSpacing = 15; // this will be overridden to match the size of the labels
var DEBUG = false;
var fullScreenChangeListener;
var webkitFullScreenChangeListener;
var mozFullScreenChangeListener;
var msFullScreenChangeListener;
var resizeListener;
var isDestroyed = false;
var resizeTimer = null;
var options = graphOptions;
// background and padding
if (!options.backgroundColor) {
options.backgroundColor = new _Color2.default.Color('#000000');
} else {
options.backgroundColor = new _Color2.default.Color(options.backgroundColor);
}
if (options.padding === undefined) {
options.padding = 0;
} else {
options.padding = parseFloat(options.padding);
}
// grid
if (!options.gridLineColor) {
options.gridLineColor = new _Color2.default.Color('#ffffff');
} else {
options.gridLineColor = new _Color2.default.Color(options.gridLineColor);
}
if (!options.gridFontColor) {
options.gridFontColor = new _Color2.default.Color('#ffffff');
} else {
options.gridFontColor = new _Color2.default.Color(options.gridFontColor);
}
if (!options.gridFontSize) {
options.gridFontSize = 12;
} else {
options.gridFontSize = parseFloat(options.gridFontSize);
}
if (!options.gridFontFamily) {
options.gridFontFamily = 'Arial';
}
// labels
if (!options.labelFontColor) {
options.labelFontColor = options.gridLineColor;
} else {
options.labelFontColor = new _Color2.default.Color(options.labelFontColor);
}
if (!options.labelFontSize) {
options.labelFontSize = 12;
} else {
options.labelFontSize = parseFloat(options.labelFontSize);
}
if (!options.labelFontFamily) {
options.labelFontFamily = 'Arial';
}
// bar
if (!options.barSpacing) {
options.barSpacing = 1; // as a percent of the width of each bar
} else {
options.barSpacing = _Numbers2.default.makeNumber(options.barSpacing);
}
// values on mouseover
if (!options.valueFontColor) {
options.valueFontColor = options.labelFontColor;
} else {
options.valueFontColor = new _Color2.default.Color(options.valueFontColor);
}
if (!options.valueFontSize) {
options.valueFontSize = 50;
} else {
options.valueFontSize = parseFloat(options.valueFontSize);
}
if (!options.valueFontFamily) {
options.valueFontFamily = 'Arial';
}
if (options.abbreviateGridValues !== true) {
options.abbreviateGridValues = false;
}
if (options.showLegend !== false) {
options.showLegend = true;
}
var needsRender = false;
this.render = function (exportAsImage, scale) {
if (!data) {
return false;
}
var canvas = htmlCanvas;
var ctx = canvas.getContext('2d');
if (!scale) {
scale = 1;
}
var backingStoreRatio = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1;
if (exportAsImage) {
canvas = document.createElement('CANVAS');
canvas.width = htmlCanvas.width * scale;
canvas.height = htmlCanvas.height * scale;
ctx = canvas.getContext('2d');
}
// upscale this thang if the device pixel ratio is higher than 1
ctx.save();
if (pxRatio > 1 || scale !== 1) {
ctx.scale(pxRatio / backingStoreRatio * scale, pxRatio / backingStoreRatio * scale);
}
var canvasWidth = canvas.width / (pxRatio / backingStoreRatio * scale);
var canvasHeight = canvas.height / (pxRatio / backingStoreRatio * scale);
// fill background
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
if (options.backgroundColor.isTransparent()) {
ctx.fillStyle = options.backgroundColor.toString();
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
}
// render the labels down the middle
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = options.labelFontColor.toString();
ctx.font = options.labelFontSize + 'px ' + options.labelFontFamily;
//var allowedBarHeight = ((canvasHeight - plotVars.graphArea.top - plotVars.graphArea.bottom) / allLabels.length);
//var barHeight = allowedBarHeight / (1 + options.barSpacing);
for (var i = 0; i < allLabels.length; i++) {
var y = plotVars.graphArea.top + allowedBarHeight * i + allowedBarHeight / 2;
ctx.fillText(allLabels[i], canvasWidth / 2, y);
}
labelBarSpacing = options.labelFontSize;
// calculate the nice round values for the y axis
var _HelpersCharts$yAxis = _Charts2.default.yAxis(canvasWidth / 2 - plotVars.graphArea.right - labelWidth / 2 - labelBarSpacing, plotVars.maxY, plotVars.minY, options.gridFontSize);
var numberOfLabels = _HelpersCharts$yAxis.numberOfLabels;
var valueBetweenLabels = _HelpersCharts$yAxis.valueBetweenLabels;
plotVars.chartMaxY = plotVars.minY + numberOfLabels * valueBetweenLabels;
// draw the grid and grid labels
ctx.strokeStyle = options.gridLineColor.toString();
ctx.fillStyle = options.gridFontColor.toString();
ctx.font = options.gridFontSize + 'px ' + options.gridFontFamily;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
// shift the grid slightly so the half of the last label that hangs off the right of the screen can be nudged back onto the visible area
var hangoverLabelWidth = ctx.measureText(_Numbers2.default.formatNumber(numberOfLabels * valueBetweenLabels)).width / 2;
plotVars.graphArea.right = options.padding + hangoverLabelWidth;
plotVars.graphArea.left = options.padding + hangoverLabelWidth;
maxBarWidth = canvasWidth / 2 - labelWidth / 2 - plotVars.graphArea.right - labelBarSpacing;
// iterate and render
for (var i = 0; i <= numberOfLabels; i++) {
var x = canvasWidth / 2 + labelWidth / 2 + labelBarSpacing + i * valueBetweenLabels / plotVars.chartMaxY * maxBarWidth;
ctx.beginPath();
ctx.moveTo(x, plotVars.graphArea.top);
ctx.lineTo(x, canvasHeight - plotVars.graphArea.bottom);
ctx.stroke();
var labelstr = options.abbreviateGridValues ? _Numbers2.default.formatNumber(i * valueBetweenLabels, 1, true) : _Numbers2.default.formatNumber(i * valueBetweenLabels);
ctx.fillText(labelstr, x, options.padding);
x = canvasWidth / 2 - labelWidth / 2 - labelBarSpacing - i * valueBetweenLabels / plotVars.chartMaxY * maxBarWidth;
ctx.beginPath();
ctx.moveTo(x, plotVars.graphArea.top);
ctx.lineTo(x, canvasHeight - plotVars.graphArea.bottom);
ctx.stroke();
labelstr = options.abbreviateGridValues ? _Numbers2.default.formatNumber(i * valueBetweenLabels, 1, true) : _Numbers2.default.formatNumber(i * valueBetweenLabels);
ctx.fillText(labelstr, x, options.padding);
}
// iterate over each data series
for (var i = 0; i < data.length; i++) {
// iterate over each bar and draw it
for (var j = 0; j < data[i].data.length; j++) {
var barWidth = data[i].data[j].value / plotVars.chartMaxY * maxBarWidth;
var x;
if (i === 1) {
x = canvasWidth / 2 + labelWidth / 2 + labelBarSpacing;
} else {
x = canvasWidth / 2 - labelWidth / 2 - labelBarSpacing - barWidth;
}
var y = plotVars.graphArea.top + allowedBarHeight * j + (allowedBarHeight - barHeight) / 2;
// draw highlight under the bar if necessary
if (data[i].data[j].isHighlighted) {
var hcolor = new _Color2.default.Color(options.gridLineColor);
hcolor.fade(0.5);
ctx.fillStyle = hcolor.getRgba();
if (i === 1) {
ctx.fillRect(x, y - (allowedBarHeight - barHeight) / 2, maxBarWidth, allowedBarHeight);
} else {
ctx.fillRect(plotVars.graphArea.left, y - (allowedBarHeight - barHeight) / 2, maxBarWidth, allowedBarHeight);
}
}
// draw the bar
ctx.fillStyle = data[i].data[j].color;
ctx.fillRect(x, y, barWidth, barHeight);
// draw highlight if necessary
if (data[i].data[j].isHighlighted) {
ctx.textBaseline = 'middle';
ctx.fillStyle = options.valueFontColor.toString();
ctx.textAlign = 'center';
var fsize = options.valueFontSize;
if (fsize > barHeight) {
fsize = barHeight;
}
ctx.font = fsize + 'px ' + options.valueFontFamily;
var displayValue = data[i].data[j].displayValue;
if (displayValue === undefined) {
displayValue = data[i].data[j].prefix + data[i].data[j].value + data[i].data[j].suffix;
}
if (i === 1) {
ctx.textAlign = 'right';
ctx.fillText(displayValue, canvasWidth - plotVars.graphArea.right - valuePadding, y + barHeight / 2);
} else {
ctx.textAlign = 'left';
ctx.fillText(displayValue, plotVars.graphArea.left + valuePadding, y + barHeight / 2);
}
}
}
}
// 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();
if (exportAsImage) {
return canvas.toDataURL('image/png');
}
};
function handleMouseDown(pt, event) {
var dataSlice = hitTestBars(pt);
if (dataSlice && dataSlice.onClick) {
dataSlice.onClick(event);
}
needsRender = true;
}
/***
* Handles mouseout
***/
function handleMouseOut(pt, event) {
// mark all bars as not highlighted
for (var i = 0; i < data.length; i++) {
for (var j = 0; j < data[i].data.length; j++) {
data[i].data[j].isHighlighted = false;
}
}
needsRender = true;
}
/***
* 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
***/
function handleMouseMove(pt) {
// mark all bars as not highlighted
for (var i = 0; i < data.length; i++) {
for (var j = 0; j < data[i].data.length; j++) {
data[i].data[j].isHighlighted = false;
}
}
// hit test all bars
var dataSlice = hitTestBars(pt);
if (dataSlice) {
dataSlice.isHighlighted = true;
}
needsRender = true;
}
function hitTestBars(pt) {
for (var i = 0; i < data.length; i++) {
for (var j = 0; j < data[i].data.length; j++) {
var barWidth = data[i].data[j].value / plotVars.chartMaxY * maxBarWidth;
var x;
if (i === 1) {
x = canvasWidth / 2 + labelWidth / 2 + labelBarSpacing;
} else {
x = plotVars.graphArea.left;
}
var y = plotVars.graphArea.top + allowedBarHeight * j + (allowedBarHeight - barHeight) / 2;
// mark as highlighted
if (pt.x >= x && pt.x < x + maxBarWidth && pt.y >= y && pt.y < y + barHeight) {
return data[i].data[j];
}
}
}
return null;
}
this.getBase64Image = function (scale) {
// call render with a special flag
return this.render(true, scale);
};
/***
* Initialize some basic vars including the html canvas to render on
***/
function _init() {
var _this = this;
htmlCanvas = document.createElement('CANVAS');
htmlContainer.appendChild(htmlCanvas);
this.setData(graphData);
this.fillParent();
needsRender = true;
var guid = _Charts2.default.createGuid();
_ChartDictionary2.default.push({ id: guid, canvasElement: htmlCanvas, chart: this });
htmlCanvas.setAttribute('data-chart-id', guid);
resizeListener = window.addEventListener('resize', function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () {
_this.fillParent.call(_this);
}, 50);
});
fullScreenChangeListener = window.addEventListener('webkitfullscreenchange', fullscreenChange);
webkitFullScreenChangeListener = window.addEventListener('fullscreenchange', fullscreenChange);
mozFullScreenChangeListener = window.addEventListener('mozfullscreenchange', fullscreenChange);
msFullScreenChangeListener = window.addEventListener('msfullscreenchange', fullscreenChange);
function fullscreenChange(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) {
canvases[c].width = 1;
canvases[c].height = 1;
canvases[c].style.width = '1px';
canvases[c].style.height = '1px';
setTimeout(self.fillParent.bind(self), 10);
break;
}
}
}
}
if (options.showLegend) {
// mousemove event
htmlCanvas.addEventListener('mousemove', function (event) {
var pt = getMouseCoords(htmlCanvas, event);
handleMouseMove(pt, event);
});
// mouseout event
htmlCanvas.addEventListener('mouseout', function (event) {
var pt = getMouseCoords(htmlCanvas, event);
handleMouseOut(pt, event);
});
htmlCanvas.addEventListener('mousedown', function (event) {
var pt = getMouseCoords(htmlCanvas, event);
handleMouseDown(pt, event);
});
}
if (window.requestAnimationFrame) {
window.requestAnimationFrame(animateFrame);
} else if (window.webkitRequestAnimationFrame) {
window.webkitRequestAnimationFrame(animateFrame);
} else if (window.mozRequestAnimationFrame) {
window.mozRequestAnimationFrame(animateFrame);
} else if (window.oRequestAnimationFrame) {
window.oRequestAnimationFrame(animateFrame);
}
}
this.setData = function (graphData) {
data = graphData || [];
// Sort data - Label and Data sort must be the same
for (var i = 0; i < data.length; i++) {
data[i].data.sort(function (objA, objB) {
if (objA.name > objB.name) {
return 1;
} else if (objA.name < objB.name) {
return -1;
} else {
// objA must be equal to objB
return 0;
}
});
}
for (var i = 0; i < data.length; i++) {
for (var j = 0; j < data[i].data.length; j++) {
var parts = _Numbers2.default.separateNumberUnits(data[i].data[j].value);
if (data[i].data[j].prefix === undefined) {
data[i].data[j].prefix = parts.prefix;
}
if (data[i].data[j].suffix === undefined) {
data[i].data[j].suffix = parts.suffix;
}
data[i].data[j].value = parts.value;
}
}
needsRender = true;
};
this.fillParent = function () {
if (htmlCanvas && htmlCanvas.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);
// upscale this thang if the device pixel ratio is higher than 1
var pxRatio = window.devicePixelRatio || 1;
htmlCanvas.width = width * pxRatio;
htmlCanvas.height = height * pxRatio;
htmlCanvas.style.width = width + 'px';
htmlCanvas.style.height = height + 'px';
}
needsRender = true;
}.bind(this);
/***
* 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
***/
function animateFrame() {
var thisFrame = new Date().getTime();
var elapsed = thisFrame - lastFrame; // elapsed time since last render
fps = 1000 / elapsed;
// calculate sizes of things to render
var ctx = htmlCanvas.getContext('2d');
backingStoreRatio = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1;
canvasWidth = htmlCanvas.width / (pxRatio / backingStoreRatio);
canvasHeight = htmlCanvas.height / (pxRatio / backingStoreRatio);
// put all the labels from both sides into one array
allLabels = [];
for (var i = 0; i < data.length; i++) {
for (var j = 0; j < data[i].data.length; j++) {
if (allLabels.indexOf(data[i].data[j].name) === -1) {
allLabels.push(data[i].data[j].name);
}
}
}
// Sort data - Label and Data sort must be the same - See "Sort data" below
allLabels.sort();
labelWidth = 0;
ctx.font = options.labelFontSize + 'px ' + options.labelFontFamily;
for (var i = 0; i < allLabels.length; i++) {
var lw = ctx.measureText(allLabels[i]).width;
if (lw > labelWidth) {
labelWidth = lw;
}
}
plotVars = {
graphArea: {
top: options.padding + options.gridFontSize + gridLabelSpacing,
right: options.padding,
bottom: options.padding,
left: options.padding
}
};
plotVars.graphArea.width = canvasWidth - plotVars.graphArea.left - plotVars.graphArea.right;
plotVars.graphArea.height = canvasHeight - plotVars.graphArea.top - plotVars.graphArea.bottom;
allowedBarHeight = (canvasHeight - plotVars.graphArea.top - plotVars.graphArea.bottom) / allLabels.length;
barHeight = allowedBarHeight / (1 + options.barSpacing);
// measure the x distance available for the bars
maxBarWidth = canvasWidth / 2 - labelWidth / 2 - plotVars.graphArea.right - labelBarSpacing;
// get the max value
plotVars.maxY = undefined;
for (var i = 0; i < data.length; i++) {
for (var j = 0; j < data[i].data.length; j++) {
if (plotVars.maxY === undefined || data[i].data[j].value > plotVars.maxY) {
plotVars.maxY = data[i].data[j].value;
}
}
}
plotVars.minY = 0;
// animation effects
if (options.animation) {
for (var i = 0; i < data.length; i++) {
if (!data[i].animationFinished) {
var tmp = elapsed / options.animationDuration * maxvalue;
if (tmp > data[i].value) {
tmp = data[i].value;
data[i].animationFinished = true;
}
data[i].displayValue = tmp;
}
}
}
if (needsRender) {
self.render();
needsRender = false;
}
lastFrame = thisFrame;
if (!isDestroyed) {
if (window.requestAnimationFrame) {
window.requestAnimationFrame(animateFrame);
} else if (window.webkitRequestAnimationFrame) {
window.webkitRequestAnimationFrame(animateFrame);
} else if (window.mozRequestAnimationFrame) {
window.mozRequestAnimationFrame(animateFrame);
} else if (window.oRequestAnimationFrame) {
window.oRequestAnimationFrame(animateFrame);
}
}
}
var lastFrame = new Date().getTime(); // the timestamp of the last time the frame was last rendered
var fps = 0;
function getMouseCoords(htmlCanvas, event) {
// get the x,y offset of the mouse move point relative to the canvas
var rect = htmlCanvas.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
}
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 = NuviBiDirectionalBarGraph;