svg-graph 1.0.0

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