ggplot_rs/render/backend.rs
1use super::{Rect, RenderError};
2
3/// Point shape types.
4#[derive(Clone, Debug, Copy, PartialEq, Eq)]
5pub enum PointShape {
6 Circle,
7 Triangle,
8 Square,
9 Diamond,
10 Cross,
11 Plus,
12}
13
14impl PointShape {
15 /// All shapes in order (for discrete scale mapping).
16 pub const ALL: &[PointShape] = &[
17 PointShape::Circle,
18 PointShape::Triangle,
19 PointShape::Square,
20 PointShape::Diamond,
21 PointShape::Cross,
22 PointShape::Plus,
23 ];
24}
25
26/// Line type patterns.
27#[derive(Clone, Debug, Copy, PartialEq, Eq)]
28pub enum Linetype {
29 Solid,
30 Dashed,
31 Dotted,
32 DashDot,
33 LongDash,
34 TwoDash,
35}
36
37impl Linetype {
38 /// All linetypes in order (for discrete scale mapping).
39 pub const ALL: &[Linetype] = &[
40 Linetype::Solid,
41 Linetype::Dashed,
42 Linetype::Dotted,
43 Linetype::DashDot,
44 Linetype::LongDash,
45 Linetype::TwoDash,
46 ];
47
48 /// Get dash pattern as (draw_len, gap_len) pairs in pixels.
49 pub fn pattern(&self) -> &[(f64, f64)] {
50 match self {
51 Linetype::Solid => &[],
52 Linetype::Dashed => &[(6.0, 3.0)],
53 Linetype::Dotted => &[(2.0, 2.0)],
54 Linetype::DashDot => &[(6.0, 2.0), (2.0, 2.0)],
55 Linetype::LongDash => &[(10.0, 4.0)],
56 Linetype::TwoDash => &[(8.0, 3.0), (3.0, 3.0)],
57 }
58 }
59}
60
61/// Style for drawing points/circles.
62#[derive(Clone, Debug)]
63pub struct PointStyle {
64 pub color: (u8, u8, u8),
65 pub alpha: f64,
66 pub filled: bool,
67 pub shape: PointShape,
68}
69
70impl Default for PointStyle {
71 fn default() -> Self {
72 PointStyle {
73 color: (0, 0, 0),
74 alpha: 1.0,
75 filled: true,
76 shape: PointShape::Circle,
77 }
78 }
79}
80
81/// Line cap style (R grid's `lineend`), applied to the stroked ends of a
82/// polyline/path. `Butt` is SVG's own default, so it is written on the wire
83/// only when it differs (byte-identical default output).
84#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
85pub enum LineEnd {
86 #[default]
87 Butt,
88 Round,
89 Square,
90}
91
92/// Style for drawing lines.
93#[derive(Clone, Debug)]
94pub struct LineStyle {
95 pub color: (u8, u8, u8),
96 pub alpha: f64,
97 pub width: f64,
98 pub linetype: Linetype,
99 pub lineend: LineEnd,
100}
101
102impl Default for LineStyle {
103 fn default() -> Self {
104 LineStyle {
105 color: (0, 0, 0),
106 alpha: 1.0,
107 width: 1.0,
108 linetype: Linetype::Solid,
109 lineend: LineEnd::Butt,
110 }
111 }
112}
113
114/// Style for drawing rectangles/polygons.
115#[derive(Clone, Debug)]
116pub struct RectStyle {
117 pub fill: Option<(u8, u8, u8)>,
118 pub stroke: Option<(u8, u8, u8)>,
119 pub stroke_width: f64,
120 pub alpha: f64,
121 /// Whether to clip this rect to the plot area. Default `true` for data elements.
122 /// Set to `false` for non-data elements (backgrounds, strips, legends).
123 pub clip: bool,
124 /// Corner radius in px, for a rounded rectangle (ggplot2 `geom_label(label.r)`,
125 /// ggchicklet `radius`). `0.0` (the default) draws a plain rectangle; backends
126 /// that cannot draw rounded corners natively fall back to a plain rectangle.
127 pub corner_radius: f64,
128}
129
130impl Default for RectStyle {
131 fn default() -> Self {
132 RectStyle {
133 fill: Some((128, 128, 128)),
134 stroke: None,
135 stroke_width: 1.0,
136 alpha: 1.0,
137 clip: true,
138 corner_radius: 0.0,
139 }
140 }
141}
142
143/// A pattern fill for an area that has no single colour to draw — today
144/// just a scale's `na_pattern` (#206): `ScaleFillBinned::na_pattern`,
145/// drawn instead of `na_value`'s flat colour by `geom_sf`/`geom_polygon`/
146/// `geom_rect`/`geom_tile` and the legend's "no data" key, so a classed
147/// choropleth's no-data cells read as "we have no reading here" rather
148/// than a greyed-out *value* easily mistaken for one.
149///
150/// `fg` stripes run at `angle_deg` (0 = horizontal, measured the usual
151/// screen way, clockwise from the x axis), each `size_px` wide, over a
152/// `bg` field — `fg` fills the first 30% of each cycle, `bg` the rest, a
153/// thin hatch rather than an even 50/50 stripe (ggplot2/R's own
154/// `pattern_stripe(density = 0.3)` default). More pattern kinds can join
155/// this enum later without breaking callers that match it today only via
156/// [`FillPattern::bg`]/[`FillPattern::color_at`].
157#[derive(Clone, Copy, Debug, PartialEq)]
158pub enum FillPattern {
159 Stripe {
160 fg: (u8, u8, u8),
161 bg: (u8, u8, u8),
162 /// The width of one fg+bg cycle, in output pixels. `<= 0.0` is
163 /// treated as `1.0` (an unreasonably fine hatch rather than a
164 /// division by zero/negative-period wraparound).
165 size_px: f64,
166 /// Stripe direction in degrees (0 = horizontal).
167 angle_deg: f64,
168 },
169}
170
171impl FillPattern {
172 /// The fraction of each cycle `fg` covers (the rest is `bg`) — a thin
173 /// hatch, not a 50/50 stripe.
174 const STRIPE_FG_FRACTION: f64 = 0.3;
175
176 /// A reasonable default stripe: `fg`/`bg` as given, 4px period, 45°.
177 pub fn stripe(fg: (u8, u8, u8), bg: (u8, u8, u8)) -> FillPattern {
178 FillPattern::Stripe {
179 fg,
180 bg,
181 size_px: 4.0,
182 angle_deg: 45.0,
183 }
184 }
185
186 /// The flat colour a backend with no pattern primitive falls back to
187 /// (`plotters` — "falls back to bg", #206) — the pattern's background,
188 /// so a no-data cell still reads as *muted*, just without the hatch.
189 pub fn bg(&self) -> (u8, u8, u8) {
190 match self {
191 FillPattern::Stripe { bg, .. } => *bg,
192 }
193 }
194
195 /// This pattern's colour at screen point `(x, y)` — a raster backend
196 /// (pixel/`canvas`) fills per pixel with this ("raster stripes", #206)
197 /// rather than approximating with [`FillPattern::bg`].
198 pub fn color_at(&self, x: f64, y: f64) -> (u8, u8, u8) {
199 match self {
200 FillPattern::Stripe {
201 fg,
202 bg,
203 size_px,
204 angle_deg,
205 } => {
206 let (s, c) = angle_deg.to_radians().sin_cos();
207 let rx = x * c + y * s;
208 let size = if *size_px > 0.0 { *size_px } else { 1.0 };
209 let frac = (rx / size).rem_euclid(1.0);
210 if frac < Self::STRIPE_FG_FRACTION {
211 *fg
212 } else {
213 *bg
214 }
215 }
216 }
217 }
218}
219
220/// Number of line segments approximating each 90° corner of a rounded rect in
221/// backends with no native rounded-rect primitive (pixel, plotters). Higher
222/// is smoother; 8 is visually indistinguishable from a true arc at the sizes
223/// these rects are drawn at (node/label boxes, a few tens to ~200px).
224///
225/// Only [`pixel_backend`](super::pixel_backend) (feature `canvas`) and
226/// [`plotters_backend`](super::plotters_backend) (feature `plotters`) call
227/// [`rounded_rect_points`] — `svg_backend` emits a native `rx`/`ry` instead —
228/// so both it and the function are cfg-gated the same way those backends
229/// are, rather than left for `--no-default-features` to flag as dead code.
230#[cfg(any(
231 feature = "canvas",
232 all(feature = "plotters", not(target_arch = "wasm32"))
233))]
234const ROUNDED_RECT_CORNER_SEGMENTS: usize = 8;
235
236/// A closed polygon approximating a rectangle with rounded corners, for
237/// backends that must rasterize/triangulate rather than emit a native rounded
238/// rect (see [`DrawBackend::draw_rect`]'s `corner_radius`, via [`RectStyle`]).
239///
240/// `radius` is clamped to half the shorter side, so an oversized radius draws
241/// a "stadium" (fully rounded) shape rather than overshooting. A
242/// non-positive radius returns the plain 4-corner rectangle.
243///
244/// Corners go clockwise in screen coordinates (y down), starting at the
245/// top-right, so the first and last points sit either side of the top-right
246/// corner — callers that stroke the result as a closed loop (last point back
247/// to the first) get a seamless boundary.
248#[cfg(any(
249 feature = "canvas",
250 all(feature = "plotters", not(target_arch = "wasm32"))
251))]
252pub(crate) fn rounded_rect_points(
253 x0: f64,
254 y0: f64,
255 x1: f64,
256 y1: f64,
257 radius: f64,
258) -> Vec<(f64, f64)> {
259 let (x0, x1) = (x0.min(x1), x0.max(x1));
260 let (y0, y1) = (y0.min(y1), y0.max(y1));
261 let r = radius.max(0.0).min((x1 - x0) / 2.0).min((y1 - y0) / 2.0);
262 if r <= 0.0 {
263 return vec![(x0, y0), (x1, y0), (x1, y1), (x0, y1)];
264 }
265 use std::f64::consts::{FRAC_PI_2, PI};
266 let n = ROUNDED_RECT_CORNER_SEGMENTS;
267 // (corner centre, start angle, end angle), clockwise, starting top-right.
268 let corners = [
269 (x1 - r, y0 + r, -FRAC_PI_2, 0.0), // top-right
270 (x1 - r, y1 - r, 0.0, FRAC_PI_2), // bottom-right
271 (x0 + r, y1 - r, FRAC_PI_2, PI), // bottom-left
272 (x0 + r, y0 + r, PI, 3.0 * FRAC_PI_2), // top-left
273 ];
274 let mut pts = Vec::with_capacity(4 * (n + 1));
275 for (cx, cy, a0, a1) in corners {
276 for i in 0..=n {
277 let t = a0 + (a1 - a0) * (i as f64 / n as f64);
278 pts.push((cx + r * t.cos(), cy + r * t.sin()));
279 }
280 }
281 pts
282}
283
284#[cfg(all(
285 test,
286 any(
287 feature = "canvas",
288 all(feature = "plotters", not(target_arch = "wasm32"))
289 )
290))]
291mod rounded_rect_tests {
292 use super::rounded_rect_points;
293
294 #[test]
295 fn zero_radius_is_a_plain_rectangle() {
296 let pts = rounded_rect_points(0.0, 0.0, 10.0, 20.0, 0.0);
297 assert_eq!(
298 pts,
299 vec![(0.0, 0.0), (10.0, 0.0), (10.0, 20.0), (0.0, 20.0)]
300 );
301 }
302
303 #[test]
304 fn negative_radius_also_falls_back_to_a_plain_rectangle() {
305 // `radius.max(0.0)` guards a caller passing a negative radius
306 // (e.g. a miscomputed value) — it must not panic or produce a
307 // garbage polygon, just the same plain-rect fallback as 0.0.
308 let pts = rounded_rect_points(0.0, 0.0, 10.0, 20.0, -5.0);
309 assert_eq!(
310 pts,
311 vec![(0.0, 0.0), (10.0, 0.0), (10.0, 20.0), (0.0, 20.0)]
312 );
313 }
314
315 #[test]
316 fn x_and_y_are_order_independent() {
317 // Callers may pass (x1, y1) before (x0, y0) (e.g. a box whose
318 // ymax < ymin before min/max normalisation elsewhere) — the
319 // function itself must normalise, not assume sorted input.
320 let forward = rounded_rect_points(0.0, 0.0, 10.0, 20.0, 3.0);
321 let swapped = rounded_rect_points(10.0, 20.0, 0.0, 0.0, 3.0);
322 assert_eq!(forward, swapped);
323 }
324
325 #[test]
326 fn radius_is_clamped_to_half_the_shorter_side() {
327 // A radius bigger than the box collapses to a "stadium": every point
328 // must stay within the rect's bounds.
329 let pts = rounded_rect_points(0.0, 0.0, 10.0, 20.0, 1000.0);
330 for (x, y) in &pts {
331 assert!((0.0..=10.0).contains(x), "x={x} out of [0,10]");
332 assert!((0.0..=20.0).contains(y), "y={y} out of [0,20]");
333 }
334 }
335
336 #[test]
337 fn corners_are_inset_by_radius_from_the_rect_corners() {
338 let pts = rounded_rect_points(0.0, 0.0, 100.0, 50.0, 10.0);
339 // The arc midpoint of the top-right corner sits at 45° in from the
340 // corner, inset by r*(1 - cos(45°)) from the straight edges.
341 assert!(pts.len() > 4);
342 for (x, y) in &pts {
343 assert!(*x >= 0.0 && *x <= 100.0);
344 assert!(*y >= 0.0 && *y <= 50.0);
345 }
346 }
347}
348
349/// Style for drawing text.
350#[derive(Clone, Debug)]
351pub struct TextStyle {
352 pub color: (u8, u8, u8),
353 pub size: f64,
354 pub anchor: TextAnchor,
355 pub angle: f64,
356 /// Font family (e.g., "serif", "monospace"). None defaults to "sans-serif".
357 pub family: Option<String>,
358 /// Font face (R's `element_text(face = ...)`).
359 pub face: FontFace,
360}
361
362/// Font face / weight (R's `element_text(face = ...)`).
363#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
364pub enum FontFace {
365 #[default]
366 Plain,
367 Bold,
368 Italic,
369}
370
371impl Default for TextStyle {
372 fn default() -> Self {
373 TextStyle {
374 color: (50, 50, 50),
375 size: 12.0,
376 anchor: TextAnchor::Middle,
377 angle: 0.0,
378 family: None,
379 face: crate::render::backend::FontFace::Plain,
380 }
381 }
382}
383
384#[derive(Clone, Debug)]
385pub enum TextAnchor {
386 Start,
387 Middle,
388 End,
389}
390
391/// Our rendering abstraction, independent of plotters details.
392pub trait DrawBackend {
393 fn draw_circle(
394 &mut self,
395 center: (f64, f64),
396 radius: f64,
397 style: &PointStyle,
398 ) -> Result<(), RenderError>;
399 fn draw_line(&mut self, points: &[(f64, f64)], style: &LineStyle) -> Result<(), RenderError>;
400 fn draw_rect(
401 &mut self,
402 top_left: (f64, f64),
403 bottom_right: (f64, f64),
404 style: &RectStyle,
405 ) -> Result<(), RenderError>;
406 fn draw_text(
407 &mut self,
408 text: &str,
409 pos: (f64, f64),
410 style: &TextStyle,
411 ) -> Result<(), RenderError>;
412 fn draw_polygon(&mut self, points: &[(f64, f64)], style: &RectStyle)
413 -> Result<(), RenderError>;
414
415 /// Draw a polygon with holes: `rings[0]` is the exterior, every other
416 /// ring a hole cut out of it under the even-odd fill rule (`geom_sf`'s
417 /// `Polygon`, OGC/`sf` convention — see
418 /// [`spatial::Geometry::Polygon`](crate::spatial::Geometry::Polygon)).
419 /// One `stroke`/`stroke_width` draws every ring's outline, exterior and
420 /// holes alike (as `ggplot2`'s `geom_sf` does).
421 ///
422 /// Default: no real hole cut-out (for backends that pre-date this
423 /// method) — the exterior ring filled and stroked, every other ring
424 /// stroked only. Backends with a resolution-independent fill (SVG) or a
425 /// scanline rasteriser (pixel/plotters) should override this to use
426 /// their fill rule across every ring at once.
427 fn draw_polygon_rings(
428 &mut self,
429 rings: &[Vec<(f64, f64)>],
430 style: &RectStyle,
431 ) -> Result<(), RenderError> {
432 for (i, ring) in rings.iter().enumerate() {
433 let ring_style = if i == 0 {
434 style.clone()
435 } else {
436 RectStyle {
437 fill: None,
438 ..style.clone()
439 }
440 };
441 self.draw_polygon(ring, &ring_style)?;
442 }
443 Ok(())
444 }
445
446 /// Like [`draw_rect`](Self::draw_rect), but filled with `pattern`
447 /// instead of `style.fill` (`geom_rect`/`geom_tile`'s no-data cells,
448 /// #206) — `style.stroke`/`stroke_width`/`clip`/`corner_radius` still
449 /// apply, only the fill differs.
450 ///
451 /// Default: flattens `pattern` to its [`FillPattern::bg`] and draws an
452 /// ordinary [`draw_rect`](Self::draw_rect) — the `plotters` backend
453 /// has no pattern-fill primitive and keeps this default ("falls back
454 /// to bg"). The native SVG backend overrides this to register a
455 /// `<defs><pattern>` (deduped, this backend's own id prefix) and fill
456 /// with its `url(#…)` reference; the pixel/`canvas` backend overrides
457 /// it to rasterise the actual stripes per pixel ("raster stripes").
458 fn draw_rect_pattern(
459 &mut self,
460 top_left: (f64, f64),
461 bottom_right: (f64, f64),
462 pattern: &FillPattern,
463 style: &RectStyle,
464 ) -> Result<(), RenderError> {
465 self.draw_rect(
466 top_left,
467 bottom_right,
468 &RectStyle {
469 fill: Some(pattern.bg()),
470 ..style.clone()
471 },
472 )
473 }
474
475 /// Like [`draw_polygon`](Self::draw_polygon), but filled with
476 /// `pattern` instead of `style.fill` (#206). Default/override split:
477 /// see [`draw_rect_pattern`](Self::draw_rect_pattern).
478 fn draw_polygon_pattern(
479 &mut self,
480 points: &[(f64, f64)],
481 pattern: &FillPattern,
482 style: &RectStyle,
483 ) -> Result<(), RenderError> {
484 self.draw_polygon(
485 points,
486 &RectStyle {
487 fill: Some(pattern.bg()),
488 ..style.clone()
489 },
490 )
491 }
492
493 /// Like [`draw_polygon_rings`](Self::draw_polygon_rings), but filled
494 /// with `pattern` instead of `style.fill` (`geom_sf`/`geom_polygon`'s
495 /// no-data features, #206) — holes still cut out under the even-odd
496 /// rule, exactly as a flat fill would. Default/override split: see
497 /// [`draw_rect_pattern`](Self::draw_rect_pattern).
498 fn draw_polygon_rings_pattern(
499 &mut self,
500 rings: &[Vec<(f64, f64)>],
501 pattern: &FillPattern,
502 style: &RectStyle,
503 ) -> Result<(), RenderError> {
504 self.draw_polygon_rings(
505 rings,
506 &RectStyle {
507 fill: Some(pattern.bg()),
508 ..style.clone()
509 },
510 )
511 }
512
513 /// Like [`draw_text`](Self::draw_text), but with an optional `<title>`
514 /// tooltip carrying the *full* text when `text` itself has been
515 /// shortened (e.g. a truncated-with-ellipsis legend label, #159).
516 /// Default: ignores `title` and draws `text` plain — backends without
517 /// native tooltip support (anything but SVG) have no better option.
518 /// SVG overrides this to emit the `<title>` child, escaped the same
519 /// way every other text run is.
520 fn draw_text_with_title(
521 &mut self,
522 text: &str,
523 pos: (f64, f64),
524 style: &TextStyle,
525 _title: Option<&str>,
526 ) -> Result<(), RenderError> {
527 self.draw_text(text, pos, style)
528 }
529
530 /// Draw one line as consecutive runs, each optionally an `http(s)`
531 /// hyperlink (ggspatial-style attribution: "© BKG" linked, a licence
532 /// code linked, plain connective text between them —
533 /// `annotation_attribution`'s `.link()`/`.runs()`, #196). `pos`/`style`
534 /// place and style the line as a whole, same as [`draw_text`](Self::draw_text)
535 /// — `style.anchor` aligns the *total* run sequence, not each run.
536 ///
537 /// Default: concatenates the runs' text and draws it plain with
538 /// [`draw_text`](Self::draw_text), dropping every link — the sensible
539 /// fallback for a backend with no link primitive (pixel/`canvas`,
540 /// `plotters`). The native SVG backend overrides this to wrap each
541 /// linked run in `<a href="…" target="_blank" rel="noopener">` with an
542 /// underline, inside one `<text>` element so the anchor still aligns
543 /// the whole line.
544 fn draw_text_runs(
545 &mut self,
546 runs: &[(&str, Option<&str>)],
547 pos: (f64, f64),
548 style: &TextStyle,
549 ) -> Result<(), RenderError> {
550 let joined: String = runs.iter().map(|(t, _)| *t).collect();
551 self.draw_text(&joined, pos, style)
552 }
553 fn plot_area(&self) -> Rect;
554 fn total_area(&self) -> Rect;
555
556 /// Attach a tooltip to subsequently-drawn marks until cleared with `None`.
557 /// Backends that produce interactive output (SVG) emit it as a `<title>`
558 /// for a native hover tooltip; others ignore it. Default: no-op.
559 fn set_tooltip(&mut self, _tooltip: Option<String>) {}
560
561 /// Attach an axis (x) key to subsequently-drawn marks — SVG emits it as a
562 /// `data-x` attribute so a host can group marks by x (axis-pointer tooltip).
563 /// Default: no-op.
564 fn set_mark_axis(&mut self, _key: Option<String>) {}
565
566 /// Attach a series key (the colour / fill / group level) to subsequently
567 /// drawn marks — SVG emits it as `data-series`, so a host can highlight or
568 /// filter one series without parsing the tooltip. Default: no-op.
569 fn set_mark_series(&mut self, _series: Option<String>) {}
570
571 /// Attach the raw measured value (the unformatted y) to subsequently drawn
572 /// marks — SVG emits it as `data-value`. Default: no-op.
573 fn set_mark_value(&mut self, _value: Option<String>) {}
574
575 /// Tag subsequently drawn elements as data marks of the named geom (the
576 /// layer's `Geom::name`) until cleared with `None`. SVG emits
577 /// `class="gg-mark gg-<kind>"` (kind from the primitive: `point`, `line`,
578 /// `band`, `bar`, `text`) plus `data-geom`, so a host can select data
579 /// marks without matching axes, grid lines or legends. Default: no-op.
580 fn set_mark_geom(&mut self, _geom: Option<&str>) {}
581
582 /// Report a non-fatal problem found while drawing (e.g. labels a repel
583 /// layout had to drop). Backends that collect warnings (the native SVG
584 /// backend) append it to the render warnings. Default: ignored.
585 fn warn(&mut self, _message: String) {}
586
587 /// Mark subsequently-drawn marks as belonging to a sub-part of a larger
588 /// geom (e.g. `"arrow"` for an arrowhead drawn alongside a segment/curve/
589 /// path/line). While set, the SVG backend writes `data-part="<part>"
590 /// aria-hidden="true"` and omits the `<title>` tooltip (the part is
591 /// decorative; the parent mark already carries the semantic info).
592 /// `data-x`/`data-series`/`data-value` (and any user `data-*` attrs) are
593 /// still written. Default: no-op.
594 fn set_mark_part(&mut self, _part: Option<&str>) {}
595
596 /// Attach arbitrary `data-*` attributes to subsequently-drawn marks
597 /// (B1-7's `Aes::data_attr`). The SVG backend writes each, escaped; an
598 /// attr with the same name as a built-in (`data-x`, `data-series`,
599 /// `data-value`) replaces it rather than duplicating it. An empty
600 /// `Vec` clears any previously set attrs. Default: no-op.
601 fn set_mark_attrs(&mut self, _attrs: Vec<(String, String)>) {}
602
603 /// Whether the caller asked for per-mark formatted values
604 /// ([`crate::plot::GGPlot::svg_formatted_values`], B2-4) — a query a
605 /// geom checks before doing the (otherwise pointless) work of resolving
606 /// a scale's formatter for `set_mark_fvalue`/`set_mark_fx`. Default
607 /// false, so the feature is byte-identical when never turned on.
608 fn wants_formatted_values(&self) -> bool {
609 false
610 }
611
612 /// `data-fvalue` on subsequently-drawn marks: the "value" aesthetic's
613 /// raw data value (y for most geoms, fill for hex/tile), formatted
614 /// through that aesthetic's scale (`Scale::format_value`) — e.g. a
615 /// de-DE currency string. Only meaningful while
616 /// [`wants_formatted_values`](Self::wants_formatted_values) is true;
617 /// `None` clears any previous mark's value, per row. Default: no-op.
618 fn set_mark_fvalue(&mut self, _value: Option<String>) {}
619
620 /// `data-fx` on subsequently-drawn marks: the x value formatted through
621 /// the x scale (e.g. a datetime axis's ISO/locale date). The SVG
622 /// backend omits it when it matches the already-written `data-x`, so a
623 /// plain numeric x doesn't get a redundant duplicate attribute. Default:
624 /// no-op.
625 fn set_mark_fx(&mut self, _value: Option<String>) {}
626
627 /// Draw a point with a specific shape. Default delegates to draw_circle for Circle.
628 fn draw_shape(
629 &mut self,
630 center: (f64, f64),
631 radius: f64,
632 style: &PointStyle,
633 ) -> Result<(), RenderError> {
634 match style.shape {
635 PointShape::Circle => self.draw_circle(center, radius, style),
636 _ => {
637 // Default: fall back to circle for unsupported backends
638 self.draw_circle(center, radius, style)
639 }
640 }
641 }
642
643 /// Draw a quadratic Bézier curve from `p0` to `p1` with control point
644 /// `ctrl`, in pixel space. The default implementation flattens the curve
645 /// to 32 line segments and draws it with [`draw_line`](Self::draw_line);
646 /// backends that support real curves (SVG) should override this.
647 /// Non-finite input draws nothing.
648 fn draw_quadratic(
649 &mut self,
650 p0: (f64, f64),
651 ctrl: (f64, f64),
652 p1: (f64, f64),
653 style: &LineStyle,
654 ) -> Result<(), RenderError> {
655 if !(p0.0.is_finite()
656 && p0.1.is_finite()
657 && ctrl.0.is_finite()
658 && ctrl.1.is_finite()
659 && p1.0.is_finite()
660 && p1.1.is_finite())
661 {
662 return Ok(());
663 }
664 const STEPS: usize = 32;
665 let mut pts = Vec::with_capacity(STEPS + 1);
666 for i in 0..=STEPS {
667 let t = i as f64 / STEPS as f64;
668 let one_minus_t = 1.0 - t;
669 let x =
670 one_minus_t * one_minus_t * p0.0 + 2.0 * one_minus_t * t * ctrl.0 + t * t * p1.0;
671 let y =
672 one_minus_t * one_minus_t * p0.1 + 2.0 * one_minus_t * t * ctrl.1 + t * t * p1.1;
673 pts.push((x, y));
674 }
675 self.draw_line(&pts, style)
676 }
677}
678
679#[cfg(test)]
680mod tests {
681 use super::*;
682 use crate::render::{Rect, RenderError};
683 use std::cell::RefCell;
684
685 /// A minimal backend that does not override `draw_quadratic`, to exercise
686 /// the trait's default flattening implementation.
687 /// One `draw_polygon` call's recorded points and fill colour.
688 type RecordedPolygon = (Vec<(f64, f64)>, Option<(u8, u8, u8)>);
689 /// One `draw_rect` call's corners and fill colour.
690 type RecordedRect = ((f64, f64), (f64, f64), Option<(u8, u8, u8)>);
691
692 #[derive(Default)]
693 struct RecordingBackend {
694 lines: RefCell<Vec<Vec<(f64, f64)>>>,
695 circles: RefCell<Vec<((f64, f64), f64)>>,
696 polygons: RefCell<Vec<RecordedPolygon>>,
697 rects: RefCell<Vec<RecordedRect>>,
698 }
699
700 impl DrawBackend for RecordingBackend {
701 fn draw_circle(
702 &mut self,
703 center: (f64, f64),
704 radius: f64,
705 _style: &PointStyle,
706 ) -> Result<(), RenderError> {
707 self.circles.borrow_mut().push((center, radius));
708 Ok(())
709 }
710 fn draw_line(
711 &mut self,
712 points: &[(f64, f64)],
713 _style: &LineStyle,
714 ) -> Result<(), RenderError> {
715 self.lines.borrow_mut().push(points.to_vec());
716 Ok(())
717 }
718 fn draw_rect(
719 &mut self,
720 top_left: (f64, f64),
721 bottom_right: (f64, f64),
722 style: &RectStyle,
723 ) -> Result<(), RenderError> {
724 self.rects
725 .borrow_mut()
726 .push((top_left, bottom_right, style.fill));
727 Ok(())
728 }
729 fn draw_text(
730 &mut self,
731 _text: &str,
732 _pos: (f64, f64),
733 _style: &TextStyle,
734 ) -> Result<(), RenderError> {
735 Ok(())
736 }
737 fn draw_polygon(
738 &mut self,
739 points: &[(f64, f64)],
740 style: &RectStyle,
741 ) -> Result<(), RenderError> {
742 self.polygons
743 .borrow_mut()
744 .push((points.to_vec(), style.fill));
745 Ok(())
746 }
747 fn plot_area(&self) -> Rect {
748 Rect {
749 x: 0.0,
750 y: 0.0,
751 width: 100.0,
752 height: 100.0,
753 }
754 }
755 fn total_area(&self) -> Rect {
756 self.plot_area()
757 }
758 }
759
760 #[test]
761 fn default_draw_quadratic_flattens_to_33_points() {
762 let mut b = RecordingBackend::default();
763 b.draw_quadratic((0.0, 0.0), (5.0, 10.0), (10.0, 0.0), &LineStyle::default())
764 .unwrap();
765 let lines = b.lines.borrow();
766 assert_eq!(lines.len(), 1);
767 assert_eq!(lines[0].len(), 33);
768 assert_eq!(lines[0][0], (0.0, 0.0));
769 assert_eq!(lines[0][32], (10.0, 0.0));
770 // Midpoint of a quadratic Bézier at t=0.5 is the average of the
771 // endpoints and the control point, each weighted 1/4, 1/2, 1/4.
772 let mid = lines[0][16];
773 assert!((mid.0 - 5.0).abs() < 1e-9);
774 assert!((mid.1 - 5.0).abs() < 1e-9);
775 }
776
777 #[test]
778 fn default_draw_quadratic_drops_non_finite() {
779 let mut b = RecordingBackend::default();
780 b.draw_quadratic(
781 (f64::NAN, 0.0),
782 (5.0, 10.0),
783 (10.0, 0.0),
784 &LineStyle::default(),
785 )
786 .unwrap();
787 assert!(b.lines.borrow().is_empty());
788 }
789
790 /// The default `draw_shape` falls back to `draw_circle` for every
791 /// `PointShape` (not just `Circle`) on a backend that doesn't override
792 /// it — e.g. a minimal backend that only implements the required
793 /// methods.
794 #[test]
795 fn default_draw_shape_falls_back_to_draw_circle_for_every_point_shape() {
796 let mut b = RecordingBackend::default();
797 for shape in PointShape::ALL {
798 b.draw_shape(
799 (1.0, 2.0),
800 3.0,
801 &PointStyle {
802 shape: *shape,
803 ..Default::default()
804 },
805 )
806 .unwrap();
807 }
808 assert_eq!(b.circles.borrow().len(), PointShape::ALL.len());
809 for (center, radius) in b.circles.borrow().iter() {
810 assert_eq!(*center, (1.0, 2.0));
811 assert_eq!(*radius, 3.0);
812 }
813 }
814
815 /// The default `draw_polygon_rings` (for a backend pre-dating #178, or
816 /// any third-party `DrawBackend` that never overrides it) falls back to
817 /// one `draw_polygon` call per ring: the exterior filled, every other
818 /// ring (a hole) with its fill cleared — the old approximation, not a
819 /// real cut-out, but it draws *something* rather than nothing.
820 #[test]
821 fn default_draw_polygon_rings_fills_only_the_exterior() {
822 let mut b = RecordingBackend::default();
823 let exterior = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
824 let hole = vec![(3.0, 3.0), (7.0, 3.0), (7.0, 7.0), (3.0, 7.0)];
825 b.draw_polygon_rings(
826 &[exterior.clone(), hole.clone()],
827 &RectStyle {
828 fill: Some((200, 50, 50)),
829 stroke: Some((0, 0, 0)),
830 stroke_width: 1.0,
831 alpha: 1.0,
832 clip: true,
833 corner_radius: 0.0,
834 },
835 )
836 .unwrap();
837 let polys = b.polygons.borrow();
838 assert_eq!(polys.len(), 2, "one draw_polygon call per ring");
839 assert_eq!(polys[0], (exterior, Some((200, 50, 50))), "exterior filled");
840 assert_eq!(polys[1].0, hole);
841 assert_eq!(polys[1].1, None, "hole not filled (no real cut-out)");
842 }
843
844 // #206: the default `draw_*_pattern` methods (a backend with no
845 // pattern-fill primitive — `plotters` is the real-world case, this
846 // mock stands in for it) flatten to the pattern's own `bg` and call
847 // the plain draw method, unchanged otherwise.
848
849 fn stripe() -> FillPattern {
850 FillPattern::stripe((10, 20, 30), (200, 210, 220))
851 }
852
853 #[test]
854 fn default_draw_rect_pattern_flattens_to_bg() {
855 let mut b = RecordingBackend::default();
856 b.draw_rect_pattern(
857 (0.0, 0.0),
858 (10.0, 10.0),
859 &stripe(),
860 &RectStyle {
861 stroke: Some((0, 0, 0)),
862 ..Default::default()
863 },
864 )
865 .unwrap();
866 let rects = b.rects.borrow();
867 assert_eq!(rects.len(), 1);
868 assert_eq!(rects[0], ((0.0, 0.0), (10.0, 10.0), Some(stripe().bg())));
869 }
870
871 #[test]
872 fn default_draw_polygon_pattern_flattens_to_bg() {
873 let mut b = RecordingBackend::default();
874 let pts = vec![(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)];
875 b.draw_polygon_pattern(&pts, &stripe(), &RectStyle::default())
876 .unwrap();
877 let polys = b.polygons.borrow();
878 assert_eq!(polys.len(), 1);
879 assert_eq!(polys[0], (pts, Some(stripe().bg())));
880 }
881
882 #[test]
883 fn default_draw_polygon_rings_pattern_flattens_to_bg_per_ring() {
884 let mut b = RecordingBackend::default();
885 let exterior = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
886 let hole = vec![(3.0, 3.0), (7.0, 3.0), (7.0, 7.0), (3.0, 7.0)];
887 b.draw_polygon_rings_pattern(&[exterior.clone(), hole], &stripe(), &RectStyle::default())
888 .unwrap();
889 let polys = b.polygons.borrow();
890 // Same fallback shape as draw_polygon_rings's own default: the
891 // exterior filled (with the pattern's bg), the hole not.
892 assert_eq!(polys[0], (exterior, Some(stripe().bg())));
893 assert_eq!(polys[1].1, None);
894 }
895
896 #[test]
897 fn recording_backend_areas_and_remaining_draw_methods_behave_as_declared() {
898 let mut b = RecordingBackend::default();
899 let pa = b.plot_area();
900 let ta = b.total_area();
901 assert_eq!(
902 (pa.x, pa.y, pa.width, pa.height),
903 (ta.x, ta.y, ta.width, ta.height)
904 );
905 assert_eq!((pa.x, pa.y, pa.width, pa.height), (0.0, 0.0, 100.0, 100.0));
906 b.draw_rect((0.0, 0.0), (1.0, 1.0), &RectStyle::default())
907 .unwrap();
908 b.draw_text("label", (0.0, 0.0), &TextStyle::default())
909 .unwrap();
910 b.draw_polygon(&[(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)], &RectStyle::default())
911 .unwrap();
912 }
913}