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/CHANGES CHANGED
@@ -1,5 +1,42 @@
1
1
  = Scruffy Changelog
2
2
 
3
+ == Version 0.2.0
4
+ (August 14th, 2006)
5
+
6
+ - Lots of changes, hold on tight:
7
+
8
+ * Redesigned rendering system to a component-based design.
9
+ All objects on the canvas are components that can be re-arranged via renderers.
10
+ * Created default renderer for basic Gruff-like layout.
11
+ * Added Reversed and Cubed renderers to demonstrate the customization abilities (plus, they're cool).
12
+ * Added Split renderer.
13
+ * Created Viewport component to help with Cubed.
14
+ - Viewport lets you scale it's inner components and move around the
15
+ graph. Its components' sizes and positions are relative to the viewport,
16
+ not the graph.
17
+ * Set title to respect marker color if available.
18
+ * Respects :to option in Graph#render for SVG output to file.
19
+ * Stacked layer type -- accepts layers which it then uses to create a stacked graph. Such as Bar graphs
20
+ and Area graphs.
21
+ * Abstracted out layer_container functionality to helper module (for stacked graph)
22
+ * Renamed value_transformers to value_formatters.
23
+ * Refined Value Formatters.
24
+ - Created default: Number.
25
+ - Respects float precision
26
+ - Allows for "auto-precision", which will use the largest precision (up to a customizable limit)
27
+ necessary to portray the values correctly. ie: 5.1, 6.32, 7.142 becomes '5.100', '6.320', '7.142'
28
+ * Modified Legend component, Layers, and Graph component to respect categories.
29
+ - ie: Creating a Bar layer with :category => :sales and a Graph with :category => :qa will result in
30
+ the Bay layer not being displayed. Allows for more than one Graph viewport on a screen with different
31
+ layers.
32
+ * Improved rasterizing at smaller sizes( < 300px) by rasterizing the image at a larger size first, then
33
+ allowing RMagick to resize the image with specific filtering/blurring. Actually looks better than just
34
+ rasterizing the SVG at the small size from the beginning.
35
+ * Fixed Opacity on stacked graphs.
36
+ * Added Style (invisible) components to allow for CSS styling. (Not recommended, however.)
37
+ * Added Label component for arbitrary text.
38
+ * Created Theme object in place of theme hash.
39
+
3
40
  == Version 0.1.0
4
41
  (August 11th, 2006)
5
42
 
@@ -7,8 +44,6 @@
7
44
  * Legend rendering
8
45
  * Rasterizing graph to multiple image types (graph.render :as => 'PNG')
9
46
 
10
-
11
-
12
47
  == Version 0.0.12
13
48
  (August 10th, 2006)
14
49
  This is not a public release.
data/lib/scruffy.rb CHANGED
@@ -4,14 +4,12 @@ $:.unshift(File.dirname(__FILE__)) unless
4
4
  require 'rubygems'
5
5
  require_gem 'builder', '>= 2.0'
6
6
 
7
- # Base files
7
+ require 'scruffy/helpers'
8
8
  require 'scruffy/graph'
9
9
  require 'scruffy/themes'
10
10
  require 'scruffy/version'
11
- require 'scruffy/transformer'
11
+ require 'scruffy/formatters'
12
12
  require 'scruffy/rasterizers'
13
- require 'scruffy/resolver'
14
13
  require 'scruffy/layers'
15
-
16
- # Renderers
17
- require 'scruffy/renderers/standard'
14
+ require 'scruffy/components'
15
+ require 'scruffy/renderers'
@@ -0,0 +1,11 @@
1
+ require 'scruffy/components/base'
2
+ require 'scruffy/components/title'
3
+ require 'scruffy/components/background'
4
+ require 'scruffy/components/graphs'
5
+ require 'scruffy/components/grid'
6
+ require 'scruffy/components/value_markers'
7
+ require 'scruffy/components/data_markers'
8
+ require 'scruffy/components/legend'
9
+ require 'scruffy/components/style_info'
10
+ require 'scruffy/components/viewport'
11
+ require 'scruffy/components/label'
@@ -0,0 +1,24 @@
1
+ module Scruffy
2
+ module Components
3
+ class Background < Base
4
+ def draw(svg, bounds, options={})
5
+ fill = "#EEEEEE"
6
+ case options[:theme].background
7
+ when Symbol, String
8
+ fill = options[:theme].background.to_s
9
+ when Array
10
+ fill = "url(#BackgroundGradient)"
11
+ svg.defs {
12
+ svg.linearGradient(:id=>'BackgroundGradient', :x1 => '0%', :y1 => '0%', :x2 => '0%', :y2 => '100%') {
13
+ svg.stop(:offset => '5%', 'stop-color' => options[:theme].background[0])
14
+ svg.stop(:offset => '95%', 'stop-color' => options[:theme].background[1])
15
+ }
16
+ }
17
+ end
18
+
19
+ # Render background (maybe)
20
+ svg.rect(:width => bounds[:width], :height => bounds[:height], :x => "0", :y => "0", :fill => fill) unless fill.nil?
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,46 @@
1
+ module Scruffy
2
+ module Components
3
+ class Base
4
+ attr_reader :id
5
+
6
+ # In terms of percentages: [10, 10] == 10% by 10%
7
+ attr_reader :position
8
+ attr_reader :size
9
+ attr_reader :options
10
+
11
+ def initialize(id, options = {})
12
+ @id = id.to_sym
13
+ @position = options[:position]
14
+ @size = options[:size]
15
+ @options = options
16
+ end
17
+
18
+ def render(svg, bounds, options={})
19
+ unless bounds.nil?
20
+ @render_height = bounds[:height]
21
+
22
+ svg.g(:id => id.to_s,
23
+ :transform => "translate(#{bounds.delete(:x)}, #{bounds.delete(:y)})") {
24
+
25
+ draw(svg, bounds, options.merge(@options))
26
+ }
27
+ else
28
+ process(svg, options.merge(@options))
29
+ end
30
+ end
31
+
32
+ def draw(svg, bounds, options={})
33
+ # Override this if visual component
34
+ end
35
+
36
+ def process(svg, options={})
37
+ # Override this NOT a visual component
38
+ end
39
+
40
+ protected
41
+ def relative(pct)
42
+ @render_height * ( pct / 100.to_f )
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,25 @@
1
+ module Scruffy
2
+ module Components
3
+
4
+ class DataMarkers < Base
5
+
6
+ def draw(svg, bounds, options={})
7
+ unless options[:point_markers].nil?
8
+ point_distance = bounds[:width] / (options[:point_markers].size - 1).to_f
9
+
10
+ (0...options[:point_markers].size).map do |idx|
11
+ x_coord = point_distance * idx
12
+ svg.text(options[:point_markers][idx], :x => x_coord, :y => bounds[:height],
13
+ 'font-size' => relative(90),
14
+ :fill => (options[:theme].marker || 'white').to_s,
15
+ 'text-anchor' => 'middle',
16
+ 'font-family' => options[:theme].font_family,
17
+ 'text-rendering' => 'optimizeLegibility') unless options[:point_markers][idx].nil?
18
+ end
19
+ end
20
+ end # draw
21
+
22
+ end # class
23
+
24
+ end
25
+ end
@@ -0,0 +1,48 @@
1
+ module Scruffy
2
+ module Components
3
+
4
+ # Component for displaying Graphs layers.
5
+ #
6
+ # Is passed all graph layers from the Graph object.
7
+ #
8
+ # (This may change as the capability for Graph filtering and such fills out.)
9
+ class Graphs < Base
10
+ STACKED_OPACITY = 0.85;
11
+
12
+ def draw(svg, bounds, options={})
13
+ # If Graph is limited to a category, reject layers outside of it's scope.
14
+ applicable_layers = options[:layers].reject do |l|
15
+ if @options[:only]
16
+ (l.options[:category].nil? && l.options[:categories].nil?) ||
17
+ (!l.options[:category].nil? && l.options[:category] != @options[:only]) ||
18
+ (!l.options[:categories].nil? && !l.options[:categories].include?(@options[:only]))
19
+ else
20
+ false
21
+ end
22
+ end
23
+
24
+ applicable_layers.each_with_index do |layer, idx|
25
+ layer_options = {}
26
+ layer_options[:index] = idx
27
+ layer_options[:min_value] = options[:min_value]
28
+ layer_options[:max_value] = options[:max_value]
29
+ layer_options[:complexity] = options[:complexity]
30
+ layer_options[:size] = [bounds[:width], bounds[:height]]
31
+ layer_options[:color] = layer.preferred_color || layer.color || options[:theme].next_color
32
+ layer_options[:opacity] = opacity_for(idx)
33
+ layer_options[:theme] = options[:theme]
34
+
35
+ svg.g(:id => "component_#{id}_graph_#{idx}", :class => 'graph_layer') {
36
+ layer.render(svg, layer_options)
37
+ }
38
+ end # applicable_layers
39
+ end # draw
40
+
41
+ protected
42
+ def opacity_for(idx)
43
+ (idx == 0) ? 1.0 : (@options[:stacked_opacity] || STACKED_OPACITY)
44
+ end
45
+
46
+ end #class
47
+ end
48
+ end
@@ -0,0 +1,14 @@
1
+ module Scruffy
2
+ module Components
3
+ class Grid < Base
4
+ def draw(svg, bounds, options={})
5
+ markers = options[:markers] || 5
6
+
7
+ (0...markers).each do |idx|
8
+ marker = ((1 / (markers - 1).to_f) * idx) * bounds[:height]
9
+ svg.line(:x1 => 0, :y1 => marker, :x2 => bounds[:width], :y2 => marker, :style => "stroke: #{options[:theme].marker.to_s}; stroke-width: 2;")
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ module Scruffy
2
+ module Components
3
+ class Label < Base
4
+ def draw(svg, bounds, options={})
5
+ svg.text(@options[:text], :class => 'text', :x => (bounds[:width] / 2), :y => bounds[:height],
6
+ 'font-size' => relative(100), :fill => options[:theme].marker, :stroke => 'none', 'stroke-width' => '0',
7
+ 'text-anchor' => (@options[:text_anchor] || 'middle'),
8
+ 'font-family' => options[:theme].font_family, 'text-rendering' => 'optimizeLegibility')
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,64 @@
1
+ module Scruffy::Components
2
+
3
+ class Legend < Base
4
+ def draw(svg, bounds, options={})
5
+ legend_info = relevant_legend_info(options[:layers])
6
+ active_width, points = layout(legend_info)
7
+
8
+ offset = (bounds[:width] - active_width) / 2 # Nudge over a bit for true centering
9
+
10
+ # Render Legend
11
+ points.each_with_index do |point, idx|
12
+
13
+ svg.rect( :x => offset + point,
14
+ :y => relative(25),
15
+ :width => relative(60),
16
+ :height => relative(50),
17
+ :fill => legend_info[idx][:color])
18
+
19
+ svg.text( legend_info[idx][:title],
20
+ :x => offset + point + relative(100),
21
+ :y => relative(80),
22
+ 'font-size' => relative(80),
23
+ :fill => (options[:theme].marker || 'white'),
24
+ 'font-family' => options[:theme].font_family,
25
+ 'text-rendering' => 'optimizeLegibility')
26
+ end
27
+ end # draw
28
+
29
+ protected
30
+ # Collects Legend Info from the provided Layers.
31
+ #
32
+ # Automatically filters by legend's categories.
33
+ def relevant_legend_info(layers, categories=(@options[:category] ? [@options[:category]] : @options[:categories]))
34
+ legend_info = layers.inject([]) do |arr, layer|
35
+ if categories.nil? ||
36
+ (categories.include?(layer.options[:category]) ||
37
+ (layer.options[:categories] && (categories & layer.options[:categories]).size > 0) )
38
+
39
+ data = layer.legend_data
40
+ arr << data if data.is_a?(Hash)
41
+ arr = arr + data if data.is_a?(Array)
42
+ end
43
+ arr
44
+ end
45
+ end # relevant_legend_info
46
+
47
+ # Returns an array consisting of the total width needed by the legend information,
48
+ # as well as an array of x-coords for each element.
49
+ #
50
+ # ie: [200, [0, 50, 100, 150]]
51
+ def layout(legend_info_array)
52
+ legend_info_array.inject([0, []]) do |enum, elem|
53
+ enum[0] += (relative(50) * 2) if enum.first != 0 # Add spacer between elements
54
+ enum[1] << enum.first # Add location to points
55
+ enum[0] += relative(50) # Add room for color box
56
+ enum[0] += (relative(50) * elem[:title].length) # Add room for text
57
+
58
+ [enum.first, enum.last]
59
+ end
60
+ end
61
+
62
+ end # class Legend
63
+
64
+ end # Scruffy::Components
@@ -0,0 +1,17 @@
1
+ module Scruffy
2
+ module Components
3
+ # Component used for adding CSS styling to SVG graphs.
4
+ #
5
+ # In hindsight, ImageMagick and Mozilla SVG's handling of CSS styling is
6
+ # extremely inconsistant, so use this at your own risk.
7
+ class StyleInfo < Base
8
+ def process(svg, options={})
9
+ svg.defs {
10
+ svg.style(:type => "text/css") {
11
+ svg.cdata!("\n#{options[:selector]} {\n #{options[:style]}\n}\n")
12
+ }
13
+ }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ module Scruffy
2
+ module Components
3
+ class Title < Base
4
+ def draw(svg, bounds, options={})
5
+ svg.text(options[:title], :class => 'title', :x => (bounds[:width] / 2), :y => bounds[:height],
6
+ 'font-size' => relative(100), :fill => options[:theme].marker, :stroke => 'none', 'stroke-width' => '0',
7
+ 'text-anchor' => (@options[:text_anchor] || 'middle'),
8
+ 'font-family' => options[:theme].font_family, 'text-rendering' => 'optimizeLegibility')
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,31 @@
1
+ module Scruffy
2
+ module Components
3
+ class ValueMarkers < Base
4
+ def draw(svg, bounds, options={})
5
+ markers = options[:markers] || 5
6
+ all_values = []
7
+
8
+ (0...markers).each do |idx|
9
+ marker = ((1 / (markers - 1).to_f) * idx) * bounds[:height]
10
+ all_values << (options[:max_value] - options[:min_value]) * ((1 / (markers - 1).to_f) * idx) + options[:min_value]
11
+ end
12
+
13
+ (0...markers).each do |idx|
14
+ marker = ((1 / (markers - 1).to_f) * idx) * bounds[:height]
15
+ marker_value = (options[:max_value] - options[:min_value]) * ((1 / (markers - 1).to_f) * idx) + options[:min_value]
16
+ marker_value = options[:value_formatter].route_format(marker_value, idx, options.merge({:all_values => all_values})) if options[:value_formatter]
17
+
18
+ svg.text( marker_value.to_s,
19
+ :x => bounds[:width],
20
+ :y => (bounds[:height] - marker),
21
+ 'font-size' => relative(10),
22
+ :fill => ((options.delete(:marker_color_override) || options[:theme].marker) || 'white').to_s,
23
+ 'text-anchor' => 'end',
24
+ 'font-family' => options[:theme].font_family,
25
+ 'text-rendering' => 'optimizeLegibility')
26
+ end
27
+
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,30 @@
1
+ module Scruffy
2
+ module Components
3
+ # Component used to limit other visual components to a certain area on the graph.
4
+ class Viewport < Base
5
+ include Scruffy::Helpers::Canvas
6
+
7
+ attr_accessor :components
8
+
9
+ def initialize(*args, &block)
10
+ super(*args)
11
+
12
+ self.components = []
13
+ if block
14
+ block.call(self.components)
15
+ end
16
+ end
17
+
18
+ def draw(svg, bounds, options={})
19
+ self.components.each do |component|
20
+ component.render(svg,
21
+ bounds_for( [bounds[:width], bounds[:height]],
22
+ component.position,
23
+ component.size ),
24
+ options)
25
+ end
26
+
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,111 @@
1
+ module Scruffy
2
+ module Formatters
3
+ class Base
4
+ def route_format(target, idx, options = {})
5
+ args = [target, idx, options]
6
+ if respond_to?(:format)
7
+ send :format, *args[0...self.method(:format).arity]
8
+ elsif respond_to?(:format!)
9
+ send :format!, *args[0...self.method(:format!).arity]
10
+ target
11
+ else
12
+ raise NameError, "Formatter subclass must container either a format() method or format!() method."
13
+ end
14
+ end
15
+
16
+ protected
17
+ def number_with_precision(number, precision=3)
18
+ sprintf("%01.#{precision}f", number)
19
+ end
20
+ end
21
+
22
+ # Default number formatter. Limits precision, beautifies numbers.
23
+ class Number < Base
24
+ attr_accessor :precision, :separator, :delimiter, :precision_limit
25
+
26
+ def initialize(options = {})
27
+ @precision = options[:precision] || :none
28
+ @separator = options[:separator] || '.'
29
+ @delimiter = options[:delimiter] || ','
30
+ @precision_limit = options[:precision_limit] || 4
31
+ end
32
+
33
+ def format(target, idx, options)
34
+ my_precision = @precision
35
+
36
+ if @precision == :auto
37
+ my_precision = options[:all_values].inject(0) do |highest, current|
38
+ cur = current.to_f.to_s.split(".").last.size
39
+ cur > highest ? cur : highest
40
+ end
41
+
42
+ my_precision = @precision_limit if my_precision > @precision_limit
43
+ elsif @precision == :none
44
+ my_precision = 0
45
+ end
46
+
47
+ my_separator = @separator
48
+ my_separator = "" unless my_precision > 0
49
+ begin
50
+ parts = number_with_precision(target, my_precision).split('.')
51
+
52
+ number = parts[0].to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{@delimiter}") + my_separator + parts[1].to_s
53
+ number
54
+ rescue StandardError => e
55
+ target
56
+ end
57
+ end
58
+ end
59
+
60
+ class Currency < Base
61
+ def initialize(options = {})
62
+ @precision = options[:precision] || 2
63
+ @unit = options[:unit] || '$'
64
+ @separator = options[:separator] || '.'
65
+ @delimiter = options[:delimiter] || ','
66
+ @negative_color = options[:negative_color] || 'red'
67
+ @special_negatives = options[:special_negatives] || false
68
+ end
69
+
70
+ def format(target, idx, options)
71
+ @separator = "" unless @precision > 0
72
+ begin
73
+ parts = number_with_precision(target, @precision).split('.')
74
+ if @special_negatives && (target.to_f < 0)
75
+ number = "(" + @unit + parts[0].to_i.abs.to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{@delimiter}") + @separator + parts[1].to_s + ")"
76
+ else
77
+ number = @unit + parts[0].to_s.gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{@delimiter}") + @separator + parts[1].to_s
78
+ end
79
+ if (target.to_f < 0) && @negative_color
80
+ options[:marker_color_override] = @negative_color
81
+ end
82
+ number
83
+ rescue
84
+ target
85
+ end
86
+ end
87
+ end
88
+
89
+ class Percentage < Base
90
+ def initialize(options = {})
91
+ @precision = options[:precision] || 3
92
+ @separator = options[:separator] || '.'
93
+ end
94
+
95
+ def format(target)
96
+ begin
97
+ number = number_with_precision(target, @precision)
98
+ parts = number.split('.')
99
+ if parts.at(1).nil?
100
+ parts[0] + "%"
101
+ else
102
+ parts[0] + @separator + parts[1].to_s + "%"
103
+ end
104
+ rescue
105
+ target
106
+ end
107
+ end
108
+ end
109
+
110
+ end
111
+ end