Mercurial > hg > soundsoftware-site
comparison lib/SVG/Graph/.svn/text-base/Schedule.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/Plot' | |
2 require 'parsedate' | |
3 | |
4 module SVG | |
5 module Graph | |
6 # === For creating SVG plots of scalar temporal data | |
7 # | |
8 # = Synopsis | |
9 # | |
10 # require 'SVG/Graph/Schedule' | |
11 # | |
12 # # Data sets are label, start, end tripples. | |
13 # data1 = [ | |
14 # "Housesitting", "6/17/04", "6/19/04", | |
15 # "Summer Session", "6/15/04", "8/15/04", | |
16 # ] | |
17 # | |
18 # graph = SVG::Graph::Schedule.new( { | |
19 # :width => 640, | |
20 # :height => 480, | |
21 # :graph_title => title, | |
22 # :show_graph_title => true, | |
23 # :no_css => true, | |
24 # :scale_x_integers => true, | |
25 # :scale_y_integers => true, | |
26 # :min_x_value => 0, | |
27 # :min_y_value => 0, | |
28 # :show_data_labels => true, | |
29 # :show_x_guidelines => true, | |
30 # :show_x_title => true, | |
31 # :x_title => "Time", | |
32 # :stagger_x_labels => true, | |
33 # :stagger_y_labels => true, | |
34 # :x_label_format => "%m/%d/%y", | |
35 # }) | |
36 # | |
37 # graph.add_data({ | |
38 # :data => data1, | |
39 # :title => 'Data', | |
40 # }) | |
41 # | |
42 # print graph.burn() | |
43 # | |
44 # = Description | |
45 # | |
46 # Produces a graph of temporal scalar data. | |
47 # | |
48 # = Examples | |
49 # | |
50 # http://www.germane-software/repositories/public/SVG/test/schedule.rb | |
51 # | |
52 # = Notes | |
53 # | |
54 # The default stylesheet handles upto 10 data sets, if you | |
55 # use more you must create your own stylesheet and add the | |
56 # additional settings for the extra data sets. You will know | |
57 # if you go over 10 data sets as they will have no style and | |
58 # be in black. | |
59 # | |
60 # Note that multiple data sets within the same chart can differ in | |
61 # length, and that the data in the datasets needn't be in order; | |
62 # they will be ordered by the plot along the X-axis. | |
63 # | |
64 # The dates must be parseable by ParseDate, but otherwise can be | |
65 # any order of magnitude (seconds within the hour, or years) | |
66 # | |
67 # = See also | |
68 # | |
69 # * SVG::Graph::Graph | |
70 # * SVG::Graph::BarHorizontal | |
71 # * SVG::Graph::Bar | |
72 # * SVG::Graph::Line | |
73 # * SVG::Graph::Pie | |
74 # * SVG::Graph::Plot | |
75 # * SVG::Graph::TimeSeries | |
76 # | |
77 # == Author | |
78 # | |
79 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> | |
80 # | |
81 # Copyright 2004 Sean E. Russell | |
82 # This software is available under the Ruby license[LICENSE.txt] | |
83 # | |
84 class Schedule < Graph | |
85 # In addition to the defaults set by Graph::initialize and | |
86 # Plot::set_defaults, sets: | |
87 # [x_label_format] '%Y-%m-%d %H:%M:%S' | |
88 # [popup_format] '%Y-%m-%d %H:%M:%S' | |
89 def set_defaults | |
90 init_with( | |
91 :x_label_format => '%Y-%m-%d %H:%M:%S', | |
92 :popup_format => '%Y-%m-%d %H:%M:%S', | |
93 :scale_x_divisions => false, | |
94 :scale_x_integers => false, | |
95 :bar_gap => true | |
96 ) | |
97 end | |
98 | |
99 # The format string use do format the X axis labels. | |
100 # See Time::strformat | |
101 attr_accessor :x_label_format | |
102 # Use this to set the spacing between dates on the axis. The value | |
103 # must be of the form | |
104 # "\d+ ?(days|weeks|months|years|hours|minutes|seconds)?" | |
105 # | |
106 # EG: | |
107 # | |
108 # graph.timescale_divisions = "2 weeks" | |
109 # | |
110 # will cause the chart to try to divide the X axis up into segments of | |
111 # two week periods. | |
112 attr_accessor :timescale_divisions | |
113 # The formatting used for the popups. See x_label_format | |
114 attr_accessor :popup_format | |
115 attr_accessor :min_x_value | |
116 attr_accessor :scale_x_divisions | |
117 attr_accessor :scale_x_integers | |
118 attr_accessor :bar_gap | |
119 | |
120 # Add data to the plot. | |
121 # | |
122 # # A data set with 1 point: Lunch from 12:30 to 14:00 | |
123 # d1 = [ "Lunch", "12:30", "14:00" ] | |
124 # # A data set with 2 points: "Cats" runs from 5/11/03 to 7/15/04, and | |
125 # # "Henry V" runs from 6/12/03 to 8/20/03 | |
126 # d2 = [ "Cats", "5/11/03", "7/15/04", | |
127 # "Henry V", "6/12/03", "8/20/03" ] | |
128 # | |
129 # graph.add_data( | |
130 # :data => d1, | |
131 # :title => 'Meetings' | |
132 # ) | |
133 # graph.add_data( | |
134 # :data => d2, | |
135 # :title => 'Plays' | |
136 # ) | |
137 # | |
138 # Note that the data must be in time,value pairs, and that the date format | |
139 # may be any date that is parseable by ParseDate. | |
140 # Also note that, in this example, we're mixing scales; the data from d1 | |
141 # will probably not be discernable if both data sets are plotted on the same | |
142 # graph, since d1 is too granular. | |
143 def add_data data | |
144 @data = [] unless @data | |
145 | |
146 raise "No data provided by #{conf.inspect}" unless data[:data] and | |
147 data[:data].kind_of? Array | |
148 raise "Data supplied must be title,from,to tripples! "+ | |
149 "The data provided contained an odd set of "+ | |
150 "data points" unless data[:data].length % 3 == 0 | |
151 return if data[:data].length == 0 | |
152 | |
153 | |
154 y = [] | |
155 x_start = [] | |
156 x_end = [] | |
157 data[:data].each_index {|i| | |
158 im3 = i%3 | |
159 if im3 == 0 | |
160 y << data[:data][i] | |
161 else | |
162 arr = ParseDate.parsedate( data[:data][i] ) | |
163 t = Time.local( *arr[0,6].compact ) | |
164 (im3 == 1 ? x_start : x_end) << t.to_i | |
165 end | |
166 } | |
167 sort( x_start, x_end, y ) | |
168 @data = [x_start, x_end, y ] | |
169 end | |
170 | |
171 | |
172 protected | |
173 | |
174 def min_x_value=(value) | |
175 arr = ParseDate.parsedate( value ) | |
176 @min_x_value = Time.local( *arr[0,6].compact ).to_i | |
177 end | |
178 | |
179 | |
180 def format x, y | |
181 Time.at( x ).strftime( popup_format ) | |
182 end | |
183 | |
184 def get_x_labels | |
185 rv = get_x_values.collect { |v| Time.at(v).strftime( x_label_format ) } | |
186 end | |
187 | |
188 def y_label_offset( height ) | |
189 height / -2.0 | |
190 end | |
191 | |
192 def get_y_labels | |
193 @data[2] | |
194 end | |
195 | |
196 def draw_data | |
197 fieldheight = field_height | |
198 fieldwidth = field_width | |
199 | |
200 bargap = bar_gap ? (fieldheight < 10 ? fieldheight / 2 : 10) : 0 | |
201 subbar_height = fieldheight - bargap | |
202 | |
203 field_count = 1 | |
204 y_mod = (subbar_height / 2) + (font_size / 2) | |
205 min,max,div = x_range | |
206 scale = (@graph_width.to_f - font_size*2) / (max-min) | |
207 @data[0].each_index { |i| | |
208 x_start = @data[0][i] | |
209 x_end = @data[1][i] | |
210 y = @graph_height - (fieldheight * field_count) | |
211 bar_width = (x_end-x_start) * scale | |
212 bar_start = x_start * scale - (min * scale) | |
213 | |
214 @graph.add_element( "rect", { | |
215 "x" => bar_start.to_s, | |
216 "y" => y.to_s, | |
217 "width" => bar_width.to_s, | |
218 "height" => subbar_height.to_s, | |
219 "class" => "fill#{field_count+1}" | |
220 }) | |
221 field_count += 1 | |
222 } | |
223 end | |
224 | |
225 def get_css | |
226 return <<EOL | |
227 /* default fill styles for multiple datasets (probably only use a single dataset on this graph though) */ | |
228 .key1,.fill1{ | |
229 fill: #ff0000; | |
230 fill-opacity: 0.5; | |
231 stroke: none; | |
232 stroke-width: 0.5px; | |
233 } | |
234 .key2,.fill2{ | |
235 fill: #0000ff; | |
236 fill-opacity: 0.5; | |
237 stroke: none; | |
238 stroke-width: 1px; | |
239 } | |
240 .key3,.fill3{ | |
241 fill: #00ff00; | |
242 fill-opacity: 0.5; | |
243 stroke: none; | |
244 stroke-width: 1px; | |
245 } | |
246 .key4,.fill4{ | |
247 fill: #ffcc00; | |
248 fill-opacity: 0.5; | |
249 stroke: none; | |
250 stroke-width: 1px; | |
251 } | |
252 .key5,.fill5{ | |
253 fill: #00ccff; | |
254 fill-opacity: 0.5; | |
255 stroke: none; | |
256 stroke-width: 1px; | |
257 } | |
258 .key6,.fill6{ | |
259 fill: #ff00ff; | |
260 fill-opacity: 0.5; | |
261 stroke: none; | |
262 stroke-width: 1px; | |
263 } | |
264 .key7,.fill7{ | |
265 fill: #00ffff; | |
266 fill-opacity: 0.5; | |
267 stroke: none; | |
268 stroke-width: 1px; | |
269 } | |
270 .key8,.fill8{ | |
271 fill: #ffff00; | |
272 fill-opacity: 0.5; | |
273 stroke: none; | |
274 stroke-width: 1px; | |
275 } | |
276 .key9,.fill9{ | |
277 fill: #cc6666; | |
278 fill-opacity: 0.5; | |
279 stroke: none; | |
280 stroke-width: 1px; | |
281 } | |
282 .key10,.fill10{ | |
283 fill: #663399; | |
284 fill-opacity: 0.5; | |
285 stroke: none; | |
286 stroke-width: 1px; | |
287 } | |
288 .key11,.fill11{ | |
289 fill: #339900; | |
290 fill-opacity: 0.5; | |
291 stroke: none; | |
292 stroke-width: 1px; | |
293 } | |
294 .key12,.fill12{ | |
295 fill: #9966FF; | |
296 fill-opacity: 0.5; | |
297 stroke: none; | |
298 stroke-width: 1px; | |
299 } | |
300 EOL | |
301 end | |
302 | |
303 private | |
304 def x_range | |
305 max_value = [ @data[0][-1], @data[1].max ].max | |
306 min_value = [ @data[0][0], @data[1].min ].min | |
307 min_value = min_value<min_x_value ? min_value : min_x_value if min_x_value | |
308 | |
309 range = max_value - min_value | |
310 right_pad = range == 0 ? 10 : range / 20.0 | |
311 scale_range = (max_value + right_pad) - min_value | |
312 | |
313 scale_division = scale_x_divisions || (scale_range / 10.0) | |
314 | |
315 if scale_x_integers | |
316 scale_division = scale_division < 1 ? 1 : scale_division.round | |
317 end | |
318 | |
319 [min_value, max_value, scale_division] | |
320 end | |
321 | |
322 def get_x_values | |
323 rv = [] | |
324 min, max, scale_division = x_range | |
325 if timescale_divisions | |
326 timescale_divisions =~ /(\d+) ?(days|weeks|months|years|hours|minutes|seconds)?/ | |
327 division_units = $2 ? $2 : "days" | |
328 amount = $1.to_i | |
329 if amount | |
330 step = nil | |
331 case division_units | |
332 when "months" | |
333 cur = min | |
334 while cur < max | |
335 rv << cur | |
336 arr = Time.at( cur ).to_a | |
337 arr[4] += amount | |
338 if arr[4] > 12 | |
339 arr[5] += (arr[4] / 12).to_i | |
340 arr[4] = (arr[4] % 12) | |
341 end | |
342 cur = Time.local(*arr).to_i | |
343 end | |
344 when "years" | |
345 cur = min | |
346 while cur < max | |
347 rv << cur | |
348 arr = Time.at( cur ).to_a | |
349 arr[5] += amount | |
350 cur = Time.local(*arr).to_i | |
351 end | |
352 when "weeks" | |
353 step = 7 * 24 * 60 * 60 * amount | |
354 when "days" | |
355 step = 24 * 60 * 60 * amount | |
356 when "hours" | |
357 step = 60 * 60 * amount | |
358 when "minutes" | |
359 step = 60 * amount | |
360 when "seconds" | |
361 step = amount | |
362 end | |
363 min.step( max, step ) {|v| rv << v} if step | |
364 | |
365 return rv | |
366 end | |
367 end | |
368 min.step( max, scale_division ) {|v| rv << v} | |
369 return rv | |
370 end | |
371 end | |
372 end | |
373 end |