scruffy 0.2.2 → 0.2.3

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 (84) hide show
  1. data/CHANGES +8 -0
  2. data/History.txt +104 -0
  3. data/License.txt +20 -0
  4. data/Manifest.txt +105 -0
  5. data/PostInstall.txt +6 -0
  6. data/README.txt +66 -0
  7. data/Rakefile +108 -104
  8. data/config/hoe.rb +78 -0
  9. data/config/requirements.rb +15 -0
  10. data/lib/scruffy.rb +8 -6
  11. data/lib/scruffy/components/base.rb +4 -0
  12. data/lib/scruffy/components/legend.rb +65 -30
  13. data/lib/scruffy/components/viewport.rb +14 -17
  14. data/lib/scruffy/formatters.rb +1 -1
  15. data/lib/scruffy/graph.rb +18 -7
  16. data/lib/scruffy/graph_state.rb +24 -0
  17. data/lib/scruffy/helpers.rb +2 -1
  18. data/lib/scruffy/helpers/canvas.rb +19 -17
  19. data/lib/scruffy/helpers/layer_container.rb +8 -3
  20. data/lib/scruffy/helpers/meta.rb +5 -5
  21. data/lib/scruffy/helpers/point_container.rb +70 -0
  22. data/lib/scruffy/layers.rb +2 -0
  23. data/lib/scruffy/layers/average.rb +6 -1
  24. data/lib/scruffy/layers/bar.rb +1 -0
  25. data/lib/scruffy/layers/base.rb +38 -14
  26. data/lib/scruffy/layers/pie.rb +123 -0
  27. data/lib/scruffy/layers/pie_slice.rb +114 -0
  28. data/lib/scruffy/layers/scatter.rb +21 -0
  29. data/lib/scruffy/layers/sparkline_bar.rb +1 -0
  30. data/lib/scruffy/layers/stacked.rb +2 -5
  31. data/lib/scruffy/rasterizers/rmagick_rasterizer.rb +1 -2
  32. data/lib/scruffy/renderers.rb +2 -1
  33. data/lib/scruffy/renderers/base.rb +6 -4
  34. data/lib/scruffy/renderers/pie.rb +20 -0
  35. data/lib/scruffy/themes.rb +54 -4
  36. data/lib/scruffy/version.rb +8 -2
  37. data/script/console +10 -0
  38. data/script/destroy +14 -0
  39. data/script/generate +14 -0
  40. data/script/txt2html +82 -0
  41. data/setup.rb +1585 -0
  42. data/spec/scruffy/graph_spec.rb +175 -0
  43. data/spec/scruffy/layers/base_spec.rb +30 -0
  44. data/spec/{layers → scruffy/layers}/line_spec.rb +2 -1
  45. data/spec/spec_helper.rb +8 -0
  46. data/tasks/deployment.rake +34 -0
  47. data/tasks/environment.rake +7 -0
  48. data/tasks/website.rake +17 -0
  49. data/test/graph_creation_test.rb +101 -0
  50. data/test/test_helper.rb +2 -0
  51. data/test/test_scruffy.rb +11 -0
  52. data/website/images/blank.gif.html +7 -0
  53. data/website/images/graphs/all_smiles.png +0 -0
  54. data/website/images/graphs/bar_test.png +0 -0
  55. data/website/images/graphs/bar_test.svg +71 -0
  56. data/website/images/graphs/line_test.png +0 -0
  57. data/website/images/graphs/line_test.svg +60 -0
  58. data/website/images/graphs/multi_test.png +0 -0
  59. data/website/images/graphs/multi_test.svg +296 -0
  60. data/website/images/graphs/pie_test.png +0 -0
  61. data/website/images/graphs/pie_test.svg +40 -0
  62. data/website/images/graphs/split_test.png +0 -0
  63. data/website/images/graphs/split_test.svg +295 -0
  64. data/website/images/graphs/stacking_test.png +0 -0
  65. data/website/images/graphs/stacking_test.svg +146 -0
  66. data/website/images/header.png +0 -0
  67. data/website/images/header_gradient.png +0 -0
  68. data/website/images/overlay.png +0 -0
  69. data/website/images/scruffy.png +0 -0
  70. data/website/index.html +294 -0
  71. data/website/index.txt +199 -0
  72. data/website/javascripts/application.js +2 -0
  73. data/website/javascripts/controls.js +815 -0
  74. data/website/javascripts/dragdrop.js +913 -0
  75. data/website/javascripts/effects.js +958 -0
  76. data/website/javascripts/lightbox.js +437 -0
  77. data/website/javascripts/prototype.js +2006 -0
  78. data/website/javascripts/rounded_corners_lite.inc.js +285 -0
  79. data/website/stylesheets/lightbox.css +27 -0
  80. data/website/stylesheets/screen.css +147 -0
  81. data/website/stylesheets/scruffy.css +227 -0
  82. data/website/template.html.erb +47 -0
  83. metadata +135 -55
  84. data/spec/graph_spec.rb +0 -162
