gruffy 0.1.2 → 0.7.1.dev

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,271 +0,0 @@
1
- require File.dirname(__FILE__) + '/base'
2
-
3
- ##
4
- # Here's how to make a Pie graph:
5
- #
6
- # g = Gruffy::Pie.new
7
- # g.title = "Visual Pie Graph Test"
8
- # g.data 'Fries', 20
9
- # g.data 'Hamburgers', 50
10
- # g.write("test/output/pie_keynote.png")
11
- #
12
- # To control where the pie chart starts creating slices, use #zero_degree.
13
-
14
- class Gruffy::Pie < Gruffy::Base
15
-
16
- DEFAULT_TEXT_OFFSET_PERCENTAGE = 0.15
17
-
18
- # Can be used to make the pie start cutting slices at the top (-90.0)
19
- # or at another angle. Default is 0.0, which starts at 3 o'clock.
20
- attr_writer :zero_degree
21
-
22
- # Do not show labels for slices that are less than this percent. Use 0 to always show all labels.
23
- # Defaults to 0
24
- attr_writer :hide_labels_less_than
25
-
26
- # Affect the distance between the percentages and the pie chart
27
- # Defaults to 0.15
28
- attr_writer :text_offset_percentage
29
-
30
- ## Use values instead of percentages
31
- attr_accessor :show_values_as_labels
32
-
33
- def initialize_ivars
34
- super
35
-
36
- @show_values_as_labels = false
37
- end
38
-
39
- def zero_degree
40
- @zero_degree ||= 0.0
41
- end
42
-
43
- def hide_labels_less_than
44
- @hide_labels_less_than ||= 0.0
45
- end
46
-
47
- def text_offset_percentage
48
- @text_offset_percentage ||= DEFAULT_TEXT_OFFSET_PERCENTAGE
49
- end
50
-
51
- def options
52
- {
53
- :zero_degree => zero_degree,
54
- :hide_labels_less_than => hide_labels_less_than,
55
- :text_offset_percentage => text_offset_percentage,
56
- :show_values_as_labels => show_values_as_labels
57
- }
58
- end
59
-
60
- def draw
61
- hide_line_markers
62
-
63
- super
64
-
65
- return unless data_given?
66
-
67
- slices.each do |slice|
68
- if slice.value > 0
69
- set_stroke_color slice
70
- set_fill_color
71
- set_stroke_width
72
- set_drawing_points_for slice
73
- process_label_for slice
74
- update_chart_degrees_with slice.degrees
75
- end
76
- end
77
-
78
- trigger_final_draw
79
- end
80
-
81
- private
82
-
83
- def slices
84
- @slices ||= begin
85
- slices = @data.map { |data| slice_class.new(data, options) }
86
-
87
- slices.sort_by(&:value) if @sort
88
-
89
- total = slices.map(&:value).inject(:+).to_f
90
- slices.each { |slice| slice.total = total }
91
- end
92
- end
93
-
94
- # General Helper Methods
95
-
96
- def hide_line_markers
97
- @hide_line_markers = true
98
- end
99
-
100
- def data_given?
101
- @has_data
102
- end
103
-
104
- def update_chart_degrees_with(degrees)
105
- @chart_degrees = chart_degrees + degrees
106
- end
107
-
108
- def slice_class
109
- PieSlice
110
- end
111
-
112
- # Spatial Value-Related Methods
113
-
114
- def chart_degrees
115
- @chart_degrees ||= zero_degree
116
- end
117
-
118
- def graph_height
119
- @graph_height
120
- end
121
-
122
- def graph_width
123
- @graph_width
124
- end
125
-
126
- def diameter
127
- graph_height
128
- end
129
-
130
- def half_width
131
- graph_width / 2.0
132
- end
133
-
134
- def half_height
135
- graph_height / 2.0
136
- end
137
-
138
- def radius
139
- @radius ||= ([graph_width, graph_height].min / 2.0) * 0.8
140
- end
141
-
142
- def center_x
143
- @center_x ||= @graph_left + half_width
144
- end
145
-
146
- def center_y
147
- @center_y ||= @graph_top + half_height - 10
148
- end
149
-
150
- def distance_from_center
151
- 20.0
152
- end
153
-
154
- def radius_offset
155
- radius + (radius * text_offset_percentage) + distance_from_center
156
- end
157
-
158
- def ellipse_factor
159
- radius_offset * text_offset_percentage
160
- end
161
-
162
- # Label-Related Methods
163
-
164
- def process_label_for(slice)
165
- if slice.percentage >= hide_labels_less_than
166
- x, y = label_coordinates_for slice
167
-
168
- @d = draw_label(x, y, slice.label)
169
- end
170
- end
171
-
172
- def label_coordinates_for(slice)
173
- angle = chart_degrees + slice.degrees / 2
174
-
175
- [x_label_coordinate(angle), y_label_coordinate(angle)]
176
- end
177
-
178
- def x_label_coordinate(angle)
179
- center_x + ((radius_offset + ellipse_factor) * Math.cos(deg2rad(angle)))
180
- end
181
-
182
- def y_label_coordinate(angle)
183
- center_y + (radius_offset * Math.sin(deg2rad(angle)))
184
- end
185
-
186
- # Drawing-Related Methods
187
-
188
- def set_stroke_width
189
- @d.stroke_width(radius)
190
- end
191
-
192
- def set_stroke_color(slice)
193
- @d = @d.stroke slice.color
194
- end
195
-
196
- def set_fill_color
197
- @d = @d.fill 'transparent'
198
- end
199
-
200
- def set_drawing_points_for(slice)
201
- @d = @d.ellipse(
202
- center_x,
203
- center_y,
204
- radius / 2.0,
205
- radius / 2.0,
206
- chart_degrees,
207
- chart_degrees + slice.degrees + 0.5
208
- )
209
- end
210
-
211
- def trigger_final_draw
212
- @d.draw(@base_image)
213
- end
214
-
215
- def configure_label_styling
216
- @d.fill = @font_color
217
- @d.font = @font if @font
218
- @d.pointsize = scale_fontsize(@marker_font_size)
219
- @d.stroke = 'transparent'
220
- @d.font_weight = BoldWeight
221
- @d.gravity = CenterGravity
222
- end
223
-
224
- def draw_label(x, y, value)
225
- configure_label_styling
226
-
227
- @d.annotate_scaled(
228
- @base_image,
229
- 0,
230
- 0,
231
- x,
232
- y,
233
- value,
234
- @scale
235
- )
236
- end
237
-
238
- # Helper Classes
239
-
240
- class PieSlice < Struct.new(:data_array, :options)
241
- attr_accessor :total
242
-
243
- def name
244
- data_array[0]
245
- end
246
-
247
- def value
248
- data_array[1].first
249
- end
250
-
251
- def color
252
- data_array[2]
253
- end
254
-
255
- def size
256
- @size ||= value / total
257
- end
258
-
259
- def percentage
260
- @percentage ||= (size * 100.0).round
261
- end
262
-
263
- def degrees
264
- @degrees ||= size * 360.0
265
- end
266
-
267
- def label
268
- options[:show_values_as_labels] ? value.to_s : "#{percentage}%"
269
- end
270
- end
271
- end
@@ -1,314 +0,0 @@
1
- require File.dirname(__FILE__) + '/base'
2
-
3
- # Here's how to set up an XY Scatter Chart
4
- #
5
- # g = Gruffy::Scatter.new(800)
6
- # g.data(:apples, [1,2,3,4], [4,3,2,1])
7
- # g.data('oranges', [5,7,8], [4,1,7])
8
- # g.write('test/output/scatter.png')
9
- #
10
- #
11
- class Gruffy::Scatter < Gruffy::Base
12
-
13
- # Maximum X Value. The value will get overwritten by the max in the
14
- # datasets.
15
- attr_accessor :maximum_x_value
16
-
17
- # Minimum X Value. The value will get overwritten by the min in the
18
- # datasets.
19
- attr_accessor :minimum_x_value
20
-
21
- # The number of vertical lines shown for reference
22
- attr_accessor :marker_x_count
23
-
24
- #~ # Draw a dashed horizontal line at the given y value
25
- #~ attr_accessor :baseline_y_value
26
-
27
- #~ # Color of the horizontal baseline
28
- #~ attr_accessor :baseline_y_color
29
-
30
- #~ # Draw a dashed horizontal line at the given y value
31
- #~ attr_accessor :baseline_x_value
32
-
33
- #~ # Color of the horizontal baseline
34
- #~ attr_accessor :baseline_x_color
35
-
36
- # Attributes to allow customising the size of the points
37
- attr_accessor :circle_radius
38
- attr_accessor :stroke_width
39
-
40
- # Allow disabling the significant rounding when labeling the X axis
41
- # This is useful when working with a small range of high values (for example, a date range of months, while seconds as units)
42
- attr_accessor :disable_significant_rounding_x_axis
43
-
44
- # Allow enabling vertical lines. When you have a lot of data, they can work great
45
- attr_accessor :enable_vertical_line_markers
46
-
47
- # Allow using vertical labels in the X axis (and setting the label margin)
48
- attr_accessor :x_label_margin
49
- attr_accessor :use_vertical_x_labels
50
-
51
- # Allow passing lambdas to format labels
52
- attr_accessor :y_axis_label_format
53
- attr_accessor :x_axis_label_format
54
-
55
-
56
- # Gruffy::Scatter takes the same parameters as the Gruffy::Line graph
57
- #
58
- # ==== Example
59
- #
60
- # g = Gruffy::Scatter.new
61
- #
62
- def initialize(*)
63
- super
64
-
65
- @baseline_x_color = @baseline_y_color = 'red'
66
- @baseline_x_value = @baseline_y_value = nil
67
- @circle_radius = nil
68
- @disable_significant_rounding_x_axis = false
69
- @enable_vertical_line_markers = false
70
- @marker_x_count = nil
71
- @maximum_x_value = @minimum_x_value = nil
72
- @stroke_width = nil
73
- @use_vertical_x_labels = false
74
- @x_axis_label_format = nil
75
- @x_label_margin = nil
76
- @y_axis_label_format = nil
77
- end
78
-
79
- def setup_drawing
80
- # TODO Need to get x-axis labels working. Current behavior will be to not allow.
81
- @labels = {}
82
-
83
- super
84
-
85
- # Translate our values so that we can use the base methods for drawing
86
- # the standard chart stuff
87
- @column_count = @x_spread
88
- end
89
-
90
- def draw
91
- super
92
- return unless @has_data
93
-
94
- # Check to see if more than one datapoint was given. NaN can result otherwise.
95
- @x_increment = (@column_count > 1) ? (@graph_width / (@column_count - 1).to_f) : @graph_width
96
-
97
- #~ if (defined?(@norm_y_baseline)) then
98
- #~ level = @graph_top + (@graph_height - @norm_baseline * @graph_height)
99
- #~ @d = @d.push
100
- #~ @d.stroke_color @baseline_color
101
- #~ @d.fill_opacity 0.0
102
- #~ @d.stroke_dasharray(10, 20)
103
- #~ @d.stroke_width 5
104
- #~ @d.line(@graph_left, level, @graph_left + @graph_width, level)
105
- #~ @d = @d.pop
106
- #~ end
107
-
108
- #~ if (defined?(@norm_x_baseline)) then
109
-
110
- #~ end
111
-
112
- @norm_data.each do |data_row|
113
- data_row[DATA_VALUES_INDEX].each_with_index do |data_point, index|
114
- x_value = data_row[DATA_VALUES_X_INDEX][index]
115
- next if data_point.nil? || x_value.nil?
116
-
117
- new_x = get_x_coord(x_value, @graph_width, @graph_left)
118
- new_y = @graph_top + (@graph_height - data_point * @graph_height)
119
-
120
- # Reset each time to avoid thin-line errors
121
- @d = @d.stroke data_row[DATA_COLOR_INDEX]
122
- @d = @d.fill data_row[DATA_COLOR_INDEX]
123
- @d = @d.stroke_opacity 1.0
124
- @d = @d.stroke_width @stroke_width || clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0)
125
-
126
- circle_radius = @circle_radius || clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 2.5), 5.0)
127
- @d = @d.circle(new_x, new_y, new_x - circle_radius, new_y)
128
- end
129
- end
130
-
131
- @d.draw(@base_image)
132
- end
133
-
134
- # The first parameter is the name of the dataset. The next two are the
135
- # x and y axis data points contain in their own array in that respective
136
- # order. The final parameter is the color.
137
- #
138
- # Can be called multiple times with different datasets for a multi-valued
139
- # graph.
140
- #
141
- # If the color argument is nil, the next color from the default theme will
142
- # be used.
143
- #
144
- # NOTE: If you want to use a preset theme, you must set it before calling
145
- # data().
146
- #
147
- # ==== Parameters
148
- # name:: String or Symbol containing the name of the dataset.
149
- # x_data_points:: An Array of of x-axis data points.
150
- # y_data_points:: An Array of of y-axis data points.
151
- # color:: The hex string for the color of the dataset. Defaults to nil.
152
- #
153
- # ==== Exceptions
154
- # Data points contain nil values::
155
- # This error will get raised if either the x or y axis data points array
156
- # contains a <tt>nil</tt> value. The graph will not make an assumption
157
- # as how to graph <tt>nil</tt>
158
- # x_data_points is empty::
159
- # This error is raised when the array for the x-axis points are empty
160
- # y_data_points is empty::
161
- # This error is raised when the array for the y-axis points are empty
162
- # x_data_points.length != y_data_points.length::
163
- # Error means that the x and y axis point arrays do not match in length
164
- #
165
- # ==== Examples
166
- # g = Gruffy::Scatter.new
167
- # g.data(:apples, [1,2,3], [3,2,1])
168
- # g.data('oranges', [1,1,1], [2,3,4])
169
- # g.data('bitter_melon', [3,5,6], [6,7,8], '#000000')
170
- #
171
- def data(name, x_data_points=[], y_data_points=[], color=nil)
172
-
173
- raise ArgumentError, 'Data Points contain nil Value!' if x_data_points.include?(nil) || y_data_points.include?(nil)
174
- raise ArgumentError, 'x_data_points is empty!' if x_data_points.empty?
175
- raise ArgumentError, 'y_data_points is empty!' if y_data_points.empty?
176
- raise ArgumentError, 'x_data_points.length != y_data_points.length!' if x_data_points.length != y_data_points.length
177
-
178
- # Call the existing data routine for the y axis data
179
- super(name, y_data_points, color)
180
-
181
- #append the x data to the last entry that was just added in the @data member
182
- last_elem = @data.length()-1
183
- @data[last_elem] << x_data_points
184
-
185
- if @maximum_x_value.nil? && @minimum_x_value.nil?
186
- @maximum_x_value = @minimum_x_value = x_data_points.first
187
- end
188
-
189
- @maximum_x_value = x_data_points.max > @maximum_x_value ?
190
- x_data_points.max : @maximum_x_value
191
- @minimum_x_value = x_data_points.min < @minimum_x_value ?
192
- x_data_points.min : @minimum_x_value
193
- end
194
-
195
- protected
196
-
197
- def calculate_spread #:nodoc:
198
- super
199
- @x_spread = @maximum_x_value.to_f - @minimum_x_value.to_f
200
- @x_spread = @x_spread > 0 ? @x_spread : 1
201
- end
202
-
203
- def normalize(force=nil)
204
- if @norm_data.nil? || force
205
- @norm_data = []
206
- return unless @has_data
207
-
208
- @data.each do |data_row|
209
- norm_data_points = [data_row[DATA_LABEL_INDEX]]
210
- norm_data_points << data_row[DATA_VALUES_INDEX].map do |r|
211
- (r.to_f - @minimum_value.to_f) / @spread
212
- end
213
- norm_data_points << data_row[DATA_COLOR_INDEX]
214
- norm_data_points << data_row[DATA_VALUES_X_INDEX].map do |r|
215
- (r.to_f - @minimum_x_value.to_f) / @x_spread
216
- end
217
- @norm_data << norm_data_points
218
- end
219
- end
220
- #~ @norm_y_baseline = (@baseline_y_value.to_f / @maximum_value.to_f) if @baseline_y_value
221
- #~ @norm_x_baseline = (@baseline_x_value.to_f / @maximum_x_value.to_f) if @baseline_x_value
222
- end
223
-
224
- def draw_line_markers
225
- # do all of the stuff for the horizontal lines on the y-axis
226
- super
227
- return if @hide_line_markers
228
-
229
- @d = @d.stroke_antialias false
230
-
231
- if @x_axis_increment.nil?
232
- # TODO Do the same for larger numbers...100, 75, 50, 25
233
- if @marker_x_count.nil?
234
- (3..7).each do |lines|
235
- if @x_spread % lines == 0.0
236
- @marker_x_count = lines
237
- break
238
- end
239
- end
240
- @marker_x_count ||= 4
241
- end
242
- @x_increment = (@x_spread > 0) ? (@x_spread / @marker_x_count) : 1
243
- unless @disable_significant_rounding_x_axis
244
- @x_increment = significant(@x_increment)
245
- end
246
- else
247
- # TODO Make this work for negative values
248
- @maximum_x_value = [@maximum_value.ceil, @x_axis_increment].max
249
- @minimum_x_value = @minimum_x_value.floor
250
- calculate_spread
251
- normalize(true)
252
-
253
- @marker_count = (@x_spread / @x_axis_increment).to_i
254
- @x_increment = @x_axis_increment
255
- end
256
- @increment_x_scaled = @graph_width.to_f / (@x_spread / @x_increment)
257
-
258
- # Draw vertical line markers and annotate with numbers
259
- (0..@marker_x_count).each do |index|
260
-
261
- # TODO Fix the vertical lines, and enable them by default. Not pretty when they don't match up with top y-axis line
262
- if @enable_vertical_line_markers
263
- x = @graph_left + @graph_width - index.to_f * @increment_x_scaled
264
- @d = @d.stroke(@marker_color)
265
- @d = @d.stroke_width 1
266
- @d = @d.line(x, @graph_top, x, @graph_bottom)
267
- end
268
-
269
- unless @hide_line_numbers
270
- marker_label = index * @x_increment + @minimum_x_value.to_f
271
- y_offset = @graph_bottom + (@x_label_margin || LABEL_MARGIN)
272
- x_offset = get_x_coord(index.to_f, @increment_x_scaled, @graph_left)
273
-
274
- @d.fill = @font_color
275
- @d.font = @font if @font
276
- @d.stroke('transparent')
277
- @d.pointsize = scale_fontsize(@marker_font_size)
278
- @d.gravity = NorthGravity
279
- @d.rotation = -90.0 if @use_vertical_x_labels
280
- @d = @d.annotate_scaled(@base_image,
281
- 1.0, 1.0,
282
- x_offset, y_offset,
283
- vertical_label(marker_label, @x_increment), @scale)
284
- @d.rotation = 90.0 if @use_vertical_x_labels
285
- end
286
- end
287
-
288
- @d = @d.stroke_antialias true
289
- end
290
-
291
-
292
- def label(value, increment)
293
- if @y_axis_label_format
294
- @y_axis_label_format.call(value)
295
- else
296
- super
297
- end
298
- end
299
-
300
- def vertical_label(value, increment)
301
- if @x_axis_label_format
302
- @x_axis_label_format.call(value)
303
- else
304
- label(value, increment)
305
- end
306
- end
307
-
308
- private
309
-
310
- def get_x_coord(x_data_point, width, offset) #:nodoc:
311
- x_data_point * width + offset
312
- end
313
-
314
- end # end Gruffy::Scatter