scruffy 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/CHANGES +37 -2
  2. data/lib/scruffy.rb +4 -6
  3. data/lib/scruffy/components.rb +11 -0
  4. data/lib/scruffy/components/background.rb +24 -0
  5. data/lib/scruffy/components/base.rb +46 -0
  6. data/lib/scruffy/components/data_markers.rb +25 -0
  7. data/lib/scruffy/components/graphs.rb +48 -0
  8. data/lib/scruffy/components/grid.rb +14 -0
  9. data/lib/scruffy/components/label.rb +12 -0
  10. data/lib/scruffy/components/legend.rb +64 -0
  11. data/lib/scruffy/components/style_info.rb +17 -0
  12. data/lib/scruffy/components/title.rb +12 -0
  13. data/lib/scruffy/components/value_markers.rb +31 -0
  14. data/lib/scruffy/components/viewport.rb +30 -0
  15. data/lib/scruffy/formatters.rb +111 -0
  16. data/lib/scruffy/graph.rb +38 -68
  17. data/lib/scruffy/helpers.rb +2 -0
  18. data/lib/scruffy/helpers/canvas.rb +22 -0
  19. data/lib/scruffy/helpers/layer_container.rb +69 -0
  20. data/lib/scruffy/layers.rb +1 -0
  21. data/lib/scruffy/layers/all_smiles.rb +4 -0
  22. data/lib/scruffy/layers/area.rb +3 -2
  23. data/lib/scruffy/layers/average.rb +3 -12
  24. data/lib/scruffy/layers/bar.rb +1 -1
  25. data/lib/scruffy/layers/base.rb +35 -20
  26. data/lib/scruffy/layers/line.rb +2 -2
  27. data/lib/scruffy/layers/stacked.rb +75 -0
  28. data/lib/scruffy/rasterizers.rb +2 -1
  29. data/lib/scruffy/rasterizers/batik_rasterizer.rb +25 -0
  30. data/lib/scruffy/rasterizers/rmagick_rasterizer.rb +6 -1
  31. data/lib/scruffy/renderers.rb +5 -0
  32. data/lib/scruffy/renderers/base.rb +37 -0
  33. data/lib/scruffy/renderers/cubed.rb +36 -0
  34. data/lib/scruffy/renderers/reversed.rb +18 -0
  35. data/lib/scruffy/renderers/split.rb +34 -0
  36. data/lib/scruffy/renderers/standard.rb +14 -152
  37. data/lib/scruffy/themes.rb +63 -19
  38. data/lib/scruffy/version.rb +1 -1
  39. data/spec/graph_spec.rb +33 -1
  40. data/spec/layers/line_spec.rb +9 -0
  41. metadata +26 -4
  42. data/lib/scruffy/resolver.rb +0 -14
  43. data/lib/scruffy/transformer.rb +0 -73
data/lib/scruffy/graph.rb CHANGED
@@ -73,12 +73,14 @@ module Scruffy
73
73
  # Of course, while you may be able to combine some things such as pie charts and line graphs, that
74
74
  # doesn't necessarily mean they will make any logical sense together. We leave those decisions up to you. :)
75
75
  class Graph
76
+ include Scruffy::Helpers::LayerContainer
77
+
76
78
  attr_accessor :title
77
79
  attr_accessor :theme
78
- attr_accessor :layers
79
80
  attr_accessor :default_type
80
81
  attr_accessor :point_markers
81
- attr_accessor :marker_transformer
82
+ attr_accessor :value_formatter
83
+ attr_accessor :rasterizer
82
84
 
83
85
  attr_reader :renderer # Writer defined below
84
86
 
@@ -94,20 +96,21 @@ module Scruffy
94
96
  # theme:: A theme hash to use when rendering graph
95
97
  # layers:: An array of Layers for this graph to use
96
98
  # default_type:: A symbol indicating the default type of Layer for this graph
97
- # renderer:: Sets the renderer to use when rendering graph
98
- # marker_transformer:: Sets a transformer used to modify marker values prior to rendering
99
+ # value_formatter:: Sets a formatter used to modify marker values prior to rendering
99
100
  # point_markers:: Sets the x-axis marker values
