P_Z
P_Z

Reputation: 309

Plot Graph in Console by printing special character say * and spaces using matrix structure in Perl

My task is to plot Least Mean Squared Error (LSME) values from each iteration of a machine learning algorithm in a Graph of X and Y axes/coordinates. I decided to print special character (say *) on the console using loops. I do not want to use any libraries for graph plotting but to be simple by printing sequence of special character so that I may be able to print first quadrant of X-Y coordinates onto console.

I recall my initial programming assignments in Java to print different shapes on console like Pyramid, Square, Rectangle, Circle etc. using for and while loops. Also, I am familiar with NDC to view port mapping in graphics programming. But I am unable to implement such nested loops that print my required graph in first quadrant on console as same that we draw on paper.

On console, the origin (0,0) is top left corner of console. But on paper the origin is left bottom if we only plot first quadrant. For overcoming this problem I cracked an idea that I use a 2 D matrix structure and some transpose operation of it and use characters (BLANK SPACE and *) for plotting my graph. I developed following code which has two arrays, one with error values (LMSE) and the other one with the count of spaces.

use strict;
use warnings;
use Data::Dumper;

$|= 1;

my @values = (0.7,0.9,2,0.1,1.2,2.4,0.4,3.5,4.9); # Float error values with 1 decimal place
my @values2;

my $XAxis_LMSE = scalar @values;
my ($minLMSE_Graph, $maxLMSE_Graph) = (sort {$a <=> $b} @values)[0, -1];

for (my $i = 0; $i < scalar @values; $i++) {
    my $rem = $maxLMSE_Graph - $values[$i];
    push (@values2, $rem);
}

I computed maximum value of my error values array and assigned the difference of Max value with original error value to another array. The logic which I am able to conceive is that I fill a matrix with spaces and * which when printed on console depict a X-Y first quadrant graph on console. Is my approach promising? Can somebody confirm my approach is correct and how to build such a matrix of " " and "*" characters?

Y(x) values are given by array @values and X is number of Iterations. Iterations can go from 1 to say 100. While Y(x) also remains an Integer. Its a simple Column Bar Graph. Below is a sample graph in Excel but the column Bars will be series of character "*" on console. It will be a vertical Bar Graph.

enter image description here

Upvotes: 2

Views: 746

Answers (2)

zdim
zdim

Reputation: 66883

While libraries are dismissed by the question, the requirements elaborated in comments (large x-axis span, floating point values, particular data ranges) make it hard to do justice to the problem by a hand-rolled snippet like in my other answer.

So here is an example using the great gnuplot library, which has many features and publication quality plots. Learning it isn't hard, with lots of resources and examples.

The library does have an option to print to what it calls "dumb" terminal, ie. ascii, the main need here. And then there is an endless stream of options for exactly how to plot, some very sophisticated. But basic plotting is dirt simple.

There is also a wrapper module for Perl, Chart::Gnuplot, which makes it simpler yet.

use warnings;
use strict;
use feature 'say';

use Path::Tiny;  # path()->slurp
use List::Util qw(max min);
use Chart::Gnuplot;

#my $outfile = shift // 'ascii_gnuplot.out';

my @y = (0.7, 0.9, 2, 0.1, 1.2, 2.4, 0.4, 3.5, 4.9);
my @x = 1 .. @y;
# Or, to see how it goes about large span of x-axis
#demo_large_x_span(\@x, \@y);

my $y_min = min @y;
my $yrange_min = $y_min > 0 ? 0 : $y_min;

my $chart = Chart::Gnuplot->new( 
    terminal  => 'dumb',  # ascii
    xrange    => [min(@x), max @x], 
    yrange    => [$yrange_min, max @y], 
    title     => "Least Mean Squared Error with iterations",
    timestamp => { 
        fmt    => '%d/%m/%y %H:%M',
        offset => "6, -1",
    },
    #output    => $outfile,
);
# All options can be given in the constructor itself
# but these are particular for plotting in terminal
$chart->set( 
    xtics => { length => 0 },  # don't show tics
    ytics => { length => 0 },
    x2tics => undef,
    y2tics => undef,
    key => undef,              # no legend with filename
);

