bash - Shell: How can I make a text-mode bar chart from parsed data (numbers)? -


i developing linux's bash shell script extracts data text file leaving numbers. these example parsed data:

3 4 4 5 6 7 8 8 9 11 

i create simple text-mode bar chart one, corresponding these values:

bar chart

details:

  • i need graphic chart vertical.
  • the first numbers should appear on left, latest on right.
  • a n (parsed number) characters high column appropriate me. first bar on left in example should 3 characters high, second 4, third 4, fourth 5, , on.

more precisely, example, (using character) like:

         █          █         ██         ███       ████      █████     ██████    ███████  █████████ ██████████ ██████████ ██████████ 

note 3 characters high first (left) column , 11 characters high last (right) column.
same example $ characters, make more readable:

         $          $         $$        $$$       $$$$      $$$$$     $$$$$$    $$$$$$$  $$$$$$$$$ $$$$$$$$$$ $$$$$$$$$$ $$$$$$$$$$ 

the closest know of method progress bar until now, have used in scripts:

printf "\033[48;5;21m"   # blue background color in $(seq 1 $n); printf " "; done   # create bar using blue spaces 

this is: filling each line printing bar n spaces. bar horizontal, not appropriate in case.

i request core loop example ideas create bar chart.

under suggestion of user boardrider, solutions based on unix-like tools accepted. solutions based on scripting languages (like perl or python) linux shell accepted too, long use implemented on many devices.

here first , naive attempt... it's not efficient solution data parsed many times, may helps. in way it's first loop idea suggested @walter_a.

#!/bin/sh # ## building vertical bar graph of data file ## https://stackoverflow.com/q/30929012 ## ## 1. required. data file 1 value per line , nothing else! ## /!\ provide (relative or absolute) file path, not file content : ${1:?" please provide 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 dollar sign) ## /!\ blank color use "\033[48;5;21m \033[0m" or you'll mess... c_dot="$2" : ${c_dot:='$'} ## 3. optional. separator characher (default dash sign) ## /!\ space not tested there characters... c_sep="$3" : ${c_sep:='-'} # init... len_w=$(wc -l "$1" | cut -d ' ' -f 1 ) l_sep='' while test "$len_w" -gt 0         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         printf '| '         a_val in $(cat "$1")                         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         printf '| '         a_val in $(cat "$1")                         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 rework on script. correct separators handling (just try ' ' or '|' third argument see), may less readable use arguments number instead of additional variable.

edit 2: deals negative integers... , may change ground (5th parameter)

#!/bin/sh # ## building vertical bar graph of data file ## https://stackoverflow.com/q/30929012 ## ## 1. required. data file 1 value per line , nothing else! ## /!\ provide (relative or absolute) file path, not file content : ${1:?" please provide 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 single character ## /!\ blank color use "\033[48;5;21m \033[0m" or you'll mess... ## 2. optional. ploting character (default dollar sign) ## 3. optional. horizontal border characher (default dash sign) ## 4. optional. columns separator characher (default pipe sign) ## (!) however, when no arg provided graph framed in table ## 5. optional. ground level integer value (default 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" ]         _long=$((_long*2-3)) fi _line='' while [ "$_long" -gt 0 ]         _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 2 flavors. first produces output previous one.

# begin echo "${4-.}${3:--}${_line}${3:--}${4-.}" # upper/positive if [ $_from -gt ${5:-0} ]         while [ $_from -gt ${5:-0} ]                         printf "${4:-| }"                 _cint in $(cat "$1" )                                         if [ $_cint -ge $_from ]                                                         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 ]         printf "${4:-| }"         _cint in $(cat "$1" )                         printf "%1s$4" $(echo "$_cint" | cut -c "$_long" )         done         echo " ${4:-|}"         _long=$((_long-1)) done unset _long # lower/negative if [ $_stop -lt ${5:-0} ]         _from=${5:-0}         echo "${4-|}${3:--}${_line}${3:--}${4-|}"         while [ $_from -gt $_stop ]                         printf "${4:-| }"                 _cint in $(cat "$1" )                                         if [ $_cint -lt $_from ]                                                         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 2 checks in order avoid loop when values positive (above ground) or negative (bellow ground) ! well, maybe should put "center/legend" part @ end? looks bit ugly when there're both positive , negative values first, , when negative integers looks strange labels don't read in opposite , have unpleasant minus sign.
notice wc -l not posix... ...so loop may needed.

here variant legend number in right size instead of bottom. doing so, save loop don't output (i prefer values on left rather right side, it's taste isn't ?)

# begin printf "${4-.}${3:--}${_line}${3:--}${4-.}" # upper/positive if [ $_from -gt ${5:-0} ]         echo ""         while [ $_from -gt ${5:-0} ]                         _ctxt=''                 printf "${4:-| }"                 _cint in $(cat "$1" )                                         if [ $_cint -ge $_from ]                                                         printf "${2:-$}$4"                         else                                 printf " $4"                         fi                         if [ $_cint -eq $_from ]                                                         _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} ]         echo "${4-|}${3:--}${_line}${3:--}${4-|}_ ${1}" fi # lower/negative if [ $_stop -lt ${5:-0} ]         _from=${5:-0}         while [ $_from -gt $_stop ]                         _ctxt=''                 printf "${4:-| }"                 _cint in $(cat "$1" )                                         if [ $_cint -lt $_from ]                                                         printf "${2:-$}$4"                         else                                 printf " $4"                         fi                         if [ $_cint -eq $((_from-1)) ]                                                         _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} ]         echo "" else         echo "_ ${1}" fi unset _stop exit 0 

edit 3: there're checks ground line isn't added when there's positive or negative numbers.

finally, think final solution mix of both, value displayed on side , position of value in center. it's more close gnu plot's output.

# init... _long=$(wc -l < "$1" ) if [ -n "$4" ]         _long=$((_long*2-3)) fi _line='' while [ $_long -gt 0 ]         _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} ]         while [ $_from -gt ${5:-0} ]                         _ctxt=''                 printf "${4:-| }"                 _cint in $(cat "$1" )                                         if [ $_cint -ge $_from ]                                                         printf "${2:-$}$4"                         else                                 printf " $4"                         fi                         if [ $_cint -eq $_from ]                                                         _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 ]         printf "%1s$4" $( expr "$_rank" : '.*\(.\)$' )         _rank=$((_rank+1)) done echo " ${4:-|}_ ${1}" # lower/negative if [ $_stop -lt ${5:-0} ]         echo "${4-|}${3:--}${_line}${3:--}${4-|}"         while [ $_from -gt $_stop ]                         _ctxt=''                 printf "${4:-| }"                 _cint in $(cat "$1" )                                         if [ $_cint -lt $_from ]                                                         printf "${2:-$}${4}"                         else                                 printf " $4"                         fi                         if [ $_cint -eq $((_from-1)) ]                                                         _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 ability scale...


Comments

Popular posts from this blog

PHP DOM loadHTML() method unusual warning -

python - How to create jsonb index using GIN on SQLAlchemy? -

c# - TransactionScope not rolling back although no complete() is called -