tlconnor-scruffy 0.2.17
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES.txt +115 -0
- data/LICENCE.txt +20 -0
- data/Manifest.txt +74 -0
- data/README.txt +66 -0
- data/lib/scruffy.rb +30 -0
- data/lib/scruffy/components.rb +22 -0
- data/lib/scruffy/components/axes.rb +23 -0
- data/lib/scruffy/components/background.rb +24 -0
- data/lib/scruffy/components/base.rb +57 -0
- data/lib/scruffy/components/data_markers.rb +41 -0
- data/lib/scruffy/components/graphs.rb +52 -0
- data/lib/scruffy/components/grid.rb +57 -0
- data/lib/scruffy/components/label.rb +17 -0
- data/lib/scruffy/components/legend.rb +147 -0
- data/lib/scruffy/components/style_info.rb +22 -0
- data/lib/scruffy/components/title.rb +19 -0
- data/lib/scruffy/components/value_markers.rb +25 -0
- data/lib/scruffy/components/viewport.rb +37 -0
- data/lib/scruffy/formatters.rb +233 -0
- data/lib/scruffy/graph.rb +205 -0
- data/lib/scruffy/graph_state.rb +29 -0
- data/lib/scruffy/helpers.rb +13 -0
- data/lib/scruffy/helpers/canvas.rb +41 -0
- data/lib/scruffy/helpers/layer_container.rb +119 -0
- data/lib/scruffy/helpers/marker_helper.rb +25 -0
- data/lib/scruffy/helpers/meta.rb +5 -0
- data/lib/scruffy/helpers/point_container.rb +99 -0
- data/lib/scruffy/layers.rb +28 -0
- data/lib/scruffy/layers/all_smiles.rb +137 -0
- data/lib/scruffy/layers/area.rb +46 -0
- data/lib/scruffy/layers/average.rb +67 -0
- data/lib/scruffy/layers/bar.rb +73 -0
- data/lib/scruffy/layers/base.rb +211 -0
- data/lib/scruffy/layers/box.rb +114 -0
- data/lib/scruffy/layers/line.rb +46 -0
- data/lib/scruffy/layers/multi.rb +74 -0
- data/lib/scruffy/layers/multi_bar.rb +51 -0
- data/lib/scruffy/layers/pie.rb +123 -0
- data/lib/scruffy/layers/pie_slice.rb +119 -0
- data/lib/scruffy/layers/scatter.rb +29 -0
- data/lib/scruffy/layers/sparkline_bar.rb +39 -0
- data/lib/scruffy/layers/stacked.rb +87 -0
- data/lib/scruffy/rasterizers.rb +14 -0
- data/lib/scruffy/rasterizers/batik_rasterizer.rb +39 -0
- data/lib/scruffy/rasterizers/mini_magick_rasterizer.rb +24 -0
- data/lib/scruffy/rasterizers/rmagick_rasterizer.rb +27 -0
- data/lib/scruffy/renderers.rb +23 -0
- data/lib/scruffy/renderers/axis_legend.rb +41 -0
- data/lib/scruffy/renderers/base.rb +95 -0
- data/lib/scruffy/renderers/cubed.rb +44 -0
- data/lib/scruffy/renderers/cubed3d.rb +53 -0
- data/lib/scruffy/renderers/empty.rb +22 -0
- data/lib/scruffy/renderers/pie.rb +20 -0
- data/lib/scruffy/renderers/reversed.rb +17 -0
- data/lib/scruffy/renderers/sparkline.rb +10 -0
- data/lib/scruffy/renderers/split.rb +48 -0
- data/lib/scruffy/renderers/standard.rb +37 -0
- data/lib/scruffy/themes.rb +177 -0
- data/lib/scruffy/version.rb +9 -0
- data/test/graph_creation_test.rb +286 -0
- data/test/test_helper.rb +2 -0
- metadata +150 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
# ==Scruffy::Layers
|
2
|
+
#
|
3
|
+
# Author:: Brasten Sager
|
4
|
+
# Date:: August 10th, 2006
|
5
|
+
#
|
6
|
+
# See documentation in Scruffy::Layers::Base
|
7
|
+
#
|
8
|
+
module Scruffy::Layers
|
9
|
+
|
10
|
+
# Should be raised whenever a predictable error during rendering occurs,
|
11
|
+
# particularly if you do not want to terminate the graph rendering process.
|
12
|
+
class RenderError < StandardError; end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
require 'scruffy/layers/base'
|
17
|
+
require 'scruffy/layers/area'
|
18
|
+
require 'scruffy/layers/all_smiles'
|
19
|
+
require 'scruffy/layers/bar'
|
20
|
+
require 'scruffy/layers/box'
|
21
|
+
require 'scruffy/layers/line'
|
22
|
+
require 'scruffy/layers/average'
|
23
|
+
require 'scruffy/layers/stacked'
|
24
|
+
require 'scruffy/layers/multi'
|
25
|
+
require 'scruffy/layers/multi_bar'
|
26
|
+
require 'scruffy/layers/pie'
|
27
|
+
require 'scruffy/layers/pie_slice'
|
28
|
+
require 'scruffy/layers/scatter'
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module Scruffy::Layers
|
2
|
+
# ==Scruffy::Layers::AllSmiles
|
3
|
+
#
|
4
|
+
# Author:: Brasten Sager
|
5
|
+
# Date:: August 8th, 2006
|
6
|
+
#
|
7
|
+
# The AllSmiles graph consists of smiley faces for data points, with smiles or frowns depending upon
|
8
|
+
# their relative location on the graph. The highest point is crowned with a wizard hat. The Wizard
|
9
|
+
# Smiley eventually become 'Scruffy', our mascot.
|
10
|
+
#
|
11
|
+
# I don't know why.
|
12
|
+
#
|
13
|
+
# This graph only looks decent in SVG mode. If you're rasterizing the graph with ImageMagick, you
|
14
|
+
# must use the :complexity => :minimal option on Graph#render. This will make the graph look really
|
15
|
+
# nasty, but still better than if you try to rasterize with all the gradients in place.
|
16
|
+
class AllSmiles < Base
|
17
|
+
attr_accessor :standalone
|
18
|
+
|
19
|
+
# Returns a new AllSmiles graph.
|
20
|
+
#
|
21
|
+
# Options:
|
22
|
+
# standalone:: If set to true, dashed lines under smilies run vertically, like bar graphs.
|
23
|
+
# If false (default), dashed lines run from smiley to smiley, like a line-graph.
|
24
|
+
def initialize(options = {})
|
25
|
+
super
|
26
|
+
@standalone = options[:standalone] || false
|
27
|
+
end
|
28
|
+
|
29
|
+
# Renders graph.
|
30
|
+
def draw(svg, coords, options={})
|
31
|
+
|
32
|
+
hero_smiley = nil
|
33
|
+
coords.each { |c| hero_smiley = c.last if (hero_smiley.nil? || c.last < hero_smiley) }
|
34
|
+
|
35
|
+
svg.defs {
|
36
|
+
svg.radialGradient(:id => 'SmileyGradient', :cx => '50%',
|
37
|
+
:cy => '50%', :r => '50%', :fx => '30%', :fy => '30%') {
|
38
|
+
|
39
|
+
svg.stop(:offset => '0%', 'stop-color' => '#FFF')
|
40
|
+
svg.stop(:offset => '20%', 'stop-color' => '#FFC')
|
41
|
+
svg.stop(:offset => '45%', 'stop-color' => '#FF3')
|
42
|
+
svg.stop(:offset => '60%', 'stop-color' => '#FF0')
|
43
|
+
svg.stop(:offset => '90%', 'stop-color' => '#990')
|
44
|
+
svg.stop(:offset => '100%', 'stop-color' => '#220')
|
45
|
+
}
|
46
|
+
svg.radialGradient(:id => 'HeroGradient', :cx => '50%',
|
47
|
+
:cy => '50%', :r => '50%', :fx => '30%', :fy => '30%') {
|
48
|
+
|
49
|
+
svg.stop(:offset => '0%', 'stop-color' => '#FEE')
|
50
|
+
svg.stop(:offset => '20%', 'stop-color' => '#F0E0C0')
|
51
|
+
svg.stop(:offset => '45%', 'stop-color' => '#8A2A1A')
|
52
|
+
svg.stop(:offset => '60%', 'stop-color' => '#821')
|
53
|
+
svg.stop(:offset => '90%', 'stop-color' => '#210')
|
54
|
+
}
|
55
|
+
svg.radialGradient(:id => 'StarGradient', :cx => '50%',
|
56
|
+
:cy => '50%', :r => '50%', :fx => '30%', :fy => '30%') {
|
57
|
+
|
58
|
+
svg.stop(:offset => '0%', 'stop-color' => '#FFF')
|
59
|
+
svg.stop(:offset => '20%', 'stop-color' => '#EFEFEF')
|
60
|
+
svg.stop(:offset => '45%', 'stop-color' => '#DDD')
|
61
|
+
svg.stop(:offset => '60%', 'stop-color' => '#BBB')
|
62
|
+
svg.stop(:offset => '90%', 'stop-color' => '#888')
|
63
|
+
}
|
64
|
+
}
|
65
|
+
|
66
|
+
unless standalone
|
67
|
+
svg.polyline( :points => stringify_coords(coords).join(' '), :fill => 'none',
|
68
|
+
:stroke => '#660', 'stroke-width' => scaled(10), 'stroke-dasharray' => "#{scaled(10)}, #{scaled(10)}" )
|
69
|
+
end
|
70
|
+
|
71
|
+
# Draw smilies.
|
72
|
+
coords.each do |coord|
|
73
|
+
if standalone
|
74
|
+
svg.line( :x1 => coord.first, :y1 => coord.last, :x2 => coord.first, :y2 => height, :fill => 'none',
|
75
|
+
:stroke => '#660', 'stroke-width' => scaled(10), 'stroke-dasharray' => "#{scaled(10)}, #{scaled(10)}" )
|
76
|
+
end
|
77
|
+
svg.circle( :cx => coord.first + scaled(2), :cy => coord.last + scaled(2), :r => scaled(15),
|
78
|
+
:fill => 'black', :stroke => 'none', :opacity => 0.4)
|
79
|
+
svg.circle( :cx => coord.first, :cy => coord.last, :r => scaled(15),
|
80
|
+
:fill => (complexity == :minimal ? 'yellow' : 'url(#SmileyGradient)'), :stroke => 'black', 'stroke-width' => scaled(1) )
|
81
|
+
svg.line( :x1 => (coord.first - scaled(3)),
|
82
|
+
:x2 => (coord.first - scaled(3)),
|
83
|
+
:y1 => (coord.last),
|
84
|
+
:y2 => (coord.last - scaled(7)), :stroke => 'black', 'stroke-width' => scaled(1.4) )
|
85
|
+
svg.line( :x1 => (coord.first + scaled(3)),
|
86
|
+
:x2 => (coord.first + scaled(3)),
|
87
|
+
:y1 => (coord.last),
|
88
|
+
:y2 => (coord.last - scaled(7)), :stroke => 'black', 'stroke-width' => scaled(1.4) )
|
89
|
+
|
90
|
+
|
91
|
+
# Some minor mathematics for the smile/frown
|
92
|
+
percent = 1.0 - (coord.last.to_f / height.to_f)
|
93
|
+
corners = scaled(8 - (5 * percent))
|
94
|
+
anchor = scaled((20 * percent) - 5)
|
95
|
+
|
96
|
+
# Draw the mouth
|
97
|
+
svg.path( :d => "M#{coord.first - scaled(9)} #{coord.last + corners} Q#{coord.first} #{coord.last + anchor} #{coord.first + scaled(9)} #{coord.last + corners}",
|
98
|
+
:stroke => 'black', 'stroke-width' => scaled(1.4), :fill => 'none' )
|
99
|
+
|
100
|
+
|
101
|
+
# Wizard hat for hero smiley.
|
102
|
+
if coord.last == hero_smiley
|
103
|
+
svg.ellipse(:cx => coord.first, :cy => (coord.last - scaled(13)),
|
104
|
+
:rx => scaled(17), :ry => scaled(6.5), :fill => (complexity == :minimal ? 'purple' : 'url(#HeroGradient)'), :stroke => 'black', 'stroke-width' => scaled(1.4) )
|
105
|
+
|
106
|
+
svg.path(:d => "M#{coord.first} #{coord.last - scaled(60)} " +
|
107
|
+
"L#{coord.first + scaled(10)} #{coord.last - scaled(14)} " +
|
108
|
+
"C#{coord.first + scaled(10)},#{coord.last - scaled(9)} #{coord.first - scaled(10)},#{coord.last - scaled(9)} #{coord.first - scaled(10)},#{coord.last - scaled(14)}" +
|
109
|
+
"L#{coord.first} #{coord.last - scaled(60)}",
|
110
|
+
:stroke => 'black', 'stroke-width' => scaled(1.4), :fill => (complexity == :minimal ? 'purple' : 'url(#HeroGradient)'))
|
111
|
+
|
112
|
+
svg.path(:d => "M#{coord.first - scaled(4)} #{coord.last - scaled(23)}" +
|
113
|
+
"l-#{scaled(2.5)} #{scaled(10)} l#{scaled(7.5)} -#{scaled(5)} l-#{scaled(10)} 0 l#{scaled(7.5)} #{scaled(5)} l-#{scaled(2.5)} -#{scaled(10)}", :stroke => 'none', :fill => (complexity == :minimal ? 'white': 'url(#StarGradient)') )
|
114
|
+
svg.path(:d => "M#{coord.first + scaled(2)} #{coord.last - scaled(30)}" +
|
115
|
+
"l-#{scaled(2.5)} #{scaled(10)} l#{scaled(7.5)} -#{scaled(5)} l-#{scaled(10)} 0 l#{scaled(7.5)} #{scaled(5)} l-#{scaled(2.5)} -#{scaled(10)}", :stroke => 'none', :fill => (complexity == :minimal ? 'white': 'url(#StarGradient)') )
|
116
|
+
svg.path(:d => "M#{coord.first - scaled(2)} #{coord.last - scaled(33)}" +
|
117
|
+
"l-#{scaled(1.25)} #{scaled(5)} l#{scaled(3.75)} -#{scaled(2.5)} l-#{scaled(5)} 0 l#{scaled(3.75)} #{scaled(2.5)} l-#{scaled(1.25)} -#{scaled(5)}", :stroke => 'none', :fill => 'white' )
|
118
|
+
svg.path(:d => "M#{coord.first - scaled(2.2)} #{coord.last - scaled(32.7)}" +
|
119
|
+
"l-#{scaled(1.25)} #{scaled(5)} l#{scaled(3.75)} -#{scaled(2.5)} l-#{scaled(5)} 0 l#{scaled(3.75)} #{scaled(2.5)} l-#{scaled(1.25)} -#{scaled(5)}", :stroke => 'none', :fill => (complexity == :minimal ? 'white': 'url(#StarGradient)') )
|
120
|
+
svg.path(:d => "M#{coord.first + scaled(4.5)} #{coord.last - scaled(20)}" +
|
121
|
+
"l-#{scaled(1.25)} #{scaled(5)} l#{scaled(3.75)} -#{scaled(2.5)} l-#{scaled(5)} 0 l#{scaled(3.75)} #{scaled(2.5)} l-#{scaled(1.25)} -#{scaled(5)}", :stroke => 'none', :fill => (complexity == :minimal ? 'white': 'url(#StarGradient)') )
|
122
|
+
svg.path(:d => "M#{coord.first} #{coord.last - scaled(40)}" +
|
123
|
+
"l-#{scaled(1.25)} #{scaled(5)} l#{scaled(3.75)} -#{scaled(2.5)} l-#{scaled(5)} 0 l#{scaled(3.75)} #{scaled(2.5)} l-#{scaled(1.25)} -#{scaled(5)}", :stroke => 'none', :fill => (complexity == :minimal ? 'white': 'url(#StarGradient)') )
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Legacy (4 days old). Removed scaled from layout engine,
|
131
|
+
# changed to #relative, with different math involved.
|
132
|
+
# Translate here so I don't have to entirely redo this graph.
|
133
|
+
def scaled(pt)
|
134
|
+
relative(pt) / 2
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Scruffy::Layers
|
2
|
+
# ==Scruffy::Layers::Area
|
3
|
+
#
|
4
|
+
# Author:: Brasten Sager
|
5
|
+
# Date:: August 6th, 2006
|
6
|
+
#
|
7
|
+
# Standard area graph.
|
8
|
+
class Area < Base
|
9
|
+
|
10
|
+
# Render area graph.
|
11
|
+
def draw(svg, coords, options={})
|
12
|
+
# svg.polygon wants a long string of coords.
|
13
|
+
points_value = "0,#{height} #{stringify_coords(coords).join(' ')} #{width},#{height}"
|
14
|
+
|
15
|
+
# Experimental, for later user.
|
16
|
+
# This was supposed to add some fun filters, 3d effects and whatnot.
|
17
|
+
# Neither ImageMagick nor Mozilla SVG render this well (at all). Maybe a future thing.
|
18
|
+
#
|
19
|
+
# svg.defs {
|
20
|
+
# svg.filter(:id => 'MyFilter', :filterUnits => 'userSpaceOnUse', :x => 0, :y => 0, :width => 200, :height => '120') {
|
21
|
+
# svg.feGaussianBlur(:in => 'SourceAlpha', :stdDeviation => 4, :result => 'blur')
|
22
|
+
# svg.feOffset(:in => 'blur', :dx => 4, :dy => 4, :result => 'offsetBlur')
|
23
|
+
# svg.feSpecularLighting( :in => 'blur', :surfaceScale => 5, :specularConstant => '.75',
|
24
|
+
# :specularExponent => 20, 'lighting-color' => '#bbbbbb',
|
25
|
+
# :result => 'specOut') {
|
26
|
+
# svg.fePointLight(:x => '-5000', :y => '-10000', :z => '20000')
|
27
|
+
# }
|
28
|
+
#
|
29
|
+
# svg.feComposite(:in => 'specOut', :in2 => 'SourceAlpha', :operator => 'in', :result => 'specOut')
|
30
|
+
# svg.feComposite(:in => 'sourceGraphic', :in2 => 'specOut', :operator => 'arithmetic',
|
31
|
+
# :k1 => 0, :k2 => 1, :k3 => 1, :k4 => 0, :result => 'litPaint')
|
32
|
+
#
|
33
|
+
# svg.feMerge {
|
34
|
+
# svg.feMergeNode(:in => 'offsetBlur')
|
35
|
+
# svg.feMergeNode(:in => 'litPaint')
|
36
|
+
# }
|
37
|
+
# }
|
38
|
+
# }
|
39
|
+
svg.g(:transform => "translate(0, -#{relative(2)})") {
|
40
|
+
svg.polygon(:points => points_value, :style => "fill: black; stroke: black; fill-opacity: 0.06; stroke-opacity: 0.06;")
|
41
|
+
}
|
42
|
+
|
43
|
+
svg.polygon(:points => points_value, :fill => color.to_s, :stroke => color.to_s, 'style' => "opacity: #{opacity}")
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Scruffy::Layers
|
2
|
+
# ==Scruffy::Layers::Average
|
3
|
+
#
|
4
|
+
# Author:: Brasten Sager
|
5
|
+
# Date:: August 7th, 2006
|
6
|
+
#
|
7
|
+
# An 'average' graph. This graph iterates through all the layers and averages
|
8
|
+
# all the data at each point, then draws a thick, translucent, shadowy line graph
|
9
|
+
# indicating the average values.
|
10
|
+
#
|
11
|
+
# This only looks decent in SVG mode. ImageMagick doesn't retain the transparency
|
12
|
+
# for some reason, creating a massive black line. Any help resolving this would
|
13
|
+
# be useful.
|
14
|
+
class Average < Base
|
15
|
+
attr_reader :layers
|
16
|
+
|
17
|
+
# Returns new Average graph.
|
18
|
+
def initialize(options = {})
|
19
|
+
# Set self's relevant_data to false. Otherwise we get stuck in a
|
20
|
+
# recursive loop.
|
21
|
+
super(options.merge({:relevant_data => false}))
|
22
|
+
|
23
|
+
# The usual :points argument is actually layers for Average, name it as such
|
24
|
+
@layers = options[:points]
|
25
|
+
end
|
26
|
+
|
27
|
+
# Render average graph.
|
28
|
+
def draw(svg, coords, options = {})
|
29
|
+
svg.polyline( :points => coords.join(' '), :fill => 'none', :stroke => 'black',
|
30
|
+
'stroke-width' => relative(5), 'opacity' => '0.4')
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
# Override default generate_coordinates method to iterate through the layers and
|
35
|
+
# generate coordinates based on the average data points.
|
36
|
+
def generate_coordinates(options = {})
|
37
|
+
key_layer = layers.find { |layer| layer.relevant_data? }
|
38
|
+
|
39
|
+
options[:point_distance] = width / (key_layer.points.size - 1).to_f
|
40
|
+
|
41
|
+
coords = []
|
42
|
+
|
43
|
+
#TODO this will likely break with the new hash model
|
44
|
+
key_layer.points.each_with_index do |layer, idx|
|
45
|
+
sum, objects = points.inject([0, 0]) do |arr, elem|
|
46
|
+
if elem.relevant_data?
|
47
|
+
arr[0] += elem.points[idx]
|
48
|
+
arr[1] += 1
|
49
|
+
end
|
50
|
+
arr
|
51
|
+
end
|
52
|
+
|
53
|
+
average = sum / objects.to_f
|
54
|
+
|
55
|
+
x_coord = options[:point_distance] * idx
|
56
|
+
|
57
|
+
relative_percent = ((average == min_value) ? 0 : ((average - min_value) / (max_value - min_value).to_f))
|
58
|
+
y_coord = (height - (height * relative_percent))
|
59
|
+
|
60
|
+
coords << [x_coord, y_coord].join(',')
|
61
|
+
end
|
62
|
+
|
63
|
+
return coords
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Scruffy::Layers
|
2
|
+
# ==Scruffy::Layers::Bar
|
3
|
+
#
|
4
|
+
# Author:: Brasten Sager
|
5
|
+
# Date:: August 6th, 2006
|
6
|
+
#
|
7
|
+
# Standard bar graph.
|
8
|
+
class Bar < Base
|
9
|
+
|
10
|
+
# Draw bar graph.
|
11
|
+
# Now handles positive and negative values gracefully.
|
12
|
+
def draw(svg, coords, options = {})
|
13
|
+
coords.each_with_index do |coord,idx|
|
14
|
+
next if coord.nil?
|
15
|
+
x, y, bar_height = (coord.first), coord.last, 1#(height - coord.last)
|
16
|
+
|
17
|
+
valh = max_value + min_value * -1 #value_height
|
18
|
+
maxh = max_value * height / valh #positive area height
|
19
|
+
minh = min_value * height / valh #negative area height
|
20
|
+
#puts "height = #{height} and max_value = #{max_value} and min_value = #{min_value} and y = #{y} and point = #{points[idx]}"
|
21
|
+
if points[idx] > 0
|
22
|
+
bar_height = points[idx]*maxh/max_value
|
23
|
+
else
|
24
|
+
bar_height = points[idx]*minh/min_value
|
25
|
+
end
|
26
|
+
|
27
|
+
#puts " y = #{y} and point = #{points[idx]}"
|
28
|
+
unless options[:border] == false
|
29
|
+
svg.g(:transform => "translate(-#{relative(0.5)}, -#{relative(0.5)})") {
|
30
|
+
svg.rect( :x => x, :y => y, :width => @bar_width + relative(1), :height => bar_height + relative(1),
|
31
|
+
:style => "fill: black; fill-opacity: 0.15; stroke: none;" )
|
32
|
+
svg.rect( :x => x+relative(0.5), :y => y+relative(2), :width => @bar_width + relative(1), :height => bar_height - relative(0.5),
|
33
|
+
:style => "fill: black; fill-opacity: 0.15; stroke: none;" )
|
34
|
+
}
|
35
|
+
end
|
36
|
+
|
37
|
+
current_colour = color.is_a?(Array) ? color[idx % color.size] : color
|
38
|
+
|
39
|
+
svg.rect( :x => x, :y => y, :width => @bar_width, :height => bar_height,
|
40
|
+
:fill => current_colour.to_s, 'style' => "opacity: #{opacity}; stroke: none;" )
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
# Due to the size of the bar graph, X-axis coords must
|
47
|
+
# be squeezed so that the bars do not hang off the ends
|
48
|
+
# of the graph.
|
49
|
+
#
|
50
|
+
# Unfortunately this just mean that bar-graphs and most other graphs
|
51
|
+
# end up on different points. Maybe adding a padding to the coordinates
|
52
|
+
# should be a graph-wide thing?
|
53
|
+
#
|
54
|
+
# Update : x-axis coords for lines and area charts should now line
|
55
|
+
# up with the center of bar charts.
|
56
|
+
|
57
|
+
def generate_coordinates(options = {})
|
58
|
+
@bar_width = (width / points.size) * 0.95
|
59
|
+
options[:point_distance] = (width - (width / points.size)) / (points.size - 1).to_f
|
60
|
+
|
61
|
+
#TODO more array work with index, try to rework to be accepting of hashes
|
62
|
+
coords = (0...points.size).map do |idx|
|
63
|
+
next if points[idx].nil?
|
64
|
+
x_coord = (options[:point_distance] * idx) + (width / points.size * 0.5) - (@bar_width * 0.5)
|
65
|
+
|
66
|
+
relative_percent = ((points[idx] == min_value) ? 0 : ((points[idx] - min_value) / (max_value - min_value).to_f))
|
67
|
+
y_coord = (height - (height * relative_percent))
|
68
|
+
[x_coord, y_coord]
|
69
|
+
end
|
70
|
+
coords
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
module Scruffy::Layers
|
2
|
+
# ==Scruffy::Layers::Base
|
3
|
+
#
|
4
|
+
# Author:: Brasten Sager
|
5
|
+
# Extended By:: A.J. Ostman
|
6
|
+
# Created:: August 5th, 2006
|
7
|
+
# Last Modified:: August 27, 2006
|
8
|
+
#
|
9
|
+
# Scruffy::Layers::Base contains the basic functionality needed by the various types of graphs. The Base
|
10
|
+
# class is responsible holding layer information such as the title and data points.
|
11
|
+
#
|
12
|
+
# When the graph is rendered, the graph renderer calls Base#render. Base#render sets up
|
13
|
+
# some standard information, and calculates the x,y coordinates of each data point. The draw() method,
|
14
|
+
# which should have been overridden by the current instance, is then called. The actual rendering of
|
15
|
+
# the graph takes place there.
|
16
|
+
#
|
17
|
+
# ====Create New Graph Types
|
18
|
+
#
|
19
|
+
# Assuming the information generated by Scruffy::Layers::Base is sufficient, you can create a new graph type
|
20
|
+
# simply by overriding the draw() method. See Base#draw for arguments.
|
21
|
+
#
|
22
|
+
class Base
|
23
|
+
# The following attributes are user-definable at any time.
|
24
|
+
# title, points, relevant_data, preferred_color, options
|
25
|
+
attr_accessor :title
|
26
|
+
attr_accessor :points
|
27
|
+
attr_accessor :relevant_data
|
28
|
+
attr_accessor :preferred_color
|
29
|
+
attr_accessor :preferred_outline
|
30
|
+
attr_accessor :options # On-the-fly values for easy customization / acts as attributes.
|
31
|
+
|
32
|
+
# The following attributes are set during the layer's render process,
|
33
|
+
# and act more as a record of what just happened for later processes.
|
34
|
+
# height, width, min_value, max_value, color, opacity, complexity
|
35
|
+
attr_reader :height, :width
|
36
|
+
attr_reader :min_value, :max_value
|
37
|
+
attr_reader :color
|
38
|
+
attr_reader :outline
|
39
|
+
attr_reader :opacity
|
40
|
+
attr_reader :complexity
|
41
|
+
|
42
|
+
# Returns a new Base object.
|
43
|
+
#
|
44
|
+
# Any options other that those specified below are stored in the @options variable for
|
45
|
+
# possible later use. This would be a good place to store options needed for a custom
|
46
|
+
# graph.
|
47
|
+
#
|
48
|
+
# Options:
|
49
|
+
# title:: Name/title of data group
|
50
|
+
# points:: Array of data points
|
51
|
+
# preferred_color:: Color used to render this graph, overrides theme color.
|
52
|
+
# preferred_outline:: Color used to render this graph outline, overrides theme outline.
|
53
|
+
# relevant_data:: Rarely used - indicates the data on this graph should not
|
54
|
+
# included in any graph data aggregations, such as averaging data points.
|
55
|
+
# style:: SVG polyline style. (default: 'fill-opacity: 0; stroke-opacity: 0.35')
|
56
|
+
# stroke_width:: numeric value for width of line (0.1 - 10, default: 1)
|
57
|
+
# relativestroke:: stroke-width relative to image size? true or false (default)
|
58
|
+
# shadow:: Display line shadow? true or false (default)
|
59
|
+
# dots:: Display co-ord dots? true or false (default)
|
60
|
+
def initialize(options = {})
|
61
|
+
@title = options.delete(:title) || ''
|
62
|
+
@preferred_color = options.delete(:color)
|
63
|
+
@preferred_outline = options.delete(:outline)
|
64
|
+
@relevant_data = options.delete(:relevant_data) || true
|
65
|
+
@points = options.delete(:points) || []
|
66
|
+
@points.extend Scruffy::Helpers::PointContainer unless @points.kind_of? Scruffy::Helpers::PointContainer
|
67
|
+
|
68
|
+
options[:stroke_width] ||= 1
|
69
|
+
options[:dots] ||= false
|
70
|
+
options[:shadow] ||= false
|
71
|
+
options[:style] ||= false
|
72
|
+
options[:relativestroke] ||= false
|
73
|
+
|
74
|
+
@options = options
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
# Builds SVG code for this graph using the provided Builder object.
|
79
|
+
# This method actually generates data needed by this graph, then passes the
|
80
|
+
# rendering responsibilities to Base#draw.
|
81
|
+
#
|
82
|
+
# svg:: a Builder object used to create SVG code.
|
83
|
+
def render(svg, options)
|
84
|
+
setup_variables(options)
|
85
|
+
coords = generate_coordinates(options)
|
86
|
+
|
87
|
+
draw(svg, coords, options)
|
88
|
+
end
|
89
|
+
|
90
|
+
# The method called by Base#draw to render the graph.
|
91
|
+
#
|
92
|
+
# svg:: a Builder object to use for creating SVG code.
|
93
|
+
# coords:: An array of coordinates relating to the graph's data points. ie: [[100, 120], [200, 140], [300, 40]]
|
94
|
+
# options:: Optional arguments.
|
95
|
+
def draw(svg, coords, options={})
|
96
|
+
raise RenderError, "You must override the Base#draw method."
|
97
|
+
end
|
98
|
+
|
99
|
+
# Returns a hash with information to be used by the legend.
|
100
|
+
#
|
101
|
+
# Alternatively, returns nil if you don't want this layer to be in the legend,
|
102
|
+
# or an array of hashes if this layer should have multiple legend entries (stacked?)
|
103
|
+
#
|
104
|
+
# By default, #legend_data returns nil automatically if relevant_data is set to false
|
105
|
+
# or the @color attribute is nil. @color is set when the layer is rendered, so legends
|
106
|
+
# must be rendered AFTER layers.
|
107
|
+
def legend_data
|
108
|
+
if relevant_data? && @color
|
109
|
+
{:title => title,
|
110
|
+
:color => @color,
|
111
|
+
:priority => :normal}
|
112
|
+
else
|
113
|
+
nil
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Returns the value of relevant_data
|
118
|
+
def relevant_data?
|
119
|
+
@relevant_data
|
120
|
+
end
|
121
|
+
|
122
|
+
# The highest data point on this layer, or nil if relevant_data == false
|
123
|
+
def top_value
|
124
|
+
@relevant_data ? points.maximum_value : nil
|
125
|
+
end
|
126
|
+
|
127
|
+
# The lowest data point on this layer, or nil if relevant_data == false
|
128
|
+
def bottom_value
|
129
|
+
@relevant_data ? points.minimum_value : nil
|
130
|
+
end
|
131
|
+
|
132
|
+
# The highest data point on this layer, or nil if relevant_data == false
|
133
|
+
def bottom_key
|
134
|
+
@relevant_data ? points.minimum_key : nil
|
135
|
+
end
|
136
|
+
|
137
|
+
# The lowest data point on this layer, or nil if relevant_data == false
|
138
|
+
def top_key
|
139
|
+
@relevant_data ? points.maximum_key : nil
|
140
|
+
end
|
141
|
+
|
142
|
+
# The sum of all values
|
143
|
+
def sum_values
|
144
|
+
points.sum
|
145
|
+
end
|
146
|
+
|
147
|
+
protected
|
148
|
+
# Sets up several variables that almost every graph layer will need to render
|
149
|
+
# itself.
|
150
|
+
def setup_variables(options = {})
|
151
|
+
@color = (preferred_color || options.delete(:color))
|
152
|
+
@outline = (preferred_outline || options.delete(:outline))
|
153
|
+
@width, @height = options.delete(:size)
|
154
|
+
@min_value, @max_value = options[:min_value], options[:max_value]
|
155
|
+
@opacity = options[:opacity] || 1.0
|
156
|
+
@complexity = options[:complexity]
|
157
|
+
end
|
158
|
+
|
159
|
+
# Optimistic generation of coordinates for layer to use. These coordinates are
|
160
|
+
# just a best guess, and can be overridden or thrown away (for example, this is overridden
|
161
|
+
# in pie charting and bar charts).
|
162
|
+
|
163
|
+
# Updated : Assuming n number of points, the graph is divided into n rectangles
|
164
|
+
# and the points are plotted in the middle of each rectangle. This allows bars to
|
165
|
+
# play nice with lines.
|
166
|
+
def generate_coordinates(options = {})
|
167
|
+
|
168
|
+
dy = height.to_f / (options[:max_value] - options[:min_value])
|
169
|
+
dx = width.to_f / (options[:max_key] - options[:min_key] + 1)
|
170
|
+
|
171
|
+
ret = []
|
172
|
+
points.each_point do |x, y|
|
173
|
+
if y
|
174
|
+
x_coord = dx * (x - options[:min_key]) + dx/2
|
175
|
+
y_coord = dy * (y - options[:min_value])
|
176
|
+
|
177
|
+
ret << [x_coord, height - y_coord]
|
178
|
+
end
|
179
|
+
end
|
180
|
+
return ret
|
181
|
+
end
|
182
|
+
|
183
|
+
# Converts a percentage into a pixel value, relative to the height.
|
184
|
+
#
|
185
|
+
# Example:
|
186
|
+
# relative(5) # On a 100px high layer, this returns 5. 200px high layer, this returns 10, etc.
|
187
|
+
def relative(pct)
|
188
|
+
# Default to Relative Height
|
189
|
+
relative_height(pct)
|
190
|
+
end
|
191
|
+
|
192
|
+
def relative_width(pct)
|
193
|
+
if pct # Added to handle nils
|
194
|
+
@width * (pct / 100.to_f)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def relative_height(pct)
|
199
|
+
if pct # Added to handle nils
|
200
|
+
@height * (pct / 100.to_f)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Some SVG elements take a long string of multiple coordinates. This is here
|
205
|
+
# to make that a little easier.
|
206
|
+
def stringify_coords(coords) # :nodoc:
|
207
|
+
coords.map { |c| c.join(',') }
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
end # scruffy::layers
|