gruff 0.14.0-java → 0.17.0-java

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +28 -12
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +20 -24
  5. data/CHANGELOG.md +52 -0
  6. data/README.md +10 -3
  7. data/gruff.gemspec +9 -10
  8. data/lib/gruff/accumulator_bar.rb +1 -1
  9. data/lib/gruff/area.rb +6 -4
  10. data/lib/gruff/bar.rb +53 -31
  11. data/lib/gruff/base.rb +292 -184
  12. data/lib/gruff/bezier.rb +4 -2
  13. data/lib/gruff/box_plot.rb +180 -0
  14. data/lib/gruff/bullet.rb +6 -6
  15. data/lib/gruff/candlestick.rb +120 -0
  16. data/lib/gruff/dot.rb +11 -12
  17. data/lib/gruff/font.rb +3 -0
  18. data/lib/gruff/helper/bar_conversion.rb +6 -10
  19. data/lib/gruff/helper/bar_mixin.rb +25 -0
  20. data/lib/gruff/helper/bar_value_label.rb +24 -40
  21. data/lib/gruff/helper/stacked_mixin.rb +19 -1
  22. data/lib/gruff/histogram.rb +9 -5
  23. data/lib/gruff/line.rb +49 -48
  24. data/lib/gruff/mini/legend.rb +11 -11
  25. data/lib/gruff/net.rb +23 -18
  26. data/lib/gruff/patch/rmagick.rb +0 -1
  27. data/lib/gruff/patch/string.rb +1 -0
  28. data/lib/gruff/pie.rb +26 -12
  29. data/lib/gruff/renderer/dash_line.rb +3 -2
  30. data/lib/gruff/renderer/dot.rb +28 -15
  31. data/lib/gruff/renderer/line.rb +1 -3
  32. data/lib/gruff/renderer/rectangle.rb +6 -2
  33. data/lib/gruff/renderer/renderer.rb +4 -8
  34. data/lib/gruff/renderer/text.rb +7 -1
  35. data/lib/gruff/scatter.rb +64 -56
  36. data/lib/gruff/side_bar.rb +64 -30
  37. data/lib/gruff/side_stacked_bar.rb +43 -54
  38. data/lib/gruff/spider.rb +52 -18
  39. data/lib/gruff/stacked_area.rb +18 -8
  40. data/lib/gruff/stacked_bar.rb +59 -29
  41. data/lib/gruff/store/xy_data.rb +2 -0
  42. data/lib/gruff/version.rb +1 -1
  43. data/lib/gruff.rb +67 -58
  44. metadata +17 -16
  45. data/.rubocop_todo.yml +0 -116
  46. data/lib/gruff/scene.rb +0 -200
  47. data/lib/gruff/store/custom_data.rb +0 -36
data/lib/gruff/bezier.rb CHANGED
@@ -22,15 +22,17 @@ class Gruff::Bezier < Gruff::Base
22
22
  private
23
23
 
24
24
  def draw_graph
25
- x_increment = @graph_width / (column_count - 1).to_f
25
+ x_increment = @graph_width / (column_count - 1)
26
26
 
27
27
  store.norm_data.each do |data_row|
28
+ next if data_row[1].empty?
29
+
28
30
  poly_points = []
29
31
 
30
32
  data_row[1].each_with_index do |data_point, index|
31
33
  # Use incremented x and scaled y
32
34
  new_x = @graph_left + (x_increment * index)
33
- new_y = @graph_top + (@graph_height - data_point * @graph_height)
35
+ new_y = @graph_top + (@graph_height - (data_point * @graph_height))
34
36
 
35
37
  if index == 0 && RUBY_PLATFORM != 'java'
