gruff 0.3.6 → 0.3.7

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.
@@ -1,28 +1,49 @@
1
1
  module Gruff
2
2
  module Mini
3
3
  module Legend
4
-
5
- attr_accessor :hide_mini_legend
6
-
4
+
5
+ attr_accessor :hide_mini_legend, :legend_position
6
+
7
7
  ##
8
8
  # The canvas needs to be bigger so we can put the legend beneath it.
9
9
 
10
10
  def expand_canvas_for_vertical_legend
11
11
  return if @hide_mini_legend
12
-
12
+
13
+ @legend_labels = @data.collect {|item| item[Gruff::Base::DATA_LABEL_INDEX] }
14
+
15
+ legend_height = scale_fontsize(
16
+ @data.length * calculate_line_height +
17
+ @top_margin + @bottom_margin)
18
+
13
19
  @original_rows = @raw_rows
14
- @rows += @data.length * calculate_caps_height(scale_fontsize(@legend_font_size)) * 1.7
20
+ @original_columns = @raw_columns
21
+
22
+ case @legend_position
23
+ when :right then
24
+ @rows = [@rows, legend_height].max
25
+ @columns += calculate_legend_width + @left_margin
26
+ else
27
+ @rows += @data.length * calculate_caps_height(scale_fontsize(@legend_font_size)) * 1.7
28
+ end
15
29
  render_background
16
30
  end
17
-
31
+
32
+ def calculate_line_height
33
+ calculate_caps_height(@legend_font_size) * 1.7
34
+ end
35
+
36
+ def calculate_legend_width
37
+ width = @legend_labels.map { |label| calculate_width(@legend_font_size, label) }.max
38
+ scale_fontsize(width + 40*1.7)
39
+ end
40
+
18
41
  ##
19
42
  # Draw the legend beneath the existing graph.
20
43
 
21
44
  def draw_vertical_legend
22
45
  return if @hide_mini_legend
23
-
24
- @legend_labels = @data.collect {|item| item[Gruff::Base::DATA_LABEL_INDEX] }
25
-
46
+
26
47
  legend_square_width = 40.0 # small square with color of this item
27
48
  legend_square_margin = 10.0
28
49
  @legend_left_margin = 100.0
@@ -32,12 +53,18 @@ module Gruff
32
53
  @d.font = @font if @font
33
54
  @d.pointsize = @legend_font_size
34
55
 
35
- current_x_offset = @legend_left_margin
36
- current_y_offset = @original_rows + legend_top_margin
56
+ case @legend_position
57
+ when :right then
58
+ current_x_offset = @original_columns + @left_margin
59
+ current_y_offset = @top_margin + legend_top_margin
60
+ else
61
+ current_x_offset = @legend_left_margin
62
+ current_y_offset = @original_rows + legend_top_margin
63
+ end
37
64
 
38
65
  debug { @d.line 0.0, current_y_offset, @raw_columns, current_y_offset }
39
66
 
40
- @legend_labels.each_with_index do |legend_label, index|
67
+ @legend_labels.each_with_index do |legend_label, index|
41
68
 
42
69
  # Draw label
43
70
  @d.fill = @font_color
@@ -46,20 +73,20 @@ module Gruff
46
73
  @d.stroke = 'transparent'
47
74
  @d.font_weight = Magick::NormalWeight
48
75
  @d.gravity = Magick::WestGravity
49
- @d = @d.annotate_scaled( @base_image,
50
- @raw_columns, 1.0,
51
- current_x_offset + (legend_square_width * 1.7), current_y_offset,
52
- truncate_legend_label(legend_label), @scale)
76
+ @d = @d.annotate_scaled( @base_image,
77
+ @raw_columns, 1.0,
78
+ current_x_offset + (legend_square_width * 1.7), current_y_offset,
79
+ truncate_legend_label(legend_label), @scale)
53
80
 
54
81
  # Now draw box with color of this dataset
55
82
  @d = @d.stroke 'transparent'
56
83
  @d = @d.fill @data[index][Gruff::Base::DATA_COLOR_INDEX]
