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