To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
root / lib / SVG / Graph / Plot.rb @ 1297:0a574315af3e
History | View | Annotate | Download (11.8 KB)
| 1 | 1294:3e4c3460b6ca | Chris | require 'SVG/Graph/Graph'
|
|---|---|---|---|
| 2 | |||
| 3 | module SVG |
||
| 4 | module Graph |
||
| 5 | # === For creating SVG plots of scalar data
|
||
| 6 | #
|
||
| 7 | # = Synopsis
|
||
| 8 | #
|
||
| 9 | # require 'SVG/Graph/Plot'
|
||
| 10 | #
|
||
| 11 | # # Data sets are x,y pairs
|
||
| 12 | # # Note that multiple data sets can differ in length, and that the
|
||
| 13 | # # data in the datasets needn't be in order; they will be ordered
|
||
| 14 | # # by the plot along the X-axis.
|
||
| 15 | # projection = [
|
||
| 16 | # 6, 11, 0, 5, 18, 7, 1, 11, 13, 9, 1, 2, 19, 0, 3, 13,
|
||
| 17 | # 7, 9
|
||
| 18 | # ]
|
||
| 19 | # actual = [
|
||
| 20 | # 0, 18, 8, 15, 9, 4, 18, 14, 10, 2, 11, 6, 14, 12,
|
||
| 21 | # 15, 6, 4, 17, 2, 12
|
||
| 22 | # ]
|
||
| 23 | #
|
||
| 24 | # graph = SVG::Graph::Plot.new({
|
||
| 25 | # :height => 500,
|
||
| 26 | # :width => 300,
|
||
| 27 | # :key => true,
|
||
| 28 | # :scale_x_integers => true,
|
||
| 29 | # :scale_y_integerrs => true,
|
||
| 30 | # })
|
||
| 31 | #
|
||
| 32 | # graph.add_data({
|
||
| 33 | # :data => projection
|
||
| 34 | # :title => 'Projected',
|
||
| 35 | # })
|
||
| 36 | #
|
||
| 37 | # graph.add_data({
|
||
| 38 | # :data => actual,
|
||
| 39 | # :title => 'Actual',
|
||
| 40 | # })
|
||
| 41 | #
|
||
| 42 | # print graph.burn()
|
||
| 43 | #
|
||
| 44 | # = Description
|
||
| 45 | #
|
||
| 46 | # Produces a graph of scalar data.
|
||
| 47 | #
|
||
| 48 | # This object aims to allow you to easily create high quality
|
||
| 49 | # SVG[http://www.w3c.org/tr/svg] scalar plots. You can either use the
|
||
| 50 | # default style sheet or supply your own. Either way there are many options
|
||
| 51 | # which can be configured to give you control over how the graph is
|
||
| 52 | # generated - with or without a key, data elements at each point, title,
|
||
| 53 | # subtitle etc.
|
||
| 54 | #
|
||
| 55 | # = Examples
|
||
| 56 | #
|
||
| 57 | # http://www.germane-software/repositories/public/SVG/test/plot.rb
|
||
| 58 | #
|
||
| 59 | # = Notes
|
||
| 60 | #
|
||
| 61 | # The default stylesheet handles upto 10 data sets, if you
|
||
| 62 | # use more you must create your own stylesheet and add the
|
||
| 63 | # additional settings for the extra data sets. You will know
|
||
| 64 | # if you go over 10 data sets as they will have no style and
|
||
| 65 | # be in black.
|
||
| 66 | #
|
||
| 67 | # Unlike the other types of charts, data sets must contain x,y pairs:
|
||
| 68 | #
|
||
| 69 | # [ 1, 2 ] # A data set with 1 point: (1,2)
|
||
| 70 | # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)
|
||
| 71 | #
|
||
| 72 | # = See also
|
||
| 73 | #
|
||
| 74 | # * SVG::Graph::Graph
|
||
| 75 | # * SVG::Graph::BarHorizontal
|
||
| 76 | # * SVG::Graph::Bar
|
||
| 77 | # * SVG::Graph::Line
|
||
| 78 | # * SVG::Graph::Pie
|
||
| 79 | # * SVG::Graph::TimeSeries
|
||
| 80 | #
|
||
| 81 | # == Author
|
||
| 82 | #
|
||
| 83 | # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
|
||
| 84 | #
|
||
| 85 | # Copyright 2004 Sean E. Russell
|
||
| 86 | # This software is available under the Ruby license[LICENSE.txt]
|
||
| 87 | #
|
||
| 88 | class Plot < Graph |
||
| 89 | |||
| 90 | # In addition to the defaults set by Graph::initialize, sets
|
||
| 91 | # [show_data_values] true
|
||
| 92 | # [show_data_points] true
|
||
| 93 | # [area_fill] false
|
||
| 94 | # [stacked] false
|
||
| 95 | def set_defaults |
||
| 96 | init_with( |
||
| 97 | :show_data_values => true, |
||
| 98 | :show_data_points => true, |
||
| 99 | :area_fill => false, |
||
| 100 | :stacked => false |
||
| 101 | ) |
||
| 102 | self.top_align = self.right_align = self.top_font = self.right_font = 1 |
||
| 103 | end
|
||
| 104 | |||
| 105 | # Determines the scaling for the X axis divisions.
|
||
| 106 | #
|
||
| 107 | # graph.scale_x_divisions = 2
|
||
| 108 | #
|
||
| 109 | # would cause the graph to attempt to generate labels stepped by 2; EG:
|
||
| 110 | # 0,2,4,6,8...
|
||
| 111 | attr_accessor :scale_x_divisions
|
||
| 112 | # Determines the scaling for the Y axis divisions.
|
||
| 113 | #
|
||
| 114 | # graph.scale_y_divisions = 0.5
|
||
| 115 | #
|
||
| 116 | # would cause the graph to attempt to generate labels stepped by 0.5; EG:
|
||
| 117 | # 0, 0.5, 1, 1.5, 2, ...
|
||
| 118 | attr_accessor :scale_y_divisions
|
||
| 119 | # Make the X axis labels integers
|
||
| 120 | attr_accessor :scale_x_integers
|
||
| 121 | # Make the Y axis labels integers
|
||
| 122 | attr_accessor :scale_y_integers
|
||
| 123 | # Fill the area under the line
|
||
| 124 | attr_accessor :area_fill
|
||
| 125 | # Show a small circle on the graph where the line
|
||
| 126 | # goes from one point to the next.
|
||
| 127 | attr_accessor :show_data_points
|
||
| 128 | # Set the minimum value of the X axis
|
||
| 129 | attr_accessor :min_x_value
|
||
| 130 | # Set the minimum value of the Y axis
|
||
| 131 | attr_accessor :min_y_value
|
||
| 132 | |||
| 133 | |||
| 134 | # Adds data to the plot. The data must be in X,Y pairs; EG
|
||
| 135 | # [ 1, 2 ] # A data set with 1 point: (1,2)
|
||
| 136 | # [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)
|
||
| 137 | def add_data data |
||
| 138 | @data = [] unless @data |
||
| 139 | |||
| 140 | raise "No data provided by #{conf.inspect}" unless data[:data] and |
||
| 141 | data[:data].kind_of? Array |
||
| 142 | raise "Data supplied must be x,y pairs! "+
|
||
| 143 | "The data provided contained an odd set of "+
|
||
| 144 | "data points" unless data[:data].length % 2 == 0 |
||
| 145 | return if data[:data].length == 0 |
||
| 146 | |||
| 147 | x = [] |
||
| 148 | y = [] |
||
| 149 | data[:data].each_index {|i|
|
||
| 150 | (i%2 == 0 ? x : y) << data[:data][i] |
||
| 151 | } |
||
| 152 | sort( x, y ) |
||
| 153 | data[:data] = [x,y]
|
||
| 154 | @data << data
|
||
| 155 | end
|
||
| 156 | |||
| 157 | |||
| 158 | protected |
||
| 159 | |||
| 160 | def keys |
||
| 161 | @data.collect{ |x| x[:title] } |
||
| 162 | end
|
||
| 163 | |||
| 164 | def calculate_left_margin |
||
| 165 | super
|
||
| 166 | label_left = get_x_labels[0].to_s.length / 2 * font_size * 0.6 |
||
| 167 | @border_left = label_left if label_left > @border_left |
||
| 168 | end
|
||
| 169 | |||
| 170 | def calculate_right_margin |
||
| 171 | super
|
||
| 172 | label_right = get_x_labels[-1].to_s.length / 2 * font_size * 0.6 |
||
| 173 | @border_right = label_right if label_right > @border_right |
||
| 174 | end
|
||
| 175 | |||
| 176 | |||
| 177 | X = 0 |
||
| 178 | Y = 1 |
||
| 179 | def x_range |
||
| 180 | max_value = @data.collect{|x| x[:data][X][-1] }.max |
||
| 181 | min_value = @data.collect{|x| x[:data][X][0] }.min |
||
| 182 | min_value = min_value<min_x_value ? min_value : min_x_value if min_x_value
|
||
| 183 | |||
| 184 | range = max_value - min_value |
||
| 185 | right_pad = range == 0 ? 10 : range / 20.0 |
||
| 186 | scale_range = (max_value + right_pad) - min_value |
||
| 187 | |||
| 188 | scale_division = scale_x_divisions || (scale_range / 10.0)
|
||
| 189 | |||
| 190 | if scale_x_integers
|
||
| 191 | scale_division = scale_division < 1 ? 1 : scale_division.round |
||
| 192 | end
|
||
| 193 | |||
| 194 | [min_value, max_value, scale_division] |
||
| 195 | end
|
||
| 196 | |||
| 197 | def get_x_values |
||
| 198 | min_value, max_value, scale_division = x_range |
||
| 199 | rv = [] |
||
| 200 | min_value.step( max_value, scale_division ) {|v| rv << v}
|
||
| 201 | return rv
|
||
| 202 | end
|
||
| 203 | alias :get_x_labels :get_x_values |
||
| 204 | |||
| 205 | def field_width |
||
| 206 | values = get_x_values |
||
| 207 | max = @data.collect{|x| x[:data][X][-1]}.max |
||
| 208 | dx = (max - values[-1]).to_f / (values[-1] - values[-2]) |
||
| 209 | (@graph_width.to_f - font_size*2*right_font) / |
||
| 210 | (values.length + dx - right_align) |
||
| 211 | end
|
||
| 212 | |||
| 213 | |||
| 214 | def y_range |
||
| 215 | max_value = @data.collect{|x| x[:data][Y].max }.max |
||
| 216 | min_value = @data.collect{|x| x[:data][Y].min }.min |
||
| 217 | min_value = min_value<min_y_value ? min_value : min_y_value if min_y_value
|
||
| 218 | |||
| 219 | range = max_value - min_value |
||
| 220 | top_pad = range == 0 ? 10 : range / 20.0 |
||
| 221 | scale_range = (max_value + top_pad) - min_value |
||
| 222 | |||
| 223 | scale_division = scale_y_divisions || (scale_range / 10.0)
|
||
| 224 | |||
| 225 | if scale_y_integers
|
||
| 226 | scale_division = scale_division < 1 ? 1 : scale_division.round |
||
| 227 | end
|
||
| 228 | |||
| 229 | return [min_value, max_value, scale_division]
|
||
| 230 | end
|
||
| 231 | |||
| 232 | def get_y_values |
||
| 233 | min_value, max_value, scale_division = y_range |
||
| 234 | rv = [] |
||
| 235 | min_value.step( max_value, scale_division ) {|v| rv << v}
|
||
| 236 | return rv
|
||
| 237 | end
|
||
| 238 | alias :get_y_labels :get_y_values |
||
| 239 | |||
| 240 | def field_height |
||
| 241 | values = get_y_values |
||
| 242 | max = @data.collect{|x| x[:data][Y].max }.max |
||
| 243 | if values.length == 1 |
||
| 244 | dx = values[-1]
|
||
| 245 | else
|
||
| 246 | dx = (max - values[-1]).to_f / (values[-1] - values[-2]) |
||
| 247 | end
|
||
| 248 | (@graph_height.to_f - font_size*2*top_font) / |
||
| 249 | (values.length + dx - top_align) |
||
| 250 | end
|
||
| 251 | |||
| 252 | def draw_data |
||
| 253 | line = 1
|
||
| 254 | |||
| 255 | x_min, x_max, x_div = x_range |
||
| 256 | y_min, y_max, y_div = y_range |
||
| 257 | x_step = (@graph_width.to_f - font_size*2) / (x_max-x_min) |
||
| 258 | y_step = (@graph_height.to_f - font_size*2) / (y_max-y_min) |
||
| 259 | |||
| 260 | for data in @data |
||
| 261 | x_points = data[:data][X] |
||
| 262 | y_points = data[:data][Y] |
||
| 263 | |||
| 264 | lpath = "L"
|
||
| 265 | x_start = 0
|
||
| 266 | y_start = 0
|
||
| 267 | x_points.each_index { |idx|
|
||
| 268 | x = (x_points[idx] - x_min) * x_step |
||
| 269 | y = @graph_height - (y_points[idx] - y_min) * y_step
|
||
| 270 | x_start, y_start = x,y if idx == 0 |
||
| 271 | lpath << "#{x} #{y} "
|
||
| 272 | } |
||
| 273 | |||
| 274 | if area_fill
|
||
| 275 | @graph.add_element( "path", { |
||
| 276 | "d" => "M#{x_start} #@graph_height #{lpath} V#@graph_height Z", |
||
| 277 | "class" => "fill#{line}" |
||
| 278 | }) |
||
| 279 | end
|
||
| 280 | |||
| 281 | @graph.add_element( "path", { |
||
| 282 | "d" => "M#{x_start} #{y_start} #{lpath}", |
||
| 283 | "class" => "line#{line}" |
||
| 284 | }) |
||
| 285 | |||
| 286 | if show_data_points || show_data_values
|
||
| 287 | x_points.each_index { |idx|
|
||
| 288 | x = (x_points[idx] - x_min) * x_step |
||
| 289 | y = @graph_height - (y_points[idx] - y_min) * y_step
|
||
| 290 | if show_data_points
|
||
| 291 | @graph.add_element( "circle", { |
||
| 292 | "cx" => x.to_s,
|
||
| 293 | "cy" => y.to_s,
|
||
| 294 | "r" => "2.5", |
||
| 295 | "class" => "dataPoint#{line}" |
||
| 296 | }) |
||
| 297 | add_popup(x, y, format( x_points[idx], y_points[idx] )) if add_popups
|
||
| 298 | end
|
||
| 299 | make_datapoint_text( x, y-6, y_points[idx] ) if show_data_values |
||
| 300 | } |
||
| 301 | end
|
||
| 302 | line += 1
|
||
| 303 | end
|
||
| 304 | end
|
||
| 305 | |||
| 306 | def format x, y |
||
| 307 | "(#{(x * 100).to_i / 100}, #{(y * 100).to_i / 100})"
|
||
| 308 | end
|
||
| 309 | |||
| 310 | def get_css |
||
| 311 | return <<EOL |
||
| 312 | /* default line styles */
|
||
| 313 | .line1{
|
||
| 314 | fill: none;
|
||
| 315 | stroke: #ff0000;
|
||
| 316 | stroke-width: 1px;
|
||
| 317 | }
|
||
| 318 | .line2{
|
||
| 319 | fill: none;
|
||
| 320 | stroke: #0000ff;
|
||
| 321 | stroke-width: 1px;
|
||
| 322 | }
|
||
| 323 | .line3{
|
||
| 324 | fill: none;
|
||
| 325 | stroke: #00ff00;
|
||
| 326 | stroke-width: 1px;
|
||
| 327 | }
|
||
| 328 | .line4{
|
||
| 329 | fill: none;
|
||
| 330 | stroke: #ffcc00;
|
||
| 331 | stroke-width: 1px;
|
||
| 332 | }
|
||
| 333 | .line5{
|
||
| 334 | fill: none;
|
||
| 335 | stroke: #00ccff;
|
||
| 336 | stroke-width: 1px;
|
||
| 337 | }
|
||
| 338 | .line6{
|
||
| 339 | fill: none;
|
||
| 340 | stroke: #ff00ff;
|
||
| 341 | stroke-width: 1px;
|
||
| 342 | }
|
||
| 343 | .line7{
|
||
| 344 | fill: none;
|
||
| 345 | stroke: #00ffff;
|
||
| 346 | stroke-width: 1px;
|
||
| 347 | }
|
||
| 348 | .line8{
|
||
| 349 | fill: none;
|
||
| 350 | stroke: #ffff00;
|
||
| 351 | stroke-width: 1px;
|
||
| 352 | }
|
||
| 353 | .line9{
|
||
| 354 | fill: none;
|
||
| 355 | stroke: #ccc6666;
|
||
| 356 | stroke-width: 1px;
|
||
| 357 | }
|
||
| 358 | .line10{
|
||
| 359 | fill: none;
|
||
| 360 | stroke: #663399;
|
||
| 361 | stroke-width: 1px;
|
||
| 362 | }
|
||
| 363 | .line11{
|
||
| 364 | fill: none;
|
||
| 365 | stroke: #339900;
|
||
| 366 | stroke-width: 1px;
|
||
| 367 | }
|
||
| 368 | .line12{
|
||
| 369 | fill: none;
|
||
| 370 | stroke: #9966FF;
|
||
| 371 | stroke-width: 1px;
|
||
| 372 | }
|
||
| 373 | /* default fill styles */
|
||
| 374 | .fill1{
|
||
| 375 | fill: #cc0000;
|
||
| 376 | fill-opacity: 0.2;
|
||
| 377 | stroke: none;
|
||
| 378 | }
|
||
| 379 | .fill2{
|
||
| 380 | fill: #0000cc;
|
||
| 381 | fill-opacity: 0.2;
|
||
| 382 | stroke: none;
|
||
| 383 | }
|
||
| 384 | .fill3{
|
||
| 385 | fill: #00cc00;
|
||
| 386 | fill-opacity: 0.2;
|
||
| 387 | stroke: none;
|
||
| 388 | }
|
||
| 389 | .fill4{
|
||
| 390 | fill: #ffcc00;
|
||
| 391 | fill-opacity: 0.2;
|
||
| 392 | stroke: none;
|
||
| 393 | }
|
||
| 394 | .fill5{
|
||
| 395 | fill: #00ccff;
|
||
| 396 | fill-opacity: 0.2;
|
||
| 397 | stroke: none;
|
||
| 398 | }
|
||
| 399 | .fill6{
|
||
| 400 | fill: #ff00ff;
|
||
| 401 | fill-opacity: 0.2;
|
||
| 402 | stroke: none;
|
||
| 403 | }
|
||
| 404 | .fill7{
|
||
| 405 | fill: #00ffff;
|
||
| 406 | fill-opacity: 0.2;
|
||
| 407 | stroke: none;
|
||
| 408 | }
|
||
| 409 | .fill8{
|
||
| 410 | fill: #ffff00;
|
||
| 411 | fill-opacity: 0.2;
|
||
| 412 | stroke: none;
|
||
| 413 | }
|
||
| 414 | .fill9{
|
||
| 415 | fill: #cc6666;
|
||
| 416 | fill-opacity: 0.2;
|
||
| 417 | stroke: none;
|
||
| 418 | }
|
||
| 419 | .fill10{
|
||
| 420 | fill: #663399;
|
||
| 421 | fill-opacity: 0.2;
|
||
| 422 | stroke: none;
|
||
| 423 | }
|
||
| 424 | .fill11{
|
||
| 425 | fill: #339900;
|
||
| 426 | fill-opacity: 0.2;
|
||
| 427 | stroke: none;
|
||
| 428 | }
|
||
| 429 | .fill12{
|
||
| 430 | fill: #9966FF;
|
||
| 431 | fill-opacity: 0.2;
|
||
| 432 | stroke: none;
|
||
| 433 | }
|
||
| 434 | /* default line styles */
|
||
| 435 | .key1,.dataPoint1{
|
||
| 436 | fill: #ff0000;
|
||
| 437 | stroke: none;
|
||
| 438 | stroke-width: 1px;
|
||
| 439 | }
|
||
| 440 | .key2,.dataPoint2{
|
||
| 441 | fill: #0000ff;
|
||
| 442 | stroke: none;
|
||
| 443 | stroke-width: 1px;
|
||
| 444 | }
|
||
| 445 | .key3,.dataPoint3{
|
||
| 446 | fill: #00ff00;
|
||
| 447 | stroke: none;
|
||
| 448 | stroke-width: 1px;
|
||
| 449 | }
|
||
| 450 | .key4,.dataPoint4{
|
||
| 451 | fill: #ffcc00;
|
||
| 452 | stroke: none;
|
||
| 453 | stroke-width: 1px;
|
||
| 454 | }
|
||
| 455 | .key5,.dataPoint5{
|
||
| 456 | fill: #00ccff;
|
||
| 457 | stroke: none;
|
||
| 458 | stroke-width: 1px;
|
||
| 459 | }
|
||
| 460 | .key6,.dataPoint6{
|
||
| 461 | fill: #ff00ff;
|
||
| 462 | stroke: none;
|
||
| 463 | stroke-width: 1px;
|
||
| 464 | }
|
||
| 465 | .key7,.dataPoint7{
|
||
| 466 | fill: #00ffff;
|
||
| 467 | stroke: none;
|
||
| 468 | stroke-width: 1px;
|
||
| 469 | }
|
||
| 470 | .key8,.dataPoint8{
|
||
| 471 | fill: #ffff00;
|
||
| 472 | stroke: none;
|
||
| 473 | stroke-width: 1px;
|
||
| 474 | }
|
||
| 475 | .key9,.dataPoint9{
|
||
| 476 | fill: #cc6666;
|
||
| 477 | stroke: none;
|
||
| 478 | stroke-width: 1px;
|
||
| 479 | }
|
||
| 480 | .key10,.dataPoint10{
|
||
| 481 | fill: #663399;
|
||
| 482 | stroke: none;
|
||
| 483 | stroke-width: 1px;
|
||
| 484 | }
|
||
| 485 | .key11,.dataPoint11{
|
||
| 486 | fill: #339900;
|
||
| 487 | stroke: none;
|
||
| 488 | stroke-width: 1px;
|
||
| 489 | }
|
||
| 490 | .key12,.dataPoint12{
|
||
| 491 | fill: #9966FF;
|
||
| 492 | stroke: none;
|
||
| 493 | stroke-width: 1px;
|
||
| 494 | }
|
||
| 495 | EOL
|
||
| 496 | end
|
||
| 497 | |||
| 498 | end
|
||
| 499 | end
|
||
| 500 | end |