summaryrefslogtreecommitdiffstats
path: root/web/gui/lib/d3pie-0.2.1-netdata-3.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-06 01:22:31 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-06 01:22:31 +0000
commit8d4f58e49b9dc7d3545651023a36729de773ad86 (patch)
tree7bc7be4a8e9e298daa1349348400aa2a653866f2 /web/gui/lib/d3pie-0.2.1-netdata-3.js
parentInitial commit. (diff)
downloadnetdata-upstream/1.12.0.tar.xz
netdata-upstream/1.12.0.zip
Adding upstream version 1.12.0.upstream/1.12.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'web/gui/lib/d3pie-0.2.1-netdata-3.js')
-rw-r--r--web/gui/lib/d3pie-0.2.1-netdata-3.js2124
1 files changed, 2124 insertions, 0 deletions
diff --git a/web/gui/lib/d3pie-0.2.1-netdata-3.js b/web/gui/lib/d3pie-0.2.1-netdata-3.js
new file mode 100644
index 0000000..3b00b49
--- /dev/null
+++ b/web/gui/lib/d3pie-0.2.1-netdata-3.js
@@ -0,0 +1,2124 @@
+/*!
+ * d3pie
+ * @author Ben Keen
+ * @version 0.1.9
+ * @date June 17th, 2015
+ * @repo http://github.com/benkeen/d3pie
+ * SPDX-License-Identifier: MIT
+ */
+
+// UMD pattern from https://github.com/umdjs/umd/blob/master/returnExports.js
+(function(root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module
+ define([], factory);
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but only CommonJS-like environments that support module.exports,
+ // like Node
+ module.exports = factory();
+ } else {
+ // browser globals (root is window)
+ root.d3pie = factory(root);
+ }
+}(this, function() {
+
+ var _scriptName = "d3pie";
+ var _version = "0.2.1";
+
+ // used to uniquely generate IDs and classes, ensuring no conflict between multiple pies on the same page
+ var _uniqueIDCounter = 0;
+
+
+ // this section includes all helper libs on the d3pie object. They're populated via grunt-template. Note: to keep
+ // the syntax highlighting from getting all messed up, I commented out each line. That REQUIRES each of the files
+ // to have an empty first line. Crumby, yes, but acceptable.
+ //// --------- _default-settings.js -----------/**
+/**
+ * Contains the out-the-box settings for the script. Any of these settings that aren't explicitly overridden for the
+ * d3pie instance will inherit from these. This is also included on the main website for use in the generation script.
+ */
+var defaultSettings = {
+ header: {
+ title: {
+ text: "",
+ color: "#333333",
+ fontSize: 18,
+ fontWeight: "bold",
+ font: "arial"
+ },
+ subtitle: {
+ text: "",
+ color: "#666666",
+ fontSize: 14,
+ fontWeight: "bold",
+ font: "arial"
+ },
+ location: "top-center",
+ titleSubtitlePadding: 8
+ },
+ footer: {
+ text: "",
+ color: "#666666",
+ fontSize: 14,
+ fontWeight: "bold",
+ font: "arial",
+ location: "left"
+ },
+ size: {
+ canvasHeight: 500,
+ canvasWidth: 500,
+ pieInnerRadius: "0%",
+ pieOuterRadius: null
+ },
+ data: {
+ sortOrder: "none",
+ ignoreSmallSegments: {
+ enabled: false,
+ valueType: "percentage",
+ value: null
+ },
+ smallSegmentGrouping: {
+ enabled: false,
+ value: 1,
+ valueType: "percentage",
+ label: "Other",
+ color: "#cccccc"
+ },
+ content: []
+ },
+ labels: {
+ outer: {
+ format: "label",
+ hideWhenLessThanPercentage: null,
+ pieDistance: 30
+ },
+ inner: {
+ format: "percentage",
+ hideWhenLessThanPercentage: null
+ },
+ mainLabel: {
+ color: "#333333",
+ font: "arial",
+ fontWeight: "normal",
+ fontSize: 10
+ },
+ percentage: {
+ color: "#dddddd",
+ font: "arial",
+ fontWeight: "bold",
+ fontSize: 10,
+ decimalPlaces: 0
+ },
+ value: {
+ color: "#cccc44",
+ fontWeight: "bold",
+ font: "arial",
+ fontSize: 10
+ },
+ lines: {
+ enabled: true,
+ style: "curved",
+ color: "segment"
+ },
+ truncation: {
+ enabled: false,
+ truncateLength: 30
+ },
+ formatter: null
+ },
+ effects: {
+ load: {
+ effect: "none", // "default", commented in the code
+ speed: 1000
+ },
+ pullOutSegmentOnClick: {
+ effect: "none", // "bounce", commented in the code
+ speed: 300,
+ size: 10
+ },
+ highlightSegmentOnMouseover: false,
+ highlightLuminosity: -0.2
+ },
+ tooltips: {
+ enabled: false,
+ type: "placeholder", // caption|placeholder
+ string: "",
+ placeholderParser: null,
+ styles: {
+ fadeInSpeed: 250,
+ backgroundColor: "#000000",
+ backgroundOpacity: 0.5,
+ color: "#efefef",
+ borderRadius: 2,
+ font: "arial",
+ fontWeight: "bold",
+ fontSize: 10,
+ padding: 4
+ }
+ },
+ misc: {
+ colors: {
+ background: null,
+ segments: [
+ "#2484c1", "#65a620", "#7b6888", "#a05d56", "#961a1a", "#d8d23a", "#e98125", "#d0743c", "#635222", "#6ada6a",
+ "#0c6197", "#7d9058", "#207f33", "#44b9b0", "#bca44a", "#e4a14b", "#a3acb2", "#8cc3e9", "#69a6f9", "#5b388f",
+ "#546e91", "#8bde95", "#d2ab58", "#273c71", "#98bf6e", "#4daa4b", "#98abc5", "#cc1010", "#31383b", "#006391",
+ "#c2643f", "#b0a474", "#a5a39c", "#a9c2bc", "#22af8c", "#7fcecf", "#987ac6", "#3d3b87", "#b77b1c", "#c9c2b6",
+ "#807ece", "#8db27c", "#be66a2", "#9ed3c6", "#00644b", "#005064", "#77979f", "#77e079", "#9c73ab", "#1f79a7"
+ ],
+ segmentStroke: "#ffffff"
+ },
+ gradient: {
+ enabled: false,
+ percentage: 95,
+ color: "#000000"
+ },
+ canvasPadding: {
+ top: 5,
+ right: 5,
+ bottom: 5,
+ left: 5
+ },
+ pieCenterOffset: {
+ x: 0,
+ y: 0
+ },
+ cssPrefix: null
+ },
+ callbacks: {
+ onload: null,
+ onMouseoverSegment: null,
+ onMouseoutSegment: null,
+ onClickSegment: null
+ }
+};
+
+ //// --------- validate.js -----------
+var validate = {
+
+ // called whenever a new pie chart is created
+ initialCheck: function(pie) {
+ var cssPrefix = pie.cssPrefix;
+ var element = pie.element;
+ var options = pie.options;
+
+ // confirm d3 is available [check minimum version]
+ if (!window.d3 || !window.d3.hasOwnProperty("version")) {
+ console.error("d3pie error: d3 is not available");
+ return false;
+ }
+
+ // confirm element is either a DOM element or a valid string for a DOM element
+ if (!(element instanceof HTMLElement || element instanceof SVGElement)) {
+ console.error("d3pie error: the first d3pie() param must be a valid DOM element (not jQuery) or a ID string.");
+ return false;
+ }
+
+ // confirm the CSS prefix is valid. It has to start with a-Z and contain nothing but a-Z0-9_-
+ if (!(/[a-zA-Z][a-zA-Z0-9_-]*$/.test(cssPrefix))) {
+ console.error("d3pie error: invalid options.misc.cssPrefix");
+ return false;
+ }
+
+ // confirm some data has been supplied
+ if (!helpers.isArray(options.data.content)) {
+ console.error("d3pie error: invalid config structure: missing data.content property.");
+ return false;
+ }
+ if (options.data.content.length === 0) {
+ console.error("d3pie error: no data supplied.");
+ return false;
+ }
+
+ // clear out any invalid data. Each data row needs a valid positive number and a label
+ var data = [];
+ for (var i=0; i<options.data.content.length; i++) {
+ if (typeof options.data.content[i].value !== "number" || isNaN(options.data.content[i].value)) {
+ console.log("not valid: ", options.data.content[i]);
+ continue;
+ }
+ if (options.data.content[i].value <= 0) {
+ console.log("not valid - should have positive value: ", options.data.content[i]);
+ continue;
+ }
+ data.push(options.data.content[i]);
+ }
+ pie.options.data.content = data;
+
+ // labels.outer.hideWhenLessThanPercentage - 1-100
+ // labels.inner.hideWhenLessThanPercentage - 1-100
+
+ return true;
+ }
+};
+
+ //// --------- helpers.js -----------
+var helpers = {
+
+ // creates the SVG element
+ addSVGSpace: function(pie) {
+ var element = pie.element;
+ var canvasWidth = pie.options.size.canvasWidth;
+ var canvasHeight = pie.options.size.canvasHeight;
+ var backgroundColor = pie.options.misc.colors.background;
+
+ var svg = d3.select(element).append("svg:svg")
+ .attr("width", canvasWidth)
+ .attr("height", canvasHeight);
+
+ if (backgroundColor !== "transparent") {
+ svg.style("background-color", function() { return backgroundColor; });
+ }
+
+ return svg;
+ },
+
+ shuffleArray: function(array) {
+ var currentIndex = array.length, tmpVal, randomIndex;
+
+ while (0 !== currentIndex) {
+ randomIndex = Math.floor(Math.random() * currentIndex);
+ currentIndex -= 1;
+
+ // and swap it with the current element
+ tmpVal = array[currentIndex];
+ array[currentIndex] = array[randomIndex];
+ array[randomIndex] = tmpVal;
+ }
+ return array;
+ },
+
+ processObj: function(obj, is, value) {
+ if (typeof is === 'string') {
+ return helpers.processObj(obj, is.split('.'), value);
+ } else if (is.length === 1 && value !== undefined) {
+ obj[is[0]] = value;
+ return obj[is[0]];
+ } else if (is.length === 0) {
+ return obj;
+ } else {
+ return helpers.processObj(obj[is[0]], is.slice(1), value);
+ }
+ },
+
+ getDimensions: function(el) {
+ if(typeof el === 'string')
+ el = document.getElementById(el);
+
+ var w = 0, h = 0;
+ if (el) {
+ var dimensions = el.getBBox();
+ w = dimensions.width;
+ h = dimensions.height;
+ }
+ else {
+ console.log("error: getDimensions() " + id + " not found.");
+ }
+
+ return { w: w, h: h };
+ },
+
+ /**
+ * This is based on the SVG coordinate system, where top-left is 0,0 and bottom right is n-n.
+ * @param r1
+ * @param r2
+ * @returns {boolean}
+ */
+ rectIntersect: function(r1, r2) {
+ var returnVal = (
+ // r2.left > r1.right
+ (r2.x > (r1.x + r1.w)) ||
+
+ // r2.right < r1.left
+ ((r2.x + r2.w) < r1.x) ||
+
+ // r2.top < r1.bottom
+ ((r2.y + r2.h) < r1.y) ||
+
+ // r2.bottom > r1.top
+ (r2.y > (r1.y + r1.h))
+ );
+
+ return !returnVal;
+ },
+
+ /**
+ * Returns a lighter/darker shade of a hex value, based on a luminance value passed.
+ * @param hex a hex color value such as “#abc” or “#123456″ (the hash is optional)
+ * @param lum the luminosity factor: -0.1 is 10% darker, 0.2 is 20% lighter, etc.
+ * @returns {string}
+ */
+ getColorShade: function(hex, lum) {
+
+ // validate hex string
+ hex = String(hex).replace(/[^0-9a-f]/gi, '');
+ if (hex.length < 6) {
+ hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
+ }
+ lum = lum || 0;
+
+ // convert to decimal and change luminosity
+ var newHex = "#";
+ for (var i=0; i<3; i++) {
+ var c = parseInt(hex.substr(i * 2, 2), 16);
+ c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16);
+ newHex += ("00" + c).substr(c.length);
+ }
+
+ return newHex;
+ },
+
+ /**
+ * Users can choose to specify segment colors in three ways (in order of precedence):
+ * 1. include a "color" attribute for each row in data.content
+ * 2. include a misc.colors.segments property which contains an array of hex codes
+ * 3. specify nothing at all and rely on this lib provide some reasonable defaults
+ *
+ * This function sees what's included and populates this.options.colors with whatever's required
+ * for this pie chart.
+ * @param data
+ */
+ initSegmentColors: function(pie) {
+ var data = pie.options.data.content;
+ var colors = pie.options.misc.colors.segments;
+
+ // TODO this needs a ton of error handling
+
+ var finalColors = [];
+ for (var i=0; i<data.length; i++) {
+ if (data[i].hasOwnProperty("color")) {
+ finalColors.push(data[i].color);
+ } else {
+ finalColors.push(colors[i]);
+ }
+ }
+
+ return finalColors;
+ },
+
+ applySmallSegmentGrouping: function(data, smallSegmentGrouping) {
+ var totalSize;
+ if (smallSegmentGrouping.valueType === "percentage") {
+ totalSize = math.getTotalPieSize(data);
+ }
+
+ // loop through each data item
+ var newData = [];
+ var groupedData = [];
+ var totalGroupedData = 0;
+ for (var i=0; i<data.length; i++) {
+ if (smallSegmentGrouping.valueType === "percentage") {
+ var dataPercent = (data[i].value / totalSize) * 100;
+ if (dataPercent <= smallSegmentGrouping.value) {
+ groupedData.push(data[i]);
+ totalGroupedData += data[i].value;
+ continue;
+ }
+ data[i].isGrouped = false;
+ newData.push(data[i]);
+ } else {
+ if (data[i].value <= smallSegmentGrouping.value) {
+ groupedData.push(data[i]);
+ totalGroupedData += data[i].value;
+ continue;
+ }
+ data[i].isGrouped = false;
+ newData.push(data[i]);
+ }
+ }
+
+ // we're done! See if there's any small segment groups to add
+ if (groupedData.length) {
+ newData.push({
+ color: smallSegmentGrouping.color,
+ label: smallSegmentGrouping.label,
+ value: totalGroupedData,
+ isGrouped: true,
+ groupedData: groupedData
+ });
+ }
+
+ return newData;
+ },
+
+ // for debugging
+ showPoint: function(svg, x, y) {
+ svg.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).style("fill", "black");
+ },
+
+ isFunction: function(functionToCheck) {
+ var getType = {};
+ return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
+ },
+
+ isArray: function(o) {
+ return Object.prototype.toString.call(o) === '[object Array]';
+ }
+};
+
+
+// taken from jQuery
+var extend = function() {
+ var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
+ i = 1,
+ length = arguments.length,
+ deep = false,
+ toString = Object.prototype.toString,
+ hasOwn = Object.prototype.hasOwnProperty,
+ class2type = {
+ "[object Boolean]": "boolean",
+ "[object Number]": "number",
+ "[object String]": "string",
+ "[object Function]": "function",
+ "[object Array]": "array",
+ "[object Date]": "date",
+ "[object RegExp]": "regexp",
+ "[object Object]": "object"
+ },
+
+ jQuery = {
+ isFunction: function (obj) {
+ return jQuery.type(obj) === "function";
+ },
+ isArray: Array.isArray ||
+ function (obj) {
+ return jQuery.type(obj) === "array";
+ },
+ isWindow: function (obj) {
+ return obj !== null && obj === obj.window;
+ },
+ isNumeric: function (obj) {
+ return !isNaN(parseFloat(obj)) && isFinite(obj);
+ },
+ type: function (obj) {
+ return obj === null ? String(obj) : class2type[toString.call(obj)] || "object";
+ },
+ isPlainObject: function (obj) {
+ if (!obj || jQuery.type(obj) !== "object" || obj.nodeType) {
+ return false;
+ }
+ try {
+ if (obj.constructor && !hasOwn.call(obj, "constructor") && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) {
+ return false;
+ }
+ } catch (e) {
+ return false;
+ }
+ var key;
+ for (key in obj) {}
+ return key === undefined || hasOwn.call(obj, key);
+ }
+ };
+ if (typeof target === "boolean") {
+ deep = target;
+ target = arguments[1] || {};
+ i = 2;
+ }
+ if (typeof target !== "object" && !jQuery.isFunction(target)) {
+ target = {};
+ }
+ if (length === i) {
+ target = this;
+ --i;
+ }
+ for (i; i < length; i++) {
+ if ((options = arguments[i]) !== null) {
+ for (name in options) {
+ src = target[name];
+ copy = options[name];
+ if (target === copy) {
+ continue;
+ }
+ if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
+ if (copyIsArray) {
+ copyIsArray = false;
+ clone = src && jQuery.isArray(src) ? src : [];
+ } else {
+ clone = src && jQuery.isPlainObject(src) ? src : {};
+ }
+ // WARNING: RECURSION
+ target[name] = extend(deep, clone, copy);
+ } else if (copy !== undefined) {
+ target[name] = copy;
+ }
+ }
+ }
+ }
+ return target;
+};
+ //// --------- math.js -----------
+var math = {
+
+ toRadians: function(degrees) {
+ return degrees * (Math.PI / 180);
+ },
+
+ toDegrees: function(radians) {
+ return radians * (180 / Math.PI);
+ },
+
+ computePieRadius: function(pie) {
+ var size = pie.options.size;
+ var canvasPadding = pie.options.misc.canvasPadding;
+
+ // outer radius is either specified (e.g. through the generator), or omitted altogether
+ // and calculated based on the canvas dimensions. Right now the estimated version isn't great - it should
+ // be possible to calculate it to precisely generate the maximum sized pie, but it's fussy as heck. Something
+ // for the next release.
+
+ // first, calculate the default _outerRadius
+ var w = size.canvasWidth - canvasPadding.left - canvasPadding.right;
+ var h = size.canvasHeight - canvasPadding.top - canvasPadding.bottom;
+
+ // now factor in the footer, title & subtitle
+ if (pie.options.header.location !== "pie-center") {
+ h -= pie.textComponents.headerHeight;
+ }
+
+ if (pie.textComponents.footer.exists) {
+ h -= pie.textComponents.footer.h;
+ }
+
+ // for really teeny pies, h may be < 0. Adjust it back
+ h = (h < 0) ? 0 : h;
+
+ var outerRadius = ((w < h) ? w : h) / 3;
+ var innerRadius, percent;
+
+ // if the user specified something, use that instead
+ if (size.pieOuterRadius !== null) {
+ if (/%/.test(size.pieOuterRadius)) {
+ percent = parseInt(size.pieOuterRadius.replace(/[\D]/, ""), 10);
+ percent = (percent > 99) ? 99 : percent;
+ percent = (percent < 0) ? 0 : percent;
+
+ var smallestDimension = (w < h) ? w : h;
+
+ // now factor in the label line size
+ if (pie.options.labels.outer.format !== "none") {
+ var pieDistanceSpace = parseInt(pie.options.labels.outer.pieDistance, 10) * 2;
+ if (smallestDimension - pieDistanceSpace > 0) {
+ smallestDimension -= pieDistanceSpace;
+ }
+ }
+
+ outerRadius = Math.floor((smallestDimension / 100) * percent) / 2;
+ } else {
+ outerRadius = parseInt(size.pieOuterRadius, 10);
+ }
+ }
+
+ // inner radius
+ if (/%/.test(size.pieInnerRadius)) {
+ percent = parseInt(size.pieInnerRadius.replace(/[\D]/, ""), 10);
+ percent = (percent > 99) ? 99 : percent;
+ percent = (percent < 0) ? 0 : percent;
+ innerRadius = Math.floor((outerRadius / 100) * percent);
+ } else {
+ innerRadius = parseInt(size.pieInnerRadius, 10);
+ }
+
+ pie.innerRadius = innerRadius;
+ pie.outerRadius = outerRadius;
+ },
+
+ getTotalPieSize: function(data) {
+ var totalSize = 0;
+ for (var i=0; i<data.length; i++) {
+ totalSize += data[i].value;
+ }
+ return totalSize;
+ },
+
+ sortPieData: function(pie) {
+ var data = pie.options.data.content;
+ var sortOrder = pie.options.data.sortOrder;
+
+ switch (sortOrder) {
+ case "none":
+ // do nothing
+ break;
+ case "random":
+ data = helpers.shuffleArray(data);
+ break;
+ case "value-asc":
+ data.sort(function(a, b) { return (a.value < b.value) ? -1 : 1; });
+ break;
+ case "value-desc":
+ data.sort(function(a, b) { return (a.value < b.value) ? 1 : -1; });
+ break;
+ case "label-asc":
+ data.sort(function(a, b) { return (a.label.toLowerCase() > b.label.toLowerCase()) ? 1 : -1; });
+ break;
+ case "label-desc":
+ data.sort(function(a, b) { return (a.label.toLowerCase() < b.label.toLowerCase()) ? 1 : -1; });
+ break;
+ }
+
+ return data;
+ },
+
+ // var pieCenter = math.getPieCenter();
+ getPieTranslateCenter: function(pieCenter) {
+ return "translate(" + pieCenter.x + "," + pieCenter.y + ")";
+ },
+
+ /**
+ * Used to determine where on the canvas the center of the pie chart should be. It takes into account the
+ * height and position of the title, subtitle and footer, and the various paddings.
+ * @private
+ */
+ calculatePieCenter: function(pie) {
+ var pieCenterOffset = pie.options.misc.pieCenterOffset;
+ var hasTopTitle = (pie.textComponents.title.exists && pie.options.header.location !== "pie-center");
+ var hasTopSubtitle = (pie.textComponents.subtitle.exists && pie.options.header.location !== "pie-center");
+
+ var headerOffset = pie.options.misc.canvasPadding.top;
+ if (hasTopTitle && hasTopSubtitle) {
+ headerOffset += pie.textComponents.title.h + pie.options.header.titleSubtitlePadding + pie.textComponents.subtitle.h;
+ } else if (hasTopTitle) {
+ headerOffset += pie.textComponents.title.h;
+ } else if (hasTopSubtitle) {
+ headerOffset += pie.textComponents.subtitle.h;
+ }
+
+ var footerOffset = 0;
+ if (pie.textComponents.footer.exists) {
+ footerOffset = pie.textComponents.footer.h + pie.options.misc.canvasPadding.bottom;
+ }
+
+ var x = ((pie.options.size.canvasWidth - pie.options.misc.canvasPadding.left - pie.options.misc.canvasPadding.right) / 2) + pie.options.misc.canvasPadding.left;
+ var y = ((pie.options.size.canvasHeight - footerOffset - headerOffset) / 2) + headerOffset;
+
+ x += pieCenterOffset.x;
+ y += pieCenterOffset.y;
+
+ pie.pieCenter = { x: x, y: y };
+ },
+
+
+ /**
+ * Rotates a point (x, y) around an axis (xm, ym) by degrees (a).
+ * @param x
+ * @param y
+ * @param xm
+ * @param ym
+ * @param a angle in degrees
+ * @returns {Array}
+ */
+ rotate: function(x, y, xm, ym, a) {
+
+ a = a * Math.PI / 180; // convert to radians
+
+ var cos = Math.cos,
+ sin = Math.sin,
+ // subtract midpoints, so that midpoint is translated to origin and add it in the end again
+ xr = (x - xm) * cos(a) - (y - ym) * sin(a) + xm,
+ yr = (x - xm) * sin(a) + (y - ym) * cos(a) + ym;
+
+ return { x: xr, y: yr };
+ },
+
+ /**
+ * Translates a point x, y by distance d, and by angle a.
+ * @param x
+ * @param y
+ * @param dist
+ * @param a angle in degrees
+ */
+ translate: function(x, y, d, a) {
+ var rads = math.toRadians(a);
+ return {
+ x: x + d * Math.sin(rads),
+ y: y - d * Math.cos(rads)
+ };
+ },
+
+ // from: http://stackoverflow.com/questions/19792552/d3-put-arc-labels-in-a-pie-chart-if-there-is-enough-space
+ pointIsInArc: function(pt, ptData, d3Arc) {
+ // Center of the arc is assumed to be 0,0
+ // (pt.x, pt.y) are assumed to be relative to the center
+ var r1 = d3Arc.innerRadius()(ptData), // Note: Using the innerRadius
+ r2 = d3Arc.outerRadius()(ptData),
+ theta1 = d3Arc.startAngle()(ptData),
+ theta2 = d3Arc.endAngle()(ptData);
+
+ var dist = pt.x * pt.x + pt.y * pt.y,
+ angle = Math.atan2(pt.x, -pt.y); // Note: different coordinate system
+
+ angle = (angle < 0) ? (angle + Math.PI * 2) : angle;
+
+ return (r1 * r1 <= dist) && (dist <= r2 * r2) &&
+ (theta1 <= angle) && (angle <= theta2);
+ }
+};
+
+ //// --------- labels.js -----------
+var labels = {
+
+ /**
+ * Adds the labels to the pie chart, but doesn't position them. There are two locations for the
+ * labels: inside (center) of the segments, or outside the segments on the edge.
+ * @param section "inner" or "outer"
+ * @param sectionDisplayType "percentage", "value", "label", "label-value1", etc.
+ * @param pie
+ */
+ add: function(pie, section, sectionDisplayType) {
+ var include = labels.getIncludes(sectionDisplayType);
+ var settings = pie.options.labels;
+
+ // group the label groups (label, percentage, value) into a single element for simpler positioning
+ var outerLabel = pie.svg.insert("g", "." + pie.cssPrefix + "labels-" + section)
+ .attr("class", pie.cssPrefix + "labels-" + section);
+
+ var labelGroup = pie.__labels[section] = outerLabel.selectAll("." + pie.cssPrefix + "labelGroup-" + section)
+ .data(pie.options.data.content)
+ .enter()
+ .append("g")
+ .attr("id", function(d, i) { return pie.cssPrefix + "labelGroup" + i + "-" + section; })
+ .attr("data-index", function(d, i) { return i; })
+ .attr("class", pie.cssPrefix + "labelGroup-" + section)
+ .style("opacity", 0);
+
+ var formatterContext = { section: section, sectionDisplayType: sectionDisplayType };
+
+ // 1. Add the main label
+ if (include.mainLabel) {
+ labelGroup.append("text")
+ .attr("id", function(d, i) { return pie.cssPrefix + "segmentMainLabel" + i + "-" + section; })
+ .attr("class", pie.cssPrefix + "segmentMainLabel-" + section)
+ .text(function(d, i) {
+ var str = d.label;
+
+ // if a custom formatter has been defined, pass it the raw label string - it can do whatever it wants with it.
+ // we only apply truncation if it's not defined
+ if (settings.formatter) {
+ formatterContext.index = i;
+ formatterContext.part = 'mainLabel';
+ formatterContext.value = d.value;
+ formatterContext.label = str;
+ str = settings.formatter(formatterContext);
+ } else if (settings.truncation.enabled && d.label.length > settings.truncation.truncateLength) {
+ str = d.label.substring(0, settings.truncation.truncateLength) + "...";
+ }
+ return str;
+ })
+ .style("font-size", settings.mainLabel.fontSize + "px")
+ .style("font-family", settings.mainLabel.font)
+ .style("font-weight", settings.mainLabel.fontWeight)
+ .style("fill", function(d, i) {
+ return (settings.mainLabel.color === "segment") ? pie.options.colors[i] : settings.mainLabel.color;
+ });
+ }
+
+ // 2. Add the percentage label
+ if (include.percentage) {
+ labelGroup.append("text")
+ .attr("id", function(d, i) { return pie.cssPrefix + "segmentPercentage" + i + "-" + section; })
+ .attr("class", pie.cssPrefix + "segmentPercentage-" + section)
+ .text(function(d, i) {
+ var percentage = d.percentage;
+ if (settings.formatter) {
+ formatterContext.index = i;
+ formatterContext.part = "percentage";
+ formatterContext.value = d.value;
+ formatterContext.label = d.percentage;
+ percentage = settings.formatter(formatterContext);
+ } else {
+ percentage += "%";
+ }
+ return percentage;
+ })
+ .style("font-size", settings.percentage.fontSize + "px")
+ .style("font-family", settings.percentage.font)
+ .style("font-weight", settings.percentage.fontWeight)
+ .style("fill", settings.percentage.color);
+ }
+
+ // 3. Add the value label
+ if (include.value) {
+ labelGroup.append("text")
+ .attr("id", function(d, i) { return pie.cssPrefix + "segmentValue" + i + "-" + section; })
+ .attr("class", pie.cssPrefix + "segmentValue-" + section)
+ .text(function(d, i) {
+ formatterContext.index = i;
+ formatterContext.part = "value";
+ formatterContext.value = d.value;
+ formatterContext.label = d.value;
+ return settings.formatter ? settings.formatter(formatterContext, d.value) : d.value;
+ })
+ .style("font-size", settings.value.fontSize + "px")
+ .style("font-family", settings.value.font)
+ .style("font-weight", settings.value.fontWeight)
+ .style("fill", settings.value.color);
+ }
+ },
+
+ /**
+ * @param section "inner" / "outer"
+ */
+ positionLabelElements: function(pie, section, sectionDisplayType) {
+ labels["dimensions-" + section] = [];
+
+ // get the latest widths, heights
+ var labelGroups = pie.__labels[section];
+ labelGroups.each(function(d, i) {
+ var mainLabel = d3.select(this).selectAll("." + pie.cssPrefix + "segmentMainLabel-" + section);
+ var percentage = d3.select(this).selectAll("." + pie.cssPrefix + "segmentPercentage-" + section);
+ var value = d3.select(this).selectAll("." + pie.cssPrefix + "segmentValue-" + section);
+
+ labels["dimensions-" + section].push({
+ mainLabel: (mainLabel.node() !== null) ? mainLabel.node().getBBox() : null,
+ percentage: (percentage.node() !== null) ? percentage.node().getBBox() : null,
+ value: (value.node() !== null) ? value.node().getBBox() : null
+ });
+ });
+
+ var singleLinePad = 5;
+ var dims = labels["dimensions-" + section];
+ switch (sectionDisplayType) {
+ case "label-value1":
+ pie.svg.selectAll("." + pie.cssPrefix + "segmentValue-" + section)
+ .attr("dx", function(d, i) { return dims[i].mainLabel.width + singleLinePad; });
+ break;
+ case "label-value2":
+ pie.svg.selectAll("." + pie.cssPrefix + "segmentValue-" + section)
+ .attr("dy", function(d, i) { return dims[i].mainLabel.height; });
+ break;
+ case "label-percentage1":
+ pie.svg.selectAll("." + pie.cssPrefix + "segmentPercentage-" + section)
+ .attr("dx", function(d, i) { return dims[i].mainLabel.width + singleLinePad; });
+ break;
+ case "label-percentage2":
+ pie.svg.selectAll("." + pie.cssPrefix + "segmentPercentage-" + section)
+ .attr("dx", function(d, i) { return (dims[i].mainLabel.width / 2) - (dims[i].percentage.width / 2); })
+ .attr("dy", function(d, i) { return dims[i].mainLabel.height; });
+ break;
+ }
+ },
+
+ computeLabelLinePositions: function(pie) {
+ pie.lineCoordGroups = [];
+ pie.__labels.outer
+ .each(function(d, i) { return labels.computeLinePosition(pie, i); });
+ },
+
+ computeLinePosition: function(pie, i) {
+ var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true });
+ var originCoords = math.rotate(pie.pieCenter.x, pie.pieCenter.y - pie.outerRadius, pie.pieCenter.x, pie.pieCenter.y, angle);
+ var heightOffset = pie.outerLabelGroupData[i].h / 5; // TODO check
+ var labelXMargin = 6; // the x-distance of the label from the end of the line [TODO configurable]
+
+ var quarter = Math.floor(angle / 90);
+ var midPoint = 4;
+ var x2, y2, x3, y3;
+
+ // this resolves an issue when the
+ if (quarter === 2 && angle === 180) {
+ quarter = 1;
+ }
+
+ switch (quarter) {
+ case 0:
+ x2 = pie.outerLabelGroupData[i].x - labelXMargin - ((pie.outerLabelGroupData[i].x - labelXMargin - originCoords.x) / 2);
+ y2 = pie.outerLabelGroupData[i].y + ((originCoords.y - pie.outerLabelGroupData[i].y) / midPoint);
+ x3 = pie.outerLabelGroupData[i].x - labelXMargin;
+ y3 = pie.outerLabelGroupData[i].y - heightOffset;
+ break;
+ case 1:
+ x2 = originCoords.x + (pie.outerLabelGroupData[i].x - originCoords.x) / midPoint;
+ y2 = originCoords.y + (pie.outerLabelGroupData[i].y - originCoords.y) / midPoint;
+ x3 = pie.outerLabelGroupData[i].x - labelXMargin;
+ y3 = pie.outerLabelGroupData[i].y - heightOffset;
+ break;
+ case 2:
+ var startOfLabelX = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin;
+ x2 = originCoords.x - (originCoords.x - startOfLabelX) / midPoint;
+ y2 = originCoords.y + (pie.outerLabelGroupData[i].y - originCoords.y) / midPoint;
+ x3 = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin;
+ y3 = pie.outerLabelGroupData[i].y - heightOffset;
+ break;
+ case 3:
+ var startOfLabel = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin;
+ x2 = startOfLabel + ((originCoords.x - startOfLabel) / midPoint);
+ y2 = pie.outerLabelGroupData[i].y + (originCoords.y - pie.outerLabelGroupData[i].y) / midPoint;
+ x3 = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin;
+ y3 = pie.outerLabelGroupData[i].y - heightOffset;
+ break;
+ }
+
+ /*
+ * x1 / y1: the x/y coords of the start of the line, at the mid point of the segments arc on the pie circumference
+ * x2 / y2: if "curved" line style is being used, this is the midpoint of the line. Other
+ * x3 / y3: the end of the line; closest point to the label
+ */
+ if (pie.options.labels.lines.style === "straight") {
+ pie.lineCoordGroups[i] = [
+ { x: originCoords.x, y: originCoords.y },
+ { x: x3, y: y3 }
+ ];
+ } else {
+ pie.lineCoordGroups[i] = [
+ { x: originCoords.x, y: originCoords.y },
+ { x: x2, y: y2 },
+ { x: x3, y: y3 }
+ ];
+ }
+ },
+
+ addLabelLines: function(pie) {
+ var lineGroups = pie.svg.insert("g", "." + pie.cssPrefix + "pieChart") // meaning, BEFORE .pieChart
+ .attr("class", pie.cssPrefix + "lineGroups")
+ .style("opacity", 1);
+
+ var lineGroup = lineGroups.selectAll("." + pie.cssPrefix + "lineGroup")
+ .data(pie.lineCoordGroups)
+ .enter()
+ .append("g")
+ .attr("class", pie.cssPrefix + "lineGroup");
+
+ var lineFunction = d3.line()
+ .curve(d3.curveBasis)
+ .x(function(d) { return d.x; })
+ .y(function(d) { return d.y; });
+
+ lineGroup.append("path")
+ .attr("d", lineFunction)
+ .attr("stroke", function(d, i) {
+ return (pie.options.labels.lines.color === "segment") ? pie.options.colors[i] : pie.options.labels.lines.color;
+ })
+ .attr("stroke-width", 1)
+ .attr("fill", "none")
+ .style("opacity", function(d, i) {
+ var percentage = pie.options.labels.outer.hideWhenLessThanPercentage;
+ var isHidden = (percentage !== null && d.percentage < percentage) || pie.options.data.content[i].label === "";
+ return isHidden ? 0 : 1;
+ });
+ },
+
+ positionLabelGroups: function(pie, section) {
+ if (pie.options.labels[section].format === "none")
+ return;
+
+ pie.__labels[section]
+ .style("opacity", function(d, i) {
+ var percentage = pie.options.labels[section].hideWhenLessThanPercentage;
+ return (percentage !== null && d.percentage < percentage) ? 0 : 1;
+ })
+ .attr("transform", function(d, i) {
+ var x, y;
+ if (section === "outer") {
+ x = pie.outerLabelGroupData[i].x;
+ y = pie.outerLabelGroupData[i].y;
+ } else {
+ var pieCenterCopy = extend(true, {}, pie.pieCenter);
+
+ // now recompute the "center" based on the current _innerRadius
+ if (pie.innerRadius > 0) {
+ var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true });
+ var newCoords = math.translate(pie.pieCenter.x, pie.pieCenter.y, pie.innerRadius, angle);
+ pieCenterCopy.x = newCoords.x;
+ pieCenterCopy.y = newCoords.y;
+ }
+
+ var dims = helpers.getDimensions(pie.cssPrefix + "labelGroup" + i + "-inner");
+ var xOffset = dims.w / 2;
+ var yOffset = dims.h / 4; // confusing! Why 4? should be 2, but it doesn't look right
+
+ x = pieCenterCopy.x + (pie.lineCoordGroups[i][0].x - pieCenterCopy.x) / 1.8;
+ y = pieCenterCopy.y + (pie.lineCoordGroups[i][0].y - pieCenterCopy.y) / 1.8;
+
+ x = x - xOffset;
+ y = y + yOffset;
+ }
+
+ return "translate(" + x + "," + y + ")";
+ });
+ },
+
+
+ getIncludes: function(val) {
+ var addMainLabel = false;
+ var addValue = false;
+ var addPercentage = false;
+
+ switch (val) {
+ case "label":
+ addMainLabel = true;
+ break;
+ case "value":
+ addValue = true;
+ break;
+ case "percentage":
+ addPercentage = true;
+ break;
+ case "label-value1":
+ case "label-value2":
+ addMainLabel = true;
+ addValue = true;
+ break;
+ case "label-percentage1":
+ case "label-percentage2":
+ addMainLabel = true;
+ addPercentage = true;
+ break;
+ }
+ return {
+ mainLabel: addMainLabel,
+ value: addValue,
+ percentage: addPercentage
+ };
+ },
+
+
+ /**
+ * This does the heavy-lifting to compute the actual coordinates for the outer label groups. It does two things:
+ * 1. Make a first pass and position them in the ideal positions, based on the pie sizes
+ * 2. Do some basic collision avoidance.
+ */
+ computeOuterLabelCoords: function(pie) {
+
+ // 1. figure out the ideal positions for the outer labels
+ pie.__labels.outer
+ .each(function(d, i) {
+ return labels.getIdealOuterLabelPositions(pie, i);
+ });
+
+ // 2. now adjust those positions to try to accommodate conflicts
+ labels.resolveOuterLabelCollisions(pie);
+ },
+
+ /**
+ * This attempts to resolve label positioning collisions.
+ */
+ resolveOuterLabelCollisions: function(pie) {
+ if (pie.options.labels.outer.format === "none") {
+ return;
+ }
+
+ var size = pie.options.data.content.length;
+ labels.checkConflict(pie, 0, "clockwise", size);
+ labels.checkConflict(pie, size-1, "anticlockwise", size);
+ },
+
+ checkConflict: function(pie, currIndex, direction, size) {
+ var i, curr;
+
+ if (size <= 1) {
+ return;
+ }
+
+ var currIndexHemisphere = pie.outerLabelGroupData[currIndex].hs;
+ if (direction === "clockwise" && currIndexHemisphere !== "right") {
+ return;
+ }
+ if (direction === "anticlockwise" && currIndexHemisphere !== "left") {
+ return;
+ }
+ var nextIndex = (direction === "clockwise") ? currIndex+1 : currIndex-1;
+
+ // this is the current label group being looked at. We KNOW it's positioned properly (the first item
+ // is always correct)
+ var currLabelGroup = pie.outerLabelGroupData[currIndex];
+
+ // this one we don't know about. That's the one we're going to look at and move if necessary
+ var examinedLabelGroup = pie.outerLabelGroupData[nextIndex];
+
+ var info = {
+ labelHeights: pie.outerLabelGroupData[0].h,
+ center: pie.pieCenter,
+ lineLength: (pie.outerRadius + pie.options.labels.outer.pieDistance),
+ heightChange: pie.outerLabelGroupData[0].h + 1 // 1 = padding
+ };
+
+ // loop through *ALL* label groups examined so far to check for conflicts. This is because when they're
+ // very tightly fitted, a later label group may still appear high up on the page
+ if (direction === "clockwise") {
+ i = 0;
+ for (; i<=currIndex; i++) {
+ curr = pie.outerLabelGroupData[i];
+
+ // if there's a conflict with this label group, shift the label to be AFTER the last known
+ // one that's been properly placed
+ if (!labels.isLabelHidden(pie, i) && helpers.rectIntersect(curr, examinedLabelGroup)) {
+ labels.adjustLabelPos(pie, nextIndex, currLabelGroup, info);
+ break;
+ }
+ }
+ } else {
+ i = size - 1;
+ for (; i >= currIndex; i--) {
+ curr = pie.outerLabelGroupData[i];
+
+ // if there's a conflict with this label group, shift the label to be AFTER the last known
+ // one that's been properly placed
+ if (!labels.isLabelHidden(pie, i) && helpers.rectIntersect(curr, examinedLabelGroup)) {
+ labels.adjustLabelPos(pie, nextIndex, currLabelGroup, info);
+ break;
+ }
+ }
+ }
+ labels.checkConflict(pie, nextIndex, direction, size);
+ },
+
+ isLabelHidden: function(pie, index) {
+ var percentage = pie.options.labels.outer.hideWhenLessThanPercentage;
+ return (percentage !== null && d.percentage < percentage) || pie.options.data.content[index].label === "";
+ },
+
+ // does a little math to shift a label into a new position based on the last properly placed one
+ adjustLabelPos: function(pie, nextIndex, lastCorrectlyPositionedLabel, info) {
+ var xDiff, yDiff, newXPos, newYPos;
+ newYPos = lastCorrectlyPositionedLabel.y + info.heightChange;
+ yDiff = info.center.y - newYPos;
+
+ if (Math.abs(info.lineLength) > Math.abs(yDiff)) {
+ xDiff = Math.sqrt((info.lineLength * info.lineLength) - (yDiff * yDiff));
+ } else {
+ xDiff = Math.sqrt((yDiff * yDiff) - (info.lineLength * info.lineLength));
+ }
+
+ if (lastCorrectlyPositionedLabel.hs === "right") {
+ newXPos = info.center.x + xDiff;
+ } else {
+ newXPos = info.center.x - xDiff - pie.outerLabelGroupData[nextIndex].w;
+ }
+
+ pie.outerLabelGroupData[nextIndex].x = newXPos;
+ pie.outerLabelGroupData[nextIndex].y = newYPos;
+ },
+
+ /**
+ * @param i 0-N where N is the dataset size - 1.
+ */
+ getIdealOuterLabelPositions: function(pie, i) {
+ var labelGroupNode = pie.svg.select("#" + pie.cssPrefix + "labelGroup" + i + "-outer").node();
+ if (!labelGroupNode) return;
+
+ var labelGroupDims = labelGroupNode.getBBox();
+ var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true });
+
+ var originalX = pie.pieCenter.x;
+ var originalY = pie.pieCenter.y - (pie.outerRadius + pie.options.labels.outer.pieDistance);
+ var newCoords = math.rotate(originalX, originalY, pie.pieCenter.x, pie.pieCenter.y, angle);
+
+ // if the label is on the left half of the pie, adjust the values
+ var hemisphere = "right"; // hemisphere
+ if (angle > 180) {
+ newCoords.x -= (labelGroupDims.width + 8);
+ hemisphere = "left";
+ } else {
+ newCoords.x += 8;
+ }
+
+ pie.outerLabelGroupData[i] = {
+ x: newCoords.x,
+ y: newCoords.y,
+ w: labelGroupDims.width,
+ h: labelGroupDims.height,
+ hs: hemisphere
+ };
+ }
+};
+
+ //// --------- segments.js -----------
+var segments = {
+
+ effectMap: {
+ "none": d3.easeLinear,
+ "bounce": d3.easeBounce,
+ "linear": d3.easeLinear,
+ "sin": d3.easeSin,
+ "elastic": d3.easeElastic,
+ "back": d3.easeBack,
+ "quad": d3.easeQuad,
+ "circle": d3.easeCircle,
+ "exp": d3.easeExp
+ },
+
+ /**
+ * Creates the pie chart segments and displays them according to the desired load effect.
+ * @private
+ */
+ create: function(pie) {
+ var pieCenter = pie.pieCenter;
+ var colors = pie.options.colors;
+ var loadEffects = pie.options.effects.load;
+ var segmentStroke = pie.options.misc.colors.segmentStroke;
+
+ // we insert the pie chart BEFORE the title, to ensure the title overlaps the pie
+ var pieChartElement = pie.svg.insert("g", "#" + pie.cssPrefix + "title")
+ .attr("transform", function() { return math.getPieTranslateCenter(pieCenter); })
+ .attr("class", pie.cssPrefix + "pieChart");
+
+ var arc = d3.arc()
+ .innerRadius(pie.innerRadius)
+ .outerRadius(pie.outerRadius)
+ .startAngle(0)
+ .endAngle(function(d) {
+ return (d.value / pie.totalSize) * 2 * Math.PI;
+ });
+
+ var g = pieChartElement.selectAll("." + pie.cssPrefix + "arc")
+ .data(pie.options.data.content)
+ .enter()
+ .append("g")
+ .attr("class", pie.cssPrefix + "arc");
+
+ // if we're not fading in the pie, just set the load speed to 0
+ //var loadSpeed = loadEffects.speed;
+ //if (loadEffects.effect === "none") {
+ // loadSpeed = 0;
+ //}
+
+ g.append("path")
+ .attr("id", function(d, i) { return pie.cssPrefix + "segment" + i; })
+ .attr("fill", function(d, i) {
+ var color = colors[i];
+ if (pie.options.misc.gradient.enabled) {
+ color = "url(#" + pie.cssPrefix + "grad" + i + ")";
+ }
+ return color;
+ })
+ .style("stroke", segmentStroke)
+ .style("stroke-width", 1)
+ //.transition()
+ //.ease(d3.easeCubicInOut)
+ //.duration(loadSpeed)
+ .attr("data-index", function(d, i) { return i; })
+ .attr("d", arc);
+/*
+ .attrTween("d", function(b) {
+ var i = d3.interpolate({ value: 0 }, b);
+ return function(t) {
+ var ret = pie.arc(i(t));
+ console.log(ret);
+ return ret;
+ };
+ });
+*/
+ pie.svg.selectAll("g." + pie.cssPrefix + "arc")
+ .attr("transform",
+ function(d, i) {
+ var angle = 0;
+ if (i > 0) {
+ angle = segments.getSegmentAngle(i-1, pie.options.data.content, pie.totalSize);
+ }
+ return "rotate(" + angle + ")";
+ }
+ );
+ pie.arc = arc;
+ },
+
+ addGradients: function(pie) {
+ var grads = pie.svg.append("defs")
+ .selectAll("radialGradient")
+ .data(pie.options.data.content)
+ .enter().append("radialGradient")
+ .attr("gradientUnits", "userSpaceOnUse")
+ .attr("cx", 0)
+ .attr("cy", 0)
+ .attr("r", "120%")
+ .attr("id", function(d, i) { return pie.cssPrefix + "grad" + i; });
+
+ grads.append("stop").attr("offset", "0%").style("stop-color", function(d, i) { return pie.options.colors[i]; });
+ grads.append("stop").attr("offset", pie.options.misc.gradient.percentage + "%").style("stop-color", pie.options.misc.gradient.color);
+ },
+
+ addSegmentEventHandlers: function(pie) {
+ var arc = pie.svg.selectAll("." + pie.cssPrefix + "arc");
+ arc = arc.merge(pie.__labels.inner.merge(pie.__labels.outer));
+
+ arc.on("click", function() {
+ var currentEl = d3.select(this);
+ var segment;
+
+ // mouseover works on both the segments AND the segment labels, hence the following
+ if (currentEl.attr("class") === pie.cssPrefix + "arc") {
+ segment = currentEl.select("path");
+ } else {
+ var index = currentEl.attr("data-index");
+ segment = d3.select("#" + pie.cssPrefix + "segment" + index);
+ }
+
+ var isExpanded = segment.attr("class") === pie.cssPrefix + "expanded";
+ segments.onSegmentEvent(pie, pie.options.callbacks.onClickSegment, segment, isExpanded);
+ if (pie.options.effects.pullOutSegmentOnClick.effect !== "none") {
+ if (isExpanded) {
+ segments.closeSegment(pie, segment.node());
+ } else {
+ segments.openSegment(pie, segment.node());
+ }
+ }
+ });
+
+ arc.on("mouseover", function() {
+ var currentEl = d3.select(this);
+ var segment, index;
+
+ if (currentEl.attr("class") === pie.cssPrefix + "arc") {
+ segment = currentEl.select("path");
+ } else {
+ index = currentEl.attr("data-index");
+ segment = d3.select("#" + pie.cssPrefix + "segment" + index);
+ }
+
+ if (pie.options.effects.highlightSegmentOnMouseover) {
+ index = segment.attr("data-index");
+ var segColor = pie.options.colors[index];
+ segment.style("fill", helpers.getColorShade(segColor, pie.options.effects.highlightLuminosity));
+ }
+
+ if (pie.options.tooltips.enabled) {
+ index = segment.attr("data-index");
+ tt.showTooltip(pie, index);
+ }
+
+ var isExpanded = segment.attr("class") === pie.cssPrefix + "expanded";
+ segments.onSegmentEvent(pie, pie.options.callbacks.onMouseoverSegment, segment, isExpanded);
+ });
+
+ arc.on("mousemove", function() {
+ tt.moveTooltip(pie);
+ });
+
+ arc.on("mouseout", function() {
+ var currentEl = d3.select(this);
+ var segment, index;
+
+ if (currentEl.attr("class") === pie.cssPrefix + "arc") {
+ segment = currentEl.select("path");
+ } else {
+ index = currentEl.attr("data-index");
+ segment = d3.select("#" + pie.cssPrefix + "segment" + index);
+ }
+
+ if (pie.options.effects.highlightSegmentOnMouseover) {
+ index = segment.attr("data-index");
+ var color = pie.options.colors[index];
+ if (pie.options.misc.gradient.enabled) {
+ color = "url(#" + pie.cssPrefix + "grad" + index + ")";
+ }
+ segment.style("fill", color);
+ }
+
+ if (pie.options.tooltips.enabled) {
+ index = segment.attr("data-index");
+ tt.hideTooltip(pie, index);
+ }
+
+ var isExpanded = segment.attr("class") === pie.cssPrefix + "expanded";
+ segments.onSegmentEvent(pie, pie.options.callbacks.onMouseoutSegment, segment, isExpanded);
+ });
+ },
+
+ // helper function used to call the click, mouseover, mouseout segment callback functions
+ onSegmentEvent: function(pie, func, segment, isExpanded) {
+ if (!helpers.isFunction(func)) {
+ return;
+ }
+ var index = parseInt(segment.attr("data-index"), 10);
+ func({
+ segment: segment.node(),
+ index: index,
+ expanded: isExpanded,
+ data: pie.options.data.content[index]
+ });
+ },
+
+ openSegment: function(pie, segment) {
+ if (pie.isOpeningSegment) {
+ return;
+ }
+ pie.isOpeningSegment = true;
+
+ segments.maybeCloseOpenSegment(pie);
+
+ d3.select(segment)
+ .transition()
+ .ease(segments.effectMap[pie.options.effects.pullOutSegmentOnClick.effect])
+ .duration(pie.options.effects.pullOutSegmentOnClick.speed)
+ .attr("transform", function(d, i) {
+ var c = pie.arc.centroid(d),
+ x = c[0],
+ y = c[1],
+ h = Math.sqrt(x*x + y*y),
+ pullOutSize = parseInt(pie.options.effects.pullOutSegmentOnClick.size, 10);
+
+ return "translate(" + ((x/h) * pullOutSize) + ',' + ((y/h) * pullOutSize) + ")";
+ })
+ .on("end", function(d, i) {
+ pie.currentlyOpenSegment = segment;
+ pie.isOpeningSegment = false;
+ d3.select(segment).attr("class", pie.cssPrefix + "expanded");
+ });
+ },
+
+ maybeCloseOpenSegment: function(pie) {
+ if (typeof pie !== 'undefined' && pie.svg.selectAll("." + pie.cssPrefix + "expanded").size() > 0) {
+ segments.closeSegment(pie, pie.svg.select("." + pie.cssPrefix + "expanded").node());
+ }
+ },
+
+ closeSegment: function(pie, segment) {
+ d3.select(segment)
+ .transition()
+ .duration(400)
+ .attr("transform", "translate(0,0)")
+ .on("end", function(d, i) {
+ d3.select(segment).attr("class", "");
+ pie.currentlyOpenSegment = null;
+ });
+ },
+
+ getCentroid: function(el) {
+ var bbox = el.getBBox();
+ return {
+ x: bbox.x + bbox.width / 2,
+ y: bbox.y + bbox.height / 2
+ };
+ },
+
+ /**
+ * General helper function to return a segment's angle, in various different ways.
+ * @param index
+ * @param opts optional object for fine-tuning exactly what you want.
+ */
+ getSegmentAngle: function(index, data, totalSize, opts) {
+ var options = extend({
+ // if true, this returns the full angle from the origin. Otherwise it returns the single segment angle
+ compounded: true,
+
+ // optionally returns the midpoint of the angle instead of the full angle
+ midpoint: false
+ }, opts);
+
+ var currValue = data[index].value;
+ var fullValue;
+ if (options.compounded) {
+ fullValue = 0;
+
+ // get all values up to and including the specified index
+ for (var i=0; i<=index; i++) {
+ fullValue += data[i].value;
+ }
+ }
+
+ if (typeof fullValue === 'undefined') {
+ fullValue = currValue;
+ }
+
+ // now convert the full value to an angle
+ var angle = (fullValue / totalSize) * 360;
+
+ // lastly, if we want the midpoint, factor that sucker in
+ if (options.midpoint) {
+ var currAngle = (currValue / totalSize) * 360;
+ angle -= (currAngle / 2);
+ }
+
+ return angle;
+ }
+
+};
+
+ //// --------- text.js -----------
+var text = {
+ offscreenCoord: -10000,
+
+ addTitle: function(pie) {
+ pie.__title = pie.svg.selectAll("." + pie.cssPrefix + "title")
+ .data([pie.options.header.title])
+ .enter()
+ .append("text")
+ .text(function(d) { return d.text; })
+ .attr("id", pie.cssPrefix + "title")
+ .attr("class", pie.cssPrefix + "title")
+ .attr("x", text.offscreenCoord)
+ .attr("y", text.offscreenCoord)
+ .attr("text-anchor", function() {
+ var location;
+ if (pie.options.header.location === "top-center" || pie.options.header.location === "pie-center") {
+ location = "middle";
+ } else {
+ location = "left";
+ }
+ return location;
+ })
+ .attr("fill", function(d) { return d.color; })
+ .style("font-size", function(d) { return d.fontSize + "px"; })
+ .style("font-weight", function(d) { return d.fontWeight; })
+ .style("font-family", function(d) { return d.font; });
+ },
+
+ positionTitle: function(pie) {
+ var textComponents = pie.textComponents;
+ var headerLocation = pie.options.header.location;
+ var canvasPadding = pie.options.misc.canvasPadding;
+ var canvasWidth = pie.options.size.canvasWidth;
+ var titleSubtitlePadding = pie.options.header.titleSubtitlePadding;
+
+ var x;
+ if (headerLocation === "top-left") {
+ x = canvasPadding.left;
+ } else {
+ x = ((canvasWidth - canvasPadding.right) / 2) + canvasPadding.left;
+ }
+
+ // add whatever offset has been added by user
+ x += pie.options.misc.pieCenterOffset.x;
+
+ var y = canvasPadding.top + textComponents.title.h;
+
+ if (headerLocation === "pie-center") {
+ y = pie.pieCenter.y;
+
+ // still not fully correct
+ if (textComponents.subtitle.exists) {
+ var totalTitleHeight = textComponents.title.h + titleSubtitlePadding + textComponents.subtitle.h;
+ y = y - (totalTitleHeight / 2) + textComponents.title.h;
+ } else {
+ y += (textComponents.title.h / 4);
+ }
+ }
+
+ pie.__title
+ .attr("x", x)
+ .attr("y", y);
+ },
+
+ addSubtitle: function(pie) {
+ var headerLocation = pie.options.header.location;
+
+ pie.__subtitle = pie.svg.selectAll("." + pie.cssPrefix + "subtitle")
+ .data([pie.options.header.subtitle])
+ .enter()
+ .append("text")
+ .text(function(d) { return d.text; })
+ .attr("x", text.offscreenCoord)
+ .attr("y", text.offscreenCoord)
+ .attr("id", pie.cssPrefix + "subtitle")
+ .attr("class", pie.cssPrefix + "subtitle")
+ .attr("text-anchor", function() {
+ var location;
+ if (headerLocation === "top-center" || headerLocation === "pie-center") {
+ location = "middle";
+ } else {
+ location = "left";
+ }
+ return location;
+ })
+ .attr("fill", function(d) { return d.color; })
+ .style("font-size", function(d) { return d.fontSize + "px"; })
+ .style("font-weight", function(d) { return d.fontWeight; })
+ .style("font-family", function(d) { return d.font; });
+ },
+
+ positionSubtitle: function(pie) {
+ var canvasPadding = pie.options.misc.canvasPadding;
+ var canvasWidth = pie.options.size.canvasWidth;
+
+ var x;
+ if (pie.options.header.location === "top-left") {
+ x = canvasPadding.left;
+ } else {
+ x = ((canvasWidth - canvasPadding.right) / 2) + canvasPadding.left;
+ }
+
+ // add whatever offset has been added by user
+ x += pie.options.misc.pieCenterOffset.x;
+
+ var y = text.getHeaderHeight(pie);
+
+ pie.__subtitle
+ .attr("x", x)
+ .attr("y", y);
+ },
+
+ addFooter: function(pie) {
+ pie.__footer = pie.svg.selectAll("." + pie.cssPrefix + "footer")
+ .data([pie.options.footer])
+ .enter()
+ .append("text")
+ .text(function(d) { return d.text; })
+ .attr("x", text.offscreenCoord)
+ .attr("y", text.offscreenCoord)
+ .attr("id", pie.cssPrefix + "footer")
+ .attr("class", pie.cssPrefix + "footer")
+ .attr("text-anchor", function() {
+ var location = "left";
+ if (pie.options.footer.location === "bottom-center") {
+ location = "middle";
+ } else if (pie.options.footer.location === "bottom-right") {
+ location = "left"; // on purpose. We have to change the x-coord to make it properly right-aligned
+ }
+ return location;
+ })
+ .attr("fill", function(d) { return d.color; })
+ .style("font-size", function(d) { return d.fontSize + "px"; })
+ .style("font-weight", function(d) { return d.fontWeight; })
+ .style("font-family", function(d) { return d.font; });
+ },
+
+ positionFooter: function(pie) {
+ var footerLocation = pie.options.footer.location;
+ var footerWidth = pie.textComponents.footer.w;
+ var canvasWidth = pie.options.size.canvasWidth;
+ var canvasHeight = pie.options.size.canvasHeight;
+ var canvasPadding = pie.options.misc.canvasPadding;
+
+ var x;
+ if (footerLocation === "bottom-left") {
+ x = canvasPadding.left;
+ } else if (footerLocation === "bottom-right") {
+ x = canvasWidth - footerWidth - canvasPadding.right;
+ } else {
+ x = canvasWidth / 2; // TODO - shouldn't this also take into account padding?
+ }
+
+ pie.__footer
+ .attr("x", x)
+ .attr("y", canvasHeight - canvasPadding.bottom);
+ },
+
+ getHeaderHeight: function(pie) {
+ var h;
+ if (pie.textComponents.title.exists) {
+
+ // if the subtitle isn't defined, it'll be set to 0
+ var totalTitleHeight = pie.textComponents.title.h + pie.options.header.titleSubtitlePadding + pie.textComponents.subtitle.h;
+ if (pie.options.header.location === "pie-center") {
+ h = pie.pieCenter.y - (totalTitleHeight / 2) + totalTitleHeight;
+ } else {
+ h = totalTitleHeight + pie.options.misc.canvasPadding.top;
+ }
+ } else {
+ if (pie.options.header.location === "pie-center") {
+ var footerPlusPadding = pie.options.misc.canvasPadding.bottom + pie.textComponents.footer.h;
+ h = ((pie.options.size.canvasHeight - footerPlusPadding) / 2) + pie.options.misc.canvasPadding.top + (pie.textComponents.subtitle.h / 2);
+ } else {
+ h = pie.options.misc.canvasPadding.top + pie.textComponents.subtitle.h;
+ }
+ }
+ return h;
+ }
+};
+
+ //// --------- validate.js -----------
+var tt = {
+ addTooltips: function(pie) {
+
+ // group the label groups (label, percentage, value) into a single element for simpler positioning
+ var tooltips = pie.svg.insert("g")
+ .attr("class", pie.cssPrefix + "tooltips");
+
+ tooltips.selectAll("." + pie.cssPrefix + "tooltip")
+ .data(pie.options.data.content)
+ .enter()
+ .append("g")
+ .attr("class", pie.cssPrefix + "tooltip")
+ .attr("id", function(d, i) { return pie.cssPrefix + "tooltip" + i; })
+ .style("opacity", 0)
+ .append("rect")
+ .attr("rx", pie.options.tooltips.styles.borderRadius)
+ .attr("ry", pie.options.tooltips.styles.borderRadius)
+ .attr("x", -pie.options.tooltips.styles.padding)
+ .attr("opacity", pie.options.tooltips.styles.backgroundOpacity)
+ .style("fill", pie.options.tooltips.styles.backgroundColor);
+
+ tooltips.selectAll("." + pie.cssPrefix + "tooltip")
+ .data(pie.options.data.content)
+ .append("text")
+ .attr("fill", function(d) { return pie.options.tooltips.styles.color; })
+ .style("font-size", function(d) { return pie.options.tooltips.styles.fontSize; })
+ .style("font-weight", function(d) { return pie.options.tooltips.styles.fontWeight; })
+ .style("font-family", function(d) { return pie.options.tooltips.styles.font; })
+ .text(function(d, i) {
+ var caption = pie.options.tooltips.string;
+ if (pie.options.tooltips.type === "caption") {
+ caption = d.caption;
+ }
+ return tt.replacePlaceholders(pie, caption, i, {
+ label: d.label,
+ value: d.value,
+ percentage: d.percentage
+ });
+ });
+
+ tooltips.selectAll("." + pie.cssPrefix + "tooltip rect")
+ .attr("width", function (d, i) {
+ var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + i);
+ return dims.w + (2 * pie.options.tooltips.styles.padding);
+ })
+ .attr("height", function (d, i) {
+ var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + i);
+ return dims.h + (2 * pie.options.tooltips.styles.padding);
+ })
+ .attr("y", function (d, i) {
+ var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + i);
+ return -(dims.h / 2) + 1;
+ });
+ },
+
+ showTooltip: function(pie, index) {
+ var fadeInSpeed = pie.options.tooltips.styles.fadeInSpeed;
+ if (tt.currentTooltip === index) {
+ fadeInSpeed = 1;
+ }
+
+ tt.currentTooltip = index;
+ d3.select("#" + pie.cssPrefix + "tooltip" + index)
+ .transition()
+ .duration(fadeInSpeed)
+ .style("opacity", function() { return 1; });
+
+ tt.moveTooltip(pie);
+ },
+
+ moveTooltip: function(pie) {
+ d3.selectAll("#" + pie.cssPrefix + "tooltip" + tt.currentTooltip)
+ .attr("transform", function(d) {
+ var mouseCoords = d3.mouse(this.parentNode);
+ var x = mouseCoords[0] + pie.options.tooltips.styles.padding + 2;
+ var y = mouseCoords[1] - (2 * pie.options.tooltips.styles.padding) - 2;
+ return "translate(" + x + "," + y + ")";
+ });
+ },
+
+ hideTooltip: function(pie, index) {
+ d3.select("#" + pie.cssPrefix + "tooltip" + index)
+ .style("opacity", function() { return 0; });
+
+ // move the tooltip offscreen. This ensures that when the user next mouseovers the segment the hidden
+ // element won't interfere
+ d3.select("#" + pie.cssPrefix + "tooltip" + tt.currentTooltip)
+ .attr("transform", function(d, i) {
+ // klutzy, but it accounts for tooltip padding which could push it onscreen
+ var x = pie.options.size.canvasWidth + 1000;
+ var y = pie.options.size.canvasHeight + 1000;
+ return "translate(" + x + "," + y + ")";
+ });
+ },
+
+ replacePlaceholders: function(pie, str, index, replacements) {
+
+ // if the user has defined a placeholderParser function, call it before doing the replacements
+ if (helpers.isFunction(pie.options.tooltips.placeholderParser)) {
+ pie.options.tooltips.placeholderParser(index, replacements);
+ }
+
+ var replacer = function() {
+ return function(match) {
+ var placeholder = arguments[1];
+ if (replacements.hasOwnProperty(placeholder)) {
+ return replacements[arguments[1]];
+ } else {
+ return arguments[0];
+ }
+ };
+ };
+ return str.replace(/\{(\w+)\}/g, replacer(replacements));
+ }
+};
+
+
+ // --------------------------------------------------------------------------------------------
+
+ // our constructor
+ var d3pie = function(element, options) {
+
+ // element can be an ID or DOM element
+ this.element = element;
+ if (typeof element === "string") {
+ var el = element.replace(/^#/, ""); // replace any jQuery-like ID hash char
+ this.element = document.getElementById(el);
+ }
+
+ var opts = {};
+ extend(true, opts, defaultSettings, options);
+ this.options = opts;
+
+ // if the user specified a custom CSS element prefix (ID, class), use it
+ if (this.options.misc.cssPrefix !== null) {
+ this.cssPrefix = this.options.misc.cssPrefix;
+ } else {
+ this.cssPrefix = "p" + _uniqueIDCounter + "_";
+ _uniqueIDCounter++;
+ }
+
+
+ // now run some validation on the user-defined info
+ if (!validate.initialCheck(this)) {
+ return;
+ }
+
+ // add a data-role to the DOM node to let anyone know that it contains a d3pie instance, and the d3pie version
+ d3.select(this.element).attr(_scriptName, _version);
+
+ // things that are done once
+ _setupData.call(this);
+ _init.call(this);
+ };
+
+ d3pie.prototype.recreate = function() {
+ // now run some validation on the user-defined info
+ if (!validate.initialCheck(this)) {
+ return;
+ }
+
+ _setupData.call(this);
+ _init.call(this);
+ };
+
+ d3pie.prototype.redraw = function() {
+ this.element.innerHTML = "";
+ _init.call(this);
+ };
+
+ d3pie.prototype.destroy = function() {
+ this.element.innerHTML = ""; // clear out the SVG
+ d3.select(this.element).attr(_scriptName, null); // remove the data attr
+ };
+
+ /**
+ * Returns all pertinent info about the current open info. Returns null if nothing's open, or if one is, an object of
+ * the following form:
+ * {
+ * element: DOM NODE,
+ * index: N,
+ * data: {}
+ * }
+ */
+ d3pie.prototype.getOpenSegment = function() {
+ var segment = this.currentlyOpenSegment;
+ if (segment !== null && typeof segment !== "undefined") {
+ var index = parseInt(d3.select(segment).attr("data-index"), 10);
+ return {
+ element: segment,
+ index: index,
+ data: this.options.data.content[index]
+ };
+ } else {
+ return null;
+ }
+ };
+
+ d3pie.prototype.openSegment = function(index) {
+ index = parseInt(index, 10);
+ if (index < 0 || index > this.options.data.content.length-1) {
+ return;
+ }
+ segments.openSegment(this, d3.select("#" + this.cssPrefix + "segment" + index).node());
+ };
+
+ d3pie.prototype.closeSegment = function() {
+ segments.maybeCloseOpenSegment(this);
+ };
+
+ // this let's the user dynamically update aspects of the pie chart without causing a complete redraw. It
+ // intelligently re-renders only the part of the pie that the user specifies. Some things cause a repaint, others
+ // just redraw the single element
+ d3pie.prototype.updateProp = function(propKey, value) {
+ switch (propKey) {
+ case "header.title.text":
+ var oldVal = helpers.processObj(this.options, propKey);
+ helpers.processObj(this.options, propKey, value);
+ d3.select("#" + this.cssPrefix + "title").html(value);
+ if ((oldVal === "" && value !== "") || (oldVal !== "" && value === "")) {
+ this.redraw();
+ }
+ break;
+
+ case "header.subtitle.text":
+ var oldValue = helpers.processObj(this.options, propKey);
+ helpers.processObj(this.options, propKey, value);
+ d3.select("#" + this.cssPrefix + "subtitle").html(value);
+ if ((oldValue === "" && value !== "") || (oldValue !== "" && value === "")) {
+ this.redraw();
+ }
+ break;
+
+ case "callbacks.onload":
+ case "callbacks.onMouseoverSegment":
+ case "callbacks.onMouseoutSegment":
+ case "callbacks.onClickSegment":
+ case "effects.pullOutSegmentOnClick.effect":
+ case "effects.pullOutSegmentOnClick.speed":
+ case "effects.pullOutSegmentOnClick.size":
+ case "effects.highlightSegmentOnMouseover":
+ case "effects.highlightLuminosity":
+ helpers.processObj(this.options, propKey, value);
+ break;
+
+ // everything else, attempt to update it & do a repaint
+ default:
+ helpers.processObj(this.options, propKey, value);
+
+ this.destroy();
+ this.recreate();
+ break;
+ }
+ };
+
+
+ // ------------------------------------------------------------------------------------------------
+
+ var _setupData = function () {
+ this.options.data.content = math.sortPieData(this);
+ if (this.options.data.smallSegmentGrouping.enabled) {
+ this.options.data.content = helpers.applySmallSegmentGrouping(this.options.data.content, this.options.data.smallSegmentGrouping);
+ }
+
+
+ this.options.colors = helpers.initSegmentColors(this);
+ this.totalSize = math.getTotalPieSize(this.options.data.content);
+
+ var dp = this.options.labels.percentage.decimalPlaces;
+
+ // add in percentage data to content
+ for (var i=0; i<this.options.data.content.length; i++) {
+ this.options.data.content[i].percentage = _getPercentage(this.options.data.content[i].value, this.totalSize, dp);
+ }
+
+ // adjust the final item to ensure the percentage always adds up to precisely 100%. This is necessary
+ var totalPercentage = 0;
+ for (var j=0; j<this.options.data.content.length; j++) {
+ if (j === this.options.data.content.length - 1) {
+ this.options.data.content[j].percentage = (100 - totalPercentage).toFixed(dp);
+ }
+ totalPercentage += parseFloat(this.options.data.content[j].percentage);
+ }
+ };
+
+ var _init = function() {
+
+ // prep-work
+ this.svg = helpers.addSVGSpace(this);
+
+ // store info about the main text components as part of the d3pie object instance
+ this.textComponents = {
+ headerHeight: 0,
+ title: {
+ exists: this.options.header.title.text !== "",
+ h: 0,
+ w: 0
+ },
+ subtitle: {
+ exists: this.options.header.subtitle.text !== "",
+ h: 0,
+ w: 0
+ },
+ footer: {
+ exists: this.options.footer.text !== "",
+ h: 0,
+ w: 0
+ }
+ };
+
+ this.outerLabelGroupData = [];
+
+ // add the key text components offscreen (title, subtitle, footer). We need to know their widths/heights for later computation
+ if (this.textComponents.title.exists) text.addTitle(this);
+ if (this.textComponents.subtitle.exists) text.addSubtitle(this);
+ text.addFooter(this);
+
+ // console.log(this);
+
+ // the footer never moves. Put it in place now
+ var self = this;
+ text.positionFooter(self);
+ var d3 = helpers.getDimensions(self.__footer.node());
+ self.textComponents.footer.h = d3.h;
+ self.textComponents.footer.w = d3.w;
+
+ if (self.textComponents.title.exists) {
+ var d1 = helpers.getDimensions(self.__title.node());
+ self.textComponents.title.h = d1.h;
+ self.textComponents.title.w = d1.w;
+ }
+
+ if (self.textComponents.subtitle.exists) {
+ var d2 = helpers.getDimensions(self.__subtitle.node());
+ self.textComponents.subtitle.h = d2.h;
+ self.textComponents.subtitle.w = d2.w;
+ }
+
+ // now compute the full header height
+ if (self.textComponents.title.exists || self.textComponents.subtitle.exists) {
+ var headerHeight = 0;
+ if (self.textComponents.title.exists) {
+ headerHeight += self.textComponents.title.h;
+ if (self.textComponents.subtitle.exists) {
+ headerHeight += self.options.header.titleSubtitlePadding;
+ }
+ }
+ if (self.textComponents.subtitle.exists) {
+ headerHeight += self.textComponents.subtitle.h;
+ }
+ self.textComponents.headerHeight = headerHeight;
+ }
+
+ // at this point, all main text component dimensions have been calculated
+ math.computePieRadius(self);
+
+ // this value is used all over the place for placing things and calculating locations. We figure it out ONCE
+ // and store it as part of the object
+ math.calculatePieCenter(self);
+
+ // position the title and subtitle
+ text.positionTitle(self);
+ text.positionSubtitle(self);
+
+ // now create the pie chart segments, and gradients if the user desired
+ if (self.options.misc.gradient.enabled) {
+ segments.addGradients(self);
+ }
+ segments.create(self); // also creates this.arc
+
+ self.__labels = {};
+ labels.add(self, "inner", self.options.labels.inner.format);
+ labels.add(self, "outer", self.options.labels.outer.format);
+
+ // position the label elements relatively within their individual group (label, percentage, value)
+ labels.positionLabelElements(self, "inner", self.options.labels.inner.format);
+ labels.positionLabelElements(self, "outer", self.options.labels.outer.format);
+ labels.computeOuterLabelCoords(self);
+
+ // this is (and should be) dumb. It just places the outer groups at their calculated, collision-free positions
+ labels.positionLabelGroups(self, "outer");
+
+ // we use the label line positions for many other calculations, so ALWAYS compute them
+ labels.computeLabelLinePositions(self);
+
+ // only add them if they're actually enabled
+ if (self.options.labels.lines.enabled && self.options.labels.outer.format !== "none") {
+ labels.addLabelLines(self);
+ }
+
+ labels.positionLabelGroups(self, "inner");
+
+ if (helpers.isFunction(self.options.callbacks.onload)) {
+ try {
+ self.options.callbacks.onload();
+ } catch (e) { }
+ }
+
+ // add and position the tooltips
+ if (self.options.tooltips.enabled) {
+ tt.addTooltips(self);
+ }
+
+ segments.addSegmentEventHandlers(self);
+ };
+
+ var _getPercentage = function(value, total, decimalPlaces) {
+ var relativeAmount = value / total;
+ if (decimalPlaces <= 0) {
+ return Math.round(relativeAmount * 100);
+ } else {
+ return (relativeAmount * 100).toFixed(decimalPlaces);
+ }
+ };
+
+ return d3pie;
+}));