class SVG::Graph::Plot
For creating SVG
plots of scalar data¶ ↑
Synopsis¶ ↑
require 'SVG/Graph/Plot' # Data sets are x,y pairs # Note that multiple data sets 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. projection = [ 6, 11, 0, 5, 18, 7, 1, 11, 13, 9, 1, 2, 19, 0, 3, 13, 7, 9 ] actual = [ 0, 18, 8, 15, 9, 4, 18, 14, 10, 2, 11, 6, 14, 12, 15, 6, 4, 17, 2, 12 ] graph = SVG::Graph::Plot.new({ :height => 500, :width => 300, :key => true, :scale_x_integers => true, :scale_y_integers => true, }) graph.add_data({ :data => projection :title => 'Projected', }) graph.add_data({ :data => actual, :title => 'Actual', }) print graph.burn()
Description¶ ↑
Produces a graph of scalar data.
This object aims to allow you to easily create high quality SVG scalar plots. You can either use the default style sheet or supply your own. Either way there are many options which can be configured to give you control over how the graph is generated - with or without a key, data elements at each point, title, subtitle etc.
Examples¶ ↑
www.germane-software/repositories/public/SVG/test/plot.rb
Notes¶ ↑
The default stylesheet handles upto 12 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 12 data sets as they will have no style and be in black.
Unlike the other types of charts, data sets must contain x,y pairs:
[ 1,2 ] # A data set with 1 point: (1,2) [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)
Additional possible notation
[ [1,2], 5,6] # A data set with 2 points: (1,2) and (5,6), mixed notation [ [1,2], [5,6]] # A data set with 2 points: (1,2) and (5,6), nested array
See also¶ ↑
Author¶ ↑
Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
Copyright 2004 Sean E. Russell This software is available under the Ruby license
Constants
- X
- Y
Attributes
Fill the area under the line, default: false
Set the maximum value of the X
axis, if nil the maximum from data is chosen, default: nil
Set the maximum value of the Y
axis, if nil the maximum from data is chosen, default: nil
Set the minimum value of the X
axis, if nil the minimum from data is chosen, default: nil
Set the minimum value of the Y
axis, if nil the minimum from data is chosen, default: nil
Round value of data points in popups to integer, default: true
Determines the scaling for the X
axis divisions.
graph.scale_x_divisions = 2
would cause the graph to attempt to generate labels stepped by 2; EG: 0,2,4,6,8… default is automatic such that there are 10 labels
Make the X
axis labels integers, default: false
Determines the scaling for the Y
axis divisions.
graph.scale_y_divisions = 0.5
would cause the graph to attempt to generate labels stepped by 0.5; EG: 0, 0.5, 1, 1.5, 2, … default is automatic such that there are 10 labels
Make the Y
axis labels integers, default: false
Show a small circle on the graph where the line goes from one point to the next. default: true
Show lines connecting data points, default: true
Public Instance Methods
Adds data to the plot. The data must be in X
,Y pairs; EG
data_set1 = [ 1, 2 ] # A data set with 1 point: (1,2) data_set2 = [ 1,2, 5,6] # A data set with 2 points: (1,2) and (5,6)
It's also supported to supply nested array or a mix (flatten is applied to the array); EG
data_set2 = [[1,2], 5,6]
or
data_set2 = [[1,2], [5,6]] graph.add_data({ :data => data_set1, :title => 'single point' }) graph.add_data({ :data => data_set2, :title => 'two points' })
@param conf [Hash] with keys
:data [Array] of x,y pairs, one pair for each point :title [String] mandatory name of data series for legend of graph :description [Array<String>] (optional) if given, description for each datapoint (shown in popups) :shape [Array<String>] (optional) if given, DataPoint shape is chosen based on this string instead of description :url [Array<String>] (optional) if given, link will be added to each datapoint
# File lib/SVG/Graph/Plot.rb, line 173 def add_data(conf) @data ||= [] raise "No data provided by #{conf.inspect}" unless conf[:data].is_a?(Array) # support array of arrays and flatten it conf[:data] = conf[:data].flatten # check that we have pairs of values raise "Data supplied must be x,y pairs! "+ "The data provided contained an odd set of "+ "data points" unless conf[:data].length % 2 == 0 # remove nil values conf[:data] = conf[:data].compact return if conf[:data].length.zero? add_data_init_or_check_optional_keys(conf, conf[:data].size / 2) x = [] y = [] conf[:data].each_index {|i| (i%2 == 0 ? x : y) << conf[:data][i] } sort(x, y, conf[:description], conf[:shape], conf[:url]) conf[:data] = [x, y] # at the end data looks like: # [ # [all x values], # [all y values] # ] @data << conf end
In addition to the defaults set by Graph::initialize, sets
show_data_points
-
true
area_fill
-
false
- stacked
-
false, will not have any effect if true
show_lines
-
true
round_popups
-
true
# File lib/SVG/Graph/Plot.rb, line 100 def set_defaults init_with( :show_data_points => true, :area_fill => false, :stacked => false, :show_lines => true, :round_popups => true, :scale_x_integers => false, :scale_y_integers => false, ) end
Protected Instance Methods
# File lib/SVG/Graph/Plot.rb, line 348 def calc_coords(x, y) coords = {:x => 0, :y => 0} # scale the coordinates, use float division / multiplication # otherwise the point will be place inaccurate coords[:x] = (x + @x_offset)/@x_scale_division.to_f * field_width coords[:y] = @graph_height - (y + @y_offset)/@y_scale_division.to_f * field_height return coords end
SVG::Graph::Graph#calculate_left_margin
# File lib/SVG/Graph/Plot.rb, line 211 def calculate_left_margin super label_left = get_x_labels[0].to_s.length / 2 * font_size * 0.6 @border_left = label_left if label_left > @border_left end
SVG::Graph::Graph#calculate_right_margin
# File lib/SVG/Graph/Plot.rb, line 217 def calculate_right_margin super label_right = get_x_labels[-1].to_s.length / 2 * font_size * 0.6 @border_right = label_right if label_right > @border_right end
# File lib/SVG/Graph/Plot.rb, line 357 def draw_data line = 1 x_min = min_x_range y_min = min_y_range for data in @data x_points = data[:data][X] y_points = data[:data][Y] lpath = "L" x_start = 0 y_start = 0 x_points.each_index { |idx| c = calc_coords(x_points[idx] - x_min, y_points[idx] - y_min) x_start, y_start = c[:x],c[:y] if idx == 0 lpath << "#{c[:x]} #{c[:y]} " } if area_fill @graph.add_element( "path", { "d" => "M#{x_start} #@graph_height #{lpath} V#@graph_height Z", "class" => "fill#{line}" }) end if show_lines @graph.add_element( "path", { "d" => "M#{x_start} #{y_start} #{lpath}", "class" => "line#{line}" }) end if show_data_points || show_data_values || add_popups x_points.each_index { |idx| c = calc_coords(x_points[idx] - x_min, y_points[idx] - y_min) if show_data_points shape_selection_string = data[:description][idx].to_s if !data[:shape][idx].nil? shape_selection_string = data[:shape][idx].to_s end DataPoint.new(c[:x], c[:y], line).shape(shape_selection_string).each{|s| @graph.add_element( *s ) } end make_datapoint_text( c[:x], c[:y]-6, y_points[idx] ) add_popup(c[:x], c[:y], format( x_points[idx], y_points[idx], data[:description][idx].to_s), "", data[:url][idx].to_s) } end line += 1 end end
# File lib/SVG/Graph/Plot.rb, line 336 def field_height # exclude values which are outside max_x_range values = get_y_values max = max_y_range if values.length == 1 dx = values[-1] else dx = (max - values[-1]).to_f / (values[-1] - values[-2]) end @graph_height.to_f / (values.length - 1) end
# File lib/SVG/Graph/Plot.rb, line 276 def field_width # exclude values which are outside max_x_range values = get_x_values @graph_width.to_f / (values.length - 1 ) # -1 is to use entire x-axis # otherwise there is always 1 division unused end
returns the formatted string which is added as popup information
# File lib/SVG/Graph/Plot.rb, line 411 def format x, y, desc info = [] info << (round_popups ? x.round : @number_format % x ) info << (round_popups ? y.round : @number_format % y ) info << desc if !desc.empty? "(#{info.compact.join(', ')})" end
# File lib/SVG/Graph/Plot.rb, line 419 def get_css return <<EOL /* default line styles */ .line1{ fill: none; stroke: #ff0000; stroke-width: 1px; } .line2{ fill: none; stroke: #0000ff; stroke-width: 1px; } .line3{ fill: none; stroke: #00ff00; stroke-width: 1px; } .line4{ fill: none; stroke: #ffcc00; stroke-width: 1px; } .line5{ fill: none; stroke: #00ccff; stroke-width: 1px; } .line6{ fill: none; stroke: #ff00ff; stroke-width: 1px; } .line7{ fill: none; stroke: #00ffff; stroke-width: 1px; } .line8{ fill: none; stroke: #ffff00; stroke-width: 1px; } .line9{ fill: none; stroke: #cc6666; stroke-width: 1px; } .line10{ fill: none; stroke: #663399; stroke-width: 1px; } .line11{ fill: none; stroke: #339900; stroke-width: 1px; } .line12{ fill: none; stroke: #9966FF; stroke-width: 1px; } /* default fill styles */ .fill1{ fill: #cc0000; fill-opacity: 0.2; stroke: none; } .fill2{ fill: #0000cc; fill-opacity: 0.2; stroke: none; } .fill3{ fill: #00cc00; fill-opacity: 0.2; stroke: none; } .fill4{ fill: #ffcc00; fill-opacity: 0.2; stroke: none; } .fill5{ fill: #00ccff; fill-opacity: 0.2; stroke: none; } .fill6{ fill: #ff00ff; fill-opacity: 0.2; stroke: none; } .fill7{ fill: #00ffff; fill-opacity: 0.2; stroke: none; } .fill8{ fill: #ffff00; fill-opacity: 0.2; stroke: none; } .fill9{ fill: #cc6666; fill-opacity: 0.2; stroke: none; } .fill10{ fill: #663399; fill-opacity: 0.2; stroke: none; } .fill11{ fill: #339900; fill-opacity: 0.2; stroke: none; } .fill12{ fill: #9966FF; fill-opacity: 0.2; stroke: none; } /* default line styles */ .key1,.dataPoint1{ fill: #ff0000; stroke: none; stroke-width: 1px; } .key2,.dataPoint2{ fill: #0000ff; stroke: none; stroke-width: 1px; } .key3,.dataPoint3{ fill: #00ff00; stroke: none; stroke-width: 1px; } .key4,.dataPoint4{ fill: #ffcc00; stroke: none; stroke-width: 1px; } .key5,.dataPoint5{ fill: #00ccff; stroke: none; stroke-width: 1px; } .key6,.dataPoint6{ fill: #ff00ff; stroke: none; stroke-width: 1px; } .key7,.dataPoint7{ fill: #00ffff; stroke: none; stroke-width: 1px; } .key8,.dataPoint8{ fill: #ffff00; stroke: none; stroke-width: 1px; } .key9,.dataPoint9{ fill: #cc6666; stroke: none; stroke-width: 1px; } .key10,.dataPoint10{ fill: #663399; stroke: none; stroke-width: 1px; } .key11,.dataPoint11{ fill: #339900; stroke: none; stroke-width: 1px; } .key12,.dataPoint12{ fill: #9966FF; stroke: none; stroke-width: 1px; } EOL end
# File lib/SVG/Graph/Plot.rb, line 268 def get_x_values min_value, max_value, @x_scale_division = x_label_range rv = [] min_value.step( max_value + @x_scale_division , @x_scale_division ) {|v| rv << v} return rv end
# File lib/SVG/Graph/Plot.rb, line 322 def get_y_values min_value, max_value, @y_scale_division = y_label_range if max_value != min_value while (max_value - min_value) < @y_scale_division @y_scale_division /= 9.0 end end rv = [] min_value.step( max_value + @y_scale_division, @y_scale_division ) {|v| rv << v} rv << rv[0] + 1 if rv.length == 1 return rv end
# File lib/SVG/Graph/Plot.rb, line 207 def keys @data.collect{ |x| x[:title] } end
# File lib/SVG/Graph/Plot.rb, line 226 def max_x_range # needs to be computed fresh when called, to cover the use-case: # add_data -> burn -> add_data -> burn # when values would be cached, the graph is not updated for second burning max_value = @data.collect{|x| x[:data][X][-1] }.max max_value = max_value > max_x_value ? max_value : max_x_value if max_x_value return max_value end
# File lib/SVG/Graph/Plot.rb, line 283 def max_y_range max_value = @data.collect{|x| x[:data][Y].max }.max max_value = max_value > max_y_value ? max_value : max_y_value if max_y_value return max_value end
# File lib/SVG/Graph/Plot.rb, line 235 def min_x_range # needs to be computed fresh when called, to cover the use-case: # add_data -> burn -> add_data -> burn # when values would be cached, the graph is not updated for second burning min_value = @data.collect{|x| x[:data][X][0] }.min min_value = min_value < min_x_value ? min_value : min_x_value if min_x_value return min_value end
# File lib/SVG/Graph/Plot.rb, line 289 def min_y_range # needs to be computed fresh when called, to cover the use-case: # add_data -> burn -> add_data -> burn # when values would be cached, the graph is not updated for second burning min_value = @data.collect{|x| x[:data][Y].min }.min min_value = min_value < min_y_value ? min_value : min_y_value if min_y_value return min_value end
# File lib/SVG/Graph/Plot.rb, line 244 def x_label_range max_value = max_x_range min_value = min_x_range range = max_value - min_value # add some padding on right if range == 0 max_value += 10 else max_value += range / 20.0 end scale_range = max_value - min_value scale_division = scale_x_divisions || (scale_range / 9.0) @x_offset = 0 if scale_x_integers scale_division = scale_division < 1 ? 1 : scale_division.ceil @x_offset = min_value.to_f - min_value.floor min_value = min_value.floor end [min_value, max_value, scale_division] end
# File lib/SVG/Graph/Plot.rb, line 298 def y_label_range max_value = max_y_range min_value = min_y_range range = max_value - min_value # add some padding on top if range == 0 max_value += 10 else max_value += range / 20.0 end scale_range = max_value - min_value scale_division = scale_y_divisions || (scale_range / 9.0) @y_offset = 0 if scale_y_integers scale_division = scale_division < 1 ? 1 : scale_division.ceil @y_offset = (min_value.to_f - min_value.floor).to_f min_value = min_value.floor end return [min_value, max_value, scale_division] end