annotate .svn/pristine/56/56f658e30dbfa38e86f4cb2d7cdd7b971352ed59.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/Plot'
Chris@909 2 require 'parsedate'
Chris@909 3
Chris@909 4 module SVG
Chris@909 5 module Graph
Chris@909 6 # === For creating SVG plots of scalar temporal data
Chris@909 7 #
Chris@909 8 # = Synopsis
Chris@909 9 #
Chris@909 10 # require 'SVG/Graph/Schedule'
Chris@909 11 #
Chris@909 12 # # Data sets are label, start, end tripples.
Chris@909 13 # data1 = [
Chris@909 14 # "Housesitting", "6/17/04", "6/19/04",
Chris@909 15 # "Summer Session", "6/15/04", "8/15/04",
Chris@909 16 # ]
Chris@909 17 #
Chris@909 18 # graph = SVG::Graph::Schedule.new( {
Chris@909 19 # :width => 640,
Chris@909 20 # :height => 480,
Chris@909 21 # :graph_title => title,
Chris@909 22 # :show_graph_title => true,
Chris@909 23 # :no_css => true,
Chris@909 24 # :scale_x_integers => true,
Chris@909 25 # :scale_y_integers => true,
Chris@909 26 # :min_x_value => 0,
Chris@909 27 # :min_y_value => 0,
Chris@909 28 # :show_data_labels => true,
Chris@909 29 # :show_x_guidelines => true,
Chris@909 30 # :show_x_title => true,
Chris@909 31 # :x_title => "Time",
Chris@909 32 # :stagger_x_labels => true,
Chris@909 33 # :stagger_y_labels => true,
Chris@909 34 # :x_label_format => "%m/%d/%y",
Chris@909 35 # })
Chris@909 36 #
Chris@909 37 # graph.add_data({
Chris@909 38 # :data => data1,
Chris@909 39 # :title => 'Data',
Chris@909 40 # })
Chris@909 41 #
Chris@909 42 # print graph.burn()
Chris@909 43 #
Chris@909 44 # = Description
Chris@909 45 #
Chris@909 46 # Produces a graph of temporal scalar data.
Chris@909 47 #
Chris@909 48 # = Examples
Chris@909 49 #
Chris@909 50 # http://www.germane-software/repositories/public/SVG/test/schedule.rb
Chris@909 51 #
Chris@909 52 # = Notes
Chris@909 53 #
Chris@909 54 # The default stylesheet handles upto 10 data sets, if you
Chris@909 55 # use more you must create your own stylesheet and add the
Chris@909 56 # additional settings for the extra data sets. You will know
Chris@909 57 # if you go over 10 data sets as they will have no style and
Chris@909 58 # be in black.
Chris@909 59 #
Chris@909 60 # Note that multiple data sets within the same chart can differ in
Chris@909 61 # length, and that the data in the datasets needn't be in order;
Chris@909 62 # they will be ordered by the plot along the X-axis.
Chris@909 63 #
Chris@909 64 # The dates must be parseable by ParseDate, but otherwise can be
Chris@909 65 # any order of magnitude (seconds within the hour, or years)
Chris@909 66 #
Chris@909 67 # = See also
Chris@909 68 #
Chris@909 69 # * SVG::Graph::Graph
Chris@909 70 # * SVG::Graph::BarHorizontal
Chris@909 71 # * SVG::Graph::Bar
Chris@909 72 # * SVG::Graph::Line
Chris@909 73 # * SVG::Graph::Pie
Chris@909 74 # * SVG::Graph::Plot
Chris@909 75 # * SVG::Graph::TimeSeries
Chris@909 76 #
Chris@909 77 # == Author
Chris@909 78 #
Chris@909 79 # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
Chris@909 80 #
Chris@909 81 # Copyright 2004 Sean E. Russell
Chris@909 82 # This software is available under the Ruby license[LICENSE.txt]
Chris@909 83 #
Chris@909 84 class Schedule < Graph
Chris@909 85 # In addition to the defaults set by Graph::initialize and
Chris@909 86 # Plot::set_defaults, sets:
Chris@909 87 # [x_label_format] '%Y-%m-%d %H:%M:%S'
Chris@909 88 # [popup_format] '%Y-%m-%d %H:%M:%S'
Chris@909 89 def set_defaults
Chris@909 90 init_with(
Chris@909 91 :x_label_format => '%Y-%m-%d %H:%M:%S',
Chris@909 92 :popup_format => '%Y-%m-%d %H:%M:%S',
Chris@909 93 :scale_x_divisions => false,
Chris@909 94 :scale_x_integers => false,
Chris@909 95 :bar_gap => true
Chris@909 96 )
Chris@909 97 end
Chris@909 98
Chris@909 99 # The format string use do format the X axis labels.
Chris@909 100 # See Time::strformat
Chris@909 101 attr_accessor :x_label_format
Chris@909 102 # Use this to set the spacing between dates on the axis. The value
Chris@909 103 # must be of the form
Chris@909 104 # "\d+ ?(days|weeks|months|years|hours|minutes|seconds)?"
Chris@909 105 #
Chris@909 106 # EG:
Chris@909 107 #
Chris@909 108 # graph.timescale_divisions = "2 weeks"
Chris@909 109 #
Chris@909 110 # will cause the chart to try to divide the X axis up into segments of
Chris@909 111 # two week periods.
Chris@909 112 attr_accessor :timescale_divisions
Chris@909 113 # The formatting used for the popups. See x_label_format
Chris@909 114 attr_accessor :popup_format
Chris@909 115 attr_accessor :min_x_value
Chris@909 116 attr_accessor :scale_x_divisions
Chris@909 117 attr_accessor :scale_x_integers
Chris@909 118 attr_accessor :bar_gap
Chris@909 119
Chris@909 120 # Add data to the plot.
Chris@909 121 #
Chris@909 122 # # A data set with 1 point: Lunch from 12:30 to 14:00
Chris@909 123 # d1 = [ "Lunch", "12:30", "14:00" ]
Chris@909 124 # # A data set with 2 points: "Cats" runs from 5/11/03 to 7/15/04, and
Chris@909 125 # # "Henry V" runs from 6/12/03 to 8/20/03
Chris@909 126 # d2 = [ "Cats", "5/11/03", "7/15/04",
Chris@909 127 # "Henry V", "6/12/03", "8/20/03" ]
Chris@909 128 #
Chris@909 129 # graph.add_data(
Chris@909 130 # :data => d1,
Chris@909 131 # :title => 'Meetings'
Chris@909 132 # )
Chris@909 133 # graph.add_data(
Chris@909 134 # :data => d2,
Chris@909 135 # :title => 'Plays'
Chris@909 136 # )
Chris@909 137 #
Chris@909 138 # Note that the data must be in time,value pairs, and that the date format
Chris@909 139 # may be any date that is parseable by ParseDate.
Chris@909 140 # Also note that, in this example, we're mixing scales; the data from d1
Chris@909 141 # will probably not be discernable if both data sets are plotted on the same
Chris@909 142 # graph, since d1 is too granular.
Chris@909 143 def add_data data
Chris@909 144 @data = [] unless @data
Chris@909 145
Chris@909 146 raise "No data provided by #{conf.inspect}" unless data[:data] and
Chris@909 147 data[:data].kind_of? Array
Chris@909 148 raise "Data supplied must be title,from,to tripples! "+
Chris@909 149 "The data provided contained an odd set of "+
Chris@909 150 "data points" unless data[:data].length % 3 == 0
Chris@909 151 return if data[:data].length == 0
Chris@909 152
Chris@909 153
Chris@909 154 y = []
Chris@909 155 x_start = []
Chris@909 156 x_end = []
Chris@909 157 data[:data].each_index {|i|
Chris@909 158 im3 = i%3
Chris@909 159 if im3 == 0
Chris@909 160 y << data[:data][i]
Chris@909 161 else
Chris@909 162 arr = ParseDate.parsedate( data[:data][i] )
Chris@909 163 t = Time.local( *arr[0,6].compact )
Chris@909 164 (im3 == 1 ? x_start : x_end) << t.to_i
Chris@909 165 end
Chris@909 166 }
Chris@909 167 sort( x_start, x_end, y )
Chris@909 168 @data = [x_start, x_end, y ]
Chris@909 169 end
Chris@909 170
Chris@909 171
Chris@909 172 protected
Chris@909 173
Chris@909 174 def min_x_value=(value)
Chris@909 175 arr = ParseDate.parsedate( value )
Chris@909 176 @min_x_value = Time.local( *arr[0,6].compact ).to_i
Chris@909 177 end
Chris@909 178
Chris@909 179
Chris@909 180 def format x, y
Chris@909 181 Time.at( x ).strftime( popup_format )
Chris@909 182 end
Chris@909 183
Chris@909 184 def get_x_labels
Chris@909 185 rv = get_x_values.collect { |v| Time.at(v).strftime( x_label_format ) }
Chris@909 186 end
Chris@909 187
Chris@909 188 def y_label_offset( height )
Chris@909 189 height / -2.0
Chris@909 190 end
Chris@909 191
Chris@909 192 def get_y_labels
Chris@909 193 @data[2]
Chris@909 194 end
Chris@909 195
Chris@909 196 def draw_data
Chris@909 197 fieldheight = field_height
Chris@909 198 fieldwidth = field_width
Chris@909 199
Chris@909 200 bargap = bar_gap ? (fieldheight < 10 ? fieldheight / 2 : 10) : 0
Chris@909 201 subbar_height = fieldheight - bargap
Chris@909 202
Chris@909 203 field_count = 1
Chris@909 204 y_mod = (subbar_height / 2) + (font_size / 2)
Chris@909 205 min,max,div = x_range
Chris@909 206 scale = (@graph_width.to_f - font_size*2) / (max-min)
Chris@909 207 @data[0].each_index { |i|
Chris@909 208 x_start = @data[0][i]
Chris@909 209 x_end = @data[1][i]
Chris@909 210 y = @graph_height - (fieldheight * field_count)
Chris@909 211 bar_width = (x_end-x_start) * scale
Chris@909 212 bar_start = x_start * scale - (min * scale)
Chris@909 213
Chris@909 214 @graph.add_element( "rect", {
Chris@909 215 "x" => bar_start.to_s,
Chris@909 216 "y" => y.to_s,
Chris@909 217 "width" => bar_width.to_s,
Chris@909 218 "height" => subbar_height.to_s,
Chris@909 219 "class" => "fill#{field_count+1}"
Chris@909 220 })
Chris@909 221 field_count += 1
Chris@909 222 }
Chris@909 223 end
Chris@909 224
Chris@909 225 def get_css
Chris@909 226 return <<EOL
Chris@909 227 /* default fill styles for multiple datasets (probably only use a single dataset on this graph though) */
Chris@909 228 .key1,.fill1{
Chris@909 229 fill: #ff0000;
Chris@909 230 fill-opacity: 0.5;
Chris@909 231 stroke: none;
Chris@909 232 stroke-width: 0.5px;
Chris@909 233 }
Chris@909 234 .key2,.fill2{
Chris@909 235 fill: #0000ff;
Chris@909 236 fill-opacity: 0.5;
Chris@909 237 stroke: none;
Chris@909 238 stroke-width: 1px;
Chris@909 239 }
Chris@909 240 .key3,.fill3{
Chris@909 241 fill: #00ff00;
Chris@909 242 fill-opacity: 0.5;
Chris@909 243 stroke: none;
Chris@909 244 stroke-width: 1px;
Chris@909 245 }
Chris@909 246 .key4,.fill4{
Chris@909 247 fill: #ffcc00;
Chris@909 248 fill-opacity: 0.5;
Chris@909 249 stroke: none;
Chris@909 250 stroke-width: 1px;
Chris@909 251 }
Chris@909 252 .key5,.fill5{
Chris@909 253 fill: #00ccff;
Chris@909 254 fill-opacity: 0.5;
Chris@909 255 stroke: none;
Chris@909 256 stroke-width: 1px;
Chris@909 257 }
Chris@909 258 .key6,.fill6{
Chris@909 259 fill: #ff00ff;
Chris@909 260 fill-opacity: 0.5;
Chris@909 261 stroke: none;
Chris@909 262 stroke-width: 1px;
Chris@909 263 }
Chris@909 264 .key7,.fill7{
Chris@909 265 fill: #00ffff;
Chris@909 266 fill-opacity: 0.5;
Chris@909 267 stroke: none;
Chris@909 268 stroke-width: 1px;
Chris@909 269 }
Chris@909 270 .key8,.fill8{
Chris@909 271 fill: #ffff00;
Chris@909 272 fill-opacity: 0.5;
Chris@909 273 stroke: none;
Chris@909 274 stroke-width: 1px;
Chris@909 275 }
Chris@909 276 .key9,.fill9{
Chris@909 277 fill: #cc6666;
Chris@909 278 fill-opacity: 0.5;
Chris@909 279 stroke: none;
Chris@909 280 stroke-width: 1px;
Chris@909 281 }
Chris@909 282 .key10,.fill10{
Chris@909 283 fill: #663399;
Chris@909 284 fill-opacity: 0.5;
Chris@909 285 stroke: none;
Chris@909 286 stroke-width: 1px;
Chris@909 287 }
Chris@909 288 .key11,.fill11{
Chris@909 289 fill: #339900;
Chris@909 290 fill-opacity: 0.5;
Chris@909 291 stroke: none;
Chris@909 292 stroke-width: 1px;
Chris@909 293 }
Chris@909 294 .key12,.fill12{
Chris@909 295 fill: #9966FF;
Chris@909 296 fill-opacity: 0.5;
Chris@909 297 stroke: none;
Chris@909 298 stroke-width: 1px;
Chris@909 299 }
Chris@909 300 EOL
Chris@909 301 end
Chris@909 302
Chris@909 303 private
Chris@909 304 def x_range
Chris@909 305 max_value = [ @data[0][-1], @data[1].max ].max
Chris@909 306 min_value = [ @data[0][0], @data[1].min ].min
Chris@909 307 min_value = min_value<min_x_value ? min_value : min_x_value if min_x_value
Chris@909 308
Chris@909 309 range = max_value - min_value
Chris@909 310 right_pad = range == 0 ? 10 : range / 20.0
Chris@909 311 scale_range = (max_value + right_pad) - min_value
Chris@909 312
Chris@909 313 scale_division = scale_x_divisions || (scale_range / 10.0)
Chris@909 314
Chris@909 315 if scale_x_integers
Chris@909 316 scale_division = scale_division < 1 ? 1 : scale_division.round
Chris@909 317 end
Chris@909 318
Chris@909 319 [min_value, max_value, scale_division]
Chris@909 320 end
Chris@909 321
Chris@909 322 def get_x_values
Chris@909 323 rv = []
Chris@909 324 min, max, scale_division = x_range
Chris@909 325 if timescale_divisions
Chris@909 326 timescale_divisions =~ /(\d+) ?(days|weeks|months|years|hours|minutes|seconds)?/
Chris@909 327 division_units = $2 ? $2 : "days"
Chris@909 328 amount = $1.to_i
Chris@909 329 if amount
Chris@909 330 step = nil
Chris@909 331 case division_units
Chris@909 332 when "months"
Chris@909 333 cur = min
Chris@909 334 while cur < max
Chris@909 335 rv << cur
Chris@909 336 arr = Time.at( cur ).to_a
Chris@909 337 arr[4] += amount
Chris@909 338 if arr[4] > 12
Chris@909 339 arr[5] += (arr[4] / 12).to_i
Chris@909 340 arr[4] = (arr[4] % 12)
Chris@909 341 end
Chris@909 342 cur = Time.local(*arr).to_i
Chris@909 343 end
Chris@909 344 when "years"
Chris@909 345 cur = min
Chris@909 346 while cur < max
Chris@909 347 rv << cur
Chris@909 348 arr = Time.at( cur ).to_a
Chris@909 349 arr[5] += amount
Chris@909 350 cur = Time.local(*arr).to_i
Chris@909 351 end
Chris@909 352 when "weeks"
Chris@909 353 step = 7 * 24 * 60 * 60 * amount
Chris@909 354 when "days"
Chris@909 355 step = 24 * 60 * 60 * amount
Chris@909 356 when "hours"
Chris@909 357 step = 60 * 60 * amount
Chris@909 358 when "minutes"
Chris@909 359 step = 60 * amount
Chris@909 360 when "seconds"
Chris@909 361 step = amount
Chris@909 362 end
Chris@909 363 min.step( max, step ) {|v| rv << v} if step
Chris@909 364
Chris@909 365 return rv
Chris@909 366 end
Chris@909 367 end
Chris@909 368 min.step( max, scale_division ) {|v| rv << v}
Chris@909 369 return rv
Chris@909 370 end
Chris@909 371 end
Chris@909 372 end
Chris@909 373 end