svg-graph19 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,148 @@
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
@@ -0,0 +1,139 @@
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
@@ -0,0 +1,149 @@
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
@@ -0,0 +1,983 @@
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
+ :y_label_font_size =>12,
145
+ :x_title_font_size =>14,
146
+ :y_label_font_size =>12,
147
+ :y_title_font_size =>14,
148
+ :key_font_size =>10,
149
+
150
+ :no_css =>false,
151
+ :add_popups =>false,
152
+ })
153
+
154
+ set_defaults if methods.include? "set_defaults"
155
+
156
+ init_with config
157
+ puts "ylfs: #{self.x_label_font_size}"
158
+ puts "ylfs: #{@x_label_font_size}"
159
+ end
160
+
161
+
162
+ # This method allows you do add data to the graph object.
163
+ # It can be called several times to add more data sets in.
164
+ #
165
+ # data_sales_02 = [12, 45, 21];
166
+ #
167
+ # graph.add_data({
168
+ # :data => data_sales_02,
169
+ # :title => 'Sales 2002'
170
+ # })
171
+ def add_data conf
172
+ @data = [] unless defined? @data
173
+
174
+ if conf[:data] and conf[:data].kind_of? Array
175
+ @data << conf
176
+ else
177
+ raise "No data provided by #{conf.inspect}"
178
+ end
179
+ end
180
+
181
+
182
+ # This method removes all data from the object so that you can
183
+ # reuse it to create a new graph but with the same config options.
184
+ #
185
+ # graph.clear_data
186
+ def clear_data
187
+ @data = []
188
+ end
189
+
190
+
191
+ # This method processes the template with the data and
192
+ # config which has been set and returns the resulting SVG.
193
+ #
194
+ # This method will croak unless at least one data set has
195
+ # been added to the graph object.
196
+ #
197
+ # print graph.burn
198
+ def burn
199
+ raise "No data available" unless @data.size > 0
200
+
201
+ calculations if methods.include? 'calculations'
202
+
203
+ start_svg
204
+ calculate_graph_dimensions
205
+ @foreground = Element.new( "g" )
206
+ draw_graph
207
+ draw_titles
208
+ draw_legend
209
+ draw_data
210
+ @graph.add_element( @foreground )
211
+ style
212
+
213
+ data = ""
214
+ @doc.write( data, 0 )
215
+
216
+ if @config[:compress]
217
+ if @@__have_zlib
218
+ inp, out = IO.pipe
219
+ gz = Zlib::GzipWriter.new( out )
220
+ gz.write data
221
+ gz.close
222
+ data = inp.read
223
+ else
224
+ data << "<!-- Ruby Zlib not available for SVGZ -->";
225
+ end
226
+ end
227
+
228
+ return data
229
+ end
230
+
231
+
232
+ # Set the height of the graph box, this is the total height
233
+ # of the SVG box created - not the graph it self which auto
234
+ # scales to fix the space.
235
+ attr_accessor :height
236
+ # Set the width of the graph box, this is the total width
237
+ # of the SVG box created - not the graph it self which auto
238
+ # scales to fix the space.
239
+ attr_accessor :width
240
+ # Set the path to an external stylesheet, set to '' if
241
+ # you want to revert back to using the defaut internal version.
242
+ #
243
+ # To create an external stylesheet create a graph using the
244
+ # default internal version and copy the stylesheet section to
245
+ # an external file and edit from there.
246
+ attr_accessor :style_sheet
247
+ # (Bool) Show the value of each element of data on the graph
248
+ attr_accessor :show_data_values
249
+ # The point at which the Y axis starts, defaults to '0',
250
+ # if set to nil it will default to the minimum data value.
251
+ attr_accessor :min_scale_value
252
+ # Whether to show labels on the X axis or not, defaults
253
+ # to true, set to false if you want to turn them off.
254
+ attr_accessor :show_x_labels
255
+ # This puts the X labels at alternative levels so if they
256
+ # are long field names they will not overlap so easily.
257
+ # Default it false, to turn on set to true.
258
+ attr_accessor :stagger_x_labels
259
+ # This puts the Y labels at alternative levels so if they
260
+ # are long field names they will not overlap so easily.
261
+ # Default it false, to turn on set to true.
262
+ attr_accessor :stagger_y_labels
263
+ # This turns the X axis labels by 90 degrees.
264
+ # Default it false, to turn on set to true.
265
+ attr_accessor :rotate_x_labels
266
+ # This turns the Y axis labels by 90 degrees.
267
+ # Default it false, to turn on set to true.
268
+ attr_accessor :rotate_y_labels
269
+ # How many "steps" to use between displayed X axis labels,
270
+ # a step of one means display every label, a step of two results
271
+ # in every other label being displayed (label <gap> label <gap> label),
272
+ # a step of three results in every third label being displayed
273
+ # (label <gap> <gap> label <gap> <gap> label) and so on.
274
+ attr_accessor :step_x_labels
275
+ # Whether to (when taking "steps" between X axis labels) step from
276
+ # the first label (i.e. always include the first label) or step from
277
+ # the X axis origin (i.e. start with a gap if step_x_labels is greater
278
+ # than one).
279
+ attr_accessor :step_include_first_x_label
280
+ # Whether to show labels on the Y axis or not, defaults
281
+ # to true, set to false if you want to turn them off.
282
+ attr_accessor :show_y_labels
283
+ # Ensures only whole numbers are used as the scale divisions.
284
+ # Default it false, to turn on set to true. This has no effect if
285
+ # scale divisions are less than 1.
286
+ attr_accessor :scale_integers
287
+ # This defines the gap between markers on the Y axis,
288
+ # default is a 10th of the max_value, e.g. you will have
289
+ # 10 markers on the Y axis. NOTE: do not set this too
290
+ # low - you are limited to 999 markers, after that the
291
+ # graph won't generate.
292
+ attr_accessor :scale_divisions
293
+ # Whether to show the title under the X axis labels,
294
+ # default is false, set to true to show.
295
+ attr_accessor :show_x_title
296
+ # What the title under X axis should be, e.g. 'Months'.
297
+ attr_accessor :x_title
298
+ # Whether to show the title under the Y axis labels,
299
+ # default is false, set to true to show.
300
+ attr_accessor :show_y_title
301
+ # Aligns writing mode for Y axis label.
302
+ # Defaults to :bt (Bottom to Top).
303
+ # Change to :tb (Top to Bottom) to reverse.
304
+ attr_accessor :y_title_text_direction
305
+ # What the title under Y axis should be, e.g. 'Sales in thousands'.
306
+ attr_accessor :y_title
307
+ # Whether to show a title on the graph, defaults
308
+ # to false, set to true to show.
309
+ attr_accessor :show_graph_title
310
+ # What the title on the graph should be.
311
+ attr_accessor :graph_title
312
+ # Whether to show a subtitle on the graph, defaults
313
+ # to false, set to true to show.
314
+ attr_accessor :show_graph_subtitle
315
+ # What the subtitle on the graph should be.
316
+ attr_accessor :graph_subtitle
317
+ # Whether to show a key, defaults to false, set to
318
+ # true if you want to show it.
319
+ attr_accessor :key
320
+ # Where the key should be positioned, defaults to
321
+ # :right, set to :bottom if you want to move it.
322
+ attr_accessor :key_position
323
+ # Set the font size (in points) of the data point labels
324
+ attr_accessor :font_size
325
+ # Set the font size of the X axis labels
326
+ attr_accessor :x_label_font_size
327
+ # Set the font size of the X axis title
328
+ attr_accessor :x_title_font_size
329
+ # Set the font size of the Y axis labels
330
+ attr_accessor :y_label_font_size
331
+ # Set the font size of the Y axis title
332
+ attr_accessor :y_title_font_size
333
+ # Set the title font size
334
+ attr_accessor :title_font_size
335
+ # Set the subtitle font size
336
+ attr_accessor :subtitle_font_size
337
+ # Set the key font size
338
+ attr_accessor :key_font_size
339
+ # Show guidelines for the X axis
340
+ attr_accessor :show_x_guidelines
341
+ # Show guidelines for the Y axis
342
+ attr_accessor :show_y_guidelines
343
+ # Do not use CSS if set to true. Many SVG viewers do not support CSS, but
344
+ # not using CSS can result in larger SVGs as well as making it impossible to
345
+ # change colors after the chart is generated. Defaults to false.
346
+ attr_accessor :no_css
347
+ # Add popups for the data points on some graphs
348
+ attr_accessor :add_popups
349
+
350
+
351
+ protected
352
+
353
+ def sort( *arrys )
354
+ sort_multiple( arrys )
355
+ end
356
+
357
+ # Overwrite configuration options with supplied options. Used
358
+ # by subclasses.
359
+ def init_with config
360
+ config.each { |key, value|
361
+ if self.respond_to? key.to_s
362
+ self.send( key.to_s+"=", value )
363
+ end
364
+ }
365
+ end
366
+
367
+ attr_accessor :top_align, :top_font, :right_align, :right_font
368
+
369
+ KEY_BOX_SIZE = 12
370
+
371
+ # Override this (and call super) to change the margin to the left
372
+ # of the plot area. Results in @border_left being set.
373
+ def calculate_left_margin
374
+ @border_left = 7
375
+ # Check for Y labels
376
+ max_y_label_height_px = @rotate_y_labels ?
377
+ @y_label_font_size :
378
+ get_y_labels.max{|a,b|
379
+ a.to_s.length<=>b.to_s.length
380
+ }.to_s.length * @y_label_font_size * 0.6
381
+ @border_left += max_y_label_height_px if @show_y_labels
382
+ @border_left += max_y_label_height_px + 10 if @stagger_y_labels
383
+ @border_left += y_title_font_size + 5 if @show_y_title
384
+ end
385
+
386
+
387
+ # Calculates the width of the widest Y label. This will be the
388
+ # character height if the Y labels are rotated
389
+ def max_y_label_width_px
390
+ return font_size if rotate_y_labels
391
+ end
392
+
393
+
394
+ # Override this (and call super) to change the margin to the right
395
+ # of the plot area. Results in @border_right being set.
396
+ def calculate_right_margin
397
+ @border_right = 7
398
+ if key and key_position == :right
399
+ val = keys.max { |a,b| a.length <=> b.length }
400
+ @border_right += val.length * key_font_size * 0.6
401
+ @border_right += KEY_BOX_SIZE
402
+ @border_right += 10 # Some padding around the box
403
+ end
404
+ end
405
+
406
+
407
+ # Override this (and call super) to change the margin to the top
408
+ # of the plot area. Results in @border_top being set.
409
+ def calculate_top_margin
410
+ @border_top = 5
411
+ @border_top += title_font_size if show_graph_title
412
+ @border_top += 5
413
+ @border_top += subtitle_font_size if show_graph_subtitle
414
+ end
415
+
416
+
417
+ # Adds pop-up point information to a graph.
418
+ def add_popup( x, y, label )
419
+ txt_width = label.length * font_size * 0.6 + 10
420
+ tx = (x+txt_width > width ? x-5 : x+5)
421
+ t = @foreground.add_element( "text", {
422
+ "x" => tx.to_s,
423
+ "y" => (y - font_size).to_s,
424
+ "visibility" => "hidden",
425
+ })
426
+ t.attributes["style"] = "fill: #000; "+
427
+ (x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;")
428
+ t.text = label.to_s
429
+ t.attributes["id"] = t.object_id.to_s
430
+
431
+ @foreground.add_element( "circle", {
432
+ "cx" => x.to_s,
433
+ "cy" => y.to_s,
434
+ "r" => "10",
435
+ "style" => "opacity: 0",
436
+ "onmouseover" =>
437
+ "document.getElementById(#{t.object_id}).setAttribute('visibility', 'visible' )",
438
+ "onmouseout" =>
439
+ "document.getElementById(#{t.object_id}).setAttribute('visibility', 'hidden' )",
440
+ })
441
+
442
+ end
443
+
444
+
445
+ # Override this (and call super) to change the margin to the bottom
446
+ # of the plot area. Results in @border_bottom being set.
447
+ def calculate_bottom_margin
448
+ @border_bottom = 7
449
+ if key and key_position == :bottom
450
+ @border_bottom += @data.size * (font_size + 5)
451
+ @border_bottom += 10
452
+ end
453
+ if show_x_labels
454
+ max_x_label_height_px = (not rotate_x_labels) ?
455
+ x_label_font_size :
456
+ get_x_labels.max{|a,b|
457
+ a.to_s.length<=>b.to_s.length
458
+ }.to_s.length * x_label_font_size * 0.6
459
+ @border_bottom += max_x_label_height_px
460
+ @border_bottom += max_x_label_height_px + 10 if stagger_x_labels
461
+ end
462
+ @border_bottom += x_title_font_size + 5 if show_x_title
463
+ end
464
+
465
+
466
+ # Draws the background, axis, and labels.
467
+ def draw_graph
468
+ @graph = @root.add_element( "g", {
469
+ "transform" => "translate( #@border_left #@border_top )"
470
+ })
471
+
472
+ # Background
473
+ @graph.add_element( "rect", {
474
+ "x" => "0",
475
+ "y" => "0",
476
+ "width" => @graph_width.to_s,
477
+ "height" => @graph_height.to_s,
478
+ "class" => "graphBackground"
479
+ })
480
+
481
+ # Axis
482
+ @graph.add_element( "path", {
483
+ "d" => "M 0 0 v#@graph_height",
484
+ "class" => "axis",
485
+ "id" => "xAxis"
486
+ })
487
+ @graph.add_element( "path", {
488
+ "d" => "M 0 #@graph_height h#@graph_width",
489
+ "class" => "axis",
490
+ "id" => "yAxis"
491
+ })
492
+
493
+ draw_x_labels
494
+ draw_y_labels
495
+ end
496
+
497
+
498
+ # Where in the X area the label is drawn
499
+ # Centered in the field, should be width/2. Start, 0.
500
+ def x_label_offset( width )
501
+ 0
502
+ end
503
+
504
+ def make_datapoint_text( x, y, value, style="" )
505
+ if show_data_values
506
+ @foreground.add_element( "text", {
507
+ "x" => x.to_s,
508
+ "y" => y.to_s,
509
+ "class" => "dataPointLabel",
510
+ "style" => "#{style} stroke: #fff; stroke-width: 2;"
511
+ }).text = value.to_s
512
+ text = @foreground.add_element( "text", {
513
+ "x" => x.to_s,
514
+ "y" => y.to_s,
515
+ "class" => "dataPointLabel"
516
+ })
517
+ text.text = value.to_s
518
+ text.attributes["style"] = style if style.length > 0
519
+ end
520
+ end
521
+
522
+
523
+ # Draws the X axis labels
524
+ def draw_x_labels
525
+ stagger = x_label_font_size + 5
526
+ if show_x_labels
527
+ label_width = field_width
528
+
529
+ count = 0
530
+ for label in get_x_labels
531
+ if step_include_first_x_label == true then
532
+ step = count % step_x_labels
533
+ else
534
+ step = (count + 1) % step_x_labels
535
+ end
536
+
537
+ if step == 0 then
538
+ text = @graph.add_element( "text" )
539
+ text.attributes["class"] = "xAxisLabels"
540
+ text.text = label.to_s
541
+
542
+ x = count * label_width + x_label_offset( label_width )
543
+ y = @graph_height + x_label_font_size + 3
544
+ t = 0 - (font_size / 2)
545
+
546
+ if stagger_x_labels and count % 2 == 1
547
+ y += stagger
548
+ @graph.add_element( "path", {
549
+ "d" => "M#{x} #@graph_height v#{stagger}",
550
+ "class" => "staggerGuideLine"
551
+ })
552
+ end
553
+
554
+ text.attributes["x"] = x.to_s
555
+ text.attributes["y"] = y.to_s
556
+ if rotate_x_labels
557
+ text.attributes["transform"] =
558
+ "rotate( 90 #{x} #{y-x_label_font_size} )"+
559
+ " translate( 0 -#{x_label_font_size/4} )"
560
+ text.attributes["style"] = "text-anchor: start"
561
+ else
562
+ text.attributes["style"] = "text-anchor: middle"
563
+ end
564
+ end
565
+
566
+ draw_x_guidelines( label_width, count ) if show_x_guidelines
567
+ count += 1
568
+ end
569
+ end
570
+ end
571
+
572
+
573
+ # Where in the Y area the label is drawn
574
+ # Centered in the field, should be width/2. Start, 0.
575
+ def y_label_offset( height )
576
+ 0
577
+ end
578
+
579
+
580
+ def field_width
581
+ (@graph_width.to_f - font_size*2*right_font) /
582
+ (get_x_labels.length - right_align)
583
+ end
584
+
585
+
586
+ def field_height
587
+ (@graph_height.to_f - font_size*2*top_font) /
588
+ (get_y_labels.length - top_align)
589
+ end
590
+
591
+
592
+ # Draws the Y axis labels
593
+ def draw_y_labels
594
+ stagger = y_label_font_size + 5
595
+ if show_y_labels
596
+ label_height = field_height
597
+
598
+ count = 0
599
+ y_offset = @graph_height + y_label_offset( label_height )
600
+ y_offset += font_size/1.2 unless rotate_y_labels
601
+ for label in get_y_labels
602
+ y = y_offset - (label_height * count)
603
+ x = rotate_y_labels ? 0 : -3
604
+
605
+ if stagger_y_labels and count % 2 == 1
606
+ x -= stagger
607
+ @graph.add_element( "path", {
608
+ "d" => "M#{x} #{y} h#{stagger}",
609
+ "class" => "staggerGuideLine"
610
+ })
611
+ end
612
+
613
+ text = @graph.add_element( "text", {
614
+ "x" => x.to_s,
615
+ "y" => y.to_s,
616
+ "class" => "yAxisLabels"
617
+ })
618
+ text.text = label.to_s
619
+ if rotate_y_labels
620
+ text.attributes["transform"] = "translate( -#{font_size} 0 ) "+
621
+ "rotate( 90 #{x} #{y} ) "
622
+ text.attributes["style"] = "text-anchor: middle"
623
+ else
624
+ text.attributes["y"] = (y - (y_label_font_size/2)).to_s
625
+ text.attributes["style"] = "text-anchor: end"
626
+ end
627
+ draw_y_guidelines( label_height, count ) if show_y_guidelines
628
+ count += 1
629
+ end
630
+ end
631
+ end
632
+
633
+
634
+ # Draws the X axis guidelines
635
+ def draw_x_guidelines( label_height, count )
636
+ if count != 0
637
+ @graph.add_element( "path", {
638
+ "d" => "M#{label_height*count} 0 v#@graph_height",
639
+ "class" => "guideLines"
640
+ })
641
+ end
642
+ end
643
+
644
+
645
+ # Draws the Y axis guidelines
646
+ def draw_y_guidelines( label_height, count )
647
+ if count != 0
648
+ @graph.add_element( "path", {
649
+ "d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width",
650
+ "class" => "guideLines"
651
+ })
652
+ end
653
+ end
654
+
655
+
656
+ # Draws the graph title and subtitle
657
+ def draw_titles
658
+ if show_graph_title
659
+ @root.add_element( "text", {
660
+ "x" => (width / 2).to_s,
661
+ "y" => (title_font_size).to_s,
662
+ "class" => "mainTitle"
663
+ }).text = graph_title.to_s
664
+ end
665
+
666
+ if show_graph_subtitle
667
+ y_subtitle = show_graph_title ?
668
+ title_font_size + 10 :
669
+ subtitle_font_size
670
+ @root.add_element("text", {
671
+ "x" => (width / 2).to_s,
672
+ "y" => (y_subtitle).to_s,
673
+ "class" => "subTitle"
674
+ }).text = graph_subtitle.to_s
675
+ end
676
+
677
+ if show_x_title
678
+ y = @graph_height + @border_top + x_title_font_size
679
+ if show_x_labels
680
+ y += x_label_font_size + 5 if stagger_x_labels
681
+ y += x_label_font_size + 5
682
+ end
683
+ x = width / 2
684
+
685
+ @root.add_element("text", {
686
+ "x" => x.to_s,
687
+ "y" => y.to_s,
688
+ "class" => "xAxisTitle",
689
+ }).text = x_title.to_s
690
+ end
691
+
692
+ if show_y_title
693
+ x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3)
694
+ y = height / 2
695
+
696
+ text = @root.add_element("text", {
697
+ "x" => x.to_s,
698
+ "y" => y.to_s,
699
+ "class" => "yAxisTitle",
700
+ })
701
+ text.text = y_title.to_s
702
+ if y_title_text_direction == :bt
703
+ text.attributes["transform"] = "rotate( -90, #{x}, #{y} )"
704
+ else
705
+ text.attributes["transform"] = "rotate( 90, #{x}, #{y} )"
706
+ end
707
+ end
708
+ end
709
+
710
+ def keys
711
+ return @data.collect{ |d| d[:title] }
712
+ end
713
+
714
+ # Draws the legend on the graph
715
+ def draw_legend
716
+ if key
717
+ group = @root.add_element( "g" )
718
+
719
+ key_count = 0
720
+ for key_name in keys
721
+ y_offset = (KEY_BOX_SIZE * key_count) + (key_count * 5)
722
+ group.add_element( "rect", {
723
+ "x" => 0.to_s,
724
+ "y" => y_offset.to_s,
725
+ "width" => KEY_BOX_SIZE.to_s,
726
+ "height" => KEY_BOX_SIZE.to_s,
727
+ "class" => "key#{key_count+1}"
728
+ })
729
+ group.add_element( "text", {
730
+ "x" => (KEY_BOX_SIZE + 5).to_s,
731
+ "y" => (y_offset + KEY_BOX_SIZE).to_s,
732
+ "class" => "keyText"
733
+ }).text = key_name.to_s
734
+ key_count += 1
735
+ end
736
+
737
+ case key_position
738
+ when :right
739
+ x_offset = @graph_width + @border_left + 10
740
+ y_offset = @border_top + 20
741
+ when :bottom
742
+ x_offset = @border_left + 20
743
+ y_offset = @border_top + @graph_height + 5
744
+ if show_x_labels
745
+ max_x_label_height_px = (not rotate_x_labels) ?
746
+ x_label_font_size :
747
+ get_x_labels.max{|a,b|
748
+ a.to_s.length<=>b.to_s.length
749
+ }.to_s.length * x_label_font_size * 0.6
750
+ x_label_font_size
751
+ y_offset += max_x_label_height_px
752
+ y_offset += max_x_label_height_px + 5 if stagger_x_labels
753
+ end
754
+ y_offset += x_title_font_size + 5 if show_x_title
755
+ end
756
+ group.attributes["transform"] = "translate(#{x_offset} #{y_offset})"
757
+ end
758
+ end
759
+
760
+
761
+ private
762
+
763
+ def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 )
764
+ if lo < hi
765
+ p = partition(arrys,lo,hi)
766
+ sort_multiple(arrys, lo, p-1)
767
+ sort_multiple(arrys, p+1, hi)
768
+ end
769
+ arrys
770
+ end
771
+
772
+ def partition( arrys, lo, hi )
773
+ p = arrys[0][lo]
774
+ l = lo
775
+ z = lo+1
776
+ while z <= hi
777
+ if arrys[0][z] < p
778
+ l += 1
779
+ arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] }
780
+ end
781
+ z += 1
782
+ end
783
+ arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] }
784
+ l
785
+ end
786
+
787
+ def style
788
+ if no_css
789
+ styles = parse_css
790
+ @root.elements.each("//*[@class]") { |el|
791
+ cl = el.attributes["class"]
792
+ style = styles[cl]
793
+ style += el.attributes["style"] if el.attributes["style"]
794
+ el.attributes["style"] = style
795
+ }
796
+ end
797
+ end
798
+
799
+ def parse_css
800
+ css = get_style
801
+ rv = {}
802
+ while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m
803
+ names_orig = names = $1
804
+ css = $'
805
+ css =~ /([^}]+)\}/m
806
+ content = $1
807
+ css = $'
808
+
809
+ nms = []
810
+ while names =~ /^\s*,?\s*\.(\w+)/
811
+ nms << $1
812
+ names = $'
813
+ end
814
+
815
+ content = content.tr( "\n\t", " ")
816
+ for name in nms
817
+ current = rv[name]
818
+ current = current ? current+"; "+content : content
819
+ rv[name] = current.strip.squeeze(" ")
820
+ end
821
+ end
822
+ return rv
823
+ end
824
+
825
+
826
+ # Override and place code to add defs here
827
+ def add_defs defs
828
+ end
829
+
830
+
831
+ def start_svg
832
+ # Base document
833
+ @doc = Document.new
834
+ @doc << XMLDecl.new
835
+ @doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } +
836
+ %q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} )
837
+ if style_sheet && style_sheet != ''
838
+ @doc << Instruction.new( "xml-stylesheet",
839
+ %Q{href="#{style_sheet}" type="text/css"} )
840
+ end
841
+ @root = @doc.add_element( "svg", {
842
+ "width" => width.to_s,
843
+ "height" => height.to_s,
844
+ "viewBox" => "0 0 #{width} #{height}",
845
+ "xmlns" => "http://www.w3.org/2000/svg",
846
+ "xmlns:xlink" => "http://www.w3.org/1999/xlink",
847
+ "xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/",
848
+ "a3:scriptImplementation" => "Adobe"
849
+ })
850
+ @root << Comment.new( " "+"\\"*66 )
851
+ @root << Comment.new( " Created with SVG::Graph " )
852
+ @root << Comment.new( " SVG::Graph by Sean E. Russell " )
853
+ @root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+
854
+ " Leo Lapworth & Stephan Morgan " )
855
+ @root << Comment.new( " "+"/"*66 )
856
+
857
+ defs = @root.add_element( "defs" )
858
+ add_defs defs
859
+ if not(style_sheet && style_sheet != '') and !no_css
860
+ @root << Comment.new(" include default stylesheet if none specified ")
861
+ style = defs.add_element( "style", {"type"=>"text/css"} )
862
+ style << CData.new( get_style )
863
+ end
864
+
865
+ @root << Comment.new( "SVG Background" )
866
+ @root.add_element( "rect", {
867
+ "width" => width.to_s,
868
+ "height" => height.to_s,
869
+ "x" => "0",
870
+ "y" => "0",
871
+ "class" => "svgBackground"
872
+ })
873
+ end
874
+
875
+
876
+ def calculate_graph_dimensions
877
+ calculate_left_margin
878
+ calculate_right_margin
879
+ calculate_bottom_margin
880
+ calculate_top_margin
881
+ @graph_width = width - @border_left - @border_right
882
+ @graph_height = height - @border_top - @border_bottom
883
+ end
884
+
885
+ def get_style
886
+ return <<EOL
887
+ /* Copy from here for external style sheet */
888
+ .svgBackground{
889
+ fill:#ffffff;
890
+ }
891
+ .graphBackground{
892
+ fill:#f0f0f0;
893
+ }
894
+
895
+ /* graphs titles */
896
+ .mainTitle{
897
+ text-anchor: middle;
898
+ fill: #000000;
899
+ font-size: #{title_font_size}px;
900
+ font-family: "Arial", sans-serif;
901
+ font-weight: normal;
902
+ }
903
+ .subTitle{
904
+ text-anchor: middle;
905
+ fill: #999999;
906
+ font-size: #{subtitle_font_size}px;
907
+ font-family: "Arial", sans-serif;
908
+ font-weight: normal;
909
+ }
910
+
911
+ .axis{
912
+ stroke: #000000;
913
+ stroke-width: 1px;
914
+ }
915
+
916
+ .guideLines{
917
+ stroke: #666666;
918
+ stroke-width: 1px;
919
+ stroke-dasharray: 5 5;
920
+ }
921
+
922
+ .xAxisLabels{
923
+ text-anchor: middle;
924
+ fill: #000000;
925
+ font-size: #{x_label_font_size}px;
926
+ font-family: "Arial", sans-serif;
927
+ font-weight: normal;
928
+ }
929
+
930
+ .yAxisLabels{
931
+ text-anchor: end;
932
+ fill: #000000;
933
+ font-size: #{y_label_font_size}px;
934
+ font-family: "Arial", sans-serif;
935
+ font-weight: normal;
936
+ }
937
+
938
+ .xAxisTitle{
939
+ text-anchor: middle;
940
+ fill: #ff0000;
941
+ font-size: #{x_title_font_size}px;
942
+ font-family: "Arial", sans-serif;
943
+ font-weight: normal;
944
+ }
945
+
946
+ .yAxisTitle{
947
+ fill: #ff0000;
948
+ text-anchor: middle;
949
+ font-size: #{y_title_font_size}px;
950
+ font-family: "Arial", sans-serif;
951
+ font-weight: normal;
952
+ }
953
+
954
+ .dataPointLabel{
955
+ fill: #000000;
956
+ text-anchor:middle;
957
+ font-size: 10px;
958
+ font-family: "Arial", sans-serif;
959
+ font-weight: normal;
960
+ }
961
+
962
+ .staggerGuideLine{
963
+ fill: none;
964
+ stroke: #000000;
965
+ stroke-width: 0.5px;
966
+ }
967
+
968
+ #{get_css}
969
+
970
+ .keyText{
971
+ fill: #000000;
972
+ text-anchor:start;
973
+ font-size: #{key_font_size}px;
974
+ font-family: "Arial", sans-serif;
975
+ font-weight: normal;
976
+ }
977
+ /* End copy for external style sheet */
978
+ EOL
979
+ end
980
+
981
+ end
982
+ end
983
+ end