Skip to main content

sim_lib_view_math/
plot.rs

1//! Plotting lenses: 2D series and function plots as `scene/plot`.
2//!
3//! A series is a list of 2D points; a plot is axes plus one or more named
4//! series. Multiple series overlay for snapshot-and-compare.
5
6use sim_kernel::{Expr, Symbol};
7use sim_lib_scene::{data_map, node, sym};
8
9use crate::num::{number, point};
10
11/// The plotting lens id.
12pub const PLOT_LENS: &str = "view:math-plot";
13
14/// Build a series value from `(x, y)` points.
15pub fn series(name: &str, points: &[(f64, f64)]) -> Expr {
16    data_map(vec![
17        ("name", Expr::Symbol(Symbol::new(name))),
18        (
19            "points",
20            Expr::List(points.iter().map(|(x, y)| point(*x, *y)).collect()),
21        ),
22    ])
23}
24
25/// A `scene/plot` of a single named series.
26pub fn plot_view(name: &str, points: &[(f64, f64)]) -> Expr {
27    multi_plot_view(&[(name.to_owned(), points.to_vec())])
28}
29
30/// A `scene/plot` overlaying several named series (snapshot and compare).
31pub fn multi_plot_view(series_list: &[(String, Vec<(f64, f64)>)]) -> Expr {
32    let bounds = bounds_of(series_list);
33    let series = series_list
34        .iter()
35        .map(|(name, points)| {
36            // Use `style`, not `kind`: `kind` is the scene-node tag. The
37            // reserved-key guard in `data_map` enforces that.
38            data_map(vec![
39                ("name", Expr::Symbol(Symbol::new(name.as_str()))),
40                ("style", sym("line")),
41                (
42                    "points",
43                    Expr::List(points.iter().map(|(x, y)| point(*x, *y)).collect()),
44                ),
45            ])
46        })
47        .collect();
48    node(
49        "plot",
50        vec![
51            (
52                "axes",
53                data_map(vec![
54                    ("x", axis(bounds.0, bounds.1)),
55                    ("y", axis(bounds.2, bounds.3)),
56                ]),
57            ),
58            ("series", Expr::List(series)),
59        ],
60    )
61}
62
63/// A named response plot for component-specific lenses.
64pub fn response_plot_view(
65    lens: &str,
66    role: &str,
67    series_list: &[(String, Vec<(f64, f64)>)],
68) -> Expr {
69    let mut plot = multi_plot_view(series_list);
70    if let Expr::Map(entries) = &mut plot {
71        entries.push((sym("lens"), Expr::Symbol(Symbol::new(lens))));
72        entries.push((sym("role"), sym(role)));
73    }
74    plot
75}
76
77fn axis(min: f64, max: f64) -> Expr {
78    data_map(vec![("min", number(min)), ("max", number(max))])
79}
80
81fn bounds_of(series_list: &[(String, Vec<(f64, f64)>)]) -> (f64, f64, f64, f64) {
82    let mut bounds: Option<(f64, f64, f64, f64)> = None;
83    for (_, points) in series_list {
84        for (x, y) in points {
85            bounds = Some(match bounds {
86                Some((min_x, max_x, min_y, max_y)) => {
87                    (min_x.min(*x), max_x.max(*x), min_y.min(*y), max_y.max(*y))
88                }
89                None => (*x, *x, *y, *y),
90            });
91        }
92    }
93    bounds.unwrap_or((0.0, 1.0, 0.0, 1.0))
94}