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 / lib / SVG / Graph / Graph.rb @ 442:753f1380d6bc

History | View | Annotate | Download (30.6 KB)

1
begin
2
  require 'zlib'
3
  @@__have_zlib = true
4
rescue
5
  @@__have_zlib = false
6
end
7

    
8
require 'rexml/document'
9

    
10
module SVG
11
  module Graph
12
    VERSION = '@ANT_VERSION@'
13

    
14
    # === Base object for generating SVG Graphs
15
    # 
16
    # == Synopsis
17
    #
18
    # This class is only used as a superclass of specialized charts.  Do not
19
    # attempt to use this class directly, unless creating a new chart type.
20
    #
21
    # For examples of how to subclass this class, see the existing specific
22
    # subclasses, such as SVG::Graph::Pie.
23
    #
24
    # == Examples
25
    #
26
    # For examples of how to use this package, see either the test files, or
27
    # the documentation for the specific class you want to use.
28
    #
29
    # * file:test/plot.rb
30
    # * file:test/single.rb
31
    # * file:test/test.rb
32
    # * file:test/timeseries.rb
33
    # 
34
    # == Description
35
    # 
36
    # This package should be used as a base for creating SVG graphs.
37
    #
38
    # == Acknowledgements
39
    #
40
    # Leo Lapworth for creating the SVG::TT::Graph package which this Ruby
41
    # port is based on.
42
    #
43
    # Stephen Morgan for creating the TT template and SVG.
44
    # 
45
    # == See
46
    #
47
    # * SVG::Graph::BarHorizontal
48
    # * SVG::Graph::Bar
49
    # * SVG::Graph::Line
50
    # * SVG::Graph::Pie
51
    # * SVG::Graph::Plot
52
    # * SVG::Graph::TimeSeries
53
    #
54
    # == Author
55
    #
56
    # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
57
    #
58
    # Copyright 2004 Sean E. Russell
59
    # This software is available under the Ruby license[LICENSE.txt]
60
    #
61
    class Graph
62
      include REXML
63

    
64
      # Initialize the graph object with the graph settings.  You won't
65
      # instantiate this class directly; see the subclass for options.
66
      # [width] 500
67
      # [height] 300
68
      # [show_x_guidelines] false
69
      # [show_y_guidelines] true
70
      # [show_data_values] true
71
      # [min_scale_value] 0
72
      # [show_x_labels] true
73
      # [stagger_x_labels] false
74
      # [rotate_x_labels] false
75
      # [step_x_labels] 1
76
      # [step_include_first_x_label] true
77
      # [show_y_labels] true
78
      # [rotate_y_labels] false
79
      # [scale_integers] false
80
      # [show_x_title] false
81
      # [x_title] 'X Field names'
82
      # [show_y_title] false
83
      # [y_title_text_direction] :bt
84
      # [y_title] 'Y Scale'
85
      # [show_graph_title] false
86
      # [graph_title] 'Graph Title'
87
      # [show_graph_subtitle] false
88
      # [graph_subtitle] 'Graph Sub Title'
89
      # [key] true,
90
      # [key_position] :right, # bottom or righ
91
      # [font_size] 12
92
      # [title_font_size] 16
93
      # [subtitle_font_size] 14
94
      # [x_label_font_size] 12
95
      # [x_title_font_size] 14
96
      # [y_label_font_size] 12
97
      # [y_title_font_size] 14
98
      # [key_font_size] 10
99
      # [no_css] false
100
      # [add_popups] false
101
      def initialize( config )
102
        @config = config
103

    
104
        self.top_align = self.top_font = self.right_align = self.right_font = 0
