Skip to main content

plotkit_core/charts/
scatter.rs

1//! Scatter chart builder methods.
2//!
3//! This module extends [`ScatterArtist`] with a fluent API for configuring
4//! scatter plot properties. Since [`Axes::scatter`] returns
5//! `Result<&mut ScatterArtist>`, these builder methods can be chained
6//! directly on the return value:
7//!
8//! ```ignore
9//! ax.scatter(&x, &y)?
10//!     .color(Color::TAB_ORANGE)
11//!     .marker(Marker::Diamond)
12//!     .size(8.0)
13//!     .label("Observations")
14//!     .alpha(0.7);
15//! ```
16
17use crate::artist::ScatterArtist;
18use crate::colormap::Colormap;
19use crate::decimate::{DecimateMethod, DecimateMode};
20use crate::primitives::Color;
21use crate::theme::Marker;
22
23impl ScatterArtist {
24    /// Sets the marker color.
25    ///
26    /// Applies the given [`Color`] to every marker rendered by this artist,
27    /// unless per-point colors have been set via [`colors`](Self::colors).
28    ///
29    /// # Arguments
30    ///
31    /// * `color` - The [`Color`] to fill each marker with.
32    ///
33    /// # Examples
34    ///
35    /// ```ignore
36    /// artist.color(Color::TAB_RED);
37    /// ```
38    pub fn color(&mut self, color: Color) -> &mut Self {
39        self.color = color;
40        self
41    }
42
43    /// Sets the marker shape.
44    ///
45    /// The [`Marker`] enum defines the available shapes (circle, square,
46    /// triangle, diamond, plus, cross, star, point). The default is
47    /// [`Marker::Circle`].
48    ///
49    /// # Arguments
50    ///
51    /// * `marker` - The [`Marker`] variant to use for every data point.
52    ///
53    /// # Examples
54    ///
55    /// ```ignore
56    /// artist.marker(Marker::Triangle);
57    /// ```
58    pub fn marker(&mut self, marker: Marker) -> &mut Self {
59        self.marker = marker;
60        self
61    }
62
63    /// Sets the marker size in pixels.
64    ///
65    /// This controls the diameter of each marker glyph. Larger values
66    /// produce more prominent data points; smaller values suit dense
67    /// scatter plots.
68    ///
69    /// # Arguments
70    ///
71    /// * `size` - The marker diameter in device-independent pixels.
72    ///
73    /// # Examples
74    ///
75    /// ```ignore
76    /// artist.size(10.0);
77    /// ```
78    pub fn size(&mut self, size: f64) -> &mut Self {
79        self.size = size;
80        self
81    }
82
83    /// Sets the legend label for this scatter series.
84    ///
85    /// When a label is set, the scatter series will appear in the legend
86    /// if one is displayed on the axes. Pass an empty string or omit this
87    /// call to exclude the series from the legend.
88    ///
89    /// # Arguments
90    ///
91    /// * `label` - A string slice that will be stored as the legend entry.
92    ///
93    /// # Examples
94    ///
95    /// ```ignore
96    /// artist.label("Measurements");
97    /// ```
98    pub fn label(&mut self, label: &str) -> &mut Self {
99        self.label = Some(label.to_string());
100        self
101    }
102
103    /// Sets the opacity (0.0 = fully transparent, 1.0 = fully opaque).
104    ///
105    /// The value is clamped to the `[0.0, 1.0]` range. The default opacity
106    /// is determined by the active theme (typically `0.8`).
107    ///
108    /// # Arguments
109    ///
110    /// * `alpha` - The desired opacity level.
111    ///
112    /// # Examples
113    ///
114    /// ```ignore
115    /// artist.alpha(0.5); // 50% transparent
116    /// ```
117    pub fn alpha(&mut self, alpha: f64) -> &mut Self {
118        self.alpha = alpha.clamp(0.0, 1.0);
119        self
120    }
121
122    /// Sets per-point colors, overriding the single uniform color.
123    ///
124    /// When set, each data point is rendered with its corresponding color
125    /// from the vector. The length of `colors` must equal the number of
126    /// data points (`x.len()` and `y.len()`). This is commonly used to
127    /// map a third variable to a colormap.
128    ///
129    /// Calling [`color`](Self::color) after this method does not clear the
130    /// per-point colors; the per-point colors take precedence during
131    /// rendering.
132    ///
133    /// # Arguments
134    ///
135    /// * `colors` - A vector of [`Color`] values, one per data point.
136    ///
137    /// # Examples
138    ///
139    /// ```ignore
140    /// artist.colors(vec![Color::TAB_BLUE, Color::TAB_RED, Color::TAB_GREEN]);
141    /// ```
142    pub fn colors(&mut self, colors: Vec<Color>) -> &mut Self {
143        self.colors = Some(colors);
144        self
145    }
146
147    /// Sets per-point scalar values for colormap-driven coloring.
148    ///
149    /// When combined with [`cmap`](Self::cmap), each scalar value is mapped
150    /// through the colormap to produce a per-point color. The length of `c`
151    /// must equal the number of data points. This takes precedence over
152    /// both the uniform [`color`](Self::color) and [`colors`](Self::colors).
153    ///
154    /// # Arguments
155    ///
156    /// * `c` - A vector of scalar values, one per data point.
157    ///
158    /// # Examples
159    ///
160    /// ```ignore
161    /// artist.c(vec![0.0, 0.5, 1.0]).cmap(Colormap::Viridis);
162    /// ```
163    pub fn c(&mut self, c: Vec<f64>) -> &mut Self {
164        self.c = Some(c);
165        self
166    }
167
168    /// Sets the colormap used to map `c` values to colors.
169    ///
170    /// Must be used together with [`c`](Self::c) to have any effect. When
171    /// both are set, the scatter plot renders each point with a color
172    /// determined by mapping its `c` value through the given colormap.
173    ///
174    /// # Arguments
175    ///
176    /// * `cmap` - The [`Colormap`] variant to use.
177    ///
178    /// # Examples
179    ///
180    /// ```ignore
181    /// artist.c(values).cmap(Colormap::Plasma);
182    /// ```
183    pub fn cmap(&mut self, cmap: Colormap) -> &mut Self {
184        self.cmap = Some(cmap);
185        self
186    }
187
188    /// Enables LTTB decimation with the given explicit point threshold.
189    ///
190    /// When the data series length exceeds `threshold`, the rendering pipeline
191    /// downsamples the points using the Largest Triangle Three Buckets
192    /// algorithm before drawing. Per-point styling (`colors`, `c`) stays
193    /// synchronized with the surviving points.
194    ///
195    /// This overrides the default [`DecimateMode::Auto`] behavior with an
196    /// explicit threshold. To disable decimation entirely, use
197    /// [`no_decimate`](Self::no_decimate).
198    ///
199    /// # Examples
200    ///
201    /// ```ignore
202    /// ax.scatter(&x, &y)?.decimate(2000);
203    /// ```
204    pub fn decimate(&mut self, threshold: usize) -> &mut Self {
205        self.decimate = DecimateMode::Explicit(threshold, DecimateMethod::Lttb);
206        self
207    }
208
209    /// Enables decimation with a specific method and explicit point threshold.
210    ///
211    /// Available methods:
212    /// - [`DecimateMethod::Lttb`] — best visual fidelity (default)
213    /// - [`DecimateMethod::MinMax`] — fastest, preserves peaks/troughs
214    ///
215    /// # Examples
216    ///
217    /// ```ignore
218    /// ax.scatter(&x, &y)?.decimate_with(2000, DecimateMethod::MinMax);
219    /// ```
220    pub fn decimate_with(&mut self, threshold: usize, method: DecimateMethod) -> &mut Self {
221        self.decimate = DecimateMode::Explicit(threshold, method);
222        self
223    }
224
225    /// Disables decimation entirely, drawing every point.
226    ///
227    /// By default large series are auto-decimated via [`DecimateMode::Auto`].
228    /// Call this when every marker must be rendered regardless of series size.
229    ///
230    /// # Examples
231    ///
232    /// ```ignore
233    /// ax.scatter(&x, &y)?.no_decimate();
234    /// ```
235    pub fn no_decimate(&mut self) -> &mut Self {
236        self.decimate = DecimateMode::Off;
237        self
238    }
239}
240
241// ---------------------------------------------------------------------------
242// Tests
243// ---------------------------------------------------------------------------
244
245#[cfg(test)]
246mod tests {
247    use crate::axes::Axes;
248    use crate::decimate::{DecimateMethod, DecimateMode, DEFAULT_DECIMATE_THRESHOLD};
249
250    fn make_axes() -> Axes {
251        Axes::new()
252    }
253
254    #[test]
255    fn scatter_defaults_to_auto_decimate() {
256        let mut ax = make_axes();
257        let artist = ax.scatter(&[0.0, 1.0, 2.0], &[0.0, 1.0, 2.0]).unwrap();
258        assert_eq!(artist.decimate, DecimateMode::Auto);
259    }
260
261    #[test]
262    fn scatter_decimate_sets_explicit_lttb() {
263        let mut ax = make_axes();
264        let artist = ax.scatter(&[0.0, 1.0], &[0.0, 1.0]).unwrap();
265        artist.decimate(2000);
266        assert_eq!(
267            artist.decimate,
268            DecimateMode::Explicit(2000, DecimateMethod::Lttb)
269        );
270    }
271
272    #[test]
273    fn scatter_decimate_with_sets_explicit_method() {
274        let mut ax = make_axes();
275        let artist = ax.scatter(&[0.0, 1.0], &[0.0, 1.0]).unwrap();
276        artist.decimate_with(750, DecimateMethod::MinMax);
277        assert_eq!(
278            artist.decimate,
279            DecimateMode::Explicit(750, DecimateMethod::MinMax)
280        );
281    }
282
283    #[test]
284    fn scatter_no_decimate_disables() {
285        let mut ax = make_axes();
286        let artist = ax.scatter(&[0.0, 1.0], &[0.0, 1.0]).unwrap();
287        artist.no_decimate();
288        assert_eq!(artist.decimate, DecimateMode::Off);
289    }
290
291    #[test]
292    fn scatter_auto_kicks_in_above_threshold() {
293        let n = DEFAULT_DECIMATE_THRESHOLD + 2500;
294        let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
295        let y: Vec<f64> = x.iter().map(|v| (v * 0.02).cos()).collect();
296        let indices = DecimateMode::Auto.resolve_indices(&x, &y);
297        assert_eq!(indices.len(), DEFAULT_DECIMATE_THRESHOLD);
298        assert_eq!(*indices.first().unwrap(), 0);
299        assert_eq!(*indices.last().unwrap(), n - 1);
300    }
301
302    #[test]
303    fn scatter_auto_no_op_below_threshold() {
304        let n = 100;
305        let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
306        let y = x.clone();
307        let indices = DecimateMode::Auto.resolve_indices(&x, &y);
308        assert_eq!(indices.len(), n);
309        assert_eq!(indices, (0..n).collect::<Vec<_>>());
310    }
311
312    #[test]
313    fn scatter_no_decimate_keeps_all_points() {
314        let n = DEFAULT_DECIMATE_THRESHOLD * 3;
315        let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
316        let y = x.clone();
317        let indices = DecimateMode::Off.resolve_indices(&x, &y);
318        assert_eq!(indices.len(), n);
319        assert_eq!(*indices.first().unwrap(), 0);
320        assert_eq!(*indices.last().unwrap(), n - 1);
321    }
322
323    #[test]
324    fn scatter_explicit_minmax_overrides_auto() {
325        let n = 8000;
326        let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
327        let y: Vec<f64> = x.iter().map(|v| (v * 0.03).sin()).collect();
328        let indices =
329            DecimateMode::Explicit(300, DecimateMethod::MinMax).resolve_indices(&x, &y);
330        assert!(indices.len() <= 300);
331        assert_eq!(*indices.first().unwrap(), 0);
332        assert_eq!(*indices.last().unwrap(), n - 1);
333    }
334}