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