Skip to main content

densa_chart/
line.rs

1use std::ops::Range;
2
3use densa_core::{Color, Rect};
4use densa_draw::Canvas;
5
6use crate::series::{points_within, with_neighbours};
7use crate::{Plot, Points};
8
9/// How a line is drawn: `LineStyle::new(color)`, [`with_width`](Self::with_width) for a
10/// thicker one.
11#[derive(Debug, Clone, Copy, PartialEq)]
12#[non_exhaustive]
13pub struct LineStyle {
14    /// The line's color.
15    pub color: Color,
16    /// Its thickness in pixels, measured vertically.
17    pub width: f32,
18}
19
20impl LineStyle {
21    /// A line one pixel thick in `color`.
22    pub const fn new(color: Color) -> Self {
23        Self { color, width: 1.0 }
24    }
25
26    /// The style `width` pixels thick (at least a tenth of a pixel).
27    #[must_use]
28    pub fn with_width(mut self, width: f32) -> Self {
29        self.width = usable_width(width);
30        self
31    }
32}
33
34/// `width` held to at least a tenth of a pixel; one pixel if it is not a number.
35fn usable_width(width: f32) -> f32 {
36    if width.is_finite() {
37        width.max(0.1)
38    } else {
39        1.0
40    }
41}
42
43/// Draws `points` as a line across `plot`, clipped to its area.
44///
45/// The line is drawn one pixel column at a time: each column gets one vertical span from
46/// the lowest to the highest point the line reaches in it, the ends shaded by how much of
47/// their pixel they cover. Far more points than columns cost one span per column (the
48/// columns still show every high and low, as M4 aggregation does), and far fewer still
49/// draw a joined line; each column gets at most one span, gaps or not, so a frame costs at
50/// most three rectangles per column. A gap (a `y` that is not finite) breaks the line. The
51/// work is proportional to the points within the plot plus the width that shows; nothing is
52/// allocated.
53pub fn paint_line<P: Points + ?Sized>(
54    canvas: &mut Canvas<'_>,
55    plot: &Plot,
56    points: &P,
57    style: LineStyle,
58) {
59    let area = plot.area();
60    if area.is_empty() {
61        return;
62    }
63    let state = canvas.state();
64    canvas.clip(area);
65    let range = with_neighbours(points_within(points, &plot.x()), points.len());
66    let at = range.map(|index| (plot.x_at(points.x(index)), plot.y_at(points.y(index))));
67    // Only the columns that show: a plot wider than its view costs the view's width.
68    let shown = canvas.clip_rect();
69    // Lossless enough: columns are whole pixels of a canvas.
70    let columns = shown.x0.floor() as i64..shown.x1.ceil() as i64;
71    let half = f64::from(usable_width(style.width)) / 2.0;
72    for_each_span(at, columns, |column, low, high| {
73        let top = plot.clamp_y(low - half);
74        let bottom = plot.clamp_y(high + half);
75        // Lossless enough: a pixel column of a canvas.
76        fill_span(canvas, column as f32, top, bottom, style.color);
77    });
78    canvas.restore(state);
79}
80
81/// Fills column `x` from `top` to `bottom` (pixels): whole pixels solid, the partly covered
82/// pixel at each end with its coverage as alpha.
83fn fill_span(canvas: &mut Canvas<'_>, x: f32, top: f64, bottom: f64, color: Color) {
84    let pixel = |canvas: &mut Canvas<'_>, y: f64, coverage: f64| {
85        // Lossless enough: pixels and a coverage within 0..=1.
86        let alpha = color.a * coverage as f32;
87        canvas.fill_rect(Rect::new(x, y as f32, 1.0, 1.0), color.with_alpha(alpha));
88    };
89    let (inner_top, inner_bottom) = (top.ceil(), bottom.floor());
90    if inner_top > inner_bottom {
91        // Within one pixel row.
92        pixel(canvas, top.floor(), bottom - top);
93        return;
94    }
95    if inner_top > top {
96        pixel(canvas, top.floor(), inner_top - top);
97    }
98    if inner_bottom > inner_top {
99        // Lossless enough: pixels.
100        let rect = Rect::from_edges(x, inner_top as f32, x + 1.0, inner_bottom as f32);
101        canvas.fill_rect(rect, color);
102    }
103    if bottom > inner_bottom {
104        pixel(canvas, inner_bottom, bottom - inner_bottom);
105    }
106}
107
108/// The part of a line in one pixel column: the lowest and highest `y` it reaches there.
109#[derive(Debug, Clone, Copy)]
110struct Span {
111    column: i64,
112    low: f64,
113    high: f64,
114}
115
116impl Span {
117    fn new(column: i64, a: f64, b: f64) -> Self {
118        Self {
119            column,
120            low: a.min(b),
121            high: a.max(b),
122        }
123    }
124
125    fn reach(&mut self, y: f64) {
126        self.low = self.low.min(y);
127        self.high = self.high.max(y);
128    }
129}
130
131/// Calls `emit(column, low, high)` for each pixel column in `columns` the polyline through
132/// `points` (canvas pixels, ascending `x`) passes, left to right: the `y` range the line
133/// covers within that column; for ascending `x`, at most one span per column. A point that
134/// is not finite breaks the line; a lone point is a span of no height.
135pub(crate) fn for_each_span(
136    points: impl Iterator<Item = (f64, f64)>,
137    columns: Range<i64>,
138    mut emit: impl FnMut(i64, f64, f64),
139) {
140    let mut flush = |span: Option<Span>| {
141        if let Some(span) = span.filter(|span| columns.contains(&span.column)) {
142            emit(span.column, span.low, span.high);
143        }
144    };
145    let mut open: Option<Span> = None;
146    let mut last: Option<(f64, f64)> = None;
147    for (x, y) in points {
148        if !(x.is_finite() && y.is_finite()) {
149            // A gap: nothing joins across it, but its column's span stays open, so a column
150            // still gets one span however many gaps fall in it.
151            last = None;
152            continue;
153        }
154        // Lossless enough: pixel columns, saturating far off the canvas.
155        let column = x.floor() as i64;
156        match (last, open.as_mut()) {
157            (_, Some(span)) if column == span.column => span.reach(y),
158            (Some((x0, y0)), Some(span)) if column > span.column => {
159                let slope = (y - y0) / (x - x0);
160                let y_at = |at: f64| y0 + (at - x0) * slope;
161                // Lossless enough: column edges.
162                let edge = |column: i64| y_at(column as f64);
163                span.reach(edge(span.column + 1));
164                let first = span.column + 1;
165                flush(open.take());
166                for between in first.max(columns.start)..column.min(columns.end) {
167                    flush(Some(Span::new(between, edge(between), edge(between + 1))));
168                }
169                open = Some(Span::new(column, edge(column), y));
170            }
171            _ => {
172                // The first point, one after a gap, or one left of the last (x out of
173                // order): start afresh.
174                flush(open.take());
175                open = Some(Span::new(column, y, y));
176            }
177        }
178        last = Some((x, y));
179    }
180    flush(open);
181}
182
183#[cfg(test)]
184mod tests {
185    use super::for_each_span;
186
187    fn spans(points: &[(f64, f64)], columns: std::ops::Range<i64>) -> Vec<(i64, f64, f64)> {
188        let mut out = Vec::new();
189        for_each_span(points.iter().copied(), columns, |column, low, high| {
190            out.push((column, low, high));
191        });
192        out
193    }
194
195    #[test]
196    fn a_flat_line_is_one_span_per_column_it_crosses() {
197        let got = spans(&[(0.5, 5.0), (9.5, 5.0)], 0..100);
198        let want: Vec<_> = (0..10).map(|column| (column, 5.0, 5.0)).collect();
199        assert_eq!(got, want);
200    }
201
202    #[test]
203    fn a_slope_covers_its_rise_in_each_column_and_neighbours_meet() {
204        let got = spans(&[(0.0, 0.0), (4.0, 8.0)], 0..100);
205        assert_eq!(
206            got,
207            [
208                (0, 0.0, 2.0),
209                (1, 2.0, 4.0),
210                (2, 4.0, 6.0),
211                (3, 6.0, 8.0),
212                (4, 8.0, 8.0)
213            ]
214        );
215    }
216
217    #[test]
218    fn points_within_one_column_become_its_lowest_and_highest() {
219        let got = spans(&[(3.1, 4.0), (3.4, 1.0), (3.6, 7.0), (3.9, 2.0)], 0..100);
220        assert_eq!(got, [(3, 1.0, 7.0)]);
221    }
222
223    #[test]
224    fn a_gap_breaks_the_line() {
225        let got = spans(
226            &[(0.0, 0.0), (1.5, f64::NAN), (3.0, 6.0), (4.0, 6.0)],
227            0..100,
228        );
229        assert_eq!(got, [(0, 0.0, 0.0), (3, 6.0, 6.0), (4, 6.0, 6.0)]);
230        let infinite = spans(&[(0.0, 0.0), (1.0, f64::INFINITY), (2.0, 0.0)], 0..100);
231        assert_eq!(infinite, [(0, 0.0, 0.0), (2, 0.0, 0.0)]);
232    }
233
234    #[test]
235    fn only_the_columns_asked_for_are_given() {
236        let got = spans(&[(-1_000.0, 0.0), (1_000.0, 2_000.0)], 0..3);
237        assert_eq!(
238            got,
239            [
240                (0, 1_000.0, 1_001.0),
241                (1, 1_001.0, 1_002.0),
242                (2, 1_002.0, 1_003.0)
243            ]
244        );
245        let far = spans(&[(-1e30, 1.0), (1e30, 1.0)], 0..2);
246        assert_eq!(far, [(0, 1.0, 1.0), (1, 1.0, 1.0)]);
247    }
248
249    #[test]
250    fn a_point_left_of_the_last_starts_the_line_afresh() {
251        let got = spans(&[(5.0, 0.0), (2.0, 1.0), (3.0, 3.0)], 0..100);
252        assert_eq!(got, [(5, 0.0, 0.0), (2, 1.0, 3.0), (3, 3.0, 3.0)]);
253    }
254
255    #[test]
256    fn gaps_within_a_column_leave_it_one_span() {
257        let points = [
258            (0.1, 1.0),
259            (0.2, f64::NAN),
260            (0.3, 5.0),
261            (0.4, f64::NAN),
262            (1.5, 2.0),
263        ];
264        assert_eq!(spans(&points, 0..9), [(0, 1.0, 5.0), (1, 2.0, 2.0)]);
265    }
266
267    #[test]
268    fn a_lone_point_is_a_span_of_no_height() {
269        assert_eq!(spans(&[(7.2, 3.0)], 0..100), [(7, 3.0, 3.0)]);
270        assert_eq!(spans(&[], 0..100), []);
271    }
272}