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}