Functional Weave
Code in Python

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 it
  • stack(series ×3, zero) → ×3 zero: a negative value pulls its band below the running top, as d3's stackOffsetNone
  • stack(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]]
seriesfloat[][]series[i][j] is series i at position j; every series the same length
offsetStackOffsetzero stacks in order, expand scales each position to a total of 1, diverging stacks positives up and negatives down
returnsStackedValue[][]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
impl/python.py · 62 lines · open · raw

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
Download for Python charts.stack-1.0.0-python.fune · 9,443 bytes sha256 7f621caee4901f28c3a01f92594ed3e0e1ca216b57fec53bf1f3005a9e311dc4

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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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

PathBytes
README.md1,968
impl/python.py2,395
impl/rust.rs3,212
impl/typescript.ts2,364
vectors.json2,566