Skip to main content

ggplot_rs/geom/
mod.rs

1#[cfg(feature = "sf")]
2pub mod annotation_scale;
3pub mod arcdiagram;
4pub mod area;
5pub mod arrow;
6pub mod bar;
7pub mod bin2d;
8#[cfg(feature = "sf")]
9pub mod bivariate_legend;
10pub mod blank;
11pub mod boxplot;
12pub mod bracket;
13pub mod bullet;
14pub mod candlestick;
15pub mod censor;
16pub mod chord;
17pub mod col;
18pub mod contour;
19pub mod cooks;
20pub mod count;
21pub mod crossbar;
22pub mod curve;
23pub(crate) mod decorative;
24pub mod density;
25pub mod density2d;
26pub mod dotplot;
27pub mod errorbar;
28pub mod errorbarh;
29pub mod flags;
30#[cfg(feature = "sf")]
31pub mod flow;
32pub mod freqpoly;
33pub mod funnel;
34pub mod hex;
35pub mod histogram;
36pub mod jitter;
37pub(crate) mod layer_attrs;
38pub mod line;
39pub mod linerange;
40pub mod lollipop;
41#[cfg(feature = "tiles")]
42pub mod map_tile;
43pub mod marimekko;
44pub mod network;
45pub mod pack;
46pub mod pareto;
47pub mod partition;
48pub mod path;
49pub mod point;
50pub mod pointrange;
51pub mod polygon;
52pub mod qq;
53pub mod raster;
54pub mod rect;
55pub mod refline;
56pub mod renko;
57pub mod repel;
58pub mod ribbon;
59pub mod rug;
60pub mod sankey;
61pub mod segment;
62#[cfg(feature = "sf")]
63pub mod sf;
64#[cfg(feature = "sf")]
65pub mod sf_text;
66pub mod smooth;
67pub mod spoke;
68pub mod step;
69pub(crate) mod support;
70pub mod text;
71pub mod tile;
72pub mod tilemap;
73pub mod timeline;
74pub mod tree;
75pub mod treemap;
76pub mod variablepie;
77pub mod venn;
78pub mod violin;
79pub mod waffle;
80pub mod waterfall;
81pub mod windbarb;
82pub mod wordcloud;
83pub mod xrange;
84
85use std::collections::HashMap;
86
87use crate::aes::Aesthetic;
88use crate::coord::Coord;
89use crate::data::DataFrame;
90use crate::position::Position;
91use crate::render::backend::DrawBackend;
92use crate::render::RenderError;
93use crate::scale::ScaleSet;
94use crate::stat::Stat;
95use crate::theme::Theme;
96
97/// Fixed (non-mapped) visual parameters for a geom.
98#[derive(Clone, Debug, Default)]
99pub struct GeomParams {
100    pub values: HashMap<String, f64>,
101    pub color: Option<(u8, u8, u8)>,
102    pub fill: Option<(u8, u8, u8)>,
103    pub alpha: Option<f64>,
104}
105
106/// Trait for geometric objects that draw data on the plot.
107pub trait Geom: Send + Sync {
108    /// Draw this geometry.
109    fn draw(
110        &self,
111        data: &DataFrame,
112        coord: &dyn Coord,
113        scales: &ScaleSet,
114        theme: &Theme,
115        backend: &mut dyn DrawBackend,
116    ) -> Result<(), RenderError>;
117
118    /// Required aesthetics.
119    fn required_aes(&self) -> Vec<Aesthetic>;
120
121    /// Default stat for this geom.
122    fn default_stat(&self) -> Box<dyn Stat>;
123
124    /// Default position adjustment.
125    fn default_position(&self) -> Box<dyn Position>;
126
127    /// Non-mapped visual defaults.
128    fn default_params(&self) -> GeomParams;
129
130    /// A figure-level layer on a faceted plot: the height of the band it
131    /// needs under the whole panel grid at `width`, when it draws once
132    /// there instead of in every panel (e.g. `GeomAttribution::figure`).
133    /// Default `None` (drawn per panel).
134    fn figure_band_height(&self, _width: f64, _theme: &Theme) -> Option<f64> {
135        None
136    }
137
138    /// Name for debug/display.
139    fn name(&self) -> &str;
140
141    /// Apply a brand/primary color to this geom's single-series default (its
142    /// `color` or `fill`). The build pipeline calls this only when the layer has
143    /// no color/fill aesthetic mapped, so an explicit mapping always wins. The
144    /// default is a no-op; series geoms override it.
145    fn set_series_color(&mut self, _color: (u8, u8, u8)) {}
146
147    /// Recolour this geom's *built-in default* colour/fill from the theme's
148    /// ink/paper/accent palette (B2-7's `element_geom`), called once per
149    /// layer before [`set_series_color`](Self::set_series_color) — and only
150    /// when the layer was *not* configured explicitly (`geom_*_with(...)`;
151    /// the build pipeline gates the call on the same `explicit_style` flag
152    /// `set_series_color`'s brand-colour application is gated on). That
153    /// gate, not a field-value comparison, is what keeps an explicit colour
154    /// untouched: a geom that honours this replaces a field still at its
155    /// own hard-coded default (black/`(50, 50, 50)` → `ink`, white →
156    /// `paper`) by comparing its *value*, which cannot by itself tell an
157    /// explicitly-set colour that happens to equal that default (e.g.
158    /// `GeomPoint { color: (0, 0, 0), .. }` under `geom_point_with`) apart
159    /// from one that was simply never touched — only skipping the call
160    /// entirely for an explicitly-configured layer closes that gap (review
161    /// PR#141 finding 4). Default no-op; `None` fields of `ElementGeom`
162    /// change nothing, so the default theme (no ink/paper/accent set)
163    /// stays byte-identical.
164    fn apply_element_geom(&mut self, _el: &crate::theme::elements::ElementGeom) {}
165
166    /// Whether this geom draws from a 0 baseline, so the Y scale should include
167    /// 0 even when `y` is explicitly mapped (bars/columns/area/histograms — as
168    /// in ggplot2). Default false.
169    fn include_zero_baseline(&self) -> bool {
170        false
171    }
172
173    /// Whether `-Inf`/`Inf` position values are meaningful for this geom
174    /// (ggplot2: "extend to the panel edge"). Rows with infinite positions are
175    /// otherwise dropped with a warning before stats run. `NaN` is always
176    /// dropped — unless [`Geom::keeps_na_as_break`] says otherwise. Default
177    /// false; `geom_rect` returns true.
178    fn allows_infinite(&self) -> bool {
179        false
180    }
181
182    /// Whether a row with a non-finite position is a *break* rather than a
183    /// removal (ggplot2's `GeomPath`/`GeomLine` NA handling): the row still
184    /// counts toward the "removed N rows" warning, but it is **not** deleted
185    /// from the built `DataFrame`, so the geom's own per-group point list
186    /// still contains it — as a non-finite pixel position after the scale
187    /// and coordinate transform, which [`crate::render::backend::DrawBackend::draw_line`]
188    /// splits on into separate polylines (ggplot2's NA-break semantics)
189    /// instead of silently joining the finite points across the gap.
190    /// Default false (the row is dropped, as for every other geom);
191    /// line/path/step return true.
192    fn keeps_na_as_break(&self) -> bool {
193        false
194    }
195
196    /// Whether this geom calls [`set_row_extras`] (so it reads
197    /// [`Aes::tooltip`](crate::aes::Aes::tooltip) and
198    /// [`Aes::data_attr`](crate::aes::Aes::data_attr), and a
199    /// [`GGPlot::tooltip`](crate::plot::GGPlot::tooltip) template on it has
200    /// a visible effect). Default false; the build pipeline warns when a
201    /// `tooltip()` template is set on a layer whose geom returns false
202    /// here (review PR#141 finding 6, LOW: a template silently did nothing
203    /// on e.g. `geom_area`/`geom_boxplot`/`geom_ribbon`). Point, segment,
204    /// curve, path, line, hex, polygon, sf, rect/tile and col/bar/
205    /// histogram return true.
206    fn consumes_tooltips(&self) -> bool {
207        false
208    }
209
210    /// Geom-specific data preparation after the stat and position steps but
211    /// before scale training (ggplot2's `GeomX$setup_data`) — e.g. a tile adds
212    /// its `xmin`/`xmax`/`ymin`/`ymax` extents so continuous scales train on
213    /// them. `warnings` is the layer's own build-warnings sink (B3-5: added
214    /// so `GeomBoxplot` can warn on an unparsable outlier value here, the
215    /// same channel every other build warning uses — never `eprintln!`).
216    /// Default: no-op.
217    fn setup_data(&self, _data: &mut DataFrame, _warnings: &mut Vec<String>) {}
218
219    /// Whether the layer inherits the plot-level aesthetic mapping (ggplot2's
220    /// `inherit.aes`). Reference-line geoms return false, as in ggplot2, so a
221    /// plot-level `color`/`x`/`y` mapping does not leak into a `geom_hline`.
222    /// Default true.
223    fn inherit_aes(&self) -> bool {
224        true
225    }
226
227    /// Canonical column names this geom only reads when the user actually
228    /// mapped the corresponding aesthetic (D3, B3-5) — never from a raw
229    /// column that merely happens to share the name (e.g. a SQL alias
230    /// literally called `lower`). `build_layer` drops any of these columns
231    /// right after `resolve_mappings` unless the aesthetic was mapped.
232    /// Default empty; `GeomBoxplot` returns `["lower", "middle", "upper",
233    /// "outliers"]`.
234    fn mapped_only_columns(&self) -> &'static [&'static str] {
235        &[]
236    }
237
238    /// Stat-dependent layer validation beyond [`required_aes`](Self::required_aes)
239    /// (B3-5): called once per layer with the resolved (pre-stat, post-D3-drop)
240    /// column names and the layer's stat name, before the stat runs. Default
241    /// no-op. `GeomBoxplot` requires `lower`/`middle`/`upper` to be mapped
242    /// under `stat="identity"`, else `y`.
243    fn validate_layer(&self, _pre_stat_columns: &[String], _stat_name: &str) -> Result<(), String> {
244        Ok(())
245    }
246}
247
248/// Format a value for a hover tooltip — strings verbatim, numbers rounded short,
249/// `Na` empty.
250pub(crate) fn tip_value(v: &crate::data::Value) -> String {
251    crate::format::format_value(v)
252}
253
254/// Half-width (normalized panel units) for bars on a continuous / date x axis:
255/// `width` × the smallest gap between distinct mapped x positions, like
256/// ggplot2's `resolution(x)`. A fixed fraction would make 60 daily bars overlap
257/// and 3 bars look like slivers. Falls back to `fallback` with < 2 distinct xs.
258pub(crate) fn continuous_bar_half_width(
259    mapped_xs: impl Iterator<Item = f64>,
260    width: f64,
261    fallback: f64,
262) -> f64 {
263    let mut xs: Vec<f64> = mapped_xs.filter(|x| x.is_finite()).collect();
264    xs.sort_by(|a, b| a.total_cmp(b));
265    xs.dedup_by(|a, b| (*a - *b).abs() < 1e-12);
266    let gap = xs
267        .windows(2)
268        .map(|w| w[1] - w[0])
269        .fold(f64::INFINITY, f64::min);
270    if gap.is_finite() && gap > 0.0 {
271        gap * width / 2.0
272    } else {
273        fallback
274    }
275}
276
277/// Raw (unformatted) value for a `data-value` attribute: numbers in shortest
278/// round-trip form, date-times as epoch seconds, strings verbatim. `None` for
279/// missing / non-finite values.
280pub(crate) fn raw_value(v: &crate::data::Value) -> Option<String> {
281    use crate::data::Value;
282    match v {
283        Value::Float(f) if f.is_finite() => Some(format!("{f}")),
284        Value::Float(_) | Value::Na => None,
285        Value::Integer(i) => Some(i.to_string()),
286        Value::DateTime(s) => Some(s.to_string()),
287        Value::Str(s) => Some(s.clone()),
288        Value::Bool(b) => Some(b.to_string()),
289    }
290}
291
292/// The series key of row `i` — its colour, else fill, else group level — for
293/// a `data-series` attribute.
294pub(crate) fn series_key(data: &DataFrame, i: usize) -> Option<String> {
295    ["color", "fill", "group"].iter().find_map(|c| {
296        data.column(c)
297            .and_then(|col| col.get(i))
298            .filter(|v| !v.is_na())
299            .map(tip_value)
300            .filter(|s| !s.is_empty())
301    })
302}
303
304/// The raw measured y of row `i`. Stacked/filled positions overwrite `y` with
305/// the cumulative top, so prefer the pre-position value they preserve.
306pub(crate) fn measured_value(data: &DataFrame, i: usize) -> Option<String> {
307    measured_raw(data, i).and_then(raw_value)
308}
309
310/// [`measured_value`], but the raw `Value` itself (for `Scale::format_value`,
311/// B2-4) instead of its display string.
312pub(crate) fn measured_raw(data: &DataFrame, i: usize) -> Option<&crate::data::Value> {
313    data.column(crate::position::RAW_Y_COL)
314        .or_else(|| data.column("y"))
315        .and_then(|c| c.get(i))
316}
317
318/// Set all per-mark metadata (tooltip, `data-x`, `data-series`, `data-value`)
319/// for the next drawn mark(s).
320pub(crate) fn set_mark(
321    backend: &mut dyn DrawBackend,
322    tooltip: Option<String>,
323    x: Option<String>,
324    series: Option<String>,
325    value: Option<String>,
326) {
327    backend.set_tooltip(tooltip);
328    backend.set_mark_axis(x);
329    backend.set_mark_series(series);
330    backend.set_mark_value(value);
331}
332
333/// Resolve the font family for a text-drawing geom (text, label, repel,
334/// bracket, annotations) — B2-6/B2-7's three-tier order: `geom_family`
335/// (the geom's own `fontfamily`, e.g. `GeomText::with_fontfamily`) when
336/// set, else `theme.geom.family` (B2-7's `element_geom`), else
337/// `theme.text.family` (read after `Theme::resolve_inheritance`, so an
338/// inherited value is already resolved), else `None` (the SVG backend's
339/// own `"sans-serif"` default). `ElementText::family` defaults to
340/// `"sans-serif"` itself and `theme.geom.family` defaults to `None`, so
341/// with nothing configured anywhere this already returns
342/// `Some("sans-serif")` — the same string the backend would have defaulted
343/// to, so default output stays byte-identical.
344pub(crate) fn resolve_font_family(geom_family: &str, theme: &Theme) -> Option<String> {
345    if !geom_family.is_empty() {
346        Some(geom_family.to_string())
347    } else if let Some(family) = &theme.geom.family {
348        Some(family.clone())
349    } else if theme.text.family.is_empty() {
350        None
351    } else {
352        Some(theme.text.family.clone())
353    }
354}
355
356/// Parse a per-row `Aes::fontface` (#228) string the same way `theme()`'s
357/// `face` is (`src/theme/config.rs::text`): `"plain"`/`"normal"` ->
358/// [`FontFace::Plain`], `"bold"` -> `Bold`, `"italic"`/`"oblique"` ->
359/// `Italic`, case-insensitive. `None` for anything else, so the caller falls
360/// back to the geom's own fixed fontface — a row's unparseable value is
361/// silently ignored rather than a build-time warning, since it's only known
362/// once the data is in hand.
363pub(crate) fn parse_fontface(raw: &str) -> Option<crate::render::backend::FontFace> {
364    use crate::render::backend::FontFace;
365    match raw.to_lowercase().as_str() {
366        "plain" | "normal" => Some(FontFace::Plain),
367        "bold" => Some(FontFace::Bold),
368        "italic" | "oblique" => Some(FontFace::Italic),
369        _ => None,
370    }
371}
372
373/// Clear all per-mark metadata after a geom has drawn its marks.
374pub(crate) fn clear_mark(backend: &mut dyn DrawBackend) {
375    set_mark(backend, None, None, None, None);
376    backend.set_mark_fvalue(None);
377    backend.set_mark_fx(None);
378}
379
380/// Set `data-fvalue`/`data-fx` (B2-4) for the mark `set_mark` just
381/// configured, formatted through the relevant scale — `value_aes`'s (Y for
382/// most geoms, Fill for hex/tile) for `data-fvalue`, X for `data-fx`.
383/// Always sets both (even to `None`), so a row without a formattable value
384/// doesn't inherit an earlier row's — the same per-row reset discipline as
385/// [`set_row_extras`]. A no-op query (`wants_formatted_values`) short-
386/// circuits the scale lookups entirely when
387/// [`crate::plot::GGPlot::svg_formatted_values`] is off (the default), so
388/// this costs nothing and changes nothing when never turned on.
389pub(crate) fn set_mark_formatted(
390    backend: &mut dyn DrawBackend,
391    scales: &crate::scale::ScaleSet,
392    x_raw: Option<&crate::data::Value>,
393    value_aes: &Aesthetic,
394    value_raw: Option<&crate::data::Value>,
395) {
396    if !backend.wants_formatted_values() {
397        backend.set_mark_fvalue(None);
398        backend.set_mark_fx(None);
399        return;
400    }
401    let fvalue = value_raw
402        .filter(|v| !v.is_na())
403        .and_then(|v| scales.get(value_aes).and_then(|s| s.format_value(v)));
404    backend.set_mark_fvalue(fvalue);
405    let fx = x_raw
406        .filter(|v| !v.is_na())
407        .and_then(|v| scales.get(&Aesthetic::X).and_then(|s| s.format_value(v)));
408    backend.set_mark_fx(fx);
409}
410
411/// Apply one row's interactive aesthetics (B1-7: `Aes::tooltip`,
412/// `Aes::data_attr`) to the backend.
413///
414/// A validated `Aes::tooltip` mapping replaces the geom's own `<title>`
415/// (call the geom's own `set_mark(...)` *before* this, so a custom tooltip
416/// wins) for *every* row — including when this row's value is NA, which
417/// explicitly resets the tooltip to `None` (no `<title>` at all, even if
418/// the geom has its own default): once `Aes::tooltip` is mapped, it owns
419/// the tooltip for the whole layer, the same row-by-row way `color` or
420/// `size` would. It never inherits an earlier row's. It's read from
421/// [`crate::aes::TOOLTIP_INTERNAL_NAME`], not a plain `"tooltip"` column
422/// name, so an unrelated raw source column literally called `"tooltip"`
423/// can never drive every mark's hover title (the same class of bug fixed
424/// for `data_attr` below). `set_tooltip` is only called at all when that
425/// internal column exists, so layers that never map `Aes::tooltip` don't
426/// touch whatever the geom already set.
427///
428/// Every validated `data_attr` column becomes a `data-<name>` attribute.
429/// These are stored under [`crate::aes::DATA_ATTR_INTERNAL_PREFIX`] rather
430/// than scanned by a `"data-"` name prefix: a raw source column that merely
431/// happens to be named like a data attribute (e.g. a SQL alias) was never
432/// validated and must never reach `push_mark` (see
433/// `aes::validated_data_attr_internal_name`, and `aes::mapping`'s defensive
434/// drop of any raw column that could forge an internal name).
435/// `set_mark_attrs` is *always* called, even with an empty list, so a row
436/// with no attrs doesn't inherit an earlier row's.
437pub(crate) fn set_row_extras(backend: &mut dyn DrawBackend, data: &DataFrame, row: usize) {
438    if let Some(col) = data.column(crate::aes::TOOLTIP_INTERNAL_NAME) {
439        let tip = col.get(row).filter(|v| !v.is_na()).map(tip_value);
440        backend.set_tooltip(tip);
441    }
442    let mut attrs = Vec::new();
443    for name in data.column_names() {
444        if let Some(target) = name.strip_prefix(crate::aes::DATA_ATTR_INTERNAL_PREFIX) {
445            if let Some(v) = data.column(name).and_then(|c| c.get(row)) {
446                if !v.is_na() {
447                    attrs.push((target.to_string(), tip_value(v)));
448                }
449            }
450        }
451    }
452    backend.set_mark_attrs(attrs);
453    // #221: `Aes::class` tokens (sanitized at build time) and the
454    // `Aes::group_wrap` group — always set, so a row never inherits an
455    // earlier row's.
456    let class = data
457        .column(crate::aes::CLASS_INTERNAL_NAME)
458        .and_then(|c| c.get(row))
459        .filter(|v| !v.is_na())
460        .map(tip_value)
461        .filter(|s| !s.is_empty());
462    backend.set_mark_class(class);
463    let mut group = None;
464    for name in data.column_names() {
465        if let Some(target) = name.strip_prefix(crate::aes::GROUP_WRAP_INTERNAL_PREFIX) {
466            if let Some(v) = data.column(name).and_then(|c| c.get(row)) {
467                if !v.is_na() {
468                    group = Some((target.to_string(), tip_value(v)));
469                    break;
470                }
471            }
472        }
473    }
474    backend.set_mark_group(group);
475}
476
477/// Clear interactive-aesthetic attributes *and* the tooltip after a layer
478/// finishes drawing, so neither leaks into a later layer, an annotation, or
479/// the legend's own background/key marks (which share the same backend
480/// instance for the rest of the render).
481pub(crate) fn clear_row_extras(backend: &mut dyn DrawBackend) {
482    backend.set_tooltip(None);
483    backend.set_mark_attrs(Vec::new());
484    backend.set_mark_class(None);
485    backend.set_mark_group(None);
486}
487
488/// Draw one group's polyline (path/line), honouring a per-point `linewidth`.
489/// A constant width draws one polyline (byte-identical to the pre-linewidth
490/// behaviour); a varying width falls back to drawing segment-by-segment,
491/// since a single stroked polyline can't vary its width along its length.
492/// An optional arrowhead is drawn once per polyline, using the width at its
493/// last point (closest to where a default `ends(Last)` head sits).
494#[allow(clippy::too_many_arguments)]
495pub(crate) fn draw_polyline_with_linewidth(
496    backend: &mut dyn DrawBackend,
497    points: &[(f64, f64)],
498    widths: &[f64],
499    color: (u8, u8, u8),
500    alpha: f64,
501    linetype: crate::render::backend::Linetype,
502    lineend: crate::render::backend::LineEnd,
503    arrow: Option<&crate::geom::arrow::Arrow>,
504    fallback_width: f64,
505) -> Result<(), RenderError> {
506    if points.len() < 2 {
507        return Ok(());
508    }
509    let w0 = widths.first().copied().unwrap_or(fallback_width);
510    let varies = widths.len() == points.len() && widths.iter().any(|&w| (w - w0).abs() > 1e-9);
511    if varies {
512        for i in 0..points.len() - 1 {
513            backend.draw_line(
514                &points[i..=i + 1],
515                &crate::render::backend::LineStyle {
516                    color,
517                    alpha,
518                    width: widths[i],
519                    linetype: linetype.clone(),
520                    lineend,
521                },
522            )?;
523        }
524    } else {
525        backend.draw_line(
526            points,
527            &crate::render::backend::LineStyle {
528                color,
529                alpha,
530                width: w0,
531                linetype,
532                lineend,
533            },
534        )?;
535    }
536    if let Some(arrow) = arrow {
537        let resolved_width = widths.last().copied().unwrap_or(fallback_width);
538        let hs = crate::geom::arrow::heads(points, arrow, resolved_width);
539        crate::geom::arrow::draw_heads(backend, &hs, arrow, color, alpha, resolved_width)?;
540    }
541    Ok(())
542}