Revision 1297:0a574315af3e lib/SVG/Graph

View differences:

lib/SVG/Graph/Bar.rb
1
require 'rexml/document'
2
require 'SVG/Graph/Graph'
3
require 'SVG/Graph/BarBase'
4

  
5
module SVG
6
  module Graph
7
    # === Create presentation quality SVG bar graphs easily
8
    #
9
    # = Synopsis
10
    #
11
    #   require 'SVG/Graph/Bar'
12
    #
13
    #   fields = %w(Jan Feb Mar);
14
    #   data_sales_02 = [12, 45, 21]
15
    #
16
    #   graph = SVG::Graph::Bar.new(
17
    #     :height => 500,
18
    #     :width => 300,
19
    #     :fields => fields
20
    #   )
21
    #
22
    #   graph.add_data(
23
    #     :data => data_sales_02,
24
    #     :title => 'Sales 2002'
25
    #   )
26
    #
27
    #   print "Content-type: image/svg+xml\r\n\r\n"
28
    #   print graph.burn
29
    #
30
    # = Description
31
    #
32
    # This object aims to allow you to easily create high quality
33
    # SVG[http://www.w3c.org/tr/svg bar graphs. You can either use the default
34
    # style sheet or supply your own. Either way there are many options which
35
    # can be configured to give you control over how the graph is generated -
36
    # with or without a key, data elements at each point, title, subtitle etc.
37
    #
38
    # = Notes
39
    #
40
    # The default stylesheet handles upto 12 data sets, if you
41
    # use more you must create your own stylesheet and add the
42
    # additional settings for the extra data sets. You will know
43
    # if you go over 12 data sets as they will have no style and
44
    # be in black.
45
    #
46
    # = Examples
47
    #
48
    # * http://germane-software.com/repositories/public/SVG/test/test.rb
49
    #
50
    # = See also
51
    #
52
    # * SVG::Graph::Graph
53
    # * SVG::Graph::BarHorizontal
54
    # * SVG::Graph::Line
55
    # * SVG::Graph::Pie
56
    # * SVG::Graph::Plot
57
    # * SVG::Graph::TimeSeries
58
    class Bar < BarBase
59
      include REXML
60

  
61
      # See Graph::initialize and BarBase::set_defaults
62
      def set_defaults 
63
        super
64
        self.top_align = self.top_font = 1
65
      end
66

  
67
      protected
68

  
69
      def get_x_labels
70
        @config[:fields]
71
      end
72

  
73
      def get_y_labels
74
        maxvalue = max_value
75
        minvalue = min_value
76
        range = maxvalue - minvalue
77

  
78
        top_pad = range == 0 ? 10 : range / 20.0
79
        scale_range = (maxvalue + top_pad) - minvalue
80

  
81
        scale_division = scale_divisions || (scale_range / 10.0)
82

  
83
        if scale_integers
84
          scale_division = scale_division < 1 ? 1 : scale_division.round
85
        end
86

  
87
        rv = []
88
        maxvalue = maxvalue%scale_division == 0 ? 
89
          maxvalue : maxvalue + scale_division
90
        minvalue.step( maxvalue, scale_division ) {|v| rv << v}
91
        return rv
92
      end
93

  
94
      def x_label_offset( width )
95
        width / 2.0
96
      end
97

  
98
      def draw_data
99
        minvalue = min_value
100
        fieldwidth = field_width
101

  
102
        unit_size =  (@graph_height.to_f - font_size*2*top_font) / 
103
                          (get_y_labels.max - get_y_labels.min)
104
        bargap = bar_gap ? (fieldwidth < 10 ? fieldwidth / 2 : 10) : 0
105

  
106
        bar_width = fieldwidth - bargap
107
        bar_width /= @data.length if stack == :side
108
        x_mod = (@graph_width-bargap)/2 - (stack==:side ? bar_width/2 : 0)
109
 
110
        bottom = @graph_height
111

  
112
        field_count = 0
