To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
root / lib / SVG / Graph / Pie.rb @ 1297:0a574315af3e
History | View | Annotate | Download (10.5 KB)
| 1 | 1294:3e4c3460b6ca | Chris | require 'SVG/Graph/Graph'
|
|---|---|---|---|
| 2 | |||
| 3 | module SVG |
||
| 4 | module Graph |
||
| 5 | # === Create presentation quality SVG pie graphs easily
|
||
| 6 | #
|
||
| 7 | # == Synopsis
|
||
| 8 | #
|
||
| 9 | # require 'SVG/Graph/Pie'
|
||
| 10 | #
|
||
| 11 | # fields = %w(Jan Feb Mar)
|
||
| 12 | # data_sales_02 = [12, 45, 21]
|
||
| 13 | #
|
||
| 14 | # graph = SVG::Graph::Pie.new({
|
||
| 15 | # :height => 500,
|
||
| 16 | # :width => 300,
|
||
| 17 | # :fields => fields,
|
||
| 18 | # })
|
||
| 19 | #
|
||
| 20 | # graph.add_data({
|
||
| 21 | # :data => data_sales_02,
|
||
| 22 | # :title => 'Sales 2002',
|
||
| 23 | # })
|
||
| 24 | #
|
||
| 25 | # print "Content-type: image/svg+xml\r\n\r\n"
|
||
| 26 | # print graph.burn();
|
||
| 27 | #
|
||
| 28 | # == Description
|
||
| 29 | #
|
||
| 30 | # This object aims to allow you to easily create high quality
|
||
| 31 | # SVG pie graphs. You can either use the default style sheet
|
||
| 32 | # or supply your own. Either way there are many options which can
|
||
| 33 | # be configured to give you control over how the graph is
|
||
| 34 | # generated - with or without a key, display percent on pie chart,
|
||
| 35 | # title, subtitle etc.
|
||
| 36 | #
|
||
| 37 | # = Examples
|
||
| 38 | #
|
||
| 39 | # http://www.germane-software/repositories/public/SVG/test/single.rb
|
||
| 40 | #
|
||
| 41 | # == See also
|
||
| 42 | #
|
||
| 43 | # * SVG::Graph::Graph
|
||
| 44 | # * SVG::Graph::BarHorizontal
|
||
| 45 | # * SVG::Graph::Bar
|
||
| 46 | # * SVG::Graph::Line
|
||
| 47 | # * SVG::Graph::Plot
|
||
| 48 | # * SVG::Graph::TimeSeries
|
||
| 49 | #
|
||
| 50 | # == Author
|
||
| 51 | #
|
||
| 52 | # Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
|
||
| 53 | #
|
||
| 54 | # Copyright 2004 Sean E. Russell
|
||
| 55 | # This software is available under the Ruby license[LICENSE.txt]
|
||
| 56 | #
|
||
| 57 | class Pie < Graph |
||
| 58 | # Defaults are those set by Graph::initialize, and
|
||
| 59 | # [show_shadow] true
|
||
| 60 | # [shadow_offset] 10
|
||
| 61 | # [show_data_labels] false
|
||
| 62 | # [show_actual_values] false
|
||
| 63 | # [show_percent] true
|
||
| 64 | # [show_key_data_labels] true
|
||
| 65 | # [show_key_actual_values] true
|
||
| 66 | # [show_key_percent] false
|
||
| 67 | # [expanded] false
|
||
| 68 | # [expand_greatest] false
|
||
| 69 | # [expand_gap] 10
|
||
| 70 | # [show_x_labels] false
|
||
| 71 | # [show_y_labels] false
|
||
| 72 | # [datapoint_font_size] 12
|
||
| 73 | def set_defaults |
||
| 74 | init_with( |
||
| 75 | :show_shadow => true, |
||
| 76 | :shadow_offset => 10, |
||
| 77 | |||
| 78 | :show_data_labels => false, |
||
| 79 | :show_actual_values => false, |
||
| 80 | :show_percent => true, |
||
| 81 | |||
| 82 | :show_key_data_labels => true, |
||
| 83 | :show_key_actual_values => true, |
||
| 84 | :show_key_percent => false, |
||
| 85 | |||
| 86 | :expanded => false, |
||
| 87 | :expand_greatest => false, |
||
| 88 | :expand_gap => 10, |
||
| 89 | |||
| 90 | :show_x_labels => false, |
||
| 91 | :show_y_labels => false, |
||
| 92 | :datapoint_font_size => 12 |
||
| 93 | ) |
||
| 94 | @data = []
|
||
| 95 | end
|
||
| 96 | |||
| 97 | # Adds a data set to the graph.
|
||
| 98 | #
|
||
| 99 | # graph.add_data( { :data => [1,2,3,4] } )
|
||
| 100 | #
|
||
| 101 | # Note that the :title is not necessary. If multiple
|
||
| 102 | # data sets are added to the graph, the pie chart will
|
||
| 103 | # display the +sums+ of the data. EG:
|
||
| 104 | #
|
||
| 105 | # graph.add_data( { :data => [1,2,3,4] } )
|
||
| 106 | # graph.add_data( { :data => [2,3,5,9] } )
|
||
| 107 | #
|
||
| 108 | # is the same as:
|
||
| 109 | #
|
||
| 110 | # graph.add_data( { :data => [3,5,8,13] } )
|
||
| 111 | def add_data arg |
||
| 112 | arg[:data].each_index {|idx|
|
||
| 113 | @data[idx] = 0 unless @data[idx] |
||
| 114 | @data[idx] += arg[:data][idx] |
||
| 115 | } |
||
| 116 | end
|
||
| 117 | |||
| 118 | # If true, displays a drop shadow for the chart
|
||
| 119 | attr_accessor :show_shadow
|
||
| 120 | # Sets the offset of the shadow from the pie chart
|
||
| 121 | attr_accessor :shadow_offset
|
||
| 122 | # If true, display the data labels on the chart
|
||
| 123 | attr_accessor :show_data_labels
|
||
| 124 | # If true, display the actual field values in the data labels
|
||
| 125 | attr_accessor :show_actual_values
|
||
| 126 | # If true, display the percentage value of each pie wedge in the data
|
||
| 127 | # labels
|
||
| 128 | attr_accessor :show_percent
|
||
| 129 | # If true, display the labels in the key
|
||
| 130 | attr_accessor :show_key_data_labels
|
||
| 131 | # If true, display the actual value of the field in the key
|
||
| 132 | attr_accessor :show_key_actual_values
|
||
| 133 | # If true, display the percentage value of the wedges in the key
|
||
| 134 | attr_accessor :show_key_percent
|
||
| 135 | # If true, "explode" the pie (put space between the wedges)
|
||
| 136 | attr_accessor :expanded
|
||
| 137 | # If true, expand the largest pie wedge
|
||
| 138 | attr_accessor :expand_greatest
|
||
| 139 | # The amount of space between expanded wedges
|
||
| 140 | attr_accessor :expand_gap
|
||
| 141 | # The font size of the data point labels
|
||
| 142 | attr_accessor :datapoint_font_size
|
||
| 143 | |||
| 144 | |||
| 145 | protected |
||
| 146 | |||
| 147 | def add_defs defs |
||
| 148 | gradient = defs.add_element( "filter", {
|
||
| 149 | "id"=>"dropshadow", |
||
| 150 | "width" => "1.2", |
||
| 151 | "height" => "1.2", |
||
| 152 | } ) |
||
| 153 | gradient.add_element( "feGaussianBlur", {
|
||
| 154 | "stdDeviation" => "4", |
||
| 155 | "result" => "blur" |
||
| 156 | }) |
||
| 157 | end
|
||
| 158 | |||
| 159 | # We don't need the graph
|
||
| 160 | def draw_graph |
||
| 161 | end
|
||
| 162 | |||
| 163 | def get_y_labels |
||
| 164 | [""]
|
||
| 165 | end
|
||
| 166 | |||
| 167 | def get_x_labels |
||
| 168 | [""]
|
||
| 169 | end
|
||
| 170 | |||
| 171 | def keys |
||
| 172 | total = 0
|
||
| 173 | max_value = 0
|
||
| 174 | @data.each {|x| total += x }
|
||
| 175 | percent_scale = 100.0 / total
|
||
| 176 | count = -1
|
||
| 177 | a = @config[:fields].collect{ |x| |
||
| 178 | count += 1
|
||
| 179 | v = @data[count]
|
||
| 180 | perc = show_key_percent ? " "+(v * percent_scale).round.to_s+"%" : "" |
||
| 181 | x + " [" + v.to_s + "]" + perc |
||
| 182 | } |
||
| 183 | end
|
||
| 184 | |||
| 185 | RADIANS = Math::PI/180 |
||
| 186 | |||
| 187 | def draw_data |
||
| 188 | @graph = @root.add_element( "g" ) |
||
| 189 | background = @graph.add_element("g") |
||
| 190 | midground = @graph.add_element("g") |
||
| 191 | |||
| 192 | diameter = @graph_height > @graph_width ? @graph_width : @graph_height |
||
| 193 | diameter -= expand_gap if expanded or expand_greatest |
||
| 194 | diameter -= datapoint_font_size if show_data_labels
|
||
| 195 | diameter -= 10 if show_shadow |
||
| 196 | radius = diameter / 2.0
|
||
| 197 | |||
| 198 | xoff = (width - diameter) / 2
|
||
| 199 | yoff = (height - @border_bottom - diameter)
|
||
| 200 | yoff -= 10 if show_shadow |
||
| 201 | @graph.attributes['transform'] = "translate( #{xoff} #{yoff} )" |
||
| 202 | |||
| 203 | wedge_text_pad = 5
|
||
| 204 | wedge_text_pad = 20 if show_percent and show_data_labels |
||
| 205 | |||
| 206 | total = 0
|
||
| 207 | max_value = 0
|
||
| 208 | @data.each {|x|
|
||
| 209 | max_value = max_value < x ? x : max_value |
||
| 210 | total += x |
||
| 211 | } |
||
| 212 | percent_scale = 100.0 / total
|
||
| 213 | |||
| 214 | prev_percent = 0
|
||
| 215 | rad_mult = 3.6 * RADIANS |
||
| 216 | @config[:fields].each_index { |count| |
||
| 217 | value = @data[count]
|
||
| 218 | percent = percent_scale * value |
||
| 219 | |||
| 220 | radians = prev_percent * rad_mult |
||
| 221 | x_start = radius+(Math.sin(radians) * radius)
|
||
| 222 | y_start = radius-(Math.cos(radians) * radius)
|
||
| 223 | radians = (prev_percent+percent) * rad_mult |
||
| 224 | x_end = radius+(Math.sin(radians) * radius)
|
||
| 225 | x_end -= 0.00001 if @data.length == 1 |
||
| 226 | y_end = radius-(Math.cos(radians) * radius)
|
||
| 227 | path = "M#{radius},#{radius} L#{x_start},#{y_start} "+
|
||
| 228 | "A#{radius},#{radius} "+
|
||
| 229 | "0, #{percent >= 50 ? '1' : '0'},1, "+
|
||
| 230 | "#{x_end} #{y_end} Z"
|
||
| 231 | |||
| 232 | |||
| 233 | wedge = @foreground.add_element( "path", { |
||
| 234 | "d" => path,
|
||
| 235 | "class" => "fill#{count+1}" |
||
| 236 | }) |
||
| 237 | |||
| 238 | translate = nil
|
||
| 239 | tx = 0
|
||
| 240 | ty = 0
|
||
| 241 | half_percent = prev_percent + percent / 2
|
||
| 242 | radians = half_percent * rad_mult |
||
| 243 | |||
| 244 | if show_shadow
|
||
| 245 | shadow = background.add_element( "path", {
|
||
| 246 | "d" => path,
|
||
| 247 | "filter" => "url(#dropshadow)", |
||
| 248 | "style" => "fill: #ccc; stroke: none;" |
||
| 249 | }) |
||
| 250 | clear = midground.add_element( "path", {
|
||
| 251 | "d" => path,
|
||
| 252 | "style" => "fill: #fff; stroke: none;" |
||
| 253 | }) |
||
| 254 | end
|
||
| 255 | |||
| 256 | if expanded or (expand_greatest && value == max_value) |
||
| 257 | tx = (Math.sin(radians) * expand_gap)
|
||
| 258 | ty = -(Math.cos(radians) * expand_gap)
|
||
| 259 | translate = "translate( #{tx} #{ty} )"
|
||
| 260 | wedge.attributes["transform"] = translate
|
||
| 261 | clear.attributes["transform"] = translate if clear |
||
| 262 | end
|
||
| 263 | |||
| 264 | if show_shadow
|
||
| 265 | shadow.attributes["transform"] =
|
||
| 266 | "translate( #{tx+shadow_offset} #{ty+shadow_offset} )"
|
||
| 267 | end
|
||
| 268 | |||
| 269 | if show_data_labels and value != 0 |
||
| 270 | label = ""
|
||
| 271 | label += @config[:fields][count] if show_key_data_labels |
||
| 272 | label += " ["+value.to_s+"]" if show_actual_values |
||
| 273 | label += " "+percent.round.to_s+"%" if show_percent |
||
| 274 | |||
| 275 | msr = Math.sin(radians)
|
||
| 276 | mcr = Math.cos(radians)
|
||
| 277 | tx = radius + (msr * radius) |
||
| 278 | ty = radius -(mcr * radius) |
||
| 279 | |||
| 280 | if expanded or (expand_greatest && value == max_value) |
||
| 281 | tx += (msr * expand_gap) |
||
| 282 | ty -= (mcr * expand_gap) |
||
| 283 | end
|
||
| 284 | @foreground.add_element( "text", { |
||
| 285 | "x" => tx.to_s,
|
||
| 286 | "y" => ty.to_s,
|
||
| 287 | "class" => "dataPointLabel", |
||
| 288 | "style" => "stroke: #fff; stroke-width: 2;" |
||
| 289 | }).text = label.to_s |
||
| 290 | @foreground.add_element( "text", { |
||
| 291 | "x" => tx.to_s,
|
||
| 292 | "y" => ty.to_s,
|
||
| 293 | "class" => "dataPointLabel", |
||
| 294 | }).text = label.to_s |
||
| 295 | end
|
||
| 296 | |||
| 297 | prev_percent += percent |
||
| 298 | } |
||
| 299 | end
|
||
| 300 | |||
| 301 | |||
| 302 | def round val, to |
||
| 303 | up = 10**to.to_f
|
||
| 304 | (val * up).to_i / up |
||
| 305 | end
|
||
| 306 | |||
| 307 | |||
| 308 | def get_css |
||
| 309 | return <<EOL |
||
| 310 | .dataPointLabel{
|
||
| 311 | fill: #000000;
|
||
| 312 | text-anchor:middle;
|
||
| 313 | font-size: #{datapoint_font_size}px;
|
||
| 314 | font-family: "Arial", sans-serif;
|
||
| 315 | font-weight: normal;
|
||
| 316 | }
|
||
| 317 |
|
||
| 318 | /* key - MUST match fill styles */
|
||
| 319 | .key1,.fill1{
|
||
| 320 | fill: #ff0000;
|
||
| 321 | fill-opacity: 0.7;
|
||
| 322 | stroke: none;
|
||
| 323 | stroke-width: 1px;
|
||
| 324 | }
|
||
| 325 | .key2,.fill2{
|
||
| 326 | fill: #0000ff;
|
||
| 327 | fill-opacity: 0.7;
|
||
| 328 | stroke: none;
|
||
| 329 | stroke-width: 1px;
|
||
| 330 | }
|
||
| 331 | .key3,.fill3{
|
||
| 332 | fill-opacity: 0.7;
|
||
| 333 | fill: #00ff00;
|
||
| 334 | stroke: none;
|
||
| 335 | stroke-width: 1px;
|
||
| 336 | }
|
||
| 337 | .key4,.fill4{
|
||
| 338 | fill-opacity: 0.7;
|
||
| 339 | fill: #ffcc00;
|
||
| 340 | stroke: none;
|
||
| 341 | stroke-width: 1px;
|
||
| 342 | }
|
||
| 343 | .key5,.fill5{
|
||
| 344 | fill-opacity: 0.7;
|
||
| 345 | fill: #00ccff;
|
||
| 346 | stroke: none;
|
||
| 347 | stroke-width: 1px;
|
||
| 348 | }
|
||
| 349 | .key6,.fill6{
|
||
| 350 | fill-opacity: 0.7;
|
||
| 351 | fill: #ff00ff;
|
||
| 352 | stroke: none;
|
||
| 353 | stroke-width: 1px;
|
||
| 354 | }
|
||
| 355 | .key7,.fill7{
|
||
| 356 | fill-opacity: 0.7;
|
||
| 357 | fill: #00ff99;
|
||
| 358 | stroke: none;
|
||
| 359 | stroke-width: 1px;
|
||
| 360 | }
|
||
| 361 | .key8,.fill8{
|
||
| 362 | fill-opacity: 0.7;
|
||
| 363 | fill: #ffff00;
|
||
| 364 | stroke: none;
|
||
| 365 | stroke-width: 1px;
|
||
| 366 | }
|
||
| 367 | .key9,.fill9{
|
||
| 368 | fill-opacity: 0.7;
|
||
| 369 | fill: #cc6666;
|
||
| 370 | stroke: none;
|
||
| 371 | stroke-width: 1px;
|
||
| 372 | }
|
||
| 373 | .key10,.fill10{
|
||
| 374 | fill-opacity: 0.7;
|
||
| 375 | fill: #663399;
|
||
| 376 | stroke: none;
|
||
| 377 | stroke-width: 1px;
|
||
| 378 | }
|
||
| 379 | .key11,.fill11{
|
||
| 380 | fill-opacity: 0.7;
|
||
| 381 | fill: #339900;
|
||
| 382 | stroke: none;
|
||
| 383 | stroke-width: 1px;
|
||
| 384 | }
|
||
| 385 | .key12,.fill12{
|
||
| 386 | fill-opacity: 0.7;
|
||
| 387 | fill: #9966FF;
|
||
| 388 | stroke: none;
|
||
| 389 | stroke-width: 1px;
|
||
| 390 | }
|
||
| 391 | EOL
|
||
| 392 | end
|
||
| 393 | end
|
||
| 394 | end
|
||
| 395 | end |