gruff 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1c618f771c03abae8519d369261cbc112e0cbc41
4
- data.tar.gz: 406a006220455d8360e8b19cd4b25f88663befca
3
+ metadata.gz: 1f51ad044a490cf08a5bc23565b9a840e8f25ec7
4
+ data.tar.gz: e491d7b01929ec882b9e9760838fe021440eab14
5
5
  SHA512:
6
- metadata.gz: cd1ceca4d3c901e2067df8f44d264e7ef66829367232fb237528453598109f4a432ed4d5f23008890fbafa960b395f65945f15d8e25efa3357acee8594d39e5d
7
- data.tar.gz: 5fe22dbd8fa28469dbe85aa2511cd0815bcb702ac958b10b32305be9d6d0e74bea47cdfbdc673396d81df64281a53e007c30ab045002cd9de7c9c692b0e65303
6
+ metadata.gz: 0e70c18694a2d3dd2339e9374e8bd26628ce738fa854d38bde0f7b7fe8b7f07298e2617b50a8e2816bcb23cd1c777617fa5bff326ff821dc7baf9e0b902a61be
7
+ data.tar.gz: d458e987e717773484d57f85f9e9754c54aea06ceccf7bb6839d19413e79cd0605faa858912d3bee9aafef90b7c6c238a33174ea879e86de17a0d8cb69abc3a1
@@ -1,17 +1,19 @@
1
1
  language: ruby
2
2
  sudo: false
3
3
  rvm:
4
- - "1.8.7"
5
4
  - "1.9.3"
6
5
  - "2.0.0"
7
6
  - "2.1"
8
7
  - "2.2"
9
- - jruby-18mode
10
- - jruby-19mode
11
- - jruby-20mode
8
+ - "2.3.1"
9
+ - jruby-1.7.25
10
+ - jruby-9.0.5.0
11
+ - jruby-9.1.2.0
12
12
  - jruby-head
13
13
  - rbx
14
14
  - rbx-2
15
+ before_install:
16
+ - gem query -i -n ^bundler$ > /dev/null || gem install --no-ri --no-rdoc bundler
15
17
  notifications:
16
18
  email:
17
19
  - uwe@kubosch.no
data/Gemfile CHANGED
@@ -2,9 +2,3 @@ source 'http://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in gruff.gemspec
4
4
  gemspec
5
-
6
- group :test do
7
- if RUBY_VERSION =~ /^1\.9\./ || RUBY_VERSION =~ /^2\./
8
- gem 'minitest-reporters', '<1.0.0'
9
- end
10
- end
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Gruff Graphs
2
2
 
