svg-graph-test 0.0.1

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