gruff 0.15.0 → 0.16.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/gruff/base.rb CHANGED
@@ -54,20 +54,18 @@ module Gruff
54
54
  # { 0 => 2005, 3 => 2006, 5 => 2007, 7 => 2008 }
55
55
  attr_writer :labels
56
56
 
57
- # Used internally for spacing.
58
- #
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
64
-
65
57
  # Set a label for the bottom of the graph.
66
58
  attr_writer :x_axis_label
67
59
 
68
60
  # Set a label for the left side of the graph.
69
61
  attr_writer :y_axis_label
70
62
 
63
+ # Allow passing lambda to format labels for x axis.
64
+ attr_writer :x_axis_label_format
65
+
66
+ # Allow passing lambda to format labels for y axis.
67
+ attr_writer :y_axis_label_format
68
+
71
69
  # Set increment of the vertical marking lines.
72
70
  attr_writer :x_axis_increment
73
71
 
@@ -135,12 +133,6 @@ module Gruff
135
133
  # Will be scaled down if graph is smaller than 800px wide.
136
134
  attr_writer :legend_box_size
137
135
 
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
136
  # If one numerical argument is given, the graph is drawn at 4/3 ratio
145
137
  # according to the given width (+800+ results in 800x600, +400+ gives 400x300,
146
138
  # etc.).
@@ -159,6 +151,9 @@ module Gruff
159
151
  @columns.freeze
160
152
  @rows.freeze
161
153
 
154
+ @has_left_labels = false
155
+ @center_labels_over_point = true
156
+
162
157
  initialize_graph_scale
163
158
  initialize_attributes
164
159
  initialize_store
@@ -209,8 +204,6 @@ module Gruff
209
204
  @no_data_message = 'No Data'
210
205
 
211
206
  @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
207
  @label_stagger_height = 0
215
208
  @label_max_size = 0
216
209
  @label_truncation_style = :absolute
@@ -429,7 +422,7 @@ module Gruff
429
422
  #
430
423
  # Set it after you have given all your data to the graph object.
431
424
  def minimum_value
432
- @minimum_value || store.min
425
+ (@minimum_value || store.min).to_f
433
426
  end
434
427
  attr_writer :minimum_value
435
428
 
@@ -439,7 +432,7 @@ module Gruff
439
432
  # If you use this, you must set it after you have given all your data to
440
433
  # the graph object.
441
434
  def maximum_value
442
- @maximum_value || store.max
435
+ (@maximum_value || store.max).to_f
443
436
  end
444
437
  attr_writer :maximum_value
445
438
 
@@ -512,8 +505,8 @@ module Gruff
512
505
  # Perform data manipulation before calculating chart measurements
513
506
  def setup_data # :nodoc:
514
507
  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
508
+ self.maximum_value = [@y_axis_increment, maximum_value, (maximum_value / @y_axis_increment).round * @y_axis_increment].max
509
+ self.minimum_value = [minimum_value, (minimum_value / @y_axis_increment).round * @y_axis_increment].min
517
510
  end
518
511
  end
519
512
 
@@ -552,7 +545,7 @@ module Gruff
552
545
  @marker_count ||= begin
553
546
  count = nil
554
547
  (3..7).each do |lines|
555
- if @spread.to_f % lines == 0.0
548
+ if @spread % lines == 0.0
556
549
  count = lines and break
557
550
  end
558
551
  end
@@ -565,8 +558,8 @@ module Gruff
565
558
  store.normalize(minimum: minimum_value, spread: @spread)
566
559
  end
567
560
 
568
- def calculate_spread # :nodoc:
569
- @spread = maximum_value.to_f - minimum_value.to_f
561
+ def calculate_spread
562
+ @spread = maximum_value - minimum_value
570
563
  @spread = @spread > 0 ? @spread : 1
571
564
  end
572
565
 
@@ -579,28 +572,24 @@ module Gruff
579
572
  end
580
573
 
581
574
  def hide_left_label_area?
582
- @hide_line_markers
575
+ @hide_line_markers && @y_axis_label.nil?
583
576
  end
584
577
 
585
578
  def hide_bottom_label_area?