36
38
  poly_points << new_x
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Here's how to set up a Gruff::BoxPlot.
5
+ #
6
+ # g = Gruff::BoxPlot.new
7
+ # g.data "A", [2, 3, 5, 6, 8, 10, 11, 15, 17, 20, 28, 29, 33, 34, 45, 46, 49, 61]
8
+ # g.data "B", [3, 4, 34, 35, 38, 39, 45, 60, 61, 69, 80, 130]
9
+ # g.data "C", [4, 40, 41, 46, 57, 64, 77, 76, 79, 78, 99, 153]
10
+ # g.write("box_plot.png")
11
+ #
12
+ class Gruff::BoxPlot < Gruff::Base
13
+ # Specifies the filling opacity in area graph. Default is +0.2+.
14
+ attr_writer :fill_opacity
15
+
16
+ # Specifies the stroke width in line. Default is +3.0+.
17
+ attr_writer :stroke_width
18
+
19
+ # Can be used to adjust the spaces between the bars.
20
+ # Accepts values between 0.00 and 1.00 where 0.00 means no spacing at all
21
+ # and 1 means that each bars' width is nearly 0 (so each bar is a simple
22
+ # line with no x dimension).
23
+ #
24
+ # Default value is +0.8+.
25
+ def spacing_factor=(space_percent)
26
+ raise ArgumentError, 'spacing_factor must be between 0.00 and 1.00' unless (space_percent >= 0) && (space_percent <= 1)
27
+
28
+ @spacing_factor = (1 - space_percent)
29
+ end
30
+
31
+ private
32
+
33
+ def initialize_attributes
34
+ super
35
+ @fill_opacity = 0.2
36
+ @stroke_width = 3.0
37
+ @spacing_factor = 0.8
38
+ end
39
+
40
+ def draw_graph
41
+ # Setup the BarConversion Object
42
+ conversion = Gruff::BarConversion.new(
43
+ top: @graph_top, bottom: @graph_bottom,
44
+ minimum_value: minimum_value, maximum_value: maximum_value, spread: @spread
45
+ )
46
+
47
+ width = (@graph_width - calculate_spacing) / column_count
48
+ bar_width = width * @spacing_factor
49
+ padding = width - bar_width
50
+
51
+ normalized_boxes.each_with_index do |box, index|
52
+ next if box.points.empty?
53
+
54
+ left_x = @graph_left + (width * index) + (padding / 2.0)
55
+ right_x = left_x + bar_width
56
+ center_x = (left_x + right_x) / 2.0
57
+
58
+ first_y, = conversion.get_top_bottom_scaled(box.first_quartile)
59
+ third_y, = conversion.get_top_bottom_scaled(box.third_quartile)
60
+ Gruff::Renderer::Rectangle.new(renderer, color: box.color, width: @stroke_width, opacity: @fill_opacity)
61
+ .render(left_x, first_y, right_x, third_y)
62
+
63
+ median_y, = conversion.get_top_bottom_scaled(box.median)
64
+ Gruff::Renderer::Line.new(renderer, color: box.color, width: @stroke_width * 2).render(left_x, median_y, right_x, median_y)
65
+
66
+ minmax_left_x = left_x + (bar_width / 4.0)
67
+ minmax_right_x = right_x - (bar_width / 4.0)
68
+ min_y, = conversion.get_top_bottom_scaled(box.lower_whisker)
69
+ Gruff::Renderer::Line.new(renderer, color: box.color, width: @stroke_width).render(minmax_left_x, min_y, minmax_right_x, min_y)
70
+ Gruff::Renderer::DashLine.new(renderer, color: box.color, width: @stroke_width, dasharray: [@stroke_width, @stroke_width * 2])
71
+ .render(center_x, min_y, center_x, first_y)
72
+
73
+ max_y, = conversion.get_top_bottom_scaled(box.upper_whisker)
74
+ Gruff::Renderer::Line.new(renderer, color: box.color, width: @stroke_width).render(minmax_left_x, max_y, minmax_right_x, max_y)
75
+ Gruff::Renderer::DashLine.new(renderer, color: box.color, width: @stroke_width, dasharray: [@stroke_width, @stroke_width * 2])
76
+ .render(center_x, max_y, center_x, third_y)
77
+
78
+ box.lower_outliers.each do |outlier|
79
+ outlier_y, = conversion.get_top_bottom_scaled(outlier)
80
+ Gruff::Renderer::Dot.new(renderer, :circle, color: box.color, opacity: @fill_opacity).render(center_x, outlier_y, @stroke_width * 2)
81
+ end
82
+
83
+ box.upper_outliers.each do |outlier|
84
+ outlier_y, = conversion.get_top_bottom_scaled(outlier)
85
+ Gruff::Renderer::Dot.new(renderer, :circle, color: box.color, opacity: @fill_opacity).render(center_x, outlier_y, @stroke_width * 2)
86
+ end
87
+
88
+ draw_label(center_x, index)
89
+ end
90
+ end
91
+
92
+ def normalized_boxes
93
+ @normalized_boxes ||= store.norm_data.map { |data| Gruff::BoxPlot::BoxData.new(data.label, data.points, data.color) }
94
+ end
95
+
96
+ def column_count
97
+ normalized_boxes.size
98
+ end
99
+
100
+ def calculate_spacing
101
+ @scale * (column_count - 1)
102
+ end
103
+
104
+ # @private
105
+ class BoxData < Struct.new(:label, :points, :color)
106
+ def initialize(label, points, color)
107
+ super(label, points.sort, color)
108
+ end
109
+
110
+ def min
111
+ points.first || 0
112
+ end
113
+
114
+ def max
115
+ points.last || 0
116
+ end
117
+
118
+ def min_whisker
119
+ [min, first_quartile - (1.5 * interquartile_range)].max
120
+ end
121
+
122
+ def max_whisker
123
+ [max, third_quartile + (1.5 * interquartile_range)].min
124
+ end
125
+
126
+ def upper_whisker
127
+ max = max_whisker
128
+ points.select { |point| point <= max }.max
129
+ end
130
+
131
+ def lower_whisker
132
+ min = min_whisker
133
+ points.select { |point| point >= min }.min
134
+ end
135
+
136
+ def median
137
+ if points.size.zero?
138
+ 0
139
+ elsif points.size.odd?
140
+ points[points.size / 2]
141
+ else
142
+ (points[points.size / 2] + points[(points.size / 2) - 1]) / 2.0
143
+ end
144
+ end
145
+
146
+ def first_quartile
147
+ if points.size.zero?
148
+ 0
149
+ elsif points.size.odd?
150
+ points[points.size / 4]
151
+ else
152
+ (points[points.size / 4] + points[(points.size / 4) - 1]) / 2.0
153
+ end
154
+ end
155
+
156
+ def third_quartile
157
+ if points.size.zero?
158
+ 0
159
+ elsif points.size.odd?
160
+ points[(points.size * 3) / 4]
161
+ else
162
+ (points[(points.size * 3) / 4] + points[((points.size * 3) / 4) - 1]) / 2.0
163
+ end
164
+ end
165
+
166
+ def lower_outliers
167
+ min = lower_whisker
168
+ points.select { |point| point < min }
169
+ end
170
+
171
+ def upper_outliers
172
+ max = upper_whisker
173
+ points.select { |point| point > max }
174
+ end
175
+
176
+ def interquartile_range
177
+ third_quartile - first_quartile
178
+ end
179
+ end
180
+ end
data/lib/gruff/bullet.rb CHANGED
@@ -64,26 +64,26 @@ class Gruff::Bullet < Gruff::Base
64
64
  rect_renderer = Gruff::Renderer::Rectangle.new(renderer, color: @colors[0])