113
        @config[:fields].each_index { |i|
114
          dataset_count = 0
115
          for dataset in @data
116
          
117
            # cases (assume 0 = +ve):
118
            #   value  min  length
119
            #    +ve   +ve  value - min
120
            #    +ve   -ve  value - 0
121
            #    -ve   -ve  value.abs - 0
122
          
123
            value = dataset[:data][i]
124
            
125
            left = (fieldwidth * field_count)
126
            
127
            length = (value.abs - (minvalue > 0 ? minvalue : 0)) * unit_size
128
            # top is 0 if value is negative
129
            top = bottom - (((value < 0 ? 0 : value) - minvalue) * unit_size)
130
            left += bar_width * dataset_count if stack == :side
131
 
132
            @graph.add_element( "rect", {
133
              "x" => left.to_s,
134
              "y" => top.to_s,
135
              "width" => bar_width.to_s,
136
              "height" => length.to_s,
137
              "class" => "fill#{dataset_count+1}"
138
            })
139

  
140
            make_datapoint_text(left + bar_width/2.0, top - 6, value.to_s)
141
            dataset_count += 1
142
          end
143
          field_count += 1
144
        }
145
      end
146
    end
147
  end
148
end
1
require 'rexml/document'
2
require 'SVG/Graph/Graph'
3
require 'SVG/Graph/BarBase'
4

  
5
module SVG
6
  module Graph
7
    # === Create presentation quality SVG bar graphs easily
8
    #
9
    # = Synopsis
10
    #
11
    #   require 'SVG/Graph/Bar'
12
    #
13
    #   fields = %w(Jan Feb Mar);
14
    #   data_sales_02 = [12, 45, 21]
15
    #
16
    #   graph = SVG::Graph::Bar.new(
17
    #     :height => 500,
18
    #     :width => 300,
19
    #     :fields => fields
20
    #   )
21
    #
22
    #   graph.add_data(
23
    #     :data => data_sales_02,
24
    #     :title => 'Sales 2002'
25
    #   )
26
    #
27
    #   print "Content-type: image/svg+xml\r\n\r\n"
28
    #   print graph.burn
29
    #
30
    # = Description
31
    #
32
    # This object aims to allow you to easily create high quality
33
    # SVG[http://www.w3c.org/tr/svg bar graphs. You can either use the default
34
    # style sheet or supply your own. Either way there are many options which
35
    # can be configured to give you control over how the graph is generated -
36
    # with or without a key, data elements at each point, title, subtitle etc.
37
    #
38
    # = Notes
39
    #
40
    # The default stylesheet handles upto 12 data sets, if you
41
    # use more you must create your own stylesheet and add the
42
    # additional settings for the extra data sets. You will know
43
    # if you go over 12 data sets as they will have no style and
44
    # be in black.
45
    #
46
    # = Examples
47
    #
48
    # * http://germane-software.com/repositories/public/SVG/test/test.rb
49
    #
50
    # = See also
51
    #
52
    # * SVG::Graph::Graph
53
    # * SVG::Graph::BarHorizontal
54
    # * SVG::Graph::Line
55
    # * SVG::Graph::Pie
56
    # * SVG::Graph::Plot
57
    # * SVG::Graph::TimeSeries
58
    class Bar < BarBase
59
      include REXML
60

  
61
      # See Graph::initialize and BarBase::set_defaults
62
      def set_defaults 
63
        super
64
        self.top_align = self.top_font = 1
65
      end
66

  
67
      protected
68

  
69
      def get_x_labels
70
        @config[:fields]
71
      end
72

  
73
      def get_y_labels
74
        maxvalue = max_value
75
        minvalue = min_value
76
        range = maxvalue - minvalue
77

  
78
        top_pad = range == 0 ? 10 : range / 20.0
79
        scale_range = (maxvalue + top_pad) - minvalue
80

  
81
        scale_division = scale_divisions || (scale_range / 10.0)
82

  
83
        if scale_integers
84
          scale_division = scale_division < 1 ? 1 : scale_division.round
85
        end
86

  
87
        rv = []
88
        maxvalue = maxvalue%scale_division == 0 ? 
89
          maxvalue : maxvalue + scale_division
