charts.sparkline
A sparkline's SVG path and end marker from a series, fitted into a box, with gaps left as breaks.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 11 tests, run in TypeScript, Python and Rust.
What it does
A sparkline is a word-sized line chart: no axes, no labels, just the shape of a series and a dot on its latest value. `sparkline(values, width, height, padding)` returns the SVG path and where to put that dot, fitted into a box.
- Values are spaced evenly from `padding` to `width - padding`. The lowest value is drawn at the bottom of the padded box and the highest at the top, so the line always uses the full height (a sparkline shows shape, not magnitude; put the number beside it if magnitude matters). - `null` is a gap: the line breaks there and resumes after it. A value on its own between gaps is drawn as a dot (`M x,y Z`, which a round line cap shows), as d3 does. - The end marker is the last value that is not a gap. With no values at all the path is `""` and the end is `null`. - A flat series (every value equal) is drawn along the middle of the box rather than dividing by a zero range. A single value sits at the right-hand end, where the latest value of a sparkline belongs. - Coordinates go through `charts.scale` (6 places) and `charts.shape`'s linePath, which rounds them to 2 places and prints them without exponents, so the path string is identical in every language. The end marker is rounded to 2 places the same way.
For example
sparkline(1, 3, 2, 100, 20, 2)→ path M2,18L50,2L98,10, end … three values across a padded boxsparkline(5, —, 7, 6, 64, 24, 2)→ path M2,22ZM42,2L62,12, end … a gap breaks the line; a lone point before it is closed into a dotsparkline(1, 2, —, 20, 10, 0)→ path M0,10L10,0, end … a trailing gap: the end marker is the last real value, not the last slot
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
export function sparkline(values: readonly (number | null)[], width: number, height: number, padding: number): Sparkline
| values | float?[] | the series, evenly spaced; null is a gap in the line |
| width | float | the box, in pixels |
| height | float | the box, in pixels |
| padding | float | kept clear on every side, so the stroke and end marker are not clipped |
| returns | Sparkline |
The types it declares, generated into your project
/** A point in the box, in pixels. */
export interface SparkPoint {
readonly x: number;
readonly y: number;
}
/** Everything needed to draw one sparkline. */
export interface Sparkline {
/** SVG path data; "" when the series has no values */
readonly path: string;
/** the last value that is not a gap, for the end dot; null when there is none */
readonly end: SparkPoint | null;
}
Your code names it in one line, in the file that uses it
import { sparkline } from "#fune/charts.sparkline@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { extent } from "./charts_extent.ts"; ← from charts.extent ^1.0.0 · built alongside by fune
import { linearScale } from "./charts_scale_linear_scale.ts";
import { linePath } from "./charts_shape_line_path.ts";
import type { Point } from "./charts_shape_types.ts";
import type { Sparkline } from "./charts_sparkline_types.ts";
import { roundFloat } from "./math_round_float.ts"; ← from math.round-float ^1.0.0 · built alongside by fune
function checkSize(value: number, what: string): void {
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
throw new RangeError(`${what} must be a finite number of zero or more, received ${value}`);
}
}
/**
* A sparkline in a box: values evenly spaced from left to right, the lowest
* at the bottom and the highest at the top of the padded box.
*
* A flat series is drawn along the middle rather than divided by a zero
* range; a single value sits at the right-hand end, where the latest value of
* a sparkline belongs.
*/
export function sparkline(values: readonly (number | null)[], width: number, height: number, padding: number): Sparkline {
checkSize(width, "width");
checkSize(height, "height");
checkSize(padding, "padding");
if (width - 2 * padding <= 0 || height - 2 * padding <= 0) {
throw new RangeError(`the box is too small for its padding: ${width} by ${height} with ${padding} on each side`);
}
const range = extent(values);
if (range === null) return { path: "", end: null };
const [lo, hi] = range;
const n = values.length;
const points: Point[] = values.map((v, i) => {
const x = n === 1 ? width - padding : linearScale([0, n - 1], [padding, width - padding], i, false);
if (v === null) return { x, y: null };
const y = lo === hi ? height / 2 : linearScale([lo, hi], [height - padding, padding], v, false);
return { x, y };
});
let end = null;
for (const p of points) {
if (p.y !== null) end = { x: roundFloat(p.x, 2), y: roundFloat(p.y, 2) };
}
return { path: linePath(points), end };
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 4 dependencies, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add charts.sparkline
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./charts.sparkline-1.0.0-typescript.fune, or fetch it from a terminal with fune pull charts.sparkline@1.0.0:typescript.
The whole function, every language, is one file too: charts.sparkline-1.0.0.fune, 12,433 bytes, sha256 8d3f1c05fa48b0bcffcde841259a9c6370d3abad10274cab976f6807996959ca. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before charts.sparkline
after — your function gets the result and the arguments, and returns the final result.
// fune: after charts.sparkline
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace charts.extent in charts.sparkline
// fune: replace charts.scale in charts.sparkline
// fune: replace charts.shape in charts.sparkline
// fune: replace math.round-float in charts.sparkline
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show charts.sparkline --steps.
// fune: step charts.sparkline after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| three values across a padded box | 1, 3, 2, 100, 20, 2 | → | path M2,18L50,2L98,10, end … |
| a gap breaks the line; a lone point before it is closed into a dot | 5, —, 7, 6, 64, 24, 2 | → | path M2,22ZM42,2L62,12, end … |
| a trailing gap: the end marker is the last real value, not the last slot | 1, 2, —, 20, 10, 0 | → | path M0,10L10,0, end … |
| a flat series runs along the middle instead of dividing by zero | 4, 4, 4, 30, 10, 0 | → | path M0,5L15,5L30,5, end … |
| a single value sits at the right-hand end, in the middle | 9, 40, 10, 1 | → | path M39,5Z, end … |
| thirds are rounded to 2 places in the path | 0, 1, 2, 3, 10, 10, 0 | → | path M0,10L3.33,6.67L6.67,3.33L10,0, end … |
| negative values: the lowest is at the bottom | -2, 2, 10, 10, 0 | → | path M0,10L10,0, end … |
| every value a gap: nothing to draw | —, —, 50, 10, 1 | → | path , end — |
| an empty series: nothing to draw | , 50, 10, 1 | → | path , end — |
| padding that leaves no room is an error | 1, 2, 10, 10, 5 | → | error: the box is too small for its padding |
Show the other 1 test
| Case | Arguments | Expected | |
|---|---|---|---|
| a negative width is an error | 1, 2, -10, 10, 0 | → | error: width must be a finite number of zero or more |
More from the author
`padding` keeps the stroke and the dot from being clipped at the box's edge; about half the dot's diameter is usual. A box with no room left inside its padding is an error.
Files
| Path | Bytes |
|---|---|
| README.md | 1,460 |
| impl/python.py | 1,791 |
| impl/rust.rs | 2,557 |
| impl/typescript.ts | 1,930 |
| vectors.json | 1,737 |