65
65
  rect_renderer.render(graph_left, 0, graph_left + graph_width, graph_height)
66
66
 
67
- [:high, :low].each_with_index do |indicator, index|
67
+ %i[high low].each_with_index do |indicator, index|
68
68
  next unless @options.key?(indicator)
69
69
 
70
- indicator_width_x = graph_left + graph_width * (@options[indicator] / maximum_value)
70
+ indicator_width_x = graph_left + (graph_width * (@options[indicator] / maximum_value))
71
71
 
72
72
  rect_renderer = Gruff::Renderer::Rectangle.new(renderer, color: @colors[index + 1])
73
73
  rect_renderer.render(graph_left, 0, indicator_width_x, graph_height)
74
74
  end
75
75
 
76
76
  if @options.key?(:target)
77
- target_x = graph_left + graph_width * (@options[:target] / maximum_value)
77
+ target_x = graph_left + (graph_width * (@options[:target] / maximum_value))
78
78
  half_thickness = thickness / 2.0
79
79
 
80
80
  rect_renderer = Gruff::Renderer::Rectangle.new(renderer, color: @marker_color)
81
- rect_renderer.render(target_x, half_thickness, target_x + half_thickness, thickness * 2 + half_thickness)
81
+ rect_renderer.render(target_x, half_thickness, target_x + half_thickness, (thickness * 2) + half_thickness)
82
82
  end