586
- @hide_line_markers
579
+ @hide_line_markers && @x_axis_label.nil?
587
580
  end
588
581
 
589
582
  ##
590
583
  # Calculates size of drawable area, general font dimensions, etc.
591
584
 
592
585
  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
586
  margin_on_right = graph_right_margin
598
587
  @graph_right = @raw_columns - margin_on_right
599
588
  @graph_left = setup_left_margin
600
589
  @graph_top = setup_top_margin
601
590
  @graph_bottom = setup_bottom_margin
602
591
 
603
- @graph_width = @raw_columns - @graph_left - margin_on_right
592
+ @graph_width = @graph_right - @graph_left
604
593
  @graph_height = @graph_bottom - @graph_top
605
594
  end
606
595
 
@@ -610,9 +599,8 @@ module Gruff
610
599
  # X Axis
611
600
  # Centered vertically and horizontally by setting the
612
601
  # 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
602
+ x_axis_label_y_coordinate = @graph_bottom + (LABEL_MARGIN * 2) + marker_caps_height
614
603
 
615
- # TODO: Center between graph area
616
604
  text_renderer = Gruff::Renderer::Text.new(renderer, @x_axis_label, font: @marker_font)
617
605
  text_renderer.add_to_render_queue(@raw_columns, 1.0, 0.0, x_axis_label_y_coordinate)
618
606
  end
@@ -620,7 +608,7 @@ module Gruff
620
608
  if @y_axis_label
621
609
  # Y Axis, rotated vertically
622
610
  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)
611
+ text_renderer.add_to_render_queue(1.0, @raw_rows, @left_margin + (marker_caps_height / 2.0), 0.0, Magick::CenterGravity)
624
612
  end
625
613
  end
626
614
 
@@ -628,17 +616,17 @@ module Gruff
628
616
  def draw_line_markers
629
617
  return if @hide_line_markers
630
618
 
631
- increment_scaled = @graph_height.to_f / (@spread / @increment)
619
+ increment_scaled = @graph_height / (@spread / @increment)
632
620
 
633
621
  # Draw horizontal line markers and annotate with numbers
634
622
  (0..marker_count).each do |index|
635
- y = @graph_top + @graph_height - index.to_f * increment_scaled
623
+ y = @graph_top + @graph_height - (index * increment_scaled)
636
624
 
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)
625
+ Gruff::Renderer::Line.new(renderer, color: @marker_color).render(@graph_left, y, @graph_right, y)
626
+ Gruff::Renderer::Line.new(renderer, color: @marker_shadow_color).render(@graph_left, y + 1, @graph_right, y + 1) if @marker_shadow_color
639
627
 
640
628
  unless @hide_line_numbers
641
- marker_label = BigDecimal(index.to_s) * BigDecimal(@increment.to_s) + BigDecimal(minimum_value.to_s)
629
+ marker_label = (BigDecimal(index.to_s) * BigDecimal(@increment.to_s)) + BigDecimal(minimum_value.to_s)
642
630
  label = y_axis_label(marker_label, @increment)
643
631
  text_renderer = Gruff::Renderer::Text.new(renderer, label, font: @marker_font)
644
632
  text_renderer.add_to_render_queue(@graph_left - LABEL_MARGIN, 1.0, 0.0, y, Magick::EastGravity)
@@ -658,45 +646,46 @@ module Gruff
658
646
 
659
647
  legend_labels = store.data.map(&:label)
660
648
  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)
649
+ legend_label_lines = calculate_legend_label_widths_for_each_line(legend_labels, legend_square_width)
650
+ line_height = [legend_caps_height, legend_square_width].max + @legend_margin
662
651
 
663
- current_x_offset = center(label_widths.first.sum)
664
652
  current_y_offset = begin
665
653
  if @legend_at_bottom
666
- @graph_bottom + @legend_margin + @legend_caps_height + LABEL_MARGIN
654
+ @graph_bottom + @legend_margin + legend_caps_height + LABEL_MARGIN + (@x_axis_label ? (LABEL_MARGIN * 2) + marker_caps_height : 0)
667
655
  else
668
- hide_title? ? @top_margin + @title_margin : @top_margin + @title_margin + @title_caps_height
656
+ hide_title? ? @top_margin + @title_margin : @top_margin + @title_margin + title_caps_height
669
657
  end
