To check out this repository please hg clone the following URL, or open the URL using EasyMercurial or your preferred Mercurial client.
root / .svn / pristine / 7d / 7d9750d88bd333d9b7606eb3694629d8e8fa3a5e.svn-base @ 1297:0a574315af3e
History | View | Annotate | Download (10.5 KB)
| 1 | 1296:038ba2d95de8 | 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 |