svg_graph 0.7

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,500 @@
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
+ @data = [] unless @data
139
+
140
+ raise "No data provided by #{conf.inspect}" unless data[:data] and
141
+ data[:data].kind_of? Array
142
+ raise "Data supplied must be x,y pairs! "+
143
+ "The data provided contained an odd set of "+
144
+ "data points" unless data[:data].length % 2 == 0
145
+ return if data[:data].length == 0
146
+
147
+ x = []
148
+ y = []
149
+ data[:data].each_index {|i|
150
+ (i%2 == 0 ? x : y) << data[:data][i]
151
+ }
152
+ sort( x, y )
153
+ data[:data] = [x,y]
154
+ @data << data
155
+ end
156
+
157
+
158
+ protected
159
+
160
+ def keys
161
+ @data.collect{ |x| x[:title] }
162
+ end
163
+
164
+ def calculate_left_margin
165
+ super
166
+ label_left = get_x_labels[0].to_s.length / 2 * font_size * 0.6
167
+ @border_left = label_left if label_left > @border_left
168
+ end
169
+
170
+ def calculate_right_margin
171
+ super
172
+ label_right = get_x_labels[-1].to_s.length / 2 * font_size * 0.6
173
+ @border_right = label_right if label_right > @border_right
174
+ end
175
+
176
+
177
+ X = 0
178
+ Y = 1
179
+ def x_range
180
+ max_value = @data.collect{|x| x[:data][X][-1] }.max
181
+ min_value = @data.collect{|x| x[:data][X][0] }.min
182
+ min_value = min_value<min_x_value ? min_value : min_x_value if min_x_value
183
+
184
+ range = max_value - min_value
185
+ right_pad = range == 0 ? 10 : range / 20.0
186
+ scale_range = (max_value + right_pad) - min_value
187
+
188
+ scale_division = scale_x_divisions || (scale_range / 10.0)
189
+
190
+ if scale_x_integers
191
+ scale_division = scale_division < 1 ? 1 : scale_division.round
192
+ end
193
+
194
+ [min_value, max_value, scale_division]
195
+ end
196
+
197
+ def get_x_values
198
+ min_value, max_value, scale_division = x_range
199
+ rv = []
200
+ min_value.step( max_value, scale_division ) {|v| rv << v}
201
+ return rv
202
+ end
203
+ alias :get_x_labels :get_x_values
204
+
205
+ def field_width
206
+ values = get_x_values
207
+ max = @data.collect{|x| x[:data][X][-1]}.max
208
+ dx = (max - values[-1]).to_f / (values[-1] - values[-2])
209
+ (@graph_width.to_f - font_size*2*right_font) /
210
+ (values.length + dx - right_align)
211
+ end
212
+
213
+
214
+ def y_range
215
+ max_value = @data.collect{|x| x[:data][Y].max }.max
216
+ min_value = @data.collect{|x| x[:data][Y].min }.min
217
+ min_value = min_value<min_y_value ? min_value : min_y_value if min_y_value
218
+
219
+ range = max_value - min_value
220
+ top_pad = range == 0 ? 10 : range / 20.0
221
+ scale_range = (max_value + top_pad) - min_value
222
+
223
+ scale_division = scale_y_divisions || (scale_range / 10.0)
224
+
225
+ if scale_y_integers
226
+ scale_division = scale_division < 1 ? 1 : scale_division.round
227
+ end
228
+
229
+ return [min_value, max_value, scale_division]
230
+ end
231
+
232
+ def get_y_values
233
+ min_value, max_value, scale_division = y_range
234
+ rv = []
235
+ min_value.step( max_value, scale_division ) {|v| rv << v}
236
+ return rv
237
+ end
238
+ alias :get_y_labels :get_y_values
239
+
240
+ def field_height
241
+ values = get_y_values
242
+ max = @data.collect{|x| x[:data][Y].max }.max
243
+ if values.length == 1
244
+ dx = values[-1]
245
+ else
246
+ dx = (max - values[-1]).to_f / (values[-1] - values[-2])
247
+ end
248
+ (@graph_height.to_f - font_size*2*top_font) /
249
+ (values.length + dx - top_align)
250
+ end
251
+
252
+ def draw_data
253
+ line = 1
254
+
255
+ x_min, x_max, x_div = x_range
256
+ y_min, y_max, y_div = y_range
257
+ x_step = (@graph_width.to_f - font_size*2) / (x_max-x_min)
258
+ y_step = (@graph_height.to_f - font_size*2) / (y_max-y_min)
259
+
260
+ for data in @data
261
+ x_points = data[:data][X]
262
+ y_points = data[:data][Y]
263
+
264
+ lpath = "L"
265
+ x_start = 0
266
+ y_start = 0
267
+ x_points.each_index { |idx|
268
+ x = (x_points[idx] - x_min) * x_step
269
+ y = @graph_height - (y_points[idx] - y_min) * y_step
270
+ x_start, y_start = x,y if idx == 0
271
+ lpath << "#{x} #{y} "
272
+ }
273
+
274
+ if area_fill
275
+ @graph.add_element( "path", {
276
+ "d" => "M#{x_start} #@graph_height #{lpath} V#@graph_height Z",
277
+ "class" => "fill#{line}"
278
+ })
279
+ end
280
+
281
+ @graph.add_element( "path", {
282
+ "d" => "M#{x_start} #{y_start} #{lpath}",
283
+ "class" => "line#{line}"
284
+ })
285
+
286
+ if show_data_points || show_data_values
287
+ x_points.each_index { |idx|
288
+ x = (x_points[idx] - x_min) * x_step
289
+ y = @graph_height - (y_points[idx] - y_min) * y_step
290
+ if show_data_points
291
+ @graph.add_element( "circle", {
292
+ "cx" => x.to_s,
293
+ "cy" => y.to_s,
294
+ "r" => "2.5",
295
+ "class" => "dataPoint#{line}"
296
+ })
297
+ add_popup(x, y, format( x_points[idx], y_points[idx] )) if add_popups
298
+ end
299
+ make_datapoint_text( x, y-6, y_points[idx] ) if show_data_values
300
+ }
301
+ end
302
+ line += 1
303
+ end
304
+ end
305
+
306
+ def format x, y
307
+ "(#{(x * 100).to_i / 100}, #{(y * 100).to_i / 100})"
308
+ end
309
+
310
+ def get_css
311
+ return <<EOL
312
+ /* default line styles */
313
+ .line1{
314
+ fill: none;
315
+ stroke: #ff0000;
316
+ stroke-width: 1px;
317
+ }
318
+ .line2{
319
+ fill: none;
320
+ stroke: #0000ff;
321
+ stroke-width: 1px;
322
+ }
323
+ .line3{
324
+ fill: none;
325
+ stroke: #00ff00;
326
+ stroke-width: 1px;
327
+ }
328
+ .line4{
329
+ fill: none;
330
+ stroke: #ffcc00;
331
+ stroke-width: 1px;
332
+ }
333
+ .line5{
334
+ fill: none;
335
+ stroke: #00ccff;
336
+ stroke-width: 1px;
337
+ }
338
+ .line6{
339
+ fill: none;
340
+ stroke: #ff00ff;
341
+ stroke-width: 1px;
342
+ }
343
+ .line7{
344
+ fill: none;
345
+ stroke: #00ffff;
346
+ stroke-width: 1px;
347
+ }
348
+ .line8{
349
+ fill: none;
350
+ stroke: #ffff00;
351
+ stroke-width: 1px;
352
+ }
353
+ .line9{
354
+ fill: none;
355
+ stroke: #ccc6666;
356
+ stroke-width: 1px;
357
+ }
358
+ .line10{
359
+ fill: none;
360
+ stroke: #663399;
361
+ stroke-width: 1px;
362
+ }
363
+ .line11{
364
+ fill: none;
365
+ stroke: #339900;
366
+ stroke-width: 1px;
367
+ }
368
+ .line12{
369
+ fill: none;
370
+ stroke: #9966FF;
371
+ stroke-width: 1px;
372
+ }
373
+ /* default fill styles */
374
+ .fill1{
375
+ fill: #cc0000;
376
+ fill-opacity: 0.2;
377
+ stroke: none;
378
+ }
379
+ .fill2{
380
+ fill: #0000cc;
381
+ fill-opacity: 0.2;
382
+ stroke: none;
383
+ }
384
+ .fill3{
385
+ fill: #00cc00;
386
+ fill-opacity: 0.2;
387
+ stroke: none;
388
+ }
389
+ .fill4{
390
+ fill: #ffcc00;
391
+ fill-opacity: 0.2;
392
+ stroke: none;
393
+ }
394
+ .fill5{
395
+ fill: #00ccff;
396
+ fill-opacity: 0.2;
397
+ stroke: none;
398
+ }
399
+ .fill6{
400
+ fill: #ff00ff;
401
+ fill-opacity: 0.2;
402
+ stroke: none;
403
+ }
404
+ .fill7{
405
+ fill: #00ffff;
406
+ fill-opacity: 0.2;
407
+ stroke: none;
408
+ }
409
+ .fill8{
410
+ fill: #ffff00;
411
+ fill-opacity: 0.2;
412
+ stroke: none;
413
+ }
414
+ .fill9{
415
+ fill: #cc6666;
416
+ fill-opacity: 0.2;
417
+ stroke: none;
418
+ }
419
+ .fill10{
420
+ fill: #663399;
421
+ fill-opacity: 0.2;
422
+ stroke: none;
423
+ }
424
+ .fill11{
425
+ fill: #339900;
426
+ fill-opacity: 0.2;
427
+ stroke: none;
428
+ }
429
+ .fill12{
430
+ fill: #9966FF;
431
+ fill-opacity: 0.2;
432
+ stroke: none;
433
+ }
434
+ /* default line styles */
435
+ .key1,.dataPoint1{
436
+ fill: #ff0000;
437
+ stroke: none;
438
+ stroke-width: 1px;
439
+ }
440
+ .key2,.dataPoint2{
441
+ fill: #0000ff;
442
+ stroke: none;
443
+ stroke-width: 1px;
444
+ }
445
+ .key3,.dataPoint3{
446
+ fill: #00ff00;
447
+ stroke: none;
448
+ stroke-width: 1px;
449
+ }
450
+ .key4,.dataPoint4{
451
+ fill: #ffcc00;
452
+ stroke: none;
453
+ stroke-width: 1px;
454
+ }
455
+ .key5,.dataPoint5{
456
+ fill: #00ccff;
457
+ stroke: none;
458
+ stroke-width: 1px;
459
+ }
460
+ .key6,.dataPoint6{
461
+ fill: #ff00ff;
462
+ stroke: none;
463
+ stroke-width: 1px;
464
+ }
465
+ .key7,.dataPoint7{
466
+ fill: #00ffff;
467
+ stroke: none;
468
+ stroke-width: 1px;
469
+ }
470
+ .key8,.dataPoint8{
471
+ fill: #ffff00;
472
+ stroke: none;
473
+ stroke-width: 1px;
474
+ }
475
+ .key9,.dataPoint9{
476
+ fill: #cc6666;
477
+ stroke: none;
478
+ stroke-width: 1px;
479
+ }
480
+ .key10,.dataPoint10{
481
+ fill: #663399;
482
+ stroke: none;
483
+ stroke-width: 1px;
484
+ }
485
+ .key11,.dataPoint11{
486
+ fill: #339900;
487
+ stroke: none;
488
+ stroke-width: 1px;
489
+ }
490
+ .key12,.dataPoint12{
491
+ fill: #9966FF;
492
+ stroke: none;
493
+ stroke-width: 1px;
494
+ }
495
+ EOL
496
+ end
497
+
498
+ end
499
+ end
500
+ end