@netdata/charts
Version:
Netdata frontend SDK and chart utilities
1,282 lines (1,231 loc) • 75.9 kB
JavaScript
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
import * as d3 from "d3"
/* eslint-disable */
/*!
* 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 === "undefined" ? "undefined" : _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: "18px",
fontWeight: "bold",
font: "arial"
},
subtitle: {
text: "",
color: "#666666",
fontSize: "14px",
fontWeight: "bold",
font: "arial"
},
location: "top-center",
titleSubtitlePadding: 8
},
footer: {
text: "",
color: "#666666",
fontSize: "14px",
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: "10px"
},
percentage: {
color: "#dddddd",
font: "arial",
fontWeight: "bold",
fontSize: "10px",
decimalPlaces: 0
},
value: {
color: "#cccc44",
fontWeight: "bold",
font: "arial",
fontSize: "10px"
},
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: "10px",
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 initialCheck(pie) {
var cssPrefix = pie.cssPrefix;
var element = pie.element;
var options = pie.options;
// 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) {
// 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 addSVGSpace(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 shuffleArray(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 processObj(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 getDimensions(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 rectIntersect(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 getColorShade(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 initSegmentColors(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 applySmallSegmentGrouping(data, smallSegmentGrouping) {
var newData = [];
var groupedData = [];
var totalGroupedData = 0;
var totalGrouped = 0;
if (smallSegmentGrouping.valueType === "count") {
var data2 = [];
var i2 = 0;
for (var i = 0; i < data.length; i++) {
data2[i2++] = {
idx: i,
value: data[i].value
};
}
data2.sort(function (a, b) {
return a.value < b.value ? 1 : -1;
});
for (i2 = 0; i2 < data2.length; i2++) {
data[data2[i2].idx].smallSegmentPriority = i2;
}
for (var i = 0; i < data.length; i++) {
if (data[i].smallSegmentPriority >= smallSegmentGrouping.value) {
groupedData.push(data[i]);
totalGroupedData += data[i].value;
totalGrouped++;
continue;
}
data[i].isGrouped = false;
newData.push(data[i]);
}
} else if (smallSegmentGrouping.valueType === "percentage") {
var totalSize = math.getTotalPieSize(data);
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;
totalGrouped++;
continue;
}
data[i].isGrouped = false;
newData.push(data[i]);
}
}
} else {
// loop through each data item
for (var i = 0; i < data.length; i++) {
if (data[i].value <= smallSegmentGrouping.value) {
groupedData.push(data[i]);
totalGroupedData += data[i].value;
totalGrouped++;
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(_objectSpread(_objectSpread({}, smallSegmentGrouping), {}, {
color: smallSegmentGrouping.color,
label: "[" + smallSegmentGrouping.label + " " + totalGrouped + "]",
value: totalGroupedData,
isGrouped: true,
groupedData: groupedData
}));
}
return newData;
},
// for debugging
showPoint: function showPoint(svg, x, y) {
svg.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).style("fill", "black");
},
isFunction: function isFunction(functionToCheck) {
var getType = {};
return functionToCheck && getType.toString.call(functionToCheck) === "[object Function]";
},
isArray: function isArray(o) {
return Object.prototype.toString.call(o) === "[object Array]";
}
};
// taken from jQuery
var _extend = function extend() {
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 isFunction(obj) {
return jQuery.type(obj) === "function";
},
isArray: Array.isArray || function (obj) {
return jQuery.type(obj) === "array";
},
isWindow: function isWindow(obj) {
return obj !== null && obj === obj.window;
},
isNumeric: function isNumeric(obj) {
return !isNaN(parseFloat(obj)) && isFinite(obj);
},
type: function type(obj) {
return obj === null ? String(obj) : class2type[toString.call(obj)] || "object";
},
isPlainObject: function isPlainObject(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 toRadians(degrees) {
return degrees * (Math.PI / 180);
},
toDegrees: function toDegrees(radians) {
return radians * (180 / Math.PI);
},
computePieRadius: function computePieRadius(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 getTotalPieSize(data) {
var totalSize = 0;
for (var i = 0; i < data.length; i++) {
totalSize += data[i].value;
}
return totalSize;
},
sortPieData: function sortPieData(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 getPieTranslateCenter(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 calculatePieCenter(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 rotate(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 translate(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 pointIsInArc(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 add(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).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).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;
formatterContext.realLabel = d.label;
return settings.formatter ? settings.formatter(formatterContext, d.value) : d.value;
}).style("font-size", settings.value.fontSize).style("font-family", settings.value.font).style("font-weight", settings.value.fontWeight).style("fill", settings.value.color);
}
},
/**
* @param section "inner" / "outer"
*/
positionLabelElements: function positionLabelElements(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 computeLabelLinePositions(pie) {
pie.lineCoordGroups = [];
pie.__labels.outer.each(function (d, i) {
return labels.computeLinePosition(pie, i);
});
},
computeLinePosition: function computeLinePosition(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 addLabelLines(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 positionLabelGroups(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 getIncludes(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 computeOuterLabelCoords(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 resolveOuterLabelCollisions(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 checkConflict(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 isLabelHidden(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 adjustLabelPos(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 getIdealOuterLabelPositions(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 create(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 addGradients(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 addSegmentEventHandlers(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