gruff 0.15.0-java → 0.18.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +21 -5
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +0 -12
  5. data/CHANGELOG.md +52 -0
  6. data/README.md +14 -3
  7. data/gruff.gemspec +3 -4
  8. data/lib/gruff/accumulator_bar.rb +1 -1
  9. data/lib/gruff/area.rb +6 -4
  10. data/lib/gruff/bar.rb +53 -32
  11. data/lib/gruff/base.rb +297 -186
  12. data/lib/gruff/bezier.rb +4 -2
  13. data/lib/gruff/box.rb +180 -0
  14. data/lib/gruff/bubble.rb +99 -0
  15. data/lib/gruff/bullet.rb +5 -5
  16. data/lib/gruff/candlestick.rb +120 -0
  17. data/lib/gruff/dot.rb +11 -12
  18. data/lib/gruff/font.rb +3 -0
  19. data/lib/gruff/helper/bar_conversion.rb +6 -10
  20. data/lib/gruff/helper/bar_mixin.rb +25 -0
  21. data/lib/gruff/helper/bar_value_label.rb +24 -43
  22. data/lib/gruff/helper/stacked_mixin.rb +19 -1
  23. data/lib/gruff/histogram.rb +9 -6
  24. data/lib/gruff/line.rb +67 -43
  25. data/lib/gruff/mini/legend.rb +15 -11
  26. data/lib/gruff/net.rb +23 -18
  27. data/lib/gruff/patch/string.rb +1 -0
  28. data/lib/gruff/pie.rb +26 -12
  29. data/lib/gruff/renderer/circle.rb +3 -1
  30. data/lib/gruff/renderer/dash_line.rb +3 -2
  31. data/lib/gruff/renderer/dot.rb +28 -15
  32. data/lib/gruff/renderer/line.rb +1 -3
  33. data/lib/gruff/renderer/rectangle.rb +6 -2
  34. data/lib/gruff/renderer/renderer.rb +0 -4
  35. data/lib/gruff/renderer/text.rb +7 -1
  36. data/lib/gruff/scatter.rb +84 -81
  37. data/lib/gruff/side_bar.rb +64 -31
  38. data/lib/gruff/side_stacked_bar.rb +43 -55
  39. data/lib/gruff/spider.rb +52 -14
  40. data/lib/gruff/stacked_area.rb +18 -8
  41. data/lib/gruff/stacked_bar.rb +59 -29
  42. data/lib/gruff/store/xy_data.rb +8 -9
  43. data/lib/gruff/store/xy_pointsizes_data.rb +60 -0
  44. data/lib/gruff/version.rb +1 -1
  45. data/lib/gruff.rb +11 -12
  46. metadata +9 -6
  47. data/lib/gruff/scene.rb +0 -208
  48. data/lib/gruff/store/custom_data.rb +0 -36
data/lib/gruff/base.rb CHANGED
@@ -45,22 +45,15 @@ module Gruff
45
45
  # Blank space below the legend. Default is +20+.
46
46
  attr_writer :legend_margin
47
47
 
48
- # A hash of names for the individual columns, where the key is the array
49
- # index for the column this label represents.
50
- #
51
- # Not all columns need to be named.
52
- #
53
- # @example
54
- # { 0 => 2005, 3 => 2006, 5 => 2007, 7 => 2008 }
55
- attr_writer :labels
48
+ # Truncates labels if longer than max specified.
49
+ attr_writer :label_max_size
56
50
 
57
- # Used internally for spacing.
51
+ # How truncated labels visually appear if they exceed {#label_max_size=}.
58
52
  #
59
- # By default, labels are centered over the point they represent.
60
- attr_writer :center_labels_over_point
61
-
62
- # Used internally for horizontal graph types. Default is +false+.
63
- attr_writer :has_left_labels
53
+ # - +:absolute+ - does not show trailing dots to indicate truncation. This is the default.
54
+ # - +:trailing_dots+ - shows trailing dots to indicate truncation (note that {#label_max_size=}
55
+ # must be greater than 3).
56
+ attr_writer :label_truncation_style
64
57
 
65
58
  # Set a label for the bottom of the graph.
66
59
  attr_writer :x_axis_label
@@ -68,31 +61,21 @@ module Gruff
68
61
  # Set a label for the left side of the graph.
69
62
  attr_writer :y_axis_label
70
63
 
64
+ # Allow passing lambda to format labels for x axis.
65
+ attr_writer :x_axis_label_format
66
+
67
+ # Allow passing lambda to format labels for y axis.
68
+ attr_writer :y_axis_label_format
69
+
71
70
  # Set increment of the vertical marking lines.
72
71
  attr_writer :x_axis_increment
73
72
 
74
73
  # Set increment of the horizontal marking lines.
75
74
  attr_writer :y_axis_increment
76
75
 
77
- # Height of staggering between labels (Bar graph only).
78
- attr_writer :label_stagger_height
79
-
80
- # Truncates labels if longer than max specified.
81
- attr_writer :label_max_size
82
-
83
- # How truncated labels visually appear if they exceed {#label_max_size=}.
84
- #
85
- # - +:absolute+ - does not show trailing dots to indicate truncation. This is the default.
86
- # - +:trailing_dots+ - shows trailing dots to indicate truncation (note that {#label_max_size=}
87
- # must be greater than 3).
88
- attr_writer :label_truncation_style
89
-
90
76
  # Get or set the list of colors that will be used to draw the bars or lines.
