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,501 @@
1
+ require 'SVG/Graph/Graph'
2
+
3
+ module SVG
4
+ module Graph
5
+ # === For creating SVG plots of scalar data
6
+ #
7
+ # = Synopsis
8
+ #
9
+ # require 'SVG/Graph/Plot'
10
+ #
11
+ # # Data sets are x,y pairs
12
+ # # Note that multiple data sets can differ in length, and that the
13
+ # # data in the datasets needn't be in order; they will be ordered
14
+ # # by the plot along the X-axis.
15
+ # projection = [
16
+ # 6, 11, 0, 5, 18, 7, 1, 11, 13, 9, 1, 2, 19, 0, 3, 13,
17
+ # 7, 9
18
+ # ]
19
+ # actual = [
20
+ # 0, 18, 8, 15, 9, 4, 18, 14, 10, 2, 11, 6, 14, 12,
21
+ # 15, 6, 4, 17, 2, 12
22
+ # ]
23
+ #
24
+ # graph = SVG::Graph::Plot.new({
25
+ # :height => 500,
26
+ # :width => 300,
27
+ # :key => true,
28
+ # :scale_x_integers => true,
29
+ # :scale_y_integerrs => true,
30
+ # })
31
+ #
32
+ # graph.add_data({
33
+ # :data => projection
34
+ # :title => 'Projected',
35
+ # })
36
+ #
37
+ # graph.add_data({
38
+ # :data => actual,
39
+ # :title => 'Actual',
40
+ # })
41
+ #
42
+ # print graph.burn()
43
+ #
44
+ # = Description
45
+ #
46
+ # Produces a graph of scalar data.
47
+ #
48
+ # This object aims to allow you to easily create high quality
49
+ # SVG[http://www.w3c.org/tr/svg] scalar plots. You can either use the
50
+ # default style sheet or supply your own. Either way there are many options
51
+ # which can be configured to give you control over how the graph is
52
+ # generated - with or without a key, data elements at each point, title,
53
+ # subtitle etc.
54
+ #
55
+ # = Examples
56
+ #
57
+ # http://www.germane-software/repositories/public/SVG/test/plot.rb
58
+ #
59
+ # = Notes
60
+ #
61
+ # The default stylesheet handles upto 10 data sets, if you
62
+ # use more you must create your own stylesheet and add the
63
+ # additional settings for the extra data sets. You will know
64
+ # if you go over 10 data sets as they will have no style and
65
+ # be in black.
66
+ #
67
+ # Unlike the other types of charts, data sets must contain x,y pairs:
68
+ #
69
+ # [ 1, 2 ] # A data set with 1 point: (1,2)
70
+ # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)
71
+ #
72
+ # = See also
73
+ #
74
+ # * SVG::Graph::Graph
75
+ # * SVG::Graph::BarHorizontal
76
+ # * SVG::Graph::Bar
77
+ # * SVG::Graph::Line
78
+ # * SVG::Graph::Pie
79
+ # * SVG::Graph::TimeSeries
80
+ #
81
+ # == Author
82
+ #
83
+ # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
84
+ #
85
+ # Copyright 2004 Sean E. Russell
86
+ # This software is available under the Ruby license[LICENSE.txt]
87
+ #
88
+ class Plot < Graph
89
+
90
+ # In addition to the defaults set by Graph::initialize, sets
91
+ # [show_data_values] true
92
+ # [show_data_points] true
93
+ # [area_fill] false
94
+ # [stacked] false
95
+ def set_defaults
96
+ init_with(
97
+ :show_data_values => true,
98
+ :show_data_points => true,
99
+ :area_fill => false,
100
+ :stacked => false
101
+ )
102
+ self.top_align = self.right_align = self.top_font = self.right_font = 1
103
+ end
104
+
105
+ # Determines the scaling for the X axis divisions.
106
+ #
107
+ # graph.scale_x_divisions = 2
108
+ #
109
+ # would cause the graph to attempt to generate labels stepped by 2; EG:
110
+ # 0,2,4,6,8...
111
+ attr_accessor :scale_x_divisions
112
+ # Determines the scaling for the Y axis divisions.
113
+ #
114
+ # graph.scale_y_divisions = 0.5
115
+ #
116
+ # would cause the graph to attempt to generate labels stepped by 0.5; EG:
117
+ # 0, 0.5, 1, 1.5, 2, ...
118
+ attr_accessor :scale_y_divisions
119
+ # Make the X axis labels integers
120
+ attr_accessor :scale_x_integers
121
+ # Make the Y axis labels integers
122
+ attr_accessor :scale_y_integers
123
+ # Fill the area under the line
124
+ attr_accessor :area_fill
125
+ # Show a small circle on the graph where the line
126
+ # goes from one point to the next.
127
+ attr_accessor :show_data_points
128
+ # Set the minimum value of the X axis
129
+ attr_accessor :min_x_value
130
+ # Set the minimum value of the Y axis
131
+ attr_accessor :min_y_value
132
+
133
+
134
+ # Adds data to the plot. The data must be in X,Y pairs; EG
135
+ # [ 1, 2 ] # A data set with 1 point: (1,2)
136
+ # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)
137
+ def add_data data
138
+
139
+ @data = [] unless @data
140
+
141
+ raise "No data provided by #{conf.inspect}" unless data[:data] and
142
+ data[:data].kind_of? Array
143
+ raise "Data supplied must be x,y pairs! "+
144
+ "The data provided contained an odd set of "+
145
+ "data points" unless data[:data].length % 2 == 0
146
+ return if data[:data].length == 0
147
+
148
+ x = []
149
+ y = []
150
+ data[:data].each_index {|i|
151
+ (i%2 == 0 ? x : y) << data[:data][i]
152
+ }
153
+ sort( x, y )
154
+ data[:data] = [x,y]
155
+ @data << data
156
+ end
157
+
158
+
159
+ protected
160
+
161
+ def keys
162
+ @data.collect{ |x| x[:title] }
163
+ end
164
+
165
+ def calculate_left_margin
166
+ super
167
+ label_left = get_x_labels[0].to_s.length / 2 * font_size * 0.6
168
+ @border_left = label_left if label_left > @border_left
169
+ end
170
+
171
+ def calculate_right_margin
172
+ super
173
+ label_right = get_x_labels[-1].to_s.length / 2 * font_size * 0.6
174
+ @border_right = label_right if label_right > @border_right
175
+ end
176
+
177
+
178
+ X = 0
179
+ Y = 1
180
+ def x_range
181
+ max_value = @data.collect{|x| x[:data][X][-1] }.max
182
+ min_value = @data.collect{|x| x[:data][X][0] }.min
183
+ min_value = min_value<min_x_value ? min_value : min_x_value if min_x_value
184
+
185
+ range = max_value - min_value
186
+ right_pad = range == 0 ? 10 : range / 20.0
187
+ scale_range = (max_value + right_pad) - min_value
188
+
189
+ scale_division = scale_x_divisions || (scale_range / 10.0)
190
+
191
+ if scale_x_integers
192
+ scale_division = scale_division < 1 ? 1 : scale_division.round
193
+ end
194
+
195
+ [min_value, max_value, scale_division]
196
+ end
197
+
198
+ def get_x_values
199
+ min_value, max_value, scale_division = x_range
200
+ rv = []
201
+ min_value.step( max_value, scale_division ) {|v| rv << v}
202
+ return rv
203
+ end
204
+ alias :get_x_labels :get_x_values
205
+
206
+ def field_width
207
+ values = get_x_values
208
+ max = @data.collect{|x| x[:data][X][-1]}.max
209
+ dx = (max - values[-1]).to_f / (values[-1] - values[-2])
210
+ (@graph_width.to_f - font_size*2*right_font) /
211
+ (values.length + dx - right_align)
212
+ end
213
+
214
+
215
+ def y_range
216
+ max_value = @data.collect{|x| x[:data][Y].max }.max
217
+ min_value = @data.collect{|x| x[:data][Y].min }.min
218
+ min_value = min_value<min_y_value ? min_value : min_y_value if min_y_value
219
+
220
+ range = max_value - min_value
221
+ top_pad = range == 0 ? 10 : range / 20.0
222
+ scale_range = (max_value + top_pad) - min_value
223
+
224
+ scale_division = scale_y_divisions || (scale_range / 10.0)
225
+
226
+ if scale_y_integers
227
+ scale_division = scale_division < 1 ? 1 : scale_division.round
228
+ end
229
+
230
+ return [min_value, max_value, scale_division]
231
+ end
232
+
233
+ def get_y_values
234
+ min_value, max_value, scale_division = y_range
235
+ rv = []
236
+ min_value.step( max_value, scale_division ) {|v| rv << v}
237
+ return rv
238
+ end
239
+ alias :get_y_labels :get_y_values
240
+
241
+ def field_height
242
+ values = get_y_values
243
+ max = @data.collect{|x| x[:data][Y].max }.max
244
+ if values.length == 1
245
+ dx = values[-1]
246
+ else
247
+ dx = (max - values[-1]).to_f / (values[-1] - values[-2])
248
+ end
249
+ (@graph_height.to_f - font_size*2*top_font) /
250
+ (values.length + dx - top_align)
251
+ end
252
+
253
+ def draw_data
254
+ line = 1
255
+
256
+ x_min, x_max, x_div = x_range
257
+ y_min, y_max, y_div = y_range
258
+ x_step = (@graph_width.to_f - font_size*2) / (x_max-x_min)
259
+ y_step = (@graph_height.to_f - font_size*2) / (y_max-y_min)
260
+
261
+ for data in @data
262
+ x_points = data[:data][X]
263
+ y_points = data[:data][Y]
264
+
265
+ lpath = "L"
266
+ x_start = 0
267
+ y_start = 0
268
+ x_points.each_index { |idx|
269
+ x = (x_points[idx] - x_min) * x_step
270
+ y = @graph_height - (y_points[idx] - y_min) * y_step
271
+ x_start, y_start = x,y if idx == 0
272
+ lpath << "#{x} #{y} "
273
+ }
274
+
275
+ if area_fill
276
+ @graph.add_element( "path", {
277
+ "d" => "M#{x_start} #@graph_height #{lpath} V#@graph_height Z",
278
+ "class" => "fill#{line}"
279
+ })
280
+ end
281
+
282
+ @graph.add_element( "path", {
283
+ "d" => "M#{x_start} #{y_start} #{lpath}",
284
+ "class" => "line#{line}"
285
+ })
286
+
287
+ if show_data_points || show_data_values
288
+ x_points.each_index { |idx|
289
+ x = (x_points[idx] - x_min) * x_step
290
+ y = @graph_height - (y_points[idx] - y_min) * y_step
291
+ if show_data_points
292
+ @graph.add_element( "circle", {
293
+ "cx" => x.to_s,
294
+ "cy" => y.to_s,
295
+ "r" => "2.5",
296
+ "class" => "dataPoint#{line}"
297
+ })
298
+ add_popup(x, y, format( x_points[idx], y_points[idx] )) if add_popups
299
+ end
300
+ make_datapoint_text( x, y-6, y_points[idx] ) if show_data_values
301
+ }
302
+ end
303
+ line += 1
304
+ end
305
+ end
306
+
307
+ def format x, y
308
+ "(#{(x * 100).to_i / 100}, #{(y * 100).to_i / 100})"
309
+ end
310
+
311
+ def get_css
312
+ return <<EOL
313
+ /* default line styles */
314
+ .line1{
315
+ fill: none;
316
+ stroke: #ff0000;
317
+ stroke-width: 1px;
318
+ }
319
+ .line2{
320
+ fill: none;
321
+ stroke: #0000ff;
322
+ stroke-width: 1px;
323
+ }
324
+ .line3{
325
+ fill: none;
326
+ stroke: #00ff00;
327
+ stroke-width: 1px;
328
+ }
329
+ .line4{
330
+ fill: none;
331
+ stroke: #ffcc00;
332
+ stroke-width: 1px;
333
+ }
334
+ .line5{
335
+ fill: none;
336
+ stroke: #00ccff;
337
+ stroke-width: 1px;
338
+ }
339
+ .line6{
340
+ fill: none;
341
+ stroke: #ff00ff;
342
+ stroke-width: 1px;
343
+ }
344
+ .line7{
345
+ fill: none;
346
+ stroke: #00ffff;
347
+ stroke-width: 1px;
348
+ }
349
+ .line8{
350
+ fill: none;
351
+ stroke: #ffff00;
352
+ stroke-width: 1px;
353
+ }
354
+ .line9{
355
+ fill: none;
356
+ stroke: #ccc6666;
357
+ stroke-width: 1px;
358
+ }
359
+ .line10{
360
+ fill: none;
361
+ stroke: #663399;
362
+ stroke-width: 1px;
363
+ }
364
+ .line11{
365
+ fill: none;
366
+ stroke: #339900;
367
+ stroke-width: 1px;
368
+ }
369
+ .line12{
370
+ fill: none;
371
+ stroke: #9966FF;
372
+ stroke-width: 1px;
373
+ }
374
+ /* default fill styles */
375
+ .fill1{
376
+ fill: #cc0000;
377
+ fill-opacity: 0.2;
378
+ stroke: none;
379
+ }
380
+ .fill2{
381
+ fill: #0000cc;
382
+ fill-opacity: 0.2;
383
+ stroke: none;
384
+ }
385
+ .fill3{
386
+ fill: #00cc00;
387
+ fill-opacity: 0.2;
388
+ stroke: none;
389
+ }
390
+ .fill4{
391
+ fill: #ffcc00;
392
+ fill-opacity: 0.2;
393
+ stroke: none;
394
+ }
395
+ .fill5{
396
+ fill: #00ccff;
397
+ fill-opacity: 0.2;
398
+ stroke: none;
399
+ }
400
+ .fill6{
401
+ fill: #ff00ff;
402
+ fill-opacity: 0.2;
403
+ stroke: none;
404
+ }
405
+ .fill7{
406
+ fill: #00ffff;
407
+ fill-opacity: 0.2;
408
+ stroke: none;
409
+ }
410
+ .fill8{
411
+ fill: #ffff00;
412
+ fill-opacity: 0.2;
413
+ stroke: none;
414
+ }
415
+ .fill9{
416
+ fill: #cc6666;
417
+ fill-opacity: 0.2;
418
+ stroke: none;
419
+ }
420
+ .fill10{
421
+ fill: #663399;
422
+ fill-opacity: 0.2;
423
+ stroke: none;
424
+ }
425
+ .fill11{
426
+ fill: #339900;
427
+ fill-opacity: 0.2;
428
+ stroke: none;
429
+ }
430
+ .fill12{
431
+ fill: #9966FF;
432
+ fill-opacity: 0.2;
433
+ stroke: none;
434
+ }
435
+ /* default line styles */
436
+ .key1,.dataPoint1{
437
+ fill: #ff0000;
438
+ stroke: none;
439
+ stroke-width: 1px;
440
+ }
441
+ .key2,.dataPoint2{
442
+ fill: #0000ff;
443
+ stroke: none;
444
+ stroke-width: 1px;
445
+ }
446
+ .key3,.dataPoint3{
447
+ fill: #00ff00;
448
+ stroke: none;
449
+ stroke-width: 1px;
450
+ }
451
+ .key4,.dataPoint4{
452
+ fill: #ffcc00;
453
+ stroke: none;
454
+ stroke-width: 1px;
455
+ }
456
+ .key5,.dataPoint5{
457
+ fill: #00ccff;
458
+ stroke: none;
459
+ stroke-width: 1px;
460
+ }
461
+ .key6,.dataPoint6{
462
+ fill: #ff00ff;
463
+ stroke: none;
464
+ stroke-width: 1px;
465
+ }
466
+ .key7,.dataPoint7{
467
+ fill: #00ffff;
468
+ stroke: none;
469
+ stroke-width: 1px;
470
+ }
471
+ .key8,.dataPoint8{
472
+ fill: #ffff00;
473
+ stroke: none;
474
+ stroke-width: 1px;
475
+ }
476
+ .key9,.dataPoint9{
477
+ fill: #cc6666;
478
+ stroke: none;
479
+ stroke-width: 1px;
480
+ }
481
+ .key10,.dataPoint10{
482
+ fill: #663399;
483
+ stroke: none;
484
+ stroke-width: 1px;
485
+ }
486
+ .key11,.dataPoint11{
487
+ fill: #339900;
488
+ stroke: none;
489
+ stroke-width: 1px;
490
+ }
491
+ .key12,.dataPoint12{
492
+ fill: #9966FF;
493
+ stroke: none;
494
+ stroke-width: 1px;
495
+ }
496
+ EOL
497
+ end
498
+
499
+ end
500
+ end
501
+ end