UNPKG

transcend-charts

Version:

Transcend is a charting and graph library for NUVI

629 lines (547 loc) 22.2 kB
'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;