gruff 0.8.0 → 0.9.0

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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE.md +18 -0
  3. data/.gitignore +3 -0
  4. data/.rubocop.yml +93 -0
  5. data/.rubocop_todo.yml +23 -810
  6. data/.travis.yml +4 -4
  7. data/.yardopts +1 -0
  8. data/CHANGELOG.md +22 -0
  9. data/Gemfile +3 -1
  10. data/README.md +44 -21
  11. data/Rakefile +2 -206
  12. data/docker/Dockerfile +14 -0
  13. data/docker/build.sh +4 -0
  14. data/docker/launch.sh +4 -0
  15. data/gruff.gemspec +11 -8
  16. data/init.rb +2 -0
  17. data/lib/gruff.rb +23 -0
  18. data/lib/gruff/accumulator_bar.rb +6 -6
  19. data/lib/gruff/area.rb +13 -17
  20. data/lib/gruff/bar.rb +58 -41
  21. data/lib/gruff/base.rb +243 -566
  22. data/lib/gruff/bezier.rb +12 -14
  23. data/lib/gruff/bullet.rb +39 -57
  24. data/lib/gruff/dot.rb +25 -59
  25. data/lib/gruff/{bar_conversion.rb → helper/bar_conversion.rb} +13 -12
  26. data/lib/gruff/helper/bar_value_label_mixin.rb +30 -0
  27. data/lib/gruff/{stacked_mixin.rb → helper/stacked_mixin.rb} +7 -6
  28. data/lib/gruff/line.rb +95 -177
  29. data/lib/gruff/mini/bar.rb +6 -7
  30. data/lib/gruff/mini/legend.rb +16 -32
  31. data/lib/gruff/mini/pie.rb +6 -7
  32. data/lib/gruff/mini/side_bar.rb +4 -5
  33. data/lib/gruff/net.rb +37 -65
  34. data/lib/gruff/patch/rmagick.rb +33 -0
  35. data/lib/gruff/patch/string.rb +8 -0
  36. data/lib/gruff/photo_bar.rb +19 -19
  37. data/lib/gruff/pie.rb +22 -73
  38. data/lib/gruff/renderer/bezier.rb +21 -0
  39. data/lib/gruff/renderer/circle.rb +21 -0
  40. data/lib/gruff/renderer/dash_line.rb +22 -0
  41. data/lib/gruff/renderer/dot.rb +39 -0
  42. data/lib/gruff/renderer/ellipse.rb +21 -0
  43. data/lib/gruff/renderer/line.rb +34 -0
  44. data/lib/gruff/renderer/polygon.rb +23 -0
  45. data/lib/gruff/renderer/polyline.rb +21 -0
  46. data/lib/gruff/renderer/rectangle.rb +19 -0
  47. data/lib/gruff/renderer/renderer.rb +127 -0
  48. data/lib/gruff/renderer/text.rb +42 -0
  49. data/lib/gruff/scatter.rb +85 -156
  50. data/lib/gruff/scene.rb +22 -30
  51. data/lib/gruff/side_bar.rb +62 -58
  52. data/lib/gruff/side_stacked_bar.rb +47 -43
  53. data/lib/gruff/spider.rb +19 -36
  54. data/lib/gruff/stacked_area.rb +17 -21
  55. data/lib/gruff/stacked_bar.rb +50 -24
  56. data/lib/gruff/store/base_data.rb +34 -0
  57. data/lib/gruff/store/custom_data.rb +34 -0
  58. data/lib/gruff/store/store.rb +80 -0
  59. data/lib/gruff/store/xy_data.rb +55 -0
  60. data/lib/gruff/themes.rb +3 -3
  61. data/lib/gruff/version.rb +3 -1
  62. metadata +41 -30
  63. data/Manifest.txt +0 -81
  64. data/assets/bubble.png +0 -0
  65. data/assets/city_scene/background/0000.png +0 -0
  66. data/assets/city_scene/background/0600.png +0 -0
  67. data/assets/city_scene/background/2000.png +0 -0
  68. data/assets/city_scene/clouds/cloudy.png +0 -0
  69. data/assets/city_scene/clouds/partly_cloudy.png +0 -0
  70. data/assets/city_scene/clouds/stormy.png +0 -0
  71. data/assets/city_scene/grass/default.png +0 -0
  72. data/assets/city_scene/haze/true.png +0 -0
  73. data/assets/city_scene/number_sample/1.png +0 -0
  74. data/assets/city_scene/number_sample/2.png +0 -0
  75. data/assets/city_scene/number_sample/default.png +0 -0
  76. data/assets/city_scene/sky/0000.png +0 -0
  77. data/assets/city_scene/sky/0200.png +0 -0
  78. data/assets/city_scene/sky/0400.png +0 -0
  79. data/assets/city_scene/sky/0600.png +0 -0
  80. data/assets/city_scene/sky/0800.png +0 -0
  81. data/assets/city_scene/sky/1000.png +0 -0
  82. data/assets/city_scene/sky/1200.png +0 -0
  83. data/assets/city_scene/sky/1400.png +0 -0
  84. data/assets/city_scene/sky/1500.png +0 -0
  85. data/assets/city_scene/sky/1700.png +0 -0
  86. data/assets/city_scene/sky/2000.png +0 -0
  87. data/assets/pc306715.jpg +0 -0
  88. data/lib/gruff/deprecated.rb +0 -38
