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