To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
root / .svn / pristine / 08 / 08ad4f57c782749abd613605456ebd0acf953d61.svn-base @ 1297:0a574315af3e
History | View | Annotate | Download (29.6 KB)
| 1 | 1296:038ba2d95de8 | Chris | 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 |