annotate lib/SVG/Graph/Schedule.rb @ 1524:82fac3dcf466 redmine-2.5-integration

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