91
77
  attr_accessor :colors
92
78
 
93
- # Set the large title of the graph displayed at the top.
94
- attr_writer :title
95
-
96
79
  # Prevent drawing of line markers. Default is +false+.
97
80
  attr_writer :hide_line_markers
98
81
 
@@ -109,9 +92,6 @@ module Gruff
109
92
  # to +"No Data."+.
110
93
  attr_writer :no_data_message
111
94
 
112
- # Display the legend under the graph. Default is +false+.
113
- attr_writer :legend_at_bottom
114
-
115
95
  # Set the color of the auxiliary lines.
116
96
  attr_writer :marker_color
117
97
 
@@ -129,18 +109,15 @@ module Gruff
129
109
  # first. This does not affect the legend. Default is +false+.
130
110
  attr_writer :sorted_drawing
131
111
 
112
+ # Display the legend under the graph. Default is +false+.
113
+ attr_writer :legend_at_bottom
114
+
132
115
  # Optionally set the size of the colored box by each item in the legend.
133
116
  # Default is +20.0+.
134
117
  #
135
118
  # Will be scaled down if graph is smaller than 800px wide.
136
119
  attr_writer :legend_box_size
137
120
 
138
- # Allow passing lambdas to format labels for x axis.
139
- attr_writer :x_axis_label_format
140
-
141
- # Allow passing lambdas to format labels for y axis.
142
- attr_writer :y_axis_label_format
143
-
144
121
  # If one numerical argument is given, the graph is drawn at 4/3 ratio
145
122
  # according to the given width (+800+ results in 800x600, +400+ gives 400x300,
146
123
  # etc.).
@@ -159,6 +136,9 @@ module Gruff
159
136
  @columns.freeze
160
137
  @rows.freeze
161
138
 
139
+ @has_left_labels = false
140
+ @center_labels_over_point = true
141
+
162
142
  initialize_graph_scale
163
143
  initialize_attributes
164
144
  initialize_store
@@ -209,11 +189,9 @@ module Gruff
209
189
  @no_data_message = 'No Data'
210
190
 
211
191
  @hide_line_markers = @hide_legend = @hide_title = @hide_line_numbers = @legend_at_bottom = false
212
- @center_labels_over_point = true
213
- @has_left_labels = false
214
- @label_stagger_height = 0
215
192
  @label_max_size = 0
216
193
  @label_truncation_style = :absolute
194
+ @label_rotation = 0
217
195
 
218
196
  @x_axis_increment = nil
219
197
  @x_axis_label = @y_axis_label = nil
@@ -224,6 +202,67 @@ module Gruff
224
202
  end
225
203
  protected :initialize_attributes
226
204
 
205
+ # A hash of names for the individual columns, where the key is the array
206
+ # index for the column this label represents.
207
+ # Not all columns need to be named with hash.
208
+ #
209
+ # Or, an array corresponding to the data values.
210
+ #
211
+ # @param labels [Hash, Array] the labels.
212
+ #
213
+ # @example
214
+ # g = Gruff::Bar.new
215
+ # g.labels = { 0 => 2005, 3 => 2006, 5 => 2007, 7 => 2008 }
216
+ #
217
+ # g = Gruff::Bar.new
218
+ # g.labels = ['2005', nil, nil, '2006', nil, nil, '2007', nil, nil, '2008'] # same labels for columns
219
+ def labels=(labels)
220
+ if labels.is_a?(Array)
221
+ labels = labels.each_with_index.each_with_object({}) do |(label, index), hash|
222
+ hash[index] = label
223
+ end
224
+ end
225
+
226
+ @labels = labels
227
+ end
228
+
229
+ # Set a rotation for labels. You can use Default is +0+.
230
+ # You can use a rotation between +0.0+ and +45.0+, or between +0.0+ and +-45.0+.
231
+ #
232
+ # @param rotation [Numeric] the rotation.
233
+ #
234
+ def label_rotation=(rotation)
235
+ raise ArgumentError, 'rotation must be between 0.0 and 45.0 or between 0.0 and -45.0' if rotation > 45.0 || rotation < -45.0
236
+
237
+ @label_rotation = rotation.to_f
238
+ end
239
+
240
+ # Height of staggering between labels.
241
+ # @deprecated
242
+ def label_stagger_height=(_value)
243
+ warn '#label_stagger_height= is deprecated. It is no longer effective.'
244
+ end
245
+
246
+ # Set the large title of the graph displayed at the top.
247
+ # You can draw a multi-line title by putting a line break in the string
248
+ # or by setting an array as argument.
249
+ #
250
+ # @param title [String, Array] the title.
251
+ #
252
+ # @example
253
+ # g = Gruff::Bar.new
254
+ # g.title = "The graph title"
255
+ #
256
+ # g = Gruff::Bar.new
257
+ # g.title = ['The first line of title', 'The second line of title']
258
+ def title=(title)
259
+ if title.is_a?(Array)
260
+ title = title.join("\n")
261
+ end
262
+
263
+ @title = title
264
+ end
265
+
227
266
  # Sets the top, bottom, left and right margins to +margin+.