105

    
106
        init_with({
107
          :width                => 500,
108
          :height                => 300,
109
          :show_x_guidelines    => false,
110
          :show_y_guidelines    => true,
111
          :show_data_values     => true,
112

    
113
#          :min_scale_value      => 0,
114

    
115
          :show_x_labels        => true,
116
          :stagger_x_labels     => false,
117
          :rotate_x_labels      => false,
118
          :step_x_labels        => 1,
119
          :step_include_first_x_label => true,
120

    
121
          :show_y_labels        => true,
122
          :rotate_y_labels      => false,
123
          :stagger_y_labels     => false,
124
          :scale_integers       => false,
125

    
126
          :show_x_title         => false,
127
          :x_title              => 'X Field names',
128

    
129
          :show_y_title         => false,
130
          :y_title_text_direction => :bt,
131
          :y_title              => 'Y Scale',
132

    
133
          :show_graph_title      => false,
134
          :graph_title          => 'Graph Title',
135
          :show_graph_subtitle  => false,
136
          :graph_subtitle        => 'Graph Sub Title',
137
          :key                  => true, 
138
          :key_position          => :right, # bottom or right
139

    
140
          :font_size            =>12,
141
          :title_font_size      =>16,
142
          :subtitle_font_size   =>14,
143
          :x_label_font_size    =>12,
144
          :x_title_font_size    =>14,
145
          :y_label_font_size    =>12,
146
          :y_title_font_size    =>14,
147
          :key_font_size        =>10,
148
          
149
          :no_css               =>false,
150
          :add_popups           =>false,
151
        })
152

    
153
                                set_defaults if respond_to? :set_defaults
154

    
155
        init_with config
156
      end
157

    
158
      
159
      # This method allows you do add data to the graph object.
160
      # It can be called several times to add more data sets in.
161
      #
162
      #   data_sales_02 = [12, 45, 21];
163
      #   
164
      #   graph.add_data({
165
      #     :data => data_sales_02,
166
      #     :title => 'Sales 2002'
167
      #   })
168
      def add_data conf
169
        @data = [] unless defined? @data
170

    
171
        if conf[:data] and conf[:data].kind_of? Array
172
          @data << conf
173
        else
174
          raise "No data provided by #{conf.inspect}"
175
        end
176
      end
177

    
178

    
179
      # This method removes all data from the object so that you can
180
      # reuse it to create a new graph but with the same config options.
181
      #
182
      #   graph.clear_data
183
      def clear_data 
184
        @data = []
185
      end
186

    
187

    
188
      # This method processes the template with the data and
189
      # config which has been set and returns the resulting SVG.
190
      #
191
      # This method will croak unless at least one data set has
192
      # been added to the graph object.
193
      #
194
      #   print graph.burn
195
      def burn
196
        raise "No data available" unless @data.size > 0
197
        
198
        calculations if respond_to? :calculations
199

    
200
        start_svg
201
        calculate_graph_dimensions
202
        @foreground = Element.new( "g" )
203
        draw_graph
204
        draw_titles
205
        draw_legend
206
        draw_data
207
        @graph.add_element( @foreground )
208
        style
209

    
210
        data = ""
211
        @doc.write( data, 0 )
212

    
213
        if @config[:compress]
214
          if @@__have_zlib
215
            inp, out = IO.pipe
216
            gz = Zlib::GzipWriter.new( out )
217
            gz.write data
218
            gz.close
219
            data = inp.read
220
          else
221
            data << "<!-- Ruby Zlib not available for SVGZ -->";
222
          end
223
        end
224
        
225
        return data
226
      end
227

    
228

    
229
      #   Set the height of the graph box, this is the total height
230
      #   of the SVG box created - not the graph it self which auto
231
      #   scales to fix the space.
232
      attr_accessor :height
233
      #   Set the width of the graph box, this is the total width
234
      #   of the SVG box created - not the graph it self which auto
235
      #   scales to fix the space.
236
      attr_accessor :width
237
      #   Set the path to an external stylesheet, set to '' if
238
      #   you want to revert back to using the defaut internal version.
239
      #
240
      #   To create an external stylesheet create a graph using the
241
      #   default internal version and copy the stylesheet section to
242
      #   an external file and edit from there.
243
      attr_accessor :style_sheet
244
      #   (Bool) Show the value of each element of data on the graph
245
      attr_accessor :show_data_values
246
      #   The point at which the Y axis starts, defaults to '0',
247
      #   if set to nil it will default to the minimum data value.
248
      attr_accessor :min_scale_value
249
      #   Whether to show labels on the X axis or not, defaults
250
      #   to true, set to false if you want to turn them off.
251
      attr_accessor :show_x_labels
252
      #   This puts the X labels at alternative levels so if they
