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
impl LegendKeySpec
Sourcepub fn point() -> Self
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
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}Sourcepub fn line() -> Self
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}Sourcepub fn rect() -> Self
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}Sourcepub fn text() -> Self
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}Sourcepub fn scaled(
self,
aesthetic: impl Into<String>,
scale_name: impl Into<String>,
) -> Self
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
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}Sourcepub fn fixed(
self,
aesthetic: impl Into<String>,
value: impl Into<Value>,
) -> Self
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
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
impl Clone for LegendKeySpec
Source§fn clone(&self) -> LegendKeySpec
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)
fn clone_from(&mut self, source: &Self)
Performs copy-assignment from
source. Read moreAuto Trait Implementations§
impl Freeze for LegendKeySpec
impl RefUnwindSafe for LegendKeySpec
impl Send for LegendKeySpec
impl Sync for LegendKeySpec
impl Unpin for LegendKeySpec
impl UnsafeUnpin for LegendKeySpec
impl UnwindSafe for LegendKeySpec
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
Source§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
Source§fn into_any(self: Box<T>) -> Box<dyn Any>
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>
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)
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)
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.