228
267
  #
229
268
  # @param margin [Numeric] The margin size.
@@ -327,7 +366,8 @@ module Gruff
327
366
  # You can set a theme manually. Assign a hash to this method before you
328
367
  # send your data.
329
368
  #
330
- # graph.theme = {
369
+ # g = Gruff::Bar.new
370
+ # g.theme = {
331
371
  # colors: %w(orange purple green white red),
332
372
  # marker_color: 'blue',
333
373
  # background_colors: ['black', 'grey'],
@@ -364,7 +404,7 @@ module Gruff
364
404
  self.marker_color = @theme_options[:marker_color]
365
405
  self.font_color = @theme_options[:font_color] || @marker_color
366
406
 
367
- @colors = @theme_options[:colors]
407
+ @colors = @theme_options[:colors].dup
368
408
  @marker_shadow_color = @theme_options[:marker_shadow_color]
369
409
 
370
410
  @renderer = Gruff::Renderer.new(@columns, @rows, @scale, @theme_options)
@@ -429,7 +469,7 @@ module Gruff
429
469
  #
430
470
  # Set it after you have given all your data to the graph object.
431
471
  def minimum_value
432
- @minimum_value || store.min
472
+ (@minimum_value || store.min).to_f
433
473
  end
434
474
  attr_writer :minimum_value
435
475
 
@@ -439,7 +479,7 @@ module Gruff
439
479
  # If you use this, you must set it after you have given all your data to
440
480
  # the graph object.
441
481
  def maximum_value
442
- @maximum_value || store.max
482
+ (@maximum_value || store.max).to_f
443
483
  end
444
484
  attr_writer :maximum_value
445
485
 
@@ -510,11 +550,13 @@ module Gruff
510
550
  attr_reader :renderer
511
551
 
512
552
  # Perform data manipulation before calculating chart measurements
513
- def setup_data # :nodoc:
553
+ def setup_data
514
554
  if @y_axis_increment && !@hide_line_markers
515
- self.maximum_value = [@y_axis_increment, maximum_value, (maximum_value.to_f / @y_axis_increment).round * @y_axis_increment].max
516
- self.minimum_value = [minimum_value, (minimum_value.to_f / @y_axis_increment).round * @y_axis_increment].min
555
+ self.maximum_value = [@y_axis_increment, maximum_value, (maximum_value / @y_axis_increment).round * @y_axis_increment].max
556
+ self.minimum_value = [minimum_value, (minimum_value / @y_axis_increment).round * @y_axis_increment].min
517
557
  end
558
+
559
+ sort_data if @sort # Sort data with avg largest values set first (for display)
518
560
  end
519
561
 
520
562
  # Calculates size of drawable area and generates normalized data.
@@ -525,7 +567,6 @@ module Gruff
525
567
  def setup_drawing
526
568
  calculate_spread
527
569
  calculate_increment
528
- sort_data if @sort # Sort data with avg largest values set first (for display)
529
570
  set_colors
530
571
  normalize
531
572
  setup_graph_measurements
@@ -552,7 +593,7 @@ module Gruff
552
593
  @marker_count ||= begin
553
594
  count = nil
554
595
  (3..7).each do |lines|
555
- if @spread.to_f % lines == 0.0
596
+ if @spread % lines == 0.0
556
597
  count = lines and break
557
598
  end
558
599
  end
@@ -565,9 +606,9 @@ module Gruff
565
606
  store.normalize(minimum: minimum_value, spread: @spread)
566
607
  end
567
608
 
568
- def calculate_spread # :nodoc:
609
+ def calculate_spread
569
610
  @spread = maximum_value.to_f - minimum_value.to_f
570
- @spread = @spread > 0 ? @spread : 1
611
+ @spread = @spread > 0 ? @spread : 1.0
571
612
  end
572
613
 
573
614
  def hide_title?
@@ -579,28 +620,23 @@ module Gruff
579
620
  end
580
621
 
581
622
  def hide_left_label_area?
582
- @hide_line_markers
623
+ @hide_line_markers && @y_axis_label.nil?
583
624
  end
584
625
 
585
626
  def hide_bottom_label_area?
586
- @hide_line_markers
627
+ @hide_line_markers && @x_axis_label.nil?
587
628
  end
588
629
 
589
630
  ##
590
631
  # Calculates size of drawable area, general font dimensions, etc.
591
632
 
592
633
  def setup_graph_measurements
593
- @marker_caps_height = setup_marker_caps_height
594
- @title_caps_height = setup_title_caps_height
595
- @legend_caps_height = setup_legend_caps_height
596
-
597
- margin_on_right = graph_right_margin
598
- @graph_right = @raw_columns - margin_on_right
634
+ @graph_right = setup_right_margin
599
635
  @graph_left = setup_left_margin
600
636
  @graph_top = setup_top_margin
601
637
  @graph_bottom = setup_bottom_margin
