gruff 0.1.1 → 0.1.2

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.
@@ -11,7 +11,7 @@ class Gruff::Pie < Gruff::Base
11
11
  return unless @has_data
12
12
 
13
13
  diameter = @graph_height
14
- radius = @graph_height / 2.0
14
+ radius = [@graph_width, @graph_height].min / 2.0
15
15
  top_x = @graph_left + (@graph_width - diameter) / 2.0
16
16
  center_x = @graph_left + (@graph_width / 2.0)
17
17
  center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit
@@ -23,12 +23,15 @@ class Gruff::Pie < Gruff::Base
23
23
  if data_row[1][0] > 0
24
24
  @d = @d.stroke data_row[DATA_COLOR_INDEX]
25
25
  @d = @d.fill 'transparent'
26
- @d.stroke_width(200.0)
26
+ @d.stroke_width(radius) # stroke width should be equal to radius. we'll draw centered on (radius / 2)
27
27
 
28
28
  current_degrees = (data_row[1][0] / total_sum) * 360.0
29
- radius = 100.0
29
+
30
+ # ellipse will draw the the stroke centered on the first two parameters offset by the second two.
31
+ # therefore, in order to draw a circle of the proper diameter we must center the stroke at
32
+ # half the radius for both x and y
30
33
  @d = @d.ellipse(center_x, center_y,
31
- radius, radius,
34
+ radius / 2.0, radius / 2.0,
32
35
  prev_degrees, prev_degrees + current_degrees + 0.5) # <= +0.5 'fudge factor' gets rid of the ugly gaps
33
36
 
34
37
  half_angle = prev_degrees + ((prev_degrees + current_degrees) - prev_degrees) / 2
@@ -49,7 +52,7 @@ class Gruff::Pie < Gruff::Base
49
52
  private
50
53
 
51
54
  def draw_label(center_x, center_y, angle, radius, amount)
52
- r_offset = 130 # The distance out from the center of the pie to get point
55
+ r_offset = 30 # The distance out from the center of the pie to get point
53
56
  x_offset = center_x + 15 # The label points need to be tweaked slightly
54
57
  y_offset = center_y + 0 # This one doesn't though
55
58
  x = x_offset + ((radius + r_offset) * Math.cos(angle.deg2rad))
@@ -83,44 +86,3 @@ class Float
83
86
  self * (Math::PI/180.0)
84
87
  end
85
88
  end
