From eb8b4c1e251560c686fcaa3a855722bee2581751 Mon Sep 17 00:00:00 2001 From: Andrea Morgan-Brist Date: Thu, 9 Feb 2017 12:09:31 -0600 Subject: [PATCH] Feature: Thresholds and Colors Inspiration and Attribution: Shout out to Alexandre Pretto Nunes (https://github.com/apretto)! Alexandre's PR (https://github.com/holman/spark/pull/88/commits/206a69d8d076cfaa4114eb6f9fc30e90445a3442) with the "fire" palette was the inspiration behind these features. Much thanks! Additional shout out to user79743 for the ansi color table code from: http://unix.stackexchange.com/questions/269077/tput-setaf-color-table-how-to-determine-color-codes Thank you kindly! Description: I use sparkline every day. Usually to provide quick and dirty graphs for spot checking peaks/valleys in data returned from various tools' apis (splunk, tealeaf, etc). Many of these tools can also return thresholds with the data and I often find that min/max must be checked in order to understand the scale of the peaks vs. valleys. Additionally, I like ansi colors in general - bash is life. This commit provides a middle class (eat the rich) feature set to compel spark's output to be more pleasant on the eyes and potentially speed up analysis of data from the shell. Features: Graph Name/Title, Threshold, Threshold Color, Threshold Inversion, Ansi Color Table, and Color Palettes Feature Details: Graph Name/Title (-n ""): Optional graph header with customizable title and min/max value. -- usage example: ./spark -n "The Graph Title" Color Palette (-p - Customizable color palette: Comma-separated 8 element list ansi color codes (0-255) -- usage example: ./spark -p 212,213,214,215,216,217,218,219 Threshold Value (-t ): All values greater than or equal (or less than or equal with the inversion switch - see below) to the threshold value will result in ticks of the specified threshold color (see below). Threshold Color (-c ): Apply color to all ticks for values exceeding threshold. This will override any colors specified in the color palette. As with the color palette option, the threshold color option supports ansi integer color codes. -- usage example: ./spark -c red -t 5 -- usage example: ./spark -c 1 -t 5 Threshold Inversion (-i): Invert the threshold logic from "greater than or equal" to "less than or equal". -- usage example: ./spark -c red -t 5 -i Ansi Color Table (-a): Output a table of ansi colors for ease of color lookups. -- usage example: ./spark -a --- spark | 172 ++++++++++++++++++++++++++++++++++++++++++++------ spark-test.sh | 138 ++++++++++++++++++++++++++++++++++------ 2 files changed, 273 insertions(+), 37 deletions(-) diff --git a/spark b/spark index 53a38be..3d93e7b 100755 --- a/spark +++ b/spark @@ -13,6 +13,8 @@ # spark takes a comma-separated or space-separated list of data and then prints # a sparkline out of it. # +# See help for color options (inspired by Alexandre Pretto Nunes - https://github.com/apretto) +# # Examples: # # spark 1 5 22 13 53 @@ -27,8 +29,7 @@ # Generates sparklines. # # $1 - The data we'd like to graph. -_echo() -{ +_echo(){ if [ "X$1" = "X-n" ]; then shift printf "%s" "$*" @@ -37,14 +38,69 @@ _echo() fi } -spark() -{ - local n numbers= +_colorize(){ + local ticks="${1}" + local colors=(${2//,/ }) + local output="" + for i in "${!colors[@]}";do + output+="$(tput setaf ${colors[$i]})${ticks:$i:1}" + done + # reset the formatting/colors at end of the graph + output+="$(tput sgr0)" + echo "${output}" +} - # find min/max values +# The following two functions are shamelessly lifted (with minimal changes) from: +# http://unix.stackexchange.com/questions/269077/tput-setaf-color-table-how-to-determine-color-codes +# Thank you user79743! +_color_table_out(){ + for c; do + printf "$(tput setab ${c}) %03d" "${c}" + done + echo "$(tput sgr0)" +} + +_color_table(){ + local IFS=$' \t\n' + echo "" + _color_table_out {0..15} + for ((i=0;i<6;i++)); do + _color_table_out $(seq $((i*36+16)) $((i*36+51))) + done + _color_table_out {232..255} + echo "" +} + +spark(){ + # Defaults + local n data numbers= local min=0xffffffff max=0 + local invert=false + local ticks=(▁ ▂ ▃ ▄ ▅ ▆ ▇ █) + local tick_colors=(7 7 7 7 7 7 7 7) # default white - for n in ${@//,/ } + while getopts :t:p:c:n:iah opt; do + case "${opt}" in + t) local thold="${OPTARG}";; # threshold + p) local pcolor="${OPTARG}";; # color palette + c) local tcolor="${OPTARG}";; # threshold color + n) local name="${OPTARG}";; # graph display name + i) local invert=true;; # invert threshold from >= to <= + a) _color_table;exit 0;; + h) help;; + :) echo "Missing Option Argument for -${OPTARG}" >&2;exit 1;; + \?) echo "Option -${OPTARG} Unknown." >&2;exit 1;; + esac + done + shift "$((OPTIND-1))" + + # opts cleanup + [ "$1" = "--" ] && shift + if [ "$#" -eq 0 ]; then data=$(cat);else data="$@";fi + # validate data + if ! [[ "${data}" =~ ^[0-9\ ,.]*$ ]];then echo "Data not valid: ${data}";exit 1;fi + # find min/max + for n in ${data//,/ } do # on Linux (or with bash4) we could use `printf %.0f $n` here to # round the number but that doesn't work on OS X (bash3) nor does @@ -55,40 +111,120 @@ spark() numbers=$numbers${numbers:+ }$n done - # print ticks - local ticks=(▁ ▂ ▃ ▄ ▅ ▆ ▇ █) - # use a high tick if data is constant (( min == max )) && ticks=(▅ ▆) local f=$(( (($max-$min)<<8)/(${#ticks[@]}-1) )) (( f < 1 )) && f=1 + # set threshold color + if [[ -n "${tcolor}" ]];then + case "${tcolor}" in + black) tcolor=0;; + red) tcolor=1;; + green) tcolor=2;; + yellow) tcolor=3;; + blue) tcolor=4;; + magenta) tcolor=5;; + cyan) tcolor=6;; + white) tcolor=7;; + [0-9]*) ;; # if an integer, pass the int as an ansi color code to tput setaf + *) echo "Threshold color option \"${tcolor}\" unknown" >&2;exit 1;; + esac + fi + + # set color palette + if [[ -n "${pcolor}" ]];then + case "${pcolor}" in + fire) tick_colors=(228 227 226 220 214 208 202 196);; + ice) tick_colors=(20 21 27 33 39 14 255 15);; + smoke) tick_colors=(254 249 247 244 242 240 238 0);; + earth) tick_colors=(12 94 100 34 22 240 244 246);; + pride) tick_colors=(205 1 214 3 2 6 4 92);; # 8 color original design by Gilbert Baker in 1978 + lolcat) for i in {0..7};do tick_colors[${i}]=$(( RANDOM % 255 ));done;; # random colors + [0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*,[0-9]*) tick_colors=(${pcolor//,/ });; # ansi codes as a comma-seperated 8 element list + *) echo "Base color option \"${pcolor}\" unknown";exit 1;; + esac + fi + + # prepend title and min/max to graph + [[ -n "${name}" ]] && echo -n "[${name}][${min}/${max}] " + local output="" for n in $numbers do - _echo -n ${ticks[$(( ((($n-$min)<<8)/$f) ))]} + local tval=$(( ((($n-$min)<<8)/$f) )) + # test for inverted threshold color + if ${invert} && [[ -n "${thold}" ]] && [[ -n "${tcolor}" ]] && [[ "${n}" -le "${thold}" ]];then + output+="$(tput setaf ${tcolor})${ticks[${tval}]}" + # test for non-iverted threshold color + elif ! ${invert} && [[ -n "${thold}" ]] && [[ -n "${tcolor}" ]] && [[ "${n}" -ge "${thold}" ]];then + output+="$(tput setaf ${tcolor})${ticks[${tval}]}" + # test for colors without triggered threshold + elif [[ -n "${pcolor}" ]] || ( [[ -n "${thold}" ]] && [[ -n "${tcolor}" ]] );then + output+="$(tput setaf ${tick_colors[${tval}]})${ticks[${tval}]}" + # draw graph without colors + else + output+="${ticks[${tval}]}" + fi done - _echo + # if colors are used, reset the format/colors at the end of the graph + if [[ -n "${pcolor}" ]] || ( [[ -n "${thold}" ]] && [[ -n "${tcolor}" ]] );then + output+="$(tput sgr0)" + fi + _echo "${output}" } # If we're being sourced, don't worry about such things if [ "$BASH_SOURCE" == "$0" ]; then # Prints the help text for spark. - help() - { + help(){ local spark=$(basename $0) cat <|-c |-n |-p ] VALUE,... - EXAMPLES: + Options: + -n + Specify name/title of graph. Includes min/max. + + -c + Specify tick color for values exceeding the threshold. + Options: + black, red, green, yellow, blue, magenta, cyan, white + ANSI color codes (0-255) + + -t + Specify threshold (greater than or equal) as an integer. + + -i + Invert the threshold logic to less than or equal. + + -p + Specify color palette for ticks. + Options: + fire, ice, earth, smoke, pride, lolcat + Comma-seperated 8 element list ansi color codes (0-255) + eg. 1,2,3,4,55,121,73,254 + -a + Print ansi color table and exit. + + Examples: $spark 1 5 22 13 53 ▁▁▃▂█ $spark 0,30,55,80,33,150 ▁▂▃▄▂█ echo 9 13 5 17 1 | $spark ▄▆▂█▁ + $spark -p fire 1 2 3 4 5 6 7 8 + $(_colorize "▁▂▃▄▅▆▇█" "228,227,226,220,214,208,202,196") + $spark -p ice -t 6 -c green 1 3 2 4 7 1 4 2 5 6 + $(_colorize "▁▃▂▄█▁▄▂▅▆" "20,27,21,33,2,20,33,21,39,2") + $spark -p 200,205,210,215,220,225,230,235 -c 9 -t 6 -i 1,2,3,4,5,6,7,8,9 + $(_colorize "▁▁▂▃▄▅▆▇█" "9,9,9,9,9,9,225,230,235") + $spark -n "Graph Title" 1 3 5 7 10 7 5 3 1 + [Graph Title][1/10] ▁▂▄▅█▅▄▂▁ + EOF } @@ -99,5 +235,5 @@ EOF exit 0 fi - spark ${@:-`cat`} + spark "${@:-`cat`}" fi diff --git a/spark-test.sh b/spark-test.sh index 4430db2..373aa72 100644 --- a/spark-test.sh +++ b/spark-test.sh @@ -4,75 +4,175 @@ describe "spark: Generates sparklines for a set of data." spark="./spark" -it_shows_help_with_no_argv() { - $spark | grep USAGE +# Handles colorization of the "expected" graph for testing color output +_colorize(){ + local ticks="${1}" + local colors=(${2//,/ }) + local output="" + for i in "${!colors[@]}";do + output+="$(tput setaf ${colors[$i]})${ticks:$i:1}" + done + output+="$(tput sgr0)" + echo "${output}" } -it_graphs_argv_data() { +it_shows_help_with_no_argv(){ + $spark | grep Usage +} + +it_graphs_argv_data(){ graph="$($spark 1,5,22,13,5)" - test $graph = '▁▂█▅▂' + test "$graph" = '▁▂█▅▂' } -it_charts_pipe_data() { +it_charts_pipe_data(){ data="0,30,55,80,33,150" graph="$(echo $data | $spark)" test $graph = '▁▂▃▄▂█' } -it_charts_spaced_data() { +it_charts_spaced_data(){ data="0 30 55 80 33 150" graph="$($spark $data)" test $graph = '▁▂▃▄▂█' } -it_charts_way_spaced_data() { +it_charts_way_spaced_data(){ data="0 30 55 80 33 150" graph="$($spark $data)" test $graph = '▁▂▃▄▂█' } -it_handles_decimals() { +it_handles_decimals(){ data="5.5,20" graph="$($spark $data)" - test $graph = '▁█' + test "$graph" = '▁█' } -it_charts_100_lt_300() { +it_charts_100_lt_300(){ data="1,2,3,4,100,5,10,20,50,300" graph="$($spark $data)" - test $graph = '▁▁▁▁▃▁▁▁▂█' + test "$graph" = '▁▁▁▁▃▁▁▁▂█' } -it_charts_50_lt_100() { +it_charts_50_lt_100(){ data="1,50,100" graph="$($spark $data)" - test $graph = '▁▄█' + test "$graph" = '▁▄█' } -it_charts_4_lt_8() { +it_charts_4_lt_8(){ data="2,4,8" graph="$($spark $data)" - test $graph = '▁▃█' + test "$graph" = '▁▃█' } -it_charts_no_tier_0() { +it_charts_no_tier_0(){ data="1,2,3,4,5" graph="$($spark $data)" - test $graph = '▁▂▄▆█' + test "$graph" = '▁▂▄▆█' } -it_equalizes_at_midtier_on_same_data() { +it_equalizes_at_midtier_on_same_data(){ data="1,1,1,1" graph="$($spark $data)" - test $graph = '▅▅▅▅' + test "$graph" = '▅▅▅▅' +} + +it_outputs_test_name(){ + data="1,2,3,4" + graph="$($spark -n name $data)" + + test "$graph" = '[name][1/4] ▁▃▅█' +} + +it_charts_red_gt_2(){ + data="1,2,3,4,5" + graph="$(./spark -c red -t 3 $data)" + expected="$(_colorize '▁▂▄▆█' '7,7,1,1,1')" + + test "$graph" = "$expected" +} + +it_charts_red_lt_4(){ + data="1,2,3,4,5" + graph="$(./spark -c red -t 3 -i $data)" + expected="$(_colorize '▁▂▄▆█' '1,1,1,7,7')" + + test "$graph" = "$expected" +} + +it_charts_custom_ansi_color_212_gt_2(){ + data="1,2,3,4,5" + graph="$(./spark -c 212 -t 3 $data)" + expected="$(_colorize '▁▂▄▆█' '7,7,212,212,212')" + + test "$graph" = "$expected" +} + +it_charts_pride(){ + data="1,2,3,4,5,6,7,8" + graph="$(./spark -p pride $data)" + expected="$(_colorize '▁▂▃▄▅▆▇█' '205,1,214,3,2,6,4,92')" + + test "$graph" = "$expected" } + +it_charts_earth(){ + data="1,2,3,4,5,6,7,8" + graph="$(./spark -p earth $data)" + expected="$(_colorize '▁▂▃▄▅▆▇█' '12,94,100,34,22,240,244,246')" + + test "$graph" = "$expected" +} + +it_charts_pipe_data_with_opts(){ + data="1,2,3,4,5,6,7,8" + graph="$(echo "${data}" | ./spark -n "Piped Data" -c red -t 5 -p fire)" + expected="$(_colorize '▁▂▃▄▅▆▇█' '1,1,1,33,1,1,1,1')" + +} + +it_charts_ice(){ + data="1,2,3,4,5,6,7,8" + graph="$(./spark -p ice $data)" + expected="$(_colorize '▁▂▃▄▅▆▇█' '20,21,27,33,39,14,255,15')" + + test "$graph" = "$expected" +} + +it_charts_fire(){ + data="1,2,3,4,5,6,7,8" + graph="$(./spark -p fire $data)" + expected="$(_colorize '▁▂▃▄▅▆▇█' '228,227,226,220,214,208,202,196')" + + test "$graph" = "$expected" +} + +it_charts_smoke(){ + data="1,2,3,4,5,6,7,8" + graph="$(./spark -p smoke $data)" + expected="$(_colorize '▁▂▃▄▅▆▇█' '254,249,247,244,242,240,238,0')" + + test "$graph" = "$expected" +} + +it_charts_custom_ansi_palette_colors(){ + data="1,2,3,4,5,6,7,8" + graph="$(./spark -p 1,2,3,4,5,6,7,8 $data)" + expected="$(_colorize '▁▂▃▄▅▆▇█' '1,2,3,4,5,6,7,8')" + + test "$graph" = "$expected" +} + +# No lolcat test as that is an effort in frustration