To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.

Statistics Download as Zip
| Branch: | Tag: | Revision:

root / .svn / pristine / 62 / 6230957c0e4d32e6d5ac196562d742563883d7c0.svn-base @ 1297:0a574315af3e

History | View | Annotate | Download (12.3 KB)

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