57
- @d = @d.rectangle(current_x_offset,
58
- current_y_offset - legend_square_width / 2.0,
59
- current_x_offset + legend_square_width,
84
+ @d = @d.rectangle(current_x_offset,
85
+ current_y_offset - legend_square_width / 2.0,
86
+ current_x_offset + legend_square_width,
60
87
  current_y_offset + legend_square_width / 2.0)
61
-
62
- current_y_offset += calculate_caps_height(@legend_font_size) * 1.7
88
+
89
+ current_y_offset += calculate_line_height
63
90
  end
64
91
  @color_index = 0
65
92
  end
@@ -68,7 +95,7 @@ module Gruff
68
95
  # Shorten long labels so they will fit on the canvas.
69
96
  #
70
97
  # Department of Hu...
71
-
98
+
72
99
  def truncate_legend_label(label)
73
100
  truncated_label = label.to_s
74
101
  while calculate_width(scale_fontsize(@legend_font_size), truncated_label) > (@columns - @legend_left_margin - @right_margin) && (truncated_label.length > 1)
@@ -76,7 +103,7 @@ module Gruff
76
103
  end
77
104
  truncated_label + (truncated_label.length < label.to_s.length ? "..." : '')
78
105
  end
79
-
106
+
80
107
  end
81
108
  end
82
109
  end
@@ -18,10 +18,14 @@ class Gruff::Pie < Gruff::Base
18
18
  # Can be used to make the pie start cutting slices at the top (-90.0)
19
19
  # or at another angle. Default is 0.0, which starts at 3 o'clock.
20
20
  attr_accessor :zero_degree
21
+ # Do not show labels for slices that are less than this percent. Use 0 to always show all labels.
22
+ # Defaults to 0
23
+ attr_accessor :hide_labels_less_than
21
24
 
22
25
  def initialize_ivars
23
26
  super
24
27
  @zero_degree = 0.0
28
+ @hide_labels_less_than = 0.0
25
29
  end
26
30
 
27
31
  def draw
@@ -58,17 +62,15 @@ class Gruff::Pie < Gruff::Base
58
62
 
59
63
  half_angle = prev_degrees + ((prev_degrees + current_degrees) - prev_degrees) / 2
60
64
 
61
- # Following line is commented to allow display of the percentiles
62
- # bug appeared between r90 and r92
63
- # unless @hide_line_markers then
65
+ label_val = ((data_row[DATA_VALUES_INDEX].first / total_sum) * 100.0).round
66
+ unless label_val < @hide_labels_less_than
64
67
  # End the string with %% to escape the single %.
65
68
  # RMagick must use sprintf with the string and % has special significance.
66
- label_string = ((data_row[DATA_VALUES_INDEX].first / total_sum) *
67
- 100.0).round.to_s + '%%'
69
+ label_string = label_val.to_s + '%%'
68
70
  @d = draw_label(center_x,center_y, half_angle,
69
71
  radius + (radius * TEXT_OFFSET_PERCENTAGE),
70
72
  label_string)
71
- # end
73
+ end
72
74
 
73
75
  prev_degrees += current_degrees
74
76
  end
@@ -0,0 +1,268 @@
1
+ require File.dirname(__FILE__) + '/base'
2
+
3
+ # Here's how to set up an XY Scatter Chart
4
+ #
5
+ # g = Gruff::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 Gruff::Scatter < Gruff::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
+
37
+ # Gruff::Scatter takes the same parameters as the Gruff::Line graph
38
+ #
39
+ # ==== Example
40
+ #
41
+ # g = Gruff::Scatter.new
42
+ #
43
+ def initialize(*args)
44
+ super(*args)
45
+
46
+ @maximum_x_value = @minimum_x_value = nil
47
+ @baseline_x_color = @baseline_y_color = 'red'
48
+ @baseline_x_value = @baseline_y_value = nil
49
+ @marker_x_count = nil
50
+ end
51
+
52
+ def draw
53
+ calculate_spread
54
+ @sort = false
55
+
56
+ # TODO Need to get x-axis labels working. Current behavior will be to not allow.
57
+ @labels = {}
58
+
59
+ # Translate our values so that we can use the base methods for drawing
60
+ # the standard chart stuff
61
+ @column_count = @x_spread
62
+
63
+ super
64
+ return unless @has_data
65
+
66
+ # Check to see if more than one datapoint was given. NaN can result otherwise.
67
+ @x_increment = (@column_count > 1) ? (@graph_width / (@column_count - 1).to_f) : @graph_width
68
+
69
+ #~ if (defined?(@norm_y_baseline)) then
70
+ #~ level = @graph_top + (@graph_height - @norm_baseline * @graph_height)
71
+ #~ @d = @d.push
72
+ #~ @d.stroke_color @baseline_color
73
+ #~ @d.fill_opacity 0.0
74
+ #~ @d.stroke_dasharray(10, 20)
75
+ #~ @d.stroke_width 5
76
+ #~ @d.line(@graph_left, level, @graph_left + @graph_width, level)
77
+ #~ @d = @d.pop
78
+ #~ end
79
+
80
+ #~ if (defined?(@norm_x_baseline)) then
81
+
82
+ #~ end
83
+
84
+ @norm_data.each do |data_row|
85
+ prev_x = prev_y = nil
86
+
87
+ data_row[DATA_VALUES_INDEX].each_with_index do |data_point, index|
88
+ x_value = data_row[DATA_VALUES_X_INDEX][index]
89
+ next if data_point.nil? || x_value.nil?
90
+
91
+ new_x = getXCoord(x_value, @graph_width, @graph_left)
92
+ new_y = @graph_top + (@graph_height - data_point * @graph_height)
93
+
94
+ # Reset each time to avoid thin-line errors
95
+ @d = @d.stroke data_row[DATA_COLOR_INDEX]
96
+ @d = @d.fill data_row[DATA_COLOR_INDEX]
97
+ @d = @d.stroke_opacity 1.0
98
+ @d = @d.stroke_width clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0)
99
+
100
+ circle_radius = clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 2.5), 5.0)
101
+ @d = @d.circle(new_x, new_y, new_x - circle_radius, new_y)
102
+
103
+ prev_x = new_x
104
+ prev_y = new_y
105
+ end
106
+ end
107
+
108
+ @d.draw(@base_image)
109
+ end
110
+
111
+ # The first parameter is the name of the dataset. The next two are the
112
+ # x and y axis data points contain in their own array in that respective
113
+ # order. The final parameter is the color.
114
+ #
115
+ # Can be called multiple times with different datasets for a multi-valued
116
+ # graph.
117
+ #
118
+ # If the color argument is nil, the next color from the default theme will
119
+ # be used.
120
+ #
121
+ # NOTE: If you want to use a preset theme, you must set it before calling
122
+ # data().
123
+ #
124
+ # ==== Parameters
125
+ # name:: String or Symbol containing the name of the dataset.
126
+ # x_data_points:: An Array of of x-axis data points.
127
+ # y_data_points:: An Array of of y-axis data points.
128
+ # color:: The hex string for the color of the dataset. Defaults to nil.
129
+ #
130
+ # ==== Exceptions
131
+ # Data points contain nil values::
132
+ # This error will get raised if either the x or y axis data points array
133
+ # contains a <tt>nil</tt> value. The graph will not make an assumption
134
+ # as how to graph <tt>nil</tt>
135
+ # x_data_points is empty::
136
+ # This error is raised when the array for the x-axis points are empty
137
+ # y_data_points is empty::
138
+ # This error is raised when the array for the y-axis points are empty
139
+ # x_data_points.length != y_data_points.length::
140
+ # Error means that the x and y axis point arrays do not match in length
141
+ #
142
+ # ==== Examples
143
+ # g = Gruff::Scatter.new
144
+ # g.data(:apples, [1,2,3], [3,2,1])
145
+ # g.data('oranges', [1,1,1], [2,3,4])
146
+ # g.data('bitter_melon', [3,5,6], [6,7,8], '#000000')
147
+ #
148
+ def data(name, x_data_points=[], y_data_points=[], color=nil)
149
+
150
+ raise ArgumentError, "Data Points contain nil Value!" if x_data_points.include?(nil) || y_data_points.include?(nil)
151
+ raise ArgumentError, "x_data_points is empty!" if x_data_points.empty?
152
+ raise ArgumentError, "y_data_points is empty!" if y_data_points.empty?
153
+ raise ArgumentError, "x_data_points.length != y_data_points.length!" if x_data_points.length != y_data_points.length
154
+
155
+ # Call the existing data routine for the y axis data
156
+ super(name, y_data_points, color)
157
+
158
+ #append the x data to the last entry that was just added in the @data member
159
+ lastElem = @data.length()-1
160
+ @data[lastElem] << x_data_points
161
+
162
+ if @maximum_x_value.nil? && @minimum_x_value.nil?
163
+ @maximum_x_value = @minimum_x_value = x_data_points.first
164
+ end
165
+
166
+ @maximum_x_value = x_data_points.max > @maximum_x_value ?
167
+ x_data_points.max : @maximum_x_value
168
+ @minimum_x_value = x_data_points.min < @minimum_x_value ?
169
+ x_data_points.min : @minimum_x_value
170
+ end
171
+
172
+ protected
173
+
174
+ def calculate_spread #:nodoc:
175
+ super
176
+ @x_spread = @maximum_x_value.to_f - @minimum_x_value.to_f
177
+ @x_spread = @x_spread > 0 ? @x_spread : 1
178
+ end
179
+
180
+ def normalize(force=@xy_normalize)
181
+ if @norm_data.nil? || force
182
+ @norm_data = []
183
+ return unless @has_data
184
+
185
+ @data.each do |data_row|
186
+ norm_data_points = [data_row[DATA_LABEL_INDEX]]
187
+ norm_data_points << data_row[DATA_VALUES_INDEX].map do |r|
188
+ (r.to_f - @minimum_value.to_f) / @spread
189
+ end
190
+ norm_data_points << data_row[DATA_COLOR_INDEX]
191
+ norm_data_points << data_row[DATA_VALUES_X_INDEX].map do |r|
192
+ (r.to_f - @minimum_x_value.to_f) / @x_spread
193
+ end
194
+ @norm_data << norm_data_points
195
+ end
196
+ end
197
+ #~ @norm_y_baseline = (@baseline_y_value.to_f / @maximum_value.to_f) if @baseline_y_value
198
+ #~ @norm_x_baseline = (@baseline_x_value.to_f / @maximum_x_value.to_f) if @baseline_x_value
199
+ end
200
+
201
+ def draw_line_markers
202
+ # do all of the stuff for the horizontal lines on the y-axis
203
+ super
204
+ return if @hide_line_markers
205
+
206
+ @d = @d.stroke_antialias false
207
+
208
+ if @x_axis_increment.nil?
209
+ # TODO Do the same for larger numbers...100, 75, 50, 25
210
+ if @marker_x_count.nil?
211
+ (3..7).each do |lines|
212
+ if @x_spread % lines == 0.0
213
+ @marker_x_count = lines
214
+ break
215
+ end
216
+ end
217
+ @marker_x_count ||= 4
218
+ end
219
+ @x_increment = (@x_spread > 0) ? significant(@x_spread / @marker_x_count) : 1
220
+ else
221
+ # TODO Make this work for negative values
222
+ @maximum_x_value = [@maximum_value.ceil, @x_axis_increment].max
223
+ @minimum_x_value = @minimum_x_value.floor
224
+ calculate_spread
225
+ normalize(true)
226
+
227
+ @marker_count = (@x_spread / @x_axis_increment).to_i
228
+ @x_increment = @x_axis_increment
229
+ end
230
+ @increment_x_scaled = @graph_width.to_f / (@x_spread / @x_increment)
231
+
232
+ # Draw vertical line markers and annotate with numbers
233
+ (0..@marker_x_count).each do |index|
234
+ x = @graph_left + @graph_width - index.to_f * @increment_x_scaled
235
+
236
+ # TODO Fix the vertical lines. Not pretty when they don't match up with top y-axis line
237
+ #~ @d = @d.stroke(@marker_color)
238
+ #~ @d = @d.stroke_width 1
239
+ #~ @d = @d.line(x, @graph_top, x, @graph_bottom)
240
+
241
+ unless @hide_line_numbers
242
+ marker_label = index * @x_increment + @minimum_x_value.to_f
243
+ y_offset = @graph_bottom + LABEL_MARGIN
244
+ x_offset = getXCoord(index.to_f, @increment_x_scaled, @graph_left)
245
+
246
+ @d.fill = @font_color
247
+ @d.font = @font if @font
248
+ @d.stroke('transparent')
249
+ @d.pointsize = scale_fontsize(@marker_font_size)
250
+ @d.gravity = NorthGravity
251
+
252
+ @d = @d.annotate_scaled(@base_image,
253
+ 1.0, 1.0,
254
+ x_offset, y_offset,
255
+ label(marker_label), @scale)
256
+ end
257
+ end
258
+
259
+ @d = @d.stroke_antialias true
260
+ end
261
+
262
+ private
263
+
264
+ def getXCoord(x_data_point, width, offset) #:nodoc:
265
+ return(x_data_point * width + offset)
266
+ end
267
+
268
+ end # end Gruff::Scatter
@@ -133,7 +133,7 @@ class Gruff::Layer
133
133
  def initialize(base_dir, folder_name)
