transcend-charts
Version:
Transcend is a charting and graph library for NUVI
1,027 lines (898 loc) • 37.6 kB
JavaScript
'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;