670
658
  end
671
659
 
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
660
+ index = 0
661
+ legend_label_lines.each do |(legend_labels_width, legend_labels_line)|
662
+ current_x_offset = center(legend_labels_width)
663
+
664
+ legend_labels_line.each do |legend_label|
665
+ unless legend_label.empty?
666
+ legend_label_width = calculate_width(@legend_font, legend_label)
667
+
668
+ # Draw label
669
+ text_renderer = Gruff::Renderer::Text.new(renderer, legend_label, font: @legend_font)
670
+ text_renderer.add_to_render_queue(legend_label_width,
671
+ legend_square_width,
672
+ current_x_offset + (legend_square_width * 1.7),
673
+ current_y_offset,
674
+ Magick::CenterGravity)
675
+
676
+ # Now draw box with color of this dataset
677
+ rect_renderer = Gruff::Renderer::Rectangle.new(renderer, color: store.data[index].color)
678
+ rect_renderer.render(current_x_offset,
679
+ current_y_offset,
680
+ current_x_offset + legend_square_width,
681
+ current_y_offset + legend_square_width)
682
+
683
+ current_x_offset += legend_label_width + (legend_square_width * 2.7)
698
684
  end
685
+ index += 1
699
686
  end
687
+
688
+ current_y_offset += line_height
700
689
  end
701
690
  end
702
691
 
@@ -704,7 +693,7 @@ module Gruff
704
693
  def draw_title
705
694
  return if hide_title?
706
695
 
707
- metrics = Gruff::Renderer::Text.new(renderer, @title, font: @title_font).metrics
696
+ metrics = text_metrics(@title_font, @title)
708
697
  if metrics.width > @raw_columns
709
698
  @title_font.size = @title_font.size * (@raw_columns / metrics.width) * 0.95
710
699
  end
@@ -714,19 +703,14 @@ module Gruff
714
703
  end
715
704
 
716
705
  # 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)
706
+ def draw_label(x_offset, index, gravity = Magick::NorthGravity, &block)
720
707
  draw_unique_label(index) do
721
708
  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
709
  y_offset += @label_stagger_height if index.odd?
727
710
 
728
711
  if x_offset >= @graph_left && x_offset <= @graph_right
729
712
  draw_label_at(1.0, 1.0, x_offset, y_offset, @labels[index], gravity)
713
+ yield if block
730
714
  end
731
715
  end
732
716
  end
@@ -748,11 +732,10 @@ module Gruff
748
732
  end
749
733
 
750
734
  # Draws the data value over the data point in bar graphs
751
- def draw_value_label(x_offset, y_offset, data_point)
735
+ def draw_value_label(width, height, x_offset, y_offset, data_point, gravity = Magick::CenterGravity)
752
736
  return if @hide_line_markers
753
737
 
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)
738
+ draw_label_at(width, height, x_offset, y_offset, data_point, gravity)
756
739
  end
757
740
 
758
741
  # Shows an error message because you have no data.
@@ -829,15 +812,15 @@ module Gruff
829
812
 
830
813
  private
831
814
 
832
- def setup_marker_caps_height
815
+ def marker_caps_height
833
816
  hide_bottom_label_area? ? 0 : calculate_caps_height(@marker_font)
834
817
  end
835
818
 
836
- def setup_title_caps_height
819
+ def title_caps_height
837
820
  hide_title? ? 0 : calculate_caps_height(@title_font) * @title.lines.to_a.size
838
821
  end
839
822
 
840
- def setup_legend_caps_height
823
+ def legend_caps_height
841
824
  @hide_legend ? 0 : calculate_caps_height(@legend_font)
842
825
  end
843
826
 
@@ -849,7 +832,11 @@ module Gruff
849
832
  # Make space for half the width of the rightmost column label.
850
833
  # Might be greater than the number of columns if between-style bar markers are used.
851
834
  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
835
+ if last_label >= (column_count - 1) && @center_labels_over_point
836
+ calculate_width(@marker_font, truncate_label_text(@labels[last_label])) / 2.0
837
+ else
838
+ 0
839
+ end
853
840
  end