134
134
  @base_dir = base_dir.to_s
135
135
  @name = folder_name.to_s
136
- @filenames = Dir.open(File.join(base_dir, folder_name)).entries.select { |file| file =~ /^[^.]+\.png$/ }
136
+ @filenames = Dir.open(File.join(base_dir, folder_name)).entries.select { |file| file =~ /^[^.]+\.png$/ }.sort
137
137
  @selected_filename = select_default
138
138
  end
139
139
 
@@ -5,22 +5,30 @@ require File.dirname(__FILE__) + '/base'
5
5
 
6
6
  class Gruff::SideBar < Gruff::Base
7
7
 
8
+ # Spacing factor applied between bars
9
+ attr_accessor :bar_spacing
10
+
8
11
  def draw
9
12
  @has_left_labels = true
10
13
  super
11
14
 
12
15
  return unless @has_data
16
+ draw_bars
17
+ end
18
+
19
+ protected
13
20
 
21
+ def draw_bars
14
22
  # Setup spacing.
15
23
  #
16
24
  @bar_spacing ||= 0.9
17
25
 
18
26
  @bars_width = @graph_height / @column_count.to_f
19
- @bar_width = @bars_width * @bar_spacing / @norm_data.size
27
+ @bar_width = @bars_width / @norm_data.size
20
28
  @d = @d.stroke_opacity 0.0