101
+ # rasterizer:: Sets the rasterizer to use when rendering to an image format. Defaults to RMagick.
100
102
  def initialize(*args)
101
103
  self.default_type = args.shift if args.first.is_a?(Symbol)
102
104
  options = args.shift.dup if args.first.is_a?(Hash)
103
105
  raise ArgumentError, "The arguments provided are not supported." if args.size > 0
104
106
 
105
107
  options ||= {}
106
- self.theme = Scruffy::Themes::KEYNOTE
107
- self.layers = []
108
- self.renderer = Scruffy::StandardRenderer.new
108
+ self.theme = Scruffy::Themes::Keynote.new
109
+ self.renderer = Scruffy::Renderers::Standard.new
110
+ self.rasterizer = Scruffy::Rasterizers::RMagickRasterizer.new
111
+ self.value_formatter = Scruffy::Formatters::Number.new
109
112
 
110
- %w(title theme layers default_type renderer marker_transformer point_markers).each do |arg|
113
+ %w(title theme layers default_type value_formatter point_markers rasterizer).each do |arg|
111
114
  self.send("#{arg}=".to_sym, options.delete(arg.to_sym)) unless options[arg.to_sym].nil?
112
115
  end
113
116
 
@@ -118,7 +121,7 @@ module Scruffy
118
121
  #
119
122
  # Options:
120
123
  # size:: An array indicating the size you wish to render the graph. ( [x, y] )
121
- # width:: The width of the rendered graph. A height is calculated at 60% of the width.
124
+ # width:: The width of the rendered graph. A height is calculated at 3/4th of the width.
122
125
  # theme:: Theme used to render graph for this render only.
123
126
  # min_value:: Overrides the calculated minimum value used for the graph.
124
127
  # max_value:: Overrides the calculated maximum value used for the graph.
@@ -127,75 +130,42 @@ module Scruffy
127
130
  # as:: File format to render to ('PNG', 'JPG', etc)
128
131
  # to:: Name of file to save graph to, if desired. If not provided, image is returned as blob.
129
132
  def render(options = {})
130
- size = options.delete(:size) || (options[:width] ? [options[:width], (options.delete(:width) * 0.6).to_i] : [600, 400])
131
- options[:theme] ||= @theme
132
- options[:marker_transformer] ||= @marker_transformer
133
- options[:point_markers] ||= @point_markers
133
+ options[:theme] ||= theme
134
+ options[:value_formatter] ||= value_formatter
135
+ options[:point_markers] ||= point_markers
136
+ options[:size] ||= (options[:width] ? [options[:width], (options.delete(:width) * 0.6).to_i] : [600, 360])
137
+ options[:title] ||= title
138
+ options[:layers] ||= layers
139
+ options[:min_value] ||= bottom_value(:padded)
140
+ options[:max_value] ||= top_value
141
+ options[:graph] ||= self
142
+
134
143
 
135
- svg = self.renderer.render( options.merge({:size => size, :title => title,
136
- :layers => layers, :min_value => (options[:min_value] || bottom_value),
137
- :max_value => (options[:max_value] || top_value) } ) )
144
+ if options[:as] && (options[:size][0] <= 300 || options[:size][1] <= 200)
145
+ options[:actual_size] = options[:size]
146
+ options[:size] = [800, (800.to_f * (options[:actual_size][1].to_f / options[:actual_size][0].to_f))]
147
+ puts options[:size].inspect
148
+ end
149
+
150
+ svg = ( options[:renderer].nil? ? self.renderer.render( options ) : options[:renderer].render( options ) )
138
151
 
