annotate lib/SVG/Graph/Line.rb @ 8:0c83d98252d9 yuya

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