Functional Weave
Code in Python

charts.bar-chart

Complete bar chart geometry as data, grouped or stacked: plot area, axes, gridlines, bar rectangles, colours, legend.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 11 tests, run in TypeScript, Python and Rust.

What it does

A complete vertical bar chart as data, grouped or stacked: the plot area, a category x axis, a number y axis, gridlines, one rectangle per bar with its series, category, value and colour, and a legend. Nothing is drawn; any renderer turns it into pixels, and every language produces the same numbers.

It is assembled from `charts.scale` (band and linear scales), `charts.ticks`, `charts.format`, `charts.stack`, `charts.shape` (bar rectangles), `charts.axis`, `charts.layout` and `charts.palette`.

For example

  • bar_chart(width 300, height 200, categories Q1, Q2, series ×2, mode grouped) → width 300, height 200, plot …, x axis …, y axis …, grid lines ×3, bars ×4, colors #e69f00, #56b4e9, legend ×2 grouped: two series side by side in each category
  • bar_chart(width 300, height 200, categories x, y, series ×2, mode stacked) → width 300, height 200, plot …, x axis …, y axis …, grid lines ×4, bars ×4, colors #e69f00, #56b4e9, legend ×2 stacked: positives stack up from zero and a negative hangs below it
  • bar_chart(width 200, height 150, categories a, b, c, series ×1, mode grouped) → width 200, height 150, plot …, x axis …, y axis …, grid lines ×3, bars ×3, colors #e69f00, legend one series: full-width bands, no legend; widths are differences of rounded edges (37.87, 37.86)

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 bar_chart(spec: BarChartSpec) -> BarChart
specBarChartSpec
returnsBarChartevery pixel value rounded to 2 places; draw it with any renderer

The types it declares, generated into your project

BarMode = Literal["grouped", "stacked"]

@dataclass(frozen=True)
class BarSeries:
    """One series: a value per category."""

    name: str
    #: one per category, in the same order
    values: List[float]

@dataclass(frozen=True)
class BarChartSpec:
    """What to draw."""

    width: float
    height: float
    #: the x axis, left to right; no repeats
    categories: List[str]
    #: no two with the same name
    series: List[BarSeries]
    #: grouped side by side, or stacked (positives up, negatives down from zero)
    mode: BarMode

@dataclass(frozen=True)
class ChartBar:
    """One bar, ready to draw."""

    series: str
    category: str
    #: the data value, for a tooltip
    value: float
    #: "#rrggbb", the series' colour
    color: str
    rect: Rect

@dataclass(frozen=True)
class BarChart:
    """The whole chart; legend[i] and colors[i] are series i."""

    width: float
    height: float
    plot: PlotArea
    x_axis: Axis
    y_axis: Axis
    #: horizontal, one per y tick
    grid_lines: List[Line]
    #: series by series, each in category order
    bars: List[ChartBar]
    #: one per series, from the Okabe-Ito palette
    colors: List[str]
    #: empty for a single series
    legend: List[LegendItem]

Your code names it in one line, in the file that uses it

from fune.charts.bar_chart import bar_chart  # charts.bar-chart@^1
impl/python.py · 132 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.

from .charts_axis_band_axis import band_axis
from .charts_axis_grid_lines import grid_lines
from .charts_axis_number_axis import number_axis
from .charts_bar_chart_types import BarChart, BarChartSpec, ChartBar
from .charts_format_format_tick import format_tick
from .charts_layout_legend_rows import legend_rows
from .charts_layout_plot_area import plot_area
from .charts_layout_types import LegendItem, Margins
from .charts_palette import palette  ← from charts.palette ^1.0.0 · built alongside by fune
from .charts_scale_band_scale import band_scale
from .charts_scale_linear_scale import linear_scale
from .charts_scale_nice_domain import nice_domain
from .charts_shape_bar_rects import bar_rects
from .charts_shape_types import BarSpec
from .charts_stack import stack  ← from charts.stack ^1.0.0 · built alongside by fune
from .charts_ticks_nice_ticks import nice_ticks
from .charts_ticks_tick_step import tick_step
from .math_round_float import round_float  ← from math.round-float ^1.0.0 · built alongside by fune

# Fixed layout constants; the README explains each.
CHAR_WIDTH = 7
SWATCH = 10
LEGEND_GAP = 16
LEGEND_ROW = 18
EDGE = 8
TICK_SIZE = 6
LABEL_PADDING = 3
TOP_WITHOUT_LEGEND = 12
RIGHT = 20
BOTTOM = 30
CATEGORY_PADDING_INNER = 0.2
CATEGORY_PADDING_OUTER = 0.1
SERIES_PADDING = 0.05


def bar_chart(spec: BarChartSpec) -> BarChart:
    """A whole bar chart as geometry, by the fixed rules in the README."""
    width, height, categories, series = spec.width, spec.height, spec.categories, spec.series
    if spec.mode not in ("grouped", "stacked"):
        raise ValueError("mode must be grouped or stacked, received %s" % (spec.mode,))
    if len(categories) == 0:
        raise ValueError("a bar chart needs at least one category")
    if len(series) == 0:
        raise ValueError("a bar chart needs at least one series")
    for s in series:
        if len(s.values) != len(categories):
            raise ValueError(
                'series "%s" has %d values but there are %d categories' % (s.name, len(s.values), len(categories))
            )
    names = [s.name for s in series]

    legend = []
    top = TOP_WITHOUT_LEGEND
    if len(series) > 1:
        rows = legend_rows(names, width - 2 * EDGE, CHAR_WIDTH, SWATCH, LEGEND_GAP, LEGEND_ROW)
        legend = [
            LegendItem(
                label=it.label,
                row=it.row,
                x=round_float(it.x + EDGE, 2),
                y=round_float(it.y + EDGE, 2),
                width=it.width,
                text_x=round_float(it.text_x + EDGE, 2),
            )
            for it in rows
        ]
        top = EDGE + (rows[-1].row + 1) * LEGEND_ROW + EDGE

    stacked = stack([list(s.values) for s in series], "diverging") if spec.mode == "stacked" else None
    lo = 0.0
    hi = 0.0
    for i, s in enumerate(series):
        for j, v in enumerate(s.values):
            ends = [v] if stacked is None else [stacked[i][j].y0, stacked[i][j].y1]
            for e in ends:
                if e < lo:
                    lo = e
                if e > hi:
                    hi = e
    if lo == hi:
        hi = 1.0
    y_count = max(2, int((height - top - BOTTOM) // 50))
    y_domain = nice_domain([lo, hi], y_count).domain
    y_step = tick_step(y_domain[0], y_domain[1], y_count)
    widest = 0
    for v in nice_ticks(y_domain[0], y_domain[1], y_count):
        widest = max(widest, len(format_tick(v, y_step)))
    left = TICK_SIZE + LABEL_PADDING + widest * CHAR_WIDTH + EDGE
    plot = plot_area(width, height, Margins(top=top, right=RIGHT, bottom=BOTTOM, left=left))
    bottom_y = plot.y + plot.height
    y_range = [bottom_y, plot.y]

    def y(v: float) -> float:
        return linear_scale(y_domain, y_range, v, False)

    y_axis = number_axis(y_domain, y_range, y_count, "left", plot.x, TICK_SIZE)
    x_range = [plot.x, plot.x + plot.width]
    x_axis = band_axis(
        categories, x_range, CATEGORY_PADDING_INNER, CATEGORY_PADDING_OUTER, "bottom", bottom_y, TICK_SIZE
    )

    colors = palette("okabe-ito", len(series), True)
    specs = []
    meta = []
    for i, s in enumerate(series):
        for j, c in enumerate(categories):
            outer = band_scale(categories, x_range, c, CATEGORY_PADDING_INNER, CATEGORY_PADDING_OUTER, 0.5)
            if stacked is not None:
                specs.append(
                    BarSpec(band=outer.start, thickness=outer.width, base=y(stacked[i][j].y0), value=y(stacked[i][j].y1))
                )
            elif len(series) == 1:
                specs.append(BarSpec(band=outer.start, thickness=outer.width, base=y(0), value=y(s.values[j])))
            else:
                inner = band_scale(names, [outer.start, outer.start + outer.width], s.name, SERIES_PADDING, 0, 0.5)
                specs.append(BarSpec(band=inner.start, thickness=inner.width, base=y(0), value=y(s.values[j])))
            meta.append((s.name, c, s.values[j], colors[i]))
    rects = bar_rects(specs, "vertical")
    bars = [
        ChartBar(series=m[0], category=m[1], value=m[2], color=m[3], rect=rects[k]) for k, m in enumerate(meta)
    ]
    return BarChart(
        width=round_float(width, 2),
        height=round_float(height, 2),
        plot=plot,
        x_axis=x_axis,
        y_axis=y_axis,
        grid_lines=grid_lines(y_axis, plot.width),
        bars=bars,
        colors=list(colors),
        legend=legend,
    )

Install

fune build

With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 9 dependencies, 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.bar-chart
Download for Python charts.bar-chart-1.0.0-python.fune · 23,615 bytes sha256 e7fc98243b38ab51c88375357b0449a05fa530b038a06258772784122daa282b

The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./charts.bar-chart-1.0.0-python.fune, or fetch it from a terminal with fune pull charts.bar-chart@1.0.0:python.

The whole function, every language, is one file too: charts.bar-chart-1.0.0.fune, 38,104 bytes, sha256 f2f19f88e0020d7eca1278bfe4cc959f8ef94dad942d2107c08b3fe086f020f3. 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.bar-chart

after — your function gets the result and the arguments, and returns the final result.

# fune: after charts.bar-chart

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.axis in charts.bar-chart
# fune: replace charts.format in charts.bar-chart
# fune: replace charts.layout in charts.bar-chart
# fune: replace charts.palette in charts.bar-chart
# fune: replace charts.scale in charts.bar-chart
# fune: replace charts.shape in charts.bar-chart
# fune: replace charts.stack in charts.bar-chart
# fune: replace charts.ticks in charts.bar-chart
# fune: replace math.round-float in charts.bar-chart

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.bar-chart --steps.

# fune: step charts.bar-chart 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
grouped: two series side by side in each category width 300, height 200, categories Q1, Q2, series ×2, mode grouped → width 300, height 200, plot …, x axis …, y axis …, grid lines ×3, bars ×4, colors #e69f00, #56b4e9, legend ×2
stacked: positives stack up from zero and a negative hangs below it width 300, height 200, categories x, y, series ×2, mode stacked → width 300, height 200, plot …, x axis …, y axis …, grid lines ×4, bars ×4, colors #e69f00, #56b4e9, legend ×2
one series: full-width bands, no legend; widths are differences of rounded edges (37.87, 37.86) width 200, height 150, categories a, b, c, series ×1, mode grouped → width 200, height 150, plot …, x axis …, y axis …, grid lines ×3, bars ×3, colors #e69f00, legend
all zero: the y axis still has a height, 0 to 1 width 200, height 150, categories a, series ×1, mode grouped → width 200, height 150, plot …, x axis …, y axis …, grid lines ×3, bars ×1, colors #e69f00, legend
no categories is an error width 300, height 200, categories , series ×1, mode grouped → error: a bar chart needs at least one category
no series is an error width 300, height 200, categories a, series , mode grouped → error: a bar chart needs at least one series
a series of the wrong length is an error width 300, height 200, categories a, b, series ×1, mode grouped → error: series "A" has 1 values but there are 2 categories
a repeated category is an error width 300, height 200, categories a, a, series ×1, mode grouped → error: domain has a repeated value
two series with one name is an error width 300, height 200, categories a, series ×2, mode grouped → error: domain has a repeated value
an unknown mode is an error width 300, height 200, categories a, series ×1, mode overlapped → error: mode must be grouped or stacked
Show the other 1 test
CaseArgumentsExpected
a chart too small for its margins is an error width 40, height 40, categories a, series ×1, mode grouped → error: margins leave no room to plot

More from the author

## Modes

- `grouped`: the series sit side by side in each category. Categories are bands of the x axis with 20% inner and 10% outer padding; inside a category each series gets a sub-band with 5% padding between them. A single series uses the whole category band. - `stacked`: the series are stacked in each category with a diverging stack (`charts.stack`): positive values stack upwards from zero, negative values downwards, so a negative value never hides inside a positive stack.

Bars always grow from zero, so zero is always on the y axis. The domain is widened to nice numbers with about one tick per 50 pixels; if every value is zero the axis runs from 0 to 1 so it still has a height.

## Rectangles

Bar edges are rounded to 2 decimal places first and the widths and heights are the differences of the rounded edges (`charts.shape` barRects). That is why neighbouring bars can be 37.87 and 37.86 wide: each edge is where it should be to the hundredth of a pixel, and bars that share an edge share it exactly, with no hairline gaps.

## Layout, colours, legend

The same fixed rules as `charts.line-chart`: 7 pixels per character, a legend only for more than one series (at the top, 18-pixel rows), margins top 12 or the legend's height plus 16, right 20, bottom 30, left sized to the widest y label; 6-pixel ticks; Okabe-Ito colours in series order, given once per series in `colors` and again on every bar. `bars` lists series by series, each in category order.

Category names and series names must each be unique. No categories, no series, a series of the wrong length or a chart too small for its margins is an error.

Files

PathBytes
README.md2,161
impl/python.py5,228
impl/rust.rs8,488
impl/typescript.ts5,497
vectors.json9,793