Chris@1295: require 'SVG/Graph/Graph' Chris@1295: Chris@1295: module SVG Chris@1295: module Graph Chris@1295: # === Create presentation quality SVG pie graphs easily Chris@1295: # Chris@1295: # == Synopsis Chris@1295: # Chris@1295: # require 'SVG/Graph/Pie' Chris@1295: # Chris@1295: # fields = %w(Jan Feb Mar) Chris@1295: # data_sales_02 = [12, 45, 21] Chris@1295: # Chris@1295: # graph = SVG::Graph::Pie.new({ Chris@1295: # :height => 500, Chris@1295: # :width => 300, Chris@1295: # :fields => fields, Chris@1295: # }) Chris@1295: # Chris@1295: # graph.add_data({ Chris@1295: # :data => data_sales_02, Chris@1295: # :title => 'Sales 2002', Chris@1295: # }) Chris@1295: # Chris@1295: # print "Content-type: image/svg+xml\r\n\r\n" Chris@1295: # print graph.burn(); Chris@1295: # Chris@1295: # == Description Chris@1295: # Chris@1295: # This object aims to allow you to easily create high quality Chris@1295: # SVG pie graphs. You can either use the default style sheet Chris@1295: # or supply your own. Either way there are many options which can Chris@1295: # be configured to give you control over how the graph is Chris@1295: # generated - with or without a key, display percent on pie chart, Chris@1295: # title, subtitle etc. Chris@1295: # Chris@1295: # = Examples Chris@1295: # Chris@1295: # http://www.germane-software/repositories/public/SVG/test/single.rb Chris@1295: # Chris@1295: # == See also Chris@1295: # Chris@1295: # * SVG::Graph::Graph Chris@1295: # * SVG::Graph::BarHorizontal Chris@1295: # * SVG::Graph::Bar Chris@1295: # * SVG::Graph::Line Chris@1295: # * SVG::Graph::Plot Chris@1295: # * SVG::Graph::TimeSeries Chris@1295: # Chris@1295: # == Author Chris@1295: # Chris@1295: # Sean E. Russell Chris@1295: # Chris@1295: # Copyright 2004 Sean E. Russell Chris@1295: # This software is available under the Ruby license[LICENSE.txt] Chris@1295: # Chris@1295: class Pie < Graph Chris@1295: # Defaults are those set by Graph::initialize, and Chris@1295: # [show_shadow] true Chris@1295: # [shadow_offset] 10 Chris@1295: # [show_data_labels] false Chris@1295: # [show_actual_values] false Chris@1295: # [show_percent] true Chris@1295: # [show_key_data_labels] true Chris@1295: # [show_key_actual_values] true Chris@1295: # [show_key_percent] false Chris@1295: # [expanded] false Chris@1295: # [expand_greatest] false Chris@1295: # [expand_gap] 10 Chris@1295: # [show_x_labels] false Chris@1295: # [show_y_labels] false Chris@1295: # [datapoint_font_size] 12 Chris@1295: def set_defaults Chris@1295: init_with( Chris@1295: :show_shadow => true, Chris@1295: :shadow_offset => 10, Chris@1295: Chris@1295: :show_data_labels => false, Chris@1295: :show_actual_values => false, Chris@1295: :show_percent => true, Chris@1295: Chris@1295: :show_key_data_labels => true, Chris@1295: :show_key_actual_values => true, Chris@1295: :show_key_percent => false, Chris@1295: Chris@1295: :expanded => false, Chris@1295: :expand_greatest => false, Chris@1295: :expand_gap => 10, Chris@1295: Chris@1295: :show_x_labels => false, Chris@1295: :show_y_labels => false, Chris@1295: :datapoint_font_size => 12 Chris@1295: ) Chris@1295: @data = [] Chris@1295: end Chris@1295: Chris@1295: # Adds a data set to the graph. Chris@1295: # Chris@1295: # graph.add_data( { :data => [1,2,3,4] } ) Chris@1295: # Chris@1295: # Note that the :title is not necessary. If multiple Chris@1295: # data sets are added to the graph, the pie chart will Chris@1295: # display the +sums+ of the data. EG: Chris@1295: # Chris@1295: # graph.add_data( { :data => [1,2,3,4] } ) Chris@1295: # graph.add_data( { :data => [2,3,5,9] } ) Chris@1295: # Chris@1295: # is the same as: Chris@1295: # Chris@1295: # graph.add_data( { :data => [3,5,8,13] } ) Chris@1295: def add_data arg Chris@1295: arg[:data].each_index {|idx| Chris@1295: @data[idx] = 0 unless @data[idx] Chris@1295: @data[idx] += arg[:data][idx] Chris@1295: } Chris@1295: end Chris@1295: Chris@1295: # If true, displays a drop shadow for the chart Chris@1295: attr_accessor :show_shadow Chris@1295: # Sets the offset of the shadow from the pie chart Chris@1295: attr_accessor :shadow_offset Chris@1295: # If true, display the data labels on the chart Chris@1295: attr_accessor :show_data_labels Chris@1295: # If true, display the actual field values in the data labels Chris@1295: attr_accessor :show_actual_values Chris@1295: # If true, display the percentage value of each pie wedge in the data Chris@1295: # labels Chris@1295: attr_accessor :show_percent Chris@1295: # If true, display the labels in the key Chris@1295: attr_accessor :show_key_data_labels Chris@1295: # If true, display the actual value of the field in the key Chris@1295: attr_accessor :show_key_actual_values Chris@1295: # If true, display the percentage value of the wedges in the key Chris@1295: attr_accessor :show_key_percent Chris@1295: # If true, "explode" the pie (put space between the wedges) Chris@1295: attr_accessor :expanded Chris@1295: # If true, expand the largest pie wedge Chris@1295: attr_accessor :expand_greatest Chris@1295: # The amount of space between expanded wedges Chris@1295: attr_accessor :expand_gap Chris@1295: # The font size of the data point labels Chris@1295: attr_accessor :datapoint_font_size Chris@1295: Chris@1295: Chris@1295: protected Chris@1295: Chris@1295: def add_defs defs Chris@1295: gradient = defs.add_element( "filter", { Chris@1295: "id"=>"dropshadow", Chris@1295: "width" => "1.2", Chris@1295: "height" => "1.2", Chris@1295: } ) Chris@1295: gradient.add_element( "feGaussianBlur", { Chris@1295: "stdDeviation" => "4", Chris@1295: "result" => "blur" Chris@1295: }) Chris@1295: end Chris@1295: Chris@1295: # We don't need the graph Chris@1295: def draw_graph Chris@1295: end Chris@1295: Chris@1295: def get_y_labels Chris@1295: [""] Chris@1295: end Chris@1295: Chris@1295: def get_x_labels Chris@1295: [""] Chris@1295: end Chris@1295: Chris@1295: def keys Chris@1295: total = 0 Chris@1295: max_value = 0 Chris@1295: @data.each {|x| total += x } Chris@1295: percent_scale = 100.0 / total Chris@1295: count = -1 Chris@1295: a = @config[:fields].collect{ |x| Chris@1295: count += 1 Chris@1295: v = @data[count] Chris@1295: perc = show_key_percent ? " "+(v * percent_scale).round.to_s+"%" : "" Chris@1295: x + " [" + v.to_s + "]" + perc Chris@1295: } Chris@1295: end Chris@1295: Chris@1295: RADIANS = Math::PI/180 Chris@1295: Chris@1295: def draw_data Chris@1295: @graph = @root.add_element( "g" ) Chris@1295: background = @graph.add_element("g") Chris@1295: midground = @graph.add_element("g") Chris@1295: Chris@1295: diameter = @graph_height > @graph_width ? @graph_width : @graph_height Chris@1295: diameter -= expand_gap if expanded or expand_greatest Chris@1295: diameter -= datapoint_font_size if show_data_labels Chris@1295: diameter -= 10 if show_shadow Chris@1295: radius = diameter / 2.0 Chris@1295: Chris@1295: xoff = (width - diameter) / 2 Chris@1295: yoff = (height - @border_bottom - diameter) Chris@1295: yoff -= 10 if show_shadow Chris@1295: @graph.attributes['transform'] = "translate( #{xoff} #{yoff} )" Chris@1295: Chris@1295: wedge_text_pad = 5 Chris@1295: wedge_text_pad = 20 if show_percent and show_data_labels Chris@1295: Chris@1295: total = 0 Chris@1295: max_value = 0 Chris@1295: @data.each {|x| Chris@1295: max_value = max_value < x ? x : max_value Chris@1295: total += x Chris@1295: } Chris@1295: percent_scale = 100.0 / total Chris@1295: Chris@1295: prev_percent = 0 Chris@1295: rad_mult = 3.6 * RADIANS Chris@1295: @config[:fields].each_index { |count| Chris@1295: value = @data[count] Chris@1295: percent = percent_scale * value Chris@1295: Chris@1295: radians = prev_percent * rad_mult Chris@1295: x_start = radius+(Math.sin(radians) * radius) Chris@1295: y_start = radius-(Math.cos(radians) * radius) Chris@1295: radians = (prev_percent+percent) * rad_mult Chris@1295: x_end = radius+(Math.sin(radians) * radius) Chris@1295: x_end -= 0.00001 if @data.length == 1 Chris@1295: y_end = radius-(Math.cos(radians) * radius) Chris@1295: path = "M#{radius},#{radius} L#{x_start},#{y_start} "+ Chris@1295: "A#{radius},#{radius} "+ Chris@1295: "0, #{percent >= 50 ? '1' : '0'},1, "+ Chris@1295: "#{x_end} #{y_end} Z" Chris@1295: Chris@1295: Chris@1295: wedge = @foreground.add_element( "path", { Chris@1295: "d" => path, Chris@1295: "class" => "fill#{count+1}" Chris@1295: }) Chris@1295: Chris@1295: translate = nil Chris@1295: tx = 0 Chris@1295: ty = 0 Chris@1295: half_percent = prev_percent + percent / 2 Chris@1295: radians = half_percent * rad_mult Chris@1295: Chris@1295: if show_shadow Chris@1295: shadow = background.add_element( "path", { Chris@1295: "d" => path, Chris@1295: "filter" => "url(#dropshadow)", Chris@1295: "style" => "fill: #ccc; stroke: none;" Chris@1295: }) Chris@1295: clear = midground.add_element( "path", { Chris@1295: "d" => path, Chris@1295: "style" => "fill: #fff; stroke: none;" Chris@1295: }) Chris@1295: end Chris@1295: Chris@1295: if expanded or (expand_greatest && value == max_value) Chris@1295: tx = (Math.sin(radians) * expand_gap) Chris@1295: ty = -(Math.cos(radians) * expand_gap) Chris@1295: translate = "translate( #{tx} #{ty} )" Chris@1295: wedge.attributes["transform"] = translate Chris@1295: clear.attributes["transform"] = translate if clear Chris@1295: end Chris@1295: Chris@1295: if show_shadow Chris@1295: shadow.attributes["transform"] = Chris@1295: "translate( #{tx+shadow_offset} #{ty+shadow_offset} )" Chris@1295: end Chris@1295: Chris@1295: if show_data_labels and value != 0 Chris@1295: label = "" Chris@1295: label += @config[:fields][count] if show_key_data_labels Chris@1295: label += " ["+value.to_s+"]" if show_actual_values Chris@1295: label += " "+percent.round.to_s+"%" if show_percent Chris@1295: Chris@1295: msr = Math.sin(radians) Chris@1295: mcr = Math.cos(radians) Chris@1295: tx = radius + (msr * radius) Chris@1295: ty = radius -(mcr * radius) Chris@1295: Chris@1295: if expanded or (expand_greatest && value == max_value) Chris@1295: tx += (msr * expand_gap) Chris@1295: ty -= (mcr * expand_gap) Chris@1295: end Chris@1295: @foreground.add_element( "text", { Chris@1295: "x" => tx.to_s, Chris@1295: "y" => ty.to_s, Chris@1295: "class" => "dataPointLabel", Chris@1295: "style" => "stroke: #fff; stroke-width: 2;" Chris@1295: }).text = label.to_s Chris@1295: @foreground.add_element( "text", { Chris@1295: "x" => tx.to_s, Chris@1295: "y" => ty.to_s, Chris@1295: "class" => "dataPointLabel", Chris@1295: }).text = label.to_s Chris@1295: end Chris@1295: Chris@1295: prev_percent += percent Chris@1295: } Chris@1295: end Chris@1295: Chris@1295: Chris@1295: def round val, to Chris@1295: up = 10**to.to_f Chris@1295: (val * up).to_i / up Chris@1295: end Chris@1295: Chris@1295: Chris@1295: def get_css Chris@1295: return <