854
841
 
855
842
  def setup_left_margin
@@ -859,32 +846,30 @@ module Gruff
859
846
  if @has_left_labels
860
847
  @labels.values.reduce('') { |value, memo| value.to_s.length > memo.to_s.length ? value : memo }
861
848
  else
862
- y_axis_label(maximum_value.to_f, @increment)
849
+ y_axis_label(maximum_value, @increment)
863
850
  end
864
851
  end
865
852
  longest_left_label_width = calculate_width(@marker_font, truncate_label_text(text))
866
- longest_left_label_width *= 1.25 if @has_left_labels
867
853
 
868
854
  # 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)
855
+ line_number_width = !@has_left_labels && (@hide_line_markers || @hide_line_numbers) ? 0.0 : (longest_left_label_width + LABEL_MARGIN)
870
856
 
871
- @left_margin + line_number_width + (@y_axis_label.nil? ? 0.0 : @marker_caps_height + LABEL_MARGIN * 2)
857
+ @left_margin + line_number_width + (@y_axis_label.nil? ? 0.0 : marker_caps_height + (LABEL_MARGIN * 2))
872
858
  end
873
859
 
874
860
  def setup_top_margin
875
861
  # When @hide title, leave a title_margin space for aesthetics.
876
862
  # Same with @hide_legend
877
863
  @top_margin +
878
- (hide_title? ? @title_margin : @title_caps_height + @title_margin) +
864
+ (hide_title? ? @title_margin : title_caps_height + @title_margin) +
879
865
  (@hide_legend || @legend_at_bottom ? @legend_margin : calculate_legend_height + @legend_margin)
880
866
  end
881
867
 
882
868
  def setup_bottom_margin
883
- graph_bottom_margin = hide_bottom_label_area? ? @bottom_margin : @bottom_margin + @marker_caps_height + LABEL_MARGIN
869
+ graph_bottom_margin = hide_bottom_label_area? ? @bottom_margin : @bottom_margin + marker_caps_height + LABEL_MARGIN
884
870
  graph_bottom_margin += (calculate_legend_height + @legend_margin) if @legend_at_bottom
885
871
 
886
- x_axis_label_height = @x_axis_label.nil? ? 0.0 : @marker_caps_height + LABEL_MARGIN
887
- # FIXME: Consider chart types other than bar
872
+ x_axis_label_height = @x_axis_label.nil? ? 0.0 : marker_caps_height + (LABEL_MARGIN * 2)
888
873
  @raw_rows - graph_bottom_margin - x_axis_label_height - @label_stagger_height
889
874
  end
890
875
 
@@ -919,7 +904,7 @@ module Gruff
919
904
  else
920
905
  value.to_s
921
906
  end
922
- elsif (@spread.to_f % (marker_count.to_f == 0 ? 1 : marker_count.to_f) == 0) || !@y_axis_increment.nil?
907
+ elsif (@spread % (marker_count == 0 ? 1 : marker_count) == 0) || !@y_axis_increment.nil?
923
908
  value.to_i.to_s
924
909
  elsif @spread > 10.0
925
910
  sprintf('%0i', value)
@@ -952,44 +937,31 @@ module Gruff
952
937
  end
953
938
 
954
939
  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
940
+ label_widths = [[]]
941
+ label_lines = [[]]
957
942
  legend_labels.each do |label|
958
943
  width = calculate_width(@legend_font, label)
959
- label_width = width + legend_square_width * 2.7
944
+ label_width = width + (legend_square_width * 2.7)
960
945
  label_widths.last.push label_width
946
+ label_lines.last.push label
961
947
 
962
948
  if label_widths.last.sum > (@raw_columns * 0.9)
963
949
  label_widths.push [label_widths.last.pop]
950
+ label_lines.push [label_lines.last.pop]
964
951
  end
965
952
  end
966
953
 
967
- label_widths
954
+ label_widths.map(&:sum).zip(label_lines)
968
955
  end
969
956
 
970
957
  def calculate_legend_height
971
958
  return 0.0 if @hide_legend
972
959
 
973
960
  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