90
        minvalue.step( maxvalue, scale_division ) {|v| rv << v}
91
        return rv
92
      end
93

  
94
      def x_label_offset( width )
95
        width / 2.0
96
      end
97

  
98
      def draw_data
99
        minvalue = min_value
100
        fieldwidth = field_width
101

  
102
        unit_size =  (@graph_height.to_f - font_size*2*top_font) / 
103
                          (get_y_labels.max - get_y_labels.min)
104
        bargap = bar_gap ? (fieldwidth < 10 ? fieldwidth / 2 : 10) : 0
105

  
106
        bar_width = fieldwidth - bargap
107
        bar_width /= @data.length if stack == :side
108
        x_mod = (@graph_width-bargap)/2 - (stack==:side ? bar_width/2 : 0)
109
 
110
        bottom = @graph_height
111

  
112
        field_count = 0
113
        @config[:fields].each_index { |i|
114
          dataset_count = 0
115
          for dataset in @data
116
          
117
            # cases (assume 0 = +ve):
118
            #   value  min  length
119
            #    +ve   +ve  value - min
120
            #    +ve   -ve  value - 0
121
            #    -ve   -ve  value.abs - 0
122
          
123
            value = dataset[:data][i]
124
            
125
            left = (fieldwidth * field_count)
126
            
127
            length = (value.abs - (minvalue > 0 ? minvalue : 0)) * unit_size
128
            # top is 0 if value is negative
129
            top = bottom - (((value < 0 ? 0 : value) - minvalue) * unit_size)
130
            left += bar_width * dataset_count if stack == :side
131
 
132
            @graph.add_element( "rect", {
133
              "x" => left.to_s,
134
              "y" => top.to_s,
135
              "width" => bar_width.to_s,
136
              "height" => length.to_s,
137
              "class" => "fill#{dataset_count+1}"
138
            })
139

  
140
            make_datapoint_text(left + bar_width/2.0, top - 6, value.to_s)
141
            dataset_count += 1
142
          end
143
          field_count += 1
144
        }
145
      end
146
    end
147
  end
148
end
lib/SVG/Graph/BarBase.rb
1
require 'rexml/document'
2
require 'SVG/Graph/Graph'
3

  
4
module SVG
5
  module Graph
6
		# = Synopsis
7
		#
8
		# A superclass for bar-style graphs.  Do not attempt to instantiate
9
		# directly; use one of the subclasses instead.
10
		#
11
    # = Author
12
    #
13
    # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
14
		#
15
    # Copyright 2004 Sean E. Russell
16
		# This software is available under the Ruby license[LICENSE.txt]
17
    #
18
    class BarBase < SVG::Graph::Graph
19
			# Ensures that :fields are provided in the configuration.
20
      def initialize config
21
        raise "fields was not supplied or is empty" unless config[:fields] &&
22
        config[:fields].kind_of?(Array) &&
23
        config[:fields].length > 0
24
				super
25
			end
26

  
27
			# In addition to the defaults set in Graph::initialize, sets
28
			# [bar_gap] true
29
			# [stack] :overlap
30
			def set_defaults
31
        init_with( :bar_gap => true, :stack => :overlap )
32
      end
33

  
34
      #   Whether to have a gap between the bars or not, default
35
      #   is true, set to false if you don't want gaps.
36
      attr_accessor :bar_gap
37
      #   How to stack data sets.  :overlap overlaps bars with
38
      #   transparent colors, :top stacks bars on top of one another,
39
      #   :side stacks the bars side-by-side. Defaults to :overlap.
40
      attr_accessor :stack
41

  
42

  
43
			protected
44

  
45
      def max_value
46
        @data.collect{|x| x[:data].max}.max
47
      end
48

  
49
      def min_value
50
        min = 0
51
        if min_scale_value.nil? 
52
          min = @data.collect{|x| x[:data].min}.min
53
          min = min > 0 ? 0 : min
54
        else
55
          min = min_scale_value
56
        end
57
        return min
58
      end
59

  
60
      def get_css
61
        return <<EOL
