gruff 0.5.1 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (120) hide show
  1. checksums.yaml +5 -5
  2. data/.editorconfig +14 -0
  3. data/.github/ISSUE_TEMPLATE.md +18 -0
  4. data/.gitignore +3 -0
  5. data/.rubocop.yml +109 -0
  6. data/.rubocop_todo.yml +130 -0
  7. data/.travis.yml +24 -12
  8. data/.yardopts +1 -0
  9. data/{History.txt → CHANGELOG.md} +61 -25
  10. data/Gemfile +3 -7
  11. data/README.md +57 -25
  12. data/Rakefile +6 -201
  13. data/assets/plastik/blue.png +0 -0
  14. data/assets/plastik/green.png +0 -0
  15. data/assets/plastik/red.png +0 -0
  16. data/docker/Dockerfile +14 -0
  17. data/docker/build.sh +4 -0
  18. data/docker/launch.sh +4 -0
  19. data/gruff.gemspec +19 -14
  20. data/init.rb +2 -0
  21. data/lib/gruff.rb +26 -2
  22. data/lib/gruff/accumulator_bar.rb +18 -8
  23. data/lib/gruff/area.rb +33 -19
  24. data/lib/gruff/bar.rb +76 -45
  25. data/lib/gruff/base.rb +337 -613
  26. data/lib/gruff/bezier.rb +34 -19
  27. data/lib/gruff/bullet.rb +51 -62
  28. data/lib/gruff/dot.rb +38 -62
  29. data/lib/gruff/helper/bar_conversion.rb +47 -0
  30. data/lib/gruff/helper/bar_value_label_mixin.rb +30 -0
  31. data/lib/gruff/helper/stacked_mixin.rb +23 -0
  32. data/lib/gruff/histogram.rb +59 -0
  33. data/lib/gruff/line.rb +130 -150
  34. data/lib/gruff/mini/bar.rb +17 -10
  35. data/lib/gruff/mini/legend.rb +24 -36
  36. data/lib/gruff/mini/pie.rb +18 -12
  37. data/lib/gruff/mini/side_bar.rb +26 -12
  38. data/lib/gruff/net.rb +60 -84
  39. data/lib/gruff/patch/rmagick.rb +33 -0
  40. data/lib/gruff/patch/string.rb +10 -0
  41. data/lib/gruff/photo_bar.rb +27 -30
  42. data/lib/gruff/pie.rb +190 -93
  43. data/lib/gruff/renderer/bezier.rb +21 -0
  44. data/lib/gruff/renderer/circle.rb +21 -0
  45. data/lib/gruff/renderer/dash_line.rb +22 -0
  46. data/lib/gruff/renderer/dot.rb +39 -0
  47. data/lib/gruff/renderer/ellipse.rb +21 -0
  48. data/lib/gruff/renderer/line.rb +34 -0
  49. data/lib/gruff/renderer/polygon.rb +23 -0
  50. data/lib/gruff/renderer/polyline.rb +21 -0
  51. data/lib/gruff/renderer/rectangle.rb +19 -0
  52. data/lib/gruff/renderer/renderer.rb +127 -0
  53. data/lib/gruff/renderer/text.rb +42 -0
  54. data/lib/gruff/scatter.rb +156 -180
  55. data/lib/gruff/scene.rb +31 -41
  56. data/lib/gruff/side_bar.rb +77 -63
  57. data/lib/gruff/side_stacked_bar.rb +77 -60
  58. data/lib/gruff/spider.rb +37 -50
  59. data/lib/gruff/stacked_area.rb +32 -30
  60. data/lib/gruff/stacked_bar.rb +87 -49
  61. data/lib/gruff/store/base_data.rb +34 -0
  62. data/lib/gruff/store/custom_data.rb +34 -0
  63. data/lib/gruff/store/store.rb +80 -0
  64. data/lib/gruff/store/xy_data.rb +55 -0
  65. data/lib/gruff/themes.rb +32 -33
  66. data/lib/gruff/version.rb +3 -1
  67. metadata +99 -92
  68. data/Manifest.txt +0 -81
  69. data/RELEASE.md +0 -30
  70. data/assets/bubble.png +0 -0
  71. data/assets/city_scene/background/0000.png +0 -0
  72. data/assets/city_scene/background/0600.png +0 -0
  73. data/assets/city_scene/background/2000.png +0 -0
  74. data/assets/city_scene/clouds/cloudy.png +0 -0
  75. data/assets/city_scene/clouds/partly_cloudy.png +0 -0
  76. data/assets/city_scene/clouds/stormy.png +0 -0
  77. data/assets/city_scene/grass/default.png +0 -0
  78. data/assets/city_scene/haze/true.png +0 -0
  79. data/assets/city_scene/number_sample/1.png +0 -0
  80. data/assets/city_scene/number_sample/2.png +0 -0
  81. data/assets/city_scene/number_sample/default.png +0 -0
  82. data/assets/city_scene/sky/0000.png +0 -0
  83. data/assets/city_scene/sky/0200.png +0 -0
  84. data/assets/city_scene/sky/0400.png +0 -0
  85. data/assets/city_scene/sky/0600.png +0 -0
  86. data/assets/city_scene/sky/0800.png +0 -0
  87. data/assets/city_scene/sky/1000.png +0 -0
  88. data/assets/city_scene/sky/1200.png +0 -0
  89. data/assets/city_scene/sky/1400.png +0 -0
  90. data/assets/city_scene/sky/1500.png +0 -0
  91. data/assets/city_scene/sky/1700.png +0 -0
  92. data/assets/city_scene/sky/2000.png +0 -0
  93. data/assets/pc306715.jpg +0 -0
  94. data/lib/gruff/bar_conversion.rb +0 -46
  95. data/lib/gruff/deprecated.rb +0 -39
  96. data/lib/gruff/stacked_mixin.rb +0 -23
  97. data/test/gruff_test_case.rb +0 -154
  98. data/test/image_compare.rb +0 -58
  99. data/test/test_accumulator_bar.rb +0 -51
  100. data/test/test_area.rb +0 -134
  101. data/test/test_bar.rb +0 -505
  102. data/test/test_base.rb +0 -8
  103. data/test/test_bezier.rb +0 -33
  104. data/test/test_bullet.rb +0 -26
  105. data/test/test_dot.rb +0 -263
  106. data/test/test_legend.rb +0 -68
  107. data/test/test_line.rb +0 -657
  108. data/test/test_mini_bar.rb +0 -33
  109. data/test/test_mini_pie.rb +0 -25
  110. data/test/test_mini_side_bar.rb +0 -36
  111. data/test/test_net.rb +0 -231
  112. data/test/test_photo.rb +0 -41
  113. data/test/test_pie.rb +0 -154
  114. data/test/test_scatter.rb +0 -233
  115. data/test/test_scene.rb +0 -100
  116. data/test/test_side_bar.rb +0 -56
  117. data/test/test_sidestacked_bar.rb +0 -105
  118. data/test/test_spider.rb +0 -226
  119. data/test/test_stacked_area.rb +0 -52
  120. data/test/test_stacked_bar.rb +0 -52
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Magick
4
+ class Draw
5
+ # Additional method to scale annotation text since Draw.scale doesn't.
6
+ def annotate_scaled(img, width, height, x, y, text, scale)
7
+ scaled_width = (width * scale) >= 1 ? (width * scale) : 1
8
+ scaled_height = (height * scale) >= 1 ? (height * scale) : 1
9
+
10
+ annotate(img,
11
+ scaled_width, scaled_height,
12
+ x * scale, y * scale,
13
+ text.gsub('%', '%%'))
14
+ end
15
+
16
+ remove_method :stroke_opacity
17
+ def stroke_opacity(_opacity)
18
+ raise '#stroke_opacity method has different behavior between RMagick and RMagick4J. Should not use this method.'
19
+ end
20
+
21
+ if defined? JRUBY_VERSION
22
+ # FIXME(uwe): We should NOT need to implement this method.
23
+ # Remove this method as soon as RMagick4J Issue #16 is fixed.
24
+ # https://github.com/Serabe/RMagick4J/issues/16
25
+ def fill=(fill)
26
+ fill = { white: '#FFFFFF' }[fill.to_sym] || fill
27
+ @draw.fill = Magick4J.ColorDatabase.query_default(fill)
28
+ self
29
+ end
30
+ # EMXIF
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class String
4
+ THOUSAND_SEPARATOR = ','
5
+
6
+ #Taken from http://codesnippets.joyent.com/posts/show/330
7
+ def commify(delimiter = THOUSAND_SEPARATOR)
8
+ gsub(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{delimiter}")
9
+ end
10
+ end
@@ -1,11 +1,12 @@
1
- require File.dirname(__FILE__) + '/base'
1
+ # frozen_string_literal: true
2
+
3
+ require 'gruff/base'
2
4
 
3
5
  # EXPERIMENTAL!
4
6
  #
5
7
  # Doesn't work yet.
6
8
  #
7
9
  class Gruff::PhotoBar < Gruff::Base
8
-
9
10
  # TODO
10
11
  #
11
12
  # define base and cap in yml
@@ -25,12 +26,12 @@ class Gruff::PhotoBar < Gruff::Base
25
26
 
26
27
  def draw
27
28
  super
28
- return unless @has_data
29
+ return unless data_given?
29
30
 
30
- return # TODO Remove for further development
31
+ return # TODO: Remove for further development
32
+
33
+ init_photo_bar_graphics
31
34
 
32
- init_photo_bar_graphics()
33
-
34
35
  #Draw#define_clip_path()
35
36
  #Draw#clip_path(pathname)
36
37
  #Draw#composite....with bar graph image OverCompositeOp
@@ -38,44 +39,41 @@ class Gruff::PhotoBar < Gruff::Base
38
39
  # See also
39
40
  #
40
41
  # Draw.pattern # define an image to tile as the filling of a draw object
41
- #
42
+ #
42
43
 
43
44
  # Setup spacing.
44
45
  #
45
46
  # Columns sit side-by-side.
46
47
  spacing_factor = 0.9
47
- @bar_width = @norm_data[0][DATA_COLOR_INDEX].columns
48
+ bar_width = store.norm_data[0].color.columns
48
49
 
49
- @norm_data.each_with_index do |data_row, row_index|
50
-
51
- data_row[DATA_VALUES_INDEX].each_with_index do |data_point, point_index|
50
+ store.norm_data.each_with_index do |data_row, row_index|
51
+ data_row.points.each_with_index do |data_point, point_index|
52
52
  data_point = 0 if data_point.nil?
53
53
  # Use incremented x and scaled y
54
- left_x = @graph_left + (@bar_width * (row_index + point_index + ((@data.length - 1) * point_index)))
54
+ left_x = @graph_left + (bar_width * (row_index + point_index + ((store.length - 1) * point_index)))
55
55
  left_y = @graph_top + (@graph_height - data_point * @graph_height) + 1
56
- right_x = left_x + @bar_width * spacing_factor
56
+ right_x = left_x + bar_width * spacing_factor
57
57
  right_y = @graph_top + @graph_height - 1
58
-
59
- bar_image_width = data_row[DATA_COLOR_INDEX].columns
58
+
59
+ bar_image_width = data_row.color.columns
60
60
  bar_image_height = right_y.to_f - left_y.to_f
61
-
61
+
62
62
  # Crop to scale for data
63
- bar_image = data_row[DATA_COLOR_INDEX].crop(0, 0, bar_image_width, bar_image_height)
64
-
65
- @d.gravity = NorthWestGravity
66
- @d = @d.composite(left_x, left_y, bar_image_width, bar_image_height, bar_image)
67
-
63
+ bar_image = data_row.color.crop(0, 0, bar_image_width, bar_image_height)
64
+
65
+ @d.gravity = Magick::NorthWestGravity
66
+ @d.composite(left_x, left_y, bar_image_width, bar_image_height, bar_image)
67
+
68
68
  # Calculate center based on bar_width and current row
69
- label_center = @graph_left + (@data.length * @bar_width * point_index) + (@data.length * @bar_width / 2.0)
69
+ label_center = @graph_left + (store.length * bar_width * point_index) + (store.length * bar_width / 2.0)
70
70
  draw_label(label_center, point_index)
71
71
  end
72
-
73
72
  end
74
73
 
75
- @d.draw(@base_image)
74
+ Gruff::Renderer.finish
76
75
  end
77
76
 
78
-
79
77
  # Return the chosen theme or the default
80
78
  def theme
81
79
  @theme || 'plastik'
@@ -85,16 +83,15 @@ protected
85
83
 
86
84
  # Sets up colors with a list of images that will be used.
87
85
  # Images should be 340px tall
88
- def init_photo_bar_graphics
89
- color_list = Array.new
86
+ def init_photo_bar_graphics
87
+ color_list = []
90
88
  theme_dir = File.dirname(__FILE__) + '/../../assets/' + theme
91
89
 
92
90
  Dir.open(theme_dir).each do |file|
93
91
  next unless /\.png$/.match(file)
94
- color_list << Image.read("#{theme_dir}/#{file}").first
92
+
93
+ color_list << Magick::Image.read("#{theme_dir}/#{file}").first
95
94
  end
96
95
  @colors = color_list
97
96
  end
98
-
99
97
  end
100
-
@@ -1,124 +1,221 @@
1
- require File.dirname(__FILE__) + '/base'
1
+ # frozen_string_literal: true
2
2
 
3
- ##
4
- # Here's how to make a Pie graph:
3
+ require 'gruff/base'
4
+
5
+ #
6
+ # Here's how to make a Gruff::Pie.
5
7
  #
6
8
  # g = Gruff::Pie.new
7
9
  # g.title = "Visual Pie Graph Test"
8
10
  # g.data 'Fries', 20
9
11
  # g.data 'Hamburgers', 50
10
- # g.write("test/output/pie_keynote.png")
12
+ # g.write("pie_keynote.png")
13
+ #
14
+ # To control where the pie chart starts creating slices, use {#zero_degree=}.
11
15
  #
12
- # To control where the pie chart starts creating slices, use #zero_degree.
13
-
14
16
  class Gruff::Pie < Gruff::Base
15
-
16
- TEXT_OFFSET_PERCENTAGE = 0.15
17
+ DEFAULT_TEXT_OFFSET_PERCENTAGE = 0.15
17
18
 
18
19
  # 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_accessor :zero_degree
20
+ # or at another angle. Default is +0.0+, which starts at 3 o'clock.
21
+ attr_writer :zero_degree
22
+
21
23
  # 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
24
+ # Defaults to +0+.
25
+ attr_writer :hide_labels_less_than
26
+
27
+ # Affect the distance between the percentages and the pie chart.
28
+ # Defaults to +0.15+.
29
+ attr_writer :text_offset_percentage
30
+
31
+ ## Use values instead of percentages.
32
+ attr_accessor :show_values_as_labels
24
33
 
25
34
  def initialize_ivars
26
35
  super
27
- @zero_degree = 0.0
28
- @hide_labels_less_than = 0.0
36
+
37
+ @show_values_as_labels = false
38
+
39
+ @store = Gruff::Store.new(Gruff::Store::CustomData)
40
+ end
41
+ private :initialize_ivars
42
+
43
+ def zero_degree
44
+ @zero_degree ||= 0.0
45
+ end
46
+
47
+ def hide_labels_less_than
48
+ @hide_labels_less_than ||= 0.0
49
+ end
50
+
51
+ def text_offset_percentage
52
+ @text_offset_percentage ||= DEFAULT_TEXT_OFFSET_PERCENTAGE
53
+ end
54
+
55
+ def options
56
+ {
57
+ zero_degree: zero_degree,
58
+ hide_labels_less_than: hide_labels_less_than,
59
+ text_offset_percentage: text_offset_percentage,
60
+ show_values_as_labels: show_values_as_labels
61
+ }
29
62
  end
30
63
 
31
64
  def draw
32
- @hide_line_markers = true
33
-
65
+ hide_line_markers
66
+
34
67
  super
35
68
 
36
- return unless @has_data
37
-
38
- diameter = @graph_height
39
- radius = ([@graph_width, @graph_height].min / 2.0) * 0.8
40
- center_x = @graph_left + (@graph_width / 2.0)
41
- center_y = @graph_top + (@graph_height / 2.0) - 10 # Move graph up a bit
42
- total_sum = sums_for_pie()
43
- prev_degrees = @zero_degree
44
-
45
- # Use full data since we can easily calculate percentages
46
- data = (@sort ? @data.sort{ |a, b| a[DATA_VALUES_INDEX].first <=> b[DATA_VALUES_INDEX].first } : @data)
47
- data.each do |data_row|
48
- if data_row[DATA_VALUES_INDEX].first > 0
49
- @d = @d.stroke data_row[DATA_COLOR_INDEX]
50
- @d = @d.fill 'transparent'
51
- @d.stroke_width(radius) # stroke width should be equal to radius. we'll draw centered on (radius / 2)
52
-
53
- current_degrees = (data_row[DATA_VALUES_INDEX].first / total_sum) * 360.0
54
-
55
- # ellipse will draw the the stroke centered on the first two parameters offset by the second two.
56
- # therefore, in order to draw a circle of the proper diameter we must center the stroke at
57
- # half the radius for both x and y
58
- @d = @d.ellipse(center_x, center_y,
59
- radius / 2.0, radius / 2.0,
60
- prev_degrees, prev_degrees + current_degrees + 0.5) # <= +0.5 'fudge factor' gets rid of the ugly gaps
61
-
62
- half_angle = prev_degrees + ((prev_degrees + current_degrees) - prev_degrees) / 2
63
-
64
- label_val = ((data_row[DATA_VALUES_INDEX].first / total_sum) * 100.0).round
65
- unless label_val < @hide_labels_less_than
66
- # RMagick must use sprintf with the string and % has special significance.
67
- label_string = label_val.to_s + '%'
68
- @d = draw_label(center_x,center_y, half_angle,
69
- radius + (radius * TEXT_OFFSET_PERCENTAGE),
70
- label_string)
71
- end
72
-
73
- prev_degrees += current_degrees
69
+ return unless data_given?
70
+
71
+ slices.each do |slice|
72
+ if slice.value > 0
73
+ Gruff::Renderer::Ellipse.new(color: slice.color, width: radius)
74
+ .render(center_x, center_y, radius / 2.0, radius / 2.0, chart_degrees, chart_degrees + slice.degrees + 0.5)
75
+ process_label_for slice
76
+ update_chart_degrees_with slice.degrees
74
77
  end
75
78
  end
76
79
 
77
- # TODO debug a circle where the text is drawn...
78
-
79
- @d.draw(@base_image)
80
+ Gruff::Renderer.finish
80
81
  end
81
82
 
82
83
  private
83
84
 
84
- ##
85
- # Labels are drawn around a slightly wider ellipse to give room for
86
- # labels on the left and right.
87
- def draw_label(center_x, center_y, angle, radius, amount)
88
- # TODO Don't use so many hard-coded numbers
89
- r_offset = 20.0 # The distance out from the center of the pie to get point
90
- x_offset = center_x # + 15.0 # The label points need to be tweaked slightly
91
- y_offset = center_y # This one doesn't though
92
- radius_offset = (radius + r_offset)
93
- ellipse_factor = radius_offset * TEXT_OFFSET_PERCENTAGE
94
- x = x_offset + ((radius_offset + ellipse_factor) * Math.cos(angle.deg2rad))
95
- y = y_offset + (radius_offset * Math.sin(angle.deg2rad))
96
-
97
- # Draw label
98
- @d.fill = @font_color
99
- @d.font = @font if @font
100
- @d.pointsize = scale_fontsize(@marker_font_size)
101
- @d.stroke = 'transparent'
102
- @d.font_weight = BoldWeight
103
- @d.gravity = CenterGravity
104
- @d.annotate_scaled( @base_image,
105
- 0, 0,
106
- x, y,
107
- amount, @scale)
108
- end
109
-
110
- def sums_for_pie
111
- total_sum = 0.0
112
- @data.collect {|data_row| total_sum += data_row[DATA_VALUES_INDEX].first }
113
- total_sum
85
+ def slices
86
+ @slices ||= begin
87
+ slices = store.data.map { |data| slice_class.new(data, options) }
88
+
89
+ slices.sort_by(&:value) if @sort
90
+
91
+ total = slices.map(&:value).inject(:+).to_f
92
+ slices.each { |slice| slice.total = total }
93
+ end
114
94
  end
115
95
 
116
- end
96
+ # General Helper Methods
117
97
 
118
- class Float
119
- # Used for degree => radian conversions
120
- def deg2rad
121
- self * (Math::PI/180.0)
98
+ def hide_line_markers
99
+ @hide_line_markers = true
122
100
  end
123
- end
124
101
 
102
+ def update_chart_degrees_with(degrees)
103
+ @chart_degrees = chart_degrees + degrees
104
+ end
105
+
106
+ def slice_class
107
+ PieSlice
108
+ end
109
+
110
+ # Spatial Value-Related Methods
111
+
112
+ def chart_degrees
113
+ @chart_degrees ||= zero_degree
114
+ end
115
+
116
+ attr_reader :graph_height
117
+
118
+ attr_reader :graph_width
119
+
120
+ def diameter
121
+ graph_height
122
+ end
123
+
124
+ def half_width
125
+ graph_width / 2.0
126
+ end
127
+
128
+ def half_height
129
+ graph_height / 2.0
130
+ end
131
+
132
+ def radius
133
+ @radius ||= ([graph_width, graph_height].min / 2.0) * 0.8
134
+ end
135
+
136
+ def center_x
137
+ @center_x ||= @graph_left + half_width
138
+ end
139
+
140
+ def center_y
141
+ @center_y ||= @graph_top + half_height - 10
142
+ end
143
+
144
+ def distance_from_center
145
+ 20.0
146
+ end
147
+
148
+ def radius_offset
149
+ radius + (radius * text_offset_percentage) + distance_from_center
150
+ end
151
+
152
+ def ellipse_factor
153
+ radius_offset * text_offset_percentage
154
+ end
155
+
156
+ # Label-Related Methods
157
+
158
+ def process_label_for(slice)
159
+ if slice.percentage >= hide_labels_less_than
160
+ x, y = label_coordinates_for slice
161
+
162
+ draw_label(x, y, slice.label)
163
+ end
164
+ end
165
+
166
+ def label_coordinates_for(slice)
167
+ angle = chart_degrees + slice.degrees / 2
168
+
169
+ [x_label_coordinate(angle), y_label_coordinate(angle)]
170
+ end
171
+
172
+ def x_label_coordinate(angle)
173
+ center_x + ((radius_offset + ellipse_factor) * Math.cos(deg2rad(angle)))
174
+ end
175
+
176
+ def y_label_coordinate(angle)
177
+ center_y + (radius_offset * Math.sin(deg2rad(angle)))
178
+ end
179
+
180
+ # Drawing-Related Methods
181
+
182
+ def draw_label(x, y, value)
183
+ text_renderer = Gruff::Renderer::Text.new(value, font: @font, size: @marker_font_size, color: @font_color, weight: Magick::BoldWeight)
184
+ text_renderer.render(0, 0, x, y, Magick::CenterGravity)
185
+ end
186
+
187
+ # Helper Classes
188
+ #
189
+ # @private
190
+ class PieSlice < Struct.new(:data_array, :options)
191
+ attr_accessor :total
192
+
193
+ def name
194
+ data_array[0]
195
+ end
196
+
197
+ def value
198
+ data_array[1].first
199
+ end
200
+
201
+ def color
202
+ data_array[2]
203
+ end
204
+
205
+ def size
206
+ @size ||= value / total
207
+ end
208
+
209
+ def percentage
210
+ @percentage ||= (size * 100.0).round
211
+ end
212
+
213
+ def degrees
214
+ @degrees ||= size * 360.0
215
+ end
216
+
217
+ def label
218
+ options[:show_values_as_labels] ? value.to_s : "#{percentage}%"
219
+ end
220
+ end
221
+ end