Sparkline

Sparkline is a term Edward Tufte invented with for "small, high resolution graphics embedded in a context of words, numbers, images". On the Internet, however, the term has come to mean rough text charts made with block characters, like this: ▁▂▃▅▂▇.

dbohdan 2014-12-29: This particular implementation was inspired by using one for Bash (sadly, incompatible with the POSIX sh). It works in both Tcl 8.4+ and recent versions of Jim Tcl.

It adds a bit of functionality not offered by the Bash script. The command-line arguments --min and --max let you set the scale of the chart.

Download with wiki-reaper: wiki-reaper -x 40990 0 > sparkline.tcl

#! /usr/bin/env tclsh
# Draw sparklines (small inline charts) with Unicode block characters.
# Copyright (c) 2014, 2021, 2023 D. Bohdan. License: MIT.

namespace eval sparkline {
    namespace export create

    variable version 0.2.0
    variable ticks [list ▁ ▂ ▃ ▄ ▅ ▆ ▇ █]
    variable tickMax [expr {[llength $ticks] - 1}]
}

proc sparkline::create {data {formalMin {}} {formalMax {}}} {
    variable tickMax

    set sorted [lsort -real $data]
    set realMin [lindex $sorted 0]
    set realMax [lindex $sorted end]
    unset sorted

    set min $formalMin
    if {$min eq {}} {
        set min $realMin
    }
    set max $formalMax
    if {$max eq {}} {
        set max $realMax
    }

    set repeatChar {}

    if {$formalMin ne {}
        && ($formalMin > $realMax
            || ($formalMax ne {} && $formalMin > $formalMax))} {
        set repeatChar { }
    } elseif {$formalMax ne {} && $realMin > $formalMax} {
        set repeatChar [tick 0 1 1]
    } elseif {$min == $max} {
        # Either the data are all the same or the formal min and max
        # are.
        set repeatChar [tick 0 $tickMax [expr { $tickMax / 2 }]]
    }

    if {$repeatChar ne {}} {
        return [string repeat $repeatChar [llength $data]]
    }

    set result {}
    foreach x $data {
        append result [tick $min $max $x]
    }

    return $result
}

proc sparkline::tick {min max x} {
    variable ticks
    variable tickMax

    set normalized [expr {
        $x < $max
        ? int($tickMax * ($x - $min) / ($max - $min))
        : $tickMax
    }]

    set tick [lindex $ticks $normalized]
    if {$tick eq {}} {
        set tick { }
    }

    return $tick
}

# The following code allows you to use this script from the command
# line.

proc main-script? {} {
    # From https://wiki.tcl-lang.org/40097.
    global argv0

    if {[info exists argv0]
        && [file exists [info script]] && [file exists $argv0]} {
        file stat $argv0        argv0Info
        file stat [info script] scriptInfo

        return [expr {
            $argv0Info(dev) == $scriptInfo(dev)
            && $argv0Info(ino) == $scriptInfo(ino)
        }]
    }

    return 0
}

if {[main-script?]} {
    if {$argv in {/? -h -help --help help {}}} {
        set minMax {[--min min] [--max max]}
        set me [file tail [info script]]
        puts "usage: $me $minMax value1 \[value2 ...\]"
        puts "       $me $minMax \"value1 \[value2 ...\]\""
        puts "       $me help"
        puts "       $me test"
        puts "       $me version"
        exit 0
    }

    if {$argv eq {test}} {
        proc assert {v1 v2} {
            if {$v1 eq $v2} {
                puts $v1
            } else {
                error [list assertion failed: $v1 ne $v2]
            }
        }

        assert [sparkline::create {1 5 22 13 53}] {▁▁▃▂█}
        assert [sparkline::create {0 30 55 80 33 150}] {▁▂▃▄▂█}
        assert [sparkline::create {9 13 5 17 1}] {▄▆▂█▁}
        assert [sparkline::create {1 1 1 1}] {▄▄▄▄}
        assert [sparkline::create {1 1 1 1} -1 -1] {████}
        assert [sparkline::create {1 1 1 1} 2 2] {    }
        assert [sparkline::create {1 2 3 4 5} 1 100] {▁▁▁▁▁}
        assert [sparkline::create {1 2 3 4 5} 3 100] {  ▁▁▁}
        assert [sparkline::create {1 2 3 4 5} 0 2] {▄████}
        assert [sparkline::create {1 2 3 4 5} -20 -10] {█████}
        assert [sparkline::create {1 2 3 4 5} -10 -20] {     }
        assert [sparkline::create {1 2 3} {} {}] {▁▄█}
        assert [sparkline::create {1 2 3} {} 3] {▁▄█}
        assert [sparkline::create {1 2 3} 1 {}] {▁▄█}
        assert [sparkline::create {1 2 3} 1 3] {▁▄█}
        assert [sparkline::create {1 2 3} 5 {}] {   }

        puts ok
        exit 0
    }

    if {$argv eq {version}} {
        puts $sparkline::version
        exit 0
    }

    set min {}
    set max {}
    while {[regexp -- {^--?(.*)$} [lindex $argv 0] flag flagTrimmed]} {
        switch -- $flagTrimmed {
            min {
                set argv [lassign $argv _ min]
            }
            max {
                set argv [lassign $argv _ max]
            }
            default {
                puts [list unknown flag: $flag]
                exit 1
            }
        }
    }

    if {[llength $argv] == 1} {
        set argv [lindex $argv 0]
    }

    puts [sparkline::create $argv $min $max]
}

See also