mirror of
				https://github.com/redmine/redmine.git
				synced 2025-10-31 02:15:52 +01:00 
			
		
		
		
	git-svn-id: http://redmine.rubyforge.org/svn/trunk@380 e93f8b46-1217-0410-a6f0-8f06a7374b81
		
			
				
	
	
		
			374 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			374 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| 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
 |