139
- options[:as] ? Scruffy::Rasterizers::RMagickRasterizer.new.rasterize(svg, options) : svg
140
- end
141
-
142
- # Adds a Layer to the Graph. Accepts either a list of arguments used to build a new layer, or
143
- # a Scruffy::Layers::Base-derived object. When passing a list of arguments, all arguments are optional, but the arguments
144
- # specified must be provided in a particular order: type (Symbol), title (String), points (Array),
145
- # options (Hash).
146
- #
147
- # Both #add and #<< can be used.
148
- #
149
- # graph.add(:line, [100, 200, 150]) # Create and add an untitled line graph
150
- #
151
- # graph << (:line, "John's Sales", [150, 100]) # Create and add a titled line graph
152
- #
153
- # graph << Scruffy::Layers::Bar.new({...}) # Adds Bar layer to graph
154
- #
155
- def <<(*args)
156
- if args[0].kind_of?(Scruffy::Layers::Base)
157
- layers << args[0]
158
- else
159
- type = args.first.is_a?(Symbol) ? args.shift : default_type
160
- title = args.shift if args.first.is_a?(String)
161
- points = args.first.is_a?(Array) ? args.shift : []
162
- options = args.first.is_a?(Hash) ? args.shift : {}
163
-
164
- title ||= ''
165
-
166
- raise ArgumentError,
167
- 'You must specify a graph type (:area, :bar, :line, etc) if you do not have a default type specified.' if type.nil?
168
-
169
- layer = Kernel::module_eval("Scruffy::Layers::#{to_camelcase(type.to_s)}").new(options.merge({:points => points, :title => title}))
170
- layers << layer
152
+ options[:size] = options[:actual_size] if options[:actual_size]
153
+
154
+ # SVG to file.
155
+ if options[:to] && options[:as].nil?
156
+ File.open(options[:to], 'w') { |file|
157
+ file.write(svg)
158
+ }
171
159
  end
172
- layer
160
+
161
+ options[:as] ? rasterizer.rasterize(svg, options) : svg
173
162
  end
174
163
 
175
- alias :add :<<
176
164
 
177
165
  def renderer=(val)
178
166
  raise ArgumentError, "Renderer must include a #render(options) method." unless (val.respond_to?(:render) && val.method(:render).arity.abs > 0)
179
167
 
180
168
  @renderer = val
181
169
  end
182
-
183
- protected
184
- def to_camelcase(type) # :nodoc:
185
- type.split('_').map { |e| e.capitalize }.join('')
186
- end
187
-
188
- def top_value # :nodoc:
189
- layers.inject(0) { |max, layer| (max = ((max < layer.highest_point) ? layer.highest_point : max)) unless layer.highest_point.nil?; max }
190
- end
191
-
192
- def bottom_value # :nodoc:
193
- botval = layers.inject(top_value) { |min, layer| (min = ((min > layer.lowest_point) ? layer.lowest_point : min)) unless layer.lowest_point.nil?; min }
194
- above_zero = (botval > 0)
195
-
196
- botval = (botval - ((top_value - botval) * 0.15))
197
- botval = 0 if (above_zero && botval < 0)
198
- botval
199
- end
200
170
  end
201
171
  end