253
      #   are long field names they will not overlap so easily.
254
      #   Default it false, to turn on set to true.
255
      attr_accessor :stagger_x_labels
256
      #   This puts the Y labels at alternative levels so if they
257
      #   are long field names they will not overlap so easily.
258
      #   Default it false, to turn on set to true.
259
      attr_accessor :stagger_y_labels
260
      #   This turns the X axis labels by 90 degrees.
261
      #   Default it false, to turn on set to true.
262
      attr_accessor :rotate_x_labels
263
      #   This turns the Y axis labels by 90 degrees.
264
      #   Default it false, to turn on set to true.
265
      attr_accessor :rotate_y_labels
266
      #   How many "steps" to use between displayed X axis labels,
267
      #   a step of one means display every label, a step of two results
268
      #   in every other label being displayed (label <gap> label <gap> label),
269
      #   a step of three results in every third label being displayed
270
      #   (label <gap> <gap> label <gap> <gap> label) and so on.
271
      attr_accessor :step_x_labels
272
      #   Whether to (when taking "steps" between X axis labels) step from 
273
      #   the first label (i.e. always include the first label) or step from
274
      #   the X axis origin (i.e. start with a gap if step_x_labels is greater
275
      #   than one).
276
      attr_accessor :step_include_first_x_label
277
      #   Whether to show labels on the Y axis or not, defaults
278
      #   to true, set to false if you want to turn them off.
279
      attr_accessor :show_y_labels
280
      #   Ensures only whole numbers are used as the scale divisions.
281
      #   Default it false, to turn on set to true. This has no effect if 
282
      #   scale divisions are less than 1.
283
      attr_accessor :scale_integers
284
      #   This defines the gap between markers on the Y axis,
285
      #   default is a 10th of the max_value, e.g. you will have
286
      #   10 markers on the Y axis. NOTE: do not set this too
287
      #   low - you are limited to 999 markers, after that the
288
      #   graph won't generate.
289
      attr_accessor :scale_divisions
290
      #   Whether to show the title under the X axis labels,
291
      #   default is false, set to true to show.
292
      attr_accessor :show_x_title
293
      #   What the title under X axis should be, e.g. 'Months'.
294
      attr_accessor :x_title
295
      #   Whether to show the title under the Y axis labels,
296
      #   default is false, set to true to show.
297
      attr_accessor :show_y_title
298
      #   Aligns writing mode for Y axis label. 
299
      #   Defaults to :bt (Bottom to Top).
300
      #   Change to :tb (Top to Bottom) to reverse.
301
      attr_accessor :y_title_text_direction
302
      #   What the title under Y axis should be, e.g. 'Sales in thousands'.
303
      attr_accessor :y_title
304
      #   Whether to show a title on the graph, defaults
305
      #   to false, set to true to show.
306
      attr_accessor :show_graph_title
307
      #   What the title on the graph should be.
308
      attr_accessor :graph_title
309
      #   Whether to show a subtitle on the graph, defaults
310
      #   to false, set to true to show.
311
      attr_accessor :show_graph_subtitle
312
      #   What the subtitle on the graph should be.
313
      attr_accessor :graph_subtitle
314
      #   Whether to show a key, defaults to false, set to
315
      #   true if you want to show it.
316
      attr_accessor :key
317
      #   Where the key should be positioned, defaults to
318
      #   :right, set to :bottom if you want to move it.
319
      attr_accessor :key_position
320
      # Set the font size (in points) of the data point labels
321
      attr_accessor :font_size
322
      # Set the font size of the X axis labels
323
      attr_accessor :x_label_font_size
324
      # Set the font size of the X axis title
325
      attr_accessor :x_title_font_size
326
      # Set the font size of the Y axis labels
327
      attr_accessor :y_label_font_size
328
      # Set the font size of the Y axis title
329
      attr_accessor :y_title_font_size
330
      # Set the title font size
331
      attr_accessor :title_font_size
332
      # Set the subtitle font size
333
      attr_accessor :subtitle_font_size
334
      # Set the key font size
335
      attr_accessor :key_font_size
336
      # Show guidelines for the X axis
337
      attr_accessor :show_x_guidelines
