scruffy 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
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