svg-graph19 0.6.3 → 0.6.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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