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
+130
-91
1 | 1 | {% load l10n %} |
2 | 2 | {% load static %} |
3 | 3 | <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> | |
5 | 4 | <script src='{% static "cloudkitty/js/rickshaw.min.js" %}' type='text/javascript' charset='utf-8'></script> |
6 | 5 | |
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> | |
10 | 20 | </div> |
11 | 21 | |
12 | 22 | <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 = [ | |
56 | 24 | {% 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 }} | |
91 | 27 | }, |
92 | 28 | {% endfor %} |
93 | 29 | ] |
94 | }); | |
95 | graph.render(); | |
96 | 30 | |
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( { | |
98 | 137 | graph: graph |
99 | } ); | |
138 | }); | |
100 | 139 | |
101 | var yAxis = new Rickshaw.Graph.Axis.Y({ | |
140 | var yAxis = new Rickshaw.Graph.Axis.Y({ | |
102 | 141 | graph: graph, |
103 | }); | |
104 | yAxis.render(); | |
142 | }); | |
143 | yAxis.render(); | |
105 | 144 | |
106 | var xAxis = new Rickshaw.Graph.Axis.Time({ | |
145 | var xAxis = new Rickshaw.Graph.Axis.Time({ | |
107 | 146 | graph: graph |
108 | }); | |
109 | xAxis.render(); | |
147 | }); | |
148 | xAxis.render(); | |
110 | 149 | </script> |
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 | /*! | |
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});⏎ |