ramhoj-scruffy 0.2.6

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 (55) hide show
  1. data/README.txt +66 -0
  2. data/lib/scruffy.rb +25 -0
  3. data/lib/scruffy/components.rb +21 -0
  4. data/lib/scruffy/components/background.rb +24 -0
  5. data/lib/scruffy/components/base.rb +57 -0
  6. data/lib/scruffy/components/data_markers.rb +26 -0
  7. data/lib/scruffy/components/graphs.rb +48 -0
  8. data/lib/scruffy/components/grid.rb +19 -0
  9. data/lib/scruffy/components/label.rb +17 -0
  10. data/lib/scruffy/components/legend.rb +105 -0
  11. data/lib/scruffy/components/style_info.rb +22 -0
  12. data/lib/scruffy/components/title.rb +19 -0
  13. data/lib/scruffy/components/value_markers.rb +32 -0
  14. data/lib/scruffy/components/viewport.rb +37 -0
  15. data/lib/scruffy/formatters.rb +213 -0
  16. data/lib/scruffy/graph.rb +190 -0
  17. data/lib/scruffy/graph_state.rb +24 -0
  18. data/lib/scruffy/helpers.rb +12 -0
  19. data/lib/scruffy/helpers/canvas.rb +41 -0
  20. data/lib/scruffy/helpers/layer_container.rb +95 -0
  21. data/lib/scruffy/helpers/meta.rb +5 -0
  22. data/lib/scruffy/helpers/point_container.rb +70 -0
  23. data/lib/scruffy/layers.rb +24 -0
  24. data/lib/scruffy/layers/all_smiles.rb +137 -0
  25. data/lib/scruffy/layers/area.rb +46 -0
  26. data/lib/scruffy/layers/average.rb +67 -0
  27. data/lib/scruffy/layers/bar.rb +52 -0
  28. data/lib/scruffy/layers/base.rb +191 -0
  29. data/lib/scruffy/layers/line.rb +46 -0
  30. data/lib/scruffy/layers/pie.rb +123 -0
  31. data/lib/scruffy/layers/pie_slice.rb +119 -0
  32. data/lib/scruffy/layers/scatter.rb +21 -0
  33. data/lib/scruffy/layers/sparkline_bar.rb +39 -0
  34. data/lib/scruffy/layers/stacked.rb +87 -0
  35. data/lib/scruffy/rasterizers.rb +14 -0
  36. data/lib/scruffy/rasterizers/batik_rasterizer.rb +39 -0
  37. data/lib/scruffy/rasterizers/rmagick_rasterizer.rb +27 -0
  38. data/lib/scruffy/renderers.rb +22 -0
  39. data/lib/scruffy/renderers/base.rb +93 -0
  40. data/lib/scruffy/renderers/cubed.rb +44 -0
  41. data/lib/scruffy/renderers/cubed3d.rb +53 -0
  42. data/lib/scruffy/renderers/empty.rb +22 -0
  43. data/lib/scruffy/renderers/pie.rb +20 -0
  44. data/lib/scruffy/renderers/reversed.rb +17 -0
  45. data/lib/scruffy/renderers/sparkline.rb +10 -0
  46. data/lib/scruffy/renderers/split.rb +48 -0
  47. data/lib/scruffy/renderers/standard.rb +36 -0
  48. data/lib/scruffy/themes.rb +156 -0
  49. data/lib/scruffy/version.rb +3 -0
  50. data/spec/output/array.svg +47 -0
  51. data/spec/output/hash.svg +47 -0
  52. data/spec/scruffy/graph_spec.rb +175 -0
  53. data/spec/scruffy/layers/base_spec.rb +30 -0
  54. data/spec/spec_helper.rb +8 -0
  55. metadata +155 -0