338
      # Show guidelines for the Y axis
339
      attr_accessor :show_y_guidelines
340
      # Do not use CSS if set to true.  Many SVG viewers do not support CSS, but
341
      # not using CSS can result in larger SVGs as well as making it impossible to
342
      # change colors after the chart is generated.  Defaults to false.
343
      attr_accessor :no_css
344
      # Add popups for the data points on some graphs
345
      attr_accessor :add_popups
346

    
347

    
348
      protected
349

    
350
      def sort( *arrys )
351
        sort_multiple( arrys )
352
      end
353

    
354
      # Overwrite configuration options with supplied options.  Used
355
      # by subclasses.
356
      def init_with config
357
        config.each { |key, value|
358
          self.send((key.to_s+"=").to_sym, value ) if respond_to? key.to_sym
359
        }
360
      end
361

    
362
      attr_accessor :top_align, :top_font, :right_align, :right_font
363

    
364
      KEY_BOX_SIZE = 12
365

    
366
      # Override this (and call super) to change the margin to the left
367
      # of the plot area.  Results in @border_left being set.
368
      def calculate_left_margin
369
        @border_left = 7
370
        # Check for Y labels
371
        max_y_label_height_px = rotate_y_labels ? 
372
          y_label_font_size :
373
          get_y_labels.max{|a,b| 
374
            a.to_s.length<=>b.to_s.length
375
          }.to_s.length * y_label_font_size * 0.6
376
        @border_left += max_y_label_height_px if show_y_labels
377
        @border_left += max_y_label_height_px + 10 if stagger_y_labels
378
        @border_left += y_title_font_size + 5 if show_y_title
379
      end
380

    
381

    
382
      # Calculates the width of the widest Y label.  This will be the
383
      # character height if the Y labels are rotated
384
      def max_y_label_width_px
385
        return font_size if rotate_y_labels
386
      end
387

    
388

    
389
      # Override this (and call super) to change the margin to the right
390
      # of the plot area.  Results in @border_right being set.
391
      def calculate_right_margin
392
        @border_right = 7
393
        if key and key_position == :right
394
          val = keys.max { |a,b| a.length <=> b.length }
395
          @border_right += val.length * key_font_size * 0.6 
396
          @border_right += KEY_BOX_SIZE
397
          @border_right += 10    # Some padding around the box
398
        end
399
      end
400

    
401

    
402
      # Override this (and call super) to change the margin to the top
403
      # of the plot area.  Results in @border_top being set.
404
      def calculate_top_margin
405
        @border_top = 5
406
        @border_top += title_font_size if show_graph_title
407
        @border_top += 5
408
        @border_top += subtitle_font_size if show_graph_subtitle
409
      end
410

    
411

    
412
      # Adds pop-up point information to a graph.
413
      def add_popup( x, y, label )
414
        txt_width = label.length * font_size * 0.6 + 10
415
        tx = (x+txt_width > width ? x-5 : x+5)
416
        t = @foreground.add_element( "text", {
417
          "x" => tx.to_s,
418
          "y" => (y - font_size).to_s,
419
          "visibility" => "hidden",
420
        })
421
        t.attributes["style"] = "fill: #000; "+
