Skip to main content

ggplot_rs/geom/
mod.rs

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