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