svg-graph 1.0.0

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