@@ -1,4 +1,6 @@
1
- require File.dirname(__FILE__) + '/base'
1
+ # frozen_string_literal: true
2
+
3
+ require 'gruff/base'
2
4
 
3
5
  ##
4
6
  # Here's how to make a Pie graph:
@@ -10,31 +12,32 @@ require File.dirname(__FILE__) + '/base'
10
12
  # g.write("test/output/pie_keynote.png")
11
13
  #
12
14
  # To control where the pie chart starts creating slices, use #zero_degree.
13
-
14
15
  class Gruff::Pie < Gruff::Base
15
-
16
16
  DEFAULT_TEXT_OFFSET_PERCENTAGE = 0.15
17
17
 
18
18
  # Can be used to make the pie start cutting slices at the top (-90.0)
19
- # or at another angle. Default is 0.0, which starts at 3 o'clock.
19
+ # or at another angle. Default is +0.0+, which starts at 3 o'clock.
20
20
  attr_writer :zero_degree
21
21
 
22
22
  # Do not show labels for slices that are less than this percent. Use 0 to always show all labels.
23
- # Defaults to 0
23
+ # Defaults to +0+.
24
24
  attr_writer :hide_labels_less_than
25
25
 
26
- # Affect the distance between the percentages and the pie chart
27
- # Defaults to 0.15
26
+ # Affect the distance between the percentages and the pie chart.
27
+ # Defaults to +0.15+.
28
28
  attr_writer :text_offset_percentage
29
29
 
30
- ## Use values instead of percentages
30
+ ## Use values instead of percentages.
31
31
  attr_accessor :show_values_as_labels
32
32
 
33
33
  def initialize_ivars
34
34
  super
35
35
 
36
36
  @show_values_as_labels = false
37
+
38
+ @store = Gruff::Store.new(Gruff::Store::CustomData)
37
39
  end
40
+ private :initialize_ivars
38
41
 
39
42
  def zero_degree
40
43
  @zero_degree ||= 0.0
@@ -66,23 +69,21 @@ class Gruff::Pie < Gruff::Base
66
69
 
67
70
  slices.each do |slice|
68
71
  if slice.value > 0
69
- set_stroke_color slice
70
- set_fill_color
71
- set_stroke_width
72
- set_drawing_points_for slice
72
+ Gruff::Renderer::Ellipse.new(color: slice.color, width: radius)
73
+ .render(center_x, center_y, radius / 2.0, radius / 2.0, chart_degrees, chart_degrees + slice.degrees + 0.5)
73
74
  process_label_for slice
74
75
  update_chart_degrees_with slice.degrees
75
76
  end
76
77
  end
77
78
 
78
- trigger_final_draw
79
+ Gruff::Renderer.finish
79
80
  end
80
81
 
81
82
  private
82
83
 
83
84
  def slices
84
85
  @slices ||= begin
85
- slices = @data.map { |data| slice_class.new(data, options) }
86
+ slices = store.data.map { |data| slice_class.new(data, options) }
86
87
 
87
88
  slices.sort_by(&:value) if @sort
88
89
 
@@ -97,10 +98,6 @@ private
97
98
  @hide_line_markers = true
98
99
  end
99
100
 