62
/* default fill styles for multiple datasets (probably only use a single dataset on this graph though) */
63
.key1,.fill1{
64
	fill: #ff0000;
65
	fill-opacity: 0.5;
66
	stroke: none;
67
	stroke-width: 0.5px;	
68
}
69
.key2,.fill2{
70
	fill: #0000ff;
71
	fill-opacity: 0.5;
72
	stroke: none;
73
	stroke-width: 1px;	
74
}
75
.key3,.fill3{
76
	fill: #00ff00;
77
	fill-opacity: 0.5;
78
	stroke: none;
79
	stroke-width: 1px;	
80
}
81
.key4,.fill4{
82
	fill: #ffcc00;
83
	fill-opacity: 0.5;
84
	stroke: none;
85
	stroke-width: 1px;	
86
}
87
.key5,.fill5{
88
	fill: #00ccff;
89
	fill-opacity: 0.5;
90
	stroke: none;
91
	stroke-width: 1px;	
92
}
93
.key6,.fill6{
94
	fill: #ff00ff;
95
	fill-opacity: 0.5;
96
	stroke: none;
97
	stroke-width: 1px;	
98
}
99
.key7,.fill7{
100
	fill: #00ffff;
101
	fill-opacity: 0.5;
102
	stroke: none;
103
	stroke-width: 1px;	
104
}
105
.key8,.fill8{
106
	fill: #ffff00;
107
	fill-opacity: 0.5;
108
	stroke: none;
109
	stroke-width: 1px;	
110
}
111
.key9,.fill9{
112
	fill: #cc6666;
113
	fill-opacity: 0.5;
114
	stroke: none;
115
	stroke-width: 1px;	
116
}
117
.key10,.fill10{
118
	fill: #663399;
119
	fill-opacity: 0.5;
120
	stroke: none;
121
	stroke-width: 1px;	
122
}
123
.key11,.fill11{
124
	fill: #339900;
125
	fill-opacity: 0.5;
126
	stroke: none;
127
	stroke-width: 1px;	
128
}
129
.key12,.fill12{
130
	fill: #9966FF;
131
	fill-opacity: 0.5;
132
	stroke: none;
133
	stroke-width: 1px;	
134
}
135
EOL
136
      end
137
    end
138
  end
139
end
1
require 'rexml/document'
2
require 'SVG/Graph/Graph'
3

  
4
module SVG
5
  module Graph
6
		# = Synopsis
7
		#
8
		# A superclass for bar-style graphs.  Do not attempt to instantiate
9
		# directly; use one of the subclasses instead.
10
		#
11
    # = Author
12
    #
13
    # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
14
		#
15
    # Copyright 2004 Sean E. Russell
16
		# This software is available under the Ruby license[LICENSE.txt]
17
    #
18
    class BarBase < SVG::Graph::Graph
19
			# Ensures that :fields are provided in the configuration.
20
      def initialize config
21
        raise "fields was not supplied or is empty" unless config[:fields] &&
22
        config[:fields].kind_of?(Array) &&
23
        config[:fields].length > 0
24
				super
25
			end
26

  
27
			# In addition to the defaults set in Graph::initialize, sets
28
			# [bar_gap] true
29
			# [stack] :overlap
30
			def set_defaults
31
        init_with( :bar_gap => true, :stack => :overlap )
32
      end
33

  
34
      #   Whether to have a gap between the bars or not, default
35
      #   is true, set to false if you don't want gaps.
36
      attr_accessor :bar_gap
37
      #   How to stack data sets.  :overlap overlaps bars with
38
      #   transparent colors, :top stacks bars on top of one another,
39
      #   :side stacks the bars side-by-side. Defaults to :overlap.
40
      attr_accessor :stack
41

  
42

  
43
			protected
44

  
45
      def max_value
46
        @data.collect{|x| x[:data].max}.max
47
      end
48

  
49
      def min_value
50
        min = 0
51
        if min_scale_value.nil? 
52
          min = @data.collect{|x| x[:data].min}.min
53
          min = min > 0 ? 0 : min
54
        else
55
          min = min_scale_value
56
        end
57
        return min
