@cryptotaxi247 / netdata-1 / commits / b15a36879

optimized d3pie for speed; removed d3dpie

Costa Tsaousis (ktsaou) committed Jan 26, 2018 at 00:49 UTC b15a36879852ada10f8825f5d664a2a38b19d6b8
6 files changed +2310 -435
web/Makefile.am
+1 -1
@@ -47,7 +47,7 @@ dist_weblib_DATA = \
47 lib/clipboard-polyfill-be05dad.js \
48 lib/c3-0.4.18.min.js \
49 lib/d3-4.12.2.min.js \
50 - lib/d3pie-0.2.1.min.js \
50 + lib/d3pie-0.2.1-netdata-1.js \
51 lib/dygraph-combined-dd74404.js \
52 lib/dygraph-smooth-plotter-dd74404.js \
53 lib/fontawesome-all-5.0.1.min.js \
web/dashboard.html
+20 -6
@@ -646,7 +646,7 @@ So, to avoid flashing the charts, we destroy and re-create the charts on each up
646
647 <hr>
648 <h1>d3pie Charts</h1>
649 -
649 +<!--
650 <div style="width: 33%; display: inline-block;">
651 <div data-netdata="acquiring.responses_by_errorcode"
652 data-dimensions="!*.0000|*"
@@ -672,16 +672,29 @@ So, to avoid flashing the charts, we destroy and re-create the charts on each up
672 <br/>
673 <small>rendered in <span id="time901">X</span> ms</small>
674 </div>
675 -
675 +-->
676 <div style="width: 33%; display: inline-block;">
677 <div data-netdata="apps.cpu"
678 - data-chart-library="d3dpie"
678 + data-chart-library="d3pie"
679 + data-d3pie-pieinnerradius="70%"
680 + data-d3pie-pieouterradius="90%"
681 data-width="100%"
680 - data-height="250px"
682 + data-height="300px"
683 + data-after="-60"
684 + data-points="60"
685 + data-dt-element-name="time901"
686 + ></div>
687 + <br/>
688 + <small>rendered in <span id="time901">X</span> ms</small>
689 + </div>
690 +
691 + <div style="width: 33%; display: inline-block;">
692 + <div data-netdata="system.cpu"
693 + data-chart-library="d3pie"
694 + data-width="100%"
695 + data-height="300px"
696 data-after="-60"
697 data-points="60"
683 - data-debug="false"
684 - data-decimal-digits="0"
698 data-dt-element-name="time902"
699 ></div>
700 <br/>
@@ -710,4 +723,5 @@ So, to avoid flashing the charts, we destroy and re-create the charts on each up
723 <!-- <script> netdataServer = "http://box:19999"; </script> -->
724
725 <!-- load the dashboard manager - it will do the rest -->
726 +<!-- <script>var netdataTheme = 'slate';</script> -->
727 <script type="text/javascript" src="dashboard.js?v20170815-15"></script>
web/dashboard.js
+101 -418
@@ -21,8 +21,6 @@
21 * (default: false) */
22 /*global netdataNoD3pie *//* boolean, disable d3pie charts
23 * (default: false) */
24 -/*global netdataNoD3dpie *//* boolean, disable d3dpie charts
25 - * (default: false) */
24 /*global netdataNoBootstrap *//* boolean, disable bootstrap - disables help too
25 * (default: false) */
26 /*global netdataDontStart *//* boolean, do not start the thread to process the charts
@@ -164,7 +162,7 @@ var NETDATA = window.NETDATA || {};
162 NETDATA.raphael_js = NETDATA.serverStatic + 'lib/raphael-2.2.4-min.js';
163 NETDATA.c3_js = NETDATA.serverStatic + 'lib/c3-0.4.18.min.js';
164 NETDATA.c3_css = NETDATA.serverStatic + 'css/c3-0.4.18.min.css';
167 - NETDATA.d3pie_js = NETDATA.serverStatic + 'lib/d3pie-0.2.1.min.js';
165 + NETDATA.d3pie_js = NETDATA.serverStatic + 'lib/d3pie-0.2.1-netdata-1.js';
166 NETDATA.d3_js = NETDATA.serverStatic + 'lib/d3-4.12.2.min.js';
167 NETDATA.morris_js = NETDATA.serverStatic + 'lib/morris-0.5.1.min.js';
168 NETDATA.morris_css = NETDATA.serverStatic + 'css/morris-0.5.1.css';
@@ -187,7 +185,20 @@ var NETDATA = window.NETDATA || {};
185 easypiechart_scale: '#dfe0e0',
186 gauge_pointer: '#C0C0C0',
187 gauge_stroke: '#F0F0F0',
190 - gauge_gradient: false
188 + gauge_gradient: false,
189 + d3pie: {
190 + title: '#333333',
191 + subtitle: '#666666',
192 + footer: '#888888',
193 + other: '#aaaaaa',
194 + mainlabel: '#333333',
195 + percentage: '#dddddd',
196 + value: '#cccc44',
197 + tooltip_bg: '#000000',
198 + tooltip_fg: '#efefef',
199 + segment_stroke: "#ffffff",
200 + gradient_color: '#000000'
201 + }
202 },
203 slate: {
204 bootstrap_css: NETDATA.serverStatic + 'css/bootstrap-slate-flat-3.3.7.css?v20161229-1',
@@ -210,7 +221,20 @@ var NETDATA = window.NETDATA || {};
221 easypiechart_scale: '#373b40',
222 gauge_pointer: '#474b50',
223 gauge_stroke: '#373b40',
213 - gauge_gradient: false
224 + gauge_gradient: false,
225 + d3pie: {
226 + title: '#C8C8C8',
227 + subtitle: '#283236',
228 + footer: '#283236',
229 + other: '#283236',
230 + mainlabel: '#C8C8C8',
231 + percentage: '#dddddd',
232 + value: '#cccc44',
233 + tooltip_bg: '#272b30',
234 + tooltip_fg: '#C8C8C8',
235 + segment_stroke: "#283236",
236 + gradient_color: '#000000'
237 + }
238 }
239 };
240
@@ -7166,19 +7190,62 @@ var NETDATA = window.NETDATA || {};
7190 var value = data.result.data[index][label];
7191 var color = colors[i - 1];
7192
7193 + if(value != null && value > 0) {
7194 + content.push({
7195 + label: label,
7196 + value: value,
7197 + color: color
7198 + });
7199 + }
7200 + }
7201 +
7202 + if(content.length === 0)
7203 content.push({
7170 - label: label,
7171 - value: value,
7172 - color: color
7204 + label: 'no data',
7205 + value: 100,
7206 + color: '#666666'
7207 });
7174 - }
7208
7209 return content;
7210 };
7211
7179 - NETDATA.d3pieChartUpdate = function(state, data) {
7180 - var content = NETDATA.d3pieSetContent(state, data, 0);
7212 + NETDATA.d3pieSetSelection = function(state, t) {
7213 + if(state.timeIsVisible(t) !== true)
7214 + return NETDATA.d3pieClearSelection(state, true);
7215 +
7216 + var slot = state.calculateRowForTime(t);
7217 + slot = state.data.result.data.length - slot - 1;
7218 +
7219 + if(slot < 0 || slot >= state.data.result.length)
7220 + return NETDATA.d3pieClearSelection(state, true);
7221 +
7222 + if(state.tmp.d3pie_timer === undefined) {
7223 + state.tmp.d3pie_timer = NETDATA.timeout.set(function() {
7224 + state.tmp.d3pie_timer = undefined;
7225 + NETDATA.d3pieChange(state, NETDATA.d3pieSetContent(state, state.data, slot));
7226 + }, 0);
7227 + }
7228 +
7229 + return true;
7230 + };
7231 +
7232 + NETDATA.d3pieClearSelection = function(state, force) {
7233 + if(typeof state.tmp.d3pie_timer !== 'undefined') {
7234 + NETDATA.timeout.clear(state.tmp.d3pie_timer);
7235 + state.tmp.d3pie_timer = undefined;
7236 + }
7237
7238 + if(state.isAutoRefreshable() === true && state.data !== null && force !== true) {
7239 + NETDATA.d3pieChange(state, NETDATA.d3pieSetContent(state, state.data, 0));
7240 + }
7241 + else {
7242 + NETDATA.d3pieChange(state, [ { label: 'no data', value: 1, color: '#666666' } ]);
7243 + }
7244 +
7245 + return true;
7246 + };
7247 +
7248 + NETDATA.d3pieChange = function(state, content) {
7249 var subtitle = NETDATA.dataAttribute(state.element, 'd3pie-subtitle', state.units_current);
7250 if(subtitle !== state.d3pie_subtitle) {
7251 state.d3pie_subtitle = subtitle;
@@ -7186,9 +7253,15 @@ var NETDATA = window.NETDATA || {};
7253 }
7254
7255 state.d3pie_instance.updateProp("data.content", content);
7256 +
7257 return true;
7258 };
7259
7260 + NETDATA.d3pieChartUpdate = function(state, data) {
7261 + var content = NETDATA.d3pieSetContent(state, data, 0);
7262 + return NETDATA.d3pieChange(state, content);
7263 + };
7264 +
7265 NETDATA.d3pieChartCreate = function(state, data) {
7266
7267 state.element_chart.id = 'd3pie-' + state.uuid;
@@ -7201,13 +7274,13 @@ var NETDATA = window.NETDATA || {};
7274 header: {
7275 title: {
7276 text: NETDATA.dataAttribute(state.element, 'd3pie-title', state.title),
7204 - color: NETDATA.dataAttribute(state.element, 'd3pie-title-color', "#333333"),
7277 + color: NETDATA.dataAttribute(state.element, 'd3pie-title-color', NETDATA.themes.current.d3pie.title),
7278 fontSize: NETDATA.dataAttribute(state.element, 'd3pie-title-fontsize', 14),
7279 font: NETDATA.dataAttribute(state.element, 'd3pie-title-font', "arial")
7280 },
7281 subtitle: {
7282 text: state.d3pie_subtitle,
7210 - color: NETDATA.dataAttribute(state.element, 'd3pie-subtitle-color', "#666666"),
7283 + color: NETDATA.dataAttribute(state.element, 'd3pie-subtitle-color', NETDATA.themes.current.d3pie.subtitle),
7284 fontSize: NETDATA.dataAttribute(state.element, 'd3pie-subtitle-fontsize', 11),
7285 font: NETDATA.dataAttribute(state.element, 'd3pie-subtitle-font', "arial")
7286 },
@@ -7215,7 +7288,7 @@ var NETDATA = window.NETDATA || {};
7288 },
7289 footer: {
7290 text: NETDATA.dataAttribute(state.element, 'd3pie-footer', ''),
7218 - color: NETDATA.dataAttribute(state.element, 'd3pie-footer-color', "#999999"),
7291 + color: NETDATA.dataAttribute(state.element, 'd3pie-footer-color', NETDATA.themes.current.d3pie.footer),
7292 fontSize: NETDATA.dataAttribute(state.element, 'd3pie-footer-fontsize', 10),
7293 font: NETDATA.dataAttribute(state.element, 'd3pie-footer-font', "arial"),
7294 location: NETDATA.dataAttribute(state.element, 'd3pie-footer-location', "bottom-left")
@@ -7230,12 +7303,12 @@ var NETDATA = window.NETDATA || {};
7303 // none, random, value-asc, value-desc, label-asc, label-desc
7304 sortOrder: NETDATA.dataAttribute(state.element, 'd3pie-sortorder', "value-desc"),
7305 smallSegmentGrouping: {
7233 - enabled: NETDATA.dataAttributeBoolean(state.element, "d3pie-smallsegmentgrouping-enabled", true),
7306 + enabled: NETDATA.dataAttributeBoolean(state.element, "d3pie-smallsegmentgrouping-enabled", false),
7307 value: NETDATA.dataAttribute(state.element, 'd3pie-smallsegmentgrouping-value', 1),
7308 // percentage, value
7309 valueType: NETDATA.dataAttribute(state.element, 'd3pie-smallsegmentgrouping-valuetype', "percentage"),
7310 label: NETDATA.dataAttribute(state.element, 'd3pie-smallsegmentgrouping-label', "other"),
7238 - color: NETDATA.dataAttribute(state.element, 'd3pie-smallsegmentgrouping-color', "#cccccc")
7311 + color: NETDATA.dataAttribute(state.element, 'd3pie-smallsegmentgrouping-color', NETDATA.themes.current.d3pie.other)
7312 },
7313
7314 // REQUIRED! This is where you enter your pie data; it needs to be an array of objects
@@ -7252,21 +7325,21 @@ var NETDATA = window.NETDATA || {};
7325 inner: {
7326 // label, value, percentage, label-value1, label-value2, label-percentage1, label-percentage2
7327 format: NETDATA.dataAttribute(state.element, 'd3pie-labels-inner-format', "percentage"),
7255 - hideWhenLessThanPercentage: NETDATA.dataAttribute(state.element, 'd3pie-labels-inner-hidewhenlessthanpercentage', null)
7328 + hideWhenLessThanPercentage: NETDATA.dataAttribute(state.element, 'd3pie-labels-inner-hidewhenlessthanpercentage', 2)
7329 },
7330 mainLabel: {
7258 - color: NETDATA.dataAttribute(state.element, 'd3pie-labels-mainLabel-color', "#333333"),
7331 + color: NETDATA.dataAttribute(state.element, 'd3pie-labels-mainLabel-color', NETDATA.themes.current.d3pie.mainlabel),
7332 font: NETDATA.dataAttribute(state.element, 'd3pie-labels-mainLabel-font', "arial"),
7333 fontSize: NETDATA.dataAttribute(state.element, 'd3pie-labels-mainLabel-fontsize', 10)
7334 },
7335 percentage: {
7263 - color: NETDATA.dataAttribute(state.element, 'd3pie-labels-percentage-color', "#dddddd"),
7336 + color: NETDATA.dataAttribute(state.element, 'd3pie-labels-percentage-color', NETDATA.themes.current.d3pie.percentage),
7337 font: NETDATA.dataAttribute(state.element, 'd3pie-labels-percentage-font', "arial"),
7338 fontSize: NETDATA.dataAttribute(state.element, 'd3pie-labels-percentage-fontsize', 10),
7339 decimalPlaces: 0
7340 },
7341 value: {
7269 - color: NETDATA.dataAttribute(state.element, 'd3pie-labels-value-color', "#cccc44"),
7342 + color: NETDATA.dataAttribute(state.element, 'd3pie-labels-value-color', NETDATA.themes.current.d3pie.value),
7343 font: NETDATA.dataAttribute(state.element, 'd3pie-labels-value-font', "arial"),
7344 fontSize: NETDATA.dataAttribute(state.element, 'd3pie-labels-value-fontsize', 10)
7345 },
@@ -7305,9 +7378,9 @@ var NETDATA = window.NETDATA || {};
7378 placeholderParser: null,
7379 styles: {
7380 fadeInSpeed: 250,
7308 - backgroundColor: "#000000",
7381 + backgroundColor: NETDATA.themes.current.d3pie.tooltip_bg,
7382 backgroundOpacity: 0.5,
7310 - color: "#efefef",
7383 + color: NETDATA.themes.current.d3pie.tooltip_fg,
7384 borderRadius: 2,
7385 font: "arial",
7386 fontSize: 10,
@@ -7316,14 +7389,14 @@ var NETDATA = window.NETDATA || {};
7389 },
7390 misc: {
7391 colors: {
7319 - background: null, // transparent
7392 + background: 'transparent', // transparent or color #
7393 // segments: state.chartColors(),
7321 - segmentStroke: NETDATA.dataAttribute(state.element, 'd3pie-misc-colors-segmentstroke', "#ffffff")
7394 + segmentStroke: NETDATA.dataAttribute(state.element, 'd3pie-misc-colors-segmentstroke', NETDATA.themes.current.d3pie.segment_stroke)
7395 },
7396 gradient: {
7397 enabled: NETDATA.dataAttributeBoolean(state.element, 'd3pie-misc-gradient-enabled', false),
7398 percentage: NETDATA.dataAttribute(state.element, 'd3pie-misc-colors-percentage', 95),
7326 - color: NETDATA.dataAttribute(state.element, 'd3pie-misc-colors-segmentstroke', "#000000")
7399 + color: NETDATA.dataAttribute(state.element, 'd3pie-misc-gradient-color', NETDATA.themes.current.d3pie.gradient_color)
7400 },
7401 canvasPadding: {
7402 top: 5,
@@ -7335,7 +7408,7 @@ var NETDATA = window.NETDATA || {};
7408 x: 0,
7409 y: 0
7410 },
7338 - cssPrefix: null
7411 + cssPrefix: NETDATA.dataAttribute(state.element, 'd3pie-cssprefix', null)
7412 },
7413 callbacks: {
7414 onload: null,
@@ -7352,377 +7425,6 @@ var NETDATA = window.NETDATA || {};
7425 return true;
7426 };
7427
7355 - // ----------------------------------------------------------------------------------------------------------------
7356 - // d3dpie
7357 -
7358 - NETDATA.d3dpieInitialize = function(callback) {
7359 - if(typeof netdataNod3dpie === 'undefined' || !netdataNod3dpie) {
7360 -
7361 - // d3dpie requires D3
7362 - if(!NETDATA.chartLibraries.d3.initialized) {
7363 - if(NETDATA.chartLibraries.d3.enabled) {
7364 - NETDATA.d3Initialize(function() {
7365 - NETDATA.d3dpieInitialize(callback);
7366 - });
7367 - }
7368 - else {
7369 - NETDATA.chartLibraries.d3dpie.enabled = false;
7370 - if(typeof callback === "function")
7371 - return callback();
7372 - }
7373 - }
7374 - else {
7375 - NETDATA.registerChartLibrary('d3dpie', "lib/d3dpie.js");
7376 - if(typeof callback === "function")
7377 - return callback();
7378 - }
7379 - }
7380 - else {
7381 - NETDATA.chartLibraries.d3dpie.enabled = false;
7382 - if(typeof callback === "function")
7383 - return callback();
7384 - }
7385 - };
7386 -
7387 - NETDATA.d3dpieSetContent = function(state, data, index) {
7388 - state.legendFormatValueDecimalsFromMinMax(
7389 - data.min,
7390 - data.max
7391 - );
7392 -
7393 - var content = [];
7394 - var colors = state.chartColors();
7395 - var len = data.result.labels.length;
7396 - var sum = 0;
7397 - for(var i = 1; i < len ; i++) {
7398 - var label = data.result.labels[i];
7399 - var value = data.result.data[index][label];
7400 - var color = colors[i - 1];
7401 -
7402 - sum += value;
7403 -
7404 - content.push({
7405 - label: label,
7406 - value: value,
7407 - color: color
7408 - });
7409 - }
7410 -
7411 - len = content.length;
7412 - for(i = 0; i < len ; i++) {
7413 - content[i].percent = content[i].value * 100 / sum;
7414 - }
7415 -
7416 - return content;
7417 - };
7418 -
7419 - NETDATA.d3dpieChange = function(d3dpie, data, duration) {
7420 - var svg = d3dpie.svg;
7421 - var width = d3dpie.width;
7422 - var height = d3dpie.height;
7423 - var radius = d3dpie.radius;
7424 - var pie = d3dpie.pie;
7425 - var arc = d3dpie.arc;
7426 - var outerArc = d3dpie.outerArc;
7427 - var key = d3dpie.key;
7428 -
7429 - function mergeWithFirstEqualZero(first, second) {
7430 - var secondSet = d3.set();
7431 - second.forEach(function (d) {
7432 - secondSet.add(d.label);
7433 - });
7434 -
7435 - var onlyFirst = first
7436 - .filter(function (d) {
7437 - return !secondSet.has(d.label)
7438 - })
7439 - .map(function (d) {
7440 - return {label: d.label, value: 0};
7441 - });
7442 - return d3.merge([second, onlyFirst])
7443 - .sort(function (a, b) {
7444 - return d3.ascending(a.label, b.label);
7445 - });
7446 - }
7447 -
7448 - var data0 = svg.select(".slices").selectAll("path.slice")
7449 - .data().map(function (d) {
7450 - return d.data
7451 - });
7452 - if (data0.length === 0) data0 = data;
7453 - var was = mergeWithFirstEqualZero(data, data0);
7454 - var is = mergeWithFirstEqualZero(data0, data);
7455 -
7456 - /* ------- SLICE ARCS -------*/
7457 -
7458 - var slice = svg.select(".slices").selectAll("path.slice")
7459 - .data(pie(was), key);
7460 -
7461 - slice.enter()
7462 - .insert("path")
7463 - .attr("class", "slice")
7464 - .style("fill", function (d) {
7465 - return d.data.color;
7466 - })
7467 - .each(function (d) {
7468 - this._current = d;
7469 - });
7470 -
7471 - slice = svg.select(".slices").selectAll("path.slice")
7472 - .data(pie(is), key);
7473 -
7474 - slice
7475 - .transition().duration(duration)
7476 - .attrTween("d", function (d) {
7477 - var interpolate = d3.interpolate(this._current, d);
7478 - var _this = this;
7479 - return function (t) {
7480 - _this._current = interpolate(t);
7481 - return arc(_this._current);
7482 - };
7483 - });
7484 -
7485 - slice = svg.select(".slices").selectAll("path.slice")
7486 - .data(pie(data), key);
7487 -
7488 - slice
7489 - .exit().transition().delay(duration).duration(0)
7490 - .remove();
7491 -
7492 - /* ------- TEXT LABELS -------*/
7493 -
7494 - var text = svg.select(".labels").selectAll("text")
7495 - .data(pie(was), key);
7496 -
7497 - text.enter()
7498 - .append("text")
7499 - .attr("dy", ".35em")
7500 - .style("opacity", 0)
7501 - .text(function (d) {
7502 - return d.data.label;
7503 - })
7504 - .each(function (d) {
7505 - this._current = d;
7506 - });
7507 -
7508 - function midAngle(d) {
7509 - return d.startAngle + (d.endAngle - d.startAngle) / 2;
7510 - }
7511 -
7512 - var slotsLeft = {}, slotsRight = {};
7513 - var setLeft = {}, setRight = {};
7514 - var fontSize = 14;
7515 -
7516 - function verticalAlign(d, pos) {
7517 - if(d.data.value === 0)
7518 - return;
7519 -
7520 - var slots = (pos[0] < 0)?slotsLeft:slotsRight;
7521 - var set = (pos[0] < 0)?setLeft:setRight;
7522 -
7523 - if(typeof(set[d.data.label]) !== 'undefined') {
7524 - pos[1] = set[d.data.label];
7525 - return;
7526 - }
7527 -
7528 - var y = Math.round(pos[1]);
7529 - y -= y % fontSize;
7530 - var idx = Math.round(y / fontSize);
7531 -
7532 - for(var i = 0; i < 10 ;i++) {
7533 - if (typeof(slots[idx + i]) === 'undefined') {
7534 - //console.log(d.data.label + ': x = ' + pos[0].toString() + ', y = ' + y.toString() + ', idx = ' + idx.toString() + ', found i = ', i.toString() + ', new y = ' + ((idx + i) * fontSize).toString());
7535 - slots[idx + i] = 1;
7536 - y = (idx + i) * fontSize;
7537 - break;
7538 - }
7539 - else if (typeof(slots[idx - i]) === 'undefined') {
7540 - //console.log(d.data.label + ': x = ' + pos[0].toString() + ', y = ' + y.toString() + ', idx = ' + idx.toString() + ', found i = ', (-i).toString() + ', new y = ' + ((idx - i) * fontSize).toString());
7541 - slots[idx - i] = 1;
7542 - y = (idx - i) * fontSize;
7543 - break;
7544 - }
7545 - }
7546 -
7547 - pos[1] = y;
7548 - set[d.data.label] = y;
7549 - }
7550 -
7551 - text = svg.select(".labels").selectAll("text")
7552 - .data(pie(is), key);
7553 -
7554 - text.transition().duration(duration)
7555 - .style("opacity", function (d) {
7556 - return (d.data.value === 0) ? 0 : 1;
7557 - })
7558 - .attrTween("transform", function (d) {
7559 - var interpolate = d3.interpolate(this._current, d);
7560 - var _this = this;
7561 - return function (t) {
7562 - var d2 = interpolate(t);
7563 - _this._current = d2;
7564 - var pos = outerArc.centroid(d2);
7565 - pos[0] = radius * (midAngle(d2) < Math.PI ? 1 : -1);
7566 - verticalAlign(d2, pos);
7567 - return "translate(" + pos + ")";
7568 - };
7569 - })
7570 - .styleTween("text-anchor", function (d) {
7571 - var interpolate = d3.interpolate(this._current, d);
7572 - return function (t) {
7573 - var d2 = interpolate(t);
7574 - return midAngle(d2) < Math.PI ? "start" : "end";
7575 - };
7576 - });
7577 -
7578 - text = svg.select(".labels").selectAll("text")
7579 - .data(pie(data), key);
7580 -
7581 - text
7582 - .exit().transition().delay(duration)
7583 - .remove();
7584 -
7585 - /* ------- SLICE TO TEXT POLYLINES -------*/
7586 -
7587 - var polyline = svg.select(".lines").selectAll("polyline")
7588 - .data(pie(was), key);
7589 -
7590 - polyline.enter()
7591 - .append("polyline")
7592 - .style("opacity", 0)
7593 - .each(function (d) {
7594 - this._current = d;
7595 - });
7596 -
7597 - polyline = svg.select(".lines").selectAll("polyline")
7598 - .data(pie(is), key);
7599 -
7600 - polyline.transition().duration(duration)
7601 - .style("opacity", function (d) {
7602 - return (d.data.value === 0) ? 0 : 1;
7603 - })
7604 - .attrTween("points", function (d) {
7605 - // this._current = this._current;
7606 - var interpolate = d3.interpolate(this._current, d);
7607 - var _this = this;
7608 - return function (t) {
7609 - var d2 = interpolate(t);
7610 - _this._current = d2;
7611 - var pos = outerArc.centroid(d2);
7612 - pos[0] = radius * 0.95 * (midAngle(d2) < Math.PI ? 1 : -1);
7613 - verticalAlign(d2, pos);
7614 -
7615 - var pos2 = outerArc.centroid(d2);
7616 - verticalAlign(d2, pos2);
7617 - return [arc.centroid(d2), pos2, pos];
7618 - };
7619 - });
7620 -
7621 - polyline = svg.select(".lines").selectAll("polyline")
7622 - .data(pie(data), key);
7623 -
7624 - polyline
7625 - .exit().transition().delay(duration)
7626 - .remove();
7627 - };
7628 -
7629 - NETDATA.d3dpieSetSelection = function(state, t) {
7630 - if(state.timeIsVisible(t) !== true)
7631 - return NETDATA.d3dpieClearSelection(state, true);
7632 -
7633 - var slot = state.calculateRowForTime(t);
7634 - slot = state.data.result.data.length - slot - 1;
7635 -
7636 - if(slot < 0 || slot >= state.data.result.length)
7637 - return NETDATA.d3dpieClearSelection(state, true);
7638 -
7639 - if(state.tmp.d3pie_timer === undefined) {
7640 - state.tmp.d3pie_timer = NETDATA.timeout.set(function() {
7641 - state.tmp.d3pie_timer = undefined;
7642 - NETDATA.d3dpieChange(state.d3dpie_instance, NETDATA.d3dpieSetContent(state, state.data, slot), 0);
7643 - }, 0);
7644 - }
7645 -
7646 - return true;
7647 - };
7648 -
7649 - NETDATA.d3dpieClearSelection = function(state, force) {
7650 - if(typeof state.tmp.d3dpie_timer !== 'undefined') {
7651 - NETDATA.timeout.clear(state.tmp.d3dpie_timer);
7652 - state.tmp.d3dpie_timer = undefined;
7653 - }
7654 -
7655 - if(state.isAutoRefreshable() === true && state.data !== null && force !== true) {
7656 - NETDATA.d3dpieChartUpdate(state, state.data);
7657 - }
7658 - else {
7659 - // FIXME: we should show empty chart here
7660 - NETDATA.d3dpieChange(state.d3dpie_instance, NETDATA.d3dpieSetContent(state, state.data, 0), 0);
7661 - }
7662 -
7663 - return true;
7664 - };
7665 -
7666 - NETDATA.d3dpieChartUpdate = function(state, data) {
7667 - NETDATA.d3dpieChange(state.d3dpie_instance, NETDATA.d3dpieSetContent(state, data, 0), 500);
7668 - return true;
7669 - };
7670 -
7671 - NETDATA.d3dpieChartCreate = function(state, data) {
7672 - state.element_chart.className = 'd3dpieChart';
7673 -
7674 - var svg = d3.select(state.element_chart)
7675 - .append("svg")
7676 - .append("g");
7677 -
7678 - svg.append("g")
7679 - .attr("class", "slices");
7680 -
7681 - svg.append("g")
7682 - .attr("class", "labels");
7683 -
7684 - svg
7685 - .append("g")
7686 - .attr("class", "lines");
7687 -
7688 - var width = state.chartWidth(),
7689 - height = state.chartHeight(),
7690 - radius = Math.min(width,height) / 2;
7691 -
7692 - var pie = d3.pie()
7693 - .sort(null)
7694 - .value(function(d) {
7695 - return d.value;
7696 - });
7697 -
7698 - var arc = d3.arc()
7699 - .outerRadius(radius * 0.9)
7700 - .innerRadius(radius * 0.6);
7701 -
7702 - var outerArc = d3.arc()
7703 - .innerRadius(radius * 0.7)
7704 - .outerRadius(radius * 0.9);
7705 -
7706 - svg.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
7707 -
7708 - var key = function(d) { return d.data.label; };
7709 -
7710 - state.d3dpie_instance = {
7711 - svg: svg,
7712 - width: width,
7713 - height: height,
7714 - radius: radius,
7715 - pie: pie,
7716 - arc: arc,
7717 - outerArc: outerArc,
7718 - key: key
7719 - };
7720 -
7721 - NETDATA.d3dpieChange(state.d3dpie_instance, NETDATA.d3dpieSetContent(state, data, 0), 500);
7722 -
7723 - return true;
7724 - };
7725 -
7428 // ----------------------------------------------------------------------------------------------------------------
7429 // D3
7430
@@ -8691,27 +8393,8 @@ var NETDATA = window.NETDATA || {};
8393 create: NETDATA.d3pieChartCreate,
8394 update: NETDATA.d3pieChartUpdate,
8395 resize: null,
8694 - setSelection: undefined, // function(state, t) { void(state); return true; },
8695 - clearSelection: undefined, // function(state) { void(state); return true; },
8696 - toolboxPanAndZoom: null,
8697 - initialized: false,
8698 - enabled: true,
8699 - format: function(state) { void(state); return 'json'; },
8700 - options: function(state) { void(state); return 'objectrows|ms'; },
8701 - legend: function(state) { void(state); return null; },
8702 - autoresize: function(state) { void(state); return false; },
8703 - max_updates_to_recreate: function(state) { void(state); return 5000; },
8704 - track_colors: function(state) { void(state); return false; },
8705 - pixels_per_point: function(state) { void(state); return 15; },
8706 - container_class: function(state) { void(state); return 'netdata-container'; }
8707 - },
8708 - "d3dpie": {
8709 - initialize: NETDATA.d3dpieInitialize,
8710 - create: NETDATA.d3dpieChartCreate,
8711 - update: NETDATA.d3dpieChartUpdate,
8712 - resize: null,
8713 - setSelection: NETDATA.d3dpieSetSelection,
8714 - clearSelection: NETDATA.d3dpieClearSelection,
8396 + setSelection: NETDATA.d3pieSetSelection,
8397 + clearSelection: NETDATA.d3pieClearSelection,
8398 toolboxPanAndZoom: null,
8399 initialized: false,
8400 enabled: true,
web/index.html
+1 -1
@@ -5622,6 +5622,6 @@
5622 </div>
5623 </div>
5624 <div id="hiddenDownloadLinks" style="display: none;" hidden></div>
5625 - <script type="text/javascript" src="dashboard.js?v20180121-2"></script>
5625 + <script type="text/javascript" src="dashboard.js?v20180126-2"></script>
5626 </body>
5627 </html>
web/lib/d3pie-0.2.1-netdata-1.js new
+2187
@@ -0,0 +1,2187 @@
1 +/*!
2 + * d3pie
3 + * @author Ben Keen
4 + * @version 0.1.9
5 + * @date June 17th, 2015
6 + * @repo http://github.com/benkeen/d3pie
7 + */
8 +
9 +// UMD pattern from https://github.com/umdjs/umd/blob/master/returnExports.js
10 +(function(root, factory) {
11 + if (typeof define === 'function' && define.amd) {
12 + // AMD. Register as an anonymous module
13 + define([], factory);
14 + } else if (typeof exports === 'object') {
15 + // Node. Does not work with strict CommonJS, but only CommonJS-like environments that support module.exports,
16 + // like Node
17 + module.exports = factory();
18 + } else {
19 + // browser globals (root is window)
20 + root.d3pie = factory(root);
21 + }
22 +}(this, function() {
23 +
24 + var _scriptName = "d3pie";
25 + var _version = "0.2.1";
26 +
27 + // used to uniquely generate IDs and classes, ensuring no conflict between multiple pies on the same page
28 + var _uniqueIDCounter = 0;
29 +
30 +
31 + // this section includes all helper libs on the d3pie object. They're populated via grunt-template. Note: to keep
32 + // the syntax highlighting from getting all messed up, I commented out each line. That REQUIRES each of the files
33 + // to have an empty first line. Crumby, yes, but acceptable.
34 + //// --------- _default-settings.js -----------/**
35 +/**
36 + * Contains the out-the-box settings for the script. Any of these settings that aren't explicitly overridden for the
37 + * d3pie instance will inherit from these. This is also included on the main website for use in the generation script.
38 + */
39 +var defaultSettings = {
40 + header: {
41 + title: {
42 + text: "",
43 + color: "#333333",
44 + fontSize: 18,
45 + font: "arial"
46 + },
47 + subtitle: {
48 + text: "",
49 + color: "#666666",
50 + fontSize: 14,
51 + font: "arial"
52 + },
53 + location: "top-center",
54 + titleSubtitlePadding: 8
55 + },
56 + footer: {
57 + text: "",
58 + color: "#666666",
59 + fontSize: 14,
60 + font: "arial",
61 + location: "left"
62 + },
63 + size: {
64 + canvasHeight: 500,
65 + canvasWidth: 500,
66 + pieInnerRadius: "0%",
67 + pieOuterRadius: null
68 + },
69 + data: {
70 + sortOrder: "none",
71 + ignoreSmallSegments: {
72 + enabled: false,
73 + valueType: "percentage",
74 + value: null
75 + },
76 + smallSegmentGrouping: {
77 + enabled: false,
78 + value: 1,
79 + valueType: "percentage",
80 + label: "Other",
81 + color: "#cccccc"
82 + },
83 + content: []
84 + },
85 + labels: {
86 + outer: {
87 + format: "label",
88 + hideWhenLessThanPercentage: null,
89 + pieDistance: 30
90 + },
91 + inner: {
92 + format: "percentage",
93 + hideWhenLessThanPercentage: null
94 + },
95 + mainLabel: {
96 + color: "#333333",
97 + font: "arial",
98 + fontSize: 10
99 + },
100 + percentage: {
101 + color: "#dddddd",
102 + font: "arial",
103 + fontSize: 10,
104 + decimalPlaces: 0
105 + },
106 + value: {
107 + color: "#cccc44",
108 + font: "arial",
109 + fontSize: 10
110 + },
111 + lines: {
112 + enabled: true,
113 + style: "curved",
114 + color: "segment"
115 + },
116 + truncation: {
117 + enabled: false,
118 + truncateLength: 30
119 + },
120 + formatter: null
121 + },
122 + effects: {
123 + load: {
124 + effect: "default",
125 + speed: 1000
126 + },
127 + pullOutSegmentOnClick: {
128 + effect: "bounce",
129 + speed: 300,
130 + size: 10
131 + },
132 + highlightSegmentOnMouseover: true,
133 + highlightLuminosity: -0.2
134 + },
135 + tooltips: {
136 + enabled: false,
137 + type: "placeholder", // caption|placeholder
138 + string: "",
139 + placeholderParser: null,
140 + styles: {
141 + fadeInSpeed: 250,
142 + backgroundColor: "#000000",
143 + backgroundOpacity: 0.5,
144 + color: "#efefef",
145 + borderRadius: 2,
146 + font: "arial",
147 + fontSize: 10,
148 + padding: 4
149 + }
150 + },
151 + misc: {
152 + colors: {
153 + background: null,
154 + segments: [
155 + "#2484c1", "#65a620", "#7b6888", "#a05d56", "#961a1a", "#d8d23a", "#e98125", "#d0743c", "#635222", "#6ada6a",
156 + "#0c6197", "#7d9058", "#207f33", "#44b9b0", "#bca44a", "#e4a14b", "#a3acb2", "#8cc3e9", "#69a6f9", "#5b388f",
157 + "#546e91", "#8bde95", "#d2ab58", "#273c71", "#98bf6e", "#4daa4b", "#98abc5", "#cc1010", "#31383b", "#006391",
158 + "#c2643f", "#b0a474", "#a5a39c", "#a9c2bc", "#22af8c", "#7fcecf", "#987ac6", "#3d3b87", "#b77b1c", "#c9c2b6",
159 + "#807ece", "#8db27c", "#be66a2", "#9ed3c6", "#00644b", "#005064", "#77979f", "#77e079", "#9c73ab", "#1f79a7"
160 + ],
161 + segmentStroke: "#ffffff"
162 + },
163 + gradient: {
164 + enabled: false,
165 + percentage: 95,
166 + color: "#000000"
167 + },
168 + canvasPadding: {
169 + top: 5,
170 + right: 5,
171 + bottom: 5,
172 + left: 5
173 + },
174 + pieCenterOffset: {
175 + x: 0,
176 + y: 0
177 + },
178 + cssPrefix: null
179 + },
180 + callbacks: {
181 + onload: null,
182 + onMouseoverSegment: null,
183 + onMouseoutSegment: null,
184 + onClickSegment: null
185 + }
186 +};
187 +
188 + //// --------- validate.js -----------
189 +var validate = {
190 +
191 + // called whenever a new pie chart is created
192 + initialCheck: function(pie) {
193 + var cssPrefix = pie.cssPrefix;
194 + var element = pie.element;
195 + var options = pie.options;
196 +
197 + // confirm d3 is available [check minimum version]
198 + if (!window.d3 || !window.d3.hasOwnProperty("version")) {
199 + console.error("d3pie error: d3 is not available");
200 + return false;
201 + }
202 +
203 + // confirm element is either a DOM element or a valid string for a DOM element
204 + if (!(element instanceof HTMLElement || element instanceof SVGElement)) {
205 + console.error("d3pie error: the first d3pie() param must be a valid DOM element (not jQuery) or a ID string.");
206 + return false;
207 + }
208 +
209 + // confirm the CSS prefix is valid. It has to start with a-Z and contain nothing but a-Z0-9_-
210 + if (!(/[a-zA-Z][a-zA-Z0-9_-]*$/.test(cssPrefix))) {
211 + console.error("d3pie error: invalid options.misc.cssPrefix");
212 + return false;
213 + }
214 +
215 + // confirm some data has been supplied
216 + if (!helpers.isArray(options.data.content)) {
217 + console.error("d3pie error: invalid config structure: missing data.content property.");
218 + return false;
219 + }
220 + if (options.data.content.length === 0) {
221 + console.error("d3pie error: no data supplied.");
222 + return false;
223 + }
224 +
225 + // clear out any invalid data. Each data row needs a valid positive number and a label
226 + var data = [];
227 + for (var i=0; i<options.data.content.length; i++) {
228 + if (typeof options.data.content[i].value !== "number" || isNaN(options.data.content[i].value)) {
229 + console.log("not valid: ", options.data.content[i]);
230 + continue;
231 + }
232 + if (options.data.content[i].value <= 0) {
233 + console.log("not valid - should have positive value: ", options.data.content[i]);
234 + continue;
235 + }
236 + data.push(options.data.content[i]);
237 + }
238 + pie.options.data.content = data;
239 +
240 + // labels.outer.hideWhenLessThanPercentage - 1-100
241 + // labels.inner.hideWhenLessThanPercentage - 1-100
242 +
243 + return true;
244 + }
245 +};
246 +
247 + //// --------- helpers.js -----------
248 +var helpers = {
249 +
250 + // creates the SVG element
251 + addSVGSpace: function(pie) {
252 + var element = pie.element;
253 + var canvasWidth = pie.options.size.canvasWidth;
254 + var canvasHeight = pie.options.size.canvasHeight;
255 + var backgroundColor = pie.options.misc.colors.background;
256 +
257 + var svg = d3.select(element).append("svg:svg")
258 + .attr("width", canvasWidth)
259 + .attr("height", canvasHeight);
260 +
261 + if (backgroundColor !== "transparent") {
262 + svg.style("background-color", function() { return backgroundColor; });
263 + }
264 +
265 + return svg;
266 + },
267 +
268 + whenIdExists: function(id, callback) {
269 + callback();
270 + /*
271 + var inc = 1;
272 + var giveupIterationCount = 1000;
273 +
274 + var interval = setInterval(function() {
275 + if (document.getElementById(id)) {
276 + clearInterval(interval);
277 + callback();
278 + }
279 + if (inc > giveupIterationCount) {
280 + clearInterval(interval);
281 + }
282 + inc++;
283 + }, 1);
284 + */
285 + },
286 +
287 + whenElementsExist: function(els, callback) {
288 + callback();
289 + /*
290 + var inc = 1;
291 + var giveupIterationCount = 1000;
292 +
293 + var interval = setInterval(function() {
294 + var allExist = true;
295 + for (var i=0; i<els.length; i++) {
296 + if (!document.getElementById(els[i])) {
297 + allExist = false;
298 + break;
299 + }
300 + }
301 + if (allExist) {
302 + clearInterval(interval);
303 + callback();
304 + }
305 + if (inc > giveupIterationCount) {
306 + clearInterval(interval);
307 + }
308 + inc++;
309 + }, 1);
310 + */
311 + },
312 +
313 + shuffleArray: function(array) {
314 + var currentIndex = array.length, tmpVal, randomIndex;
315 +
316 + while (0 !== currentIndex) {
317 + randomIndex = Math.floor(Math.random() * currentIndex);
318 + currentIndex -= 1;
319 +
320 + // and swap it with the current element
321 + tmpVal = array[currentIndex];
322 + array[currentIndex] = array[randomIndex];
323 + array[randomIndex] = tmpVal;
324 + }
325 + return array;
326 + },
327 +
328 + processObj: function(obj, is, value) {
329 + if (typeof is === 'string') {
330 + return helpers.processObj(obj, is.split('.'), value);
331 + } else if (is.length === 1 && value !== undefined) {
332 + obj[is[0]] = value;
333 + return obj[is[0]];
334 + } else if (is.length === 0) {
335 + return obj;
336 + } else {
337 + return helpers.processObj(obj[is[0]], is.slice(1), value);
338 + }
339 + },
340 +
341 + getDimensions: function(id) {
342 + var el = document.getElementById(id);
343 + var w = 0, h = 0;
344 + if (el) {
345 + var dimensions = el.getBBox();
346 + w = dimensions.width;
347 + h = dimensions.height;
348 + } else {
349 + console.log("error: getDimensions() " + id + " not found.");
350 + }
351 + return { w: w, h: h };
352 + },
353 +
354 + /**
355 + * This is based on the SVG coordinate system, where top-left is 0,0 and bottom right is n-n.
356 + * @param r1
357 + * @param r2
358 + * @returns {boolean}
359 + */
360 + rectIntersect: function(r1, r2) {
361 + var returnVal = (
362 + // r2.left > r1.right
363 + (r2.x > (r1.x + r1.w)) ||
364 +
365 + // r2.right < r1.left
366 + ((r2.x + r2.w) < r1.x) ||
367 +
368 + // r2.top < r1.bottom
369 + ((r2.y + r2.h) < r1.y) ||
370 +
371 + // r2.bottom > r1.top
372 + (r2.y > (r1.y + r1.h))
373 + );
374 +
375 + return !returnVal;
376 + },
377 +
378 + /**
379 + * Returns a lighter/darker shade of a hex value, based on a luminance value passed.
380 + * @param hex a hex color value such as “#abc” or “#123456″ (the hash is optional)
381 + * @param lum the luminosity factor: -0.1 is 10% darker, 0.2 is 20% lighter, etc.
382 + * @returns {string}
383 + */
384 + getColorShade: function(hex, lum) {
385 +
386 + // validate hex string
387 + hex = String(hex).replace(/[^0-9a-f]/gi, '');
388 + if (hex.length < 6) {
389 + hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
390 + }
391 + lum = lum || 0;
392 +
393 + // convert to decimal and change luminosity
394 + var newHex = "#";
395 + for (var i=0; i<3; i++) {
396 + var c = parseInt(hex.substr(i * 2, 2), 16);
397 + c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16);
398 + newHex += ("00" + c).substr(c.length);
399 + }
400 +
401 + return newHex;
402 + },
403 +
404 + /**
405 + * Users can choose to specify segment colors in three ways (in order of precedence):
406 + * 1. include a "color" attribute for each row in data.content
407 + * 2. include a misc.colors.segments property which contains an array of hex codes
408 + * 3. specify nothing at all and rely on this lib provide some reasonable defaults
409 + *
410 + * This function sees what's included and populates this.options.colors with whatever's required
411 + * for this pie chart.
412 + * @param data
413 + */
414 + initSegmentColors: function(pie) {
415 + var data = pie.options.data.content;
416 + var colors = pie.options.misc.colors.segments;
417 +
418 + // TODO this needs a ton of error handling
419 +
420 + var finalColors = [];
421 + for (var i=0; i<data.length; i++) {
422 + if (data[i].hasOwnProperty("color")) {
423 + finalColors.push(data[i].color);
424 + } else {
425 + finalColors.push(colors[i]);
426 + }
427 + }
428 +
429 + return finalColors;
430 + },
431 +
432 + applySmallSegmentGrouping: function(data, smallSegmentGrouping) {
433 + var totalSize;
434 + if (smallSegmentGrouping.valueType === "percentage") {
435 + totalSize = math.getTotalPieSize(data);
436 + }
437 +
438 + // loop through each data item
439 + var newData = [];
440 + var groupedData = [];
441 + var totalGroupedData = 0;
442 + for (var i=0; i<data.length; i++) {
443 + if (smallSegmentGrouping.valueType === "percentage") {
444 + var dataPercent = (data[i].value / totalSize) * 100;
445 + if (dataPercent <= smallSegmentGrouping.value) {
446 + groupedData.push(data[i]);
447 + totalGroupedData += data[i].value;
448 + continue;
449 + }
450 + data[i].isGrouped = false;
451 + newData.push(data[i]);
452 + } else {
453 + if (data[i].value <= smallSegmentGrouping.value) {
454 + groupedData.push(data[i]);
455 + totalGroupedData += data[i].value;
456 + continue;
457 + }
458 + data[i].isGrouped = false;
459 + newData.push(data[i]);
460 + }
461 + }
462 +
463 + // we're done! See if there's any small segment groups to add
464 + if (groupedData.length) {
465 + newData.push({
466 + color: smallSegmentGrouping.color,
467 + label: smallSegmentGrouping.label,
468 + value: totalGroupedData,
469 + isGrouped: true,
470 + groupedData: groupedData
471 + });
472 + }
473 +
474 + return newData;
475 + },
476 +
477 + // for debugging
478 + showPoint: function(svg, x, y) {
479 + svg.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).style("fill", "black");
480 + },
481 +
482 + isFunction: function(functionToCheck) {
483 + var getType = {};
484 + return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
485 + },
486 +
487 + isArray: function(o) {
488 + return Object.prototype.toString.call(o) === '[object Array]';
489 + }
490 +};
491 +
492 +
493 +// taken from jQuery
494 +var extend = function() {
495 + var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
496 + i = 1,
497 + length = arguments.length,
498 + deep = false,
499 + toString = Object.prototype.toString,
500 + hasOwn = Object.prototype.hasOwnProperty,
501 + class2type = {
502 + "[object Boolean]": "boolean",
503 + "[object Number]": "number",
504 + "[object String]": "string",
505 + "[object Function]": "function",
506 + "[object Array]": "array",
507 + "[object Date]": "date",
508 + "[object RegExp]": "regexp",
509 + "[object Object]": "object"
510 + },
511 +
512 + jQuery = {
513 + isFunction: function (obj) {
514 + return jQuery.type(obj) === "function";
515 + },
516 + isArray: Array.isArray ||
517 + function (obj) {
518 + return jQuery.type(obj) === "array";
519 + },
520 + isWindow: function (obj) {
521 + return obj !== null && obj === obj.window;
522 + },
523 + isNumeric: function (obj) {
524 + return !isNaN(parseFloat(obj)) && isFinite(obj);
525 + },
526 + type: function (obj) {
527 + return obj === null ? String(obj) : class2type[toString.call(obj)] || "object";
528 + },
529 + isPlainObject: function (obj) {
530 + if (!obj || jQuery.type(obj) !== "object" || obj.nodeType) {
531 + return false;
532 + }
533 + try {
534 + if (obj.constructor && !hasOwn.call(obj, "constructor") && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) {
535 + return false;
536 + }
537 + } catch (e) {
538 + return false;
539 + }
540 + var key;
541 + for (key in obj) {}
542 + return key === undefined || hasOwn.call(obj, key);
543 + }
544 + };
545 + if (typeof target === "boolean") {
546 + deep = target;
547 + target = arguments[1] || {};
548 + i = 2;
549 + }
550 + if (typeof target !== "object" && !jQuery.isFunction(target)) {
551 + target = {};
552 + }
553 + if (length === i) {
554 + target = this;
555 + --i;
556 + }
557 + for (i; i < length; i++) {
558 + if ((options = arguments[i]) !== null) {
559 + for (name in options) {
560 + src = target[name];
561 + copy = options[name];
562 + if (target === copy) {
563 + continue;
564 + }
565 + if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
566 + if (copyIsArray) {
567 + copyIsArray = false;
568 + clone = src && jQuery.isArray(src) ? src : [];
569 + } else {
570 + clone = src && jQuery.isPlainObject(src) ? src : {};
571 + }
572 + // WARNING: RECURSION
573 + target[name] = extend(deep, clone, copy);
574 + } else if (copy !== undefined) {
575 + target[name] = copy;
576 + }
577 + }
578 + }
579 + }
580 + return target;
581 +};
582 + //// --------- math.js -----------
583 +var math = {
584 +
585 + toRadians: function(degrees) {
586 + return degrees * (Math.PI / 180);
587 + },
588 +
589 + toDegrees: function(radians) {
590 + return radians * (180 / Math.PI);
591 + },
592 +
593 + computePieRadius: function(pie) {
594 + var size = pie.options.size;
595 + var canvasPadding = pie.options.misc.canvasPadding;
596 +
597 + // outer radius is either specified (e.g. through the generator), or omitted altogether
598 + // and calculated based on the canvas dimensions. Right now the estimated version isn't great - it should
599 + // be possible to calculate it to precisely generate the maximum sized pie, but it's fussy as heck. Something
600 + // for the next release.
601 +
602 + // first, calculate the default _outerRadius
603 + var w = size.canvasWidth - canvasPadding.left - canvasPadding.right;
604 + var h = size.canvasHeight - canvasPadding.top - canvasPadding.bottom;
605 +
606 + // now factor in the footer, title & subtitle
607 + if (pie.options.header.location !== "pie-center") {
608 + h -= pie.textComponents.headerHeight;
609 + }
610 +
611 + if (pie.textComponents.footer.exists) {
612 + h -= pie.textComponents.footer.h;
613 + }
614 +
615 + // for really teeny pies, h may be < 0. Adjust it back
616 + h = (h < 0) ? 0 : h;
617 +
618 + var outerRadius = ((w < h) ? w : h) / 3;
619 + var innerRadius, percent;
620 +
621 + // if the user specified something, use that instead
622 + if (size.pieOuterRadius !== null) {
623 + if (/%/.test(size.pieOuterRadius)) {
624 + percent = parseInt(size.pieOuterRadius.replace(/[\D]/, ""), 10);
625 + percent = (percent > 99) ? 99 : percent;
626 + percent = (percent < 0) ? 0 : percent;
627 +
628 + var smallestDimension = (w < h) ? w : h;
629 +
630 + // now factor in the label line size
631 + if (pie.options.labels.outer.format !== "none") {
632 + var pieDistanceSpace = parseInt(pie.options.labels.outer.pieDistance, 10) * 2;
633 + if (smallestDimension - pieDistanceSpace > 0) {
634 + smallestDimension -= pieDistanceSpace;
635 + }
636 + }
637 +
638 + outerRadius = Math.floor((smallestDimension / 100) * percent) / 2;
639 + } else {
640 + outerRadius = parseInt(size.pieOuterRadius, 10);
641 + }
642 + }
643 +
644 + // inner radius
645 + if (/%/.test(size.pieInnerRadius)) {
646 + percent = parseInt(size.pieInnerRadius.replace(/[\D]/, ""), 10);
647 + percent = (percent > 99) ? 99 : percent;
648 + percent = (percent < 0) ? 0 : percent;
649 + innerRadius = Math.floor((outerRadius / 100) * percent);
650 + } else {
651 + innerRadius = parseInt(size.pieInnerRadius, 10);
652 + }
653 +
654 + pie.innerRadius = innerRadius;
655 + pie.outerRadius = outerRadius;
656 + },
657 +
658 + getTotalPieSize: function(data) {
659 + var totalSize = 0;
660 + for (var i=0; i<data.length; i++) {
661 + totalSize += data[i].value;
662 + }
663 + return totalSize;
664 + },
665 +
666 + sortPieData: function(pie) {
667 + var data = pie.options.data.content;
668 + var sortOrder = pie.options.data.sortOrder;
669 +
670 + switch (sortOrder) {
671 + case "none":
672 + // do nothing
673 + break;
674 + case "random":
675 + data = helpers.shuffleArray(data);
676 + break;
677 + case "value-asc":
678 + data.sort(function(a, b) { return (a.value < b.value) ? -1 : 1; });
679 + break;
680 + case "value-desc":
681 + data.sort(function(a, b) { return (a.value < b.value) ? 1 : -1; });
682 + break;
683 + case "label-asc":
684 + data.sort(function(a, b) { return (a.label.toLowerCase() > b.label.toLowerCase()) ? 1 : -1; });
685 + break;
686 + case "label-desc":
687 + data.sort(function(a, b) { return (a.label.toLowerCase() < b.label.toLowerCase()) ? 1 : -1; });
688 + break;
689 + }
690 +
691 + return data;
692 + },
693 +
694 + // var pieCenter = math.getPieCenter();
695 + getPieTranslateCenter: function(pieCenter) {
696 + return "translate(" + pieCenter.x + "," + pieCenter.y + ")";
697 + },
698 +
699 + /**
700 + * Used to determine where on the canvas the center of the pie chart should be. It takes into account the
701 + * height and position of the title, subtitle and footer, and the various paddings.
702 + * @private
703 + */
704 + calculatePieCenter: function(pie) {
705 + var pieCenterOffset = pie.options.misc.pieCenterOffset;
706 + var hasTopTitle = (pie.textComponents.title.exists && pie.options.header.location !== "pie-center");
707 + var hasTopSubtitle = (pie.textComponents.subtitle.exists && pie.options.header.location !== "pie-center");
708 +
709 + var headerOffset = pie.options.misc.canvasPadding.top;
710 + if (hasTopTitle && hasTopSubtitle) {
711 + headerOffset += pie.textComponents.title.h + pie.options.header.titleSubtitlePadding + pie.textComponents.subtitle.h;
712 + } else if (hasTopTitle) {
713 + headerOffset += pie.textComponents.title.h;
714 + } else if (hasTopSubtitle) {
715 + headerOffset += pie.textComponents.subtitle.h;
716 + }
717 +
718 + var footerOffset = 0;
719 + if (pie.textComponents.footer.exists) {
720 + footerOffset = pie.textComponents.footer.h + pie.options.misc.canvasPadding.bottom;
721 + }
722 +
723 + var x = ((pie.options.size.canvasWidth - pie.options.misc.canvasPadding.left - pie.options.misc.canvasPadding.right) / 2) + pie.options.misc.canvasPadding.left;
724 + var y = ((pie.options.size.canvasHeight - footerOffset - headerOffset) / 2) + headerOffset;
725 +
726 + x += pieCenterOffset.x;
727 + y += pieCenterOffset.y;
728 +
729 + pie.pieCenter = { x: x, y: y };
730 + },
731 +
732 +
733 + /**
734 + * Rotates a point (x, y) around an axis (xm, ym) by degrees (a).
735 + * @param x
736 + * @param y
737 + * @param xm
738 + * @param ym
739 + * @param a angle in degrees
740 + * @returns {Array}
741 + */
742 + rotate: function(x, y, xm, ym, a) {
743 +
744 + a = a * Math.PI / 180; // convert to radians
745 +
746 + var cos = Math.cos,
747 + sin = Math.sin,
748 + // subtract midpoints, so that midpoint is translated to origin and add it in the end again
749 + xr = (x - xm) * cos(a) - (y - ym) * sin(a) + xm,
750 + yr = (x - xm) * sin(a) + (y - ym) * cos(a) + ym;
751 +
752 + return { x: xr, y: yr };
753 + },
754 +
755 + /**
756 + * Translates a point x, y by distance d, and by angle a.
757 + * @param x
758 + * @param y
759 + * @param dist
760 + * @param a angle in degrees
761 + */
762 + translate: function(x, y, d, a) {
763 + var rads = math.toRadians(a);
764 + return {
765 + x: x + d * Math.sin(rads),
766 + y: y - d * Math.cos(rads)
767 + };
768 + },
769 +
770 + // from: http://stackoverflow.com/questions/19792552/d3-put-arc-labels-in-a-pie-chart-if-there-is-enough-space
771 + pointIsInArc: function(pt, ptData, d3Arc) {
772 + // Center of the arc is assumed to be 0,0
773 + // (pt.x, pt.y) are assumed to be relative to the center
774 + var r1 = d3Arc.innerRadius()(ptData), // Note: Using the innerRadius
775 + r2 = d3Arc.outerRadius()(ptData),
776 + theta1 = d3Arc.startAngle()(ptData),
777 + theta2 = d3Arc.endAngle()(ptData);
778 +
779 + var dist = pt.x * pt.x + pt.y * pt.y,
780 + angle = Math.atan2(pt.x, -pt.y); // Note: different coordinate system
781 +
782 + angle = (angle < 0) ? (angle + Math.PI * 2) : angle;
783 +
784 + return (r1 * r1 <= dist) && (dist <= r2 * r2) &&
785 + (theta1 <= angle) && (angle <= theta2);
786 + }
787 +};
788 +
789 + //// --------- labels.js -----------
790 +var labels = {
791 +
792 + /**
793 + * Adds the labels to the pie chart, but doesn't position them. There are two locations for the
794 + * labels: inside (center) of the segments, or outside the segments on the edge.
795 + * @param section "inner" or "outer"
796 + * @param sectionDisplayType "percentage", "value", "label", "label-value1", etc.
797 + * @param pie
798 + */
799 + add: function(pie, section, sectionDisplayType) {
800 + var include = labels.getIncludes(sectionDisplayType);
801 + var settings = pie.options.labels;
802 +
803 + // group the label groups (label, percentage, value) into a single element for simpler positioning
804 + var outerLabel = pie.svg.insert("g", "." + pie.cssPrefix + "labels-" + section)
805 + .attr("class", pie.cssPrefix + "labels-" + section);
806 +
807 + var labelGroup = outerLabel.selectAll("." + pie.cssPrefix + "labelGroup-" + section)
808 + .data(pie.options.data.content)
809 + .enter()
810 + .append("g")
811 + .attr("id", function(d, i) { return pie.cssPrefix + "labelGroup" + i + "-" + section; })
812 + .attr("data-index", function(d, i) { return i; })
813 + .attr("class", pie.cssPrefix + "labelGroup-" + section)
814 + .style("opacity", 0);
815 +
816 + var formatterContext = { section: section, sectionDisplayType: sectionDisplayType };
817 +
818 + // 1. Add the main label
819 + if (include.mainLabel) {
820 + labelGroup.append("text")
821 + .attr("id", function(d, i) { return pie.cssPrefix + "segmentMainLabel" + i + "-" + section; })
822 + .attr("class", pie.cssPrefix + "segmentMainLabel-" + section)
823 + .text(function(d, i) {
824 + var str = d.label;
825 +
826 + // if a custom formatter has been defined, pass it the raw label string - it can do whatever it wants with it.
827 + // we only apply truncation if it's not defined
828 + if (settings.formatter) {
829 + formatterContext.index = i;
830 + formatterContext.part = 'mainLabel';
831 + formatterContext.value = d.value;
832 + formatterContext.label = str;
833 + str = settings.formatter(formatterContext);
834 + } else if (settings.truncation.enabled && d.label.length > settings.truncation.truncateLength) {
835 + str = d.label.substring(0, settings.truncation.truncateLength) + "...";
836 + }
837 + return str;
838 + })
839 + .style("font-size", settings.mainLabel.fontSize + "px")
840 + .style("font-family", settings.mainLabel.font)
841 + .style("fill", settings.mainLabel.color);
842 + }
843 +
844 + // 2. Add the percentage label
845 + if (include.percentage) {
846 + labelGroup.append("text")
847 + .attr("id", function(d, i) { return pie.cssPrefix + "segmentPercentage" + i + "-" + section; })
848 + .attr("class", pie.cssPrefix + "segmentPercentage-" + section)
849 + .text(function(d, i) {
850 + var percentage = d.percentage;
851 + if (settings.formatter) {
852 + formatterContext.index = i;
853 + formatterContext.part = "percentage";
854 + formatterContext.value = d.value;
855 + formatterContext.label = d.percentage;
856 + percentage = settings.formatter(formatterContext);
857 + } else {
858 + percentage += "%";
859 + }
860 + return percentage;
861 + })
862 + .style("font-size", settings.percentage.fontSize + "px")
863 + .style("font-family", settings.percentage.font)
864 + .style("fill", settings.percentage.color);
865 + }
866 +
867 + // 3. Add the value label
868 + if (include.value) {
869 + labelGroup.append("text")
870 + .attr("id", function(d, i) { return pie.cssPrefix + "segmentValue" + i + "-" + section; })
871 + .attr("class", pie.cssPrefix + "segmentValue-" + section)
872 + .text(function(d, i) {
873 + formatterContext.index = i;
874 + formatterContext.part = "value";
875 + formatterContext.value = d.value;
876 + formatterContext.label = d.value;
877 + return settings.formatter ? settings.formatter(formatterContext, d.value) : d.value;
878 + })
879 + .style("font-size", settings.value.fontSize + "px")
880 + .style("font-family", settings.value.font)
881 + .style("fill", settings.value.color);
882 + }
883 + },
884 +
885 + /**
886 + * @param section "inner" / "outer"
887 + */
888 + positionLabelElements: function(pie, section, sectionDisplayType) {
889 + labels["dimensions-" + section] = [];
890 +
891 + // get the latest widths, heights
892 + var labelGroups = d3.selectAll("." + pie.cssPrefix + "labelGroup-" + section);
893 + labelGroups.each(function(d, i) {
894 + var mainLabel = d3.select(this).selectAll("." + pie.cssPrefix + "segmentMainLabel-" + section);
895 + var percentage = d3.select(this).selectAll("." + pie.cssPrefix + "segmentPercentage-" + section);
896 + var value = d3.select(this).selectAll("." + pie.cssPrefix + "segmentValue-" + section);
897 +
898 + labels["dimensions-" + section].push({
899 + mainLabel: (mainLabel.node() !== null) ? mainLabel.node().getBBox() : null,
900 + percentage: (percentage.node() !== null) ? percentage.node().getBBox() : null,
901 + value: (value.node() !== null) ? value.node().getBBox() : null
902 + });
903 + });
904 +
905 + var singleLinePad = 5;
906 + var dims = labels["dimensions-" + section];
907 + switch (sectionDisplayType) {
908 + case "label-value1":
909 + d3.selectAll("." + pie.cssPrefix + "segmentValue-" + section)
910 + .attr("dx", function(d, i) { return dims[i].mainLabel.width + singleLinePad; });
911 + break;
912 + case "label-value2":
913 + d3.selectAll("." + pie.cssPrefix + "segmentValue-" + section)
914 + .attr("dy", function(d, i) { return dims[i].mainLabel.height; });
915 + break;
916 + case "label-percentage1":
917 + d3.selectAll("." + pie.cssPrefix + "segmentPercentage-" + section)
918 + .attr("dx", function(d, i) { return dims[i].mainLabel.width + singleLinePad; });
919 + break;
920 + case "label-percentage2":
921 + d3.selectAll("." + pie.cssPrefix + "segmentPercentage-" + section)
922 + .attr("dx", function(d, i) { return (dims[i].mainLabel.width / 2) - (dims[i].percentage.width / 2); })
923 + .attr("dy", function(d, i) { return dims[i].mainLabel.height; });
924 + break;
925 + }
926 + },
927 +
928 + computeLabelLinePositions: function(pie) {
929 + pie.lineCoordGroups = [];
930 + d3.selectAll("." + pie.cssPrefix + "labelGroup-outer")
931 + .each(function(d, i) { return labels.computeLinePosition(pie, i); });
932 + },
933 +
934 + computeLinePosition: function(pie, i) {
935 + var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true });
936 + var originCoords = math.rotate(pie.pieCenter.x, pie.pieCenter.y - pie.outerRadius, pie.pieCenter.x, pie.pieCenter.y, angle);
937 + var heightOffset = pie.outerLabelGroupData[i].h / 5; // TODO check
938 + var labelXMargin = 6; // the x-distance of the label from the end of the line [TODO configurable]
939 +
940 + var quarter = Math.floor(angle / 90);
941 + var midPoint = 4;
942 + var x2, y2, x3, y3;
943 +
944 + // this resolves an issue when the
945 + if (quarter === 2 && angle === 180) {
946 + quarter = 1;
947 + }
948 +
949 + switch (quarter) {
950 + case 0:
951 + x2 = pie.outerLabelGroupData[i].x - labelXMargin - ((pie.outerLabelGroupData[i].x - labelXMargin - originCoords.x) / 2);
952 + y2 = pie.outerLabelGroupData[i].y + ((originCoords.y - pie.outerLabelGroupData[i].y) / midPoint);
953 + x3 = pie.outerLabelGroupData[i].x - labelXMargin;
954 + y3 = pie.outerLabelGroupData[i].y - heightOffset;
955 + break;
956 + case 1:
957 + x2 = originCoords.x + (pie.outerLabelGroupData[i].x - originCoords.x) / midPoint;
958 + y2 = originCoords.y + (pie.outerLabelGroupData[i].y - originCoords.y) / midPoint;
959 + x3 = pie.outerLabelGroupData[i].x - labelXMargin;
960 + y3 = pie.outerLabelGroupData[i].y - heightOffset;
961 + break;
962 + case 2:
963 + var startOfLabelX = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin;
964 + x2 = originCoords.x - (originCoords.x - startOfLabelX) / midPoint;
965 + y2 = originCoords.y + (pie.outerLabelGroupData[i].y - originCoords.y) / midPoint;
966 + x3 = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin;
967 + y3 = pie.outerLabelGroupData[i].y - heightOffset;
968 + break;
969 + case 3:
970 + var startOfLabel = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin;
971 + x2 = startOfLabel + ((originCoords.x - startOfLabel) / midPoint);
972 + y2 = pie.outerLabelGroupData[i].y + (originCoords.y - pie.outerLabelGroupData[i].y) / midPoint;
973 + x3 = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin;
974 + y3 = pie.outerLabelGroupData[i].y - heightOffset;
975 + break;
976 + }
977 +
978 + /*
979 + * x1 / y1: the x/y coords of the start of the line, at the mid point of the segments arc on the pie circumference
980 + * x2 / y2: if "curved" line style is being used, this is the midpoint of the line. Other
981 + * x3 / y3: the end of the line; closest point to the label
982 + */
983 + if (pie.options.labels.lines.style === "straight") {
984 + pie.lineCoordGroups[i] = [
985 + { x: originCoords.x, y: originCoords.y },
986 + { x: x3, y: y3 }
987 + ];
988 + } else {
989 + pie.lineCoordGroups[i] = [
990 + { x: originCoords.x, y: originCoords.y },
991 + { x: x2, y: y2 },
992 + { x: x3, y: y3 }
993 + ];
994 + }
995 + },
996 +
997 + addLabelLines: function(pie) {
998 + var lineGroups = pie.svg.insert("g", "." + pie.cssPrefix + "pieChart") // meaning, BEFORE .pieChart
999 + .attr("class", pie.cssPrefix + "lineGroups")
1000 + .style("opacity", 0);
1001 +
1002 + var lineGroup = lineGroups.selectAll("." + pie.cssPrefix + "lineGroup")
1003 + .data(pie.lineCoordGroups)
1004 + .enter()
1005 + .append("g")
1006 + .attr("class", pie.cssPrefix + "lineGroup");
1007 +
1008 + var lineFunction = d3.line()
1009 + .curve(d3.curveBasis)
1010 + .x(function(d) { return d.x; })
1011 + .y(function(d) { return d.y; });
1012 +
1013 + lineGroup.append("path")
1014 + .attr("d", lineFunction)
1015 + .attr("stroke", function(d, i) {
1016 + return (pie.options.labels.lines.color === "segment") ? pie.options.colors[i] : pie.options.labels.lines.color;
1017 + })
1018 + .attr("stroke-width", 1)
1019 + .attr("fill", "none")
1020 + .style("opacity", function(d, i) {
1021 + var percentage = pie.options.labels.outer.hideWhenLessThanPercentage;
1022 + var isHidden = (percentage !== null && d.percentage < percentage) || pie.options.data.content[i].label === "";
1023 + return isHidden ? 0 : 1;
1024 + });
1025 + },
1026 +
1027 + positionLabelGroups: function(pie, section) {
1028 + if (pie.options.labels[section].format === "none") {
1029 + return;
1030 + }
1031 +
1032 + d3.selectAll("." + pie.cssPrefix + "labelGroup-" + section)
1033 + .style("opacity", 0)
1034 + .attr("transform", function(d, i) {
1035 + var x, y;
1036 + if (section === "outer") {
1037 + x = pie.outerLabelGroupData[i].x;
1038 + y = pie.outerLabelGroupData[i].y;
1039 + } else {
1040 + var pieCenterCopy = extend(true, {}, pie.pieCenter);
1041 +
1042 + // now recompute the "center" based on the current _innerRadius
1043 + if (pie.innerRadius > 0) {
1044 + var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true });
1045 + var newCoords = math.translate(pie.pieCenter.x, pie.pieCenter.y, pie.innerRadius, angle);
1046 + pieCenterCopy.x = newCoords.x;
1047 + pieCenterCopy.y = newCoords.y;
1048 + }
1049 +
1050 + var dims = helpers.getDimensions(pie.cssPrefix + "labelGroup" + i + "-inner");
1051 + var xOffset = dims.w / 2;
1052 + var yOffset = dims.h / 4; // confusing! Why 4? should be 2, but it doesn't look right
1053 +
1054 + x = pieCenterCopy.x + (pie.lineCoordGroups[i][0].x - pieCenterCopy.x) / 1.8;
1055 + y = pieCenterCopy.y + (pie.lineCoordGroups[i][0].y - pieCenterCopy.y) / 1.8;
1056 +
1057 + x = x - xOffset;
1058 + y = y + yOffset;
1059 + }
1060 +
1061 + return "translate(" + x + "," + y + ")";
1062 + });
1063 + },
1064 +
1065 +
1066 + fadeInLabelsAndLines: function(pie) {
1067 +
1068 + // fade in the labels when the load effect is complete - or immediately if there's no load effect
1069 + var loadSpeed = (pie.options.effects.load.effect === "default") ? pie.options.effects.load.speed : 1;
1070 + //setTimeout(function() {
1071 + var labelFadeInTime = (pie.options.effects.load.effect === "default") ? 400 : 1; // 400 is hardcoded for the present
1072 +
1073 + d3.selectAll("." + pie.cssPrefix + "labelGroup-outer")
1074 + //.transition()
1075 + //.duration(labelFadeInTime)
1076 + .style("opacity", function(d, i) {
1077 + var percentage = pie.options.labels.outer.hideWhenLessThanPercentage;
1078 + return (percentage !== null && d.percentage < percentage) ? 0 : 1;
1079 + });
1080 +
1081 + d3.selectAll("." + pie.cssPrefix + "labelGroup-inner")
1082 + //.transition()
1083 + //.duration(labelFadeInTime)
1084 + .style("opacity", function(d, i) {
1085 + var percentage = pie.options.labels.inner.hideWhenLessThanPercentage;
1086 + return (percentage !== null && d.percentage < percentage) ? 0 : 1;
1087 + });
1088 +
1089 + d3.selectAll("g." + pie.cssPrefix + "lineGroups")
1090 + //.transition()
1091 + //.duration(labelFadeInTime)
1092 + .style("opacity", 1);
1093 +
1094 + // once everything's done loading, trigger the onload callback if defined
1095 + if (helpers.isFunction(pie.options.callbacks.onload)) {
1096 + //setTimeout(function() {
1097 + try {
1098 + pie.options.callbacks.onload();
1099 + } catch (e) { }
1100 + //}, labelFadeInTime);
1101 + }
1102 + //}, loadSpeed);
1103 + },
1104 +
1105 + getIncludes: function(val) {
1106 + var addMainLabel = false;
1107 + var addValue = false;
1108 + var addPercentage = false;
1109 +
1110 + switch (val) {
1111 + case "label":
1112 + addMainLabel = true;
1113 + break;
1114 + case "value":
1115 + addValue = true;
1116 + break;
1117 + case "percentage":
1118 + addPercentage = true;
1119 + break;
1120 + case "label-value1":
1121 + case "label-value2":
1122 + addMainLabel = true;
1123 + addValue = true;
1124 + break;
1125 + case "label-percentage1":
1126 + case "label-percentage2":
1127 + addMainLabel = true;
1128 + addPercentage = true;
1129 + break;
1130 + }
1131 + return {
1132 + mainLabel: addMainLabel,
1133 + value: addValue,
1134 + percentage: addPercentage
1135 + };
1136 + },
1137 +
1138 +
1139 + /**
1140 + * This does the heavy-lifting to compute the actual coordinates for the outer label groups. It does two things:
1141 + * 1. Make a first pass and position them in the ideal positions, based on the pie sizes
1142 + * 2. Do some basic collision avoidance.
1143 + */
1144 + computeOuterLabelCoords: function(pie) {
1145 +
1146 + // 1. figure out the ideal positions for the outer labels
1147 + pie.svg.selectAll("." + pie.cssPrefix + "labelGroup-outer")
1148 + .each(function(d, i) {
1149 + return labels.getIdealOuterLabelPositions(pie, i);
1150 + });
1151 +
1152 + // 2. now adjust those positions to try to accommodate conflicts
1153 + labels.resolveOuterLabelCollisions(pie);
1154 + },
1155 +
1156 + /**
1157 + * This attempts to resolve label positioning collisions.
1158 + */
1159 + resolveOuterLabelCollisions: function(pie) {
1160 + if (pie.options.labels.outer.format === "none") {
1161 + return;
1162 + }
1163 +
1164 + var size = pie.options.data.content.length;
1165 + labels.checkConflict(pie, 0, "clockwise", size);
1166 + labels.checkConflict(pie, size-1, "anticlockwise", size);
1167 + },
1168 +
1169 + checkConflict: function(pie, currIndex, direction, size) {
1170 + var i, curr;
1171 +
1172 + if (size <= 1) {
1173 + return;
1174 + }
1175 +
1176 + var currIndexHemisphere = pie.outerLabelGroupData[currIndex].hs;
1177 + if (direction === "clockwise" && currIndexHemisphere !== "right") {
1178 + return;
1179 + }
1180 + if (direction === "anticlockwise" && currIndexHemisphere !== "left") {
1181 + return;
1182 + }
1183 + var nextIndex = (direction === "clockwise") ? currIndex+1 : currIndex-1;
1184 +
1185 + // this is the current label group being looked at. We KNOW it's positioned properly (the first item
1186 + // is always correct)
1187 + var currLabelGroup = pie.outerLabelGroupData[currIndex];
1188 +
1189 + // this one we don't know about. That's the one we're going to look at and move if necessary
1190 + var examinedLabelGroup = pie.outerLabelGroupData[nextIndex];
1191 +
1192 + var info = {
1193 + labelHeights: pie.outerLabelGroupData[0].h,
1194 + center: pie.pieCenter,
1195 + lineLength: (pie.outerRadius + pie.options.labels.outer.pieDistance),
1196 + heightChange: pie.outerLabelGroupData[0].h + 1 // 1 = padding
1197 + };
1198 +
1199 + // loop through *ALL* label groups examined so far to check for conflicts. This is because when they're
1200 + // very tightly fitted, a later label group may still appear high up on the page
1201 + if (direction === "clockwise") {
1202 + i = 0;
1203 + for (; i<=currIndex; i++) {
1204 + curr = pie.outerLabelGroupData[i];
1205 +
1206 + // if there's a conflict with this label group, shift the label to be AFTER the last known
1207 + // one that's been properly placed
1208 + if (!labels.isLabelHidden(pie, i) && helpers.rectIntersect(curr, examinedLabelGroup)) {
1209 + labels.adjustLabelPos(pie, nextIndex, currLabelGroup, info);
1210 + break;
1211 + }
1212 + }
1213 + } else {
1214 + i = size - 1;
1215 + for (; i >= currIndex; i--) {
1216 + curr = pie.outerLabelGroupData[i];
1217 +
1218 + // if there's a conflict with this label group, shift the label to be AFTER the last known
1219 + // one that's been properly placed
1220 + if (!labels.isLabelHidden(pie, i) && helpers.rectIntersect(curr, examinedLabelGroup)) {
1221 + labels.adjustLabelPos(pie, nextIndex, currLabelGroup, info);
1222 + break;
1223 + }
1224 + }
1225 + }
1226 + labels.checkConflict(pie, nextIndex, direction, size);
1227 + },
1228 +
1229 + isLabelHidden: function(pie, index) {
1230 + var percentage = pie.options.labels.outer.hideWhenLessThanPercentage;
1231 + return (percentage !== null && d.percentage < percentage) || pie.options.data.content[index].label === "";
1232 + },
1233 +
1234 + // does a little math to shift a label into a new position based on the last properly placed one
1235 + adjustLabelPos: function(pie, nextIndex, lastCorrectlyPositionedLabel, info) {
1236 + var xDiff, yDiff, newXPos, newYPos;
1237 + newYPos = lastCorrectlyPositionedLabel.y + info.heightChange;
1238 + yDiff = info.center.y - newYPos;
1239 +
1240 + if (Math.abs(info.lineLength) > Math.abs(yDiff)) {
1241 + xDiff = Math.sqrt((info.lineLength * info.lineLength) - (yDiff * yDiff));
1242 + } else {
1243 + xDiff = Math.sqrt((yDiff * yDiff) - (info.lineLength * info.lineLength));
1244 + }
1245 +
1246 + if (lastCorrectlyPositionedLabel.hs === "right") {
1247 + newXPos = info.center.x + xDiff;
1248 + } else {
1249 + newXPos = info.center.x - xDiff - pie.outerLabelGroupData[nextIndex].w;
1250 + }
1251 +
1252 + pie.outerLabelGroupData[nextIndex].x = newXPos;
1253 + pie.outerLabelGroupData[nextIndex].y = newYPos;
1254 + },
1255 +
1256 + /**
1257 + * @param i 0-N where N is the dataset size - 1.
1258 + */
1259 + getIdealOuterLabelPositions: function(pie, i) {
1260 + var labelGroupNode = d3.select("#" + pie.cssPrefix + "labelGroup" + i + "-outer").node();
1261 + if (!labelGroupNode) {
1262 + return;
1263 + }
1264 + var labelGroupDims = labelGroupNode.getBBox();
1265 + var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true });
1266 +
1267 + var originalX = pie.pieCenter.x;
1268 + var originalY = pie.pieCenter.y - (pie.outerRadius + pie.options.labels.outer.pieDistance);
1269 + var newCoords = math.rotate(originalX, originalY, pie.pieCenter.x, pie.pieCenter.y, angle);
1270 +
1271 + // if the label is on the left half of the pie, adjust the values
1272 + var hemisphere = "right"; // hemisphere
1273 + if (angle > 180) {
1274 + newCoords.x -= (labelGroupDims.width + 8);
1275 + hemisphere = "left";
1276 + } else {
1277 + newCoords.x += 8;
1278 + }
1279 +
1280 + pie.outerLabelGroupData[i] = {
1281 + x: newCoords.x,
1282 + y: newCoords.y,
1283 + w: labelGroupDims.width,
1284 + h: labelGroupDims.height,
1285 + hs: hemisphere
1286 + };
1287 + }
1288 +};
1289 +
1290 + //// --------- segments.js -----------
1291 +var segments = {
1292 +
1293 + effectMap: {
1294 + "none": d3.easeLinear,
1295 + "bounce": d3.easeBounce,
1296 + "linear": d3.easeLinear,
1297 + "sin": d3.easeSin,
1298 + "elastic": d3.easeElastic,
1299 + "back": d3.easeBack,
1300 + "quad": d3.easeQuad,
1301 + "circle": d3.easeCircle,
1302 + "exp": d3.easeExp
1303 + },
1304 +
1305 + /**
1306 + * Creates the pie chart segments and displays them according to the desired load effect.
1307 + * @private
1308 + */
1309 + create: function(pie) {
1310 + var pieCenter = pie.pieCenter;
1311 + var colors = pie.options.colors;
1312 + var loadEffects = pie.options.effects.load;
1313 + var segmentStroke = pie.options.misc.colors.segmentStroke;
1314 +
1315 + // we insert the pie chart BEFORE the title, to ensure the title overlaps the pie
1316 + var pieChartElement = pie.svg.insert("g", "#" + pie.cssPrefix + "title")
1317 + .attr("transform", function() { return math.getPieTranslateCenter(pieCenter); })
1318 + .attr("class", pie.cssPrefix + "pieChart");
1319 +
1320 + var arc = d3.arc()
1321 + .innerRadius(pie.innerRadius)
1322 + .outerRadius(pie.outerRadius)
1323 + .startAngle(0)
1324 + .endAngle(function(d) {
1325 + return (d.value / pie.totalSize) * 2 * Math.PI;
1326 + });
1327 +
1328 + var g = pieChartElement.selectAll("." + pie.cssPrefix + "arc")
1329 + .data(pie.options.data.content)
1330 + .enter()
1331 + .append("g")
1332 + .attr("class", pie.cssPrefix + "arc");
1333 +
1334 + // if we're not fading in the pie, just set the load speed to 0
1335 + var loadSpeed = loadEffects.speed;
1336 + if (loadEffects.effect === "none") {
1337 + loadSpeed = 0;
1338 + }
1339 +
1340 + g.append("path")
1341 + .attr("id", function(d, i) { return pie.cssPrefix + "segment" + i; })
1342 + .attr("fill", function(d, i) {
1343 + var color = colors[i];
1344 + if (pie.options.misc.gradient.enabled) {
1345 + color = "url(#" + pie.cssPrefix + "grad" + i + ")";
1346 + }
1347 + return color;
1348 + })
1349 + .style("stroke", segmentStroke)
1350 + .style("stroke-width", 1)
1351 + //.transition()
1352 + //.ease(d3.easeCubicInOut)
1353 + //.duration(loadSpeed)
1354 + .attr("data-index", function(d, i) { return i; })
1355 + .attr("d", pie.arc);
1356 +/* function(b) {
1357 + //var i = d3.interpolate({ value: 0 }, b);
1358 + return function(t) {
1359 + console.log(b);
1360 + return pie.arc(i(t));
1361 + };
1362 + });
1363 +*/
1364 + pie.svg.selectAll("g." + pie.cssPrefix + "arc")
1365 + .attr("transform",
1366 + function(d, i) {
1367 + var angle = 0;
1368 + if (i > 0) {
1369 + angle = segments.getSegmentAngle(i-1, pie.options.data.content, pie.totalSize);
1370 + }
1371 + return "rotate(" + angle + ")";
1372 + }
1373 + );
1374 + pie.arc = arc;
1375 + },
1376 +
1377 + addGradients: function(pie) {
1378 + var grads = pie.svg.append("defs")
1379 + .selectAll("radialGradient")
1380 + .data(pie.options.data.content)
1381 + .enter().append("radialGradient")
1382 + .attr("gradientUnits", "userSpaceOnUse")
1383 + .attr("cx", 0)
1384 + .attr("cy", 0)
1385 + .attr("r", "120%")
1386 + .attr("id", function(d, i) { return pie.cssPrefix + "grad" + i; });
1387 +
1388 + grads.append("stop").attr("offset", "0%").style("stop-color", function(d, i) { return pie.options.colors[i]; });
1389 + grads.append("stop").attr("offset", pie.options.misc.gradient.percentage + "%").style("stop-color", pie.options.misc.gradient.color);
1390 + },
1391 +
1392 + addSegmentEventHandlers: function(pie) {
1393 + var arc = d3.selectAll("." + pie.cssPrefix + "arc,." + pie.cssPrefix + "labelGroup-inner,." + pie.cssPrefix + "labelGroup-outer");
1394 +
1395 + arc.on("click", function() {
1396 + var currentEl = d3.select(this);
1397 + var segment;
1398 +
1399 + // mouseover works on both the segments AND the segment labels, hence the following
1400 + if (currentEl.attr("class") === pie.cssPrefix + "arc") {
1401 + segment = currentEl.select("path");
1402 + } else {
1403 + var index = currentEl.attr("data-index");
1404 + segment = d3.select("#" + pie.cssPrefix + "segment" + index);
1405 + }
1406 +
1407 + var isExpanded = segment.attr("class") === pie.cssPrefix + "expanded";
1408 + segments.onSegmentEvent(pie, pie.options.callbacks.onClickSegment, segment, isExpanded);
1409 + if (pie.options.effects.pullOutSegmentOnClick.effect !== "none") {
1410 + if (isExpanded) {
1411 + segments.closeSegment(pie, segment.node());
1412 + } else {
1413 + segments.openSegment(pie, segment.node());
1414 + }
1415 + }
1416 + });
1417 +
1418 + arc.on("mouseover", function() {
1419 + var currentEl = d3.select(this);
1420 + var segment, index;
1421 +
1422 + if (currentEl.attr("class") === pie.cssPrefix + "arc") {
1423 + segment = currentEl.select("path");
1424 + } else {
1425 + index = currentEl.attr("data-index");
1426 + segment = d3.select("#" + pie.cssPrefix + "segment" + index);
1427 + }
1428 +
1429 + if (pie.options.effects.highlightSegmentOnMouseover) {
1430 + index = segment.attr("data-index");
1431 + var segColor = pie.options.colors[index];
1432 + segment.style("fill", helpers.getColorShade(segColor, pie.options.effects.highlightLuminosity));
1433 + }
1434 +
1435 + if (pie.options.tooltips.enabled) {
1436 + index = segment.attr("data-index");
1437 + tt.showTooltip(pie, index);
1438 + }
1439 +
1440 + var isExpanded = segment.attr("class") === pie.cssPrefix + "expanded";
1441 + segments.onSegmentEvent(pie, pie.options.callbacks.onMouseoverSegment, segment, isExpanded);
1442 + });
1443 +
1444 + arc.on("mousemove", function() {
1445 + tt.moveTooltip(pie);
1446 + });
1447 +
1448 + arc.on("mouseout", function() {
1449 + var currentEl = d3.select(this);
1450 + var segment, index;
1451 +
1452 + if (currentEl.attr("class") === pie.cssPrefix + "arc") {
1453 + segment = currentEl.select("path");
1454 + } else {
1455 + index = currentEl.attr("data-index");
1456 + segment = d3.select("#" + pie.cssPrefix + "segment" + index);
1457 + }
1458 +
1459 + if (pie.options.effects.highlightSegmentOnMouseover) {
1460 + index = segment.attr("data-index");
1461 + var color = pie.options.colors[index];
1462 + if (pie.options.misc.gradient.enabled) {
1463 + color = "url(#" + pie.cssPrefix + "grad" + index + ")";
1464 + }
1465 + segment.style("fill", color);
1466 + }
1467 +
1468 + if (pie.options.tooltips.enabled) {
1469 + index = segment.attr("data-index");
1470 + tt.hideTooltip(pie, index);
1471 + }
1472 +
1473 + var isExpanded = segment.attr("class") === pie.cssPrefix + "expanded";
1474 + segments.onSegmentEvent(pie, pie.options.callbacks.onMouseoutSegment, segment, isExpanded);
1475 + });
1476 + },
1477 +
1478 + // helper function used to call the click, mouseover, mouseout segment callback functions
1479 + onSegmentEvent: function(pie, func, segment, isExpanded) {
1480 + if (!helpers.isFunction(func)) {
1481 + return;
1482 + }
1483 + var index = parseInt(segment.attr("data-index"), 10);
1484 + func({
1485 + segment: segment.node(),
1486 + index: index,
1487 + expanded: isExpanded,
1488 + data: pie.options.data.content[index]
1489 + });
1490 + },
1491 +
1492 + openSegment: function(pie, segment) {
1493 + if (pie.isOpeningSegment) {
1494 + return;
1495 + }
1496 + pie.isOpeningSegment = true;
1497 +
1498 + segments.maybeCloseOpenSegment();
1499 +
1500 + d3.select(segment)
1501 + //.transition()
1502 + //.ease(segments.effectMap[pie.options.effects.pullOutSegmentOnClick.effect])
1503 + //.duration(pie.options.effects.pullOutSegmentOnClick.speed)
1504 + .attr("transform", function(d, i) {
1505 + var c = pie.arc.centroid(d),
1506 + x = c[0],
1507 + y = c[1],
1508 + h = Math.sqrt(x*x + y*y),
1509 + pullOutSize = parseInt(pie.options.effects.pullOutSegmentOnClick.size, 10);
1510 +
1511 + return "translate(" + ((x/h) * pullOutSize) + ',' + ((y/h) * pullOutSize) + ")";
1512 + })
1513 + .on("end", function(d, i) {
1514 + pie.currentlyOpenSegment = segment;
1515 + pie.isOpeningSegment = false;
1516 + d3.select(segment).attr("class", pie.cssPrefix + "expanded");
1517 + });
1518 + },
1519 +
1520 + maybeCloseOpenSegment: function() {
1521 + if (d3.selectAll("." + pie.cssPrefix + "expanded").size() > 0) {
1522 + segments.closeSegment(pie, d3.select("." + pie.cssPrefix + "expanded").node());
1523 + }
1524 + },
1525 +
1526 + closeSegment: function(pie, segment) {
1527 + d3.select(segment)
1528 + //.transition()
1529 + //.duration(400)
1530 + .attr("transform", "translate(0,0)")
1531 + .on("end", function(d, i) {
1532 + d3.select(segment).attr("class", "");
1533 + pie.currentlyOpenSegment = null;
1534 + });
1535 + },
1536 +
1537 + getCentroid: function(el) {
1538 + var bbox = el.getBBox();
1539 + return {
1540 + x: bbox.x + bbox.width / 2,
1541 + y: bbox.y + bbox.height / 2
1542 + };
1543 + },
1544 +
1545 + /**
1546 + * General helper function to return a segment's angle, in various different ways.
1547 + * @param index
1548 + * @param opts optional object for fine-tuning exactly what you want.
1549 + */
1550 + getSegmentAngle: function(index, data, totalSize, opts) {
1551 + var options = extend({
1552 + // if true, this returns the full angle from the origin. Otherwise it returns the single segment angle
1553 + compounded: true,
1554 +
1555 + // optionally returns the midpoint of the angle instead of the full angle
1556 + midpoint: false
1557 + }, opts);
1558 +
1559 + var currValue = data[index].value;
1560 + var fullValue;
1561 + if (options.compounded) {
1562 + fullValue = 0;
1563 +
1564 + // get all values up to and including the specified index
1565 + for (var i=0; i<=index; i++) {
1566 + fullValue += data[i].value;
1567 + }
1568 + }
1569 +
1570 + if (typeof fullValue === 'undefined') {
1571 + fullValue = currValue;
1572 + }
1573 +
1574 + // now convert the full value to an angle
1575 + var angle = (fullValue / totalSize) * 360;
1576 +
1577 + // lastly, if we want the midpoint, factor that sucker in
1578 + if (options.midpoint) {
1579 + var currAngle = (currValue / totalSize) * 360;
1580 + angle -= (currAngle / 2);
1581 + }
1582 +
1583 + return angle;
1584 + }
1585 +
1586 +};
1587 +
1588 + //// --------- text.js -----------
1589 +var text = {
1590 + offscreenCoord: -10000,
1591 +
1592 + addTitle: function(pie) {
1593 +
1594 +
1595 + var title = pie.svg.selectAll("." + pie.cssPrefix + "title")
1596 + .data([pie.options.header.title])
1597 + .enter()
1598 + .append("text")
1599 + .text(function(d) { return d.text; })
1600 + .attr("id", pie.cssPrefix + "title")
1601 + .attr("class", pie.cssPrefix + "title")
1602 + .attr("x", text.offscreenCoord)
1603 + .attr("y", text.offscreenCoord)
1604 + .attr("text-anchor", function() {
1605 + var location;
1606 + if (pie.options.header.location === "top-center" || pie.options.header.location === "pie-center") {
1607 + location = "middle";
1608 + } else {
1609 + location = "left";
1610 + }
1611 + return location;
1612 + })
1613 + .attr("fill", function(d) { return d.color; })
1614 + .style("font-size", function(d) { return d.fontSize + "px"; })
1615 + .style("font-family", function(d) { return d.font; });
1616 + },
1617 +
1618 + positionTitle: function(pie) {
1619 + var textComponents = pie.textComponents;
1620 + var headerLocation = pie.options.header.location;
1621 + var canvasPadding = pie.options.misc.canvasPadding;
1622 + var canvasWidth = pie.options.size.canvasWidth;
1623 + var titleSubtitlePadding = pie.options.header.titleSubtitlePadding;
1624 +
1625 + var x;
1626 + if (headerLocation === "top-left") {
1627 + x = canvasPadding.left;
1628 + } else {
1629 + x = ((canvasWidth - canvasPadding.right) / 2) + canvasPadding.left;
1630 + }
1631 +
1632 + // add whatever offset has been added by user
1633 + x += pie.options.misc.pieCenterOffset.x;
1634 +
1635 + var y = canvasPadding.top + textComponents.title.h;
1636 +
1637 + if (headerLocation === "pie-center") {
1638 + y = pie.pieCenter.y;
1639 +
1640 + // still not fully correct
1641 + if (textComponents.subtitle.exists) {
1642 + var totalTitleHeight = textComponents.title.h + titleSubtitlePadding + textComponents.subtitle.h;
1643 + y = y - (totalTitleHeight / 2) + textComponents.title.h;
1644 + } else {
1645 + y += (textComponents.title.h / 4);
1646 + }
1647 + }
1648 +
1649 + pie.svg.select("#" + pie.cssPrefix + "title")
1650 + .attr("x", x)
1651 + .attr("y", y);
1652 + },
1653 +
1654 + addSubtitle: function(pie) {
1655 + var headerLocation = pie.options.header.location;
1656 +
1657 + pie.svg.selectAll("." + pie.cssPrefix + "subtitle")
1658 + .data([pie.options.header.subtitle])
1659 + .enter()
1660 + .append("text")
1661 + .text(function(d) { return d.text; })
1662 + .attr("x", text.offscreenCoord)
1663 + .attr("y", text.offscreenCoord)
1664 + .attr("id", pie.cssPrefix + "subtitle")
1665 + .attr("class", pie.cssPrefix + "subtitle")
1666 + .attr("text-anchor", function() {
1667 + var location;
1668 + if (headerLocation === "top-center" || headerLocation === "pie-center") {
1669 + location = "middle";
1670 + } else {
1671 + location = "left";
1672 + }
1673 + return location;
1674 + })
1675 + .attr("fill", function(d) { return d.color; })
1676 + .style("font-size", function(d) { return d.fontSize + "px"; })
1677 + .style("font-family", function(d) { return d.font; });
1678 + },
1679 +
1680 + positionSubtitle: function(pie) {
1681 + var canvasPadding = pie.options.misc.canvasPadding;
1682 + var canvasWidth = pie.options.size.canvasWidth;
1683 +
1684 + var x;
1685 + if (pie.options.header.location === "top-left") {
1686 + x = canvasPadding.left;
1687 + } else {
1688 + x = ((canvasWidth - canvasPadding.right) / 2) + canvasPadding.left;
1689 + }
1690 +
1691 + // add whatever offset has been added by user
1692 + x += pie.options.misc.pieCenterOffset.x;
1693 +
1694 + var y = text.getHeaderHeight(pie);
1695 + pie.svg.select("#" + pie.cssPrefix + "subtitle")
1696 + .attr("x", x)
1697 + .attr("y", y);
1698 + },
1699 +
1700 + addFooter: function(pie) {
1701 + pie.svg.selectAll("." + pie.cssPrefix + "footer")
1702 + .data([pie.options.footer])
1703 + .enter()
1704 + .append("text")
1705 + .text(function(d) { return d.text; })
1706 + .attr("x", text.offscreenCoord)
1707 + .attr("y", text.offscreenCoord)
1708 + .attr("id", pie.cssPrefix + "footer")
1709 + .attr("class", pie.cssPrefix + "footer")
1710 + .attr("text-anchor", function() {
1711 + var location = "left";
1712 + if (pie.options.footer.location === "bottom-center") {
1713 + location = "middle";
1714 + } else if (pie.options.footer.location === "bottom-right") {
1715 + location = "left"; // on purpose. We have to change the x-coord to make it properly right-aligned
1716 + }
1717 + return location;
1718 + })
1719 + .attr("fill", function(d) { return d.color; })
1720 + .style("font-size", function(d) { return d.fontSize + "px"; })
1721 + .style("font-family", function(d) { return d.font; });
1722 + },
1723 +
1724 + positionFooter: function(pie) {
1725 + var footerLocation = pie.options.footer.location;
1726 + var footerWidth = pie.textComponents.footer.w;
1727 + var canvasWidth = pie.options.size.canvasWidth;
1728 + var canvasHeight = pie.options.size.canvasHeight;
1729 + var canvasPadding = pie.options.misc.canvasPadding;
1730 +
1731 + var x;
1732 + if (footerLocation === "bottom-left") {
1733 + x = canvasPadding.left;
1734 + } else if (footerLocation === "bottom-right") {
1735 + x = canvasWidth - footerWidth - canvasPadding.right;
1736 + } else {
1737 + x = canvasWidth / 2; // TODO - shouldn't this also take into account padding?
1738 + }
1739 +
1740 + pie.svg.select("#" + pie.cssPrefix + "footer")
1741 + .attr("x", x)
1742 + .attr("y", canvasHeight - canvasPadding.bottom);
1743 + },
1744 +
1745 + getHeaderHeight: function(pie) {
1746 + var h;
1747 + if (pie.textComponents.title.exists) {
1748 +
1749 + // if the subtitle isn't defined, it'll be set to 0
1750 + var totalTitleHeight = pie.textComponents.title.h + pie.options.header.titleSubtitlePadding + pie.textComponents.subtitle.h;
1751 + if (pie.options.header.location === "pie-center") {
1752 + h = pie.pieCenter.y - (totalTitleHeight / 2) + totalTitleHeight;
1753 + } else {
1754 + h = totalTitleHeight + pie.options.misc.canvasPadding.top;
1755 + }
1756 + } else {
1757 + if (pie.options.header.location === "pie-center") {
1758 + var footerPlusPadding = pie.options.misc.canvasPadding.bottom + pie.textComponents.footer.h;
1759 + h = ((pie.options.size.canvasHeight - footerPlusPadding) / 2) + pie.options.misc.canvasPadding.top + (pie.textComponents.subtitle.h / 2);
1760 + } else {
1761 + h = pie.options.misc.canvasPadding.top + pie.textComponents.subtitle.h;
1762 + }
1763 + }
1764 + return h;
1765 + }
1766 +};
1767 +
1768 + //// --------- validate.js -----------
1769 +var tt = {
1770 + addTooltips: function(pie) {
1771 +
1772 + // group the label groups (label, percentage, value) into a single element for simpler positioning
1773 + var tooltips = pie.svg.insert("g")
1774 + .attr("class", pie.cssPrefix + "tooltips");
1775 +
1776 + tooltips.selectAll("." + pie.cssPrefix + "tooltip")
1777 + .data(pie.options.data.content)
1778 + .enter()
1779 + .append("g")
1780 + .attr("class", pie.cssPrefix + "tooltip")
1781 + .attr("id", function(d, i) { return pie.cssPrefix + "tooltip" + i; })
1782 + .style("opacity", 0)
1783 + .append("rect")
1784 + .attr("rx", pie.options.tooltips.styles.borderRadius)
1785 + .attr("ry", pie.options.tooltips.styles.borderRadius)
1786 + .attr("x", -pie.options.tooltips.styles.padding)
1787 + .attr("opacity", pie.options.tooltips.styles.backgroundOpacity)
1788 + .style("fill", pie.options.tooltips.styles.backgroundColor);
1789 +
1790 + tooltips.selectAll("." + pie.cssPrefix + "tooltip")
1791 + .data(pie.options.data.content)
1792 + .append("text")
1793 + .attr("fill", function(d) { return pie.options.tooltips.styles.color; })
1794 + .style("font-size", function(d) { return pie.options.tooltips.styles.fontSize; })
1795 + .style("font-family", function(d) { return pie.options.tooltips.styles.font; })
1796 + .text(function(d, i) {
1797 + var caption = pie.options.tooltips.string;
1798 + if (pie.options.tooltips.type === "caption") {
1799 + caption = d.caption;
1800 + }
1801 + return tt.replacePlaceholders(pie, caption, i, {
1802 + label: d.label,
1803 + value: d.value,
1804 + percentage: d.percentage
1805 + });
1806 + });
1807 +
1808 + tooltips.selectAll("." + pie.cssPrefix + "tooltip rect")
1809 + .attr("width", function (d, i) {
1810 + var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + i);
1811 + return dims.w + (2 * pie.options.tooltips.styles.padding);
1812 + })
1813 + .attr("height", function (d, i) {
1814 + var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + i);
1815 + return dims.h + (2 * pie.options.tooltips.styles.padding);
1816 + })
1817 + .attr("y", function (d, i) {
1818 + var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + i);
1819 + return -(dims.h / 2) + 1;
1820 + });
1821 + },
1822 +
1823 + showTooltip: function(pie, index) {
1824 + var fadeInSpeed = pie.options.tooltips.styles.fadeInSpeed;
1825 + if (tt.currentTooltip === index) {
1826 + fadeInSpeed = 1;
1827 + }
1828 +
1829 + tt.currentTooltip = index;
1830 + d3.select("#" + pie.cssPrefix + "tooltip" + index)
1831 + .transition()
1832 + .duration(fadeInSpeed)
1833 + .style("opacity", function() { return 1; });
1834 +
1835 + tt.moveTooltip(pie);
1836 + },
1837 +
1838 + moveTooltip: function(pie) {
1839 + d3.selectAll("#" + pie.cssPrefix + "tooltip" + tt.currentTooltip)
1840 + .attr("transform", function(d) {
1841 + var mouseCoords = d3.mouse(this.parentNode);
1842 + var x = mouseCoords[0] + pie.options.tooltips.styles.padding + 2;
1843 + var y = mouseCoords[1] - (2 * pie.options.tooltips.styles.padding) - 2;
1844 + return "translate(" + x + "," + y + ")";
1845 + });
1846 + },
1847 +
1848 + hideTooltip: function(pie, index) {
1849 + d3.select("#" + pie.cssPrefix + "tooltip" + index)
1850 + .style("opacity", function() { return 0; });
1851 +
1852 + // move the tooltip offscreen. This ensures that when the user next mouseovers the segment the hidden
1853 + // element won't interfere
1854 + d3.select("#" + pie.cssPrefix + "tooltip" + tt.currentTooltip)
1855 + .attr("transform", function(d, i) {
1856 + // klutzy, but it accounts for tooltip padding which could push it onscreen
1857 + var x = pie.options.size.canvasWidth + 1000;
1858 + var y = pie.options.size.canvasHeight + 1000;
1859 + return "translate(" + x + "," + y + ")";
1860 + });
1861 + },
1862 +
1863 + replacePlaceholders: function(pie, str, index, replacements) {
1864 +
1865 + // if the user has defined a placeholderParser function, call it before doing the replacements
1866 + if (helpers.isFunction(pie.options.tooltips.placeholderParser)) {
1867 + pie.options.tooltips.placeholderParser(index, replacements);
1868 + }
1869 +
1870 + var replacer = function() {
1871 + return function(match) {
1872 + var placeholder = arguments[1];
1873 + if (replacements.hasOwnProperty(placeholder)) {
1874 + return replacements[arguments[1]];
1875 + } else {
1876 + return arguments[0];
1877 + }
1878 + };
1879 + };
1880 + return str.replace(/\{(\w+)\}/g, replacer(replacements));
1881 + }
1882 +};
1883 +
1884 +
1885 + // --------------------------------------------------------------------------------------------
1886 +
1887 + // our constructor
1888 + var d3pie = function(element, options) {
1889 +
1890 + // element can be an ID or DOM element
1891 + this.element = element;
1892 + if (typeof element === "string") {
1893 + var el = element.replace(/^#/, ""); // replace any jQuery-like ID hash char
1894 + this.element = document.getElementById(el);
1895 + }
1896 +
1897 + var opts = {};
1898 + extend(true, opts, defaultSettings, options);
1899 + this.options = opts;
1900 +
1901 + // if the user specified a custom CSS element prefix (ID, class), use it
1902 + if (this.options.misc.cssPrefix !== null) {
1903 + this.cssPrefix = this.options.misc.cssPrefix;
1904 + } else {
1905 + this.cssPrefix = "p" + _uniqueIDCounter + "_";
1906 + _uniqueIDCounter++;
1907 + }
1908 +
1909 +
1910 + // now run some validation on the user-defined info
1911 + if (!validate.initialCheck(this)) {
1912 + return;
1913 + }
1914 +
1915 + // add a data-role to the DOM node to let anyone know that it contains a d3pie instance, and the d3pie version
1916 + d3.select(this.element).attr(_scriptName, _version);
1917 +
1918 + // things that are done once
1919 + _setupData.call(this);
1920 + _init.call(this);
1921 + };
1922 +
1923 + d3pie.prototype.recreate = function() {
1924 + // now run some validation on the user-defined info
1925 + if (!validate.initialCheck(this)) {
1926 + return;
1927 + }
1928 +
1929 + _setupData.call(this);
1930 + _init.call(this);
1931 + };
1932 +
1933 + d3pie.prototype.redraw = function() {
1934 + this.element.innerHTML = "";
1935 + _init.call(this);
1936 + };
1937 +
1938 + d3pie.prototype.destroy = function() {
1939 + this.element.innerHTML = ""; // clear out the SVG
1940 + d3.select(this.element).attr(_scriptName, null); // remove the data attr
1941 + };
1942 +
1943 + /**
1944 + * Returns all pertinent info about the current open info. Returns null if nothing's open, or if one is, an object of
1945 + * the following form:
1946 + * {
1947 + * element: DOM NODE,
1948 + * index: N,
1949 + * data: {}
1950 + * }
1951 + */
1952 + d3pie.prototype.getOpenSegment = function() {
1953 + var segment = this.currentlyOpenSegment;
1954 + if (segment !== null && typeof segment !== "undefined") {
1955 + var index = parseInt(d3.select(segment).attr("data-index"), 10);
1956 + return {
1957 + element: segment,
1958 + index: index,
1959 + data: this.options.data.content[index]
1960 + };
1961 + } else {
1962 + return null;
1963 + }
1964 + };
1965 +
1966 + d3pie.prototype.openSegment = function(index) {
1967 + index = parseInt(index, 10);
1968 + if (index < 0 || index > this.options.data.content.length-1) {
1969 + return;
1970 + }
1971 + segments.openSegment(this, d3.select("#" + this.cssPrefix + "segment" + index).node());
1972 + };
1973 +
1974 + d3pie.prototype.closeSegment = function() {
1975 + segments.maybeCloseOpenSegment();
1976 + };
1977 +
1978 + // this let's the user dynamically update aspects of the pie chart without causing a complete redraw. It
1979 + // intelligently re-renders only the part of the pie that the user specifies. Some things cause a repaint, others
1980 + // just redraw the single element
1981 + d3pie.prototype.updateProp = function(propKey, value) {
1982 + switch (propKey) {
1983 + case "header.title.text":
1984 + var oldVal = helpers.processObj(this.options, propKey);
1985 + helpers.processObj(this.options, propKey, value);
1986 + d3.select("#" + this.cssPrefix + "title").html(value);
1987 + if ((oldVal === "" && value !== "") || (oldVal !== "" && value === "")) {
1988 + this.redraw();
1989 + }
1990 + break;
1991 +
1992 + case "header.subtitle.text":
1993 + var oldValue = helpers.processObj(this.options, propKey);
1994 + helpers.processObj(this.options, propKey, value);
1995 + d3.select("#" + this.cssPrefix + "subtitle").html(value);
1996 + if ((oldValue === "" && value !== "") || (oldValue !== "" && value === "")) {
1997 + this.redraw();
1998 + }
1999 + break;
2000 +
2001 + case "callbacks.onload":
2002 + case "callbacks.onMouseoverSegment":
2003 + case "callbacks.onMouseoutSegment":
2004 + case "callbacks.onClickSegment":
2005 + case "effects.pullOutSegmentOnClick.effect":
2006 + case "effects.pullOutSegmentOnClick.speed":
2007 + case "effects.pullOutSegmentOnClick.size":
2008 + case "effects.highlightSegmentOnMouseover":
2009 + case "effects.highlightLuminosity":
2010 + helpers.processObj(this.options, propKey, value);
2011 + break;
2012 +
2013 + // everything else, attempt to update it & do a repaint
2014 + default:
2015 + helpers.processObj(this.options, propKey, value);
2016 +
2017 + this.destroy();
2018 + this.recreate();
2019 + break;
2020 + }
2021 + };
2022 +
2023 +
2024 + // ------------------------------------------------------------------------------------------------
2025 +
2026 + var _setupData = function () {
2027 + this.options.data.content = math.sortPieData(this);
2028 + if (this.options.data.smallSegmentGrouping.enabled) {
2029 + this.options.data.content = helpers.applySmallSegmentGrouping(this.options.data.content, this.options.data.smallSegmentGrouping);
2030 + }
2031 +
2032 +
2033 + this.options.colors = helpers.initSegmentColors(this);
2034 + this.totalSize = math.getTotalPieSize(this.options.data.content);
2035 +
2036 + var dp = this.options.labels.percentage.decimalPlaces;
2037 +
2038 + // add in percentage data to content
2039 + for (var i=0; i<this.options.data.content.length; i++) {
2040 + this.options.data.content[i].percentage = _getPercentage(this.options.data.content[i].value, this.totalSize, dp);
2041 + }
2042 +
2043 + // adjust the final item to ensure the percentage always adds up to precisely 100%. This is necessary
2044 + var totalPercentage = 0;
2045 + for (var j=0; j<this.options.data.content.length; j++) {
2046 + if (j === this.options.data.content.length - 1) {
2047 + this.options.data.content[j].percentage = (100 - totalPercentage).toFixed(dp);
2048 + }
2049 + totalPercentage += parseFloat(this.options.data.content[j].percentage);
2050 + }
2051 + };
2052 +
2053 + var _init = function() {
2054 +
2055 + // prep-work
2056 + this.svg = helpers.addSVGSpace(this);
2057 +
2058 + // store info about the main text components as part of the d3pie object instance
2059 + this.textComponents = {
2060 + headerHeight: 0,
2061 + title: {
2062 + exists: this.options.header.title.text !== "",
2063 + h: 0,
2064 + w: 0
2065 + },
2066 + subtitle: {
2067 + exists: this.options.header.subtitle.text !== "",
2068 + h: 0,
2069 + w: 0
2070 + },
2071 + footer: {
2072 + exists: this.options.footer.text !== "",
2073 + h: 0,
2074 + w: 0
2075 + }
2076 + };
2077 +
2078 + this.outerLabelGroupData = [];
2079 +
2080 + // add the key text components offscreen (title, subtitle, footer). We need to know their widths/heights for later computation
2081 + if (this.textComponents.title.exists) {
2082 + text.addTitle(this);
2083 + }
2084 + if (this.textComponents.subtitle.exists) {
2085 + text.addSubtitle(this);
2086 + }
2087 + text.addFooter(this);
2088 +
2089 + // the footer never moves. Put it in place now
2090 + var self = this;
2091 + helpers.whenIdExists(this.cssPrefix + "footer", function() {
2092 + text.positionFooter(self);
2093 + var d3 = helpers.getDimensions(self.cssPrefix + "footer");
2094 + self.textComponents.footer.h = d3.h;
2095 + self.textComponents.footer.w = d3.w;
2096 + });
2097 +
2098 + // now create the pie chart and position everything accordingly
2099 + var reqEls = [];
2100 + if (this.textComponents.title.exists) { reqEls.push(this.cssPrefix + "title"); }
2101 + if (this.textComponents.subtitle.exists) { reqEls.push(this.cssPrefix + "subtitle"); }
2102 + if (this.textComponents.footer.exists) { reqEls.push(this.cssPrefix + "footer"); }
2103 +
2104 + helpers.whenElementsExist(reqEls, function() {
2105 + if (self.textComponents.title.exists) {
2106 + var d1 = helpers.getDimensions(self.cssPrefix + "title");
2107 + self.textComponents.title.h = d1.h;
2108 + self.textComponents.title.w = d1.w;
2109 + }
2110 + if (self.textComponents.subtitle.exists) {
2111 + var d2 = helpers.getDimensions(self.cssPrefix + "subtitle");
2112 + self.textComponents.subtitle.h = d2.h;
2113 + self.textComponents.subtitle.w = d2.w;
2114 + }
2115 + // now compute the full header height
2116 + if (self.textComponents.title.exists || self.textComponents.subtitle.exists) {
2117 + var headerHeight = 0;
2118 + if (self.textComponents.title.exists) {
2119 + headerHeight += self.textComponents.title.h;
2120 + if (self.textComponents.subtitle.exists) {
2121 + headerHeight += self.options.header.titleSubtitlePadding;
2122 + }
2123 + }
2124 + if (self.textComponents.subtitle.exists) {
2125 + headerHeight += self.textComponents.subtitle.h;
2126 + }
2127 + self.textComponents.headerHeight = headerHeight;
2128 + }
2129 +
2130 + // at this point, all main text component dimensions have been calculated
2131 + math.computePieRadius(self);
2132 +
2133 + // this value is used all over the place for placing things and calculating locations. We figure it out ONCE
2134 + // and store it as part of the object
2135 + math.calculatePieCenter(self);
2136 +
2137 + // position the title and subtitle
2138 + text.positionTitle(self);
2139 + text.positionSubtitle(self);
2140 +
2141 + // now create the pie chart segments, and gradients if the user desired
2142 + if (self.options.misc.gradient.enabled) {
2143 + segments.addGradients(self);
2144 + }
2145 + segments.create(self); // also creates this.arc
2146 + labels.add(self, "inner", self.options.labels.inner.format);
2147 + labels.add(self, "outer", self.options.labels.outer.format);
2148 +
2149 + // position the label elements relatively within their individual group (label, percentage, value)
2150 + labels.positionLabelElements(self, "inner", self.options.labels.inner.format);
2151 + labels.positionLabelElements(self, "outer", self.options.labels.outer.format);
2152 + labels.computeOuterLabelCoords(self);
2153 +
2154 + // this is (and should be) dumb. It just places the outer groups at their calculated, collision-free positions
2155 + labels.positionLabelGroups(self, "outer");
2156 +
2157 + // we use the label line positions for many other calculations, so ALWAYS compute them
2158 + labels.computeLabelLinePositions(self);
2159 +
2160 + // only add them if they're actually enabled
2161 + if (self.options.labels.lines.enabled && self.options.labels.outer.format !== "none") {
2162 + labels.addLabelLines(self);
2163 + }
2164 +
2165 + labels.positionLabelGroups(self, "inner");
2166 + labels.fadeInLabelsAndLines(self);
2167 +
2168 + // add and position the tooltips
2169 + if (self.options.tooltips.enabled) {
2170 + tt.addTooltips(self);
2171 + }
2172 +
2173 + segments.addSegmentEventHandlers(self);
2174 + });
2175 + };
2176 +
2177 + var _getPercentage = function(value, total, decimalPlaces) {
2178 + var relativeAmount = value / total;
2179 + if (decimalPlaces <= 0) {
2180 + return Math.round(relativeAmount * 100);
2181 + } else {
2182 + return (relativeAmount * 100).toFixed(decimalPlaces);
2183 + }
2184 + };
2185 +
2186 + return d3pie;
2187 +}));
web/lib/d3pie-0.2.1.min.js deleted
-9
@@ -1,9 +0,0 @@
1 -/*!
2 -* d3pie
3 -* @author Ben Keen
4 -* @version 0.2.1
5 -* @date March 11, 2017
6 -* @repo http://github.com/benkeen/d3pie
7 -*/
8 -!function(a,b){"function"==typeof define&&define.amd?define([],b):"object"==typeof exports?module.exports=b():a.d3pie=b(a)}(this,function(){var a="d3pie",b="0.2.1",c=0,e={header:{title:{text:"",color:"#333333",fontSize:18,font:"arial"},subtitle:{text:"",color:"#666666",fontSize:14,font:"arial"},location:"top-center",titleSubtitlePadding:8},footer:{text:"",color:"#666666",fontSize:14,font:"arial",location:"left"},size:{canvasHeight:500,canvasWidth:500,pieInnerRadius:"0%",pieOuterRadius:null},data:{sortOrder:"none",ignoreSmallSegments:{enabled:!1,valueType:"percentage",value:null},smallSegmentGrouping:{enabled:!1,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",fontSize:10},percentage:{color:"#dddddd",font:"arial",fontSize:10,decimalPlaces:0},value:{color:"#cccc44",font:"arial",fontSize:10},lines:{enabled:!0,style:"curved",color:"segment"},truncation:{enabled:!1,truncateLength:30},formatter:null},effects:{load:{effect:"default",speed:1e3},pullOutSegmentOnClick:{effect:"bounce",speed:300,size:10},highlightSegmentOnMouseover:!0,highlightLuminosity:-.2},tooltips:{enabled:!1,type:"placeholder",string:"",placeholderParser:null,styles:{fadeInSpeed:250,backgroundColor:"#000000",backgroundOpacity:.5,color:"#efefef",borderRadius:2,font:"arial",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:!1,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}},f={initialCheck:function(a){var b=a.cssPrefix,c=a.element,d=a.options;if(!window.d3||!window.d3.hasOwnProperty("version"))return console.error("d3pie error: d3 is not available"),!1;if(!(c instanceof HTMLElement||c instanceof SVGElement))return console.error("d3pie error: the first d3pie() param must be a valid DOM element (not jQuery) or a ID string."),!1;if(!/[a-zA-Z][a-zA-Z0-9_-]*$/.test(b))return console.error("d3pie error: invalid options.misc.cssPrefix"),!1;if(!g.isArray(d.data.content))return console.error("d3pie error: invalid config structure: missing data.content property."),!1;if(0===d.data.content.length)return console.error("d3pie error: no data supplied."),!1;for(var e=[],f=0;f<d.data.content.length;f++)"number"!=typeof d.data.content[f].value||isNaN(d.data.content[f].value)?console.log("not valid: ",d.data.content[f]):d.data.content[f].value<=0?console.log("not valid - should have positive value: ",d.data.content[f]):e.push(d.data.content[f]);return a.options.data.content=e,!0}},g={addSVGSpace:function(a){var b=a.element,c=a.options.size.canvasWidth,d=a.options.size.canvasHeight,e=a.options.misc.colors.background,f=d3.select(b).append("svg:svg").attr("width",c).attr("height",d);return"transparent"!==e&&f.style("background-color",function(){return e}),f},whenIdExists:function(a,b){var c=1,d=1e3,e=setInterval(function(){document.getElementById(a)&&(clearInterval(e),b()),c>d&&clearInterval(e),c++},1)},whenElementsExist:function(a,b){var c=1,d=1e3,e=setInterval(function(){for(var f=!0,g=0;g<a.length;g++)if(!document.getElementById(a[g])){f=!1;break}f&&(clearInterval(e),b()),c>d&&clearInterval(e),c++},1)},shuffleArray:function(a){for(var b,c,d=a.length;0!==d;)c=Math.floor(Math.random()*d),d-=1,b=a[d],a[d]=a[c],a[c]=b;return a},processObj:function(a,b,c){return"string"==typeof b?g.processObj(a,b.split("."),c):1===b.length&&void 0!==c?(a[b[0]]=c,a[b[0]]):0===b.length?a:g.processObj(a[b[0]],b.slice(1),c)},getDimensions:function(a){var b=document.getElementById(a),c=0,d=0;if(b){var e=b.getBBox();c=e.width,d=e.height}else console.log("error: getDimensions() "+a+" not found.");return{w:c,h:d}},rectIntersect:function(a,b){var c=b.x>a.x+a.w||b.x+b.w<a.x||b.y+b.h<a.y||b.y>a.y+a.h;return!c},getColorShade:function(a,b){a=String(a).replace(/[^0-9a-f]/gi,""),a.length<6&&(a=a[0]+a[0]+a[1]+a[1]+a[2]+a[2]),b=b||0;for(var c="#",d=0;3>d;d++){var e=parseInt(a.substr(2*d,2),16);e=Math.round(Math.min(Math.max(0,e+e*b),255)).toString(16),c+=("00"+e).substr(e.length)}return c},initSegmentColors:function(a){for(var b=a.options.data.content,c=a.options.misc.colors.segments,d=[],e=0;e<b.length;e++)b[e].hasOwnProperty("color")?d.push(b[e].color):d.push(c[e]);return d},applySmallSegmentGrouping:function(a,b){var c;"percentage"===b.valueType&&(c=i.getTotalPieSize(a));for(var d=[],e=[],f=0,g=0;g<a.length;g++)if("percentage"===b.valueType){var h=a[g].value/c*100;if(h<=b.value){e.push(a[g]),f+=a[g].value;continue}a[g].isGrouped=!1,d.push(a[g])}else{if(a[g].value<=b.value){e.push(a[g]),f+=a[g].value;continue}a[g].isGrouped=!1,d.push(a[g])}return e.length&&d.push({color:b.color,label:b.label,value:f,isGrouped:!0,groupedData:e}),d},showPoint:function(a,b,c){a.append("circle").attr("cx",b).attr("cy",c).attr("r",2).style("fill","black")},isFunction:function(a){var b={};return a&&"[object Function]"===b.toString.call(a)},isArray:function(a){return"[object Array]"===Object.prototype.toString.call(a)}},h=function(){var a,b,c,d,e,f,g=arguments[0]||{},i=1,j=arguments.length,k=!1,l=Object.prototype.toString,m=Object.prototype.hasOwnProperty,n={"[object Boolean]":"boolean","[object Number]":"number","[object String]":"string","[object Function]":"function","[object Array]":"array","[object Date]":"date","[object RegExp]":"regexp","[object Object]":"object"},o={isFunction:function(a){return"function"===o.type(a)},isArray:Array.isArray||function(a){return"array"===o.type(a)},isWindow:function(a){return null!==a&&a===a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return null===a?String(a):n[l.call(a)]||"object"},isPlainObject:function(a){if(!a||"object"!==o.type(a)||a.nodeType)return!1;try{if(a.constructor&&!m.call(a,"constructor")&&!m.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(b){return!1}var c;for(c in a);return void 0===c||m.call(a,c)}};for("boolean"==typeof g&&(k=g,g=arguments[1]||{},i=2),"object"==typeof g||o.isFunction(g)||(g={}),j===i&&(g=this,--i),i;j>i;i++)if(null!==(a=arguments[i]))for(b in a)c=g[b],d=a[b],g!==d&&(k&&d&&(o.isPlainObject(d)||(e=o.isArray(d)))?(e?(e=!1,f=c&&o.isArray(c)?c:[]):f=c&&o.isPlainObject(c)?c:{},g[b]=h(k,f,d)):void 0!==d&&(g[b]=d));return g},i={toRadians:function(a){return a*(Math.PI/180)},toDegrees:function(a){return a*(180/Math.PI)},computePieRadius:function(a){var b=a.options.size,c=a.options.misc.canvasPadding,d=b.canvasWidth-c.left-c.right,e=b.canvasHeight-c.top-c.bottom;"pie-center"!==a.options.header.location&&(e-=a.textComponents.headerHeight),a.textComponents.footer.exists&&(e-=a.textComponents.footer.h),e=0>e?0:e;var f,g,h=(e>d?d:e)/3;if(null!==b.pieOuterRadius)if(/%/.test(b.pieOuterRadius)){g=parseInt(b.pieOuterRadius.replace(/[\D]/,""),10),g=g>99?99:g,g=0>g?0:g;var i=e>d?d:e;if("none"!==a.options.labels.outer.format){var j=2*parseInt(a.options.labels.outer.pieDistance,10);i-j>0&&(i-=j)}h=Math.floor(i/100*g)/2}else h=parseInt(b.pieOuterRadius,10);/%/.test(b.pieInnerRadius)?(g=parseInt(b.pieInnerRadius.replace(/[\D]/,""),10),g=g>99?99:g,g=0>g?0:g,f=Math.floor(h/100*g)):f=parseInt(b.pieInnerRadius,10),a.innerRadius=f,a.outerRadius=h},getTotalPieSize:function(a){for(var b=0,c=0;c<a.length;c++)b+=a[c].value;return b},sortPieData:function(a){var b=a.options.data.content,c=a.options.data.sortOrder;switch(c){case"none":break;case"random":b=g.shuffleArray(b);break;case"value-asc":b.sort(function(a,b){return a.value<b.value?-1:1});break;case"value-desc":b.sort(function(a,b){return a.value<b.value?1:-1});break;case"label-asc":b.sort(function(a,b){return a.label.toLowerCase()>b.label.toLowerCase()?1:-1});break;case"label-desc":b.sort(function(a,b){return a.label.toLowerCase()<b.label.toLowerCase()?1:-1})}return b},getPieTranslateCenter:function(a){return"translate("+a.x+","+a.y+")"},calculatePieCenter:function(a){var b=a.options.misc.pieCenterOffset,c=a.textComponents.title.exists&&"pie-center"!==a.options.header.location,d=a.textComponents.subtitle.exists&&"pie-center"!==a.options.header.location,e=a.options.misc.canvasPadding.top;c&&d?e+=a.textComponents.title.h+a.options.header.titleSubtitlePadding+a.textComponents.subtitle.h:c?e+=a.textComponents.title.h:d&&(e+=a.textComponents.subtitle.h);var f=0;a.textComponents.footer.exists&&(f=a.textComponents.footer.h+a.options.misc.canvasPadding.bottom);var g=(a.options.size.canvasWidth-a.options.misc.canvasPadding.left-a.options.misc.canvasPadding.right)/2+a.options.misc.canvasPadding.left,h=(a.options.size.canvasHeight-f-e)/2+e;g+=b.x,h+=b.y,a.pieCenter={x:g,y:h}},rotate:function(a,b,c,d,e){e=e*Math.PI/180;var f=Math.cos,g=Math.sin,h=(a-c)*f(e)-(b-d)*g(e)+c,i=(a-c)*g(e)+(b-d)*f(e)+d;return{x:h,y:i}},translate:function(a,b,c,d){var e=i.toRadians(d);return{x:a+c*Math.sin(e),y:b-c*Math.cos(e)}},pointIsInArc:function(a,b,c){var d=c.innerRadius()(b),e=c.outerRadius()(b),f=c.startAngle()(b),g=c.endAngle()(b),h=a.x*a.x+a.y*a.y,i=Math.atan2(a.x,-a.y);return i=0>i?i+2*Math.PI:i,h>=d*d&&e*e>=h&&i>=f&&g>=i}},j={add:function(a,b,c){var d=j.getIncludes(c),e=a.options.labels,f=a.svg.insert("g","."+a.cssPrefix+"labels-"+b).attr("class",a.cssPrefix+"labels-"+b),g=f.selectAll("."+a.cssPrefix+"labelGroup-"+b).data(a.options.data.content).enter().append("g").attr("id",function(c,d){return a.cssPrefix+"labelGroup"+d+"-"+b}).attr("data-index",function(a,b){return b}).attr("class",a.cssPrefix+"labelGroup-"+b).style("opacity",0),h={section:b,sectionDisplayType:c};d.mainLabel&&g.append("text").attr("id",function(c,d){return a.cssPrefix+"segmentMainLabel"+d+"-"+b}).attr("class",a.cssPrefix+"segmentMainLabel-"+b).text(function(a,b){var c=a.label;return e.formatter?(h.index=b,h.part="mainLabel",h.value=a.value,h.label=c,c=e.formatter(h)):e.truncation.enabled&&a.label.length>e.truncation.truncateLength&&(c=a.label.substring(0,e.truncation.truncateLength)+"..."),c}).style("font-size",e.mainLabel.fontSize+"px").style("font-family",e.mainLabel.font).style("fill",e.mainLabel.color),d.percentage&&g.append("text").attr("id",function(c,d){return a.cssPrefix+"segmentPercentage"+d+"-"+b}).attr("class",a.cssPrefix+"segmentPercentage-"+b).text(function(a,b){var c=a.percentage;return e.formatter?(h.index=b,h.part="percentage",h.value=a.value,h.label=a.percentage,c=e.formatter(h)):c+="%",c}).style("font-size",e.percentage.fontSize+"px").style("font-family",e.percentage.font).style("fill",e.percentage.color),d.value&&g.append("text").attr("id",function(c,d){return a.cssPrefix+"segmentValue"+d+"-"+b}).attr("class",a.cssPrefix+"segmentValue-"+b).text(function(a,b){return h.index=b,h.part="value",h.value=a.value,h.label=a.value,e.formatter?e.formatter(h,a.value):a.value}).style("font-size",e.value.fontSize+"px").style("font-family",e.value.font).style("fill",e.value.color)},positionLabelElements:function(a,b,c){j["dimensions-"+b]=[];var d=d3.selectAll("."+a.cssPrefix+"labelGroup-"+b);d.each(function(c,d){var e=d3.select(this).selectAll("."+a.cssPrefix+"segmentMainLabel-"+b),f=d3.select(this).selectAll("."+a.cssPrefix+"segmentPercentage-"+b),g=d3.select(this).selectAll("."+a.cssPrefix+"segmentValue-"+b);j["dimensions-"+b].push({mainLabel:null!==e.node()?e.node().getBBox():null,percentage:null!==f.node()?f.node().getBBox():null,value:null!==g.node()?g.node().getBBox():null})});var e=5,f=j["dimensions-"+b];switch(c){case"label-value1":d3.selectAll("."+a.cssPrefix+"segmentValue-"+b).attr("dx",function(a,b){return f[b].mainLabel.width+e});break;case"label-value2":d3.selectAll("."+a.cssPrefix+"segmentValue-"+b).attr("dy",function(a,b){return f[b].mainLabel.height});break;case"label-percentage1":d3.selectAll("."+a.cssPrefix+"segmentPercentage-"+b).attr("dx",function(a,b){return f[b].mainLabel.width+e});break;case"label-percentage2":d3.selectAll("."+a.cssPrefix+"segmentPercentage-"+b).attr("dx",function(a,b){return f[b].mainLabel.width/2-f[b].percentage.width/2}).attr("dy",function(a,b){return f[b].mainLabel.height})}},computeLabelLinePositions:function(a){a.lineCoordGroups=[],d3.selectAll("."+a.cssPrefix+"labelGroup-outer").each(function(b,c){return j.computeLinePosition(a,c)})},computeLinePosition:function(a,b){var c,d,e,f,g=k.getSegmentAngle(b,a.options.data.content,a.totalSize,{midpoint:!0}),h=i.rotate(a.pieCenter.x,a.pieCenter.y-a.outerRadius,a.pieCenter.x,a.pieCenter.y,g),j=a.outerLabelGroupData[b].h/5,l=6,m=Math.floor(g/90),n=4;switch(2===m&&180===g&&(m=1),m){case 0:c=a.outerLabelGroupData[b].x-l-(a.outerLabelGroupData[b].x-l-h.x)/2,d=a.outerLabelGroupData[b].y+(h.y-a.outerLabelGroupData[b].y)/n,e=a.outerLabelGroupData[b].x-l,f=a.outerLabelGroupData[b].y-j;break;case 1:c=h.x+(a.outerLabelGroupData[b].x-h.x)/n,d=h.y+(a.outerLabelGroupData[b].y-h.y)/n,e=a.outerLabelGroupData[b].x-l,f=a.outerLabelGroupData[b].y-j;break;case 2:var o=a.outerLabelGroupData[b].x+a.outerLabelGroupData[b].w+l;c=h.x-(h.x-o)/n,d=h.y+(a.outerLabelGroupData[b].y-h.y)/n,e=a.outerLabelGroupData[b].x+a.outerLabelGroupData[b].w+l,f=a.outerLabelGroupData[b].y-j;break;case 3:var p=a.outerLabelGroupData[b].x+a.outerLabelGroupData[b].w+l;c=p+(h.x-p)/n,d=a.outerLabelGroupData[b].y+(h.y-a.outerLabelGroupData[b].y)/n,e=a.outerLabelGroupData[b].x+a.outerLabelGroupData[b].w+l,f=a.outerLabelGroupData[b].y-j}"straight"===a.options.labels.lines.style?a.lineCoordGroups[b]=[{x:h.x,y:h.y},{x:e,y:f}]:a.lineCoordGroups[b]=[{x:h.x,y:h.y},{x:c,y:d},{x:e,y:f}]},addLabelLines:function(a){var b=a.svg.insert("g","."+a.cssPrefix+"pieChart").attr("class",a.cssPrefix+"lineGroups").style("opacity",0),c=b.selectAll("."+a.cssPrefix+"lineGroup").data(a.lineCoordGroups).enter().append("g").attr("class",a.cssPrefix+"lineGroup"),d=d3.line().curve(d3.curveBasis).x(function(a){return a.x}).y(function(a){return a.y});c.append("path").attr("d",d).attr("stroke",function(b,c){return"segment"===a.options.labels.lines.color?a.options.colors[c]:a.options.labels.lines.color}).attr("stroke-width",1).attr("fill","none").style("opacity",function(b,c){var d=a.options.labels.outer.hideWhenLessThanPercentage,e=null!==d&&b.percentage<d||""===a.options.data.content[c].label;return e?0:1})},positionLabelGroups:function(a,b){"none"!==a.options.labels[b].format&&d3.selectAll("."+a.cssPrefix+"labelGroup-"+b).style("opacity",0).attr("transform",function(c,d){var e,f;if("outer"===b)e=a.outerLabelGroupData[d].x,f=a.outerLabelGroupData[d].y;else{var j=h(!0,{},a.pieCenter);if(a.innerRadius>0){var l=k.getSegmentAngle(d,a.options.data.content,a.totalSize,{midpoint:!0}),m=i.translate(a.pieCenter.x,a.pieCenter.y,a.innerRadius,l);j.x=m.x,j.y=m.y}var n=g.getDimensions(a.cssPrefix+"labelGroup"+d+"-inner"),o=n.w/2,p=n.h/4;e=j.x+(a.lineCoordGroups[d][0].x-j.x)/1.8,f=j.y+(a.lineCoordGroups[d][0].y-j.y)/1.8,e-=o,f+=p}return"translate("+e+","+f+")"})},fadeInLabelsAndLines:function(a){var b="default"===a.options.effects.load.effect?a.options.effects.load.speed:1;setTimeout(function(){var b="default"===a.options.effects.load.effect?400:1;d3.selectAll("."+a.cssPrefix+"labelGroup-outer").transition().duration(b).style("opacity",function(b,c){var d=a.options.labels.outer.hideWhenLessThanPercentage;return null!==d&&b.percentage<d?0:1}),d3.selectAll("."+a.cssPrefix+"labelGroup-inner").transition().duration(b).style("opacity",function(b,c){var d=a.options.labels.inner.hideWhenLessThanPercentage;return null!==d&&b.percentage<d?0:1}),d3.selectAll("g."+a.cssPrefix+"lineGroups").transition().duration(b).style("opacity",1),g.isFunction(a.options.callbacks.onload)&&setTimeout(function(){try{a.options.callbacks.onload()}catch(b){}},b)},b)},getIncludes:function(a){var b=!1,c=!1,d=!1;switch(a){case"label":b=!0;break;case"value":c=!0;break;case"percentage":d=!0;break;case"label-value1":case"label-value2":b=!0,c=!0;break;case"label-percentage1":case"label-percentage2":b=!0,d=!0}return{mainLabel:b,value:c,percentage:d}},computeOuterLabelCoords:function(a){a.svg.selectAll("."+a.cssPrefix+"labelGroup-outer").each(function(b,c){return j.getIdealOuterLabelPositions(a,c)}),j.resolveOuterLabelCollisions(a)},resolveOuterLabelCollisions:function(a){if("none"!==a.options.labels.outer.format){var b=a.options.data.content.length;j.checkConflict(a,0,"clockwise",b),j.checkConflict(a,b-1,"anticlockwise",b)}},checkConflict:function(a,b,c,d){var e,f;if(!(1>=d)){var h=a.outerLabelGroupData[b].hs;if(!("clockwise"===c&&"right"!==h||"anticlockwise"===c&&"left"!==h)){var i="clockwise"===c?b+1:b-1,k=a.outerLabelGroupData[b],l=a.outerLabelGroupData[i],m={labelHeights:a.outerLabelGroupData[0].h,center:a.pieCenter,lineLength:a.outerRadius+a.options.labels.outer.pieDistance,heightChange:a.outerLabelGroupData[0].h+1};if("clockwise"===c){for(e=0;b>=e;e++)if(f=a.outerLabelGroupData[e],!j.isLabelHidden(a,e)&&g.rectIntersect(f,l)){j.adjustLabelPos(a,i,k,m);break}}else for(e=d-1;e>=b;e--)if(f=a.outerLabelGroupData[e],!j.isLabelHidden(a,e)&&g.rectIntersect(f,l)){j.adjustLabelPos(a,i,k,m);break}j.checkConflict(a,i,c,d)}}},isLabelHidden:function(a,b){var c=a.options.labels.outer.hideWhenLessThanPercentage;return null!==c&&d.percentage<c||""===a.options.data.content[b].label},adjustLabelPos:function(a,b,c,d){var e,f,g,h;h=c.y+d.heightChange,f=d.center.y-h,e=Math.abs(d.lineLength)>Math.abs(f)?Math.sqrt(d.lineLength*d.lineLength-f*f):Math.sqrt(f*f-d.lineLength*d.lineLength),g="right"===c.hs?d.center.x+e:d.center.x-e-a.outerLabelGroupData[b].w,a.outerLabelGroupData[b].x=g,a.outerLabelGroupData[b].y=h},getIdealOuterLabelPositions:function(a,b){var c=d3.select("#"+a.cssPrefix+"labelGroup"+b+"-outer").node();if(c){var d=c.getBBox(),e=k.getSegmentAngle(b,a.options.data.content,a.totalSize,{midpoint:!0}),f=a.pieCenter.x,g=a.pieCenter.y-(a.outerRadius+a.options.labels.outer.pieDistance),h=i.rotate(f,g,a.pieCenter.x,a.pieCenter.y,e),j="right";e>180?(h.x-=d.width+8,j="left"):h.x+=8,a.outerLabelGroupData[b]={x:h.x,y:h.y,w:d.width,h:d.height,hs:j}}}},k={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},create:function(a){var b=a.pieCenter,c=a.options.colors,d=a.options.effects.load,e=a.options.misc.colors.segmentStroke,f=a.svg.insert("g","#"+a.cssPrefix+"title").attr("transform",function(){return i.getPieTranslateCenter(b)}).attr("class",a.cssPrefix+"pieChart"),g=d3.arc().innerRadius(a.innerRadius).outerRadius(a.outerRadius).startAngle(0).endAngle(function(b){return b.value/a.totalSize*2*Math.PI}),h=f.selectAll("."+a.cssPrefix+"arc").data(a.options.data.content).enter().append("g").attr("class",a.cssPrefix+"arc"),j=d.speed;"none"===d.effect&&(j=0),h.append("path").attr("id",function(b,c){return a.cssPrefix+"segment"+c}).attr("fill",function(b,d){var e=c[d];return a.options.misc.gradient.enabled&&(e="url(#"+a.cssPrefix+"grad"+d+")"),e}).style("stroke",e).style("stroke-width",1).transition().ease(d3.easeCubicInOut).duration(j).attr("data-index",function(a,b){return b}).attrTween("d",function(b){var c=d3.interpolate({value:0},b);return function(b){return a.arc(c(b))}}),a.svg.selectAll("g."+a.cssPrefix+"arc").attr("transform",function(b,c){var d=0;return c>0&&(d=k.getSegmentAngle(c-1,a.options.data.content,a.totalSize)),"rotate("+d+")"}),a.arc=g},addGradients:function(a){var b=a.svg.append("defs").selectAll("radialGradient").data(a.options.data.content).enter().append("radialGradient").attr("gradientUnits","userSpaceOnUse").attr("cx",0).attr("cy",0).attr("r","120%").attr("id",function(b,c){return a.cssPrefix+"grad"+c});b.append("stop").attr("offset","0%").style("stop-color",function(b,c){return a.options.colors[c]}),b.append("stop").attr("offset",a.options.misc.gradient.percentage+"%").style("stop-color",a.options.misc.gradient.color)},addSegmentEventHandlers:function(a){var b=d3.selectAll("."+a.cssPrefix+"arc,."+a.cssPrefix+"labelGroup-inner,."+a.cssPrefix+"labelGroup-outer");b.on("click",function(){var b,c=d3.select(this);if(c.attr("class")===a.cssPrefix+"arc")b=c.select("path");else{var d=c.attr("data-index");b=d3.select("#"+a.cssPrefix+"segment"+d)}var e=b.attr("class")===a.cssPrefix+"expanded";k.onSegmentEvent(a,a.options.callbacks.onClickSegment,b,e),"none"!==a.options.effects.pullOutSegmentOnClick.effect&&(e?k.closeSegment(a,b.node()):k.openSegment(a,b.node()))}),b.on("mouseover",function(){var b,c,d=d3.select(this);if(d.attr("class")===a.cssPrefix+"arc"?b=d.select("path"):(c=d.attr("data-index"),b=d3.select("#"+a.cssPrefix+"segment"+c)),a.options.effects.highlightSegmentOnMouseover){c=b.attr("data-index");var e=a.options.colors[c];b.style("fill",g.getColorShade(e,a.options.effects.highlightLuminosity))}a.options.tooltips.enabled&&(c=b.attr("data-index"),m.showTooltip(a,c));var f=b.attr("class")===a.cssPrefix+"expanded";k.onSegmentEvent(a,a.options.callbacks.onMouseoverSegment,b,f)}),b.on("mousemove",function(){m.moveTooltip(a)}),b.on("mouseout",function(){var b,c,d=d3.select(this);if(d.attr("class")===a.cssPrefix+"arc"?b=d.select("path"):(c=d.attr("data-index"),b=d3.select("#"+a.cssPrefix+"segment"+c)),a.options.effects.highlightSegmentOnMouseover){c=b.attr("data-index");var e=a.options.colors[c];a.options.misc.gradient.enabled&&(e="url(#"+a.cssPrefix+"grad"+c+")"),b.style("fill",e)}a.options.tooltips.enabled&&(c=b.attr("data-index"),m.hideTooltip(a,c));var f=b.attr("class")===a.cssPrefix+"expanded";k.onSegmentEvent(a,a.options.callbacks.onMouseoutSegment,b,f)})},onSegmentEvent:function(a,b,c,d){if(g.isFunction(b)){var e=parseInt(c.attr("data-index"),10);b({segment:c.node(),index:e,expanded:d,data:a.options.data.content[e]})}},openSegment:function(a,b){a.isOpeningSegment||(a.isOpeningSegment=!0,k.maybeCloseOpenSegment(),d3.select(b).transition().ease(k.effectMap[a.options.effects.pullOutSegmentOnClick.effect]).duration(a.options.effects.pullOutSegmentOnClick.speed).attr("transform",function(b,c){var d=a.arc.centroid(b),e=d[0],f=d[1],g=Math.sqrt(e*e+f*f),h=parseInt(a.options.effects.pullOutSegmentOnClick.size,10);return"translate("+e/g*h+","+f/g*h+")"}).on("end",function(c,d){a.currentlyOpenSegment=b,a.isOpeningSegment=!1,d3.select(b).attr("class",a.cssPrefix+"expanded")}))},maybeCloseOpenSegment:function(){d3.selectAll("."+pie.cssPrefix+"expanded").size()>0&&k.closeSegment(pie,d3.select("."+pie.cssPrefix+"expanded").node())},closeSegment:function(a,b){d3.select(b).transition().duration(400).attr("transform","translate(0,0)").on("end",function(c,d){d3.select(b).attr("class",""),a.currentlyOpenSegment=null})},getCentroid:function(a){var b=a.getBBox();return{x:b.x+b.width/2,y:b.y+b.height/2}},getSegmentAngle:function(a,b,c,d){var e,f=h({compounded:!0,midpoint:!1},d),g=b[a].value;if(f.compounded){e=0;for(var i=0;a>=i;i++)e+=b[i].value}"undefined"==typeof e&&(e=g);var j=e/c*360;if(f.midpoint){var k=g/c*360;j-=k/2}return j}},l={offscreenCoord:-1e4,addTitle:function(a){a.svg.selectAll("."+a.cssPrefix+"title").data([a.options.header.title]).enter().append("text").text(function(a){return a.text}).attr("id",a.cssPrefix+"title").attr("class",a.cssPrefix+"title").attr("x",l.offscreenCoord).attr("y",l.offscreenCoord).attr("text-anchor",function(){var b;return b="top-center"===a.options.header.location||"pie-center"===a.options.header.location?"middle":"left"}).attr("fill",function(a){return a.color}).style("font-size",function(a){return a.fontSize+"px"}).style("font-family",function(a){return a.font})},positionTitle:function(a){var b,c=a.textComponents,d=a.options.header.location,e=a.options.misc.canvasPadding,f=a.options.size.canvasWidth,g=a.options.header.titleSubtitlePadding;b="top-left"===d?e.left:(f-e.right)/2+e.left,b+=a.options.misc.pieCenterOffset.x;var h=e.top+c.title.h;if("pie-center"===d)if(h=a.pieCenter.y,c.subtitle.exists){var i=c.title.h+g+c.subtitle.h;h=h-i/2+c.title.h}else h+=c.title.h/4;a.svg.select("#"+a.cssPrefix+"title").attr("x",b).attr("y",h)},addSubtitle:function(a){var b=a.options.header.location;a.svg.selectAll("."+a.cssPrefix+"subtitle").data([a.options.header.subtitle]).enter().append("text").text(function(a){return a.text}).attr("x",l.offscreenCoord).attr("y",l.offscreenCoord).attr("id",a.cssPrefix+"subtitle").attr("class",a.cssPrefix+"subtitle").attr("text-anchor",function(){var a;return a="top-center"===b||"pie-center"===b?"middle":"left"}).attr("fill",function(a){return a.color}).style("font-size",function(a){return a.fontSize+"px"}).style("font-family",function(a){return a.font})},positionSubtitle:function(a){var b,c=a.options.misc.canvasPadding,d=a.options.size.canvasWidth;b="top-left"===a.options.header.location?c.left:(d-c.right)/2+c.left,b+=a.options.misc.pieCenterOffset.x;var e=l.getHeaderHeight(a);a.svg.select("#"+a.cssPrefix+"subtitle").attr("x",b).attr("y",e)},addFooter:function(a){a.svg.selectAll("."+a.cssPrefix+"footer").data([a.options.footer]).enter().append("text").text(function(a){return a.text}).attr("x",l.offscreenCoord).attr("y",l.offscreenCoord).attr("id",a.cssPrefix+"footer").attr("class",a.cssPrefix+"footer").attr("text-anchor",function(){var b="left";return"bottom-center"===a.options.footer.location?b="middle":"bottom-right"===a.options.footer.location&&(b="left"),b}).attr("fill",function(a){return a.color}).style("font-size",function(a){return a.fontSize+"px"}).style("font-family",function(a){return a.font})},positionFooter:function(a){var b,c=a.options.footer.location,d=a.textComponents.footer.w,e=a.options.size.canvasWidth,f=a.options.size.canvasHeight,g=a.options.misc.canvasPadding;b="bottom-left"===c?g.left:"bottom-right"===c?e-d-g.right:e/2,a.svg.select("#"+a.cssPrefix+"footer").attr("x",b).attr("y",f-g.bottom)},getHeaderHeight:function(a){var b;if(a.textComponents.title.exists){var c=a.textComponents.title.h+a.options.header.titleSubtitlePadding+a.textComponents.subtitle.h;b="pie-center"===a.options.header.location?a.pieCenter.y-c/2+c:c+a.options.misc.canvasPadding.top}else if("pie-center"===a.options.header.location){var d=a.options.misc.canvasPadding.bottom+a.textComponents.footer.h;b=(a.options.size.canvasHeight-d)/2+a.options.misc.canvasPadding.top+a.textComponents.subtitle.h/2}else b=a.options.misc.canvasPadding.top+a.textComponents.subtitle.h;return b}},m={addTooltips:function(a){var b=a.svg.insert("g").attr("class",a.cssPrefix+"tooltips");b.selectAll("."+a.cssPrefix+"tooltip").data(a.options.data.content).enter().append("g").attr("class",a.cssPrefix+"tooltip").attr("id",function(b,c){return a.cssPrefix+"tooltip"+c}).style("opacity",0).append("rect").attr("rx",a.options.tooltips.styles.borderRadius).attr("ry",a.options.tooltips.styles.borderRadius).attr("x",-a.options.tooltips.styles.padding).attr("opacity",a.options.tooltips.styles.backgroundOpacity).style("fill",a.options.tooltips.styles.backgroundColor),b.selectAll("."+a.cssPrefix+"tooltip").data(a.options.data.content).append("text").attr("fill",function(b){return a.options.tooltips.styles.color}).style("font-size",function(b){return a.options.tooltips.styles.fontSize}).style("font-family",function(b){return a.options.tooltips.styles.font}).text(function(b,c){var d=a.options.tooltips.string;return"caption"===a.options.tooltips.type&&(d=b.caption),m.replacePlaceholders(a,d,c,{label:b.label,value:b.value,percentage:b.percentage})}),b.selectAll("."+a.cssPrefix+"tooltip rect").attr("width",function(b,c){var d=g.getDimensions(a.cssPrefix+"tooltip"+c);return d.w+2*a.options.tooltips.styles.padding}).attr("height",function(b,c){var d=g.getDimensions(a.cssPrefix+"tooltip"+c);return d.h+2*a.options.tooltips.styles.padding}).attr("y",function(b,c){var d=g.getDimensions(a.cssPrefix+"tooltip"+c);return-(d.h/2)+1})},showTooltip:function(a,b){var c=a.options.tooltips.styles.fadeInSpeed;m.currentTooltip===b&&(c=1),m.currentTooltip=b,d3.select("#"+a.cssPrefix+"tooltip"+b).transition().duration(c).style("opacity",function(){return 1}),m.moveTooltip(a)},moveTooltip:function(a){d3.selectAll("#"+a.cssPrefix+"tooltip"+m.currentTooltip).attr("transform",function(b){var c=d3.mouse(this.parentNode),d=c[0]+a.options.tooltips.styles.padding+2,e=c[1]-2*a.options.tooltips.styles.padding-2;return"translate("+d+","+e+")"})},hideTooltip:function(a,b){d3.select("#"+a.cssPrefix+"tooltip"+b).style("opacity",function(){return 0}),d3.select("#"+a.cssPrefix+"tooltip"+m.currentTooltip).attr("transform",function(b,c){var d=a.options.size.canvasWidth+1e3,e=a.options.size.canvasHeight+1e3;return"translate("+d+","+e+")"})},replacePlaceholders:function(a,b,c,d){g.isFunction(a.options.tooltips.placeholderParser)&&a.options.tooltips.placeholderParser(c,d);var e=function(){return function(a){var b=arguments[1];return d.hasOwnProperty(b)?d[arguments[1]]:arguments[0]}};return b.replace(/\{(\w+)\}/g,e(d))}},n=function(d,g){if(this.element=d,"string"==typeof d){var i=d.replace(/^#/,"");this.element=document.getElementById(i)}var j={};h(!0,j,e,g),this.options=j,null!==this.options.misc.cssPrefix?this.cssPrefix=this.options.misc.cssPrefix:(this.cssPrefix="p"+c+"_",c++),f.initialCheck(this)&&(d3.select(this.element).attr(a,b),o.call(this),p.call(this))};n.prototype.recreate=function(){f.initialCheck(this)&&(o.call(this),p.call(this))},n.prototype.redraw=function(){this.element.innerHTML="",p.call(this)},n.prototype.destroy=function(){this.element.innerHTML="",d3.select(this.element).attr(a,null)},n.prototype.getOpenSegment=function(){var a=this.currentlyOpenSegment;if(null!==a&&"undefined"!=typeof a){var b=parseInt(d3.select(a).attr("data-index"),10);return{element:a,index:b,data:this.options.data.content[b]}}return null},n.prototype.openSegment=function(a){a=parseInt(a,10),0>a||a>this.options.data.content.length-1||k.openSegment(this,d3.select("#"+this.cssPrefix+"segment"+a).node())},n.prototype.closeSegment=function(){k.maybeCloseOpenSegment()},n.prototype.updateProp=function(a,b){switch(a){case"header.title.text":var c=g.processObj(this.options,a);g.processObj(this.options,a,b),d3.select("#"+this.cssPrefix+"title").html(b),(""===c&&""!==b||""!==c&&""===b)&&this.redraw();break;case"header.subtitle.text":var d=g.processObj(this.options,a);g.processObj(this.options,a,b),d3.select("#"+this.cssPrefix+"subtitle").html(b),(""===d&&""!==b||""!==d&&""===b)&&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":g.processObj(this.options,a,b);break;default:g.processObj(this.options,a,b),this.destroy(),this.recreate()}};var o=function(){this.options.data.content=i.sortPieData(this),this.options.data.smallSegmentGrouping.enabled&&(this.options.data.content=g.applySmallSegmentGrouping(this.options.data.content,this.options.data.smallSegmentGrouping)),this.options.colors=g.initSegmentColors(this),this.totalSize=i.getTotalPieSize(this.options.data.content);for(var a=this.options.labels.percentage.decimalPlaces,b=0;b<this.options.data.content.length;b++)this.options.data.content[b].percentage=q(this.options.data.content[b].value,this.totalSize,a);for(var c=0,d=0;d<this.options.data.content.length;d++)d===this.options.data.content.length-1&&(this.options.data.content[d].percentage=(100-c).toFixed(a)),c+=parseFloat(this.options.data.content[d].percentage)},p=function(){this.svg=g.addSVGSpace(this),this.textComponents={
9 -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=[],this.textComponents.title.exists&&l.addTitle(this),this.textComponents.subtitle.exists&&l.addSubtitle(this),l.addFooter(this);var a=this;g.whenIdExists(this.cssPrefix+"footer",function(){l.positionFooter(a);var b=g.getDimensions(a.cssPrefix+"footer");a.textComponents.footer.h=b.h,a.textComponents.footer.w=b.w});var b=[];this.textComponents.title.exists&&b.push(this.cssPrefix+"title"),this.textComponents.subtitle.exists&&b.push(this.cssPrefix+"subtitle"),this.textComponents.footer.exists&&b.push(this.cssPrefix+"footer"),g.whenElementsExist(b,function(){if(a.textComponents.title.exists){var b=g.getDimensions(a.cssPrefix+"title");a.textComponents.title.h=b.h,a.textComponents.title.w=b.w}if(a.textComponents.subtitle.exists){var c=g.getDimensions(a.cssPrefix+"subtitle");a.textComponents.subtitle.h=c.h,a.textComponents.subtitle.w=c.w}if(a.textComponents.title.exists||a.textComponents.subtitle.exists){var d=0;a.textComponents.title.exists&&(d+=a.textComponents.title.h,a.textComponents.subtitle.exists&&(d+=a.options.header.titleSubtitlePadding)),a.textComponents.subtitle.exists&&(d+=a.textComponents.subtitle.h),a.textComponents.headerHeight=d}i.computePieRadius(a),i.calculatePieCenter(a),l.positionTitle(a),l.positionSubtitle(a),a.options.misc.gradient.enabled&&k.addGradients(a),k.create(a),j.add(a,"inner",a.options.labels.inner.format),j.add(a,"outer",a.options.labels.outer.format),j.positionLabelElements(a,"inner",a.options.labels.inner.format),j.positionLabelElements(a,"outer",a.options.labels.outer.format),j.computeOuterLabelCoords(a),j.positionLabelGroups(a,"outer"),j.computeLabelLinePositions(a),a.options.labels.lines.enabled&&"none"!==a.options.labels.outer.format&&j.addLabelLines(a),j.positionLabelGroups(a,"inner"),j.fadeInLabelsAndLines(a),a.options.tooltips.enabled&&m.addTooltips(a),k.addSegmentEventHandlers(a)})},q=function(a,b,c){var d=a/b;return 0>=c?Math.round(100*d):(100*d).toFixed(c)};return n});
\ No newline at end of file