100
- def data_given?
101
- @has_data
102
- end
103
-
104
101
  def update_chart_degrees_with(degrees)
105
102
  @chart_degrees = chart_degrees + degrees
106
103
  end
@@ -115,13 +112,9 @@ private
115
112
  @chart_degrees ||= zero_degree
116
113
  end
117
114
 
118
- def graph_height
119
- @graph_height
120
- end
115
+ attr_reader :graph_height
121
116
 
122
- def graph_width
123
- @graph_width
124
- end
117
+ attr_reader :graph_width
125
118
 
126
119
  def diameter
127
120
  graph_height
@@ -165,7 +158,7 @@ private
165
158
  if slice.percentage >= hide_labels_less_than
166
159
  x, y = label_coordinates_for slice
167
160
 
168
- @d = draw_label(x, y, slice.label)
161
+ draw_label(x, y, slice.label)
169
162
  end
170
163
  end
171
164
 
@@ -185,58 +178,14 @@ private
185
178
 
186
179
  # Drawing-Related Methods
187
180
 
188
- def set_stroke_width
189
- @d.stroke_width(radius)
190
- end
191
-
192
- def set_stroke_color(slice)
193
- @d = @d.stroke slice.color
194
- end
195
-
196
- def set_fill_color
197
- @d = @d.fill 'transparent'
198
- end
199
-
200
- def set_drawing_points_for(slice)
201
- @d = @d.ellipse(
202
- center_x,
203
- center_y,
204
- radius / 2.0,
205
- radius / 2.0,
206
- chart_degrees,
207
- chart_degrees + slice.degrees + 0.5
208
- )
209
- end
210
-
211
- def trigger_final_draw
212
- @d.draw(@base_image)
213
- end
214
-
215
- def configure_label_styling
216
- @d.fill = @font_color
217
- @d.font = @font if @font
218
- @d.pointsize = scale_fontsize(@marker_font_size)
219
- @d.stroke = 'transparent'
220
- @d.font_weight = BoldWeight
221
- @d.gravity = CenterGravity
222
- end
223
-
224
181
  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
- )
182
+ text_renderer = Gruff::Renderer::Text.new(value, font: @font, size: @marker_font_size, color: @font_color, weight: Magick::BoldWeight)
183
+ text_renderer.render(0, 0, x, y, Magick::CenterGravity)
236
184
  end
237
185
 
238
186
  # Helper Classes
239
-
187
+ #
188
+ # @private
240
189
  class PieSlice < Struct.new(:data_array, :options)
241
190
  attr_accessor :total
