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