Functional Weave
Code in Rust

charts.axis@1.0.1

impl/rust/axis_from_ticks.rs

5,577 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::math_round_float::round_float;  ← from math.round-float ^1.0.0 · built alongside by fune

// d3-axis's default gap between the end of a tick mark and its label.
const LABEL_PADDING: f64 = 3.0;

/// A pixel coordinate rounded to 2 places, the precision every axis output uses.
pub fn axis_pixel(value: f64) -> f64 {
    round_float(value, 2)
}

pub fn axis_line(x1: f64, y1: f64, x2: f64, y2: f64) -> Line {
    Line { x1: axis_pixel(x1), y1: axis_pixel(y1), x2: axis_pixel(x2), y2: axis_pixel(y2) }
}

pub fn check_orient(orient: &str) {
    if !matches!(orient, "bottom" | "top" | "left" | "right") {
        panic!("orient must be bottom, top, left or right, received \"{}\"", orient);
    }
}

/// Axis geometry from tick positions and labels, for any scale (d3-axis layout).
///
/// # Panics
/// Panics on an unknown orient, positions and labels of different lengths, or
/// a range without exactly two values.
pub fn axis_from_ticks(positions: &[f64], labels: &[String], range: &[f64], orient: &str, position: f64, tick_size: f64) -> Axis {
    check_orient(orient);
    if positions.len() != labels.len() {
        panic!(
            "positions and labels must be the same length, received {} and {}",
            positions.len(),
            labels.len()
        );
    }
    if range.len() != 2 {
        panic!("range must have exactly 2 values, [from, to]; got {}", range.len());
    }
    let horizontal = orient == "bottom" || orient == "top";
    let sign = if orient == "bottom" || orient == "right" { 1.0 } else { -1.0 };
    let mark_end = position + sign * tick_size;
    let label_at = position + sign * (tick_size + LABEL_PADDING);
    let ticks = positions
        .iter()
        .zip(labels)
        .map(|(&p, label)| {
            if horizontal {
                AxisTick {
                    position: axis_pixel(p),
                    label: label.clone(),
                    tick: axis_line(p, position, p, mark_end),
                    label_x: axis_pixel(p),
                    label_y: axis_pixel(label_at),
                    anchor: "middle".to_string(),
                    baseline: if orient == "bottom" { "hanging" } else { "alphabetic" }.to_string(),
                }
            } else {
                AxisTick {
                    position: axis_pixel(p),
                    label: label.clone(),
                    tick: axis_line(position, p, mark_end, p),
                    label_x: axis_pixel(label_at),
                    label_y: axis_pixel(p),
                    anchor: if orient == "left" { "end" } else { "start" }.to_string(),
                    baseline: "middle".to_string(),
                }
            }
        })
        .collect();
    let line = if horizontal {
        axis_line(range[0], position, range[1], position)
    } else {
        axis_line(position, range[0], position, range[1])
    };
    Axis { orient: orient.to_string(), position: axis_pixel(position), line, ticks }
}

pub fn line_to_value(l: &Line) -> Value {
    Value::obj(vec![
        ("x1", Value::Float(l.x1)),
        ("y1", Value::Float(l.y1)),
        ("x2", Value::Float(l.x2)),
        ("y2", Value::Float(l.y2)),
    ])
}

pub fn line_from_value(v: &Value) -> Line {
    Line { x1: v.get("x1").as_f64(), y1: v.get("y1").as_f64(), x2: v.get("x2").as_f64(), y2: v.get("y2").as_f64() }
}

pub fn axis_to_value(a: &Axis) -> Value {
    Value::obj(vec![
        ("orient", Value::str(&a.orient)),
        ("position", Value::Float(a.position)),
        ("line", line_to_value(&a.line)),
        (
            "ticks",
            Value::Arr(
                a.ticks
                    .iter()
                    .map(|t| {
                        Value::obj(vec![
                            ("position", Value::Float(t.position)),
                            ("label", Value::str(&t.label)),
                            ("tick", line_to_value(&t.tick)),
                            ("labelX", Value::Float(t.label_x)),
                            ("labelY", Value::Float(t.label_y)),
                            ("anchor", Value::str(&t.anchor)),
                            ("baseline", Value::str(&t.baseline)),
                        ])
                    })
                    .collect(),
            ),
        ),
    ])
}

pub fn axis_from_value(v: &Value) -> Axis {
    Axis {
        orient: v.get("orient").as_str().to_string(),
        position: v.get("position").as_f64(),
        line: line_from_value(v.get("line")),
        ticks: v
            .get("ticks")
            .as_arr()
            .iter()
            .map(|t| AxisTick {
                position: t.get("position").as_f64(),
                label: t.get("label").as_str().to_string(),
                tick: line_from_value(t.get("tick")),
                label_x: t.get("labelX").as_f64(),
                label_y: t.get("labelY").as_f64(),
                anchor: t.get("anchor").as_str().to_string(),
                baseline: t.get("baseline").as_str().to_string(),
            })
            .collect(),
    }
}

/// A JSON list of numbers or strings, for the vector adapters.
pub fn axis_floats(v: &Value) -> Vec<f64> {
    v.as_arr().iter().map(|x| x.as_f64()).collect()
}

pub fn axis_strings(v: &Value) -> Vec<String> {
    v.as_arr().iter().map(|x| x.as_str().to_string()).collect()
}

pub fn fune_vector(args: &[Value]) -> Value {
    axis_to_value(&axis_from_ticks(
        &axis_floats(&args[0]),
        &axis_strings(&args[1]),
        &axis_floats(&args[2]),
        args[3].as_str(),
        args[4].as_f64(),
        args[5].as_f64(),
    ))
}