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 ELLIPSIS: char = '\u{2026}';
22
23const Y_TICK_LINES: f32 = 2.5;
25
26const FINER: usize = 10;
28
29#[derive(Debug)]
37pub struct ChartPainter {
38 glyphs: GlyphMap,
39 palette: ChartPalette,
40 label: FmtBuf<LABEL_BYTES>,
41 label_chars: f32,
42}
43
44impl ChartPainter {
45 pub fn new(font: Font, px: f32, palette: ChartPalette) -> Self {
48 Self {
49 glyphs: GlyphMap::new(font, px),
50 palette,
51 label: FmtBuf::new(),
52 label_chars: 8.0,
53 }
54 }
55
56 #[must_use]
58 pub fn with_label_chars(mut self, chars: u16) -> Self {
59 self.label_chars = f32::from(chars.max(1));
60 self
61 }
62
63 pub fn palette(&self) -> &ChartPalette {
65 &self.palette
66 }
67
68 pub fn set_palette(&mut self, palette: ChartPalette) {
70 self.palette = palette;
71 }
72
73 pub fn prewarm(&mut self, cache: &mut GlyphCache) -> usize {
77 let mut missing = 0;
78 for ch in LABEL_CHARS.chars() {
79 let Some(mapped) = self.glyphs.get(ch) else {
80 continue;
81 };
82 for bin in 0..SUBPIXEL_STEPS {
83 let cached = cache.get(self.glyphs.font(), mapped.id, self.glyphs.px(), bin);
84 missing += usize::from(cached.is_err());
85 }
86 }
87 missing
88 }
89
90 pub fn plot_area(&mut self, bounds: Rect, x_labels: bool) -> Rect {
94 let right = (self.char_width() * self.label_chars + 2.0 * self.pad()).round();
95 let below = if x_labels {
96 (self.glyphs.line_metrics().line_height() + self.pad()).round()
97 } else {
98 0.0
99 };
100 Rect::from_edges(
101 bounds.x0.round(),
102 bounds.y0.round(),
103 (bounds.x1 - right).round(),
104 (bounds.y1 - below).round(),
105 )
106 }
107
108 pub fn paint_axes(&mut self, canvas: &mut Canvas<'_>, plot: &Plot, x_labels: bool) {
113 self.paint_axes_named(canvas, plot, x_labels, &|_| None);
114 }
115
116 pub fn paint_axes_named<'n>(
121 &mut self,
122 canvas: &mut Canvas<'_>,
123 plot: &Plot,
124 x_labels: bool,
125 names: &dyn Fn(usize) -> Option<&'n str>,
126 ) {
127 let area = plot.area();
128 if area.is_empty() {
129 return;
130 }
131 let metrics = self.glyphs.line_metrics();
132 let pad = self.pad();
133 let palette = self.palette;
134
135 let ticks = self.y_ticks(plot, 1);
136 for value in ticks.clone() {
137 let y = plot.y_at(value).round() as f32;
139 if !(area.y0..area.y1).contains(&y) {
140 continue;
141 }
142 canvas.fill_rect(Rect::new(area.x0, y, area.width(), 1.0), palette.grid);
143 self.label.clear();
144 if ticks.write_label(value, &mut self.label).is_ok() {
145 let baseline = (y + (metrics.ascent + metrics.descent) / 2.0).round();
146 let at = Point::new(area.x1 + pad, baseline);
147 draw_text(
148 &mut self.glyphs,
149 canvas,
150 self.label.as_str(),
151 at,
152 palette.label,
153 );
154 }
155 }
156
157 let ticks = self.x_ticks(plot, 1);
158 let category = plot.x().kind() == AxisKind::Category;
159 let room = (ticks.step() / plot.x().span()) as f32 * area.width() - self.char_width();
161 for value in ticks.clone() {
162 let x = plot.x_at(value).round() as f32;
164 if !(area.x0..area.x1).contains(&x) {
165 continue;
166 }
167 canvas.fill_rect(Rect::new(x, area.y0, 1.0, area.height()), palette.grid);
168 if category {
169 if let Some(name) = place(value).and_then(names).filter(|_| x_labels) {
170 let before = (room / 2.0).min(x - area.x0);
174 let after = (room / 2.0).min(area.x1 - x);
175 let fitted = fit(&mut self.glyphs, name, before + after);
176 let left = (x - fitted.width / 2.0)
177 .clamp(x - before, (x + after - fitted.width).max(x - before));
178 let at = Point::new(left.round(), (area.y1 + pad + metrics.ascent).round());
179 draw_fit(&mut self.glyphs, canvas, name, fitted, at, palette.label);
180 }
181 continue;
182 }
183 self.label.clear();
184 if x_labels && ticks.write_label(value, &mut self.label).is_ok() {
185 let width = self.glyphs.measure(self.label.as_str());
186 let left = (x - width / 2.0).clamp(area.x0, (area.x1 - width).max(area.x0));
187 let at = Point::new(left.round(), (area.y1 + pad + metrics.ascent).round());
188 draw_text(
189 &mut self.glyphs,
190 canvas,
191 self.label.as_str(),
192 at,
193 palette.label,
194 );
195 }
196 }
197
198 let right = Rect::new(area.x1, area.y0, 1.0, area.height() + 1.0);
199 canvas.fill_rect(right, palette.axis);
200 canvas.fill_rect(Rect::new(area.x0, area.y1, area.width(), 1.0), palette.axis);
201 }
202
203 pub fn paint_crosshair(
210 &mut self,
211 canvas: &mut Canvas<'_>,
212 plot: &Plot,
213 at: Point,
214 x_label: bool,
215 ) {
216 self.paint_crosshair_named(canvas, plot, at, x_label, &|_| None);
217 }
218
219 pub fn paint_crosshair_named<'n>(
223 &mut self,
224 canvas: &mut Canvas<'_>,
225 plot: &Plot,
226 at: Point,
227 x_label: bool,
228 names: &dyn Fn(usize) -> Option<&'n str>,
229 ) {
230 let area = plot.area();
231 let x = at.x.round();
232 if area.is_empty() || !(area.x0..area.x1).contains(&x) {
233 return;
234 }
235 let metrics = self.glyphs.line_metrics();
236 let (pad, line) = (self.pad(), metrics.line_height().round());
237 let palette = self.palette;
238 canvas.fill_rect(Rect::new(x, area.y0, 1.0, area.height()), palette.crosshair);
239
240 let y = at.y.round();
241 if (area.y0..area.y1).contains(&y) {
242 canvas.fill_rect(Rect::new(area.x0, y, area.width(), 1.0), palette.crosshair);
243 let fine = self.y_ticks(plot, FINER);
244 self.label.clear();
245 if fine.write_label(plot.y_value(y), &mut self.label).is_ok() {
246 let width = self.glyphs.measure(self.label.as_str());
247 let top = (y - line / 2.0).round();
248 let marker = Rect::new(area.x1 + 1.0, top, width + 2.0 * pad, line);
249 canvas.fill_rect(marker, palette.marker);
250 let baseline = (y + (metrics.ascent + metrics.descent) / 2.0).round();
251 let at = Point::new(area.x1 + 1.0 + pad, baseline);
252 draw_text(
253 &mut self.glyphs,
254 canvas,
255 self.label.as_str(),
256 at,
257 palette.marker_text,
258 );
259 }
260 }
261
262 let category = plot.x().kind() == AxisKind::Category;
263 if let Some(name) = place(plot.x_value(x).round())
264 .and_then(names)
265 .filter(|_| x_label && category)
266 {
267 let fitted = fit(&mut self.glyphs, name, (area.width() - 2.0 * pad).max(0.0));
268 let width = fitted.width + 2.0 * pad;
269 let left = (x - width / 2.0)
270 .clamp(area.x0, (area.x1 - width).max(area.x0))
271 .round();
272 let marker = Rect::new(left, area.y1 + 1.0, width, pad + line);
273 canvas.fill_rect(marker, palette.marker);
274 let at = Point::new(left + pad, (area.y1 + pad + metrics.ascent).round());
275 draw_fit(
276 &mut self.glyphs,
277 canvas,
278 name,
279 fitted,
280 at,
281 palette.marker_text,
282 );
283 } else if x_label && !category {
284 let fine = self.x_ticks(plot, FINER);
285 self.label.clear();
286 if fine.write_label(plot.x_value(x), &mut self.label).is_ok() {
287 let width = self.glyphs.measure(self.label.as_str()) + 2.0 * pad;
288 let left = (x - width / 2.0)
289 .clamp(area.x0, (area.x1 - width).max(area.x0))
290 .round();
291 let marker = Rect::new(left, area.y1 + 1.0, width, pad + line);
292 canvas.fill_rect(marker, palette.marker);
293 let at = Point::new(left + pad, (area.y1 + pad + metrics.ascent).round());
294 draw_text(
295 &mut self.glyphs,
296 canvas,
297 self.label.as_str(),
298 at,
299 palette.marker_text,
300 );
301 }
302 }
303 }
304
305 pub fn title_height(&self) -> f32 {
308 self.glyphs.line_metrics().line_height().round()
309 }
310
311 pub fn paint_title(&mut self, canvas: &mut Canvas<'_>, plot: &Plot, title: &str) {
315 let area = plot.area();
316 if area.is_empty() {
317 return;
318 }
319 let line = self.title_height();
320 let top = area.y0 - line;
321 let outside = canvas.state();
322 canvas.clip(Rect::new(area.x0, top, area.width(), line));
323 let ascent = self.glyphs.line_metrics().ascent.round();
324 let at = Point::new(area.x0, top + ascent);
325 draw_text(&mut self.glyphs, canvas, title, at, self.palette.label);
326 canvas.restore(outside);
327 }
328
329 fn y_ticks(&self, plot: &Plot, finer: usize) -> Ticks {
331 let line = self.glyphs.line_metrics().line_height();
332 let most = (plot.area().height() / (line * Y_TICK_LINES)).floor() as usize;
334 plot.y().ticks(most.saturating_mul(finer))
335 }
336
337 fn x_ticks(&mut self, plot: &Plot, finer: usize) -> Ticks {
340 let (width, char_width) = (plot.area().width(), self.char_width());
341 let most =
343 |room: f32| ((width / (char_width * room)).floor() as usize).saturating_mul(finer);
344 let ticks = plot.x().ticks(most(X_LABEL_ROOM));
345 if plot.x().kind() == AxisKind::Time && ticks.step() < 1.0 {
346 return plot.x().ticks(most(X_FINE_LABEL_ROOM));
347 }
348 ticks
349 }
350
351 fn char_width(&mut self) -> f32 {
353 let px = self.glyphs.px();
354 self.glyphs.get('0').map_or(px * 0.6, |glyph| glyph.advance)
355 }
356
357 fn pad(&self) -> f32 {
359 (self.glyphs.px() * 0.5).round()
360 }
361}
362
363fn place(value: f64) -> Option<usize> {
365 (value >= 0.0 && value < usize::MAX as f64).then_some(value as usize)
367}
368
369#[derive(Clone, Copy)]
371struct Fitted {
372 end: usize,
374 width: f32,
376 cut: bool,
378}
379
380fn fit(glyphs: &mut GlyphMap, text: &str, room: f32) -> Fitted {
384 let ellipsis = glyphs.get(ELLIPSIS).map_or(0.0, |glyph| glyph.advance);
385 let none = Fitted {
386 end: 0,
387 width: 0.0,
388 cut: false,
389 };
390 let mut cut = (ellipsis <= room).then_some(Fitted {
392 cut: true,
393 width: ellipsis,
394 ..none
395 });
396 let mut width = 0.0;
397 for (at, ch) in text.char_indices() {
398 let advance = match glyphs.get(ch) {
399 Some(mapped) if !ch.is_control() => mapped.advance,
400 _ => 0.0,
401 };
402 if width + advance > room {
403 return cut.unwrap_or(none);
404 }
405 width += advance;
406 if width + ellipsis <= room {
407 let end = at + ch.len_utf8();
408 cut = Some(Fitted {
409 end,
410 width: width + ellipsis,
411 cut: true,
412 });
413 }
414 }
415 Fitted {
416 end: text.len(),
417 width,
418 cut: false,
419 }
420}
421
422fn draw_fit(
424 glyphs: &mut GlyphMap,
425 canvas: &mut Canvas<'_>,
426 text: &str,
427 fitted: Fitted,
428 at: Point,
429 color: Color,
430) {
431 let mut pen = at.x;
432 let kept = text[..fitted.end].chars().filter(|ch| !ch.is_control());
433 let ellipsis = fitted.cut.then_some(ELLIPSIS);
434 for ch in kept.chain(ellipsis) {
435 let Some(mapped) = glyphs.get(ch) else {
436 continue;
437 };
438 canvas.draw_glyph(
439 glyphs.font(),
440 mapped.id,
441 glyphs.px(),
442 Point::new(pen, at.y),
443 color,
444 );
445 pen += mapped.advance;
446 }
447}
448
449fn draw_text(glyphs: &mut GlyphMap, canvas: &mut Canvas<'_>, text: &str, at: Point, color: Color) {
451 let mut pen = at.x;
452 for ch in text.chars() {
453 let Some(mapped) = glyphs.get(ch) else {
454 continue;
455 };
456 let at = Point::new(pen, at.y);
457 canvas.draw_glyph(glyphs.font(), mapped.id, glyphs.px(), at, color);
458 pen += mapped.advance;
459 }
460}