annotate .svn/pristine/5e/5ea13b75716eac3a96472978f3ec5fb6a73f65c5.svn-base @ 1519:afce8026aaeb redmine-2.4-integration

Merge from branch "live"
author Chris Cannam
date Tue, 09 Sep 2014 09:34:53 +0100
parents cbb26bc654de
children
rev   line source
Chris@909 1 require 'SVG/Graph/Graph'
Chris@909 2
Chris@909 3 module SVG
Chris@909 4 module Graph
Chris@909 5 # === Create presentation quality SVG line graphs easily
Chris@909 6 #
Chris@909 7 # = Synopsis
Chris@909 8 #
Chris@909 9 # require 'SVG/Graph/Line'
Chris@909 10 #
Chris@909 11 # fields = %w(Jan Feb Mar);
Chris@909 12 # data_sales_02 = [12, 45, 21]
Chris@909 13 # data_sales_03 = [15, 30, 40]
Chris@909 14 #
Chris@909 15 # graph = SVG::Graph::Line.new({
Chris@909 16 # :height => 500,
Chris@909 17 # :width => 300,
Chris@909 18 # :fields => fields,
Chris@909 19 # })
Chris@909 20 #
Chris@909 21 # graph.add_data({
Chris@909 22 # :data => data_sales_02,
Chris@909 23 # :title => 'Sales 2002',
Chris@909 24 # })
Chris@909 25 #
Chris@909 26 # graph.add_data({
Chris@909 27 # :data => data_sales_03,
Chris@909 28 # :title => 'Sales 2003',
Chris@909 29 # })
Chris@909 30 #
Chris@909 31 # print "Content-type: image/svg+xml\r\n\r\n";
Chris@909 32 # print graph.burn();
Chris@909 33 #
Chris@909 34 # = Description
Chris@909 35 #
Chris@909 36 # This object aims to allow you to easily create high quality
Chris@909 37 # SVG line graphs. You can either use the default style sheet
Chris@909 38 # or supply your own. Either way there are many options which can
Chris@909 39 # be configured to give you control over how the graph is
Chris@909 40 # generated - with or without a key, data elements at each point,
Chris@909 41 # title, subtitle etc.
Chris@909 42 #
Chris@909 43 # = Examples
Chris@909 44 #
Chris@909 45 # http://www.germane-software/repositories/public/SVG/test/single.rb
Chris@909 46 #
Chris@909 47 # = Notes
Chris@909 48 #
Chris@909 49 # The default stylesheet handles upto 10 data sets, if you
Chris@909 50 # use more you must create your own stylesheet and add the
Chris@909 51 # additional settings for the extra data sets. You will know
Chris@909 52 # if you go over 10 data sets as they will have no style and
Chris@909 53 # be in black.
Chris@909 54 #
Chris@909 55 # = See also
Chris@909 56 #
Chris@909 57 # * SVG::Graph::Graph
Chris@909 58 # * SVG::Graph::BarHorizontal
Chris@909 59 # * SVG::Graph::Bar
Chris@909 60 # * SVG::Graph::Pie
Chris@909 61 # * SVG::Graph::Plot
Chris@909 62 # * SVG::Graph::TimeSeries
Chris@909 63 #
Chris@909 64 # == Author
Chris@909 65 #
Chris@909 66 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
Chris@909 67 #
Chris@909 68 # Copyright 2004 Sean E. Russell
Chris@909 69 # This software is available under the Ruby license[LICENSE.txt]
Chris@909 70 #
Chris@909 71 class Line < SVG::Graph::Graph
Chris@909 72 # Show a small circle on the graph where the line
Chris@909 73 # goes from one point to the next.
Chris@909 74 attr_accessor :show_data_points
Chris@909 75 # Accumulates each data set. (i.e. Each point increased by sum of
Chris@909 76 # all previous series at same point). Default is 0, set to '1' to show.
Chris@909 77 attr_accessor :stacked
Chris@909 78 # Fill in the area under the plot if true
Chris@909 79 attr_accessor :area_fill
Chris@909 80
Chris@909 81 # The constructor takes a hash reference, fields (the names for each
Chris@909 82 # field on the X axis) MUST be set, all other values are defaulted to
Chris@909 83 # those shown above - with the exception of style_sheet which defaults
Chris@909 84 # to using the internal style sheet.
Chris@909 85 def initialize config
Chris@909 86 raise "fields was not supplied or is empty" unless config[:fields] &&
Chris@909 87 config[:fields].kind_of?(Array) &&
Chris@909 88 config[:fields].length > 0
Chris@909 89 super
Chris@909 90 end
Chris@909 91
Chris@909 92 # In addition to the defaults set in Graph::initialize, sets
Chris@909 93 # [show_data_points] true
Chris@909 94 # [show_data_values] true
Chris@909 95 # [stacked] false
Chris@909 96 # [area_fill] false
Chris@909 97 def set_defaults
Chris@909 98 init_with(
Chris@909 99 :show_data_points => true,
Chris@909 100 :show_data_values => true,
Chris@909 101 :stacked => false,
Chris@909 102 :area_fill => false
Chris@909 103 )
Chris@909 104
Chris@909 105 self.top_align = self.top_font = self.right_align = self.right_font = 1
Chris@909 106 end
Chris@909 107
Chris@909 108 protected
Chris@909 109
Chris@909 110 def max_value
Chris@909 111 max = 0
Chris@909 112
Chris@909 113 if (stacked == true) then
Chris@909 114 sums = Array.new(@config[:fields].length).fill(0)
Chris@909 115
Chris@909 116 @data.each do |data|
Chris@909 117 sums.each_index do |i|
Chris@909 118 sums[i] += data[:data][i].to_f
Chris@909 119 end
Chris@909 120 end
Chris@909 121
Chris@909 122 max = sums.max
Chris@909 123 else
Chris@909 124 max = @data.collect{|x| x[:data].max}.max
Chris@909 125 end
Chris@909 126
Chris@909 127 return max
Chris@909 128 end
Chris@909 129
Chris@909 130 def min_value
Chris@909 131 min = 0
Chris@909 132
Chris@909 133 if (min_scale_value.nil? == false) then
Chris@909 134 min = min_scale_value
Chris@909 135 elsif (stacked == true) then
Chris@909 136 min = @data[-1][:data].min
Chris@909 137 else
Chris@909 138 min = @data.collect{|x| x[:data].min}.min
Chris@909 139 end
Chris@909 140
Chris@909 141 return min
Chris@909 142 end
Chris@909 143
Chris@909 144 def get_x_labels
Chris@909 145 @config[:fields]
Chris@909 146 end
Chris@909 147
Chris@909 148 def calculate_left_margin
Chris@909 149 super
Chris@909 150 label_left = @config[:fields][0].length / 2 * font_size * 0.6
Chris@909 151 @border_left = label_left if label_left > @border_left
Chris@909 152 end
Chris@909 153
Chris@909 154 def get_y_labels
Chris@909 155 maxvalue = max_value
Chris@909 156 minvalue = min_value
Chris@909 157 range = maxvalue - minvalue
Chris@909 158 top_pad = range == 0 ? 10 : range / 20.0
Chris@909 159 scale_range = (maxvalue + top_pad) - minvalue
Chris@909 160
Chris@909 161 scale_division = scale_divisions || (scale_range / 10.0)
Chris@909 162
Chris@909 163 if scale_integers
Chris@909 164 scale_division = scale_division < 1 ? 1 : scale_division.round
Chris@909 165 end
Chris@909 166
Chris@909 167 rv = []
Chris@909 168 maxvalue = maxvalue%scale_division == 0 ?
Chris@909 169 maxvalue : maxvalue + scale_division
Chris@909 170 minvalue.step( maxvalue, scale_division ) {|v| rv << v}
Chris@909 171 return rv
Chris@909 172 end
Chris@909 173
Chris@909 174 def calc_coords(field, value, width = field_width, height = field_height)
Chris@909 175 coords = {:x => 0, :y => 0}
Chris@909 176 coords[:x] = width * field
Chris@909 177 coords[:y] = @graph_height - value * height
Chris@909 178
Chris@909 179 return coords
Chris@909 180 end
Chris@909 181
Chris@909 182 def draw_data
Chris@909 183 minvalue = min_value
Chris@909 184 fieldheight = (@graph_height.to_f - font_size*2*top_font) /
Chris@909 185 (get_y_labels.max - get_y_labels.min)
Chris@909 186 fieldwidth = field_width
Chris@909 187 line = @data.length
Chris@909 188
Chris@909 189 prev_sum = Array.new(@config[:fields].length).fill(0)
Chris@909 190 cum_sum = Array.new(@config[:fields].length).fill(-minvalue)
Chris@909 191
Chris@909 192 for data in @data.reverse
Chris@909 193 lpath = ""
Chris@909 194 apath = ""
Chris@909 195
Chris@909 196 if not stacked then cum_sum.fill(-minvalue) end
Chris@909 197
Chris@909 198 data[:data].each_index do |i|
Chris@909 199 cum_sum[i] += data[:data][i]
Chris@909 200
Chris@909 201 c = calc_coords(i, cum_sum[i], fieldwidth, fieldheight)
Chris@909 202
Chris@909 203 lpath << "#{c[:x]} #{c[:y]} "
Chris@909 204 end
Chris@909 205
Chris@909 206 if area_fill
Chris@909 207 if stacked then
Chris@909 208 (prev_sum.length - 1).downto 0 do |i|
Chris@909 209 c = calc_coords(i, prev_sum[i], fieldwidth, fieldheight)
Chris@909 210
Chris@909 211 apath << "#{c[:x]} #{c[:y]} "
Chris@909 212 end
Chris@909 213
Chris@909 214 c = calc_coords(0, prev_sum[0], fieldwidth, fieldheight)
Chris@909 215 else
Chris@909 216 apath = "V#@graph_height"
Chris@909 217 c = calc_coords(0, 0, fieldwidth, fieldheight)
Chris@909 218 end
Chris@909 219
Chris@909 220 @graph.add_element("path", {
Chris@909 221 "d" => "M#{c[:x]} #{c[:y]} L" + lpath + apath + "Z",
Chris@909 222 "class" => "fill#{line}"
Chris@909 223 })
Chris@909 224 end
Chris@909 225
Chris@909 226 @graph.add_element("path", {
Chris@909 227 "d" => "M0 #@graph_height L" + lpath,
Chris@909 228 "class" => "line#{line}"
Chris@909 229 })
Chris@909 230
Chris@909 231 if show_data_points || show_data_values
Chris@909 232 cum_sum.each_index do |i|
Chris@909 233 if show_data_points
Chris@909 234 @graph.add_element( "circle", {
Chris@909 235 "cx" => (fieldwidth * i).to_s,
Chris@909 236 "cy" => (@graph_height - cum_sum[i] * fieldheight).to_s,
Chris@909 237 "r" => "2.5",
Chris@909 238 "class" => "dataPoint#{line}"
Chris@909 239 })
Chris@909 240 end
Chris@909 241 make_datapoint_text(
Chris@909 242 fieldwidth * i,
Chris@909 243 @graph_height - cum_sum[i] * fieldheight - 6,
Chris@909 244 cum_sum[i] + minvalue
Chris@909 245 )
Chris@909 246 end
Chris@909 247 end
Chris@909 248
Chris@909 249 prev_sum = cum_sum.dup
Chris@909 250 line -= 1
Chris@909 251 end
Chris@909 252 end
Chris@909 253
Chris@909 254
Chris@909 255 def get_css
Chris@909 256 return <<EOL
Chris@909 257 /* default line styles */
Chris@909 258 .line1{
Chris@909 259 fill: none;
Chris@909 260 stroke: #ff0000;
Chris@909 261 stroke-width: 1px;
Chris@909 262 }
Chris@909 263 .line2{
Chris@909 264 fill: none;
Chris@909 265 stroke: #0000ff;
Chris@909 266 stroke-width: 1px;
Chris@909 267 }
Chris@909 268 .line3{
Chris@909 269 fill: none;
Chris@909 270 stroke: #00ff00;
Chris@909 271 stroke-width: 1px;
Chris@909 272 }
Chris@909 273 .line4{
Chris@909 274 fill: none;
Chris@909 275 stroke: #ffcc00;
Chris@909 276 stroke-width: 1px;
Chris@909 277 }
Chris@909 278 .line5{
Chris@909 279 fill: none;
Chris@909 280 stroke: #00ccff;
Chris@909 281 stroke-width: 1px;
Chris@909 282 }
Chris@909 283 .line6{
Chris@909 284 fill: none;
Chris@909 285 stroke: #ff00ff;
Chris@909 286 stroke-width: 1px;
Chris@909 287 }
Chris@909 288 .line7{
Chris@909 289 fill: none;
Chris@909 290 stroke: #00ffff;
Chris@909 291 stroke-width: 1px;
Chris@909 292 }
Chris@909 293 .line8{
Chris@909 294 fill: none;
Chris@909 295 stroke: #ffff00;
Chris@909 296 stroke-width: 1px;
Chris@909 297 }
Chris@909 298 .line9{
Chris@909 299 fill: none;
Chris@909 300 stroke: #ccc6666;
Chris@909 301 stroke-width: 1px;
Chris@909 302 }
Chris@909 303 .line10{
Chris@909 304 fill: none;
Chris@909 305 stroke: #663399;
Chris@909 306 stroke-width: 1px;
Chris@909 307 }
Chris@909 308 .line11{
Chris@909 309 fill: none;
Chris@909 310 stroke: #339900;
Chris@909 311 stroke-width: 1px;
Chris@909 312 }
Chris@909 313 .line12{
Chris@909 314 fill: none;
Chris@909 315 stroke: #9966FF;
Chris@909 316 stroke-width: 1px;
Chris@909 317 }
Chris@909 318 /* default fill styles */
Chris@909 319 .fill1{
Chris@909 320 fill: #cc0000;
Chris@909 321 fill-opacity: 0.2;
Chris@909 322 stroke: none;
Chris@909 323 }
Chris@909 324 .fill2{
Chris@909 325 fill: #0000cc;
Chris@909 326 fill-opacity: 0.2;
Chris@909 327 stroke: none;
Chris@909 328 }
Chris@909 329 .fill3{
Chris@909 330 fill: #00cc00;
Chris@909 331 fill-opacity: 0.2;
Chris@909 332 stroke: none;
Chris@909 333 }
Chris@909 334 .fill4{
Chris@909 335 fill: #ffcc00;
Chris@909 336 fill-opacity: 0.2;
Chris@909 337 stroke: none;
Chris@909 338 }
Chris@909 339 .fill5{
Chris@909 340 fill: #00ccff;
Chris@909 341 fill-opacity: 0.2;
Chris@909 342 stroke: none;
Chris@909 343 }
Chris@909 344 .fill6{
Chris@909 345 fill: #ff00ff;
Chris@909 346 fill-opacity: 0.2;
Chris@909 347 stroke: none;
Chris@909 348 }
Chris@909 349 .fill7{
Chris@909 350 fill: #00ffff;
Chris@909 351 fill-opacity: 0.2;
Chris@909 352 stroke: none;
Chris@909 353 }
Chris@909 354 .fill8{
Chris@909 355 fill: #ffff00;
Chris@909 356 fill-opacity: 0.2;
Chris@909 357 stroke: none;
Chris@909 358 }
Chris@909 359 .fill9{
Chris@909 360 fill: #cc6666;
Chris@909 361 fill-opacity: 0.2;
Chris@909 362 stroke: none;
Chris@909 363 }
Chris@909 364 .fill10{
Chris@909 365 fill: #663399;
Chris@909 366 fill-opacity: 0.2;
Chris@909 367 stroke: none;
Chris@909 368 }
Chris@909 369 .fill11{
Chris@909 370 fill: #339900;
Chris@909 371 fill-opacity: 0.2;
Chris@909 372 stroke: none;
Chris@909 373 }
Chris@909 374 .fill12{
Chris@909 375 fill: #9966FF;
Chris@909 376 fill-opacity: 0.2;
Chris@909 377 stroke: none;
Chris@909 378 }
Chris@909 379 /* default line styles */
Chris@909 380 .key1,.dataPoint1{
Chris@909 381 fill: #ff0000;
Chris@909 382 stroke: none;
Chris@909 383 stroke-width: 1px;
Chris@909 384 }
Chris@909 385 .key2,.dataPoint2{
Chris@909 386 fill: #0000ff;
Chris@909 387 stroke: none;
Chris@909 388 stroke-width: 1px;
Chris@909 389 }
Chris@909 390 .key3,.dataPoint3{
Chris@909 391 fill: #00ff00;
Chris@909 392 stroke: none;
Chris@909 393 stroke-width: 1px;
Chris@909 394 }
Chris@909 395 .key4,.dataPoint4{
Chris@909 396 fill: #ffcc00;
Chris@909 397 stroke: none;
Chris@909 398 stroke-width: 1px;
Chris@909 399 }
Chris@909 400 .key5,.dataPoint5{
Chris@909 401 fill: #00ccff;
Chris@909 402 stroke: none;
Chris@909 403 stroke-width: 1px;
Chris@909 404 }
Chris@909 405 .key6,.dataPoint6{
Chris@909 406 fill: #ff00ff;
Chris@909 407 stroke: none;
Chris@909 408 stroke-width: 1px;
Chris@909 409 }
Chris@909 410 .key7,.dataPoint7{
Chris@909 411 fill: #00ffff;
Chris@909 412 stroke: none;
Chris@909 413 stroke-width: 1px;
Chris@909 414 }
Chris@909 415 .key8,.dataPoint8{
Chris@909 416 fill: #ffff00;
Chris@909 417 stroke: none;
Chris@909 418 stroke-width: 1px;
Chris@909 419 }
Chris@909 420 .key9,.dataPoint9{
Chris@909 421 fill: #cc6666;
Chris@909 422 stroke: none;
Chris@909 423 stroke-width: 1px;
Chris@909 424 }
Chris@909 425 .key10,.dataPoint10{
Chris@909 426 fill: #663399;
Chris@909 427 stroke: none;
Chris@909 428 stroke-width: 1px;
Chris@909 429 }
Chris@909 430 .key11,.dataPoint11{
Chris@909 431 fill: #339900;
Chris@909 432 stroke: none;
Chris@909 433 stroke-width: 1px;
Chris@909 434 }
Chris@909 435 .key12,.dataPoint12{
Chris@909 436 fill: #9966FF;
Chris@909 437 stroke: none;
Chris@909 438 stroke-width: 1px;
Chris@909 439 }
Chris@909 440 EOL
Chris@909 441 end
Chris@909 442 end
Chris@909 443 end
Chris@909 444 end