@@ -0,0 +1,2 @@
1
+ require 'scruffy/helpers/canvas'
2
+ require 'scruffy/helpers/layer_container'
@@ -0,0 +1,22 @@
1
+ module Scruffy
2
+ module Helpers
3
+
4
+ # Helper methods for objects that act as canvases/viewports/etc.
5
+ #
6
+ # Primarily used for calculating relative positions.
7
+ module Canvas
8
+
9
+ protected
10
+ def bounds_for(canvas_size, position, size)
11
+ return nil if (position.nil? || size.nil?)
12
+ bounds = {}
13
+ bounds[:x] = canvas_size.first * (position.first / 100.to_f)
14
+ bounds[:y] = canvas_size.last * (position.last / 100.to_f)
15
+ bounds[:width] = canvas_size.first * (size.first / 100.to_f)
16
+ bounds[:height] = canvas_size.last * (size.last / 100.to_f)
17
+ bounds
18
+ end
19
+
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,69 @@
1
+ module Scruffy::Helpers
2
+ module LayerContainer
3
+ # Adds a Layer to the Graph/Container. Accepts either a list of arguments used to build a new layer, or
4
+ # a Scruffy::Layers::Base-derived object. When passing a list of arguments, all arguments are optional, but the arguments
5
+ # specified must be provided in a particular order: type (Symbol), title (String), points (Array),
6
+ # options (Hash).
7
+ #
8
+ # Both #add and #<< can be used.
9
+ #
10
+ # graph.add(:line, [100, 200, 150]) # Create and add an untitled line graph
11
+ #
12
+ # graph << (:line, "John's Sales", [150, 100]) # Create and add a titled line graph
13
+ #
14
+ # graph << Scruffy::Layers::Bar.new({...}) # Adds Bar layer to graph
15
+ #
16
+ def <<(*args, &block)
17
+ if args[0].kind_of?(Scruffy::Layers::Base)
18
+ layers << args[0]
19
+ else
20
+ type = args.first.is_a?(Symbol) ? args.shift : @default_type
21
+ title = args.shift if args.first.is_a?(String)
22
+ points = args.first.is_a?(Array) ? args.shift : []
23
+ options = args.first.is_a?(Hash) ? args.shift : {}
24
+
25
+ title ||= ''
26
+
27
+ raise ArgumentError,
28
+ 'You must specify a graph type (:area, :bar, :line, etc) if you do not have a default type specified.' if type.nil?
29
+
30
+ layer = Kernel::module_eval("Scruffy::Layers::#{to_camelcase(type.to_s)}").new(options.merge({:points => points, :title => title}), &block)
31
+ layers << layer
32
+ end
33
+ layer
34
+ end
35
+
36
+ alias :add :<<
37
+
38
+
39
+ # Custom Layer Accessors
40
+ def layers=(val)
41
+ @layers = val
42
+ end
43
+ def layers
44
+ @layers ||= []
45
+ end
46
+
47
+ def top_value(padding=nil) # :nodoc:
48
+ topval = layers.inject(0) { |max, layer| (max = ((max < layer.top_value) ? layer.top_value : max)) unless layer.top_value.nil?; max }
49
+ padding == :padded ? (topval - ((topval - bottom_value) * 0.15)) : topval
50
+ end
51
+
52
+ def bottom_value(padding=nil) # :nodoc:
53
+ botval = layers.inject(top_value) { |min, layer| (min = ((min > layer.bottom_value) ? layer.bottom_value : min)) unless layer.bottom_value.nil?; min }
54
+ above_zero = (botval > 0)
55
+ botval = (botval - ((top_value - botval) * 0.15))
56
+
57
+ # Don't introduce negative values solely due to padding.
58
+ # A user-provided value must be negative before padding will extend into negative values.
59
+ (above_zero && botval < 0) ? 0 : botval
60
+ end
61
+
62
+
63
+ protected
64
+ def to_camelcase(type) # :nodoc:
65
+ type.split('_').map { |e| e.capitalize }.join('')
66
+ end
67
+
68
+ end
69
+ end
@@ -5,3 +5,4 @@ require 'scruffy/layers/all_smiles'
5
5
  require 'scruffy/layers/bar'
6
6
  require 'scruffy/layers/line'
7
7
  require 'scruffy/layers/average'
8
+ require 'scruffy/layers/stacked'
@@ -104,6 +104,10 @@ module Scruffy
104
104
 
105
105
  end
106
106
  end
107
+
108
+ def scaled(pt)
109
+ relative(pt) / 2
110
+ end
107
111
  end
108
112
  end
109
113
  end
@@ -5,6 +5,7 @@ module Scruffy
5
5
  points_value = "0,#{height} #{stringify_coords(coords).join(' ')} #{width},#{height}"
6
6
 
7
7
  # Experimental, for later user.
8
+ # Neither ImageMagick nor Mozilla SVG render this well (at all). Maybe a future thing.
8
9
  #
9
10
  # svg.defs {
10
11
  # svg.filter(:id => 'MyFilter', :filterUnits => 'userSpaceOnUse', :x => 0, :y => 0, :width => 200, :height => '120') {
@@ -26,8 +27,8 @@ module Scruffy
26
27
  # }
27
28
  # }
28
29
  # }
29
-
30
- svg.polygon(:points => points_value, :fill => color.to_s, :stroke => color.to_s, :opacity => options[:opacity])
30
+
31
+ svg.polygon(:points => points_value, :fill => color.to_s, :stroke => color.to_s, 'style' => "opacity: #{opacity}")
31
32
  end
32
33
  end
