Skip to main content

theme_text_align_to/
theme_text_align_to.rs

1//! Compares `theme.plot_text_align_to`. Renders two side-by-side
2//! variants of the same plot — one with `AlignTo::Plot` (title spans
3//! the full plot interior, centered relative to legend + axes +
4//! panel), one with `AlignTo::Panel` (title spans only the panel
5//! column, centered above the data area regardless of side chrome).
6//!
7//! Each plot has a wide y-axis title + a right-side legend, so the
8//! difference between the two modes is visible: under `Plot` the
9//! title is offset right by half the left chrome; under `Panel` it
10//! sits flush above the panel.
11
12use hephaestus::backend::vello::VelloRenderer;
13use hephaestus::color::{rgb, rgb8, Color};
14use hephaestus::composition::{beside, Patch};
15use hephaestus::geometry::Size;
16use hephaestus::plot::chrome::axis::{Axis, AxisPlacement};
17use hephaestus::plot::chrome::legend::Legend;
18use hephaestus::plot::theme::{AlignTo, Element, HAlign, Theme};
19use hephaestus::plot::{scale, Plot, PlotComposition, PointGeom};
20use hephaestus::scales::chrome::{AxisSide, LegendSide};
21use hephaestus::scene::SceneBuilder;
22use hephaestus::Renderer;
23
24fn main() {
25    let (w, h) = (1400u32, 500u32);
26    let dpi = 96.0;
27
28    let comp = || beside(Patch::new("a"), Patch::new("b"));
29    let xs: Vec<f64> = (0..40).map(|i| i as f64 * 0.15).collect();
30    let ys: Vec<f64> = xs.iter().map(|x| (x * 0.7).sin() * 0.4 + 0.5).collect();
31    let categories: Vec<&str> = xs
32        .iter()
33        .map(|x| match (*x as usize) % 4 {
34            0 => "A",
35            1 => "B",
36            2 => "C",
37            _ => "D",
38        })
39        .collect();
40
41    let make_plot = |patch_id: &str, title: &str| {
42        let mut plot = Plot::new(&comp(), patch_id)
43            .bind("x", "x_scale")
44            .bind("y", "y_scale")
45            .bind("stroke", "category")
46            .title(title);
47        plot.add_geom(
48            PointGeom::builder()
49                .set("x", xs.clone())
50                .set("y", ys.clone())
51                .set("size", 6.0_f64)
52                .set("fill", rgb(0.20, 0.45, 0.85))
53                .set("stroke", categories.clone())
54                .set("linewidth", 1.0_f64)
55                .build(),
56        );
57        plot.add_axis(
58            Axis::rail("x_scale", AxisPlacement::Cartesian(AxisSide::Bottom)).title("Time (s)"),
59        );
60        plot.add_axis(
61            Axis::rail("y_scale", AxisPlacement::Cartesian(AxisSide::Left))
62                .title("A wide y-axis title"),
63        );
64        plot.add_legend(
65            Legend::new("category")
66                .side(LegendSide::Right)
67                .title("Group")
68                .key(
69                    hephaestus::plot::chrome::legend::LegendKeySpec::point()
70                        .scaled("stroke", "category"),
71                ),
72        );
73        plot
74    };
75
76    let category_scale = scale::discrete([
77        hephaestus::scales::Value::String(std::sync::Arc::from("A")),
78        hephaestus::scales::Value::String(std::sync::Arc::from("B")),
79        hephaestus::scales::Value::String(std::sync::Arc::from("C")),
80        hephaestus::scales::Value::String(std::sync::Arc::from("D")),
81    ])
82    .range_colors([
83        hephaestus::color::rgb(0.20, 0.20, 0.20),
84        hephaestus::color::rgb(0.70, 0.20, 0.20),
85        hephaestus::color::rgb(0.20, 0.60, 0.20),
86        hephaestus::color::rgb(0.20, 0.20, 0.70),
87    ]);
88
89    // Left-align the title so its left edge anchors visibly differ
90    // between the two `AlignTo` modes — under `Plot` it lands at
91    // the left edge of the legend / plot interior; under `Panel`
92    // it lands at the left edge of the panel itself. Mutating
93    // `plot_title` in place (rather than constructing a new
94    // `Element::Set(...)`) preserves the existing 16pt-bold styling
95    // from `Theme::default`.
96    let mut theme = Theme {
97        plot_text_align_to: AlignTo::Plot,
98        ..Theme::default()
99    };
100    if let Element::Set(t) = &mut theme.plot_title {
101        t.align = Some(HAlign::Start);
102    }
103    let mut view = PlotComposition::new(&comp())
104        .add_scale("x_scale", scale::continuous(0.0..=6.0))
105        .add_scale("y_scale", scale::continuous(0.0..=1.0))
106        .add_scale("category", category_scale)
107        .theme(theme);
108    view.attach_plot(make_plot(
109        "a",
110        "AlignTo::Plot — title left-edge aligns to plot interior",
111    ));
112    // Second plot uses a per-plot theme override to flip to Panel.
113    view.attach_plot(
114        make_plot("b", "AlignTo::Panel — title left-edge aligns to panel").theme_override(
115            hephaestus::plot::theme::ThemePart {
116                plot_text_align_to: Some(AlignTo::Panel),
117                ..hephaestus::plot::theme::ThemePart::default()
118            },
119        ),
120    );
121
122    let mut renderer = VelloRenderer::new().expect("vello renderer init");
123    let bg: Color = rgb8(245, 245, 245);
124    {
125        let scene = renderer.scene();
126        scene.clear();
127        view.render(scene, Size::new(w as f64, h as f64), dpi);
128    }
129    let mut pixels = vec![0u8; (w * h * 4) as usize];
130    renderer
131        .render_to_buffer(w, h, bg, &mut pixels)
132        .expect("render");
133    let path = std::env::current_dir()
134        .unwrap()
135        .join("examples/theme_text_align_to.png");
136    hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
137    println!("wrote {}", path.display());
138}