961
+ legend_label_lines = calculate_legend_label_widths_for_each_line(legend_labels, @legend_box_size)
962
+ line_height = [legend_caps_height, @legend_box_size].max
991
963
 
992
- legend_height + @legend_caps_height
964
+ (line_height * legend_label_lines.count) + (@legend_margin * (legend_label_lines.count - 1))
993
965
  end
994
966
 
995
967
  # Returns the height of the capital letter 'X' for the current font and
@@ -998,7 +970,18 @@ module Gruff
998
970
  # Not scaled since it deals with dimensions that the regular scaling will
999
971
  # handle.
1000
972
  def calculate_caps_height(font)
1001
- metrics = Gruff::Renderer::Text.new(renderer, 'X', font: font).metrics
973
+ calculate_height(font, 'X')
974
+ end
975
+
976
+ # Returns the height of a string at this point size.
977
+ #
978
+ # Not scaled since it deals with dimensions that the regular scaling will
979
+ # handle.
980
+ def calculate_height(font, text)
981
+ text = text.to_s
982
+ return 0 if text.empty?
983
+
984
+ metrics = text_metrics(font, text)
1002
985
  metrics.height
1003
986
  end
1004
987
 
@@ -1010,10 +993,14 @@ module Gruff
1010
993
  text = text.to_s
1011
994
  return 0 if text.empty?
1012
995
 
1013
- metrics = Gruff::Renderer::Text.new(renderer, text, font: font).metrics
996
+ metrics = text_metrics(font, text)
1014
997
  metrics.width
1015
998
  end
1016
999
 
1000
+ def text_metrics(font, text)
1001
+ Gruff::Renderer::Text.new(renderer, text, font: font).metrics
1002
+ end
1003
+
1017
1004
  def calculate_increment
1018
1005
  if @y_axis_increment.nil?
1019
1006
  # Try to use a number of horizontal lines that will come out even.
@@ -1027,9 +1014,13 @@ module Gruff
1027
1014
  end
1028
1015
  end
1029
1016
 
1030
- # Used for degree => radian conversions
1017
+ # Used for degree <=> radian conversions
1031
1018
  def deg2rad(angle)
1032
- angle * (Math::PI / 180.0)
1019
+ (angle * Math::PI) / 180.0
1020
+ end
1021
+
1022
+ def rad2deg(angle)
1023
+ (angle / Math::PI) * 180.0
1033
1024
  end
1034
1025
  end
1035
1026
 
data/lib/gruff/bezier.rb CHANGED
@@ -22,7 +22,7 @@ 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
28
  poly_points = []
@@ -30,7 +30,7 @@ private
30
30
  data_row[1].each_with_index do |data_point, index|
31
31
  # Use incremented x and scaled y
32
32
  new_x = @graph_left + (x_increment * index)
33
- new_y = @graph_top + (@graph_height - data_point * @graph_height)
33
+ new_y = @graph_top + (@graph_height - (data_point * @graph_height))
34
34
 
35
35
  if index == 0 && RUBY_PLATFORM != 'java'
36
36
  poly_points << new_x