33
34
  end
@@ -2,21 +2,12 @@ module Scruffy
2
2
  module Layers
3
3
  class Average < Base
4
4
  def initialize(options = {})
5
- super
6
-
7
- @relevant_data = false
5
+ super(options.merge({:relevant_data => false}))
8
6
  end
7
+
9
8
  def draw(svg, coords, options = {})
10
9
  svg.polyline( :points => coords.join(' '), :fill => 'none', :stroke => 'black',
11
- 'stroke-width' => scaled(25), :opacity => '0.4')
12
- end
13
-
14
- def highest_point
15
- nil
16
- end
17
-
18
- def lowest_point
19
- nil
10
+ 'stroke-width' => relative(5), 'opacity' => '0.4')
20
11
  end
21
12
 
22
13
  protected
@@ -7,7 +7,7 @@ module Scruffy
7
7
  x, y, bar_height = (coord.first-(@bar_width * 0.5)), coord.last, (height - coord.last)
8
8
 
9
9
  svg.rect( :x => x, :y => y, :width => @bar_width, :height => bar_height,
10
- :fill => color.to_s, :stroke => color.to_s, :opacity => opacity )
10
+ :fill => color.to_s, :stroke => color.to_s, 'style' => "opacity: #{opacity}" )
11
11
  end
12
12
  end
13
13
 
@@ -35,6 +35,7 @@ module Scruffy
35
35
  attr_accessor :resolver
36
36
  attr_accessor :complexity
37
37
  attr_accessor :relevant_data
38
+ attr_accessor :options # On-the-fly values for easy customization / acts as attributes.
38
39
 
39
40
  # Returns a new Base object.
40
41
  #
@@ -45,10 +46,11 @@ module Scruffy
45
46
  # relevant_data:: Rarely used - indicates the data on this graph should not
46
47
  # included in any graph data aggregations, such as averaging data points.
47
48
  def initialize(options = {})
48
- @title = options[:title] || ''
49
- @points = options[:points] || []
50
- @preferred_color = options[:color]
51
- @relevant_data = options[:relevant_data] || true
49
+ @title = options.delete(:title) || ''
50
+ @points = options.delete(:points) || []
51
+ @preferred_color = options.delete(:color)
52
+ @relevant_data = options.delete(:relevant_data) || true
53
+ @options = options
52
54
  end
53
55
 
54
56
  # Builds SVG code for this graph using the provided Builder object.
@@ -72,28 +74,45 @@ module Scruffy
72
74
  # This is what the various graphs need to implement.
73
75
  end
74
76
 
77
+ # Returns a hash with information to be used by the legend.
78
+ #
79
+ # Alternatively, returns nil if you don't want this layer to be in the legend,
80
+ # or an array of hashes if this layer should have multiple legend entries (stacked?)
81
+ #
82
+ # By default, #legend_data returns nil automatically if relevant_data is set to false
83
+ # or the @color attribute is nil. @color is set when the layer is rendered, so legends
84
+ # must be rendered AFTER layers.
85
+ def legend_data
86
+ if relevant_data? && @color
87
+ {:title => title,
88
+ :color => @color,
89
+ :priority => :normal}
90
+ else
91
+ nil
92
+ end
93
+ end
94
+
75
95
  # Returns the value of relevant_data
76
96
  def relevant_data?
77
97
  @relevant_data
78
98
  end
79
99
 
80
- # The highest data point on this layer
81
- def highest_point
82
- points.sort.last
100
+ # The highest data point on this layer, or nil if relevant_data == false
101
+ def top_value
102
+ @relevant_data ? points.sort.last : nil
83
103
  end
84
104
 
85
- # The lowest data point on this layer
86
- def lowest_point
87
- points.sort.first
105
+ # The lowest data point on this layer, or nil if relevant_data == false
106
+ def bottom_value
107
+ @relevant_data ? points.sort.first: nil
88
108
  end
89
-
109
+
90
110
  protected
91
111
  def setup_variables(options = {}) # :nodoc:
92
112
  @color = (preferred_color || options.delete(:color))
93
- @resolver = options.delete(:resolver)
94
113
  @width, @height = options.delete(:size)