@@ -30,15 +30,20 @@ module Scruffy::Helpers
30
30
  else
31
31
  type = args.first.is_a?(Symbol) ? args.shift : @default_type
32
32
  title = args.shift if args.first.is_a?(String)
33
- points = args.first.is_a?(Array) ? args.shift : []
33
+
34
+ # Layer handles PointContainer mixin, don't do it here
35
+ points = [Array, Hash].include?(args.first.class) ? args.shift : []
34
36
  options = args.first.is_a?(Hash) ? args.shift : {}
35
37
 
36
38
  title ||= ''
37
39
 
38
40
  raise ArgumentError,
39
41
  'You must specify a graph type (:area, :bar, :line, etc) if you do not have a default type specified.' if type.nil?
40
-
41
- layer = Kernel::module_eval("Scruffy::Layers::#{to_camelcase(type.to_s)}").new(options.merge({:points => points, :title => title}), &block)
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)
42
47
  layers << layer
43
48
  end
44
49
  layer
@@ -1,5 +1,5 @@
1
- module Scruffy::MetaAttributes
2
- def singleton_class
3
- (class << self; self; end)
4
- end
5
- end
1
+ # module Scruffy::Helpers::MetaAttributes
2
+ # def singleton_class
3
+ # (class << self; self; end)
4
+ # end
5
+ # end
@@ -0,0 +1,70 @@
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))
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
+ module Array
44
+ def values
45
+ self
46
+ end
47
+ end
48
+
49
+ module Hash
50
+ def minimum_key
51
+ self.keys.sort.first
52
+ end
53
+
54
+ def maximum_key
55
+ self.keys.sort.last
56
+ end
57
+
58
+ def inject memo
59
+ (minimum_key..maximum_key).each do |i|
60
+ memo = yield memo, self[i]
61
+ end
62
+ memo
63
+ end
64
+
65
+ def size
66
+ maximum_key - minimum_key + 1
67
+ end
68
+ end
69
+ end
70
+ end
@@ -20,3 +20,5 @@ require 'scruffy/layers/bar'
20
20
  require 'scruffy/layers/line'
21
21
  require 'scruffy/layers/average'
22
22
  require 'scruffy/layers/stacked'
23
+ require 'scruffy/layers/pie'
24
+ require 'scruffy/layers/pie_slice'
@@ -12,12 +12,16 @@ module Scruffy::Layers
12
12
  # for some reason, creating a massive black line. Any help resolving this would
13
13
  # be useful.
14
14
  class Average < Base
15
+ attr_reader :layers
15
16
 
16
17
  # Returns new Average graph.
17
18
  def initialize(options = {})
18
19
  # Set self's relevant_data to false. Otherwise we get stuck in a
19
20
  # recursive loop.
20
21
  super(options.merge({:relevant_data => false}))
22
+
23
+ # The usual :points argument is actually layers for Average, name it as such
24
+ @layers = options[:points]
21
25
  end
22
26
 
23
27
  # Render average graph.
