gruff 0.15.0-java → 0.16.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.
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