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,205 @@
1
+ require 'forwardable'
2
+
3
+ module Scruffy
4
+
5
+ # ==Scruffy Graphs
6
+ #
7
+ # Author:: Brasten Sager
8
+ # Date:: August 5th, 2006
9
+ #
10
+ #
11
+ # ====Graphs vs. Layers (Graph Types)
12
+ #
13
+ # Scruffy::Graph is the primary class you will use to generate your graphs. A Graph does not
14
+ # define a graph type nor does it directly hold any data. Instead, a Graph object can be thought
15
+ # of as a canvas on which other graphs are draw. (The actual graphs themselves are subclasses of Scruffy::Layers::Base)
16
+ # Despite the technical distinction, we will refer to Scruffy::Graph objects as 'graphs' and Scruffy::Layers as
17
+ # 'layers' or 'graph types.'
18
+ #
19
+ #
20
+ # ==== Creating a Graph
21
+ #
22
+ # You can begin building a graph by instantiating a Graph object and optionally passing a hash
23
+ # of properties.
24
+ #
25
+ # graph = Scruffy::Graph.new
26
+ #
27
+ # OR
28
+ #
29
+ # graph = Scruffy::Graph.new(:title => "Monthly Profits", :theme => Scruffy::Themes::RubyBlog.new)
30
+ #
31
+ # Once you have a Graph object, you can set any Graph-level properties (title, theme, etc), or begin adding
32
+ # graph layers. You can add a graph layer to a graph by using the Graph#add or Graph#<< methods. The two
33
+ # methods are identical and used to accommodate syntax preferences.
34
+ #
35
+ # graph.add(:line, 'John', [100, -20, 30, 60])
36
+ # graph.add(:line, 'Sara', [120, 50, -80, 20])
37
+ #
38
+ # OR
39
+ #
40
+ # graph << Scruffy::Layers::Line.new(:title => 'John', :points => [100, -20, 30, 60])
41
+ # graph << Scruffy::Layers::Line.new(:title => 'Sara', :points => [120, 50, -80, 20])
42
+ #
43
+ # Now that we've created our graph and added a layer to it, we're ready to render! You can render the graph
44
+ # directly to SVG or any other image format (supported by RMagick) with the Graph#render method:
45
+ #
46
+ # graph.render # Renders a 600x400 SVG graph
47
+ #
48
+ # OR
49
+ #
50
+ # graph.render(:width => 1200)
51
+ #
52
+ # # For image formats other than SVG:
53
+ # graph.render(:width => 1200, :as => 'PNG')
54
+ #
55
+ # # To render directly to a file:
56
+ # graph.render(:width => 5000, :to => '<filename>')
57
+ #
58
+ # graph.render(:width => 700, :as => 'PNG', :to => '<filename>')
59
+ #
60
+ # And that's your basic Scruffy graph! Please check the documentation for the various methods and
61
+ # classes you'll be using, as there are a bunch of options not demonstrated here.
62
+ #
63
+ # A couple final things worth noting:
64
+ # * You can call Graph#render as often as you wish with different rendering options. In
65
+ # fact, you can modify the graph any way you wish between renders.
66
+ #
67
+ #
68
+ # * There are no restrictions to the combination of graph layers you can add. It is perfectly
69
+ # valid to do something like:
70
+ # graph.add(:line, [100, 200, 300])
71
+ # graph.add(:bar, [200, 150, 150])
72
+ #
73
+ # Of course, while you may be able to combine some things such as pie charts and line graphs, that
74
+ # doesn't necessarily mean they will make any logical sense together. We leave those decisions up to you. :)
75
+
76
+ class Graph
77
+ extend Forwardable;
78
+
79
+ include Scruffy::Helpers::LayerContainer
80
+
81
+ # Delegating these getters to the internal state object.
82
+ def_delegators :internal_state, :title,:x_legend,:y_legend, :theme, :default_type,
83
+ :point_markers,:point_markers_rotation,:point_markers_ticks, :value_formatter, :rasterizer,
84
+ :key_formatter
85
+
86
+ def_delegators :internal_state, :title=, :theme=,:x_legend=,:y_legend=, :default_type=,
87
+ :point_markers=,:point_markers_rotation=,:point_markers_ticks=, :value_formatter=, :rasterizer=,
88
+ :key_formatter=
89
+
90
+ attr_reader :renderer # Writer defined below
91
+
92
+ # Returns a new Graph. You can optionally pass in a default graph type and an options hash.
93
+ #
94
+ # Graph.new # New graph
95
+ # Graph.new(:line) # New graph with default graph type of Line
96
+ # Graph.new({...}) # New graph with options.
97
+ #
98
+ # Options:
99
+ #
100
+ # title:: Graph's title
101
+ # x_legend :: Title for X Axis
102
+ # y_legend :: Title for Y Axis
103
+ # theme:: A theme object to use when rendering graph
104
+ # layers:: An array of Layers for this graph to use
105
+ # default_type:: A symbol indicating the default type of Layer for this graph
106
+ # value_formatter:: Sets a formatter used to modify marker values prior to rendering
107
+ # point_markers:: Sets the x-axis marker values
108
+ # point_markers_rotation:: Sets the angle of rotation for x-axis marker values
109
+ # point_markers_ticks:: Sets a small tick mark above each marker value. Helful when used with rotation.
110
+ # rasterizer:: Sets the rasterizer to use when rendering to an image format. Defaults to RMagick.
111
+ def initialize(*args)
112
+ self.default_type = args.shift if args.first.is_a?(Symbol)
113
+ options = args.shift.dup if args.first.is_a?(Hash)
114
+ raise ArgumentError, "The arguments provided are not supported." if args.size > 0
115
+
116
+ options ||= {}
117
+
118
+
119
+ self.theme = Scruffy::Themes::Standard.new
120
+ self.renderer = Scruffy::Renderers::Standard.new
121
+ self.rasterizer = Scruffy::Rasterizers::MiniMagickRasterizer.new
122
+ self.value_formatter = Scruffy::Formatters::Number.new
123
+ self.key_formatter = Scruffy::Formatters::Number.new
124
+
125
+ %w(title x_legend y_legend theme layers default_type value_formatter point_markers point_markers_rotation point_markers_ticks rasterizer key_formatter).each do |arg|
126
+ self.send("#{arg}=".to_sym, options.delete(arg.to_sym)) unless options[arg.to_sym].nil?
127
+ end
128
+
129
+ raise ArgumentError, "Some options provided are not supported: #{options.keys.join(' ')}." if options.size > 0
130
+ end
131
+
132
+ # Renders the graph in it's current state to an SVG object.
133
+ #
134
+ # Options:
135
+ # size:: An array indicating the size you wish to render the graph. ( [x, y] )
136
+ # width:: The width of the rendered graph. A height is calculated at 3/4th of the width.
137
+ # theme:: Theme used to render graph for this render only.
138
+ # min_value:: Overrides the calculated minimum value used for the graph.
139
+ # max_value:: Overrides the calculated maximum value used for the graph.
140
+ # renderer:: Provide a Renderer object to use instead of the default.
141
+ #
142
+ # For other image formats:
143
+ # as:: File format to render to ('PNG', 'JPG', etc)
144
+ # to:: Name of file to save graph to, if desired. If not provided, image is returned as blob/string.
145
+ def render(options = {})
146
+ options[:theme] ||= theme
147
+ options[:value_formatter] ||= value_formatter
148
+ options[:key_formatter] ||= key_formatter
149
+ options[:point_markers] ||= point_markers
150
+ options[:point_markers_rotation] ||= point_markers_rotation
151
+ options[:point_markers_ticks] ||= point_markers_ticks
152
+ options[:size] ||= (options[:width] ? [options[:width], (options.delete(:width) * 0.6).to_i] : [600, 360])
153
+ options[:title] ||= title
154
+ options[:x_legend] ||= x_legend
155
+ options[:y_legend] ||= y_legend
156
+ options[:layers] ||= layers
157
+ options[:min_value] ||= bottom_value(options[:padding] ? options[:padding] : nil)
158
+ options[:max_value] ||= top_value(options[:padding] ? options[:padding] : nil)
159
+ options[:min_key] ||= bottom_key
160
+ options[:max_key] ||= top_key
161
+ options[:graph] ||= self
162
+
163
+ # Removed for now.
164
+ # Added for making smaller fonts more legible, but may not be needed after all.
165
+ #
166
+ # if options[:as] && (options[:size][0] <= 300 || options[:size][1] <= 200)
167
+ # options[:actual_size] = options[:size]
168
+ # options[:size] = [800, (800.to_f * (options[:actual_size][1].to_f / options[:actual_size][0].to_f))]
169
+ # end
170
+
171
+ svg = ( options[:renderer].nil? ? self.renderer.render( options ) : options[:renderer].render( options ) )
172
+
173
+ # SVG to file.
174
+ if options[:to] && options[:as].nil?
175
+ File.open(options[:to], 'w') { |file|
176
+ file.write(svg)
177
+ }
178
+ end
179
+
180
+ options[:as] ? rasterizer.rasterize(svg, options) : svg
181
+ end
182
+
183
+ def renderer=(val)
184
+ raise ArgumentError, "Renderer must include a #render(options) method." unless (val.respond_to?(:render) && val.method(:render).arity.abs > 0)
185
+
186
+ @renderer = val
187
+ end
188
+
189
+ alias :layout :renderer
190
+
191
+ def component(id)
192
+ renderer.component(id)
193
+ end
194
+
195
+ def remove(id)
196
+ renderer.remove(id)
197
+ end
198
+
199
+ private
200
+ def internal_state
201
+ @internal_state ||= GraphState.new
202
+ end
203
+
204
+ end
205
+ end
@@ -0,0 +1,29 @@
1
+ # ===GraphState
2
+ #
3
+ # Author:: Brasten Sager
4
+ # Date:: September 27th, 2007
5
+ #
6
+ # State object for holding all of the graph's
7
+ # settings. Attempting to clean up the
8
+ # graph interface a bit.
9
+
10
+ module Scruffy
11
+ class GraphState
12
+
13
+ attr_accessor :title
14
+ attr_accessor :x_legend
15
+ attr_accessor :y_legend
16
+ attr_accessor :theme
17
+ attr_accessor :default_type
18
+ attr_accessor :point_markers
19
+ attr_accessor :point_markers_rotation
20
+ attr_accessor :point_markers_ticks
21
+ attr_accessor :value_formatter
22
+ attr_accessor :key_formatter
23
+ attr_accessor :rasterizer
24
+
25
+ def initialize
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,13 @@
1
+ # ==Scruffy Helpers
2
+ #
3
+ # Author:: Brasten Sager
4
+ # Date:: August 16th, 2006
5
+ #
6
+ # Modules which provide helper methods for various situations.
7
+ module Scruffy::Helpers; end
8
+
9
+ require 'scruffy/helpers/canvas'
10
+ require 'scruffy/helpers/layer_container'
11
+ require 'scruffy/helpers/point_container'
12
+ require 'scruffy/helpers/marker_helper'
13
+ #require 'scruffy/helpers/meta'
@@ -0,0 +1,41 @@
1
+ module Scruffy::Helpers
2
+
3
+ # ==Scruffy::Helpers::Canvas
4
+ #
5
+ # Author:: Brasten Sager
6
+ # Date:: August 16th, 2006
7
+ #
8
+ # Provides common methods for canvas objects. Primarily used for providing
9
+ # spacial-type calculations where necessary.
10
+ module Canvas
11
+ attr_accessor :components
12
+
13
+ def reset_settings!
14
+ self.options = {}
15
+ end
16
+
17
+ def component(id, components=self.components)
18
+ components.find {|elem| elem.id == id}
19
+ end
20
+
21
+ def remove(id, components=self.components)
22
+ components.delete(component(id))
23
+ end
24
+
25
+ protected
26
+ # Converts percentage values into actual pixel values based on the known
27
+ # render size.
28
+ #
29
+ # Returns a hash consisting of :x, :y, :width, and :height elements.
30
+ def bounds_for(canvas_size, position, size)
31
+ return nil if (position.nil? || size.nil?)
32
+ bounds = {}
33
+ bounds[:x] = canvas_size.first * (position.first / 100.to_f)
34
+ bounds[:y] = canvas_size.last * (position.last / 100.to_f)
35
+ bounds[:width] = canvas_size.first * (size.first / 100.to_f)
36
+ bounds[:height] = canvas_size.last * (size.last / 100.to_f)
37
+ bounds
38
+ end
39
+ end # canvas
40
+
41
+ end # scruffy::helpers
@@ -0,0 +1,119 @@
1
+ module Scruffy::Helpers
2
+
3
+ # ==Scruffy::Helpers::LayerContainer
4
+ #
5
+ # Author:: Brasten Sager
6
+ # Date:: August 16th, 2006
7
+ #
8
+ # Adds some common functionality to any object which needs to act as a
9
+ # container for graph layers. The best example of this is the Scruffy::Graph
10
+ # object itself, but this module is also used by Scruffy::Layer::Stacked.
11
+ module LayerContainer
12
+
13
+ # Adds a Layer to the Graph/Container. Accepts either a list of
14
+ # arguments used to build a new layer, or a Scruffy::Layers::Base-derived
15
+ # object. When passing a list of arguments, all arguments are optional,
16
+ # but the arguments specified must be provided in a particular order:
17
+ # type (Symbol), title (String), points (Array), options (Hash).
18
+ #
19
+ # Both #add and #<< can be used.
20
+ #
21
+ # graph.add(:line, [100, 200, 150]) # Create and add an untitled line graph
22
+ #
23
+ # graph << (:line, "John's Sales", [150, 100]) # Create and add a titled line graph
24
+ #
25
+ # graph << Scruffy::Layers::Bar.new({...}) # Adds Bar layer to graph
26
+ #
27
+ def <<(*args, &block)
28
+ if args[0].kind_of?(Scruffy::Layers::Base)
29
+ layers << args[0]
30
+ else
31
+ type = args.first.is_a?(Symbol) ? args.shift : @default_type
32
+ title = args.shift if args.first.is_a?(String)
33
+
34
+ # Layer handles PointContainer mixin, don't do it here
35
+ points = [Array, Hash].include?(args.first.class) ? args.shift : []
36
+ options = args.first.is_a?(Hash) ? args.shift : {}
37
+
38
+ title ||= ''
39
+
40
+ raise ArgumentError,
41
+ 'You must specify a graph type (:area, :bar, :line, etc) if you do not have a default type specified.' if type.nil?
42
+
43
+ class_name = "Scruffy::Layers::#{to_camelcase(type.to_s)}"
44
+ layer_class = Kernel::module_eval(class_name)
45
+ options = {:points => points, :title => title}.merge options
46
+ layer = layer_class.new(options, &block)
47
+ layers << layer
48
+ end
49
+ layer
50
+ end
51
+
52
+ alias :add :<<
53
+
54
+
55
+ # Layer Writer
56
+ def layers=(val)
57
+ @layers = val
58
+ end
59
+
60
+ # Layer Reader
61
+ def layers
62
+ @layers ||= []
63
+ end
64
+
65
+ # Returns the highest value in any of this container's layers.
66
+ #
67
+ # If padding is set to :padded, a 15% padding is added to the highest value.
68
+ def top_value(padding=nil) # :nodoc:
69
+ topval = layers.inject(0) { |max, layer| (max = ((max < layer.top_value) ? layer.top_value : max)) unless layer.top_value.nil?; max }
70
+ below_zero = (topval <= 0)
71
+ topval = padding == :padded ? (topval + ((topval - bottom_value) * 0.15)) : topval
72
+ (below_zero && topval > 0) ? 0 : topval
73
+ end
74
+
75
+ # Returns the lowest value in any of this container's layers.
76
+ #
77
+ # If padding is set to :padded, a 15% padding is added below the lowest value.
78
+ # If the lowest value is greater than zero, then the padding will not cross the zero line, preventing
79
+ # negative values from being introduced into the graph purely due to padding.
80
+ def bottom_value(padding=nil) # :nodoc:
81
+ botval = layers.inject(0) do |min, layer|
82
+ (min = ((min > layer.bottom_value) ? layer.bottom_value : min)) unless layer.bottom_value.nil?
83
+ min
84
+ end
85
+ above_zero = (botval >= 0)
86
+ botval = (botval - ((top_value - botval) * 0.15)) if padding == :padded
87
+
88
+ # Don't introduce negative values solely due to padding.
89
+ # A user-provided value must be negative before padding will extend into negative values.
90
+ (above_zero && botval < 0) ? 0 : botval
91
+ end
92
+
93
+ def bottom_key(padding=nil)
94
+ return 0 unless layers.any?
95
+ min = layers[0].bottom_key
96
+ layers.each do |layer|
97
+ min = layer.bottom_key if min.nil? && !layer.bottom_key.nil?
98
+ (min = ((min > layer.bottom_key) ? layer.bottom_key : min)) unless layer.bottom_key.nil?
99
+ end
100
+ min
101
+ end
102
+
103
+ def top_key(padding=nil)
104
+ return 1 unless layers.any?
105
+ max = layers[0].top_key
106
+ layers.each do |layer|
107
+ max = layer.top_key if max.nil? && !layer.top_key.nil?
108
+ (max = ((max < layer.top_key) ? layer.top_key : max)) unless layer.top_key.nil?
109
+ end
110
+ max
111
+ end
112
+
113
+ protected
114
+ def to_camelcase(type) # :nodoc:
115
+ type.split('_').map { |e| e.capitalize }.join('')
116
+ end
117
+
118
+ end
119
+ end
@@ -0,0 +1,25 @@
1
+ module Scruffy::Helpers
2
+
3
+ module Marker
4
+
5
+ def each_marker(markers, min, max, width, options, format_key)
6
+ all_values = []
7
+
8
+ (0...markers).each do |idx|
9
+ percent = idx.to_f / (markers-1)
10
+ all_values << min + (max - min) * percent
11
+ end
12
+
13
+ all_values.size.times do |idx|
14
+ dx = width/(markers - 1)
15
+
16
+ location = idx.to_f * dx #+ dx/2
17
+ marker_value = all_values[idx]
18
+ marker_value = options[format_key].route_format(marker_value, idx, options.merge({:all_values => all_values})) if options[format_key]
19
+
20
+ yield marker_value, location
21
+ end
22
+
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,5 @@
1
+ # module Scruffy::Helpers::MetaAttributes
2
+ # def singleton_class
3
+ # (class << self; self; end)
4
+ # end
5
+ # end
@@ -0,0 +1,99 @@
1
+ module Scruffy::Helpers
2
+
3
+ # ==Scruffy::Helpers::PointContainer
4
+ #
5
+ # Author:: Mat Schaffer
6
+ # Date:: March 22nd, 2007
7
+ #
8
+ # Allows all standard point operations to be called on both Array and Hash
9
+ module PointContainer
10
+ def self.extended point_set
11
+ point_set.extend(const_get("#{point_set.class.to_s}_ext"))
12
+ end
13
+
14
+ def sortable(values)
15
+ values.find_all { |v| v.respond_to? :<=> }
16
+ end
17
+
18
+ def summable(values)
19
+ values.find_all { |v| v.respond_to? :+ }
20
+ end
21
+
22
+ def maximum_value
23
+ sortable(values).sort.last
24
+ end
25
+
26
+ def minimum_value
27
+ sortable(values).sort.first
28
+ end
29
+
30
+ def sum
31
+ summable(values).inject(0) { |sum, i| sum += i }
32
+ end
33
+
34
+ def inject_with_index memo
35
+ index = 0
36
+ inject(memo) do |memo, item|
37
+ ret = yield memo, item, index
38
+ index = index.succ
39
+ ret
40
+ end
41
+ end
42
+
43
+ def minimum_key
44
+ sortable(keys).sort.first
45
+ end
46
+
47
+ def maximum_key
48
+ sortable(keys).sort.last
49
+ end
50
+
51
+ module Array_ext
52
+ def values
53
+ return self unless is_coordinate_list?
54
+ collect { |x,y| y}
55
+ end
56
+
57
+ def keys
58
+ return [0,size-1] unless is_coordinate_list?
59
+ collect { |x,y| x}
60
+ end
61
+
62
+ def is_coordinate_list?
63
+ if any? && first.is_a?(Array) && first.size == 2
64
+ return true
65
+ end
66
+ return false
67
+ end
68
+
69
+ def each_point(&block)
70
+ if is_coordinate_list?
71
+ each{|x,y|block.call(x,y)}
72
+ else
73
+ size.times{|k|block.call(k,self[k])}
74
+ end
75
+ end
76
+ end
77
+
78
+ module Hash_ext
79
+ def is_coordinate_list?
80
+ true
81
+ end
82
+
83
+ def each_point(&block)
84
+ keys.sort.each{|k|block.call(k,self[k])}
85
+ end
86
+
87
+ def inject memo
88
+ keys.sort.each do |k|
89
+ memo = yield memo, self[k]
90
+ end
91
+ memo
92
+ end
93
+
94
+ def size
95
+ maximum_key - minimum_key + 1
96
+ end
97
+ end
98
+ end
99
+ end