@@ -30,12 +34,13 @@ module Scruffy::Layers
30
34
  # Override default generate_coordinates method to iterate through the layers and
31
35
  # generate coordinates based on the average data points.
32
36
  def generate_coordinates(options = {})
33
- key_layer = points.find { |layer| layer.relevant_data? }
37
+ key_layer = layers.find { |layer| layer.relevant_data? }
34
38
 
35
39
  options[:point_distance] = width / (key_layer.points.size - 1).to_f
36
40
 
37
41
  coords = []
38
42
 
43
+ #TODO this will likely break with the new hash model
39
44
  key_layer.points.each_with_index do |layer, idx|
40
45
  sum, objects = points.inject([0, 0]) do |arr, elem|
41
46
  if elem.relevant_data?
@@ -38,6 +38,7 @@ module Scruffy::Layers
38
38
  @bar_width = (width / points.size) * 0.9
39
39
  options[:point_distance] = (width - (width / points.size)) / (points.size - 1).to_f
40
40
 
41
+ #TODO more array work with index, try to rework to be accepting of hashes
41
42
  coords = (0...points.size).map do |idx|
42
43
  x_coord = (options[:point_distance] * idx) + (width / points.size * 0.5)
43
44
 
@@ -2,7 +2,9 @@ module Scruffy::Layers
2
2
  # ==Scruffy::Layers::Base
3
3
  #
4
4
  # Author:: Brasten Sager
5
- # Date:: August 5th, 2006
5
+ # Extended By:: A.J. Ostman
6
+ # Created:: August 5th, 2006
7
+ # Last Modified:: August 27, 2006
6
8
  #
7
9
  # Scruffy::Layers::Base contains the basic functionality needed by the various types of graphs. The Base
8
10
  # class is responsible holding layer information such as the title and data points.
@@ -49,9 +51,11 @@ module Scruffy::Layers
49
51
  # included in any graph data aggregations, such as averaging data points.
50
52
  def initialize(options = {})
51
53
  @title = options.delete(:title) || ''
52
- @points = options.delete(:points) || []
53
54
  @preferred_color = options.delete(:preferred_color)
54
55
  @relevant_data = options.delete(:relevant_data) || true
56
+ @points = options.delete(:points) || []
57
+ @points.extend Scruffy::Helpers::PointContainer unless @points.kind_of? Scruffy::Helpers::PointContainer
58
+
55
59
  @options = options
56
60
  end
57
61
 
@@ -98,15 +102,20 @@ module Scruffy::Layers
98
102
  def relevant_data?
99
103
  @relevant_data
100
104
  end
101
-
105
+
102
106
  # The highest data point on this layer, or nil if relevant_data == false
103
107
  def top_value
104
- @relevant_data ? points.sort.last : nil
108
+ @relevant_data ? points.maximum_value : nil
105
109
  end
106
110
 
107
111
  # The lowest data point on this layer, or nil if relevant_data == false
108
112
  def bottom_value
109
- @relevant_data ? points.sort.first: nil
113
+ @relevant_data ? points.minimum_value : nil
114
+ end
115
+
116
+ # The sum of all values
117
+ def sum_values
118
+ points.sum
110
119
  end
111
120
 
112
121
  protected
@@ -125,17 +134,19 @@ module Scruffy::Layers
125
134
  # in pie charting and bar charts).
126
135
  def generate_coordinates(options = {})
127
136
  options[:point_distance] = width / (points.size - 1).to_f
128
-
129
- coords = (0...points.size).map do |idx|
130
- x_coord = options[:point_distance] * idx
131
137
 
132
- relative_percent = ((points[idx] == min_value) ? 0 : ((points[idx] - min_value) / (max_value - min_value).to_f))
133
- y_coord = (height - (height * relative_percent))
138
+ points.inject_with_index([]) do |memo, point, idx|
139
+ x_coord = options[:point_distance] * idx
134
140
 
