##// END OF EJS Templates
Update SVG library to latest stable (0.6.1) (#3056)....
Jean-Philippe Lang -
r2553:c9c269abf7e9
parent child
Show More
@@ -1,137 +1,148
1 1 require 'rexml/document'
2 2 require 'SVG/Graph/Graph'
3 3 require 'SVG/Graph/BarBase'
4 4
5 5 module SVG
6 6 module Graph
7 7 # === Create presentation quality SVG bar graphs easily
8 8 #
9 9 # = Synopsis
10 10 #
11 11 # require 'SVG/Graph/Bar'
12 12 #
13 13 # fields = %w(Jan Feb Mar);
14 14 # data_sales_02 = [12, 45, 21]
15 15 #
16 16 # graph = SVG::Graph::Bar.new(
17 17 # :height => 500,
18 18 # :width => 300,
19 19 # :fields => fields
20 20 # )
21 21 #
22 22 # graph.add_data(
23 23 # :data => data_sales_02,
24 24 # :title => 'Sales 2002'
25 25 # )
26 26 #
27 27 # print "Content-type: image/svg+xml\r\n\r\n"
28 28 # print graph.burn
29 29 #
30 30 # = Description
31 31 #
32 32 # This object aims to allow you to easily create high quality
33 33 # SVG[http://www.w3c.org/tr/svg bar graphs. You can either use the default
34 34 # style sheet or supply your own. Either way there are many options which
35 35 # can be configured to give you control over how the graph is generated -
36 36 # with or without a key, data elements at each point, title, subtitle etc.
37 37 #
38 38 # = Notes
39 39 #
40 40 # The default stylesheet handles upto 12 data sets, if you
41 41 # use more you must create your own stylesheet and add the
42 42 # additional settings for the extra data sets. You will know
43 43 # if you go over 12 data sets as they will have no style and
44 44 # be in black.
45 45 #
46 46 # = Examples
47 47 #
48 48 # * http://germane-software.com/repositories/public/SVG/test/test.rb
49 49 #
50 50 # = See also
51 51 #
52 52 # * SVG::Graph::Graph
53 53 # * SVG::Graph::BarHorizontal
54 54 # * SVG::Graph::Line
55 55 # * SVG::Graph::Pie
56 56 # * SVG::Graph::Plot
57 57 # * SVG::Graph::TimeSeries
58 58 class Bar < BarBase
59 59 include REXML
60 60
61 61 # See Graph::initialize and BarBase::set_defaults
62 62 def set_defaults
63 63 super
64 64 self.top_align = self.top_font = 1
65 65 end
66 66
67 67 protected
68 68
69 69 def get_x_labels
70 70 @config[:fields]
71 71 end
72 72
73 73 def get_y_labels
74 74 maxvalue = max_value
75 75 minvalue = min_value
76 76 range = maxvalue - minvalue
77 77
78 78 top_pad = range == 0 ? 10 : range / 20.0
79 79 scale_range = (maxvalue + top_pad) - minvalue
80 80
81 81 scale_division = scale_divisions || (scale_range / 10.0)
82 82
83 83 if scale_integers
84 84 scale_division = scale_division < 1 ? 1 : scale_division.round
85 85 end
86 86
87 87 rv = []
88 88 maxvalue = maxvalue%scale_division == 0 ?
89 89 maxvalue : maxvalue + scale_division
90 90 minvalue.step( maxvalue, scale_division ) {|v| rv << v}
91 91 return rv
92 92 end
93 93
94 94 def x_label_offset( width )
95 95 width / 2.0
96 96 end
97 97
98 98 def draw_data
99 fieldwidth = field_width
100 maxvalue = max_value
101 99 minvalue = min_value
100 fieldwidth = field_width
102 101
103 fieldheight = (@graph_height.to_f - font_size*2*top_font) /
102 unit_size = (@graph_height.to_f - font_size*2*top_font) /
104 103 (get_y_labels.max - get_y_labels.min)
105 104 bargap = bar_gap ? (fieldwidth < 10 ? fieldwidth / 2 : 10) : 0
106 105
107 subbar_width = fieldwidth - bargap
108 subbar_width /= @data.length if stack == :side
109 x_mod = (@graph_width-bargap)/2 - (stack==:side ? subbar_width/2 : 0)
110 # Y1
111 p2 = @graph_height
112 # to X2
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
113 112 field_count = 0
114 113 @config[:fields].each_index { |i|
115 114 dataset_count = 0
116 115 for dataset in @data
117 # X1
118 p1 = (fieldwidth * field_count)
119 # to Y2
120 p3 = @graph_height - ((dataset[:data][i] - minvalue) * fieldheight)
121 p1 += subbar_width * dataset_count if stack == :side
122 @graph.add_element( "path", {
123 "class" => "fill#{dataset_count+1}",
124 "d" => "M#{p1} #{p2} V#{p3} h#{subbar_width} V#{p2} Z"
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}"
125 138 })
126 make_datapoint_text(
127 p1 + subbar_width/2.0,
128 p3 - 6,
129 dataset[:data][i].to_s)
139
140 make_datapoint_text(left + bar_width/2.0, top - 6, value.to_s)
130 141 dataset_count += 1
131 142 end
132 143 field_count += 1
133 144 }
134 145 end
135 146 end
136 147 end
137 148 end
@@ -1,140 +1,139
1 1 require 'rexml/document'
2 2 require 'SVG/Graph/Graph'
3 3
4 4 module SVG
5 5 module Graph
6 6 # = Synopsis
7 7 #
8 8 # A superclass for bar-style graphs. Do not attempt to instantiate
9 9 # directly; use one of the subclasses instead.
10 10 #
11 11 # = Author
12 12 #
13 13 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
14 14 #
15 15 # Copyright 2004 Sean E. Russell
16 16 # This software is available under the Ruby license[LICENSE.txt]
17 17 #
18 18 class BarBase < SVG::Graph::Graph
19 19 # Ensures that :fields are provided in the configuration.
20 20 def initialize config
21 21 raise "fields was not supplied or is empty" unless config[:fields] &&
22 22 config[:fields].kind_of?(Array) &&
23 23 config[:fields].length > 0
24 24 super
25 25 end
26 26
27 27 # In addition to the defaults set in Graph::initialize, sets
28 28 # [bar_gap] true
29 29 # [stack] :overlap
30 30 def set_defaults
31 31 init_with( :bar_gap => true, :stack => :overlap )
32 32 end
33 33
34 34 # Whether to have a gap between the bars or not, default
35 35 # is true, set to false if you don't want gaps.
36 36 attr_accessor :bar_gap
37 37 # How to stack data sets. :overlap overlaps bars with
38 38 # transparent colors, :top stacks bars on top of one another,
39 39 # :side stacks the bars side-by-side. Defaults to :overlap.
40 40 attr_accessor :stack
41 41
42 42
43 43 protected
44 44
45 45 def max_value
46 return @data.collect{|x| x[:data].max}.max
46 @data.collect{|x| x[:data].max}.max
47 47 end
48 48
49 49 def min_value
50 50 min = 0
51
52 if (min_scale_value.nil? == false) then
53 min = min_scale_value
54 else
51 if min_scale_value.nil?
55 52 min = @data.collect{|x| x[:data].min}.min
53 min = min > 0 ? 0 : min
54 else
55 min = min_scale_value
56 56 end
57
58 57 return min
59 58 end
60 59
61 60 def get_css
62 61 return <<EOL
63 62 /* default fill styles for multiple datasets (probably only use a single dataset on this graph though) */
64 63 .key1,.fill1{
65 64 fill: #ff0000;
66 65 fill-opacity: 0.5;
67 66 stroke: none;
68 67 stroke-width: 0.5px;
69 68 }
70 69 .key2,.fill2{
71 70 fill: #0000ff;
72 71 fill-opacity: 0.5;
73 72 stroke: none;
74 73 stroke-width: 1px;
75 74 }
76 75 .key3,.fill3{
77 76 fill: #00ff00;
78 77 fill-opacity: 0.5;
79 78 stroke: none;
80 79 stroke-width: 1px;
81 80 }
82 81 .key4,.fill4{
83 82 fill: #ffcc00;
84 83 fill-opacity: 0.5;
85 84 stroke: none;
86 85 stroke-width: 1px;
87 86 }
88 87 .key5,.fill5{
89 88 fill: #00ccff;
90 89 fill-opacity: 0.5;
91 90 stroke: none;
92 91 stroke-width: 1px;
93 92 }
94 93 .key6,.fill6{
95 94 fill: #ff00ff;
96 95 fill-opacity: 0.5;
97 96 stroke: none;
98 97 stroke-width: 1px;
99 98 }
100 99 .key7,.fill7{
101 100 fill: #00ffff;
102 101 fill-opacity: 0.5;
103 102 stroke: none;
104 103 stroke-width: 1px;
105 104 }
106 105 .key8,.fill8{
107 106 fill: #ffff00;
108 107 fill-opacity: 0.5;
109 108 stroke: none;
110 109 stroke-width: 1px;
111 110 }
112 111 .key9,.fill9{
113 112 fill: #cc6666;
114 113 fill-opacity: 0.5;
115 114 stroke: none;
116 115 stroke-width: 1px;
117 116 }
118 117 .key10,.fill10{
119 118 fill: #663399;
120 119 fill-opacity: 0.5;
121 120 stroke: none;
122 121 stroke-width: 1px;
123 122 }
124 123 .key11,.fill11{
125 124 fill: #339900;
126 125 fill-opacity: 0.5;
127 126 stroke: none;
128 127 stroke-width: 1px;
129 128 }
130 129 .key12,.fill12{
131 130 fill: #9966FF;
132 131 fill-opacity: 0.5;
133 132 stroke: none;
134 133 stroke-width: 1px;
135 134 }
136 135 EOL
137 136 end
138 137 end
139 138 end
140 139 end
@@ -1,136 +1,149
1 1 require 'rexml/document'
2 2 require 'SVG/Graph/BarBase'
3 3
4 4 module SVG
5 5 module Graph
6 6 # === Create presentation quality SVG horitonzal bar graphs easily
7 7 #
8 8 # = Synopsis
9 9 #
10 10 # require 'SVG/Graph/BarHorizontal'
11 11 #
12 12 # fields = %w(Jan Feb Mar)
13 13 # data_sales_02 = [12, 45, 21]
14 14 #
15 15 # graph = SVG::Graph::BarHorizontal.new({
16 16 # :height => 500,
17 17 # :width => 300,
18 18 # :fields => fields,
19 19 # })
20 20 #
21 21 # graph.add_data({
22 22 # :data => data_sales_02,
23 23 # :title => 'Sales 2002',
24 24 # })
25 25 #
26 26 # print "Content-type: image/svg+xml\r\n\r\n"
27 27 # print graph.burn
28 28 #
29 29 # = Description
30 30 #
31 31 # This object aims to allow you to easily create high quality
32 32 # SVG horitonzal bar graphs. You can either use the default style sheet
33 33 # or supply your own. Either way there are many options which can
34 34 # be configured to give you control over how the graph is
35 35 # generated - with or without a key, data elements at each point,
36 36 # title, subtitle etc.
37 37 #
38 38 # = Examples
39 39 #
40 40 # * http://germane-software.com/repositories/public/SVG/test/test.rb
41 41 #
42 42 # = See also
43 43 #
44 44 # * SVG::Graph::Graph
45 45 # * SVG::Graph::Bar
46 46 # * SVG::Graph::Line
47 47 # * SVG::Graph::Pie
48 48 # * SVG::Graph::Plot
49 49 # * SVG::Graph::TimeSeries
50 50 #
51 51 # == Author
52 52 #
53 53 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
54 54 #
55 55 # Copyright 2004 Sean E. Russell
56 56 # This software is available under the Ruby license[LICENSE.txt]
57 57 #
58 58 class BarHorizontal < BarBase
59 59 # In addition to the defaults set in BarBase::set_defaults, sets
60 60 # [rotate_y_labels] true
61 61 # [show_x_guidelines] true
62 62 # [show_y_guidelines] false
63 63 def set_defaults
64 64 super
65 65 init_with(
66 66 :rotate_y_labels => true,
67 67 :show_x_guidelines => true,
68 68 :show_y_guidelines => false
69 69 )
70 70 self.right_align = self.right_font = 1
71 71 end
72 72
73 73 protected
74 74
75 75 def get_x_labels
76 76 maxvalue = max_value
77 77 minvalue = min_value
78 78 range = maxvalue - minvalue
79 79 top_pad = range == 0 ? 10 : range / 20.0
80 80 scale_range = (maxvalue + top_pad) - minvalue
81 81
82 82 scale_division = scale_divisions || (scale_range / 10.0)
83 83
84 84 if scale_integers
85 85 scale_division = scale_division < 1 ? 1 : scale_division.round
86 86 end
87 87
88 88 rv = []
89 89 maxvalue = maxvalue%scale_division == 0 ?
90 90 maxvalue : maxvalue + scale_division
91 91 minvalue.step( maxvalue, scale_division ) {|v| rv << v}
92 92 return rv
93 93 end
94 94
95 95 def get_y_labels
96 96 @config[:fields]
97 97 end
98 98
99 99 def y_label_offset( height )
100 100 height / -2.0
101 101 end
102 102
103 103 def draw_data
104 104 minvalue = min_value
105 105 fieldheight = field_height
106 fieldwidth = (@graph_width.to_f - font_size*2*right_font ) /
106
107 unit_size = (@graph_width.to_f - font_size*2*right_font ) /
107 108 (get_x_labels.max - get_x_labels.min )
108 109 bargap = bar_gap ? (fieldheight < 10 ? fieldheight / 2 : 10) : 0
109 110
110 subbar_height = fieldheight - bargap
111 subbar_height /= @data.length if stack == :side
111 bar_height = fieldheight - bargap
112 bar_height /= @data.length if stack == :side
113 y_mod = (bar_height / 2) + (font_size / 2)
112 114
113 115 field_count = 1
114 y_mod = (subbar_height / 2) + (font_size / 2)
115 116 @config[:fields].each_index { |i|
116 117 dataset_count = 0
117 118 for dataset in @data
118 y = @graph_height - (fieldheight * field_count)
119 y += (subbar_height * dataset_count) if stack == :side
120 x = (dataset[:data][i] - minvalue) * fieldwidth
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
121 130
122 @graph.add_element( "path", {
123 "d" => "M0 #{y} H#{x} v#{subbar_height} H0 Z",
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,
124 136 "class" => "fill#{dataset_count+1}"
125 137 })
138
126 139 make_datapoint_text(
127 x+5, y+y_mod, dataset[:data][i], "text-anchor: start; "
140 left+length+5, top+y_mod, value, "text-anchor: start; "
128 141 )
129 142 dataset_count += 1
130 143 end
131 144 field_count += 1
132 145 }
133 146 end
134 147 end
135 148 end
136 149 end
@@ -1,977 +1,978
1 1 begin
2 2 require 'zlib'
3 3 @@__have_zlib = true
4 4 rescue
5 5 @@__have_zlib = false
6 6 end
7 7
8 8 require 'rexml/document'
9 9
10 10 module SVG
11 11 module Graph
12 12 VERSION = '@ANT_VERSION@'
13 13
14 14 # === Base object for generating SVG Graphs
15 15 #
16 16 # == Synopsis
17 17 #
18 18 # This class is only used as a superclass of specialized charts. Do not
19 19 # attempt to use this class directly, unless creating a new chart type.
20 20 #
21 21 # For examples of how to subclass this class, see the existing specific
22 22 # subclasses, such as SVG::Graph::Pie.
23 23 #
24 24 # == Examples
25 25 #
26 26 # For examples of how to use this package, see either the test files, or
27 27 # the documentation for the specific class you want to use.
28 28 #
29 29 # * file:test/plot.rb
30 30 # * file:test/single.rb
31 31 # * file:test/test.rb
32 32 # * file:test/timeseries.rb
33 33 #
34 34 # == Description
35 35 #
36 36 # This package should be used as a base for creating SVG graphs.
37 37 #
38 38 # == Acknowledgements
39 39 #
40 40 # Leo Lapworth for creating the SVG::TT::Graph package which this Ruby
41 41 # port is based on.
42 42 #
43 43 # Stephen Morgan for creating the TT template and SVG.
44 44 #
45 45 # == See
46 46 #
47 47 # * SVG::Graph::BarHorizontal
48 48 # * SVG::Graph::Bar
49 49 # * SVG::Graph::Line
50 50 # * SVG::Graph::Pie
51 51 # * SVG::Graph::Plot
52 52 # * SVG::Graph::TimeSeries
53 53 #
54 54 # == Author
55 55 #
56 56 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
57 57 #
58 58 # Copyright 2004 Sean E. Russell
59 59 # This software is available under the Ruby license[LICENSE.txt]
60 60 #
61 61 class Graph
62 62 include REXML
63 63
64 64 # Initialize the graph object with the graph settings. You won't
65 65 # instantiate this class directly; see the subclass for options.
66 66 # [width] 500
67 67 # [height] 300
68 68 # [show_x_guidelines] false
69 69 # [show_y_guidelines] true
70 70 # [show_data_values] true
71 71 # [min_scale_value] 0
72 72 # [show_x_labels] true
73 73 # [stagger_x_labels] false
74 74 # [rotate_x_labels] false
75 75 # [step_x_labels] 1
76 76 # [step_include_first_x_label] true
77 77 # [show_y_labels] true
78 78 # [rotate_y_labels] false
79 79 # [scale_integers] false
80 80 # [show_x_title] false
81 81 # [x_title] 'X Field names'
82 82 # [show_y_title] false
83 83 # [y_title_text_direction] :bt
84 84 # [y_title] 'Y Scale'
85 85 # [show_graph_title] false
86 86 # [graph_title] 'Graph Title'
87 87 # [show_graph_subtitle] false
88 88 # [graph_subtitle] 'Graph Sub Title'
89 89 # [key] true,
90 90 # [key_position] :right, # bottom or righ
91 91 # [font_size] 12
92 92 # [title_font_size] 16
93 93 # [subtitle_font_size] 14
94 94 # [x_label_font_size] 12
95 95 # [x_title_font_size] 14
96 96 # [y_label_font_size] 12
97 97 # [y_title_font_size] 14
98 98 # [key_font_size] 10
99 99 # [no_css] false
100 100 # [add_popups] false
101 101 def initialize( config )
102 102 @config = config
103 103
104 104 self.top_align = self.top_font = self.right_align = self.right_font = 0
105 105
106 106 init_with({
107 107 :width => 500,
108 108 :height => 300,
109 109 :show_x_guidelines => false,
110 110 :show_y_guidelines => true,
111 111 :show_data_values => true,
112 112
113 :min_scale_value => 0,
113 # :min_scale_value => 0,
114 114
115 115 :show_x_labels => true,
116 116 :stagger_x_labels => false,
117 117 :rotate_x_labels => false,
118 118 :step_x_labels => 1,
119 119 :step_include_first_x_label => true,
120 120
121 121 :show_y_labels => true,
122 122 :rotate_y_labels => false,
123 123 :stagger_y_labels => false,
124 124 :scale_integers => false,
125 125
126 126 :show_x_title => false,
127 127 :x_title => 'X Field names',
128 128
129 129 :show_y_title => false,
130 130 :y_title_text_direction => :bt,
131 131 :y_title => 'Y Scale',
132 132
133 133 :show_graph_title => false,
134 134 :graph_title => 'Graph Title',
135 135 :show_graph_subtitle => false,
136 136 :graph_subtitle => 'Graph Sub Title',
137 137 :key => true,
138 138 :key_position => :right, # bottom or right
139 139
140 :font_size =>10,
141 :title_font_size =>12,
140 :font_size =>12,
141 :title_font_size =>16,
142 142 :subtitle_font_size =>14,
143 :x_label_font_size =>11,
143 :x_label_font_size =>12,
144 144 :x_title_font_size =>14,
145 :y_label_font_size =>11,
145 :y_label_font_size =>12,
146 146 :y_title_font_size =>14,
147 :key_font_size => 9,
147 :key_font_size =>10,
148 148
149 149 :no_css =>false,
150 150 :add_popups =>false,
151 151 })
152 152
153 153 set_defaults if methods.include? "set_defaults"
154 154
155 155 init_with config
156 156 end
157 157
158 158
159 159 # This method allows you do add data to the graph object.
160 160 # It can be called several times to add more data sets in.
161 161 #
162 162 # data_sales_02 = [12, 45, 21];
163 163 #
164 164 # graph.add_data({
165 165 # :data => data_sales_02,
166 166 # :title => 'Sales 2002'
167 167 # })
168 168 def add_data conf
169 169 @data = [] unless defined? @data
170 170
171 171 if conf[:data] and conf[:data].kind_of? Array
172 172 @data << conf
173 173 else
174 174 raise "No data provided by #{conf.inspect}"
175 175 end
176 176 end
177 177
178 178
179 179 # This method removes all data from the object so that you can
180 180 # reuse it to create a new graph but with the same config options.
181 181 #
182 182 # graph.clear_data
183 183 def clear_data
184 184 @data = []
185 185 end
186 186
187 187
188 188 # This method processes the template with the data and
189 189 # config which has been set and returns the resulting SVG.
190 190 #
191 191 # This method will croak unless at least one data set has
192 192 # been added to the graph object.
193 193 #
194 194 # print graph.burn
195 195 def burn
196 196 raise "No data available" unless @data.size > 0
197 197
198 198 calculations if methods.include? 'calculations'
199 199
200 200 start_svg
201 201 calculate_graph_dimensions
202 202 @foreground = Element.new( "g" )
203 203 draw_graph
204 204 draw_titles
205 205 draw_legend
206 206 draw_data
207 207 @graph.add_element( @foreground )
208 208 style
209 209
210 210 data = ""
211 211 @doc.write( data, 0 )
212 212
213 213 if @config[:compress]
214 214 if @@__have_zlib
215 215 inp, out = IO.pipe
216 216 gz = Zlib::GzipWriter.new( out )
217 217 gz.write data
218 218 gz.close
219 219 data = inp.read
220 220 else
221 221 data << "<!-- Ruby Zlib not available for SVGZ -->";
222 222 end
223 223 end
224 224
225 225 return data
226 226 end
227 227
228 228
229 229 # Set the height of the graph box, this is the total height
230 230 # of the SVG box created - not the graph it self which auto
231 231 # scales to fix the space.
232 232 attr_accessor :height
233 233 # Set the width of the graph box, this is the total width
234 234 # of the SVG box created - not the graph it self which auto
235 235 # scales to fix the space.
236 236 attr_accessor :width
237 237 # Set the path to an external stylesheet, set to '' if
238 238 # you want to revert back to using the defaut internal version.
239 239 #
240 240 # To create an external stylesheet create a graph using the
241 241 # default internal version and copy the stylesheet section to
242 242 # an external file and edit from there.
243 243 attr_accessor :style_sheet
244 244 # (Bool) Show the value of each element of data on the graph
245 245 attr_accessor :show_data_values
246 246 # The point at which the Y axis starts, defaults to '0',
247 247 # if set to nil it will default to the minimum data value.
248 248 attr_accessor :min_scale_value
249 249 # Whether to show labels on the X axis or not, defaults
250 250 # to true, set to false if you want to turn them off.
251 251 attr_accessor :show_x_labels
252 252 # This puts the X labels at alternative levels so if they
253 253 # are long field names they will not overlap so easily.
254 254 # Default it false, to turn on set to true.
255 255 attr_accessor :stagger_x_labels
256 256 # This puts the Y labels at alternative levels so if they
257 257 # are long field names they will not overlap so easily.
258 258 # Default it false, to turn on set to true.
259 259 attr_accessor :stagger_y_labels
260 260 # This turns the X axis labels by 90 degrees.
261 261 # Default it false, to turn on set to true.
262 262 attr_accessor :rotate_x_labels
263 263 # This turns the Y axis labels by 90 degrees.
264 264 # Default it false, to turn on set to true.
265 265 attr_accessor :rotate_y_labels
266 266 # How many "steps" to use between displayed X axis labels,
267 267 # a step of one means display every label, a step of two results
268 268 # in every other label being displayed (label <gap> label <gap> label),
269 269 # a step of three results in every third label being displayed
270 270 # (label <gap> <gap> label <gap> <gap> label) and so on.
271 271 attr_accessor :step_x_labels
272 272 # Whether to (when taking "steps" between X axis labels) step from
273 273 # the first label (i.e. always include the first label) or step from
274 274 # the X axis origin (i.e. start with a gap if step_x_labels is greater
275 275 # than one).
276 276 attr_accessor :step_include_first_x_label
277 277 # Whether to show labels on the Y axis or not, defaults
278 278 # to true, set to false if you want to turn them off.
279 279 attr_accessor :show_y_labels
280 280 # Ensures only whole numbers are used as the scale divisions.
281 281 # Default it false, to turn on set to true. This has no effect if
282 282 # scale divisions are less than 1.
283 283 attr_accessor :scale_integers
284 284 # This defines the gap between markers on the Y axis,
285 285 # default is a 10th of the max_value, e.g. you will have
286 286 # 10 markers on the Y axis. NOTE: do not set this too
287 287 # low - you are limited to 999 markers, after that the
288 288 # graph won't generate.
289 289 attr_accessor :scale_divisions
290 290 # Whether to show the title under the X axis labels,
291 291 # default is false, set to true to show.
292 292 attr_accessor :show_x_title
293 293 # What the title under X axis should be, e.g. 'Months'.
294 294 attr_accessor :x_title
295 295 # Whether to show the title under the Y axis labels,
296 296 # default is false, set to true to show.
297 297 attr_accessor :show_y_title
298 298 # Aligns writing mode for Y axis label.
299 299 # Defaults to :bt (Bottom to Top).
300 300 # Change to :tb (Top to Bottom) to reverse.
301 301 attr_accessor :y_title_text_direction
302 302 # What the title under Y axis should be, e.g. 'Sales in thousands'.
303 303 attr_accessor :y_title
304 304 # Whether to show a title on the graph, defaults
305 305 # to false, set to true to show.
306 306 attr_accessor :show_graph_title
307 307 # What the title on the graph should be.
308 308 attr_accessor :graph_title
309 309 # Whether to show a subtitle on the graph, defaults
310 310 # to false, set to true to show.
311 311 attr_accessor :show_graph_subtitle
312 312 # What the subtitle on the graph should be.
313 313 attr_accessor :graph_subtitle
314 314 # Whether to show a key, defaults to false, set to
315 315 # true if you want to show it.
316 316 attr_accessor :key
317 317 # Where the key should be positioned, defaults to
318 318 # :right, set to :bottom if you want to move it.
319 319 attr_accessor :key_position
320 320 # Set the font size (in points) of the data point labels
321 321 attr_accessor :font_size
322 322 # Set the font size of the X axis labels
323 323 attr_accessor :x_label_font_size
324 324 # Set the font size of the X axis title
325 325 attr_accessor :x_title_font_size
326 326 # Set the font size of the Y axis labels
327 327 attr_accessor :y_label_font_size
328 328 # Set the font size of the Y axis title
329 329 attr_accessor :y_title_font_size
330 330 # Set the title font size
331 331 attr_accessor :title_font_size
332 332 # Set the subtitle font size
333 333 attr_accessor :subtitle_font_size
334 334 # Set the key font size
335 335 attr_accessor :key_font_size
336 336 # Show guidelines for the X axis
337 337 attr_accessor :show_x_guidelines
338 338 # Show guidelines for the Y axis
339 339 attr_accessor :show_y_guidelines
340 340 # Do not use CSS if set to true. Many SVG viewers do not support CSS, but
341 341 # not using CSS can result in larger SVGs as well as making it impossible to
342 342 # change colors after the chart is generated. Defaults to false.
343 343 attr_accessor :no_css
344 344 # Add popups for the data points on some graphs
345 345 attr_accessor :add_popups
346 346
347 347
348 348 protected
349 349
350 350 def sort( *arrys )
351 351 sort_multiple( arrys )
352 352 end
353 353
354 354 # Overwrite configuration options with supplied options. Used
355 355 # by subclasses.
356 356 def init_with config
357 357 config.each { |key, value|
358 358 self.send( key.to_s+"=", value ) if methods.include? key.to_s
359 359 }
360 360 end
361 361
362 362 attr_accessor :top_align, :top_font, :right_align, :right_font
363 363
364 364 KEY_BOX_SIZE = 12
365 365
366 366 # Override this (and call super) to change the margin to the left
367 367 # of the plot area. Results in @border_left being set.
368 368 def calculate_left_margin
369 369 @border_left = 7
370 370 # Check for Y labels
371 371 max_y_label_height_px = rotate_y_labels ?
372 372 y_label_font_size :
373 373 get_y_labels.max{|a,b|
374 374 a.to_s.length<=>b.to_s.length
375 375 }.to_s.length * y_label_font_size * 0.6
376 376 @border_left += max_y_label_height_px if show_y_labels
377 377 @border_left += max_y_label_height_px + 10 if stagger_y_labels
378 378 @border_left += y_title_font_size + 5 if show_y_title
379 379 end
380 380
381 381
382 382 # Calculates the width of the widest Y label. This will be the
383 383 # character height if the Y labels are rotated
384 384 def max_y_label_width_px
385 385 return font_size if rotate_y_labels
386 386 end
387 387
388 388
389 389 # Override this (and call super) to change the margin to the right
390 390 # of the plot area. Results in @border_right being set.
391 391 def calculate_right_margin
392 392 @border_right = 7
393 393 if key and key_position == :right
394 394 val = keys.max { |a,b| a.length <=> b.length }
395 @border_right += val.length * key_font_size * 0.7
395 @border_right += val.length * key_font_size * 0.6
396 396 @border_right += KEY_BOX_SIZE
397 397 @border_right += 10 # Some padding around the box
398 398 end
399 399 end
400 400
401 401
402 402 # Override this (and call super) to change the margin to the top
403 403 # of the plot area. Results in @border_top being set.
404 404 def calculate_top_margin
405 405 @border_top = 5
406 406 @border_top += title_font_size if show_graph_title
407 407 @border_top += 5
408 408 @border_top += subtitle_font_size if show_graph_subtitle
409 409 end
410 410
411 411
412 412 # Adds pop-up point information to a graph.
413 413 def add_popup( x, y, label )
414 414 txt_width = label.length * font_size * 0.6 + 10
415 415 tx = (x+txt_width > width ? x-5 : x+5)
416 416 t = @foreground.add_element( "text", {
417 417 "x" => tx.to_s,
418 418 "y" => (y - font_size).to_s,
419 419 "visibility" => "hidden",
420 420 })
421 421 t.attributes["style"] = "fill: #000; "+
422 422 (x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;")
423 423 t.text = label.to_s
424 t.attributes["id"] = t.id.to_s
424 t.attributes["id"] = t.object_id.to_s
425 425
426 426 @foreground.add_element( "circle", {
427 427 "cx" => x.to_s,
428 428 "cy" => y.to_s,
429 429 "r" => "10",
430 430 "style" => "opacity: 0",
431 431 "onmouseover" =>
432 "document.getElementById(#{t.id}).setAttribute('visibility', 'visible' )",
432 "document.getElementById(#{t.object_id}).setAttribute('visibility', 'visible' )",
433 433 "onmouseout" =>
434 "document.getElementById(#{t.id}).setAttribute('visibility', 'hidden' )",
434 "document.getElementById(#{t.object_id}).setAttribute('visibility', 'hidden' )",
435 435 })
436 436
437 437 end
438 438
439 439
440 440 # Override this (and call super) to change the margin to the bottom
441 441 # of the plot area. Results in @border_bottom being set.
442 442 def calculate_bottom_margin
443 443 @border_bottom = 7
444 444 if key and key_position == :bottom
445 445 @border_bottom += @data.size * (font_size + 5)
446 446 @border_bottom += 10
447 447 end
448 448 if show_x_labels
449 max_x_label_height_px = rotate_x_labels ?
449 max_x_label_height_px = (not rotate_x_labels) ?
450 x_label_font_size :
450 451 get_x_labels.max{|a,b|
451 a.length<=>b.length
452 }.length * x_label_font_size * 0.6 :
453 x_label_font_size
452 a.to_s.length<=>b.to_s.length
453 }.to_s.length * x_label_font_size * 0.6
454 454 @border_bottom += max_x_label_height_px
455 455 @border_bottom += max_x_label_height_px + 10 if stagger_x_labels
456 456 end
457 457 @border_bottom += x_title_font_size + 5 if show_x_title
458 458 end
459 459
460 460
461 461 # Draws the background, axis, and labels.
462 462 def draw_graph
463 463 @graph = @root.add_element( "g", {
464 464 "transform" => "translate( #@border_left #@border_top )"
465 465 })
466 466
467 467 # Background
468 468 @graph.add_element( "rect", {
469 469 "x" => "0",
470 470 "y" => "0",
471 471 "width" => @graph_width.to_s,
472 472 "height" => @graph_height.to_s,
473 473 "class" => "graphBackground"
474 474 })
475 475
476 476 # Axis
477 477 @graph.add_element( "path", {
478 478 "d" => "M 0 0 v#@graph_height",
479 479 "class" => "axis",
480 480 "id" => "xAxis"
481 481 })
482 482 @graph.add_element( "path", {
483 483 "d" => "M 0 #@graph_height h#@graph_width",
484 484 "class" => "axis",
485 485 "id" => "yAxis"
486 486 })
487 487
488 488 draw_x_labels
489 489 draw_y_labels
490 490 end
491 491
492 492
493 493 # Where in the X area the label is drawn
494 494 # Centered in the field, should be width/2. Start, 0.
495 495 def x_label_offset( width )
496 496 0
497 497 end
498 498
499 499 def make_datapoint_text( x, y, value, style="" )
500 500 if show_data_values
501 501 @foreground.add_element( "text", {
502 502 "x" => x.to_s,
503 503 "y" => y.to_s,
504 504 "class" => "dataPointLabel",
505 505 "style" => "#{style} stroke: #fff; stroke-width: 2;"
506 506 }).text = value.to_s
507 507 text = @foreground.add_element( "text", {
508 508 "x" => x.to_s,
509 509 "y" => y.to_s,
510 510 "class" => "dataPointLabel"
511 511 })
512 512 text.text = value.to_s
513 513 text.attributes["style"] = style if style.length > 0
514 514 end
515 515 end
516 516
517 517
518 518 # Draws the X axis labels
519 519 def draw_x_labels
520 520 stagger = x_label_font_size + 5
521 521 if show_x_labels
522 522 label_width = field_width
523 523
524 524 count = 0
525 525 for label in get_x_labels
526 526 if step_include_first_x_label == true then
527 527 step = count % step_x_labels
528 528 else
529 529 step = (count + 1) % step_x_labels
530 530 end
531 531
532 532 if step == 0 then
533 533 text = @graph.add_element( "text" )
534 534 text.attributes["class"] = "xAxisLabels"
535 535 text.text = label.to_s
536 536
537 537 x = count * label_width + x_label_offset( label_width )
538 538 y = @graph_height + x_label_font_size + 3
539 539 t = 0 - (font_size / 2)
540 540
541 541 if stagger_x_labels and count % 2 == 1
542 542 y += stagger
543 543 @graph.add_element( "path", {
544 544 "d" => "M#{x} #@graph_height v#{stagger}",
545 545 "class" => "staggerGuideLine"
546 546 })
547 547 end
548 548
549 549 text.attributes["x"] = x.to_s
550 550 text.attributes["y"] = y.to_s
551 551 if rotate_x_labels
552 552 text.attributes["transform"] =
553 553 "rotate( 90 #{x} #{y-x_label_font_size} )"+
554 554 " translate( 0 -#{x_label_font_size/4} )"
555 555 text.attributes["style"] = "text-anchor: start"
556 556 else
557 557 text.attributes["style"] = "text-anchor: middle"
558 558 end
559 559 end
560 560
561 561 draw_x_guidelines( label_width, count ) if show_x_guidelines
562 562 count += 1
563 563 end
564 564 end
565 565 end
566 566
567 567
568 568 # Where in the Y area the label is drawn
569 569 # Centered in the field, should be width/2. Start, 0.
570 570 def y_label_offset( height )
571 571 0
572 572 end
573 573
574 574
575 575 def field_width
576 576 (@graph_width.to_f - font_size*2*right_font) /
577 577 (get_x_labels.length - right_align)
578 578 end
579 579
580 580
581 581 def field_height
582 582 (@graph_height.to_f - font_size*2*top_font) /
583 583 (get_y_labels.length - top_align)
584 584 end
585 585
586 586
587 587 # Draws the Y axis labels
588 588 def draw_y_labels
589 589 stagger = y_label_font_size + 5
590 590 if show_y_labels
591 591 label_height = field_height
592 592
593 593 count = 0
594 594 y_offset = @graph_height + y_label_offset( label_height )
595 595 y_offset += font_size/1.2 unless rotate_y_labels
596 596 for label in get_y_labels
597 597 y = y_offset - (label_height * count)
598 598 x = rotate_y_labels ? 0 : -3
599 599
600 600 if stagger_y_labels and count % 2 == 1
601 601 x -= stagger
602 602 @graph.add_element( "path", {
603 603 "d" => "M#{x} #{y} h#{stagger}",
604 604 "class" => "staggerGuideLine"
605 605 })
606 606 end
607 607
608 608 text = @graph.add_element( "text", {
609 609 "x" => x.to_s,
610 610 "y" => y.to_s,
611 611 "class" => "yAxisLabels"
612 612 })
613 613 text.text = label.to_s
614 614 if rotate_y_labels
615 615 text.attributes["transform"] = "translate( -#{font_size} 0 ) "+
616 616 "rotate( 90 #{x} #{y} ) "
617 617 text.attributes["style"] = "text-anchor: middle"
618 618 else
619 619 text.attributes["y"] = (y - (y_label_font_size/2)).to_s
620 620 text.attributes["style"] = "text-anchor: end"
621 621 end
622 622 draw_y_guidelines( label_height, count ) if show_y_guidelines
623 623 count += 1
624 624 end
625 625 end
626 626 end
627 627
628 628
629 629 # Draws the X axis guidelines
630 630 def draw_x_guidelines( label_height, count )
631 631 if count != 0
632 632 @graph.add_element( "path", {
633 633 "d" => "M#{label_height*count} 0 v#@graph_height",
634 634 "class" => "guideLines"
635 635 })
636 636 end
637 637 end
638 638
639 639
640 640 # Draws the Y axis guidelines
641 641 def draw_y_guidelines( label_height, count )
642 642 if count != 0
643 643 @graph.add_element( "path", {
644 644 "d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width",
645 645 "class" => "guideLines"
646 646 })
647 647 end
648 648 end
649 649
650 650
651 651 # Draws the graph title and subtitle
652 652 def draw_titles
653 653 if show_graph_title
654 654 @root.add_element( "text", {
655 655 "x" => (width / 2).to_s,
656 656 "y" => (title_font_size).to_s,
657 657 "class" => "mainTitle"
658 658 }).text = graph_title.to_s
659 659 end
660 660
661 661 if show_graph_subtitle
662 662 y_subtitle = show_graph_title ?
663 663 title_font_size + 10 :
664 664 subtitle_font_size
665 665 @root.add_element("text", {
666 666 "x" => (width / 2).to_s,
667 667 "y" => (y_subtitle).to_s,
668 668 "class" => "subTitle"
669 669 }).text = graph_subtitle.to_s
670 670 end
671 671
672 672 if show_x_title
673 673 y = @graph_height + @border_top + x_title_font_size
674 674 if show_x_labels
675 675 y += x_label_font_size + 5 if stagger_x_labels
676 676 y += x_label_font_size + 5
677 677 end
678 678 x = width / 2
679 679
680 680 @root.add_element("text", {
681 681 "x" => x.to_s,
682 682 "y" => y.to_s,
683 683 "class" => "xAxisTitle",
684 684 }).text = x_title.to_s
685 685 end
686 686
687 687 if show_y_title
688 688 x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3)
689 689 y = height / 2
690 690
691 691 text = @root.add_element("text", {
692 692 "x" => x.to_s,
693 693 "y" => y.to_s,
694 694 "class" => "yAxisTitle",
695 695 })
696 696 text.text = y_title.to_s
697 697 if y_title_text_direction == :bt
698 698 text.attributes["transform"] = "rotate( -90, #{x}, #{y} )"
699 699 else
700 700 text.attributes["transform"] = "rotate( 90, #{x}, #{y} )"
701 701 end
702 702 end
703 703 end
704 704
705 705 def keys
706 706 return @data.collect{ |d| d[:title] }
707 707 end
708 708
709 709 # Draws the legend on the graph
710 710 def draw_legend
711 711 if key
712 712 group = @root.add_element( "g" )
713 713
714 714 key_count = 0
715 715 for key_name in keys
716 716 y_offset = (KEY_BOX_SIZE * key_count) + (key_count * 5)
717 717 group.add_element( "rect", {
718 718 "x" => 0.to_s,
719 719 "y" => y_offset.to_s,
720 720 "width" => KEY_BOX_SIZE.to_s,
721 721 "height" => KEY_BOX_SIZE.to_s,
722 722 "class" => "key#{key_count+1}"
723 723 })
724 724 group.add_element( "text", {
725 725 "x" => (KEY_BOX_SIZE + 5).to_s,
726 "y" => (y_offset + KEY_BOX_SIZE - 2).to_s,
726 "y" => (y_offset + KEY_BOX_SIZE).to_s,
727 727 "class" => "keyText"
728 728 }).text = key_name.to_s
729 729 key_count += 1
730 730 end
731 731
732 732 case key_position
733 733 when :right
734 734 x_offset = @graph_width + @border_left + 10
735 735 y_offset = @border_top + 20
736 736 when :bottom
737 737 x_offset = @border_left + 20
738 738 y_offset = @border_top + @graph_height + 5
739 739 if show_x_labels
740 max_x_label_height_px = rotate_x_labels ?
740 max_x_label_height_px = (not rotate_x_labels) ?
741 x_label_font_size :
741 742 get_x_labels.max{|a,b|
742 a.length<=>b.length
743 }.length * x_label_font_size :
743 a.to_s.length<=>b.to_s.length
744 }.to_s.length * x_label_font_size * 0.6
744 745 x_label_font_size
745 746 y_offset += max_x_label_height_px
746 747 y_offset += max_x_label_height_px + 5 if stagger_x_labels
747 748 end
748 749 y_offset += x_title_font_size + 5 if show_x_title
749 750 end
750 751 group.attributes["transform"] = "translate(#{x_offset} #{y_offset})"
751 752 end
752 753 end
753 754
754 755
755 756 private
756 757
757 758 def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 )
758 759 if lo < hi
759 760 p = partition(arrys,lo,hi)
760 761 sort_multiple(arrys, lo, p-1)
761 762 sort_multiple(arrys, p+1, hi)
762 763 end
763 764 arrys
764 765 end
765 766
766 767 def partition( arrys, lo, hi )
767 768 p = arrys[0][lo]
768 769 l = lo
769 770 z = lo+1
770 771 while z <= hi
771 772 if arrys[0][z] < p
772 773 l += 1
773 774 arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] }
774 775 end
775 776 z += 1
776 777 end
777 778 arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] }
778 779 l
779 780 end
780 781
781 782 def style
782 783 if no_css
783 784 styles = parse_css
784 785 @root.elements.each("//*[@class]") { |el|
785 786 cl = el.attributes["class"]
786 787 style = styles[cl]
787 788 style += el.attributes["style"] if el.attributes["style"]
788 789 el.attributes["style"] = style
789 790 }
790 791 end
791 792 end
792 793
793 794 def parse_css
794 795 css = get_style
795 796 rv = {}
796 797 while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m
797 798 names_orig = names = $1
798 799 css = $'
799 800 css =~ /([^}]+)\}/m
800 801 content = $1
801 802 css = $'
802 803
803 804 nms = []
804 805 while names =~ /^\s*,?\s*\.(\w+)/
805 806 nms << $1
806 807 names = $'
807 808 end
808 809
809 810 content = content.tr( "\n\t", " ")
810 811 for name in nms
811 812 current = rv[name]
812 813 current = current ? current+"; "+content : content
813 814 rv[name] = current.strip.squeeze(" ")
814 815 end
815 816 end
816 817 return rv
817 818 end
818 819
819 820
820 821 # Override and place code to add defs here
821 822 def add_defs defs
822 823 end
823 824
824 825
825 826 def start_svg
826 827 # Base document
827 828 @doc = Document.new
828 829 @doc << XMLDecl.new
829 830 @doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } +
830 831 %q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} )
831 832 if style_sheet && style_sheet != ''
832 833 @doc << Instruction.new( "xml-stylesheet",
833 834 %Q{href="#{style_sheet}" type="text/css"} )
834 835 end
835 836 @root = @doc.add_element( "svg", {
836 837 "width" => width.to_s,
837 838 "height" => height.to_s,
838 839 "viewBox" => "0 0 #{width} #{height}",
839 840 "xmlns" => "http://www.w3.org/2000/svg",
840 841 "xmlns:xlink" => "http://www.w3.org/1999/xlink",
841 842 "xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/",
842 843 "a3:scriptImplementation" => "Adobe"
843 844 })
844 845 @root << Comment.new( " "+"\\"*66 )
845 846 @root << Comment.new( " Created with SVG::Graph " )
846 847 @root << Comment.new( " SVG::Graph by Sean E. Russell " )
847 848 @root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+
848 849 " Leo Lapworth & Stephan Morgan " )
849 850 @root << Comment.new( " "+"/"*66 )
850 851
851 852 defs = @root.add_element( "defs" )
852 853 add_defs defs
853 854 if not(style_sheet && style_sheet != '') and !no_css
854 855 @root << Comment.new(" include default stylesheet if none specified ")
855 856 style = defs.add_element( "style", {"type"=>"text/css"} )
856 857 style << CData.new( get_style )
857 858 end
858 859
859 860 @root << Comment.new( "SVG Background" )
860 861 @root.add_element( "rect", {
861 862 "width" => width.to_s,
862 863 "height" => height.to_s,
863 864 "x" => "0",
864 865 "y" => "0",
865 866 "class" => "svgBackground"
866 867 })
867 868 end
868 869
869 870
870 871 def calculate_graph_dimensions
871 872 calculate_left_margin
872 873 calculate_right_margin
873 874 calculate_bottom_margin
874 875 calculate_top_margin
875 876 @graph_width = width - @border_left - @border_right
876 877 @graph_height = height - @border_top - @border_bottom
877 878 end
878 879
879 880 def get_style
880 881 return <<EOL
881 882 /* Copy from here for external style sheet */
882 883 .svgBackground{
883 884 fill:#ffffff;
884 885 }
885 886 .graphBackground{
886 fill:#f5f5f5;
887 fill:#f0f0f0;
887 888 }
888 889
889 890 /* graphs titles */
890 891 .mainTitle{
891 892 text-anchor: middle;
892 fill: #555555;
893 fill: #000000;
893 894 font-size: #{title_font_size}px;
894 font-family: "Verdana", sans-serif;
895 font-weight: bold;
895 font-family: "Arial", sans-serif;
896 font-weight: normal;
896 897 }
897 898 .subTitle{
898 899 text-anchor: middle;
899 900 fill: #999999;
900 901 font-size: #{subtitle_font_size}px;
901 font-family: "Verdana", sans-serif;
902 font-family: "Arial", sans-serif;
902 903 font-weight: normal;
903 904 }
904 905
905 906 .axis{
906 stroke: #666666;
907 stroke: #000000;
907 908 stroke-width: 1px;
908 909 }
909 910
910 911 .guideLines{
911 912 stroke: #666666;
912 913 stroke-width: 1px;
913 stroke-dasharray:2,2,2;
914 stroke-dasharray: 5 5;
914 915 }
915 916
916 917 .xAxisLabels{
917 918 text-anchor: middle;
918 919 fill: #000000;
919 920 font-size: #{x_label_font_size}px;
920 font-family: "Verdana", sans-serif;
921 font-family: "Arial", sans-serif;
921 922 font-weight: normal;
922 923 }
923 924
924 925 .yAxisLabels{
925 926 text-anchor: end;
926 927 fill: #000000;
927 928 font-size: #{y_label_font_size}px;
928 font-family: "Verdana", sans-serif;
929 font-family: "Arial", sans-serif;
929 930 font-weight: normal;
930 931 }
931 932
932 933 .xAxisTitle{
933 934 text-anchor: middle;
934 935 fill: #ff0000;
935 936 font-size: #{x_title_font_size}px;
936 font-family: "Verdana", sans-serif;
937 font-family: "Arial", sans-serif;
937 938 font-weight: normal;
938 939 }
939 940
940 941 .yAxisTitle{
941 942 fill: #ff0000;
942 943 text-anchor: middle;
943 944 font-size: #{y_title_font_size}px;
944 font-family: "Verdana", sans-serif;
945 font-family: "Arial", sans-serif;
945 946 font-weight: normal;
946 947 }
947 948
948 949 .dataPointLabel{
949 950 fill: #000000;
950 951 text-anchor:middle;
951 952 font-size: 10px;
952 font-family: "Verdana", sans-serif;
953 font-family: "Arial", sans-serif;
953 954 font-weight: normal;
954 955 }
955 956
956 957 .staggerGuideLine{
957 958 fill: none;
958 959 stroke: #000000;
959 960 stroke-width: 0.5px;
960 961 }
961 962
962 963 #{get_css}
963 964
964 965 .keyText{
965 966 fill: #000000;
966 967 text-anchor:start;
967 968 font-size: #{key_font_size}px;
968 font-family: "Verdana", sans-serif;
969 font-family: "Arial", sans-serif;
969 970 font-weight: normal;
970 971 }
971 972 /* End copy for external style sheet */
972 973 EOL
973 974 end
974 975
975 976 end
976 977 end
977 978 end
@@ -1,394 +1,395
1 1 require 'SVG/Graph/Graph'
2 2
3 3 module SVG
4 4 module Graph
5 5 # === Create presentation quality SVG pie graphs easily
6 6 #
7 7 # == Synopsis
8 8 #
9 9 # require 'SVG/Graph/Pie'
10 10 #
11 11 # fields = %w(Jan Feb Mar)
12 12 # data_sales_02 = [12, 45, 21]
13 13 #
14 14 # graph = SVG::Graph::Pie.new({
15 15 # :height => 500,
16 16 # :width => 300,
17 17 # :fields => fields,
18 18 # })
19 19 #
20 20 # graph.add_data({
21 21 # :data => data_sales_02,
22 22 # :title => 'Sales 2002',
23 23 # })
24 24 #
25 25 # print "Content-type: image/svg+xml\r\n\r\n"
26 26 # print graph.burn();
27 27 #
28 28 # == Description
29 29 #
30 30 # This object aims to allow you to easily create high quality
31 31 # SVG pie graphs. You can either use the default style sheet
32 32 # or supply your own. Either way there are many options which can
33 33 # be configured to give you control over how the graph is
34 34 # generated - with or without a key, display percent on pie chart,
35 35 # title, subtitle etc.
36 36 #
37 37 # = Examples
38 38 #
39 39 # http://www.germane-software/repositories/public/SVG/test/single.rb
40 40 #
41 41 # == See also
42 42 #
43 43 # * SVG::Graph::Graph
44 44 # * SVG::Graph::BarHorizontal
45 45 # * SVG::Graph::Bar
46 46 # * SVG::Graph::Line
47 47 # * SVG::Graph::Plot
48 48 # * SVG::Graph::TimeSeries
49 49 #
50 50 # == Author
51 51 #
52 52 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
53 53 #
54 54 # Copyright 2004 Sean E. Russell
55 55 # This software is available under the Ruby license[LICENSE.txt]
56 56 #
57 57 class Pie < Graph
58 58 # Defaults are those set by Graph::initialize, and
59 59 # [show_shadow] true
60 60 # [shadow_offset] 10
61 61 # [show_data_labels] false
62 62 # [show_actual_values] false
63 63 # [show_percent] true
64 64 # [show_key_data_labels] true
65 65 # [show_key_actual_values] true
66 66 # [show_key_percent] false
67 67 # [expanded] false
68 68 # [expand_greatest] false
69 69 # [expand_gap] 10
70 70 # [show_x_labels] false
71 71 # [show_y_labels] false
72 72 # [datapoint_font_size] 12
73 73 def set_defaults
74 74 init_with(
75 75 :show_shadow => true,
76 76 :shadow_offset => 10,
77 77
78 78 :show_data_labels => false,
79 79 :show_actual_values => false,
80 80 :show_percent => true,
81 81
82 82 :show_key_data_labels => true,
83 83 :show_key_actual_values => true,
84 84 :show_key_percent => false,
85 85
86 86 :expanded => false,
87 87 :expand_greatest => false,
88 88 :expand_gap => 10,
89 89
90 90 :show_x_labels => false,
91 91 :show_y_labels => false,
92 92 :datapoint_font_size => 12
93 93 )
94 94 @data = []
95 95 end
96 96
97 97 # Adds a data set to the graph.
98 98 #
99 99 # graph.add_data( { :data => [1,2,3,4] } )
100 100 #
101 101 # Note that the :title is not necessary. If multiple
102 102 # data sets are added to the graph, the pie chart will
103 103 # display the +sums+ of the data. EG:
104 104 #
105 105 # graph.add_data( { :data => [1,2,3,4] } )
106 106 # graph.add_data( { :data => [2,3,5,9] } )
107 107 #
108 108 # is the same as:
109 109 #
110 110 # graph.add_data( { :data => [3,5,8,13] } )
111 111 def add_data arg
112 112 arg[:data].each_index {|idx|
113 113 @data[idx] = 0 unless @data[idx]
114 114 @data[idx] += arg[:data][idx]
115 115 }
116 116 end
117 117
118 118 # If true, displays a drop shadow for the chart
119 119 attr_accessor :show_shadow
120 120 # Sets the offset of the shadow from the pie chart
121 121 attr_accessor :shadow_offset
122 122 # If true, display the data labels on the chart
123 123 attr_accessor :show_data_labels
124 124 # If true, display the actual field values in the data labels
125 125 attr_accessor :show_actual_values
126 126 # If true, display the percentage value of each pie wedge in the data
127 127 # labels
128 128 attr_accessor :show_percent
129 129 # If true, display the labels in the key
130 130 attr_accessor :show_key_data_labels
131 131 # If true, display the actual value of the field in the key
132 132 attr_accessor :show_key_actual_values
133 133 # If true, display the percentage value of the wedges in the key
134 134 attr_accessor :show_key_percent
135 135 # If true, "explode" the pie (put space between the wedges)
136 136 attr_accessor :expanded
137 137 # If true, expand the largest pie wedge
138 138 attr_accessor :expand_greatest
139 139 # The amount of space between expanded wedges
140 140 attr_accessor :expand_gap
141 141 # The font size of the data point labels
142 142 attr_accessor :datapoint_font_size
143 143
144 144
145 145 protected
146 146
147 147 def add_defs defs
148 148 gradient = defs.add_element( "filter", {
149 149 "id"=>"dropshadow",
150 150 "width" => "1.2",
151 151 "height" => "1.2",
152 152 } )
153 153 gradient.add_element( "feGaussianBlur", {
154 154 "stdDeviation" => "4",
155 155 "result" => "blur"
156 156 })
157 157 end
158 158
159 159 # We don't need the graph
160 160 def draw_graph
161 161 end
162 162
163 163 def get_y_labels
164 164 [""]
165 165 end
166 166
167 167 def get_x_labels
168 168 [""]
169 169 end
170 170
171 171 def keys
172 172 total = 0
173 173 max_value = 0
174 174 @data.each {|x| total += x }
175 175 percent_scale = 100.0 / total
176 176 count = -1
177 177 a = @config[:fields].collect{ |x|
178 178 count += 1
179 179 v = @data[count]
180 180 perc = show_key_percent ? " "+(v * percent_scale).round.to_s+"%" : ""
181 181 x + " [" + v.to_s + "]" + perc
182 182 }
183 183 end
184 184
185 185 RADIANS = Math::PI/180
186 186
187 187 def draw_data
188 188 @graph = @root.add_element( "g" )
189 189 background = @graph.add_element("g")
190 190 midground = @graph.add_element("g")
191 191
192 192 diameter = @graph_height > @graph_width ? @graph_width : @graph_height
193 193 diameter -= expand_gap if expanded or expand_greatest
194 194 diameter -= datapoint_font_size if show_data_labels
195 195 diameter -= 10 if show_shadow
196 196 radius = diameter / 2.0
197 197
198 198 xoff = (width - diameter) / 2
199 199 yoff = (height - @border_bottom - diameter)
200 200 yoff -= 10 if show_shadow
201 201 @graph.attributes['transform'] = "translate( #{xoff} #{yoff} )"
202 202
203 203 wedge_text_pad = 5
204 204 wedge_text_pad = 20 if show_percent and show_data_labels
205 205
206 206 total = 0
207 207 max_value = 0
208 208 @data.each {|x|
209 209 max_value = max_value < x ? x : max_value
210 210 total += x
211 211 }
212 212 percent_scale = 100.0 / total
213 213
214 214 prev_percent = 0
215 215 rad_mult = 3.6 * RADIANS
216 216 @config[:fields].each_index { |count|
217 217 value = @data[count]
218 218 percent = percent_scale * value
219 219
220 220 radians = prev_percent * rad_mult
221 221 x_start = radius+(Math.sin(radians) * radius)
222 222 y_start = radius-(Math.cos(radians) * radius)
223 223 radians = (prev_percent+percent) * rad_mult
224 224 x_end = radius+(Math.sin(radians) * radius)
225 x_end -= 0.00001 if @data.length == 1
225 226 y_end = radius-(Math.cos(radians) * radius)
226 227 path = "M#{radius},#{radius} L#{x_start},#{y_start} "+
227 228 "A#{radius},#{radius} "+
228 229 "0, #{percent >= 50 ? '1' : '0'},1, "+
229 230 "#{x_end} #{y_end} Z"
230 231
231 232
232 233 wedge = @foreground.add_element( "path", {
233 234 "d" => path,
234 235 "class" => "fill#{count+1}"
235 236 })
236 237
237 238 translate = nil
238 239 tx = 0
239 240 ty = 0
240 241 half_percent = prev_percent + percent / 2
241 242 radians = half_percent * rad_mult
242 243
243 244 if show_shadow
244 245 shadow = background.add_element( "path", {
245 246 "d" => path,
246 247 "filter" => "url(#dropshadow)",
247 248 "style" => "fill: #ccc; stroke: none;"
248 249 })
249 250 clear = midground.add_element( "path", {
250 251 "d" => path,
251 252 "style" => "fill: #fff; stroke: none;"
252 253 })
253 254 end
254 255
255 256 if expanded or (expand_greatest && value == max_value)
256 257 tx = (Math.sin(radians) * expand_gap)
257 258 ty = -(Math.cos(radians) * expand_gap)
258 259 translate = "translate( #{tx} #{ty} )"
259 260 wedge.attributes["transform"] = translate
260 clear.attributes["transform"] = translate
261 clear.attributes["transform"] = translate if clear
261 262 end
262 263
263 264 if show_shadow
264 265 shadow.attributes["transform"] =
265 266 "translate( #{tx+shadow_offset} #{ty+shadow_offset} )"
266 267 end
267 268
268 269 if show_data_labels and value != 0
269 270 label = ""
270 271 label += @config[:fields][count] if show_key_data_labels
271 272 label += " ["+value.to_s+"]" if show_actual_values
272 273 label += " "+percent.round.to_s+"%" if show_percent
273 274
274 275 msr = Math.sin(radians)
275 276 mcr = Math.cos(radians)
276 277 tx = radius + (msr * radius)
277 278 ty = radius -(mcr * radius)
278 279
279 280 if expanded or (expand_greatest && value == max_value)
280 281 tx += (msr * expand_gap)
281 282 ty -= (mcr * expand_gap)
282 283 end
283 284 @foreground.add_element( "text", {
284 285 "x" => tx.to_s,
285 286 "y" => ty.to_s,
286 287 "class" => "dataPointLabel",
287 288 "style" => "stroke: #fff; stroke-width: 2;"
288 289 }).text = label.to_s
289 290 @foreground.add_element( "text", {
290 291 "x" => tx.to_s,
291 292 "y" => ty.to_s,
292 293 "class" => "dataPointLabel",
293 294 }).text = label.to_s
294 295 end
295 296
296 297 prev_percent += percent
297 298 }
298 299 end
299 300
300 301
301 302 def round val, to
302 303 up = 10**to.to_f
303 304 (val * up).to_i / up
304 305 end
305 306
306 307
307 308 def get_css
308 309 return <<EOL
309 310 .dataPointLabel{
310 311 fill: #000000;
311 312 text-anchor:middle;
312 313 font-size: #{datapoint_font_size}px;
313 314 font-family: "Arial", sans-serif;
314 315 font-weight: normal;
315 316 }
316 317
317 318 /* key - MUST match fill styles */
318 319 .key1,.fill1{
319 320 fill: #ff0000;
320 321 fill-opacity: 0.7;
321 322 stroke: none;
322 323 stroke-width: 1px;
323 324 }
324 325 .key2,.fill2{
325 326 fill: #0000ff;
326 327 fill-opacity: 0.7;
327 328 stroke: none;
328 329 stroke-width: 1px;
329 330 }
330 331 .key3,.fill3{
331 332 fill-opacity: 0.7;
332 333 fill: #00ff00;
333 334 stroke: none;
334 335 stroke-width: 1px;
335 336 }
336 337 .key4,.fill4{
337 338 fill-opacity: 0.7;
338 339 fill: #ffcc00;
339 340 stroke: none;
340 341 stroke-width: 1px;
341 342 }
342 343 .key5,.fill5{
343 344 fill-opacity: 0.7;
344 345 fill: #00ccff;
345 346 stroke: none;
346 347 stroke-width: 1px;
347 348 }
348 349 .key6,.fill6{
349 350 fill-opacity: 0.7;
350 351 fill: #ff00ff;
351 352 stroke: none;
352 353 stroke-width: 1px;
353 354 }
354 355 .key7,.fill7{
355 356 fill-opacity: 0.7;
356 357 fill: #00ff99;
357 358 stroke: none;
358 359 stroke-width: 1px;
359 360 }
360 361 .key8,.fill8{
361 362 fill-opacity: 0.7;
362 363 fill: #ffff00;
363 364 stroke: none;
364 365 stroke-width: 1px;
365 366 }
366 367 .key9,.fill9{
367 368 fill-opacity: 0.7;
368 369 fill: #cc6666;
369 370 stroke: none;
370 371 stroke-width: 1px;
371 372 }
372 373 .key10,.fill10{
373 374 fill-opacity: 0.7;
374 375 fill: #663399;
375 376 stroke: none;
376 377 stroke-width: 1px;
377 378 }
378 379 .key11,.fill11{
379 380 fill-opacity: 0.7;
380 381 fill: #339900;
381 382 stroke: none;
382 383 stroke-width: 1px;
383 384 }
384 385 .key12,.fill12{
385 386 fill-opacity: 0.7;
386 387 fill: #9966FF;
387 388 stroke: none;
388 389 stroke-width: 1px;
389 390 }
390 391 EOL
391 392 end
392 393 end
393 394 end
394 395 end
@@ -1,494 +1,500
1 1 require 'SVG/Graph/Graph'
2 2
3 3 module SVG
4 4 module Graph
5 5 # === For creating SVG plots of scalar data
6 6 #
7 7 # = Synopsis
8 8 #
9 9 # require 'SVG/Graph/Plot'
10 10 #
11 11 # # Data sets are x,y pairs
12 12 # # Note that multiple data sets can differ in length, and that the
13 13 # # data in the datasets needn't be in order; they will be ordered
14 14 # # by the plot along the X-axis.
15 15 # projection = [
16 16 # 6, 11, 0, 5, 18, 7, 1, 11, 13, 9, 1, 2, 19, 0, 3, 13,
17 17 # 7, 9
18 18 # ]
19 19 # actual = [
20 20 # 0, 18, 8, 15, 9, 4, 18, 14, 10, 2, 11, 6, 14, 12,
21 21 # 15, 6, 4, 17, 2, 12
22 22 # ]
23 23 #
24 24 # graph = SVG::Graph::Plot.new({
25 25 # :height => 500,
26 26 # :width => 300,
27 27 # :key => true,
28 28 # :scale_x_integers => true,
29 29 # :scale_y_integerrs => true,
30 30 # })
31 31 #
32 32 # graph.add_data({
33 33 # :data => projection
34 34 # :title => 'Projected',
35 35 # })
36 36 #
37 37 # graph.add_data({
38 38 # :data => actual,
39 39 # :title => 'Actual',
40 40 # })
41 41 #
42 42 # print graph.burn()
43 43 #
44 44 # = Description
45 45 #
46 46 # Produces a graph of scalar data.
47 47 #
48 48 # This object aims to allow you to easily create high quality
49 49 # SVG[http://www.w3c.org/tr/svg] scalar plots. You can either use the
50 50 # default style sheet or supply your own. Either way there are many options
51 51 # which can be configured to give you control over how the graph is
52 52 # generated - with or without a key, data elements at each point, title,
53 53 # subtitle etc.
54 54 #
55 55 # = Examples
56 56 #
57 57 # http://www.germane-software/repositories/public/SVG/test/plot.rb
58 58 #
59 59 # = Notes
60 60 #
61 61 # The default stylesheet handles upto 10 data sets, if you
62 62 # use more you must create your own stylesheet and add the
63 63 # additional settings for the extra data sets. You will know
64 64 # if you go over 10 data sets as they will have no style and
65 65 # be in black.
66 66 #
67 67 # Unlike the other types of charts, data sets must contain x,y pairs:
68 68 #
69 69 # [ 1, 2 ] # A data set with 1 point: (1,2)
70 70 # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)
71 71 #
72 72 # = See also
73 73 #
74 74 # * SVG::Graph::Graph
75 75 # * SVG::Graph::BarHorizontal
76 76 # * SVG::Graph::Bar
77 77 # * SVG::Graph::Line
78 78 # * SVG::Graph::Pie
79 79 # * SVG::Graph::TimeSeries
80 80 #
81 81 # == Author
82 82 #
83 83 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
84 84 #
85 85 # Copyright 2004 Sean E. Russell
86 86 # This software is available under the Ruby license[LICENSE.txt]
87 87 #
88 88 class Plot < Graph
89 89
90 90 # In addition to the defaults set by Graph::initialize, sets
91 # [show_data_values] true
91 92 # [show_data_points] true
92 93 # [area_fill] false
93 94 # [stacked] false
94 95 def set_defaults
95 96 init_with(
97 :show_data_values => true,
96 98 :show_data_points => true,
97 99 :area_fill => false,
98 100 :stacked => false
99 101 )
100 102 self.top_align = self.right_align = self.top_font = self.right_font = 1
101 103 end
102 104
103 105 # Determines the scaling for the X axis divisions.
104 106 #
105 107 # graph.scale_x_divisions = 2
106 108 #
107 109 # would cause the graph to attempt to generate labels stepped by 2; EG:
108 110 # 0,2,4,6,8...
109 111 attr_accessor :scale_x_divisions
110 112 # Determines the scaling for the Y axis divisions.
111 113 #
112 114 # graph.scale_y_divisions = 0.5
113 115 #
114 116 # would cause the graph to attempt to generate labels stepped by 0.5; EG:
115 117 # 0, 0.5, 1, 1.5, 2, ...
116 118 attr_accessor :scale_y_divisions
117 119 # Make the X axis labels integers
118 120 attr_accessor :scale_x_integers
119 121 # Make the Y axis labels integers
120 122 attr_accessor :scale_y_integers
121 123 # Fill the area under the line
122 124 attr_accessor :area_fill
123 125 # Show a small circle on the graph where the line
124 126 # goes from one point to the next.
125 127 attr_accessor :show_data_points
126 128 # Set the minimum value of the X axis
127 129 attr_accessor :min_x_value
128 130 # Set the minimum value of the Y axis
129 131 attr_accessor :min_y_value
130 132
131 133
132 134 # Adds data to the plot. The data must be in X,Y pairs; EG
133 135 # [ 1, 2 ] # A data set with 1 point: (1,2)
134 136 # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)
135 137 def add_data data
136 138 @data = [] unless @data
137 139
138 140 raise "No data provided by #{conf.inspect}" unless data[:data] and
139 141 data[:data].kind_of? Array
140 142 raise "Data supplied must be x,y pairs! "+
141 143 "The data provided contained an odd set of "+
142 144 "data points" unless data[:data].length % 2 == 0
143 145 return if data[:data].length == 0
144 146
145 147 x = []
146 148 y = []
147 149 data[:data].each_index {|i|
148 150 (i%2 == 0 ? x : y) << data[:data][i]
149 151 }
150 152 sort( x, y )
151 153 data[:data] = [x,y]
152 154 @data << data
153 155 end
154 156
155 157
156 158 protected
157 159
158 160 def keys
159 161 @data.collect{ |x| x[:title] }
160 162 end
161 163
162 164 def calculate_left_margin
163 165 super
164 166 label_left = get_x_labels[0].to_s.length / 2 * font_size * 0.6
165 167 @border_left = label_left if label_left > @border_left
166 168 end
167 169
168 170 def calculate_right_margin
169 171 super
170 172 label_right = get_x_labels[-1].to_s.length / 2 * font_size * 0.6
171 173 @border_right = label_right if label_right > @border_right
172 174 end
173 175
174 176
175 177 X = 0
176 178 Y = 1
177 179 def x_range
178 180 max_value = @data.collect{|x| x[:data][X][-1] }.max
179 181 min_value = @data.collect{|x| x[:data][X][0] }.min
180 182 min_value = min_value<min_x_value ? min_value : min_x_value if min_x_value
181 183
182 184 range = max_value - min_value
183 185 right_pad = range == 0 ? 10 : range / 20.0
184 186 scale_range = (max_value + right_pad) - min_value
185 187
186 188 scale_division = scale_x_divisions || (scale_range / 10.0)
187 189
188 190 if scale_x_integers
189 191 scale_division = scale_division < 1 ? 1 : scale_division.round
190 192 end
191 193
192 194 [min_value, max_value, scale_division]
193 195 end
194 196
195 197 def get_x_values
196 198 min_value, max_value, scale_division = x_range
197 199 rv = []
198 200 min_value.step( max_value, scale_division ) {|v| rv << v}
199 201 return rv
200 202 end
201 203 alias :get_x_labels :get_x_values
202 204
203 205 def field_width
204 206 values = get_x_values
205 207 max = @data.collect{|x| x[:data][X][-1]}.max
206 208 dx = (max - values[-1]).to_f / (values[-1] - values[-2])
207 209 (@graph_width.to_f - font_size*2*right_font) /
208 210 (values.length + dx - right_align)
209 211 end
210 212
211 213
212 214 def y_range
213 215 max_value = @data.collect{|x| x[:data][Y].max }.max
214 216 min_value = @data.collect{|x| x[:data][Y].min }.min
215 217 min_value = min_value<min_y_value ? min_value : min_y_value if min_y_value
216 218
217 219 range = max_value - min_value
218 220 top_pad = range == 0 ? 10 : range / 20.0
219 221 scale_range = (max_value + top_pad) - min_value
220 222
221 223 scale_division = scale_y_divisions || (scale_range / 10.0)
222 224
223 225 if scale_y_integers
224 226 scale_division = scale_division < 1 ? 1 : scale_division.round
225 227 end
226 228
227 229 return [min_value, max_value, scale_division]
228 230 end
229 231
230 232 def get_y_values
231 233 min_value, max_value, scale_division = y_range
232 234 rv = []
233 235 min_value.step( max_value, scale_division ) {|v| rv << v}
234 236 return rv
235 237 end
236 238 alias :get_y_labels :get_y_values
237 239
238 240 def field_height
239 241 values = get_y_values
240 242 max = @data.collect{|x| x[:data][Y].max }.max
243 if values.length == 1
244 dx = values[-1]
245 else
241 246 dx = (max - values[-1]).to_f / (values[-1] - values[-2])
247 end
242 248 (@graph_height.to_f - font_size*2*top_font) /
243 249 (values.length + dx - top_align)
244 250 end
245 251
246 252 def draw_data
247 253 line = 1
248 254
249 255 x_min, x_max, x_div = x_range
250 256 y_min, y_max, y_div = y_range
251 257 x_step = (@graph_width.to_f - font_size*2) / (x_max-x_min)
252 258 y_step = (@graph_height.to_f - font_size*2) / (y_max-y_min)
253 259
254 260 for data in @data
255 261 x_points = data[:data][X]
256 262 y_points = data[:data][Y]
257 263
258 264 lpath = "L"
259 265 x_start = 0
260 266 y_start = 0
261 267 x_points.each_index { |idx|
262 268 x = (x_points[idx] - x_min) * x_step
263 269 y = @graph_height - (y_points[idx] - y_min) * y_step
264 270 x_start, y_start = x,y if idx == 0
265 271 lpath << "#{x} #{y} "
266 272 }
267 273
268 274 if area_fill
269 275 @graph.add_element( "path", {
270 276 "d" => "M#{x_start} #@graph_height #{lpath} V#@graph_height Z",
271 277 "class" => "fill#{line}"
272 278 })
273 279 end
274 280
275 281 @graph.add_element( "path", {
276 282 "d" => "M#{x_start} #{y_start} #{lpath}",
277 283 "class" => "line#{line}"
278 284 })
279 285
280 286 if show_data_points || show_data_values
281 287 x_points.each_index { |idx|
282 288 x = (x_points[idx] - x_min) * x_step
283 289 y = @graph_height - (y_points[idx] - y_min) * y_step
284 290 if show_data_points
285 291 @graph.add_element( "circle", {
286 292 "cx" => x.to_s,
287 293 "cy" => y.to_s,
288 294 "r" => "2.5",
289 295 "class" => "dataPoint#{line}"
290 296 })
291 297 add_popup(x, y, format( x_points[idx], y_points[idx] )) if add_popups
292 298 end
293 make_datapoint_text( x, y-6, y_points[idx] )
299 make_datapoint_text( x, y-6, y_points[idx] ) if show_data_values
294 300 }
295 301 end
296 302 line += 1
297 303 end
298 304 end
299 305
300 306 def format x, y
301 307 "(#{(x * 100).to_i / 100}, #{(y * 100).to_i / 100})"
302 308 end
303 309
304 310 def get_css
305 311 return <<EOL
306 312 /* default line styles */
307 313 .line1{
308 314 fill: none;
309 315 stroke: #ff0000;
310 316 stroke-width: 1px;
311 317 }
312 318 .line2{
313 319 fill: none;
314 320 stroke: #0000ff;
315 321 stroke-width: 1px;
316 322 }
317 323 .line3{
318 324 fill: none;
319 325 stroke: #00ff00;
320 326 stroke-width: 1px;
321 327 }
322 328 .line4{
323 329 fill: none;
324 330 stroke: #ffcc00;
325 331 stroke-width: 1px;
326 332 }
327 333 .line5{
328 334 fill: none;
329 335 stroke: #00ccff;
330 336 stroke-width: 1px;
331 337 }
332 338 .line6{
333 339 fill: none;
334 340 stroke: #ff00ff;
335 341 stroke-width: 1px;
336 342 }
337 343 .line7{
338 344 fill: none;
339 345 stroke: #00ffff;
340 346 stroke-width: 1px;
341 347 }
342 348 .line8{
343 349 fill: none;
344 350 stroke: #ffff00;
345 351 stroke-width: 1px;
346 352 }
347 353 .line9{
348 354 fill: none;
349 355 stroke: #ccc6666;
350 356 stroke-width: 1px;
351 357 }
352 358 .line10{
353 359 fill: none;
354 360 stroke: #663399;
355 361 stroke-width: 1px;
356 362 }
357 363 .line11{
358 364 fill: none;
359 365 stroke: #339900;
360 366 stroke-width: 1px;
361 367 }
362 368 .line12{
363 369 fill: none;
364 370 stroke: #9966FF;
365 371 stroke-width: 1px;
366 372 }
367 373 /* default fill styles */
368 374 .fill1{
369 375 fill: #cc0000;
370 376 fill-opacity: 0.2;
371 377 stroke: none;
372 378 }
373 379 .fill2{
374 380 fill: #0000cc;
375 381 fill-opacity: 0.2;
376 382 stroke: none;
377 383 }
378 384 .fill3{
379 385 fill: #00cc00;
380 386 fill-opacity: 0.2;
381 387 stroke: none;
382 388 }
383 389 .fill4{
384 390 fill: #ffcc00;
385 391 fill-opacity: 0.2;
386 392 stroke: none;
387 393 }
388 394 .fill5{
389 395 fill: #00ccff;
390 396 fill-opacity: 0.2;
391 397 stroke: none;
392 398 }
393 399 .fill6{
394 400 fill: #ff00ff;
395 401 fill-opacity: 0.2;
396 402 stroke: none;
397 403 }
398 404 .fill7{
399 405 fill: #00ffff;
400 406 fill-opacity: 0.2;
401 407 stroke: none;
402 408 }
403 409 .fill8{
404 410 fill: #ffff00;
405 411 fill-opacity: 0.2;
406 412 stroke: none;
407 413 }
408 414 .fill9{
409 415 fill: #cc6666;
410 416 fill-opacity: 0.2;
411 417 stroke: none;
412 418 }
413 419 .fill10{
414 420 fill: #663399;
415 421 fill-opacity: 0.2;
416 422 stroke: none;
417 423 }
418 424 .fill11{
419 425 fill: #339900;
420 426 fill-opacity: 0.2;
421 427 stroke: none;
422 428 }
423 429 .fill12{
424 430 fill: #9966FF;
425 431 fill-opacity: 0.2;
426 432 stroke: none;
427 433 }
428 434 /* default line styles */
429 435 .key1,.dataPoint1{
430 436 fill: #ff0000;
431 437 stroke: none;
432 438 stroke-width: 1px;
433 439 }
434 440 .key2,.dataPoint2{
435 441 fill: #0000ff;
436 442 stroke: none;
437 443 stroke-width: 1px;
438 444 }
439 445 .key3,.dataPoint3{
440 446 fill: #00ff00;
441 447 stroke: none;
442 448 stroke-width: 1px;
443 449 }
444 450 .key4,.dataPoint4{
445 451 fill: #ffcc00;
446 452 stroke: none;
447 453 stroke-width: 1px;
448 454 }
449 455 .key5,.dataPoint5{
450 456 fill: #00ccff;
451 457 stroke: none;
452 458 stroke-width: 1px;
453 459 }
454 460 .key6,.dataPoint6{
455 461 fill: #ff00ff;
456 462 stroke: none;
457 463 stroke-width: 1px;
458 464 }
459 465 .key7,.dataPoint7{
460 466 fill: #00ffff;
461 467 stroke: none;
462 468 stroke-width: 1px;
463 469 }
464 470 .key8,.dataPoint8{
465 471 fill: #ffff00;
466 472 stroke: none;
467 473 stroke-width: 1px;
468 474 }
469 475 .key9,.dataPoint9{
470 476 fill: #cc6666;
471 477 stroke: none;
472 478 stroke-width: 1px;
473 479 }
474 480 .key10,.dataPoint10{
475 481 fill: #663399;
476 482 stroke: none;
477 483 stroke-width: 1px;
478 484 }
479 485 .key11,.dataPoint11{
480 486 fill: #339900;
481 487 stroke: none;
482 488 stroke-width: 1px;
483 489 }
484 490 .key12,.dataPoint12{
485 491 fill: #9966FF;
486 492 stroke: none;
487 493 stroke-width: 1px;
488 494 }
489 495 EOL
490 496 end
491 497
492 498 end
493 499 end
494 500 end
@@ -1,241 +1,241
1 1 require 'SVG/Graph/Plot'
2 2 require 'parsedate'
3 3
4 4 module SVG
5 5 module Graph
6 6 # === For creating SVG plots of scalar temporal data
7 7 #
8 8 # = Synopsis
9 9 #
10 10 # require 'SVG/Graph/TimeSeriess'
11 11 #
12 12 # # Data sets are x,y pairs
13 13 # data1 = ["6/17/72", 11, "1/11/72", 7, "4/13/04 17:31", 11,
14 14 # "9/11/01", 9, "9/1/85", 2, "9/1/88", 1, "1/15/95", 13]
15 15 # data2 = ["8/1/73", 18, "3/1/77", 15, "10/1/98", 4,
16 16 # "5/1/02", 14, "3/1/95", 6, "8/1/91", 12, "12/1/87", 6,
17 17 # "5/1/84", 17, "10/1/80", 12]
18 18 #
19 19 # graph = SVG::Graph::TimeSeries.new( {
20 20 # :width => 640,
21 21 # :height => 480,
22 22 # :graph_title => title,
23 23 # :show_graph_title => true,
24 24 # :no_css => true,
25 25 # :key => true,
26 26 # :scale_x_integers => true,
27 27 # :scale_y_integers => true,
28 28 # :min_x_value => 0,
29 29 # :min_y_value => 0,
30 30 # :show_data_labels => true,
31 31 # :show_x_guidelines => true,
32 32 # :show_x_title => true,
33 33 # :x_title => "Time",
34 34 # :show_y_title => true,
35 35 # :y_title => "Ice Cream Cones",
36 36 # :y_title_text_direction => :bt,
37 37 # :stagger_x_labels => true,
38 38 # :x_label_format => "%m/%d/%y",
39 39 # })
40 40 #
41 41 # graph.add_data({
42 42 # :data => projection
43 43 # :title => 'Projected',
44 44 # })
45 45 #
46 46 # graph.add_data({
47 47 # :data => actual,
48 48 # :title => 'Actual',
49 49 # })
50 50 #
51 51 # print graph.burn()
52 52 #
53 53 # = Description
54 54 #
55 55 # Produces a graph of temporal scalar data.
56 56 #
57 57 # = Examples
58 58 #
59 59 # http://www.germane-software/repositories/public/SVG/test/timeseries.rb
60 60 #
61 61 # = Notes
62 62 #
63 63 # The default stylesheet handles upto 10 data sets, if you
64 64 # use more you must create your own stylesheet and add the
65 65 # additional settings for the extra data sets. You will know
66 66 # if you go over 10 data sets as they will have no style and
67 67 # be in black.
68 68 #
69 69 # Unlike the other types of charts, data sets must contain x,y pairs:
70 70 #
71 71 # [ "12:30", 2 ] # A data set with 1 point: ("12:30",2)
72 72 # [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and
73 73 # # ("14:20",6)
74 74 #
75 75 # Note that multiple data sets within the same chart can differ in length,
76 76 # and that the data in the datasets needn't be in order; they will be ordered
77 77 # by the plot along the X-axis.
78 78 #
79 79 # The dates must be parseable by ParseDate, but otherwise can be
80 80 # any order of magnitude (seconds within the hour, or years)
81 81 #
82 82 # = See also
83 83 #
84 84 # * SVG::Graph::Graph
85 85 # * SVG::Graph::BarHorizontal
86 86 # * SVG::Graph::Bar
87 87 # * SVG::Graph::Line
88 88 # * SVG::Graph::Pie
89 89 # * SVG::Graph::Plot
90 90 #
91 91 # == Author
92 92 #
93 93 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
94 94 #
95 95 # Copyright 2004 Sean E. Russell
96 96 # This software is available under the Ruby license[LICENSE.txt]
97 97 #
98 98 class TimeSeries < Plot
99 99 # In addition to the defaults set by Graph::initialize and
100 100 # Plot::set_defaults, sets:
101 101 # [x_label_format] '%Y-%m-%d %H:%M:%S'
102 102 # [popup_format] '%Y-%m-%d %H:%M:%S'
103 103 def set_defaults
104 104 super
105 105 init_with(
106 106 #:max_time_span => '',
107 107 :x_label_format => '%Y-%m-%d %H:%M:%S',
108 108 :popup_format => '%Y-%m-%d %H:%M:%S'
109 109 )
110 110 end
111 111
112 112 # The format string use do format the X axis labels.
113 113 # See Time::strformat
114 114 attr_accessor :x_label_format
115 115 # Use this to set the spacing between dates on the axis. The value
116 116 # must be of the form
117 117 # "\d+ ?(days|weeks|months|years|hours|minutes|seconds)?"
118 118 #
119 119 # EG:
120 120 #
121 121 # graph.timescale_divisions = "2 weeks"
122 122 #
123 123 # will cause the chart to try to divide the X axis up into segments of
124 124 # two week periods.
125 125 attr_accessor :timescale_divisions
126 126 # The formatting used for the popups. See x_label_format
127 127 attr_accessor :popup_format
128 128
129 129 # Add data to the plot.
130 130 #
131 131 # d1 = [ "12:30", 2 ] # A data set with 1 point: ("12:30",2)
132 132 # d2 = [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and
133 133 # # ("14:20",6)
134 134 # graph.add_data(
135 135 # :data => d1,
136 136 # :title => 'One'
137 137 # )
138 138 # graph.add_data(
139 139 # :data => d2,
140 140 # :title => 'Two'
141 141 # )
142 142 #
143 143 # Note that the data must be in time,value pairs, and that the date format
144 144 # may be any date that is parseable by ParseDate.
145 145 def add_data data
146 146 @data = [] unless @data
147 147
148 raise "No data provided by #{conf.inspect}" unless data[:data] and
148 raise "No data provided by #{@data.inspect}" unless data[:data] and
149 149 data[:data].kind_of? Array
150 150 raise "Data supplied must be x,y pairs! "+
151 151 "The data provided contained an odd set of "+
152 152 "data points" unless data[:data].length % 2 == 0
153 153 return if data[:data].length == 0
154 154
155 155
156 156 x = []
157 157 y = []
158 158 data[:data].each_index {|i|
159 159 if i%2 == 0
160 160 arr = ParseDate.parsedate( data[:data][i] )
161 161 t = Time.local( *arr[0,6].compact )
162 162 x << t.to_i
163 163 else
164 164 y << data[:data][i]
165 165 end
166 166 }
167 167 sort( x, y )
168 168 data[:data] = [x,y]
169 169 @data << data
170 170 end
171 171
172 172
173 173 protected
174 174
175 175 def min_x_value=(value)
176 176 arr = ParseDate.parsedate( value )
177 177 @min_x_value = Time.local( *arr[0,6].compact ).to_i
178 178 end
179 179
180 180
181 181 def format x, y
182 182 Time.at( x ).strftime( popup_format )
183 183 end
184 184
185 185 def get_x_labels
186 186 get_x_values.collect { |v| Time.at(v).strftime( x_label_format ) }
187 187 end
188 188
189 189 private
190 190 def get_x_values
191 191 rv = []
192 192 min, max, scale_division = x_range
193 193 if timescale_divisions
194 timescale_divisions =~ /(\d+) ?(days|weeks|months|years|hours|minutes|seconds)?/
195 division_units = $2 ? $2 : "days"
194 timescale_divisions =~ /(\d+) ?(day|week|month|year|hour|minute|second)?/
195 division_units = $2 ? $2 : "day"
196 196 amount = $1.to_i
197 197 if amount
198 198 step = nil
199 199 case division_units
200 when "months"
200 when "month"
201 201 cur = min
202 202 while cur < max
203 203 rv << cur
204 204 arr = Time.at( cur ).to_a
205 205 arr[4] += amount
206 206 if arr[4] > 12
207 207 arr[5] += (arr[4] / 12).to_i
208 208 arr[4] = (arr[4] % 12)
209 209 end
210 210 cur = Time.local(*arr).to_i
211 211 end
212 when "years"
212 when "year"
213 213 cur = min
214 214 while cur < max
215 215 rv << cur
216 216 arr = Time.at( cur ).to_a
217 217 arr[5] += amount
218 218 cur = Time.local(*arr).to_i
219 219 end
220 when "weeks"
220 when "week"
221 221 step = 7 * 24 * 60 * 60 * amount
222 when "days"
222 when "day"
223 223 step = 24 * 60 * 60 * amount
224 when "hours"
224 when "hour"
225 225 step = 60 * 60 * amount
226 when "minutes"
226 when "minute"
227 227 step = 60 * amount
228 when "seconds"
228 when "second"
229 229 step = amount
230 230 end
231 231 min.step( max, step ) {|v| rv << v} if step
232 232
233 233 return rv
234 234 end
235 235 end
236 236 min.step( max, scale_division ) {|v| rv << v}
237 237 return rv
238 238 end
239 239 end
240 240 end
241 241 end
General Comments 0
You need to be logged in to leave comments. Login now