Chris@909: require 'SVG/Graph/Graph' Chris@909: Chris@909: module SVG Chris@909: module Graph Chris@909: # === Create presentation quality SVG line graphs easily Chris@909: # Chris@909: # = Synopsis Chris@909: # Chris@909: # require 'SVG/Graph/Line' Chris@909: # Chris@909: # fields = %w(Jan Feb Mar); Chris@909: # data_sales_02 = [12, 45, 21] Chris@909: # data_sales_03 = [15, 30, 40] Chris@909: # Chris@909: # graph = SVG::Graph::Line.new({ Chris@909: # :height => 500, Chris@909: # :width => 300, Chris@909: # :fields => fields, Chris@909: # }) Chris@909: # Chris@909: # graph.add_data({ Chris@909: # :data => data_sales_02, Chris@909: # :title => 'Sales 2002', Chris@909: # }) Chris@909: # Chris@909: # graph.add_data({ Chris@909: # :data => data_sales_03, Chris@909: # :title => 'Sales 2003', Chris@909: # }) Chris@909: # Chris@909: # print "Content-type: image/svg+xml\r\n\r\n"; Chris@909: # print graph.burn(); Chris@909: # Chris@909: # = Description Chris@909: # Chris@909: # This object aims to allow you to easily create high quality Chris@909: # SVG line graphs. You can either use the default style sheet Chris@909: # or supply your own. Either way there are many options which can Chris@909: # be configured to give you control over how the graph is Chris@909: # generated - with or without a key, data elements at each point, Chris@909: # title, subtitle etc. Chris@909: # Chris@909: # = Examples Chris@909: # Chris@909: # http://www.germane-software/repositories/public/SVG/test/single.rb Chris@909: # Chris@909: # = Notes Chris@909: # Chris@909: # The default stylesheet handles upto 10 data sets, if you Chris@909: # use more you must create your own stylesheet and add the Chris@909: # additional settings for the extra data sets. You will know Chris@909: # if you go over 10 data sets as they will have no style and Chris@909: # be in black. Chris@909: # Chris@909: # = See also Chris@909: # Chris@909: # * SVG::Graph::Graph Chris@909: # * SVG::Graph::BarHorizontal Chris@909: # * SVG::Graph::Bar Chris@909: # * SVG::Graph::Pie Chris@909: # * SVG::Graph::Plot Chris@909: # * SVG::Graph::TimeSeries Chris@909: # Chris@909: # == Author Chris@909: # Chris@909: # Sean E. Russell Chris@909: # Chris@909: # Copyright 2004 Sean E. Russell Chris@909: # This software is available under the Ruby license[LICENSE.txt] Chris@909: # Chris@909: class Line < SVG::Graph::Graph Chris@909: # Show a small circle on the graph where the line Chris@909: # goes from one point to the next. Chris@909: attr_accessor :show_data_points Chris@909: # Accumulates each data set. (i.e. Each point increased by sum of Chris@909: # all previous series at same point). Default is 0, set to '1' to show. Chris@909: attr_accessor :stacked Chris@909: # Fill in the area under the plot if true Chris@909: attr_accessor :area_fill Chris@909: Chris@909: # The constructor takes a hash reference, fields (the names for each Chris@909: # field on the X axis) MUST be set, all other values are defaulted to Chris@909: # those shown above - with the exception of style_sheet which defaults Chris@909: # to using the internal style sheet. Chris@909: def initialize config Chris@909: raise "fields was not supplied or is empty" unless config[:fields] && Chris@909: config[:fields].kind_of?(Array) && Chris@909: config[:fields].length > 0 Chris@909: super Chris@909: end Chris@909: Chris@909: # In addition to the defaults set in Graph::initialize, sets Chris@909: # [show_data_points] true Chris@909: # [show_data_values] true Chris@909: # [stacked] false Chris@909: # [area_fill] false Chris@909: def set_defaults Chris@909: init_with( Chris@909: :show_data_points => true, Chris@909: :show_data_values => true, Chris@909: :stacked => false, Chris@909: :area_fill => false Chris@909: ) Chris@909: Chris@909: self.top_align = self.top_font = self.right_align = self.right_font = 1 Chris@909: end Chris@909: Chris@909: protected Chris@909: Chris@909: def max_value Chris@909: max = 0 Chris@909: Chris@909: if (stacked == true) then Chris@909: sums = Array.new(@config[:fields].length).fill(0) Chris@909: Chris@909: @data.each do |data| Chris@909: sums.each_index do |i| Chris@909: sums[i] += data[:data][i].to_f Chris@909: end Chris@909: end Chris@909: Chris@909: max = sums.max Chris@909: else Chris@909: max = @data.collect{|x| x[:data].max}.max Chris@909: end Chris@909: Chris@909: return max Chris@909: end Chris@909: Chris@909: def min_value Chris@909: min = 0 Chris@909: Chris@909: if (min_scale_value.nil? == false) then Chris@909: min = min_scale_value Chris@909: elsif (stacked == true) then Chris@909: min = @data[-1][:data].min Chris@909: else Chris@909: min = @data.collect{|x| x[:data].min}.min Chris@909: end Chris@909: Chris@909: return min Chris@909: end Chris@909: Chris@909: def get_x_labels Chris@909: @config[:fields] Chris@909: end Chris@909: Chris@909: def calculate_left_margin Chris@909: super Chris@909: label_left = @config[:fields][0].length / 2 * font_size * 0.6 Chris@909: @border_left = label_left if label_left > @border_left Chris@909: end Chris@909: Chris@909: def get_y_labels Chris@909: maxvalue = max_value Chris@909: minvalue = min_value Chris@909: range = maxvalue - minvalue Chris@909: top_pad = range == 0 ? 10 : range / 20.0 Chris@909: scale_range = (maxvalue + top_pad) - minvalue Chris@909: Chris@909: scale_division = scale_divisions || (scale_range / 10.0) Chris@909: Chris@909: if scale_integers Chris@909: scale_division = scale_division < 1 ? 1 : scale_division.round Chris@909: end Chris@909: Chris@909: rv = [] Chris@909: maxvalue = maxvalue%scale_division == 0 ? Chris@909: maxvalue : maxvalue + scale_division Chris@909: minvalue.step( maxvalue, scale_division ) {|v| rv << v} Chris@909: return rv Chris@909: end Chris@909: Chris@909: def calc_coords(field, value, width = field_width, height = field_height) Chris@909: coords = {:x => 0, :y => 0} Chris@909: coords[:x] = width * field Chris@909: coords[:y] = @graph_height - value * height Chris@909: Chris@909: return coords Chris@909: end Chris@909: Chris@909: def draw_data Chris@909: minvalue = min_value Chris@909: fieldheight = (@graph_height.to_f - font_size*2*top_font) / Chris@909: (get_y_labels.max - get_y_labels.min) Chris@909: fieldwidth = field_width Chris@909: line = @data.length Chris@909: Chris@909: prev_sum = Array.new(@config[:fields].length).fill(0) Chris@909: cum_sum = Array.new(@config[:fields].length).fill(-minvalue) Chris@909: Chris@909: for data in @data.reverse Chris@909: lpath = "" Chris@909: apath = "" Chris@909: Chris@909: if not stacked then cum_sum.fill(-minvalue) end Chris@909: Chris@909: data[:data].each_index do |i| Chris@909: cum_sum[i] += data[:data][i] Chris@909: Chris@909: c = calc_coords(i, cum_sum[i], fieldwidth, fieldheight) Chris@909: Chris@909: lpath << "#{c[:x]} #{c[:y]} " Chris@909: end Chris@909: Chris@909: if area_fill Chris@909: if stacked then Chris@909: (prev_sum.length - 1).downto 0 do |i| Chris@909: c = calc_coords(i, prev_sum[i], fieldwidth, fieldheight) Chris@909: Chris@909: apath << "#{c[:x]} #{c[:y]} " Chris@909: end Chris@909: Chris@909: c = calc_coords(0, prev_sum[0], fieldwidth, fieldheight) Chris@909: else Chris@909: apath = "V#@graph_height" Chris@909: c = calc_coords(0, 0, fieldwidth, fieldheight) Chris@909: end Chris@909: Chris@909: @graph.add_element("path", { Chris@909: "d" => "M#{c[:x]} #{c[:y]} L" + lpath + apath + "Z", Chris@909: "class" => "fill#{line}" Chris@909: }) Chris@909: end Chris@909: Chris@909: @graph.add_element("path", { Chris@909: "d" => "M0 #@graph_height L" + lpath, Chris@909: "class" => "line#{line}" Chris@909: }) Chris@909: Chris@909: if show_data_points || show_data_values Chris@909: cum_sum.each_index do |i| Chris@909: if show_data_points Chris@909: @graph.add_element( "circle", { Chris@909: "cx" => (fieldwidth * i).to_s, Chris@909: "cy" => (@graph_height - cum_sum[i] * fieldheight).to_s, Chris@909: "r" => "2.5", Chris@909: "class" => "dataPoint#{line}" Chris@909: }) Chris@909: end Chris@909: make_datapoint_text( Chris@909: fieldwidth * i, Chris@909: @graph_height - cum_sum[i] * fieldheight - 6, Chris@909: cum_sum[i] + minvalue Chris@909: ) Chris@909: end Chris@909: end Chris@909: Chris@909: prev_sum = cum_sum.dup Chris@909: line -= 1 Chris@909: end Chris@909: end Chris@909: Chris@909: Chris@909: def get_css Chris@909: return <