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