Functional Weave
Code in Rust

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.

pub fn 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 is a string in Rust, one of: "grouped", "stacked".
// Parameters take it as &str and results hold it as String.

/// One series: a value per category.
#[derive(Debug, Clone, PartialEq)]
pub struct BarSeries {
    pub name: String,
    /// one per category, in the same order
    pub values: Vec<f64>,
}

/// What to draw.
#[derive(Debug, Clone, PartialEq)]
pub struct BarChartSpec {
    pub width: f64,
    pub height: f64,
    /// the x axis, left to right; no repeats
    pub categories: Vec<String>,
    /// no two with the same name
    pub series: Vec<BarSeries>,
    /// grouped side by side, or stacked (positives up, negatives down from zero)
    pub mode: String,
}

/// One bar, ready to draw.
#[derive(Debug, Clone, PartialEq)]
pub struct ChartBar {
    pub series: String,
    pub category: String,
    /// the data value, for a tooltip
    pub value: f64,
    /// "#rrggbb", the series' colour
    pub color: String,
    pub rect: Rect,
}

/// The whole chart; legend[i] and colors[i] are series i.
#[derive(Debug, Clone, PartialEq)]
pub struct BarChart {
    pub width: f64,
    pub height: f64,
    pub plot: PlotArea,
    pub x_axis: Axis,
    pub y_axis: Axis,
    /// horizontal, one per y tick
    pub grid_lines: Vec<Line>,
    /// series by series, each in category order
    pub bars: Vec<ChartBar>,
    /// one per series, from the Okabe-Ito palette
    pub colors: Vec<String>,
    /// empty for a single series
    pub legend: Vec<LegendItem>,
}

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

fune!(charts.bar-chart@^1);  // then call bar_chart(…)
impl/rust.rs · 217 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.

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_axis_axis_from_ticks::{axis_to_value, line_to_value};
use super::charts_axis_band_axis::band_axis;
use super::charts_axis_grid_lines::grid_lines;
use super::charts_axis_number_axis::number_axis;
use super::charts_format_format_tick::format_tick;
use super::charts_layout_legend_rows::{legend_item_to_value, legend_rows};
use super::charts_layout_plot_area::{plot_area, plot_area_to_value};
use super::charts_layout_types::{LegendItem, Margins};
use super::charts_palette::palette;  ← from charts.palette ^1.0.0 · built alongside by fune
use super::charts_scale_band_scale::band_scale;
use super::charts_scale_linear_scale::linear_scale;
use super::charts_scale_nice_domain::nice_domain;
use super::charts_shape_bar_rects::{bar_rects, rect_to_value};
use super::charts_shape_types::BarSpec;
use super::charts_stack::stack;  ← from charts.stack ^1.0.0 · built alongside by fune
use super::charts_ticks_nice_ticks::nice_ticks;
use super::charts_ticks_tick_step::tick_step;
use super::math_round_float::round_float;  ← from math.round-float ^1.0.0 · built alongside by fune

// Fixed layout constants; the README explains each.
const CHAR_WIDTH: f64 = 7.0;
const SWATCH: f64 = 10.0;
const LEGEND_GAP: f64 = 16.0;
const LEGEND_ROW: f64 = 18.0;
const EDGE: f64 = 8.0;
const TICK_SIZE: f64 = 6.0;
const LABEL_PADDING: f64 = 3.0;
const TOP_WITHOUT_LEGEND: f64 = 12.0;
const RIGHT: f64 = 20.0;
const BOTTOM: f64 = 30.0;
const CATEGORY_PADDING_INNER: f64 = 0.2;
const CATEGORY_PADDING_OUTER: f64 = 0.1;
const SERIES_PADDING: f64 = 0.05;

