Skip to main content

theme_legend_variants/
theme_legend_variants.rs

1//! Demonstrates named legend variants — a plot with two legends
2//! where one opts into a `"hero"` variant registered on the theme,
3//! and the other uses the default. This phase wires the API
4//! surface (`Legend::theme_variant(name)` + `Theme::legend_for`);
5//! full per-variant styling consumption happens in F.2 when the
6//! legend renderer migrates to consume LegendTheme fields directly.
7
8use hephaestus::backend::vello::VelloRenderer;
9use hephaestus::color::{rgb, rgb8, Color};
10use hephaestus::composition::{Composition, Patch, Span};
11use hephaestus::geometry::Size;
12use hephaestus::plot::chrome::axis::{Axis, AxisPlacement};
13use hephaestus::plot::chrome::legend::{Legend, LegendKeySpec};
14use hephaestus::plot::theme::{Element, LegendTheme, Length, RectElement, Theme, ThemeColor};
15use hephaestus::plot::{scale, Plot, PlotComposition, PointGeom};
16use hephaestus::scales::chrome::{AxisSide, LegendSide};
17use hephaestus::scales::value::Value;
18use hephaestus::scene::SceneBuilder;
19use hephaestus::Renderer;
20
21fn comp() -> Composition {
22    Composition::empty(1, 1).place(1, 1, Span::cell(), Patch::new("panel"))
23}
24
25fn main() {
26    let (w, h) = (900u32, 600u32);
27    let dpi = 96.0;
28
29    let n = 24;
30    let xs: Vec<f64> = (0..n).map(|i| i as f64 * 0.4).collect();
31    let ys: Vec<f64> = (0..n)
32        .map(|i| ((i as f64) * 0.5).sin() * 0.4 + 0.5)
33        .collect();
34    let cats: [&'static str; 4] = ["A", "B", "C", "D"];
35    let fill_col: Vec<&'static str> = (0..n).map(|i| cats[i % 4]).collect();
36
37    let mut plot = Plot::new(&comp(), "panel")
38        .bind("x", "x")
39        .bind("y", "y")
40        .bind("fill", "category_color")
41        .title("Two legends, one opts into the \"hero\" theme variant");
42    plot.add_geom(
43        PointGeom::builder()
44            .set("x", xs)
45            .set("y", ys)
46            .set("fill", fill_col.clone())
47            .set("size", 6.0_f64)
48            .build(),
49    );
50    plot.add_axis(Axis::rail("x", AxisPlacement::Cartesian(AxisSide::Bottom)));
51    plot.add_axis(Axis::rail("y", AxisPlacement::Cartesian(AxisSide::Left)));
52
53    // First legend — opts into the "hero" variant.
54    plot.add_legend(
55        Legend::new("category_color")
56            .side(LegendSide::Right)
57            .title("Category (hero)")
58            .theme_variant("hero")
59            .key(
60                LegendKeySpec::point()
61                    .scaled("fill", "category_color")
62                    .fixed("stroke", Value::Color(rgb(0.0, 0.0, 0.0)))
63                    .fixed("size", 6.0_f64),
64            ),
65    );
66    // Second legend — uses the default LegendTheme.
67    plot.add_legend(
68        Legend::new("category_size")
69            .side(LegendSide::Bottom)
70            .title("Category (default)")
71            .key(
72                LegendKeySpec::point()
73                    .scaled("fill", "category_color")
74                    .fixed("size", 6.0_f64),
75            ),
76    );
77
78    // Register a "hero" variant on the theme. Distinct background
79    // tint + a denser margin to telegraph the emphasis.
80    let hero = LegendTheme {
81        background: Element::Set(RectElement {
82            fill: Some(ThemeColor::mix(ThemeColor::Paper, ThemeColor::Accent, 0.18)),
83            color: Some(ThemeColor::Accent),
84            linewidth_pt: Some(Length::Abs(1.0)),
85            ..RectElement::default()
86        }),
87        ..LegendTheme::default()
88    };
89
90    let theme = Theme::default().with_legend_variant("hero", hero);
91
92    let mut view = PlotComposition::new(&comp())
93        .add_scale("x", scale::continuous(0.0..=12.0))
94        .add_scale("y", scale::continuous(0.0..=1.0))
95        .add_scale(
96            "category_color",
97            scale::discrete(cats.iter().map(|s| Value::String((*s).into()))).range_colors([
98                rgb8(220, 100, 80),
99                rgb8(80, 160, 100),
100                rgb8(80, 130, 200),
101                rgb8(180, 100, 200),
102            ]),
103        )
104        .add_scale(
105            "category_size",
106            scale::discrete(cats.iter().map(|s| Value::String((*s).into())))
107                .range_numbers([4.0, 6.0, 8.0, 10.0]),
108        )
109        .theme(theme);
110    view.attach_plot(plot);
111
112    let mut renderer = VelloRenderer::new().expect("vello renderer init");
113    let bg: Color = rgb8(252, 252, 252);
114    {
115        let scene = renderer.scene();
116        scene.clear();
117        view.render(scene, Size::new(w as f64, h as f64), dpi);
118    }
119    let mut pixels = vec![0u8; (w * h * 4) as usize];
120    renderer
121        .render_to_buffer(w, h, bg, &mut pixels)
122        .expect("render");
123    let path = std::env::current_dir()
124        .unwrap()
125        .join("examples/theme_legend_variants.png");
126    hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
127    println!("wrote {}", path.display());
128}