Functional Weave
Code in Rust

charts.bar-chart@1.0.0

impl/rust.rs

8,488 bytes · the Rust implementation · view 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])))
}