UNPKG

transcend-charts

Version:

Transcend is a charting and graph library for NUVI

1,027 lines (898 loc) 37.6 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _lodash = require('lodash'); var _lodash2 = _interopRequireDefault(_lodash); var _Color = require('./Color'); var _Color2 = _interopRequireDefault(_Color); var _Numbers = require('../helpers/Numbers'); var _Numbers2 = _interopRequireDefault(_Numbers); var _Charts = require('../helpers/Charts'); var _Charts2 = _interopRequireDefault(_Charts); var _ChartDictionary = require('./ChartDictionary'); var _ChartDictionary2 = _interopRequireDefault(_ChartDictionary); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var PieCircleChart = function PieCircleChart(htmlContainer, chartData, chartOptions) { var htmlCanvas = null; var self = this; var needsRender = false; var options = chartOptions; var maxValue = 0; var pxRatio = window.devicePixelRatio || 1; var DEBUG = false; var fullScreenChangeListener; var webkitFullScreenChangeListener; var mozFullScreenChangeListener; var msFullScreenChangeListener; var resizeListener; var isDestroyed = false; var resizeTimer = null; // background and padding if (!options.backgroundColor) { options.backgroundColor = new _Color2.default.Color('transparent'); } else { options.backgroundColor = new _Color2.default.Color(options.backgroundColor); } if (!options.padding) { options.padding = 0; } else { options.padding = parseFloat(options.padding); } // circles if (!options.fillColor) { options.fillColor = new _Color2.default.Color('#00ffff'); } else { options.fillColor = new _Color2.default.Color(options.fillColor); } if (!options.zeroFillColor) { if (options.backgroundColor.toString() !== 'transparent') { options.zeroFillColor = _Color2.default.makeColorBetween(options.fillColor, options.backgroundColor, 0.5); } else { options.zeroFillColor = new _Color2.default.Color(options.fillColor); } } else { options.zeroFillColor = new _Color2.default.Color(options.zeroFillColor); } if (!options.borderColor) { options.borderColor = new _Color2.default.Color('transparent'); } else { options.borderColor = new _Color2.default.Color(options.borderColor); } // labels if (!options.labelFontColor) { options.labelFontColor = new _Color2.default.Color('#444444'); } else { options.labelFontColor = new _Color2.default.Color(options.labelFontColor); } if (!options.labelFontFamily) { options.labelFontFamily = 'Arial'; } if (!options.labelFontWeight) { options.labelFontWeight = '400'; } // values if (!options.valueFontColor) { options.valueFontColor = new _Color2.default.Color('#444444'); } else { options.valueFontColor = new _Color2.default.Color(options.valueFontColor); } if (!options.valueFontFamily) { options.valueFontFamily = 'Arial'; } if (!options.valueFontWeight) { options.valueFontWeight = '400'; } if (!options.valueFontSize) { options.valueFontSize = null; options.valueFontSizeAsPct = 0.08; } else if (typeof options.valueFontSize === 'string' && options.valueFontSize.indexOf('%') !== -1) { options.valueFontSizeAsPct = parseFloat(options.valueFontSize) / 100; } else { options.valueFontSize = parseFloat(options.valueFontSize); } // knockout circle if (!options.knockoutCenterColor) { options.knockoutCenterColor = new _Color2.default.Color('rgba(47, 47, 47, 1)'); } else { options.knockoutCenterColor = new _Color2.default.Color(options.knockoutCenterColor); } if (!options.knockoutOuterColor) { options.knockoutOuterColor = new _Color2.default.Color('rgba(47, 47, 47, 0.85)'); } else { options.knockoutOuterColor = new _Color2.default.Color(options.knockoutOuterColor); } // stripes if (!options.stripeWidth) { options.stripeWidth = 0; } else { options.stripeWidth = parseFloat(options.stripeWidth); } if (!options.stripeSpacing) { options.stripeSpacing = 10; } else { options.stripeSpacing = parseFloat(options.stripeSpacing); } if (!options.stripeColor) { options.stripeColor = new _Color2.default.Color('#ffffff'); } else { options.stripeColor = new _Color2.default.Color(options.stripeColor); } // draggabilitiy if (options.allowDrag !== false) { options.allowDrag = true; } var data = []; var hollowCircleRingWidth = 25; var knockoutCircleRadiusAsPct = 0.75; var preferredLabelWidthAsPctOfDiameter = 0.9; var spirographRadiusAsPct = 0.85; this.render = function (exportAsImage, scale) { // If there's no HTML canvas made something went horribly wrong. Abort! if (!htmlCanvas) { return; } 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); // clear the background, ready to re-render ctx.clearRect(0, 0, canvasWidth, canvasHeight); if (options.backgroundColor !== 'transparent') { ctx.fillStyle = options.backgroundColor; ctx.fillRect(0, 0, canvasWidth, canvasHeight); } // sort the effing circles data.sort(function (a, b) { return b.value - a.value; }); if (circlesArePacked == false) { packCircles(); } // actually render the circles now for (var i = 0; i < data.length; i++) { // determine if something is highlighted var isSomethingHighlighted = data.some(function (datum) { return datum.segments.some(function (segment) { return segment.isHighlighted; }); }); // render the blasted pie slices var movingRadian = -Math.PI / 2; for (var s = 0; s < data[i].segments.length; s++) { var totalValue = _lodash2.default.sum(data[i].segments, function (segment) { return segment.value; }); var percent = data[i].segments[s].value / totalValue; data[i].segments[s].startRadian = movingRadian; data[i].segments[s].endRadian = movingRadian + percent * Math.PI * 2; var fillColor = new _Color2.default.Color(data[i].segments[s].fillColor); if (isSomethingHighlighted && !data[i].segments[s].isHighlighted) { fillColor.fade(0.7); } ctx.fillStyle = fillColor.toString(); var strokeColor = new _Color2.default.Color(data[i].segments[s].borderColor); ctx.strokeStyle = strokeColor.toString(); ctx.lineWidth = options.borderWidth; ctx.beginPath(); ctx.moveTo(data[i].x, data[i].y); ctx.lineTo(data[i].x + Math.cos(data[i].segments[s].startRadian) * data[i].radius, data[i].y + Math.sin(data[i].segments[s].startRadian) * data[i].radius); ctx.arc(data[i].x, data[i].y, data[i].radius, data[i].segments[s].startRadian, data[i].segments[s].endRadian); ctx.lineTo(data[i].x, data[i].y); ctx.fill(); ctx.stroke(); // stripes if (options.stripeWidth && (!isSomethingHighlighted || data[i].segments[s].isHighlighted)) { ctx.save(); ctx.clip(); ctx.lineWidth = options.stripeWidth; ctx.strokeStyle = options.stripeColor.toString(); ctx.beginPath(); for (var x = data[i].x - data[i].radius - data[i].radius * 2; x < data[i].x + data[i].radius; x += options.stripeSpacing) { ctx.moveTo(x, data[i].y + data[i].radius); ctx.lineTo(x + data[i].radius * 2, data[i].y - data[i].radius); } ctx.stroke(); ctx.restore(); } movingRadian = data[i].segments[s].endRadian; } // hollow center var fillGradient = ctx.createRadialGradient(data[i].x, data[i].y, 0, data[i].x, data[i].y, data[i].radius * knockoutCircleRadiusAsPct); fillGradient.addColorStop(0.55, options.knockoutCenterColor); fillGradient.addColorStop(1, options.knockoutOuterColor); ctx.fillStyle = fillGradient; ctx.beginPath(); ctx.arc(data[i].x, data[i].y, data[i].radius * knockoutCircleRadiusAsPct, 0, Math.PI * 2); ctx.fill(); var preferredLabelWidth = data[i].radius * knockoutCircleRadiusAsPct * 2 * preferredLabelWidthAsPctOfDiameter; // calculate the right font size to use for the label so it fits inside the circle var fsize = 24; while (true) { ctx.font = options.labelFontWeight + ' ' + fsize + 'px ' + options.labelFontFamily; var isRightSize = true; if (fsize * data[i].nameAsTokens.length > preferredLabelWidth) { isRightSize = false; } else { for (var l = 0; l < data[i].nameAsTokens.length; l++) { if (ctx.measureText(data[i].nameAsTokens[l]).width > preferredLabelWidth) { isRightSize = false; } } } if (isRightSize) { break; } fsize = Math.floor(0.9 * fsize); if (fsize <= 1) { break; } } // render the label var labelFontColor = new _Color2.default.Color(options.labelFontColor); var isPartOfPieHighlighted = data[i].segments.some(function (segment) { return segment.isHighlighted; }); if (isSomethingHighlighted && !isPartOfPieHighlighted) { labelFontColor.fade(0.7); } ctx.fillStyle = labelFontColor.toString(); ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; for (var l = 0; l < data[i].nameAsTokens.length; l++) { ctx.fillText(data[i].nameAsTokens[l], data[i].x, data[i].y + (l - (data[i].nameAsTokens.length - 1) / 2) * fsize); } } // show value for highlighted circle (if any) for (var i = 0; i < data.length; i++) { for (var s = 0; s < data[i].segments.length; s++) { if (data[i].segments[s].isHighlighted) { ctx.fillStyle = options.valueFontColor.toString(); var displayValue = data[i].segments[s].prefix + data[i].segments[s].value + data[i].segments[s].suffix; if (data[i].segments[s].displayValue) { displayValue = data[i].segments[s].displayValue; } if (options.valueFontSizeAsPct) { options.valueFontSize = options.valueFontSizeAsPct * canvasHeight; } var labelFontSize = options.valueFontSize * 0.5; var totalLabelHeight = options.valueFontSize + labelFontSize; var valueTop = data[i].y + data[i].radius + 2; if (valueTop + totalLabelHeight > canvasHeight - options.padding) { valueTop = canvasHeight - options.padding - totalLabelHeight; } ctx.textBaseline = 'top'; // label ctx.font = '300 ' + labelFontSize + 'px ' + options.valueFontFamily; ctx.fillText(data[i].segments[s].name, data[i].x, valueTop); // value ctx.font = options.valueFontWeight + ' ' + options.valueFontSize + 'px ' + options.valueFontFamily; ctx.fillText(displayValue, data[i].x, valueTop + labelFontSize); } } } // 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'); } }; /*** * The function that handles all the circle packing. This method should be called when the class is first rendered * and any time the canvas size changes ***/ var circlesArePacked = false; var circlesArePacking = false; function packCircles() { if (circlesArePacking) { return; } circlesArePacking = true; circlesArePacked = false; if (DEBUG) { console.log('packCircles start: ' + new Date().getTime()); } var ctx = htmlCanvas.getContext('2d'); var backingStoreRatio = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1; var canvasWidth = htmlCanvas.width / (pxRatio / backingStoreRatio); var canvasHeight = htmlCanvas.height / (pxRatio / backingStoreRatio); // sort the data largest to smallest data.sort(function (a, b) { return b.value - a.value; }); // get the highest value in the set (it's the first one since we just sorted it) maxValue = data[0].value; // calculate total relative area of all datas var totalArea = _lodash2.default.sum(data, function (datum) { return datum.value / maxValue; }); // get the max radius and area var shortestCanvasSide = canvasWidth > canvasHeight ? canvasHeight - options.padding * 2 : canvasWidth - options.padding * 2; var maxRadius = shortestCanvasSide * 0.5 / Math.sqrt(totalArea); while (true) { var maxArea = maxRadius * maxRadius * Math.PI; // calculate the radii of all the circles based on the given max for (var i = 0; i < data.length; i++) { var ratio = data[i].value / maxValue; var area = ratio * maxArea; var radius = Math.sqrt(area / Math.PI); data[i].radius = radius; } // pack the circles var x = options.padding + (canvasWidth - options.padding * 2) / 2; var y = options.padding + (canvasHeight - options.padding * 2) / 2; data[0].x = x; data[0].y = y; var tmp = fitCircle(1); if (tmp) { break; } maxRadius *= 0.9; } if (DEBUG) { console.log('packCircles end: ' + new Date().getTime()); } needsRender = true; circlesArePacking = false; circlesArePacked = true; // The recursive method that finds where each circle can best fit. // This method should only be called by packCircles() - that's why it's defined within the scope of packCircles() function fitCircle(index) { if (index >= data.length) return true; // iterate over all the circles that have already been mentally placed for (var a = 0; a < index; a++) { // try several angles branching off of the other circle for (var angle = 0; angle < Math.PI * 2; angle += Math.PI / 12) { // use some trig to calculate x, y var x = data[a].x + Math.cos(angle) * (data[a].radius + data[index].radius); var y = data[a].y + Math.sin(angle) * (data[a].radius + data[index].radius); // test if it fits within the canvas if (x - data[index].radius < options.padding || x + data[index].radius > canvasWidth - options.padding || y - data[index].radius < options.padding || y + data[index].radius > canvasHeight - options.padding) { continue; } // assign the coordinates to this circle data[index].x = x; data[index].y = y; // test if it collides with any other circles var didFit = true; for (var j = 0; j < index; j++) { var sqdist = (data[j].x - data[index].x) * (data[j].x - data[index].x) + (data[j].y - data[index].y) * (data[j].y - data[index].y); if (sqdist < (data[j].radius + data[index].radius) * (data[j].radius + data[index].radius)) { didFit = false; break; } } // if it fits let's recursively add the next circle if (didFit) { // pack the next circle if (fitCircle(index + 1)) { return true; } } } } return false; } } /*** * Handles the window.resize event so the canvas can be made to fill the parent automatically ***/ var _packTimer = null; function fillParent() { 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 htmlCanvas.width = width * pxRatio; htmlCanvas.height = height * pxRatio; htmlCanvas.style.width = width + 'px'; htmlCanvas.style.height = height + 'px'; if (data && data.length) { clearTimeout(_packTimer); _packTimer = setTimeout(packCircles, 250); } } } var mouseProperties = { isDown: false, isDragging: false, lastPt: { x: 0, y: 0 }, clickOffset: { x: 0, y: 0 }, draggedObject: null }; /*** * Handles mouse down events on the map canvas ***/ function handleMouseDown(pt, e) { // set some kind of flag indicating that the mouse is down (so we can track dragging) mouseProperties.isDown = true; mouseProperties.isDragging = false; // find which bubble was clicked mouseProperties.draggedObject = hitTestCircles(pt); if (mouseProperties.draggedObject) { mouseProperties.clickOffset = { x: pt.x - mouseProperties.draggedObject.x, y: pt.y - mouseProperties.draggedObject.y }; // mouseProperties.isDragging = true; } // update the mouseProperties.lastPt mouseProperties.lastPt = { x: pt.x, y: pt.y }; e.stopPropagation(); e.preventDefault(); needsRender = true; } function hitTestCircles(pt) { for (var i = 0; i < data.length; i++) { var sqdist = (data[i].x - pt.x) * (data[i].x - pt.x) + (data[i].y - pt.y) * (data[i].y - pt.y); if (sqdist < data[i].radius * data[i].radius) { return data[i]; } } return null; } /*** * Handles mouse move events on the map canvas ***/ function handleMouseMove(pt, e) { // if we're dragging, move the circle under the mouse if (mouseProperties.draggedObject) { mouseProperties.isDragging = true; mouseProperties.draggedObject.x = pt.x - mouseProperties.clickOffset.x; mouseProperties.draggedObject.y = pt.y - mouseProperties.clickOffset.y; // sort all circles into an array ordered by nearest to dragged circle data.sort(function (a, b) { var dist_a = (a.x - mouseProperties.draggedObject.x) * (a.x - mouseProperties.draggedObject.x) + (a.y - mouseProperties.draggedObject.y) * (a.y - mouseProperties.draggedObject.y); var dist_b = (b.x - mouseProperties.draggedObject.x) * (b.x - mouseProperties.draggedObject.x) + (b.y - mouseProperties.draggedObject.y) * (b.y - mouseProperties.draggedObject.y); return dist_a > dist_b; }); // iterate through sorted circles for (var j = 1; j < data.length; j++) { for (var i = 0; i < j; i++) { // item i has positional priority var primaryObj = data[i]; var secondaryObj = data[j]; // if secondaryObj is too close to primaryObj var sqdist = (primaryObj.x - secondaryObj.x) * (primaryObj.x - secondaryObj.x) + (primaryObj.y - secondaryObj.y) * (primaryObj.y - secondaryObj.y); if (sqdist < (primaryObj.radius + secondaryObj.radius) * (primaryObj.radius + secondaryObj.radius)) { // calculate the perpendicluar angle from the previous circle var angle = Math.atan((secondaryObj.y - primaryObj.y) / (secondaryObj.x - primaryObj.x)); var newRelativePos = { x: Math.cos(angle) * (primaryObj.radius + secondaryObj.radius), y: Math.sin(angle) * (primaryObj.radius + secondaryObj.radius) }; if (secondaryObj.x - primaryObj.x > 0) { newRelativePos.x = Math.abs(newRelativePos.x); } else { newRelativePos.x = -Math.abs(newRelativePos.x); } if (secondaryObj.y - primaryObj.y > 0) { newRelativePos.y = Math.abs(newRelativePos.y); } else { newRelativePos.y = -Math.abs(newRelativePos.y); } // move the circle secondaryObj.x = primaryObj.x + newRelativePos.x; secondaryObj.y = primaryObj.y + newRelativePos.y; } } } } else { // if no circles are being dragged, let's see if one is highlighted var hoveredDataSlice = hitTestPieSlices(pt, function (dataSlice) { dataSlice.isHighlighted = true; }, function (dataSlice) { dataSlice.isHighlighted = false; }); } // update the mouseProperties.lastPt mouseProperties.lastPt = { x: pt.x, y: pt.y }; e.stopPropagation(); e.preventDefault(); needsRender = true; } /*** * Handles mouse up events on the map canvas ***/ function handleMouseUp(pt, e) { if (!mouseProperties.isDragging) { var clickedSlice = hitTestPieSlices(pt); if (clickedSlice.onClick) { if (clickedSlice && clickedSlice.onClick) { clickedSlice.onClick(e); } } } // set the mousedown and dragging flags to false mouseProperties.isDown = false; mouseProperties.isDragging = false; mouseProperties.draggedObject = null; e.stopPropagation(); e.preventDefault(); needsRender = true; } /*** * Handles the mouse out event (so we can stop dragging if you were.) * Thanks, Blake for finding this issue ***/ function handleMouseOut(pt, e) { for (var i = 0; i < data.length; i++) { for (var s = 0; s < data[i].segments.length; s++) { data[i].segments[s].isHighlighted = false; } } mouseProperties.isDragging = false; mouseProperties.isDown = false; mouseProperties.draggedObject = null; e.stopPropagation(); e.preventDefault(); needsRender = true; } function hitTestPieSlices(pt, onHit, onMiss) { // hit test all the pie slices var highlightedDataSlice = null; for (var i = 0; i < data.length; i++) { var pieCenter = { x: data[i].x, y: data[i].y }; var pieRadius = data[i].radius; // calculate the theta and radius based on the x, y var theta = Math.atan((pt.y - pieCenter.y) / (pt.x - pieCenter.x)); if (pt.y >= pieCenter.y && pt.x >= pieCenter.x) { // quadrant 1 theta += 0; } else if (pt.y >= pieCenter.y && pt.x < pieCenter.x) { // quadrant 2 theta += Math.PI; } else if (pt.y < pieCenter.y && pt.x < pieCenter.x) { // quadrant 3 theta += Math.PI; } else if (pt.y < pieCenter.y && pt.x >= pieCenter.x) { // quadrant 4 theta += 0; } var radius = Math.sqrt((pt.x - pieCenter.x) * (pt.x - pieCenter.x) + (pt.y - pieCenter.y) * (pt.y - pieCenter.y)); var totalValue = _lodash2.default.sum(data[i].segments, function (segment) { return segment.value; }); for (var s = 0; s < data[i].segments.length; s++) { var startRadian = data[i].segments[s].startRadian; var endRadian = data[i].segments[s].endRadian; // if the mouse is over this pie slice, mark it as highlighted if (theta >= startRadian && theta < endRadian && radius <= pieRadius) { if (onHit) { onHit(data[i].segments[s]); } highlightedDataSlice = data[i].segments[s]; } else { if (onMiss) { onMiss(data[i].segments[s]); } } } } // if you've already highlighted a pie slice, exit now if (highlightedDataSlice) return highlightedDataSlice; } /*** * 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; if (!circlesArePacking && circlesArePacked) { // Get the context of the canvas var ctx = htmlCanvas.getContext('2d'); // upscale this thang if the device pixel ratio is higher than 1 var backingStoreRatio = ctx.webkitBackingStorePixelRatio || ctx.mozBackingStorePixelRatio || ctx.msBackingStorePixelRatio || ctx.oBackingStorePixelRatio || ctx.backingStorePixelRatio || 1; var canvasWidth = htmlCanvas.width / (pxRatio / backingStoreRatio); var canvasHeight = htmlCanvas.height / (pxRatio / backingStoreRatio); if (options.allowDrag) { // sort all circles into an array ordered by nearest to center data.sort(function (a, b) { var dist_a = (a.x - canvasWidth / 2) * (a.x - canvasWidth / 2) + (a.y - canvasHeight / 2) * (a.y - canvasHeight / 2); var dist_b = (b.x - canvasWidth / 2) * (b.x - canvasWidth / 2) + (b.y - canvasHeight / 2) * (b.y - canvasHeight / 2); return dist_a > dist_b; }); // iterate through sorted circles for (var j = 0; j < data.length; j++) { var secondaryObj = data[j]; var oldPos = { x: secondaryObj.x, y: secondaryObj.y }; // move toward the center if (data[j] !== mouseProperties.draggedObject && Math.abs(secondaryObj.y - canvasHeight / 2) !== 0 && Math.abs(secondaryObj.x - canvasWidth / 2) !== 0) { // only move if you're not already at the center var v; if (Math.abs(secondaryObj.y - canvasHeight / 2) <= 1 && Math.abs(secondaryObj.x - canvasWidth / 2) <= 1) { secondaryObj.x = canvasWidth / 2; secondaryObj.y = canvasHeight / 2; } else { var angleToCenter = Math.PI * 0.5; // divide by zero case if (secondaryObj.x - canvasWidth / 2 !== 0) { angleToCenter = Math.atan((secondaryObj.y - canvasHeight / 2) / (secondaryObj.x - canvasWidth / 2)); } var circleSpeed = _lodash2.default.min([canvasWidth, canvasHeight]) / 250; var v = { x: Math.cos(angleToCenter), y: Math.sin(angleToCenter) }; if (secondaryObj.x - canvasWidth / 2 > 0) { v.x = -Math.abs(v.x) * circleSpeed; } else { v.x = Math.abs(v.x) * circleSpeed; } if (secondaryObj.y - canvasHeight / 2 > 0) { v.y = -Math.abs(v.y) * circleSpeed; } else { v.y = Math.abs(v.y) * circleSpeed; } secondaryObj.v.x = 0.75 * secondaryObj.v.x + v.x; secondaryObj.v.y = 0.75 * secondaryObj.v.y + v.y; if (Math.abs(secondaryObj.v.x) > 2 || Math.abs(secondaryObj.v.y) > 2) { needsRender = true; } secondaryObj.x += secondaryObj.v.x; secondaryObj.y += secondaryObj.v.y; } } // check all circles with higher priority and move out of their way for (var i = 0; i < j; i++) { // item i has positional priority var primaryObj = data[i]; // if secondaryObj is too close to primaryObj var sqdist = (primaryObj.x - secondaryObj.x) * (primaryObj.x - secondaryObj.x) + (primaryObj.y - secondaryObj.y) * (primaryObj.y - secondaryObj.y); if (sqdist < (primaryObj.radius + secondaryObj.radius) * (primaryObj.radius + secondaryObj.radius)) { // calculate the perpendicluar angle from the previous circle var angle = Math.atan((secondaryObj.y - primaryObj.y) / (secondaryObj.x - primaryObj.x)); var newRelativePos = { x: Math.cos(angle) * (primaryObj.radius + secondaryObj.radius), y: Math.sin(angle) * (primaryObj.radius + secondaryObj.radius) }; if (secondaryObj.x - primaryObj.x > 0) { newRelativePos.x = Math.abs(newRelativePos.x); } else { newRelativePos.x = -Math.abs(newRelativePos.x); } if (secondaryObj.y - primaryObj.y > 0) { newRelativePos.y = Math.abs(newRelativePos.y); } else { newRelativePos.y = -Math.abs(newRelativePos.y); } // move the circle secondaryObj.x = primaryObj.x + newRelativePos.x; secondaryObj.y = primaryObj.y + newRelativePos.y; } } secondaryObj.v = { x: secondaryObj.x - oldPos.x, y: secondaryObj.y - oldPos.y }; } } 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; this.getBase64Image = function (scale) { // call render with a special flag return this.render(true, scale); }; /*** * Initialize the canvas on the DOM and add event listening ***/ function _init() { var _this = this; htmlCanvas = document.createElement('CANVAS'); htmlContainer.appendChild(htmlCanvas); // set the data data = chartData.slice(0); for (var i = 0; i < data.length; i++) { // set position and velocity vector for the circles data[i].x = 0; data[i].y = 0; data[i].v = { x: 0, y: 0 }; // set prefix/suffix strings for the values of the data var parts = _Numbers2.default.separateNumberUnits(data[i].value); if (data[i].prefix === undefined) { data[i].prefix = parts.prefix; } if (data[i].suffix === undefined) { data[i].suffix = parts.suffix; } data[i].value = parts.value; for (var s = 0; s < data[i].segments.length; s++) { if (!data[i].segments[s].fillColor) { data[i].segments[s].fillColor = new _Color2.default.Color(options.fillColor); } if (!data[i].segments[s].borderColor) { data[i].segments[s].borderColor = new _Color2.default.Color(options.borderColor); } } data[i].nameAsTokens = data[i].name.split(' '); // if there's more than one word in this name, try to find the smallest way of rendering the label if (data[i].nameAsTokens.length > 1) { var labelPermutations = []; getLabelLinesArray([], data[i].nameAsTokens); var permutationWithSmallestFootprint = _lodash2.default.min(labelPermutations, function (permutation) { var size = measureLabels(permutation); return size.width > size.height ? size.width : size.height; }); data[i].nameAsTokens = permutationWithSmallestFootprint; labelPermutations = null; // help the garbage collector pick this up } } function getLabelLinesArray(tokenLines, remainingTokens) { var allRemainingAsString = remainingTokens.join(' '); var thisPermutation = tokenLines.slice(0); thisPermutation.push(allRemainingAsString); labelPermutations.push(thisPermutation); if (remainingTokens.length >= 2) { for (var j = remainingTokens.length - 1; j > 0; j--) { var thisLabelLine = remainingTokens.slice(0, j).join(' '); var tokenLinesCopy = tokenLines.slice(0); tokenLinesCopy.push(thisLabelLine); getLabelLinesArray(tokenLinesCopy, remainingTokens.slice(j)); } } } function measureLabels(labelsArray) { var ctx = htmlCanvas.getContext('2d'); var fontSize = 24; // arbitrary font size used so we can just get relative sizes ctx.font = fontSize + 'px ' + options.labelFontFamily; var maxWidthLabel = _lodash2.default.max(labelsArray, function (label) { return ctx.measureText(label).width; }); var size = { width: ctx.measureText(maxWidthLabel).width, height: labelsArray.length * fontSize }; return size; } // get the highest value in the set (it's the first one since we just sorted it) maxValue = _lodash2.default.max(data, function (datum) { return datum.value; }).value; fillParent(); var guid = _Charts2.default.createGuid(); _ChartDictionary2.default.push({ id: guid, canvasElement: htmlCanvas, chart: this }); htmlCanvas.setAttribute('data-chart-id', guid); // Set up some event handling, please // we want the canvas to always fill its parent resizeListener = window.addEventListener('resize', function () { clearTimeout(resizeTimer); resizeTimer = setTimeout(function () { 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'; break; } } } } if (options.allowDrag) { htmlCanvas.addEventListener('mousedown', function (event) { var pt = getMouseCoords(htmlCanvas, event); handleMouseDown(pt, event); }, false); htmlCanvas.addEventListener('mousemove', function (event) { var pt = getMouseCoords(htmlCanvas, event); handleMouseMove(pt, event); }, false); htmlCanvas.addEventListener('mouseup', function (event) { var pt = getMouseCoords(htmlCanvas, event); handleMouseUp(pt, event); }, false); htmlCanvas.addEventListener('mouseout', function (event) { var pt = getMouseCoords(htmlCanvas, event); handleMouseOut(pt, event); }, false); } 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.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); }; 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 }; } exports.default = PieCircleChart;