422
          (x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;")
423
        t.text = label.to_s
424
        t.attributes["id"] = t.object_id.to_s
425

    
426
        @foreground.add_element( "circle", {
427
          "cx" => x.to_s,
428
          "cy" => y.to_s,
429
          "r" => "10",
430
          "style" => "opacity: 0",
431
          "onmouseover" => 
432
            "document.getElementById(#{t.object_id}).setAttribute('visibility', 'visible' )",
433
          "onmouseout" => 
434
            "document.getElementById(#{t.object_id}).setAttribute('visibility', 'hidden' )",
435
        })
436

    
437
      end
438

    
439
      
440
      # Override this (and call super) to change the margin to the bottom
441
      # of the plot area.  Results in @border_bottom being set.
442
      def calculate_bottom_margin
443
        @border_bottom = 7
444
        if key and key_position == :bottom
445
          @border_bottom += @data.size * (font_size + 5)
446
          @border_bottom += 10
447
        end
448
        if show_x_labels
449
                  max_x_label_height_px = (not rotate_x_labels) ? 
450
            x_label_font_size :
451
            get_x_labels.max{|a,b| 
452
              a.to_s.length<=>b.to_s.length
453
            }.to_s.length * x_label_font_size * 0.6
454
          @border_bottom += max_x_label_height_px
455
          @border_bottom += max_x_label_height_px + 10 if stagger_x_labels
456
        end
457
        @border_bottom += x_title_font_size + 5 if show_x_title
458
      end
459

    
460

    
461
      # Draws the background, axis, and labels.
462
      def draw_graph
463
        @graph = @root.add_element( "g", {
464
          "transform" => "translate( #@border_left #@border_top )"
465
        })
466

    
467
        # Background
468
        @graph.add_element( "rect", {
469
          "x" => "0",
470
          "y" => "0",
471
          "width" => @graph_width.to_s,
472
          "height" => @graph_height.to_s,
473
          "class" => "graphBackground"
474
        })
475

    
476
        # Axis
477
        @graph.add_element( "path", {
478
          "d" => "M 0 0 v#@graph_height",
479
          "class" => "axis",
480
          "id" => "xAxis"
481
        })
482
        @graph.add_element( "path", {
483
          "d" => "M 0 #@graph_height h#@graph_width",
484
          "class" => "axis",
485
          "id" => "yAxis"
486
        })
487

    
488
        draw_x_labels
489
        draw_y_labels
490
      end
491

    
492

    
493
      # Where in the X area the label is drawn
494
      # Centered in the field, should be width/2.  Start, 0.
495
      def x_label_offset( width )
496
        0
497
      end
498

    
499
      def make_datapoint_text( x, y, value, style="" )
500
        if show_data_values
501
          @foreground.add_element( "text", {
502
            "x" => x.to_s,
503
            "y" => y.to_s,
504
            "class" => "dataPointLabel",
505
            "style" => "#{style} stroke: #fff; stroke-width: 2;"
506
          }).text = value.to_s
507
          text = @foreground.add_element( "text", {
508
            "x" => x.to_s,
509
            "y" => y.to_s,
510
            "class" => "dataPointLabel"
511
          })
512
          text.text = value.to_s
513
          text.attributes["style"] = style if style.length > 0
514
        end
515
      end
516

    
517

    
518
      # Draws the X axis labels
519
      def draw_x_labels
520
        stagger = x_label_font_size + 5
521
        if show_x_labels
522
          label_width = field_width
523

    
524
          count = 0
525
          for label in get_x_labels
526
            if step_include_first_x_label == true then
527
              step = count % step_x_labels
528
            else
529
              step = (count + 1) % step_x_labels
530
            end
531

    
532
            if step == 0 then
533
              text = @graph.add_element( "text" )
534
              text.attributes["class"] = "xAxisLabels"
535
              text.text = label.to_s
536

    
537
              x = count * label_width + x_label_offset( label_width )
538
              y = @graph_height + x_label_font_size + 3
539
              t = 0 - (font_size / 2)
540

    
541
              if stagger_x_labels and count % 2 == 1
542
                y += stagger
543
                @graph.add_element( "path", {
544
                  "d" => "M#{x} #@graph_height v#{stagger}",
545
                  "class" => "staggerGuideLine"
546
                })
547
              end
548

    
549
              text.attributes["x"] = x.to_s
550
              text.attributes["y"] = y.to_s
551
              if rotate_x_labels
552
                text.attributes["transform"] = 
553
                  "rotate( 90 #{x} #{y-x_label_font_size} )"+
554
                  " translate( 0 -#{x_label_font_size/4} )"
555
                text.attributes["style"] = "text-anchor: start"
556
              else
557
                text.attributes["style"] = "text-anchor: middle"
558
              end
559
            end
560

    
561
            draw_x_guidelines( label_width, count ) if show_x_guidelines
562
            count += 1
563
          end
564
        end
565
      end
566

    
567

    
568
      # Where in the Y area the label is drawn
569
      # Centered in the field, should be width/2.  Start, 0.
570
      def y_label_offset( height )
571
        0
572
      end
573

    
574

    
575
      def field_width
576
        (@graph_width.to_f - font_size*2*right_font) /
577
           (get_x_labels.length - right_align)
578
      end
579

    
580

    
581
      def field_height
582
        (@graph_height.to_f - font_size*2*top_font) /
583
           (get_y_labels.length - top_align)
584
      end
585

    
586

    
587
      # Draws the Y axis labels
588
      def draw_y_labels
589
        stagger = y_label_font_size + 5
590
        if show_y_labels
591
          label_height = field_height
592

    
593
          count = 0
594
          y_offset = @graph_height + y_label_offset( label_height )
595
          y_offset += font_size/1.2 unless rotate_y_labels
596
          for label in get_y_labels
597
            y = y_offset - (label_height * count)
598
            x = rotate_y_labels ? 0 : -3
599

    
600
            if stagger_y_labels and count % 2 == 1
601
              x -= stagger
602
              @graph.add_element( "path", {
603
                "d" => "M#{x} #{y} h#{stagger}",
604
                "class" => "staggerGuideLine"
605
              })
606
            end
607

    
608
            text = @graph.add_element( "text", {
609
              "x" => x.to_s,
610
              "y" => y.to_s,
611
              "class" => "yAxisLabels"
612
            })
613
            text.text = label.to_s
614
            if rotate_y_labels
615
              text.attributes["transform"] = "translate( -#{font_size} 0 ) "+
616
                "rotate( 90 #{x} #{y} ) "
617
              text.attributes["style"] = "text-anchor: middle"
618
            else
619
              text.attributes["y"] = (y - (y_label_font_size/2)).to_s
620
              text.attributes["style"] = "text-anchor: end"
621
            end
622
            draw_y_guidelines( label_height, count ) if show_y_guidelines
623
            count += 1
624
          end
625
        end
626
      end
627

    
628

    
629
      # Draws the X axis guidelines
630
      def draw_x_guidelines( label_height, count )
631
        if count != 0
632
          @graph.add_element( "path", {
633
            "d" => "M#{label_height*count} 0 v#@graph_height",
634
            "class" => "guideLines"
635
          })
636
        end
637
      end
638

    
639

    
640
      # Draws the Y axis guidelines
641
      def draw_y_guidelines( label_height, count )
642
        if count != 0
643
          @graph.add_element( "path", {
644
            "d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width",
645
            "class" => "guideLines"
646
          })
647
        end
648
      end
649

    
650

    
651
      # Draws the graph title and subtitle
652
      def draw_titles
653
        if show_graph_title
654
          @root.add_element( "text", {
655
            "x" => (width / 2).to_s,
656
            "y" => (title_font_size).to_s,
657
            "class" => "mainTitle"
658
          }).text = graph_title.to_s
659
        end
660

    
661
        if show_graph_subtitle
662
          y_subtitle = show_graph_title ? 
663
            title_font_size + 10 :
664
            subtitle_font_size
665
          @root.add_element("text", {
666
            "x" => (width / 2).to_s,
667
            "y" => (y_subtitle).to_s,
668
            "class" => "subTitle"
669
          }).text = graph_subtitle.to_s
670
        end
671

    
672
        if show_x_title
673
          y = @graph_height + @border_top + x_title_font_size
674
          if show_x_labels
675
            y += x_label_font_size + 5 if stagger_x_labels
676
            y += x_label_font_size + 5
677
          end
678
          x = width / 2
679

    
680
          @root.add_element("text", {
681
            "x" => x.to_s,
682
            "y" => y.to_s,
683
            "class" => "xAxisTitle",
684
          }).text = x_title.to_s
685
        end
686

    
687
        if show_y_title
688
          x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3)