602
638
 
603
- @graph_width = @raw_columns - @graph_left - margin_on_right
639
+ @graph_width = @graph_right - @graph_left
604
640
  @graph_height = @graph_bottom - @graph_top
605
641
  end
606
642
 
@@ -610,9 +646,8 @@ module Gruff
610
646
  # X Axis
611
647
  # Centered vertically and horizontally by setting the
612
648
  # height to 1.0 and the width to the width of the graph.
613
- x_axis_label_y_coordinate = @graph_bottom + LABEL_MARGIN + @marker_caps_height
649
+ x_axis_label_y_coordinate = @graph_bottom + (LABEL_MARGIN * 2) + labels_caps_height
614
650
 
615
- # TODO: Center between graph area
616
651
  text_renderer = Gruff::Renderer::Text.new(renderer, @x_axis_label, font: @marker_font)
617
652
  text_renderer.add_to_render_queue(@raw_columns, 1.0, 0.0, x_axis_label_y_coordinate)
618
653
  end
@@ -620,7 +655,7 @@ module Gruff
620
655
  if @y_axis_label
621
656
  # Y Axis, rotated vertically
622
657
  text_renderer = Gruff::Renderer::Text.new(renderer, @y_axis_label, font: @marker_font, rotation: -90)
623
- text_renderer.add_to_render_queue(1.0, @raw_rows, @left_margin + @marker_caps_height / 2.0, 0.0, Magick::CenterGravity)
658
+ text_renderer.add_to_render_queue(1.0, @raw_rows, @left_margin + (marker_caps_height / 2.0), 0.0, Magick::CenterGravity)
624
659
  end
625
660
  end
626
661
 
@@ -628,17 +663,15 @@ module Gruff
628
663
  def draw_line_markers
629
664
  return if @hide_line_markers
630
665
 
631
- increment_scaled = @graph_height.to_f / (@spread / @increment)
666
+ increment_scaled = (@graph_height / (@spread / @increment)).to_f
632
667
 
633
668
  # Draw horizontal line markers and annotate with numbers
634
669
  (0..marker_count).each do |index|
635
- y = @graph_top + @graph_height - index.to_f * increment_scaled
636
-
637
- line_renderer = Gruff::Renderer::Line.new(renderer, color: @marker_color, shadow_color: @marker_shadow_color)
638
- line_renderer.render(@graph_left, y, @graph_right, y)
670
+ y = @graph_top + @graph_height - (index * increment_scaled)
671
+ draw_marker_horizontal_line(y)
639
672
 
640
673
  unless @hide_line_numbers
641
- marker_label = BigDecimal(index.to_s) * BigDecimal(@increment.to_s) + BigDecimal(minimum_value.to_s)
674
+ marker_label = (BigDecimal(index.to_s) * BigDecimal(@increment.to_s)) + BigDecimal(minimum_value.to_s)
642
675
  label = y_axis_label(marker_label, @increment)
643
676
  text_renderer = Gruff::Renderer::Text.new(renderer, label, font: @marker_font)
644
677
  text_renderer.add_to_render_queue(@graph_left - LABEL_MARGIN, 1.0, 0.0, y, Magick::EastGravity)
@@ -646,6 +679,25 @@ module Gruff
646
679
  end
647
680
  end
648
681
 
682
+ def draw_marker_horizontal_line(y)
683
+ Gruff::Renderer::Line.new(renderer, color: @marker_color).render(@graph_left, y, @graph_right, y)
684
+ Gruff::Renderer::Line.new(renderer, color: @marker_shadow_color).render(@graph_left, y + 1, @graph_right, y + 1) if @marker_shadow_color
685
+ end
686
+
687
+ def draw_marker_vertical_line(x, tick_mark_mode: false)
688
+ if tick_mark_mode
689
+ Gruff::Renderer::Line.new(renderer, color: @marker_color).render(x, @graph_bottom, x, @graph_bottom + 5)
690
+ if @marker_shadow_color
691
+ Gruff::Renderer::Line.new(renderer, color: @marker_shadow_color).render(x + 1, @graph_bottom, x + 1, @graph_bottom + 5)
692
+ end
693
+ else
694
+ Gruff::Renderer::Line.new(renderer, color: @marker_color).render(x, @graph_bottom, x, @graph_top)
695
+ if @marker_shadow_color
696
+ Gruff::Renderer::Line.new(renderer, color: @marker_shadow_color).render(x + 1, @graph_bottom, x + 1, @graph_top)
697
+ end
698
+ end
699
+ end
700
+
649
701
  # Return a calculation of center
650
702
  def center(size)
651
703
  (@raw_columns - size) / 2
@@ -658,45 +710,46 @@ module Gruff
658
710
 
659
711
  legend_labels = store.data.map(&:label)
660
712
  legend_square_width = @legend_box_size # small square with color of this item
661
- label_widths = calculate_legend_label_widths_for_each_line(legend_labels, legend_square_width)
713
+ legend_label_lines = calculate_legend_label_widths_for_each_line(legend_labels, legend_square_width)
714
+ line_height = [legend_caps_height, legend_square_width].max + @legend_margin
662
715
 