@@ -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,52 @@
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
+ def draw(svg, coords, options = {})
12
+ coords.each do |coord|
13
+ x, y, bar_height = (coord.first-(@bar_width * 0.5)), coord.last, (height - coord.last)
14
+
15
+ svg.g(:transform => "translate(-#{relative(0.5)}, -#{relative(0.5)})") {
16
+ svg.rect( :x => x, :y => y, :width => @bar_width + relative(1), :height => bar_height + relative(1),
17
+ :style => "fill: black; fill-opacity: 0.15; stroke: none;" )
18
+ svg.rect( :x => x+relative(0.5), :y => y+relative(2), :width => @bar_width + relative(1), :height => bar_height - relative(0.5),
19
+ :style => "fill: black; fill-opacity: 0.15; stroke: none;" )
20
+
21
+ }
22
+
23
+ svg.rect( :x => x, :y => y, :width => @bar_width, :height => bar_height,
24
+ :fill => color.to_s, 'style' => "opacity: #{opacity}; stroke: none;" )
25
+ end
26
+ end
27
+
28
+ protected
29
+
30
+ # Due to the size of the bar graph, X-axis coords must
31
+ # be squeezed so that the bars do not hang off the ends
32
+ # of the graph.
33
+ #
34
+ # Unfortunately this just mean that bar-graphs and most other graphs
35
+ # end up on different points. Maybe adding a padding to the coordinates
36
+ # should be a graph-wide thing?
37
+ def generate_coordinates(options = {})
38
+ @bar_width = (width / points.size) * 0.9
39
+ options[:point_distance] = (width - (width / points.size)) / (points.size - 1).to_f
40
+
41
+ #TODO more array work with index, try to rework to be accepting of hashes
42
+ coords = (0...points.size).map do |idx|
43
+ x_coord = (options[:point_distance] * idx) + (width / points.size * 0.5)
44
+
45
+ relative_percent = ((points[idx] == min_value) ? 0 : ((points[idx] - min_value) / (max_value - min_value).to_f))
46
+ y_coord = (height - (height * relative_percent))
47
+ [x_coord, y_coord]
48
+ end
49
+ coords
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,191 @@
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 :options # On-the-fly values for easy customization / acts as attributes.
30
+
31
+ # The following attributes are set during the layer's render process,
32
+ # and act more as a record of what just happened for later processes.
33
+ # height, width, min_value, max_value, color, opacity, complexity
34
+ attr_reader :height, :width
35
+ attr_reader :min_value, :max_value
36
+ attr_reader :color
37
+ attr_reader :opacity
38
+ attr_reader :complexity
39
+
40
+ # Returns a new Base object.
41
+ #
42
+ # Any options other that those specified below are stored in the @options variable for
43
+ # possible later use. This would be a good place to store options needed for a custom
44
+ # graph.
45
+ #
46
+ # Options:
47
+ # title:: Name/title of data group
48
+ # points:: Array of data points
49
+ # preferred_color:: Color used to render this graph, overrides theme color.
50
+ # relevant_data:: Rarely used - indicates the data on this graph should not
51
+ # included in any graph data aggregations, such as averaging data points.
52
+ # style:: SVG polyline style. (default: 'fill-opacity: 0; stroke-opacity: 0.35')
53
+ # stroke_width:: numeric value for width of line (0.1 - 10, default: 1)
54
+ # relativestroke:: stroke-width relative to image size? true or false (default)
55
+ # shadow:: Display line shadow? true or false (default)
56
+ # dots:: Display co-ord dots? true or false (default)
57
+ def initialize(options = {})
58
+ @title = options.delete(:title) || ''
59
+ @preferred_color = options.delete(:color)
60
+ @relevant_data = options.delete(:relevant_data) || true
61
+ @points = options.delete(:points) || []
62
+ @points.extend Scruffy::Helpers::PointContainer unless @points.kind_of? Scruffy::Helpers::PointContainer
63
+
64
+ options[:stroke_width] ||= 1
65
+ options[:dots] ||= false
66
+ options[:shadow] ||= false
67
+ options[:style] ||= false
68
+ options[:relativestroke] ||= false
69
+
70
+ @options = options
71
+ end
72
+
73
+ # Builds SVG code for this graph using the provided Builder object.
74
+ # This method actually generates data needed by this graph, then passes the
75
+ # rendering responsibilities to Base#draw.
76
+ #
77
+ # svg:: a Builder object used to create SVG code.
78
+ def render(svg, options = {})
79
+ setup_variables(options)
80
+ coords = generate_coordinates(options)
81
+
82
+ draw(svg, coords, options)
83
+ end
84
+
85
+ # The method called by Base#draw to render the graph.
86
+ #
87
+ # svg:: a Builder object to use for creating SVG code.
88
+ # coords:: An array of coordinates relating to the graph's data points. ie: [[100, 120], [200, 140], [300, 40]]
89
+ # options:: Optional arguments.
90
+ def draw(svg, coords, options={})
91
+ raise RenderError, "You must override the Base#draw method."
92
+ end
93
+
94
+ # Returns a hash with information to be used by the legend.
95
+ #
96
+ # Alternatively, returns nil if you don't want this layer to be in the legend,
97
+ # or an array of hashes if this layer should have multiple legend entries (stacked?)
98
+ #
99
+ # By default, #legend_data returns nil automatically if relevant_data is set to false
100
+ # or the @color attribute is nil. @color is set when the layer is rendered, so legends
101
+ # must be rendered AFTER layers.
102
+ def legend_data
103
+ if relevant_data? && @color
104
+ {:title => title,
105
+ :color => @color,
106
+ :priority => :normal}
107
+ else
108
+ nil
109
+ end
110
+ end
111
+
112
+ # Returns the value of relevant_data
113
+ def relevant_data?
114
+ @relevant_data
115
+ end
116
+
117
+ # The highest data point on this layer, or nil if relevant_data == false
118
+ def top_value
119
+ @relevant_data ? points.maximum_value : nil
120
+ end
121
+
122
+ # The lowest data point on this layer, or nil if relevant_data == false
123
+ def bottom_value
124
+ @relevant_data ? points.minimum_value : nil
125
+ end
126
+
127
+ # The sum of all values
128
+ def sum_values
129
+ points.sum
130
+ end
131
+
132
+ protected
133
+ # Sets up several variables that almost every graph layer will need to render
134
+ # itself.
135
+ def setup_variables(options = {})
136
+ @color = (preferred_color || options.delete(:color))
137
+ @width, @height = options.delete(:size)
138
+ @min_value, @max_value = options[:min_value], options[:max_value]
139
+ @opacity = options[:opacity] || 1.0
140
+ @complexity = options[:complexity]
141
+ end
142
+
143
+ # Optimistic generation of coordinates for layer to use. These coordinates are
144
+ # just a best guess, and can be overridden or thrown away (for example, this is overridden
145
+ # in pie charting and bar charts).
146
+ def generate_coordinates(options = {})
147
+ options[:point_distance] = width / (points.size - 1).to_f
148
+
149
+ points.inject_with_index([]) do |memo, point, idx|
150
+ x_coord = options[:point_distance] * idx
151
+
152
+ if point
153
+ relative_percent = ((point == min_value) ? 0 : ((point - min_value) / (max_value - min_value).to_f))
154
+ y_coord = (height - (height * relative_percent))
155
+
156
+ memo << [x_coord, y_coord]
157
+ end
158
+
159
+ memo
160
+ end
161
+ end
162
+
163
+ # Converts a percentage into a pixel value, relative to the height.
164
+ #
165
+ # Example:
166
+ # relative(5) # On a 100px high layer, this returns 5. 200px high layer, this returns 10, etc.
167
+ def relative(pct)
168
+ # Default to Relative Height
169
+ relative_height(pct)
170
+ end
171
+
172
+ def relative_width(pct)
173
+ if pct # Added to handle nils
174
+ @width * (pct / 100.to_f)
175
+ end
176
+ end
177
+
178
+ def relative_height(pct)
179
+ if pct # Added to handle nils
180
+ @height * (pct / 100.to_f)
181
+ end
182
+ end
183
+
184
+ # Some SVG elements take a long string of multiple coordinates. This is here
185
+ # to make that a little easier.
186
+ def stringify_coords(coords) # :nodoc:
187
+ coords.map { |c| c.join(',') }
188
+ end
189
+ end
190
+
191
+ end # scruffy::layers
@@ -0,0 +1,46 @@
1
+ module Scruffy::Layers
2
+ # ==Scruffy::Layers::Line
3
+ #
4
+ # Author:: Brasten Sager
5
+ # Date:: August 7th, 2006
6
+ #
7
+ # Line graph.
8
+ class Line < Base
9
+
10
+ # Renders line graph.
11
+ #
12
+ # Options:
13
+ # See initialize()
14
+ def draw(svg, coords, options={})
15
+
16
+ # Include options provided when the object was created
17
+ options.merge!(@options)
18
+
19
+ stroke_width = (options[:relativestroke]) ? relative(options[:stroke_width]) : options[:stroke_width]
20
+ style = (options[:style]) ? options[:style] : ''
21
+
22
+ if options[:shadow]
23
+ svg.g(:class => 'shadow', :transform => "translate(#{relative(0.5)}, #{relative(0.5)})") {
24
+ svg.polyline( :points => stringify_coords(coords).join(' '), :fill => 'transparent',
25
+ :stroke => 'black', 'stroke-width' => stroke_width,
26
+ :style => 'fill-opacity: 0; stroke-opacity: 0.35' )
27
+
28
+ if options[:dots]
29
+ coords.each { |coord| svg.circle( :cx => coord.first, :cy => coord.last + relative(0.9), :r => stroke_width,
30
+ :style => "stroke-width: #{stroke_width}; stroke: black; opacity: 0.35;" ) }
31
+ end
32
+ }
33
+ end
34
+
35
+
36
+ svg.polyline( :points => stringify_coords(coords).join(' '), :fill => 'none', :stroke => @color.to_s,
37
+ 'stroke-width' => stroke_width, :style => style )
38
+
39
+ if options[:dots]
40
+ coords.each { |coord| svg.circle( :cx => coord.first, :cy => coord.last, :r => stroke_width,
41
+ :style => "stroke-width: #{stroke_width}; stroke: #{color.to_s}; fill: #{color.to_s}" ) }
42
+ end
43
+
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,123 @@
1
+ module Scruffy::Layers
2
+ # ==Scruffy::Layers::Pie
3
+ #
4
+ # Author:: A.J. Ostman
5
+ # Date:: August 15, 2006
6
+ #
7
+ # Provides a container for pie slice.
8
+ class Pie < Base
9
+ include Scruffy::Helpers::LayerContainer
10
+
11
+ # Basic Example:
12
+ #
13
+ # graph = Scruffy::Graph.new
14
+ # graph.title = "Snack Preference"
15
+ # graph.renderer = Scruffy::Renderers::Pie.new
16
+ # graph.add :pie, 'Example', {'Apples' => 90, 'Orange' => 60, 'Taco' => 30}
17
+ #
18
+ # Or, using a block to add slices:
19
+ #
20
+ # graph = Scruffy::Graph.new
21
+ # graph.title = "Snack Preference"
22
+ # graph.renderer = Scruffy::Renderers::Pie.new
23
+ # graph.add :pie do |pie|
24
+ # pie.add :pie_slice, 'Apple', [90]
25
+ # pie.add :pie_slice, 'Orange', [60]
26
+ # pie.add :pie_slice, 'Taco', [30]
27
+ # end
28
+ #
29
+ # Another Example:
30
+ # graph.title = "Scruff-Pac!"
31
+ # graph.renderer = Scruffy::Renderers::Pie.new
32
+ # graph.add :pie, :diameter => 40, :degree_offset => 30 do |pie|
33
+ # pie.add :pie_slice, '', [160], :preferred_color => "yellow", :shadow => true,
34
+ # :shadow_x => -1, :shadow_y => 1, :shadow_color=>"black", :shadow_opacity => 0.4
35
+ # pie.add :pie_slice, '', [50], :preferred_color => "green", :explode => 5, :diameter => 20,
36
+ # :shadow => true, :shadow_x => -1, :shadow_y => 1, :shadow_color => "black", :shadow_opacity => 0.4
37
+ # end
38
+ #
39
+ # graph.add :pie, :diameter => 3, :center_x => 48, :center_y=> 37, :degree_offset => 20 do |pie|
40
+ # pie.add :pie_slice, '', [160], :preferred_color => "blue", :stroke => "black"
41
+ # end
42
+
43
+
44
+ # Setup Constants
45
+ RADIANS = Math::PI/180
46
+
47
+ attr_accessor :diameter
48
+ attr_accessor :percent_used
49
+ attr_accessor :degree_offset
50
+ attr_accessor :scaler
51
+ attr_accessor :center_x, :center_y
52
+
53
+
54
+ # The initialize method passes itself to the block, and since Pie is a
55
+ # LayerContainer, layers (pie slice) can be added just as if they were being
56
+ # added to Graph.
57
+ def initialize(options = {}, &block)
58
+ super(options)
59
+
60
+ # Allow for population of data with a block during initialization.
61
+ if block
62
+ block.call(self)
63
+ else
64
+ # Otherwise, just iterate over the points, adding the slices
65
+ if @points.class == Hash
66
+ @points.keys.each {|k|
67
+ self.add :pie_slice, k.to_s, [@points[k]]}
68
+ end
69
+ if @points.class == Array
70
+ @points.each {|v|
71
+ self.add :pie_slice, '', [v]}
72
+ end
73
+ end
74
+ end
75
+
76
+
77
+ # Overrides Base#render to fiddle with layers' points to achieve a stacked
78
+ # effect.
79
+ def render(svg, options = {})
80
+ # #current_points = points.dup
81
+
82
+ @scaler = 1
83
+ total = 0
84
+
85
+ layers.each do |layer|
86
+ total += layer.sum_values
87
+ end
88
+
89
+ @scaler = 100.0 / total
90
+
91
+ @percent_used = 30
92
+
93
+ layers.each do |layer|
94
+ layer_options = options.dup
95
+ layer_options = layer_options.merge(@options)
96
+ layer_options = layer_options.merge(layer.options)
97
+ layer_options[:scaler] = @scaler
98
+ layer_options[:percent_used] = @percent_used
99
+ @percent_used += @scaler * layer.sum_values
100
+ layer_options[:color] = layer.preferred_color || layer.color || options[:theme].next_color
101
+
102
+ layer.render(svg, layer_options)
103
+ end
104
+ end
105
+
106
+ # A stacked graph has many data sets. Return legend information for all of them.
107
+ def legend_data
108
+ if relevant_data?
109
+ retval = []
110
+ layers.each do |layer|
111
+ retval << layer.legend_data
112
+ end
113
+ retval
114
+ else
115
+ nil
116
+ end
117
+ end
118
+
119
+ def points=(val)
120
+ throw ArgumentsError, "Pie layers cannot accept points, only pie slices."
121
+ end
122
+ end
123
+ end