86
-
87
- # From sparklines...not currently used
88
- class Magick::Draw
89
-
90
- def pie_slice(center_x=0.0, center_y=0.0, radius=100.0, percent=0.0, rot_x=0.0)
91
- # Okay, this part is as confusing as hell, so pay attention:
92
- # This line determines the horizontal portion of the point on the circle where the X-Axis
93
- # should end. It's caculated by taking the center of the on-image circle and adding that
94
- # to the radius multiplied by the formula for determinig the point on a unit circle that a
95
- # angle corresponds to. 3.6 * percent gives us that angle, but it's in degrees, so we need to
96
- # convert, hence the muliplication by Pi over 180
97
- arc_end_x = radius + (radius * Math.cos((3.6 * percent)*(Math::PI/180.0)))
98
-
99
- # The same goes for here, except it's the vertical point instead of the horizontal one
100
- arc_end_y = radius + (radius * Math.sin((3.6 * percent)*(Math::PI/180.0)))
101
-
102
- # Because the SVG path format is seriously screwy, we need to set the large-arc-flag to 1
103
- # if the angle of an arc is greater than 180 degrees. I have no idea why this is, but it is.
104
- percent > 50 ? large_arc_flag = 1 : large_arc_flag = 0
105
-
106
- # This is also confusing
107
- # M tells us to move to an absolute point on the image.
108
- # We're moving to the center of the pie
109
- # h tells us to move to a relative point.
110
- # We're moving to the right edge of the circle.
111
- # A tells us to start an absolute elliptical arc.
112
- # The first two values are the radii of the ellipse
113
- # The third value is the x-axis-rotation (how to rotate the ellipse)
114
- # The fourth value is our large-arc-flag
115
- # The fifth is the sweep-flag
116
- # The sixth and seventh values are the end point of the arc which we calculated previously
117
- # More info on the SVG path string format at: http://www.w3.org/TR/SVG/paths.html
118
- #
119
- #path = "M#{radius + 2},#{radius + 2} h#{radius} A#{radius},#{radius} #{rot_x} #{large_arc_flag},1 #{arc_end_x},#{arc_end_y} z"
120
- path = "M#{radius},#{radius} h#{radius} A#{radius},#{radius} #{rot_x} #{large_arc_flag},1 #{arc_end_x},#{arc_end_y} z"
121
- puts "PATH: #{path}"
122
-
123
- self.path(path)
124
- end
125
-
126
- end
@@ -0,0 +1,196 @@
1
+
2
+ require "observer"
3
+ require File.dirname(__FILE__) + '/base'
4
+
5
+ # EXPERIMENTAL!
6
+ #
7
+ # Started by Geoffrey Grosenbach at Canada on Rails, April 2006.
8
+ #
9
+ # A scene is a non-linear graph that assembles layers together to tell a story.
10
+ # Layers are folders with appropriately named files (see below). You can group
11
+ # layers and control them together or just set their values individually.
12
+ #
13
+ # Examples:
14
+ #
15
+ # * A city scene that changes with the time of day and the weather conditions.
16
+ # * A traffic map that shows red lines on streets that are crowded and green on free-flowing ones.
17
+ #
18
+ # Usage:
19
+ #
20
+ # g = Gruff::Scene.new("500x100", "artwork/city_scene")
21
+ # g.layers = %w(background haze sky clouds)
22
+ # g.weather_group = %w(clouds)
23
+ # g.time_group = %w(background sky)
24
+ # g.weather = "cloudy"
25
+ # g.time = Time.now
26
+ # g.haze = true
27
+ # g.write "hazy_daytime_city_scene.png"
28
+ #
29
+ #
30
+ #
31
+ # If there is a file named 'default.png', it will be selected (unless other values are provided to override it).
32
+ #
33
+ class Gruff::Scene < Gruff::Base
34
+
35
+ # An array listing the foldernames that will be rendered, from back to front.
36
+ #
37
+ # g.layers = %w(sky clouds buildings street people)
38
+ #
39
+ attr_accessor :layers
40
+
41
+ def initialize(target_width, base_dir)
42
+ @base_dir = base_dir
43
+ @groups = {}
44
+ @layers = []
45
+ super target_width
46
+ end
47
+
48
+ def draw
49
+ # Join all the custom paths and filter out the empty ones
50
+ image_paths = @layers.map { |layer| layer.path }.select { |path| !path.empty? }
51
+ images = Magick::ImageList.new(*image_paths)
52
+ @base_image = images.flatten_images
53
+ end
54
+
55
+ def layers=(ordered_list)
56
+ ordered_list.each do |layer_name|
57
+ @layers << Gruff::Layer.new(@base_dir, layer_name)
58
+ end
59
+ end
60
+
61
+ # Group layers to input values
62
+ #
63
+ # g.weather_group = ["sky", "sea", "clouds"]
64
+ #
65
+ # Set input values
66
+ #
67
+ # g.weather = "cloudy"
68
+ #
69
+ def method_missing(method_name, *args)
70
+ case method_name.to_s
71
+ when /^(\w+)_group=$/
72
+ add_group $1, *args
73
+ return
74
+ when /^(\w+)=$/
75
+ set_input $1, args.first
76
+ return
77
+ end
78
+ super
79
+ end
80
+
81
+ private
82
+
83
+ def add_group(input_name, layer_names)
84
+ @groups[input_name] = Gruff::Group.new(input_name, @layers.select { |layer| layer_names.include?(layer.name) })
85
+ end
86
+
87
+ def set_input(input_name, input_value)
88
+ if not @groups[input_name].nil?
89
+ @groups[input_name].send_updates(input_value)
90
+ else
91
+ if chosen_layer = @layers.detect { |layer| layer.name == input_name }
92
+ chosen_layer.update input_value
93
+ end
94
+ end
95
+ end
96
+
97
+ end
98
+
99
+
100
+ class Gruff::Group
101
+
102
+ include Observable
103
+ attr_reader :name
104
+
105
+ def initialize(folder_name, layers)
106
+ @name = folder_name
107
+ layers.each do |layer|
108
+ layer.observe self
109
+ end
110
+ end
111
+
112
+ def send_updates(value)
113
+ changed
114
+ notify_observers value
115
+ end
116
+
117
+ end
118
+
119
+
120
+ class Gruff::Layer
121
+
122
+ attr_reader :name
123
+
124
+ def initialize(base_dir, folder_name)
125
+ @base_dir = base_dir.to_s
126
+ @name = folder_name.to_s
127
+ @filenames = Dir.open(File.join(base_dir, folder_name)).entries.select { |file| file =~ /^[^.]+\.png$/ }
128
+ @selected_filename = select_default
129
+ end
130
+
131
+ # Register this layer so it receives updates from the group
132
+ def observe(obj)
133
+ obj.add_observer self
134
+ end
135
+
136
+ # Choose the appropriate filename for this layer, based on the input
137
+ def update(value)
138
+ @selected_filename = case value.to_s
139
+ when /^(true|false)$/
140
+ select_boolean value
141
+ when /^(\w|\s)+$/
142
+ select_string value
143
+ when /^-?(\d+\.)?\d+$/
144
+ select_numeric value
145
+ when /(\d\d):(\d\d):\d\d/
146
+ select_time "#{$1}#{$2}"
147
+ else
148
+ select_default
149
+ end
150
+ end
151
+
152
+ # Returns the full path to the selected image, or a blank string
153
+ def path
154
+ unless @selected_filename.nil? || @selected_filename.empty?
155
+ return File.join(@base_dir, @name, @selected_filename)
156
+ end
157
+ ''
158
+ end
159
+
160
+ private
161
+
162
+ # Match "true.png" or "false.png"
163
+ def select_boolean(value)
164
+ file_exists_or_blank value.to_s
165
+ end
166
+
167
+ # Match -5 to _5.png
168
+ def select_numeric(value)
169
+ file_exists_or_blank value.to_s.gsub('-', '_')
170
+ end
171
+
172
+ def select_time(value)
173
+ times = @filenames.map { |filename| filename.gsub('.png', '') }
174
+ times.each_with_index do |time, index|
175
+ if (time > value) && (index > 0)
176
+ return "#{times[index - 1]}.png"
177
+ end
178
+ end
179
+ return "#{times.last}.png"
180
+ end
181
+
182
+ # Match "partly cloudy" to "partly_cloudy.png"
183
+ def select_string(value)
184
+ file_exists_or_blank value.to_s.gsub(' ', '_')
185
+ end
186
+
187
+ def select_default
188
+ file_exists_or_blank "default"
189
+ end
190
+
191
+ # Returns the string "#{filename}.png", if it exists
192
+ def file_exists_or_blank(filename)
193
+ @filenames.include?("#{filename}.png") ? "#{filename}.png" : ''
194
+ end
195
+
196
+ end
@@ -0,0 +1,130 @@
1
+
2
+ require File.dirname(__FILE__) + '/base'
3
+
4
+ # Experimental!!! See also the Net graph.
5
+ #
6
+ # Submitted by Kevin Clark http://glu.ttono.us/
7
+ class Gruff::Spider < Gruff::Base
8
+
9
+ # Hide all text
10
+ attr_accessor :hide_text
11
+ attr_accessor :hide_axes
12
+ attr_accessor :transparent_background
13
+
14
+ def transparent_background=(value)
15
+ @transparent_background = value
16
+ @base_image = render_transparent_background if value
17
+ end
18
+
19
+ def hide_text=(value)
20
+ @hide_title = @hide_text = value
21
+ end
22
+
23
+ def initialize(max_value, target_width = 800)
24
+ super(target_width)
25
+ @max_value = max_value
26
+ @hide_legend = true;
27
+ end
28
+
29
+ def draw
30
+ @hide_line_markers = true
31
+
32
+ super
33
+
34
+ return unless @has_data
35
+
36
+ # Setup basic positioning
37
+ diameter = @graph_height
38
+ radius = @graph_height / 2.0
39
+ top_x = @graph_left + (@graph_width - diameter) / 2.0
40
+ center_x = @graph_left + (@graph_width / 2.0)
41
+ center_y = @graph_top + (@graph_height / 2.0) - 25 # Move graph up a bit
42
+
43
+ @unit_length = radius / @max_value
44
+
45
+
46
+ total_sum = sums_for_spider
47
+ prev_degrees = 0.0
48
+ additive_angle = (2 * Math::PI)/ @data.size
49
+
50
+ current_angle = 0.0
51
+
52
+ # Draw axes
53
+ draw_axes(center_x, center_y, radius, additive_angle) unless hide_axes
54
+
55
+ # Draw polygon
56
+ draw_polygon(center_x, center_y, additive_angle)
57
+
58
+
59
+ @d.draw(@base_image)
60
+ end
61
+
62
+ private
63
+
64
+ def normalize_points(value)
65
+ value * @unit_length
66
+ end
67
+
68
+ def draw_label(center_x, center_y, angle, radius, amount)
69
+ r_offset = 50 # The distance out from the center of the pie to get point
70
+ x_offset = center_x # The label points need to be tweaked slightly
71
+ y_offset = center_y + 0 # This one doesn't though
72
+ x = x_offset + ((radius + r_offset) * Math.cos(angle))
73
+ y = y_offset + ((radius + r_offset) * Math.sin(angle))
74
+
75
+ # Draw label
76
+ @d.fill = @marker_color
77
+ @d.font = @font if @font
78
+ @d.pointsize = scale_fontsize(legend_font_size)
79
+ @d.stroke = 'transparent'
80
+ @d.font_weight = BoldWeight
81
+ @d.gravity = CenterGravity
82
+ @d.annotate_scaled( @base_image,
83
+ 0, 0,
84
+ x, y,
85
+ amount, @scale)
86
+ end
87
+
88
+ def draw_axes(center_x, center_y, radius, additive_angle, line_color = nil)
89
+ return if hide_axes
90
+
91
+ current_angle = 0.0
92
+
93
+ @data.each do |data_row|
94
+ @d.stroke(line_color || data_row[DATA_COLOR_INDEX])
95
+ @d.stroke_width 5.0
96
+
97
+ x_offset = radius * Math.cos(current_angle)
98
+ y_offset = radius * Math.sin(current_angle)
99
+
100
+ @d.line(center_x, center_y,
101
+ center_x + x_offset,
102
+ center_y + y_offset)
103
+
104
+ draw_label(center_x, center_y, current_angle, radius, data_row[0].to_s) unless hide_text
105
+
106
+ current_angle += additive_angle
107
+ end
108
+ end
109
+
110
+ def draw_polygon(center_x, center_y, additive_angle, color = nil)
111
+ points = []
112
+ current_angle = 0.0
113
+ @data.each do |data_row|
114
+ points << center_x + normalize_points(data_row[1][0]) * Math.cos(current_angle)
115
+ points << center_y + normalize_points(data_row[1][0]) * Math.sin(current_angle)
116
+ current_angle += additive_angle
117
+ end
118
+
119
+ @d.stroke_width 1.0
120
+ @d.stroke(color || @marker_color)
121
+ @d.fill(color || @marker_color)
122
+ @d.fill_opacity 0.4
123
+ @d.polygon(*points)
124
+ end
125
+
126
+ def sums_for_spider
127
+ @data.inject(0.0) {|sum, data_row| sum += data_row[1][0]}
128
+ end
129
+
130
+ end
@@ -1,12 +1,8 @@
1
1
  #!/usr/bin/ruby
