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,467 @@
1
+ require_relative 'Graph'
2
+
3
+ module SVG
4
+ module Graph
5
+ # === Create presentation quality SVG line graphs easily
6
+ #
7
+ # = Synopsis
8
+ #
9
+ # require 'SVG/Graph/Line'
10
+ #
11
+ # fields = %w(Jan Feb Mar);
12
+ # data_sales_02 = [12, 45, 21]
13
+ # data_sales_03 = [15, 30, 40]
14
+ #
15
+ # graph = SVG::Graph::Line.new({
16
+ # :height => 500,
17
+ # :width => 300,
18
+ # :fields => fields,
19
+ # })
20
+ #
21
+ # graph.add_data({
22
+ # :data => data_sales_02,
23
+ # :title => 'Sales 2002',
24
+ # })
25
+ #
26
+ # graph.add_data({
27
+ # :data => data_sales_03,
28
+ # :title => 'Sales 2003',
29
+ # })
30
+ #
31
+ # print "Content-type: image/svg+xml\r\n\r\n";
32
+ # print graph.burn();
33
+ #
34
+ # = Description
35
+ #
36
+ # This object aims to allow you to easily create high quality
37
+ # SVG line graphs. You can either use the default style sheet
38
+ # or supply your own. Either way there are many options which can
39
+ # be configured to give you control over how the graph is
40
+ # generated - with or without a key, data elements at each point,
41
+ # title, subtitle etc.
42
+ #
43
+ # = Examples
44
+ #
45
+ # https://github.com/lumean/svg-graph2/blob/master/examples/line.rb
46
+ #
47
+ # = Notes
48
+ # Only number of fileds datapoints will be drawn, additional data values
49
+ # are ignored. Nil values in data are skipped and
50
+ # interpolated as straight line to the next datapoint.
51
+ #
52
+ # The default stylesheet handles upto 10 data sets, if you
53
+ # use more you must create your own stylesheet and add the
54
+ # additional settings for the extra data sets. You will know
55
+ # if you go over 10 data sets as they will have no style and
56
+ # be in black.
57
+ #
58
+ # = See also
59
+ #
60
+ # * SVG::Graph::Graph
61
+ # * SVG::Graph::BarHorizontal
62
+ # * SVG::Graph::Bar
63
+ # * SVG::Graph::Pie
64
+ # * SVG::Graph::Plot
65
+ # * SVG::Graph::TimeSeries
66
+ #
67
+ # == Author
68
+ #
69
+ # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
70
+ #
71
+ # Copyright 2004 Sean E. Russell
72
+ # This software is available under the Ruby license[LICENSE.txt]
73
+ #
74
+ class Line < SVG::Graph::Graph
75
+ # Show a small circle on the graph where the line
76
+ # goes from one point to the next.
77
+ attr_accessor :show_data_points
78
+ # Accumulates each data set. (i.e. Each point increased by sum of
79
+ # all previous series at same point). Default is 0, set to '1' to show.
80
+ attr_accessor :stacked
81
+ # Fill in the area under the plot if true
82
+ attr_accessor :area_fill
83
+
84
+ # The constructor takes a hash reference, :fields (the names for each
85
+ # field on the X axis) MUST be set, all other values are defaulted to
86
+ # those shown above - with the exception of style_sheet which defaults
87
+ # to using the internal style sheet.
88
+ def initialize config
89
+ raise "fields was not supplied or is empty" unless config[:fields] &&
90
+ config[:fields].kind_of?(Array) &&
91
+ config[:fields].length > 0
92
+ super
93
+ end
94
+
95
+ # In addition to the defaults set in Graph::initialize, sets
96
+ # [show_data_points] true
97
+ # [show_data_values] true
98
+ # [stacked] false
99
+ # [area_fill] false
100
+ def set_defaults
101
+ init_with(
102
+ :show_data_points => true,
103
+ :show_data_values => true,
104
+ :stacked => false,
105
+ :area_fill => false
106
+ )
107
+ end
108
+
109
+ protected
110
+
111
+ def max_value
112
+ max = 0
113
+ if stacked
114
+ sums = Array.new(@config[:fields].length).fill(0)
115
+
116
+ @data.each do |data|
117
+ sums.each_index do |i|
118
+ sums[i] += data[:data][i].to_f
119
+ end
120
+ end
121
+ max = sums.max
122
+ else
123
+ # compact removes nil values when computing the max
124
+ max = @data.collect{ |x|
125
+ x[:data].compact.max
126
+ }.max
127
+ end
128
+
129
+ return max
130
+ end
131
+
132
+ def min_value
133
+ min = 0
134
+ # compact removes nil values
135
+ if (!min_scale_value.nil?) then
136
+ min = min_scale_value
137
+ elsif (stacked == true) then
138
+ min = @data[-1][:data].compact.min
139
+ else
140
+ min = @data.collect{|x| x[:data].compact.min}.min
141
+ end
142
+
143
+ return min
144
+ end
145
+
146
+ def get_x_labels
147
+ @config[:fields]
148
+ end
149
+
150
+ def calculate_left_margin
151
+ super
152
+ label_left = @config[:fields][0].length / 2 * font_size * 0.6
153
+ @border_left = label_left if label_left > @border_left
154
+ end
155
+
156
+ def get_y_labels
157
+ maxvalue = max_value
158
+ minvalue = min_value
159
+ range = maxvalue - minvalue
160
+ # add some padding on top of the graph
161
+ if range == 0
162
+ maxvalue += 10
163
+ else
164
+ maxvalue += range / 20.0
165
+ end
166
+ scale_range = maxvalue - minvalue
167
+
168
+ @y_scale_division = scale_divisions || (scale_range / 10.0)
169
+ @y_offset = 0
170
+
171
+ if scale_integers
172
+ @y_scale_division = @y_scale_division < 1 ? 1 : @y_scale_division.round
173
+ @y_offset = (minvalue.to_f - minvalue.floor).to_f
174
+ minvalue = minvalue.floor
175
+ end
176
+
177
+ rv = []
178
+
179
+ minvalue.step( maxvalue, @y_scale_division ) {|v| rv << v}
180
+ return rv
181
+ end
182
+
183
+ def calc_coords(field, value, width = field_width, height = field_height)
184
+ coords = {:x => 0, :y => 0}
185
+ coords[:x] = width * field
186
+ # make sure we do float division, otherwise coords get messed up
187
+ coords[:y] = @graph_height - (value + @y_offset)/@y_scale_division.to_f * height
188
+ return coords
189
+ end
190
+
191
+ def draw_data
192
+ minvalue = min_value
193
+ fieldheight = field_height
194
+ fieldwidth = field_width
195
+ line = @data.length
196
+ # always zero for filling areas
197
+ prev_sum = Array.new(@config[:fields].length).fill(-@y_offset)
198
+ # cumulated sum (used for stacked graphs)
199
+ cum_sum = Array.new(@config[:fields].length).fill(nil)
200
+
201
+ for data in @data.reverse
202
+ lpath = ""
203
+ apath = ""
204
+
205
+ # reset cum_sum if we are not in a stacked graph
206
+ if not stacked then cum_sum.fill(nil) end
207
+
208
+ # only consider as many datapoints as we have fields
209
+ @config[:fields].each_index do |i|
210
+ next if data[:data][i].nil?
211
+ if cum_sum[i].nil? #first time init
212
+ cum_sum[i] = data[:data][i] - minvalue
213
+ else # in case of stacked
214
+ cum_sum[i] += data[:data][i]
215
+ end
216
+ c = calc_coords(i, cum_sum[i], fieldwidth, fieldheight)
217
+ lpath << "#{c[:x]} #{c[:y]} "
218
+ end
219
+
220
+ if area_fill
221
+ if stacked then
222
+ (prev_sum.length - 1).downto 0 do |i|
223
+ next if prev_sum[i].nil?
224
+ c = calc_coords(i, prev_sum[i], fieldwidth, fieldheight)
225
+
226
+ apath << "#{c[:x]} #{c[:y]} "
227
+ end
228
+
229
+ c = calc_coords(0, prev_sum[0], fieldwidth, fieldheight)
230
+ else
231
+ apath = "V#@graph_height"
232
+ c = calc_coords(0, -@y_offset, fieldwidth, fieldheight)
233
+ end
234
+
235
+ @graph.add_element("path", {
236
+ "d" => "M#{c[:x]} #{c[:y]} L" + lpath + apath + "Z",
237
+ "class" => "fill#{line}"
238
+ })
239
+ end
240
+
241
+ matcher = /^(\S+ \S+) (.*)/.match lpath
242
+ @graph.add_element("path", {
243
+ "d" => "M#{matcher[1]} L#{matcher[2]}",
244
+ "class" => "line#{line}"
245
+ })
246
+
247
+ if show_data_points || show_data_values || add_popups
248
+ cum_sum.each_index do |i|
249
+ # skip datapoint if nil
250
+ next if cum_sum[i].nil?
251
+ c = calc_coords(i, cum_sum[i], fieldwidth, fieldheight)
252
+ if show_data_points
253
+ shape_selection_string = data[:description][i].to_s
254
+ if !data[:shape][i].nil?
255
+ shape_selection_string = data[:shape][i].to_s
256
+ end
257
+ DataPoint.new(c[:x], c[:y], line).shape(shape_selection_string).each{|s|
258
+ @graph.add_element( *s )
259
+ }
260
+ end
261
+
262
+ make_datapoint_text( c[:x], c[:y] - font_size/2, cum_sum[i] + minvalue)
263
+ # number format shall not apply to popup (use .to_s conversion)
264
+ descr = ""
265
+ if !data[:description][i].to_s.empty?
266
+ descr = ", #{data[:description][i].to_s}"
267
+ end
268
+ add_popup(c[:x], c[:y], (cum_sum[i] + minvalue).to_s + descr, "", data[:url][i].to_s)
269
+ end
270
+ end
271
+
272
+ prev_sum = cum_sum.dup
273
+ line -= 1
274
+ end
275
+ end
276
+
277
+
278
+ def get_css
279
+ return <<EOL
280
+ /* default line styles */
281
+ .line1{
282
+ fill: none;
283
+ stroke: #ff0000;
284
+ stroke-width: 1px;
285
+ }
286
+ .line2{
287
+ fill: none;
288
+ stroke: #0000ff;
289
+ stroke-width: 1px;
290
+ }
291
+ .line3{
292
+ fill: none;
293
+ stroke: #00ff00;
294
+ stroke-width: 1px;
295
+ }
296
+ .line4{
297
+ fill: none;
298
+ stroke: #ffcc00;
299
+ stroke-width: 1px;
300
+ }
301
+ .line5{
302
+ fill: none;
303
+ stroke: #00ccff;
304
+ stroke-width: 1px;
305
+ }
306
+ .line6{
307
+ fill: none;
308
+ stroke: #ff00ff;
309
+ stroke-width: 1px;
310
+ }
311
+ .line7{
312
+ fill: none;
313
+ stroke: #00ffff;
314
+ stroke-width: 1px;
315
+ }
316
+ .line8{
317
+ fill: none;
318
+ stroke: #ffff00;
319
+ stroke-width: 1px;
320
+ }
321
+ .line9{
322
+ fill: none;
323
+ stroke: #cc6666;
324
+ stroke-width: 1px;
325
+ }
326
+ .line10{
327
+ fill: none;
328
+ stroke: #663399;
329
+ stroke-width: 1px;
330
+ }
331
+ .line11{
332
+ fill: none;
333
+ stroke: #339900;
334
+ stroke-width: 1px;
335
+ }
336
+ .line12{
337
+ fill: none;
338
+ stroke: #9966FF;
339
+ stroke-width: 1px;
340
+ }
341
+ /* default fill styles */
342
+ .fill1{
343
+ fill: #cc0000;
344
+ fill-opacity: 0.2;
345
+ stroke: none;
346
+ }
347
+ .fill2{
348
+ fill: #0000cc;
349
+ fill-opacity: 0.2;
350
+ stroke: none;
351
+ }
352
+ .fill3{
353
+ fill: #00cc00;
354
+ fill-opacity: 0.2;
355
+ stroke: none;
356
+ }
357
+ .fill4{
358
+ fill: #ffcc00;
359
+ fill-opacity: 0.2;
360
+ stroke: none;
361
+ }
362
+ .fill5{
363
+ fill: #00ccff;
364
+ fill-opacity: 0.2;
365
+ stroke: none;
366
+ }
367
+ .fill6{
368
+ fill: #ff00ff;
369
+ fill-opacity: 0.2;
370
+ stroke: none;
371
+ }
372
+ .fill7{
373
+ fill: #00ffff;
374
+ fill-opacity: 0.2;
375
+ stroke: none;
376
+ }
377
+ .fill8{
378
+ fill: #ffff00;
379
+ fill-opacity: 0.2;
380
+ stroke: none;
381
+ }
382
+ .fill9{
383
+ fill: #cc6666;
384
+ fill-opacity: 0.2;
385
+ stroke: none;
386
+ }
387
+ .fill10{
388
+ fill: #663399;
389
+ fill-opacity: 0.2;
390
+ stroke: none;
391
+ }
392
+ .fill11{
393
+ fill: #339900;
394
+ fill-opacity: 0.2;
395
+ stroke: none;
396
+ }
397
+ .fill12{
398
+ fill: #9966FF;
399
+ fill-opacity: 0.2;
400
+ stroke: none;
401
+ }
402
+ /* default line styles */
403
+ .key1,.dataPoint1{
404
+ fill: #ff0000;
405
+ stroke: none;
406
+ stroke-width: 1px;
407
+ }
408
+ .key2,.dataPoint2{
409
+ fill: #0000ff;
410
+ stroke: none;
411
+ stroke-width: 1px;
412
+ }
413
+ .key3,.dataPoint3{
414
+ fill: #00ff00;
415
+ stroke: none;
416
+ stroke-width: 1px;
417
+ }
418
+ .key4,.dataPoint4{
419
+ fill: #ffcc00;
420
+ stroke: none;
421
+ stroke-width: 1px;
422
+ }
423
+ .key5,.dataPoint5{
424
+ fill: #00ccff;
425
+ stroke: none;
426
+ stroke-width: 1px;
427
+ }
428
+ .key6,.dataPoint6{
429
+ fill: #ff00ff;
430
+ stroke: none;
431
+ stroke-width: 1px;
432
+ }
433
+ .key7,.dataPoint7{
434
+ fill: #00ffff;
435
+ stroke: none;
436
+ stroke-width: 1px;
437
+ }
438
+ .key8,.dataPoint8{
439
+ fill: #ffff00;
440
+ stroke: none;
441
+ stroke-width: 1px;
442
+ }
443
+ .key9,.dataPoint9{
444
+ fill: #cc6666;
445
+ stroke: none;
446
+ stroke-width: 1px;
447
+ }
448
+ .key10,.dataPoint10{
449
+ fill: #663399;
450
+ stroke: none;
451
+ stroke-width: 1px;
452
+ }
453
+ .key11,.dataPoint11{
454
+ fill: #339900;
455
+ stroke: none;
456
+ stroke-width: 1px;
457
+ }
458
+ .key12,.dataPoint12{
459
+ fill: #9966FF;
460
+ stroke: none;
461
+ stroke-width: 1px;
462
+ }
463
+ EOL
464
+ end
465
+ end
466
+ end
467
+ end