58
      end
59

  
60
      def get_css
61
        return <<EOL
62
/* default fill styles for multiple datasets (probably only use a single dataset on this graph though) */
63
.key1,.fill1{
64
	fill: #ff0000;
65
	fill-opacity: 0.5;
66
	stroke: none;
67
	stroke-width: 0.5px;	
68
}
69
.key2,.fill2{
70
	fill: #0000ff;
71
	fill-opacity: 0.5;
72
	stroke: none;
73
	stroke-width: 1px;	
74
}
75
.key3,.fill3{
76
	fill: #00ff00;
77
	fill-opacity: 0.5;
78
	stroke: none;
79
	stroke-width: 1px;	
80
}
81
.key4,.fill4{
82
	fill: #ffcc00;
83
	fill-opacity: 0.5;
84
	stroke: none;
85
	stroke-width: 1px;	
86
}
87
.key5,.fill5{
88
	fill: #00ccff;
89
	fill-opacity: 0.5;
90
	stroke: none;
91
	stroke-width: 1px;	
92
}
93
.key6,.fill6{
94
	fill: #ff00ff;
95
	fill-opacity: 0.5;
96
	stroke: none;
97
	stroke-width: 1px;	
98
}
99
.key7,.fill7{
100
	fill: #00ffff;
101
	fill-opacity: 0.5;
102
	stroke: none;
103
	stroke-width: 1px;	
104
}
105
.key8,.fill8{
106
	fill: #ffff00;
107
	fill-opacity: 0.5;
108
	stroke: none;
109
	stroke-width: 1px;	
110
}
111
.key9,.fill9{
112
	fill: #cc6666;
113
	fill-opacity: 0.5;
114
	stroke: none;
115
	stroke-width: 1px;	
116
}
117
.key10,.fill10{
118
	fill: #663399;
119
	fill-opacity: 0.5;
120
	stroke: none;
121
	stroke-width: 1px;	
122
}
123
.key11,.fill11{
124
	fill: #339900;
125
	fill-opacity: 0.5;
126
	stroke: none;
127
	stroke-width: 1px;	
128
}
129
.key12,.fill12{
130
	fill: #9966FF;
131
	fill-opacity: 0.5;
132
	stroke: none;
133
	stroke-width: 1px;	
134
}
135
EOL
136
      end
137
    end
138
  end
139
end
lib/SVG/Graph/BarHorizontal.rb
1
require 'rexml/document'
2
require 'SVG/Graph/BarBase'
3

  
4
module SVG
5
  module Graph
6
    # === Create presentation quality SVG horitonzal bar graphs easily
7
    # 
8
    # = Synopsis
9
    # 
10
    #   require 'SVG/Graph/BarHorizontal'
11
    #   
12
    #   fields = %w(Jan Feb Mar)
13
    #   data_sales_02 = [12, 45, 21]
14
    #   
15
    #   graph = SVG::Graph::BarHorizontal.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
    #   print "Content-type: image/svg+xml\r\n\r\n"
27
    #   print graph.burn
28
    # 
29
    # = Description
30
    # 
31
    # This object aims to allow you to easily create high quality
32
    # SVG horitonzal bar graphs. You can either use the default style sheet
33
    # or supply your own. Either way there are many options which can
34
    # be configured to give you control over how the graph is
35
    # generated - with or without a key, data elements at each point,
36
    # title, subtitle etc.
37
    # 
38
    # = Examples
39
    # 
40
    # * http://germane-software.com/repositories/public/SVG/test/test.rb
41
    # 
42
    # = See also
43
    # 
44
    # * SVG::Graph::Graph
45
    # * SVG::Graph::Bar
46
    # * SVG::Graph::Line
47
    # * SVG::Graph::Pie
48
    # * SVG::Graph::Plot
49
    # * SVG::Graph::TimeSeries
50
    #
51
    # == Author
52
    #
53
    # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
54
    #
55
    # Copyright 2004 Sean E. Russell
56
    # This software is available under the Ruby license[LICENSE.txt]
57
    #
58
    class BarHorizontal < BarBase
