svg_graph 0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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