ruport 0.4.23 → 0.4.99

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.
Files changed (71) hide show
  1. data/AUTHORS +16 -8
  2. data/CHANGELOG +30 -1
  3. data/README +144 -114
  4. data/Rakefile +12 -4
  5. data/TODO +4 -7
  6. data/bin/rope +21 -28
  7. data/examples/line_graph.rb +36 -0
  8. data/examples/sample_invoice_report.rb +1 -1
  9. data/examples/simple_graph.rb +8 -0
  10. data/lib/SVG/Graph/Bar.rb +137 -0
  11. data/lib/SVG/Graph/BarBase.rb +140 -0
  12. data/lib/SVG/Graph/BarHorizontal.rb +136 -0
  13. data/lib/SVG/Graph/Graph.rb +977 -0
  14. data/lib/SVG/Graph/Line.rb +444 -0
  15. data/lib/SVG/Graph/Pie.rb +394 -0
  16. data/lib/SVG/Graph/Plot.rb +494 -0
  17. data/lib/SVG/Graph/Schedule.rb +373 -0
  18. data/lib/SVG/Graph/TimeSeries.rb +241 -0
  19. data/lib/ruport.rb +2 -2
  20. data/lib/ruport/config.rb +47 -3
  21. data/lib/ruport/data/collection.rb +17 -1
  22. data/lib/ruport/data/record.rb +101 -8
  23. data/lib/ruport/data/set.rb +81 -2
  24. data/lib/ruport/data/set.rb.rej +147 -0
  25. data/lib/ruport/data/set.rb~ +73 -0
  26. data/lib/ruport/data/table.rb +127 -2
  27. data/lib/ruport/data/taggable.rb +21 -2
  28. data/lib/ruport/format.rb +36 -44
  29. data/lib/ruport/format/engine.rb +21 -1
  30. data/lib/ruport/format/plugin.rb +64 -1
  31. data/lib/ruport/mailer.rb +70 -36
  32. data/lib/ruport/meta_tools.rb +15 -6
  33. data/lib/ruport/query.rb +1 -1
  34. data/lib/ruport/rails/reportable.rb +23 -1
  35. data/lib/ruport/report.rb +11 -11
  36. data/lib/ruport/report/invoice.rb +16 -0
  37. data/lib/ruport/system_extensions.rb +3 -55
  38. data/test/{tc_database.rb → _test_database.rb} +0 -0
  39. data/test/{tc_config.rb → test_config.rb} +0 -0
  40. data/test/{tc_format.rb → test_format.rb} +1 -0
  41. data/test/{tc_format_engine.rb → test_format_engine.rb} +14 -2
  42. data/test/test_graph.rb +101 -0
  43. data/test/{tc_invoice.rb → test_invoice.rb} +7 -1
  44. data/test/test_mailer.rb +108 -0
  45. data/test/test_meta_tools.rb +14 -0
  46. data/test/{tc_plugin.rb → test_plugin.rb} +12 -1
  47. data/test/{tc_query.rb → test_query.rb} +0 -0
  48. data/test/{tc_record.rb → test_record.rb} +9 -0
  49. data/test/{tc_report.rb → test_report.rb} +2 -1
  50. data/test/{tc_ruport.rb → test_ruport.rb} +0 -0
  51. data/test/test_set.rb +118 -0
  52. data/test/test_set.rb.rej +16 -0
  53. data/test/{tc_set.rb → test_set.rb~} +17 -0
  54. data/test/{tc_sql_split.rb → test_sql_split.rb} +0 -0
  55. data/test/{tc_table.rb → test_table.rb} +15 -0
  56. data/test/{tc_taggable.rb → test_taggable.rb} +0 -0
  57. data/test/unit.log +361 -0
  58. metadata +52 -30
  59. data/examples/bar.pdf +0 -193
  60. data/examples/f.log +0 -5
  61. data/examples/foo.pdf +0 -193
  62. data/lib/ruport/format/document.rb +0 -78
  63. data/lib/ruport/format/open_node.rb +0 -38
  64. data/test/tc_data_row.rb +0 -132
  65. data/test/tc_data_set.rb +0 -386
  66. data/test/tc_document.rb +0 -42
  67. data/test/tc_element.rb +0 -18
  68. data/test/tc_page.rb +0 -42
  69. data/test/tc_section.rb +0 -45
  70. data/test/ts_all.rb +0 -12
  71. data/test/ts_format.rb +0 -7