242
191
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gruff
4
+ class Renderer::Bezier
5
+ def initialize(args = {})
6
+ @color = args[:color]
7
+ @width = args[:width] || 1.0
8
+ end
9
+
10
+ def render(points)
11
+ draw = Renderer.instance.draw
12
+
13
+ draw.push
14
+ draw.stroke(@color)
15
+ draw.stroke_width(@width)
16
+ draw.fill_opacity(0.0)
17
+ draw.bezier(*points)
18
+ draw.pop
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gruff
4
+ class Renderer::Circle
5
+ def initialize(args = {})
6
+ @color = args[:color]
7
+ @width = args[:width] || 1.0
8
+ end
9
+
10
+ def render(origin_x, origin_y, perim_x, perim_y)
11
+ draw = Renderer.instance.draw
12
+
13
+ draw.push
14
+ draw.fill(@color)
15
+ draw.stroke(@color)
16
+ draw.stroke_width(@width)
17
+ draw.circle(origin_x, origin_y, perim_x, perim_y)
18
+ draw.pop
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gruff
4
+ class Renderer::DashLine
5
+ def initialize(args = {})
6
+ @color = args[:color]
7
+ @width = args[:width]
8
+ end
9
+
10
+ def render(start_x, start_y, end_x, end_y)
11
+ draw = Renderer.instance.draw
12
+
13
+ draw.push
14
+ draw.stroke_color(@color)
15
+ draw.fill_opacity(0.0)
16
+ draw.stroke_dasharray(10, 20)
17
+ draw.stroke_width(@width)
18
+ draw.line(start_x, start_y, end_x, end_y)
19
+ draw.pop
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gruff
4
+ class Renderer::Dot
5
+ def initialize(style, config)
6
+ @style = style
7
+ @color = config[:color]
8
+ @width = config[:width] || 1.0
9
+ end
10
+
11
+ def render(new_x, new_y, circle_radius)
12
+ draw = Renderer.instance.draw
13
+
14
+ # draw.push # TODO
15
+ draw.stroke_width(@width)
16
+ draw.stroke(@color)
17
+ draw.fill(@color)
18
+ if @style.to_s == 'square'
19
+ square(draw, new_x, new_y, circle_radius)
20
+ else
21
+ circle(draw, new_x, new_y, circle_radius)
22
+ end
23
+ # draw.pop # TODO
24
+ end
25
+
26
+ def circle(draw, new_x, new_y, circle_radius)
27
+ draw.circle(new_x, new_y, new_x - circle_radius, new_y)
28
+ end
29
+
30
+ def square(draw, new_x, new_y, circle_radius)
31
+ offset = (circle_radius * 0.8).to_i
32
+ corner1 = new_x - offset
33
+ corner2 = new_y - offset
34
+ corner3 = new_x + offset
35
+ corner4 = new_y + offset
36
+ draw.rectangle(corner1, corner2, corner3, corner4)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gruff
4
+ class Renderer::Ellipse
5
+ def initialize(args = {})
6
+ @color = args[:color]
7
+ @width = args[:width] || 1.0
8
+ end
9
+
10
+ def render(origin_x, origin_y, width, height, arc_start, arc_end)
11
+ draw = Renderer.instance.draw
12
+
13
+ draw.push
14
+ draw.stroke_width(@width)
15
+ draw.stroke(@color)
16
+ draw.fill('transparent')
17
+ draw.ellipse(origin_x, origin_y, width, height, arc_start, arc_end)
18
+ draw.pop
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gruff
4
+ class Renderer::Line
5
+ EPSILON = 0.001
6
+
7
+ def initialize(args = {})
8
+ @color = args[:color]
9
+ @width = args[:width]
10
+ end
11
+
12
+ def render(start_x, start_y, end_x, end_y)
13
+ # FIXME(uwe): Workaround for Issue #66
14
+ # https://github.com/topfunky/gruff/issues/66
15
+ # https://github.com/rmagick/rmagick/issues/82
16
+ # Remove if the issue gets fixed.
17
+ unless defined?(JRUBY_VERSION)
18
+ start_x += EPSILON
19
+ end_x += EPSILON
20
+ start_y += EPSILON
21
+ end_y += EPSILON
22
+ end
23
+
24
+ draw = Renderer.instance.draw
25
+
26
+ draw.push
27
+ draw.stroke(@color)
28
+ draw.fill(@color)
29
+ draw.stroke_width(@width) if @width
30
+ draw.line(start_x, start_y, end_x, end_y)
31
+ draw.pop
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gruff
4
+ class Renderer::Polygon
5
+ def initialize(args = {})
6
+ @color = args[:color]
7
+ @width = args[:width] || 1.0
8
+ @opacity = args[:opacity] || 1.0
9
+ end
10
+
11
+ def render(points)
12
+ draw = Renderer.instance.draw
13
+
14
+ draw.push
15
+ draw.stroke_width(@width)
16
+ draw.stroke(@color)
17
+ draw.fill(@color)
18
+ draw.fill_opacity(@opacity)
19
+ draw.polygon(*points)
20
+ draw.pop
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gruff
4
+ class Renderer::Polyline
5
+ def initialize(args = {})
6
+ @color = args[:color]
7
+ @width = args[:width]
8
+ end
9
+
10
+ def render(points)
11
+ draw = Renderer.instance.draw
12
+
13
+ draw.push
14
+ draw.stroke(@color)
15
+ draw.fill('transparent')
16
+ draw.stroke_width(@width)
17
+ draw.polyline(*points)
18
+ draw.pop
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Gruff
4
+ class Renderer::Rectangle
5
+ def initialize(args = {})
6
+ @color = args[:color]
7
+ end
8
+
9
+ def render(upper_left_x, upper_left_y, lower_right_x, lower_right_y)
10
+ draw = Renderer.instance.draw
11
+
12
+ draw.push
13
+ draw.stroke('transparent')
14
+ draw.fill(@color) if @color
15
+ draw.rectangle(upper_left_x, upper_left_y, lower_right_x, lower_right_y)
16
+ draw.pop
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module Gruff
6
+ # @private
7
+ class Renderer
8
+ include Singleton
9
+
10
+ attr_accessor :draw, :image, :scale
11
+
12
+ class << self
13
+ def setup(columns, rows, font, scale, theme_options)
14
+ draw = Magick::Draw.new
15
+ draw.font = font if font
16
+ # Scale down from 800x600 used to calculate drawing.
17
+ draw.scale(scale, scale)
18
+
19
+ image = Renderer.instance.background(columns, rows, scale, theme_options)
20
+
21
+ Renderer.instance.draw = draw
22
+ Renderer.instance.scale = scale
23
+ Renderer.instance.image = image
24
+ end
25
+
26
+ def setup_transparent_background(columns, rows)
27
+ image = Renderer.instance.render_transparent_background(columns, rows)
28
+ Renderer.instance.image = image
29
+ end
30
+
31
+ def background_image=(image)
32
+ Renderer.instance.image = image
33
+ end
34
+
35
+ def font=(font)
36
+ draw = Renderer.instance.draw
37
+ draw.font = font if font
38
+ end
39
+
40
+ def finish
41
+ draw = Renderer.instance.draw
42
+ image = Renderer.instance.image
43
+
44
+ draw.draw(image)
45
+ end
46
+
47
+ def write(file_name)
48
+ Renderer.instance.image.write(file_name)
49
+ end
50
+
51
+ def to_blob(file_format)
52
+ Renderer.instance.image.to_blob do
53
+ self.format = file_format
54
+ end
55
+ end
56
+ end
57
+
58
+ def background(columns, rows, scale, theme_options)
59
+ case theme_options[:background_colors]
60
+ when Array
61
+ gradated_background(columns, rows, theme_options[:background_colors][0], theme_options[:background_colors][1], theme_options[:background_direction])
62
+ when String
63
+ solid_background(columns, rows, theme_options[:background_colors])
64
+ else
65
+ image_background(scale, *theme_options[:background_image])
66
+ end
67
+ end
68
+
69
+ # Make a new image at the current size with a solid +color+.
70
+ def solid_background(columns, rows, color)
71
+ Magick::Image.new(columns, rows) do
72
+ self.background_color = color
73
+ end
74
+ end
75
+
76
+ # Use with a theme definition method to draw a gradated background.
77
+ def gradated_background(columns, rows, top_color, bottom_color, direct = :top_bottom)
78
+ gradient_fill = begin
79
+ case direct
80
+ when :bottom_top
81
+ Magick::GradientFill.new(0, 0, 100, 0, bottom_color, top_color)
82
+ when :left_right
83
+ Magick::GradientFill.new(0, 0, 0, 100, top_color, bottom_color)
84
+ when :right_left
85
+ Magick::GradientFill.new(0, 0, 0, 100, bottom_color, top_color)
86
+ when :topleft_bottomright
87
+ Magick::GradientFill.new(0, 100, 100, 0, top_color, bottom_color)
88
+ when :topright_bottomleft
89
+ Magick::GradientFill.new(0, 0, 100, 100, bottom_color, top_color)
90
+ else
91
+ Magick::GradientFill.new(0, 0, 100, 0, top_color, bottom_color)
92
+ end
93
+ end
94
+
95
+ image = Magick::Image.new(columns, rows, gradient_fill)
96
+ @gradated_background_retry_count = 0
97
+
98
+ image
99
+ rescue StandardError => e
100
+ @gradated_background_retry_count ||= 0
101
+ GC.start
102
+
103
+ if @gradated_background_retry_count < 3
104
+ @gradated_background_retry_count += 1
105
+ gradated_background(top_color, bottom_color, direct)
106
+ else
107
+ raise e
108
+ end
109
+ end
110
+
111
+ # Use with a theme to use an image (800x600 original) background.
112
+ def image_background(scale, image_path)
113
+ image = Magick::Image.read(image_path)
114
+ if scale != 1.0
115
+ image[0].resize!(scale) # TODO: Resize with new scale (crop if necessary for wide graph)
116
+ end
117
+ image[0]
118
+ end
119
+
120
+ # Use with a theme to make a transparent background
121
+ def render_transparent_background(columns, rows)
122
+ Magick::Image.new(columns, rows) do
123
+ self.background_color = 'transparent'
124
+ end
125
+ end
126
+ end
127
+ end