3
- [![Build Status](https://travis-ci.org/topfunky/gruff.png?branch=master)](https://travis-ci.org/topfunky/gruff)
4
- [![Gem Version](https://badge.fury.io/rb/gruff.png)](http://badge.fury.io/rb/gruff)
3
+ [![Build Status](https://travis-ci.org/topfunky/gruff.svg?branch=master)](https://travis-ci.org/topfunky/gruff)
4
+ [![Gem Version](https://badge.fury.io/rb/gruff.svg)](https://badge.fury.io/rb/gruff)
5
5
 
6
6
  A library for making beautiful graphs.
7
7
 
@@ -117,6 +117,10 @@ In progress!
117
117
 
118
118
  http://www.rubydoc.info/github/topfunky/gruff/frames
119
119
 
120
+ ## Supported Ruby Versions
121
+
122
+ We aim to support all Ruby implementations supporting Ruby language level 1.9.3
123
+ or later. Currently we are running CI for MRI, JRuby, and Rubinius.
120
124
 
121
125
  ## Contributing
122
126
 
data/Rakefile CHANGED
@@ -3,7 +3,7 @@ require 'bundler/gem_tasks'
3
3
  require 'rake/testtask'
4
4
  require 'rake/clean'
5
5
 
6
- CLEAN << %w(pkg test/output/*)
6
+ CLEAN.concat %w(pkg test/output/*)
7
7
 
8
8
  desc 'Run tests'
9
9
  task :default => :test
@@ -15,16 +15,18 @@ Gem::Specification.new do |s|
15
15
  s.homepage = %q{https://github.com/topfunky/gruff}
16
16
  s.require_paths = %w(lib)
17
17
  s.summary = %q{Beautiful graphs for one or multiple datasets.}
18
+ s.license = 'MIT'
18
19
  s.test_files = s.files.grep(%r{^(test|spec|features)/})
19
20
  s.executables = s.files.grep(%r{^bin/}).map { |f| File.basename(f) }
20
21
  s.specification_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
22
+ s.required_ruby_version = ['>= 1.9.3', '< 3']
23
+
21
24
  if defined? JRUBY_VERSION
22
25
  s.platform = 'java'
23
- s.add_dependency 'rmagick4j', '>= 0.3.9'
26
+ s.add_dependency 'rmagick4j', '~> 0.3', '>= 0.3.9'
24
27
  else
25
- s.add_dependency 'rmagick', '>= 2.13.4'
28
+ s.add_dependency 'rmagick', '~> 2.13', '>= 2.13.4'
26
29
  end
27
30
  s.add_development_dependency('rake')
28
- s.add_development_dependency('test-unit')
29
- s.license = 'MIT'
31
+ s.add_development_dependency('minitest-reporters')
30
32
  end
@@ -82,7 +82,8 @@ module Gruff
82
82
  # A label for the left side of the graph
83
83
  attr_accessor :y_axis_label
84
84
 
85
- # attr_accessor :x_axis_increment
85
+ # Manually set increment of the vertical marking lines
86
+ attr_accessor :x_axis_increment
86
87
 
87
88
  # Manually set increment of the horizontal marking lines
88
89
  attr_accessor :y_axis_increment
@@ -118,7 +119,7 @@ module Gruff
118
119
  attr_reader :font
119
120
 
120
121
  # Same as font but for the title.
121
- attr_reader :title_font
122
+ attr_accessor :title_font
122
123
 
123
124
  # Specifies whether to draw the title bolded or not.
124
125
  attr_accessor :bold_title
@@ -242,19 +243,23 @@ module Gruff
242
243
  @raw_columns = 800.0
243
244
  @raw_rows = 800.0 * (@rows/@columns)
244
245
  @column_count = 0
246
+ @data = Array.new
245
247
  @marker_count = nil
246
248
  @maximum_value = @minimum_value = nil
247
249
  @has_data = false
248
- @data = Array.new
250
+ @increment = nil
249
251
  @labels = Hash.new
252
+ @label_formatting = nil
250
253
  @labels_seen = Hash.new
251
254
  @sort = false
255
+ @sorted_drawing = false
252
256
  @title = nil
257
+ @title_font = nil
253
258
 
254
259
  @scale = @columns / @raw_columns
255
260
 
256
261
  vera_font_path = File.expand_path('Vera.ttf', ENV['MAGICK_FONT_PATH'])
257
- @font = File.exists?(vera_font_path) ? vera_font_path : nil
262
+ @font = File.exist?(vera_font_path) ? vera_font_path : nil
258
263
  @bold_title = true
259
264
 
260
265
  @marker_font_size = 21.0
@@ -280,6 +285,8 @@ module Gruff
280
285
  @additional_line_colors = []
281
286
  @theme_options = {}
282
287
 
288
+ @use_data_label = false
289
+ @x_axis_increment = nil
283
290
  @x_axis_label = @y_axis_label = nil
284
291
  @y_axis_increment = nil
285
292
  @stacked = nil
@@ -297,11 +304,6 @@ module Gruff
297
304
  @d.font = @font
298
305
  end
299
306
 
300
- # Sets the title font to the font at +font_path+
301
- def title_font=(font_path)
302
- @title_font = font_path
303
- end
304
-
305
307
  # Add a color to the list of available colors for lines.
306
308
  #
307
309
  # Example:
@@ -836,7 +838,7 @@ module Gruff
836
838
  # TODO: See if index.odd? is the best stragegy
837
839
  y_offset += @label_stagger_height if index.odd?
838
840
 
839
- label_text = @labels[index]
841
+ label_text = labels[index].to_s
840
842
 
841
843
  # TESTME
842
844
  # FIXME: Consider chart types other than bar
@@ -58,7 +58,7 @@ class Gruff::Bullet < Gruff::Base
58
58
  @margin = 30.0
59
59
  @thickness = @raw_rows / 6.0
60
60
  @right_margin = @margin
61
- @graph_left = @title_width * 1.3 rescue @margin # HACK Need to calculate real width
61
+ @graph_left = (@title && (@title_width * 1.3)) || @margin
62
62
  @graph_width = @raw_columns - @graph_left - @right_margin
63
63
  @graph_height = @thickness * 3.0
64
64
 
@@ -25,6 +25,9 @@ class Gruff::Line < Gruff::Base
25
25
  attr_accessor :line_width
26
26
  attr_accessor :dot_radius
27
27
 
28
+ # default is a circle, other options include square
29
+ attr_accessor :dot_style
30
+
28
31
  # Hide parts of the graph to fit more datapoints, or for a different appearance.
29
32
  attr_accessor :hide_dots, :hide_lines
30
33
 
@@ -67,7 +70,7 @@ class Gruff::Line < Gruff::Base
67
70
  # g = Gruff::Line.new(400, false) # 400px wide, no lines (for backwards compatibility)
68
71
  #
69
72
  # g = Gruff::Line.new(false) # Defaults to 800px wide, no lines (for backwards compatibility)
70
- #
73
+ #
71
74
  # The preferred way is to call hide_dots or hide_lines instead.
72
75
  def initialize(*args)
73
76
  raise ArgumentError, 'Wrong number of arguments' if args.length > 2
@@ -84,6 +87,10 @@ class Gruff::Line < Gruff::Base
84
87
  @hide_dots = @hide_lines = false
85
88
  @maximum_x_value = nil
86
89
  @minimum_x_value = nil
90
+
91
+ @dot_style = 'circle'
92
+
93
+ @show_vertical_markers = false
87
94
  end
88
95
 
89
96
  # This method allows one to plot a dataset with both X and Y data.
@@ -101,7 +108,7 @@ class Gruff::Line < Gruff::Base
101
108
  # color: hex number indicating the line color as an RGB triplet
102
109
  #
103
110
  # Notes:
104
- # -if (x_data_points.length != y_data_points.length) an error is
111
+ # -if (x_data_points.length != y_data_points.length) an error is
105
112
  # returned.
106
113
  # -if the color argument is nil, the next color from the default theme will
107
114
  # be used.
@@ -115,7 +122,7 @@ class Gruff::Line < Gruff::Base
115
122
  # g.dataxy("Bapples", [1,3,4,5,7,9], [1, 1, 2, 2, 3, 3])
116
123
  # g.dataxy("Capples", [[1,1],[2,3],[3,4],[4,5],[5,7],[6,9]])
117
124
  # #you can still use the old data method too if you want:
118
- # g.data("Capples", [1, 1, 2, 2, 3, 3])
125
+ # g.data("Capples", [1, 1, 2, 2, 3, 3])
119
126
  # #labels will be drawn at the x locations of the keys passed in.
120
127
  # In this example the lables are drawn at x positions 2, 4, and 6:
121
128
  # g.labels = {0 => '2003', 2 => '2004', 4 => '2005', 6 => '2006'}
@@ -178,7 +185,7 @@ class Gruff::Line < Gruff::Base
178
185
 
179
186
  return unless @has_data
180
187
 
181
- # Check to see if more than one datapoint was given. NaN can result otherwise.
188
+ # Check to see if more than one datapoint was given. NaN can result otherwise.
182
189
  @x_increment = (@column_count > 1) ? (@graph_width / (@column_count - 1).to_f) : @graph_width
183
190
 
184
191
  @reference_lines.each_value do |curr_reference_line|
@@ -246,9 +253,12 @@ class Gruff::Line < Gruff::Base
246
253
  @d = @d.line(prev_x, prev_y, new_x, new_y)
247
254
  elsif @one_point
248
255
  # Show a circle if there's just one_point
249
- @d = @d.circle(new_x, new_y, new_x - circle_radius, new_y)
256
+ @d = DotRenderers.renderer(@dot_style).render(@d, new_x, new_y, circle_radius)
257
+ end
258
+
259
+ unless @hide_dots
260
+ @d = DotRenderers.renderer(@dot_style).render(@d, new_x, new_y, circle_radius)
250
261
  end
251
- @d = @d.circle(new_x, new_y, new_x - circle_radius, new_y) unless @hide_dots
252
262
 
253
263
  prev_x, prev_y = new_x, new_y
254
264
  end
@@ -281,7 +291,7 @@ class Gruff::Line < Gruff::Base
281
291
 
282
292
  @reference_lines.each_value do |curr_reference_line|
283
293
 
284
- # We only care about horizontal markers ... for normalization.
294
+ # We only care about horizontal markers ... for normalization.
285
295
  # Vertical markers won't have a :value, they will have an :index
286
296
 
287
297
  curr_reference_line[:norm_value] = ((curr_reference_line[:value].to_f - @minimum_value) / @spread.to_f) if (curr_reference_line.key?(:value))
@@ -326,4 +336,30 @@ class Gruff::Line < Gruff::Base
326
336
  one_point
327
337
  end
328
338
 
339
+ module DotRenderers
340
+ class Circle
341
+ def render(d, new_x, new_y, circle_radius)
342
+ d.circle(new_x, new_y, new_x - circle_radius, new_y)
343
+ end
344
+ end
345
+
346
+ class Square
347
+ def render(d, new_x, new_y, circle_radius)
348
+ offset = (circle_radius * 0.8).to_i
349
+ corner_1 = new_x - offset
350
+ corner_2 = new_y - offset
351
+ corner_3 = new_x + offset
352
+ corner_4 = new_y + offset
353
+ d.rectangle(corner_1, corner_2, corner_3, corner_4)
354
+ end
355
+ end
356
+
357
+ def self.renderer(style)
358
+ if style.to_s == 'square'
359
+ Square.new
360
+ else
361
+ Circle.new
362
+ end
363
+ end
364
+ end
329
365
  end
@@ -4,6 +4,12 @@ module Gruff
4
4
 
5
5
  attr_accessor :hide_mini_legend, :legend_position
6
6
 
7
+ def initialize(*)
8
+ @hide_mini_legend = false
9
+ @legend_position = nil
10
+ super
11
+ end
12
+
7
13
  ##
8
14
  # The canvas needs to be bigger so we can put the legend beneath it.
9
15
 
@@ -45,7 +51,6 @@ module Gruff
45
51
  return if @hide_mini_legend
46
52
 
47
53
  legend_square_width = 40.0 # small square with color of this item
48
- legend_square_margin = 10.0
49
54
  @legend_left_margin = 100.0
50
55
  legend_top_margin = 40.0
51
56
 
@@ -17,107 +17,255 @@ class Gruff::Pie < Gruff::Base
17
17
 
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
- attr_accessor :zero_degree
20
+ attr_writer :zero_degree
21
+
21
22
  # Do not show labels for slices that are less than this percent. Use 0 to always show all labels.
22
23
  # Defaults to 0
23
- attr_accessor :hide_labels_less_than
24
+ attr_writer :hide_labels_less_than
25
+
24
26
  # Affect the distance between the percentages and the pie chart
25
27
  # Defaults to 0.15
26
- attr_accessor :text_offset_percentage
28
+ attr_writer :text_offset_percentage
29
+
27
30
  ## Use values instead of percentages
28
31
  attr_accessor :show_values_as_labels
29
32
 
30
33
  def initialize_ivars
31
34
  super
32
- @zero_degree = 0.0
33
- @hide_labels_less_than = 0.0
34
- @text_offset_percentage = DEFAULT_TEXT_OFFSET_PERCENTAGE
35
+
35
36
  @show_values_as_labels = false
36
37
  end
37
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
+
38
60
  def draw
39
- @hide_line_markers = true
40
-
61
+ hide_line_markers
62
+
41
63
  super
42
64
 
43
- return unless @has_data
44
-
45
- diameter = @graph_height
46
- radius = ([@graph_width, @graph_height].min / 2.0) * 0.8
47
- center_x = @graph_left + (@graph_width / 2.0)
48
- center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit
49
- total_sum = sums_for_pie()
50
- prev_degrees = @zero_degree
51
-
52
- # Use full data since we can easily calculate percentages
53
- data = (@sort ? @data.sort{ |a, b| a[DATA_VALUES_INDEX].first <=> b[DATA_VALUES_INDEX].first } : @data)
54
- data.each do |data_row|
55
- if data_row[DATA_VALUES_INDEX].first > 0
56
- @d = @d.stroke data_row[DATA_COLOR_INDEX]
57
- @d = @d.fill 'transparent'
58
- @d.stroke_width(radius) # stroke width should be equal to radius. we'll draw centered on (radius / 2)
59
-
60
- current_degrees = (data_row[DATA_VALUES_INDEX].first / total_sum) * 360.0
61
-
62
- # ellipse will draw the the stroke centered on the first two parameters offset by the second two.
63
- # therefore, in order to draw a circle of the proper diameter we must center the stroke at
64
- # half the radius for both x and y
65
- @d = @d.ellipse(center_x, center_y,
66
- radius / 2.0, radius / 2.0,
67
- prev_degrees, prev_degrees + current_degrees + 0.5) # <= +0.5 'fudge factor' gets rid of the ugly gaps
68
-
69
- half_angle = prev_degrees + ((prev_degrees + current_degrees) - prev_degrees) / 2
70
-
71
- label_val = ((data_row[DATA_VALUES_INDEX].first / total_sum) * 100.0).round
72
- unless label_val < @hide_labels_less_than
73
- # RMagick must use sprintf with the string and % has special significance.
74
- label_string = @show_values_as_labels ? data_row[DATA_VALUES_INDEX].first.to_s : label_val.to_s + '%'
75
- @d = draw_label(center_x,center_y, half_angle,
76
- radius + (radius * @text_offset_percentage),
77
- label_string)
78
- end
79
-
80
- prev_degrees += current_degrees
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
81
75
  end
82
76
  end
83
77
 
84
- # TODO debug a circle where the text is drawn...
85
-
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
86
212
  @d.draw(@base_image)
87
213
  end
88
214
 
89
- private
90
-
91
- ##
92
- # Labels are drawn around a slightly wider ellipse to give room for
93
- # labels on the left and right.
94
- def draw_label(center_x, center_y, angle, radius, amount)
95
- # TODO Don't use so many hard-coded numbers
96
- r_offset = 20.0 # The distance out from the center of the pie to get point
97
- x_offset = center_x # + 15.0 # The label points need to be tweaked slightly
98
- y_offset = center_y # This one doesn't though
99
- radius_offset = (radius + r_offset)
100
- ellipse_factor = radius_offset * @text_offset_percentage
101
- x = x_offset + ((radius_offset + ellipse_factor) * Math.cos(deg2rad(angle)))
102
- y = y_offset + (radius_offset * Math.sin(deg2rad(angle)))
103
-
104
- # Draw label
105
- @d.fill = @font_color
106
- @d.font = @font if @font
107
- @d.pointsize = scale_fontsize(@marker_font_size)
108
- @d.stroke = 'transparent'
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'
109
220
  @d.font_weight = BoldWeight
110
- @d.gravity = CenterGravity
111
- @d.annotate_scaled( @base_image,
112
- 0, 0,
113
- x, y,
114
- amount, @scale)
221
+ @d.gravity = CenterGravity
115
222
  end
116
223
 
117
- def sums_for_pie
118
- total_sum = 0.0
119
- @data.collect {|data_row| total_sum += data_row[DATA_VALUES_INDEX].first }
120
- total_sum
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
+ )
121
236
  end
122
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
123
271
  end
@@ -32,6 +32,25 @@ class Gruff::Scatter < Gruff::Base
32
32
 
33
33
  #~ # Color of the horizontal baseline
34
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
35
54
 
36
55
 
37
56
  # Gruff::Scatter takes the same parameters as the Gruff::Line graph
@@ -40,13 +59,21 @@ class Gruff::Scatter < Gruff::Base
40
59
  #
41
60
  # g = Gruff::Scatter.new
42
61
  #
43
- def initialize(*args)
44
- super(*args)
45
-
46
- @maximum_x_value = @minimum_x_value = nil
62
+ def initialize(*)
63
+ super
64
+
47
65
  @baseline_x_color = @baseline_y_color = 'red'
48
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
49
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
50
77
  end
51
78
 
52
79
  def setup_drawing
@@ -94,9 +121,9 @@ class Gruff::Scatter < Gruff::Base
94
121
  @d = @d.stroke data_row[DATA_COLOR_INDEX]
95
122
  @d = @d.fill data_row[DATA_COLOR_INDEX]
96
123
  @d = @d.stroke_opacity 1.0
97
- @d = @d.stroke_width clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0)
124
+ @d = @d.stroke_width @stroke_width || clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 4), 5.0)
98
125
 
99
- circle_radius = clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 2.5), 5.0)
126
+ circle_radius = @circle_radius || clip_value_if_greater_than(@columns / (@norm_data.first[1].size * 2.5), 5.0)
100
127
  @d = @d.circle(new_x, new_y, new_x - circle_radius, new_y)
101
128
  end
102
129
  end
@@ -173,7 +200,7 @@ protected
173
200
  @x_spread = @x_spread > 0 ? @x_spread : 1
174
201
  end
175
202
 
176
- def normalize(force=@xy_normalize)
203
+ def normalize(force=nil)
177
204
  if @norm_data.nil? || force
178
205
  @norm_data = []
179
206
  return unless @has_data
@@ -212,7 +239,10 @@ protected
212
239
  end
213
240
  @marker_x_count ||= 4
214
241
  end
215
- @x_increment = (@x_spread > 0) ? significant(@x_spread / @marker_x_count) : 1
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
216
246
  else
217
247
  # TODO Make this work for negative values
218
248
  @maximum_x_value = [@maximum_value.ceil, @x_axis_increment].max
@@ -228,15 +258,17 @@ protected
228
258
  # Draw vertical line markers and annotate with numbers
229
259
  (0..@marker_x_count).each do |index|
230
260
 
231
- # TODO Fix the vertical lines. Not pretty when they don't match up with top y-axis line
232
- # x = @graph_left + @graph_width - index.to_f * @increment_x_scaled
233
- # @d = @d.stroke(@marker_color)
234
- # @d = @d.stroke_width 1
235
- # @d = @d.line(x, @graph_top, x, @graph_bottom)
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
236
268
 
237
269
  unless @hide_line_numbers
238
270
  marker_label = index * @x_increment + @minimum_x_value.to_f
239
- y_offset = @graph_bottom + LABEL_MARGIN
271
+ y_offset = @graph_bottom + (@x_label_margin || LABEL_MARGIN)
240
272
  x_offset = get_x_coord(index.to_f, @increment_x_scaled, @graph_left)
241
273
 
242
274
  @d.fill = @font_color
@@ -244,16 +276,34 @@ protected
244
276
  @d.stroke('transparent')
245
277
  @d.pointsize = scale_fontsize(@marker_font_size)
246
278
  @d.gravity = NorthGravity
247
-
279
+ @d.rotation = -90.0 if @use_vertical_x_labels
248
280
  @d = @d.annotate_scaled(@base_image,
249
281
  1.0, 1.0,
250
282
  x_offset, y_offset,
251
- label(marker_label, @x_increment), @scale)
283
+ vertical_label(marker_label, @x_increment), @scale)
284
+ @d.rotation = 90.0 if @use_vertical_x_labels
252
285
  end
253
286
  end
254
287
 
255
288
  @d = @d.stroke_antialias true
256
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
257
307
 
258
308
  private
259
309
 
@@ -24,7 +24,7 @@ class Gruff::Spider < Gruff::Base
24
24
  def initialize(max_value, target_width = 800)
25
25
  super(target_width)
26
26
  @max_value = max_value
27
- @hide_legend = true;
27
+ @hide_legend = true
28
28
  @rotation = 0
29
29
  end
30
30
 
@@ -36,20 +36,14 @@ class Gruff::Spider < Gruff::Base
36
36
  return unless @has_data
37
37
 
38
38
  # Setup basic positioning
39
- diameter = @graph_height
40
39
  radius = @graph_height / 2.0
41
- top_x = @graph_left + (@graph_width - diameter) / 2.0
42
40
  center_x = @graph_left + (@graph_width / 2.0)
43
41
  center_y = @graph_top + (@graph_height / 2.0) - 25 # Move graph up a bit
44
42
 
45
43
  @unit_length = radius / @max_value
46
44
 
47
- total_sum = sums_for_spider
48
- prev_degrees = 0.0
49
45
  additive_angle = (2 * Math::PI)/ @data.size
50
46
 
51
- current_angle = rotation * Math::PI / 180.0
52
-
53
47
  # Draw axes
54
48
  draw_axes(center_x, center_y, radius, additive_angle) unless hide_axes
55
49
 
@@ -125,7 +119,7 @@ private
125
119
  end
126
120
 
127
121
  def sums_for_spider
128
- @data.inject(0.0) {|sum, data_row| sum += data_row[DATA_VALUES_INDEX].first}
122
+ @data.inject(0.0) {|sum, data_row| sum + data_row[DATA_VALUES_INDEX].first}
129
123
  end
130
124
 
131
125
  end
@@ -1,3 +1,3 @@
1
1
  module Gruff
2
- VERSION = '0.6.0'
2
+ VERSION = '0.7.0'
3
3
  end
@@ -2,7 +2,6 @@ $:.unshift(File.dirname(__FILE__) + '/../lib/')
2
2
 
3
3
  RMAGICK_BYPASS_VERSION_TEST = true
4
4
 
5
- require 'test/unit'
6
5
  require 'gruff'
7
6
  require 'fileutils'
8
7
 
@@ -10,10 +9,9 @@ TEST_OUTPUT_DIR = File.dirname(__FILE__) + "/output#{'_java' if RUBY_PLATFORM ==
10
9
  FileUtils.mkdir_p(TEST_OUTPUT_DIR)
11
10
  FileUtils.rm_f Dir[TEST_OUTPUT_DIR + '/*']
12
11
 
13
- if ENV['RM_INFO'] && RUBY_VERSION =~ /^(1\.9|2\.0)\./
14
- require 'minitest/reporters'
15
- MiniTest::Reporters.use!
16
- end
12
+ require 'minitest/autorun'
13
+ require 'minitest/reporters'
14
+ Minitest::Reporters.use!
17
15
 
18
16
  class Gruff::Base
19
17
  alias :write_org :write
@@ -23,7 +21,7 @@ class Gruff::Base
23
21
  extension = filename.slice(/\.[^\.]*$/)
24
22
  testfilename = File.join(TEST_OUTPUT_DIR, basefilename) + extension
25
23
  counter = 0
26
- while File.exists?(testfilename)
24
+ while File.exist?(testfilename)
27
25
  counter += 1
28
26
  testfilename = File.join(TEST_OUTPUT_DIR, basefilename) + "-#{counter}#{extension}"
29
27
  end
@@ -31,7 +29,7 @@ class Gruff::Base
31
29
  end
32
30
  end
33
31
 
34
- class GruffTestCase < Test::Unit::TestCase
32
+ class GruffTestCase < Minitest::Test
35
33
  def setup
36
34
  srand 42
37
35
  @datasets = [
@@ -106,7 +104,7 @@ class GruffTestCase < Test::Unit::TestCase
106
104
  basefilename = filename.split('.')[0..-2].join('.')
107
105
  extension = filename.slice(/\..*$/)
108
106
  counter = 0
109
- while File.exists? testfilename
107
+ while File.exist? testfilename
110
108
  counter += 1
111
109
  testfilename = [TEST_OUTPUT_DIR, basefilename].join('/') + "-#{counter}#{extension}"
112
110
  end
@@ -40,7 +40,7 @@ class TestGruffAccumulatorBar < GruffTestCase
40
40
  end
41
41
 
42
42
  def test_too_many_args
43
- assert_raise(Gruff::IncorrectNumberOfDatasetsException) {
43
+ assert_raises(Gruff::IncorrectNumberOfDatasetsException) {
44
44
  g = Gruff::AccumulatorBar.new
45
45
  g.data 'First', [1,1,1]
46
46
  g.data 'Too Many', [1,1,1]
@@ -259,11 +259,11 @@ class TestGruffBar < GruffTestCase
259
259
  def test_spacing_factor_does_not_accept_values_lt_0_and_gt_1
260
260
  g = Gruff::Bar.new
261
261
 
262
- assert_raise ArgumentError do
262
+ assert_raises ArgumentError do
263
263
  g.spacing_factor = 1.01
264
264
  end
265
265
 
266
- assert_raise ArgumentError do
266
+ assert_raises ArgumentError do
267
267
  g.spacing_factor = -0.01
268
268
  end
269
269
  end
@@ -4,5 +4,30 @@ require File.dirname(__FILE__) + "/gruff_test_case"
4
4
 
5
5
  class TestGruffBase < GruffTestCase
6
6
 
7
+ def setup
8
+ @sample_numeric_labels = {
9
+ 0 => 6,
10
+ 1 => 15,
11
+ 2 => 24,
12
+ 3 => 30,
13
+ 4 => 4,
14
+ 5 => 12,
15
+ 6 => 21,
16
+ 7 => 28,
17
+ }
18
+ end
7
19
 
8
- end
20
+ def test_labels_can_be_any_object
21
+ g = Gruff::Bar.new
22
+ g.title = 'Bar Graph With Manual Colors'
23
+ g.legend_margin = 50
24
+ g.labels = @sample_numeric_labels
25
+ g.data(:Art, [0, 5, 8, 15], '#990000')
26
+ g.data(:Philosophy, [10, 3, 2, 8], '#009900')
27
+ g.data(:Science, [2, 15, 8, 11], '#990099')
28
+
29
+ g.minimum_value = 0
30
+
31
+ g.write('test/output/bar_object_labels.png')
32
+ end
33
+ end
@@ -13,13 +13,13 @@ class TestGruffBullet < GruffTestCase
13
13
  def test_bullet_graph
14
14
  g = Gruff::Bullet.new
15
15
  g.title = "Monthly Revenue"
16
- g.data *@data_args
16
+ g.data(*@data_args)
17
17
  g.write("test/output/bullet_greyscale.png")
18
18
  end
19
19
 
20
20
  def test_no_options
21
21
  g = Gruff::Bullet.new
22
- g.data *@data_args
22
+ g.data(*@data_args)
23
23
  g.write("test/output/bullet_no_options.png")
24
24
  end
25
25
 
@@ -234,11 +234,11 @@ class TestGruffDot < GruffTestCase
234
234
  g.hide_legend = true
235
235
  g.title = 'Full speed ahead'
236
236
  g.labels = (0..10).inject({}) { |memo, i| memo.merge({ i => (i*10).to_s}) }
237
- g.data(:apples, (0..9).map { rand(20)/10.0 })
237
+ g.data(:apples, [1.7, 0.8, 0.1, 1.9, 1.4, 0.6, 1.1, 0.7, 1.4, 0.2])
238
238
  g.y_axis_increment = 1.0
239
239
  g.x_axis_label = 'Score (%)'
240
240
  g.y_axis_label = 'Students'
241
- write_test_file g, 'enhancements.png'
241
+ write_test_file g, 'enhancements_dot.png'
242
242
  end
243
243
 
244
244
 
@@ -192,6 +192,23 @@ class TestGruffLine < GruffTestCase
192
192
  end
193
193
 
194
194
 
195
+ def test_dot_style_square
196
+ g = Gruff::Line.new
197
+ g.title = 'Square points'
198
+ g.labels = {
199
+ 0 => 'June',
200
+ 10 => 'July',
201
+ 30 => 'August',
202
+ 50 => 'September',
203
+ }
204
+ g.dot_style = :square
205
+ g.data('many points', (0..50).collect { |i| rand(100) })
206
+ g.x_axis_label = 'Months'
207
+
208
+ # Default theme
209
+ g.write('test/output/line_dot_style_square.png')
210
+ end
211
+
195
212
  def test_similar_high_end_values
196
213
  @dataset = %w(29.43 29.459 29.498 29.53 29.548 29.589 29.619 29.66 29.689 29.849 29.878 29.74 29.769 29.79 29.808 29.828).collect { |i| i.to_f }
197
214
 
@@ -577,7 +594,7 @@ class TestGruffLine < GruffTestCase
577
594
  g.title = 'Line Chart WEBP'
578
595
  g.write('line_webp.webp')
579
596
  rescue Magick::ImageMagickError
580
- assert_match /no encode delegate for this image format .*\.webp/, $!.message
597
+ assert_match(/no encode delegate for this image format .*\.webp/, $!.message)
581
598
  end
582
599
 
583
600
  private
@@ -127,7 +127,8 @@ class TestGruffPie < GruffTestCase
127
127
 
128
128
 
129
129
  def test_tiny_simple_pie
130
- @datasets = (1..5).map {|n| ['Auto', [rand(100)]]}
130
+ r = Random.new(297427)
131
+ @datasets = (1..5).map {|n| ['Auto', [r.rand(100)]]}
131
132
 
132
133
  g = setup_basic_graph 200
133
134
  g.hide_legend = true
@@ -140,13 +141,25 @@ class TestGruffPie < GruffTestCase
140
141
  write_test_file g, "pie_simple.png"
141
142
  end
142
143
 
143
- def test_pie_with_adjusted_text_offset_percentage
144
+ def test_pie_with_adjusted_text_offset_percentage
144
145
  g = setup_basic_graph
145
146
  g.title = "Adjusted Text Offset Percentage"
146
147
  g.text_offset_percentage = 0.03
147
148
  g.write "test/output/pie_adjusted_text_offset_percentage.png"
148
149
  end
149
150
 
151
+ def test_subclassed_pie_with_custom_labels
152
+ CustomLabeledPie.new(800).tap do |graph|
153
+ graph.title = "Subclassed Pie with Custom Lables"
154
+
155
+ @datasets.map { |set| set << set.join(': ') }.each do |data|
156
+ graph.data(data[0], data[1], :label => data[2])
157
+ end
158
+
159
+ graph.write 'test/output/pie_subclass_custom_labels.png'
160
+ end
161
+ end
162
+
150
163
  protected
151
164
 
152
165
  def setup_basic_graph(size=800)
@@ -157,5 +170,25 @@ protected
157
170
  end
158
171
  return g
159
172
  end
160
-
173
+
174
+ # Example Gruff::Pie Subclass demonstrating custom labels
175
+ class CustomLabeledPie < Gruff::Pie
176
+ def data(name, data_points = [], options = {})
177
+ super(name, data_points, options[:color])
178
+
179
+ @data.each { |data_array| data_array << options[:label] }
180
+ end
181
+
182
+ private
183
+
184
+ def slice_class
185
+ CustomLabeledSlice
186
+ end
187
+
188
+ class CustomLabeledSlice < ::Gruff::Pie::PieSlice
189
+ def label
190
+ data_array[3] || super
191
+ end
192
+ end
193
+ end
161
194
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require File.dirname(__FILE__) + '/gruff_test_case'
4
4
 
5
- class TestGruffScatter < Test::Unit::TestCase
5
+ class TestGruffScatter < Minitest::Test
6
6
 
7
7
  def setup
8
8
  @datasets = [
@@ -31,6 +31,43 @@ class TestGruffScatter < Test::Unit::TestCase
31
31
  g.write('test/output/scatter_many.png')
32
32
  end
33
33
 
34
+ def test_custom_label_format
35
+ g = Gruff::Scatter.new('1000x500')
36
+ g.top_margin = 0
37
+ g.hide_legend = true
38
+ g.hide_title = true
39
+ g.marker_font_size = 10
40
+ g.theme = {
41
+ :colors => ['#12a702', '#aedaa9'],
42
+ :marker_color => '#dddddd',
43
+ :font_color => 'black',
44
+ :background_colors => 'white'
45
+ }
46
+
47
+ # Points style
48
+ g.circle_radius = 1
49
+ g.stroke_width = 0.01
50
+
51
+ # Axis labels
52
+ g.x_label_margin = 40
53
+ g.bottom_margin = 60
54
+ g.disable_significant_rounding_x_axis = true
55
+ g.use_vertical_x_labels = true
56
+ g.enable_vertical_line_markers = true
57
+ g.marker_x_count = 50 # One label every 2 days
58
+ g.x_axis_label_format = lambda do |value|
59
+ DateTime.strptime(value.to_i.to_s,'%s').strftime('%d.%m.%Y')
60
+ end
61
+ g.y_axis_increment = 1
62
+
63
+ # Fake data (100 days, random times of day between 5 and 16)
64
+ r = Random.new(269155)
65
+ y_values = (0..100).map { 5 + r.rand(12) }
66
+ x_values = (0..100).map { |i| Date.today.strftime('%s').to_i + i*3600*24 }
67
+ g.data('many points', x_values, y_values)
68
+ g.write('test/output/scatter_custom_label_format.png')
69
+ end
70
+
34
71
  # Done
35
72
  def test_no_data
36
73
  g = Gruff::Scatter.new(400)
@@ -65,7 +102,7 @@ class TestGruffScatter < Test::Unit::TestCase
65
102
  ]
66
103
 
67
104
  @datasets.each do |data|
68
- assert_raise ArgumentError do
105
+ assert_raises ArgumentError do
69
106
  g.data(*data)
70
107
  end
71
108
  end
@@ -82,7 +119,7 @@ class TestGruffScatter < Test::Unit::TestCase
82
119
  ]
83
120
 
84
121
  @datasets.each do |data|
85
- assert_raise ArgumentError do
122
+ assert_raises ArgumentError do
86
123
  g.data(*data)
87
124
  end
88
125
  end
@@ -98,7 +135,7 @@ class TestGruffScatter < Test::Unit::TestCase
98
135
  ]
99
136
 
100
137
  @datasets.each do |data|
101
- assert_raise ArgumentError do
138
+ assert_raises ArgumentError do
102
139
  g.data(*data)
103
140
  end
104
141
  end
@@ -87,7 +87,7 @@ class TestGruffSideStackedBar < GruffTestCase
87
87
  end
88
88
  g.show_labels_for_bar_values = true
89
89
  g.write "test/output/side_stacked_bar_labels.png"
90
- end
90
+ end
91
91
 
92
92
  protected
93
93
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gruff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geoffrey Grosenbach
@@ -9,12 +9,15 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2015-05-31 00:00:00.000000000 Z
12
+ date: 2016-06-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rmagick
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '2.13'
18
21
  - - ">="
19
22
  - !ruby/object:Gem::Version
20
23
  version: 2.13.4
@@ -22,6 +25,9 @@ dependencies:
22
25
  prerelease: false
23
26
  version_requirements: !ruby/object:Gem::Requirement
24
27
  requirements:
28
+ - - "~>"
29
+ - !ruby/object:Gem::Version
30
+ version: '2.13'
25
31
  - - ">="
26
32
  - !ruby/object:Gem::Version
27
33
  version: 2.13.4
@@ -40,7 +46,7 @@ dependencies:
40
46
  - !ruby/object:Gem::Version
41
47
  version: '0'
42
48
  - !ruby/object:Gem::Dependency
43
- name: test-unit
49
+ name: minitest-reporters
44
50
  requirement: !ruby/object:Gem::Requirement
45
51
  requirements:
46
52
  - - ">="
@@ -166,7 +172,10 @@ required_ruby_version: !ruby/object:Gem::Requirement
166
172
  requirements:
167
173
  - - ">="
168
174
  - !ruby/object:Gem::Version
169
- version: '0'
175
+ version: 1.9.3
176
+ - - "<"
177
+ - !ruby/object:Gem::Version
178
+ version: '3'
170
179
  required_rubygems_version: !ruby/object:Gem::Requirement
171
180
  requirements:
172
181
  - - ">="
@@ -174,7 +183,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
174
183
  version: '0'
175
184
  requirements: []
176
185
  rubyforge_project:
177
- rubygems_version: 2.4.6
186
+ rubygems_version: 2.5.1
178
187
  signing_key:
179
188
  specification_version: 4
180
189
  summary: Beautiful graphs for one or multiple datasets.