Skip to main content

LegendKeySpec

Struct LegendKeySpec 

Source
pub struct LegendKeySpec {
    pub kind: LegendKey,
    pub bindings: HashMap<String, AestheticSource>,
}
Expand description

One key in a legend’s stack — what to draw + how to resolve its aesthetics for the current row.

Fields§

§kind: LegendKey§bindings: HashMap<String, AestheticSource>

Per-aesthetic name → source. Aesthetics not listed fall back to the key’s built-in default.

Implementations§

Source§

impl LegendKeySpec

Source

pub fn point() -> Self

Start a Point key with no aesthetic bindings.

Examples found in repository?
examples/rich_text_chrome.rs (line 82)
38fn main() {
39    let (w, h) = (900u32, 560u32);
40    let dpi = 96.0;
41    let bg: Color = rgb8(250, 250, 253);
42
43    let comp = || Composition::empty(1, 1).place(1, 1, Span::cell(), Patch::new("p"));
44
45    let n = 60;
46    let xs: Vec<f64> = (0..n).map(|i| i as f64 / (n - 1) as f64 * 10.0).collect();
47    let ys: Vec<f64> = xs
48        .iter()
49        .map(|x| 2.5 + 1.2 * (x * 0.7).sin() + 0.4 * (x * 2.1).cos())
50        .collect();
51
52    // Break labels are data-derived, so a category that spells
53    // markdown gets parsed like any other string.
54    let bands: [&'static str; 3] = ["*low*", "**mid**", "{.red high}"];
55    let groups: Vec<&'static str> = ys
56        .iter()
57        .map(|y| match *y {
58            v if v < 2.0 => bands[0],
59            v if v < 3.0 => bands[1],
60            _ => bands[2],
61        })
62        .collect();
63
64    let mut plot = Plot::new(&comp(), "p")
65        .bind("x", "x")
66        .bind("y", "y")
67        .bind("fill", "band")
68        .title("Trend of **{#c14b4b price}** across the day")
69        .subtitle("Metric: *closing_bid* — sampled hourly")
70        .caption("n = **60**, source: {.gray internal}");
71    plot.add_geom(
72        PointGeom::builder()
73            .set("x", xs)
74            .set("y", ys)
75            .set("fill", groups)
76            .set("size", 8.0_f64)
77            .build(),
78    );
79    plot.add_legend(
80        Legend::new("band")
81            .title("**band** of the *close*")
82            .key(LegendKeySpec::point().scaled("fill", "band")),
83    );
84    plot.add_axis(
85        Axis::rail("x", AxisPlacement::Cartesian(AxisSide::Bottom))
86            .title("hour of day, {.gray *UTC*}"),
87    );
88    plot.add_axis(
89        Axis::rail("y", AxisPlacement::Cartesian(AxisSide::Left)).title("**price** (USD)"),
90    );
91
92    let mut view = PlotComposition::new(&comp())
93        .theme(markdown_chrome_theme())
94        .add_scale("x", scale::continuous(0.0..=10.0))
95        .add_scale("y", scale::continuous(0.0..=5.0))
96        .add_scale(
97            "band",
98            scale::discrete(bands.iter().map(|b| Value::String(Arc::from(*b)))).range_colors([
99                rgb8(88, 106, 195),
100                rgb8(214, 146, 60),
101                rgb8(193, 75, 75),
102            ]),
103        )
104        .with_plot(plot);
105
106    let mut renderer = VelloRenderer::new().expect("vello renderer init");
107    {
108        let scene = renderer.scene();
109        scene.clear();
110        view.render(scene, Size::new(w as f64, h as f64), dpi);
111    }
112    let mut pixels = vec![0u8; (w * h * 4) as usize];
113    renderer
114        .render_to_buffer(w, h, bg, &mut pixels)
115        .expect("render");
116    let path = std::env::current_dir()
117        .unwrap()
118        .join("examples/rich_text_chrome.png");
119    hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
120    println!("wrote {}", path.display());
121}
More examples
Hide additional examples
examples/theme_legend_variants.rs (line 60)
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}
examples/faceted.rs (line 106)
58fn main() {
59    let (w, h) = (1400u32, 700u32);
60    let dpi = 96.0;
61
62    let xs: Vec<f64> = (0..50).map(|i| i as f64 * 2.0).collect();
63    let make = |phase: f64, amp: f64| -> Vec<f64> {
64        xs.iter()
65            .map(|x| 50.0 + amp * (x * 0.05 + phase).sin())
66            .collect()
67    };
68
69    let datasets = [
70        ("q1", make(0.0, 25.0), rgb8(220, 90, 70)),
71        ("q2", make(1.5, 18.0), rgb8(70, 120, 220)),
72        ("q3", make(3.0, 22.0), rgb8(70, 180, 120)),
73        ("q4", make(4.5, 28.0), rgb8(180, 130, 80)),
74        ("summary", make(0.0, 35.0), rgb8(130, 80, 180)),
75    ];
76
77    let mut renderer = VelloRenderer::new().expect("vello renderer init");
78    let bg: Color = rgb8(248, 248, 252);
79
80    // ── Renders 1 & 2: shared "time" scale across the unlocked layout
81    {
82        #[allow(unused_mut)]
83        let mut view = PlotComposition::new(&comp_shape(None))
84            .add_scale("time", scale::continuous(0.0..=100.0))
85            .add_scale("y", scale::continuous(0.0..=100.0))
86            .title("Sensor array")
87            .subtitle("Four quadrants and a summary, sharing one time scale")
88            .caption("Composition-level chrome spans every panel");
89        attach_all(&mut view, &xs, &datasets);
90
91        // One axis title and one legend for the whole grid, set on the
92        // composition rather than on any single plot. The legend reads
93        // its rows from the "series" scale's domain; no plot needs to
94        // bind that scale for the legend to resolve it.
95        let mut view = {
96            let series: Vec<Value> = datasets.iter().map(|(id, _, _)| Value::from(*id)).collect();
97            let colors: Vec<Color> = datasets.iter().map(|(_, _, c)| *c).collect();
98            let mut view = view
99                .add_scale("series", scale::discrete(series).range_colors(colors))
100                .axis_title(AxisSide::Bottom, "Time (s)");
101            view.add_legend(
102                Legend::new("series")
103                    .side(LegendSide::Right)
104                    .title("Series")
105                    .key(
106                        LegendKeySpec::point()
107                            .scaled("fill", "series")
108                            .fixed("size", 6.0_f64),
109                    ),
110            );
111            view
112        };
113
114        let issues = view.validate();
115        if !issues.is_empty() {
116            panic!("validate() reported issues: {issues:?}");
117        }
118
119        render_to(
120            &mut renderer,
121            &mut view,
122            w,
123            h,
124            dpi,
125            bg,
126            "examples/faceted_1_initial.png",
127        );
128
129        view.update_scale("time", |s| s.set_domain_continuous(20.0, 60.0));
130        render_to(
131            &mut renderer,
132            &mut view,
133            w,
134            h,
135            dpi,
136            bg,
137            "examples/faceted_2_shared_zoom.png",
138        );
139    }
140
141    // ── Render 3: aspect-locked. Outer `.aspect(1, 1)` propagates to
142    //    every leaf panel; selective respect on the layout solver
143    //    couples panel col/row at the locked ratio and lets unmarked
144    //    fr tracks absorb slack. Wider viewport (1800×600) makes the
145    //    lock visually obvious — without it, the 1×2 outer would give
146    //    half the width to each side; with it, the leaf panels land
147    //    at the locked ratio and the surrounding tracks soak up the
148    //    horizontal slack.
149    {
150        let (lw, lh) = (1800u32, 600u32);
151        let mut view = PlotComposition::new(&comp_shape(Some((1.0, 1.0))))
152            .add_scale("time", scale::continuous(20.0..=60.0))
153            .add_scale("y", scale::continuous(0.0..=100.0));
154        attach_all(&mut view, &xs, &datasets);
155        render_to(
156            &mut renderer,
157            &mut view,
158            lw,
159            lh,
160            dpi,
161            bg,
162            "examples/faceted_3_aspect_locked.png",
163        );
164    }
165}
examples/theme_text_align_to.rs (line 69)
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}
examples/theme_plot_background.rs (line 91)
18fn main() {
19    let (w, h) = (900u32, 600u32);
20    let dpi = 96.0;
21
22    let comp = || Composition::empty(1, 1).place(1, 1, Span::cell(), Patch::new("p"));
23    // Generate data that explicitly reaches the corners so the
24    // rounded clip is visibly exercised. The dense corner clusters
25    // get cropped by the rounded panel boundary.
26    let mut xs: Vec<f64> = Vec::new();
27    let mut ys: Vec<f64> = Vec::new();
28    for i in 0..40 {
29        let t = i as f64 / 39.0;
30        xs.push(t * 6.0);
31        ys.push((t * 4.4).sin() * 0.4 + 0.5);
32    }
33    // Add a corner-hugging cluster at all four corners to make clip
34    // behaviour visible.
35    for (cx, cy) in &[(0.05, 0.97), (5.95, 0.97), (0.05, 0.03), (5.95, 0.03)] {
36        for di in 0..8 {
37            let theta = di as f64 * 0.3;
38            xs.push(cx + theta.cos() * 0.15);
39            ys.push(cy + theta.sin() * 0.04);
40        }
41    }
42
43    // Use the y values themselves as the colour-mapped channel so a
44    // colorbar legend makes sense.
45    let colours: Vec<f64> = ys.clone();
46    // A second, discrete category column (assigned by x bucket),
47    // used to drive the categorical stroke colour and a discrete
48    // legend on the right next to the colorbar.
49    let categories: Vec<&str> = xs
50        .iter()
51        .map(|x| match (*x as usize) % 4 {
52            0 => "A",
53            1 => "B",
54            2 => "C",
55            _ => "D",
56        })
57        .collect();
58    let mut plot = Plot::new(&comp(), "p")
59        .bind("x", "x_scale")
60        .bind("y", "y_scale")
61        .bind("fill", "fill_scale")
62        .bind("stroke", "stroke_scale")
63        .title("Rounded corners — plot bg, panel bg, frames");
64    plot.add_geom(
65        PointGeom::builder()
66            .set("x", xs)
67            .set("y", ys)
68            .set("size", 8.0_f64)
69            .set("fill", colours)
70            .set("stroke", categories)
71            .set("linewidth", 1.0_f64)
72            .build(),
73    );
74    plot.add_axis(Axis::rail(
75        "x_scale",
76        AxisPlacement::Cartesian(AxisSide::Bottom),
77    ));
78    plot.add_axis(Axis::rail(
79        "y_scale",
80        AxisPlacement::Cartesian(AxisSide::Left),
81    ));
82    plot.add_legend(
83        Legend::colorbar("fill_scale")
84            .side(LegendSide::Right)
85            .title("Amplitude"),
86    );
87    plot.add_legend(
88        Legend::new("stroke_scale")
89            .side(LegendSide::Right)
90            .title("Group")
91            .key(LegendKeySpec::point().scaled("stroke", "stroke_scale")),
92    );
93
94    // Two outer bands: 24pt `plot_margin` sits outside the
95    // background; 18pt `plot_padding` sits inside it. Both feed the
96    // anatomical ring tracks, so chrome lands in the correct rhythm
97    // automatically.
98    let theme = Theme {
99        plot_margin: Margin::all(Length::Abs(24.0)),
100        plot_padding: Margin::all(Length::Abs(18.0)),
101        plot_background: Element::Set(RectElement {
102            fill: Some(ThemeColor::mix(ThemeColor::Paper, ThemeColor::Accent, 0.3)),
103            color: Some(ThemeColor::Ink),
104            linewidth_pt: Some(Length::Abs(2.0)),
105            corner_radius: Some(Length::Abs(12.0)),
106            ..RectElement::default()
107        }),
108        // Round the panel corners too — the geom clip mask uses the
109        // same rounded path so data points crop cleanly.
110        panel_background: Element::Set(RectElement {
111            corner_radius: Some(Length::Abs(8.0)),
112            ..Theme::default().panel_background.as_set().unwrap().clone()
113        }),
114        // Colorbar bar + discrete key frames share `RectElement`
115        // semantics: fill paints under the inner content (gradient
116        // for the colorbar, marker for the key) so transparent
117        // colours show the frame fill; stroke + corner_radius paint
118        // on top.
119        legend: hephaestus::plot::theme::LegendTheme {
120            bar: hephaestus::plot::theme::BarTheme {
121                frame: Element::Set(RectElement {
122                    fill: Some(ThemeColor::mix(ThemeColor::Paper, ThemeColor::Ink, 0.08)),
123                    color: Some(ThemeColor::Ink),
124                    linewidth_pt: Some(Length::Abs(1.5)),
125                    corner_radius: Some(Length::Abs(6.0)),
126                    ..RectElement::default()
127                }),
128                ..hephaestus::plot::theme::BarTheme::default()
129            },
130            key: hephaestus::plot::theme::KeyTheme {
131                width: Length::Abs(20.0),
132                height: Length::Abs(20.0),
133                frame: Element::Set(RectElement {
134                    fill: Some(ThemeColor::mix(ThemeColor::Paper, ThemeColor::Ink, 0.08)),
135                    color: Some(ThemeColor::Ink),
136                    linewidth_pt: Some(Length::Abs(0.75)),
137                    corner_radius: Some(Length::Abs(4.0)),
138                    ..RectElement::default()
139                }),
140                ..hephaestus::plot::theme::KeyTheme::default()
141            },
142            ..Theme::default().legend
143        },
144        ..Theme::default()
145    };
146
147    let fill_scale = scale::continuous(0.0..=1.0).range_colors([
148        hephaestus::color::rgb(0.2, 0.3, 0.6),
149        hephaestus::color::rgb(0.85, 0.45, 0.2),
150    ]);
151    let stroke_scale = scale::discrete([
152        hephaestus::scales::Value::String(std::sync::Arc::from("A")),
153        hephaestus::scales::Value::String(std::sync::Arc::from("B")),
154        hephaestus::scales::Value::String(std::sync::Arc::from("C")),
155        hephaestus::scales::Value::String(std::sync::Arc::from("D")),
156    ])
157    .range_colors([
158        hephaestus::color::rgb(0.20, 0.20, 0.20),
159        hephaestus::color::rgb(0.70, 0.20, 0.20),
160        hephaestus::color::rgb(0.20, 0.60, 0.20),
161        hephaestus::color::rgb(0.20, 0.20, 0.70),
162    ]);
163    let mut view = PlotComposition::new(&comp())
164        .add_scale("x_scale", scale::continuous(0.0..=6.0))
165        .add_scale("y_scale", scale::continuous(0.0..=1.0))
166        .add_scale("fill_scale", fill_scale)
167        .add_scale("stroke_scale", stroke_scale)
168        .theme(theme);
169    view.attach_plot(plot);
170
171    let mut renderer = VelloRenderer::new().expect("vello renderer init");
172    // Contrasting canvas bg makes the plot_margin band visible
173    // outside the (warm-cream) plot_background.
174    let bg: Color = rgb8(60, 70, 90);
175    {
176        let scene = renderer.scene();
177        scene.clear();
178        view.render(scene, Size::new(w as f64, h as f64), dpi);
179    }
180    let mut pixels = vec![0u8; (w * h * 4) as usize];
181    renderer
182        .render_to_buffer(w, h, bg, &mut pixels)
183        .expect("render");
184    let path = std::env::current_dir()
185        .unwrap()
186        .join("examples/theme_plot_background.png");
187    hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
188    println!("wrote {}", path.display());
189}
examples/legends.rs (line 106)
44fn main() {
45    let (w, h) = (900u32, 600u32);
46    let dpi = 96.0;
47
48    // ── Data ──
49    let n = 24;
50    let xs: Vec<f64> = (0..n).map(|i| i as f64 * 0.4).collect();
51    let ys: Vec<f64> = (0..n)
52        .map(|i| ((i as f64) * 0.5).sin() * 0.4 + 0.5)
53        .collect();
54    let cats: [&'static str; 4] = ["A", "B", "C", "D"];
55    let fill_col: Vec<&'static str> = (0..n).map(|i| cats[i % 4]).collect();
56    let size_col: Vec<&'static str> = (0..n).map(|i| cats[i % 4]).collect();
57
58    // Build the plot's shape registry with the built-in vector
59    // shapes plus a handful of emoji glyphs used by the binned
60    // legend below. Glyph shapes share the same registry surface
61    // as the vector ones (insert by name; look up by name) so the
62    // legend chrome can resolve either.
63    let glyph_style = TextStyle::new(16.0);
64    let mut shapes = ShapeRegistry::with_builtins();
65    // U+1F4A7 droplet, U+1F31E sun, U+1F525 fire, U+1F33F herb,
66    // U+1F30A wave — picked to read as a "low → high" intensity
67    // ramp matching the gradient_scale values.
68    shapes.insert("droplet", glyph_marker("\u{1F4A7}", &glyph_style));
69    shapes.insert("herb", glyph_marker("\u{1F33F}", &glyph_style));
70    shapes.insert("sun", glyph_marker("\u{1F31E}", &glyph_style));
71    shapes.insert("wave", glyph_marker("\u{1F30A}", &glyph_style));
72    shapes.insert("fire", glyph_marker("\u{1F525}", &glyph_style));
73
74    let mut p = Plot::new(&comp(), "panel")
75        .bind("x", "x")
76        .bind("y", "y")
77        .bind("fill", "category_color")
78        .bind("size", "category_size")
79        .shape_registry(shapes);
80    p.add_geom(
81        PointGeom::builder()
82            .set("x", xs)
83            .set("y", ys)
84            .set("fill", fill_col)
85            .set("size", size_col)
86            .build(),
87    );
88    p.add_axis(Axis::rail("x", AxisPlacement::Cartesian(AxisSide::Bottom)));
89    p.add_axis(Axis::rail("y", AxisPlacement::Cartesian(AxisSide::Left)));
90
91    // ── Right side, legend #1: line + point, both driven by
92    // category_color. Attached as two legends; they share a side,
93    // title and domain scale, so the render-time collapse folds the
94    // second one's key into the first.
95    p.add_legend(
96        Legend::new("category_color")
97            .side(LegendSide::Right)
98            .title("Category")
99            .key(LegendKeySpec::line().scaled("stroke", "category_color")),
100    );
101    p.add_legend(
102        Legend::new("category_color")
103            .side(LegendSide::Right)
104            .title("Category")
105            .key(
106                LegendKeySpec::point()
107                    .scaled("fill", "category_color")
108                    .fixed("stroke", Value::Color(rgb(0.0, 0.0, 0.0)))
109                    .fixed("size", 6.0_f64),
110            ),
111    );
112
113    // ── Right side, legend #2 (stacks below #1): a size legend that
114    // shares the Right slot. `category_size` is trained to the same
115    // categories as `category_color`, so what keeps this a block of
116    // its own is the differing title; the per-side stacker then
117    // arranges both legends vertically.
118    p.add_legend(
119        Legend::new("category_size")
120            .side(LegendSide::Right)
121            .title("Size")
122            .key(
123                LegendKeySpec::point()
124                    .scaled("size", "category_size")
125                    .fixed("fill", Value::Color(rgb(0.25, 0.25, 0.25))),
126            ),
127    );
128
129    // ── Right side, legend #3: a continuous gradient colorbar
130    // driven by `gradient_scale`. Stacks below the discrete ones
131    // via the same per-side stacker. The colorbar also pulls its
132    // opacity from `gradient_opacity` — same domain → low values are
133    // mostly transparent, high values fully opaque.
134    p.add_legend(
135        Legend::colorbar("gradient_scale")
136            .side(LegendSide::Right)
137            .title("Gradient")
138            .scaled("fill_opacity", "gradient_opacity"),
139    );
140
141    // ── Bottom side, legend #3: same gradient scale but rendered
142    // as discrete steps at each break — useful for showing a
143    // binned colour mapping or any colorbar you want to read as
144    // categorical bands. Routed to the Bottom slot so it stacks
145    // horizontally alongside the existing "Pattern" + "Colour"
146    // legends.
147    p.add_legend(
148        Legend::colorbar("gradient_scale")
149            .side(LegendSide::Bottom)
150            .title("Steps")
151            .binned()
152            .open_upper(),
153    );
154
155    // ── Bottom side, legend #4: a **binned stack** that varies
156    // SHAPE per bin (instead of colour like the stepped colorbar
157    // next to it). Six bin boundaries from `gradient_scale` → five
158    // bins, each rendered as a different emoji-glyph shape via the
159    // `bin_shape` scale (resolved against the plot's shape
160    // registry). Demonstrates that the same legend key wiring
161    // works with vector and glyph-backed shapes.
162    //
163    // Emoji glyphs fill their em-bbox almost completely (Latin
164    // letters and vector circles only fill ~70–80 % of the same
165    // bbox), so a 12pt emoji reads at roughly the same visual
166    // weight as the 16pt circle markers used by the other legends.
167    p.add_legend(
168        Legend::new("gradient_scale")
169            .side(LegendSide::Bottom)
170            .title("Bins")
171            .binned()
172            .equal_bins()
173            .key(
174                LegendKeySpec::point()
175                    .scaled("shape", "bin_shape")
176                    .fixed("fill", Value::Color(rgb(0.15, 0.15, 0.15)))
177                    .fixed("size", 12.0_f64),
178            ),
179    );
180
181    // ── Left side: a text key. The swatch is a glyph sample rather
182    // than a marker, so a font-size scale shows what it actually
183    // does to type. `size` is the font size in pt and `fill` is the
184    // ink; both resolve per row through their own scale.
185    p.add_legend(
186        Legend::new("category_size")
187            .side(LegendSide::Left)
188            .title("Font size")
189            .key(
190                LegendKeySpec::text()
191                    .scaled("size", "category_size")
192                    .scaled("fill", "category_color")
193                    .fixed("text", Value::String(Arc::from("Aa"))),
194            ),
195    );
196
197    // ── Bottom side: two legends side-by-side (horizontal stack).
198    p.add_legend(
199        Legend::new("category_line")
200            .side(LegendSide::Bottom)
201            .title("Pattern")
202            .key(
203                LegendKeySpec::line()
204                    .scaled("linetype", "category_line")
205                    .fixed("linewidth", 1.5_f64),
206            ),
207    );
208    p.add_legend(
209        Legend::new("category_color")
210            .side(LegendSide::Bottom)
211            .title("Colour")
212            .key(LegendKeySpec::rect().scaled("fill", "category_color")),
213    );
214
215    // ── In-panel overlay: a compact category legend pinned to the
216    // top-right corner of the panel area. Reserves no chrome space —
217    // the data marks beneath continue to occupy the full panel rect.
218    p.add_legend(
219        Legend::new("category_color")
220            .side(LegendSide::InPanel {
221                anchor: Anchor::TopRight,
222                inset_pt: 8.0,
223            })
224            .title("Overlay")
225            .key(
226                LegendKeySpec::point()
227                    .scaled("fill", "category_color")
228                    .fixed("size", 6.0_f64),
229            ),
230    );
231
232    let cat_values: Vec<Value> = cats.iter().map(|s| Value::String(Arc::from(*s))).collect();
233    let line_cats: [&'static str; 3] = ["Solid", "Dashed", "Dotted"];
234    let line_values: Vec<Value> = line_cats
235        .iter()
236        .map(|s| Value::String(Arc::from(*s)))
237        .collect();
238
239    let mut view = PlotComposition::new(&comp())
240        .add_scale("x", scale::continuous(0.0..=10.0))
241        .add_scale("y", scale::continuous(0.0..=1.0))
242        .add_scale(
243            "category_color",
244            scale::discrete(cat_values.clone()).range_colors([
245                rgb8(220, 90, 70),
246                rgb8(70, 160, 90),
247                rgb8(70, 120, 220),
248                rgb8(180, 120, 200),
249            ]),
250        )
251        .add_scale(
252            "category_size",
253            scale::discrete(cat_values).range_numbers([4.0, 8.0, 12.0, 16.0]),
254        )
255        .add_scale(
256            "category_line",
257            scale::discrete(line_values).range_linetypes([solid(), dashed(), dotted()]),
258        )
259        .add_scale(
260            "gradient_scale",
261            scale::continuous(0.0..=100.0).range_colors([
262                rgb8(20, 30, 90),
263                rgb8(60, 160, 200),
264                rgb8(230, 220, 100),
265                rgb8(220, 60, 40),
266            ]),
267        )
268        .add_scale(
269            "gradient_opacity",
270            scale::continuous(0.0..=100.0).range_numbers([0.1, 1.0]),
271        )
272        .add_scale(
273            "bin_shape",
274            scale::continuous(0.0..=100.0).range_strings([
275                Arc::from("droplet"),
276                Arc::from("herb"),
277                Arc::from("sun"),
278                Arc::from("wave"),
279                Arc::from("fire"),
280            ]),
281        );
282    view.attach_plot(p);
283
284    let issues = view.validate();
285    if !issues.is_empty() {
286        panic!("validate(): {issues:?}");
287    }
288
289    let mut renderer = VelloRenderer::new().expect("vello renderer init");
290    let bg: Color = rgb8(252, 252, 252);
291    {
292        let scene = renderer.scene();
293        scene.clear();
294        view.render(scene, Size::new(w as f64, h as f64), dpi);
295    }
296    let mut pixels = vec![0u8; (w * h * 4) as usize];
297    renderer
298        .render_to_buffer(w, h, bg, &mut pixels)
299        .expect("render");
300    let path = std::env::current_dir()
301        .unwrap()
302        .join("examples/legends.png");
303    hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
304    println!("wrote {}", path.display());
305}
Source

pub fn line() -> Self

Start a Line key with no aesthetic bindings.

Examples found in repository?
examples/legends.rs (line 99)
44fn main() {
45    let (w, h) = (900u32, 600u32);
46    let dpi = 96.0;
47
48    // ── Data ──
49    let n = 24;
50    let xs: Vec<f64> = (0..n).map(|i| i as f64 * 0.4).collect();
51    let ys: Vec<f64> = (0..n)
52        .map(|i| ((i as f64) * 0.5).sin() * 0.4 + 0.5)
53        .collect();
54    let cats: [&'static str; 4] = ["A", "B", "C", "D"];
55    let fill_col: Vec<&'static str> = (0..n).map(|i| cats[i % 4]).collect();
56    let size_col: Vec<&'static str> = (0..n).map(|i| cats[i % 4]).collect();
57
58    // Build the plot's shape registry with the built-in vector
59    // shapes plus a handful of emoji glyphs used by the binned
60    // legend below. Glyph shapes share the same registry surface
61    // as the vector ones (insert by name; look up by name) so the
62    // legend chrome can resolve either.
63    let glyph_style = TextStyle::new(16.0);
64    let mut shapes = ShapeRegistry::with_builtins();
65    // U+1F4A7 droplet, U+1F31E sun, U+1F525 fire, U+1F33F herb,
66    // U+1F30A wave — picked to read as a "low → high" intensity
67    // ramp matching the gradient_scale values.
68    shapes.insert("droplet", glyph_marker("\u{1F4A7}", &glyph_style));
69    shapes.insert("herb", glyph_marker("\u{1F33F}", &glyph_style));
70    shapes.insert("sun", glyph_marker("\u{1F31E}", &glyph_style));
71    shapes.insert("wave", glyph_marker("\u{1F30A}", &glyph_style));
72    shapes.insert("fire", glyph_marker("\u{1F525}", &glyph_style));
73
74    let mut p = Plot::new(&comp(), "panel")
75        .bind("x", "x")
76        .bind("y", "y")
77        .bind("fill", "category_color")
78        .bind("size", "category_size")
79        .shape_registry(shapes);
80    p.add_geom(
81        PointGeom::builder()
82            .set("x", xs)
83            .set("y", ys)
84            .set("fill", fill_col)
85            .set("size", size_col)
86            .build(),
87    );
88    p.add_axis(Axis::rail("x", AxisPlacement::Cartesian(AxisSide::Bottom)));
89    p.add_axis(Axis::rail("y", AxisPlacement::Cartesian(AxisSide::Left)));
90
91    // ── Right side, legend #1: line + point, both driven by
92    // category_color. Attached as two legends; they share a side,
93    // title and domain scale, so the render-time collapse folds the
94    // second one's key into the first.
95    p.add_legend(
96        Legend::new("category_color")
97            .side(LegendSide::Right)
98            .title("Category")
99            .key(LegendKeySpec::line().scaled("stroke", "category_color")),
100    );
101    p.add_legend(
102        Legend::new("category_color")
103            .side(LegendSide::Right)
104            .title("Category")
105            .key(
106                LegendKeySpec::point()
107                    .scaled("fill", "category_color")
108                    .fixed("stroke", Value::Color(rgb(0.0, 0.0, 0.0)))
109                    .fixed("size", 6.0_f64),
110            ),
111    );
112
113    // ── Right side, legend #2 (stacks below #1): a size legend that
114    // shares the Right slot. `category_size` is trained to the same
115    // categories as `category_color`, so what keeps this a block of
116    // its own is the differing title; the per-side stacker then
117    // arranges both legends vertically.
118    p.add_legend(
119        Legend::new("category_size")
120            .side(LegendSide::Right)
121            .title("Size")
122            .key(
123                LegendKeySpec::point()
124                    .scaled("size", "category_size")
125                    .fixed("fill", Value::Color(rgb(0.25, 0.25, 0.25))),
126            ),
127    );
128
129    // ── Right side, legend #3: a continuous gradient colorbar
130    // driven by `gradient_scale`. Stacks below the discrete ones
131    // via the same per-side stacker. The colorbar also pulls its
132    // opacity from `gradient_opacity` — same domain → low values are
133    // mostly transparent, high values fully opaque.
134    p.add_legend(
135        Legend::colorbar("gradient_scale")
136            .side(LegendSide::Right)
137            .title("Gradient")
138            .scaled("fill_opacity", "gradient_opacity"),
139    );
140
141    // ── Bottom side, legend #3: same gradient scale but rendered
142    // as discrete steps at each break — useful for showing a
143    // binned colour mapping or any colorbar you want to read as
144    // categorical bands. Routed to the Bottom slot so it stacks
145    // horizontally alongside the existing "Pattern" + "Colour"
146    // legends.
147    p.add_legend(
148        Legend::colorbar("gradient_scale")
149            .side(LegendSide::Bottom)
150            .title("Steps")
151            .binned()
152            .open_upper(),
153    );
154
155    // ── Bottom side, legend #4: a **binned stack** that varies
156    // SHAPE per bin (instead of colour like the stepped colorbar
157    // next to it). Six bin boundaries from `gradient_scale` → five
158    // bins, each rendered as a different emoji-glyph shape via the
159    // `bin_shape` scale (resolved against the plot's shape
160    // registry). Demonstrates that the same legend key wiring
161    // works with vector and glyph-backed shapes.
162    //
163    // Emoji glyphs fill their em-bbox almost completely (Latin
164    // letters and vector circles only fill ~70–80 % of the same
165    // bbox), so a 12pt emoji reads at roughly the same visual
166    // weight as the 16pt circle markers used by the other legends.
167    p.add_legend(
168        Legend::new("gradient_scale")
169            .side(LegendSide::Bottom)
170            .title("Bins")
171            .binned()
172            .equal_bins()
173            .key(
174                LegendKeySpec::point()
175                    .scaled("shape", "bin_shape")
176                    .fixed("fill", Value::Color(rgb(0.15, 0.15, 0.15)))
177                    .fixed("size", 12.0_f64),
178            ),
179    );
180
181    // ── Left side: a text key. The swatch is a glyph sample rather
182    // than a marker, so a font-size scale shows what it actually
183    // does to type. `size` is the font size in pt and `fill` is the
184    // ink; both resolve per row through their own scale.
185    p.add_legend(
186        Legend::new("category_size")
187            .side(LegendSide::Left)
188            .title("Font size")
189            .key(
190                LegendKeySpec::text()
191                    .scaled("size", "category_size")
192                    .scaled("fill", "category_color")
193                    .fixed("text", Value::String(Arc::from("Aa"))),
194            ),
195    );
196
197    // ── Bottom side: two legends side-by-side (horizontal stack).
198    p.add_legend(
199        Legend::new("category_line")
200            .side(LegendSide::Bottom)
201            .title("Pattern")
202            .key(
203                LegendKeySpec::line()
204                    .scaled("linetype", "category_line")
205                    .fixed("linewidth", 1.5_f64),
206            ),
207    );
208    p.add_legend(
209        Legend::new("category_color")
210            .side(LegendSide::Bottom)
211            .title("Colour")
212            .key(LegendKeySpec::rect().scaled("fill", "category_color")),
213    );
214
215    // ── In-panel overlay: a compact category legend pinned to the
216    // top-right corner of the panel area. Reserves no chrome space —
217    // the data marks beneath continue to occupy the full panel rect.
218    p.add_legend(
219        Legend::new("category_color")
220            .side(LegendSide::InPanel {
221                anchor: Anchor::TopRight,
222                inset_pt: 8.0,
223            })
224            .title("Overlay")
225            .key(
226                LegendKeySpec::point()
227                    .scaled("fill", "category_color")
228                    .fixed("size", 6.0_f64),
229            ),
230    );
231
232    let cat_values: Vec<Value> = cats.iter().map(|s| Value::String(Arc::from(*s))).collect();
233    let line_cats: [&'static str; 3] = ["Solid", "Dashed", "Dotted"];
234    let line_values: Vec<Value> = line_cats
235        .iter()
236        .map(|s| Value::String(Arc::from(*s)))
237        .collect();
238
239    let mut view = PlotComposition::new(&comp())
240        .add_scale("x", scale::continuous(0.0..=10.0))
241        .add_scale("y", scale::continuous(0.0..=1.0))
242        .add_scale(
243            "category_color",
244            scale::discrete(cat_values.clone()).range_colors([
245                rgb8(220, 90, 70),
246                rgb8(70, 160, 90),
247                rgb8(70, 120, 220),
248                rgb8(180, 120, 200),
249            ]),
250        )
251        .add_scale(
252            "category_size",
253            scale::discrete(cat_values).range_numbers([4.0, 8.0, 12.0, 16.0]),
254        )
255        .add_scale(
256            "category_line",
257            scale::discrete(line_values).range_linetypes([solid(), dashed(), dotted()]),
258        )
259        .add_scale(
260            "gradient_scale",
261            scale::continuous(0.0..=100.0).range_colors([
262                rgb8(20, 30, 90),
263                rgb8(60, 160, 200),
264                rgb8(230, 220, 100),
265                rgb8(220, 60, 40),
266            ]),
267        )
268        .add_scale(
269            "gradient_opacity",
270            scale::continuous(0.0..=100.0).range_numbers([0.1, 1.0]),
271        )
272        .add_scale(
273            "bin_shape",
274            scale::continuous(0.0..=100.0).range_strings([
275                Arc::from("droplet"),
276                Arc::from("herb"),
277                Arc::from("sun"),
278                Arc::from("wave"),
279                Arc::from("fire"),
280            ]),
281        );
282    view.attach_plot(p);
283
284    let issues = view.validate();
285    if !issues.is_empty() {
286        panic!("validate(): {issues:?}");
287    }
288
289    let mut renderer = VelloRenderer::new().expect("vello renderer init");
290    let bg: Color = rgb8(252, 252, 252);
291    {
292        let scene = renderer.scene();
293        scene.clear();
294        view.render(scene, Size::new(w as f64, h as f64), dpi);
295    }
296    let mut pixels = vec![0u8; (w * h * 4) as usize];
297    renderer
298        .render_to_buffer(w, h, bg, &mut pixels)
299        .expect("render");
300    let path = std::env::current_dir()
301        .unwrap()
302        .join("examples/legends.png");
303    hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
304    println!("wrote {}", path.display());
305}
Source

pub fn rect() -> Self

Start a Rect key with no aesthetic bindings.

Examples found in repository?
examples/legends.rs (line 212)
44fn main() {
45    let (w, h) = (900u32, 600u32);
46    let dpi = 96.0;
47
48    // ── Data ──
49    let n = 24;
50    let xs: Vec<f64> = (0..n).map(|i| i as f64 * 0.4).collect();
51    let ys: Vec<f64> = (0..n)
52        .map(|i| ((i as f64) * 0.5).sin() * 0.4 + 0.5)
53        .collect();
54    let cats: [&'static str; 4] = ["A", "B", "C", "D"];
55    let fill_col: Vec<&'static str> = (0..n).map(|i| cats[i % 4]).collect();
56    let size_col: Vec<&'static str> = (0..n).map(|i| cats[i % 4]).collect();
57
58    // Build the plot's shape registry with the built-in vector
59    // shapes plus a handful of emoji glyphs used by the binned
60    // legend below. Glyph shapes share the same registry surface
61    // as the vector ones (insert by name; look up by name) so the
62    // legend chrome can resolve either.
63    let glyph_style = TextStyle::new(16.0);
64    let mut shapes = ShapeRegistry::with_builtins();
65    // U+1F4A7 droplet, U+1F31E sun, U+1F525 fire, U+1F33F herb,
66    // U+1F30A wave — picked to read as a "low → high" intensity
67    // ramp matching the gradient_scale values.
68    shapes.insert("droplet", glyph_marker("\u{1F4A7}", &glyph_style));
69    shapes.insert("herb", glyph_marker("\u{1F33F}", &glyph_style));
70    shapes.insert("sun", glyph_marker("\u{1F31E}", &glyph_style));
71    shapes.insert("wave", glyph_marker("\u{1F30A}", &glyph_style));
72    shapes.insert("fire", glyph_marker("\u{1F525}", &glyph_style));
73
74    let mut p = Plot::new(&comp(), "panel")
75        .bind("x", "x")
76        .bind("y", "y")
77        .bind("fill", "category_color")
78        .bind("size", "category_size")
79        .shape_registry(shapes);
80    p.add_geom(
81        PointGeom::builder()
82            .set("x", xs)
83            .set("y", ys)
84            .set("fill", fill_col)
85            .set("size", size_col)
86            .build(),
87    );
88    p.add_axis(Axis::rail("x", AxisPlacement::Cartesian(AxisSide::Bottom)));
89    p.add_axis(Axis::rail("y", AxisPlacement::Cartesian(AxisSide::Left)));
90
91    // ── Right side, legend #1: line + point, both driven by
92    // category_color. Attached as two legends; they share a side,
93    // title and domain scale, so the render-time collapse folds the
94    // second one's key into the first.
95    p.add_legend(
96        Legend::new("category_color")
97            .side(LegendSide::Right)
98            .title("Category")
99            .key(LegendKeySpec::line().scaled("stroke", "category_color")),
100    );
101    p.add_legend(
102        Legend::new("category_color")
103            .side(LegendSide::Right)
104            .title("Category")
105            .key(
106                LegendKeySpec::point()
107                    .scaled("fill", "category_color")
108                    .fixed("stroke", Value::Color(rgb(0.0, 0.0, 0.0)))
109                    .fixed("size", 6.0_f64),
110            ),
111    );
112
113    // ── Right side, legend #2 (stacks below #1): a size legend that
114    // shares the Right slot. `category_size` is trained to the same
115    // categories as `category_color`, so what keeps this a block of
116    // its own is the differing title; the per-side stacker then
117    // arranges both legends vertically.
118    p.add_legend(
119        Legend::new("category_size")
120            .side(LegendSide::Right)
121            .title("Size")
122            .key(
123                LegendKeySpec::point()
124                    .scaled("size", "category_size")
125                    .fixed("fill", Value::Color(rgb(0.25, 0.25, 0.25))),
126            ),
127    );
128
129    // ── Right side, legend #3: a continuous gradient colorbar
130    // driven by `gradient_scale`. Stacks below the discrete ones
131    // via the same per-side stacker. The colorbar also pulls its
132    // opacity from `gradient_opacity` — same domain → low values are
133    // mostly transparent, high values fully opaque.
134    p.add_legend(
135        Legend::colorbar("gradient_scale")
136            .side(LegendSide::Right)
137            .title("Gradient")
138            .scaled("fill_opacity", "gradient_opacity"),
139    );
140
141    // ── Bottom side, legend #3: same gradient scale but rendered
142    // as discrete steps at each break — useful for showing a
143    // binned colour mapping or any colorbar you want to read as
144    // categorical bands. Routed to the Bottom slot so it stacks
145    // horizontally alongside the existing "Pattern" + "Colour"
146    // legends.
147    p.add_legend(
148        Legend::colorbar("gradient_scale")
149            .side(LegendSide::Bottom)
150            .title("Steps")
151            .binned()
152            .open_upper(),
153    );
154
155    // ── Bottom side, legend #4: a **binned stack** that varies
156    // SHAPE per bin (instead of colour like the stepped colorbar
157    // next to it). Six bin boundaries from `gradient_scale` → five
158    // bins, each rendered as a different emoji-glyph shape via the
159    // `bin_shape` scale (resolved against the plot's shape
160    // registry). Demonstrates that the same legend key wiring
161    // works with vector and glyph-backed shapes.
162    //
163    // Emoji glyphs fill their em-bbox almost completely (Latin
164    // letters and vector circles only fill ~70–80 % of the same
165    // bbox), so a 12pt emoji reads at roughly the same visual
166    // weight as the 16pt circle markers used by the other legends.
167    p.add_legend(
168        Legend::new("gradient_scale")
169            .side(LegendSide::Bottom)
170            .title("Bins")
171            .binned()
172            .equal_bins()
173            .key(
174                LegendKeySpec::point()
175                    .scaled("shape", "bin_shape")
176                    .fixed("fill", Value::Color(rgb(0.15, 0.15, 0.15)))
177                    .fixed("size", 12.0_f64),
178            ),
179    );
180
181    // ── Left side: a text key. The swatch is a glyph sample rather
182    // than a marker, so a font-size scale shows what it actually
183    // does to type. `size` is the font size in pt and `fill` is the
184    // ink; both resolve per row through their own scale.
185    p.add_legend(
186        Legend::new("category_size")
187            .side(LegendSide::Left)
188            .title("Font size")
189            .key(
190                LegendKeySpec::text()
191                    .scaled("size", "category_size")
192                    .scaled("fill", "category_color")
193                    .fixed("text", Value::String(Arc::from("Aa"))),
194            ),
195    );
196
197    // ── Bottom side: two legends side-by-side (horizontal stack).
198    p.add_legend(
199        Legend::new("category_line")
200            .side(LegendSide::Bottom)
201            .title("Pattern")
202            .key(
203                LegendKeySpec::line()
204                    .scaled("linetype", "category_line")
205                    .fixed("linewidth", 1.5_f64),
206            ),
207    );
208    p.add_legend(
209        Legend::new("category_color")
210            .side(LegendSide::Bottom)
211            .title("Colour")
212            .key(LegendKeySpec::rect().scaled("fill", "category_color")),
213    );
214
215    // ── In-panel overlay: a compact category legend pinned to the
216    // top-right corner of the panel area. Reserves no chrome space —
217    // the data marks beneath continue to occupy the full panel rect.
218    p.add_legend(
219        Legend::new("category_color")
220            .side(LegendSide::InPanel {
221                anchor: Anchor::TopRight,
222                inset_pt: 8.0,
223            })
224            .title("Overlay")
225            .key(
226                LegendKeySpec::point()
227                    .scaled("fill", "category_color")
228                    .fixed("size", 6.0_f64),
229            ),
230    );
231
232    let cat_values: Vec<Value> = cats.iter().map(|s| Value::String(Arc::from(*s))).collect();
233    let line_cats: [&'static str; 3] = ["Solid", "Dashed", "Dotted"];
234    let line_values: Vec<Value> = line_cats
235        .iter()
236        .map(|s| Value::String(Arc::from(*s)))
237        .collect();
238
239    let mut view = PlotComposition::new(&comp())
240        .add_scale("x", scale::continuous(0.0..=10.0))
241        .add_scale("y", scale::continuous(0.0..=1.0))
242        .add_scale(
243            "category_color",
244            scale::discrete(cat_values.clone()).range_colors([
245                rgb8(220, 90, 70),
246                rgb8(70, 160, 90),
247                rgb8(70, 120, 220),
248                rgb8(180, 120, 200),
249            ]),
250        )
251        .add_scale(
252            "category_size",
253            scale::discrete(cat_values).range_numbers([4.0, 8.0, 12.0, 16.0]),
254        )
255        .add_scale(
256            "category_line",
257            scale::discrete(line_values).range_linetypes([solid(), dashed(), dotted()]),
258        )
259        .add_scale(
260            "gradient_scale",
261            scale::continuous(0.0..=100.0).range_colors([
262                rgb8(20, 30, 90),
263                rgb8(60, 160, 200),
264                rgb8(230, 220, 100),
265                rgb8(220, 60, 40),
266            ]),
267        )
268        .add_scale(
269            "gradient_opacity",
270            scale::continuous(0.0..=100.0).range_numbers([0.1, 1.0]),
271        )
272        .add_scale(
273            "bin_shape",
274            scale::continuous(0.0..=100.0).range_strings([
275                Arc::from("droplet"),
276                Arc::from("herb"),
277                Arc::from("sun"),
278                Arc::from("wave"),
279                Arc::from("fire"),
280            ]),
281        );
282    view.attach_plot(p);
283
284    let issues = view.validate();
285    if !issues.is_empty() {
286        panic!("validate(): {issues:?}");
287    }
288
289    let mut renderer = VelloRenderer::new().expect("vello renderer init");
290    let bg: Color = rgb8(252, 252, 252);
291    {
292        let scene = renderer.scene();
293        scene.clear();
294        view.render(scene, Size::new(w as f64, h as f64), dpi);
295    }
296    let mut pixels = vec![0u8; (w * h * 4) as usize];
297    renderer
298        .render_to_buffer(w, h, bg, &mut pixels)
299        .expect("render");
300    let path = std::env::current_dir()
301        .unwrap()
302        .join("examples/legends.png");
303    hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
304    println!("wrote {}", path.display());
305}
Source

pub fn text() -> Self

Start a Text key with no aesthetic bindings. The glyph is DEFAULT_KEY_TEXT until a "text" aesthetic names another one.

Examples found in repository?
examples/legends.rs (line 190)
44fn main() {
45    let (w, h) = (900u32, 600u32);
46    let dpi = 96.0;
47
48    // ── Data ──
49    let n = 24;
50    let xs: Vec<f64> = (0..n).map(|i| i as f64 * 0.4).collect();
51    let ys: Vec<f64> = (0..n)
52        .map(|i| ((i as f64) * 0.5).sin() * 0.4 + 0.5)
53        .collect();
54    let cats: [&'static str; 4] = ["A", "B", "C", "D"];
55    let fill_col: Vec<&'static str> = (0..n).map(|i| cats[i % 4]).collect();
56    let size_col: Vec<&'static str> = (0..n).map(|i| cats[i % 4]).collect();
57
58    // Build the plot's shape registry with the built-in vector
59    // shapes plus a handful of emoji glyphs used by the binned
60    // legend below. Glyph shapes share the same registry surface
61    // as the vector ones (insert by name; look up by name) so the
62    // legend chrome can resolve either.
63    let glyph_style = TextStyle::new(16.0);
64    let mut shapes = ShapeRegistry::with_builtins();
65    // U+1F4A7 droplet, U+1F31E sun, U+1F525 fire, U+1F33F herb,
66    // U+1F30A wave — picked to read as a "low → high" intensity
67    // ramp matching the gradient_scale values.
68    shapes.insert("droplet", glyph_marker("\u{1F4A7}", &glyph_style));
69    shapes.insert("herb", glyph_marker("\u{1F33F}", &glyph_style));
70    shapes.insert("sun", glyph_marker("\u{1F31E}", &glyph_style));
71    shapes.insert("wave", glyph_marker("\u{1F30A}", &glyph_style));
72    shapes.insert("fire", glyph_marker("\u{1F525}", &glyph_style));
73
74    let mut p = Plot::new(&comp(), "panel")
75        .bind("x", "x")
76        .bind("y", "y")
77        .bind("fill", "category_color")
78        .bind("size", "category_size")
79        .shape_registry(shapes);
80    p.add_geom(
81        PointGeom::builder()
82            .set("x", xs)
83            .set("y", ys)
84            .set("fill", fill_col)
85            .set("size", size_col)
86            .build(),
87    );
88    p.add_axis(Axis::rail("x", AxisPlacement::Cartesian(AxisSide::Bottom)));
89    p.add_axis(Axis::rail("y", AxisPlacement::Cartesian(AxisSide::Left)));
90
91    // ── Right side, legend #1: line + point, both driven by
92    // category_color. Attached as two legends; they share a side,
93    // title and domain scale, so the render-time collapse folds the
94    // second one's key into the first.
95    p.add_legend(
96        Legend::new("category_color")
97            .side(LegendSide::Right)
98            .title("Category")
99            .key(LegendKeySpec::line().scaled("stroke", "category_color")),
100    );
101    p.add_legend(
102        Legend::new("category_color")
103            .side(LegendSide::Right)
104            .title("Category")
105            .key(
106                LegendKeySpec::point()
107                    .scaled("fill", "category_color")
108                    .fixed("stroke", Value::Color(rgb(0.0, 0.0, 0.0)))
109                    .fixed("size", 6.0_f64),
110            ),
111    );
112
113    // ── Right side, legend #2 (stacks below #1): a size legend that
114    // shares the Right slot. `category_size` is trained to the same
115    // categories as `category_color`, so what keeps this a block of
116    // its own is the differing title; the per-side stacker then
117    // arranges both legends vertically.
118    p.add_legend(
119        Legend::new("category_size")
120            .side(LegendSide::Right)
121            .title("Size")
122            .key(
123                LegendKeySpec::point()
124                    .scaled("size", "category_size")
125                    .fixed("fill", Value::Color(rgb(0.25, 0.25, 0.25))),
126            ),
127    );
128
129    // ── Right side, legend #3: a continuous gradient colorbar
130    // driven by `gradient_scale`. Stacks below the discrete ones
131    // via the same per-side stacker. The colorbar also pulls its
132    // opacity from `gradient_opacity` — same domain → low values are
133    // mostly transparent, high values fully opaque.
134    p.add_legend(
135        Legend::colorbar("gradient_scale")
136            .side(LegendSide::Right)
137            .title("Gradient")
138            .scaled("fill_opacity", "gradient_opacity"),
139    );
140
141    // ── Bottom side, legend #3: same gradient scale but rendered
142    // as discrete steps at each break — useful for showing a
143    // binned colour mapping or any colorbar you want to read as
144    // categorical bands. Routed to the Bottom slot so it stacks
145    // horizontally alongside the existing "Pattern" + "Colour"
146    // legends.
147    p.add_legend(
148        Legend::colorbar("gradient_scale")
149            .side(LegendSide::Bottom)
150            .title("Steps")
151            .binned()
152            .open_upper(),
153    );
154
155    // ── Bottom side, legend #4: a **binned stack** that varies
156    // SHAPE per bin (instead of colour like the stepped colorbar
157    // next to it). Six bin boundaries from `gradient_scale` → five
158    // bins, each rendered as a different emoji-glyph shape via the
159    // `bin_shape` scale (resolved against the plot's shape
160    // registry). Demonstrates that the same legend key wiring
161    // works with vector and glyph-backed shapes.
162    //
163    // Emoji glyphs fill their em-bbox almost completely (Latin
164    // letters and vector circles only fill ~70–80 % of the same
165    // bbox), so a 12pt emoji reads at roughly the same visual
166    // weight as the 16pt circle markers used by the other legends.
167    p.add_legend(
168        Legend::new("gradient_scale")
169            .side(LegendSide::Bottom)
170            .title("Bins")
171            .binned()
172            .equal_bins()
173            .key(
174                LegendKeySpec::point()
175                    .scaled("shape", "bin_shape")
176                    .fixed("fill", Value::Color(rgb(0.15, 0.15, 0.15)))
177                    .fixed("size", 12.0_f64),
178            ),
179    );
180
181    // ── Left side: a text key. The swatch is a glyph sample rather
182    // than a marker, so a font-size scale shows what it actually
183    // does to type. `size` is the font size in pt and `fill` is the
184    // ink; both resolve per row through their own scale.
185    p.add_legend(
186        Legend::new("category_size")
187            .side(LegendSide::Left)
188            .title("Font size")
189            .key(
190                LegendKeySpec::text()
191                    .scaled("size", "category_size")
192                    .scaled("fill", "category_color")
193                    .fixed("text", Value::String(Arc::from("Aa"))),
194            ),
195    );
196
197    // ── Bottom side: two legends side-by-side (horizontal stack).
198    p.add_legend(
199        Legend::new("category_line")
200            .side(LegendSide::Bottom)
201            .title("Pattern")
202            .key(
203                LegendKeySpec::line()
204                    .scaled("linetype", "category_line")
205                    .fixed("linewidth", 1.5_f64),
206            ),
207    );
208    p.add_legend(
209        Legend::new("category_color")
210            .side(LegendSide::Bottom)
211            .title("Colour")
212            .key(LegendKeySpec::rect().scaled("fill", "category_color")),
213    );
214
215    // ── In-panel overlay: a compact category legend pinned to the
216    // top-right corner of the panel area. Reserves no chrome space —
217    // the data marks beneath continue to occupy the full panel rect.
218    p.add_legend(
219        Legend::new("category_color")
220            .side(LegendSide::InPanel {
221                anchor: Anchor::TopRight,
222                inset_pt: 8.0,
223            })
224            .title("Overlay")
225            .key(
226                LegendKeySpec::point()
227                    .scaled("fill", "category_color")
228                    .fixed("size", 6.0_f64),
229            ),
230    );
231
232    let cat_values: Vec<Value> = cats.iter().map(|s| Value::String(Arc::from(*s))).collect();
233    let line_cats: [&'static str; 3] = ["Solid", "Dashed", "Dotted"];
234    let line_values: Vec<Value> = line_cats
235        .iter()
236        .map(|s| Value::String(Arc::from(*s)))
237        .collect();
238
239    let mut view = PlotComposition::new(&comp())
240        .add_scale("x", scale::continuous(0.0..=10.0))
241        .add_scale("y", scale::continuous(0.0..=1.0))
242        .add_scale(
243            "category_color",
244            scale::discrete(cat_values.clone()).range_colors([
245                rgb8(220, 90, 70),
246                rgb8(70, 160, 90),
247                rgb8(70, 120, 220),
248                rgb8(180, 120, 200),
249            ]),
250        )
251        .add_scale(
252            "category_size",
253            scale::discrete(cat_values).range_numbers([4.0, 8.0, 12.0, 16.0]),
254        )
255        .add_scale(
256            "category_line",
257            scale::discrete(line_values).range_linetypes([solid(), dashed(), dotted()]),
258        )
259        .add_scale(
260            "gradient_scale",
261            scale::continuous(0.0..=100.0).range_colors([
262                rgb8(20, 30, 90),
263                rgb8(60, 160, 200),
264                rgb8(230, 220, 100),
265                rgb8(220, 60, 40),
266            ]),
267        )
268        .add_scale(
269            "gradient_opacity",
270            scale::continuous(0.0..=100.0).range_numbers([0.1, 1.0]),
271        )
272        .add_scale(
273            "bin_shape",
274            scale::continuous(0.0..=100.0).range_strings([
275                Arc::from("droplet"),
276                Arc::from("herb"),
277                Arc::from("sun"),
278                Arc::from("wave"),
279                Arc::from("fire"),
280            ]),
281        );
282    view.attach_plot(p);
283
284    let issues = view.validate();
285    if !issues.is_empty() {
286        panic!("validate(): {issues:?}");
287    }
288
289    let mut renderer = VelloRenderer::new().expect("vello renderer init");
290    let bg: Color = rgb8(252, 252, 252);
291    {
292        let scene = renderer.scene();
293        scene.clear();
294        view.render(scene, Size::new(w as f64, h as f64), dpi);
295    }
296    let mut pixels = vec![0u8; (w * h * 4) as usize];
297    renderer
298        .render_to_buffer(w, h, bg, &mut pixels)
299        .expect("render");
300    let path = std::env::current_dir()
301        .unwrap()
302        .join("examples/legends.png");
303    hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
304    println!("wrote {}", path.display());
305}
Source

pub fn scaled( self, aesthetic: impl Into<String>, scale_name: impl Into<String>, ) -> Self

Pull this aesthetic from scale_name at the row’s domain value.

Examples found in repository?
examples/rich_text_chrome.rs (line 82)
38fn main() {
39    let (w, h) = (900u32, 560u32);
40    let dpi = 96.0;
41    let bg: Color = rgb8(250, 250, 253);
42
43    let comp = || Composition::empty(1, 1).place(1, 1, Span::cell(), Patch::new("p"));
44
45    let n = 60;
46    let xs: Vec<f64> = (0..n).map(|i| i as f64 / (n - 1) as f64 * 10.0).collect();
47    let ys: Vec<f64> = xs
48        .iter()
49        .map(|x| 2.5 + 1.2 * (x * 0.7).sin() + 0.4 * (x * 2.1).cos())
50        .collect();
51
52    // Break labels are data-derived, so a category that spells
53    // markdown gets parsed like any other string.
54    let bands: [&'static str; 3] = ["*low*", "**mid**", "{.red high}"];
55    let groups: Vec<&'static str> = ys
56        .iter()
57        .map(|y| match *y {
58            v if v < 2.0 => bands[0],
59            v if v < 3.0 => bands[1],
60            _ => bands[2],
61        })
62        .collect();
63
64    let mut plot = Plot::new(&comp(), "p")
65        .bind("x", "x")
66        .bind("y", "y")
67        .bind("fill", "band")
68        .title("Trend of **{#c14b4b price}** across the day")
69        .subtitle("Metric: *closing_bid* — sampled hourly")
70        .caption("n = **60**, source: {.gray internal}");
71    plot.add_geom(
72        PointGeom::builder()
73            .set("x", xs)
74            .set("y", ys)
75            .set("fill", groups)
76            .set("size", 8.0_f64)
77            .build(),
78    );
79    plot.add_legend(
80        Legend::new("band")
81            .title("**band** of the *close*")
82            .key(LegendKeySpec::point().scaled("fill", "band")),
83    );
84    plot.add_axis(
85        Axis::rail("x", AxisPlacement::Cartesian(AxisSide::Bottom))
86            .title("hour of day, {.gray *UTC*}"),
87    );
88    plot.add_axis(
89        Axis::rail("y", AxisPlacement::Cartesian(AxisSide::Left)).title("**price** (USD)"),
90    );
91
92    let mut view = PlotComposition::new(&comp())
93        .theme(markdown_chrome_theme())
94        .add_scale("x", scale::continuous(0.0..=10.0))
95        .add_scale("y", scale::continuous(0.0..=5.0))
96        .add_scale(
97            "band",
98            scale::discrete(bands.iter().map(|b| Value::String(Arc::from(*b)))).range_colors([
99                rgb8(88, 106, 195),
100                rgb8(214, 146, 60),
101                rgb8(193, 75, 75),
102            ]),
103        )
104        .with_plot(plot);
105
106    let mut renderer = VelloRenderer::new().expect("vello renderer init");
107    {
108        let scene = renderer.scene();
109        scene.clear();
110        view.render(scene, Size::new(w as f64, h as f64), dpi);
111    }
112    let mut pixels = vec![0u8; (w * h * 4) as usize];
113    renderer
114        .render_to_buffer(w, h, bg, &mut pixels)
115        .expect("render");
116    let path = std::env::current_dir()
117        .unwrap()
118        .join("examples/rich_text_chrome.png");
119    hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
120    println!("wrote {}", path.display());
121}
More examples
Hide additional examples
examples/theme_legend_variants.rs (line 61)
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}
examples/faceted.rs (line 107)
58fn main() {
59    let (w, h) = (1400u32, 700u32);
60    let dpi = 96.0;
61
62    let xs: Vec<f64> = (0..50).map(|i| i as f64 * 2.0).collect();
63    let make = |phase: f64, amp: f64| -> Vec<f64> {
64        xs.iter()
65            .map(|x| 50.0 + amp * (x * 0.05 + phase).sin())
66            .collect()
67    };
68
69    let datasets = [
70        ("q1", make(0.0, 25.0), rgb8(220, 90, 70)),
71        ("q2", make(1.5, 18.0), rgb8(70, 120, 220)),
72        ("q3", make(3.0, 22.0), rgb8(70, 180, 120)),
73        ("q4", make(4.5, 28.0), rgb8(180, 130, 80)),
74        ("summary", make(0.0, 35.0), rgb8(130, 80, 180)),
75    ];
76
77    let mut renderer = VelloRenderer::new().expect("vello renderer init");
78    let bg: Color = rgb8(248, 248, 252);
79
80    // ── Renders 1 & 2: shared "time" scale across the unlocked layout
81    {
82        #[allow(unused_mut)]
83        let mut view = PlotComposition::new(&comp_shape(None))
84            .add_scale("time", scale::continuous(0.0..=100.0))
85            .add_scale("y", scale::continuous(0.0..=100.0))
86            .title("Sensor array")
87            .subtitle("Four quadrants and a summary, sharing one time scale")
88            .caption("Composition-level chrome spans every panel");
89        attach_all(&mut view, &xs, &datasets);
90
91        // One axis title and one legend for the whole grid, set on the
92        // composition rather than on any single plot. The legend reads
93        // its rows from the "series" scale's domain; no plot needs to
94        // bind that scale for the legend to resolve it.
95        let mut view = {
96            let series: Vec<Value> = datasets.iter().map(|(id, _, _)| Value::from(*id)).collect();
97            let colors: Vec<Color> = datasets.iter().map(|(_, _, c)| *c).collect();
98            let mut view = view
99                .add_scale("series", scale::discrete(series).range_colors(colors))
100                .axis_title(AxisSide::Bottom, "Time (s)");
101            view.add_legend(
102                Legend::new("series")
103                    .side(LegendSide::Right)
104                    .title("Series")
105                    .key(
106                        LegendKeySpec::point()
107                            .scaled("fill", "series")
108                            .fixed("size", 6.0_f64),
109                    ),
110            );
111            view
112        };
113
114        let issues = view.validate();
115        if !issues.is_empty() {
116            panic!("validate() reported issues: {issues:?}");
117        }
118
119        render_to(
120            &mut renderer,
121            &mut view,
122            w,
123            h,
124            dpi,
125            bg,
126            "examples/faceted_1_initial.png",
127        );
128
129        view.update_scale("time", |s| s.set_domain_continuous(20.0, 60.0));
130        render_to(
131            &mut renderer,
132            &mut view,
133            w,
134            h,
135            dpi,
136            bg,
137            "examples/faceted_2_shared_zoom.png",
138        );
139    }
140
141    // ── Render 3: aspect-locked. Outer `.aspect(1, 1)` propagates to
142    //    every leaf panel; selective respect on the layout solver
143    //    couples panel col/row at the locked ratio and lets unmarked
144    //    fr tracks absorb slack. Wider viewport (1800×600) makes the
145    //    lock visually obvious — without it, the 1×2 outer would give
146    //    half the width to each side; with it, the leaf panels land
147    //    at the locked ratio and the surrounding tracks soak up the
148    //    horizontal slack.
149    {
150        let (lw, lh) = (1800u32, 600u32);
151        let mut view = PlotComposition::new(&comp_shape(Some((1.0, 1.0))))
152            .add_scale("time", scale::continuous(20.0..=60.0))
153            .add_scale("y", scale::continuous(0.0..=100.0));
154        attach_all(&mut view, &xs, &datasets);
155        render_to(
156            &mut renderer,
157            &mut view,
158            lw,
159            lh,
160            dpi,
161            bg,
162            "examples/faceted_3_aspect_locked.png",
163        );
164    }
165}
examples/theme_text_align_to.rs (line 70)
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}
examples/theme_plot_background.rs (line 91)
18fn main() {
19    let (w, h) = (900u32, 600u32);
20    let dpi = 96.0;
21
22    let comp = || Composition::empty(1, 1).place(1, 1, Span::cell(), Patch::new("p"));
23    // Generate data that explicitly reaches the corners so the
24    // rounded clip is visibly exercised. The dense corner clusters
25    // get cropped by the rounded panel boundary.
26    let mut xs: Vec<f64> = Vec::new();
27    let mut ys: Vec<f64> = Vec::new();
28    for i in 0..40 {
29        let t = i as f64 / 39.0;
30        xs.push(t * 6.0);
31        ys.push((t * 4.4).sin() * 0.4 + 0.5);
32    }
33    // Add a corner-hugging cluster at all four corners to make clip
34    // behaviour visible.
35    for (cx, cy) in &[(0.05, 0.97), (5.95, 0.97), (0.05, 0.03), (5.95, 0.03)] {
36        for di in 0..8 {
37            let theta = di as f64 * 0.3;
38            xs.push(cx + theta.cos() * 0.15);
39            ys.push(cy + theta.sin() * 0.04);
40        }
41    }
42
43    // Use the y values themselves as the colour-mapped channel so a
44    // colorbar legend makes sense.
45    let colours: Vec<f64> = ys.clone();
46    // A second, discrete category column (assigned by x bucket),
47    // used to drive the categorical stroke colour and a discrete
48    // legend on the right next to the colorbar.
49    let categories: Vec<&str> = xs
50        .iter()
51        .map(|x| match (*x as usize) % 4 {
52            0 => "A",
53            1 => "B",
54            2 => "C",
55            _ => "D",
56        })
57        .collect();
58    let mut plot = Plot::new(&comp(), "p")
59        .bind("x", "x_scale")
60        .bind("y", "y_scale")
61        .bind("fill", "fill_scale")
62        .bind("stroke", "stroke_scale")
63        .title("Rounded corners — plot bg, panel bg, frames");
64    plot.add_geom(
65        PointGeom::builder()
66            .set("x", xs)
67            .set("y", ys)
68            .set("size", 8.0_f64)
69            .set("fill", colours)
70            .set("stroke", categories)
71            .set("linewidth", 1.0_f64)
72            .build(),
73    );
74    plot.add_axis(Axis::rail(
75        "x_scale",
76        AxisPlacement::Cartesian(AxisSide::Bottom),
77    ));
78    plot.add_axis(Axis::rail(
79        "y_scale",
80        AxisPlacement::Cartesian(AxisSide::Left),
81    ));
82    plot.add_legend(
83        Legend::colorbar("fill_scale")
84            .side(LegendSide::Right)
85            .title("Amplitude"),
86    );
87    plot.add_legend(
88        Legend::new("stroke_scale")
89            .side(LegendSide::Right)
90            .title("Group")
91            .key(LegendKeySpec::point().scaled("stroke", "stroke_scale")),
92    );
93
94    // Two outer bands: 24pt `plot_margin` sits outside the
95    // background; 18pt `plot_padding` sits inside it. Both feed the
96    // anatomical ring tracks, so chrome lands in the correct rhythm
97    // automatically.
98    let theme = Theme {
99        plot_margin: Margin::all(Length::Abs(24.0)),
100        plot_padding: Margin::all(Length::Abs(18.0)),
101        plot_background: Element::Set(RectElement {
102            fill: Some(ThemeColor::mix(ThemeColor::Paper, ThemeColor::Accent, 0.3)),
103            color: Some(ThemeColor::Ink),
104            linewidth_pt: Some(Length::Abs(2.0)),
105            corner_radius: Some(Length::Abs(12.0)),
106            ..RectElement::default()
107        }),
108        // Round the panel corners too — the geom clip mask uses the
109        // same rounded path so data points crop cleanly.
110        panel_background: Element::Set(RectElement {
111            corner_radius: Some(Length::Abs(8.0)),
112            ..Theme::default().panel_background.as_set().unwrap().clone()
113        }),
114        // Colorbar bar + discrete key frames share `RectElement`
115        // semantics: fill paints under the inner content (gradient
116        // for the colorbar, marker for the key) so transparent
117        // colours show the frame fill; stroke + corner_radius paint
118        // on top.
119        legend: hephaestus::plot::theme::LegendTheme {
120            bar: hephaestus::plot::theme::BarTheme {
121                frame: Element::Set(RectElement {
122                    fill: Some(ThemeColor::mix(ThemeColor::Paper, ThemeColor::Ink, 0.08)),
123                    color: Some(ThemeColor::Ink),
124                    linewidth_pt: Some(Length::Abs(1.5)),
125                    corner_radius: Some(Length::Abs(6.0)),
126                    ..RectElement::default()
127                }),
128                ..hephaestus::plot::theme::BarTheme::default()
129            },
130            key: hephaestus::plot::theme::KeyTheme {
131                width: Length::Abs(20.0),
132                height: Length::Abs(20.0),
133                frame: Element::Set(RectElement {
134                    fill: Some(ThemeColor::mix(ThemeColor::Paper, ThemeColor::Ink, 0.08)),
135                    color: Some(ThemeColor::Ink),
136                    linewidth_pt: Some(Length::Abs(0.75)),
137                    corner_radius: Some(Length::Abs(4.0)),
138                    ..RectElement::default()
139                }),
140                ..hephaestus::plot::theme::KeyTheme::default()
141            },
142            ..Theme::default().legend
143        },
144        ..Theme::default()
145    };
146
147    let fill_scale = scale::continuous(0.0..=1.0).range_colors([
148        hephaestus::color::rgb(0.2, 0.3, 0.6),
149        hephaestus::color::rgb(0.85, 0.45, 0.2),
150    ]);
151    let stroke_scale = scale::discrete([
152        hephaestus::scales::Value::String(std::sync::Arc::from("A")),
153        hephaestus::scales::Value::String(std::sync::Arc::from("B")),
154        hephaestus::scales::Value::String(std::sync::Arc::from("C")),
155        hephaestus::scales::Value::String(std::sync::Arc::from("D")),
156    ])
157    .range_colors([
158        hephaestus::color::rgb(0.20, 0.20, 0.20),
159        hephaestus::color::rgb(0.70, 0.20, 0.20),
160        hephaestus::color::rgb(0.20, 0.60, 0.20),
161        hephaestus::color::rgb(0.20, 0.20, 0.70),
162    ]);
163    let mut view = PlotComposition::new(&comp())
164        .add_scale("x_scale", scale::continuous(0.0..=6.0))
165        .add_scale("y_scale", scale::continuous(0.0..=1.0))
166        .add_scale("fill_scale", fill_scale)
167        .add_scale("stroke_scale", stroke_scale)
168        .theme(theme);
169    view.attach_plot(plot);
170
171    let mut renderer = VelloRenderer::new().expect("vello renderer init");
172    // Contrasting canvas bg makes the plot_margin band visible
173    // outside the (warm-cream) plot_background.
174    let bg: Color = rgb8(60, 70, 90);
175    {
176        let scene = renderer.scene();
177        scene.clear();
178        view.render(scene, Size::new(w as f64, h as f64), dpi);
179    }
180    let mut pixels = vec![0u8; (w * h * 4) as usize];
181    renderer
182        .render_to_buffer(w, h, bg, &mut pixels)
183        .expect("render");
184    let path = std::env::current_dir()
185        .unwrap()
186        .join("examples/theme_plot_background.png");
187    hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
188    println!("wrote {}", path.display());
189}
examples/legends.rs (line 99)
44fn main() {
45    let (w, h) = (900u32, 600u32);
46    let dpi = 96.0;
47
48    // ── Data ──
49    let n = 24;
50    let xs: Vec<f64> = (0..n).map(|i| i as f64 * 0.4).collect();
51    let ys: Vec<f64> = (0..n)
52        .map(|i| ((i as f64) * 0.5).sin() * 0.4 + 0.5)
53        .collect();
54    let cats: [&'static str; 4] = ["A", "B", "C", "D"];
55    let fill_col: Vec<&'static str> = (0..n).map(|i| cats[i % 4]).collect();
56    let size_col: Vec<&'static str> = (0..n).map(|i| cats[i % 4]).collect();
57
58    // Build the plot's shape registry with the built-in vector
59    // shapes plus a handful of emoji glyphs used by the binned
60    // legend below. Glyph shapes share the same registry surface
61    // as the vector ones (insert by name; look up by name) so the
62    // legend chrome can resolve either.
63    let glyph_style = TextStyle::new(16.0);
64    let mut shapes = ShapeRegistry::with_builtins();
65    // U+1F4A7 droplet, U+1F31E sun, U+1F525 fire, U+1F33F herb,
66    // U+1F30A wave — picked to read as a "low → high" intensity
67    // ramp matching the gradient_scale values.
68    shapes.insert("droplet", glyph_marker("\u{1F4A7}", &glyph_style));
69    shapes.insert("herb", glyph_marker("\u{1F33F}", &glyph_style));
70    shapes.insert("sun", glyph_marker("\u{1F31E}", &glyph_style));
71    shapes.insert("wave", glyph_marker("\u{1F30A}", &glyph_style));
72    shapes.insert("fire", glyph_marker("\u{1F525}", &glyph_style));
73
74    let mut p = Plot::new(&comp(), "panel")
75        .bind("x", "x")
76        .bind("y", "y")
77        .bind("fill", "category_color")
78        .bind("size", "category_size")
79        .shape_registry(shapes);
80    p.add_geom(
81        PointGeom::builder()
82            .set("x", xs)
83            .set("y", ys)
84            .set("fill", fill_col)
85            .set("size", size_col)
86            .build(),
87    );
88    p.add_axis(Axis::rail("x", AxisPlacement::Cartesian(AxisSide::Bottom)));
89    p.add_axis(Axis::rail("y", AxisPlacement::Cartesian(AxisSide::Left)));
90
91    // ── Right side, legend #1: line + point, both driven by
92    // category_color. Attached as two legends; they share a side,
93    // title and domain scale, so the render-time collapse folds the
94    // second one's key into the first.
95    p.add_legend(
96        Legend::new("category_color")
97            .side(LegendSide::Right)
98            .title("Category")
99            .key(LegendKeySpec::line().scaled("stroke", "category_color")),
100    );
101    p.add_legend(
102        Legend::new("category_color")
103            .side(LegendSide::Right)
104            .title("Category")
105            .key(
106                LegendKeySpec::point()
107                    .scaled("fill", "category_color")
108                    .fixed("stroke", Value::Color(rgb(0.0, 0.0, 0.0)))
109                    .fixed("size", 6.0_f64),
110            ),
111    );
112
113    // ── Right side, legend #2 (stacks below #1): a size legend that
114    // shares the Right slot. `category_size` is trained to the same
115    // categories as `category_color`, so what keeps this a block of
116    // its own is the differing title; the per-side stacker then
117    // arranges both legends vertically.
118    p.add_legend(
119        Legend::new("category_size")
120            .side(LegendSide::Right)
121            .title("Size")
122            .key(
123                LegendKeySpec::point()
124                    .scaled("size", "category_size")
125                    .fixed("fill", Value::Color(rgb(0.25, 0.25, 0.25))),
126            ),
127    );
128
129    // ── Right side, legend #3: a continuous gradient colorbar
130    // driven by `gradient_scale`. Stacks below the discrete ones
131    // via the same per-side stacker. The colorbar also pulls its
132    // opacity from `gradient_opacity` — same domain → low values are
133    // mostly transparent, high values fully opaque.
134    p.add_legend(
135        Legend::colorbar("gradient_scale")
136            .side(LegendSide::Right)
137            .title("Gradient")
138            .scaled("fill_opacity", "gradient_opacity"),
139    );
140
141    // ── Bottom side, legend #3: same gradient scale but rendered
142    // as discrete steps at each break — useful for showing a
143    // binned colour mapping or any colorbar you want to read as
144    // categorical bands. Routed to the Bottom slot so it stacks
145    // horizontally alongside the existing "Pattern" + "Colour"
146    // legends.
147    p.add_legend(
148        Legend::colorbar("gradient_scale")
149            .side(LegendSide::Bottom)
150            .title("Steps")
151            .binned()
152            .open_upper(),
153    );
154
155    // ── Bottom side, legend #4: a **binned stack** that varies
156    // SHAPE per bin (instead of colour like the stepped colorbar
157    // next to it). Six bin boundaries from `gradient_scale` → five
158    // bins, each rendered as a different emoji-glyph shape via the
159    // `bin_shape` scale (resolved against the plot's shape
160    // registry). Demonstrates that the same legend key wiring
161    // works with vector and glyph-backed shapes.
162    //
163    // Emoji glyphs fill their em-bbox almost completely (Latin
164    // letters and vector circles only fill ~70–80 % of the same
165    // bbox), so a 12pt emoji reads at roughly the same visual
166    // weight as the 16pt circle markers used by the other legends.
167    p.add_legend(
168        Legend::new("gradient_scale")
169            .side(LegendSide::Bottom)
170            .title("Bins")
171            .binned()
172            .equal_bins()
173            .key(
174                LegendKeySpec::point()
175                    .scaled("shape", "bin_shape")
176                    .fixed("fill", Value::Color(rgb(0.15, 0.15, 0.15)))
177                    .fixed("size", 12.0_f64),
178            ),
179    );
180
181    // ── Left side: a text key. The swatch is a glyph sample rather
182    // than a marker, so a font-size scale shows what it actually
183    // does to type. `size` is the font size in pt and `fill` is the
184    // ink; both resolve per row through their own scale.
185    p.add_legend(
186        Legend::new("category_size")
187            .side(LegendSide::Left)
188            .title("Font size")
189            .key(
190                LegendKeySpec::text()
191                    .scaled("size", "category_size")
192                    .scaled("fill", "category_color")
193                    .fixed("text", Value::String(Arc::from("Aa"))),
194            ),
195    );
196
197    // ── Bottom side: two legends side-by-side (horizontal stack).
198    p.add_legend(
199        Legend::new("category_line")
200            .side(LegendSide::Bottom)
201            .title("Pattern")
202            .key(
203                LegendKeySpec::line()
204                    .scaled("linetype", "category_line")
205                    .fixed("linewidth", 1.5_f64),
206            ),
207    );
208    p.add_legend(
209        Legend::new("category_color")
210            .side(LegendSide::Bottom)
211            .title("Colour")
212            .key(LegendKeySpec::rect().scaled("fill", "category_color")),
213    );
214
215    // ── In-panel overlay: a compact category legend pinned to the
216    // top-right corner of the panel area. Reserves no chrome space —
217    // the data marks beneath continue to occupy the full panel rect.
218    p.add_legend(
219        Legend::new("category_color")
220            .side(LegendSide::InPanel {
221                anchor: Anchor::TopRight,
222                inset_pt: 8.0,
223            })
224            .title("Overlay")
225            .key(
226                LegendKeySpec::point()
227                    .scaled("fill", "category_color")
228                    .fixed("size", 6.0_f64),
229            ),
230    );
231
232    let cat_values: Vec<Value> = cats.iter().map(|s| Value::String(Arc::from(*s))).collect();
233    let line_cats: [&'static str; 3] = ["Solid", "Dashed", "Dotted"];
234    let line_values: Vec<Value> = line_cats
235        .iter()
236        .map(|s| Value::String(Arc::from(*s)))
237        .collect();
238
239    let mut view = PlotComposition::new(&comp())
240        .add_scale("x", scale::continuous(0.0..=10.0))
241        .add_scale("y", scale::continuous(0.0..=1.0))
242        .add_scale(
243            "category_color",
244            scale::discrete(cat_values.clone()).range_colors([
245                rgb8(220, 90, 70),
246                rgb8(70, 160, 90),
247                rgb8(70, 120, 220),
248                rgb8(180, 120, 200),
249            ]),
250        )
251        .add_scale(
252            "category_size",
253            scale::discrete(cat_values).range_numbers([4.0, 8.0, 12.0, 16.0]),
254        )
255        .add_scale(
256            "category_line",
257            scale::discrete(line_values).range_linetypes([solid(), dashed(), dotted()]),
258        )
259        .add_scale(
260            "gradient_scale",
261            scale::continuous(0.0..=100.0).range_colors([
262                rgb8(20, 30, 90),
263                rgb8(60, 160, 200),
264                rgb8(230, 220, 100),
265                rgb8(220, 60, 40),
266            ]),
267        )
268        .add_scale(
269            "gradient_opacity",
270            scale::continuous(0.0..=100.0).range_numbers([0.1, 1.0]),
271        )
272        .add_scale(
273            "bin_shape",
274            scale::continuous(0.0..=100.0).range_strings([
275                Arc::from("droplet"),
276                Arc::from("herb"),
277                Arc::from("sun"),
278                Arc::from("wave"),
279                Arc::from("fire"),
280            ]),
281        );
282    view.attach_plot(p);
283
284    let issues = view.validate();
285    if !issues.is_empty() {
286        panic!("validate(): {issues:?}");
287    }
288
289    let mut renderer = VelloRenderer::new().expect("vello renderer init");
290    let bg: Color = rgb8(252, 252, 252);
291    {
292        let scene = renderer.scene();
293        scene.clear();
294        view.render(scene, Size::new(w as f64, h as f64), dpi);
295    }
296    let mut pixels = vec![0u8; (w * h * 4) as usize];
297    renderer
298        .render_to_buffer(w, h, bg, &mut pixels)
299        .expect("render");
300    let path = std::env::current_dir()
301        .unwrap()
302        .join("examples/legends.png");
303    hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
304    println!("wrote {}", path.display());
305}
Source

pub fn fixed( self, aesthetic: impl Into<String>, value: impl Into<Value>, ) -> Self

Pin this aesthetic to a fixed value across every row.

Examples found in repository?
examples/theme_legend_variants.rs (line 62)
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}
More examples
Hide additional examples
examples/faceted.rs (line 108)
58fn main() {
59    let (w, h) = (1400u32, 700u32);
60    let dpi = 96.0;
61
62    let xs: Vec<f64> = (0..50).map(|i| i as f64 * 2.0).collect();
63    let make = |phase: f64, amp: f64| -> Vec<f64> {
64        xs.iter()
65            .map(|x| 50.0 + amp * (x * 0.05 + phase).sin())
66            .collect()
67    };
68
69    let datasets = [
70        ("q1", make(0.0, 25.0), rgb8(220, 90, 70)),
71        ("q2", make(1.5, 18.0), rgb8(70, 120, 220)),
72        ("q3", make(3.0, 22.0), rgb8(70, 180, 120)),
73        ("q4", make(4.5, 28.0), rgb8(180, 130, 80)),
74        ("summary", make(0.0, 35.0), rgb8(130, 80, 180)),
75    ];
76
77    let mut renderer = VelloRenderer::new().expect("vello renderer init");
78    let bg: Color = rgb8(248, 248, 252);
79
80    // ── Renders 1 & 2: shared "time" scale across the unlocked layout
81    {
82        #[allow(unused_mut)]
83        let mut view = PlotComposition::new(&comp_shape(None))
84            .add_scale("time", scale::continuous(0.0..=100.0))
85            .add_scale("y", scale::continuous(0.0..=100.0))
86            .title("Sensor array")
87            .subtitle("Four quadrants and a summary, sharing one time scale")
88            .caption("Composition-level chrome spans every panel");
89        attach_all(&mut view, &xs, &datasets);
90
91        // One axis title and one legend for the whole grid, set on the
92        // composition rather than on any single plot. The legend reads
93        // its rows from the "series" scale's domain; no plot needs to
94        // bind that scale for the legend to resolve it.
95        let mut view = {
96            let series: Vec<Value> = datasets.iter().map(|(id, _, _)| Value::from(*id)).collect();
97            let colors: Vec<Color> = datasets.iter().map(|(_, _, c)| *c).collect();
98            let mut view = view
99                .add_scale("series", scale::discrete(series).range_colors(colors))
100                .axis_title(AxisSide::Bottom, "Time (s)");
101            view.add_legend(
102                Legend::new("series")
103                    .side(LegendSide::Right)
104                    .title("Series")
105                    .key(
106                        LegendKeySpec::point()
107                            .scaled("fill", "series")
108                            .fixed("size", 6.0_f64),
109                    ),
110            );
111            view
112        };
113
114        let issues = view.validate();
115        if !issues.is_empty() {
116            panic!("validate() reported issues: {issues:?}");
117        }
118
119        render_to(
120            &mut renderer,
121            &mut view,
122            w,
123            h,
124            dpi,
125            bg,
126            "examples/faceted_1_initial.png",
127        );
128
129        view.update_scale("time", |s| s.set_domain_continuous(20.0, 60.0));
130        render_to(
131            &mut renderer,
132            &mut view,
133            w,
134            h,
135            dpi,
136            bg,
137            "examples/faceted_2_shared_zoom.png",
138        );
139    }
140
141    // ── Render 3: aspect-locked. Outer `.aspect(1, 1)` propagates to
142    //    every leaf panel; selective respect on the layout solver
143    //    couples panel col/row at the locked ratio and lets unmarked
144    //    fr tracks absorb slack. Wider viewport (1800×600) makes the
145    //    lock visually obvious — without it, the 1×2 outer would give
146    //    half the width to each side; with it, the leaf panels land
147    //    at the locked ratio and the surrounding tracks soak up the
148    //    horizontal slack.
149    {
150        let (lw, lh) = (1800u32, 600u32);
151        let mut view = PlotComposition::new(&comp_shape(Some((1.0, 1.0))))
152            .add_scale("time", scale::continuous(20.0..=60.0))
153            .add_scale("y", scale::continuous(0.0..=100.0));
154        attach_all(&mut view, &xs, &datasets);
155        render_to(
156            &mut renderer,
157            &mut view,
158            lw,
159            lh,
160            dpi,
161            bg,
162            "examples/faceted_3_aspect_locked.png",
163        );
164    }
165}
examples/legends.rs (line 108)
44fn main() {
45    let (w, h) = (900u32, 600u32);
46    let dpi = 96.0;
47
48    // ── Data ──
49    let n = 24;
50    let xs: Vec<f64> = (0..n).map(|i| i as f64 * 0.4).collect();
51    let ys: Vec<f64> = (0..n)
52        .map(|i| ((i as f64) * 0.5).sin() * 0.4 + 0.5)
53        .collect();
54    let cats: [&'static str; 4] = ["A", "B", "C", "D"];
55    let fill_col: Vec<&'static str> = (0..n).map(|i| cats[i % 4]).collect();
56    let size_col: Vec<&'static str> = (0..n).map(|i| cats[i % 4]).collect();
57
58    // Build the plot's shape registry with the built-in vector
59    // shapes plus a handful of emoji glyphs used by the binned
60    // legend below. Glyph shapes share the same registry surface
61    // as the vector ones (insert by name; look up by name) so the
62    // legend chrome can resolve either.
63    let glyph_style = TextStyle::new(16.0);
64    let mut shapes = ShapeRegistry::with_builtins();
65    // U+1F4A7 droplet, U+1F31E sun, U+1F525 fire, U+1F33F herb,
66    // U+1F30A wave — picked to read as a "low → high" intensity
67    // ramp matching the gradient_scale values.
68    shapes.insert("droplet", glyph_marker("\u{1F4A7}", &glyph_style));
69    shapes.insert("herb", glyph_marker("\u{1F33F}", &glyph_style));
70    shapes.insert("sun", glyph_marker("\u{1F31E}", &glyph_style));
71    shapes.insert("wave", glyph_marker("\u{1F30A}", &glyph_style));
72    shapes.insert("fire", glyph_marker("\u{1F525}", &glyph_style));
73
74    let mut p = Plot::new(&comp(), "panel")
75        .bind("x", "x")
76        .bind("y", "y")
77        .bind("fill", "category_color")
78        .bind("size", "category_size")
79        .shape_registry(shapes);
80    p.add_geom(
81        PointGeom::builder()
82            .set("x", xs)
83            .set("y", ys)
84            .set("fill", fill_col)
85            .set("size", size_col)
86            .build(),
87    );
88    p.add_axis(Axis::rail("x", AxisPlacement::Cartesian(AxisSide::Bottom)));
89    p.add_axis(Axis::rail("y", AxisPlacement::Cartesian(AxisSide::Left)));
90
91    // ── Right side, legend #1: line + point, both driven by
92    // category_color. Attached as two legends; they share a side,
93    // title and domain scale, so the render-time collapse folds the
94    // second one's key into the first.
95    p.add_legend(
96        Legend::new("category_color")
97            .side(LegendSide::Right)
98            .title("Category")
99            .key(LegendKeySpec::line().scaled("stroke", "category_color")),
100    );
101    p.add_legend(
102        Legend::new("category_color")
103            .side(LegendSide::Right)
104            .title("Category")
105            .key(
106                LegendKeySpec::point()
107                    .scaled("fill", "category_color")
108                    .fixed("stroke", Value::Color(rgb(0.0, 0.0, 0.0)))
109                    .fixed("size", 6.0_f64),
110            ),
111    );
112
113    // ── Right side, legend #2 (stacks below #1): a size legend that
114    // shares the Right slot. `category_size` is trained to the same
115    // categories as `category_color`, so what keeps this a block of
116    // its own is the differing title; the per-side stacker then
117    // arranges both legends vertically.
118    p.add_legend(
119        Legend::new("category_size")
120            .side(LegendSide::Right)
121            .title("Size")
122            .key(
123                LegendKeySpec::point()
124                    .scaled("size", "category_size")
125                    .fixed("fill", Value::Color(rgb(0.25, 0.25, 0.25))),
126            ),
127    );
128
129    // ── Right side, legend #3: a continuous gradient colorbar
130    // driven by `gradient_scale`. Stacks below the discrete ones
131    // via the same per-side stacker. The colorbar also pulls its
132    // opacity from `gradient_opacity` — same domain → low values are
133    // mostly transparent, high values fully opaque.
134    p.add_legend(
135        Legend::colorbar("gradient_scale")
136            .side(LegendSide::Right)
137            .title("Gradient")
138            .scaled("fill_opacity", "gradient_opacity"),
139    );
140
141    // ── Bottom side, legend #3: same gradient scale but rendered
142    // as discrete steps at each break — useful for showing a
143    // binned colour mapping or any colorbar you want to read as
144    // categorical bands. Routed to the Bottom slot so it stacks
145    // horizontally alongside the existing "Pattern" + "Colour"
146    // legends.
147    p.add_legend(
148        Legend::colorbar("gradient_scale")
149            .side(LegendSide::Bottom)
150            .title("Steps")
151            .binned()
152            .open_upper(),
153    );
154
155    // ── Bottom side, legend #4: a **binned stack** that varies
156    // SHAPE per bin (instead of colour like the stepped colorbar
157    // next to it). Six bin boundaries from `gradient_scale` → five
158    // bins, each rendered as a different emoji-glyph shape via the
159    // `bin_shape` scale (resolved against the plot's shape
160    // registry). Demonstrates that the same legend key wiring
161    // works with vector and glyph-backed shapes.
162    //
163    // Emoji glyphs fill their em-bbox almost completely (Latin
164    // letters and vector circles only fill ~70–80 % of the same
165    // bbox), so a 12pt emoji reads at roughly the same visual
166    // weight as the 16pt circle markers used by the other legends.
167    p.add_legend(
168        Legend::new("gradient_scale")
169            .side(LegendSide::Bottom)
170            .title("Bins")
171            .binned()
172            .equal_bins()
173            .key(
174                LegendKeySpec::point()
175                    .scaled("shape", "bin_shape")
176                    .fixed("fill", Value::Color(rgb(0.15, 0.15, 0.15)))
177                    .fixed("size", 12.0_f64),
178            ),
179    );
180
181    // ── Left side: a text key. The swatch is a glyph sample rather
182    // than a marker, so a font-size scale shows what it actually
183    // does to type. `size` is the font size in pt and `fill` is the
184    // ink; both resolve per row through their own scale.
185    p.add_legend(
186        Legend::new("category_size")
187            .side(LegendSide::Left)
188            .title("Font size")
189            .key(
190                LegendKeySpec::text()
191                    .scaled("size", "category_size")
192                    .scaled("fill", "category_color")
193                    .fixed("text", Value::String(Arc::from("Aa"))),
194            ),
195    );
196
197    // ── Bottom side: two legends side-by-side (horizontal stack).
198    p.add_legend(
199        Legend::new("category_line")
200            .side(LegendSide::Bottom)
201            .title("Pattern")
202            .key(
203                LegendKeySpec::line()
204                    .scaled("linetype", "category_line")
205                    .fixed("linewidth", 1.5_f64),
206            ),
207    );
208    p.add_legend(
209        Legend::new("category_color")
210            .side(LegendSide::Bottom)
211            .title("Colour")
212            .key(LegendKeySpec::rect().scaled("fill", "category_color")),
213    );
214
215    // ── In-panel overlay: a compact category legend pinned to the
216    // top-right corner of the panel area. Reserves no chrome space —
217    // the data marks beneath continue to occupy the full panel rect.
218    p.add_legend(
219        Legend::new("category_color")
220            .side(LegendSide::InPanel {
221                anchor: Anchor::TopRight,
222                inset_pt: 8.0,
223            })
224            .title("Overlay")
225            .key(
226                LegendKeySpec::point()
227                    .scaled("fill", "category_color")
228                    .fixed("size", 6.0_f64),
229            ),
230    );
231
232    let cat_values: Vec<Value> = cats.iter().map(|s| Value::String(Arc::from(*s))).collect();
233    let line_cats: [&'static str; 3] = ["Solid", "Dashed", "Dotted"];
234    let line_values: Vec<Value> = line_cats
235        .iter()
236        .map(|s| Value::String(Arc::from(*s)))
237        .collect();
238
239    let mut view = PlotComposition::new(&comp())
240        .add_scale("x", scale::continuous(0.0..=10.0))
241        .add_scale("y", scale::continuous(0.0..=1.0))
242        .add_scale(
243            "category_color",
244            scale::discrete(cat_values.clone()).range_colors([
245                rgb8(220, 90, 70),
246                rgb8(70, 160, 90),
247                rgb8(70, 120, 220),
248                rgb8(180, 120, 200),
249            ]),
250        )
251        .add_scale(
252            "category_size",
253            scale::discrete(cat_values).range_numbers([4.0, 8.0, 12.0, 16.0]),
254        )
255        .add_scale(
256            "category_line",
257            scale::discrete(line_values).range_linetypes([solid(), dashed(), dotted()]),
258        )
259        .add_scale(
260            "gradient_scale",
261            scale::continuous(0.0..=100.0).range_colors([
262                rgb8(20, 30, 90),
263                rgb8(60, 160, 200),
264                rgb8(230, 220, 100),
265                rgb8(220, 60, 40),
266            ]),
267        )
268        .add_scale(
269            "gradient_opacity",
270            scale::continuous(0.0..=100.0).range_numbers([0.1, 1.0]),
271        )
272        .add_scale(
273            "bin_shape",
274            scale::continuous(0.0..=100.0).range_strings([
275                Arc::from("droplet"),
276                Arc::from("herb"),
277                Arc::from("sun"),
278                Arc::from("wave"),
279                Arc::from("fire"),
280            ]),
281        );
282    view.attach_plot(p);
283
284    let issues = view.validate();
285    if !issues.is_empty() {
286        panic!("validate(): {issues:?}");
287    }
288
289    let mut renderer = VelloRenderer::new().expect("vello renderer init");
290    let bg: Color = rgb8(252, 252, 252);
291    {
292        let scene = renderer.scene();
293        scene.clear();
294        view.render(scene, Size::new(w as f64, h as f64), dpi);
295    }
296    let mut pixels = vec![0u8; (w * h * 4) as usize];
297    renderer
298        .render_to_buffer(w, h, bg, &mut pixels)
299        .expect("render");
300    let path = std::env::current_dir()
301        .unwrap()
302        .join("examples/legends.png");
303    hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
304    println!("wrote {}", path.display());
305}

Trait Implementations§

Source§

impl Clone for LegendKeySpec

Source§

fn clone(&self) -> LegendKeySpec

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for LegendKeySpec

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Convert Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
Source§

fn as_any(&self) -> &(dyn Any + 'static)

Convert &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
Source§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
Source§

impl<T> Downcast<T> for T

Source§

fn downcast(&self) -> &T

Source§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync>

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> ErasedDestructor for T
where T: 'static,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, S> SimdFrom<T, S> for T
where S: Simd,

Source§

fn simd_from(_simd: S, value: T) -> T

Source§

impl<F, T, S> SimdInto<T, S> for F
where T: SimdFrom<F, S>, S: Simd,

Source§

fn simd_into(self, simd: S) -> T

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> Upcast<T> for T

Source§

fn upcast(&self) -> Option<&T>

Source§

impl<S, T> Upcast<T> for S
where T: UpcastFrom<S> + ?Sized, S: ?Sized,

Source§

fn upcast(&self) -> &T
where Self: ErasableGeneric, T: Sized + ErasableGeneric<Repr = Self::Repr>,

Perform a zero-cost type-safe upcast to a wider ref type within the Wasm bindgen generics type system. Read more
Source§

fn upcast_into(self) -> T
where Self: Sized + ErasableGeneric, T: Sized + ErasableGeneric<Repr = Self::Repr>,

Perform a zero-cost type-safe upcast to a wider type within the Wasm bindgen generics type system. Read more
Source§

impl<T> WasmNotSend for T
where T: Send,

Source§

impl<T> WasmNotSendSync for T

Source§

impl<T> WasmNotSync for T
where T: Sync,

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more