my $dataset = Chart::Gnuplot::DataSet->new(
    xdata => \@x,
    ydata => \@y,
    style => "impulses",  # "dots" for scatter plot
);

$chart->plot2d($dataset);  # done. shows on screen or in "output" file

sub demo_large_x_span {
    my ($xref, $yref, $max_x) = @_;
    $max_x //= 200;
    for (1 .. $max_x) {
        if ($_ % 10 == 0) {
           push @$xref, $_;
           push @$yref, sqrt($_); #$_*0.5;
        }
    }
}

If this fails to print the ascii graph to terminal then uncomment the output => $outfile option (and the filename's definition). Then open and print the file out of the program.

The above program prints


                    Least Mean Squared Error with iterations

      +-------+------+-------+-------+------+-------+-------+------+-------*
  4.5 +                                                                    *
      |                                                                    *
    4 +                                                                    *
      |                                                                    *
  3.5 +                                                            *       *
    3 +                                                            *       *
      |                                                            *       *
  2.5 +                                             *              *       *
    2 +                      *                      *              *       *
      |                      *                      *              *       *
  1.5 +                      *                      *              *       *
      |                      *              *       *              *       *
    1 +              *       *              *       *              *       *
  0.5 +       *      *       *              *       *              *       *
      |       *      *       *              *       *       *      *       *
    0 +-------*------*-------*-------*------*-------*-------*------*-------*
      0       1      2       3       4      5       6       7      8       9

      12/09/22 01:09

I suggest to review the many, many options available so to get a feel for what kinds of plots are possible and how well one can craft a plot.

A nice overview is in gnuplot demos and then in Chart::Gnuplot gallery and examples.

Finally, a grand advantage of using a library for this is that by "flipping a switch" one can have a top-notch plot in one of many formats (change terminal setting, or simply drop it and name the output file with the extension of the desired graphics format).


Use of this module isn't necessary. One can write a file with specifications for gnuplot and then run gnuplot on that file, out of the program. (I'd consider this to be an overall simpler way, if one is comfortable with gnuplot's commands.)

Here is how to obtain the above plot that way

use warnings;
use strict;
use feature 'say';

use List::Util qw(max min);

my @y = (0.7, 0.9, 2, 0.1, 1.2, 2.4, 0.4, 3.5, 4.9);
my @x = 1 .. @y;

my ($datafile, $gp_file) = build_plot_cmd(\@x, \@y);

system( "gnuplot $gp_file" ) == 0 or die $!;

sub build_plot_cmd {
    my ($xref, $yref) = @_; 

    # Write data to a file for gnuplot (can also use "inline" data)
    my $data_gp = 'data.gp';
    open my $fh_d, '>', $data_gp or die $!;
    for my $i (0 .. $#$xref) {
        say $fh_d "$xref->[$i] $yref->[$i]";
    }
    close $fh_d;

    my $x_max = max @$xref;
    my $y_min = min @$yref;
    my $yrange_min = $y_min > 0 ? 0 : $y_min;
    my $y_max = max @$yref;

    # Build gnuplot command-file and then write it
    my $gp_cmd = <<"END_CMD_FILE";
# Plot LMSE vs number of iterations
set term dumb   # feed 80 40
set xrange [0:$x_max]
set yrange [$yrange_min:$y_max]
set title "Least Mean Squared Error with iterations"

set xtics scale 0 nomirror
set ytics scale 0
unset y2tics
unset key

plot '$data_gp' with impulses

END_CMD_FILE

    my $gp_file = 'set_cmd.gp';
    open my $fh_gp, '>', $gp_file  or die $!;
    say $fh_gp $gp_cmd;
    close $fh_gp;

    return $data_gp, $gp_file;
}

This prints out the same plot to terminal, with tiny differences. Do as you please with generated files; the program can remove them, or use File::Temp in the first place -- or name them carefully and keep for posteriority (in a separate directory perhaps).


Files meant to be temporary can be made and managed using File::Temp

Upvotes: 3

zdim
zdim

Reputation: 66883

Update in requirements is a game-changer. See discussion and code furhter below.


One way, with originally posted integer data (see below for updated requirements)

use warnings;
use strict;
use feature 'say';

use List::Util qw(max min);

# =================================
# Data posted originally (integer): 
#   in code; with a negative value added; in Excel graph
# ======================================================
my @vals = (7,9,2,0,1,2,4,3,9);
#my @vals = (7,9,2,0,1,2,4,3,-2,9);
#my @vals = (38, 32, 28, 29, 34, 31, 15, 43, 43, 11, 4, 34);

my $max_y = max @vals;
my $min_y = min @vals;

my $min_y_to_show = ($min_y >= 0) ? 1 : $min_y;

for my $y (reverse $min_y_to_show .. $max_y) {
    printf "%2d | ", $y;  # y-axis: value for this row (and "axis")
    say join '',
        map { $_ >= $y ? ' * ' : ' 'x3 } @vals;
}
# x-axis, with its values
say ' 'x4, '-'x(3*@vals);
say ' 'x4, join '', map { sprintf "%3d", $_ } 1..@vals; 

Prints

 9 |     *                    * 
 8 |     *                    * 
 7 |  *  *                    * 
 6 |  *  *                    * 
 5 |  *  *                    * 
 4 |  *  *              *     * 
 3 |  *  *              *  *  * 
 2 |  *  *  *        *  *  *  * 
 1 |  *  *  *     *  *  *  *  * 
    ---------------------------
      1  2  3  4  5  6  7  8  9

I've made a few presentational choices of substance: to always plot down to 1 (even if all data are greater) and to not show zero -- unless there are negative values, when all is shown (add a negative value to @vals to test). These are changed fairly easily.

There's also some trivial formatting choices, for layout/spacing etc.

Otherwise there isn't anything manual really. Change @vals to plot a different data set, hopefully in the same style. This wasn't tested much.


Update in the question introduces floating point (decimal) values. This is further elaborated in comments, what altogether amounts to a library-grade project. And some of these wants are just not possible in ASCII in a terminal, where "plotting" goes by character and we only have a 100 or so. Here is code updated for what is feasible here, and some discussion.

To accommodate floating point values (with one digit of precision we are told), the y-axis now need be plotted in smaller increments ("divisions" -- "ticks"), lest we fail to show a lot of data if they are lumped within an integer.

Then, how to divide it? Below I show all data within 20 rows, and with a row for the smallest value added if needed. From that a division is worked out, for the given data set (updated in the question). If the data are clustered around some value far from zero then this isn't good of course (imagine data between 2.8 and 3.9, going by 0.1; why would we plot bars all the way from zero?). But one has to make decisions for a given data set, what can be done automatically as well.

This necessarily leads to some imprecision in how data is shown. Showing every data point correctly isn't feasible in general in a terminal.

use warnings;
use strict;
use feature 'say';

use List::Util qw(max min);
    
my @vals = (0.7, 0.9, 2, 0.1, 1.2, 2.4, 0.4, 3.5, 4.9);

my $n_rows = 20;

my $max_y = max @vals;
my $min_y = min @vals;

# Show from at least the smallest y-division ("tick");
# at first use 0 and then work out the "tick" and adjust
my $min_y_to_show = $min_y >= 0 ? 0 : $min_y;
my $y_tick = ($max_y - $min_y_to_show) / $n_rows;
# Now once we have the y-division ("tick") adjust 
$min_y_to_show = $min_y >= $y_tick ? $y_tick : $min_y;

say "Smallest division for y = $y_tick\n";

my @y_axis = map { $y_tick * $_ } 1 .. $n_rows;
unshift @y_axis, $min_y_to_show if $min_y_to_show < $y_axis[0];

for my $y (reverse @y_axis) {
    printf "%4.2f | ", $y;
    say join '', 
        map { $_ >= $y ? ' * ' : ' 'x3 } @vals;
}
say ' 'x6, '-'x(3*@vals);
say ' 'x6, join '', map { sprintf "%3d", $_ } 1..@vals; 

Prints

Smallest division for y = 0.245

4.90 |                          * 
4.66 |                          * 
4.41 |                          * 
4.17 |                          * 
3.92 |                          * 
3.68 |                          * 
3.43 |                       *  * 
3.19 |                       *  * 
2.94 |                       *  * 
2.70 |                       *  * 
2.45 |                       *  * 
2.21 |                 *     *  * 
1.96 |        *        *     *  * 
1.72 |        *        *     *  * 
1.47 |        *        *     *  * 
1.23 |        *        *     *  * 
0.98 |        *     *  *     *  * 
0.74 |     *  *     *  *     *  * 
0.49 |  *  *  *     *  *     *  * 
0.25 |  *  *  *     *  *  *  *  * 
0.10 |  *  *  *  *  *  *  *  *  * 
      ---------------------------
        1  2  3  4  5  6  7  8  9

In further discussion in comments it is explained that x-values may actually go into hundreds. That would have to be scaled (can't show 500 data points in a 100-char wide terminal) but then that comes with further decisions to make since not all data can be shown.

This amounts to much too much for a Stackoverflow Q-A. There are just too many details to be specified and decided on. Hopefully the discussion and code above is helpful for people to work out more elaborate scenarios.

Finally, if all this adds up to too much I can recommend gnuplot used out of Perl. It produces publication quality plots and it's fairly simple to use for simple things -- once learned, what isn't a terrible task with all the resources and example out there.

Otherwise, there are a number of other Perl libraries for graphing of various kinds.


This is for data shown in the original version of the question (seen in the code here)

With the values picked from the image of an Excel graph shown in the question, instead of the @vals used above (from the question's code), it prints

43 |                       *  *          
42 |                       *  *          
41 |                       *  *          
40 |                       *  *          
39 |                       *  *          
38 |  *                    *  *   
37 |  *                    *  *          
36 |  *                    *  *          
35 |  *                    *  *          
34 |  *           *        *  *        * 
33 |  *           *        *  *        * 
32 |  *  *        *        *  *        * 
31 |  *  *        *  *     *  *        * 
30 |  *  *        *  *     *  *        * 
29 |  *  *     *  *  *     *  *        * 
28 |  *  *  *  *  *  *     *  *        * 
27 |  *  *  *  *  *  *     *  *        * 
26 |  *  *  *  *  *  *     *  *        * 
25 |  *  *  *  *  *  *     *  *        * 
24 |  *  *  *  *  *  *     *  *        * 
23 |  *  *  *  *  *  *     *  *        * 
22 |  *  *  *  *  *  *     *  *        * 
21 |  *  *  *  *  *  *     *  *        * 
20 |  *  *  *  *  *  *     *  *        * 
19 |  *  *  *  *  *  *     *  *        * 
18 |  *  *  *  *  *  *     *  *        * 
17 |  *  *  *  *  *  *     *  *        * 
16 |  *  *  *  *  *  *     *  *        * 
15 |  *  *  *  *  *  *  *  *  *        * 
14 |  *  *  *  *  *  *  *  *  *        * 
13 |  *  *  *  *  *  *  *  *  *        * 
12 |  *  *  *  *  *  *  *  *  *        * 
11 |  *  *  *  *  *  *  *  *  *  *     * 
10 |  *  *  *  *  *  *  *  *  *  *     * 
 9 |  *  *  *  *  *  *  *  *  *  *     * 
 8 |  *  *  *  *  *  *  *  *  *  *     * 
 7 |  *  *  *  *  *  *  *  *  *  *     * 
 6 |  *  *  *  *  *  *  *  *  *  *     * 
 5 |  *  *  *  *  *  *  *  *  *  *     * 
 4 |  *  *  *  *  *  *  *  *  *  *  *  * 
 3 |  *  *  *  *  *  *  *  *  *  *  *  * 
 2 |  *  *  *  *  *  *  *  *  *  *  *  * 
 1 |  *  *  *  *  *  *  *  *  *  *  *  * 
    ------------------------------------
      1  2  3  4  5  6  7  8  9 10 11 12

Upvotes: 5

Related Questions