From b71bed8ebe1041b35a3c0f8e57ec0005eaf2a704 Mon Sep 17 00:00:00 2001 From: masukomi Date: Fri, 3 Feb 2023 10:33:18 -0500 Subject: [PATCH] Added Horizontal graph - refactored comment stuff out into core - renamed test & tester to be vertical specific - added horizontal_tester.raku - added Horizontal.rakumod - added 02-horizontal.rakutest --- META6.json | 2 + horizontal-graph-tester.raku | 43 ++++++ lib/CLI/Graphing/BarChart/Core.rakumod | 65 +++++++++ lib/CLI/Graphing/BarChart/Horizontal.rakumod | 90 ++++++++++++ lib/CLI/Graphing/BarChart/Vertical.rakumod | 138 +++++++----------- t/{01-basic.rakutest => 01-vertical.rakutest} | 0 t/02-horizontal.rakutest | 58 ++++++++ tester.raku => vertical-graph-tester.raku | 0 8 files changed, 314 insertions(+), 82 deletions(-) create mode 100755 horizontal-graph-tester.raku create mode 100644 lib/CLI/Graphing/BarChart/Core.rakumod create mode 100644 lib/CLI/Graphing/BarChart/Horizontal.rakumod rename t/{01-basic.rakutest => 01-vertical.rakutest} (100%) create mode 100644 t/02-horizontal.rakutest rename tester.raku => vertical-graph-tester.raku (100%) diff --git a/META6.json b/META6.json index bb2994c..0575456 100644 --- a/META6.json +++ b/META6.json @@ -14,6 +14,8 @@ "name": "CLI::Graphing::BarChart", "perl": "6.d", "provides": { + "CLI::Graphing::BarChart::Core": "lib/CLI/Graphing/BarChart/Core.rakumod", + "CLI::Graphing::BarChart::Horizontal": "lib/CLI/Graphing/BarChart/Horizontal.rakumod", "CLI::Graphing::BarChart::Vertical": "lib/CLI/Graphing/BarChart/Vertical.rakumod" }, "resources": [ diff --git a/horizontal-graph-tester.raku b/horizontal-graph-tester.raku new file mode 100755 index 0000000..82972dd --- /dev/null +++ b/horizontal-graph-tester.raku @@ -0,0 +1,43 @@ +#!/usr/bin/env raku + +use lib 'lib'; +use CLI::Graphing::BarChart::Horizontal; + + +say "X and Y axis\n"; +my $x_and_y_axis_graph = CLI::Graphing::BarChart::Horizontal.new( + data => [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100], + graph_height => 10, + x_axis_labels => , + y_axis_labels => <0 1 2 3 4 5 6 7 8 9 10> +); + +$x_and_y_axis_graph.print(); + + +say "\n\nJust Y axis\n"; +my $y_axis_graph = CLI::Graphing::BarChart::Horizontal.new( + data => [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100], + graph_height => 10, + y_axis_labels => <0 1 2 3 4 5 6 7 8 9 10 11>, +); +$y_axis_graph.print(); + + +say "\n\nWide Y axis labels\n"; +$x_and_y_axis_graph = CLI::Graphing::BarChart::Horizontal.new( + data => [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100], + graph_height => 10, + x_axis_labels => , + y_axis_labels => , +); +$x_and_y_axis_graph.print(); + +say "\n\nInsufficent Wide Y axis labels\n"; +$x_and_y_axis_graph = CLI::Graphing::BarChart::Horizontal.new( + data => [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100], + graph_height => 10, + x_axis_labels => , + y_axis_labels => , +); +$x_and_y_axis_graph.print(); diff --git a/lib/CLI/Graphing/BarChart/Core.rakumod b/lib/CLI/Graphing/BarChart/Core.rakumod new file mode 100644 index 0000000..0cd01af --- /dev/null +++ b/lib/CLI/Graphing/BarChart/Core.rakumod @@ -0,0 +1,65 @@ +class CLI::Graphing::BarChart::Core { + use Listicles; + has $.data; + has $.bar_length is rw = 10; + has $.y_axis_labels is rw = []; + has $.x_axis_labels is rw = []; + # has $.bar_drawing_character = '█'; + + # Implement these in subclasses + method generate(){...} + + #| Prints the graph to standard out. + method print() { + say self.generate(); + } + + method validate-bar-length() { + unless $!bar_length { + die("You must specify a bar_length (number of lines for the core graph)."); + } + } + + # each row is an array of the characters that make up that row + method generate-core-graph(Array $data, + Int $bar_length, + Str $bar_element_character + ) returns Array { + + my @rows = []; + for $data.pairs -> $pair { + my $num_chars = $pair.value * ($bar_length / 100); + + my $bar_chars = ($bar_element_character x $num_chars); + my @new_row = $bar_length == 1 + ?? $bar_chars.split('', skip-empty => True).Array + !! self.pad-with-x-array($bar_chars, $bar_length); + + # remember, the graph is sideways, so the x axis is currently on the left + # if it's not there we don't do anything + # if it is there we prepend to the left side of the row + @rows.push: @new_row; + } + return @rows; + } + # pads the string you passed in with spaces up to $width + method pad-with-space(Str $string, Int $width, Bool $pad_right=True) returns Str { + return $string if $string.chars >= $width; + + return sprintf('%-' ~ $width ~ 's', $string) if $pad_right; + return sprintf('%' ~ $width ~ 's', $string); + } + # method !pad-with-x-array(Str $string, Int $width, Str $padding_char=' ') returns Array { + method pad-with-x-array(Str $string, + Int $width, + Str $padding_char=' ', + Bool $pad_right = True) returns Array { + my @response = $string.split('', skip-empty => True).Array; + return @response if @response.elems >= $width; + if $pad_right { + return @response.append($padding_char xx $width - @response.elems); + } + return ($padding_char xx $width - @response.elems).Array.append: @response; + } + +} diff --git a/lib/CLI/Graphing/BarChart/Horizontal.rakumod b/lib/CLI/Graphing/BarChart/Horizontal.rakumod new file mode 100644 index 0000000..29d75bf --- /dev/null +++ b/lib/CLI/Graphing/BarChart/Horizontal.rakumod @@ -0,0 +1,90 @@ +use CLI::Graphing::BarChart::Core; + +class CLI::Graphing::BarChart::Horizontal is CLI::Graphing::BarChart::Core { + use Listicles; + # From Core... + # has $.data; + # has $.bar_length; + # has $.y_axis_labels = []; + # has $.x_axis_labels = []; + + has $.bar_drawing_character = '▄'; + + method generate() returns Str { + self.validate-or-die(); + + + my $rows = self.generate-core-graph($.data, $.bar_length, $.bar_drawing_character); + # X axis labels are just another row + # Y axis labels just get prepended + # 0x0 is top left + # the last row is the x axis labels (if present) + # + # 1. find the length of the longest y_axis_labels + my $max_y_label = $.y_axis_labels.map({.chars}).max; + my $border =[' ', '│', ' ']; + if $.y_axis_labels { + for (0..^$.data.elems) -> $index { + if $index < $.y_axis_labels.elems { + my $prepension = self.pad-with-x-array($.y_axis_labels[$index], + $max_y_label, + ' ', + False); + $rows[$index] = $prepension + .append($border.Array) + .append($rows[$index].Array); + } else { + # they have fewer Y axis items than data points + $rows[$index] = self.pad-with-x-array(" │ ", + $max_y_label + 3, + ' ', + False + ).append($rows[$index].Array); + } + } + if ! $.x_axis_labels.is-empty { + $rows.push(self.pad-with-x-array(" │ ", + $max_y_label + 3, + ' ', + False + ) + .append($.x_axis_labels.Array)) + } + } elsif ! $.x_axis_labels.is-empty { + $rows.push($.x_axis_labels) + } + + self!join-rows($rows); + } + method !join-rows($rows){ + $rows.map({.join('')}).join("\n"); + } + + method validate-or-die(){ + self.validate-bar-length(); + if $.x_axis_labels { + self.validate-x-labels($.x_axis_labels); + self.x_axis_labels = $.x_axis_labels.map({.Str}).Array; + } + if $.y_axis_labels { + self.validate-y-labels($.y_axis_labels, $.data); + self.y_axis_labels = $.y_axis_labels.map({.Str}).Array; + } + + } + #| either works or dies + method validate-x-labels($x_labels) { + return True unless $x_labels; + my $all_good = $x_labels.all-are(-> $x {$x.chars <= 1 }); + die("x labels on horizontal graphs must be 1 or zero characters long") unless $all_good; + if $x_labels.elems > $.bar_length { + die ("You can't have more x labels than characters in the bar_length"); + } + } + method validate-y-labels($y_labels, $data){ + return True unless $y_labels; + if $y_labels.elems > 0 & $y_labels.elems > $data.elems { + die("You can't have more y labels than data elements.") + } + } +} diff --git a/lib/CLI/Graphing/BarChart/Vertical.rakumod b/lib/CLI/Graphing/BarChart/Vertical.rakumod index 5955b22..d09458a 100644 --- a/lib/CLI/Graphing/BarChart/Vertical.rakumod +++ b/lib/CLI/Graphing/BarChart/Vertical.rakumod @@ -1,51 +1,40 @@ -class CLI::Graphing::BarChart::Vertical { +use CLI::Graphing::BarChart::Core; + +class CLI::Graphing::BarChart::Vertical is CLI::Graphing::BarChart::Core { use Listicles; - has $.data; - has $.graph_height; - has $.y_axis_labels = []; - has $.x_axis_labels = []; - has $.bar_column_character = '█'; + + # From Core... + # has $.data; + # has $.bar_length; + # has $.y_axis_labels = []; + # has $.x_axis_labels = []; + + has $.bar_drawing_character = '█'; has $.x_axis_divider_character = '─'; has $.space_between_columns = True; + has $.graph_height is rw = 10; - #| Prints the graph to standard out. - method print() { - say self.generate(); - } #| Generates a string representation of the graph. method generate() returns Str { - unless $!graph_height { - die("You must specify a graph_height (number of lines for the core graph)."); - } - if $!x_axis_labels.elems > 0 and $!x_axis_labels.elems != $!data.elems { - die("There must be 1 x axis label for each data element."); - } elsif $!x_axis_labels.elems > 0 { - $!x_axis_labels = $!x_axis_labels.map({.Str}).Array; - } - if $!y_axis_labels.elems > 0 and $!y_axis_labels.elems != $!graph_height { - die("There must be 1 y axis label for each row of the graph."); - } elsif $!y_axis_labels.elems > 0 { - $!y_axis_labels = $!y_axis_labels.map({.Str}).Array; - } - + self.bar_length = $.graph_height; + self.validate-or-die(); # data is a list of numbers # we're going to assume numbers are percentages - # graph_height is the max height of the table - # graph_height = 100% - # column height is $graph_height * ($datum / 100) + # bar_length is the max height of the table + # bar_length = 100% + # column height is $bar_length * ($datum / 100) # # BUT we're actually going to bulid the table horizontally # then rotate it left 90° # 1st datum = first row. # - my $rows = self!generate-sideways-graph($!data, - $!x_axis_labels, - $!graph_height, - $!bar_column_character); + my $rows = self.generate-core-graph($.data, + $.bar_length, + $.bar_drawing_character); # now we've got a bunch of horizontal bars @@ -72,12 +61,12 @@ class CLI::Graphing::BarChart::Vertical { # reversing the labels because 0x0 on the grid is bottom left # but we need 0 to be the top one not the bottom - my @reversed_y_labels = $!y_axis_labels.reverse; + my @reversed_y_labels = $.y_axis_labels.reverse; # Going to need these in a moment. - my $max_x_label = $!x_axis_labels.is-empty + my $max_x_label = $.x_axis_labels.is-empty ?? 0 - !! $!x_axis_labels.map({.chars}).max; + !! $.x_axis_labels.map({.chars}).max; my $max_y_label = @reversed_y_labels.is-empty ?? 0 !! @reversed_y_labels.map({.chars}).max; @@ -95,15 +84,15 @@ class CLI::Graphing::BarChart::Vertical { # This is separated out because the special handling # required for the divider line resulted in a whole # pile of "oh but if there's an x label" complications - if ! $!x_axis_labels.is-empty { - $padded_rows = self!append-x-label-rows($padded_rows, $!x_axis_labels, $max_y_label); + if ! $.x_axis_labels.is-empty { + $padded_rows = self!append-x-label-rows($padded_rows, $.x_axis_labels, $max_y_label); } # join the cells together into one big string # with or without spaces between columns. my $response = self!join-rows($padded_rows, - $!space_between_columns, - (! $!x_axis_labels.is-empty), + $.space_between_columns, + (! $.x_axis_labels.is-empty), $max_y_label); return $response; } @@ -112,45 +101,6 @@ class CLI::Graphing::BarChart::Vertical { #### Private stuff you don't need to worry about #### ... unless there's a bug ;) - # pads the string you passed in with spaces up to $width - method !pad-with-space(Str $string, Int $width, Bool $pad_right=True) returns Str { - return $string if $string.chars >= $width; - - return sprintf('%-' ~ $width ~ 's', $string) if $pad_right; - return sprintf('%' ~ $width ~ 's', $string); - } - # method !pad-with-x-array(Str $string, Int $width, Str $padding_char=' ') returns Array { - method !pad-with-x-array(Str $string, - Int $width, - Str $padding_char=' ', - Bool $pad_right = True) returns Array { - my @response = $string.split('', skip-empty => True).Array; - return @response if @response.elems >= $width; - if $pad_right { - return @response.append($padding_char xx $width - @response.elems); - } - return ($padding_char xx $width - @response.elems).Array.append: @response; - } - - method !generate-sideways-graph(Array $data, - Array $x_axis_labels, - Int $graph_height, - Str $bar_column_character) returns Array { - - my @rows = []; - my @col_widths = []; # if your x axis label is > 1 char we need to compensate - for $data.pairs -> $pair { - my $num_chars = $graph_height * ($pair.value / 100); - - my @new_row = self!pad-with-x-array(($bar_column_character x $num_chars), $graph_height); - # remember, the graph is sideways, so the x axis is currently on the left - # if it's not there we don't do anything - # if it is there we prepend to the left side of the row - @rows.push: @new_row; - } - return @rows; - } - method !append-x-label-rows(Array $rows, Array $x_axis_labels, Int $max_y_label) returns Array { @@ -166,10 +116,10 @@ class CLI::Graphing::BarChart::Vertical { my @label_row = @divider_row; # (one column's worth of ─) repeated for num columns @divider_row.append: ( - ($!x_axis_divider_character x $max_x_label) xx $x_axis_labels.elems + ($.x_axis_divider_character x $max_x_label) xx $x_axis_labels.elems ).Array; - @label_row.append: $x_axis_labels.map({self!pad-with-space($_, $max_x_label)}).Array; + @label_row.append: $x_axis_labels.map({self.pad-with-space($_, $max_x_label)}).Array; $rows.push: @divider_row; $rows.push: @label_row; @@ -196,7 +146,7 @@ class CLI::Graphing::BarChart::Vertical { my @row = []; # $row_pair: 9 => $(" ", " ", " ", " ", " ", "█") if $has_y_labels { - my $left_chars = self!pad-with-x-array( + my $left_chars = self.pad-with-x-array( $reversed_y_labels[$row_pair.key], $max_y_label, ' ', @@ -206,7 +156,7 @@ class CLI::Graphing::BarChart::Vertical { @row.push: $left_chars; } for $original_row.Array -> $column { - my $padded_column = self!pad-with-space($column, $column_width); + my $padded_column = self.pad-with-space($column, $column_width); @row.append: $padded_column; } @result.push: @row; @@ -237,7 +187,7 @@ class CLI::Graphing::BarChart::Vertical { } # now deal with the last 2 rows @response.append: $padded_rows[*-2].join( - $has_x_axis ?? $!x_axis_divider_character !! ' ' + $has_x_axis ?? $.x_axis_divider_character !! ' ' ); @response.append: "\n"; @response.append: $padded_rows[*-1].join(' '); @@ -246,4 +196,28 @@ class CLI::Graphing::BarChart::Vertical { return $padded_rows.map({.join('')}).join("\n"); } } + method validate-or-die(){ + self.validate-bar-length(); + if $.x_axis_labels { + self.validate-x-labels($.x_axis_labels, $.data); + self.x_axis_labels = $.x_axis_labels.map({.Str}).Array; + # $.x_axis_labels.map({.Str}).Array; + } + if $.y_axis_labels { + self.validate-y-labels($.y_axis_labels); + self.y_axis_labels = $.y_axis_labels.map({.Str}).Array; + # $.y_axis_labels.map({.Str}).Array; + } + } + method validate-x-labels($x_labels, $data){ + return True unless $x_labels; + if $x_labels.elems > 0 and $x_labels.elems != $data.elems { + die("There must be 1 x axis label for each data element."); + } + } + method validate-y-labels($y_labels){ + if $y_labels.elems > 0 and $y_labels.elems != $.bar_length { + die("There must be 1 y axis label for each row of the graph."); + } + } } diff --git a/t/01-basic.rakutest b/t/01-vertical.rakutest similarity index 100% rename from t/01-basic.rakutest rename to t/01-vertical.rakutest diff --git a/t/02-horizontal.rakutest b/t/02-horizontal.rakutest new file mode 100644 index 0000000..3b46d31 --- /dev/null +++ b/t/02-horizontal.rakutest @@ -0,0 +1,58 @@ +use Test; +use lib 'lib'; +use CLI::Graphing::BarChart::Horizontal; + +my $data = [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]; +my $bare_graph = CLI::Graphing::BarChart::Horizontal.new( + data => $data, + bar_length => 10 +); + + +my $expected_graph = +" \n▄ \n▄▄ \n▄▄▄ \n▄▄▄▄ \n▄▄▄▄▄ \n▄▄▄▄▄▄ \n▄▄▄▄▄▄▄ \n▄▄▄▄▄▄▄▄ \n▄▄▄▄▄▄▄▄▄ \n▄▄▄▄▄▄▄▄▄▄"; + +is $bare_graph.generate(), $expected_graph, 'bad bare graph'; + + + +my $x_axis_graph = CLI::Graphing::BarChart::Horizontal.new( + data => $data, + bar_length => 10, + x_axis_labels => .Array +); +throws-like { $x_axis_graph.generate }, Exception, message => /"can't have more x labels"/; + +$x_axis_graph = CLI::Graphing::BarChart::Horizontal.new( + data => $data, + bar_length => 10, + x_axis_labels => .Array +); + +$expected_graph = +" \n▄ \n▄▄ \n▄▄▄ \n▄▄▄▄ \n▄▄▄▄▄ \n▄▄▄▄▄▄ \n▄▄▄▄▄▄▄ \n▄▄▄▄▄▄▄▄ \n▄▄▄▄▄▄▄▄▄ \n▄▄▄▄▄▄▄▄▄▄\nabcdefghij"; + +is $x_axis_graph.generate, $expected_graph, 'bad x axis only graph'; + +my $y_axis_graph = CLI::Graphing::BarChart::Horizontal.new( + data => $data, + bar_length => 10, + y_axis_labels => .Array +); + +$expected_graph = "a │ \nb │ ▄ \nc │ ▄▄ \nd │ ▄▄▄ \ne │ ▄▄▄▄ \nf │ ▄▄▄▄▄ \ng │ ▄▄▄▄▄▄ \nh │ ▄▄▄▄▄▄▄ \ni │ ▄▄▄▄▄▄▄▄ \nj │ ▄▄▄▄▄▄▄▄▄ \n │ ▄▄▄▄▄▄▄▄▄▄"; + +is $y_axis_graph.generate, $expected_graph, 'bad y axis only graph'; + +my $x_and_y_axis_graph = CLI::Graphing::BarChart::Horizontal.new( + data => $data, + bar_length => 10, + x_axis_labels => .Array, + y_axis_labels => <0 1 2 3 4 5 6 7 8 9>.Array +); + +$expected_graph ="0 │ \n1 │ ▄ \n2 │ ▄▄ \n3 │ ▄▄▄ \n4 │ ▄▄▄▄ \n5 │ ▄▄▄▄▄ \n6 │ ▄▄▄▄▄▄ \n7 │ ▄▄▄▄▄▄▄ \n8 │ ▄▄▄▄▄▄▄▄ \n9 │ ▄▄▄▄▄▄▄▄▄ \n │ ▄▄▄▄▄▄▄▄▄▄\n │ abcdefghij"; + +is $x_and_y_axis_graph.generate, $expected_graph, 'bad x + y axis graph'; + +done-testing; diff --git a/tester.raku b/vertical-graph-tester.raku similarity index 100% rename from tester.raku rename to vertical-graph-tester.raku