tlconnor-scruffy 0.2.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/CHANGES.txt +115 -0
  2. data/LICENCE.txt +20 -0
  3. data/Manifest.txt +74 -0
  4. data/README.txt +66 -0
  5. data/lib/scruffy.rb +30 -0
  6. data/lib/scruffy/components.rb +22 -0
  7. data/lib/scruffy/components/axes.rb +23 -0
  8. data/lib/scruffy/components/background.rb +24 -0
  9. data/lib/scruffy/components/base.rb +57 -0
  10. data/lib/scruffy/components/data_markers.rb +41 -0
  11. data/lib/scruffy/components/graphs.rb +52 -0
  12. data/lib/scruffy/components/grid.rb +57 -0
  13. data/lib/scruffy/components/label.rb +17 -0
  14. data/lib/scruffy/components/legend.rb +147 -0
  15. data/lib/scruffy/components/style_info.rb +22 -0
  16. data/lib/scruffy/components/title.rb +19 -0
  17. data/lib/scruffy/components/value_markers.rb +25 -0
  18. data/lib/scruffy/components/viewport.rb +37 -0
  19. data/lib/scruffy/formatters.rb +233 -0
  20. data/lib/scruffy/graph.rb +205 -0
  21. data/lib/scruffy/graph_state.rb +29 -0
  22. data/lib/scruffy/helpers.rb +13 -0
  23. data/lib/scruffy/helpers/canvas.rb +41 -0
  24. data/lib/scruffy/helpers/layer_container.rb +119 -0
  25. data/lib/scruffy/helpers/marker_helper.rb +25 -0
  26. data/lib/scruffy/helpers/meta.rb +5 -0
  27. data/lib/scruffy/helpers/point_container.rb +99 -0
  28. data/lib/scruffy/layers.rb +28 -0
  29. data/lib/scruffy/layers/all_smiles.rb +137 -0
  30. data/lib/scruffy/layers/area.rb +46 -0
  31. data/lib/scruffy/layers/average.rb +67 -0
  32. data/lib/scruffy/layers/bar.rb +73 -0
  33. data/lib/scruffy/layers/base.rb +211 -0
  34. data/lib/scruffy/layers/box.rb +114 -0
  35. data/lib/scruffy/layers/line.rb +46 -0
  36. data/lib/scruffy/layers/multi.rb +74 -0
  37. data/lib/scruffy/layers/multi_bar.rb +51 -0
  38. data/lib/scruffy/layers/pie.rb +123 -0
  39. data/lib/scruffy/layers/pie_slice.rb +119 -0
  40. data/lib/scruffy/layers/scatter.rb +29 -0
  41. data/lib/scruffy/layers/sparkline_bar.rb +39 -0
  42. data/lib/scruffy/layers/stacked.rb +87 -0
  43. data/lib/scruffy/rasterizers.rb +14 -0
  44. data/lib/scruffy/rasterizers/batik_rasterizer.rb +39 -0
  45. data/lib/scruffy/rasterizers/mini_magick_rasterizer.rb +24 -0
  46. data/lib/scruffy/rasterizers/rmagick_rasterizer.rb +27 -0
  47. data/lib/scruffy/renderers.rb +23 -0
  48. data/lib/scruffy/renderers/axis_legend.rb +41 -0
  49. data/lib/scruffy/renderers/base.rb +95 -0
  50. data/lib/scruffy/renderers/cubed.rb +44 -0
  51. data/lib/scruffy/renderers/cubed3d.rb +53 -0
  52. data/lib/scruffy/renderers/empty.rb +22 -0
  53. data/lib/scruffy/renderers/pie.rb +20 -0
  54. data/lib/scruffy/renderers/reversed.rb +17 -0
  55. data/lib/scruffy/renderers/sparkline.rb +10 -0
  56. data/lib/scruffy/renderers/split.rb +48 -0
  57. data/lib/scruffy/renderers/standard.rb +37 -0
  58. data/lib/scruffy/themes.rb +177 -0
  59. data/lib/scruffy/version.rb +9 -0
  60. data/test/graph_creation_test.rb +286 -0
  61. data/test/test_helper.rb +2 -0
  62. 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