@@ -0,0 +1,174 @@
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) / normalized_boxes.size
48
+ bar_width = width * @spacing_factor
49
+ padding = width - bar_width
50
+
51
+ normalized_boxes.each_with_index do |box, index|
52
+ left_x = @graph_left + (width * index) + (padding / 2.0)
53
+ right_x = left_x + bar_width
54
+ center_x = (left_x + right_x) / 2.0
55
+
56
+ first_y, = conversion.get_top_bottom_scaled(box.first_quartile)
57
+ third_y, = conversion.get_top_bottom_scaled(box.third_quartile)
58
+ Gruff::Renderer::Rectangle.new(renderer, color: box.color, width: @stroke_width, opacity: @fill_opacity)
59
+ .render(left_x, first_y, right_x, third_y)
60
+
61
+ median_y, = conversion.get_top_bottom_scaled(box.median)
62
+ Gruff::Renderer::Line.new(renderer, color: box.color, width: @stroke_width * 2).render(left_x, median_y, right_x, median_y)
63
+
64
+ minmax_left_x = left_x + (bar_width / 4.0)
65
+ minmax_right_x = right_x - (bar_width / 4.0)
66
+ min_y, = conversion.get_top_bottom_scaled(box.lower_whisker)
67
+ Gruff::Renderer::Line.new(renderer, color: box.color, width: @stroke_width).render(minmax_left_x, min_y, minmax_right_x, min_y)
68
+ Gruff::Renderer::DashLine.new(renderer, color: box.color, width: @stroke_width, dasharray: [@stroke_width, @stroke_width * 2])
69
+ .render(center_x, min_y, center_x, first_y)
70
+
71
+ max_y, = conversion.get_top_bottom_scaled(box.upper_whisker)
72
+ Gruff::Renderer::Line.new(renderer, color: box.color, width: @stroke_width).render(minmax_left_x, max_y, minmax_right_x, max_y)
73
+ Gruff::Renderer::DashLine.new(renderer, color: box.color, width: @stroke_width, dasharray: [@stroke_width, @stroke_width * 2])
74
+ .render(center_x, max_y, center_x, third_y)
75
+
76
+ box.lower_outliers.each do |outlier|
77
+ outlier_y, = conversion.get_top_bottom_scaled(outlier)
78
+ Gruff::Renderer::Dot.new(renderer, :circle, color: box.color, opacity: @fill_opacity).render(center_x, outlier_y, @stroke_width * 2)
79
+ end
80
+
81
+ box.upper_outliers.each do |outlier|
82
+ outlier_y, = conversion.get_top_bottom_scaled(outlier)
83
+ Gruff::Renderer::Dot.new(renderer, :circle, color: box.color, opacity: @fill_opacity).render(center_x, outlier_y, @stroke_width * 2)
84
+ end
85
+
86
+ draw_label(center_x, index)
87
+ end
88
+ end
89
+
90
+ def normalized_boxes
91
+ @normalized_boxes ||= store.norm_data.map { |data| Gruff::BoxPlot::BoxData.new(data.label, data.points, data.color) }
92
+ end
93
+
94
+ def calculate_spacing
95
+ @scale * (column_count - 1)
96
+ end
97
+
98
+ # @private
99
+ class BoxData < Struct.new(:label, :points, :color)
100
+ def initialize(label, points, color)
101
+ super(label, points.sort, color)
102
+ end
103
+
104
+ def min
105
+ points.first || 0
106
+ end
107
+
108
+ def max
109
+ points.last || 0
110
+ end
111
+
112
+ def min_whisker
113
+ [min, first_quartile - (1.5 * interquartile_range)].max
114
+ end
115
+
116
+ def max_whisker
117
+ [max, third_quartile + (1.5 * interquartile_range)].min
118
+ end
119
+
120
+ def upper_whisker
121
+ max = max_whisker
122
+ points.select { |point| point <= max }.max
123
+ end
124
+
125
+ def lower_whisker
126
+ min = min_whisker
127
+ points.select { |point| point >= min }.min
128
+ end
129
+
130
+ def median
131
+ if points.size.zero?
132
+ 0
133
+ elsif points.size.odd?
134
+ points[points.size / 2]
135
+ else
136
+ (points[points.size / 2] + points[(points.size / 2) - 1]) / 2.0
137
+ end
138
+ end
139
+
140
+ def first_quartile
141
+ if points.size.zero?
142
+ 0
143
+ elsif points.size.odd?
144
+ points[points.size / 4]
145
+ else
146
+ (points[points.size / 4] + points[(points.size / 4) - 1]) / 2.0
147
+ end
148
+ end
149
+
150
+ def third_quartile
151
+ if points.size.zero?
152
+ 0
153
+ elsif points.size.odd?
154
+ points[(points.size * 3) / 4]
155
+ else
156
+ (points[(points.size * 3) / 4] + points[((points.size * 3) / 4) - 1]) / 2.0
157
+ end
158
+ end
159
+
160
+ def lower_outliers
161
+ min = lower_whisker
162
+ points.select { |point| point < min }
163
+ end
164
+
165
+ def upper_outliers
166
+ max = upper_whisker
167
+ points.select { |point| point > max }
168
+ end
169
+
170
+ def interquartile_range
171
+ third_quartile - first_quartile
172
+ end
173
+ end
174
+ end