From 30f10bbdf1aca33b847bfe2910e6f10e21b92036 Mon Sep 17 00:00:00 2001 From: Ole Tange Date: Mon, 5 Oct 2020 23:48:16 +0200 Subject: [PATCH] plotpipe: Updated to support >3 columns, titles, and headers. --- plotpipe/example1.csv | 6 ++ plotpipe/example2.csv | 6 ++ plotpipe/example3.csv | 9 ++ plotpipe/example4.csv | 6 ++ plotpipe/example5.csv | 6 ++ plotpipe/plotpipe | 217 ++++++++++++++++++++++++++++++++++++++---- 6 files changed, 231 insertions(+), 19 deletions(-) create mode 100644 plotpipe/example1.csv create mode 100644 plotpipe/example2.csv create mode 100644 plotpipe/example3.csv create mode 100644 plotpipe/example4.csv create mode 100644 plotpipe/example5.csv diff --git a/plotpipe/example1.csv b/plotpipe/example1.csv new file mode 100644 index 0000000..a521745 --- /dev/null +++ b/plotpipe/example1.csv @@ -0,0 +1,6 @@ +Header1,header2,header3 +1,2,3 +2,4,5 +3,4,7 +4,2,1 +5,1,2 diff --git a/plotpipe/example2.csv b/plotpipe/example2.csv new file mode 100644 index 0000000..f909e34 --- /dev/null +++ b/plotpipe/example2.csv @@ -0,0 +1,6 @@ +Header1;header2;header3 +1;2;3 +2;4;5 +3;4;7 +4;2;1 +5;1;2 diff --git a/plotpipe/example3.csv b/plotpipe/example3.csv new file mode 100644 index 0000000..1435134 --- /dev/null +++ b/plotpipe/example3.csv @@ -0,0 +1,9 @@ +#Title line1 with " +#Title line2 with ' +Header1 header 2 header3 +1 2 3 +2 100 0 +3 4 7 +5 1 +6 1 +6 4 4 diff --git a/plotpipe/example4.csv b/plotpipe/example4.csv new file mode 100644 index 0000000..6fff51d --- /dev/null +++ b/plotpipe/example4.csv @@ -0,0 +1,6 @@ +Header1 header2 header3 +1 2 3 +2 4 5 +3 4 7 +4 2 1 +5 1 2 diff --git a/plotpipe/example5.csv b/plotpipe/example5.csv new file mode 100644 index 0000000..2400a48 --- /dev/null +++ b/plotpipe/example5.csv @@ -0,0 +1,6 @@ +Header1 header2 header3 +1 4 3 +2 4 5 +3 4 7 +4 2 1 +5 1 2 diff --git a/plotpipe/plotpipe b/plotpipe/plotpipe index 86ef3c3..1e826f5 100755 --- a/plotpipe/plotpipe +++ b/plotpipe/plotpipe @@ -4,7 +4,7 @@ =head1 NAME -plotpipe - Plot 1D-data or 2D-data from a pipe +plotpipe - Plot CSV data from a pipe =head1 SYNOPSIS @@ -16,6 +16,9 @@ I | B [-H] [-0] [-C str] [-h] [-V] B is a simple wrapper for GNUPlot to simply plot 1D and 2D-data. +The input is a CSV-file. Lines starting with '#' will be used as +titles on the plot. + =head1 OPTIONS @@ -25,14 +28,17 @@ B is a simple wrapper for GNUPlot to simply plot 1D and 2D-data. =item B<-C> I -Use I as column separator. +Use I as column separator. B will try to autoguess +the separator. If it guesses wrong, use B<--colsep>. =item B<--header> =item B<-H> -Ignore first line of input (it is a header). +Use the first line as legend for data in columns. B will try +to autoguess if the first line is a header. If it guesses wrong, use +B<--header>. =item B<--help> @@ -65,10 +71,40 @@ Plot the points (1,101) .. (100,200): paste <(seq 100) <(seq 101 200) | plotpipe - +Plot the points (1,101) .. (100,200) and (1,300) .. (100,102): + + paste <(seq 100) <(seq 101 200) <(seq 300 -2 102) | plotpipe + +=head1 EXAMPLE + +input.csv: + + #Title line 1 + #This is title line 2 + X-axis-header Values1 Values2 + 1 28 32 + 2 12 35 + 3 3.5 3.5 + + cat input.csv | plotpipe + +=head1 EXAMPLE + +input.csv: + + #Title line 1 + #This is title line 2 + X axis header,Values 1,Values 2 + 1,28,32 + 2,12,35 + 3,3.5,3.5 + + cat input.csv | plotpipe + + =head1 AUTHOR -Copyright (C) 2019 Ole Tange, +Copyright (C) 2019-2020 Ole Tange, http://ole.tange.dk and Free Software Foundation, Inc. @@ -150,25 +186,168 @@ sub help() { "",); } +sub shell_quote_scalar_default($) { + # Quote for other shells (Bourne compatibles) + # Inputs: + # $string = string to be quoted + # Returns: + # $shell_quoted = string quoted as needed by the shell + my $s = $_[0]; + if($s =~ /[^-_.+a-z0-9\/]/i) { + $s =~ s/'/'"'"'/g; # "-quote single quotes + $s = "'$s'"; # '-quote entire string + $s =~ s/^''//; # Remove unneeded '' at ends + $s =~ s/''$//; # (faster than s/^''|''$//g) + return $s; + } elsif ($s eq "") { + return "''"; + } else { + # No quoting needed + return $s; + } +} + +sub Q($) { + return shell_quote_scalar_default($_[0]); +} + +sub my_dump(@) { + # Returns: + # ascii expression of object if Data::Dump(er) is installed + # error code otherwise + my @dump_this = (@_); + eval "use Data::Dump qw(dump);"; + if ($@) { + # Data::Dump not installed + eval "use Data::Dumper;"; + if ($@) { + my $err = "Neither Data::Dump nor Data::Dumper is installed\n". + "Not dumping output\n"; + ::status($err); + return $err; + } else { + return Dumper(@dump_this); + } + } else { + # Create a dummy Data::Dump:dump as Hans Schou sometimes has + # it undefined + eval "sub Data::Dump:dump {}"; + eval "use Data::Dump qw(dump);"; + return (Data::Dump::dump(@dump_this)); + } +} + +sub max(@) { + # Returns: + # Maximum value of array + my $max; + for (@_) { + # Skip undefs + defined $_ or next; + defined $max or do { $max = $_; next; }; # Set $_ to the first non-undef + $max = ($max > $_) ? $max : $_; + } + return $max; +} + +sub find_sep(@) { + # Try common find the separators. + # Do we get the same for each line? + my @csv = grep { not /^#/ } @_; + my @sep = (",", "\t", ";", '\s+'); + my $columns; + my %col; + for my $sep (@sep) { + for my $line (@csv) { + $columns = split /$sep/, $line; + if($columns > 1) { + $col{$sep."\0".$columns}++ + } + } + } + # Find max $col{$sep,$columns} + my $most_lines = max(values %col); + + my %sepcol = (map { split /\0/, $_ } + grep { $col{$_} == $most_lines } keys %col); + my $most_cols = max(values %sepcol); + return ((grep { $sepcol{$_} == $most_cols } keys %sepcol)[0]); +} + + Getopt::Long::Configure("bundling","require_order"); my $retval = GetOptions(options_hash()); $Global::progname = "plotpipe"; -$Global::version = 20200810; +$Global::version = 20201005; if($opt::version) { version(); exit 0; } if($opt::help) { help(); exit 0; } if($opt::null) { $/ = "\0"; } -$opt::colsep ||= '\s+'; -if($opt::header) { <> } -$line1 = <>; -@col = split /$opt::colsep/, $line1; -if($#col == 1) { - # 2 col - open GNUPLOT,"|-", q(gnuplot -p -e 'plot "/dev/stdin"') or die; -} elsif($#col == 2) { - # 3 col (3rd = color) - open GNUPLOT,"|-", q(gnuplot -p -e 'plot "/dev/stdin" using 1:2:3 with points palette') or die; -} else { - die "$#col,@col,$line1"; + +# Read csv +my @csv = <>; + +# Title = lines starting with # +my @title = map { s/^#//; s/"/''/g; $_ } map { "$_" } grep { /^#/ } @csv; +if(@title) { chomp($title[$#title]); } +@csv = grep { not /^#/ } @csv; + +# Autoguess separator +if(not defined $opt::colsep) { + $opt::colsep = find_sep(@csv); } -print GNUPLOT $line1, <>; + +# Autoguess header +my @header; +if(not defined $opt::header) { + # Autodetect header + # if line 1 contains a-z => header + @header = split /$opt::colsep/, $csv[0]; + for(@header) { + if(/[a-z]/i) { $opt::header = 1; last; } + } +} +if($opt::header) { + @header = split /$opt::colsep/, $csv[0]; + chomp(@header); + shift @csv; +} else { + @header = (); +} + +# Save data to tmpfile that will be read by Gnuplot +use File::Temp qw(tempfile); +$ENV{'TMPDIR'} ||= "/tmp"; +my($filehandle,$filename) = + tempfile(DIR=>$ENV{'TMPDIR'}, TEMPLATE => 'plotXXXXX'); +for(@csv) { + chomp; +# print((join "\t", split /$opt::colsep/, $_),"\n"); + print $filehandle ((join "\001", split /$opt::colsep/, $_),"\n"); +} + +# Generate the variant part of Gnuplot script +my $ncols = split /$opt::colsep/, $csv[0]; +for(my $col = 2; $col <= $ncols; $col++) { + my $legend; + if($opt::header) { + $legend = qq( title "$header[$col-1]"); + } + push @plotscript, qq("$filename" using 1:$col with lines $legend,); +} + +# Make full Gnuplot script +my $plotscript=<<_EOS ; +set title "@title"; +set xtics rotate; +set autoscale; +set xlabel "$header[0]"; +set grid; +set key right center; +set datafile separator "\001"; +plot @plotscript +_EOS + +open GNUPLOT,"|-", "gnuplot -p -e ".Q($plotscript) or die; +# print "gnuplot -p -e ".($plotscript); close GNUPLOT; +unlink $filename;