charts.stack
Stack several series for stacked bars and areas: on zero, normalised to 100%, or diverging around zero.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 14 tests, run in TypeScript, Python and Rust.
What it does
Stacks several series so they can be drawn as stacked bars or stacked areas. `series[i][j]` is series `i` at position `j` (a category or an x value); the result has the same shape, and each entry is that value's band `[y0, y1]` in data units. Map both edges through a scale and hand them to `charts.shape`'s `areaPath` (as `y0`/`y1`) or `barRects` (as `base`/`value`).
Series are stacked in the order given, first at the bottom. The offsets are d3-shape's:
For example
stack(series ×3, zero)→ ×3 zero: each series sits on the one before itstack(series ×3, zero)→ ×3 zero: a negative value pulls its band below the running top, as d3's stackOffsetNonestack(series ×3, zero)→ ×3 zero: binary noise is rounded away (0.1 + 0.2 is 0.30000000000000004)
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.
def stack(series: Sequence[Sequence[float]], offset: StackOffset) -> List[List[StackedValue]]
| series | float[][] | series[i][j] is series i at position j; every series the same length |
| offset | StackOffset | zero stacks in order, expand scales each position to a total of 1, diverging stacks positives up and negatives down |
| returns | StackedValue[][] | the same shape as series: each value's band [y0, y1], rounded to 6 places |
The types it declares, generated into your project
StackOffset = Literal["zero", "expand", "diverging"]
@dataclass(frozen=True)
class StackedValue:
"""One value's band in the stack, in data units: y0 is the lower edge, y1 the upper."""
y0: float
y1: float
Your code names it in one line, in the file that uses it
from fune.charts.stack import stack # charts.stack@^1
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import math
from typing import List, Sequence
from .charts_stack_types import StackedValue, StackOffset
from .math_round_float import round_float ← from math.round-float ^1.0.0 · built alongside by fune
def stack(series: Sequence[Sequence[float]], offset: StackOffset) -> List[List[StackedValue]]:
"""Stack series position by position, as d3.stack does with the series in
the given order: stackOffsetNone (zero), stackOffsetExpand and
stackOffsetDiverging. Running totals stay unrounded; only the returned
edges are rounded."""
if offset not in ("zero", "expand", "diverging"):
raise ValueError("stack offset must be zero, expand or diverging, received %r" % (offset,))
n = len(series)
if n == 0:
return []
m = len(series[0])
for s in series:
if len(s) != m:
raise ValueError("every series must have the same number of values")
for v in s:
if isinstance(v, bool) or not isinstance(v, (int, float)) or not math.isfinite(v):
raise TypeError("stack values must be finite numbers, received %r" % (v,))
if offset == "expand" and v < 0:
raise ValueError("expand needs values of zero or more, received %r" % (v,))
lower = [[0.0] * m for _ in range(n)]
upper = [[0.0] * m for _ in range(n)]
for j in range(m):
if offset == "diverging":
up = 0.0
down = 0.0
for i in range(n):
v = float(series[i][j])
if v > 0:
lower[i][j] = up
up += v
upper[i][j] = up
elif v < 0:
upper[i][j] = down
down += v
lower[i][j] = down
else:
lower[i][j] = 0.0
upper[i][j] = 0.0
else:
total = 0.0
if offset == "expand":
for i in range(n):
total += float(series[i][j])
top = 0.0
for i in range(n):
v = float(series[i][j])
if offset == "expand":
v = v / total if total > 0 else 0.0
lower[i][j] = top
top += v
upper[i][j] = top
return [
[StackedValue(y0=round_float(lower[i][j], 6), y1=round_float(upper[i][j], 6)) for j in range(m)]
for i in range(n)
]Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Python 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.stack
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./charts.stack-1.0.0-python.fune, or fetch it from a terminal with fune pull charts.stack@1.0.0:python.
The whole function, every language, is one file too: charts.stack-1.0.0.fune, 15,255 bytes, sha256 ee6034afe10372edb9e49cb658d077cb040170c09a161bae7b4b58c9a7a1a5d0. 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.stack
after — your function gets the result and the arguments, and returns the final result.
# fune: after charts.stack
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 math.round-float in charts.stack
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.stack --steps.
# fune: step charts.stack 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 | |
|---|---|---|---|
| zero: each series sits on the one before it | series ×3, zero | → | ×3 |
| zero: a negative value pulls its band below the running top, as d3's stackOffsetNone | series ×3, zero | → | ×3 |
| zero: binary noise is rounded away (0.1 + 0.2 is 0.30000000000000004) | series ×3, zero | → | ×3 |
| expand: each position scaled to a total of 1 | series ×3, expand | → | ×3 |
| expand: thirds round to 6 places and the top is exactly 1 | series ×3, expand | → | ×3 |
| expand: a position whose values are all zero stays at zero | series ×2, expand | → | ×2 |
| diverging: positives stack up from zero, negatives down from zero | series ×3, diverging | → | ×3 |
| diverging: a zero value is an empty band at zero | series ×3, diverging | → | ×3 |
| a single series | series ×1, zero | → | ×1 |
| no series | , zero | → |
Show the other 4 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| series with no positions | series ×2, diverging | → | ×2 |
| series of different lengths are an error | series ×2, zero | → | error: every series must have the same number of values |
| expand with a negative value is an error | series ×2, expand | → | error: expand needs values of zero or more |
| an unknown offset is an error | series ×1, silhouette | → | error: stack offset must be zero, expand or diverging |
More from the author
- `zero` (d3.stackOffsetNone): each band starts where the one below ended. A negative value makes a band whose top is below its bottom, overlapping the one beneath, which is what d3 does and is fine for areas of mostly positive data. For bars with negatives use `diverging`. - `expand` (d3.stackOffsetExpand): each value is first divided by its position's total, then stacked, so every position runs from 0 to 1, a 100% chart. Multiply by 100 for percentages (or format with `charts.format`'s `formatPercent`). A position whose values are all zero stays at 0. Negative values have no meaning as shares of a total and are an error (d3 produces nonsense for them). - `diverging` (d3.stackOffsetDiverging): positive values stack upwards from zero and negative values downwards from zero, each in series order; a zero value is an empty band at 0. This is the right one for bars that go both ways, such as profit and loss.
Running totals are kept at full precision and only the returned edges are rounded, to 6 decimal places by `math.round-float`. So `0.1 + 0.2` stacks to `0.3`, not `0.30000000000000004`, and an `expand` stack ends at exactly 1.
Every series must have the same length; there are no gaps (pass 0 for a missing value, which is what a stack means by it). d3's stackOrder options are not offered: reorder the series before calling.
Source: d3-shape, "Stacks" (github.com/d3/d3-shape#stacks): stack, stackOffsetNone, stackOffsetExpand, stackOffsetDiverging.
Files
| Path | Bytes |
|---|---|
| README.md | 1,968 |
| impl/python.py | 2,395 |
| impl/rust.rs | 3,212 |
| impl/typescript.ts | 2,364 |
| vectors.json | 2,566 |