83
83
 
84
84
  # Value
85
85
  rect_renderer = Gruff::Renderer::Rectangle.new(renderer, color: @marker_color)
86
- rect_renderer.render(graph_left, thickness, graph_left + graph_width * (@value / maximum_value), thickness * 2)
86
+ rect_renderer.render(graph_left, thickness, graph_left + (graph_width * (@value / maximum_value)), thickness * 2)
87
87
  end
88
88
 
89
89
  private
@@ -94,6 +94,6 @@ private
94
94
  font_height = calculate_caps_height(@title_font)
95
95
 
96
96
  text_renderer = Gruff::Renderer::Text.new(renderer, @title, font: @title_font)
97
- text_renderer.add_to_render_queue(1.0, 1.0, font_height / 2, font_height / 2, Magick::NorthWestGravity)
97
+ text_renderer.add_to_render_queue(1.0, 1.0, font_height / 2.0, font_height / 2.0, Magick::NorthWestGravity)
98
98
  end
99
99
  end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Here's how to set up a Gruff::Candlestick.
5
+ #
6
+ # g = Gruff::Candlestick.new
7
+ # g.data low: 79.30, high: 93.10, open: 79.44, close: 91.20
8
+ # g.data low: 89.14, high: 106.42, open: 91.28, close: 106.26
9
+ # g.data low: 107.89, high: 131.00, open: 108.20, close: 129.04
10
+ # g.data low: 103.10, high: 137.98, open: 132.76, close: 115.81
11
+ # g.write("candlestick.png")
12
+ #
13
+ class Gruff::Candlestick < Gruff::Base
14
+ # Allow for vertical marker lines.
15
+ attr_writer :show_vertical_markers
16
+
17
+ # Specifies the filling opacity in area graph. Default is +0.4+.
18
+ attr_writer :fill_opacity
19
+
20
+ # Specifies the stroke width in line. Default is +2.0+.
21
+ attr_writer :stroke_width
22
+
23
+ # Specifies the color with up bar. Default is +'#579773'+.
24
+ attr_writer :up_color
25
+
26
+ # Specifies the color with down bar. Default is +'#eb5242'+.
27
+ attr_writer :down_color
28
+
29
+ # Can be used to adjust the spaces between the bars.
30
+ # Accepts values between 0.00 and 1.00 where 0.00 means no spacing at all
31
+ # and 1 means that each bars' width is nearly 0 (so each bar is a simple
32
+ # line with no x dimension).
33
+ #
34
+ # Default value is +0.9+.
35
+ def spacing_factor=(space_percent)
36
+ raise ArgumentError, 'spacing_factor must be between 0.00 and 1.00' unless (space_percent >= 0) && (space_percent <= 1)
37
+
38
+ @spacing_factor = (1 - space_percent)
39
+ end
40
+
41
+ # The sort feature is not supported in this graph.
42
+ def sort=(_value)
43
+ raise 'Not support #sort= in Gruff::Candlestick'
44
+ end
45
+
46
+ # The sort feature is not supported in this graph.
47
+ def sorted_drawing=(_value)
48
+ raise 'Not support #sorted_drawing= in Gruff::Candlestick'
49
+ end
50
+
51
+ def data(low:, high:, open:, close:)
52
+ super('', [low, high, open, close])
53
+ end
54
+
55
+ private
56
+
57
+ def initialize_attributes
58
+ super
59
+ @show_vertical_markers = false
60
+ @fill_opacity = 0.4
61
+ @spacing_factor = 0.9
62
+ @stroke_width = 2.0
63
+ @up_color = '#579773'
64
+ @down_color = '#eb5242'
65
+
66
+ @hide_legend = true
67
+ end
68
+
69
+ def draw_graph
70
+ # Setup the BarConversion Object
71
+ conversion = Gruff::BarConversion.new(
72
+ top: @graph_top, bottom: @graph_bottom,
73
+ minimum_value: minimum_value, maximum_value: maximum_value, spread: @spread
74
+ )
75
+
76
+ width = (@graph_width - calculate_spacing) / column_count
77
+ bar_width = width * @spacing_factor
78
+ padding = width - bar_width
79
+
80
+ normalized_candlesticks.each_with_index do |candlestick, index|
81
+ left_x = @graph_left + (width * index) + (padding / 2.0)
82
+ right_x = left_x + bar_width
83
+ center_x = (left_x + right_x) / 2.0
84
+ color = candlestick.close >= candlestick.open ? @up_color : @down_color
85
+
86
+ draw_label(center_x, index) { draw_marker_vertical_line(center_x) if show_marker_vertical_line? }
87
+
88
+ open_y, = conversion.get_top_bottom_scaled(candlestick.open)
89
+ close_y, = conversion.get_top_bottom_scaled(candlestick.close)
90
+ Gruff::Renderer::Rectangle.new(renderer, color: color, opacity: @fill_opacity, width: @stroke_width).render(left_x, open_y, right_x, close_y)
91
+
92
+ low_y, = conversion.get_top_bottom_scaled(candlestick.low)
93
+ y = open_y < close_y ? close_y : open_y
94
+ Gruff::Renderer::Line.new(renderer, color: color, width: @stroke_width).render(center_x, low_y, center_x, y)
95
+ high_y, = conversion.get_top_bottom_scaled(candlestick.high)
96
+ y = open_y > close_y ? close_y : open_y
97
+ Gruff::Renderer::Line.new(renderer, color: color, width: @stroke_width).render(center_x, y, center_x, high_y)
98
+ end
99
+ end
100
+
101
+ def normalized_candlesticks
102
+ @normalized_candlesticks ||= store.norm_data.map { |data| Gruff::Candlestick::CandlestickData.new(*data.points) }
103
+ end
104
+
105
+ def column_count
106
+ normalized_candlesticks.size
107
+ end
108
+
109
+ def calculate_spacing
110
+ @scale * (column_count - 1)
111
+ end
112
+
113
+ def show_marker_vertical_line?
114
+ !@hide_line_markers && @show_vertical_markers
115
+ end
116
+
117
+ # @private
118
+ class CandlestickData < Struct.new(:low, :high, :open, :close)
119
+ end
120
+ end
data/lib/gruff/dot.rb CHANGED
@@ -14,32 +14,33 @@
14
14
  # g.write('dot.png')
