Pie.rb
395 lines
| 10.9 KiB
| text/x-ruby
|
RubyLexer
|
r2553 | require 'SVG/Graph/Graph' | ||
module SVG | ||||
module Graph | ||||
# === Create presentation quality SVG pie graphs easily | ||||
# | ||||
# == Synopsis | ||||
# | ||||
# require 'SVG/Graph/Pie' | ||||
# | ||||
# fields = %w(Jan Feb Mar) | ||||
# data_sales_02 = [12, 45, 21] | ||||
# | ||||
# graph = SVG::Graph::Pie.new({ | ||||
# :height => 500, | ||||
# :width => 300, | ||||
# :fields => fields, | ||||
# }) | ||||
# | ||||
# graph.add_data({ | ||||
# :data => data_sales_02, | ||||
# :title => 'Sales 2002', | ||||
# }) | ||||
# | ||||
# print "Content-type: image/svg+xml\r\n\r\n" | ||||
# print graph.burn(); | ||||
# | ||||
# == Description | ||||
# | ||||
# This object aims to allow you to easily create high quality | ||||
# SVG pie graphs. 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, display percent on pie chart, | ||||
# title, subtitle etc. | ||||
# | ||||
# = Examples | ||||
# | ||||
# http://www.germane-software/repositories/public/SVG/test/single.rb | ||||
# | ||||
# == See also | ||||
# | ||||
# * SVG::Graph::Graph | ||||
# * SVG::Graph::BarHorizontal | ||||
# * SVG::Graph::Bar | ||||
# * SVG::Graph::Line | ||||
# * 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 Pie < Graph | ||||
# Defaults are those set by Graph::initialize, and | ||||
# [show_shadow] true | ||||
# [shadow_offset] 10 | ||||
# [show_data_labels] false | ||||
# [show_actual_values] false | ||||
# [show_percent] true | ||||
# [show_key_data_labels] true | ||||
# [show_key_actual_values] true | ||||
# [show_key_percent] false | ||||
# [expanded] false | ||||
# [expand_greatest] false | ||||
# [expand_gap] 10 | ||||
# [show_x_labels] false | ||||
# [show_y_labels] false | ||||
# [datapoint_font_size] 12 | ||||
def set_defaults | ||||
init_with( | ||||
:show_shadow => true, | ||||
:shadow_offset => 10, | ||||
:show_data_labels => false, | ||||
:show_actual_values => false, | ||||
:show_percent => true, | ||||
:show_key_data_labels => true, | ||||
:show_key_actual_values => true, | ||||
:show_key_percent => false, | ||||
:expanded => false, | ||||
:expand_greatest => false, | ||||
:expand_gap => 10, | ||||
:show_x_labels => false, | ||||
:show_y_labels => false, | ||||
:datapoint_font_size => 12 | ||||
) | ||||
@data = [] | ||||
end | ||||
# Adds a data set to the graph. | ||||
# | ||||
# graph.add_data( { :data => [1,2,3,4] } ) | ||||
# | ||||
# Note that the :title is not necessary. If multiple | ||||
# data sets are added to the graph, the pie chart will | ||||
# display the +sums+ of the data. EG: | ||||
# | ||||
# graph.add_data( { :data => [1,2,3,4] } ) | ||||
# graph.add_data( { :data => [2,3,5,9] } ) | ||||
# | ||||
# is the same as: | ||||
# | ||||
# graph.add_data( { :data => [3,5,8,13] } ) | ||||
def add_data arg | ||||
arg[:data].each_index {|idx| | ||||
@data[idx] = 0 unless @data[idx] | ||||
@data[idx] += arg[:data][idx] | ||||
} | ||||
end | ||||
# If true, displays a drop shadow for the chart | ||||
attr_accessor :show_shadow | ||||
# Sets the offset of the shadow from the pie chart | ||||
attr_accessor :shadow_offset | ||||
# If true, display the data labels on the chart | ||||
attr_accessor :show_data_labels | ||||
# If true, display the actual field values in the data labels | ||||
attr_accessor :show_actual_values | ||||
# If true, display the percentage value of each pie wedge in the data | ||||
# labels | ||||
attr_accessor :show_percent | ||||
# If true, display the labels in the key | ||||
attr_accessor :show_key_data_labels | ||||
# If true, display the actual value of the field in the key | ||||
attr_accessor :show_key_actual_values | ||||
# If true, display the percentage value of the wedges in the key | ||||
attr_accessor :show_key_percent | ||||
# If true, "explode" the pie (put space between the wedges) | ||||
attr_accessor :expanded | ||||
# If true, expand the largest pie wedge | ||||
attr_accessor :expand_greatest | ||||
# The amount of space between expanded wedges | ||||
attr_accessor :expand_gap | ||||
# The font size of the data point labels | ||||
attr_accessor :datapoint_font_size | ||||
protected | ||||
def add_defs defs | ||||
gradient = defs.add_element( "filter", { | ||||
"id"=>"dropshadow", | ||||
"width" => "1.2", | ||||
"height" => "1.2", | ||||
} ) | ||||
gradient.add_element( "feGaussianBlur", { | ||||
"stdDeviation" => "4", | ||||
"result" => "blur" | ||||
}) | ||||
end | ||||
# We don't need the graph | ||||
def draw_graph | ||||
end | ||||
def get_y_labels | ||||
[""] | ||||
end | ||||
def get_x_labels | ||||
[""] | ||||
end | ||||
def keys | ||||
total = 0 | ||||
max_value = 0 | ||||
@data.each {|x| total += x } | ||||
percent_scale = 100.0 / total | ||||
count = -1 | ||||
a = @config[:fields].collect{ |x| | ||||
count += 1 | ||||
v = @data[count] | ||||
perc = show_key_percent ? " "+(v * percent_scale).round.to_s+"%" : "" | ||||
x + " [" + v.to_s + "]" + perc | ||||
} | ||||
end | ||||
RADIANS = Math::PI/180 | ||||
def draw_data | ||||
@graph = @root.add_element( "g" ) | ||||
background = @graph.add_element("g") | ||||
midground = @graph.add_element("g") | ||||
diameter = @graph_height > @graph_width ? @graph_width : @graph_height | ||||
diameter -= expand_gap if expanded or expand_greatest | ||||
diameter -= datapoint_font_size if show_data_labels | ||||
diameter -= 10 if show_shadow | ||||
radius = diameter / 2.0 | ||||
xoff = (width - diameter) / 2 | ||||
yoff = (height - @border_bottom - diameter) | ||||
yoff -= 10 if show_shadow | ||||
@graph.attributes['transform'] = "translate( #{xoff} #{yoff} )" | ||||
wedge_text_pad = 5 | ||||
wedge_text_pad = 20 if show_percent and show_data_labels | ||||
total = 0 | ||||
max_value = 0 | ||||
@data.each {|x| | ||||
max_value = max_value < x ? x : max_value | ||||
total += x | ||||
} | ||||
percent_scale = 100.0 / total | ||||
prev_percent = 0 | ||||
rad_mult = 3.6 * RADIANS | ||||
@config[:fields].each_index { |count| | ||||
value = @data[count] | ||||
percent = percent_scale * value | ||||
radians = prev_percent * rad_mult | ||||
x_start = radius+(Math.sin(radians) * radius) | ||||
y_start = radius-(Math.cos(radians) * radius) | ||||
radians = (prev_percent+percent) * rad_mult | ||||
x_end = radius+(Math.sin(radians) * radius) | ||||
x_end -= 0.00001 if @data.length == 1 | ||||
y_end = radius-(Math.cos(radians) * radius) | ||||
path = "M#{radius},#{radius} L#{x_start},#{y_start} "+ | ||||
"A#{radius},#{radius} "+ | ||||
"0, #{percent >= 50 ? '1' : '0'},1, "+ | ||||
"#{x_end} #{y_end} Z" | ||||
wedge = @foreground.add_element( "path", { | ||||
"d" => path, | ||||
"class" => "fill#{count+1}" | ||||
}) | ||||
translate = nil | ||||
tx = 0 | ||||
ty = 0 | ||||
half_percent = prev_percent + percent / 2 | ||||
radians = half_percent * rad_mult | ||||
if show_shadow | ||||
shadow = background.add_element( "path", { | ||||
"d" => path, | ||||
"filter" => "url(#dropshadow)", | ||||
"style" => "fill: #ccc; stroke: none;" | ||||
}) | ||||
clear = midground.add_element( "path", { | ||||
"d" => path, | ||||
"style" => "fill: #fff; stroke: none;" | ||||
}) | ||||
end | ||||
if expanded or (expand_greatest && value == max_value) | ||||
tx = (Math.sin(radians) * expand_gap) | ||||
ty = -(Math.cos(radians) * expand_gap) | ||||
translate = "translate( #{tx} #{ty} )" | ||||
wedge.attributes["transform"] = translate | ||||
clear.attributes["transform"] = translate if clear | ||||
end | ||||
if show_shadow | ||||
shadow.attributes["transform"] = | ||||
"translate( #{tx+shadow_offset} #{ty+shadow_offset} )" | ||||
end | ||||
if show_data_labels and value != 0 | ||||
label = "" | ||||
label += @config[:fields][count] if show_key_data_labels | ||||
label += " ["+value.to_s+"]" if show_actual_values | ||||
label += " "+percent.round.to_s+"%" if show_percent | ||||
msr = Math.sin(radians) | ||||
mcr = Math.cos(radians) | ||||
tx = radius + (msr * radius) | ||||
ty = radius -(mcr * radius) | ||||
if expanded or (expand_greatest && value == max_value) | ||||
tx += (msr * expand_gap) | ||||
ty -= (mcr * expand_gap) | ||||
end | ||||
@foreground.add_element( "text", { | ||||
"x" => tx.to_s, | ||||
"y" => ty.to_s, | ||||
"class" => "dataPointLabel", | ||||
"style" => "stroke: #fff; stroke-width: 2;" | ||||
}).text = label.to_s | ||||
@foreground.add_element( "text", { | ||||
"x" => tx.to_s, | ||||
"y" => ty.to_s, | ||||
"class" => "dataPointLabel", | ||||
}).text = label.to_s | ||||
end | ||||
prev_percent += percent | ||||
} | ||||
end | ||||
def round val, to | ||||
up = 10**to.to_f | ||||
(val * up).to_i / up | ||||
end | ||||
def get_css | ||||
return <<EOL | ||||
.dataPointLabel{ | ||||
fill: #000000; | ||||
text-anchor:middle; | ||||
font-size: #{datapoint_font_size}px; | ||||
font-family: "Arial", sans-serif; | ||||
font-weight: normal; | ||||
} | ||||
/* key - MUST match fill styles */ | ||||
.key1,.fill1{ | ||||
fill: #ff0000; | ||||
fill-opacity: 0.7; | ||||
stroke: none; | ||||
stroke-width: 1px; | ||||
} | ||||
.key2,.fill2{ | ||||
fill: #0000ff; | ||||
fill-opacity: 0.7; | ||||
stroke: none; | ||||
stroke-width: 1px; | ||||
} | ||||
.key3,.fill3{ | ||||
fill-opacity: 0.7; | ||||
fill: #00ff00; | ||||
stroke: none; | ||||
stroke-width: 1px; | ||||
} | ||||
.key4,.fill4{ | ||||
fill-opacity: 0.7; | ||||
fill: #ffcc00; | ||||
stroke: none; | ||||
stroke-width: 1px; | ||||
} | ||||
.key5,.fill5{ | ||||
fill-opacity: 0.7; | ||||
fill: #00ccff; | ||||
stroke: none; | ||||
stroke-width: 1px; | ||||
} | ||||
.key6,.fill6{ | ||||
fill-opacity: 0.7; | ||||
fill: #ff00ff; | ||||
stroke: none; | ||||
stroke-width: 1px; | ||||
} | ||||
.key7,.fill7{ | ||||
fill-opacity: 0.7; | ||||
fill: #00ff99; | ||||
stroke: none; | ||||
stroke-width: 1px; | ||||
} | ||||
.key8,.fill8{ | ||||
fill-opacity: 0.7; | ||||
fill: #ffff00; | ||||
stroke: none; | ||||
stroke-width: 1px; | ||||
} | ||||
.key9,.fill9{ | ||||
fill-opacity: 0.7; | ||||
fill: #cc6666; | ||||
stroke: none; | ||||
stroke-width: 1px; | ||||
} | ||||
.key10,.fill10{ | ||||
fill-opacity: 0.7; | ||||
fill: #663399; | ||||
stroke: none; | ||||
stroke-width: 1px; | ||||
} | ||||
.key11,.fill11{ | ||||
fill-opacity: 0.7; | ||||
fill: #339900; | ||||
stroke: none; | ||||
stroke-width: 1px; | ||||
} | ||||
.key12,.fill12{ | ||||
fill-opacity: 0.7; | ||||
fill: #9966FF; | ||||
stroke: none; | ||||
stroke-width: 1px; | ||||
} | ||||
EOL | ||||
end | ||||
end | ||||
end | ||||
end | ||||