689
          y = height / 2
690

    
691
          text = @root.add_element("text", {
692
            "x" => x.to_s,
693
            "y" => y.to_s,
694
            "class" => "yAxisTitle",
695
          })
696
          text.text = y_title.to_s
697
          if y_title_text_direction == :bt
698
            text.attributes["transform"] = "rotate( -90, #{x}, #{y} )"
699
          else
700
            text.attributes["transform"] = "rotate( 90, #{x}, #{y} )"
701
          end
702
        end
703
      end
704

    
705
      def keys 
706
        return @data.collect{ |d| d[:title] }
707
      end
708

    
709
      # Draws the legend on the graph
710
      def draw_legend
711
        if key
712
          group = @root.add_element( "g" )
713

    
714
          key_count = 0
715
          for key_name in keys
716
            y_offset = (KEY_BOX_SIZE * key_count) + (key_count * 5)
717
            group.add_element( "rect", {
718
              "x" => 0.to_s,
719
              "y" => y_offset.to_s,
720
              "width" => KEY_BOX_SIZE.to_s,
721
              "height" => KEY_BOX_SIZE.to_s,
722
              "class" => "key#{key_count+1}"
723
            })
724
            group.add_element( "text", {
725
              "x" => (KEY_BOX_SIZE + 5).to_s,
726
              "y" => (y_offset + KEY_BOX_SIZE).to_s,
727
              "class" => "keyText"
728
            }).text = key_name.to_s
