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.
pub fn sparkline(values: &[Option<f64>], width: f64, height: f64, padding: f64) -> 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.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SparkPoint {
pub x: f64,
pub y: f64,
}
/// Everything needed to draw one sparkline.
#[derive(Debug, Clone, PartialEq)]
pub struct Sparkline {
/// SVG path data; "" when the series has no values
pub path: String,
/// the last value that is not a gap, for the end dot; null when there is none
pub end: Option<SparkPoint>,
}
Your code names it in one line, in the file that uses it
fune!(charts.sparkline@^1); // then call sparkline(…)
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::charts_extent::extent; ← from charts.extent ^1.0.0 · built alongside by fune
use super::charts_scale_linear_scale::linear_scale;
use super::charts_shape_line_path::line_path;
use super::charts_shape_types::Point;
use super::math_round_float::round_float; ← from math.round-float ^1.0.0 · built alongside by fune
fn check_size(value: f64, what: &str) {
if !value.is_finite() || value < 0.0 {
panic!("{} must be a finite number of zero or more, received {}", what, value);
}
}
/// A sparkline in a box; see the README for flat and single-value series.
///
/// # Panics
/// Panics on a negative or non-finite size, or a box too small for its padding.
pub fn sparkline(values: &[Option<f64>], width: f64, height: f64, padding: f64) -> Sparkline {
check_size(width, "width");
check_size(height, "height");
check_size(padding, "padding");
if width - 2.0 * padding <= 0.0 || height - 2.0 * padding <= 0.0 {
panic!(
"the box is too small for its padding: {} by {} with {} on each side",
width, height, padding
);
}
let range = match extent(values) {
None => return Sparkline { path: String::new(), end: None },
Some(r) => r,
};
let (lo, hi) = (range[0], range[1]);
let n = values.len();
let points: Vec<Point> = values
.iter()
.enumerate()
.map(|(i, v)| {
let x = if n == 1 {
width - padding
} else {
linear_scale(&[0.0, (n - 1) as f64], &[padding, width - padding], i as f64, false)
};
let y = v.map(|v| if lo == hi { height / 2.0 } else { linear_scale(&[lo, hi], &[height - padding, padding], v, false) });
Point { x, y }
})
.collect();
let end = points
.iter()
.filter_map(|p| p.y.map(|y| SparkPoint { x: round_float(p.x, 2), y: round_float(y, 2) }))
.last();
Sparkline { path: line_path(&points), end }
}
pub fn sparkline_to_value(s: &Sparkline) -> Value {
Value::obj(vec![
("path", Value::str(&s.path)),
(
"end",
match &s.end {
None => Value::Null,
Some(p) => Value::obj(vec![("x", Value::Float(p.x)), ("y", Value::Float(p.y))]),
},
),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let values: Vec<Option<f64>> = args[0]
.as_arr()
.iter()
.map(|v| if v.is_null() { None } else { Some(v.as_f64()) })
.collect();
sparkline_to_value(&sparkline(&values, args[1].as_f64(), args[2].as_f64(), args[3].as_f64()))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 4 dependencies, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:
fune add charts.sparkline
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./charts.sparkline-1.0.0-rust.fune, or fetch it from a terminal with fune pull charts.sparkline@1.0.0:rust.
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 |