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}