15
15
  #
16
16
  class Gruff::Dot < Gruff::Base
17
- private
18
-
19
- def initialize_attributes
17
+ def initialize(*)
20
18
  super
21
19
  @has_left_labels = true
20
+ @dot_style = 'circle'
22
21
  end
23
22
 
23
+ private
24
+
24
25
  def draw_graph
25
26
  # Setup spacing.
26
27
  #
27
28
  spacing_factor = 1.0
28
29
 
29
- items_width = @graph_height / column_count.to_f
30
+ items_width = @graph_height / column_count
30
31
  item_width = items_width * spacing_factor / store.length
31
32
  padding = (items_width * (1 - spacing_factor)) / 2
32
33
 
33
34
  store.norm_data.each_with_index do |data_row, row_index|
34
35
  data_row.points.each_with_index do |data_point, point_index|
35
36
  x_pos = @graph_left + (data_point * @graph_width)
36
- y_pos = @graph_top + (items_width * point_index) + padding + (items_width.to_f / 2.0).round
37
+ y_pos = @graph_top + (items_width * point_index) + padding + (items_width / 2.0)
37
38
 
38
39
  if row_index == 0
39
40
  Gruff::Renderer::Line.new(renderer, color: @marker_color).render(@graph_left, y_pos, @graph_left + @graph_width, y_pos)
