gruff 0.3.6 → 0.3.7

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