svg-graph 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,373 @@
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
@@ -0,0 +1,241 @@
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/TimeSeriess'
11
+ #
12
+ # # Data sets are x,y pairs
13
+ # data1 = ["6/17/72", 11, "1/11/72", 7, "4/13/04 17:31", 11,
14
+ # "9/11/01", 9, "9/1/85", 2, "9/1/88", 1, "1/15/95", 13]
15
+ # data2 = ["8/1/73", 18, "3/1/77", 15, "10/1/98", 4,
16
+ # "5/1/02", 14, "3/1/95", 6, "8/1/91", 12, "12/1/87", 6,
17
+ # "5/1/84", 17, "10/1/80", 12]
18
+ #
19
+ # graph = SVG::Graph::TimeSeries.new( {
20
+ # :width => 640,
21
+ # :height => 480,
22
+ # :graph_title => title,
23
+ # :show_graph_title => true,
24
+ # :no_css => true,
25
+ # :key => true,
26
+ # :scale_x_integers => true,
27
+ # :scale_y_integers => true,
28
+ # :min_x_value => 0,
29
+ # :min_y_value => 0,
30
+ # :show_data_labels => true,
31
+ # :show_x_guidelines => true,
32
+ # :show_x_title => true,
33
+ # :x_title => "Time",
34
+ # :show_y_title => true,
35
+ # :y_title => "Ice Cream Cones",
36
+ # :y_title_text_direction => :bt,
37
+ # :stagger_x_labels => true,
38
+ # :x_label_format => "%m/%d/%y",
39
+ # })
40
+ #
41
+ # graph.add_data({
42
+ # :data => projection
43
+ # :title => 'Projected',
44
+ # })
45
+ #
46
+ # graph.add_data({
47
+ # :data => actual,
48
+ # :title => 'Actual',
49
+ # })
50
+ #
51
+ # print graph.burn()
52
+ #
53
+ # = Description
54
+ #
55
+ # Produces a graph of temporal scalar data.
56
+ #
57
+ # = Examples
58
+ #
59
+ # http://www.germane-software/repositories/public/SVG/test/timeseries.rb
60
+ #
61
+ # = Notes
62
+ #
63
+ # The default stylesheet handles upto 10 data sets, if you
64
+ # use more you must create your own stylesheet and add the
65
+ # additional settings for the extra data sets. You will know
66
+ # if you go over 10 data sets as they will have no style and
67
+ # be in black.
68
+ #
69
+ # Unlike the other types of charts, data sets must contain x,y pairs:
70
+ #
71
+ # [ "12:30", 2 ] # A data set with 1 point: ("12:30",2)
72
+ # [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and
73
+ # # ("14:20",6)
74
+ #
75
+ # Note that multiple data sets within the same chart can differ in length,
76
+ # and that the data in the datasets needn't be in order; they will be ordered
77
+ # by the plot along the X-axis.
78
+ #
79
+ # The dates must be parseable by ParseDate, but otherwise can be
80
+ # any order of magnitude (seconds within the hour, or years)
81
+ #
82
+ # = See also
83
+ #
84
+ # * SVG::Graph::Graph
85
+ # * SVG::Graph::BarHorizontal
86
+ # * SVG::Graph::Bar
87
+ # * SVG::Graph::Line
88
+ # * SVG::Graph::Pie
89
+ # * SVG::Graph::Plot
90
+ #
91
+ # == Author
92
+ #
93
+ # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
94
+ #
95
+ # Copyright 2004 Sean E. Russell
96
+ # This software is available under the Ruby license[LICENSE.txt]
97
+ #
98
+ class TimeSeries < Plot
99
+ # In addition to the defaults set by Graph::initialize and
100
+ # Plot::set_defaults, sets:
101
+ # [x_label_format] '%Y-%m-%d %H:%M:%S'
102
+ # [popup_format] '%Y-%m-%d %H:%M:%S'
103
+ def set_defaults
104
+ super
105
+ init_with(
106
+ #:max_time_span => '',
107
+ :x_label_format => '%Y-%m-%d %H:%M:%S',
108
+ :popup_format => '%Y-%m-%d %H:%M:%S'
109
+ )
110
+ end
111
+
112
+ # The format string use do format the X axis labels.
113
+ # See Time::strformat
114
+ attr_accessor :x_label_format
115
+ # Use this to set the spacing between dates on the axis. The value
116
+ # must be of the form
117
+ # "\d+ ?(days|weeks|months|years|hours|minutes|seconds)?"
118
+ #
119
+ # EG:
120
+ #
121
+ # graph.timescale_divisions = "2 weeks"
122
+ #
123
+ # will cause the chart to try to divide the X axis up into segments of
124
+ # two week periods.
125
+ attr_accessor :timescale_divisions
126
+ # The formatting used for the popups. See x_label_format
127
+ attr_accessor :popup_format
128
+
129
+ # Add data to the plot.
130
+ #
131
+ # d1 = [ "12:30", 2 ] # A data set with 1 point: ("12:30",2)
132
+ # d2 = [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and
133
+ # # ("14:20",6)
134
+ # graph.add_data(
135
+ # :data => d1,
136
+ # :title => 'One'
137
+ # )
138
+ # graph.add_data(
139
+ # :data => d2,
140
+ # :title => 'Two'
141
+ # )
142
+ #
143
+ # Note that the data must be in time,value pairs, and that the date format
144
+ # may be any date that is parseable by ParseDate.
145
+ def add_data data
146
+ @data = [] unless @data
147
+
148
+ raise "No data provided by #{@data.inspect}" unless data[:data] and
149
+ data[:data].kind_of? Array
150
+ raise "Data supplied must be x,y pairs! "+
151
+ "The data provided contained an odd set of "+
152
+ "data points" unless data[:data].length % 2 == 0
153
+ return if data[:data].length == 0
154
+
155
+
156
+ x = []
157
+ y = []
158
+ data[:data].each_index {|i|
159
+ if i%2 == 0
160
+ arr = ParseDate.parsedate( data[:data][i] )
161
+ t = Time.local( *arr[0,6].compact )
162
+ x << t.to_i
163
+ else
164
+ y << data[:data][i]
165
+ end
166
+ }
167
+ sort( x, y )
168
+ data[:data] = [x,y]
169
+ @data << data
170
+ end
171
+
172
+
173
+ protected
174
+
175
+ def min_x_value=(value)
176
+ arr = ParseDate.parsedate( value )
177
+ @min_x_value = Time.local( *arr[0,6].compact ).to_i
178
+ end
179
+
180
+
181
+ def format x, y
182
+ Time.at( x ).strftime( popup_format )
183
+ end
184
+
185
+ def get_x_labels
186
+ get_x_values.collect { |v| Time.at(v).strftime( x_label_format ) }
187
+ end
188
+
189
+ private
190
+ def get_x_values
191
+ rv = []
192
+ min, max, scale_division = x_range
193
+ if timescale_divisions
194
+ timescale_divisions =~ /(\d+) ?(day|week|month|year|hour|minute|second)?/
195
+ division_units = $2 ? $2 : "day"
196
+ amount = $1.to_i
197
+ if amount
198
+ step = nil
199
+ case division_units
200
+ when "month"
201
+ cur = min
202
+ while cur < max
203
+ rv << cur
204
+ arr = Time.at( cur ).to_a
205
+ arr[4] += amount
206
+ if arr[4] > 12
207
+ arr[5] += (arr[4] / 12).to_i
208
+ arr[4] = (arr[4] % 12)
209
+ end
210
+ cur = Time.local(*arr).to_i
211
+ end
212
+ when "year"
213
+ cur = min
214
+ while cur < max
215
+ rv << cur
216
+ arr = Time.at( cur ).to_a
217
+ arr[5] += amount
218
+ cur = Time.local(*arr).to_i
219
+ end
220
+ when "week"
221
+ step = 7 * 24 * 60 * 60 * amount
222
+ when "day"
223
+ step = 24 * 60 * 60 * amount
224
+ when "hour"
225
+ step = 60 * 60 * amount
226
+ when "minute"
227
+ step = 60 * amount
228
+ when "second"
229
+ step = amount
230
+ end
231
+ min.step( max, step ) {|v| rv << v} if step
232
+
233
+ return rv
234
+ end
235
+ end
236
+ min.step( max, scale_division ) {|v| rv << v}
237
+ return rv
238
+ end
239
+ end
240
+ end
241
+ end