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,32 @@
1
+ module Scruffy
2
+ module Components
3
+ class ValueMarkers < Base
4
+ attr_accessor :markers
5
+
6
+ def draw(svg, bounds, options={})
7
+ markers = (options[:markers] || self.markers) || 5
8
+ all_values = []
9
+
10
+ (0...markers).each do |idx|
11
+ marker = ((1 / (markers - 1).to_f) * idx) * bounds[:height]
12
+ all_values << (options[:max_value] - options[:min_value]) * ((1 / (markers - 1).to_f) * idx) + options[:min_value]
13
+ end
14
+
15
+ (0...markers).each do |idx|
16
+ marker = ((1 / (markers - 1).to_f) * idx) * bounds[:height]
17
+ marker_value = (options[:max_value] - options[:min_value]) * ((1 / (markers - 1).to_f) * idx) + options[:min_value]
18
+ marker_value = options[:value_formatter].route_format(marker_value, idx, options.merge({:all_values => all_values})) if options[:value_formatter]
19
+
20
+ svg.text( marker_value.to_s,
21
+ :x => bounds[:width],
22
+ :y => (bounds[:height] - marker),
23
+ 'font-size' => relative(8),
24
+ 'font-family' => options[:theme].font_family,
25
+ :fill => ((options.delete(:marker_color_override) || options[:theme].marker) || 'white').to_s,
26
+ 'text-anchor' => 'end')
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,37 @@
1
+ module Scruffy::Components
2
+ # Component used to limit other visual components to a certain area on the graph.
3
+ class Viewport < Base
4
+ include Scruffy::Helpers::Canvas
5
+
6
+ def initialize(*args, &block)
7
+ super(*args)
8
+
9
+ self.components = []
10
+ block.call(self.components) if block
11
+ end
12
+
13
+ def draw(svg, bounds, options={})
14
+ svg.g(options_for) {
15
+ self.components.each do |component|
16
+ component.render(svg,
17
+ bounds_for([bounds[:width], bounds[:height]],
18
+ component.position,
19
+ component.size),
20
+ options)
21
+ end
22
+ }
23
+ end
24
+
25
+ private
26
+ def options_for
27
+ options = {}
28
+ %w(skewX skewY).each do |option|
29
+ if @options[option.to_sym]
30
+ options[:transform] ||= ''
31
+ options[:transform] = options[:transform] + "#{option.to_s}(#{@options[option.to_sym]})"
32
+ end
33
+ end
34
+ options
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,213 @@
1
+ # ===Scruffy Formatters
2
+ #
3
+ # Author:: Brasten Sager
4
+ # Date:: August 16th, 2006
5
+ #
6
+ # Formatters are used to format the values displayed on the y-axis by
7
+ # setting graph.value_formatter.
8
+ #
9
+ # Example:
10
+ #
11
+ # graph.value_formatter = Scruffy::Formatters::Currency.new(:precision => 0)
12
+ #
13
+ module Scruffy::Formatters
14
+
15
+ # == Scruffy::Formatters::Base
16
+ #
17
+ # Author:: Brasten Sager
18
+ # Date:: August 16th, 2006
19
+ #
20
+ # Formatters are used to format the values displayed on the y-axis by
21
+ # setting graph.value_formatter.
22
+ class Base
23
+
24
+ # Called by the value marker component. Routes the format call
25
+ # to one of a couple possible methods.
26
+ #
27
+ # If the formatter defines a #format method, the returned value is used
28
+ # as the value. If the formatter defines a #format! method, the value passed is
29
+ # expected to be modified, and is used as the value. (This may not actually work,
30
+ # in hindsight.)
31
+ def route_format(target, idx, options = {})
32
+ args = [target, idx, options]
33
+ if respond_to?(:format)
34
+ send :format, *args[0...self.method(:format).arity]
35
+ elsif respond_to?(:format!)
36
+ send :format!, *args[0...self.method(:format!).arity]
37
+ target
38
+ else
39
+ raise NameError, "Formatter subclass must container either a format() method or format!() method."
40
+ end
41
+ end
42
+
43
+ protected
44
+ def number_with_precision(number, precision=3) #:nodoc:
45
+ sprintf("%01.#{precision}f", number)
46
+ end
47
+ end
48
+
49
+ # Allows you to pass in a Proc for use as a formatter.
50
+ #
51
+ # Use:
52
+ #
53
+ # graph.value_formatter = Scruffy::Formatters::Custom.new { |value, idx, options| "Displays Returned Value" }
54
+ class Custom < Base
55
+ attr_reader :proc
56
+
57
+ def initialize(&block)
58
+ @proc = block
59
+ end
60
+
61
+ def format(target, idx, options)
62
+ proc.call(target, idx, options)
63
+ end
64
+ end
65
+
66
+
67
+
68
+ # Default number formatter.
69
+ # Limits precision, beautifies numbers.
70
+ class Number < Base
71
+ attr_accessor :precision, :separator, :delimiter, :precision_limit
72
+
73
+ # Returns a new Number formatter.
74
+ #
75
+ # Options:
76
+ # precision:: precision to use for value. Can be set to an integer, :none or :auto.
77
+ # :auto will use whatever precision is necessary to portray all the numerical
78
+ # information, up to :precision_limit.
79
+ #
80
+ # Example: [100.1, 100.44, 200.323] will result in [100.100, 100.440, 200.323]
81
+ #
82
+ # separator:: decimal separator. Defaults to '.'
83
+ # delimiter:: delimiter character. Defaults to ','
84
+ # precision_limit:: upper limit for auto precision. (Ignored if roundup is specified)
85
+ # roundup:: round up the number to the given interval
86
+ def initialize(options = {})
87
+ @precision = options[:precision] || :none
88
+ @roundup = options[:roundup] || :none
89
+ @separator = options[:separator] || '.'
90
+ @delimiter = options[:delimiter] || ','
91
+ @precision_limit = options[:precision_limit] || 4
92
+ end
93
+
94
+ # Formats the value.
95
+ def format(target, idx, options)
96
+ my_precision = @precision
97
+
98
+ if @precision == :auto
99
+ my_precision = options[:all_values].inject(0) do |highest, current|
100
+ cur = current.to_f.to_s.split(".").last.size
101
+ cur > highest ? cur : highest
102
+ end
103
+
104
+ my_precision = @precision_limit if my_precision > @precision_limit
105
+ elsif @precision == :none
106
+ my_precision = 0
107
+ end
108
+
109
+ my_separator = @separator
110
+ my_separator = "" unless my_precision > 0
111
+ begin
112
+ number = ""
113
+
114
+ if @roundup == :none
115
+ parts = number_with_precision(target, my_precision).split('.')
116
+ number = parts[0].to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{@delimiter}") + my_separator + parts[1].to_s
117
+ else
118
+ number = roundup(target.to_f, @roundup).to_i.to_s
119
+ end
120
+
121
+ number
122
+ rescue StandardError => e
123
+ target
124
+ end
125
+ end
126
+
127
+
128
+ def roundup(target, nearest=100)
129
+ target % nearest == 0 ? target : target + nearest - (target % nearest)
130
+ end
131
+ def rounddown(target, nearest=100)
132
+ target % nearest == 0 ? target : target - (target % nearest)
133
+ end
134
+ end
135
+
136
+
137
+
138
+ # Currency formatter.
139
+ #
140
+ # Provides formatting for currencies.
141
+ class Currency < Base
142
+
143
+ # Returns a new Currency class.
144
+ #
145
+ # Options:
146
+ # precision:: precision of value
147
+ # unit:: Defaults to '$'
148
+ # separator:: Defaults to '.'
149
+ # delimiter:: Defaults to ','
150
+ # negative_color:: Color of value marker for negative values. Defaults to 'red'
151
+ # special_negatives:: If set to true, parenthesizes negative numbers. ie: -$150.50 becomes ($150.50).
152
+ # Defaults to false.
153
+ def initialize(options = {})
154
+ @precision = options[:precision] || 2
155
+ @unit = options[:unit] || '$'
156
+ @separator = options[:separator] || '.'
157
+ @delimiter = options[:delimiter] || ','
158
+ @negative_color = options[:negative_color] || 'red'
159
+ @special_negatives = options[:special_negatives] || false
160
+ end
161
+
162
+ # Formats value marker.
163
+ def format(target, idx, options)
164
+ @separator = "" unless @precision > 0
165
+ begin
166
+ parts = number_with_precision(target, @precision).split('.')
167
+ if @special_negatives && (target.to_f < 0)
168
+ number = "(" + @unit + parts[0].to_i.abs.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{@delimiter}") + @separator + parts[1].to_s + ")"
169
+ else
170
+ number = @unit + parts[0].to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{@delimiter}") + @separator + parts[1].to_s
171
+ end
172
+ if (target.to_f < 0) && @negative_color
173
+ options[:marker_color_override] = @negative_color
174
+ end
175
+ number
176
+ rescue
177
+ target
178
+ end
179
+ end
180
+ end
181
+
182
+ # Percentage formatter.
183
+ #
184
+ # Provides formatting for percentages.
185
+ class Percentage < Base
186
+
187
+ # Returns new Percentage formatter.
188
+ #
189
+ # Options:
190
+ # precision:: Defaults to 3.
191
+ # separator:: Defaults to '.'
192
+ def initialize(options = {})
193
+ @precision = options[:precision] || 3
194
+ @separator = options[:separator] || '.'
195
+ end
196
+
197
+ # Formats percentages.
198
+ def format(target)
199
+ begin
200
+ number = number_with_precision(target, @precision)
201
+ parts = number.split('.')
202
+ if parts.at(1).nil?
203
+ parts[0] + "%"
204
+ else
205
+ parts[0] + @separator + parts[1].to_s + "%"
206
+ end
207
+ rescue
208
+ target
209
+ end
210
+ end
211
+ end
212
+
213
+ end
@@ -0,0 +1,190 @@
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, :theme, :default_type,
83
+ :point_markers, :value_formatter, :rasterizer
84
+
85
+ def_delegators :internal_state, :title=, :theme=, :default_type=,
86
+ :point_markers=, :value_formatter=, :rasterizer=
87
+
88
+ attr_reader :renderer # Writer defined below
89
+
90
+ # Returns a new Graph. You can optionally pass in a default graph type and an options hash.
91
+ #
92
+ # Graph.new # New graph
93
+ # Graph.new(:line) # New graph with default graph type of Line
94
+ # Graph.new({...}) # New graph with options.
95
+ #
96
+ # Options:
97
+ #
98
+ # title:: Graph's title
99
+ # theme:: A theme object to use when rendering graph
100
+ # layers:: An array of Layers for this graph to use
101
+ # default_type:: A symbol indicating the default type of Layer for this graph
102
+ # value_formatter:: Sets a formatter used to modify marker values prior to rendering
103
+ # point_markers:: Sets the x-axis marker values
104
+ # rasterizer:: Sets the rasterizer to use when rendering to an image format. Defaults to RMagick.
105
+ def initialize(*args)
106
+ self.default_type = args.shift if args.first.is_a?(Symbol)
107
+ options = args.shift.dup if args.first.is_a?(Hash)
108
+ raise ArgumentError, "The arguments provided are not supported." if args.size > 0
109
+
110
+ options ||= {}
111
+ self.theme = Scruffy::Themes::Standard.new
112
+ self.renderer = Scruffy::Renderers::Standard.new
113
+ self.rasterizer = Scruffy::Rasterizers::RMagickRasterizer.new
114
+ self.value_formatter = Scruffy::Formatters::Number.new
115
+
116
+ %w(title theme layers default_type value_formatter point_markers rasterizer).each do |arg|
117
+ self.send("#{arg}=".to_sym, options.delete(arg.to_sym)) unless options[arg.to_sym].nil?
118
+ end
119
+
120
+ raise ArgumentError, "Some options provided are not supported: #{options.keys.join(' ')}." if options.size > 0
121
+ end
122
+
123
+ # Renders the graph in it's current state to an SVG object.
124
+ #
125
+ # Options:
126
+ # size:: An array indicating the size you wish to render the graph. ( [x, y] )
127
+ # width:: The width of the rendered graph. A height is calculated at 3/4th of the width.
128
+ # theme:: Theme used to render graph for this render only.
129
+ # min_value:: Overrides the calculated minimum value used for the graph.
130
+ # max_value:: Overrides the calculated maximum value used for the graph.
131
+ # renderer:: Provide a Renderer object to use instead of the default.
132
+ #
133
+ # For other image formats:
134
+ # as:: File format to render to ('PNG', 'JPG', etc)
135
+ # to:: Name of file to save graph to, if desired. If not provided, image is returned as blob/string.
136
+ def render(options = {})
137
+ options[:theme] ||= theme
138
+ options[:value_formatter] ||= value_formatter
139
+ options[:point_markers] ||= point_markers
140
+ options[:size] ||= (options[:width] ? [options[:width], (options.delete(:width) * 0.6).to_i] : [600, 360])
141
+ options[:title] ||= title
142
+ options[:layers] ||= layers
143
+ options[:min_value] ||= bottom_value(:padded)
144
+ options[:max_value] ||= top_value
145
+ options[:graph] ||= self
146
+
147
+
148
+ # Removed for now.
149
+ # Added for making smaller fonts more legible, but may not be needed after all.
150
+ #
151
+ # if options[:as] && (options[:size][0] <= 300 || options[:size][1] <= 200)
152
+ # options[:actual_size] = options[:size]
153
+ # options[:size] = [800, (800.to_f * (options[:actual_size][1].to_f / options[:actual_size][0].to_f))]
154
+ # end
155
+
156
+ svg = ( options[:renderer].nil? ? self.renderer.render( options ) : options[:renderer].render( options ) )
157
+
158
+ # SVG to file.
159
+ if options[:to] && options[:as].nil?
160
+ File.open(options[:to], 'w') { |file|
161
+ file.write(svg)
162
+ }
163
+ end
164
+
165
+ options[:as] ? rasterizer.rasterize(svg, options) : svg
166
+ end
167
+
168
+ def renderer=(val)
169
+ raise ArgumentError, "Renderer must include a #render(options) method." unless (val.respond_to?(:render) && val.method(:render).arity.abs > 0)
170
+
171
+ @renderer = val
172
+ end
173
+
174
+ alias :layout :renderer
175
+
176
+ def component(id)
177
+ renderer.component(id)
178
+ end
179
+
180
+ def remove(id)
181
+ renderer.remove(id)
182
+ end
183
+
184
+ private
185
+ def internal_state
186
+ @internal_state ||= GraphState.new
187
+ end
188
+
189
+ end
190
+ end