annotate .svn/pristine/83/83300e07f9013be5c854520eee231d3e094963c6.svn-base @ 1298:4f746d8966dd redmine_2.3_integration

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