135
- [x_coord, y_coord]
141
+ if point
142
+ relative_percent = ((point == min_value) ? 0 : ((point - min_value) / (max_value - min_value).to_f))
143
+ y_coord = (height - (height * relative_percent))
144
+
145
+ memo << [x_coord, y_coord]
146
+ end
147
+
148
+ memo
136
149
  end
137
-
138
- coords
139
150
  end
140
151
 
141
152
  # Converts a percentage into a pixel value, relative to the height.
@@ -143,8 +154,21 @@ module Scruffy::Layers
143
154
  # Example:
144
155
  # relative(5) # On a 100px high layer, this returns 5. 200px high layer, this returns 10, etc.
145
156
  def relative(pct)
146
- @height * (pct / 100.to_f)
157
+ # Default to Relative Height
158
+ relative_height(pct)
159
+ end
160
+
161
+ def relative_width(pct)
162
+ if pct # Added to handle nils
163
+ @width * (pct / 100.to_f)
164
+ end
147
165
  end
166
+
167
+ def relative_height(pct)
168
+ if pct # Added to handle nils
169
+ @height * (pct / 100.to_f)
170
+ end
171
+ end
148
172
 
149
173
  # Some SVG elements take a long string of multiple coordinates. This is here
150
174
  # to make that a little easier.
