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
8const LABEL_BYTES: usize = 32;
10
11const LABEL_CHARS: &str = "0123456789-.:";
13
14const X_LABEL_ROOM: f32 = 11.0;
16
17const X_FINE_LABEL_ROOM: f32 = 15.0;
19
20const Y_TICK_LINES: f32 = 2.5;
22
23const FINER: usize = 10;
25
26#[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 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 #[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 pub fn palette(&self) -> &ChartPalette {
61 &self.palette
62 }
63
64 pub fn set_palette(&mut self, palette: ChartPalette) {
66 self.palette = palette;
67 }
68
69 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 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 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 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 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 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 pub fn title_height(&self) -> f32 {
234 self.glyphs.line_metrics().line_height().round()
235 }
236
237 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 fn y_ticks(&self, plot: &Plot, finer: usize) -> Ticks {
257 let line = self.glyphs.line_metrics().line_height();
258 let most = (plot.area().height() / (line * Y_TICK_LINES)).floor() as usize;
260 plot.y().ticks(most.saturating_mul(finer))
261 }
262
263 fn x_ticks(&mut self, plot: &Plot, finer: usize) -> Ticks {
266 let (width, char_width) = (plot.area().width(), self.char_width());
267 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 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 fn pad(&self) -> f32 {
285 (self.glyphs.px() * 0.5).round()
286 }
287}
288
289fn 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}