40
41
  end
41
42
 
42
- Gruff::Renderer::Circle.new(renderer, color: data_row.color).render(x_pos, y_pos, x_pos + (item_width.to_f / 3.0).round, y_pos)
43
+ Gruff::Renderer::Dot.new(renderer, @dot_style, color: data_row.color).render(x_pos, y_pos, item_width / 3.0)
43
44
 
44
45
  draw_label(y_pos, point_index)
45
46
  end
@@ -51,11 +52,9 @@ private
51
52
  return if @hide_line_markers
52
53
 
53
54
  (0..marker_count).each do |index|
54
- marker_label = BigDecimal(index.to_s) * BigDecimal(@increment.to_s) + BigDecimal(minimum_value.to_s)
55
- x = @graph_left + (marker_label - minimum_value) * @graph_width / @spread
56
-
57
- line_renderer = Gruff::Renderer::Line.new(renderer, color: @marker_color, shadow_color: @marker_shadow_color)
58
- line_renderer.render(x, @graph_bottom, x, @graph_bottom + 5)
55
+ marker_label = (BigDecimal(index.to_s) * BigDecimal(@increment.to_s)) + BigDecimal(minimum_value.to_s)
56
+ x = @graph_left + ((marker_label - minimum_value) * @graph_width / @spread)
57
+ draw_marker_vertical_line(x, tick_mark_mode: true)
59
58
 
60
59
  unless @hide_line_numbers
61
60
  label = y_axis_label(marker_label, @increment)
@@ -70,7 +69,7 @@ private
70
69
 
71
70
  def draw_label(y_offset, index)
72
71
  draw_unique_label(index) do
73
- draw_label_at(@graph_left - LABEL_MARGIN, 1.0, 0.0, y_offset, @labels[index], Magick::EastGravity)
72
+ draw_label_at(@graph_left - LABEL_MARGIN, 1.0, 0.0, y_offset, @labels[index], gravity: Magick::EastGravity)
74
73
  end
75
74
  end
76
75
  end
data/lib/gruff/font.rb CHANGED
@@ -3,7 +3,10 @@
3
3
  # Handle font setting to draw texts
4
4
  class Gruff::Font
5
5
  BOLD_PATH = File.expand_path(File.join(__FILE__, '../../../assets/fonts/Roboto-Bold.ttf')).freeze
6
+ private_constant :BOLD_PATH
7
+
6
8
  REGULAR_PATH = File.expand_path(File.join(__FILE__, '../../../assets/fonts/Roboto-Regular.ttf')).freeze
9
+ private_constant :REGULAR_PATH
7
10
 
8
11
  # Get/set font path.
9
12
  attr_accessor :path
@@ -13,8 +13,6 @@
13
13
  #
14
14
  # @private
15
15
  class Gruff::BarConversion
16
- attr_writer :mode
17
-
18
16
  def initialize(top:, bottom:, minimum_value:, maximum_value:, spread:)
19
17
  @graph_top = top
20
18
  @graph_height = bottom - top
@@ -36,25 +34,23 @@ class Gruff::BarConversion
36
34
  end
37
35
 
38
36
  def get_top_bottom_scaled(data_point)
37
+ data_point = data_point.to_f
39
38
  result = []
40
39
 
41
40
  case @mode
42
41
  when 1
43
42
  # minimum value >= 0 ( only positive values )
44
- result[0] = @graph_top + @graph_height * (1 - data_point)
43
+ result[0] = @graph_top + (@graph_height * (1 - data_point))
45
44
  result[1] = @graph_top + @graph_height
46
45
  when 2
47
46
  # only negative values
48
47
  result[0] = @graph_top
49
- result[1] = @graph_top + @graph_height * (1 - data_point)
48
+ result[1] = @graph_top + (@graph_height * (1 - data_point))
50
49
  when 3
51
50
  # positive and negative values
52
- val = data_point - @minimum_value / @spread
53
- result[0] = @graph_top + @graph_height * (1 - (val - @zero))
54
- result[1] = @graph_top + @graph_height * (1 - @zero)
55
- else
56
- result[0] = 0.0
57
- result[1] = 0.0
51
+ val = data_point - (@minimum_value / @spread)
52
+ result[0] = @graph_top + (@graph_height * (1 - (val - @zero)))
53
+ result[1] = @graph_top + (@graph_height * (1 - @zero))
58
54
  end
59
55
 
60
56
  result
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @private
4
+ module Gruff::Base::BarMixin
5
+ def normalized_group_bars
6
+ @normalized_group_bars ||= begin
7
+ group_bars = Array.new(column_count) { [] }
8
+ store.norm_data.each_with_index do |data_row, row_index|
9
+ data_row.points.each_with_index do |data_point, point_index|
10
+ group_bars[point_index] << BarData.new(data_point, store.data[row_index].points[point_index], data_row.color)
11
+ end
12
+
13
+ # Adjust the number of each group with empty bar
14
+ (data_row.points.size..(column_count - 1)).each do |index|
15
+ group_bars[index] << BarData.new(0, nil, data_row.color)
16
+ end
17
+ end
18
+ group_bars
19
+ end
20
+ end
21
+
22
+ # @private
23
+ class BarData < Struct.new(:point, :value, :color)
24
+ end
25
+ end
@@ -4,6 +4,20 @@
4
4
  module Gruff::BarValueLabel
5
5
  using String::GruffCommify
6
6
 
7
+ # @private
8
+ def self.metrics(value, format, proc_text_metrics)
9
+ val = begin
10
+ if format.is_a?(Proc)
11
+ format.call(value)
12
+ else
13
+ sprintf(format || '%.2f', value).commify
14
+ end
15
+ end
16
+
17
+ metrics = proc_text_metrics.call(val)
18
+ [val, metrics]
19
+ end
20
+
7
21
  # @private
8
22
  class Base
9
23
  attr_reader :coordinate, :value
@@ -16,53 +30,23 @@ module Gruff::BarValueLabel
16
30
 
17
31
  # @private
18
32
  class Bar < Base
19
- def prepare_rendering(format, _bar_width = 0)
20
- left_x, left_y, right_x, _right_y = @coordinate
21
- if format.is_a?(Proc)
22
- val = format.call(@value)
23
- else
24
- val = sprintf(format || '%.2f', @value).commify
25
- end
33
+ def prepare_rendering(format, proc_text_metrics)
34
+ left_x, left_y, _right_x, _right_y = @coordinate
35
+ val, metrics = Gruff::BarValueLabel.metrics(@value, format, proc_text_metrics)
26
36
 
27
- y = @value >= 0 ? left_y - 30 : left_y + 12
28
- yield left_x + (right_x - left_x) / 2, y, val
37
+ y = @value >= 0 ? left_y - metrics.height - 5 : left_y + 5
38
+ yield left_x, y, val, metrics.width, metrics.height
29
39
  end
30
40
  end
31
41
 
32
42
  # @private
33
43
  class SideBar < Base
34
- def prepare_rendering(format, bar_width = 0)
35
- left_x, _left_y, right_x, right_y = @coordinate
36
- if format.is_a?(Proc)
37
- val = format.call(@value)
38
- else
39
- val = sprintf(format || '%.2f', @value).commify
40
- end
41
-
42
- x = @value >= 0 ? right_x + 40 : left_x - 40
43
- yield x, right_y - bar_width / 2, val
44
- end
45
- end
46
-
47
- # @private
48
- class StackedBar
49
- def initialize
50
- @bars = []
51
- end
52
-
53
- def add(bar, index)
54
- bars = @bars[index] || []
55
- bars << bar
56
- @bars[index] = bars
57
- end
44
+ def prepare_rendering(format, proc_text_metrics)
45
+ left_x, left_y, right_x, _right_y = @coordinate
46
+ val, metrics = Gruff::BarValueLabel.metrics(@value, format, proc_text_metrics)
58
47
 
59
- def prepare_rendering(format, bar_width = 0, &block)
60
- @bars.each do |bars|
61
- value = bars.sum(&:value)
62
- bar = bars.last
63
- bar_value_label = bar.class.new(bar.coordinate, value)
64
- bar_value_label.prepare_rendering(format, bar_width, &block)
65
- end
48
+ x = @value >= 0 ? right_x + 10 : left_x - metrics.width - 10
49
+ yield x, left_y, val, metrics.width, metrics.height
66
50
  end
67
51
  end
68
52
  end
@@ -16,7 +16,25 @@ module Gruff::Base::StackedMixin
16
16
 
17
17
  max_hash.each_key do |key|
18
18
  self.maximum_value = max_hash[key] if max_hash[key] > maximum_value
19
+ self.minimum_value = max_hash[key] if max_hash[key] < minimum_value
19
20
  end
20
- self.minimum_value = 0
21
+
22
+ raise "Can't handle negative values in stacked graph" if minimum_value < 0
23
+ end
24
+
25
+ def normalized_stacked_bars
26
+ @normalized_stacked_bars ||= begin
27
+ stacked_bars = Array.new(column_count) { [] }
28
+ store.norm_data.each_with_index do |data_row, row_index|
29
+ data_row.points.each_with_index do |data_point, point_index|
30
+ stacked_bars[point_index] << BarData.new(data_point, store.data[row_index].points[point_index], data_row.color)
31
+ end
32
+ end
33
+ stacked_bars
34
+ end
35
+ end
36
+
37
+ # @private
38
+ class BarData < Struct.new(:point, :value, :color)
21
39
  end
22
40
  end
@@ -29,7 +29,7 @@ class Gruff::Histogram < Gruff::Bar
29
29
  end
30
30
 
31
31
  def data(name, data_points = [], color = nil)
32
- @data << [name, data_points, color]
32
+ @data << [name, Array(data_points), color]
33
33
  end
34
34
 
35
35
  private
@@ -44,11 +44,15 @@ private
44
44
 
45
45
  def setup_data
46
46
  @data.each do |(name, data_points, color)|
47
- bins, freqs = HistogramArray.new(data_points).histogram(bin_width: @bin_width, min: @minimum_bin, max: @maximum_bin)
48
- bins.each_with_index do |bin, index|
49
- @labels[index] = bin
47
+ if data_points.empty?
48
+ store.add(name, [], color)
49
+ else
50
+ bins, freqs = HistogramArray.new(data_points).histogram(bin_width: @bin_width, min: @minimum_bin, max: @maximum_bin)
51
+ bins.each_with_index do |bin, index|
52
+ @labels[index] = bin
53
+ end
54
+ store.add(name, freqs, color)
50
55
  end
51
- store.add(name, freqs, color)
52
56
  end
53
57
 
54
58
  super