21
29
  height = Array.new(@column_count, 0)
22
30
  length = Array.new(@column_count, @graph_left)
23
- padding = (@bars_width * (1 - @bar_spacing)) / 2
31
+ padding = (@bar_width * (1 - @bar_spacing)) / 2
24
32
 
25
33
  @norm_data.each_with_index do |data_row, row_index|
26
34
  @d = @d.fill data_row[DATA_COLOR_INDEX]
@@ -37,7 +45,7 @@ class Gruff::SideBar < Gruff::Base
37
45
  left_x = length[point_index] - 1
38
46
  left_y = @graph_top + (@bars_width * point_index) + (@bar_width * row_index) + padding
39
47
  right_x = left_x + difference
40
- right_y = left_y + @bar_width
48
+ right_y = left_y + @bar_width * @bar_spacing
41
49
 
42
50
  height[point_index] += (data_point * @graph_width)
43
51
 
@@ -53,8 +61,6 @@ class Gruff::SideBar < Gruff::Base
53
61
  @d.draw(@base_image)
54
62
  end
55
63
 
56
- protected
57
-
58
64
  # Instead of base class version, draws vertical background lines and label
59
65
  def draw_line_markers
60
66
 
@@ -68,14 +74,14 @@ protected
68
74
  number_of_lines = 5
69
75
 
70
76
  # TODO Round maximum marker value to a round number like 100, 0.1, 0.5, etc.
71
- increment = significant(@maximum_value.to_f / number_of_lines)
77
+ increment = significant(@spread.to_f / number_of_lines)
72
78
  (0..number_of_lines).each do |index|
73
79
 
74
80
  line_diff = (@graph_right - @graph_left) / number_of_lines
75
81
  x = @graph_right - (line_diff * index) - 1
76
82
  @d = @d.line(x, @graph_bottom, x, @graph_top)
77
83
  diff = index - number_of_lines
78
- marker_label = diff.abs * increment
84
+ marker_label = diff.abs * increment + @minimum_value
79
85
 
80
86
  unless @hide_line_numbers
81
87
  @d.fill = @font_color