Skip to content

Commit 22184ed

Browse files
authored
Floating legend (#679)
1 parent 094a92f commit 22184ed

473 files changed

Lines changed: 172 additions & 274 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

lib/gruff/bar.rb

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,6 @@ def hide_left_label_area?
9090
@hide_line_markers && @y_axis_label.nil?
9191
end
9292

93-
# @rbs return: bool
94-
def hide_bottom_label_area?
95-
hide_labels? && @x_axis_label.nil? && @legend_at_bottom == false
96-
end
97-
9893
# @rbs return: void
9994
def setup_graph_measurements
10095
super

lib/gruff/base.rb

Lines changed: 89 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ module Gruff
2323
# A common base class inherited from class of drawing a graph.
2424
class Base
2525
# Space around text elements. Mostly used for vertical spacing.
26-
LEGEND_MARGIN = 20.0
26+
LEGEND_MARGIN = 10.0
2727
TITLE_MARGIN = 20.0
2828
LABEL_MARGIN = 15.0
2929
DEFAULT_MARGIN = 20.0
@@ -48,9 +48,6 @@ class Base
4848
# Blank space below the title. Default is +20+.
4949
attr_writer :title_margin #: Float | Integer
5050

51-
# Blank space below the legend. Default is +20+.
52-
attr_writer :legend_margin #: Float | Integer
53-
5451
# Truncates labels if longer than max specified.
5552
attr_writer :label_max_size #: Float | Integer
5653

@@ -115,13 +112,16 @@ class Base
115112
# first. This does not affect the legend. Default is +false+.
116113
attr_writer :sorted_drawing #: bool
117114

118-
# Display the legend under the graph. Default is +false+.
119-
attr_writer :legend_at_bottom #: bool
115+
# Blank space below the legend. Default is +20+.
116+
attr_writer :legend_margin #: Float | Integer
120117

121-
# Optionally set the size of the colored box by each item in the legend.
122-
# Default is +20.0+.
123-
#
124-
# Will be scaled down if graph is smaller than 800px wide.
118+
# Set the vertical spacing between individual legend items. Default is +5.0+.
119+
attr_writer :legend_spacing #: Float | Integer
120+
121+
# Set the inner padding between the legend frame and its contents. Default is +7.0+.
122+
attr_writer :legend_padding #: Float | Integer
123+
124+
# Optionally set the size of the colored box by each item in the legend. Default is +20.0+.
125125
attr_writer :legend_box_size #: Float | Integer
126126

127127
# If one numerical argument is given, the graph is drawn at 4/3 ratio
@@ -190,19 +190,22 @@ def initialize_attributes
190190

191191
@title_font = Gruff::Font.new(size: 36.0, bold: true)
192192
@marker_font = Gruff::Font.new(size: 21.0)
193-
@legend_font = Gruff::Font.new(size: 20.0)
193+
@legend_font = Gruff::Font.new(size: 12.0)
194194
@no_data_font = Gruff::Font.new(size: 80.0)
195195

196196
@label_margin = LABEL_MARGIN
197197
@top_margin = @bottom_margin = @left_margin = @right_margin = DEFAULT_MARGIN
198-
@legend_margin = LEGEND_MARGIN
199198
@title_margin = TITLE_MARGIN
200199

201-
@legend_box_size = 20.0
200+
@legend_margin = LEGEND_MARGIN
201+
@legend_spacing = 5.0
202+
@legend_padding = 7.0
203+
@legend_box_size = 10.0
204+
@legend_position = :top_right
202205

203206
@no_data_message = 'No Data'
204207

205-
@hide_line_markers = @hide_legend = @hide_title = @hide_line_numbers = @legend_at_bottom = false
208+
@hide_line_markers = @hide_legend = @hide_title = @hide_line_numbers = false
206209
@label_max_size = 0
207210
@label_truncation_style = :absolute
208211
@label_rotation = 0
@@ -255,13 +258,34 @@ def label_rotation=(rotation)
255258
@label_rotation = rotation.to_f
256259
end
257260

261+
# Set the corner position of the floating legend. Accepts +:top_right+ (default),
262+
# +:top_left+, +:bottom_right+, or +:bottom_left+.
263+
#
264+
# @param position [Symbol] the position.
265+
# @rbs position: Symbol
266+
# @rbs return: void
267+
def legend_position=(position)
268+
unless %i[top_right top_left bottom_right bottom_left].include?(position)
269+
raise ArgumentError, 'Invalid legend position. Must be :top_right, :top_left, :bottom_right, or :bottom_left.'
270+
end
271+
272+
@legend_position = position
273+
end
274+
258275
# Height of staggering between labels.
259276
# @deprecated
260277
# @rbs return: void
261278
def label_stagger_height=(_value)
262279
warn '#label_stagger_height= is deprecated. It is no longer effective.'
263280
end
264281

282+
# Set the legend position to the bottom of the graph.
283+
# @deprecated
284+
# @rbs return: void
285+
def legend_at_bottom=(_value)
286+
warn '#legend_at_bottom= is deprecated. It is no longer effective.'
287+
end
288+
265289
# Set the large title of the graph displayed at the top.
266290
# You can draw a multi-line title by putting a line break in the string
267291
# or by setting an array as argument.
@@ -629,11 +653,11 @@ def draw
629653

630654
setup_drawing
631655

632-
draw_legend
633656
draw_line_markers
634657
draw_axis_labels
635658
draw_title
636659
draw_graph
660+
draw_legend
637661
end
638662

639663
protected
@@ -822,46 +846,57 @@ def center(size)
822846
def draw_legend
823847
return if @hide_legend
824848

825-
legend_labels = store.data.map(&:label)
826-
legend_square_width = @legend_box_size # small square with color of this item
827-
legend_label_lines = calculate_legend_label_widths_for_each_line(legend_labels, legend_square_width)
828-
line_height = [legend_caps_height, legend_square_width].max + @legend_margin
829-
830-
current_y_offset = begin
831-
if @legend_at_bottom
832-
@graph_bottom + @legend_margin + labels_caps_height + @label_margin + (@x_axis_label ? (@label_margin * 2) + marker_caps_height : 0)
833-
else
834-
hide_title? ? @top_margin + @title_margin : @top_margin + @title_margin + title_caps_height
835-
end
849+
legend_labels = store.data.map(&:label).reject(&:empty?)
850+
return if legend_labels.empty?
851+
852+
line_height = [legend_caps_height, @legend_box_size].max + @legend_spacing
853+
max_legend_label_width = legend_labels.map { |l| calculate_width(@legend_font, l) }.max || 0.0
854+
855+
legend_content_width = (@legend_box_size * 1.5) + max_legend_label_width
856+
legend_content_height = (line_height * (legend_labels.size - 1)) + @legend_box_size
857+
858+
case @legend_position
859+
when :top_left
860+
current_x_offset = @graph_left + @legend_margin
861+
current_y_offset = @graph_top + @legend_margin + @legend_spacing
862+
when :bottom_right
863+
current_x_offset = @graph_right - @legend_margin - legend_content_width
864+
current_y_offset = @graph_bottom - @legend_margin - legend_content_height
865+
when :bottom_left
866+
current_x_offset = @graph_left + @legend_margin
867+
current_y_offset = @graph_bottom - @legend_margin - legend_content_height
868+
else # :top_right (default)
869+
current_x_offset = @graph_right - @legend_margin - legend_content_width
870+
current_y_offset = @graph_top + @legend_margin + @legend_spacing
836871
end
837872

838-
index = 0
839-
legend_label_lines.each do |(legend_labels_width, legend_labels_line)|
840-
current_x_offset = center(legend_labels_width)
841-
842-
legend_labels_line.each do |legend_label|
843-
unless legend_label.empty?
844-
legend_label_width = calculate_width(@legend_font, legend_label)
845-
846-
# Draw label
847-
text_renderer = Gruff::Renderer::Text.new(renderer, legend_label, font: @legend_font)
848-
text_renderer.add_to_render_queue(legend_label_width,
849-
legend_square_width,
850-
current_x_offset + (legend_square_width * 1.7),
851-
current_y_offset,
852-
Magick::CenterGravity)
853-
854-
# Now draw box with color of this dataset
855-
rect_renderer = Gruff::Renderer::Rectangle.new(renderer, color: store.data[index].color)
856-
rect_renderer.render(current_x_offset,
857-
current_y_offset,
858-
current_x_offset + legend_square_width,
859-
current_y_offset + legend_square_width)
860-
861-
current_x_offset += legend_label_width + (legend_square_width * 2.7)
862-
end
863-
index += 1
864-
end
873+
legend_items_end_x = current_x_offset + legend_content_width
874+
legend_items_end_y = current_y_offset + legend_content_height
875+
876+
frame_renderer = Gruff::Renderer::Rectangle.new(renderer, color: @marker_color, width: 1.1, opacity: 0.2, round: true)
877+
frame_renderer.render(current_x_offset - @legend_padding,
878+
current_y_offset - @legend_padding,
879+
legend_items_end_x + @legend_padding,
880+
legend_items_end_y + @legend_padding)
881+
882+
store.data.each do |data_row|
883+
legend_label = data_row.label
884+
next if legend_label.empty?
885+
886+
legend_label_width = calculate_width(@legend_font, legend_label)
887+
888+
text_renderer = Gruff::Renderer::Text.new(renderer, legend_label, font: @legend_font)
889+
text_renderer.add_to_render_queue(legend_label_width,
890+
@legend_box_size,
891+
current_x_offset + (@legend_box_size * 1.5),
892+
current_y_offset,
893+
Magick::CenterGravity)
894+
895+
rect_renderer = Gruff::Renderer::Rectangle.new(renderer, color: data_row.color)
896+
rect_renderer.render(current_x_offset,
897+
current_y_offset,
898+
current_x_offset + @legend_box_size,
899+
current_y_offset + @legend_box_size)
865900

866901
current_y_offset += line_height
867902
end
@@ -1123,14 +1158,12 @@ def setup_top_margin
11231158
# When @hide title, leave a title_margin space for aesthetics.
11241159
# Same with @hide_legend
11251160
@top_margin +
1126-
(hide_title? ? @title_margin : title_caps_height + @title_margin) +
1127-
(@hide_legend || @legend_at_bottom ? @legend_margin : calculate_legend_height + @legend_margin)
1161+
(hide_title? ? @title_margin : title_caps_height + @title_margin)
11281162
end
11291163

11301164
# @rbs return: Float
11311165
def setup_bottom_margin
11321166
graph_bottom_margin = hide_bottom_label_area? ? @bottom_margin : @bottom_margin + labels_caps_height + @label_margin
1133-
graph_bottom_margin += (calculate_legend_height + @legend_margin) if @legend_at_bottom
11341167

11351168
x_axis_label_height = @x_axis_label.nil? ? 0.0 : marker_caps_height + (@label_margin * 2)
11361169
@raw_rows - graph_bottom_margin - x_axis_label_height
@@ -1211,42 +1244,6 @@ def y_axis_label(value, increment)
12111244
end
12121245
end
12131246

1214-
# @rbs legend_labels: Array[String]
1215-
# @rbs legend_square_width: Float | Integer
1216-
# @rbs return: Array[Array[Float | Integer]]
1217-
def calculate_legend_label_widths_for_each_line(legend_labels, legend_square_width)
1218-
label_widths = [[]]
1219-
label_lines = [[]]
1220-
legend_labels.each do |label|
1221-
if label.empty?
1222-
label_width = 0.0
1223-
else
1224-
width = calculate_width(@legend_font, label)
1225-
label_width = width + (legend_square_width * 2.7)
1226-
end
1227-
label_widths.last.push label_width
1228-
label_lines.last.push label
1229-
1230-
if label_widths.last.sum > (@raw_columns * 0.9)
1231-
label_widths.push [label_widths.last.pop]
1232-
label_lines.push [label_lines.last.pop]
1233-
end
1234-
end
1235-
1236-
label_widths.map(&:sum).zip(label_lines)
1237-
end
1238-
1239-
# @rbs return: Float
1240-
def calculate_legend_height
1241-
return 0.0 if @hide_legend
1242-
1243-
legend_labels = store.data.map(&:label)
1244-
legend_label_lines = calculate_legend_label_widths_for_each_line(legend_labels, @legend_box_size)
1245-
line_height = [legend_caps_height, @legend_box_size].max
1246-
1247-
(line_height * legend_label_lines.count) + (@legend_margin * (legend_label_lines.count - 1))
1248-
end
1249-
12501247
# Returns the height of the capital letter 'X' for the current font and
12511248
# size.
12521249
#

lib/gruff/dot.rb

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,6 @@ def hide_left_label_area?
4545
hide_labels? && @y_axis_label.nil?
4646
end
4747

48-
# @rbs return: bool
49-
def hide_bottom_label_area?
50-
@hide_line_markers && @x_axis_label.nil? && @legend_at_bottom == false
51-
end
52-
5348
# @rbs return: void
5449
def draw_graph
5550
# Setup spacing.

lib/gruff/renderer/rectangle.rb

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ class Renderer::Rectangle
99
# @rbs color: String
1010
# @rbs width: Float | Integer
1111
# @rbs opacity: Float | Integer
12+
# @rbs round: bool
1213
# @rbs return: void
13-
def initialize(renderer, color: nil, width: 1.0, opacity: 1.0)
14+
def initialize(renderer, color: nil, width: 1.0, opacity: 1.0, round: false)
1415
@renderer = renderer
1516
@color = color
1617
@width = width
1718
@opacity = opacity
19+
@round = round
1820
end
1921

2022
# @rbs upper_left_x: Float | Integer
@@ -25,10 +27,17 @@ def initialize(renderer, color: nil, width: 1.0, opacity: 1.0)
2527
def render(upper_left_x, upper_left_y, lower_right_x, lower_right_y)
2628
@renderer.draw.push
2729
@renderer.draw.stroke_width(@width)
28-
@renderer.draw.stroke(@color) if @color && @width > 1.0
29-
@renderer.draw.fill_opacity(@opacity)
30-
@renderer.draw.fill(@color) if @color
31-
@renderer.draw.rectangle(upper_left_x, upper_left_y, lower_right_x, lower_right_y)
30+
if @color
31+
@renderer.draw.stroke(@color) if @width > 1.0
32+
@renderer.draw.fill_opacity(@opacity)
33+
@renderer.draw.fill(@color)
34+
end
35+
36+
if @round
37+
@renderer.draw.roundrectangle(upper_left_x, upper_left_y, lower_right_x, lower_right_y, 3, 3)
38+
else
39+
@renderer.draw.rectangle(upper_left_x, upper_left_y, lower_right_x, lower_right_y)
40+
end
3241
@renderer.draw.pop
3342
end
3443
end

lib/gruff/side_bar.rb

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,6 @@ def hide_left_label_area?
8383
hide_labels? && @y_axis_label.nil?
8484
end
8585

86-
# @rbs return: bool
87-
def hide_bottom_label_area?
88-
@hide_line_markers && @x_axis_label.nil? && @legend_at_bottom == false
89-
end
90-
9186
# @rbs return: void
9287
def setup_graph_measurements
9388
super

lib/gruff/stacked_bar.rb

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,6 @@ def hide_left_label_area?
134134
@hide_line_markers && @y_axis_label.nil?
135135
end
136136

137-
# @rbs return: bool
138-
def hide_bottom_label_area?
139-
hide_labels? && @x_axis_label.nil? && @legend_at_bottom == false
140-
end
141-
142137
# @rbs return: Proc
143138
def proc_text_metrics
144139
->(text) { text_metrics(@marker_font, text) }

sig/generated/gruff/bar.rbs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,6 @@ class Gruff::Bar < Gruff::Base
6262
# @rbs return: bool
6363
def hide_left_label_area?: () -> bool
6464

65-
# @rbs return: bool
66-
def hide_bottom_label_area?: () -> bool
67-
6865
# @rbs return: void
6966
def setup_graph_measurements: () -> void
7067

0 commit comments

Comments
 (0)