d3pie-rails 0.1.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ }));