Sparkline in unicode
You are encouraged to solve this task according to the task description, using any language you may know.
A sparkline is a graph of successive values laid out horizontally where the height of the line is proportional to the values in succession.
Use the following series of Unicode characters to create a program that takes a series of numbers separated by one or more whitespace or comma characters and generates a sparkline-type bar graph of the values on a single line of output.
The eight characters: '▁▂▃▄▅▆▇█'
(Unicode values U+2581 through U+2588).
Use your program to show sparklines for the following input, here on this page:
- 1 2 3 4 5 6 7 8 7 6 5 4 3 2 1
- 1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5
- (note the mix of separators in this second case)!
- Notes
- A space is not part of the generated sparkline.
- The sparkline may be accompanied by simple statistics of the data such as its range.
C++
<lang cpp>#include <iostream>
- include <sstream>
- include <vector>
- include <cmath>
- include <algorithm>
- include <locale>
class Sparkline {
public: Sparkline(std::wstring &cs) : charset( cs ){ } virtual ~Sparkline(){ }
void print(std::string spark){ const char *delim = ", "; std::vector<float> data; // Get first non-delimiter std::string::size_type last = spark.find_first_not_of(delim, 0); // Get end of token std::string::size_type pos = spark.find_first_of(delim, last);
while( pos != std::string::npos || last != std::string::npos ){ std::string tok = spark.substr(last, pos-last); // Convert to float: std::stringstream ss(tok); float entry; ss >> entry;
data.push_back( entry );
last = spark.find_first_not_of(delim, pos); pos = spark.find_first_of(delim, last); }
// Get range of dataset float min = *std::min_element( data.begin(), data.end() ); float max = *std::max_element( data.begin(), data.end() );
float skip = (charset.length()-1) / (max - min);
std::wcout<<L"Min: "<<min<<L"; Max: "<<max<<L"; Range: "<<(max-min)<<std::endl; std::vector<float>::const_iterator it; for(it = data.begin(); it != data.end(); it++){ float v = ( (*it) - min ) * skip; std::wcout<<charset[ (int)floor( v ) ]; } std::wcout<<std::endl; } private: std::wstring &charset;
};
int main( int argc, char **argv ){
std::wstring charset = L"\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
// Mainly just set up utf-8, so wcout won't narrow our characters. std::locale::global(std::locale("en_US.utf8"));
Sparkline sl(charset);
sl.print("1 2 3 4 5 6 7 8 7 6 5 4 3 2 1"); sl.print("1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5");
return 0;
}</lang>
- Output:
Min: 1; Max: 8; Range: 7 ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁ Min: 0.5; Max: 7.5; Range: 7 ▂▁▄▃▆▅█▇
D
<lang d>void main() {
import std.stdio, std.range, std.algorithm, std.conv, std.string, std.regex;
"Numbers please separated by space/commas: ".write; /*immutable*/ const numbers = readln .strip .splitter(r"[\s,]+".regex) .array /**/ .to!(real[]); immutable mm = numbers.reduce!(min, max); "min: %5f; max: %5f".writefln(mm[]); immutable bars = iota(9601, 9609).map!(i => i.to!dchar).dtext; immutable div = (mm[1] - mm[0]) / (bars.length - 1); numbers.map!(n => bars[cast(int)((n - mm[0]) / div)]).writeln;
}</lang> The output is the same as the Python entry (but it only accepts one series of values at a time).
F#
<lang fsharp>open System open System.Globalization open System.Text.RegularExpressions
let bars = Array.map Char.ToString ("▁▂▃▄▅▆▇█".ToCharArray())
while true do
printf "Numbers separated by anything: " let numbers = [for x in Regex.Matches(Console.ReadLine(), @"-?\d+(?:\.\d*)?") do yield x.Value] |> List.map (fun x -> Double.Parse(x, CultureInfo.InvariantCulture)) if numbers.Length = 0 then System.Environment.Exit(0) if numbers.Length = 1 then printfn "A sparkline for 1 value is not very useful... ignoring entry" else let min, max = List.min numbers, List.max numbers printfn "min: %5f; max: %5f" min max let barsCount = float (bars.GetUpperBound(0)) numbers |> List.map (fun x -> bars.[int ((x - min)/(max - min) * barsCount)]) |> String.Concat |> printfn "%s"</lang>
- Output:
Numbers separated by anything: 1 2 3 4 5 6 7 8 7 6 5 4 3 2 1 min: 1.000000; max: 8.000000 ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁ Numbers separated by anything: 1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5 min: 0.500000; max: 7.500000 ▂▁▄▃▆▅█▇ Numbers separated by anything:
FALSE
<lang false>{
variables: s: sign (1 or -1) u: current number f: current number fraction length v: current number is valid t: number of numbers read x: biggest fraction y: smallest number (without fraction) z: biggest number (without fraction)
}
{function a: test if top is 0-9, without popping the value, codes 48-57 are in range} [$$47>\57>~&]a:
{function b: test if top is ',' or ' ', without popping the value} [$$',=\' =|]b:
{function c: read a number from the input, given that the first character of the input is already on the stack} [
1s:0u:0f:0v: {reset values}
$'-=[1_s:%^]? {if (it is negative) set the sign value to -1 move to next} [a;!][48-u;10*+u:1_v:^]# {while (isnumber) do number = number * 10 + decimal and set valid number and move to next} $'.=[ {if (it is a decimal) move forward and read fraction} %^ [a;!][48-u;10*+u:f;1+f:1_v:^]# {while (isnumber) do number = number * 10 + decimal and increase fraction length and set valid number and move to next} ]? $$'-=\'.=|[0v:]? {if next charachter is a '-' or a '.', set invalid}
]c:
{function d: normalize number/fraction from stack to max fraction and push that number} [
[$x;=~][1+\10*\]# {while (fraction != max) fraction + 1, value * 10} % {pop fraction}
]d:
0t: 0x: 1_v: { nothing read, so we are still valid } ^[b;!][%^]# {read away any initial separators} [$1_=~v;&][ {while input != -1 and valid input, leaving input on the stack}
c;! {read a number} t;1+t:u;s;*f;@ {increase count, push number * sign and fraction length onto the stack and bring input back up} f;x;>[f;x:]? {set fraction to biggest of current and previous biggest} [b;!][%^]# {while (isseparator) move forward}
]# v;~["error at charachter ",]? {if invalid number, tell them when} v;[ {if last number also valid, do the math}
% {pop the -1} t;2*1-q: {var q: points to next value} 0p: {var p: whether min/max have been set} [q;1+t;>][ {while q + 1 > t} q;ø {current number} q;ø {current fraction} d;! {normalize} p;[$y;\>[$y:]? $z;>[$z:]?]? {compare min/max} p;~[1_p:$y:$z:]? {if (first)) set min/max} q;1-q: {move pointer} ]#
t;q: {point q to first value} [q;0>][ {while q > 0} q;1-øy;-7*z;y;-/ {(number - minvalue) * 7 / (maxvalue - minvalue), should result in 0..7} 9601+, {print character} q;1-q: {move pointer} ]#
]?</lang> This implementation can only accept one series of numbers at a time.
- Output:
▁▂▃▄▅▆▇█▇▆▅▄▃▂▁ ▂▁▄▃▆▅█▇
Go
<lang go>package main
import (
"bufio" "errors" "fmt" "math" "os" "regexp" "strconv" "strings"
)
func main() {
fmt.Println("Numbers please separated by space/commas:") sc := bufio.NewScanner(os.Stdin) sc.Scan() s, n, min, max, err := spark(sc.Text()) if err != nil { fmt.Println(err) return } if n == 1 { fmt.Println("1 value =", min) } else { fmt.Println(n, "values. Min:", min, "Max:", max) } fmt.Println(s)
}
var sep = regexp.MustCompile(`[\s,]+`)
func spark(s0 string) (sp string, n int, min, max float64, err error) {
ss := sep.Split(s0, -1) n = len(ss) vs := make([]float64, n) var v float64 min = math.Inf(1) max = math.Inf(-1) for i, s := range ss { switch v, err = strconv.ParseFloat(s, 64); { case err != nil: case math.IsNaN(v): err = errors.New("NaN not supported.") case math.IsInf(v, 0): err = errors.New("Inf not supported.") default: if v < min { min = v } if v > max { max = v } vs[i] = v continue } return } if min == max { sp = strings.Repeat("▄", n) } else { rs := make([]rune, n) f := 8 / (max - min) for j, v := range vs { i := rune(f * (v - min)) if i > 7 { i = 7 } rs[j] = '▁' + i } sp = string(rs) } return
}</lang>
- Output:
Numbers please separated by space/commas: 1 2 3 4 5 6 7 8 7 6 5 4 3 2 1 15 values. Min: 1 Max: 8 ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁ Numbers please separated by space/commas: 1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5 8 values. Min: 0.5 Max: 7.5 ▂▁▄▃▆▅█▇ Numbers please separated by space/commas: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 24 values. Min: 1 Max: 24 ▁▁▁▂▂▂▃▃▃▄▄▄▅▅▅▆▆▆▇▇▇███ Numbers please separated by space/commas: 0 99 101 699 701 800 6 values. Min: 0 Max: 800 ▁▁▂▇██ Numbers please separated by space/commas: 0 -.09 -.11 -.69 -.71 -.8 6 values. Min: -0.8 Max: 0 ██▇▂▁▁ Numbers please separated by space/commas: 3 3 3 3 values. Min: 3 Max: 3 ▄▄▄ Numbers please separated by space/commas: 1e99 1 value = 1e+99 ▄ Numbers please separated by space/commas: strconv.ParseFloat: parsing "": invalid syntax
Groovy
<lang groovy>def sparkline(List<Number> list) {
def (min, max) = [list.min(), list.max()] def div = (max - min) / 7 list.collect { (char)(0x2581 + (it-min) * div) }.join()
} def sparkline(String text) { sparkline(text.split(/[ ,]+/).collect { it as Double }) }</lang> Test Code <lang groovy>["1 2 3 4 5 6 7 8 7 6 5 4 3 2 1", "1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5"].each { dataset ->
println " Dataset: $dataset" println "Sparkline: ${sparkline(dataset)}"
}</lang>
- Output:
Dataset: 1 2 3 4 5 6 7 8 7 6 5 4 3 2 1 Sparkline: ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁ Dataset: 1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5 Sparkline: ▂▁▄▃▆▅█▇
Haskell
<lang haskell> import Data.Char (chr) import Data.List.Split (splitOneOf)
toSparkLine :: [Double] -> [Char] toSparkLine xs = map cl xs
where top = maximum xs bot = minimum xs range = top - bot cl x = chr $ 0x2581 + round ((x - bot) / range * 7)
makeSparkLine :: String -> (String, Stats) makeSparkLine xs = (toSparkLine parsed, stats parsed)
where parsed = map read $ filter (not . null) $ splitOneOf " ," xs
data Stats = Stats { minValue, maxValue, rangeOfValues :: Double,
numberOfValues :: Int }
instance Show Stats where
show (Stats mn mx r n) = "min: " ++ show mn ++ "; max: " ++ show mx ++ "; range: " ++ show r ++ "; no. of values: " ++ show n
stats :: [Double] -> Stats stats xs = Stats { minValue = mn, maxValue = mx,
rangeOfValues = mx - mn, numberOfValues = length xs } where mn = minimum xs mx = maximum xs
drawSparkLineWithStats :: String -> IO () drawSparkLineWithStats xs = putStrLn sp >> print st
where (sp, st) = makeSparkLine xs
main :: IO () main = mapM_ drawSparkLineWithStats
["1 2 3 4 5 6 7 8 7 6 5 4 3 2 1", "1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5", "3 2 1 0 -1 -2 -3 -4 -3 -2 -1 0 1 2 3", "-1000 100 1000 500 200 -400 -700 621 -189 3"]
</lang> Output:
▁▂▃▄▅▆▇█▇▆▅▄▃▂▁ min: 1.0; max: 8.0; range: 7.0; no. of values: 15 ▂▁▄▃▆▅█▇ min: 0.5; max: 7.5; range: 7.0; no. of values: 8 █▇▆▅▄▃▂▁▂▃▄▅▆▇█ min: -4.0; max: 3.0; range: 7.0; no. of values: 15 ▁▅█▆▅▃▂▇▄▅ min: -1000.0; max: 1000.0; range: 2000.0; no. of values: 10
Java
<lang java> public class Sparkline { String bars="▁▂▃▄▅▆▇█"; public static void main(String[] args) { Sparkline now=new Sparkline(); float[] arr={1, 2, 3, 4, 5, 6, 7, 8, 7, 6, 5, 4, 3, 2, 1}; now.display1D(arr); System.out.println(now.getSparkline(arr)); float[] arr1={1.5f, 0.5f, 3.5f, 2.5f, 5.5f, 4.5f, 7.5f, 6.5f}; now.display1D(arr1); System.out.println(now.getSparkline(arr1)); } public void display1D(float[] arr) { for(int i=0;i<arr.length;i++) System.out.print(arr[i]+" "); System.out.println(); } public String getSparkline(float[] arr) { float min=Integer.MAX_VALUE; float max=Integer.MIN_VALUE; for(int i=0;i<arr.length;i++) { if(arr[i]<min) min=arr[i]; if(arr[i]>max) max=arr[i]; } float range=max-min; int num=bars.length()-1; String line=""; for(int i=0;i<arr.length;i++) {
line+=bars.charAt((int)Math.ceil(((arr[i]-min)/range*num))); } return line; } } </lang> Output:
1.0 2.0 3.0 4.0 5.0 6.0 7.0 8.0 7.0 6.0 5.0 4.0 3.0 2.0 1.0 ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁ 1.5 0.5 3.5 2.5 5.5 4.5 7.5 6.5 ▂▁▄▃▆▅█▇
NetRexx
<lang NetRexx>/* NetRexx */ options replace format comments java crossref symbols nobinary
runSample(arg) return
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ method sparkline(spark) private static
spark = spark.changestr(',', ' ') bars = '\u2581 \u2582 \u2583 \u2584 \u2585 \u2586 \u2587 \u2588' barK = bars.words() nmin = spark.word(1) nmax = nmin -- get min & max values loop iw = 1 to spark.words() nval = spark.word(iw) nmin = nval.min(nmin) nmax = nval.max(nmax) end iw range = nmax - nmin + 1 slope = loop iw = 1 to spark.words() point = Math.ceil((spark.word(iw) - nmin + 1) / range * barK) slope = slope || bars.word(point) end iw return slope nmin nmax range
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ method runSample(arg) private static
-- sample data setup parse arg vals sparks = 0 sparks[0] = 0 if vals = then do si = sparks[0] + 1; sparks[0] = si; sparks[si] = 1 2 3 4 5 6 7 8 7 6 5 4 3 2 1 si = sparks[0] + 1; sparks[0] = si; sparks[si] = '1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5' end else do loop until vals = -- split input on a ! character parse vals lst '!' vals si = sparks[0] + 1; sparks[0] = si; sparks[si] = lst end end -- run the samples loop si = 1 to sparks[0] vals = sparks[si] parse sparkline(vals) slope . say 'Input: ' vals say 'Sparkline: ' slope say end si return
</lang>
- Output:
Input: 1 2 3 4 5 6 7 8 7 6 5 4 3 2 1 Sparkline: ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁ Input: 1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5 Sparkline: ▂▁▄▃▆▅█▇
Perl 6
<lang perl6>constant @bars = '▁' ... '█'; while prompt 'Numbers separated by anything: ' -> $_ {
my @numbers = map +*, .comb(/ '-'? \d+ ['.' \d+]? /); my ($mn,$mx) = @numbers.minmax.bounds; say "min: $mn.fmt('%5f'); max: $mx.fmt('%5f')"; my $div = ($mx - $mn) / (@bars - 1); say @bars[ (@numbers X- $mn) X/ $div ].join;
}</lang>
- Output:
Numbers separated by anything: 9 18 27 36 45 54 63 72 63 54 45 36 27 18 9 9 18 27 36 45 54 63 72 63 54 45 36 27 18 9 min: 9.000000; max: 72.000000 ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁ Numbers separated by anything: 1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5 1.5 0.5 3.5 2.5 5.5 4.5 7.5 6.5 min: 0.500000; max: 7.500000 ▂▁▄▃▆▅█▇ Numbers separated by anything: 3 2 1 0 -1 -2 -3 -4 -3 -2 -1 0 1 2 3 min: -4.000000; max: 3.000000 █▇▆▅▄▃▂▁▂▃▄▅▆▇█ Numbers separated by anything: ^D
Python
<lang python>import re try: raw_input except: raw_input = input
- Unicode: 9601, 9602, 9603, 9604, 9605, 9606, 9607, 9608
try: bar = u'▁▂▃▄▅▆▇█' except: bar = '▁▂▃▄▅▆▇█' barcount = len(bar) - 1 while True:
line = raw_input('Numbers please separated by space/commas: ') numbers = [float(n) for n in re.split(r'[\s,]+', line.strip())] mn, mx = min(numbers), max(numbers) extent = mx - mn sparkline = .join(bar[int( (n - mn) / extent * barcount)] for n in numbers) print('min: %5f; max: %5f' % (mn, mx)) print(sparkline)</lang>
- Output:
Numbers separated by space/commas: 1 2 3 4 5 6 7 8 7 6 5 4 3 2 1 min: 1.000000; max: 7.000000 ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁ Numbers separated by space/commas: 1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5 min: 0.500000; max: 7.500000 ▂▁▄▃▆▅█▇
REXX
version 1
<lang REXX>/* Rexx */
parse arg aaa call runSample aaa return
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ sparkline:
procedure parse arg spark spark = changestr(',', spark, ' ') bars = '▁ ▂ ▃ ▄ ▅ ▆ ▇ █' barK = words(bars) nmin = word(spark, 1) nmax = nmin -- get min & max values do iw = 1 to words(spark) nval = word(spark, iw) nmin = min(nval, nmin) nmax = max(nval, nmax) end iw range = nmax - nmin + 1 slope = do iw = 1 to words(spark) point = ceiling((word(spark, iw) - nmin + 1) / range * barK) slope = slope || word(bars, point) end iw return slope nmin nmax range
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ceiling: procedure
parse arg ceil return trunc(ceil) + (ceil > 0) * (ceil \= trunc(ceil))
floor: procedure
parse arg flor return trunc(flor) - (flor < 0) * (flor \= trunc(flor))
-- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ runSample: procedure
-- sample data setup parse arg vals sparks = 0 sparks.0 = 0 if vals = then do si = sparks.0 + 1; sparks.0 = si; sparks.si = 1 2 3 4 5 6 7 8 7 6 5 4 3 2 1 si = sparks.0 + 1; sparks.0 = si; sparks.si = '1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5' end else do do until vals = -- split input on a ! character parse var vals lst '!' vals si = sparks.0 + 1; sparks.0 = si; sparks.si = lst end end -- run the samples do si = 1 to sparks.0 vals = sparks.si parse value sparkline(vals) with slope . say 'Input: ' vals say 'Sparkline: ' slope say end si return
</lang>
- Output:
Input: 1 2 3 4 5 6 7 8 7 6 5 4 3 2 1 Sparkline: ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁ Input: 1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5 Sparkline: ▂▁▄▃▆▅█▇
version 2
(A re-work of REXX version 1)
This version works on:
- all versions of Regina (which may or may not support single line comments)
- R4 and ROO (which don't support single line comments)
- older versions of REXX such as PC/REXX and Personal REXX which don't support the changestr BIF
This version also removed some dead code, simplified the program structure and subroutines, added comments.
Single line comments were introduced in Regina 3.4.
Regina 3.6 introducted the options: single_line_comments and noSingle_line_comments.
It should also be noted that the CMS and TSO versions of REXX (and others) don't support single line comments.
<lang rexx>/*REXX program displays a sparkline (spark graph) for a group of values.*/
if arg()==0 then do /*No arguments? Use defaults.*/
call sparkGraph 1 2 3 4 5 6 7 8 7 6 5 4 3 2 1 call sparkGraph '1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5' end else call sparkGraph arg(1)
exit /*stick a fork in it, we're done.*/ /*──────────────────────────────────CEIL subroutine─────────────────────*/ ceil: procedure; parse arg ?; _=trunc(?); return _+(?>0)*(?\=_) /*──────────────────────────────────SPARKGRAPH subroutine───────────────*/ sparkGraph: procedure; parse arg x; say ' input: ' x /*echo values*/ x=translate(x, ' ', ",") /*remove any superfluous commas. */ $='▁▂▃▄▅▆▇█' /*chars to be used for the graph.*/ xmin=word(x,1); xmax=xmin /*assume a minimum and a maximum.*/
do n=2 to words(x); _=word(x,n) /*examine successive words in X.*/ xmin=min(_,xmin) /*find the minimum value in X. */ xmax=max(_,xmax) /* " " maximum " " " */ end /*n*/
z=; do j=1 for words(x) /*build the output spark graph. */
z=z||substr($,ceil((word(x,j)-xmin+1)/(xmax-xmin+1)*length($)),1) end /*j*/
say 'output: ' z; say /*show the output, + a blank line*/ return</lang> output using the default input(s):
input: 1 2 3 4 5 6 7 8 7 6 5 4 3 2 1 output: ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁ input: 1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5 output: ▂▁▄▃▆▅█▇
Racket
<lang racket>
- lang racket (require syntax/parse)
(define bars "▁▂▃▄▅▆▇█") (define bar-count (string-length bars))
(define (sparks str)
(define ns (map string->number (string-split str #rx"[ ,]" #:repeat? #t))) (define mn (apply min ns)) (define bar-width (/ (- (apply max ns) mn) (- bar-count 1))) (apply string (for/list ([n ns]) (string-ref bars (exact-floor (/ (- n mn) bar-width))))))
(sparks "1 2 3 4 5 6 7 8 7 6 5 4 3 2 1") (sparks "1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5") </lang> Output: <lang racket> "▁▂▃▄▅▆▇█▇▆▅▄▃▂▁" "▂▁▄▃▆▅█▇" </lang>
Tcl
<lang tcl>package require Tcl 8.6
proc extractValues {series} {
return [regexp -all -inline {\d+(?:\.\d*)?|\.\d+} $series]
} proc renderValue {min max value} {
set band [expr {int(8*($value-$min)/(($max-$min)*1.01))}] return [format "%c" [expr {0x2581 + $band}]]
} proc sparkline {series} {
set values [extractValues $series] set min [tcl::mathfunc::min {*}$values] set max [tcl::mathfunc::max {*}$values] return [join [lmap v $values {renderValue $min $max $v}] ""]
}</lang> Demonstrating: <lang tcl>set data {
"1 2 3 4 5 6 7 8 7 6 5 4 3 2 1" "1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5"
} foreach series $data {
puts "Series: $series" puts "Sparkline: [sparkline $series]"
}</lang>
- Output:
Series: 1 2 3 4 5 6 7 8 7 6 5 4 3 2 1 Sparkline: ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁ Series: 1.5, 0.5 3.5, 2.5 5.5, 4.5 7.5, 6.5 Sparkline: ▂▁▄▃▆▅█▇