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