@@ -0,0 +1,123 @@
1
+ module Scruffy::Layers
2
+ # ==Scruffy::Layers::Pie
3
+ #
4
+ # Author:: A.J. Ostman
5
+ # Date:: August 15, 2006
6
+ #
7
+ # Provides a container for pie slice.
8
+ class Pie < Base
9
+ include Scruffy::Helpers::LayerContainer
10
+
11
+ # Basic Example:
12
+ #
13
+ # graph = Scruffy::Graph.new
14
+ # graph.title = "Snack Preference"
15
+ # graph.renderer = Scruffy::Renderers::Pie.new
16
+ # graph.add :pie, 'Example', {'Apples' => 90, 'Orange' => 60, 'Taco' => 30}
17
+ #
18
+ # Or, using a block to add slices:
19
+ #
20
+ # graph = Scruffy::Graph.new
21
+ # graph.title = "Snack Preference"
22
+ # graph.renderer = Scruffy::Renderers::Pie.new
23
+ # graph.add :pie do |pie|
24
+ # pie.add :pie_slice, 'Apple', [90]
25
+ # pie.add :pie_slice, 'Orange', [60]
26
+ # pie.add :pie_slice, 'Taco', [30]
27
+ # end
28
+ #
29
+ # Another Example:
30
+ # graph.title = "Scruff-Pac!"
31
+ # graph.renderer = Scruffy::Renderers::Pie.new
32
+ # graph.add :pie, :diameter => 40, :degree_offset => 30 do |pie|
33
+ # pie.add :pie_slice, '', [160], :preferred_color => "yellow", :shadow => true,
34
+ # :shadow_x => -1, :shadow_y => 1, :shadow_color=>"black", :shadow_opacity => 0.4
35
+ # pie.add :pie_slice, '', [50], :preferred_color => "green", :explode => 5, :diameter => 20,
36
+ # :shadow => true, :shadow_x => -1, :shadow_y => 1, :shadow_color => "black", :shadow_opacity => 0.4
37
+ # end
38
+ #
39
+ # graph.add :pie, :diameter => 3, :center_x => 48, :center_y=> 37, :degree_offset => 20 do |pie|
40
+ # pie.add :pie_slice, '', [160], :preferred_color => "blue", :stroke => "black"
41
+ # end
42
+
43
+
44
+ # Setup Constants
45
+ RADIANS = Math::PI/180
46
+
47
+ attr_accessor :diameter
48
+ attr_accessor :percent_used
49
+ attr_accessor :degree_offset
50
+ attr_accessor :scaler
51
+ attr_accessor :center_x, :center_y
52
+
53
+
54
+ # The initialize method passes itself to the block, and since Pie is a
55
+ # LayerContainer, layers (pie slice) can be added just as if they were being
56
+ # added to Graph.
57
+ def initialize(options = {}, &block)
58
+ super(options)
59
+
60
+ # Allow for population of data with a block during initialization.
61
+ if block
62
+ block.call(self)
63
+ else
64
+ # Otherwise, just iterate over the points, adding the slices
65
+ if @points.class == Hash
66
+ @points.keys.each {|k|
67
+ self.add :pie_slice, k.to_s, [@points[k]]}
68
+ end
69
+ if @points.class == Array
70
+ @points.each {|v|
71
+ self.add :pie_slice, '', [v]}
72
+ end
73
+ end
74
+ end
75
+
76
+
77
+ # Overrides Base#render to fiddle with layers' points to achieve a stacked
78
+ # effect.
79
+ def render(svg, options = {})
80
+ # #current_points = points.dup
81
+
82
+ @scaler = 1
83
+ total = 0
84
+
85
+ layers.each do |layer|
86
+ total += layer.sum_values
87
+ end
88
+
89
+ @scaler = 100.0 / total
90
+
91
+ @percent_used = 30
92
+
93
+ layers.each do |layer|
94
+ layer_options = options.dup
95
+ layer_options = layer_options.merge(@options)
96
+ layer_options = layer_options.merge(layer.options)
97
+ layer_options[:scaler] = @scaler
98
+ layer_options[:percent_used] = @percent_used
99
+ @percent_used += @scaler * layer.sum_values
100
+ layer_options[:color] = layer.preferred_color || layer.color || options[:theme].next_color
101
+
102
+ layer.render(svg, layer_options)
103
+ end
104
+ end
105
+
106
+ # A stacked graph has many data sets. Return legend information for all of them.
107
+ def legend_data
108
+ if relevant_data?
109
+ retval = []
110
+ layers.each do |layer|
111
+ retval << layer.legend_data
112
+ end
113
+ retval
114
+ else
115
+ nil
116
+ end
117
+ end
118
+
119
+ def points=(val)
120
+ throw ArgumentsError, "Pie layers cannot accept points, only pie slices."
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,114 @@
1
+ module Scruffy::Layers
2
+ # ==Scruffy::Layers::PieSlice
3
+ #
4
+ # Author:: A.J. Ostman
5
+ # Date:: August 14th, 2006
6
+ #
7
+ # Basic Pie Chart Slice..
8
+
9
+ class PieSlice < Base
10
+
11
+ # Setup Constants
12
+ RADIANS = Math::PI / 180
13
+ MARKER_OFFSET_RATIO = 1.2
14
+ MARKER_FONT_SIZE = 6
15
+
16
+ attr_accessor :diameter
17
+ attr_accessor :percent_used
18
+ attr_accessor :degree_offset
19
+ attr_accessor :scaler
20
+ attr_accessor :center_x, :center_y
21
+
22
+ def draw(svg, coords, options = {})
23
+ # Scaler is the multiplier to normalize the values to a percentage across the Pie Chart
24
+ @scaler = options[:scaler] || 1
25
+
26
+ # Degree Offset is degrees by which the pie chart is twisted when it begins
27
+ @degree_offset = options[:degree_offset] || @options[:degree_offset] || 0
28
+
29
+ # Percent Used keeps track of where in the pie chart we are
30
+ @percent_used = options[:percent_used] || @options[:percent_used] || 0
31
+
32
+ # Diameter of the pie chart defaults to 80% of the height
33
+ @diameter = relative(options[:diameter]) || relative(@options[:diameter]) || relative(80.0)
34
+
35
+ # Stroke
36
+ stroke = options[:stroke] || @options[:stroke] || "none"
37
+
38
+ # Shadow
39
+ shadow = options[:shadow] || @options[:shadow_] || false
40
+ shadow_x = relative(options[:shadow_x]) || relative(@options[:shadow_x]) || relative(-0.5)
41
+ shadow_y = relative(options[:shadow_y]) || relative(@options[:shadow_y]) || relative(0.5)
42
+ shadow_color = options[:shadow_color] || @options[:shadow_color] || "white"
43
+ shadow_opacity = options[:shadow_opacity] || @options[:shadow_opacity] || 0.06
44
+
45
+ # Coordinates for the center of the pie chart.
46
+ @center_x = relative_width(options[:center_x]) || relative_width(@options[:center_x]) || relative_width(50)
47
+ @center_y = relative_height(options[:center_y]) || relative_height(@options[:center_y]) || relative_height(50)
48
+ radius = @diameter / 2.0
49
+
50
+ # Graphing calculated using percent of graph.
51
+ # We later multiply by 3.6 to convert to 360 degree system.
52
+ percent = @scaler * sum_values
53
+
54
+
55
+ # Calculate the Radian Start Point
56
+ radians_start = ((@percent_used * 3.6) + @degree_offset) * RADIANS
57
+ # Calculate the Radian End Point
58
+ radians_end = ((@percent_used + percent) * 3.6 + @degree_offset) * RADIANS
59
+
60
+ radians_mid_point = radians_start + ((radians_end - radians_start) / 2)
61
+
62
+ if options[:explode]
63
+ @center_x = @center_x + (Math.sin(radians_mid_point) * relative(options[:explode]))
64
+ @center_y = @center_y - (Math.cos(radians_mid_point) * relative(options[:explode]))
65
+ end
66
+
67
+
68
+ # Calculate the beginning coordinates
69
+ x_start = @center_x + (Math.sin(radians_start) * radius)
70
+ y_start = @center_y - (Math.cos(radians_start) * radius)
71
+
72
+ # Calculate the End Coords
73
+ x_end = @center_x + (Math.sin(radians_end) * radius)
74
+ y_end = @center_y - (Math.cos(radians_end) * radius)
75
+
76
+
77
+
78
+ # If percentage is really really close to 100% then draw a circle instead!
79
+ if percent >= 99.9999
80
+
81
+ if shadow
82
+ svg.circle(:cx => "#{@center_x + shadow_x}", :cy => "#{@center_y + shadow_y}", :r=>"#{radius}",:stroke => "none",
83
+ :fill => shadow_color.to_s, :style => "fill-opacity: #{shadow_opacity.to_s};")
84
+ end
85
+
86
+ svg.circle(:cx => "#{@center_x}", :cy => "#{@center_y}", :r=>"#{radius}",:stroke => stroke, :fill => color.to_s)
87
+
88
+ else
89
+ if shadow
90
+ svg.path(:d => "M#{@center_x + shadow_x},#{@center_y + shadow_y} L#{x_start + shadow_x},#{y_start + shadow_y} A#{radius},#{radius} 0, #{percent >= 50 ? '1' : '0'}, 1, #{x_end + shadow_x} #{y_end + shadow_y} Z",
91
+ :fill => shadow_color.to_s, :style => "fill-opacity: #{shadow_opacity.to_s};")
92
+ end
93
+
94
+ svg.path(:d => "M#{@center_x},#{@center_y} L#{x_start},#{y_start} A#{radius},#{radius} 0, #{percent >= 50 ? '1' : '0'}, 1, #{x_end} #{y_end} Z",
95
+ :stroke => stroke, :fill => color.to_s)
96
+ end
97
+
98
+ text_x = @center_x + (Math.sin(radians_mid_point) * radius * MARKER_OFFSET_RATIO)
99
+ text_y = @center_y - (Math.cos(radians_mid_point) * radius * MARKER_OFFSET_RATIO)
100
+
101
+ svg.text("#{sprintf('%d', percent)}%", :x => text_x, :y => text_y + relative(MARKER_FONT_SIZE / 2),
102
+ 'font-size' => relative(MARKER_FONT_SIZE),
103
+ :fill => (options[:theme].marker || 'black').to_s,
104
+ 'text-anchor' => 'middle')
105
+ end
106
+
107
+ protected
108
+ def generate_coordinates(options = {})
109
+ # Coordinate Generation didn't make much sense here.
110
+ # Overridden just because Brasten said this would be overridden.
111
+ end
112
+ end
113
+
114
+ end