tlconnor-scruffy 0.2.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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