1use densa_chart::{Axis, AxisKind, ChartModel, ChartPainter, ChartPalette, Plot};
5use densa_core::{Color, Point, Rect, Size};
6use densa_draw::Canvas;
7use densa_text::Font;
8use densa_ui::{
9 AccessNode, ActionKind, Event, EventCx, Handled, Key, KeyEvent, LayoutCx, Length, NamedKey,
10 Overflow, PaintCx, PointerButton, PointerEvent, PointerKind, Role, ScrollDelta, Style, Theme,
11 Widget,
12};
13
14use crate::paint::{luminance, outline};
15
16const ZOOM_STEP: f64 = 1.15;
18const NOTCH: f32 = 48.0;
20const KEY_PAN: f64 = 0.1;
22const LEAD: f64 = 0.03;
25const MIN_STEPS: f64 = 5.0;
28const MIN_SHARE: f64 = 1e-4;
30const CLICK_SLOP: f32 = 4.0;
32const PRECISION: f64 = 1e-12;
35const LABEL_CHARS: u16 = 8;
37const MAX_SHARE: f64 = 2.0;
39
40#[derive(Debug, Clone, Copy, PartialEq)]
42enum View {
43 Follow { span: f64 },
45 Fixed(Axis),
47}
48
49#[derive(Debug, Clone, Copy, PartialEq)]
51struct Pan {
52 x: f32,
53 from: Axis,
54 width: f32,
55}
56
57#[derive(Debug)]
74pub struct Chart<M> {
75 model: M,
76 view: View,
77 initial: f64,
79 label_chars: u16,
80 painter: Option<ChartPainter>,
82 built: Option<(f32, ChartPalette)>,
83 pan: Option<Pan>,
84 dragged: bool,
87 hover: Option<Point>,
89}
90
91impl<M: ChartModel> Chart<M> {
92 pub fn new(model: M, span: f64) -> Self {
95 let span = if span.is_finite() && span > 0.0 {
96 span
97 } else {
98 1.0
99 };
100 Self {
101 model,
102 view: View::Follow { span },
103 initial: span,
104 label_chars: LABEL_CHARS,
105 painter: None,
106 built: None,
107 pan: None,
108 dragged: false,
109 hover: None,
110 }
111 }
112
113 #[must_use]
115 pub fn with_label_chars(mut self, chars: u16) -> Self {
116 self.label_chars = chars;
117 self.built = None;
118 self
119 }
120
121 pub fn style() -> Style {
123 Style::column()
124 .grow(1.0)
125 .min_size(Length::Px(0.0), Length::Px(0.0))
126 .overflow(Overflow::Clip)
127 }
128
129 pub fn model(&self) -> &M {
131 &self.model
132 }
133
134 pub fn model_mut(&mut self, cx: &mut EventCx<'_>) -> &mut M {
137 cx.request_paint();
138 &mut self.model
139 }
140
141 pub fn x_axis(&self) -> Option<Axis> {
143 let (_, newest) = self.extent()?;
144 let kind = self.model.x_kind();
145 Some(match self.current() {
146 View::Follow { span } => {
147 let end = newest + self.lead(span);
148 Axis::new(end - span, end, kind)
149 }
150 View::Fixed(axis) => Axis::new(axis.start(), axis.end(), kind),
151 })
152 }
153
154 pub fn is_following(&self) -> bool {
156 matches!(self.current(), View::Follow { .. })
157 }
158
159 fn current(&self) -> View {
161 match self.view {
162 View::Fixed(axis) => self.settle(axis.start(), axis.span()),
163 follow @ View::Follow { .. } => follow,
164 }
165 }
166
167 pub fn follow(&mut self, cx: &mut EventCx<'_>) {
169 if let Some(x) = self.x_axis() {
170 self.view = View::Follow { span: x.span() };
171 cx.request_paint();
172 }
173 }
174
175 pub fn show(&mut self, cx: &mut EventCx<'_>, x: Axis) {
178 let (low, high) = self.limits();
179 let span = x.span().clamp(low, high);
180 let middle = x.start() + x.span() / 2.0;
181 self.view = self.settle(middle - span / 2.0, span);
182 cx.request_paint();
183 }
184
185 fn extent(&self) -> Option<(f64, f64)> {
187 self.model
188 .x_extent()
189 .filter(|(low, high)| low.is_finite() && high.is_finite() && low <= high)
190 }
191
192 fn limits(&self) -> (f64, f64) {
194 let Some((oldest, newest)) = self.extent() else {
195 return (f64::MIN_POSITIVE, f64::MAX);
196 };
197 let width = newest - oldest;
198 let low = match self.model.x_step() {
199 Some(step) if step.is_finite() && step > 0.0 => {
201 (step * MIN_STEPS).min(self.whole(width, step))
202 }
203 _ => width * MIN_SHARE,
204 };
205 let magnitude = oldest.abs().max(newest.abs()).max(1.0);
206 let low = low.max(magnitude * PRECISION);
207 let high = (width * MAX_SHARE).max(self.initial).max(low * 2.0);
208 (low, high)
209 }
210
211 fn whole(&self, width: f64, step: f64) -> f64 {
216 match self.model.x_kind() {
217 AxisKind::Category => width.max(step),
218 _ => width + step,
219 }
220 }
221
222 fn lead(&self, span: f64) -> f64 {
226 let share = span * LEAD;
227 match (self.model.x_kind(), self.model.x_step()) {
228 (AxisKind::Category, _) => share,
229 (_, Some(step)) if step.is_finite() && step > 0.0 => share.max(step / 2.0),
230 _ => share,
231 }
232 }
233
234 fn settle(&self, start: f64, span: f64) -> View {
237 let kind = self.model.x_kind();
238 let Some((oldest, newest)) = self.extent() else {
239 return View::Fixed(Axis::new(start, start + span, kind));
240 };
241 let start = start.max(oldest - span);
242 if start + span >= newest + self.lead(span) {
243 return View::Follow { span };
244 }
245 View::Fixed(Axis::new(start, start + span, kind))
246 }
247
248 fn zoom(&mut self, factor: f64, x: Option<f32>, rect: Rect) -> bool {
251 let Some(shown) = self.x_axis() else {
252 return false;
253 };
254 let (low, high) = self.limits();
255 let span = (shown.span() * factor).clamp(low, high);
256 if span == shown.span() || !span.is_finite() || (span < shown.span()) != (factor < 1.0) {
259 return false;
260 }
261 let area = self.x_area(rect);
262 let anchor = match (self.current(), x, area) {
263 (View::Fixed(_), Some(x), Some(area)) if (area.x0..area.x1).contains(&x) => {
264 Plot::new(area, shown, shown).x_value(x)
265 }
266 (View::Fixed(_), _, _) => shown.start() + shown.span() / 2.0,
267 (View::Follow { .. }, _, _) => {
268 self.view = View::Follow { span };
269 return true;
270 }
271 };
272 let zoomed = shown.zoomed(span / shown.span(), anchor);
273 self.view = self.settle(zoomed.start(), span);
274 true
275 }
276
277 fn pan_by(&mut self, dx: f32, rect: Rect) -> bool {
279 let (Some(shown), Some(area)) = (self.x_axis(), self.x_area(rect)) else {
280 return false;
281 };
282 let delta = f64::from(dx) / f64::from(area.width()) * shown.span();
283 self.shift(shown, delta)
284 }
285
286 fn shift(&mut self, shown: Axis, delta: f64) -> bool {
288 if delta == 0.0 || !delta.is_finite() {
289 return false;
290 }
291 let before = self.view;
292 self.view = self.settle(shown.start() + delta, shown.span());
293 self.view != before
294 }
295
296 fn x_area(&mut self, rect: Rect) -> Option<Rect> {
298 let area = self.painter.as_mut()?.plot_area(rect, true);
299 (area.width() > 0.0).then_some(area)
300 }
301
302 fn build(&mut self, px: f32, palette: ChartPalette) {
305 let chars = self.label_chars;
306 self.painter = Font::bundled_mono()
307 .ok()
308 .map(|font| ChartPainter::new(font, px, palette).with_label_chars(chars));
309 self.built = Some((px, palette));
310 }
311
312 fn pointer(&mut self, cx: &mut EventCx<'_>, pointer: &PointerEvent) -> Handled {
313 let at = pointer.position;
314 let rect = cx.rect();
315 let moved = match pointer.kind {
316 PointerKind::Scroll(delta) => {
317 self.wheel(cx, delta, at, pointer.modifiers.shift(), rect)
318 }
319 PointerKind::Down(PointerButton::Primary) if pointer.clicks >= 2 && !self.dragged => {
320 self.pan = None;
321 let following = self.is_following();
322 self.follow(cx);
323 !following
324 }
325 PointerKind::Down(PointerButton::Primary) => {
326 self.dragged = false;
327 if let (Some(from), Some(area)) = (self.x_axis(), self.x_area(rect)) {
328 let width = area.width();
329 self.pan = Some(Pan {
330 x: at.x,
331 from,
332 width,
333 });
334 }
335 cx.request_paint();
336 return Handled::Yes;
337 }
338 PointerKind::Move => match self.pan {
339 Some(pan) => {
340 self.hover = Some(at);
341 self.dragged |= (at.x - pan.x).abs() > cx.px(CLICK_SLOP);
342 let delta = f64::from(pan.x - at.x) / f64::from(pan.width) * pan.from.span();
343 let before = self.view;
344 self.view = self.settle(pan.from.start() + delta, pan.from.span());
345 self.view != before
346 }
347 None => {
348 self.hover = Some(at);
349 cx.request_paint();
350 return Handled::No;
351 }
352 },
353 PointerKind::Leave => {
354 self.hover = None;
355 cx.request_paint();
356 return Handled::No;
357 }
358 PointerKind::Up(_) | PointerKind::Cancel => {
359 let held = self.pan.take().is_some();
360 cx.request_paint();
361 return if held { Handled::Yes } else { Handled::No };
362 }
363 _ => return Handled::No,
364 };
365 self.moved(cx, moved)
366 }
367
368 fn wheel(
370 &mut self,
371 cx: &mut EventCx<'_>,
372 delta: ScrollDelta,
373 at: Point,
374 shift: bool,
375 rect: Rect,
376 ) -> bool {
377 let notch = cx.px(NOTCH);
378 let (zoom, dx) = match delta {
379 ScrollDelta::Lines(dx, dy) if shift && dx == 0.0 => (0.0, dy * notch),
380 ScrollDelta::Lines(dx, dy) => (dy, dx * notch),
381 ScrollDelta::Pixels(dx, dy) if shift && dx == 0.0 => (0.0, dy),
382 ScrollDelta::Pixels(dx, dy) => (dy / notch, dx),
383 _ => (0.0, 0.0),
384 };
385 let mut moved = false;
386 if zoom != 0.0 && zoom.is_finite() {
387 moved |= self.zoom(ZOOM_STEP.powf(f64::from(zoom)), Some(at.x), rect);
388 }
389 if dx != 0.0 && dx.is_finite() {
390 moved |= self.pan_by(dx, rect);
391 }
392 moved
393 }
394
395 fn key(&mut self, cx: &mut EventCx<'_>, key: &KeyEvent<'_>) -> Handled {
396 let m = key.modifiers;
397 if m.shift() || m.control() || m.alt() || m.logo() {
398 return Handled::No;
399 }
400 let rect = cx.rect();
401 let Some(shown) = self.x_axis() else {
402 return Handled::No;
403 };
404 let moved = match key.key {
405 Key::Named(NamedKey::ArrowLeft) => self.shift(shown, -shown.span() * KEY_PAN),
406 Key::Named(NamedKey::ArrowRight) => self.shift(shown, shown.span() * KEY_PAN),
407 Key::Named(NamedKey::ArrowUp) => self.zoom(1.0 / ZOOM_STEP, None, rect),
408 Key::Named(NamedKey::ArrowDown) => self.zoom(ZOOM_STEP, None, rect),
409 Key::Named(NamedKey::End) => {
410 let following = self.is_following();
411 self.follow(cx);
412 !following
413 }
414 Key::Named(NamedKey::Home) => {
415 let oldest = self.extent().map_or(shown.start(), |(oldest, _)| oldest);
416 let before = self.view;
417 self.view = self.settle(oldest, shown.span());
418 self.view != before
419 }
420 _ => return Handled::No,
421 };
422 self.moved(cx, moved);
423 Handled::Yes
424 }
425
426 fn moved(&mut self, cx: &mut EventCx<'_>, moved: bool) -> Handled {
428 if moved {
429 cx.request_paint();
430 cx.emit(ActionKind::Changed);
431 Handled::Yes
432 } else {
433 Handled::No
434 }
435 }
436
437 fn paint_chart(&mut self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>) {
439 let rect = cx.rect();
440 self.view = self.current();
442 let x = self.x_axis();
443 let hover = self.hover.filter(|_| self.pan.is_none());
444 let inside = canvas.state();
445 canvas.clip(rect);
446 if let Some((_, palette)) = self.built {
447 canvas.fill_rect(rect, palette.background);
448 }
449 let Some(painter) = self.painter.as_mut() else {
450 canvas.restore(inside);
451 return;
452 };
453 if let Some(x) = x {
454 let panes = self.model.panes().max(1);
455 let weight = |pane| {
456 let weight = f64::from(self.model.pane_weight(pane));
457 if weight.is_finite() {
458 weight.max(0.0)
459 } else {
460 0.0
461 }
462 };
463 let total: f64 = (0..panes).map(weight).sum();
464 let (mut top, mut before) = (f64::from(rect.top()), 0.0);
465 for pane in 0..panes {
466 before += if total > 0.0 {
467 weight(pane) / total
468 } else {
469 1.0 / panes as f64
471 };
472 let last = pane + 1 == panes;
473 let bottom = f64::from(rect.top()) + f64::from(rect.height()) * before;
475 let bounds = Rect::from_edges(rect.left(), top as f32, rect.right(), bottom as f32);
476 top = bottom;
477 let title = self.model.pane_title(pane);
478 let bounds = if title.is_empty() {
479 bounds
480 } else {
481 let below = (bounds.y0 + painter.title_height()).min(bounds.y1);
483 Rect::from_edges(bounds.x0, below, bounds.x1, bounds.y1)
484 };
485 let area = painter.plot_area(bounds, last);
486 let y = self
487 .model
488 .y_axis(pane, &x)
489 .unwrap_or_else(|| Axis::number(0.0, 1.0));
490 let plot = Plot::new(area, x, y);
491 let model = &self.model;
492 let names = |index| model.category(index);
493 painter.paint_axes_named(canvas, &plot, last, &names);
494 model.paint(pane, &plot, canvas, painter.palette());
495 painter.paint_title(canvas, &plot, title);
496 if let Some(at) = hover {
497 painter.paint_crosshair_named(canvas, &plot, at, last, &names);
498 }
499 }
500 }
501 if cx.is_focused() {
502 let line = cx.px(1.0).round().max(1.0);
503 outline(canvas, rect, line, cx.theme().focus);
504 }
505 canvas.restore(inside);
506 }
507}
508
509fn palette(theme: &Theme) -> ChartPalette {
511 let mut palette = ChartPalette::new();
512 palette.background = theme.surface;
513 palette.grid = theme.text.with_alpha(0.06);
514 palette.axis = theme.border;
515 palette.label = theme.text_dim;
516 palette.line = theme.accent;
517 palette.crosshair = theme.text_dim.with_alpha(0.7);
518 palette.marker = theme.raised;
519 palette.marker_text = theme.text;
520 if luminance(theme.surface) > 0.5 {
522 palette.up = Color::srgb(0x1A7F37);
523 palette.down = Color::srgb(0xCF222E);
524 }
525 palette
526}
527
528impl<M: ChartModel + 'static> Widget for Chart<M> {
529 fn layout(&mut self, cx: &mut LayoutCx<'_>, _size: Size) {
530 let px = cx.px(cx.theme().font_size * 0.9).round();
531 let palette = palette(cx.theme());
532 if self.built != Some((px, palette)) {
533 self.build(px, palette);
534 }
535 }
536
537 fn paint(&mut self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>) {
538 self.model.prepare();
539 self.paint_chart(cx, canvas);
540 }
541
542 fn event(&mut self, cx: &mut EventCx<'_>, event: &Event<'_>) -> Handled {
543 match event {
544 Event::Pointer(pointer) => self.pointer(cx, pointer),
545 Event::Key(key) if key.pressed => self.key(cx, key),
546 Event::FocusGained | Event::FocusLost => {
547 cx.request_paint();
548 Handled::No
549 }
550 _ => Handled::No,
551 }
552 }
553
554 fn accepts_focus(&self) -> bool {
555 true
556 }
557
558 fn accessibility(&self, _cx: &PaintCx<'_>, node: &mut AccessNode) {
559 node.set_role(Role::Image);
560 let description = self.model.describe();
561 node.set_label(if description.is_empty() {
562 "Chart".to_owned()
563 } else {
564 description
565 });
566 }
567}