▚Sparklinecustom
Mini time-series chart with gradient fill, dot and reveal
$ 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
| prop | type | default | description |
|---|---|---|---|
| data | number[] | [] | Series of values (windowed to width) |
| width | number | 24 | Number of glyphs to render |
| fg | string | "#7dcfff" | Line color (glyph top) |
| fill | string | "#1a1b26" | Gradient end color (glyph base) |
| min / max | number | auto | Optional fixed scale bounds |
| reveal | number | 1 | 0..1 reveal progress |
| loading | boolean | false | Shimmer sweep skeleton |
| dot | boolean | true | ● marker on the last revealed column |
##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.
1/* eslint-disable react/no-array-index-key -- These renderers draw a fixed2 terminal grid: a child's identity *is* its row and column, and the grid3 never reorders, so the index is the stable key rather than a stand-in4 for one. */56import { createElement as h, useEffect, useMemo, useState } from "react";7import { mergeRuns, mixColor } from "./chart";89const BLOCKS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"] as const;1011export 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}2223function 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}3233export 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);4647 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 }5657 if (window.length === 0) return null;5859 const lo = min ?? Math.min(...window);60 const hi = max ?? Math.max(...window);61 const span = hi - lo || 1;6263 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 }8081 return h("text", { fg }, mergeRuns(cells).map((s, i) => h("span", { key: i, fg: s.fg }, s.ch)));82}83