svg-graph19 0.6.2

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