2
2
 
3
- $:.unshift(File.dirname(__FILE__) + "/../lib/")
4
- #$:.unshift File.dirname(__FILE__) + "/fixtures/helpers"
3
+ require File.dirname(__FILE__) + "/gruff_test_case"
5
4
 
6
- require 'test/unit'
7
- require 'gruff'
8
-
9
- class TestGruffArea < Test::Unit::TestCase
5
+ class TestGruffArea < GruffTestCase
10
6
 
11
7
  def setup
12
8
  @datasets = [
@@ -1,12 +1,8 @@
1
1
  #!/usr/bin/ruby
2
2
 
3
- $:.unshift(File.dirname(__FILE__) + "/../lib/")
4
- #$:.unshift File.dirname(__FILE__) + "/fixtures/helpers"
3
+ require File.dirname(__FILE__) + "/gruff_test_case"
5
4
 
6
- require 'test/unit'
7
- require 'gruff'
8
-
9
- class TestGruffBar < Test::Unit::TestCase
5
+ class TestGruffBar < GruffTestCase
10
6
 
11
7
  # TODO Delete old output files once when starting tests
12
8
 
@@ -76,6 +72,8 @@ class TestGruffBar < Test::Unit::TestCase
76
72
  g.data(:Art, [0, 5, 8, 15], '#990000')
77
73
  g.data(:Philosophy, [10, 3, 2, 8], '#009900')
78
74
  g.data(:Science, [2, 15, 8, 11], '#990099')
75
+
76
+ g.minimum_value = 0
79
77
 
80
78
  g.write("test/output/bar_manual_colors.png")
81
79
  end
@@ -96,22 +94,6 @@ class TestGruffBar < Test::Unit::TestCase
96
94
  g.write("test/output/bar_keynote_small.png")
97
95
  end
98
96
 
99
- def test_bar_image_bg
100
- g = setup_basic_graph()
101
- g.title = "With Image Background"
102
- g.theme_image_based
103
- g.write("test/output/bar_image.png")
104
-
105
- g = setup_basic_graph(400)
106
- g.title = "With Image Background Small"
107
- g.theme_image_based
108
- g.write("test/output/bar_image_small.png")
109
-
110
- g = setup_basic_graph('800x400')
111
- g.title = "With Image Background Small"
112
- g.theme_image_based
113
- g.write("test/output/bar_image_wide.png")
114
- end
115
97
 
116
98
  def test_nil_font
117
99
  g = setup_basic_graph 400
@@ -139,6 +121,19 @@ class TestGruffBar < Test::Unit::TestCase
139
121
  end
140
122
 
141
123
 
124
+ def test_one_value
125
+ g = Gruff::Bar.new
126
+ g.title = "One Value Graph Test"
127
+ g.labels = {
128
+ 0 => '1',
129
+ 1 => '2'
130
+ }
131
+ g.data('one', [1,1])
132
+
133
+ g.write("test/output/bar_one_value.png")
134
+ end
135
+
136
+
142
137
  def test_negative
143
138
  g = Gruff::Bar.new
144
139
  g.title = "Pos/Neg Bar Graph Test"
@@ -155,6 +150,50 @@ class TestGruffBar < Test::Unit::TestCase
155
150
  end
156
151
 
157
152
 
153
+ def test_nearly_zero
154
+ g = Gruff::Bar.new
155
+ g.title = "Nearly Zero Graph"
156
+ g.labels = {
157
+ 0 => '5/6',
158
+ 1 => '5/15',
159
+ 2 => '5/24',
160
+ 3 => '5/30',
161
+ }
162
+ g.data(:apples, [1, 2, 3, 4])
163
+ g.data(:peaches, [4, 3, 2, 1])
164
+ g.minimum_value = 0
165
+ g.maximum_value = 10
166
+ g.write("test/output/bar_nearly_zero.png")
167
+ end
168
+
169
+
170
+ def test_custom_theme
171
+ g = Gruff::Bar.new
172
+ g.title = "Custom Theme"
173
+ g.title_font_size = 60
174
+ g.legend_font_size = 32
175
+ g.marker_font_size = 32
176
+ g.theme = {
177
+ :colors => %w(#efd250 #666699 #e5573f #9595e2),
178
+ :marker_color => 'white',
179
+ :background_image => "assets/pc306715.jpg"
180
+ }
181
+ g.labels = {
182
+ 0 => '5/6',
183
+ 1 => '5/15',
184
+ 2 => '5/24',
185
+ 3 => '5/30',
186
+ }
187
+ g.data(:vancouver, [1, 2, 3, 4])
188
+ g.data(:seattle, [2, 4, 6, 8])
189
+ g.data(:portland, [3, 1, 7, 3])
190
+ g.data(:victoria, [4, 3, 5, 7])
191
+ g.minimum_value = 0
192
+ g.write("test/output/bar_themed.png")
193
+ end
194
+
195
+
196
+
158
197
  protected
159
198
 
160
199
  def setup_basic_graph(size=800)
@@ -175,22 +214,3 @@ protected
175
214
 
176
215
  end
177
216
 
178
- class Gruff::Base
179
- # A color scheme from the colors used on the 2005 Rails keynote presentation at RubyConf.
180
- def theme_image_based
181
- reset_themes()
182
- # Colors
183
- @green = '#00ff00'
184
- @grey = '#333333'
185
- @orange = '#ff5d00'
186
- @red = '#f61100'
187
- @white = 'white'
188
- @light_grey = '#999999'
189
- @black = 'black'
190
- @colors = [@green, @grey, @orange, @red, @white, @light_grey, @black]
191
-
192
- @marker_color = 'white'
193
-
194
- @base_image = render_image_background('assets/pc306715.jpg')
195
- end
196
- end