Mercurial > hg > soundsoftware-site
comparison lib/SVG/Graph/Graph.rb @ 0:513646585e45
* Import Redmine trunk SVN rev 3859
author | Chris Cannam |
---|---|
date | Fri, 23 Jul 2010 15:52:44 +0100 |
parents | |
children | 3e4c3460b6ca |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:513646585e45 |
---|---|
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 respond_to? :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 respond_to? :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+"=").to_sym, value ) if respond_to? key.to_sym | |
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.object_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.object_id}).setAttribute('visibility', 'visible' )", | |
433 "onmouseout" => | |
434 "document.getElementById(#{t.object_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 = (not rotate_x_labels) ? | |
450 x_label_font_size : | |
451 get_x_labels.max{|a,b| | |
452 a.to_s.length<=>b.to_s.length | |
453 }.to_s.length * x_label_font_size * 0.6 | |
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 = (not rotate_x_labels) ? | |
741 x_label_font_size : | |
742 get_x_labels.max{|a,b| | |
743 a.to_s.length<=>b.to_s.length | |
744 }.to_s.length * x_label_font_size * 0.6 | |
745 x_label_font_size | |
746 y_offset += max_x_label_height_px | |
747 y_offset += max_x_label_height_px + 5 if stagger_x_labels | |
748 end | |
749 y_offset += x_title_font_size + 5 if show_x_title | |
750 end | |
751 group.attributes["transform"] = "translate(#{x_offset} #{y_offset})" | |
752 end | |
753 end | |
754 | |
755 | |
756 private | |
757 | |
758 def sort_multiple( arrys, lo=0, hi=arrys[0].length-1 ) | |
759 if lo < hi | |
760 p = partition(arrys,lo,hi) | |
761 sort_multiple(arrys, lo, p-1) | |
762 sort_multiple(arrys, p+1, hi) | |
763 end | |
764 arrys | |
765 end | |
766 | |
767 def partition( arrys, lo, hi ) | |
768 p = arrys[0][lo] | |
769 l = lo | |
770 z = lo+1 | |
771 while z <= hi | |
772 if arrys[0][z] < p | |
773 l += 1 | |
774 arrys.each { |arry| arry[z], arry[l] = arry[l], arry[z] } | |
775 end | |
776 z += 1 | |
777 end | |
778 arrys.each { |arry| arry[lo], arry[l] = arry[l], arry[lo] } | |
779 l | |
780 end | |
781 | |
782 def style | |
783 if no_css | |
784 styles = parse_css | |
785 @root.elements.each("//*[@class]") { |el| | |
786 cl = el.attributes["class"] | |
787 style = styles[cl] | |
788 style += el.attributes["style"] if el.attributes["style"] | |
789 el.attributes["style"] = style | |
790 } | |
791 end | |
792 end | |
793 | |
794 def parse_css | |
795 css = get_style | |
796 rv = {} | |
797 while css =~ /^(\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{/m | |
798 names_orig = names = $1 | |
799 css = $' | |
800 css =~ /([^}]+)\}/m | |
801 content = $1 | |
802 css = $' | |
803 | |
804 nms = [] | |
805 while names =~ /^\s*,?\s*\.(\w+)/ | |
806 nms << $1 | |
807 names = $' | |
808 end | |
809 | |
810 content = content.tr( "\n\t", " ") | |
811 for name in nms | |
812 current = rv[name] | |
813 current = current ? current+"; "+content : content | |
814 rv[name] = current.strip.squeeze(" ") | |
815 end | |
816 end | |
817 return rv | |
818 end | |
819 | |
820 | |
821 # Override and place code to add defs here | |
822 def add_defs defs | |
823 end | |
824 | |
825 | |
826 def start_svg | |
827 # Base document | |
828 @doc = Document.new | |
829 @doc << XMLDecl.new | |
830 @doc << DocType.new( %q{svg PUBLIC "-//W3C//DTD SVG 1.0//EN" } + | |
831 %q{"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"} ) | |
832 if style_sheet && style_sheet != '' | |
833 @doc << Instruction.new( "xml-stylesheet", | |
834 %Q{href="#{style_sheet}" type="text/css"} ) | |
835 end | |
836 @root = @doc.add_element( "svg", { | |
837 "width" => width.to_s, | |
838 "height" => height.to_s, | |
839 "viewBox" => "0 0 #{width} #{height}", | |
840 "xmlns" => "http://www.w3.org/2000/svg", | |
841 "xmlns:xlink" => "http://www.w3.org/1999/xlink", | |
842 "xmlns:a3" => "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/", | |
843 "a3:scriptImplementation" => "Adobe" | |
844 }) | |
845 @root << Comment.new( " "+"\\"*66 ) | |
846 @root << Comment.new( " Created with SVG::Graph " ) | |
847 @root << Comment.new( " SVG::Graph by Sean E. Russell " ) | |
848 @root << Comment.new( " Losely based on SVG::TT::Graph for Perl by"+ | |
849 " Leo Lapworth & Stephan Morgan " ) | |
850 @root << Comment.new( " "+"/"*66 ) | |
851 | |
852 defs = @root.add_element( "defs" ) | |
853 add_defs defs | |
854 if not(style_sheet && style_sheet != '') and !no_css | |
855 @root << Comment.new(" include default stylesheet if none specified ") | |
856 style = defs.add_element( "style", {"type"=>"text/css"} ) | |
857 style << CData.new( get_style ) | |
858 end | |
859 | |
860 @root << Comment.new( "SVG Background" ) | |
861 @root.add_element( "rect", { | |
862 "width" => width.to_s, | |
863 "height" => height.to_s, | |
864 "x" => "0", | |
865 "y" => "0", | |
866 "class" => "svgBackground" | |
867 }) | |
868 end | |
869 | |
870 | |
871 def calculate_graph_dimensions | |
872 calculate_left_margin | |
873 calculate_right_margin | |
874 calculate_bottom_margin | |
875 calculate_top_margin | |
876 @graph_width = width - @border_left - @border_right | |
877 @graph_height = height - @border_top - @border_bottom | |
878 end | |
879 | |
880 def get_style | |
881 return <<EOL | |
882 /* Copy from here for external style sheet */ | |
883 .svgBackground{ | |
884 fill:#ffffff; | |
885 } | |
886 .graphBackground{ | |
887 fill:#f0f0f0; | |
888 } | |
889 | |
890 /* graphs titles */ | |
891 .mainTitle{ | |
892 text-anchor: middle; | |
893 fill: #000000; | |
894 font-size: #{title_font_size}px; | |
895 font-family: "Arial", sans-serif; | |
896 font-weight: normal; | |
897 } | |
898 .subTitle{ | |
899 text-anchor: middle; | |
900 fill: #999999; | |
901 font-size: #{subtitle_font_size}px; | |
902 font-family: "Arial", sans-serif; | |
903 font-weight: normal; | |
904 } | |
905 | |
906 .axis{ | |
907 stroke: #000000; | |
908 stroke-width: 1px; | |
909 } | |
910 | |
911 .guideLines{ | |
912 stroke: #666666; | |
913 stroke-width: 1px; | |
914 stroke-dasharray: 5 5; | |
915 } | |
916 | |
917 .xAxisLabels{ | |
918 text-anchor: middle; | |
919 fill: #000000; | |
920 font-size: #{x_label_font_size}px; | |
921 font-family: "Arial", sans-serif; | |
922 font-weight: normal; | |
923 } | |
924 | |
925 .yAxisLabels{ | |
926 text-anchor: end; | |
927 fill: #000000; | |
928 font-size: #{y_label_font_size}px; | |
929 font-family: "Arial", sans-serif; | |
930 font-weight: normal; | |
931 } | |
932 | |
933 .xAxisTitle{ | |
934 text-anchor: middle; | |
935 fill: #ff0000; | |
936 font-size: #{x_title_font_size}px; | |
937 font-family: "Arial", sans-serif; | |
938 font-weight: normal; | |
939 } | |
940 | |
941 .yAxisTitle{ | |
942 fill: #ff0000; | |
943 text-anchor: middle; | |
944 font-size: #{y_title_font_size}px; | |
945 font-family: "Arial", sans-serif; | |
946 font-weight: normal; | |
947 } | |
948 | |
949 .dataPointLabel{ | |
950 fill: #000000; | |
951 text-anchor:middle; | |
952 font-size: 10px; | |
953 font-family: "Arial", sans-serif; | |
954 font-weight: normal; | |
955 } | |
956 | |
957 .staggerGuideLine{ | |
958 fill: none; | |
959 stroke: #000000; | |
960 stroke-width: 0.5px; | |
961 } | |
962 | |
963 #{get_css} | |
964 | |
965 .keyText{ | |
966 fill: #000000; | |
967 text-anchor:start; | |
968 font-size: #{key_font_size}px; | |
969 font-family: "Arial", sans-serif; | |
970 font-weight: normal; | |
971 } | |
972 /* End copy for external style sheet */ | |
973 EOL | |
974 end | |
975 | |
976 end | |
977 end | |
978 end |