Skip to main content

egui_charts/chart/
primitives.rs

1//! Custom drawing attached to a series: primitives, and the renderers of custom series.
2//!
3//! A [`Primitive`] is host code that the chart calls while it draws a frame. It is attached to a
4//! series with [`Chart::attach_primitive`](crate::widget::Chart::attach_primitive) and draws in
5//! the pane of that series, on its price scale. A primitive can draw behind the series, above
6//! them, or above the drawings as well ([`PrimitivePass`]), draw inside the price and time axes
7//! and put labels on them, widen the price range its scale fits, take part in hit testing with
8//! its own pointer cursor, and follow the bars of its series through lifecycle hooks.
9//!
10//! A custom series is a study that supplies the values (legend, scale, crosshair and last-value
11//! tag) together with a primitive that draws its points in place of the study's plots and hit
12//! tests them: [`Chart::add_custom_series`](crate::widget::Chart::add_custom_series).
13//!
14//! Every hook has a default that does nothing, so a primitive implements only what it needs.
15//! `examples/primitives.rs` shades the regular session behind the bars with labels on the time
16//! axis, and adds a custom series that draws the range of each bar in a pane of its own.
17//!
18//! ```rust
19//! use egui::{Color32, Painter};
20//! use egui_charts::chart::primitives::{Primitive, PrimitivePass, PrimitiveView};
21//! use egui_charts::widget::{Chart, SeriesId};
22//!
23//! /// Shades the bars whose close is above `level`.
24//! struct AboveLevel {
25//!     level: f64,
26//! }
27//!
28//! impl Primitive for AboveLevel {
29//!     fn draw(&self, painter: &Painter, view: &PrimitiveView<'_>, pass: PrimitivePass) {
30//!         if pass != PrimitivePass::Below {
31//!             return;
32//!         }
33//!         let rect = view.mapping.rect;
34//!         let half = view.mapping.bar_spacing / 2.0;
35//!         for index in view.visible.clone() {
36//!             if view.bars[index].close > self.level {
37//!                 let x = view.index_to_x(index as f64);
38//!                 painter.rect_filled(
39//!                     egui::Rect::from_x_y_ranges(x - half..=x + half, rect.y_range()),
40//!                     0.0,
41//!                     Color32::from_rgba_unmultiplied(41, 98, 255, 30),
42//!                 );
43//!             }
44//!         }
45//!     }
46//! }
47//!
48//! # fn example(chart: &mut Chart) {
49//! let id = chart.attach_primitive(SeriesId::MAIN, AboveLevel { level: 100.0 });
50//! # assert!(id.is_some());
51//! # }
52//! ```
53
54use crate::chart::coords::ChartMapping;
55use crate::chart::selection::SeriesId;
56use crate::model::Bar;
57use crate::studies::Indicator;
58use chrono::{DateTime, Utc};
59use egui::{Color32, CursorIcon, Painter, Pos2, Rect};
60use std::ops::Range;
61
62/// Identifies a primitive attached to a chart, for
63/// [`Chart::detach_primitive`](crate::widget::Chart::detach_primitive) and pointer targets.
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
65pub struct PrimitiveId(pub(crate) u64);
66
67/// When a primitive draws in its pane.
68#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
69pub enum PrimitivePass {
70    /// Behind the series and studies of the pane, above its background, grid and sessions.
71    Below,
72    /// Above the series and studies of the pane, below drawings.
73    Series,
74    /// Above the series, studies and drawings of the pane. In the main pane it stays below the
75    /// crosshair and legend.
76    Above,
77}
78
79/// The pane and scale a primitive draws on in the current frame.
80#[derive(Clone)]
81pub struct PrimitiveView<'a> {
82    /// The series the primitive is attached to.
83    pub series: SeriesId,
84    /// The study the primitive is attached to, whose values a custom series renderer draws.
85    /// `None` for a bar series.
86    pub study: Option<&'a dyn Indicator>,
87    /// Bar index to x on the time axis the panes share, and price to y on the series' price
88    /// scale. `mapping.rect` is the plot area of the pane.
89    pub mapping: ChartMapping,
90    /// The bars laid out on the time axis, oldest first: the main series, or the elements of a
91    /// price-based chart type. Bar `i` is centred at `index_to_x(i as f64)`.
92    pub bars: &'a [Bar],
93    /// The indices of `bars` in view.
94    pub visible: Range<usize>,
95}
96
97impl PrimitiveView<'_> {
98    /// Aligned source ranges for a custom renderer's merge hook. Groups start at multiples
99    /// of `factor`, include the visible range's edge groups and never exceed `bars`.
100    /// A factor of zero is treated as one. Merge values in source order so open/close and
101    /// extrema keep their meaning; host payload aggregation belongs to the renderer.
102    pub fn point_groups(&self, factor: usize) -> impl Iterator<Item = Range<usize>> {
103        let factor = factor.max(1);
104        let end = self.visible.end.min(self.bars.len());
105        let start = if self.visible.start >= end {
106            end
107        } else {
108            self.visible.start / factor * factor
109        };
110        (start..end)
111            .step_by(factor)
112            .map(move |start| start..start.saturating_add(factor).min(self.bars.len()))
113    }
114
115    /// The x of a fractional bar index, the centre of bar `index` when it is whole.
116    pub fn index_to_x(&self, index: f64) -> f32 {
117        self.mapping.index_to_x(index)
118    }
119
120    /// The x of a UTC time, interpolated between bars. Without a timeframe, whitespace extends
121    /// the most common positive spacing among the outermost 16 bars (shorter spacing breaks
122    /// ties, one second if none). `None` without bars.
123    pub fn time_to_x(&self, time: DateTime<Utc>) -> Option<f32> {
124        crate::model::index_at(self.bars, time).map(|index| self.index_to_x(index))
125    }
126}
127
128/// A label a primitive puts on the price axis of its pane, on a tag in `color`.
129#[derive(Clone, Debug, PartialEq)]
130pub struct PriceAxisLabel {
131    /// Price at the centre of the tag, on the scale of the primitive's series.
132    pub price: f64,
133    /// Text of the tag.
134    pub text: String,
135    /// Fill of the tag; the text is white or black, whichever reads better on it.
136    pub color: Color32,
137}
138
139/// A label a primitive puts on the time axis, on a tag in `color`.
140#[derive(Clone, Debug, PartialEq)]
141pub struct TimeAxisLabel {
142    /// Time at the centre of the tag.
143    pub time: DateTime<Utc>,
144    /// Text of the tag.
145    pub text: String,
146    /// Fill of the tag; the text is white or black, whichever reads better on it.
147    pub color: Color32,
148}
149
150/// Host drawing attached to a series, called by the chart while it draws each frame.
151///
152/// The chart calls the drawing hooks only while the series is visible. A primitive attached to
153/// the main series, an added bar series or an overlay study draws in the main pane; one attached
154/// to a study in a pane of its own draws in that pane. See the [module](self) documentation.
155pub trait Primitive: Send {
156    /// Draws in `pass` with `painter`, clipped to the plot area of the pane.
157    fn draw(&self, painter: &Painter, view: &PrimitiveView<'_>, pass: PrimitivePass) {
158        let _ = (painter, view, pass);
159    }
160
161    /// Draws with `factor` consecutive source slots per physical pixel group, one while
162    /// zoomed in. Override this for custom point aggregation; use
163    /// [`PrimitiveView::point_groups`] to visit the aligned ranges. Cache aggregates in
164    /// [`data_changed`](Self::data_changed) when visiting all source values would be costly.
165    /// The default preserves [`draw`](Self::draw) for existing renderers.
166    fn draw_merged(
167        &self,
168        painter: &Painter,
169        view: &PrimitiveView<'_>,
170        pass: PrimitivePass,
171        factor: usize,
172    ) {
173        let _ = factor;
174        self.draw(painter, view, pass);
175    }
176
177    /// Labels for the price axis of the pane, placed at their prices next to the chart's own
178    /// tags. A label off the scale or without room is left out.
179    fn price_axis_labels(&self, view: &PrimitiveView<'_>) -> Vec<PriceAxisLabel> {
180        let _ = view;
181        Vec::new()
182    }
183
184    /// Labels for the time axis, centred at their times. Axis labels they cover are left out.
185    ///
186    /// The time axis is shared by every pane, so the view of a primitive in a study pane maps
187    /// prices on the main pane here.
188    fn time_axis_labels(&self, view: &PrimitiveView<'_>) -> Vec<TimeAxisLabel> {
189        let _ = view;
190        Vec::new()
191    }
192
193    /// Draws inside `axis`, the price axis of the pane, behind its labels.
194    fn draw_price_axis(&self, painter: &Painter, axis: Rect, view: &PrimitiveView<'_>) {
195        let _ = (painter, axis, view);
196    }
197
198    /// Draws inside `axis`, the time axis, behind its labels. The view is as in
199    /// [`time_axis_labels`](Self::time_axis_labels).
200    fn draw_time_axis(&self, painter: &Painter, axis: Rect, view: &PrimitiveView<'_>) {
201        let _ = (painter, axis, view);
202    }
203
204    /// The lowest and highest price the scale of the series must show while it fits itself to
205    /// the bars in view, `visible` indexing `bars` as in [`PrimitiveView`]. `None` adds nothing.
206    /// An overlay study on an independent scale fits that scale to its own values only.
207    fn autoscale(&self, bars: &[Bar], visible: Range<usize>) -> Option<(f64, f64)> {
208        let _ = (bars, visible);
209        None
210    }
211
212    /// Whether the pointer at `pos` is over the primitive, and the cursor to show there.
213    ///
214    /// A hit is the pointer target of chart events. Drawings come first, then primitives,
215    /// the latest attached first, then the series and studies.
216    fn hit_test(&self, pos: Pos2, view: &PrimitiveView<'_>) -> Option<CursorIcon> {
217        let _ = (pos, view);
218        None
219    }
220
221    /// Called once, at the first frame after the primitive was attached to `series`. `ctx` is
222    /// the chart's egui context; keep a clone to request repaints, from any thread.
223    fn attached(&mut self, ctx: &egui::Context, series: SeriesId) {
224        let _ = (ctx, series);
225    }
226
227    /// Called once when the primitive is detached, directly or by removing its series. Undoing
228    /// the removal of a study restores the study without its primitives.
229    fn detached(&mut self) {}
230
231    /// Called after [`attached`](Self::attached) and at every frame after the bars of the series
232    /// changed, before anything is drawn. `bars` are the series' own bars, oldest first; for a
233    /// study they are the main series bars the study is computed from.
234    fn data_changed(&mut self, bars: &[Bar]) {
235        let _ = bars;
236    }
237}