729
            key_count += 1
730
          end
731

    
732
          case key_position
733
          when :right
734
            x_offset = @graph_width + @border_left + 10
735
            y_offset = @border_top + 20
736
          when :bottom
737
            x_offset = @border_left + 20
738
            y_offset = @border_top + @graph_height + 5
739
            if show_x_labels
740
                          max_x_label_height_px = (not rotate_x_labels) ? 
741
                                x_label_font_size :
742
                                get_x_labels.max{|a,b| 
743
                                  a.to_s.length<=>b.to_s.length
744
                                }.to_s.length * x_label_font_size * 0.6
745
                x_label_font_size
746
              y_offset += max_x_label_height_px
747
              y_offset += max_x_label_height_px + 5 if stagger_x_labels
748
            end
749
            y_offset += x_title_font_size + 5 if show_x_title
750
          end
751
          group.attributes["transform"] = "translate(#{x_offset} #{y_offset})"
752
        end
753
      end
754

    
755

    
756
      private
757

    
758
      def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 )
759
        if lo < hi
760
          p = partition(arrys,lo,hi)
761
          sort_multiple(arrys, lo, p-1)
762
          sort_multiple(arrys, p+1, hi)
763
        end
764
        arrys 
765
      end
766

    
767
      def partition( arrys, lo, hi )
768
        p = arrys[0][lo]
769
        l = lo
770
        z = lo+1
771
        while z <= hi
772
          if arrys[0][z] < p
773
            l += 1
774
            arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] }
775
          end
776
          z += 1
777
        end
778
        arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] }
779
        l
780
      end
781

    
782
      def style
783
        if no_css
784
          styles = parse_css
785
          @root.elements.each("//*[@class]") { |el|
786
            cl = el.attributes["class"]
787
            style = styles[cl]
788
            style += el.attributes["style"] if el.attributes["style"]
789
            el.attributes["style"] = style
790
          }
791
        end
792
      end
793

    
794
      def parse_css
795
        css = get_style
796
        rv = {}
797
        while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m
798
          names_orig = names = $1
799
          css = $'
800
          css =~ /([^}]+)\}/m
801
          content = $1
802
          css = $'
803

    
804
          nms = []
805
          while names =~ /^\s*,?\s*\.(\w+)/
806
            nms << $1
807
            names = $'
808
          end
809

    
810
          content = content.tr( "\n\t", " ")
811
          for name in nms
812
            current = rv[name]
813
            current = current ? current+"; "+content : content
814
            rv[name] = current.strip.squeeze(" ")
815
          end
816
        end
817
        return rv
818
      end
819

    
820

    
821
      # Override and place code to add defs here
822
      def add_defs defs
823
      end
824

    
825

    
826
      def start_svg
827
        # Base document
828
        @doc = Document.new
829
        @doc << XMLDecl.new
830
        @doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } +
831
          %q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} )
832
        if style_sheet && style_sheet != ''
833
          @doc << Instruction.new( "xml-stylesheet",
834
            %Q{href="#{style_sheet}" type="text/css"} )
835
        end