59
      # In addition to the defaults set in BarBase::set_defaults, sets
60
      # [rotate_y_labels] true
61
      # [show_x_guidelines] true
62
      # [show_y_guidelines] false
63
      def set_defaults
64
        super
65
        init_with( 
66
          :rotate_y_labels    => true,
67
          :show_x_guidelines  => true,
68
          :show_y_guidelines  => false
69
        )
70
        self.right_align = self.right_font = 1
71
      end
72
  
73
      protected
74

  
75
      def get_x_labels
76
        maxvalue = max_value
77
        minvalue = min_value
78
        range = maxvalue - minvalue
79
        top_pad = range == 0 ? 10 : range / 20.0
80
        scale_range = (maxvalue + top_pad) - minvalue
81

  
82
        scale_division = scale_divisions || (scale_range / 10.0)
83

  
84
        if scale_integers
85
          scale_division = scale_division < 1 ? 1 : scale_division.round
86
        end
87

  
88
        rv = []
89
        maxvalue = maxvalue%scale_division == 0 ? 
90
          maxvalue : maxvalue + scale_division
91
        minvalue.step( maxvalue, scale_division ) {|v| rv << v}
92
        return rv
93
      end
94

  
95
      def get_y_labels
96
        @config[:fields]
97
      end
98

  
99
      def y_label_offset( height )
100
        height / -2.0
101
      end
102

  
103
      def draw_data
104
        minvalue = min_value
105
        fieldheight = field_height
106

  
107
        unit_size = (@graph_width.to_f - font_size*2*right_font ) /
108
                        (get_x_labels.max - get_x_labels.min )
109
        bargap = bar_gap ? (fieldheight < 10 ? fieldheight / 2 : 10) : 0
110

  
111
        bar_height = fieldheight - bargap
112
        bar_height /= @data.length if stack == :side
113
        y_mod = (bar_height / 2) + (font_size / 2)
114
        
115
        field_count = 1
116
        @config[:fields].each_index { |i|
117
          dataset_count = 0
118
          for dataset in @data
119
            value = dataset[:data][i]
120
            
121
            top = @graph_height - (fieldheight * field_count)
122
            top += (bar_height * dataset_count) if stack == :side
123
            # cases (assume 0 = +ve):
124
            #   value  min  length          left
125
            #    +ve   +ve  value.abs - min minvalue.abs
126
            #    +ve   -ve  value.abs - 0   minvalue.abs
127
            #    -ve   -ve  value.abs - 0   minvalue.abs + value
128
            length = (value.abs - (minvalue > 0 ? minvalue : 0)) * unit_size
129
            left = (minvalue.abs + (value < 0 ? value : 0)) * unit_size
130

  
131
            @graph.add_element( "rect", {
132
              "x" => left.to_s,
133
              "y" => top.to_s,
134
              "width" => length.to_s,
135
              "height" => bar_height.to_s,
136
              "class" => "fill#{dataset_count+1}"
137
            })
138

  
139
            make_datapoint_text( 
140
              left+length+5, top+y_mod, value, "text-anchor: start; "
141
              )
142
            dataset_count += 1
143
          end
144
          field_count += 1
145
        }
146
      end
147
    end
148
  end
149
end
1
require 'rexml/document'
2
require 'SVG/Graph/BarBase'
3

  
4
module SVG
5
  module Graph
6
    # === Create presentation quality SVG horitonzal bar graphs easily
7
    # 
8
    # = Synopsis
9
    # 
10
    #   require 'SVG/Graph/BarHorizontal'
11
    #   
12
    #   fields = %w(Jan Feb Mar)
13
    #   data_sales_02 = [12, 45, 21]
14
    #   
15
    #   graph = SVG::Graph::BarHorizontal.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
    #   print "Content-type: image/svg+xml\r\n\r\n"
27
    #   print graph.burn
28
    # 
29
    # = Description
30
    # 
31
    # This object aims to allow you to easily create high quality
32
    # SVG horitonzal bar graphs. You can either use the default style sheet
33
    # or supply your own. Either way there are many options which can
