svg-graph 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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