diff options
Diffstat (limited to 'lib/SVG/Graph/Schedule.rb')
-rw-r--r-- | lib/SVG/Graph/Schedule.rb | 373 |
1 files changed, 373 insertions, 0 deletions
diff --git a/lib/SVG/Graph/Schedule.rb b/lib/SVG/Graph/Schedule.rb new file mode 100644 index 000000000..70bb519d7 --- /dev/null +++ b/lib/SVG/Graph/Schedule.rb @@ -0,0 +1,373 @@ +require 'SVG/Graph/Plot' +require 'parsedate' + +module SVG + module Graph + # === For creating SVG plots of scalar temporal data + # + # = Synopsis + # + # require 'SVG/Graph/Schedule' + # + # # Data sets are label, start, end tripples. + # data1 = [ + # "Housesitting", "6/17/04", "6/19/04", + # "Summer Session", "6/15/04", "8/15/04", + # ] + # + # graph = SVG::Graph::Schedule.new( { + # :width => 640, + # :height => 480, + # :graph_title => title, + # :show_graph_title => true, + # :no_css => true, + # :scale_x_integers => true, + # :scale_y_integers => true, + # :min_x_value => 0, + # :min_y_value => 0, + # :show_data_labels => true, + # :show_x_guidelines => true, + # :show_x_title => true, + # :x_title => "Time", + # :stagger_x_labels => true, + # :stagger_y_labels => true, + # :x_label_format => "%m/%d/%y", + # }) + # + # graph.add_data({ + # :data => data1, + # :title => 'Data', + # }) + # + # print graph.burn() + # + # = Description + # + # Produces a graph of temporal scalar data. + # + # = Examples + # + # http://www.germane-software/repositories/public/SVG/test/schedule.rb + # + # = Notes + # + # The default stylesheet handles upto 10 data sets, if you + # use more you must create your own stylesheet and add the + # additional settings for the extra data sets. You will know + # if you go over 10 data sets as they will have no style and + # be in black. + # + # Note that multiple data sets within the same chart can differ in + # length, and that the data in the datasets needn't be in order; + # they will be ordered by the plot along the X-axis. + # + # The dates must be parseable by ParseDate, but otherwise can be + # any order of magnitude (seconds within the hour, or years) + # + # = See also + # + # * SVG::Graph::Graph + # * SVG::Graph::BarHorizontal + # * SVG::Graph::Bar + # * SVG::Graph::Line + # * SVG::Graph::Pie + # * SVG::Graph::Plot + # * SVG::Graph::TimeSeries + # + # == Author + # + # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> + # + # Copyright 2004 Sean E. Russell + # This software is available under the Ruby license[LICENSE.txt] + # + class Schedule < Graph + # In addition to the defaults set by Graph::initialize and + # Plot::set_defaults, sets: + # [x_label_format] '%Y-%m-%d %H:%M:%S' + # [popup_format] '%Y-%m-%d %H:%M:%S' + def set_defaults + init_with( + :x_label_format => '%Y-%m-%d %H:%M:%S', + :popup_format => '%Y-%m-%d %H:%M:%S', + :scale_x_divisions => false, + :scale_x_integers => false, + :bar_gap => true + ) + end + + # The format string use do format the X axis labels. + # See Time::strformat + attr_accessor :x_label_format + # Use this to set the spacing between dates on the axis. The value + # must be of the form + # "\d+ ?(days|weeks|months|years|hours|minutes|seconds)?" + # + # EG: + # + # graph.timescale_divisions = "2 weeks" + # + # will cause the chart to try to divide the X axis up into segments of + # two week periods. + attr_accessor :timescale_divisions + # The formatting used for the popups. See x_label_format + attr_accessor :popup_format + attr_accessor :min_x_value + attr_accessor :scale_x_divisions + attr_accessor :scale_x_integers + attr_accessor :bar_gap + + # Add data to the plot. + # + # # A data set with 1 point: Lunch from 12:30 to 14:00 + # d1 = [ "Lunch", "12:30", "14:00" ] + # # A data set with 2 points: "Cats" runs from 5/11/03 to 7/15/04, and + # # "Henry V" runs from 6/12/03 to 8/20/03 + # d2 = [ "Cats", "5/11/03", "7/15/04", + # "Henry V", "6/12/03", "8/20/03" ] + # + # graph.add_data( + # :data => d1, + # :title => 'Meetings' + # ) + # graph.add_data( + # :data => d2, + # :title => 'Plays' + # ) + # + # Note that the data must be in time,value pairs, and that the date format + # may be any date that is parseable by ParseDate. + # Also note that, in this example, we're mixing scales; the data from d1 + # will probably not be discernable if both data sets are plotted on the same + # graph, since d1 is too granular. + def add_data data + @data = [] unless @data + + raise "No data provided by #{conf.inspect}" unless data[:data] and + data[:data].kind_of? Array + raise "Data supplied must be title,from,to tripples! "+ + "The data provided contained an odd set of "+ + "data points" unless data[:data].length % 3 == 0 + return if data[:data].length == 0 + + + y = [] + x_start = [] + x_end = [] + data[:data].each_index {|i| + im3 = i%3 + if im3 == 0 + y << data[:data][i] + else + arr = ParseDate.parsedate( data[:data][i] ) + t = Time.local( *arr[0,6].compact ) + (im3 == 1 ? x_start : x_end) << t.to_i + end + } + sort( x_start, x_end, y ) + @data = [x_start, x_end, y ] + end + + + protected + + def min_x_value=(value) + arr = ParseDate.parsedate( value ) + @min_x_value = Time.local( *arr[0,6].compact ).to_i + end + + + def format x, y + Time.at( x ).strftime( popup_format ) + end + + def get_x_labels + rv = get_x_values.collect { |v| Time.at(v).strftime( x_label_format ) } + end + + def y_label_offset( height ) + height / -2.0 + end + + def get_y_labels + @data[2] + end + + def draw_data + fieldheight = field_height + fieldwidth = field_width + + bargap = bar_gap ? (fieldheight < 10 ? fieldheight / 2 : 10) : 0 + subbar_height = fieldheight - bargap + + field_count = 1 + y_mod = (subbar_height / 2) + (font_size / 2) + min,max,div = x_range + scale = (@graph_width.to_f - font_size*2) / (max-min) + @data[0].each_index { |i| + x_start = @data[0][i] + x_end = @data[1][i] + y = @graph_height - (fieldheight * field_count) + bar_width = (x_end-x_start) * scale + bar_start = x_start * scale - (min * scale) + + @graph.add_element( "rect", { + "x" => bar_start.to_s, + "y" => y.to_s, + "width" => bar_width.to_s, + "height" => subbar_height.to_s, + "class" => "fill#{field_count+1}" + }) + field_count += 1 + } + end + + def get_css + return <<EOL +/* default fill styles for multiple datasets (probably only use a single dataset on this graph though) */ +.key1,.fill1{ + fill: #ff0000; + fill-opacity: 0.5; + stroke: none; + stroke-width: 0.5px; +} +.key2,.fill2{ + fill: #0000ff; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key3,.fill3{ + fill: #00ff00; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key4,.fill4{ + fill: #ffcc00; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key5,.fill5{ + fill: #00ccff; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key6,.fill6{ + fill: #ff00ff; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key7,.fill7{ + fill: #00ffff; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key8,.fill8{ + fill: #ffff00; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key9,.fill9{ + fill: #cc6666; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key10,.fill10{ + fill: #663399; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key11,.fill11{ + fill: #339900; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +.key12,.fill12{ + fill: #9966FF; + fill-opacity: 0.5; + stroke: none; + stroke-width: 1px; +} +EOL + end + + private + def x_range + max_value = [ @data[0][-1], @data[1].max ].max + min_value = [ @data[0][0], @data[1].min ].min + min_value = min_value<min_x_value ? min_value : min_x_value if min_x_value + + range = max_value - min_value + right_pad = range == 0 ? 10 : range / 20.0 + scale_range = (max_value + right_pad) - min_value + + scale_division = scale_x_divisions || (scale_range / 10.0) + + if scale_x_integers + scale_division = scale_division < 1 ? 1 : scale_division.round + end + + [min_value, max_value, scale_division] + end + + def get_x_values + rv = [] + min, max, scale_division = x_range + if timescale_divisions + timescale_divisions =~ /(\d+) ?(days|weeks|months|years|hours|minutes|seconds)?/ + division_units = $2 ? $2 : "days" + amount = $1.to_i + if amount + step = nil + case division_units + when "months" + cur = min + while cur < max + rv << cur + arr = Time.at( cur ).to_a + arr[4] += amount + if arr[4] > 12 + arr[5] += (arr[4] / 12).to_i + arr[4] = (arr[4] % 12) + end + cur = Time.local(*arr).to_i + end + when "years" + cur = min + while cur < max + rv << cur + arr = Time.at( cur ).to_a + arr[5] += amount + cur = Time.local(*arr).to_i + end + when "weeks" + step = 7 * 24 * 60 * 60 * amount + when "days" + step = 24 * 60 * 60 * amount + when "hours" + step = 60 * 60 * amount + when "minutes" + step = 60 * amount + when "seconds" + step = amount + end + min.step( max, step ) {|v| rv << v} if step + + return rv + end + end + min.step( max, scale_division ) {|v| rv << v} + return rv + end + end + end +end |