Codebase list cloudkitty-dashboard / 9802b37
Rework the "reporting" tab This is a rework of the Project/Reporting tag. Its main goal is to draw the "Cumulative cost repartition" piechart without D3pie, in order to get rid of that dependency. Work items: * Remove d3pie dependency, and replace the piechart by a D3-only donut. * Use consistent colors between the piechart and time chart. * Add a color legend. Change-Id: Ie2207be3c027b6042251fbcb7d93a3cd5455ad3d Story: 2003578 Task: 24923 Luka Peschke 5 years ago
4 changed file(s) with 136 addition(s) and 2207 deletion(s). Raw diff Collapse all Expand all
11 {% load l10n %}
22 {% load static %}
33 <script src='{% static "cloudkitty/js/d3.min.js" %}' type='text/javascript' charset='utf-8'></script>
4 <script src='{% static "cloudkitty/js/d3pie.min.js" %}' type='text/javascript' charset='utf-8'></script>
54 <script src='{% static "cloudkitty/js/rickshaw.min.js" %}' type='text/javascript' charset='utf-8'></script>
65
7 <div class="col-md-6">
8 <h4>{% trans "Cumulative Cost Repartition" %}</h4>
9 <div id="repartition_cumulated"></div>
6 <div class="container-fluid">
7 <div class="col-lg-3 col-md-4">
8 <h4>{% trans "Legend" %}</h4>
9 <div id="graph_legend"></div>
10 </div>
11 <div class="col-lg-4 col-md-8" style="max-width:25vw;">
12 <h4>{% trans "Cumulative Cost Repartition" %}</h4>
13 <div id="repartition_cumulated"></div>
14 </div>
15 <div class="col-lg-5 col-sm-12">
16 <h4>{% trans "Cost Per Service Per Hour" %}</h4>
17 <div id="cost_progress" style="max-width:100%;"></div>
18 <div id="cost_progress_legend"></div>
19 </div>
1020 </div>
1121
1222 <script type="text/javascript">
13 var cumulated = new d3pie("repartition_cumulated", {
14 "size": {
15 "canvasHeight": 300,
16 "canvasWidth": 500
17 },
18 "labels": {
19 "outer": {
20 "pieDistance": 20
21 },
22 "inner": {
23 "hideWhenLessThanPercentage": 3
24 },
25 "mainLabel": {
26 "fontSize": 11
27 },
28 "percentage": {
29 "color": "#ffffff",
30 "decimalPlaces": 0
31 },
32 "value": {
33 "color": "#adadad",
34 "fontSize": 11
35 },
36 "lines": {
37 "enabled": true
38 }
39 },
40 "effects": {
41 "pullOutSegmentOnClick": {
42 "effect": "linear",
43 "speed": 400,
44 "size": 8
45 }
46 },
47 "misc": {
48 "gradient": {
49 "enabled": true,
50 "percentage": 100
51 }
52 },
53 "data": {
54 "sortOrder": "value-desc",
55 "content": [
23 var data = [
5624 {% for service, data in repartition_data.items %}
57 {"label": "{{ service }}",
58 "value": {{ data.cumulated|unlocalize }}
59 },
60 {% endfor %}
61 ]}});
62 </script>
63
64 <div class="col-md-6">
65 <h4>{% trans "Cost Per Service Per Hour" %}</h4>
66 <div id="cost_progress"></div>
67 <div id="cost_progress_legend"></div>
68 </div>
69
70 <script>
71 var palette = new Rickshaw.Color.Palette();
72 var graph = new Rickshaw.Graph({
73 element: document.querySelector('#cost_progress'),
74 width: 500,
75 height: 300,
76 interpolation: 'linear',
77 onComplete: function(w) {
78 var legend = new Rickshaw.Graph.Legend({
79 element: document.querySelector('#cost_progress_legend'),
80 graph: w.graph
81 });
82 },
83 series: [
84 {% for service, data in repartition_data.items %}
85 {
86 color: palette.color(),
87 name: "{{ service }}",
88 data: [
89 {% for timestamp, rating in data.hourly.items %}{x: {{ timestamp }}, y: {{ rating|unlocalize }}},{% endfor %}
90 ]
25 {"label": "{{ service }}",
26 "value": {{ data.cumulated|unlocalize }}
9127 },
9228 {% endfor %}
9329 ]
94 });
95 graph.render();
9630
97 var hoverDetail = new Rickshaw.Graph.HoverDetail( {
31 // Pie Chart
32 var innerRadius = 75;
33 var outerRadius = 150;
34 var height = 300;
35 var width = 300;
36
37 var colors = d3.scale.category20c();
38
39 var vis = d3.select("#repartition_cumulated")
40 .append("svg:svg") // create the SVG element inside the DOM
41 .data([data]) // associate our data
42 .attr("width", "75%")
43 .attr("height", "75%")
44 .attr("viewBox", "0 0 300 300")
45 .append("svg:g")
46 .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")") // move the center of the pie chart from 0, 0 to radius, radius
47
48 var arc = d3.svg.arc() // Creating <path> elements using arc data
49 .outerRadius(outerRadius)
50 .innerRadius(innerRadius);
51
52 var pie = d3.layout.pie() // Creating arc data for us given a list of values
53 .value(function(d) { return d.value; });
54
55 var arcs = vis.selectAll("g.slice") // Selecting all <g> elements (there are none yet)
56 .data(pie) // associate data
57 .enter() // creating a <g> for each element of data
58 .append("svg:g")
59 .attr("class", "slice");
60
61 arcs.append("svg:path")
62 .attr("fill", function(d, i) { return colors(i); } ) // Setting the color of each slice
63 .attr("d", arc); // creating the actual svg
64
65 arcs.append("svg:title") //add a label to each slice
66 .attr("text-anchor", "middle") //center the text on it's origin
67 .text(function(d, i) { return data[i].label; }); //get the label from our original data array
68
69
70 // Legend
71 var legendHeight = 20;
72 var legendSpace = 5;
73 var viewBoxHeight = data.length * (legendHeight + legendSpace);
74 console.log('data length', data.length)
75
76 var legend_vis = d3.select("#graph_legend")
77 .append("svg:svg")
78 .data([data])
79 .attr("viewBox", "0 0 250 " + viewBoxHeight)
80 .attr("width", "100%")
81 .attr("height", "100%")
82 .append("svg:g")
83 .attr("transform", "translate(0,0)");
84
85 var legend = legend_vis.selectAll("g")
86 .data(data)
87 .enter()
88 .append('g')
89 .attr('class', 'legend')
90 .attr('transform', function(d, i) {
91 var x = 0;
92 var y = i * (legendHeight + legendSpace);
93 return 'translate(' + x + ',' + y + ')';
94 });
95
96 var legendRectSize = 20;
97 var legendSpacing = 5;
98 legend.append('rect')
99 .attr('width', legendRectSize)
100 .attr('height', legendRectSize)
101 .style('fill', function(d, i) { return colors(i); })
102 .style('stroke', function(d, i) { return colors(i); });
103
104 legend.append('text')
105 .attr('x', legendRectSize + legendSpacing)
106 .attr('y', legendRectSize - legendSpacing)
107 .text(function(d) { return d.label; });
108 </script>
109
110 <script>
111
112 var colors = d3.scale.category20c();
113 var graph = new Rickshaw.Graph({
114 element: document.querySelector('#cost_progress'),
115 interpolation: 'linear',
116 onComplete: function(w) {
117 var legend = new Rickshaw.Graph.Legend({
118 element: document.querySelector('#cost_progress_legend'),
119 graph: w.graph
120 });
121 },
122 series: [
123 {% for service, data in repartition_data.items %}
124 {
125 color: colors({{ forloop.counter }} - 1),
126 name: "{{ service }}",
127 data: [
128 {% for timestamp, rating in data.hourly.items %}{x: {{ timestamp }}, y: {{ rating|unlocalize }}},{% endfor %}
129 ]
130 },
131 {% endfor %}
132 ]
133 });
134 graph.render();
135
136 var hoverDetail = new Rickshaw.Graph.HoverDetail( {
98137 graph: graph
99 } );
138 });
100139
101 var yAxis = new Rickshaw.Graph.Axis.Y({
140 var yAxis = new Rickshaw.Graph.Axis.Y({
102141 graph: graph,
103 });
104 yAxis.render();
142 });
143 yAxis.render();
105144
106 var xAxis = new Rickshaw.Graph.Axis.Time({
145 var xAxis = new Rickshaw.Graph.Axis.Time({
107146 graph: graph
108 });
109 xAxis.render();
147 });
148 xAxis.render();
110149 </script>
+0
-2107
cloudkittydashboard/static/cloudkitty/js/d3pie.js less more
0 /*!
1 * d3pie
2 * @author Ben Keen
3 * @version 0.1.4
4 * @date Oct 2014 - [still in dev!]
5 * @repo http://github.com/benkeen/d3pie
6 */
7
8 // UMD pattern from https://github.com/umdjs/umd/blob/master/returnExports.js
9 (function(root, factory) {
10 if (typeof define === 'function' && define.amd) {
11 // AMD. Register as an anonymous module
12 define([], factory);
13 } else if (typeof exports === 'object') {
14 // Node. Does not work with strict CommonJS, but only CommonJS-like environments that support module.exports,
15 // like Node
16 module.exports = factory(require());
17 } else {
18 // browser globals (root is window)
19 root.d3pie = factory(root);
20 }
21 }(this, function() {
22
23 var _scriptName = "d3pie";
24 var _version = "0.1.5";
25
26 // used to uniquely generate IDs and classes, ensuring no conflict between multiple pies on the same page
27 var _uniqueIDCounter = 0;
28
29
30 // this section includes all helper libs on the d3pie object. They're populated via grunt-template. Note: to keep
31 // the syntax highlighting from getting all messed up, I commented out each line. That REQUIRES each of the files
32 // to have an empty first line. Crumby, yes, but acceptable.
33 //// --------- _default-settings.js -----------/**
34 /**
35 * Contains the out-the-box settings for the script. Any of these settings that aren't explicitly overridden for the
36 * d3pie instance will inherit from these. This is also included on the main website for use in the generation script.
37 */
38 var defaultSettings = {
39 header: {
40 title: {
41 text: "",
42 color: "#333333",
43 fontSize: 18,
44 font: "arial"
45 },
46 subtitle: {
47 text: "",
48 color: "#666666",
49 fontSize: 14,
50 font: "arial"
51 },
52 location: "top-center",
53 titleSubtitlePadding: 8
54 },
55 footer: {
56 text: "",
57 color: "#666666",
58 fontSize: 14,
59 font: "arial",
60 location: "left"
61 },
62 size: {
63 canvasHeight: 500,
64 canvasWidth: 500,
65 pieInnerRadius: "0%",
66 pieOuterRadius: null
67 },
68 data: {
69 sortOrder: "none",
70 ignoreSmallSegments: {
71 enabled: false,
72 valueType: "percentage",
73 value: null
74 },
75 smallSegmentGrouping: {
76 enabled: false,
77 value: 1,
78 valueType: "percentage",
79 label: "Other",
80 color: "#cccccc"
81 },
82 content: []
83 },
84 labels: {
85 outer: {
86 format: "label",
87 hideWhenLessThanPercentage: null,
88 pieDistance: 30
89 },
90 inner: {
91 format: "percentage",
92 hideWhenLessThanPercentage: null
93 },
94 mainLabel: {
95 color: "#333333",
96 font: "arial",
97 fontSize: 10
98 },
99 percentage: {
100 color: "#dddddd",
101 font: "arial",
102 fontSize: 10,
103 decimalPlaces: 0
104 },
105 value: {
106 color: "#cccc44",
107 font: "arial",
108 fontSize: 10
109 },
110 lines: {
111 enabled: true,
112 style: "curved",
113 color: "segment"
114 },
115 truncation: {
116 enabled: false,
117 length: 30
118 }
119 },
120 effects: {
121 load: {
122 effect: "default",
123 speed: 1000
124 },
125 pullOutSegmentOnClick: {
126 effect: "bounce",
127 speed: 300,
128 size: 10
129 },
130 highlightSegmentOnMouseover: true,
131 highlightLuminosity: -0.2
132 },
133 tooltips: {
134 enabled: false,
135 type: "placeholder", // caption|placeholder
136 string: "",
137 placeholderParser: null,
138 styles: {
139 fadeInSpeed: 250,
140 backgroundColor: "#000000",
141 backgroundOpacity: 0.5,
142 color: "#efefef",
143 borderRadius: 2,
144 font: "arial",
145 fontSize: 10,
146 padding: 4
147 }
148 },
149 misc: {
150 colors: {
151 background: null,
152 segments: [
153 "#2484c1", "#65a620", "#7b6888", "#a05d56", "#961a1a", "#d8d23a", "#e98125", "#d0743c", "#635222", "#6ada6a",
154 "#0c6197", "#7d9058", "#207f33", "#44b9b0", "#bca44a", "#e4a14b", "#a3acb2", "#8cc3e9", "#69a6f9", "#5b388f",
155 "#546e91", "#8bde95", "#d2ab58", "#273c71", "#98bf6e", "#4daa4b", "#98abc5", "#cc1010", "#31383b", "#006391",
156 "#c2643f", "#b0a474", "#a5a39c", "#a9c2bc", "#22af8c", "#7fcecf", "#987ac6", "#3d3b87", "#b77b1c", "#c9c2b6",
157 "#807ece", "#8db27c", "#be66a2", "#9ed3c6", "#00644b", "#005064", "#77979f", "#77e079", "#9c73ab", "#1f79a7"
158 ],
159 segmentStroke: "#ffffff"
160 },
161 gradient: {
162 enabled: false,
163 percentage: 95,
164 color: "#000000"
165 },
166 canvasPadding: {
167 top: 5,
168 right: 5,
169 bottom: 5,
170 left: 5
171 },
172 pieCenterOffset: {
173 x: 0,
174 y: 0
175 },
176 cssPrefix: null
177 },
178 callbacks: {
179 onload: null,
180 onMouseoverSegment: null,
181 onMouseoutSegment: null,
182 onClickSegment: null
183 }
184 };
185
186 //// --------- validate.js -----------
187 var validate = {
188
189 // called whenever a new pie chart is created
190 initialCheck: function(pie) {
191 var cssPrefix = pie.cssPrefix;
192 var element = pie.element;
193 var options = pie.options;
194
195 // confirm d3 is available [check minimum version]
196 if (!window.d3 || !window.d3.hasOwnProperty("version")) {
197 console.error("d3pie error: d3 is not available");
198 return false;
199 }
200
201 // confirm element is either a DOM element or a valid string for a DOM element
202 if (!(element instanceof HTMLElement)) {
203 console.error("d3pie error: the first d3pie() param must be a valid DOM element (not jQuery) or a ID string.");
204 return false;
205 }
206
207 // confirm the CSS prefix is valid. It has to start with a-Z and contain nothing but a-Z0-9_-
208 if (!(/[a-zA-Z][a-zA-Z0-9_-]*$/.test(cssPrefix))) {
209 console.error("d3pie error: invalid options.misc.cssPrefix");
210 return false;
211 }
212
213 // confirm some data has been supplied
214 if (!helpers.isArray(options.data.content)) {
215 console.error("d3pie error: invalid config structure: missing data.content property.");
216 return false;
217 }
218 if (options.data.content.length === 0) {
219 console.error("d3pie error: no data supplied.");
220 return false;
221 }
222
223 // clear out any invalid data. Each data row needs a valid positive number and a label
224 var data = [];
225 for (var i=0; i<options.data.content.length; i++) {
226 if (typeof options.data.content[i].value !== "number" || isNaN(options.data.content[i].value)) {
227 console.log("not valid: ", options.data.content[i]);
228 continue;
229 }
230 if (options.data.content[i].value <= 0) {
231 console.log("not valid - should have positive value: ", options.data.content[i]);
232 continue;
233 }
234 data.push(options.data.content[i]);
235 }
236 pie.options.data.content = data;
237
238 // labels.outer.hideWhenLessThanPercentage - 1-100
239 // labels.inner.hideWhenLessThanPercentage - 1-100
240
241 return true;
242 }
243 };
244 //// --------- helpers.js -----------
245 var helpers = {
246
247 // creates the SVG element
248 addSVGSpace: function(pie) {
249 var element = pie.element;
250 var canvasWidth = pie.options.size.canvasWidth;
251 var canvasHeight = pie.options.size.canvasHeight;
252 var backgroundColor = pie.options.misc.colors.background;
253
254 var svg = d3.select(element).append("svg:svg")
255 .attr("width", canvasWidth)
256 .attr("height", canvasHeight);
257
258 if (backgroundColor !== "transparent") {
259 svg.style("background-color", function() { return backgroundColor; });
260 }
261
262 return svg;
263 },
264
265 whenIdExists: function(id, callback) {
266 var inc = 1;
267 var giveupIterationCount = 1000;
268
269 var interval = setInterval(function() {
270 if (document.getElementById(id)) {
271 clearInterval(interval);
272 callback();
273 }
274 if (inc > giveupIterationCount) {
275 clearInterval(interval);
276 }
277 inc++;
278 }, 1);
279 },
280
281 whenElementsExist: function(els, callback) {
282 var inc = 1;
283 var giveupIterationCount = 1000;
284
285 var interval = setInterval(function() {
286 var allExist = true;
287 for (var i=0; i<els.length; i++) {
288 if (!document.getElementById(els[i])) {
289 allExist = false;
290 break;
291 }
292 }
293 if (allExist) {
294 clearInterval(interval);
295 callback();
296 }
297 if (inc > giveupIterationCount) {
298 clearInterval(interval);
299 }
300 inc++;
301 }, 1);
302 },
303
304 shuffleArray: function(array) {
305 var currentIndex = array.length, tmpVal, randomIndex;
306
307 while (0 !== currentIndex) {
308 randomIndex = Math.floor(Math.random() * currentIndex);
309 currentIndex -= 1;
310
311 // and swap it with the current element
312 tmpVal = array[currentIndex];
313 array[currentIndex] = array[randomIndex];
314 array[randomIndex] = tmpVal;
315 }
316 return array;
317 },
318
319 processObj: function(obj, is, value) {
320 if (typeof is === 'string') {
321 return helpers.processObj(obj, is.split('.'), value);
322 } else if (is.length === 1 && value !== undefined) {
323 obj[is[0]] = value;
324 return obj[is[0]];
325 } else if (is.length === 0) {
326 return obj;
327 } else {
328 return helpers.processObj(obj[is[0]], is.slice(1), value);
329 }
330 },
331
332 getDimensions: function(id) {
333 var el = document.getElementById(id);
334 var w = 0, h = 0;
335 if (el) {
336 var dimensions = el.getBBox();
337 w = dimensions.width;
338 h = dimensions.height;
339 } else {
340 console.log("error: getDimensions() " + id + " not found.");
341 }
342 return { w: w, h: h };
343 },
344
345 /**
346 * This is based on the SVG coordinate system, where top-left is 0,0 and bottom right is n-n.
347 * @param r1
348 * @param r2
349 * @returns {boolean}
350 */
351 rectIntersect: function(r1, r2) {
352 var returnVal = (
353 // r2.left > r1.right
354 (r2.x > (r1.x + r1.w)) ||
355
356 // r2.right < r1.left
357 ((r2.x + r2.w) < r1.x) ||
358
359 // r2.top < r1.bottom
360 ((r2.y + r2.h) < r1.y) ||
361
362 // r2.bottom > r1.top
363 (r2.y > (r1.y + r1.h))
364 );
365
366 return !returnVal;
367 },
368
369 /**
370 * Returns a lighter/darker shade of a hex value, based on a luminance value passed.
371 * @param hex a hex color value such as “#abc” or “#123456″ (the hash is optional)
372 * @param lum the luminosity factor: -0.1 is 10% darker, 0.2 is 20% lighter, etc.
373 * @returns {string}
374 */
375 getColorShade: function(hex, lum) {
376
377 // validate hex string
378 hex = String(hex).replace(/[^0-9a-f]/gi, '');
379 if (hex.length < 6) {
380 hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
381 }
382 lum = lum || 0;
383
384 // convert to decimal and change luminosity
385 var newHex = "#";
386 for (var i=0; i<3; i++) {
387 var c = parseInt(hex.substr(i * 2, 2), 16);
388 c = Math.round(Math.min(Math.max(0, c + (c * lum)), 255)).toString(16);
389 newHex += ("00" + c).substr(c.length);
390 }
391
392 return newHex;
393 },
394
395 /**
396 * Users can choose to specify segment colors in three ways (in order of precedence):
397 * 1. include a "color" attribute for each row in data.content
398 * 2. include a misc.colors.segments property which contains an array of hex codes
399 * 3. specify nothing at all and rely on this lib provide some reasonable defaults
400 *
401 * This function sees what's included and populates this.options.colors with whatever's required
402 * for this pie chart.
403 * @param data
404 */
405 initSegmentColors: function(pie) {
406 var data = pie.options.data.content;
407 var colors = pie.options.misc.colors.segments;
408
409 // TODO this needs a ton of error handling
410
411 var finalColors = [];
412 for (var i=0; i<data.length; i++) {
413 if (data[i].hasOwnProperty("color")) {
414 finalColors.push(data[i].color);
415 } else {
416 finalColors.push(colors[i]);
417 }
418 }
419
420 return finalColors;
421 },
422
423 applySmallSegmentGrouping: function(data, smallSegmentGrouping) {
424 var totalSize;
425 if (smallSegmentGrouping.valueType === "percentage") {
426 totalSize = math.getTotalPieSize(data);
427 }
428
429 // loop through each data item
430 var newData = [];
431 var groupedData = [];
432 var totalGroupedData = 0;
433 for (var i=0; i<data.length; i++) {
434 if (smallSegmentGrouping.valueType === "percentage") {
435 var dataPercent = (data[i].value / totalSize) * 100;
436 if (dataPercent <= smallSegmentGrouping.value) {
437 groupedData.push(data[i]);
438 totalGroupedData += data[i].value;
439 continue;
440 }
441 data[i].isGrouped = false;
442 newData.push(data[i]);
443 } else {
444 if (data[i].value <= smallSegmentGrouping.value) {
445 groupedData.push(data[i]);
446 totalGroupedData += data[i].value;
447 continue;
448 }
449 data[i].isGrouped = false;
450 newData.push(data[i]);
451 }
452 }
453
454 // we're done! See if there's any small segment groups to add
455 if (groupedData.length) {
456 newData.push({
457 color: smallSegmentGrouping.color,
458 label: smallSegmentGrouping.label,
459 value: totalGroupedData,
460 isGrouped: true,
461 groupedData: groupedData
462 });
463 }
464
465 return newData;
466 },
467
468 // for debugging
469 showPoint: function(svg, x, y) {
470 svg.append("circle").attr("cx", x).attr("cy", y).attr("r", 2).style("fill", "black");
471 },
472
473 isFunction: function(functionToCheck) {
474 var getType = {};
475 return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
476 },
477
478 isArray: function(o) {
479 return Object.prototype.toString.call(o) === '[object Array]';
480 }
481 };
482
483
484 // taken from jQuery
485 var extend = function() {
486 var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
487 i = 1,
488 length = arguments.length,
489 deep = false,
490 toString = Object.prototype.toString,
491 hasOwn = Object.prototype.hasOwnProperty,
492 class2type = {
493 "[object Boolean]": "boolean",
494 "[object Number]": "number",
495 "[object String]": "string",
496 "[object Function]": "function",
497 "[object Array]": "array",
498 "[object Date]": "date",
499 "[object RegExp]": "regexp",
500 "[object Object]": "object"
501 },
502
503 jQuery = {
504 isFunction: function (obj) {
505 return jQuery.type(obj) === "function";
506 },
507 isArray: Array.isArray ||
508 function (obj) {
509 return jQuery.type(obj) === "array";
510 },
511 isWindow: function (obj) {
512 return obj !== null && obj === obj.window;
513 },
514 isNumeric: function (obj) {
515 return !isNaN(parseFloat(obj)) && isFinite(obj);
516 },
517 type: function (obj) {
518 return obj === null ? String(obj) : class2type[toString.call(obj)] || "object";
519 },
520 isPlainObject: function (obj) {
521 if (!obj || jQuery.type(obj) !== "object" || obj.nodeType) {
522 return false;
523 }
524 try {
525 if (obj.constructor && !hasOwn.call(obj, "constructor") && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) {
526 return false;
527 }
528 } catch (e) {
529 return false;
530 }
531 var key;
532 for (key in obj) {}
533 return key === undefined || hasOwn.call(obj, key);
534 }
535 };
536 if (typeof target === "boolean") {
537 deep = target;
538 target = arguments[1] || {};
539 i = 2;
540 }
541 if (typeof target !== "object" && !jQuery.isFunction(target)) {
542 target = {};
543 }
544 if (length === i) {
545 target = this;
546 --i;
547 }
548 for (i; i < length; i++) {
549 if ((options = arguments[i]) !== null) {
550 for (name in options) {
551 src = target[name];
552 copy = options[name];
553 if (target === copy) {
554 continue;
555 }
556 if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
557 if (copyIsArray) {
558 copyIsArray = false;
559 clone = src && jQuery.isArray(src) ? src : [];
560 } else {
561 clone = src && jQuery.isPlainObject(src) ? src : {};
562 }
563 // WARNING: RECURSION
564 target[name] = extend(deep, clone, copy);
565 } else if (copy !== undefined) {
566 target[name] = copy;
567 }
568 }
569 }
570 }
571 return target;
572 };
573 //// --------- math.js -----------
574 var math = {
575
576 toRadians: function(degrees) {
577 return degrees * (Math.PI / 180);
578 },
579
580 toDegrees: function(radians) {
581 return radians * (180 / Math.PI);
582 },
583
584 computePieRadius: function(pie) {
585 var size = pie.options.size;
586 var canvasPadding = pie.options.misc.canvasPadding;
587
588 // outer radius is either specified (e.g. through the generator), or omitted altogether
589 // and calculated based on the canvas dimensions. Right now the estimated version isn't great - it should
590 // be possible to calculate it to precisely generate the maximum sized pie, but it's fussy as heck. Something
591 // for the next release.
592
593 // first, calculate the default _outerRadius
594 var w = size.canvasWidth - canvasPadding.left - canvasPadding.right;
595 var h = size.canvasHeight - canvasPadding.top - canvasPadding.bottom;
596
597 // now factor in the footer, title & subtitle
598 if (pie.options.header.location !== "pie-center") {
599 h -= pie.textComponents.headerHeight;
600 }
601
602 if (pie.textComponents.footer.exists) {
603 h -= pie.textComponents.footer.h;
604 }
605
606 // for really teeny pies, h may be < 0. Adjust it back
607 h = (h < 0) ? 0 : h;
608
609 var outerRadius = ((w < h) ? w : h) / 3;
610 var innerRadius, percent;
611
612 // if the user specified something, use that instead
613 if (size.pieOuterRadius !== null) {
614 if (/%/.test(size.pieOuterRadius)) {
615 percent = parseInt(size.pieOuterRadius.replace(/[\D]/, ""), 10);
616 percent = (percent > 99) ? 99 : percent;
617 percent = (percent < 0) ? 0 : percent;
618
619 var smallestDimension = (w < h) ? w : h;
620
621 // now factor in the label line size
622 if (pie.options.labels.outer.format !== "none") {
623 var pieDistanceSpace = parseInt(pie.options.labels.outer.pieDistance, 10) * 2;
624 if (smallestDimension - pieDistanceSpace > 0) {
625 smallestDimension -= pieDistanceSpace;
626 }
627 }
628
629 outerRadius = Math.floor((smallestDimension / 100) * percent) / 2;
630 } else {
631 outerRadius = parseInt(size.pieOuterRadius, 10);
632 }
633 }
634
635 // inner radius
636 if (/%/.test(size.pieInnerRadius)) {
637 percent = parseInt(size.pieInnerRadius.replace(/[\D]/, ""), 10);
638 percent = (percent > 99) ? 99 : percent;
639 percent = (percent < 0) ? 0 : percent;
640 innerRadius = Math.floor((outerRadius / 100) * percent);
641 } else {
642 innerRadius = parseInt(size.pieInnerRadius, 10);
643 }
644
645 pie.innerRadius = innerRadius;
646 pie.outerRadius = outerRadius;
647 },
648
649 getTotalPieSize: function(data) {
650 var totalSize = 0;
651 for (var i=0; i<data.length; i++) {
652 totalSize += data[i].value;
653 }
654 return totalSize;
655 },
656
657 sortPieData: function(pie) {
658 var data = pie.options.data.content;
659 var sortOrder = pie.options.data.sortOrder;
660
661 switch (sortOrder) {
662 case "none":
663 // do nothing
664 break;
665 case "random":
666 data = helpers.shuffleArray(data);
667 break;
668 case "value-asc":
669 data.sort(function(a, b) { return (a.value < b.value) ? -1 : 1; });
670 break;
671 case "value-desc":
672 data.sort(function(a, b) { return (a.value < b.value) ? 1 : -1; });
673 break;
674 case "label-asc":
675 data.sort(function(a, b) { return (a.label.toLowerCase() > b.label.toLowerCase()) ? 1 : -1; });
676 break;
677 case "label-desc":
678 data.sort(function(a, b) { return (a.label.toLowerCase() < b.label.toLowerCase()) ? 1 : -1; });
679 break;
680 }
681
682 return data;
683 },
684
685
686
687 // var pieCenter = math.getPieCenter();
688 getPieTranslateCenter: function(pieCenter) {
689 return "translate(" + pieCenter.x + "," + pieCenter.y + ")";
690 },
691
692 /**
693 * Used to determine where on the canvas the center of the pie chart should be. It takes into account the
694 * height and position of the title, subtitle and footer, and the various paddings.
695 * @private
696 */
697 calculatePieCenter: function(pie) {
698 var pieCenterOffset = pie.options.misc.pieCenterOffset;
699 var hasTopTitle = (pie.textComponents.title.exists && pie.options.header.location !== "pie-center");
700 var hasTopSubtitle = (pie.textComponents.subtitle.exists && pie.options.header.location !== "pie-center");
701
702 var headerOffset = pie.options.misc.canvasPadding.top;
703 if (hasTopTitle && hasTopSubtitle) {
704 headerOffset += pie.textComponents.title.h + pie.options.header.titleSubtitlePadding + pie.textComponents.subtitle.h;
705 } else if (hasTopTitle) {
706 headerOffset += pie.textComponents.title.h;
707 } else if (hasTopSubtitle) {
708 headerOffset += pie.textComponents.subtitle.h;
709 }
710
711 var footerOffset = 0;
712 if (pie.textComponents.footer.exists) {
713 footerOffset = pie.textComponents.footer.h + pie.options.misc.canvasPadding.bottom;
714 }
715
716 var x = ((pie.options.size.canvasWidth - pie.options.misc.canvasPadding.left - pie.options.misc.canvasPadding.right) / 2) + pie.options.misc.canvasPadding.left;
717 var y = ((pie.options.size.canvasHeight - footerOffset - headerOffset) / 2) + headerOffset;
718
719 x += pieCenterOffset.x;
720 y += pieCenterOffset.y;
721
722 pie.pieCenter = { x: x, y: y };
723 },
724
725
726 /**
727 * Rotates a point (x, y) around an axis (xm, ym) by degrees (a).
728 * @param x
729 * @param y
730 * @param xm
731 * @param ym
732 * @param a angle in degrees
733 * @returns {Array}
734 */
735 rotate: function(x, y, xm, ym, a) {
736
737 a = a * Math.PI / 180; // convert to radians
738
739 var cos = Math.cos,
740 sin = Math.sin,
741 // subtract midpoints, so that midpoint is translated to origin and add it in the end again
742 xr = (x - xm) * cos(a) - (y - ym) * sin(a) + xm,
743 yr = (x - xm) * sin(a) + (y - ym) * cos(a) + ym;
744
745 return { x: xr, y: yr };
746 },
747
748 /**
749 * Translates a point x, y by distance d, and by angle a.
750 * @param x
751 * @param y
752 * @param dist
753 * @param a angle in degrees
754 */
755 translate: function(x, y, d, a) {
756 var rads = math.toRadians(a);
757 return {
758 x: x + d * Math.sin(rads),
759 y: y - d * Math.cos(rads)
760 };
761 },
762
763 // from: http://stackoverflow.com/questions/19792552/d3-put-arc-labels-in-a-pie-chart-if-there-is-enough-space
764 pointIsInArc: function(pt, ptData, d3Arc) {
765 // Center of the arc is assumed to be 0,0
766 // (pt.x, pt.y) are assumed to be relative to the center
767 var r1 = d3Arc.innerRadius()(ptData), // Note: Using the innerRadius
768 r2 = d3Arc.outerRadius()(ptData),
769 theta1 = d3Arc.startAngle()(ptData),
770 theta2 = d3Arc.endAngle()(ptData);
771
772 var dist = pt.x * pt.x + pt.y * pt.y,
773 angle = Math.atan2(pt.x, -pt.y); // Note: different coordinate system
774
775 angle = (angle < 0) ? (angle + Math.PI * 2) : angle;
776
777 return (r1 * r1 <= dist) && (dist <= r2 * r2) &&
778 (theta1 <= angle) && (angle <= theta2);
779 }
780 };
781
782 //// --------- labels.js -----------
783 var labels = {
784
785 /**
786 * Adds the labels to the pie chart, but doesn't position them. There are two locations for the
787 * labels: inside (center) of the segments, or outside the segments on the edge.
788 * @param section "inner" or "outer"
789 * @param sectionDisplayType "percentage", "value", "label", "label-value1", etc.
790 * @param pie
791 */
792 add: function(pie, section, sectionDisplayType) {
793 var include = labels.getIncludes(sectionDisplayType);
794 var settings = pie.options.labels;
795
796 // group the label groups (label, percentage, value) into a single element for simpler positioning
797 var outerLabel = pie.svg.insert("g", "." + pie.cssPrefix + "labels-" + section)
798 .attr("class", pie.cssPrefix + "labels-" + section);
799
800 var labelGroup = outerLabel.selectAll("." + pie.cssPrefix + "labelGroup-" + section)
801 .data(pie.options.data.content)
802 .enter()
803 .append("g")
804 .attr("id", function(d, i) { return pie.cssPrefix + "labelGroup" + i + "-" + section; })
805 .attr("data-index", function(d, i) { return i; })
806 .attr("class", pie.cssPrefix + "labelGroup-" + section)
807 .style("opacity", 0);
808
809 // 1. Add the main label
810 if (include.mainLabel) {
811 labelGroup.append("text")
812 .attr("id", function(d, i) { return pie.cssPrefix + "segmentMainLabel" + i + "-" + section; })
813 .attr("class", pie.cssPrefix + "segmentMainLabel-" + section)
814 .text(function(d) {
815 var str = d.label;
816 if (settings.truncation.enabled && d.label.length > settings.truncation.length) {
817 str = d.label.substring(0, settings.truncation.length) + "...";
818 }
819 return str;
820 })
821 .style("font-size", settings.mainLabel.fontSize + "px")
822 .style("font-family", settings.mainLabel.font)
823 .style("fill", settings.mainLabel.color);
824 }
825
826 // 2. Add the percentage label
827 if (include.percentage) {
828 labelGroup.append("text")
829 .attr("id", function(d, i) { return pie.cssPrefix + "segmentPercentage" + i + "-" + section; })
830 .attr("class", pie.cssPrefix + "segmentPercentage-" + section)
831 .text(function(d, i) {
832 return segments.getPercentage(pie, i) + "%";
833 })
834 .style("font-size", settings.percentage.fontSize + "px")
835 .style("font-family", settings.percentage.font)
836 .style("fill", settings.percentage.color);
837 }
838
839 // 3. Add the value label
840 if (include.value) {
841 labelGroup.append("text")
842 .attr("id", function(d, i) { return pie.cssPrefix + "segmentValue" + i + "-" + section; })
843 .attr("class", pie.cssPrefix + "segmentValue-" + section)
844 .text(function(d) { return d.value; })
845 .style("font-size", settings.value.fontSize + "px")
846 .style("font-family", settings.value.font)
847 .style("fill", settings.value.color);
848 }
849 },
850
851 /**
852 * @param section "inner" / "outer"
853 */
854 positionLabelElements: function(pie, section, sectionDisplayType) {
855 labels["dimensions-" + section] = [];
856
857 // get the latest widths, heights
858 var labelGroups = d3.selectAll("." + pie.cssPrefix + "labelGroup-" + section);
859 labelGroups.each(function(d, i) {
860 var mainLabel = d3.select(this).selectAll("." + pie.cssPrefix + "segmentMainLabel-" + section);
861 var percentage = d3.select(this).selectAll("." + pie.cssPrefix + "segmentPercentage-" + section);
862 var value = d3.select(this).selectAll("." + pie.cssPrefix + "segmentValue-" + section);
863
864 labels["dimensions-" + section].push({
865 mainLabel: (mainLabel.node() !== null) ? mainLabel.node().getBBox() : null,
866 percentage: (percentage.node() !== null) ? percentage.node().getBBox() : null,
867 value: (value.node() !== null) ? value.node().getBBox() : null
868 });
869 });
870
871 var singleLinePad = 5;
872 var dims = labels["dimensions-" + section];
873 switch (sectionDisplayType) {
874 case "label-value1":
875 d3.selectAll("." + pie.cssPrefix + "segmentValue-" + section)
876 .attr("dx", function(d, i) { return dims[i].mainLabel.width + singleLinePad; });
877 break;
878 case "label-value2":
879 d3.selectAll("." + pie.cssPrefix + "segmentValue-" + section)
880 .attr("dy", function(d, i) { return dims[i].mainLabel.height; });
881 break;
882 case "label-percentage1":
883 d3.selectAll("." + pie.cssPrefix + "segmentPercentage-" + section)
884 .attr("dx", function(d, i) { return dims[i].mainLabel.width + singleLinePad; });
885 break;
886 case "label-percentage2":
887 d3.selectAll("." + pie.cssPrefix + "segmentPercentage-" + section)
888 .attr("dx", function(d, i) { return (dims[i].mainLabel.width / 2) - (dims[i].percentage.width / 2); })
889 .attr("dy", function(d, i) { return dims[i].mainLabel.height; });
890 break;
891 }
892 },
893
894 computeLabelLinePositions: function(pie) {
895 pie.lineCoordGroups = [];
896 d3.selectAll("." + pie.cssPrefix + "labelGroup-outer")
897 .each(function(d, i) { return labels.computeLinePosition(pie, i); });
898 },
899
900 computeLinePosition: function(pie, i) {
901 var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true });
902 var originCoords = math.rotate(pie.pieCenter.x, pie.pieCenter.y - pie.outerRadius, pie.pieCenter.x, pie.pieCenter.y, angle);
903 var heightOffset = pie.outerLabelGroupData[i].h / 5; // TODO check
904 var labelXMargin = 6; // the x-distance of the label from the end of the line [TODO configurable]
905
906 var quarter = Math.floor(angle / 90);
907 var midPoint = 4;
908 var x2, y2, x3, y3;
909
910 // this resolves an issue when the
911 if (quarter === 2 && angle === 180) {
912 quarter = 1;
913 }
914
915 switch (quarter) {
916 case 0:
917 x2 = pie.outerLabelGroupData[i].x - labelXMargin - ((pie.outerLabelGroupData[i].x - labelXMargin - originCoords.x) / 2);
918 y2 = pie.outerLabelGroupData[i].y + ((originCoords.y - pie.outerLabelGroupData[i].y) / midPoint);
919 x3 = pie.outerLabelGroupData[i].x - labelXMargin;
920 y3 = pie.outerLabelGroupData[i].y - heightOffset;
921 break;
922 case 1:
923 x2 = originCoords.x + (pie.outerLabelGroupData[i].x - originCoords.x) / midPoint;
924 y2 = originCoords.y + (pie.outerLabelGroupData[i].y - originCoords.y) / midPoint;
925 x3 = pie.outerLabelGroupData[i].x - labelXMargin;
926 y3 = pie.outerLabelGroupData[i].y - heightOffset;
927 break;
928 case 2:
929 var startOfLabelX = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin;
930 x2 = originCoords.x - (originCoords.x - startOfLabelX) / midPoint;
931 y2 = originCoords.y + (pie.outerLabelGroupData[i].y - originCoords.y) / midPoint;
932 x3 = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin;
933 y3 = pie.outerLabelGroupData[i].y - heightOffset;
934 break;
935 case 3:
936 var startOfLabel = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin;
937 x2 = startOfLabel + ((originCoords.x - startOfLabel) / midPoint);
938 y2 = pie.outerLabelGroupData[i].y + (originCoords.y - pie.outerLabelGroupData[i].y) / midPoint;
939 x3 = pie.outerLabelGroupData[i].x + pie.outerLabelGroupData[i].w + labelXMargin;
940 y3 = pie.outerLabelGroupData[i].y - heightOffset;
941 break;
942 }
943
944 /*
945 * x1 / y1: the x/y coords of the start of the line, at the mid point of the segments arc on the pie circumference
946 * x2 / y2: if "curved" line style is being used, this is the midpoint of the line. Other
947 * x3 / y3: the end of the line; closest point to the label
948 */
949 if (pie.options.labels.lines.style === "straight") {
950 pie.lineCoordGroups[i] = [
951 { x: originCoords.x, y: originCoords.y },
952 { x: x3, y: y3 }
953 ];
954 } else {
955 pie.lineCoordGroups[i] = [
956 { x: originCoords.x, y: originCoords.y },
957 { x: x2, y: y2 },
958 { x: x3, y: y3 }
959 ];
960 }
961 },
962
963 addLabelLines: function(pie) {
964 var lineGroups = pie.svg.insert("g", "." + pie.cssPrefix + "pieChart") // meaning, BEFORE .pieChart
965 .attr("class", pie.cssPrefix + "lineGroups")
966 .style("opacity", 0);
967
968 var lineGroup = lineGroups.selectAll("." + pie.cssPrefix + "lineGroup")
969 .data(pie.lineCoordGroups)
970 .enter()
971 .append("g")
972 .attr("class", pie.cssPrefix + "lineGroup");
973
974 var lineFunction = d3.svg.line()
975 .interpolate("basis")
976 .x(function(d) { return d.x; })
977 .y(function(d) { return d.y; });
978
979 lineGroup.append("path")
980 .attr("d", lineFunction)
981 .attr("stroke", function(d, i) {
982 return (pie.options.labels.lines.color === "segment") ? pie.options.colors[i] : pie.options.labels.lines.color;
983 })
984 .attr("stroke-width", 1)
985 .attr("fill", "none")
986 .style("opacity", function(d, i) {
987 var percentage = pie.options.labels.outer.hideWhenLessThanPercentage;
988 var segmentPercentage = segments.getPercentage(pie, i);
989 var isHidden = (percentage !== null && segmentPercentage < percentage) || pie.options.data.content[i].label === "";
990 return isHidden ? 0 : 1;
991 });
992 },
993
994 positionLabelGroups: function(pie, section) {
995 d3.selectAll("." + pie.cssPrefix + "labelGroup-" + section)
996 .style("opacity", 0)
997 .attr("transform", function(d, i) {
998 var x, y;
999 if (section === "outer") {
1000 x = pie.outerLabelGroupData[i].x;
1001 y = pie.outerLabelGroupData[i].y;
1002 } else {
1003 var pieCenterCopy = extend(true, {}, pie.pieCenter);
1004
1005 // now recompute the "center" based on the current _innerRadius
1006 if (pie.innerRadius > 0) {
1007 var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true });
1008 var newCoords = math.translate(pie.pieCenter.x, pie.pieCenter.y, pie.innerRadius, angle);
1009 pieCenterCopy.x = newCoords.x;
1010 pieCenterCopy.y = newCoords.y;
1011 }
1012
1013 var dims = helpers.getDimensions(pie.cssPrefix + "labelGroup" + i + "-inner");
1014 var xOffset = dims.w / 2;
1015 var yOffset = dims.h / 4; // confusing! Why 4? should be 2, but it doesn't look right
1016
1017 x = pieCenterCopy.x + (pie.lineCoordGroups[i][0].x - pieCenterCopy.x) / 1.8;
1018 y = pieCenterCopy.y + (pie.lineCoordGroups[i][0].y - pieCenterCopy.y) / 1.8;
1019
1020 x = x - xOffset;
1021 y = y + yOffset;
1022 }
1023
1024 return "translate(" + x + "," + y + ")";
1025 });
1026 },
1027
1028
1029 fadeInLabelsAndLines: function(pie) {
1030
1031 // fade in the labels when the load effect is complete - or immediately if there's no load effect
1032 var loadSpeed = (pie.options.effects.load.effect === "default") ? pie.options.effects.load.speed : 1;
1033 setTimeout(function() {
1034 var labelFadeInTime = (pie.options.effects.load.effect === "default") ? 400 : 1; // 400 is hardcoded for the present
1035
1036 d3.selectAll("." + pie.cssPrefix + "labelGroup-outer")
1037 .transition()
1038 .duration(labelFadeInTime)
1039 .style("opacity", function(d, i) {
1040 var percentage = pie.options.labels.outer.hideWhenLessThanPercentage;
1041 var segmentPercentage = segments.getPercentage(pie, i);
1042 return (percentage !== null && segmentPercentage < percentage) ? 0 : 1;
1043 });
1044
1045 d3.selectAll("." + pie.cssPrefix + "labelGroup-inner")
1046 .transition()
1047 .duration(labelFadeInTime)
1048 .style("opacity", function(d, i) {
1049 var percentage = pie.options.labels.inner.hideWhenLessThanPercentage;
1050 var segmentPercentage = segments.getPercentage(pie, i);
1051 return (percentage !== null && segmentPercentage < percentage) ? 0 : 1;
1052 });
1053
1054 d3.selectAll("g." + pie.cssPrefix + "lineGroups")
1055 .transition()
1056 .duration(labelFadeInTime)
1057 .style("opacity", 1);
1058
1059 // once everything's done loading, trigger the onload callback if defined
1060 if (helpers.isFunction(pie.options.callbacks.onload)) {
1061 setTimeout(function() {
1062 try {
1063 pie.options.callbacks.onload();
1064 } catch (e) { }
1065 }, labelFadeInTime);
1066 }
1067 }, loadSpeed);
1068 },
1069
1070 getIncludes: function(val) {
1071 var addMainLabel = false;
1072 var addValue = false;
1073 var addPercentage = false;
1074
1075 // TODO refactor... somehow
1076 switch (val) {
1077 case "label":
1078 addMainLabel = true;
1079 break;
1080 case "value":
1081 addValue = true;
1082 break;
1083 case "percentage":
1084 addPercentage = true;
1085 break;
1086 case "label-value1":
1087 case "label-value2":
1088 addMainLabel = true;
1089 addValue = true;
1090 break;
1091 case "label-percentage1":
1092 case "label-percentage2":
1093 addMainLabel = true;
1094 addPercentage = true;
1095 break;
1096 }
1097 return {
1098 mainLabel: addMainLabel,
1099 value: addValue,
1100 percentage: addPercentage
1101 };
1102 },
1103
1104
1105 /**
1106 * This does the heavy-lifting to compute the actual coordinates for the outer label groups. It does two things:
1107 * 1. Make a first pass and position them in the ideal positions, based on the pie sizes
1108 * 2. Do some basic collision avoidance.
1109 */
1110 computeOuterLabelCoords: function(pie) {
1111
1112 // 1. figure out the ideal positions for the outer labels
1113 pie.svg.selectAll("." + pie.cssPrefix + "labelGroup-outer")
1114 .each(function(d, i) {
1115 return labels.getIdealOuterLabelPositions(pie, i);
1116 });
1117
1118 // 2. now adjust those positions to try to accommodate conflicts
1119 labels.resolveOuterLabelCollisions(pie);
1120 },
1121
1122 /**
1123 * This attempts to resolve label positioning collisions.
1124 */
1125 resolveOuterLabelCollisions: function(pie) {
1126 var size = pie.options.data.content.length;
1127 labels.checkConflict(pie, 0, "clockwise", size);
1128 labels.checkConflict(pie, size-1, "anticlockwise", size);
1129 },
1130
1131 checkConflict: function(pie, currIndex, direction, size) {
1132 var i,curr;
1133
1134 if (size <= 1) {
1135 return;
1136 }
1137
1138 var currIndexHemisphere = pie.outerLabelGroupData[currIndex].hs;
1139 if (direction === "clockwise" && currIndexHemisphere !== "right") {
1140 return;
1141 }
1142 if (direction === "anticlockwise" && currIndexHemisphere !== "left") {
1143 return;
1144 }
1145 var nextIndex = (direction === "clockwise") ? currIndex+1 : currIndex-1;
1146
1147 // this is the current label group being looked at. We KNOW it's positioned properly (the first item
1148 // is always correct)
1149 var currLabelGroup = pie.outerLabelGroupData[currIndex];
1150
1151 // this one we don't know about. That's the one we're going to look at and move if necessary
1152 var examinedLabelGroup = pie.outerLabelGroupData[nextIndex];
1153
1154 var info = {
1155 labelHeights: pie.outerLabelGroupData[0].h,
1156 center: pie.pieCenter,
1157 lineLength: (pie.outerRadius + pie.options.labels.outer.pieDistance),
1158 heightChange: pie.outerLabelGroupData[0].h + 1 // 1 = padding
1159 };
1160
1161 // loop through *ALL* label groups examined so far to check for conflicts. This is because when they're
1162 // very tightly fitted, a later label group may still appear high up on the page
1163 if (direction === "clockwise") {
1164 i=0;
1165 for (; i<=currIndex; i++) {
1166 curr = pie.outerLabelGroupData[i];
1167
1168 // if there's a conflict with this label group, shift the label to be AFTER the last known
1169 // one that's been properly placed
1170 if (helpers.rectIntersect(curr, examinedLabelGroup)) {
1171 labels.adjustLabelPos(pie, nextIndex, currLabelGroup, info);
1172 break;
1173 }
1174 }
1175 } else {
1176 i=size-1;
1177 for (; i>=currIndex; i--) {
1178 curr = pie.outerLabelGroupData[i];
1179
1180 // if there's a conflict with this label group, shift the label to be AFTER the last known
1181 // one that's been properly placed
1182 if (helpers.rectIntersect(curr, examinedLabelGroup)) {
1183 labels.adjustLabelPos(pie, nextIndex, currLabelGroup, info);
1184 break;
1185 }
1186 }
1187 }
1188 labels.checkConflict(pie, nextIndex, direction, size);
1189 },
1190
1191 // does a little math to shift a label into a new position based on the last properly placed one
1192 adjustLabelPos: function(pie, nextIndex, lastCorrectlyPositionedLabel, info) {
1193 var xDiff, yDiff, newXPos, newYPos;
1194 newYPos = lastCorrectlyPositionedLabel.y + info.heightChange;
1195 yDiff = info.center.y - newYPos;
1196
1197 if (Math.abs(info.lineLength) > Math.abs(yDiff)) {
1198 xDiff = Math.sqrt((info.lineLength * info.lineLength) - (yDiff * yDiff));
1199 } else {
1200 xDiff = Math.sqrt((yDiff * yDiff) - (info.lineLength * info.lineLength));
1201 }
1202
1203 // ahhh! info.lineLength is no longer a constant.....
1204
1205 if (lastCorrectlyPositionedLabel.hs === "right") {
1206 newXPos = info.center.x + xDiff;
1207 } else {
1208 newXPos = info.center.x - xDiff - pie.outerLabelGroupData[nextIndex].w;
1209 }
1210
1211 pie.outerLabelGroupData[nextIndex].x = newXPos;
1212 pie.outerLabelGroupData[nextIndex].y = newYPos;
1213 },
1214
1215 /**
1216 * @param i 0-N where N is the dataset size - 1.
1217 */
1218 getIdealOuterLabelPositions: function(pie, i) {
1219 var labelGroupDims = d3.select("#" + pie.cssPrefix + "labelGroup" + i + "-outer").node().getBBox();
1220 var angle = segments.getSegmentAngle(i, pie.options.data.content, pie.totalSize, { midpoint: true });
1221
1222 var originalX = pie.pieCenter.x;
1223 var originalY = pie.pieCenter.y - (pie.outerRadius + pie.options.labels.outer.pieDistance);
1224 var newCoords = math.rotate(originalX, originalY, pie.pieCenter.x, pie.pieCenter.y, angle);
1225
1226 // if the label is on the left half of the pie, adjust the values
1227 var hemisphere = "right"; // hemisphere
1228 if (angle > 180) {
1229 newCoords.x -= (labelGroupDims.width + 8);
1230 hemisphere = "left";
1231 } else {
1232 newCoords.x += 8;
1233 }
1234
1235 pie.outerLabelGroupData[i] = {
1236 x: newCoords.x,
1237 y: newCoords.y,
1238 w: labelGroupDims.width,
1239 h: labelGroupDims.height,
1240 hs: hemisphere
1241 };
1242 }
1243 };
1244
1245 //// --------- segments.js -----------
1246 var segments = {
1247
1248 /**
1249 * Creates the pie chart segments and displays them according to the desired load effect.
1250 * @private
1251 */
1252 create: function(pie) {
1253 var pieCenter = pie.pieCenter;
1254 var colors = pie.options.colors;
1255 var loadEffects = pie.options.effects.load;
1256 var segmentStroke = pie.options.misc.colors.segmentStroke;
1257
1258 // we insert the pie chart BEFORE the title, to ensure the title overlaps the pie
1259 var pieChartElement = pie.svg.insert("g", "#" + pie.cssPrefix + "title")
1260 .attr("transform", function() { return math.getPieTranslateCenter(pieCenter); })
1261 .attr("class", pie.cssPrefix + "pieChart");
1262
1263 var arc = d3.svg.arc()
1264 .innerRadius(pie.innerRadius)
1265 .outerRadius(pie.outerRadius)
1266 .startAngle(0)
1267 .endAngle(function(d) {
1268 return (d.value / pie.totalSize) * 2 * Math.PI;
1269 });
1270
1271 var g = pieChartElement.selectAll("." + pie.cssPrefix + "arc")
1272 .data(pie.options.data.content)
1273 .enter()
1274 .append("g")
1275 .attr("class", pie.cssPrefix + "arc");
1276
1277 // if we're not fading in the pie, just set the load speed to 0
1278 var loadSpeed = loadEffects.speed;
1279 if (loadEffects.effect === "none") {
1280 loadSpeed = 0;
1281 }
1282
1283 g.append("path")
1284 .attr("id", function(d, i) { return pie.cssPrefix + "segment" + i; })
1285 .attr("fill", function(d, i) {
1286 var color = colors[i];
1287 if (pie.options.misc.gradient.enabled) {
1288 color = "url(#" + pie.cssPrefix + "grad" + i + ")";
1289 }
1290 return color;
1291 })
1292 .style("stroke", segmentStroke)
1293 .style("stroke-width", 1)
1294 .transition()
1295 .ease("cubic-in-out")
1296 .duration(loadSpeed)
1297 .attr("data-index", function(d, i) { return i; })
1298 .attrTween("d", function(b) {
1299 var i = d3.interpolate({ value: 0 }, b);
1300 return function(t) {
1301 return pie.arc(i(t));
1302 };
1303 });
1304
1305 pie.svg.selectAll("g." + pie.cssPrefix + "arc")
1306 .attr("transform",
1307 function(d, i) {
1308 var angle = 0;
1309 if (i > 0) {
1310 angle = segments.getSegmentAngle(i-1, pie.options.data.content, pie.totalSize);
1311 }
1312 return "rotate(" + angle + ")";
1313 }
1314 );
1315 pie.arc = arc;
1316 },
1317
1318 addGradients: function(pie) {
1319 var grads = pie.svg.append("defs")
1320 .selectAll("radialGradient")
1321 .data(pie.options.data.content)
1322 .enter().append("radialGradient")
1323 .attr("gradientUnits", "userSpaceOnUse")
1324 .attr("cx", 0)
1325 .attr("cy", 0)
1326 .attr("r", "120%")
1327 .attr("id", function(d, i) { return pie.cssPrefix + "grad" + i; });
1328
1329 grads.append("stop").attr("offset", "0%").style("stop-color", function(d, i) { return pie.options.colors[i]; });
1330 grads.append("stop").attr("offset", pie.options.misc.gradient.percentage + "%").style("stop-color", pie.options.misc.gradient.color);
1331 },
1332
1333 addSegmentEventHandlers: function(pie) {
1334 var arc = d3.selectAll("." + pie.cssPrefix + "arc,." + pie.cssPrefix + "labelGroup-inner,." + pie.cssPrefix + "labelGroup-outer");
1335
1336 arc.on("click", function() {
1337 var currentEl = d3.select(this);
1338 var segment;
1339
1340 // mouseover works on both the segments AND the segment labels, hence the following
1341 if (currentEl.attr("class") === pie.cssPrefix + "arc") {
1342 segment = currentEl.select("path");
1343 } else {
1344 var index = currentEl.attr("data-index");
1345 segment = d3.select("#" + pie.cssPrefix + "segment" + index);
1346 }
1347 var isExpanded = segment.attr("class") === pie.cssPrefix + "expanded";
1348 segments.onSegmentEvent(pie, pie.options.callbacks.onClickSegment, segment, isExpanded);
1349 if (pie.options.effects.pullOutSegmentOnClick.effect !== "none") {
1350 if (isExpanded) {
1351 segments.closeSegment(pie, segment.node());
1352 } else {
1353 segments.openSegment(pie, segment.node());
1354 }
1355 }
1356 });
1357
1358 arc.on("mouseover", function() {
1359 var currentEl = d3.select(this);
1360 var segment, index;
1361
1362 if (currentEl.attr("class") === pie.cssPrefix + "arc") {
1363 segment = currentEl.select("path");
1364 } else {
1365 index = currentEl.attr("data-index");
1366 segment = d3.select("#" + pie.cssPrefix + "segment" + index);
1367 }
1368
1369 if (pie.options.effects.highlightSegmentOnMouseover) {
1370 index = segment.attr("data-index");
1371 var segColor = pie.options.colors[index];
1372 segment.style("fill", helpers.getColorShade(segColor, pie.options.effects.highlightLuminosity));
1373 }
1374
1375 if (pie.options.tooltips.enabled) {
1376 index = segment.attr("data-index");
1377 tt.showTooltip(pie, index);
1378 }
1379
1380 var isExpanded = segment.attr("class") === pie.cssPrefix + "expanded";
1381 segments.onSegmentEvent(pie, pie.options.callbacks.onMouseoverSegment, segment, isExpanded);
1382 });
1383
1384 arc.on("mousemove", function() {
1385 tt.moveTooltip(pie);
1386 });
1387
1388 arc.on("mouseout", function() {
1389 var currentEl = d3.select(this);
1390 var segment, index;
1391
1392 if (currentEl.attr("class") === pie.cssPrefix + "arc") {
1393 segment = currentEl.select("path");
1394 } else {
1395 index = currentEl.attr("data-index");
1396 segment = d3.select("#" + pie.cssPrefix + "segment" + index);
1397 }
1398
1399 if (pie.options.effects.highlightSegmentOnMouseover) {
1400 index = segment.attr("data-index");
1401 var color = pie.options.colors[index];
1402 if (pie.options.misc.gradient.enabled) {
1403 color = "url(#" + pie.cssPrefix + "grad" + index + ")";
1404 }
1405 segment.style("fill", color);
1406 }
1407
1408 if (pie.options.tooltips.enabled) {
1409 index = segment.attr("data-index");
1410 tt.hideTooltip(pie, index);
1411 }
1412
1413 var isExpanded = segment.attr("class") === pie.cssPrefix + "expanded";
1414 segments.onSegmentEvent(pie, pie.options.callbacks.onMouseoutSegment, segment, isExpanded);
1415 });
1416 },
1417
1418 // helper function used to call the click, mouseover, mouseout segment callback functions
1419 onSegmentEvent: function(pie, func, segment, isExpanded) {
1420 if (!helpers.isFunction(func)) {
1421 return;
1422 }
1423 var index = parseInt(segment.attr("data-index"), 10);
1424 func({
1425 segment: segment.node(),
1426 index: index,
1427 expanded: isExpanded,
1428 data: pie.options.data.content[index]
1429 });
1430 },
1431
1432 openSegment: function(pie, segment) {
1433 if (pie.isOpeningSegment) {
1434 return;
1435 }
1436 pie.isOpeningSegment = true;
1437
1438 // close any open segments
1439 if (d3.selectAll("." + pie.cssPrefix + "expanded").length > 0) {
1440 segments.closeSegment(pie, d3.select("." + pie.cssPrefix + "expanded").node());
1441 }
1442
1443 d3.select(segment).transition()
1444 .ease(pie.options.effects.pullOutSegmentOnClick.effect)
1445 .duration(pie.options.effects.pullOutSegmentOnClick.speed)
1446 .attr("transform", function(d, i) {
1447 var c = pie.arc.centroid(d),
1448 x = c[0],
1449 y = c[1],
1450 h = Math.sqrt(x*x + y*y),
1451 pullOutSize = parseInt(pie.options.effects.pullOutSegmentOnClick.size, 10);
1452
1453 return "translate(" + ((x/h) * pullOutSize) + ',' + ((y/h) * pullOutSize) + ")";
1454 })
1455 .each("end", function(d, i) {
1456 pie.currentlyOpenSegment = segment;
1457 pie.isOpeningSegment = false;
1458 d3.select(this).attr("class", pie.cssPrefix + "expanded");
1459 });
1460 },
1461
1462 closeSegment: function(pie, segment) {
1463 d3.select(segment).transition()
1464 .duration(400)
1465 .attr("transform", "translate(0,0)")
1466 .each("end", function(d, i) {
1467 d3.select(this).attr("class", "");
1468 pie.currentlyOpenSegment = null;
1469 });
1470 },
1471
1472 getCentroid: function(el) {
1473 var bbox = el.getBBox();
1474 return {
1475 x: bbox.x + bbox.width / 2,
1476 y: bbox.y + bbox.height / 2
1477 };
1478 },
1479
1480 /**
1481 * General helper function to return a segment's angle, in various different ways.
1482 * @param index
1483 * @param opts optional object for fine-tuning exactly what you want.
1484 */
1485 getSegmentAngle: function(index, data, totalSize, opts) {
1486 var options = extend({
1487 // if true, this returns the full angle from the origin. Otherwise it returns the single segment angle
1488 compounded: true,
1489
1490 // optionally returns the midpoint of the angle instead of the full angle
1491 midpoint: false
1492 }, opts);
1493
1494 var currValue = data[index].value;
1495 var fullValue;
1496 if (options.compounded) {
1497 fullValue = 0;
1498
1499 // get all values up to and including the specified index
1500 for (var i=0; i<=index; i++) {
1501 fullValue += data[i].value;
1502 }
1503 }
1504
1505 if (typeof fullValue === 'undefined') {
1506 fullValue = currValue;
1507 }
1508
1509 // now convert the full value to an angle
1510 var angle = (fullValue / totalSize) * 360;
1511
1512 // lastly, if we want the midpoint, factor that sucker in
1513 if (options.midpoint) {
1514 var currAngle = (currValue / totalSize) * 360;
1515 angle -= (currAngle / 2);
1516 }
1517
1518 return angle;
1519 },
1520
1521 getPercentage: function(pie, index) {
1522 return Math.floor((pie.options.data.content[index].value / pie.totalSize) * 100);
1523 }
1524 };
1525
1526 //// --------- text.js -----------
1527 var text = {
1528 offscreenCoord: -10000,
1529
1530 addTitle: function(pie) {
1531 var title = pie.svg.selectAll("." + pie.cssPrefix + "title")
1532 .data([pie.options.header.title])
1533 .enter()
1534 .append("text")
1535 .text(function(d) { return d.text; })
1536 .attr({
1537 id: pie.cssPrefix + "title",
1538 class: pie.cssPrefix + "title",
1539 x: text.offscreenCoord,
1540 y: text.offscreenCoord
1541 })
1542 .attr("text-anchor", function() {
1543 var location;
1544 if (pie.options.header.location === "top-center" || pie.options.header.location === "pie-center") {
1545 location = "middle";
1546 } else {
1547 location = "left";
1548 }
1549 return location;
1550 })
1551 .attr("fill", function(d) { return d.color; })
1552 .style("font-size", function(d) { return d.fontSize + "px"; })
1553 .style("font-family", function(d) { return d.font; });
1554 },
1555
1556 positionTitle: function(pie) {
1557 var textComponents = pie.textComponents;
1558 var headerLocation = pie.options.header.location;
1559 var canvasPadding = pie.options.misc.canvasPadding;
1560 var canvasWidth = pie.options.size.canvasWidth;
1561 var titleSubtitlePadding = pie.options.header.titleSubtitlePadding;
1562
1563 var x;
1564 if (headerLocation === "top-left") {
1565 x = canvasPadding.left;
1566 } else {
1567 x = ((canvasWidth - canvasPadding.right) / 2) + canvasPadding.left;
1568 }
1569
1570 // add whatever offset has been added by user
1571 x += pie.options.misc.pieCenterOffset.x;
1572
1573 var y = canvasPadding.top + textComponents.title.h;
1574
1575 if (headerLocation === "pie-center") {
1576 y = pie.pieCenter.y;
1577
1578 // still not fully correct
1579 if (textComponents.subtitle.exists) {
1580 var totalTitleHeight = textComponents.title.h + titleSubtitlePadding + textComponents.subtitle.h;
1581 y = y - (totalTitleHeight / 2) + textComponents.title.h;
1582 } else {
1583 y += (textComponents.title.h / 4);
1584 }
1585 }
1586
1587 pie.svg.select("#" + pie.cssPrefix + "title")
1588 .attr("x", x)
1589 .attr("y", y);
1590 },
1591
1592 addSubtitle: function(pie) {
1593 var headerLocation = pie.options.header.location;
1594
1595 pie.svg.selectAll("." + pie.cssPrefix + "subtitle")
1596 .data([pie.options.header.subtitle])
1597 .enter()
1598 .append("text")
1599 .text(function(d) { return d.text; })
1600 .attr("x", text.offscreenCoord)
1601 .attr("y", text.offscreenCoord)
1602 .attr("id", pie.cssPrefix + "subtitle")
1603 .attr("class", pie.cssPrefix + "subtitle")
1604 .attr("text-anchor", function() {
1605 var location;
1606 if (headerLocation === "top-center" || headerLocation === "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 positionSubtitle: function(pie) {
1619 var canvasPadding = pie.options.misc.canvasPadding;
1620 var canvasWidth = pie.options.size.canvasWidth;
1621
1622 var x;
1623 if (pie.options.header.location === "top-left") {
1624 x = canvasPadding.left;
1625 } else {
1626 x = ((canvasWidth - canvasPadding.right) / 2) + canvasPadding.left;
1627 }
1628
1629 // add whatever offset has been added by user
1630 x += pie.options.misc.pieCenterOffset.x;
1631
1632 var y = text.getHeaderHeight(pie);
1633 pie.svg.select("#" + pie.cssPrefix + "subtitle")
1634 .attr("x", x)
1635 .attr("y", y);
1636 },
1637
1638 addFooter: function(pie) {
1639 pie.svg.selectAll("." + pie.cssPrefix + "footer")
1640 .data([pie.options.footer])
1641 .enter()
1642 .append("text")
1643 .text(function(d) { return d.text; })
1644 .attr("x", text.offscreenCoord)
1645 .attr("y", text.offscreenCoord)
1646 .attr("id", pie.cssPrefix + "footer")
1647 .attr("class", pie.cssPrefix + "footer")
1648 .attr("text-anchor", function() {
1649 var location = "left";
1650 if (pie.options.footer.location === "bottom-center") {
1651 location = "middle";
1652 } else if (pie.options.footer.location === "bottom-right") {
1653 location = "left"; // on purpose. We have to change the x-coord to make it properly right-aligned
1654 }
1655 return location;
1656 })
1657 .attr("fill", function(d) { return d.color; })
1658 .style("font-size", function(d) { return d.fontSize + "px"; })
1659 .style("font-family", function(d) { return d.font; });
1660 },
1661
1662 positionFooter: function(pie) {
1663 var footerLocation = pie.options.footer.location;
1664 var footerWidth = pie.textComponents.footer.w;
1665 var canvasWidth = pie.options.size.canvasWidth;
1666 var canvasHeight = pie.options.size.canvasHeight;
1667 var canvasPadding = pie.options.misc.canvasPadding;
1668
1669 var x;
1670 if (footerLocation === "bottom-left") {
1671 x = canvasPadding.left;
1672 } else if (footerLocation === "bottom-right") {
1673 x = canvasWidth - footerWidth - canvasPadding.right;
1674 } else {
1675 x = canvasWidth / 2; // TODO - shouldn't this also take into account padding?
1676 }
1677
1678 pie.svg.select("#" + pie.cssPrefix + "footer")
1679 .attr("x", x)
1680 .attr("y", canvasHeight - canvasPadding.bottom);
1681 },
1682
1683 getHeaderHeight: function(pie) {
1684 var h;
1685 if (pie.textComponents.title.exists) {
1686
1687 // if the subtitle isn't defined, it'll be set to 0
1688 var totalTitleHeight = pie.textComponents.title.h + pie.options.header.titleSubtitlePadding + pie.textComponents.subtitle.h;
1689 if (pie.options.header.location === "pie-center") {
1690 h = pie.pieCenter.y - (totalTitleHeight / 2) + totalTitleHeight;
1691 } else {
1692 h = totalTitleHeight + pie.options.misc.canvasPadding.top;
1693 }
1694 } else {
1695 if (pie.options.header.location === "pie-center") {
1696 var footerPlusPadding = pie.options.misc.canvasPadding.bottom + pie.textComponents.footer.h;
1697 h = ((pie.options.size.canvasHeight - footerPlusPadding) / 2) + pie.options.misc.canvasPadding.top + (pie.textComponents.subtitle.h / 2);
1698 } else {
1699 h = pie.options.misc.canvasPadding.top + pie.textComponents.subtitle.h;
1700 }
1701 }
1702 return h;
1703 }
1704 };
1705
1706 //// --------- validate.js -----------
1707 var tt = {
1708 addTooltips: function(pie) {
1709
1710 // group the label groups (label, percentage, value) into a single element for simpler positioning
1711 var tooltips = pie.svg.insert("g")
1712 .attr("class", pie.cssPrefix + "tooltips");
1713
1714 tooltips.selectAll("." + pie.cssPrefix + "tooltip")
1715 .data(pie.options.data.content)
1716 .enter()
1717 .append("g")
1718 .attr("class", pie.cssPrefix + "tooltip")
1719 .attr("id", function(d, i) { return pie.cssPrefix + "tooltip" + i; })
1720 .style("opacity", 0)
1721 .append("rect")
1722 .attr({
1723 rx: pie.options.tooltips.styles.borderRadius,
1724 ry: pie.options.tooltips.styles.borderRadius,
1725 x: -pie.options.tooltips.styles.padding,
1726 opacity: pie.options.tooltips.styles.backgroundOpacity
1727 })
1728 .style("fill", pie.options.tooltips.styles.backgroundColor);
1729
1730 tooltips.selectAll("." + pie.cssPrefix + "tooltip")
1731 .data(pie.options.data.content)
1732 .append("text")
1733 .attr("fill", function(d) { return pie.options.tooltips.styles.color; })
1734 .style("font-size", function(d) { return pie.options.tooltips.styles.fontSize; })
1735 .style("font-family", function(d) { return pie.options.tooltips.styles.font; })
1736 .text(function(d, i) {
1737 var caption = pie.options.tooltips.string;
1738 if (pie.options.tooltips.type === "caption") {
1739 caption = d.caption;
1740 }
1741 return tt.replacePlaceholders(pie, caption, i, {
1742 label: d.label,
1743 value: d.value,
1744 percentage: segments.getPercentage(pie, i)
1745 });
1746 });
1747
1748 tooltips.selectAll("." + pie.cssPrefix + "tooltip rect")
1749 .attr({
1750 width: function (d, i) {
1751 var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + i);
1752 return dims.w + (2 * pie.options.tooltips.styles.padding);
1753 },
1754 height: function (d, i) {
1755 var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + i);
1756 return dims.h + (2 * pie.options.tooltips.styles.padding);
1757 },
1758 y: function (d, i) {
1759 var dims = helpers.getDimensions(pie.cssPrefix + "tooltip" + i);
1760 return -(dims.h / 2) + 1;
1761 }
1762 });
1763 },
1764
1765 showTooltip: function(pie, index) {
1766
1767 var fadeInSpeed = pie.options.tooltips.styles.fadeInSpeed;
1768 if (tt.currentTooltip === index) {
1769 fadeInSpeed = 1;
1770 }
1771
1772 tt.currentTooltip = index;
1773 d3.select("#" + pie.cssPrefix + "tooltip" + index)
1774 .transition()
1775 .duration(fadeInSpeed)
1776 .style("opacity", function() { return 1; });
1777
1778 tt.moveTooltip(pie);
1779 },
1780
1781 moveTooltip: function(pie) {
1782 d3.selectAll("#" + pie.cssPrefix + "tooltip" + tt.currentTooltip)
1783 .attr("transform", function(d) {
1784 var mouseCoords = d3.mouse(this.parentElement);
1785 var x = mouseCoords[0] + pie.options.tooltips.styles.padding + 2;
1786 var y = mouseCoords[1] - (2 * pie.options.tooltips.styles.padding) - 2;
1787 return "translate(" + x + "," + y + ")";
1788 });
1789 },
1790
1791 hideTooltip: function(pie, index) {
1792 d3.select("#" + pie.cssPrefix + "tooltip" + index)
1793 .style("opacity", function() { return 0; });
1794
1795 // move the tooltip offscreen. This ensures that when the user next mousovers the segment the hidden
1796 // element won't interfere
1797 d3.select("#" + pie.cssPrefix + "tooltip" + tt.currentTooltip)
1798 .attr("transform", function(d, i) {
1799
1800 // klutzy, but it accounts for tooltip padding which could push it onscreen
1801 var x = pie.options.size.canvasWidth + 1000;
1802 var y = pie.options.size.canvasHeight + 1000;
1803 return "translate(" + x + "," + y + ")";
1804 });
1805 },
1806
1807 replacePlaceholders: function(pie, str, index, replacements) {
1808
1809 // if the user has defined a placeholderParser function, call it before doing the replacements
1810 if (helpers.isFunction(pie.options.tooltips.placeholderParser)) {
1811 pie.options.tooltips.placeholderParser(index, replacements);
1812 }
1813
1814 var replacer = function() {
1815 return function(match) {
1816 var placeholder = arguments[1];
1817 if (replacements.hasOwnProperty(placeholder)) {
1818 return replacements[arguments[1]];
1819 } else {
1820 return arguments[0];
1821 }
1822 };
1823 };
1824 return str.replace(/\{(\w+)\}/g, replacer(replacements));
1825 }
1826 };
1827
1828
1829 // --------------------------------------------------------------------------------------------
1830
1831 // our constructor
1832 var d3pie = function(element, options) {
1833
1834 // element can be an ID or DOM element
1835 this.element = element;
1836 if (typeof element === "string") {
1837 var el = element.replace(/^#/, ""); // replace any jQuery-like ID hash char
1838 this.element = document.getElementById(el);
1839 }
1840
1841 var opts = {};
1842 extend(true, opts, defaultSettings, options);
1843 this.options = opts;
1844
1845 // if the user specified a custom CSS element prefix (ID, class), use it
1846 if (this.options.misc.cssPrefix !== null) {
1847 this.cssPrefix = this.options.misc.cssPrefix;
1848 } else {
1849 this.cssPrefix = "p" + _uniqueIDCounter + "_";
1850 _uniqueIDCounter++;
1851 }
1852
1853
1854 // now run some validation on the user-defined info
1855 if (!validate.initialCheck(this)) {
1856 return;
1857 }
1858
1859 // add a data-role to the DOM node to let anyone know that it contains a d3pie instance, and the d3pie version
1860 d3.select(this.element).attr(_scriptName, _version);
1861
1862 // things that are done once
1863 this.options.data.content = math.sortPieData(this);
1864 if (this.options.data.smallSegmentGrouping.enabled) {
1865 this.options.data.content = helpers.applySmallSegmentGrouping(this.options.data.content, this.options.data.smallSegmentGrouping);
1866 }
1867 this.options.colors = helpers.initSegmentColors(this);
1868 this.totalSize = math.getTotalPieSize(this.options.data.content);
1869
1870 _init.call(this);
1871 };
1872
1873 d3pie.prototype.recreate = function() {
1874 this.options.data.content = math.sortPieData(this);
1875 if (this.options.data.smallSegmentGrouping.enabled) {
1876 this.options.data.content = helpers.applySmallSegmentGrouping(this.options.data.content, this.options.data.smallSegmentGrouping);
1877 }
1878 this.options.colors = helpers.initSegmentColors(this);
1879 this.totalSize = math.getTotalPieSize(this.options.data.content);
1880
1881 _init.call(this);
1882 };
1883
1884 d3pie.prototype.redraw = function() {
1885 this.element.innerHTML = "";
1886 _init.call(this);
1887 };
1888
1889 d3pie.prototype.destroy = function() {
1890 this.element.innerHTML = ""; // clear out the SVG
1891 d3.select(this.element).attr(_scriptName, null); // remove the data attr
1892 };
1893
1894 /**
1895 * Returns all pertinent info about the current open info. Returns null if nothing's open, or if one is, an object of
1896 * the following form:
1897 * {
1898 * element: DOM NODE,
1899 * index: N,
1900 * data: {}
1901 * }
1902 */
1903 d3pie.prototype.getOpenSegment = function() {
1904 var segment = this.currentlyOpenSegment;
1905 if (segment !== null && typeof segment !== "undefined") {
1906 var index = parseInt(d3.select(segment).attr("data-index"), 10);
1907 return {
1908 element: segment,
1909 index: index,
1910 data: this.options.data.content[index]
1911 };
1912 } else {
1913 return null;
1914 }
1915 };
1916
1917 d3pie.prototype.openSegment = function(index) {
1918 index = parseInt(index, 10);
1919 if (index < 0 || index > this.options.data.content.length-1) {
1920 return;
1921 }
1922 segments.openSegment(this, d3.select("#" + this.cssPrefix + "segment" + index).node());
1923 };
1924
1925 d3pie.prototype.closeSegment = function() {
1926 var segment = this.currentlyOpenSegment;
1927 if (segment) {
1928 segments.closeSegment(this, segment);
1929 }
1930 };
1931
1932 // this let's the user dynamically update aspects of the pie chart without causing a complete redraw. It
1933 // intelligently re-renders only the part of the pie that the user specifies. Some things cause a repaint, others
1934 // just redraw the single element
1935 d3pie.prototype.updateProp = function(propKey, value) {
1936 switch (propKey) {
1937 case "header.title.text":
1938 var oldVal = helpers.processObj(this.options, propKey);
1939 helpers.processObj(this.options, propKey, value);
1940 d3.select("#" + this.cssPrefix + "title").html(value);
1941 if ((oldVal === "" && value !== "") || (oldVal !== "" && value === "")) {
1942 this.redraw();
1943 }
1944 break;
1945
1946 case "header.subtitle.text":
1947 var oldValue = helpers.processObj(this.options, propKey);
1948 helpers.processObj(this.options, propKey, value);
1949 d3.select("#" + this.cssPrefix + "subtitle").html(value);
1950 if ((oldValue === "" && value !== "") || (oldValue !== "" && value === "")) {
1951 this.redraw();
1952 }
1953 break;
1954
1955 case "callbacks.onload":
1956 case "callbacks.onMouseoverSegment":
1957 case "callbacks.onMouseoutSegment":
1958 case "callbacks.onClickSegment":
1959 case "effects.pullOutSegmentOnClick.effect":
1960 case "effects.pullOutSegmentOnClick.speed":
1961 case "effects.pullOutSegmentOnClick.size":
1962 case "effects.highlightSegmentOnMouseover":
1963 case "effects.highlightLuminosity":
1964 helpers.processObj(this.options, propKey, value);
1965 break;
1966
1967 // everything else, attempt to update it & do a repaint
1968 default:
1969 helpers.processObj(this.options, propKey, value);
1970
1971 this.destroy();
1972 this.recreate();
1973 break;
1974 }
1975 };
1976
1977
1978 // ------------------------------------------------------------------------------------------------
1979
1980
1981 var _init = function() {
1982
1983 // prep-work
1984 this.svg = helpers.addSVGSpace(this);
1985
1986 // store info about the main text components as part of the d3pie object instance
1987 this.textComponents = {
1988 headerHeight: 0,
1989 title: {
1990 exists: this.options.header.title.text !== "",
1991 h: 0,
1992 w: 0
1993 },
1994 subtitle: {
1995 exists: this.options.header.subtitle.text !== "",
1996 h: 0,
1997 w: 0
1998 },
1999 footer: {
2000 exists: this.options.footer.text !== "",
2001 h: 0,
2002 w: 0
2003 }
2004 };
2005
2006 this.outerLabelGroupData = [];
2007
2008 // add the key text components offscreen (title, subtitle, footer). We need to know their widths/heights for later computation
2009 if (this.textComponents.title.exists) {
2010 text.addTitle(this);
2011 }
2012 if (this.textComponents.subtitle.exists) {
2013 text.addSubtitle(this);
2014 }
2015 text.addFooter(this);
2016
2017 // the footer never moves. Put it in place now
2018 var self = this;
2019 helpers.whenIdExists(this.cssPrefix + "footer", function() {
2020 text.positionFooter(self);
2021 var d3 = helpers.getDimensions(self.cssPrefix + "footer");
2022 self.textComponents.footer.h = d3.h;
2023 self.textComponents.footer.w = d3.w;
2024 });
2025
2026 // now create the pie chart and position everything accordingly
2027 var reqEls = [];
2028 if (this.textComponents.title.exists) { reqEls.push(this.cssPrefix + "title"); }
2029 if (this.textComponents.subtitle.exists) { reqEls.push(this.cssPrefix + "subtitle"); }
2030 if (this.textComponents.footer.exists) { reqEls.push(this.cssPrefix + "footer"); }
2031
2032 helpers.whenElementsExist(reqEls, function() {
2033 if (self.textComponents.title.exists) {
2034 var d1 = helpers.getDimensions(self.cssPrefix + "title");
2035 self.textComponents.title.h = d1.h;
2036 self.textComponents.title.w = d1.w;
2037 }
2038 if (self.textComponents.subtitle.exists) {
2039 var d2 = helpers.getDimensions(self.cssPrefix + "subtitle");
2040 self.textComponents.subtitle.h = d2.h;
2041 self.textComponents.subtitle.w = d2.w;
2042 }
2043 // now compute the full header height
2044 if (self.textComponents.title.exists || self.textComponents.subtitle.exists) {
2045 var headerHeight = 0;
2046 if (self.textComponents.title.exists) {
2047 headerHeight += self.textComponents.title.h;
2048 if (self.textComponents.subtitle.exists) {
2049 headerHeight += self.options.header.titleSubtitlePadding;
2050 }
2051 }
2052 if (self.textComponents.subtitle.exists) {
2053 headerHeight += self.textComponents.subtitle.h;
2054 }
2055 self.textComponents.headerHeight = headerHeight;
2056 }
2057
2058 // at this point, all main text component dimensions have been calculated
2059 math.computePieRadius(self);
2060
2061 // this value is used all over the place for placing things and calculating locations. We figure it out ONCE
2062 // and store it as part of the object
2063 math.calculatePieCenter(self);
2064
2065 // position the title and subtitle
2066 text.positionTitle(self);
2067 text.positionSubtitle(self);
2068
2069 // now create the pie chart segments, and gradients if the user desired
2070 if (self.options.misc.gradient.enabled) {
2071 segments.addGradients(self);
2072 }
2073 segments.create(self); // also creates this.arc
2074 labels.add(self, "inner", self.options.labels.inner.format);
2075 labels.add(self, "outer", self.options.labels.outer.format);
2076
2077 // position the label elements relatively within their individual group (label, percentage, value)
2078 labels.positionLabelElements(self, "inner", self.options.labels.inner.format);
2079 labels.positionLabelElements(self, "outer", self.options.labels.outer.format);
2080 labels.computeOuterLabelCoords(self);
2081
2082 // this is (and should be) dumb. It just places the outer groups at their calculated, collision-free positions
2083 labels.positionLabelGroups(self, "outer");
2084
2085 // we use the label line positions for many other calculations, so ALWAYS compute them
2086 labels.computeLabelLinePositions(self);
2087
2088 // only add them if they're actually enabled
2089 if (self.options.labels.lines.enabled && self.options.labels.outer.format !== "none") {
2090 labels.addLabelLines(self);
2091 }
2092
2093 labels.positionLabelGroups(self, "inner");
2094 labels.fadeInLabelsAndLines(self);
2095
2096 // add and position the tooltips
2097 if (self.options.tooltips.enabled) {
2098 tt.addTooltips(self);
2099 }
2100
2101 segments.addSegmentEventHandlers(self);
2102 });
2103 };
2104
2105 return d3pie;
2106 }));
+0
-9
cloudkittydashboard/static/cloudkitty/js/d3pie.min.js less more
0 /*!
1 * d3pie
2 * @author Ben Keen
3 * @version 0.1.5
4 * @date June 2014
5 * @repo http://github.com/benkeen/d3pie
6 */
7 !function(a,b){"function"==typeof define&&define.amd?define([],b):"object"==typeof exports?module.exports=b(require()):a.d3pie=b(a)}(this,function(){var a="d3pie",b="0.1.5",c=0,d={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,length:30}},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}},e={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))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(!f.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=[],g=0;g<d.data.content.length;g++)"number"!=typeof d.data.content[g].value||isNaN(d.data.content[g].value)?console.log("not valid: ",d.data.content[g]):d.data.content[g].value<=0?console.log("not valid - should have positive value: ",d.data.content[g]):e.push(d.data.content[g]);return a.options.data.content=e,!0}},f={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?f.processObj(a,b.split("."),c):1===b.length&&void 0!==c?(a[b[0]]=c,a[b[0]]):0===b.length?a:f.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++)d.push(b[e].hasOwnProperty("color")?b[e].color:c[e]);return d},applySmallSegmentGrouping:function(a,b){var c;"percentage"===b.valueType&&(c=h.getTotalPieSize(a));for(var d=[],e=[],f=0,g=0;g<a.length;g++)if("percentage"===b.valueType){var i=a[g].value/c*100;if(i<=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)}},g=function(){var a,b,c,d,e,f,h=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 h&&(k=h,h=arguments[1]||{},i=2),"object"==typeof h||o.isFunction(h)||(h={}),j===i&&(h=this,--i),i;j>i;i++)if(null!==(a=arguments[i]))for(b in a)c=h[b],d=a[b],h!==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:{},h[b]=g(k,f,d)):void 0!==d&&(h[b]=d));return h},h={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=f.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=h.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}},i={add:function(a,b,c){var d=i.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);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){var b=a.label;return e.truncation.enabled&&a.label.length>e.truncation.length&&(b=a.label.substring(0,e.truncation.length)+"..."),b}).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(b,c){return j.getPercentage(a,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){return 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){i["dimensions-"+b]=[];var d=d3.selectAll("."+a.cssPrefix+"labelGroup-"+b);d.each(function(){var c=d3.select(this).selectAll("."+a.cssPrefix+"segmentMainLabel-"+b),d=d3.select(this).selectAll("."+a.cssPrefix+"segmentPercentage-"+b),e=d3.select(this).selectAll("."+a.cssPrefix+"segmentValue-"+b);i["dimensions-"+b].push({mainLabel:null!==c.node()?c.node().getBBox():null,percentage:null!==d.node()?d.node().getBBox():null,value:null!==e.node()?e.node().getBBox():null})});var e=5,f=i["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 i.computeLinePosition(a,c)})},computeLinePosition:function(a,b){var c,d,e,f,g=j.getSegmentAngle(b,a.options.data.content,a.totalSize,{midpoint:!0}),i=h.rotate(a.pieCenter.x,a.pieCenter.y-a.outerRadius,a.pieCenter.x,a.pieCenter.y,g),k=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-i.x)/2,d=a.outerLabelGroupData[b].y+(i.y-a.outerLabelGroupData[b].y)/n,e=a.outerLabelGroupData[b].x-l,f=a.outerLabelGroupData[b].y-k;break;case 1:c=i.x+(a.outerLabelGroupData[b].x-i.x)/n,d=i.y+(a.outerLabelGroupData[b].y-i.y)/n,e=a.outerLabelGroupData[b].x-l,f=a.outerLabelGroupData[b].y-k;break;case 2:var o=a.outerLabelGroupData[b].x+a.outerLabelGroupData[b].w+l;c=i.x-(i.x-o)/n,d=i.y+(a.outerLabelGroupData[b].y-i.y)/n,e=a.outerLabelGroupData[b].x+a.outerLabelGroupData[b].w+l,f=a.outerLabelGroupData[b].y-k;break;case 3:var p=a.outerLabelGroupData[b].x+a.outerLabelGroupData[b].w+l;c=p+(i.x-p)/n,d=a.outerLabelGroupData[b].y+(i.y-a.outerLabelGroupData[b].y)/n,e=a.outerLabelGroupData[b].x+a.outerLabelGroupData[b].w+l,f=a.outerLabelGroupData[b].y-k}a.lineCoordGroups[b]="straight"===a.options.labels.lines.style?[{x:i.x,y:i.y},{x:e,y:f}]:[{x:i.x,y:i.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.svg.line().interpolate("basis").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=j.getPercentage(a,c),f=null!==d&&d>e||""===a.options.data.content[c].label;return f?0:1})},positionLabelGroups:function(a,b){d3.selectAll("."+a.cssPrefix+"labelGroup-"+b).style("opacity",0).attr("transform",function(c,d){var e,i;if("outer"===b)e=a.outerLabelGroupData[d].x,i=a.outerLabelGroupData[d].y;else{var k=g(!0,{},a.pieCenter);if(a.innerRadius>0){var l=j.getSegmentAngle(d,a.options.data.content,a.totalSize,{midpoint:!0}),m=h.translate(a.pieCenter.x,a.pieCenter.y,a.innerRadius,l);k.x=m.x,k.y=m.y}var n=f.getDimensions(a.cssPrefix+"labelGroup"+d+"-inner"),o=n.w/2,p=n.h/4;e=k.x+(a.lineCoordGroups[d][0].x-k.x)/1.8,i=k.y+(a.lineCoordGroups[d][0].y-k.y)/1.8,e-=o,i+=p}return"translate("+e+","+i+")"})},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,e=j.getPercentage(a,c);return null!==d&&d>e?0:1}),d3.selectAll("."+a.cssPrefix+"labelGroup-inner").transition().duration(b).style("opacity",function(b,c){var d=a.options.labels.inner.hideWhenLessThanPercentage,e=j.getPercentage(a,c);return null!==d&&d>e?0:1}),d3.selectAll("g."+a.cssPrefix+"lineGroups").transition().duration(b).style("opacity",1),f.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 i.getIdealOuterLabelPositions(a,c)}),i.resolveOuterLabelCollisions(a)},resolveOuterLabelCollisions:function(a){var b=a.options.data.content.length;i.checkConflict(a,0,"clockwise",b),i.checkConflict(a,b-1,"anticlockwise",b)},checkConflict:function(a,b,c,d){var e,g;if(!(1>=d)){var h=a.outerLabelGroupData[b].hs;if(!("clockwise"===c&&"right"!==h||"anticlockwise"===c&&"left"!==h)){var j="clockwise"===c?b+1:b-1,k=a.outerLabelGroupData[b],l=a.outerLabelGroupData[j],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(g=a.outerLabelGroupData[e],f.rectIntersect(g,l)){i.adjustLabelPos(a,j,k,m);break}}else for(e=d-1;e>=b;e--)if(g=a.outerLabelGroupData[e],f.rectIntersect(g,l)){i.adjustLabelPos(a,j,k,m);break}i.checkConflict(a,j,c,d)}}},adjustLabelPos:function(a,b,c,d){var e,f,g,h;h=c.y+d.heightChange,f=d.center.y-h,e=Math.sqrt(Math.abs(d.lineLength)>Math.abs(f)?d.lineLength*d.lineLength-f*f: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().getBBox(),d=j.getSegmentAngle(b,a.options.data.content,a.totalSize,{midpoint:!0}),e=a.pieCenter.x,f=a.pieCenter.y-(a.outerRadius+a.options.labels.outer.pieDistance),g=h.rotate(e,f,a.pieCenter.x,a.pieCenter.y,d),i="right";d>180?(g.x-=c.width+8,i="left"):g.x+=8,a.outerLabelGroupData[b]={x:g.x,y:g.y,w:c.width,h:c.height,hs:i}}},j={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 h.getPieTranslateCenter(b)}).attr("class",a.cssPrefix+"pieChart"),g=d3.svg.arc().innerRadius(a.innerRadius).outerRadius(a.outerRadius).startAngle(0).endAngle(function(b){return b.value/a.totalSize*2*Math.PI}),i=f.selectAll("."+a.cssPrefix+"arc").data(a.options.data.content).enter().append("g").attr("class",a.cssPrefix+"arc"),k=d.speed;"none"===d.effect&&(k=0),i.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("cubic-in-out").duration(k).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=j.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";j.onSegmentEvent(a,a.options.callbacks.onClickSegment,b,e),"none"!==a.options.effects.pullOutSegmentOnClick.effect&&(e?j.closeSegment(a,b.node()):j.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",f.getColorShade(e,a.options.effects.highlightLuminosity))}a.options.tooltips.enabled&&(c=b.attr("data-index"),l.showTooltip(a,c));var g=b.attr("class")===a.cssPrefix+"expanded";j.onSegmentEvent(a,a.options.callbacks.onMouseoverSegment,b,g)}),b.on("mousemove",function(){l.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"),l.hideTooltip(a,c));var f=b.attr("class")===a.cssPrefix+"expanded";j.onSegmentEvent(a,a.options.callbacks.onMouseoutSegment,b,f)})},onSegmentEvent:function(a,b,c,d){if(f.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,d3.selectAll("."+a.cssPrefix+"expanded").length>0&&j.closeSegment(a,d3.select("."+a.cssPrefix+"expanded").node()),d3.select(b).transition().ease(a.options.effects.pullOutSegmentOnClick.effect).duration(a.options.effects.pullOutSegmentOnClick.speed).attr("transform",function(b){var c=a.arc.centroid(b),d=c[0],e=c[1],f=Math.sqrt(d*d+e*e),g=parseInt(a.options.effects.pullOutSegmentOnClick.size,10);return"translate("+d/f*g+","+e/f*g+")"}).each("end",function(){a.currentlyOpenSegment=b,a.isOpeningSegment=!1,d3.select(this).attr("class",a.cssPrefix+"expanded")}))},closeSegment:function(a,b){d3.select(b).transition().duration(400).attr("transform","translate(0,0)").each("end",function(){d3.select(this).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=g({compounded:!0,midpoint:!1},d),h=b[a].value;if(f.compounded){e=0;for(var i=0;a>=i;i++)e+=b[i].value}"undefined"==typeof e&&(e=h);var j=e/c*360;if(f.midpoint){var k=h/c*360;j-=k/2}return j},getPercentage:function(a,b){return Math.floor(a.options.data.content[b].value/a.totalSize*100)}},k={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","class":a.cssPrefix+"title",x:k.offscreenCoord,y:k.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",k.offscreenCoord).attr("y",k.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=k.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",k.offscreenCoord).attr("y",k.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}},l={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,ry:a.options.tooltips.styles.borderRadius,x:-a.options.tooltips.styles.padding,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(){return a.options.tooltips.styles.color}).style("font-size",function(){return a.options.tooltips.styles.fontSize}).style("font-family",function(){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),l.replacePlaceholders(a,d,c,{label:b.label,value:b.value,percentage:j.getPercentage(a,c)})}),b.selectAll("."+a.cssPrefix+"tooltip rect").attr({width:function(b,c){var d=f.getDimensions(a.cssPrefix+"tooltip"+c);return d.w+2*a.options.tooltips.styles.padding},height:function(b,c){var d=f.getDimensions(a.cssPrefix+"tooltip"+c);return d.h+2*a.options.tooltips.styles.padding},y:function(b,c){var d=f.getDimensions(a.cssPrefix+"tooltip"+c);return-(d.h/2)+1}})},showTooltip:function(a,b){var c=a.options.tooltips.styles.fadeInSpeed;l.currentTooltip===b&&(c=1),l.currentTooltip=b,d3.select("#"+a.cssPrefix+"tooltip"+b).transition().duration(c).style("opacity",function(){return 1}),l.moveTooltip(a)},moveTooltip:function(a){d3.selectAll("#"+a.cssPrefix+"tooltip"+l.currentTooltip).attr("transform",function(){var b=d3.mouse(this.parentElement),c=b[0]+a.options.tooltips.styles.padding+2,d=b[1]-2*a.options.tooltips.styles.padding-2;return"translate("+c+","+d+")"})},hideTooltip:function(a,b){d3.select("#"+a.cssPrefix+"tooltip"+b).style("opacity",function(){return 0}),d3.select("#"+a.cssPrefix+"tooltip"+l.currentTooltip).attr("transform",function(){var b=a.options.size.canvasWidth+1e3,c=a.options.size.canvasHeight+1e3;return"translate("+b+","+c+")"})},replacePlaceholders:function(a,b,c,d){f.isFunction(a.options.tooltips.placeholderParser)&&a.options.tooltips.placeholderParser(c,d);var e=function(){return function(){var a=arguments[1];return d.hasOwnProperty(a)?d[arguments[1]]:arguments[0]}};return b.replace(/\{(\w+)\}/g,e(d))}},m=function(i,j){if(this.element=i,"string"==typeof i){var k=i.replace(/^#/,"");this.element=document.getElementById(k)}var l={};g(!0,l,d,j),this.options=l,null!==this.options.misc.cssPrefix?this.cssPrefix=this.options.misc.cssPrefix:(this.cssPrefix="p"+c+"_",c++),e.initialCheck(this)&&(d3.select(this.element).attr(a,b),this.options.data.content=h.sortPieData(this),this.options.data.smallSegmentGrouping.enabled&&(this.options.data.content=f.applySmallSegmentGrouping(this.options.data.content,this.options.data.smallSegmentGrouping)),this.options.colors=f.initSegmentColors(this),this.totalSize=h.getTotalPieSize(this.options.data.content),n.call(this))};m.prototype.recreate=function(){this.options.data.content=h.sortPieData(this),this.options.data.smallSegmentGrouping.enabled&&(this.options.data.content=f.applySmallSegmentGrouping(this.options.data.content,this.options.data.smallSegmentGrouping)),this.options.colors=f.initSegmentColors(this),this.totalSize=h.getTotalPieSize(this.options.data.content),n.call(this)},m.prototype.redraw=function(){this.element.innerHTML="",n.call(this)},m.prototype.destroy=function(){this.element.innerHTML="",d3.select(this.element).attr(a,null)},m.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},m.prototype.openSegment=function(a){a=parseInt(a,10),0>a||a>this.options.data.content.length-1||j.openSegment(this,d3.select("#"+this.cssPrefix+"segment"+a).node())},m.prototype.closeSegment=function(){var a=this.currentlyOpenSegment;a&&j.closeSegment(this,a)},m.prototype.updateProp=function(a,b){switch(a){case"header.title.text":var c=f.processObj(this.options,a);f.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=f.processObj(this.options,a);f.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":f.processObj(this.options,a,b);break;default:f.processObj(this.options,a,b),this.destroy(),this.recreate()}};var n=function(){this.svg=f.addSVGSpace(this),this.textComponents={headerHeight:0,title:{exists:""!==this.options.header.title.text,h:0,w:0},subtitle:{exists:""!==this.options.header.subtitle.text,h:0,w:0},footer:{exists:""!==this.options.footer.text,h:0,w:0}},this.outerLabelGroupData=[],this.textComponents.title.exists&&k.addTitle(this),this.textComponents.subtitle.exists&&k.addSubtitle(this),k.addFooter(this);var a=this;f.whenIdExists(this.cssPrefix+"footer",function(){k.positionFooter(a);var b=f.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"),f.whenElementsExist(b,function(){if(a.textComponents.title.exists){var b=f.getDimensions(a.cssPrefix+"title");a.textComponents.title.h=b.h,a.textComponents.title.w=b.w}if(a.textComponents.subtitle.exists){var c=f.getDimensions(a.cssPrefix+"subtitle");
8 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}h.computePieRadius(a),h.calculatePieCenter(a),k.positionTitle(a),k.positionSubtitle(a),a.options.misc.gradient.enabled&&j.addGradients(a),j.create(a),i.add(a,"inner",a.options.labels.inner.format),i.add(a,"outer",a.options.labels.outer.format),i.positionLabelElements(a,"inner",a.options.labels.inner.format),i.positionLabelElements(a,"outer",a.options.labels.outer.format),i.computeOuterLabelCoords(a),i.positionLabelGroups(a,"outer"),i.computeLabelLinePositions(a),a.options.labels.lines.enabled&&"none"!==a.options.labels.outer.format&&i.addLabelLines(a),i.positionLabelGroups(a,"inner"),i.fadeInLabelsAndLines(a),a.options.tooltips.enabled&&l.addTooltips(a),j.addSegmentEventHandlers(a)})};return m});
0 ---
1 upgrade:
2 - |
3 The "reporting" tab has been reworked and the dashboard does not require
4 D3pie anymore. The colors between the charts are now consistent and a
5 color legend has been added.