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 /// Whether this geom draws from a 0 baseline, so the Y scale should include
104 /// 0 even when `y` is explicitly mapped (bars/columns/area/histograms — as
105 /// in ggplot2). Default false.
106 fn include_zero_baseline(&self) -> bool {
107 false
108 }
109
110 /// Whether `-Inf`/`Inf` position values are meaningful for this geom
111 /// (ggplot2: "extend to the panel edge"). Rows with infinite positions are
112 /// otherwise dropped with a warning before stats run. `NaN` is always
113 /// dropped. Default false; `geom_rect` returns true.
114 fn allows_infinite(&self) -> bool {
115 false
116 }
117
118 /// Geom-specific data preparation after the stat and position steps but
119 /// before scale training (ggplot2's `GeomX$setup_data`) — e.g. a tile adds
120 /// its `xmin`/`xmax`/`ymin`/`ymax` extents so continuous scales train on
121 /// them. Default: no-op.
122 fn setup_data(&self, _data: &mut DataFrame) {}
123
124 /// Whether the layer inherits the plot-level aesthetic mapping (ggplot2's
125 /// `inherit.aes`). Reference-line geoms return false, as in ggplot2, so a
126 /// plot-level `color`/`x`/`y` mapping does not leak into a `geom_hline`.
127 /// Default true.
128 fn inherit_aes(&self) -> bool {
129 true
130 }
131}
132
133/// Format a value for a hover tooltip — strings verbatim, numbers rounded short,
134/// `Na` empty.
135pub(crate) fn tip_value(v: &crate::data::Value) -> String {
136 crate::format::format_value(v)
137}
138
139/// Half-width (normalized panel units) for bars on a continuous / date x axis:
140/// `width` × the smallest gap between distinct mapped x positions, like
141/// ggplot2's `resolution(x)`. A fixed fraction would make 60 daily bars overlap
142/// and 3 bars look like slivers. Falls back to `fallback` with < 2 distinct xs.
143pub(crate) fn continuous_bar_half_width(
144 mapped_xs: impl Iterator<Item = f64>,
145 width: f64,
146 fallback: f64,
147) -> f64 {
148 let mut xs: Vec<f64> = mapped_xs.filter(|x| x.is_finite()).collect();
149 xs.sort_by(|a, b| a.total_cmp(b));
150 xs.dedup_by(|a, b| (*a - *b).abs() < 1e-12);
151 let gap = xs
152 .windows(2)
153 .map(|w| w[1] - w[0])
154 .fold(f64::INFINITY, f64::min);
155 if gap.is_finite() && gap > 0.0 {
156 gap * width / 2.0
157 } else {
158 fallback
159 }
160}
161
162/// Raw (unformatted) value for a `data-value` attribute: numbers in shortest
163/// round-trip form, date-times as epoch seconds, strings verbatim. `None` for
164/// missing / non-finite values.
165pub(crate) fn raw_value(v: &crate::data::Value) -> Option<String> {
166 use crate::data::Value;
167 match v {
168 Value::Float(f) if f.is_finite() => Some(format!("{f}")),
169 Value::Float(_) | Value::Na => None,
170 Value::Integer(i) => Some(i.to_string()),
171 Value::DateTime(s) => Some(s.to_string()),
172 Value::Str(s) => Some(s.clone()),
173 Value::Bool(b) => Some(b.to_string()),
174 }
175}
176
177/// The series key of row `i` — its colour, else fill, else group level — for
178/// a `data-series` attribute.
179pub(crate) fn series_key(data: &DataFrame, i: usize) -> Option<String> {
180 ["color", "fill", "group"].iter().find_map(|c| {
181 data.column(c)
182 .and_then(|col| col.get(i))
183 .filter(|v| !v.is_na())
184 .map(tip_value)
185 .filter(|s| !s.is_empty())
186 })
187}
188
189/// The raw measured y of row `i`. Stacked/filled positions overwrite `y` with
190/// the cumulative top, so prefer the pre-position value they preserve.
191pub(crate) fn measured_value(data: &DataFrame, i: usize) -> Option<String> {
192 data.column(crate::position::RAW_Y_COL)
193 .or_else(|| data.column("y"))
194 .and_then(|c| c.get(i))
195 .and_then(raw_value)
196}
197
198/// Set all per-mark metadata (tooltip, `data-x`, `data-series`, `data-value`)
199/// for the next drawn mark(s).
200pub(crate) fn set_mark(
201 backend: &mut dyn DrawBackend,
202 tooltip: Option<String>,
203 x: Option<String>,
204 series: Option<String>,
205 value: Option<String>,
206) {
207 backend.set_tooltip(tooltip);
208 backend.set_mark_axis(x);
209 backend.set_mark_series(series);
210 backend.set_mark_value(value);
211}
212
213/// Clear all per-mark metadata after a geom has drawn its marks.
214pub(crate) fn clear_mark(backend: &mut dyn DrawBackend) {
215 set_mark(backend, None, None, None, None);
216}
217
218/// Apply one row's interactive aesthetics (B1-7: `Aes::tooltip`,
219/// `Aes::data_attr`) to the backend.
220///
221/// A validated `Aes::tooltip` mapping replaces the geom's own `<title>`
222/// (call the geom's own `set_mark(...)` *before* this, so a custom tooltip
223/// wins) for *every* row — including when this row's value is NA, which
224/// explicitly resets the tooltip to `None` (no `<title>` at all, even if
225/// the geom has its own default): once `Aes::tooltip` is mapped, it owns
226/// the tooltip for the whole layer, the same row-by-row way `color` or
227/// `size` would. It never inherits an earlier row's. It's read from
228/// [`crate::aes::TOOLTIP_INTERNAL_NAME`], not a plain `"tooltip"` column
229/// name, so an unrelated raw source column literally called `"tooltip"`
230/// can never drive every mark's hover title (the same class of bug fixed
231/// for `data_attr` below). `set_tooltip` is only called at all when that
232/// internal column exists, so layers that never map `Aes::tooltip` don't
233/// touch whatever the geom already set.
234///
235/// Every validated `data_attr` column becomes a `data-<name>` attribute.
236/// These are stored under [`crate::aes::DATA_ATTR_INTERNAL_PREFIX`] rather
237/// than scanned by a `"data-"` name prefix: a raw source column that merely
238/// happens to be named like a data attribute (e.g. a SQL alias) was never
239/// validated and must never reach `push_mark` (see
240/// `aes::validated_data_attr_internal_name`, and `aes::mapping`'s defensive
241/// drop of any raw column that could forge an internal name).
242/// `set_mark_attrs` is *always* called, even with an empty list, so a row
243/// with no attrs doesn't inherit an earlier row's.
244pub(crate) fn set_row_extras(backend: &mut dyn DrawBackend, data: &DataFrame, row: usize) {
245 if let Some(col) = data.column(crate::aes::TOOLTIP_INTERNAL_NAME) {
246 let tip = col.get(row).filter(|v| !v.is_na()).map(tip_value);
247 backend.set_tooltip(tip);
248 }
249 let mut attrs = Vec::new();
250 for name in data.column_names() {
251 if let Some(target) = name.strip_prefix(crate::aes::DATA_ATTR_INTERNAL_PREFIX) {
252 if let Some(v) = data.column(name).and_then(|c| c.get(row)) {
253 if !v.is_na() {
254 attrs.push((target.to_string(), tip_value(v)));
255 }
256 }
257 }
258 }
259 backend.set_mark_attrs(attrs);
260}
261
262/// Clear interactive-aesthetic attributes *and* the tooltip after a layer
263/// finishes drawing, so neither leaks into a later layer, an annotation, or
264/// the legend's own background/key marks (which share the same backend
265/// instance for the rest of the render).
266pub(crate) fn clear_row_extras(backend: &mut dyn DrawBackend) {
267 backend.set_tooltip(None);
268 backend.set_mark_attrs(Vec::new());
269}
270
271/// Draw one group's polyline (path/line), honouring a per-point `linewidth`.
272/// A constant width draws one polyline (byte-identical to the pre-linewidth
273/// behaviour); a varying width falls back to drawing segment-by-segment,
274/// since a single stroked polyline can't vary its width along its length.
275/// An optional arrowhead is drawn once per polyline, using the width at its
276/// last point (closest to where a default `ends(Last)` head sits).
277#[allow(clippy::too_many_arguments)]
278pub(crate) fn draw_polyline_with_linewidth(
279 backend: &mut dyn DrawBackend,
280 points: &[(f64, f64)],
281 widths: &[f64],
282 color: (u8, u8, u8),
283 alpha: f64,
284 linetype: crate::render::backend::Linetype,
285 lineend: crate::render::backend::LineEnd,
286 arrow: Option<&crate::geom::arrow::Arrow>,
287 fallback_width: f64,
288) -> Result<(), RenderError> {
289 if points.len() < 2 {
290 return Ok(());
291 }
292 let w0 = widths.first().copied().unwrap_or(fallback_width);
293 let varies = widths.len() == points.len() && widths.iter().any(|&w| (w - w0).abs() > 1e-9);
294 if varies {
295 for i in 0..points.len() - 1 {
296 backend.draw_line(
297 &points[i..=i + 1],
298 &crate::render::backend::LineStyle {
299 color,
300 alpha,
301 width: widths[i],
302 linetype,
303 lineend,
304 },
305 )?;
306 }
307 } else {
308 backend.draw_line(
309 points,
310 &crate::render::backend::LineStyle {
311 color,
312 alpha,
313 width: w0,
314 linetype,
315 lineend,
316 },
317 )?;
318 }
319 if let Some(arrow) = arrow {
320 let resolved_width = widths.last().copied().unwrap_or(fallback_width);
321 let hs = crate::geom::arrow::heads(points, arrow, resolved_width);
322 crate::geom::arrow::draw_heads(backend, &hs, arrow, color, alpha, resolved_width)?;
323 }
324 Ok(())
325}