/// A whole bar chart as geometry, grouped or stacked, by the fixed rules in
/// the README.
///
/// # Panics
/// Panics on an unknown mode, no categories or series, a series of the wrong
/// length, repeated categories or series names, or a chart too small for its
/// margins.
pub fn bar_chart(spec: &BarChartSpec) -> BarChart {
    let (width, height, categories, series) = (spec.width, spec.height, &spec.categories, &spec.series);
    if spec.mode != "grouped" && spec.mode != "stacked" {
        panic!("mode must be grouped or stacked, received {}", spec.mode);
    }
    if categories.is_empty() {
        panic!("a bar chart needs at least one category");
    }
    if series.is_empty() {
        panic!("a bar chart needs at least one series");
    }
    for s in series {
        if s.values.len() != categories.len() {
            panic!(
                "series \"{}\" has {} values but there are {} categories",
                s.name,
                s.values.len(),
                categories.len()
            );
        }
    }
    let names: Vec<String> = series.iter().map(|s| s.name.clone()).collect();

    let mut legend: Vec<LegendItem> = Vec::new();
    let mut top = TOP_WITHOUT_LEGEND;
    if series.len() > 1 {
        let rows = legend_rows(&names, width - 2.0 * EDGE, CHAR_WIDTH, SWATCH, LEGEND_GAP, LEGEND_ROW);
        legend = rows
            .iter()
            .map(|it| LegendItem {
                label: it.label.clone(),
                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),
            })
            .collect();
        top = EDGE + (rows[rows.len() - 1].row + 1) as f64 * LEGEND_ROW + EDGE;
    }

    let stacked = if spec.mode == "stacked" {
        let values: Vec<Vec<f64>> = series.iter().map(|s| s.values.clone()).collect();
        Some(stack(&values, "diverging"))
    } else {
        None
    };
    let (mut lo, mut hi) = (0.0f64, 0.0f64);
    for (i, s) in series.iter().enumerate() {
        for (j, v) in s.values.iter().enumerate() {
            let ends = match &stacked {
                None => vec![*v],
                Some(st) => vec![st[i][j].y0, st[i][j].y1],
            };
            for e in ends {
                if e < lo {
                    lo = e;
                }
                if e > hi {
                    hi = e;
                }
            }
        }
    }
    if lo == hi {
        hi = 1.0;
    }
    let y_count = (((height - top - BOTTOM) / 50.0).floor() as i64).max(2);
    let y_domain = nice_domain(&[lo, hi], y_count).domain;
    let y_step = tick_step(y_domain[0], y_domain[1], y_count);
    let widest = nice_ticks(y_domain[0], y_domain[1], y_count)
        .iter()
        .map(|v| format_tick(*v, y_step).chars().count())
        .max()
        .unwrap_or(0);
    let left = TICK_SIZE + LABEL_PADDING + widest as f64 * CHAR_WIDTH + EDGE;
    let plot = plot_area(width, height, &Margins { top, right: RIGHT, bottom: BOTTOM, left });
    let bottom_y = plot.y + plot.height;
    let y_range = vec![bottom_y, plot.y];
    let y = |v: f64| linear_scale(&y_domain, &y_range, v, false);
    let y_axis = number_axis(&y_domain, &y_range, y_count, "left", plot.x, TICK_SIZE);
    let x_range = vec![plot.x, plot.x + plot.width];
    let x_axis = band_axis(categories, &x_range, CATEGORY_PADDING_INNER, CATEGORY_PADDING_OUTER, "bottom", bottom_y, TICK_SIZE);

    let colors = palette("okabe-ito", series.len() as i64, true);
    let mut specs: Vec<BarSpec> = Vec::new();
    let mut meta: Vec<(String, String, f64, String)> = Vec::new();
    for (i, s) in series.iter().enumerate() {
        for (j, c) in categories.iter().enumerate() {
            let outer = band_scale(categories, &x_range, c, CATEGORY_PADDING_INNER, CATEGORY_PADDING_OUTER, 0.5);
            let bar = match &stacked {
                Some(st) => BarSpec { band: outer.start, thickness: outer.width, base: y(st[i][j].y0), value: y(st[i][j].y1) },
                None if series.len() == 1 => {
                    BarSpec { band: outer.start, thickness: outer.width, base: y(0.0), value: y(s.values[j]) }
                }
                None => {
                    let inner = band_scale(&names, &[outer.start, outer.start + outer.width], &s.name, SERIES_PADDING, 0.0, 0.5);
                    BarSpec { band: inner.start, thickness: inner.width, base: y(0.0), value: y(s.values[j]) }
                }
            };
            specs.push(bar);
            meta.push((s.name.clone(), c.clone(), s.values[j], colors[i].clone()));
        }
    }
    let rects = bar_rects(&specs, "vertical");
    let bars: Vec<ChartBar> = meta
        .into_iter()
        .zip(rects)
        .map(|((series, category, value, color), rect)| ChartBar { series, category, value, color, rect })
        .collect();

    BarChart {
        width: round_float(width, 2),
        height: round_float(height, 2),
        grid_lines: grid_lines(&y_axis, plot.width),
        plot,
        x_axis,
        y_axis,
        bars,
        colors,
        legend,
    }
}

pub fn bar_chart_spec_from_value(v: &Value) -> BarChartSpec {
    BarChartSpec {
        width: v.get("width").as_f64(),
        height: v.get("height").as_f64(),
        categories: v.get("categories").as_arr().iter().map(|c| c.as_str().to_string()).collect(),
        series: v
            .get("series")
            .as_arr()
            .iter()
            .map(|s| BarSeries {
                name: s.get("name").as_str().to_string(),
                values: s.get("values").as_arr().iter().map(|x| x.as_f64()).collect(),
            })
            .collect(),
        mode: v.get("mode").as_str().to_string(),
    }
}

pub fn bar_chart_to_value(c: &BarChart) -> Value {
    Value::obj(vec![
        ("width", Value::Float(c.width)),
        ("height", Value::Float(c.height)),
        ("plot", plot_area_to_value(&c.plot)),
        ("xAxis", axis_to_value(&c.x_axis)),
        ("yAxis", axis_to_value(&c.y_axis)),
        ("gridLines", Value::Arr(c.grid_lines.iter().map(line_to_value).collect())),
        (
            "bars",
            Value::Arr(
                c.bars
                    .iter()
                    .map(|b| {
                        Value::obj(vec![
                            ("series", Value::str(&b.series)),
                            ("category", Value::str(&b.category)),
                            ("value", Value::Float(b.value)),
                            ("color", Value::str(&b.color)),
                            ("rect", rect_to_value(&b.rect)),
                        ])
                    })
                    .collect(),
            ),
        ),
        ("colors", Value::Arr(c.colors.iter().map(|s| Value::str(s)).collect())),
        ("legend", Value::Arr(c.legend.iter().map(legend_item_to_value).collect())),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    bar_chart_to_value(&bar_chart(&bar_chart_spec_from_value(&args[0])))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 9 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.bar-chart
Download for Rust charts.bar-chart-1.0.0-rust.fune · 26,996 bytes sha256 595ea5d6d4b4c2d0b06a0548425742ba939a216fcf617ee46e8f13992f6ec7ef

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

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