663
- current_x_offset = center(label_widths.first.sum)
664
716
  current_y_offset = begin
665
717
  if @legend_at_bottom
666
- @graph_bottom + @legend_margin + @legend_caps_height + LABEL_MARGIN
718
+ @graph_bottom + @legend_margin + labels_caps_height + LABEL_MARGIN + (@x_axis_label ? (LABEL_MARGIN * 2) + marker_caps_height : 0)
667
719
  else
668
- hide_title? ? @top_margin + @title_margin : @top_margin + @title_margin + @title_caps_height
720
+ hide_title? ? @top_margin + @title_margin : @top_margin + @title_margin + title_caps_height
669
721
  end
670
722
  end
671
723
 
672
- legend_labels.each_with_index do |legend_label, index|
673
- next if legend_label.empty?
674
-
675
- # Draw label
676
- text_renderer = Gruff::Renderer::Text.new(renderer, legend_label, font: @legend_font)
677
- text_renderer.add_to_render_queue(@raw_columns, 1.0, current_x_offset + (legend_square_width * 1.7), current_y_offset, Magick::WestGravity)
678
-
679
- # Now draw box with color of this dataset
680
- rect_renderer = Gruff::Renderer::Rectangle.new(renderer, color: store.data[index].color)
681
- rect_renderer.render(current_x_offset,
682
- current_y_offset - legend_square_width / 2.0,
683
- current_x_offset + legend_square_width,
684
- current_y_offset + legend_square_width / 2.0)
685
-
686
- width = calculate_width(@legend_font, legend_label)
687
- current_x_offset += width + (legend_square_width * 2.7)
688
- label_widths.first.shift
689
-
690
- # Handle wrapping
691
- if label_widths.first.empty?
692
- label_widths.shift
693
- current_x_offset = center(label_widths.first.sum) unless label_widths.empty?
694
- line_height = [@legend_caps_height, legend_square_width].max + @legend_margin
695
- unless label_widths.empty?
696
- # Wrap to next line and shrink available graph dimensions
697
- current_y_offset += line_height
724
+ index = 0
725
+ legend_label_lines.each do |(legend_labels_width, legend_labels_line)|
726
+ current_x_offset = center(legend_labels_width)
727
+
728
+ legend_labels_line.each do |legend_label|
729
+ unless legend_label.empty?
730
+ legend_label_width = calculate_width(@legend_font, legend_label)
731
+
732
+ # Draw label
733
+ text_renderer = Gruff::Renderer::Text.new(renderer, legend_label, font: @legend_font)
734
+ text_renderer.add_to_render_queue(legend_label_width,
735
+ legend_square_width,
736
+ current_x_offset + (legend_square_width * 1.7),
737
+ current_y_offset,
738
+ Magick::CenterGravity)
739
+
740
+ # Now draw box with color of this dataset
741
+ rect_renderer = Gruff::Renderer::Rectangle.new(renderer, color: store.data[index].color)
742
+ rect_renderer.render(current_x_offset,
743
+ current_y_offset,
744
+ current_x_offset + legend_square_width,
745
+ current_y_offset + legend_square_width)
746
+
747
+ current_x_offset += legend_label_width + (legend_square_width * 2.7)
698
748
  end
749
+ index += 1
699
750
  end
751
+
752
+ current_y_offset += line_height
700
753
  end
701
754
  end
702
755
 
@@ -704,7 +757,7 @@ module Gruff
704
757
  def draw_title
705
758
  return if hide_title?
706
759
 
707
- metrics = Gruff::Renderer::Text.new(renderer, @title, font: @title_font).metrics
760
+ metrics = text_metrics(@title_font, @title)
708
761
  if metrics.width > @raw_columns
709
762
  @title_font.size = @title_font.size * (@raw_columns / metrics.width) * 0.95
710
763
  end
@@ -713,20 +766,15 @@ module Gruff
713
766
  text_renderer.add_to_render_queue(@raw_columns, 1.0, 0, @top_margin)
714
767
  end
715
768
 
716
- # Draws column labels below graph, centered over x_offset
717
- #--
718
- # TODO Allow WestGravity as an option
719
- def draw_label(x_offset, index, gravity = Magick::NorthGravity)
769
+ # Draws column labels below graph, centered over x
770
+ def draw_label(x, index, gravity = Magick::NorthGravity, &block)
720
771
  draw_unique_label(index) do
721
- y_offset = @graph_bottom + LABEL_MARGIN
722
-
723
- # TESTME
724
- # FIXME: Consider chart types other than bar
725
- # TODO: See if index.odd? is the best strategy
726
- y_offset += @label_stagger_height if index.odd?
772
+ if x >= @graph_left && x <= @graph_right
773
+ y = @graph_bottom
774
+ x_offset, y_offset = calculate_label_offset(@marker_font, @labels[index], LABEL_MARGIN, @label_rotation)
727
775
 
728
- if x_offset >= @graph_left && x_offset <= @graph_right
729
- draw_label_at(1.0, 1.0, x_offset, y_offset, @labels[index], gravity)
776
+ draw_label_at(1.0, 1.0, x + x_offset, y + y_offset, @labels[index], gravity: gravity, rotation: @label_rotation)
777
+ yield if block
730
778
  end
