termino
~/docs/custom/sparkline

Sparklinecustom

Mini time-series chart with gradient fill, dot and reveal

~/custom/sparkline
live
$ termino sparkline — gradient + dot
cpu sys/mon
net rx/s
reveal 0% blocks ▁▂▃▄▅▆▇█

##API

Single-row block-glyph chart (▁▂▃▄▅▆▇█). Per-column gradient mixes `fg → fill` by glyph depth; a ● tracks the latest value. No keyboard input.

properties
proptypedefaultdescription
datanumber[][]Series of values (windowed to width)
widthnumber24Number of glyphs to render
fgstring"#7dcfff"Line color (glyph top)
fillstring"#1a1b26"Gradient end color (glyph base)
min / maxnumberautoOptional fixed scale bounds
revealnumber10..1 reveal progress
loadingbooleanfalseShimmer sweep skeleton
dotbooleantrue● marker on the last revealed column

##Keymap

~/keymap
Display only

##Notes

  • Scales to the last `width` values — feed it a growing array and it slides.
  • Loading reuses the same traveling-▓ shimmer as LineChart, in one row.

##Source

Real implementation — this is the code that runs in your terminal.

tsxsparkline.tsx
1/* eslint-disable react/no-array-index-key -- These renderers draw a fixed
2 terminal grid: a child's identity *is* its row and column, and the grid
3 never reorders, so the index is the stable key rather than a stand-in
4 for one. */
5
6import { createElement as h, useEffect, useMemo, useState } from "react";
7import { mergeRuns, mixColor } from "./chart";
8
9const BLOCKS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"] as const;
10
11export interface SparklineProps {
12 data: number[];
13 width?: number;
14 fg?: string;
15 fill?: string;
16 min?: number;
17 max?: number;
18 reveal?: number;
19 loading?: boolean;
20 dot?: boolean;
21}
22
23function useSweep(active: boolean, width: number): number {
24 const [sweep, setSweep] = useState(-3);
25 useEffect(() => {
26 if (!active) return;
27 const timer = setInterval(() => setSweep((s) => (s + 1) % (width + 6)), 90);
28 return () => clearInterval(timer);
29 }, [active, width]);
30 return sweep;
31}
32
33export function Sparkline({
34 data,
35 width = 24,
36 fg = "#7dcfff",
37 fill = "#1a1b26",
38 min,
39 max,
40 reveal = 1,
41 loading = false,
42 dot = true,
43}: Readonly<SparklineProps>) {
44 const window = useMemo(() => data.slice(-width), [data, width]);
45 const sweep = useSweep(loading, width);
46
47 if (loading) {
48 const cells = [];
49 for (let c = 0; c < width; c++) {
50 const dist = sweep - c;
51 if (dist >= -2 && dist <= 0) cells.push({ ch: "▓", fg: mixColor(fg, "#ffffff", 0.45) });
52 else cells.push({ ch: "░", fg: mixColor(fg, fill, 0.5) });
53 }
54 return h("text", { fg }, mergeRuns(cells).map((s, i) => h("span", { key: i, fg: s.fg }, s.ch)));
55 }
56
57 if (window.length === 0) return null;
58
59 const lo = min ?? Math.min(...window);
60 const hi = max ?? Math.max(...window);
61 const span = hi - lo || 1;
62
63 const shown = Math.round(reveal * width);
64 const cells: { ch: string; fg: string }[] = [];
65 for (let c = 0; c < width; c++) {
66 if (c >= shown) {
67 cells.push({ ch: " ", fg });
68 continue;
69 }
70 const v = window[Math.min(c, window.length - 1)] ?? lo;
71 const t = (v - lo) / span;
72 const g = Math.min(7, Math.max(0, Math.floor(t * 7.999)));
73 if (dot && c === shown - 1 && shown > 0) {
74 cells.push({ ch: "●", fg: mixColor(fg, "#ffffff", 0.6) });
75 continue;
76 }
77 const depth = (7 - g) / 7;
78 cells.push({ ch: BLOCKS[g] ?? BLOCKS[0], fg: mixColor(fg, fill, depth) });
79 }
80
81 return h("text", { fg }, mergeRuns(cells).map((s, i) => h("span", { key: i, fg: s.fg }, s.ch)));
82}
83