Skip to main content

plotkit_core/charts/
line.rs

1//! Line chart builder methods.
2//!
3//! This module extends [`LineArtist`] with a fluent API for configuring
4//! line chart properties. Since [`Axes::plot`] returns `Result<&mut LineArtist>`,
5//! these builder methods can be chained directly on the return value:
6//!
7//! ```ignore
8//! ax.plot(&x, &y)?
9//!     .color(Color::rgb(0.2, 0.4, 0.8))
10//!     .width(2.0)
11//!     .style(LineStyle::Dashed)
12//!     .label("Series A")
13//!     .alpha(0.9);
14//! ```
15
16use crate::artist::LineArtist;
17use crate::decimate::{DecimateMethod, DecimateMode};
18use crate::primitives::Color;
19use crate::theme::LineStyle;
20
21impl LineArtist {
22    /// Sets the line color.
23    ///
24    /// Accepts any [`Color`] value, which can be constructed from RGB components,
25    /// hex strings, or named color constants.
26    ///
27    /// # Examples
28    ///
29    /// ```ignore
30    /// artist.color(Color::rgb(1.0, 0.0, 0.0)); // red
31    /// ```
32    pub fn color(&mut self, color: Color) -> &mut Self {
33        self.color = color;
34        self
35    }
36
37    /// Sets the line width in pixels.
38    ///
39    /// A width of `1.0` is the default hairline width. Values below `1.0` may
40    /// produce sub-pixel rendering depending on the backend.
41    ///
42    /// # Examples
43    ///
44    /// ```ignore
45    /// artist.width(2.5);
46    /// ```
47    pub fn width(&mut self, width: f64) -> &mut Self {
48        self.width = width;
49        self
50    }
51
52    /// Sets the line style (solid, dashed, dotted, dash-dot).
53    ///
54    /// The [`LineStyle`] enum defines the available stroke patterns. The default
55    /// is [`LineStyle::Solid`].
56    ///
57    /// # Examples
58    ///
59    /// ```ignore
60    /// artist.style(LineStyle::Dashed);
61    /// ```
62    pub fn style(&mut self, style: LineStyle) -> &mut Self {
63        self.style = style;
64        self
65    }
66
67    /// Sets the legend label for this line.
68    ///
69    /// When a label is set, the line will appear in the legend if one is
70    /// displayed on the axes. Pass an empty string or omit this call to
71    /// exclude the line from the legend.
72    ///
73    /// # Examples
74    ///
75    /// ```ignore
76    /// artist.label("Temperature");
77    /// ```
78    pub fn label(&mut self, label: &str) -> &mut Self {
79        self.label = Some(label.to_string());
80        self
81    }
82
83    /// Sets the opacity (0.0 = fully transparent, 1.0 = fully opaque).
84    ///
85    /// The value is clamped to the `[0.0, 1.0]` range. The default opacity
86    /// is `1.0`.
87    ///
88    /// # Examples
89    ///
90    /// ```ignore
91    /// artist.alpha(0.5); // 50% transparent
92    /// ```
93    pub fn alpha(&mut self, alpha: f64) -> &mut Self {
94        self.alpha = alpha.clamp(0.0, 1.0);
95        self
96    }
97
98    /// Enables LTTB decimation with the given explicit point threshold.
99    ///
100    /// When the data series length exceeds `threshold`, the rendering
101    /// pipeline downsamples the data using the Largest Triangle Three
102    /// Buckets algorithm before drawing. This dramatically improves
103    /// rendering performance for large datasets (100k+ points) with
104    /// negligible visual impact.
105    ///
106    /// This overrides the default [`DecimateMode::Auto`] behavior with an
107    /// explicit threshold. To disable decimation entirely, use
108    /// [`no_decimate`](Self::no_decimate).
109    ///
110    /// # Examples
111    ///
112    /// ```ignore
113    /// ax.plot(&x, &y)?.decimate(1000);
114    /// ```
115    pub fn decimate(&mut self, threshold: usize) -> &mut Self {
116        self.decimate = DecimateMode::Explicit(threshold, DecimateMethod::Lttb);
117        self
118    }
119
120    /// Enables decimation with a specific method and explicit point threshold.
121    ///
122    /// Available methods:
123    /// - [`DecimateMethod::Lttb`] — best visual fidelity (default)
124    /// - [`DecimateMethod::MinMax`] — fastest, preserves peaks/troughs
125    ///
126    /// # Examples
127    ///
128    /// ```ignore
129    /// ax.plot(&x, &y)?.decimate_with(1000, DecimateMethod::MinMax);
130    /// ```
131    pub fn decimate_with(&mut self, threshold: usize, method: DecimateMethod) -> &mut Self {
132        self.decimate = DecimateMode::Explicit(threshold, method);
133        self
134    }
135
136    /// Disables decimation entirely, drawing every point.
137    ///
138    /// By default large series are auto-decimated via
139    /// [`DecimateMode::Auto`]. Call this when exact point-for-point rendering
140    /// is required regardless of series size.
141    ///
142    /// # Examples
143    ///
144    /// ```ignore
145    /// ax.plot(&x, &y)?.no_decimate();
146    /// ```
147    pub fn no_decimate(&mut self) -> &mut Self {
148        self.decimate = DecimateMode::Off;
149        self
150    }
151}
152
153// ---------------------------------------------------------------------------
154// Tests
155// ---------------------------------------------------------------------------
156
157#[cfg(test)]
158mod tests {
159    use crate::axes::Axes;
160    use crate::decimate::{DecimateMethod, DecimateMode, DEFAULT_DECIMATE_THRESHOLD};
161
162    fn make_axes() -> Axes {
163        Axes::new()
164    }
165
166    #[test]
167    fn line_defaults_to_auto_decimate() {
168        let mut ax = make_axes();
169        let artist = ax.plot([0.0, 1.0, 2.0], [0.0, 1.0, 2.0]).unwrap();
170        assert_eq!(artist.decimate, DecimateMode::Auto);
171    }
172
173    #[test]
174    fn line_decimate_sets_explicit_lttb() {
175        let mut ax = make_axes();
176        let artist = ax.plot([0.0, 1.0], [0.0, 1.0]).unwrap();
177        artist.decimate(1000);
178        assert_eq!(
179            artist.decimate,
180            DecimateMode::Explicit(1000, DecimateMethod::Lttb)
181        );
182    }
183
184    #[test]
185    fn line_decimate_with_sets_explicit_method() {
186        let mut ax = make_axes();
187        let artist = ax.plot([0.0, 1.0], [0.0, 1.0]).unwrap();
188        artist.decimate_with(500, DecimateMethod::MinMax);
189        assert_eq!(
190            artist.decimate,
191            DecimateMode::Explicit(500, DecimateMethod::MinMax)
192        );
193    }
194
195    #[test]
196    fn line_no_decimate_disables() {
197        let mut ax = make_axes();
198        let artist = ax.plot([0.0, 1.0], [0.0, 1.0]).unwrap();
199        artist.no_decimate();
200        assert_eq!(artist.decimate, DecimateMode::Off);
201    }
202
203    #[test]
204    fn line_auto_kicks_in_above_threshold() {
205        let n = DEFAULT_DECIMATE_THRESHOLD + 1000;
206        let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
207        let y: Vec<f64> = x.iter().map(|v| (v * 0.01).sin()).collect();
208        let indices = DecimateMode::Auto.resolve_indices(&x, &y);
209        assert_eq!(indices.len(), DEFAULT_DECIMATE_THRESHOLD);
210        assert_eq!(*indices.first().unwrap(), 0);
211        assert_eq!(*indices.last().unwrap(), n - 1);
212    }
213
214    #[test]
215    fn line_auto_no_op_at_or_below_threshold() {
216        let n = DEFAULT_DECIMATE_THRESHOLD;
217        let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
218        let y = x.clone();
219        let indices = DecimateMode::Auto.resolve_indices(&x, &y);
220        assert_eq!(indices.len(), n);
221        assert_eq!(indices.first().copied(), Some(0));
222        assert_eq!(indices.last().copied(), Some(n - 1));
223    }
224
225    #[test]
226    fn line_no_decimate_draws_all_points_even_when_huge() {
227        let n = DEFAULT_DECIMATE_THRESHOLD * 2;
228        let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
229        let y = x.clone();
230        let indices = DecimateMode::Off.resolve_indices(&x, &y);
231        assert_eq!(indices.len(), n);
232    }
233
234    #[test]
235    fn line_explicit_overrides_auto_threshold() {
236        // 6000 points: auto would cut to 5000, but explicit 100 wins.
237        let n = 6000;
238        let x: Vec<f64> = (0..n).map(|i| i as f64).collect();
239        let y: Vec<f64> = x.iter().map(|v| (v * 0.05).sin()).collect();
240        let indices = DecimateMode::Explicit(100, DecimateMethod::Lttb).resolve_indices(&x, &y);
241        assert_eq!(indices.len(), 100);
242        assert_eq!(*indices.first().unwrap(), 0);
243        assert_eq!(*indices.last().unwrap(), n - 1);
244    }
245}