731
779
  end
732
780
  end
@@ -741,18 +789,17 @@ module Gruff
741
789
  end
742
790
  end
743
791
 
744
- def draw_label_at(width, height, x, y, text, gravity = Magick::NorthGravity)
792
+ def draw_label_at(width, height, x, y, text, gravity: Magick::NorthGravity, rotation: 0)
745
793
  label_text = truncate_label_text(text)
746
- text_renderer = Gruff::Renderer::Text.new(renderer, label_text, font: @marker_font)
794
+ text_renderer = Gruff::Renderer::Text.new(renderer, label_text, font: @marker_font, rotation: rotation)
747
795
  text_renderer.add_to_render_queue(width, height, x, y, gravity)
748
796
  end
749
797
 
750
798
  # Draws the data value over the data point in bar graphs
751
- def draw_value_label(x_offset, y_offset, data_point)
799
+ def draw_value_label(width, height, x_offset, y_offset, data_point, gravity: Magick::CenterGravity)
752
800
  return if @hide_line_markers
753
801
 
754
- text_renderer = Gruff::Renderer::Text.new(renderer, data_point, font: @marker_font)
755
- text_renderer.add_to_render_queue(1.0, 1.0, x_offset, y_offset)
802
+ draw_label_at(width, height, x_offset, y_offset, data_point, gravity: gravity)
756
803
  end
757
804
 
758
805
  # Shows an error message because you have no data.
@@ -773,20 +820,11 @@ module Gruff
773
820
  @theme_options = {}
774
821
  end
775
822
 
776
- def scale(value) # :nodoc:
777
- value * @scale
778
- end
779
-
780
- # Return a comparable fontsize for the current graph.
781
- def scale_fontsize(value)
782
- value * @scale
783
- end
784
-
785
- def clip_value_if_greater_than(value, max_value) # :nodoc:
823
+ def clip_value_if_greater_than(value, max_value)
786
824
  value > max_value ? max_value : value
787
825
  end
788
826
 
789
- def significant(i) # :nodoc:
827
+ def significant(i)
790
828
  return 1.0 if i == 0 # Keep from going into infinite loop
791
829
 
792
830
  inc = BigDecimal(i.to_s)
@@ -829,27 +867,20 @@ module Gruff
829
867
 
830
868
  private
831
869
 
832
- def setup_marker_caps_height
870
+ def marker_caps_height
833
871
  hide_bottom_label_area? ? 0 : calculate_caps_height(@marker_font)
834
872
  end
835
873
 
836
- def setup_title_caps_height
837
- hide_title? ? 0 : calculate_caps_height(@title_font) * @title.lines.to_a.size
874
+ def labels_caps_height
875
+ hide_bottom_label_area? ? 0 : calculate_labels_height(@marker_font)
838
876
  end
839
877
 
840
- def setup_legend_caps_height
841
- @hide_legend ? 0 : calculate_caps_height(@legend_font)
842
- end
843
-
844
- def graph_right_margin
845
- @hide_line_markers ? @right_margin : @right_margin + extra_room_for_long_label
878
+ def title_caps_height
879
+ hide_title? ? 0 : calculate_caps_height(@title_font) * @title.lines.to_a.size
846
880
  end
847
881
 
848
- def extra_room_for_long_label
849
- # Make space for half the width of the rightmost column label.
850
- # Might be greater than the number of columns if between-style bar markers are used.
851
- last_label = @labels.keys.max.to_i
852
- last_label >= (column_count - 1) && @center_labels_over_point ? calculate_width(@marker_font, @labels[last_label]) / 2.0 : 0
882
+ def legend_caps_height
883
+ @hide_legend ? 0 : calculate_caps_height(@legend_font)
853
884
  end
854
885
 
855
886
  def setup_left_margin
@@ -859,33 +890,83 @@ module Gruff
859
890
  if @has_left_labels
860
891
  @labels.values.reduce('') { |value, memo| value.to_s.length > memo.to_s.length ? value : memo }
861
892
  else
862
- y_axis_label(maximum_value.to_f, @increment)
893
+ y_axis_label(maximum_value, @increment)
863
894
  end
864
895
  end
865
896
  longest_left_label_width = calculate_width(@marker_font, truncate_label_text(text))
866
- longest_left_label_width *= 1.25 if @has_left_labels
867
897
 
868
- # Shift graph if left line numbers are hidden
869
- line_number_width = @hide_line_numbers && !@has_left_labels ? 0.0 : (longest_left_label_width + LABEL_MARGIN * 2)
898
+ line_number_width = begin
899
+ if !@has_left_labels && (@hide_line_markers || @hide_line_numbers)
900
+ 0.0
901
+ else
902
+ longest_left_label_width + LABEL_MARGIN
903
+ end
904
+ end
905
+ y_axis_label_width = @y_axis_label.nil? ? 0.0 : marker_caps_height + (LABEL_MARGIN * 2)
870
906
 
