Functional Weave
Code in Python

charts.scale@1.1.0

impl/rust/band_scale.rs

3,286 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_scale_linear_scale::{floats, pair};  ← linearScale, another function of this group · built into the same file, even by a slim install
use super::math_round_float::round_float;  ← from math.round-float ^1.0.0 · built alongside by fune

/// The band a category occupies, as d3.scaleBand lays it out (without
/// rounding to whole pixels). A reversed range reverses the order of the
/// bands, not their direction, so `start` is always the lower coordinate.
///
/// # Panics
/// Panics on an empty or repeating domain, a value not in it, a range without
/// two values, or padding or align out of bounds.
pub fn band_scale(domain: &[String], range: &[f64], value: &str, padding_inner: f64, padding_outer: f64, align: f64) -> Band {
    if !(padding_inner >= 0.0 && padding_inner <= 1.0) {
        panic!("paddingInner must be between 0 and 1; got {}", padding_inner);
    }
    check_outer(padding_outer, "paddingOuter");
    check_align(align);
    let (start, width) = band_position(domain, range, value, padding_inner, padding_outer, align);
    Band {
        start: round_float(start, 6),
        center: round_float(start + width / 2.0, 6),
        width: round_float(width, 6),
    }
}

// Exported for point_scale, which is a band scale whose bands have no width.

pub fn check_outer(padding: f64, what: &str) {
    if !(padding >= 0.0) {
        panic!("{} must be 0 or more; got {}", what, padding);
    }
}

pub fn check_align(align: f64) {
    if !(align >= 0.0 && align <= 1.0) {
        panic!("align must be between 0 and 1; got {}", align);
    }
}

/// The unrounded lower coordinate of value's band, and the band width.
pub fn band_position(domain: &[String], range: &[f64], value: &str, padding_inner: f64, padding_outer: f64, align: f64) -> (f64, f64) {
    let (r0, r1) = pair(range, "range");
    let n = domain.len();
    if n == 0 {
        panic!("domain must not be empty");
    }
    let mut index: Option<usize> = None;
    for i in 0..n {
        for j in 0..i {
            if domain[j] == domain[i] {
                panic!("domain has a repeated value: \"{}\"", domain[i]);
            }
        }
        if domain[i] == value {
            index = Some(i);
        }
    }
    let index = match index {
        Some(i) => i,
        None => panic!("value is not in the domain: \"{}\"", value),
    };
    let reverse = r1 < r0;
    let mut start = if reverse { r1 } else { r0 };
    let stop = if reverse { r0 } else { r1 };
    let nf = n as f64;
    let step = (stop - start) / (nf - padding_inner + padding_outer * 2.0).max(1.0);
    start += (stop - start - step * (nf - padding_inner)) * align;
    let slot = if reverse { n - 1 - index } else { index };
    (start + step * slot as f64, step * (1.0 - padding_inner))
}

pub fn band_to_value(band: &Band) -> Value {
    Value::obj(vec![
        ("start", Value::Float(band.start)),
        ("center", Value::Float(band.center)),
        ("width", Value::Float(band.width)),
    ])
}

/// A JSON list of strings, for the vector adapters.
pub fn band_strings(value: &Value) -> Vec<String> {
    value.as_arr().iter().map(|v| v.as_str().to_string()).collect()
}

pub fn fune_vector(args: &[Value]) -> Value {
    band_to_value(&band_scale(
        &band_strings(&args[0]),
        &floats(&args[1]),
        args[2].as_str(),
        args[3].as_f64(),
        args[4].as_f64(),
        args[5].as_f64(),
    ))
}