Mercurial > hg > soundsoftware-site
comparison lib/SVG/Graph/Pie.rb @ 0:513646585e45
* Import Redmine trunk SVN rev 3859
author | Chris Cannam |
---|---|
date | Fri, 23 Jul 2010 15:52:44 +0100 |
parents | |
children | 3e4c3460b6ca |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:513646585e45 |
---|---|
1 require 'SVG/Graph/Graph' | |
2 | |
3 module SVG | |
4 module Graph | |
5 # === Create presentation quality SVG pie graphs easily | |
6 # | |
7 # == Synopsis | |
8 # | |
9 # require 'SVG/Graph/Pie' | |
10 # | |
11 # fields = %w(Jan Feb Mar) | |
12 # data_sales_02 = [12, 45, 21] | |
13 # | |
14 # graph = SVG::Graph::Pie.new({ | |
15 # :height => 500, | |
16 # :width => 300, | |
17 # :fields => fields, | |
18 # }) | |
19 # | |
20 # graph.add_data({ | |
21 # :data => data_sales_02, | |
22 # :title => 'Sales 2002', | |
23 # }) | |
24 # | |
25 # print "Content-type: image/svg+xml\r\n\r\n" | |
26 # print graph.burn(); | |
27 # | |
28 # == Description | |
29 # | |
30 # This object aims to allow you to easily create high quality | |
31 # SVG pie graphs. You can either use the default style sheet | |
32 # or supply your own. Either way there are many options which can | |
33 # be configured to give you control over how the graph is | |
34 # generated - with or without a key, display percent on pie chart, | |
35 # title, subtitle etc. | |
36 # | |
37 # = Examples | |
38 # | |
39 # http://www.germane-software/repositories/public/SVG/test/single.rb | |
40 # | |
41 # == See also | |
42 # | |
43 # * SVG::Graph::Graph | |
44 # * SVG::Graph::BarHorizontal | |
45 # * SVG::Graph::Bar | |
46 # * SVG::Graph::Line | |
47 # * SVG::Graph::Plot | |
48 # * SVG::Graph::TimeSeries | |
49 # | |
50 # == Author | |
51 # | |
52 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> | |
53 # | |
54 # Copyright 2004 Sean E. Russell | |
55 # This software is available under the Ruby license[LICENSE.txt] | |
56 # | |
57 class Pie < Graph | |
58 # Defaults are those set by Graph::initialize, and | |
59 # [show_shadow] true | |
60 # [shadow_offset] 10 | |
61 # [show_data_labels] false | |
62 # [show_actual_values] false | |
63 # [show_percent] true | |
64 # [show_key_data_labels] true | |
65 # [show_key_actual_values] true | |
66 # [show_key_percent] false | |
67 # [expanded] false | |
68 # [expand_greatest] false | |
69 # [expand_gap] 10 | |
70 # [show_x_labels] false | |
71 # [show_y_labels] false | |
72 # [datapoint_font_size] 12 | |
73 def set_defaults | |
74 init_with( | |
75 :show_shadow => true, | |
76 :shadow_offset => 10, | |
77 | |
78 :show_data_labels => false, | |
79 :show_actual_values => false, | |
80 :show_percent => true, | |
81 | |
82 :show_key_data_labels => true, | |
83 :show_key_actual_values => true, | |
84 :show_key_percent => false, | |
85 | |
86 :expanded => false, | |
87 :expand_greatest => false, | |
88 :expand_gap => 10, | |
89 | |
90 :show_x_labels => false, | |
91 :show_y_labels => false, | |
92 :datapoint_font_size => 12 | |
93 ) | |
94 @data = [] | |
95 end | |
96 | |
97 # Adds a data set to the graph. | |
98 # | |
99 # graph.add_data( { :data => [1,2,3,4] } ) | |
100 # | |
101 # Note that the :title is not necessary. If multiple | |
102 # data sets are added to the graph, the pie chart will | |
103 # display the +sums+ of the data. EG: | |
104 # | |
105 # graph.add_data( { :data => [1,2,3,4] } ) | |
106 # graph.add_data( { :data => [2,3,5,9] } ) | |
107 # | |
108 # is the same as: | |
109 # | |
110 # graph.add_data( { :data => [3,5,8,13] } ) | |
111 def add_data arg | |
112 arg[:data].each_index {|idx| | |
113 @data[idx] = 0 unless @data[idx] | |
114 @data[idx] += arg[:data][idx] | |
115 } | |
116 end | |
117 | |
118 # If true, displays a drop shadow for the chart | |
119 attr_accessor :show_shadow | |
120 # Sets the offset of the shadow from the pie chart | |
121 attr_accessor :shadow_offset | |
122 # If true, display the data labels on the chart | |
123 attr_accessor :show_data_labels | |
124 # If true, display the actual field values in the data labels | |
125 attr_accessor :show_actual_values | |
126 # If true, display the percentage value of each pie wedge in the data | |
127 # labels | |
128 attr_accessor :show_percent | |
129 # If true, display the labels in the key | |
130 attr_accessor :show_key_data_labels | |
131 # If true, display the actual value of the field in the key | |
132 attr_accessor :show_key_actual_values | |
133 # If true, display the percentage value of the wedges in the key | |
134 attr_accessor :show_key_percent | |
135 # If true, "explode" the pie (put space between the wedges) | |
136 attr_accessor :expanded | |
137 # If true, expand the largest pie wedge | |
138 attr_accessor :expand_greatest | |
139 # The amount of space between expanded wedges | |
140 attr_accessor :expand_gap | |
141 # The font size of the data point labels | |
142 attr_accessor :datapoint_font_size | |
143 | |
144 | |
145 protected | |
146 | |
147 def add_defs defs | |
148 gradient = defs.add_element( "filter", { | |
149 "id"=>"dropshadow", | |
150 "width" => "1.2", | |
151 "height" => "1.2", | |
152 } ) | |
153 gradient.add_element( "feGaussianBlur", { | |
154 "stdDeviation" => "4", | |
155 "result" => "blur" | |
156 }) | |
157 end | |
158 | |
159 # We don't need the graph | |
160 def draw_graph | |
161 end | |
162 | |
163 def get_y_labels | |
164 [""] | |
165 end | |
166 | |
167 def get_x_labels | |
168 [""] | |
169 end | |
170 | |
171 def keys | |
172 total = 0 | |
173 max_value = 0 | |
174 @data.each {|x| total += x } | |
175 percent_scale = 100.0 / total | |
176 count = -1 | |
177 a = @config[:fields].collect{ |x| | |
178 count += 1 | |
179 v = @data[count] | |
180 perc = show_key_percent ? " "+(v * percent_scale).round.to_s+"%" : "" | |
181 x + " [" + v.to_s + "]" + perc | |
182 } | |
183 end | |
184 | |
185 RADIANS = Math::PI/180 | |
186 | |
187 def draw_data | |
188 @graph = @root.add_element( "g" ) | |
189 background = @graph.add_element("g") | |
190 midground = @graph.add_element("g") | |
191 | |
192 diameter = @graph_height > @graph_width ? @graph_width : @graph_height | |
193 diameter -= expand_gap if expanded or expand_greatest | |
194 diameter -= datapoint_font_size if show_data_labels | |
195 diameter -= 10 if show_shadow | |
196 radius = diameter / 2.0 | |
197 | |
198 xoff = (width - diameter) / 2 | |
199 yoff = (height - @border_bottom - diameter) | |
200 yoff -= 10 if show_shadow | |
201 @graph.attributes['transform'] = "translate( #{xoff} #{yoff} )" | |
202 | |
203 wedge_text_pad = 5 | |
204 wedge_text_pad = 20 if show_percent and show_data_labels | |
205 | |
206 total = 0 | |
207 max_value = 0 | |
208 @data.each {|x| | |
209 max_value = max_value < x ? x : max_value | |
210 total += x | |
211 } | |
212 percent_scale = 100.0 / total | |
213 | |
214 prev_percent = 0 | |
215 rad_mult = 3.6 * RADIANS | |
216 @config[:fields].each_index { |count| | |
217 value = @data[count] | |
218 percent = percent_scale * value | |
219 | |
220 radians = prev_percent * rad_mult | |
221 x_start = radius+(Math.sin(radians) * radius) | |
222 y_start = radius-(Math.cos(radians) * radius) | |
223 radians = (prev_percent+percent) * rad_mult | |
224 x_end = radius+(Math.sin(radians) * radius) | |
225 x_end -= 0.00001 if @data.length == 1 | |
226 y_end = radius-(Math.cos(radians) * radius) | |
227 path = "M#{radius},#{radius} L#{x_start},#{y_start} "+ | |
228 "A#{radius},#{radius} "+ | |
229 "0, #{percent >= 50 ? '1' : '0'},1, "+ | |
230 "#{x_end} #{y_end} Z" | |
231 | |
232 | |
233 wedge = @foreground.add_element( "path", { | |
234 "d" => path, | |
235 "class" => "fill#{count+1}" | |
236 }) | |
237 | |
238 translate = nil | |
239 tx = 0 | |
240 ty = 0 | |
241 half_percent = prev_percent + percent / 2 | |
242 radians = half_percent * rad_mult | |
243 | |
244 if show_shadow | |
245 shadow = background.add_element( "path", { | |
246 "d" => path, | |
247 "filter" => "url(#dropshadow)", | |
248 "style" => "fill: #ccc; stroke: none;" | |
249 }) | |
250 clear = midground.add_element( "path", { | |
251 "d" => path, | |
252 "style" => "fill: #fff; stroke: none;" | |
253 }) | |
254 end | |
255 | |
256 if expanded or (expand_greatest && value == max_value) | |
257 tx = (Math.sin(radians) * expand_gap) | |
258 ty = -(Math.cos(radians) * expand_gap) | |
259 translate = "translate( #{tx} #{ty} )" | |
260 wedge.attributes["transform"] = translate | |
261 clear.attributes["transform"] = translate if clear | |
262 end | |
263 | |
264 if show_shadow | |
265 shadow.attributes["transform"] = | |
266 "translate( #{tx+shadow_offset} #{ty+shadow_offset} )" | |
267 end | |
268 | |
269 if show_data_labels and value != 0 | |
270 label = "" | |
271 label += @config[:fields][count] if show_key_data_labels | |
272 label += " ["+value.to_s+"]" if show_actual_values | |
273 label += " "+percent.round.to_s+"%" if show_percent | |
274 | |
275 msr = Math.sin(radians) | |
276 mcr = Math.cos(radians) | |
277 tx = radius + (msr * radius) | |
278 ty = radius -(mcr * radius) | |
279 | |
280 if expanded or (expand_greatest && value == max_value) | |
281 tx += (msr * expand_gap) | |
282 ty -= (mcr * expand_gap) | |
283 end | |
284 @foreground.add_element( "text", { | |
285 "x" => tx.to_s, | |
286 "y" => ty.to_s, | |
287 "class" => "dataPointLabel", | |
288 "style" => "stroke: #fff; stroke-width: 2;" | |
289 }).text = label.to_s | |
290 @foreground.add_element( "text", { | |
291 "x" => tx.to_s, | |
292 "y" => ty.to_s, | |
293 "class" => "dataPointLabel", | |
294 }).text = label.to_s | |
295 end | |
296 | |
297 prev_percent += percent | |
298 } | |
299 end | |
300 | |
301 | |
302 def round val, to | |
303 up = 10**to.to_f | |
304 (val * up).to_i / up | |
305 end | |
306 | |
307 | |
308 def get_css | |
309 return <<EOL | |
310 .dataPointLabel{ | |
311 fill: #000000; | |
312 text-anchor:middle; | |
313 font-size: #{datapoint_font_size}px; | |
314 font-family: "Arial", sans-serif; | |
315 font-weight: normal; | |
316 } | |
317 | |
318 /* key - MUST match fill styles */ | |
319 .key1,.fill1{ | |
320 fill: #ff0000; | |
321 fill-opacity: 0.7; | |
322 stroke: none; | |
323 stroke-width: 1px; | |
324 } | |
325 .key2,.fill2{ | |
326 fill: #0000ff; | |
327 fill-opacity: 0.7; | |
328 stroke: none; | |
329 stroke-width: 1px; | |
330 } | |
331 .key3,.fill3{ | |
332 fill-opacity: 0.7; | |
333 fill: #00ff00; | |
334 stroke: none; | |
335 stroke-width: 1px; | |
336 } | |
337 .key4,.fill4{ | |
338 fill-opacity: 0.7; | |
339 fill: #ffcc00; | |
340 stroke: none; | |
341 stroke-width: 1px; | |
342 } | |
343 .key5,.fill5{ | |
344 fill-opacity: 0.7; | |
345 fill: #00ccff; | |
346 stroke: none; | |
347 stroke-width: 1px; | |
348 } | |
349 .key6,.fill6{ | |
350 fill-opacity: 0.7; | |
351 fill: #ff00ff; | |
352 stroke: none; | |
353 stroke-width: 1px; | |
354 } | |
355 .key7,.fill7{ | |
356 fill-opacity: 0.7; | |
357 fill: #00ff99; | |
358 stroke: none; | |
359 stroke-width: 1px; | |
360 } | |
361 .key8,.fill8{ | |
362 fill-opacity: 0.7; | |
363 fill: #ffff00; | |
364 stroke: none; | |
365 stroke-width: 1px; | |
366 } | |
367 .key9,.fill9{ | |
368 fill-opacity: 0.7; | |
369 fill: #cc6666; | |
370 stroke: none; | |
371 stroke-width: 1px; | |
372 } | |
373 .key10,.fill10{ | |
374 fill-opacity: 0.7; | |
375 fill: #663399; | |
376 stroke: none; | |
377 stroke-width: 1px; | |
378 } | |
379 .key11,.fill11{ | |
380 fill-opacity: 0.7; | |
381 fill: #339900; | |
382 stroke: none; | |
383 stroke-width: 1px; | |
384 } | |
385 .key12,.fill12{ | |
386 fill-opacity: 0.7; | |
387 fill: #9966FF; | |
388 stroke: none; | |
389 stroke-width: 1px; | |
390 } | |
391 EOL | |
392 end | |
393 end | |
394 end | |
395 end |