871
- @left_margin + line_number_width + (@y_axis_label.nil? ? 0.0 : @marker_caps_height + LABEL_MARGIN * 2)
907
+ bottom_label_width = extra_left_room_for_long_label
908
+
909
+ margin = line_number_width + y_axis_label_width
910
+ @left_margin + (margin > bottom_label_width ? margin : bottom_label_width)
911
+ end
912
+
913
+ def setup_right_margin
914
+ @raw_columns - (@hide_line_markers ? @right_margin : @right_margin + extra_right_room_for_long_label)
915
+ end
916
+
917
+ def extra_left_room_for_long_label
918
+ if require_extra_side_margin?
919
+ width = calculate_width(@marker_font, truncate_label_text(@labels[0]), rotation: @label_rotation)
920
+ case @label_rotation
921
+ when 0
922
+ width / 2.0
923
+ when 0..45
924
+ 0
925
+ when -45..0
926
+ width
927
+ end
928
+ else
929
+ 0
930
+ end
931
+ end
932
+
933
+ def extra_right_room_for_long_label
934
+ # Make space for half the width of the rightmost column label.
935
+ # Might be greater than the number of columns if between-style bar markers are used.
936
+ last_label = @labels.keys.max.to_i
937
+ if last_label >= (column_count - 1) && require_extra_side_margin?
938
+ width = calculate_width(@marker_font, truncate_label_text(@labels[last_label]), rotation: @label_rotation)
939
+ case @label_rotation
940
+ when 0
941
+ width / 2.0
942
+ when 0..45
943
+ width
944
+ when -45..0
945
+ 0
946
+ end
947
+ else
948
+ 0
949
+ end
950
+ end
951
+
952
+ def require_extra_side_margin?
953
+ !hide_bottom_label_area? && @center_labels_over_point
872
954
  end
873
955
 
874
956
  def setup_top_margin
875
957
  # When @hide title, leave a title_margin space for aesthetics.
876
958
  # Same with @hide_legend
877
959
  @top_margin +
878
- (hide_title? ? @title_margin : @title_caps_height + @title_margin) +
960
+ (hide_title? ? @title_margin : title_caps_height + @title_margin) +
879
961
  (@hide_legend || @legend_at_bottom ? @legend_margin : calculate_legend_height + @legend_margin)
880
962
  end
881
963
 
882
964
  def setup_bottom_margin
883
- graph_bottom_margin = hide_bottom_label_area? ? @bottom_margin : @bottom_margin + @marker_caps_height + LABEL_MARGIN
965
+ graph_bottom_margin = hide_bottom_label_area? ? @bottom_margin : @bottom_margin + labels_caps_height + LABEL_MARGIN
884
966
  graph_bottom_margin += (calculate_legend_height + @legend_margin) if @legend_at_bottom
885
967
 
886
- x_axis_label_height = @x_axis_label.nil? ? 0.0 : @marker_caps_height + LABEL_MARGIN
887
- # FIXME: Consider chart types other than bar
888
- @raw_rows - graph_bottom_margin - x_axis_label_height - @label_stagger_height
968
+ x_axis_label_height = @x_axis_label.nil? ? 0.0 : marker_caps_height + (LABEL_MARGIN * 2)
969
+ @raw_rows - graph_bottom_margin - x_axis_label_height
889
970
  end
890
971
 
891
972
  def truncate_label_text(text)
@@ -919,7 +1000,7 @@ module Gruff
919
1000
  else
920
1001
  value.to_s
921
1002
  end
922
- elsif (@spread.to_f % (marker_count.to_f == 0 ? 1 : marker_count.to_f) == 0) || !@y_axis_increment.nil?
1003
+ elsif (@spread % (marker_count == 0 ? 1 : marker_count) == 0) || !@y_axis_increment.nil?
923
1004
  value.to_i.to_s
924
1005
  elsif @spread > 10.0
925
1006
  sprintf('%0i', value)
@@ -952,44 +1033,31 @@ module Gruff
952
1033
  end
953
1034
 
954
1035
  def calculate_legend_label_widths_for_each_line(legend_labels, legend_square_width)
955
- # May fix legend drawing problem at small sizes
956
- label_widths = [[]] # Used to calculate line wrap
1036
+ label_widths = [[]]
1037
+ label_lines = [[]]
957
1038
  legend_labels.each do |label|
958
1039
  width = calculate_width(@legend_font, label)
959
- label_width = width + legend_square_width * 2.7
1040
+ label_width = width + (legend_square_width * 2.7)
960
1041
  label_widths.last.push label_width
1042
+ label_lines.last.push label
961
1043
 
962
1044
  if label_widths.last.sum > (@raw_columns * 0.9)
963
1045
  label_widths.push [label_widths.last.pop]
1046
+ label_lines.push [label_lines.last.pop]
964
1047
  end
965
1048
  end
966
1049
 
967
- label_widths
1050
+ label_widths.map(&:sum).zip(label_lines)
968
1051
  end
969
1052
 
970
1053
  def calculate_legend_height
971
1054
  return 0.0 if @hide_legend
972
1055
 
973
1056
  legend_labels = store.data.map(&:label)