34
    # be configured to give you control over how the graph is
35
    # generated - with or without a key, data elements at each point,
36
    # title, subtitle etc.
37
    # 
38
    # = Examples
39
    # 
40
    # * http://germane-software.com/repositories/public/SVG/test/test.rb
41
    # 
42
    # = See also
43
    # 
44
    # * SVG::Graph::Graph
45
    # * SVG::Graph::Bar
46
    # * SVG::Graph::Line
47
    # * SVG::Graph::Pie
48
    # * SVG::Graph::Plot
49
    # * SVG::Graph::TimeSeries
50
    #
51
    # == Author
52
    #
53
    # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
54
    #
55
    # Copyright 2004 Sean E. Russell
56
    # This software is available under the Ruby license[LICENSE.txt]
57
    #
58
    class BarHorizontal < BarBase
59
      # In addition to the defaults set in BarBase::set_defaults, sets
60
      # [rotate_y_labels] true
61
      # [show_x_guidelines] true
62
      # [show_y_guidelines] false
63
      def set_defaults
64
        super
65
        init_with( 
66
          :rotate_y_labels    => true,
67
          :show_x_guidelines  => true,
68
          :show_y_guidelines  => false
69
        )
70
        self.right_align = self.right_font = 1
71
      end
72
  
73
      protected
74

  
75
      def get_x_labels
76
        maxvalue = max_value
77
        minvalue = min_value
78
        range = maxvalue - minvalue
79
        top_pad = range == 0 ? 10 : range / 20.0
80
        scale_range = (maxvalue + top_pad) - minvalue
81

  
82
        scale_division = scale_divisions || (scale_range / 10.0)
83

  
84
        if scale_integers
85
          scale_division = scale_division < 1 ? 1 : scale_division.round
86
        end
87

  
88
        rv = []
89
        maxvalue = maxvalue%scale_division == 0 ? 
90
          maxvalue : maxvalue + scale_division
91
        minvalue.step( maxvalue, scale_division ) {|v| rv << v}
92
        return rv
93
      end
94

  
95
      def get_y_labels
96
        @config[:fields]
97
      end
98

  
99
      def y_label_offset( height )
100
        height / -2.0
101
      end
102

  
103
      def draw_data
104
        minvalue = min_value
105
        fieldheight = field_height
106

  
107
        unit_size = (@graph_width.to_f - font_size*2*right_font ) /
108
                        (get_x_labels.max - get_x_labels.min )
109
        bargap = bar_gap ? (fieldheight < 10 ? fieldheight / 2 : 10) : 0
110

  
111
        bar_height = fieldheight - bargap
112
        bar_height /= @data.length if stack == :side
113
        y_mod = (bar_height / 2) + (font_size / 2)
114
        
115
        field_count = 1
116
        @config[:fields].each_index { |i|
117
          dataset_count = 0
118
          for dataset in @data
119
            value = dataset[:data][i]
120
            
121
            top = @graph_height - (fieldheight * field_count)
122
            top += (bar_height * dataset_count) if stack == :side
123
            # cases (assume 0 = +ve):
124
            #   value  min  length          left
125
            #    +ve   +ve  value.abs - min minvalue.abs
126
            #    +ve   -ve  value.abs - 0   minvalue.abs
127
            #    -ve   -ve  value.abs - 0   minvalue.abs + value
128
            length = (value.abs - (minvalue > 0 ? minvalue : 0)) * unit_size
129
            left = (minvalue.abs + (value < 0 ? value : 0)) * unit_size
130

  
131
            @graph.add_element( "rect", {
132
              "x" => left.to_s,
133
              "y" => top.to_s,
134
              "width" => length.to_s,
135
              "height" => bar_height.to_s,
136
              "class" => "fill#{dataset_count+1}"
137
            })
138

  
139
            make_datapoint_text( 
140
              left+length+5, top+y_mod, value, "text-anchor: start; "
141
              )
142
            dataset_count += 1
143
          end
144
          field_count += 1
145
        }
146
      end
147
    end
148
  end
149
end
lib/SVG/Graph/Graph.rb
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
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff