ruport 0.4.23 → 0.4.99

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