svg_graph 0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,980 @@
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.5
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
+ if val
396
+ @border_right += val.length * key_font_size * 0.6
397
+ @border_right += KEY_BOX_SIZE
398
+ end
399
+ @border_right += 10 # Some padding around the box
400
+ end
401
+ end
402
+
403
+
404
+ # Override this (and call super) to change the margin to the top
405
+ # of the plot area. Results in @border_top being set.
406
+ def calculate_top_margin
407
+ @border_top = 5
408
+ @border_top += title_font_size if show_graph_title
409
+ @border_top += 5
410
+ @border_top += subtitle_font_size if show_graph_subtitle
411
+ end
412
+
413
+
414
+ # Adds pop-up point information to a graph.
415
+ def add_popup( x, y, label )
416
+ txt_width = label.length * font_size * 0.6 + 10
417
+ tx = (x+txt_width > width ? x-5 : x+5)
418
+ t = @foreground.add_element( "text", {
419
+ "x" => tx.to_s,
420
+ "y" => (y - font_size).to_s,
421
+ "visibility" => "hidden",
422
+ })
423
+ t.attributes["style"] = "fill: #000; "+
424
+ (x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;")
425
+ t.text = label.to_s
426
+ t.attributes["id"] = t.object_id.to_s
427
+
428
+ @foreground.add_element( "circle", {
429
+ "cx" => x.to_s,
430
+ "cy" => y.to_s,
431
+ "r" => "10",
432
+ "style" => "opacity: 0",
433
+ "onmouseover" =>
434
+ "document.getElementById(#{t.object_id}).setAttribute('visibility', 'visible' )",
435
+ "onmouseout" =>
436
+ "document.getElementById(#{t.object_id}).setAttribute('visibility', 'hidden' )",
437
+ })
438
+
439
+ end
440
+
441
+
442
+ # Override this (and call super) to change the margin to the bottom
443
+ # of the plot area. Results in @border_bottom being set.
444
+ def calculate_bottom_margin
445
+ @border_bottom = 7
446
+ if key and key_position == :bottom
447
+ @border_bottom += @data.size * (font_size + 5)
448
+ @border_bottom += 10
449
+ end
450
+ if show_x_labels
451
+ max_x_label_height_px = (not rotate_x_labels) ?
452
+ x_label_font_size :
453
+ get_x_labels.max{|a,b|
454
+ a.to_s.length<=>b.to_s.length
455
+ }.to_s.length * x_label_font_size * 0.6
456
+ @border_bottom += max_x_label_height_px
457
+ @border_bottom += max_x_label_height_px + 10 if stagger_x_labels
458
+ end
459
+ @border_bottom += x_title_font_size + 5 if show_x_title
460
+ end
461
+
462
+
463
+ # Draws the background, axis, and labels.
464
+ def draw_graph
465
+ @graph = @root.add_element( "g", {
466
+ "transform" => "translate( #@border_left #@border_top )"
467
+ })
468
+
469
+ # Background
470
+ @graph.add_element( "rect", {
471
+ "x" => "0",
472
+ "y" => "0",
473
+ "width" => @graph_width.to_s,
474
+ "height" => @graph_height.to_s,
475
+ "class" => "graphBackground"
476
+ })
477
+
478
+ # Axis
479
+ @graph.add_element( "path", {
480
+ "d" => "M 0 0 v#@graph_height",
481
+ "class" => "axis",
482
+ "id" => "xAxis"
483
+ })
484
+ @graph.add_element( "path", {
485
+ "d" => "M 0 #@graph_height h#@graph_width",
486
+ "class" => "axis",
487
+ "id" => "yAxis"
488
+ })
489
+
490
+ draw_x_labels
491
+ draw_y_labels
492
+ end
493
+
494
+
495
+ # Where in the X area the label is drawn
496
+ # Centered in the field, should be width/2. Start, 0.
497
+ def x_label_offset( width )
498
+ 0
499
+ end
500
+
501
+ def make_datapoint_text( x, y, value, style="" )
502
+ if show_data_values
503
+ @foreground.add_element( "text", {
504
+ "x" => x.to_s,
505
+ "y" => y.to_s,
506
+ "class" => "dataPointLabel",
507
+ "style" => "#{style} stroke: #fff; stroke-width: 2;"
508
+ }).text = value.to_s
509
+ text = @foreground.add_element( "text", {
510
+ "x" => x.to_s,
511
+ "y" => y.to_s,
512
+ "class" => "dataPointLabel"
513
+ })
514
+ text.text = value.to_s
515
+ text.attributes["style"] = style if style.length > 0
516
+ end
517
+ end
518
+
519
+
520
+ # Draws the X axis labels
521
+ def draw_x_labels
522
+ stagger = x_label_font_size + 5
523
+ if show_x_labels
524
+ label_width = field_width
525
+
526
+ count = 0
527
+ for label in get_x_labels
528
+ if step_include_first_x_label == true then
529
+ step = count % step_x_labels
530
+ else
531
+ step = (count + 1) % step_x_labels
532
+ end
533
+
534
+ if step == 0 then
535
+ text = @graph.add_element( "text" )
536
+ text.attributes["class"] = "xAxisLabels"
537
+ text.text = label.to_s
538
+
539
+ x = count * label_width + x_label_offset( label_width )
540
+ y = @graph_height + x_label_font_size + 3
541
+ t = 0 - (font_size / 2)
542
+
543
+ if stagger_x_labels and count % 2 == 1
544
+ y += stagger
545
+ @graph.add_element( "path", {
546
+ "d" => "M#{x} #@graph_height v#{stagger}",
547
+ "class" => "staggerGuideLine"
548
+ })
549
+ end
550
+
551
+ text.attributes["x"] = x.to_s
552
+ text.attributes["y"] = y.to_s
553
+ if rotate_x_labels
554
+ text.attributes["transform"] =
555
+ "rotate( 90 #{x} #{y-x_label_font_size} )"+
556
+ " translate( 0 -#{x_label_font_size/4} )"
557
+ text.attributes["style"] = "text-anchor: start"
558
+ else
559
+ text.attributes["style"] = "text-anchor: middle"
560
+ end
561
+ end
562
+
563
+ draw_x_guidelines( label_width, count ) if show_x_guidelines
564
+ count += 1
565
+ end
566
+ end
567
+ end
568
+
569
+
570
+ # Where in the Y area the label is drawn
571
+ # Centered in the field, should be width/2. Start, 0.
572
+ def y_label_offset( height )
573
+ 0
574
+ end
575
+
576
+
577
+ def field_width
578
+ (@graph_width.to_f - font_size*2*right_font) /
579
+ (get_x_labels.length - right_align)
580
+ end
581
+
582
+
583
+ def field_height
584
+ (@graph_height.to_f - font_size*2*top_font) /
585
+ (get_y_labels.length - top_align)
586
+ end
587
+
588
+
589
+ # Draws the Y axis labels
590
+ def draw_y_labels
591
+ stagger = y_label_font_size + 5
592
+ if show_y_labels
593
+ label_height = field_height
594
+
595
+ count = 0
596
+ y_offset = @graph_height + y_label_offset( label_height )
597
+ y_offset += font_size/1.2 unless rotate_y_labels
598
+ for label in get_y_labels
599
+ y = y_offset - (label_height * count)
600
+ x = rotate_y_labels ? 0 : -3
601
+
602
+ if stagger_y_labels and count % 2 == 1
603
+ x -= stagger
604
+ @graph.add_element( "path", {
605
+ "d" => "M#{x} #{y} h#{stagger}",
606
+ "class" => "staggerGuideLine"
607
+ })
608
+ end
609
+
610
+ text = @graph.add_element( "text", {
611
+ "x" => x.to_s,
612
+ "y" => y.to_s,
613
+ "class" => "yAxisLabels"
614
+ })
615
+ text.text = label.to_s
616
+ if rotate_y_labels
617
+ text.attributes["transform"] = "translate( -#{font_size} 0 ) "+
618
+ "rotate( 90 #{x} #{y} ) "
619
+ text.attributes["style"] = "text-anchor: middle"
620
+ else
621
+ text.attributes["y"] = (y - (y_label_font_size/2)).to_s
622
+ text.attributes["style"] = "text-anchor: end"
623
+ end
624
+ draw_y_guidelines( label_height, count ) if show_y_guidelines
625
+ count += 1
626
+ end
627
+ end
628
+ end
629
+
630
+
631
+ # Draws the X axis guidelines
632
+ def draw_x_guidelines( label_height, count )
633
+ if count != 0
634
+ @graph.add_element( "path", {
635
+ "d" => "M#{label_height*count} 0 v#@graph_height",
636
+ "class" => "guideLines"
637
+ })
638
+ end
639
+ end
640
+
641
+
642
+ # Draws the Y axis guidelines
643
+ def draw_y_guidelines( label_height, count )
644
+ if count != 0
645
+ @graph.add_element( "path", {
646
+ "d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width",
647
+ "class" => "guideLines"
648
+ })
649
+ end
650
+ end
651
+
652
+
653
+ # Draws the graph title and subtitle
654
+ def draw_titles
655
+ if show_graph_title
656
+ @root.add_element( "text", {
657
+ "x" => (width / 2).to_s,
658
+ "y" => (title_font_size).to_s,
659
+ "class" => "mainTitle"
660
+ }).text = graph_title.to_s
661
+ end
662
+
663
+ if show_graph_subtitle
664
+ y_subtitle = show_graph_title ?
665
+ title_font_size + 10 :
666
+ subtitle_font_size
667
+ @root.add_element("text", {
668
+ "x" => (width / 2).to_s,
669
+ "y" => (y_subtitle).to_s,
670
+ "class" => "subTitle"
671
+ }).text = graph_subtitle.to_s
672
+ end
673
+
674
+ if show_x_title
675
+ y = @graph_height + @border_top + x_title_font_size
676
+ if show_x_labels
677
+ y += x_label_font_size + 5 if stagger_x_labels
678
+ y += x_label_font_size + 5
679
+ end
680
+ x = width / 2
681
+
682
+ @root.add_element("text", {
683
+ "x" => x.to_s,
684
+ "y" => y.to_s,
685
+ "class" => "xAxisTitle",
686
+ }).text = x_title.to_s
687
+ end
688
+
689
+ if show_y_title
690
+ x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3)
691
+ y = height / 2
692
+
693
+ text = @root.add_element("text", {
694
+ "x" => x.to_s,
695
+ "y" => y.to_s,
696
+ "class" => "yAxisTitle",
697
+ })
698
+ text.text = y_title.to_s
699
+ if y_title_text_direction == :bt
700
+ text.attributes["transform"] = "rotate( -90, #{x}, #{y} )"
701
+ else
702
+ text.attributes["transform"] = "rotate( 90, #{x}, #{y} )"
703
+ end
704
+ end
705
+ end
706
+
707
+ def keys
708
+ return @data.collect{ |d| d[:title] }
709
+ end
710
+
711
+ # Draws the legend on the graph
712
+ def draw_legend
713
+ if key
714
+ group = @root.add_element( "g" )
715
+
716
+ key_count = 0
717
+ keys.compact.each do |key_name|
718
+ y_offset = (KEY_BOX_SIZE * key_count) + (key_count * 5)
719
+ group.add_element( "rect", {
720
+ "x" => 0.to_s,
721
+ "y" => y_offset.to_s,
722
+ "width" => KEY_BOX_SIZE.to_s,
723
+ "height" => KEY_BOX_SIZE.to_s,
724
+ "class" => "key#{key_count+1}"
725
+ })
726
+ group.add_element( "text", {
727
+ "x" => (KEY_BOX_SIZE + 5).to_s,
728
+ "y" => (y_offset + KEY_BOX_SIZE).to_s,
729
+ "class" => "keyText"
730
+ }).text = key_name.to_s
731
+ key_count += 1
732
+ end
733
+
734
+ case key_position
735
+ when :right
736
+ x_offset = @graph_width + @border_left + 10
737
+ y_offset = @border_top + 20
738
+ when :bottom
739
+ x_offset = @border_left + 20
740
+ y_offset = @border_top + @graph_height + 5
741
+ if show_x_labels
742
+ max_x_label_height_px = (not rotate_x_labels) ?
743
+ x_label_font_size :
744
+ get_x_labels.max{|a,b|
745
+ a.to_s.length<=>b.to_s.length
746
+ }.to_s.length * x_label_font_size * 0.6
747
+ x_label_font_size
748
+ y_offset += max_x_label_height_px
749
+ y_offset += max_x_label_height_px + 5 if stagger_x_labels
750
+ end
751
+ y_offset += x_title_font_size + 5 if show_x_title
752
+ end
753
+ group.attributes["transform"] = "translate(#{x_offset} #{y_offset})"
754
+ end
755
+ end
756
+
757
+
758
+ private
759
+
760
+ def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 )
761
+ if lo < hi
762
+ p = partition(arrys,lo,hi)
763
+ sort_multiple(arrys, lo, p-1)
764
+ sort_multiple(arrys, p+1, hi)
765
+ end
766
+ arrys
767
+ end
768
+
769
+ def partition( arrys, lo, hi )
770
+ p = arrys[0][lo]
771
+ l = lo
772
+ z = lo+1
773
+ while z <= hi
774
+ if arrys[0][z] < p
775
+ l += 1
776
+ arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] }
777
+ end
778
+ z += 1
779
+ end
780
+ arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] }
781
+ l
782
+ end
783
+
784
+ def style
785
+ if no_css
786
+ styles = parse_css
787
+ @root.elements.each("//*[@class]") { |el|
788
+ cl = el.attributes["class"]
789
+ style = styles[cl]
790
+ style += el.attributes["style"] if el.attributes["style"]
791
+ el.attributes["style"] = style
792
+ }
793
+ end
794
+ end
795
+
796
+ def parse_css
797
+ css = get_style
798
+ rv = {}
799
+ while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m
800
+ names_orig = names = $1
801
+ css = $'
802
+ css =~ /([^}]+)\}/m
803
+ content = $1
804
+ css = $'
805
+
806
+ nms = []
807
+ while names =~ /^\s*,?\s*\.(\w+)/
808
+ nms << $1
809
+ names = $'
810
+ end
811
+
812
+ content = content.tr( "\n\t", " ")
813
+ for name in nms
814
+ current = rv[name]
815
+ current = current ? current+"; "+content : content
816
+ rv[name] = current.strip.squeeze(" ")
817
+ end
818
+ end
819
+ return rv
820
+ end
821
+
822
+
823
+ # Override and place code to add defs here
824
+ def add_defs defs
825
+ end
826
+
827
+
828
+ def start_svg
829
+ # Base document
830
+ @doc = Document.new
831
+ @doc << XMLDecl.new
832
+ @doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } +
833
+ %q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} )
834
+ if style_sheet && style_sheet != ''
835
+ @doc << Instruction.new( "xml-stylesheet",
836
+ %Q{href="#{style_sheet}" type="text/css"} )
837
+ end
838
+ @root = @doc.add_element( "svg", {
839
+ "width" => width.to_s,
840
+ "height" => height.to_s,
841
+ "viewBox" => "0 0 #{width} #{height}",
842
+ "xmlns" => "http://www.w3.org/2000/svg",
843
+ "xmlns:xlink" => "http://www.w3.org/1999/xlink",
844
+ "xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/",
845
+ "a3:scriptImplementation" => "Adobe"
846
+ })
847
+ @root << Comment.new( " "+"\\"*66 )
848
+ @root << Comment.new( " Created with SVG::Graph " )
849
+ @root << Comment.new( " SVG::Graph by Sean E. Russell " )
850
+ @root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+
851
+ " Leo Lapworth & Stephan Morgan " )
852
+ @root << Comment.new( " "+"/"*66 )
853
+
854
+ defs = @root.add_element( "defs" )
855
+ add_defs defs
856
+ if not(style_sheet && style_sheet != '') and !no_css
857
+ @root << Comment.new(" include default stylesheet if none specified ")
858
+ style = defs.add_element( "style", {"type"=>"text/css"} )
859
+ style << CData.new( get_style )
860
+ end
861
+
862
+ @root << Comment.new( "SVG Background" )
863
+ @root.add_element( "rect", {
864
+ "width" => width.to_s,
865
+ "height" => height.to_s,
866
+ "x" => "0",
867
+ "y" => "0",
868
+ "class" => "svgBackground"
869
+ })
870
+ end
871
+
872
+
873
+ def calculate_graph_dimensions
874
+ calculate_left_margin
875
+ calculate_right_margin
876
+ calculate_bottom_margin
877
+ calculate_top_margin
878
+ @graph_width = width - @border_left - @border_right
879
+ @graph_height = height - @border_top - @border_bottom
880
+ end
881
+
882
+ def get_style
883
+ return <<EOL
884
+ /* Copy from here for external style sheet */
885
+ .svgBackground{
886
+ fill:#ffffff;
887
+ }
888
+ .graphBackground{
889
+ fill:#f0f0f0;
890
+ }
891
+
892
+ /* graphs titles */
893
+ .mainTitle{
894
+ text-anchor: middle;
895
+ fill: #000000;
896
+ font-size: #{title_font_size}px;
897
+ font-family: "Arial", sans-serif;
898
+ font-weight: normal;
899
+ }
900
+ .subTitle{
901
+ text-anchor: middle;
902
+ fill: #999999;
903
+ font-size: #{subtitle_font_size}px;
904
+ font-family: "Arial", sans-serif;
905
+ font-weight: normal;
906
+ }
907
+
908
+ .axis{
909
+ stroke: #000000;
910
+ stroke-width: 1px;
911
+ }
912
+
913
+ .guideLines{
914
+ stroke: #666666;
915
+ stroke-width: 1px;
916
+ stroke-dasharray: 5 5;
917
+ }
918
+
919
+ .xAxisLabels{
920
+ text-anchor: middle;
921
+ fill: #000000;
922
+ font-size: #{x_label_font_size}px;
923
+ font-family: "Arial", sans-serif;
924
+ font-weight: normal;
925
+ }
926
+
927
+ .yAxisLabels{
928
+ text-anchor: end;
929
+ fill: #000000;
930
+ font-size: #{y_label_font_size}px;
931
+ font-family: "Arial", sans-serif;
932
+ font-weight: normal;
933
+ }
934
+
935
+ .xAxisTitle{
936
+ text-anchor: middle;
937
+ fill: #ff0000;
938
+ font-size: #{x_title_font_size}px;
939
+ font-family: "Arial", sans-serif;
940
+ font-weight: normal;
941
+ }
942
+
943
+ .yAxisTitle{
944
+ fill: #ff0000;
945
+ text-anchor: middle;
946
+ font-size: #{y_title_font_size}px;
947
+ font-family: "Arial", sans-serif;
948
+ font-weight: normal;
949
+ }
950
+
951
+ .dataPointLabel{
952
+ fill: #000000;
953
+ text-anchor:middle;
954
+ font-size: 10px;
955
+ font-family: "Arial", sans-serif;
956
+ font-weight: normal;
957
+ }
958
+
959
+ .staggerGuideLine{
960
+ fill: none;
961
+ stroke: #000000;
962
+ stroke-width: 0.5px;
963
+ }
964
+
965
+ #{get_css}
966
+
967
+ .keyText{
968
+ fill: #000000;
969
+ text-anchor:start;
970
+ font-size: #{key_font_size}px;
971
+ font-family: "Arial", sans-serif;
972
+ font-weight: normal;
973
+ }
974
+ /* End copy for external style sheet */
975
+ EOL
976
+ end
977
+
978
+ end
979
+ end
980
+ end