svg-graph19 0.6.2

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