tangetools/plotpipe/plotpipe
2020-11-06 09:48:29 +01:00

388 lines
8.3 KiB
Perl
Executable file

#!/usr/bin/perl
=pod
=head1 NAME
plotpipe - Plot CSV data from a pipe
=head1 SYNOPSIS
I<datagenerator> | B<plotpipe> [-n] [-H] [-0] [-C str] [-h] [-V]
=head1 DESCRIPTION
B<plotpipe> is a simple wrapper for Gnuplot to simply plot data.
The input is a CSV-file. Lines starting with '#' will be used as
titles on the plot.
=head1 OPTIONS
=over 4
=item B<--colsep> I<string>
=item B<-C> I<string>
Use I<string> as column separator. B<plotpipe> will try to autoguess
the separator. If it guesses wrong, use B<--colsep>.
=item B<--header>
=item B<-H>
Use the first line as legend for data in columns. B<plotpipe> will try
to autoguess if the first line is a header. If it guesses wrong, use
B<--header>.
=item B<--help>
=item B<-h>
Show help.
=item B<--nox>
=item B<-n>
No x-value. In a multi-column input the first value will normally be
used as x-value. B<--nox> will use line number as x-value.
=item B<--null>
=item B<-0>
Use \0 (NUL) instead of newline (\n) as record separator.
=item B<--version>
=item B<-V>
Show version
=back
=head1 EXAMPLE
Plot the points (1,100) .. (100,1):
paste <(seq 100 -1 1) | plotpipe
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-2020 Ole Tange,
http://ole.tange.dk and Free Software Foundation, Inc.
=head1 LICENSE
Copyright (C) 2012 Free Software Foundation, Inc.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
at your option any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
=head1 DEPENDENCIES
B<pipeplot> uses B<gnuplot>.
=head1 SEE ALSO
B<gnuplot>
=cut
use Getopt::Long;
sub options_hash() {
# Returns:
# %hash = the GetOptions config
return
("debug|D=s" => \$opt::D,
"version|V" => \$opt::version,
"colsep|col-sep|C=s" => \$opt::colsep,
"help|h" => \$opt::help,
"null|0" => \$opt::null,
"nox|n" => \$opt::nox,
"header|H" => \$opt::header,
);
}
sub version() {
# Returns: N/A
print join
("\n",
"GNU $Global::progname $Global::version",
"Copyright (C) 2020 Ole Tange, http://ole.tange.dk and Free Software",
"Foundation, Inc.",
"License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>",
"This is free software: you are free to change and redistribute it.",
"GNU $Global::progname comes with no warranty.",
"",
"Web site: https://gitlab.com/ole.tange/tangetools/-/tree/master/${Global::progname}\n",
"",
);
}
sub help() {
# Returns: N/A
print join
("\n",
"Usage:",
"",
"... | plotpipe [-H] [-0] [-C str]",
"",
"-H Ignore first line (header)",
'-0 Records separated by \0 instead of \n',
'-C str Columns separator',
'-V Show version',
"",
"See 'man $Global::progname' for details",
"",);
}
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 = 20201005;
if($opt::version) { version(); exit 0; }
if($opt::help) { help(); exit 0; }
if($opt::null) { $/ = "\0"; }
# 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);
}
if($opt::colsep eq "") {
$opt::colsep = "\001soMe valUE tHat dOes nOT eXisT\002";
}
# 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');
my $ncols = split /$opt::colsep/, $csv[0];
if($ncols >= 2 and not $opt::nox) {
# Column 1 = x-axis
for(@csv) {
chomp;
print $filehandle ((join "\001", split /$opt::colsep/, $_),"\n");
}
} else {
# All data = y-axis, invent x-axis
my $x = 1;
for(@csv) {
chomp;
print $filehandle ((join "\001", $x++, split /$opt::colsep/, $_),"\n");
}
# Prepend dummy header for x-axis
unshift(@header,"");
$ncols += 1;
}
# Generate the variant part of Gnuplot script
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
if(fork) {
open GNUPLOT,"|-", "gnuplot -p -e ".Q($plotscript) or die;
#print "gnuplot -p -e ".($plotscript);
close GNUPLOT;
unlink $filename;
} else {
# If script dies unexpectedly, remove the file after 5 sec
sleep(5);
unlink $filename;
}