Skip to main content

gpui_base/plot/
mod.rs

1//! Unstyled plotting: scales, shapes, axes, grids, labels, and the element and
2//! hover tracking behind every [`Plot`].
3//!
4//! Colors are always handed in by the caller. A styled layer supplies chart
5//! defaults, the tooltip overlay, and hover and appear timing through
6//! [`PlotMotion`].
7mod appear;
8mod axis;
9mod element;
10mod grid;
11mod hover;
12pub mod label;
13mod path_cache;
14pub mod scale;
15pub mod shape;
16
17use std::{fmt::Debug, ops::Add, time::Duration};
18
19use gpui::{
20    AnyElement, App, Bounds, ElementId, IntoElement, Path, PathBuilder, Pixels, Point, Window,
21    point, px,
22};
23
24use crate::{Spring, motion::Transition};
25
26pub use appear::{PlotAppear, PlotAppearScope};
27#[allow(deprecated)]
28pub use axis::AXIS_GAP;
29pub use axis::{AxisLabelPlacement, AxisLabelSide, AxisText, PlotAxis, axis_gutter};
30pub use element::PlotElement;
31pub use grid::Grid;
32pub use hover::{PlotHover, TooltipState, hover_progress, is_hover_entering, pointer_spring};
33pub use label::PlotLabel;
34pub use path_cache::{PathCache, PathCaches, ShapeKey};
35pub use scale::PlotValue;
36
37/// The timing of a plot's motion: how its data marks appear when it is first
38/// painted, how its hover progress fades in and out, and the spring a pointer
39/// follows the hovered datum with.
40///
41/// Base installs no motion of its own: every duration defaults to zero, so the
42/// hover appears, fades and glides at once. Product timing belongs to the
43/// styled layer, which projects it through [`crate::PlotTheme`].
44#[derive(Clone)]
45pub struct PlotMotion {
46    pointer: Spring,
47    enter: Transition,
48    exit: Transition,
49    appear: Transition,
50}
51
52impl Default for PlotMotion {
53    fn default() -> Self {
54        Self {
55            pointer: Spring::new(Duration::ZERO),
56            enter: Transition::new(Duration::ZERO),
57            exit: Transition::new(Duration::ZERO),
58            appear: Transition::new(Duration::ZERO),
59        }
60    }
61}
62
63impl PlotMotion {
64    /// The spring a crosshair, highlight band or hover dot follows the hovered
65    /// datum with.
66    pub fn with_pointer(mut self, pointer: Spring) -> Self {
67        self.pointer = pointer;
68        self
69    }
70
71    /// How the hover fades in when the cursor lands on a datum.
72    pub fn with_enter(mut self, enter: Transition) -> Self {
73        self.enter = enter;
74        self
75    }
76
77    /// How the hover fades out after the cursor leaves.
78    pub fn with_exit(mut self, exit: Transition) -> Self {
79        self.exit = exit;
80        self
81    }
82
83    /// How a plot's data marks appear the first time it is painted; see
84    /// [`PlotAppear`].
85    pub fn with_appear(mut self, appear: Transition) -> Self {
86        self.appear = appear;
87        self
88    }
89
90    pub fn pointer(&self) -> Spring {
91        self.pointer
92    }
93
94    pub fn enter(&self) -> &Transition {
95        &self.enter
96    }
97
98    pub fn exit(&self) -> &Transition {
99        &self.exit
100    }
101
102    pub fn appear(&self) -> &Transition {
103        &self.appear
104    }
105}
106
107pub trait Plot: IntoElement {
108    /// Lay out and place the child elements this plot hosts (e.g. element labels).
109    ///
110    /// Called during the element's prepaint phase, so implementations may use
111    /// [`AnyElement::layout_as_root`] / [`AnyElement::prepaint_at`] to measure and
112    /// position children — neither is legal from [`Plot::paint`]. The returned
113    /// elements are painted right after `paint`, below the tooltip overlay.
114    ///
115    /// Runs before [`Plot::tooltip_state`] and [`Plot::tooltip`], so anything
116    /// resolved here can be reused by them.
117    ///
118    /// The default returns no children.
119    fn prepaint(
120        &mut self,
121        _bounds: Bounds<Pixels>,
122        _window: &mut Window,
123        _cx: &mut App,
124    ) -> Vec<AnyElement> {
125        vec![]
126    }
127
128    fn paint(&mut self, bounds: Bounds<Pixels>, window: &mut Window, cx: &mut App);
129
130    /// A stable element id that keeps this plot's state across frames.
131    ///
132    /// Return `Some(id)` to opt in to appear motion and, unless
133    /// [`Plot::interactive`] says otherwise, tooltips and hover motion; the id
134    /// must be unique among sibling elements. Returning `None` (the default for
135    /// a hand-written plot) leaves the plot a pure element that neither appears
136    /// nor tracks hover.
137    ///
138    /// The charts in GPUI Component always return `Some`: their id defaults to the
139    /// source location they were constructed at, and `id` renames it.
140    fn id(&self) -> Option<ElementId> {
141        None
142    }
143
144    /// Whether a plot with an [`Plot::id`] tracks hover and shows its tooltip.
145    ///
146    /// `false` keeps the id's state — appear motion and path caches — without
147    /// the hitbox, hover tracking or overlay. The default is `true`, so a plot
148    /// opts in to tooltips by returning an id.
149    fn interactive(&self) -> bool {
150        true
151    }
152
153    /// Receive how far the plot's data marks have appeared this frame, before
154    /// [`Plot::hover`] and [`Plot::paint`] run.
155    ///
156    /// Called on every frame the plot has an [`Plot::id`]; see [`PlotAppear`].
157    /// Without an [`Plot::appear_generation`] the appear is always complete.
158    /// The default ignores it.
159    fn appear(&mut self, _appear: PlotAppear, _window: &mut Window, _cx: &mut App) {}
160
161    /// Opt in to appear motion: `Some` draws the plot in the first time its
162    /// id is painted, and again whenever the value changes, such as when a
163    /// chart switches to another symbol or period.
164    ///
165    /// The default, `None`, tracks no appear, keeps no state for it and asks
166    /// for no frames, so a plot that does not draw in costs nothing.
167    fn appear_generation(&self) -> Option<u64> {
168        None
169    }
170
171    /// Map the cursor to the tooltip state to display.
172    ///
173    /// `position` is the cursor position relative to the plot's top-left origin (already
174    /// origin-subtracted), and `bounds` is the painted area. Return the [`TooltipState`]
175    /// to display (highlighted index, crosshair point, dots), or `None` to show
176    /// nothing. Only called while the cursor is inside `bounds`.
177    ///
178    /// The default returns `None`.
179    fn tooltip_state(
180        &self,
181        _position: Point<Pixels>,
182        _bounds: Bounds<Pixels>,
183        _cx: &App,
184    ) -> Option<TooltipState> {
185        None
186    }
187
188    /// Receive the hovered datum this frame, before [`Plot::tooltip`] and
189    /// [`Plot::paint`] run.
190    ///
191    /// `hover` carries the [`TooltipState`] the cursor resolved to, and it
192    /// lingers after the cursor leaves while [`PlotHover::progress`] eases back to
193    /// zero, so a hover-driven presentation can fade out over the last datum
194    /// instead of vanishing. `None` means nothing is hovered and nothing is
195    /// fading.
196    ///
197    /// Called on every frame the plot has an [`Plot::id`], so this is where a
198    /// plot samples its hover motion ([`crate::motion::transition`],
199    /// [`PlotHover::glide`]) and keeps the result for the other two methods.
200    /// The default ignores the hover.
201    fn hover(&mut self, _hover: Option<&PlotHover>, _window: &mut Window, _cx: &mut App) {}
202
203    /// Render the tooltip overlay for the active [`TooltipState`].
204    ///
205    /// `cursor` is the live cursor position (relative to the plot origin) and `bounds` is the
206    /// plot's painted area, so the tooltip box can follow the cursor (pass `cursor` and
207    /// `bounds.size` to the styled layer's tooltip). Return the overlay element; it is
208    /// painted absolutely positioned above the plot graphics but below sibling content
209    /// drawn after the plot (a box that may overflow the plot should `deferred` itself).
210    /// The default returns `None`.
211    ///
212    /// Also called while the hover fades out, with the lingering `state` and the
213    /// last `cursor`; the overlay renders within the plot's element scope, so it can
214    /// fade with [`hover_progress`].
215    fn tooltip(
216        &self,
217        _state: &TooltipState,
218        _cursor: Point<Pixels>,
219        _bounds: Bounds<Pixels>,
220        _window: &mut Window,
221        _cx: &mut App,
222    ) -> Option<AnyElement> {
223        None
224    }
225}
226
227/// How a [`Line`](shape::Line) or [`Area`](shape::Area) connects its points,
228/// like d3's curve factories.
229#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
230pub enum Curve {
231    /// A smooth curve through every point (`d3.curveNatural`).
232    #[default]
233    Natural,
234    /// Straight segments between points (`d3.curveLinear`).
235    Linear,
236    /// A step that holds each value until the next point (`d3.curveStepAfter`).
237    StepAfter,
238}
239
240pub fn origin_point<T>(x: T, y: T, origin: Point<T>) -> Point<T>
241where
242    T: Default + Clone + Debug + PartialEq + Add<Output = T>,
243{
244    point(x, y) + origin
245}
246
247pub fn polygon<T>(points: &[Point<T>], bounds: &Bounds<Pixels>) -> Option<Path<Pixels>>
248where
249    T: Default + Clone + Copy + Debug + Into<f32> + PartialEq,
250{
251    let mut path = PathBuilder::stroke(px(1.));
252    let points = &points
253        .iter()
254        .map(|p| {
255            point(
256                px(p.x.into() + bounds.origin.x.as_f32()),
257                px(p.y.into() + bounds.origin.y.as_f32()),
258            )
259        })
260        .collect::<Vec<_>>();
261    path.add_polygon(points, false);
262    path.build().ok()
263}