gruff 0.15.0-java → 0.18.0-java

Sign up to get free protection for your applications and to get access to all the features.
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