Primitives and rendering extensions
Use a
Primitive
to draw host geometry on an existing series. Use a
custom series when a study supplies values and
your renderer replaces its normal plots. Both need only the widget feature;
neither requires a change to the crate’s renderer dispatch.
For another horizontal domain, see custom horizontal scales.
Shade sessions behind the bars
Section titled “Shade sessions behind the bars”The scene adapts the session highlight from the native
primitives example.
It caches session boundaries when the source bars change, shades them in the
Below pass, and labels each open on the time axis. This sample uses fixed
14:30 to 21:00 UTC hours; a production host supplies the relevant exchange calendar,
holidays and daylight-saving rules.
Pan across session opens and zoom in: the shading stays behind the candles and the open tags follow the time axis.
The interactive example needs JavaScript and WebAssembly. You can still use the chart setup alongside it.
/// Shade regular sessions and label their opens on the shared time axis.pub fn primitives() -> egui_charts::Chart { use chrono::{DateTime, Duration, Utc}; use eframe::egui::{Color32, Painter, Rect}; use egui_charts::model::Bar; use egui_charts::widget::{Primitive, PrimitivePass, PrimitiveView, SeriesId, TimeAxisLabel};
/// Shades the regular session of every day behind the bars and labels its open on the time axis. struct SessionHighlight { /// Open and close after midnight UTC. open: Duration, close: Duration, /// Open and close of each day the bars cover. sessions: Vec<(DateTime<Utc>, DateTime<Utc>)>, }
impl Primitive for SessionHighlight { fn data_changed(&mut self, bars: &[Bar]) { self.sessions.clear(); let (Some(first), Some(last)) = (bars.first(), bars.last()) else { return; }; let mut day = first.time.date_naive(); while day <= last.time.date_naive() { let midnight = day.and_hms_opt(0, 0, 0).unwrap().and_utc(); self.sessions .push((midnight + self.open, midnight + self.close)); day = day.succ_opt().unwrap(); } }
fn draw(&self, painter: &Painter, view: &PrimitiveView<'_>, pass: PrimitivePass) { if pass != PrimitivePass::Below { return; } let plot = view.mapping.rect; // Bars are centred on their open times, so a session starts half a bar before its first. let half = view.mapping.bar_spacing / 2.0; for (open, close) in &self.sessions { if let (Some(left), Some(right)) = (view.time_to_x(*open), view.time_to_x(*close)) { painter.rect_filled( Rect::from_x_y_ranges(left - half..=right - half, plot.y_range()), 0.0, Color32::from_rgba_unmultiplied(41, 98, 255, 24), ); } } }
fn time_axis_labels(&self, _view: &PrimitiveView<'_>) -> Vec<TimeAxisLabel> { self.sessions .iter() .map(|(open, _)| TimeAxisLabel { time: *open, text: format!("Open {}", open.format("%b %-d")), color: Color32::from_rgb(41, 98, 255), }) .collect() } }
let mut chart = sample_chart(); chart.attach_primitive( SeriesId::MAIN, SessionHighlight { // 14:30 to 21:00 UTC, the regular hours of a US exchange in winter. open: Duration::minutes(14 * 60 + 30), close: Duration::hours(21), sessions: Vec::new(), }, ); chart}
/// Shared fixed fifteen-minute history for the two primitive scenes.fn sample_chart() -> egui_charts::Chart { use egui_charts::{Chart, model::BarData};
let mut chart = Chart::new(BarData::from_bars((0..480).map(sample_bar).collect())); chart.set_symbol("SAMPLE"); chart.set_visible_bars(80); chart.config_mut().show_volume = false; chart}
/// Sample bar `i` of fifteen minutes: a deterministic wave, not market data.fn sample_bar(i: usize) -> egui_charts::model::Bar { use chrono::{TimeZone, Utc}; use egui_charts::model::Bar;
let t = i as f64; let open = 100.0 + 8.0 * (t / 50.0).sin() + 2.0 * (t / 9.0).sin(); let close = open + 1.2 * (t * 1.3).sin(); let spread = 0.4 + 0.6 * (t / 6.0).sin().abs(); let time = Utc .timestamp_opt(1_700_006_400 + 900 * i as i64, 0) .unwrap(); Bar::new( time, open, open.max(close) + spread, open.min(close) - spread, close, 1000.0 + 500.0 * (t / 3.0).cos(), )}Attach and detach
Section titled “Attach and detach”Chart::attach_primitive(series, primitive)
accepts the main series, an added bar series or a study and returns an
Option<PrimitiveId>.
An absent series returns None and drops the supplied primitive. Keep the ID
when you need to remove the attachment with
detach_primitive;
it returns false for an unknown or already detached ID.
The primitive follows its series’ pane and price scale, including pane moves.
Its drawing hooks run while that series is visible. Implement only the hooks
you need; all have defaults that do nothing. Primitive requires Send.
| Hook | When it runs | What to keep there |
|---|---|---|
attached | Once, on the first frame after attachment | The series ID and a cloned egui context if host work needs to request a repaint |
data_changed | After attachment and before drawing a frame whose source bars changed | Cached domain geometry; bars are oldest first, and a study receives the main bars it computes from |
detached | On detachment after the attachment hook ran, including series removal | Release resources owned by the attachment |
Attach once during setup, then call chart.show(ui) each frame. Reattaching
every frame would accumulate primitives. Undoing a study removal restores the
study without its primitive attachments, so the host must recreate those.
Draw passes and coordinates
Section titled “Draw passes and coordinates”draw
receives a painter clipped to the pane’s plot, a
PrimitiveView
and a PrimitivePass.
Choose the pass explicitly to avoid drawing the same geometry three times:
| Pass | Position |
|---|---|
Below | Above the pane background, grid and sessions; behind its series and studies |
Series | Above series and studies; below drawings |
Above | Above series, studies and drawings; still below the main pane’s crosshair and legend |
Several attachments draw in attachment order within a pass. Read geometry from the current view instead of caching screen coordinates across pan, zoom or resize:
view.visibleis the visible index range intoview.bars.index_to_xmaps a fractional index to screen points; whole indices are bar centres.time_to_xinterpolates times and returnsNonewithout bars.view.mappinggives the plot rectangle, bar spacing and the series’ vertical mapping.price_to_ymaps prices on that scale, respecting its mode and inversion.
The session rectangle starts half a bar before its opening bar’s centre and
ends half a bar before the closing time, so its edges align with bar boundaries.
For dense custom geometry, override
draw_merged
and visit aligned source ranges with
point_groups.
The default calls draw without aggregating your payload.
Axes and autoscale
Section titled “Axes and autoscale”Return TimeAxisLabel
values from
time_axis_labels,
as this scene does, or
PriceAxisLabel
values from
price_axis_labels.
The chart places tags, chooses readable text against their fill, and omits price
tags off the scale or without room. Time tags suppress underlying axis labels
they cover.
For other axis geometry, use
draw_price_axis
or draw_time_axis;
their painters are clipped to the supplied axis rectangle and draw behind labels.
The time axis is shared: in time-axis hooks, a study-pane primitive’s vertical
mapping is the main pane’s mapping. Use time or x there, rather than its study’s y.
autoscale
returns an optional (minimum, maximum) in data values to widen the series’
automatic price fit. None adds nothing. It does not change a manual range.
An overlay study on an independent scale fits that scale to its study values
only. The custom-series scene includes zero so
that lollipop stems remain inside their pane’s automatic range.
Hit testing and host actions
Section titled “Hit testing and host actions”hit_test
receives the pointer in egui screen points and the current view. Return a
CursorIcon for a hit,
or None to let testing continue. Reuse the drawing geometry so the hit area
follows pan and zoom. Drawings have priority, then attached primitives in reverse
attachment order, then built-in series and studies.
A normal attachment reports
PointerTarget::Primitive(id)
in chart pointer events. Read events with
drain_events
after show and use them for host actions; a hit test chooses a target and cursor,
it does not execute an application command. A custom series’ renderer instead
targets its study, as the next guide explains.