I am developing a Linux's Bash shell script that extracts data from a text file leaving only the numbers. These are my example parsed data:
3
4
4
5
6
7
8
8
9
11
I would like to create a simple text-mode bar chart like this one, but corresponding to these values:
Details:
n
(parsed number) characters high column is appropriate for me. So the first bar on the left in my example should be 3 characters high, the second 4, the third 4, the fourth 5, and so on.More precisely, for this example, something (using the █
character) like:
█
█
██
███
████
█████
██████
███████
█████████
██████████
██████████
██████████
Note the 3 characters high for the first (left) column and 11 characters high for the last (right) column.
Same example with $
characters, to make it more readable:
$
$
$$
$$$
$$$$
$$$$$
$$$$$$
$$$$$$$
$$$$$$$$$
$$$$$$$$$$
$$$$$$$$$$
$$$$$$$$$$
The closest I know of is my method for progress bar until now, that I have used in another scripts:
printf "\033[48;5;21m" # Blue background color
for i in $(seq 1 $n); do printf " "; done # Create bar using blue spaces
This is: filling each line printing a bar with n
spaces. But this bar is horizontal, so it is not appropriate in this case.
I request some core loop example ideas to create this bar chart.
Under the suggestion of user Boardrider, solutions based on any unix-like tools are accepted. Solutions based on scripting languages (like Perl or Python) for the Linux shell are accepted too, as long as they use to be implemented on many devices.
Here is a first and naive attempt... It's not a very efficient solution as data are parsed many times, but may it helps. In a way it's the first loop idea suggested by @Walter_A.
#!/bin/sh
#
## building a vertical bar graph of data file
## https://stackoverflow.com/q/30929012
##
## 1. required. Data file with one value per line and nothing else!
## /!\ provide the (relative or absolute) file path, not file content
: ${1:?" Please provide a file name"}
test -e "$1" || { echo "Sorry, can't find $1" 1>&2 ; exit 2 ; }
test -r "$1" || { echo "Sorry, can't access $1" 1>&2 ; exit 2 ; }
test -f "$1" || { echo "Sorry, bad format file $1" 1>&2 ; exit 2 ; }
test $( grep -cv '^[0-9][0-9]*$' "$1" 2>/dev/null ) -ne 0 || { echo "Sorry, bad data in $1" 1>&2 ; exit 3 ; }
# setting characters
## 2. optional. Ploting character (default is Dollar sign)
## /!\ for blank color use "\033[48;5;21m \033[0m" or you'll mess...
c_dot="$2"
: ${c_dot:='$'}
## 3. optional. Separator characher (default is Dash sign)
## /!\ as Space is not tested there will be extra characters...
c_sep="$3"
: ${c_sep:='-'}
# init...
len_w=$(wc -l "$1" | cut -d ' ' -f 1 )
l_sep=''
while test "$len_w" -gt 0
do
l_sep="${l_sep}${c_sep}";
len_w=$(($len_w-1))
done
unset len_w
# part1: chart
echo ".${c_sep}${l_sep}${c_sep}."
len_h=$(sort -n "$1" | tail -n 1)
nbr_d=${#len_h}
while test "$len_h" -gt 0
do
printf '| '
for a_val in $(cat "$1")
do
test "$a_val" -ge "$len_h" && printf "$c_dot" || printf ' '
done
echo ' |'
len_h=$(($len_h-1))
done
unset len_h
# part2: legend
echo "|${c_sep}${l_sep}${c_sep}|"
while test "$nbr_d" -gt 0
do
printf '| '
for a_val in $(cat "$1")
do
printf "%1s" $(echo "$a_val" | cut -c "$nbr_d")
done
echo ' |'
nbr_d=$(($nbr_d-1))
done
unset nbr_d
# end
echo "'${c_sep}${l_sep}${c_sep}'"
unset c_sep
exit 0
EDIT 1: Here is a rework on the script. It correct separators handling (just try with ' ' or '|' as third argument to see), but it may look less readable as I use arguments number instead of a additional variable.
EDIT 2: It also deals with negative integers... and you may change the ground (5th parameter)
#!/bin/sh
#
## building a vertical bar graph of data file
## https://stackoverflow.com/q/30929012
##
## 1. required. Data file with one value per line and nothing else!
## /!\ provide the (relative or absolute) file path, not file content
: ${1:?" Please provide a file name"}
[ -e "$1" ] || { echo "Sorry, can't find $1" 1>&2 ; exit 2 ; }
[ -r "$1" ] || { echo "Sorry, can't access $1" 1>&2 ; exit 2 ; }
[ -f "$1" ] || { echo "Sorry, bad format file $1" 1>&2 ; exit 2 ; }
[ $( grep -cv '^[-0-9][0-9]*$' "$1" 2>/dev/null ) -ne 0 ] || { echo "Sorry, bad data in $1" 1>&2 ; exit 3 ; }
## /!\ following parameters should result to a single character
## /!\ for blank color use "\033[48;5;21m \033[0m" or you'll mess...
## 2. optional. Ploting character (default is Dollar sign)
## 3. optional. Horizontal border characher (default is Dash sign)
## 4. optional. Columns separator characher (default is Pipe sign)
## (!) however, when no arg provided the graph is just framed in a table
## 5. optional. Ground level integer value (default is Zero)
test "${5:-0}" -eq "${5:-0}" 2>/dev/null || { echo "oops, bad parameter $5" 1>&2 ; exit 3 ; }
# init...
_long=$(wc -l < "$1" ) # width : number of data/lines in file
if [ -n "$4" ]
then
_long=$((_long*2-3))
fi
_line=''
while [ "$_long" -gt 0 ]
do
_line="${_line}${3:--}"
_long=$((_long-1))
done
unset _long
_from=$(sort -n "$1" | tail -n 1 ) # max int
_stop=$(sort -n "$1" | head -n 1 ) # min int
This rework comes in two flavors. The first produces an output like the previous one.
# begin
echo "${4-.}${3:--}${_line}${3:--}${4-.}"
# upper/positive
if [ $_from -gt ${5:-0} ]
then
while [ $_from -gt ${5:-0} ]
do
printf "${4:-| }"
for _cint in $(cat "$1" )
do
if [ $_cint -ge $_from ]
then
printf "${2:-$}$4"
else
printf " $4"
fi
done
echo " ${4:-|}"
_from=$((_from-1))
done
echo "${4-|}${3:--}${_line}${3:--}${4-|}"
fi
unset _from
# center/legend
_long=$(wc -L < "$1" ) # height : number of chararcters in longuest line...
while [ $_long -ge 0 ]
do
printf "${4:-| }"
for _cint in $(cat "$1" )
do
printf "%1s$4" $(echo "$_cint" | cut -c "$_long" )
done
echo " ${4:-|}"
_long=$((_long-1))
done
unset _long
# lower/negative
if [ $_stop -lt ${5:-0} ]
then
_from=${5:-0}
echo "${4-|}${3:--}${_line}${3:--}${4-|}"
while [ $_from -gt $_stop ]
do
printf "${4:-| }"
for _cint in $(cat "$1" )
do
if [ $_cint -lt $_from ]
then
printf "${2:-$}$4"
else
printf " $4"
fi
done
echo " ${4:-|}"
_from=$((_from-1))
done
fi
unset _stop
# end
echo "${4-'}${3:--}${_line}${3:--}${4-'}"
exit 0
Notice : There're two checks in order to avoid extra loop when all values are positive (above the ground) or negative (bellow the ground) !
Well, maybe I should always put the "center/legend" part at the end? It looks a bit ugly when there're both positive and negative values first, and when only negative integers it looks strange that labels don't read in the opposite and have unpleasant minus sign.
Also notice that wc -L
is not POSIX... ...so another loop may be needed.
Here is another variant with legend number in the right size instead of the bottom. Doing so, I save an extra loop but I don't really like the output (I prefer values on the left rather than the right side, but it's a taste isn't it ?)
# begin
printf "${4-.}${3:--}${_line}${3:--}${4-.}"
# upper/positive
if [ $_from -gt ${5:-0} ]
then
echo ""
while [ $_from -gt ${5:-0} ]
do
_ctxt=''
printf "${4:-| }"
for _cint in $(cat "$1" )
do
if [ $_cint -ge $_from ]
then
printf "${2:-$}$4"
else
printf " $4"
fi
if [ $_cint -eq $_from ]
then
_ctxt="_ $_from"
fi
done
echo " ${4:-}${_ctxt}"
_from=$((_from-1))
done
_from=$((_from+1))
else
echo "_ ${1}"
fi
# center/ground
if [ $_stop -lt ${5:-0} ] && [ $_from -gt ${5:-0} ]
then
echo "${4-|}${3:--}${_line}${3:--}${4-|}_ ${1}"
fi
# lower/negative
if [ $_stop -lt ${5:-0} ]
then
_from=${5:-0}
while [ $_from -gt $_stop ]
do
_ctxt=''
printf "${4:-| }"
for _cint in $(cat "$1" )
do
if [ $_cint -lt $_from ]
then
printf "${2:-$}$4"
else
printf " $4"
fi
if [ $_cint -eq $((_from-1)) ]
then
_ctxt="_ $_cint"
fi
done
echo " ${4:-|}${_ctxt}"
_from=$((_from-1))
done
fi
# end
unset _from
printf "${4-'}${3:--}${_line}${3:--}${4-'}"
if [ $_stop -lt ${5:-0} ]
then
echo ""
else
echo "_ ${1}"
fi
unset _stop
exit 0
EDIT 3: There're some extra checks so an extra ground line isn't added when there's only positive or negative numbers.
Finally, i think the final solution is a mix of both, where value are displayed on the side and the position of value in the center. Then it's more close to GNU Plot's output.
# init...
_long=$(wc -l < "$1" )
if [ -n "$4" ]
then
_long=$((_long*2-3))
fi
_line=''
while [ $_long -gt 0 ]
do
_line="${_line}${3:--}"
_long=$((_long-1))
done
unset _long
_from=$(sort -n "$1" | tail -n 1 ) # max int
_stop=$(sort -n "$1" | head -n 1 ) # min int
# begin
echo "${4-.}${3:--}${_line}${3:--}${4-.}"
# upper/positive
if [ $_from -gt ${5:-0} ]
then
while [ $_from -gt ${5:-0} ]
do
_ctxt=''
printf "${4:-| }"
for _cint in $(cat "$1" )
do
if [ $_cint -ge $_from ]
then
printf "${2:-$}$4"
else
printf " $4"
fi
if [ $_cint -eq $_from ]
then
_ctxt="_ $_from"
fi
done
echo " ${4:-|}$_ctxt"
_from=$((_from-1))
done
echo "${4-|}${3:--}${_line}${3:--}${4-|}"
fi
# center/ground
_size=$(wc -l < "$1" ) # width : number of data/lines in file
##_long=${#_size} # height : number of chararcters in long
#_long=1
##while [ $_long -gt 0 ]
#while [ $_long -le ${#_size} ]
#do
#_rank=1
#printf "${4:-| }"
#while [ $_rank -le $_size ]
#do
#printf "%1s$4" $( printf "%0${#_size}d" $_rank | cut -c $_long )
#_rank=$((_rank+1))
#done
#printf " ${4:-|}"
##_long=$((_long-1))
#_long=$((_long+1))
##if [ $_long -eq 0 ]
#if [ $_long -eq ${#_size} ]
#then
#printf "_ ${1}"
#fi
#echo ''
#done
_rank=1
printf "${4:-| }"
while [ $_rank -le $_size ]
do
printf "%1s$4" $( expr "$_rank" : '.*\(.\)$' )
_rank=$((_rank+1))
done
echo " ${4:-|}_ ${1}"
# lower/negative
if [ $_stop -lt ${5:-0} ]
then
echo "${4-|}${3:--}${_line}${3:--}${4-|}"
while [ $_from -gt $_stop ]
do
_ctxt=''
printf "${4:-| }"
for _cint in $(cat "$1" )
do
if [ $_cint -lt $_from ]
then
printf "${2:-$}${4}"
else
printf " $4"
fi
if [ $_cint -eq $((_from-1)) ]
then
_ctxt="_ $_cint"
fi
done
echo " ${4:-|}$_ctxt"
_from=$((_from-1))
done
fi
unset _from
unset _stop
# end
echo "${4-'}${3:--}${_line}${3:--}${4-'}"
exit 0
A last improvement would be the ability to scale...
Methinks it can be a bit simpler...
#!/bin/bash
# Asume the data is in a textfile "/tmp/testdata.txt". One value per line.
MAXVALUE=$( sort -nr /tmp/testdata.txt | head -n1 ) # get the highest value
CHARTHEIGHT=$MAXVALUE # use it for height of chart
# (can be other value)
CHARTLINE=$CHARTHEIGHT # variable used to parse the lines
while [ $CHARTLINE -gt 0 ]; do # start the first line
(( CHARTLINE-- ))
REDUCTION=$(( $MAXVALUE*$CHARTLINE/$CHARTHEIGHT )) # subtract this from the VALUE
while read VALUE; do
VALUE=$(( $VALUE-$REDUCTION ))
if [ $VALUE -le 0 ]; then # check new VALUE
echo -n " "
else
echo -n "▓▓▓ "
fi
done < /tmp/testdata.txt
echo
done
echo
exit
This script will parse the data every line, reduces the read value and checks if there's something left. If so, display a block; if not, display a space. Repeat every line with different REDUCTION value. The script can be expanded to included a legend, colours, half/quarter blocks, etc...
Apart from SORT and HEAD it's all in BASH commands
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With