Functional Weave
Code in TypeScript

charts.shape@1.0.1

impl/rust/monotone_curve.rs

2,572 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_shape_line_path::{point_runs, points_from_value, svg_pair};  ← linePath, another function of this group · built into the same file, even by a slim install

fn sign(x: f64) -> f64 {
    if x < 0.0 {
        -1.0
    } else {
        1.0
    }
}

// Fritsch–Carlson: the tangent at the middle point, limited so the curve
// cannot overshoot either neighbouring value. d3's slope3.
fn slope3(x0: f64, y0: f64, x1: f64, y1: f64, x2: f64, y2: f64) -> f64 {
    let h0 = x1 - x0;
    let h1 = x2 - x1;
    let s0 = (y1 - y0) / h0;
    let s1 = (y2 - y1) / h1;
    let p = (s0 * h1 + s1 * h0) / (h0 + h1);
    let t = (sign(s0) + sign(s1)) * s0.abs().min(s1.abs()).min(0.5 * p.abs());
    if t == 0.0 {
        0.0
    } else {
        t
    }
}

fn slope2(x0: f64, y0: f64, x1: f64, y1: f64, t: f64) -> f64 {
    (3.0 * (y1 - y0) / (x1 - x0) - t) / 2.0
}

fn bezier(x0: f64, y0: f64, x1: f64, y1: f64, t0: f64, t1: f64) -> String {
    let dx = (x1 - x0) / 3.0;
    format!("C{},{},{}", svg_pair(x0 + dx, y0 + dx * t0), svg_pair(x1 - dx, y1 - dx * t1), svg_pair(x1, y1))
}

/// A smooth line that stays monotone wherever the data is: d3.curveMonotoneX,
/// Hermite segments with Fritsch–Carlson tangents, as cubic Béziers.
///
/// # Panics
/// Panics if x does not strictly increase within an unbroken run.
pub fn monotone_curve(points: &[Point]) -> String {
    let mut out = String::new();
    for run in point_runs(points) {
        for i in 1..run.len() {
            if !(run[i].0 > run[i - 1].0) {
                panic!("monotone curve needs x strictly increasing, but {} follows {}", run[i].0, run[i - 1].0);
            }
        }
        out.push('M');
        out.push_str(&svg_pair(run[0].0, run[0].1));
        let n = run.len();
        if n == 1 {
            out.push('Z');
            continue;
        }
        if n == 2 {
            out.push('L');
            out.push_str(&svg_pair(run[1].0, run[1].1));
            continue;
        }
        let mut t0 = 0.0;
        for i in 2..n {
            let (ax, ay) = run[i - 2];
            let (bx, by) = run[i - 1];
            let (cx, cy) = run[i];
            let t1 = slope3(ax, ay, bx, by, cx, cy);
            let start = if i == 2 { slope2(ax, ay, bx, by, t1) } else { t0 };
            out.push_str(&bezier(ax, ay, bx, by, start, t1));
            t0 = t1;
        }
        let (ax, ay) = run[n - 2];
        let (bx, by) = run[n - 1];
        out.push_str(&bezier(ax, ay, bx, by, t0, slope2(ax, ay, bx, by, t0)));
    }
    out
}

pub fn fune_vector(args: &[Value]) -> Value {
    Value::str(&monotone_curve(&points_from_value(&args[0])))
}