Mercurial > hg > soundsoftware-site
comparison lib/SVG/Graph/.svn/text-base/Line.rb.svn-base @ 0:513646585e45
* Import Redmine trunk SVN rev 3859
author | Chris Cannam |
---|---|
date | Fri, 23 Jul 2010 15:52:44 +0100 |
parents | |
children |
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 line graphs easily | |
6 # | |
7 # = Synopsis | |
8 # | |
9 # require 'SVG/Graph/Line' | |
10 # | |
11 # fields = %w(Jan Feb Mar); | |
12 # data_sales_02 = [12, 45, 21] | |
13 # data_sales_03 = [15, 30, 40] | |
14 # | |
15 # graph = SVG::Graph::Line.new({ | |
16 # :height => 500, | |
17 # :width => 300, | |
18 # :fields => fields, | |
19 # }) | |
20 # | |
21 # graph.add_data({ | |
22 # :data => data_sales_02, | |
23 # :title => 'Sales 2002', | |
24 # }) | |
25 # | |
26 # graph.add_data({ | |
27 # :data => data_sales_03, | |
28 # :title => 'Sales 2003', | |
29 # }) | |
30 # | |
31 # print "Content-type: image/svg+xml\r\n\r\n"; | |
32 # print graph.burn(); | |
33 # | |
34 # = Description | |
35 # | |
36 # This object aims to allow you to easily create high quality | |
37 # SVG line graphs. You can either use the default style sheet | |
38 # or supply your own. Either way there are many options which can | |
39 # be configured to give you control over how the graph is | |
40 # generated - with or without a key, data elements at each point, | |
41 # title, subtitle etc. | |
42 # | |
43 # = Examples | |
44 # | |
45 # http://www.germane-software/repositories/public/SVG/test/single.rb | |
46 # | |
47 # = Notes | |
48 # | |
49 # The default stylesheet handles upto 10 data sets, if you | |
50 # use more you must create your own stylesheet and add the | |
51 # additional settings for the extra data sets. You will know | |
52 # if you go over 10 data sets as they will have no style and | |
53 # be in black. | |
54 # | |
55 # = See also | |
56 # | |
57 # * SVG::Graph::Graph | |
58 # * SVG::Graph::BarHorizontal | |
59 # * SVG::Graph::Bar | |
60 # * SVG::Graph::Pie | |
61 # * SVG::Graph::Plot | |
62 # * SVG::Graph::TimeSeries | |
63 # | |
64 # == Author | |
65 # | |
66 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> | |
67 # | |
68 # Copyright 2004 Sean E. Russell | |
69 # This software is available under the Ruby license[LICENSE.txt] | |
70 # | |
71 class Line < SVG::Graph::Graph | |
72 # Show a small circle on the graph where the line | |
73 # goes from one point to the next. | |
74 attr_accessor :show_data_points | |
75 # Accumulates each data set. (i.e. Each point increased by sum of | |
76 # all previous series at same point). Default is 0, set to '1' to show. | |
77 attr_accessor :stacked | |
78 # Fill in the area under the plot if true | |
79 attr_accessor :area_fill | |
80 | |
81 # The constructor takes a hash reference, fields (the names for each | |
82 # field on the X axis) MUST be set, all other values are defaulted to | |
83 # those shown above - with the exception of style_sheet which defaults | |
84 # to using the internal style sheet. | |
85 def initialize config | |
86 raise "fields was not supplied or is empty" unless config[:fields] && | |
87 config[:fields].kind_of?(Array) && | |
88 config[:fields].length > 0 | |
89 super | |
90 end | |
91 | |
92 # In addition to the defaults set in Graph::initialize, sets | |
93 # [show_data_points] true | |
94 # [show_data_values] true | |
95 # [stacked] false | |
96 # [area_fill] false | |
97 def set_defaults | |
98 init_with( | |
99 :show_data_points => true, | |
100 :show_data_values => true, | |
101 :stacked => false, | |
102 :area_fill => false | |
103 ) | |
104 | |
105 self.top_align = self.top_font = self.right_align = self.right_font = 1 | |
106 end | |
107 | |
108 protected | |
109 | |
110 def max_value | |
111 max = 0 | |
112 | |
113 if (stacked == true) then | |
114 sums = Array.new(@config[:fields].length).fill(0) | |
115 | |
116 @data.each do |data| | |
117 sums.each_index do |i| | |
118 sums[i] += data[:data][i].to_f | |
119 end | |
120 end | |
121 | |
122 max = sums.max | |
123 else | |
124 max = @data.collect{|x| x[:data].max}.max | |
125 end | |
126 | |
127 return max | |
128 end | |
129 | |
130 def min_value | |
131 min = 0 | |
132 | |
133 if (min_scale_value.nil? == false) then | |
134 min = min_scale_value | |
135 elsif (stacked == true) then | |
136 min = @data[-1][:data].min | |
137 else | |
138 min = @data.collect{|x| x[:data].min}.min | |
139 end | |
140 | |
141 return min | |
142 end | |
143 | |
144 def get_x_labels | |
145 @config[:fields] | |
146 end | |
147 | |
148 def calculate_left_margin | |
149 super | |
150 label_left = @config[:fields][0].length / 2 * font_size * 0.6 | |
151 @border_left = label_left if label_left > @border_left | |
152 end | |
153 | |
154 def get_y_labels | |
155 maxvalue = max_value | |
156 minvalue = min_value | |
157 range = maxvalue - minvalue | |
158 top_pad = range == 0 ? 10 : range / 20.0 | |
159 scale_range = (maxvalue + top_pad) - minvalue | |
160 | |
161 scale_division = scale_divisions || (scale_range / 10.0) | |
162 | |
163 if scale_integers | |
164 scale_division = scale_division < 1 ? 1 : scale_division.round | |
165 end | |
166 | |
167 rv = [] | |
168 maxvalue = maxvalue%scale_division == 0 ? | |
169 maxvalue : maxvalue + scale_division | |
170 minvalue.step( maxvalue, scale_division ) {|v| rv << v} | |
171 return rv | |
172 end | |
173 | |
174 def calc_coords(field, value, width = field_width, height = field_height) | |
175 coords = {:x => 0, :y => 0} | |
176 coords[:x] = width * field | |
177 coords[:y] = @graph_height - value * height | |
178 | |
179 return coords | |
180 end | |
181 | |
182 def draw_data | |
183 minvalue = min_value | |
184 fieldheight = (@graph_height.to_f - font_size*2*top_font) / | |
185 (get_y_labels.max - get_y_labels.min) | |
186 fieldwidth = field_width | |
187 line = @data.length | |
188 | |
189 prev_sum = Array.new(@config[:fields].length).fill(0) | |
190 cum_sum = Array.new(@config[:fields].length).fill(-minvalue) | |
191 | |
192 for data in @data.reverse | |
193 lpath = "" | |
194 apath = "" | |
195 | |
196 if not stacked then cum_sum.fill(-minvalue) end | |
197 | |
198 data[:data].each_index do |i| | |
199 cum_sum[i] += data[:data][i] | |
200 | |
201 c = calc_coords(i, cum_sum[i], fieldwidth, fieldheight) | |
202 | |
203 lpath << "#{c[:x]} #{c[:y]} " | |
204 end | |
205 | |
206 if area_fill | |
207 if stacked then | |
208 (prev_sum.length - 1).downto 0 do |i| | |
209 c = calc_coords(i, prev_sum[i], fieldwidth, fieldheight) | |
210 | |
211 apath << "#{c[:x]} #{c[:y]} " | |
212 end | |
213 | |
214 c = calc_coords(0, prev_sum[0], fieldwidth, fieldheight) | |
215 else | |
216 apath = "V#@graph_height" | |
217 c = calc_coords(0, 0, fieldwidth, fieldheight) | |
218 end | |
219 | |
220 @graph.add_element("path", { | |
221 "d" => "M#{c[:x]} #{c[:y]} L" + lpath + apath + "Z", | |
222 "class" => "fill#{line}" | |
223 }) | |
224 end | |
225 | |
226 @graph.add_element("path", { | |
227 "d" => "M0 #@graph_height L" + lpath, | |
228 "class" => "line#{line}" | |
229 }) | |
230 | |
231 if show_data_points || show_data_values | |
232 cum_sum.each_index do |i| | |
233 if show_data_points | |
234 @graph.add_element( "circle", { | |
235 "cx" => (fieldwidth * i).to_s, | |
236 "cy" => (@graph_height - cum_sum[i] * fieldheight).to_s, | |
237 "r" => "2.5", | |
238 "class" => "dataPoint#{line}" | |
239 }) | |
240 end | |
241 make_datapoint_text( | |
242 fieldwidth * i, | |
243 @graph_height - cum_sum[i] * fieldheight - 6, | |
244 cum_sum[i] + minvalue | |
245 ) | |
246 end | |
247 end | |
248 | |
249 prev_sum = cum_sum.dup | |
250 line -= 1 | |
251 end | |
252 end | |
253 | |
254 | |
255 def get_css | |
256 return <<EOL | |
257 /* default line styles */ | |
258 .line1{ | |
259 fill: none; | |
260 stroke: #ff0000; | |
261 stroke-width: 1px; | |
262 } | |
263 .line2{ | |
264 fill: none; | |
265 stroke: #0000ff; | |
266 stroke-width: 1px; | |
267 } | |
268 .line3{ | |
269 fill: none; | |
270 stroke: #00ff00; | |
271 stroke-width: 1px; | |
272 } | |
273 .line4{ | |
274 fill: none; | |
275 stroke: #ffcc00; | |
276 stroke-width: 1px; | |
277 } | |
278 .line5{ | |
279 fill: none; | |
280 stroke: #00ccff; | |
281 stroke-width: 1px; | |
282 } | |
283 .line6{ | |
284 fill: none; | |
285 stroke: #ff00ff; | |
286 stroke-width: 1px; | |
287 } | |
288 .line7{ | |
289 fill: none; | |
290 stroke: #00ffff; | |
291 stroke-width: 1px; | |
292 } | |
293 .line8{ | |
294 fill: none; | |
295 stroke: #ffff00; | |
296 stroke-width: 1px; | |
297 } | |
298 .line9{ | |
299 fill: none; | |
300 stroke: #ccc6666; | |
301 stroke-width: 1px; | |
302 } | |
303 .line10{ | |
304 fill: none; | |
305 stroke: #663399; | |
306 stroke-width: 1px; | |
307 } | |
308 .line11{ | |
309 fill: none; | |
310 stroke: #339900; | |
311 stroke-width: 1px; | |
312 } | |
313 .line12{ | |
314 fill: none; | |
315 stroke: #9966FF; | |
316 stroke-width: 1px; | |
317 } | |
318 /* default fill styles */ | |
319 .fill1{ | |
320 fill: #cc0000; | |
321 fill-opacity: 0.2; | |
322 stroke: none; | |
323 } | |
324 .fill2{ | |
325 fill: #0000cc; | |
326 fill-opacity: 0.2; | |
327 stroke: none; | |
328 } | |
329 .fill3{ | |
330 fill: #00cc00; | |
331 fill-opacity: 0.2; | |
332 stroke: none; | |
333 } | |
334 .fill4{ | |
335 fill: #ffcc00; | |
336 fill-opacity: 0.2; | |
337 stroke: none; | |
338 } | |
339 .fill5{ | |
340 fill: #00ccff; | |
341 fill-opacity: 0.2; | |
342 stroke: none; | |
343 } | |
344 .fill6{ | |
345 fill: #ff00ff; | |
346 fill-opacity: 0.2; | |
347 stroke: none; | |
348 } | |
349 .fill7{ | |
350 fill: #00ffff; | |
351 fill-opacity: 0.2; | |
352 stroke: none; | |
353 } | |
354 .fill8{ | |
355 fill: #ffff00; | |
356 fill-opacity: 0.2; | |
357 stroke: none; | |
358 } | |
359 .fill9{ | |
360 fill: #cc6666; | |
361 fill-opacity: 0.2; | |
362 stroke: none; | |
363 } | |
364 .fill10{ | |
365 fill: #663399; | |
366 fill-opacity: 0.2; | |
367 stroke: none; | |
368 } | |
369 .fill11{ | |
370 fill: #339900; | |
371 fill-opacity: 0.2; | |
372 stroke: none; | |
373 } | |
374 .fill12{ | |
375 fill: #9966FF; | |
376 fill-opacity: 0.2; | |
377 stroke: none; | |
378 } | |
379 /* default line styles */ | |
380 .key1,.dataPoint1{ | |
381 fill: #ff0000; | |
382 stroke: none; | |
383 stroke-width: 1px; | |
384 } | |
385 .key2,.dataPoint2{ | |
386 fill: #0000ff; | |
387 stroke: none; | |
388 stroke-width: 1px; | |
389 } | |
390 .key3,.dataPoint3{ | |
391 fill: #00ff00; | |
392 stroke: none; | |
393 stroke-width: 1px; | |
394 } | |
395 .key4,.dataPoint4{ | |
396 fill: #ffcc00; | |
397 stroke: none; | |
398 stroke-width: 1px; | |
399 } | |
400 .key5,.dataPoint5{ | |
401 fill: #00ccff; | |
402 stroke: none; | |
403 stroke-width: 1px; | |
404 } | |
405 .key6,.dataPoint6{ | |
406 fill: #ff00ff; | |
407 stroke: none; | |
408 stroke-width: 1px; | |
409 } | |
410 .key7,.dataPoint7{ | |
411 fill: #00ffff; | |
412 stroke: none; | |
413 stroke-width: 1px; | |
414 } | |
415 .key8,.dataPoint8{ | |
416 fill: #ffff00; | |
417 stroke: none; | |
418 stroke-width: 1px; | |
419 } | |
420 .key9,.dataPoint9{ | |
421 fill: #cc6666; | |
422 stroke: none; | |
423 stroke-width: 1px; | |
424 } | |
425 .key10,.dataPoint10{ | |
426 fill: #663399; | |
427 stroke: none; | |
428 stroke-width: 1px; | |
429 } | |
430 .key11,.dataPoint11{ | |
431 fill: #339900; | |
432 stroke: none; | |
433 stroke-width: 1px; | |
434 } | |
435 .key12,.dataPoint12{ | |
436 fill: #9966FF; | |
437 stroke: none; | |
438 stroke-width: 1px; | |
439 } | |
440 EOL | |
441 end | |
442 end | |
443 end | |
444 end |