0
0
Fork 0
mirror of https://github.com/netdata/netdata.git synced 2025-05-03 08:50:02 +00:00
netdata_netdata/web/gui/dashboard/lib/d3pie-0.2.1-netdata-3.js
Austin S. Hemmelgarn 9574cb4c95
Bundle the react dashboard code into the agent repo directly. ()
* Remove code for bundling the dashoard on install.

* Bundle the dashboard code directly into the agent repo.

This diffstat looks huge, but it’s actually relatively simple. The only
_actual_ changes are in the Makefiles, `configure.ac`, and the addition of
`generate_dashboard_makefile.py`. Everything else consists of removing
files that are included in the dashboard tarball, and extracting the
contents of the tarball into `web/gui/dashboard`.

* CI cleanup.

* Automate bundling of the dashboard code.

This replaces the makefile generator script with one that handles
bundling of the dashboard code in it’s entirety, and updates the GHA
workflow used for generating dashboard PRs to use that instead of the
existing shell commands.

It also removes the packaging/dashboard.* files, as they are no longer
needed.
2021-05-14 11:41:16 -04:00

2124 lines
78 KiB
JavaScript

/*!
* 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;
}));