Skip to main content

densa_chart/
painter.rs

1use densa_core::{Color, Point, Rect};
2use densa_draw::Canvas;
3use densa_fmt::FmtBuf;
4use densa_text::{Font, GlyphCache, GlyphMap, SUBPIXEL_STEPS};
5
6use crate::{AxisKind, ChartPalette, Plot, Ticks};
7
8/// Bytes a tick label may take.
9const LABEL_BYTES: usize = 32;
10
11/// Every character a tick label can use.
12const LABEL_CHARS: &str = "0123456789-.:";
13
14/// Characters an x label is given room for (a date, and a gap to the next one).
15const X_LABEL_ROOM: f32 = 11.0;
16
17/// Characters an x label below a second is given room for (`HH:MM:SS.fff` and a gap).
18const X_FINE_LABEL_ROOM: f32 = 15.0;
19
20/// Line heights between two y ticks, at least.
21const Y_TICK_LINES: f32 = 2.5;
22
23/// How many times more ticks a crosshair's value is written for: one digit finer.
24const FINER: usize = 10;
25
26/// Draws what is around a chart's series: gridlines at the ticks, the axis edges and the
27/// tick labels, right of the plot for `y` and under it for `x`, and a crosshair over it.
28///
29/// Owns the label font's character map; glyphs come from the canvas's shared cache.
30/// Painting allocates nothing once the label characters are cached (see
31/// [`prewarm`](Self::prewarm)).
32#[derive(Debug)]
33pub struct ChartPainter {
34    glyphs: GlyphMap,
35    palette: ChartPalette,
36    label: FmtBuf<LABEL_BYTES>,
37    label_chars: f32,
38}
39
40impl ChartPainter {
41    /// A painter writing labels in `font` at `px` pixels per em, with room for `y` labels
42    /// of 8 characters.
43    pub fn new(font: Font, px: f32, palette: ChartPalette) -> Self {
44        Self {
45            glyphs: GlyphMap::new(font, px),
46            palette,
47            label: FmtBuf::new(),
48            label_chars: 8.0,
49        }
50    }
51
52    /// The painter with room for `y` labels of `chars` characters.
53    #[must_use]
54    pub fn with_label_chars(mut self, chars: u16) -> Self {
55        self.label_chars = f32::from(chars.max(1));
56        self
57    }
58
59    /// The colors this painter draws with.
60    pub fn palette(&self) -> &ChartPalette {
61        &self.palette
62    }
63
64    /// Draws with `palette` from now on.
65    pub fn set_palette(&mut self, palette: ChartPalette) {
66        self.palette = palette;
67    }
68
69    /// Rasterizes every character a label can use into `cache` at every subpixel position
70    /// now, so frames never rasterize (or allocate) for labels. Returns how many glyphs could
71    /// not be cached.
72    pub fn prewarm(&mut self, cache: &mut GlyphCache) -> usize {
73        let mut missing = 0;
74        for ch in LABEL_CHARS.chars() {
75            let Some(mapped) = self.glyphs.get(ch) else {
76                continue;
77            };
78            for bin in 0..SUBPIXEL_STEPS {
79                let cached = cache.get(self.glyphs.font(), mapped.id, self.glyphs.px(), bin);
80                missing += usize::from(cached.is_err());
81            }
82        }
83        missing
84    }
85
86    /// The area of `bounds` left for the plot once the `y` labels (right) and, if
87    /// `x_labels`, the `x` labels (below) have their room; edges on whole pixels. Charts
88    /// stacked on one time axis label `x` under the lowest only.
89    pub fn plot_area(&mut self, bounds: Rect, x_labels: bool) -> Rect {
90        let right = (self.char_width() * self.label_chars + 2.0 * self.pad()).round();
91        let below = if x_labels {
92            (self.glyphs.line_metrics().line_height() + self.pad()).round()
93        } else {
94            0.0
95        };
96        Rect::from_edges(
97            bounds.x0.round(),
98            bounds.y0.round(),
99            (bounds.x1 - right).round(),
100            (bounds.y1 - below).round(),
101        )
102    }
103
104    /// Draws the gridlines, the axis edges and the `y` labels of `plot`, and its `x` labels
105    /// if `x_labels`. Call it before the series so they draw over the grid.
106    pub fn paint_axes(&mut self, canvas: &mut Canvas<'_>, plot: &Plot, x_labels: bool) {
107        let area = plot.area();
108        if area.is_empty() {
109            return;
110        }
111        let metrics = self.glyphs.line_metrics();
112        let pad = self.pad();
113        let palette = self.palette;
114
115        let ticks = self.y_ticks(plot, 1);
116        for value in ticks.clone() {
117            // Lossless enough: pixels.
118            let y = plot.y_at(value).round() as f32;
119            if !(area.y0..area.y1).contains(&y) {
120                continue;
121            }
122            canvas.fill_rect(Rect::new(area.x0, y, area.width(), 1.0), palette.grid);
123            self.label.clear();
124            if ticks.write_label(value, &mut self.label).is_ok() {
125                let baseline = (y + (metrics.ascent + metrics.descent) / 2.0).round();
126                let at = Point::new(area.x1 + pad, baseline);
127                draw_text(
128                    &mut self.glyphs,
129                    canvas,
130                    self.label.as_str(),
131                    at,
132                    palette.label,
133                );
134            }
135        }
136
137        let ticks = self.x_ticks(plot, 1);
138        for value in ticks.clone() {
139            // Lossless enough: pixels.
140            let x = plot.x_at(value).round() as f32;
141            if !(area.x0..area.x1).contains(&x) {
142                continue;
143            }
144            canvas.fill_rect(Rect::new(x, area.y0, 1.0, area.height()), palette.grid);
145            self.label.clear();
146            if x_labels && ticks.write_label(value, &mut self.label).is_ok() {
147                let width = self.glyphs.measure(self.label.as_str());
148                let left = (x - width / 2.0).clamp(area.x0, (area.x1 - width).max(area.x0));
149                let at = Point::new(left.round(), (area.y1 + pad + metrics.ascent).round());
150                draw_text(
151                    &mut self.glyphs,
152                    canvas,
153                    self.label.as_str(),
154                    at,
155                    palette.label,
156                );
157            }
158        }
159
160        let right = Rect::new(area.x1, area.y0, 1.0, area.height() + 1.0);
161        canvas.fill_rect(right, palette.axis);
162        canvas.fill_rect(Rect::new(area.x0, area.y1, area.width(), 1.0), palette.axis);
163    }
164
165    /// Draws a crosshair at `at` over `plot`: a line down the plot through `at`, and, if
166    /// `at` is within the plot, a line across it with the `y` there in a box right of the
167    /// plot; with `x_label`, the `x` there in a box under the plot. The values are written
168    /// finer than the tick labels. For stacked plots, give each one the same `at` and label
169    /// `x` under the lowest.
170    pub fn paint_crosshair(
171        &mut self,
172        canvas: &mut Canvas<'_>,
173        plot: &Plot,
174        at: Point,
175        x_label: bool,
176    ) {
177        let area = plot.area();
178        let x = at.x.round();
179        if area.is_empty() || !(area.x0..area.x1).contains(&x) {
180            return;
181        }
182        let metrics = self.glyphs.line_metrics();
183        let (pad, line) = (self.pad(), metrics.line_height().round());
184        let palette = self.palette;
185        canvas.fill_rect(Rect::new(x, area.y0, 1.0, area.height()), palette.crosshair);
186
187        let y = at.y.round();
188        if (area.y0..area.y1).contains(&y) {
189            canvas.fill_rect(Rect::new(area.x0, y, area.width(), 1.0), palette.crosshair);
190            let fine = self.y_ticks(plot, FINER);
191            self.label.clear();
192            if fine.write_label(plot.y_value(y), &mut self.label).is_ok() {
193                let width = self.glyphs.measure(self.label.as_str());
194                let top = (y - line / 2.0).round();
195                let marker = Rect::new(area.x1 + 1.0, top, width + 2.0 * pad, line);
196                canvas.fill_rect(marker, palette.marker);
197                let baseline = (y + (metrics.ascent + metrics.descent) / 2.0).round();
198                let at = Point::new(area.x1 + 1.0 + pad, baseline);
199                draw_text(
200                    &mut self.glyphs,
201                    canvas,
202                    self.label.as_str(),
203                    at,
204                    palette.marker_text,
205                );
206            }
207        }
208
209        if x_label {
210            let fine = self.x_ticks(plot, FINER);
211            self.label.clear();
212            if fine.write_label(plot.x_value(x), &mut self.label).is_ok() {
213                let width = self.glyphs.measure(self.label.as_str()) + 2.0 * pad;
214                let left = (x - width / 2.0)
215                    .clamp(area.x0, (area.x1 - width).max(area.x0))
216                    .round();
217                let marker = Rect::new(left, area.y1 + 1.0, width, pad + line);
218                canvas.fill_rect(marker, palette.marker);
219                let at = Point::new(left + pad, (area.y1 + pad + metrics.ascent).round());
220                draw_text(
221                    &mut self.glyphs,
222                    canvas,
223                    self.label.as_str(),
224                    at,
225                    palette.marker_text,
226                );
227            }
228        }
229    }
230
231    /// The height of the line a title takes above a plot: leave it out of the bounds given
232    /// to [`plot_area`](Self::plot_area) for a plot with a title.
233    pub fn title_height(&self) -> f32 {
234        self.glyphs.line_metrics().line_height().round()
235    }
236
237    /// Writes `title` on the line of [`title_height`](Self::title_height) above `plot`,
238    /// from its left edge, cut off where the plot ends: above the plot, it hides none of
239    /// it. An empty title draws nothing.
240    pub fn paint_title(&mut self, canvas: &mut Canvas<'_>, plot: &Plot, title: &str) {
241        let area = plot.area();
242        if area.is_empty() {
243            return;
244        }
245        let line = self.title_height();
246        let top = area.y0 - line;
247        let outside = canvas.state();
248        canvas.clip(Rect::new(area.x0, top, area.width(), line));
249        let ascent = self.glyphs.line_metrics().ascent.round();
250        let at = Point::new(area.x0, top + ascent);
251        draw_text(&mut self.glyphs, canvas, title, at, self.palette.label);
252        canvas.restore(outside);
253    }
254
255    /// The `y` ticks `plot` has room for, `finer` times as many.
256    fn y_ticks(&self, plot: &Plot, finer: usize) -> Ticks {
257        let line = self.glyphs.line_metrics().line_height();
258        // Lossless enough: a whole number of ticks, saturating for absurd heights.
259        let most = (plot.area().height() / (line * Y_TICK_LINES)).floor() as usize;
260        plot.y().ticks(most.saturating_mul(finer))
261    }
262
263    /// The `x` ticks `plot` has room for, `finer` times as many: labels of times below a
264    /// second are longer, and get more room.
265    fn x_ticks(&mut self, plot: &Plot, finer: usize) -> Ticks {
266        let (width, char_width) = (plot.area().width(), self.char_width());
267        // Lossless enough: a whole number of ticks, saturating for absurd widths.
268        let most =
269            |room: f32| ((width / (char_width * room)).floor() as usize).saturating_mul(finer);
270        let ticks = plot.x().ticks(most(X_LABEL_ROOM));
271        if plot.x().kind() == AxisKind::Time && ticks.step() < 1.0 {
272            return plot.x().ticks(most(X_FINE_LABEL_ROOM));
273        }
274        ticks
275    }
276
277    /// The advance of a digit.
278    fn char_width(&mut self) -> f32 {
279        let px = self.glyphs.px();
280        self.glyphs.get('0').map_or(px * 0.6, |glyph| glyph.advance)
281    }
282
283    /// Space between the plot and its labels.
284    fn pad(&self) -> f32 {
285        (self.glyphs.px() * 0.5).round()
286    }
287}
288
289/// Draws `text` with its pen starting at `at` (`y` is the baseline).
290fn draw_text(glyphs: &mut GlyphMap, canvas: &mut Canvas<'_>, text: &str, at: Point, color: Color) {
291    let mut pen = at.x;
292    for ch in text.chars() {
293        let Some(mapped) = glyphs.get(ch) else {
294            continue;
295        };
296        let at = Point::new(pen, at.y);
297        canvas.draw_glyph(glyphs.font(), mapped.id, glyphs.px(), at, color);
298        pen += mapped.advance;
299    }
300}