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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +2 -0
- data/D3PIE-LICENSE +20 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +109 -0
- data/LICENSE +24 -0
- data/README.md +51 -0
- data/Rakefile +18 -0
- data/app/assets/javascripts/d3pie.js +2155 -0
- data/app/assets/javascripts/d3pie.min.js +9 -0
- data/d3pie-rails.gemspec +23 -0
- data/lib/d3pie-rails.rb +1 -0
- data/lib/d3pie/rails.rb +2 -0
- data/lib/d3pie/rails/engine.rb +6 -0
- data/lib/d3pie/rails/version.rb +5 -0
- metadata +101 -0
checksums.yaml
ADDED
|
@@ -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
|
data/CHANGELOG.md
ADDED
data/D3PIE-LICENSE
ADDED
|
@@ -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
data/Gemfile.lock
ADDED
|
@@ -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.
|
data/README.md
ADDED
|
@@ -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.
|
data/Rakefile
ADDED
|
@@ -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
|
+
}));
|