95
114
  @min_value, @max_value = options[:min_value], options[:max_value]
96
- @opacity = options[:opacity]
115
+ @opacity = options[:opacity] || 1.0
97
116
  @complexity = options[:complexity]
98
117
  end
99
118
 
@@ -112,13 +131,9 @@ module Scruffy
112
131
  coords
113
132
  end
114
133
 
115
- # Returns a 'scaled' value of the given integer.
116
- #
117
- # Scaled adjusts the given point depending on the height of the graph, where 400px is considered 1:1.
118
- #
119
- # ie: On a graph that is 800px high, scaled(1) => 2. For 200px high, scaled(1) => 0.5, and so on...
120
- def scaled(point)
121
- resolver.to_point(point)
134
+ # Converts a percentage into a pixel value, related to the height.
135
+ def relative(pct)
136
+ @height * (pct / 100.to_f)
122
137
  end
123
138
 
124
139
  def stringify_coords(coords) # :nodoc:
@@ -3,9 +3,9 @@ module Scruffy
3
3
  class Line < Base
4
4
  def draw(svg, coords, options={})
5
5
  svg.polyline( :points => stringify_coords(coords).join(' '), :fill => 'none',
6
- :stroke => color.to_s, 'stroke-width' => scaled(4) )
6
+ :stroke => color.to_s, 'stroke-width' => relative(2) )
7
7
 
8
- coords.each { |coord| svg.circle( :cx => coord.first, :cy => coord.last, :r => scaled(5),
8
+ coords.each { |coord| svg.circle( :cx => coord.first, :cy => coord.last, :r => relative(2.5),
9
9
  :fill => color.to_s ) }
10
10
  end
11
11
  end
@@ -0,0 +1,75 @@
1
+ module Scruffy
2
+ module Layers
3
+ class Stacked < Base
4
+ include Scruffy::Helpers::LayerContainer
5
+
6
+ def initialize(options={}, &block)
7
+ super(options)
8
+
9
+ block.call(self) # Allow for population of data with a block during initialization.
10
+ end
11
+
12
+ # Builds SVG code for this graph using the provided Builder object.
13
+ # This method actually generates data needed by this graph, then passes the
14
+ # rendering responsibilities to Base#draw.
15
+ #
16
+ # svg:: a Builder object used to create SVG code.
17
+ def render(svg, options = {})
18
+ current_points = points.dup
19
+
20
+ layers.each do |layer|
21
+ real_points = layer.points
22
+ layer.points = current_points
23
+ layer_options = options.dup
24
+ layer_options[:color] = layer.preferred_color || layer.color || options[:theme].next_color
25
+ layer.render(svg, layer_options)
26
+ options.merge(layer_options)
27
+ layer.points = real_points
28
+
29
+ layer.points.each_with_index { |val, idx| current_points[idx] -= val }
30
+ end
31
+ end
32
+
33
+ def legend_data
34
+ if relevant_data?
35
+ retval = []
36
+ layers.each do |layer|
37
+ retval << layer.legend_data
38
+ end
39
+ retval
40
+ else
41
+ nil
42
+ end
43
+ end
44
+
45
+ # The highest data point on this layer, or nil if relevant_data == false
46
+ def top_value
47
+ @relevant_data ? points.sort.last : nil
48
+ end
49
+
50
+ def points
51
+ longest_arr = layers.inject(nil) do |longest, layer|
52
+ longest = layer.points if (longest.nil? || longest.size < layer.points.size)
53
+ end
54
+
55
+ summed_points = (0...longest_arr.size).map do |idx|
56
+ layers.inject(nil) do |sum, layer|
57
+ if sum.nil? && !layer.points[idx].nil?
58
+ sum = layer.points[idx]
59
+ elsif !layer.points[idx].nil?
60
+ sum += layer.points[idx]
61
+ end
62
+
63
+ sum
64
+ end
65
+ end
66
+
67
+ summed_points
68
+ end
69
+
70
+ def points=(val)
71
+ throw ArgumentsError, "Stacked layers cannot accept points, only other layers."
72
+ end
73
+ end
74
+ end
75
+ end