836
        @root = @doc.add_element( "svg", {
837
          "width" => width.to_s,
838
          "height" => height.to_s,
839
          "viewBox" => "0 0 #{width} #{height}",
840
          "xmlns" => "http://www.w3.org/2000/svg",
841
          "xmlns:xlink" => "http://www.w3.org/1999/xlink",
842
          "xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/",
843
          "a3:scriptImplementation" => "Adobe"
844
        })
845
        @root << Comment.new( " "+"\\"*66 )
846
        @root << Comment.new( " Created with SVG::Graph " )
847
        @root << Comment.new( " SVG::Graph by Sean E. Russell " )
848
        @root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+
849
        " Leo Lapworth & Stephan Morgan " )
850
        @root << Comment.new( " "+"/"*66 )
851

    
852
        defs = @root.add_element( "defs" )
853
        add_defs defs
854
        if not(style_sheet && style_sheet != '') and !no_css
855
          @root << Comment.new(" include default stylesheet if none specified ")
856
          style = defs.add_element( "style", {"type"=>"text/css"} )
857
          style << CData.new( get_style )
858
        end
859

    
860
        @root << Comment.new( "SVG Background" )
861
        @root.add_element( "rect", {
862
          "width" => width.to_s,
863
          "height" => height.to_s,
864
          "x" => "0",
865
          "y" => "0",
866
          "class" => "svgBackground"
867
        })
868
      end
869

    
870

    
871
      def calculate_graph_dimensions
872
        calculate_left_margin
873
        calculate_right_margin
874
        calculate_bottom_margin
875
        calculate_top_margin
876
        @graph_width = width - @border_left - @border_right
877
        @graph_height = height - @border_top - @border_bottom
878
      end
879

    
880
      def get_style
881
        return <<EOL
882
/* Copy from here for external style sheet */
883
.svgBackground{
884
  fill:#ffffff;
885
}
886
.graphBackground{
887
  fill:#f0f0f0;
888
}
889

890
/* graphs titles */
891
.mainTitle{
892
  text-anchor: middle;
893
  fill: #000000;
894
  font-size: #{title_font_size}px;
895
  font-family: "Arial", sans-serif;
896
  font-weight: normal;
897
}
898
.subTitle{
899
  text-anchor: middle;
900
  fill: #999999;
901
  font-size: #{subtitle_font_size}px;
902
  font-family: "Arial", sans-serif;
903
  font-weight: normal;
904
}
905

906
.axis{
907
  stroke: #000000;
908
  stroke-width: 1px;
909
}
910

911
.guideLines{
912
  stroke: #666666;
913
  stroke-width: 1px;
914
  stroke-dasharray: 5 5;
915
}
916

917
.xAxisLabels{
918
  text-anchor: middle;
919
  fill: #000000;
920
  font-size: #{x_label_font_size}px;
921
  font-family: "Arial", sans-serif;
922
  font-weight: normal;
923
}
924

925
.yAxisLabels{
926
  text-anchor: end;
927
  fill: #000000;
928
  font-size: #{y_label_font_size}px;
929
  font-family: "Arial", sans-serif;
930
  font-weight: normal;
931
}
932

933
.xAxisTitle{
934
  text-anchor: middle;
935
  fill: #ff0000;
936
  font-size: #{x_title_font_size}px;
937
  font-family: "Arial", sans-serif;
938
  font-weight: normal;
939
}
940

941
.yAxisTitle{
942
  fill: #ff0000;
943
  text-anchor: middle;
944
  font-size: #{y_title_font_size}px;
945
  font-family: "Arial", sans-serif;
946
  font-weight: normal;
947
}
948

949
.dataPointLabel{
950
  fill: #000000;
951
  text-anchor:middle;
952
  font-size: 10px;
953
  font-family: "Arial", sans-serif;
954
  font-weight: normal;
955
}
956

957
.staggerGuideLine{
958
  fill: none;
959
  stroke: #000000;
960
  stroke-width: 0.5px;  
961
}
962

963
#{get_css}
964

965
.keyText{
966
  fill: #000000;
967
  text-anchor:start;
968
  font-size: #{key_font_size}px;
969
  font-family: "Arial", sans-serif;
970
  font-weight: normal;
971
}
972
/* End copy for external style sheet */
973
EOL
974
      end
975

    
976
    end
977
  end
978
end