d3pie-rails 0.1.9

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