A Simplified Gnuplot Interface
I really like Gnuplot for plotting data, but the user interface has
always felt rather clunky to me. I love that it's a command line
program, but it doesn't play well with stdin
or pipes, which is odd.
The main problem is that Gnuplot only expects the plot parameters in
stdin
, and the data to plot must come from files specified within
those plot parameters. The parameters and data are two different
streams and there isn't a clean way to send data via stdin
. The
result is that you often write a shell script to wrap calls to
Gnuplot, usually with a "here-doc" to set the parameters and the
filenames using string interpolation in the shell. Examples of such
scripts can be found on StackExchange.
After writing enough of those wrapper scripts, I came up with this
universal wrapper script to simplify the interface and make the common
case of plotting from stdin
(or one or more .csv
files) very easy.
The main idea is that the plot parameters are now passed as command
line arguments and data comes from stdin
or files named on the
command line with -f
. The command line arguments for this script
form a stateful mini-language. Each parameter starts with a useful
default value and the script keeps track of each parameter's value as
it parses the command line. Each time the -p
flag is passed, the
current plot parameters are used to add a new plot line to the graph.
In other words, each -p
plot can use different parameters and/or
files, as needed.
The most basic invocation simply pipes data to gp_wrap.sh
:
seq 10 20 | ./gp_wrap.sh
This will output plot.png
with a plot labeled stdin
. The script
acts as if there is an implicit -p
here.
The next most basic invocation uses only -p
to plot columns from a
file:
./gp_wrap.sh -f file.csv -p -p -p
The default x-axis plots against the row numbers in the data file and
the y-axis defaults to column 1. The -f
sets the file that
following plots will use. Each -p
increments the current column in
the plot parameter state, so the above invocation will plot columns 1,
2, and 3 from file.csv
.
If the plot data starts with a header row, then those values can be
used to automatically label each plot line with the -h
option, like
so:
./gp_wrap.sh -f file.csv -h -p -p -p
The entire graph's title can be set with -T title
, and the x and y
axes can be labelled with -a x-axis-label
and -o y-axis-label
,
respectively.
The usage notes in the script should make the rest of the options clear.
This isn't a complete replacement for all the features in Gnuplot, of course, but it does make it easier to make the types of plots I need most often.
#!/bin/bash # gp_wrap.sh simplifies the Gnuplot interface and makes plotting # data from CSV files easier. # Copyright (C) 2021 Remington Furman # 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 <https://www.gnu.org/licenses/>. # The command line arguments form a mini language that modify the # current state of the plot parameters which are used when the -p # (plot) argument is given to add a line to the plot. By default, the # plot parameters will plot the first column of the CSV file with no # labels or title, and subsequent plots will plot the next column (-y # will increment with each -p). # Examples: # ./gp_wrap.sh -f file.csv -p -p -p # Plot columns 1, 2, 3 vs row number. # ./gp_wrap.sh -f file.csv -h -p -p -p # Same, but use headers from first row. # ./gp_wrap.sh -h -p -p -p < file.csv # Same, but read from stdin. # ./gp_wrap.sh -f file.csv -x 2 -y 3 -p # Plot col3 vs col2. usage () { cat <<EOF Usage: -f file Plot data from File (default: stdin) -w file Write output to file -F string Use 'string' as Field separator (default: ",") -x n Use column 'n' for X coordinates (default: use row number) -y n Use column 'n' for Y coordinates (default: 1) -r range Use Range for x and y axes (default: none) -a string Label x axis (Abscissa) with 'string' (default: none) -o string Label y axis (Ordinate) with 'string' (default: none) -t string Label next plots with 'string' (default: filename) -T string Set Title to 'string' (default: none) -s string Style next plots with 'string' (default: lines) -k string Place plot key at 'string' (default: top left) -i Increment current column after each plot (default) -I Don't Increment current column after each plot -m Format Math in plot titles with TeX (default: off) -p Make a Plot with the current parameters -l List current parameters -h Use first row (Header) as plot labels -c string Add Custom Gnuplot commands to preamble (default: none) -u Display Usage -d Debug (print Gnuplot input) EOF } # Set default parameters STDIN=true # Read from stdin. FILE=$(mktemp -u) OUTPUT="./plot.png" FIELD_SEP="," X_COL= Y_COL=1 RANGE= X_LABEL= Y_LABEL= TITLE= TITLE_MATH="set key noenhanced" STYLE="with lines" KEY_LOC="set key left top" HEADER= INCREMENT=true CUSTOM= PLOTS= DEBUG= # Read command line arguments optstring="f:w:F:x:y:r:a:o:t:T:s:k:c:iImplhud" while getopts ${optstring} arg; do case ${arg} in f) FILE="${OPTARG}" STDIN=false ;; w) OUTPUT="${OPTARG}" ;; F) FIELD_SEP="${OPTARG}" ;; x) # Note the trailing : which is necessary for specifying # the x and y coordinates. X_COL="${OPTARG}:" ;; y) Y_COL="${OPTARG}" ;; r) RANGE="${OPTARG}" ;; a) X_LABEL="${OPTARG}" ;; o) Y_LABEL="${OPTARG}" ;; t) PLOT_TITLE="title '${OPTARG}'" ;; T) TITLE="${OPTARG}" ;; s) STYLE="${OPTARG}" ;; k) KEY_LOC="set key ${OPTARG}" ;; i) INCREMENT=true ;; I) INCREMENT=false ;; m) TITLE_MATH="" ;; p) # Add a new plot command with current parameters. if [ "${STDIN}" = true -a \ -z "${HEADER}" -a \ -z "${PLOT_TITLE}" ] ; then PLOT_TITLE="title 'stdin'" fi PLOTS="${PLOTS:+${PLOTS}, }" # Append comma if plots exist. PLOTS+="'${FILE}' using ${X_COL}${Y_COL} ${STYLE} ${PLOT_TITLE}" if "$INCREMENT" = true ; then # Move to next column. Y_COL=$((Y_COL+1)) fi ;; l) echo FILE="$FILE" echo X_COL="$X_COL" echo Y_COL="$Y_COL" echo X_LABEL="$X_LABEL" echo Y_LABEL="$Y_LABEL" echo TITLE="$TITLE" echo HEADER="$HEADER" echo INCREMENT="$INCREMENT" echo CUSTOM="$CUSTOM" echo PLOTS="$PLOTS" ;; h) HEADER="set key autotitle columnheader" ;; c) CUSTOM+=$'\n' CUSTOM+="${OPTARG}" ;; u) usage exit ;; d) DEBUG=true ;; esac done if [ "${STDIN}" = true ] ; then # Save stdin to a file so that Gnuplot can read it for multiple # plots. cat /dev/stdin > "${FILE}" trap "rm ${FILE}" EXIT # Make sure the temp file is deleted # when this script exits. PLOT_TITLE="${PLOT_TITLE:-title 'stdin'}" fi if [ -z "${PLOTS}" ] ; then # Make at least one plot if none are requested. PLOTS="'${FILE}' using ${X_COL}${Y_COL} ${STYLE} ${PLOT_TITLE}" fi if [ "${DEBUG}" = true ] ; then COMMAND=cat else COMMAND=gnuplot fi # Generate Gnuplot command and call Gnuplot "${COMMAND}" <<-EOF set terminal pngcairo ${CUSTOM} set datafile separator ',' set output '${OUTPUT}' set title '${TITLE}' ${TITLE_MATH} ${KEY_LOC} ${HEADER} set xlabel '${X_LABEL}' set ylabel '${Y_LABEL}' plot ${RANGE} ${PLOTS} EOF if [ "${DEBUG}" = true ] ; then cat "${FILE}" fi