scruffy 0.1.0 → 0.2.0

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 (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