svg-graph-test 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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