974
- legend_square_width = @legend_box_size
975
- label_widths = calculate_legend_label_widths_for_each_line(legend_labels, legend_square_width)
976
- legend_height = 0.0
977
-
978
- legend_labels.each_with_index do |legend_label, _index|
979
- next if legend_label.empty?
980
-
981
- label_widths.first.shift
982
- if label_widths.first.empty?
983
- label_widths.shift
984
- line_height = [@legend_caps_height, legend_square_width].max + @legend_margin
985
- unless label_widths.empty?
986
- # Wrap to next line and shrink available graph dimensions
987
- legend_height += line_height
988
- end
989
- end
990
- end
1057
+ legend_label_lines = calculate_legend_label_widths_for_each_line(legend_labels, @legend_box_size)
1058
+ line_height = [legend_caps_height, @legend_box_size].max
991
1059
 
992
- legend_height + @legend_caps_height
1060
+ (line_height * legend_label_lines.count) + (@legend_margin * (legend_label_lines.count - 1))
993
1061
  end
994
1062
 
995
1063
  # Returns the height of the capital letter 'X' for the current font and
@@ -998,20 +1066,41 @@ module Gruff
998
1066
  # Not scaled since it deals with dimensions that the regular scaling will
999
1067
  # handle.
1000
1068
  def calculate_caps_height(font)
1001
- metrics = Gruff::Renderer::Text.new(renderer, 'X', font: font).metrics
1002
- metrics.height
1069
+ calculate_height(font, 'X')
1070
+ end
1071
+
1072
+ def calculate_labels_height(font)
1073
+ @labels.values.map { |label| calculate_height(font, label, rotation: @label_rotation) }.max || marker_caps_height
1074
+ end
1075
+
1076
+ # Returns the height of a string at this point size.
1077
+ #
1078
+ # Not scaled since it deals with dimensions that the regular scaling will
1079
+ # handle.
1080
+ def calculate_height(font, text, rotation: 0)
1081
+ text = text.to_s
1082
+ return 0 if text.empty?
1083
+
1084
+ metrics = text_metrics(font, text, rotation: rotation)
1085
+ # Calculate manually because it does not return the height after rotation.
1086
+ (metrics.width * Math.sin(deg2rad(rotation))).abs + (metrics.height * Math.cos(deg2rad(rotation))).abs
1003
1087
  end
1004
1088
 
1005
1089
  # Returns the width of a string at this point size.
1006
1090
  #
1007
1091
  # Not scaled since it deals with dimensions that the regular
1008
1092
  # scaling will handle.
1009
- def calculate_width(font, text)
1093
+ def calculate_width(font, text, rotation: 0)
1010
1094
  text = text.to_s
1011
1095
  return 0 if text.empty?
1012
1096
 
1013
- metrics = Gruff::Renderer::Text.new(renderer, text, font: font).metrics
1014
- metrics.width
1097
+ metrics = text_metrics(font, text, rotation: rotation)
1098
+ # Calculate manually because it does not return the width after rotation.
1099
+ (metrics.width * Math.cos(deg2rad(rotation))).abs - (metrics.height * Math.sin(deg2rad(rotation))).abs
1100
+ end
1101
+
1102
+ def text_metrics(font, text, rotation: 0)
1103
+ Gruff::Renderer::Text.new(renderer, text, font: font, rotation: rotation).metrics
1015
1104
  end
1016
1105
 
1017
1106
  def calculate_increment
@@ -1019,17 +1108,39 @@ module Gruff
1019
1108
  # Try to use a number of horizontal lines that will come out even.
1020
1109
  #
1021
1110
  # TODO Do the same for larger numbers...100, 75, 50, 25
1022
- @increment = @spread > 0 && marker_count > 0 ? significant(@spread / marker_count) : 1
1111
+ @increment = @spread > 0 && marker_count > 0 ? significant(@spread / marker_count) : 1.0
1023
1112
  else
1024
1113
  # TODO: Make this work for negative values
1025
1114
  self.marker_count = (@spread / @y_axis_increment).to_i
1026
- @increment = @y_axis_increment
1115
+ @increment = @y_axis_increment.to_f
1027
1116
  end
1028
1117
  end
1029
1118
 
1030
- # Used for degree => radian conversions
1119
+ def calculate_label_offset(font, label, margin, rotation)
1120
+ width = calculate_width(font, label, rotation: rotation)
1121
+ height = calculate_height(font, label, rotation: rotation)
1122
+ x_offset = begin
1123
+ case rotation
1124
+ when 0
1125
+ 0
1126
+ when 0..45
1127
+ width / 2.0
1128
+ when -45..0
1129
+ -(width / 2.0)
1130
+ end
1131
+ end
1132
+ y_offset = (height / 2.0) > margin ? (height / 2.0) : margin
1133
+
1134
+ [x_offset, y_offset]
1135
+ end
1136
+
1137
+ # Used for degree <=> radian conversions
1031
1138
  def deg2rad(angle)
1032
- angle * (Math::PI / 180.0)
1139
+ (angle * Math::PI) / 180.0
1140
+ end
1141
+
1142
+ def rad2deg(angle)
1143
+ (angle / Math::PI) * 180.0
1033
1144
  end
1034
1145
  end
1035
1146