Mercurial > hg > soundsoftware-site
comparison .svn/pristine/83/83300e07f9013be5c854520eee231d3e094963c6.svn-base @ 1295:622f24f53b42 redmine-2.3
Update to Redmine SVN revision 11972 on 2.3-stable branch
author | Chris Cannam |
---|---|
date | Fri, 14 Jun 2013 09:02:21 +0100 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
1294:3e4c3460b6ca | 1295:622f24f53b42 |
---|---|
1 require 'SVG/Graph/Graph' | |
2 | |
3 module SVG | |
4 module Graph | |
5 # === For creating SVG plots of scalar data | |
6 # | |
7 # = Synopsis | |
8 # | |
9 # require 'SVG/Graph/Plot' | |
10 # | |
11 # # Data sets are x,y pairs | |
12 # # Note that multiple data sets can differ in length, and that the | |
13 # # data in the datasets needn't be in order; they will be ordered | |
14 # # by the plot along the X-axis. | |
15 # projection = [ | |
16 # 6, 11, 0, 5, 18, 7, 1, 11, 13, 9, 1, 2, 19, 0, 3, 13, | |
17 # 7, 9 | |
18 # ] | |
19 # actual = [ | |
20 # 0, 18, 8, 15, 9, 4, 18, 14, 10, 2, 11, 6, 14, 12, | |
21 # 15, 6, 4, 17, 2, 12 | |
22 # ] | |
23 # | |
24 # graph = SVG::Graph::Plot.new({ | |
25 # :height => 500, | |
26 # :width => 300, | |
27 # :key => true, | |
28 # :scale_x_integers => true, | |
29 # :scale_y_integerrs => true, | |
30 # }) | |
31 # | |
32 # graph.add_data({ | |
33 # :data => projection | |
34 # :title => 'Projected', | |
35 # }) | |
36 # | |
37 # graph.add_data({ | |
38 # :data => actual, | |
39 # :title => 'Actual', | |
40 # }) | |
41 # | |
42 # print graph.burn() | |
43 # | |
44 # = Description | |
45 # | |
46 # Produces a graph of scalar data. | |
47 # | |
48 # This object aims to allow you to easily create high quality | |
49 # SVG[http://www.w3c.org/tr/svg] scalar plots. You can either use the | |
50 # default style sheet or supply your own. Either way there are many options | |
51 # which can be configured to give you control over how the graph is | |
52 # generated - with or without a key, data elements at each point, title, | |
53 # subtitle etc. | |
54 # | |
55 # = Examples | |
56 # | |
57 # http://www.germane-software/repositories/public/SVG/test/plot.rb | |
58 # | |
59 # = Notes | |
60 # | |
61 # The default stylesheet handles upto 10 data sets, if you | |
62 # use more you must create your own stylesheet and add the | |
63 # additional settings for the extra data sets. You will know | |
64 # if you go over 10 data sets as they will have no style and | |
65 # be in black. | |
66 # | |
67 # Unlike the other types of charts, data sets must contain x,y pairs: | |
68 # | |
69 # [ 1, 2 ] # A data set with 1 point: (1,2) | |
70 # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6) | |
71 # | |
72 # = See also | |
73 # | |
74 # * SVG::Graph::Graph | |
75 # * SVG::Graph::BarHorizontal | |
76 # * SVG::Graph::Bar | |
77 # * SVG::Graph::Line | |
78 # * SVG::Graph::Pie | |
79 # * SVG::Graph::TimeSeries | |
80 # | |
81 # == Author | |
82 # | |
83 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> | |
84 # | |
85 # Copyright 2004 Sean E. Russell | |
86 # This software is available under the Ruby license[LICENSE.txt] | |
87 # | |
88 class Plot < Graph | |
89 | |
90 # In addition to the defaults set by Graph::initialize, sets | |
91 # [show_data_values] true | |
92 # [show_data_points] true | |
93 # [area_fill] false | |
94 # [stacked] false | |
95 def set_defaults | |
96 init_with( | |
97 :show_data_values => true, | |
98 :show_data_points => true, | |
99 :area_fill => false, | |
100 :stacked => false | |
101 ) | |
102 self.top_align = self.right_align = self.top_font = self.right_font = 1 | |
103 end | |
104 | |
105 # Determines the scaling for the X axis divisions. | |
106 # | |
107 # graph.scale_x_divisions = 2 | |
108 # | |
109 # would cause the graph to attempt to generate labels stepped by 2; EG: | |
110 # 0,2,4,6,8... | |
111 attr_accessor :scale_x_divisions | |
112 # Determines the scaling for the Y axis divisions. | |
113 # | |
114 # graph.scale_y_divisions = 0.5 | |
115 # | |
116 # would cause the graph to attempt to generate labels stepped by 0.5; EG: | |
117 # 0, 0.5, 1, 1.5, 2, ... | |
118 attr_accessor :scale_y_divisions | |
119 # Make the X axis labels integers | |
120 attr_accessor :scale_x_integers | |
121 # Make the Y axis labels integers | |
122 attr_accessor :scale_y_integers | |
123 # Fill the area under the line | |
124 attr_accessor :area_fill | |
125 # Show a small circle on the graph where the line | |
126 # goes from one point to the next. | |
127 attr_accessor :show_data_points | |
128 # Set the minimum value of the X axis | |
129 attr_accessor :min_x_value | |
130 # Set the minimum value of the Y axis | |
131 attr_accessor :min_y_value | |
132 | |
133 | |
134 # Adds data to the plot. The data must be in X,Y pairs; EG | |
135 # [ 1, 2 ] # A data set with 1 point: (1,2) | |
136 # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6) | |
137 def add_data data | |
138 @data = [] unless @data | |
139 | |
140 raise "No data provided by #{conf.inspect}" unless data[:data] and | |
141 data[:data].kind_of? Array | |
142 raise "Data supplied must be x,y pairs! "+ | |
143 "The data provided contained an odd set of "+ | |
144 "data points" unless data[:data].length % 2 == 0 | |
145 return if data[:data].length == 0 | |
146 | |
147 x = [] | |
148 y = [] | |
149 data[:data].each_index {|i| | |
150 (i%2 == 0 ? x : y) << data[:data][i] | |
151 } | |
152 sort( x, y ) | |
153 data[:data] = [x,y] | |
154 @data << data | |
155 end | |
156 | |
157 | |
158 protected | |
159 | |
160 def keys | |
161 @data.collect{ |x| x[:title] } | |
162 end | |
163 | |
164 def calculate_left_margin | |
165 super | |
166 label_left = get_x_labels[0].to_s.length / 2 * font_size * 0.6 | |
167 @border_left = label_left if label_left > @border_left | |
168 end | |
169 | |
170 def calculate_right_margin | |
171 super | |
172 label_right = get_x_labels[-1].to_s.length / 2 * font_size * 0.6 | |
173 @border_right = label_right if label_right > @border_right | |
174 end | |
175 | |
176 | |
177 X = 0 | |
178 Y = 1 | |
179 def x_range | |
180 max_value = @data.collect{|x| x[:data][X][-1] }.max | |
181 min_value = @data.collect{|x| x[:data][X][0] }.min | |
182 min_value = min_value<min_x_value ? min_value : min_x_value if min_x_value | |
183 | |
184 range = max_value - min_value | |
185 right_pad = range == 0 ? 10 : range / 20.0 | |
186 scale_range = (max_value + right_pad) - min_value | |
187 | |
188 scale_division = scale_x_divisions || (scale_range / 10.0) | |
189 | |
190 if scale_x_integers | |
191 scale_division = scale_division < 1 ? 1 : scale_division.round | |
192 end | |
193 | |
194 [min_value, max_value, scale_division] | |
195 end | |
196 | |
197 def get_x_values | |
198 min_value, max_value, scale_division = x_range | |
199 rv = [] | |
200 min_value.step( max_value, scale_division ) {|v| rv << v} | |
201 return rv | |
202 end | |
203 alias :get_x_labels :get_x_values | |
204 | |
205 def field_width | |
206 values = get_x_values | |
207 max = @data.collect{|x| x[:data][X][-1]}.max | |
208 dx = (max - values[-1]).to_f / (values[-1] - values[-2]) | |
209 (@graph_width.to_f - font_size*2*right_font) / | |
210 (values.length + dx - right_align) | |
211 end | |
212 | |
213 | |
214 def y_range | |
215 max_value = @data.collect{|x| x[:data][Y].max }.max | |
216 min_value = @data.collect{|x| x[:data][Y].min }.min | |
217 min_value = min_value<min_y_value ? min_value : min_y_value if min_y_value | |
218 | |
219 range = max_value - min_value | |
220 top_pad = range == 0 ? 10 : range / 20.0 | |
221 scale_range = (max_value + top_pad) - min_value | |
222 | |
223 scale_division = scale_y_divisions || (scale_range / 10.0) | |
224 | |
225 if scale_y_integers | |
226 scale_division = scale_division < 1 ? 1 : scale_division.round | |
227 end | |
228 | |
229 return [min_value, max_value, scale_division] | |
230 end | |
231 | |
232 def get_y_values | |
233 min_value, max_value, scale_division = y_range | |
234 rv = [] | |
235 min_value.step( max_value, scale_division ) {|v| rv << v} | |
236 return rv | |
237 end | |
238 alias :get_y_labels :get_y_values | |
239 | |
240 def field_height | |
241 values = get_y_values | |
242 max = @data.collect{|x| x[:data][Y].max }.max | |
243 if values.length == 1 | |
244 dx = values[-1] | |
245 else | |
246 dx = (max - values[-1]).to_f / (values[-1] - values[-2]) | |
247 end | |
248 (@graph_height.to_f - font_size*2*top_font) / | |
249 (values.length + dx - top_align) | |
250 end | |
251 | |
252 def draw_data | |
253 line = 1 | |
254 | |
255 x_min, x_max, x_div = x_range | |
256 y_min, y_max, y_div = y_range | |
257 x_step = (@graph_width.to_f - font_size*2) / (x_max-x_min) | |
258 y_step = (@graph_height.to_f - font_size*2) / (y_max-y_min) | |
259 | |
260 for data in @data | |
261 x_points = data[:data][X] | |
262 y_points = data[:data][Y] | |
263 | |
264 lpath = "L" | |
265 x_start = 0 | |
266 y_start = 0 | |
267 x_points.each_index { |idx| | |
268 x = (x_points[idx] - x_min) * x_step | |
269 y = @graph_height - (y_points[idx] - y_min) * y_step | |
270 x_start, y_start = x,y if idx == 0 | |
271 lpath << "#{x} #{y} " | |
272 } | |
273 | |
274 if area_fill | |
275 @graph.add_element( "path", { | |
276 "d" => "M#{x_start} #@graph_height #{lpath} V#@graph_height Z", | |
277 "class" => "fill#{line}" | |
278 }) | |
279 end | |
280 | |
281 @graph.add_element( "path", { | |
282 "d" => "M#{x_start} #{y_start} #{lpath}", | |
283 "class" => "line#{line}" | |
284 }) | |
285 | |
286 if show_data_points || show_data_values | |
287 x_points.each_index { |idx| | |
288 x = (x_points[idx] - x_min) * x_step | |
289 y = @graph_height - (y_points[idx] - y_min) * y_step | |
290 if show_data_points | |
291 @graph.add_element( "circle", { | |
292 "cx" => x.to_s, | |
293 "cy" => y.to_s, | |
294 "r" => "2.5", | |
295 "class" => "dataPoint#{line}" | |
296 }) | |
297 add_popup(x, y, format( x_points[idx], y_points[idx] )) if add_popups | |
298 end | |
299 make_datapoint_text( x, y-6, y_points[idx] ) if show_data_values | |
300 } | |
301 end | |
302 line += 1 | |
303 end | |
304 end | |
305 | |
306 def format x, y | |
307 "(#{(x * 100).to_i / 100}, #{(y * 100).to_i / 100})" | |
308 end | |
309 | |
310 def get_css | |
311 return <<EOL | |
312 /* default line styles */ | |
313 .line1{ | |
314 fill: none; | |
315 stroke: #ff0000; | |
316 stroke-width: 1px; | |
317 } | |
318 .line2{ | |
319 fill: none; | |
320 stroke: #0000ff; | |
321 stroke-width: 1px; | |
322 } | |
323 .line3{ | |
324 fill: none; | |
325 stroke: #00ff00; | |
326 stroke-width: 1px; | |
327 } | |
328 .line4{ | |
329 fill: none; | |
330 stroke: #ffcc00; | |
331 stroke-width: 1px; | |
332 } | |
333 .line5{ | |
334 fill: none; | |
335 stroke: #00ccff; | |
336 stroke-width: 1px; | |
337 } | |
338 .line6{ | |
339 fill: none; | |
340 stroke: #ff00ff; | |
341 stroke-width: 1px; | |
342 } | |
343 .line7{ | |
344 fill: none; | |
345 stroke: #00ffff; | |
346 stroke-width: 1px; | |
347 } | |
348 .line8{ | |
349 fill: none; | |
350 stroke: #ffff00; | |
351 stroke-width: 1px; | |
352 } | |
353 .line9{ | |
354 fill: none; | |
355 stroke: #ccc6666; | |
356 stroke-width: 1px; | |
357 } | |
358 .line10{ | |
359 fill: none; | |
360 stroke: #663399; | |
361 stroke-width: 1px; | |
362 } | |
363 .line11{ | |
364 fill: none; | |
365 stroke: #339900; | |
366 stroke-width: 1px; | |
367 } | |
368 .line12{ | |
369 fill: none; | |
370 stroke: #9966FF; | |
371 stroke-width: 1px; | |
372 } | |
373 /* default fill styles */ | |
374 .fill1{ | |
375 fill: #cc0000; | |
376 fill-opacity: 0.2; | |
377 stroke: none; | |
378 } | |
379 .fill2{ | |
380 fill: #0000cc; | |
381 fill-opacity: 0.2; | |
382 stroke: none; | |
383 } | |
384 .fill3{ | |
385 fill: #00cc00; | |
386 fill-opacity: 0.2; | |
387 stroke: none; | |
388 } | |
389 .fill4{ | |
390 fill: #ffcc00; | |
391 fill-opacity: 0.2; | |
392 stroke: none; | |
393 } | |
394 .fill5{ | |
395 fill: #00ccff; | |
396 fill-opacity: 0.2; | |
397 stroke: none; | |
398 } | |
399 .fill6{ | |
400 fill: #ff00ff; | |
401 fill-opacity: 0.2; | |
402 stroke: none; | |
403 } | |
404 .fill7{ | |
405 fill: #00ffff; | |
406 fill-opacity: 0.2; | |
407 stroke: none; | |
408 } | |
409 .fill8{ | |
410 fill: #ffff00; | |
411 fill-opacity: 0.2; | |
412 stroke: none; | |
413 } | |
414 .fill9{ | |
415 fill: #cc6666; | |
416 fill-opacity: 0.2; | |
417 stroke: none; | |
418 } | |
419 .fill10{ | |
420 fill: #663399; | |
421 fill-opacity: 0.2; | |
422 stroke: none; | |
423 } | |
424 .fill11{ | |
425 fill: #339900; | |
426 fill-opacity: 0.2; | |
427 stroke: none; | |
428 } | |
429 .fill12{ | |
430 fill: #9966FF; | |
431 fill-opacity: 0.2; | |
432 stroke: none; | |
433 } | |
434 /* default line styles */ | |
435 .key1,.dataPoint1{ | |
436 fill: #ff0000; | |
437 stroke: none; | |
438 stroke-width: 1px; | |
439 } | |
440 .key2,.dataPoint2{ | |
441 fill: #0000ff; | |
442 stroke: none; | |
443 stroke-width: 1px; | |
444 } | |
445 .key3,.dataPoint3{ | |
446 fill: #00ff00; | |
447 stroke: none; | |
448 stroke-width: 1px; | |
449 } | |
450 .key4,.dataPoint4{ | |
451 fill: #ffcc00; | |
452 stroke: none; | |
453 stroke-width: 1px; | |
454 } | |
455 .key5,.dataPoint5{ | |
456 fill: #00ccff; | |
457 stroke: none; | |
458 stroke-width: 1px; | |
459 } | |
460 .key6,.dataPoint6{ | |
461 fill: #ff00ff; | |
462 stroke: none; | |
463 stroke-width: 1px; | |
464 } | |
465 .key7,.dataPoint7{ | |
466 fill: #00ffff; | |
467 stroke: none; | |
468 stroke-width: 1px; | |
469 } | |
470 .key8,.dataPoint8{ | |
471 fill: #ffff00; | |
472 stroke: none; | |
473 stroke-width: 1px; | |
474 } | |
475 .key9,.dataPoint9{ | |
476 fill: #cc6666; | |
477 stroke: none; | |
478 stroke-width: 1px; | |
479 } | |
480 .key10,.dataPoint10{ | |
481 fill: #663399; | |
482 stroke: none; | |
483 stroke-width: 1px; | |
484 } | |
485 .key11,.dataPoint11{ | |
486 fill: #339900; | |
487 stroke: none; | |
488 stroke-width: 1px; | |
489 } | |
490 .key12,.dataPoint12{ | |
491 fill: #9966FF; | |
492 stroke: none; | |
493 stroke-width: 1px; | |
494 } | |
495 EOL | |
496 end | |
497 | |
498 end | |
499 end | |
500 end |