@@ -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
+ VERSION = '@ANT_VERSION@'
13
+
14
+ # === Base object for generating SVG Graphs
15
+ #
16
+ # == Synopsis
17
+ #
18
+ # This class is only used as a superclass of specialized charts. Do not
19
+ # attempt to use this class directly, unless creating a new chart type.
20
+ #
21
+ # For examples of how to subclass this class, see the existing specific
22
+ # subclasses, such as SVG::Graph::Pie.
23
+ #
24
+ # == Examples
25
+ #
26
+ # For examples of how to use this package, see either the test files, or
27
+ # the documentation for the specific class you want to use.
28
+ #
29
+ # * file:test/plot.rb
30
+ # * file:test/single.rb
31
+ # * file:test/test.rb
32
+ # * file:test/timeseries.rb
33
+ #
34
+ # == Description
35
+ #
36
+ # This package should be used as a base for creating SVG graphs.
37
+ #
38
+ # == Acknowledgements
39
+ #
40
+ # Leo Lapworth for creating the SVG::TT::Graph package which this Ruby
41
+ # port is based on.
42
+ #
43
+ # Stephen Morgan for creating the TT template and SVG.
44
+ #
45
+ # == See
46
+ #
47
+ # * SVG::Graph::BarHorizontal
48
+ # * SVG::Graph::Bar
49
+ # * SVG::Graph::Line
50
+ # * SVG::Graph::Pie
51
+ # * SVG::Graph::Plot
52
+ # * SVG::Graph::TimeSeries
53
+ #
54
+ # == Author
55
+ #
56
+ # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
57
+ #
58
+ # Copyright 2004 Sean E. Russell
59
+ # This software is available under the Ruby license[LICENSE.txt]
60
+ #
61
+ class Graph
62
+ include REXML
63
+
64
+ # Initialize the graph object with the graph settings. You won't
65
+ # instantiate this class directly; see the subclass for options.
66
+ # [width] 500
67
+ # [height] 300
68
+ # [show_x_guidelines] false
69
+ # [show_y_guidelines] true
70
+ # [show_data_values] true
71
+ # [min_scale_value] 0
72
+ # [show_x_labels] true
73
+ # [stagger_x_labels] false
74
+ # [rotate_x_labels] false
75
+ # [step_x_labels] 1
76
+ # [step_include_first_x_label] true
77
+ # [show_y_labels] true
78
+ # [rotate_y_labels] false
79
+ # [scale_integers] false
80
+ # [show_x_title] false
81
+ # [x_title] 'X Field names'
82
+ # [show_y_title] false
83
+ # [y_title_text_direction] :bt
84
+ # [y_title] 'Y Scale'
85
+ # [show_graph_title] false
86
+ # [graph_title] 'Graph Title'
87
+ # [show_graph_subtitle] false
88
+ # [graph_subtitle] 'Graph Sub Title'
89
+ # [key] true,
90
+ # [key_position] :right, # bottom or righ
91
+ # [font_size] 12
92
+ # [title_font_size] 16
93
+ # [subtitle_font_size] 14
94
+ # [x_label_font_size] 12
95
+ # [x_title_font_size] 14
96
+ # [y_label_font_size] 12
97
+ # [y_title_font_size] 14
98
+ # [key_font_size] 10
99
+ # [no_css] false
100
+ # [add_popups] false
101
+ def initialize( config )
102
+ @config = config
103
+
104
+ self.top_align = self.top_font = self.right_align = self.right_font = 0
105
+
106
+ init_with({
107
+ :width => 500,
108
+ :height => 300,
109
+ :show_x_guidelines => false,
110
+ :show_y_guidelines => true,
111
+ :show_data_values => true,
112
+
113
+ :min_scale_value => 0,
114
+
115
+ :show_x_labels => true,
116
+ :stagger_x_labels => false,
117
+ :rotate_x_labels => false,
118
+ :step_x_labels => 1,
119
+ :step_include_first_x_label => true,
120
+
121
+ :show_y_labels => true,
122
+ :rotate_y_labels => false,
123
+ :stagger_y_labels => false,
124
+ :scale_integers => false,
125
+
126
+ :show_x_title => false,
127
+ :x_title => 'X Field names',
128
+
129
+ :show_y_title => false,
130
+ :y_title_text_direction => :bt,
131
+ :y_title => 'Y Scale',
132
+
133
+ :show_graph_title => false,
134
+ :graph_title => 'Graph Title',
135
+ :show_graph_subtitle => false,
136
+ :graph_subtitle => 'Graph Sub Title',
137
+ :key => true,
138
+ :key_position => :right, # bottom or right
139
+
140
+ :font_size =>12,
141
+ :title_font_size =>16,
142
+ :subtitle_font_size =>14,
143
+ :x_label_font_size =>12,
144
+ :x_title_font_size =>14,
145
+ :y_label_font_size =>12,
146
+ :y_title_font_size =>14,
147
+ :key_font_size =>10,
148
+
149
+ :no_css =>false,
150
+ :add_popups =>false,
151
+ })
152
+
153
+ set_defaults if methods.include? "set_defaults"
154
+
155
+ init_with config
156
+ end
157
+
158
+
159
+ # This method allows you do add data to the graph object.
160
+ # It can be called several times to add more data sets in.
161
+ #
162
+ # data_sales_02 = [12, 45, 21];
163
+ #
164
+ # graph.add_data({
165
+ # :data => data_sales_02,
166
+ # :title => 'Sales 2002'
167
+ # })
168
+ def add_data conf
169
+ @data = [] unless defined? @data
170
+
171
+ if conf[:data] and conf[:data].kind_of? Array
172
+ @data << conf
173
+ else
174
+ raise "No data provided by #{conf.inspect}"
175
+ end
176
+ end
177
+
178
+
179
+ # This method removes all data from the object so that you can
180
+ # reuse it to create a new graph but with the same config options.
181
+ #
182
+ # graph.clear_data
183
+ def clear_data
184
+ @data = []
185
+ end
186
+
187
+
188
+ # This method processes the template with the data and
189
+ # config which has been set and returns the resulting SVG.
190
+ #
191
+ # This method will croak unless at least one data set has
192
+ # been added to the graph object.
193
+ #
194
+ # print graph.burn
195
+ def burn
196
+ raise "No data available" unless @data.size > 0
197
+
198
+ calculations if methods.include? 'calculations'
199
+
200
+ start_svg
201
+ calculate_graph_dimensions
202
+ @foreground = Element.new( "g" )
203
+ draw_graph
204
+ draw_titles
205
+ draw_legend
206
+ draw_data
207
+ @graph.add_element( @foreground )
208
+ style
209
+
210
+ data = ""
211
+ @doc.write( data, 0 )
212
+
213
+ if @config[:compress]
214
+ if @@__have_zlib
215
+ inp, out = IO.pipe
216
+ gz = Zlib::GzipWriter.new( out )
217
+ gz.write data
218
+ gz.close
219
+ data = inp.read
220
+ else
221
+ data << "<!-- Ruby Zlib not available for SVGZ -->";
222
+ end
223
+ end
224
+
225
+ return data
226
+ end
227
+
228
+
229
+ # Set the height of the graph box, this is the total height
230
+ # of the SVG box created - not the graph it self which auto
231
+ # scales to fix the space.
232
+ attr_accessor :height
233
+ # Set the width of the graph box, this is the total width
234
+ # of the SVG box created - not the graph it self which auto
235
+ # scales to fix the space.
236
+ attr_accessor :width
237
+ # Set the path to an external stylesheet, set to '' if
238
+ # you want to revert back to using the defaut internal version.
239
+ #
240
+ # To create an external stylesheet create a graph using the
241
+ # default internal version and copy the stylesheet section to
242
+ # an external file and edit from there.
243
+ attr_accessor :style_sheet
244
+ # (Bool) Show the value of each element of data on the graph
245
+ attr_accessor :show_data_values
246
+ # The point at which the Y axis starts, defaults to '0',
247
+ # if set to nil it will default to the minimum data value.
248
+ attr_accessor :min_scale_value
249
+ # Whether to show labels on the X axis or not, defaults
250
+ # to true, set to false if you want to turn them off.
251
+ attr_accessor :show_x_labels
252
+ # This puts the X labels at alternative levels so if they
253
+ # are long field names they will not overlap so easily.
254
+ # Default it false, to turn on set to true.
255
+ attr_accessor :stagger_x_labels
256
+ # This puts the Y labels at alternative levels so if they
257
+ # are long field names they will not overlap so easily.
258
+ # Default it false, to turn on set to true.
259
+ attr_accessor :stagger_y_labels
260
+ # This turns the X axis labels by 90 degrees.
261
+ # Default it false, to turn on set to true.
262
+ attr_accessor :rotate_x_labels
263
+ # This turns the Y axis labels by 90 degrees.
264
+ # Default it false, to turn on set to true.
265
+ attr_accessor :rotate_y_labels
266
+ # How many "steps" to use between displayed X axis labels,
267
+ # a step of one means display every label, a step of two results
268
+ # in every other label being displayed (label <gap> label <gap> label),
269
+ # a step of three results in every third label being displayed
270
+ # (label <gap> <gap> label <gap> <gap> label) and so on.
271
+ attr_accessor :step_x_labels
272
+ # Whether to (when taking "steps" between X axis labels) step from
273
+ # the first label (i.e. always include the first label) or step from
274
+ # the X axis origin (i.e. start with a gap if step_x_labels is greater
275
+ # than one).
276
+ attr_accessor :step_include_first_x_label
277
+ # Whether to show labels on the Y axis or not, defaults
278
+ # to true, set to false if you want to turn them off.
279
+ attr_accessor :show_y_labels
280
+ # Ensures only whole numbers are used as the scale divisions.
281
+ # Default it false, to turn on set to true. This has no effect if
282
+ # scale divisions are less than 1.
283
+ attr_accessor :scale_integers
284
+ # This defines the gap between markers on the Y axis,
285
+ # default is a 10th of the max_value, e.g. you will have
286
+ # 10 markers on the Y axis. NOTE: do not set this too
287
+ # low - you are limited to 999 markers, after that the
288
+ # graph won't generate.
289
+ attr_accessor :scale_divisions
290
+ # Whether to show the title under the X axis labels,
291
+ # default is false, set to true to show.
292
+ attr_accessor :show_x_title
293
+ # What the title under X axis should be, e.g. 'Months'.
294
+ attr_accessor :x_title
295
+ # Whether to show the title under the Y axis labels,
296
+ # default is false, set to true to show.
297
+ attr_accessor :show_y_title
298
+ # Aligns writing mode for Y axis label.
299
+ # Defaults to :bt (Bottom to Top).
300
+ # Change to :tb (Top to Bottom) to reverse.
301
+ attr_accessor :y_title_text_direction
302
+ # What the title under Y axis should be, e.g. 'Sales in thousands'.
303
+ attr_accessor :y_title
304
+ # Whether to show a title on the graph, defaults
305
+ # to false, set to true to show.
306
+ attr_accessor :show_graph_title
307
+ # What the title on the graph should be.
308
+ attr_accessor :graph_title
309
+ # Whether to show a subtitle on the graph, defaults
310
+ # to false, set to true to show.
311
+ attr_accessor :show_graph_subtitle
312
+ # What the subtitle on the graph should be.
313
+ attr_accessor :graph_subtitle
314
+ # Whether to show a key, defaults to false, set to
315
+ # true if you want to show it.
316
+ attr_accessor :key
317
+ # Where the key should be positioned, defaults to
318
+ # :right, set to :bottom if you want to move it.
319
+ attr_accessor :key_position
320
+ # Set the font size (in points) of the data point labels
321
+ attr_accessor :font_size
322
+ # Set the font size of the X axis labels
323
+ attr_accessor :x_label_font_size
324
+ # Set the font size of the X axis title
325
+ attr_accessor :x_title_font_size
326
+ # Set the font size of the Y axis labels
327
+ attr_accessor :y_label_font_size
328
+ # Set the font size of the Y axis title
329
+ attr_accessor :y_title_font_size
330
+ # Set the title font size
331
+ attr_accessor :title_font_size
332
+ # Set the subtitle font size
333
+ attr_accessor :subtitle_font_size
334
+ # Set the key font size
335
+ attr_accessor :key_font_size
336
+ # Show guidelines for the X axis
337
+ attr_accessor :show_x_guidelines
338
+ # Show guidelines for the Y axis
339
+ attr_accessor :show_y_guidelines
340
+ # Do not use CSS if set to true. Many SVG viewers do not support CSS, but
341
+ # not using CSS can result in larger SVGs as well as making it impossible to
342
+ # change colors after the chart is generated. Defaults to false.
343
+ attr_accessor :no_css
344
+ # Add popups for the data points on some graphs
345
+ attr_accessor :add_popups
346
+
347
+
348
+ protected
349
+
350
+ def sort( *arrys )
351
+ sort_multiple( arrys )
352
+ end
353
+
354
+ # Overwrite configuration options with supplied options. Used
355
+ # by subclasses.
356
+ def init_with config
357
+ config.each { |key, value|
358
+ self.send( key.to_s+"=", value ) if methods.include? key.to_s
359
+ }
360
+ end
361
+
362
+ attr_accessor :top_align, :top_font, :right_align, :right_font
363
+
364
+ KEY_BOX_SIZE = 12
365
+
366
+ # Override this (and call super) to change the margin to the left
367
+ # of the plot area. Results in @border_left being set.
368
+ def calculate_left_margin
369
+ @border_left = 7
370
+ # Check for Y labels
371
+ max_y_label_height_px = rotate_y_labels ?
372
+ y_label_font_size :
373
+ get_y_labels.max{|a,b|
374
+ a.to_s.length<=>b.to_s.length
375
+ }.to_s.length * y_label_font_size * 0.6
376
+ @border_left += max_y_label_height_px if show_y_labels
377
+ @border_left += max_y_label_height_px + 10 if stagger_y_labels
378
+ @border_left += y_title_font_size + 5 if show_y_title
379
+ end
380
+
381
+
382
+ # Calculates the width of the widest Y label. This will be the
383
+ # character height if the Y labels are rotated
384
+ def max_y_label_width_px
385
+ return font_size if rotate_y_labels
386
+ end
387
+
388
+
389
+ # Override this (and call super) to change the margin to the right
390
+ # of the plot area. Results in @border_right being set.
391
+ def calculate_right_margin
392
+ @border_right = 7
393
+ if key and key_position == :right
394
+ val = keys.max { |a,b| a.length <=> b.length }
395
+ @border_right += val.length * key_font_size * 0.6
396
+ @border_right += KEY_BOX_SIZE
397
+ @border_right += 10 # Some padding around the box
398
+ end
399
+ end
400
+
401
+
402
+ # Override this (and call super) to change the margin to the top
403
+ # of the plot area. Results in @border_top being set.
404
+ def calculate_top_margin
405
+ @border_top = 5
406
+ @border_top += title_font_size if show_graph_title
407
+ @border_top += 5
408
+ @border_top += subtitle_font_size if show_graph_subtitle
409
+ end
410
+
411
+
412
+ # Adds pop-up point information to a graph.
413
+ def add_popup( x, y, label )
414
+ txt_width = label.length * font_size * 0.6 + 10
415
+ tx = (x+txt_width > width ? x-5 : x+5)
416
+ t = @foreground.add_element( "text", {
417
+ "x" => tx.to_s,
418
+ "y" => (y - font_size).to_s,
419
+ "visibility" => "hidden",
420
+ })
421
+ t.attributes["style"] = "fill: #000; "+
422
+ (x+txt_width > width ? "text-anchor: end;" : "text-anchor: start;")
423
+ t.text = label.to_s
424
+ t.attributes["id"] = t.id.to_s
425
+
426
+ @foreground.add_element( "circle", {
427
+ "cx" => x.to_s,
428
+ "cy" => y.to_s,
429
+ "r" => "10",
430
+ "style" => "opacity: 0",
431
+ "onmouseover" =>
432
+ "document.getElementById(#{t.id}).setAttribute('visibility', 'visible' )",
433
+ "onmouseout" =>
434
+ "document.getElementById(#{t.id}).setAttribute('visibility', 'hidden' )",
435
+ })
436
+
437
+ end
438
+
439
+
440
+ # Override this (and call super) to change the margin to the bottom
441
+ # of the plot area. Results in @border_bottom being set.
442
+ def calculate_bottom_margin
443
+ @border_bottom = 7
444
+ if key and key_position == :bottom
445
+ @border_bottom += @data.size * (font_size + 5)
446
+ @border_bottom += 10
447
+ end
448
+ if show_x_labels
449
+ max_x_label_height_px = rotate_x_labels ?
450
+ get_x_labels.max{|a,b|
451
+ a.length<=>b.length
452
+ }.length * x_label_font_size * 0.6 :
453
+ x_label_font_size
454
+ @border_bottom += max_x_label_height_px
455
+ @border_bottom += max_x_label_height_px + 10 if stagger_x_labels
456
+ end
457
+ @border_bottom += x_title_font_size + 5 if show_x_title
458
+ end
459
+
460
+
461
+ # Draws the background, axis, and labels.
462
+ def draw_graph
463
+ @graph = @root.add_element( "g", {
464
+ "transform" => "translate( #@border_left #@border_top )"
465
+ })
466
+
467
+ # Background
468
+ @graph.add_element( "rect", {
469
+ "x" => "0",
470
+ "y" => "0",
471
+ "width" => @graph_width.to_s,
472
+ "height" => @graph_height.to_s,
473
+ "class" => "graphBackground"
474
+ })
475
+
476
+ # Axis
477
+ @graph.add_element( "path", {
478
+ "d" => "M 0 0 v#@graph_height",
479
+ "class" => "axis",
480
+ "id" => "xAxis"
481
+ })
482
+ @graph.add_element( "path", {
483
+ "d" => "M 0 #@graph_height h#@graph_width",
484
+ "class" => "axis",
485
+ "id" => "yAxis"
486
+ })
487
+
488
+ draw_x_labels
489
+ draw_y_labels
490
+ end
491
+
492
+
493
+ # Where in the X area the label is drawn
494
+ # Centered in the field, should be width/2. Start, 0.
495
+ def x_label_offset( width )
496
+ 0
497
+ end
498
+
499
+ def make_datapoint_text( x, y, value, style="" )
500
+ if show_data_values
501
+ @foreground.add_element( "text", {
502
+ "x" => x.to_s,
503
+ "y" => y.to_s,
504
+ "class" => "dataPointLabel",
505
+ "style" => "#{style} stroke: #fff; stroke-width: 2;"
506
+ }).text = value.to_s
507
+ text = @foreground.add_element( "text", {
508
+ "x" => x.to_s,
509
+ "y" => y.to_s,
510
+ "class" => "dataPointLabel"
511
+ })
512
+ text.text = value.to_s
513
+ text.attributes["style"] = style if style.length > 0
514
+ end
515
+ end
516
+
517
+
518
+ # Draws the X axis labels
519
+ def draw_x_labels
520
+ stagger = x_label_font_size + 5
521
+ if show_x_labels
522
+ label_width = field_width
523
+
524
+ count = 0
525
+ for label in get_x_labels
526
+ if step_include_first_x_label == true then
527
+ step = count % step_x_labels
528
+ else
529
+ step = (count + 1) % step_x_labels
530
+ end
531
+
532
+ if step == 0 then
533
+ text = @graph.add_element( "text" )
534
+ text.attributes["class"] = "xAxisLabels"
535
+ text.text = label.to_s
536
+
537
+ x = count * label_width + x_label_offset( label_width )
538
+ y = @graph_height + x_label_font_size + 3
539
+ t = 0 - (font_size / 2)
540
+
541
+ if stagger_x_labels and count % 2 == 1
542
+ y += stagger
543
+ @graph.add_element( "path", {
544
+ "d" => "M#{x} #@graph_height v#{stagger}",
545
+ "class" => "staggerGuideLine"
546
+ })
547
+ end
548
+
549
+ text.attributes["x"] = x.to_s
550
+ text.attributes["y"] = y.to_s
551
+ if rotate_x_labels
552
+ text.attributes["transform"] =
553
+ "rotate( 90 #{x} #{y-x_label_font_size} )"+
554
+ " translate( 0 -#{x_label_font_size/4} )"
555
+ text.attributes["style"] = "text-anchor: start"
556
+ else
557
+ text.attributes["style"] = "text-anchor: middle"
558
+ end
559
+ end
560
+
561
+ draw_x_guidelines( label_width, count ) if show_x_guidelines
562
+ count += 1
563
+ end
564
+ end
565
+ end
566
+
567
+
568
+ # Where in the Y area the label is drawn
569
+ # Centered in the field, should be width/2. Start, 0.
570
+ def y_label_offset( height )
571
+ 0
572
+ end
573
+
574
+
575
+ def field_width
576
+ (@graph_width.to_f - font_size*2*right_font) /
577
+ (get_x_labels.length - right_align)
578
+ end
579
+
580
+
581
+ def field_height
582
+ (@graph_height.to_f - font_size*2*top_font) /
583
+ (get_y_labels.length - top_align)
584
+ end
585
+
586
+
587
+ # Draws the Y axis labels
588
+ def draw_y_labels
589
+ stagger = y_label_font_size + 5
590
+ if show_y_labels
591
+ label_height = field_height
592
+
593
+ count = 0
594
+ y_offset = @graph_height + y_label_offset( label_height )
595
+ y_offset += font_size/1.2 unless rotate_y_labels
596
+ for label in get_y_labels
597
+ y = y_offset - (label_height * count)
598
+ x = rotate_y_labels ? 0 : -3
599
+
600
+ if stagger_y_labels and count % 2 == 1
601
+ x -= stagger
602
+ @graph.add_element( "path", {
603
+ "d" => "M#{x} #{y} h#{stagger}",
604
+ "class" => "staggerGuideLine"
605
+ })
606
+ end
607
+
608
+ text = @graph.add_element( "text", {
609
+ "x" => x.to_s,
610
+ "y" => y.to_s,
611
+ "class" => "yAxisLabels"
612
+ })
613
+ text.text = label.to_s
614
+ if rotate_y_labels
615
+ text.attributes["transform"] = "translate( -#{font_size} 0 ) "+
616
+ "rotate( 90 #{x} #{y} ) "
617
+ text.attributes["style"] = "text-anchor: middle"
618
+ else
619
+ text.attributes["y"] = (y - (y_label_font_size/2)).to_s
620
+ text.attributes["style"] = "text-anchor: end"
621
+ end
622
+ draw_y_guidelines( label_height, count ) if show_y_guidelines
623
+ count += 1
624
+ end
625
+ end
626
+ end
627
+
628
+
629
+ # Draws the X axis guidelines
630
+ def draw_x_guidelines( label_height, count )
631
+ if count != 0
632
+ @graph.add_element( "path", {
633
+ "d" => "M#{label_height*count} 0 v#@graph_height",
634
+ "class" => "guideLines"
635
+ })
636
+ end
637
+ end
638
+
639
+
640
+ # Draws the Y axis guidelines
641
+ def draw_y_guidelines( label_height, count )
642
+ if count != 0
643
+ @graph.add_element( "path", {
644
+ "d" => "M0 #{@graph_height-(label_height*count)} h#@graph_width",
645
+ "class" => "guideLines"
646
+ })
647
+ end
648
+ end
649
+
650
+
651
+ # Draws the graph title and subtitle
652
+ def draw_titles
653
+ if show_graph_title
654
+ @root.add_element( "text", {
655
+ "x" => (width / 2).to_s,
656
+ "y" => (title_font_size).to_s,
657
+ "class" => "mainTitle"
658
+ }).text = graph_title.to_s
659
+ end
660
+
661
+ if show_graph_subtitle
662
+ y_subtitle = show_graph_title ?
663
+ title_font_size + 10 :
664
+ subtitle_font_size
665
+ @root.add_element("text", {
666
+ "x" => (width / 2).to_s,
667
+ "y" => (y_subtitle).to_s,
668
+ "class" => "subTitle"
669
+ }).text = graph_subtitle.to_s
670
+ end
671
+
672
+ if show_x_title
673
+ y = @graph_height + @border_top + x_title_font_size
674
+ if show_x_labels
675
+ y += x_label_font_size + 5 if stagger_x_labels
676
+ y += x_label_font_size + 5
677
+ end
678
+ x = width / 2
679
+
680
+ @root.add_element("text", {
681
+ "x" => x.to_s,
682
+ "y" => y.to_s,
683
+ "class" => "xAxisTitle",
684
+ }).text = x_title.to_s
685
+ end
686
+
687
+ if show_y_title
688
+ x = y_title_font_size + (y_title_text_direction==:bt ? 3 : -3)
689
+ y = height / 2
690
+
691
+ text = @root.add_element("text", {
692
+ "x" => x.to_s,
693
+ "y" => y.to_s,
694
+ "class" => "yAxisTitle",
695
+ })
696
+ text.text = y_title.to_s
697
+ if y_title_text_direction == :bt
698
+ text.attributes["transform"] = "rotate( -90, #{x}, #{y} )"
699
+ else
700
+ text.attributes["transform"] = "rotate( 90, #{x}, #{y} )"
701
+ end
702
+ end
703
+ end
704
+
705
+ def keys
706
+ return @data.collect{ |d| d[:title] }
707
+ end
708
+
709
+ # Draws the legend on the graph
710
+ def draw_legend
711
+ if key
712
+ group = @root.add_element( "g" )
713
+
714
+ key_count = 0
715
+ for key_name in keys
716
+ y_offset = (KEY_BOX_SIZE * key_count) + (key_count * 5)
717
+ group.add_element( "rect", {
718
+ "x" => 0.to_s,
719
+ "y" => y_offset.to_s,
720
+ "width" => KEY_BOX_SIZE.to_s,
721
+ "height" => KEY_BOX_SIZE.to_s,
722
+ "class" => "key#{key_count+1}"
723
+ })
724
+ group.add_element( "text", {
725
+ "x" => (KEY_BOX_SIZE + 5).to_s,
726
+ "y" => (y_offset + KEY_BOX_SIZE).to_s,
727
+ "class" => "keyText"
728
+ }).text = key_name.to_s
729
+ key_count += 1
730
+ end
731
+
732
+ case key_position
733
+ when :right
734
+ x_offset = @graph_width + @border_left + 10
735
+ y_offset = @border_top + 20
736
+ when :bottom
737
+ x_offset = @border_left + 20
738
+ y_offset = @border_top + @graph_height + 5
739
+ if show_x_labels
740
+ max_x_label_height_px = rotate_x_labels ?
741
+ get_x_labels.max{|a,b|
742
+ a.length<=>b.length
743
+ }.length * x_label_font_size :
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 << ProcessingInstruction.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