pub use axis::{Axis, AxisHints, HPlacement, Placement, VPlacement};
pub use items::{
Line, LineStyle, MarkerShape, Orientation, PlotPoint, PlotPoints, Points, Polygon, StackedLine,
Text,
};
pub use legend::{Corner, Legend};
pub use transform::{PlotBounds, PlotTransform};
use axis::AxisWidget;
use egui::{
ahash::{self, HashMap},
epaint::{self, util::FloatOrd, Hsva},
lerp, remap_clamp, vec2, Align2, Color32, Context, CursorIcon, Id, Layout, Margin, NumExt as _,
PointerButton, Pos2, Rect, Response, Rounding, Sense, Shape, Stroke, TextStyle, Ui, Vec2,
WidgetText,
};
use items::{horizontal_line, rulers_color, vertical_line, PlotItem};
use legend::LegendWidget;
use std::{ops::RangeInclusive, sync::Arc};
mod axis;
mod items;
mod legend;
mod transform;
type LabelFormatterFn = dyn Fn(&str, &PlotPoint) -> String;
type LabelFormatter = Option<Box<LabelFormatterFn>>;
type GridSpacerFn = dyn Fn(GridInput) -> Vec<GridMark>;
type GridSpacer = Box<GridSpacerFn>;
type CoordinatesFormatterFn = dyn Fn(&PlotPoint, &PlotBounds) -> String;
pub struct CoordinatesFormatter {
function: Box<CoordinatesFormatterFn>,
}
impl CoordinatesFormatter {
pub fn new(function: impl Fn(&PlotPoint, &PlotBounds) -> String + 'static) -> Self {
Self {
function: Box::new(function),
}
}
pub fn with_decimals(num_decimals: usize) -> Self {
Self {
function: Box::new(move |value, _| {
format!("x: {:.d$}\ny: {:.d$}", value.x, value.y, d = num_decimals)
}),
}
}
fn format(&self, value: &PlotPoint, bounds: &PlotBounds) -> String {
(self.function)(value, bounds)
}
}
impl Default for CoordinatesFormatter {
fn default() -> Self {
Self::with_decimals(3)
}
}
const MIN_LINE_SPACING_IN_POINTS: f64 = 6.0;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct AxisBools {
pub x: bool,
pub y: bool,
}
impl AxisBools {
#[inline]
pub fn new(x: bool, y: bool) -> Self {
Self { x, y }
}
#[inline]
pub fn any(&self) -> bool {
self.x || self.y
}
}
impl From<bool> for AxisBools {
#[inline]
fn from(val: bool) -> Self {
AxisBools { x: val, y: val }
}
}
impl From<[bool; 2]> for AxisBools {
#[inline]
fn from([x, y]: [bool; 2]) -> Self {
AxisBools { x, y }
}
}
#[derive(Clone)]
struct PlotMemory {
bounds_modified: AxisBools,
hovered_entry: Option<String>,
hidden_items: ahash::HashSet<String>,
last_plot_transform: PlotTransform,
last_click_pos_for_zoom: Option<Pos2>,
}
impl PlotMemory {
pub fn load(ctx: &Context, id: Id) -> Option<Self> {
ctx.data_mut(|d| d.get_temp(id))
}
pub fn store(self, ctx: &Context, id: Id) {
ctx.data_mut(|d| d.insert_temp(id, self));
}
}
#[derive(Copy, Clone, PartialEq)]
enum Cursor {
Horizontal { y: f64 },
Vertical { x: f64 },
}
#[derive(PartialEq, Clone)]
struct PlotFrameCursors {
id: Id,
cursors: Vec<Cursor>,
}
#[derive(Default, Clone)]
struct CursorLinkGroups(HashMap<Id, Vec<PlotFrameCursors>>);
#[derive(Clone)]
struct LinkedBounds {
bounds: PlotBounds,
bounds_modified: AxisBools,
}
#[derive(Default, Clone)]
struct BoundsLinkGroups(HashMap<Id, LinkedBounds>);
pub struct PlotResponse<R> {
pub inner: R,
pub response: Response,
pub transform: PlotTransform,
}
pub struct Plot {
id_source: Id,
center_axis: AxisBools,
allow_zoom: AxisBools,
allow_drag: AxisBools,
allow_scroll: bool,
allow_double_click_reset: bool,
allow_boxed_zoom: bool,
auto_bounds: AxisBools,
min_auto_bounds: PlotBounds,
margin_fraction: Vec2,
boxed_zoom_pointer_button: PointerButton,
linked_axes: Option<(Id, AxisBools)>,
linked_cursors: Option<(Id, AxisBools)>,
min_size: Vec2,
width: Option<f32>,
height: Option<f32>,
data_aspect: Option<f32>,
view_aspect: Option<f32>,
reset: bool,
show_x: bool,
show_y: bool,
label_formatter: LabelFormatter,
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
x_axes: Vec<AxisHints>, y_axes: Vec<AxisHints>, legend_config: Option<Legend>,
show_background: bool,
show_axes: AxisBools,
show_grid: AxisBools,
grid_spacers: [GridSpacer; 2],
sharp_grid_lines: bool,
clamp_grid: bool,
}
impl Plot {
pub fn new(id_source: impl std::hash::Hash) -> Self {
Self {
id_source: Id::new(id_source),
center_axis: false.into(),
allow_zoom: true.into(),
allow_drag: true.into(),
allow_scroll: true,
allow_double_click_reset: true,
allow_boxed_zoom: true,
auto_bounds: false.into(),
min_auto_bounds: PlotBounds::NOTHING,
margin_fraction: Vec2::splat(0.05),
boxed_zoom_pointer_button: PointerButton::Secondary,
linked_axes: None,
linked_cursors: None,
min_size: Vec2::splat(64.0),
width: None,
height: None,
data_aspect: None,
view_aspect: None,
reset: false,
show_x: true,
show_y: true,
label_formatter: None,
coordinates_formatter: None,
x_axes: vec![Default::default()],
y_axes: vec![Default::default()],
legend_config: None,
show_background: true,
show_axes: true.into(),
show_grid: true.into(),
grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)],
sharp_grid_lines: true,
clamp_grid: false,
}
}
pub fn data_aspect(mut self, data_aspect: f32) -> Self {
self.data_aspect = Some(data_aspect);
self
}
pub fn view_aspect(mut self, view_aspect: f32) -> Self {
self.view_aspect = Some(view_aspect);
self
}
pub fn width(mut self, width: f32) -> Self {
self.min_size.x = width;
self.width = Some(width);
self
}
pub fn height(mut self, height: f32) -> Self {
self.min_size.y = height;
self.height = Some(height);
self
}
pub fn min_size(mut self, min_size: Vec2) -> Self {
self.min_size = min_size;
self
}
pub fn show_x(mut self, show_x: bool) -> Self {
self.show_x = show_x;
self
}
pub fn show_y(mut self, show_y: bool) -> Self {
self.show_y = show_y;
self
}
pub fn center_x_axis(mut self, on: bool) -> Self {
self.center_axis.x = on;
self
}
pub fn center_y_axis(mut self, on: bool) -> Self {
self.center_axis.y = on;
self
}
pub fn allow_zoom<T>(mut self, on: T) -> Self
where
T: Into<AxisBools>,
{
self.allow_zoom = on.into();
self
}
pub fn allow_scroll(mut self, on: bool) -> Self {
self.allow_scroll = on;
self
}
pub fn allow_double_click_reset(mut self, on: bool) -> Self {
self.allow_double_click_reset = on;
self
}
pub fn set_margin_fraction(mut self, margin_fraction: Vec2) -> Self {
self.margin_fraction = margin_fraction;
self
}
pub fn allow_boxed_zoom(mut self, on: bool) -> Self {
self.allow_boxed_zoom = on;
self
}
pub fn boxed_zoom_pointer_button(mut self, boxed_zoom_pointer_button: PointerButton) -> Self {
self.boxed_zoom_pointer_button = boxed_zoom_pointer_button;
self
}
pub fn allow_drag<T>(mut self, on: T) -> Self
where
T: Into<AxisBools>,
{
self.allow_drag = on.into();
self
}
pub fn label_formatter(
mut self,
label_formatter: impl Fn(&str, &PlotPoint) -> String + 'static,
) -> Self {
self.label_formatter = Some(Box::new(label_formatter));
self
}
pub fn coordinates_formatter(
mut self,
position: Corner,
formatter: CoordinatesFormatter,
) -> Self {
self.coordinates_formatter = Some((position, formatter));
self
}
pub fn x_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec<GridMark> + 'static) -> Self {
self.grid_spacers[0] = Box::new(spacer);
self
}
pub fn y_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec<GridMark> + 'static) -> Self {
self.grid_spacers[1] = Box::new(spacer);
self
}
pub fn clamp_grid(mut self, clamp_grid: bool) -> Self {
self.clamp_grid = clamp_grid;
self
}
pub fn include_x(mut self, x: impl Into<f64>) -> Self {
self.min_auto_bounds.extend_with_x(x.into());
self
}
pub fn include_y(mut self, y: impl Into<f64>) -> Self {
self.min_auto_bounds.extend_with_y(y.into());
self
}
pub fn auto_bounds_x(mut self) -> Self {
self.auto_bounds.x = true;
self
}
pub fn auto_bounds_y(mut self) -> Self {
self.auto_bounds.y = true;
self
}
pub fn legend(mut self, legend: Legend) -> Self {
self.legend_config = Some(legend);
self
}
pub fn show_background(mut self, show: bool) -> Self {
self.show_background = show;
self
}
pub fn show_axes(mut self, show: impl Into<AxisBools>) -> Self {
self.show_axes = show.into();
self
}
pub fn show_grid(mut self, show: impl Into<AxisBools>) -> Self {
self.show_grid = show.into();
self
}
pub fn link_axis(mut self, group_id: impl Into<Id>, link_x: bool, link_y: bool) -> Self {
self.linked_axes = Some((
group_id.into(),
AxisBools {
x: link_x,
y: link_y,
},
));
self
}
pub fn link_cursor(mut self, group_id: impl Into<Id>, link_x: bool, link_y: bool) -> Self {
self.linked_cursors = Some((
group_id.into(),
AxisBools {
x: link_x,
y: link_y,
},
));
self
}
pub fn sharp_grid_lines(mut self, enabled: bool) -> Self {
self.sharp_grid_lines = enabled;
self
}
pub fn reset(mut self) -> Self {
self.reset = true;
self
}
pub fn x_axis_label(mut self, label: impl Into<WidgetText>) -> Self {
if let Some(main) = self.x_axes.first_mut() {
main.label = label.into();
}
self
}
pub fn y_axis_label(mut self, label: impl Into<WidgetText>) -> Self {
if let Some(main) = self.y_axes.first_mut() {
main.label = label.into();
}
self
}
pub fn x_axis_position(mut self, placement: axis::VPlacement) -> Self {
if let Some(main) = self.x_axes.first_mut() {
main.placement = placement.into();
}
self
}
pub fn y_axis_position(mut self, placement: axis::HPlacement) -> Self {
if let Some(main) = self.y_axes.first_mut() {
main.placement = placement.into();
}
self
}
pub fn x_axis_formatter(
mut self,
fmt: impl Fn(f64, usize, &RangeInclusive<f64>) -> String + 'static,
) -> Self {
if let Some(main) = self.x_axes.first_mut() {
main.formatter = Arc::new(fmt);
}
self
}
pub fn y_axis_formatter(
mut self,
fmt: impl Fn(f64, usize, &RangeInclusive<f64>) -> String + 'static,
) -> Self {
if let Some(main) = self.y_axes.first_mut() {
main.formatter = Arc::new(fmt);
}
self
}
pub fn y_axis_width(mut self, digits: usize) -> Self {
if let Some(main) = self.y_axes.first_mut() {
main.digits = digits;
}
self
}
pub fn custom_x_axes(mut self, hints: Vec<AxisHints>) -> Self {
self.x_axes = hints;
self
}
pub fn custom_y_axes(mut self, hints: Vec<AxisHints>) -> Self {
self.y_axes = hints;
self
}
pub fn show<R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> PlotResponse<R> {
self.show_dyn(ui, Box::new(build_fn))
}
fn show_dyn<'a, R>(
self,
ui: &mut Ui,
build_fn: Box<dyn FnOnce(&mut PlotUi) -> R + 'a>,
) -> PlotResponse<R> {
let Self {
id_source,
center_axis,
allow_zoom,
allow_drag,
allow_scroll,
allow_double_click_reset,
allow_boxed_zoom,
boxed_zoom_pointer_button: boxed_zoom_pointer,
auto_bounds,
min_auto_bounds,
margin_fraction,
width,
height,
min_size,
data_aspect,
view_aspect,
mut show_x,
mut show_y,
label_formatter,
coordinates_formatter,
x_axes,
y_axes,
legend_config,
reset,
show_background,
show_axes,
show_grid,
linked_axes,
linked_cursors,
clamp_grid,
grid_spacers,
sharp_grid_lines,
} = self;
let pos = ui.available_rect_before_wrap().min;
let size = {
let width = width
.unwrap_or_else(|| {
if let (Some(height), Some(aspect)) = (height, view_aspect) {
height * aspect
} else {
ui.available_size_before_wrap().x
}
})
.at_least(min_size.x);
let height = height
.unwrap_or_else(|| {
if let Some(aspect) = view_aspect {
width / aspect
} else {
ui.available_size_before_wrap().y
}
})
.at_least(min_size.y);
vec2(width, height)
};
let complete_rect = Rect {
min: pos,
max: pos + size,
};
let mut plot_rect: Rect = {
let mut margin = Margin::ZERO;
if show_axes.x {
for cfg in &x_axes {
match cfg.placement {
axis::Placement::LeftBottom => {
margin.bottom += cfg.thickness(Axis::X);
}
axis::Placement::RightTop => {
margin.top += cfg.thickness(Axis::X);
}
}
}
}
if show_axes.y {
for cfg in &y_axes {
match cfg.placement {
axis::Placement::LeftBottom => {
margin.left += cfg.thickness(Axis::Y);
}
axis::Placement::RightTop => {
margin.right += cfg.thickness(Axis::Y);
}
}
}
}
margin.shrink_rect(complete_rect)
};
let [mut x_axis_widgets, mut y_axis_widgets] =
axis_widgets(show_axes, plot_rect, [&x_axes, &y_axes]);
if plot_rect.width() <= 0.0 || plot_rect.height() <= 0.0 {
y_axis_widgets.clear();
x_axis_widgets.clear();
plot_rect = complete_rect;
}
let response = ui.allocate_rect(plot_rect, Sense::drag());
let rect = plot_rect;
let plot_id = ui.make_persistent_id(id_source);
ui.ctx().check_for_id_clash(plot_id, rect, "Plot");
let memory = if reset {
if let Some((name, _)) = linked_axes.as_ref() {
ui.memory_mut(|memory| {
let link_groups: &mut BoundsLinkGroups =
memory.data.get_temp_mut_or_default(Id::NULL);
link_groups.0.remove(name);
});
};
None
} else {
PlotMemory::load(ui.ctx(), plot_id)
}
.unwrap_or_else(|| PlotMemory {
bounds_modified: false.into(),
hovered_entry: None,
hidden_items: Default::default(),
last_plot_transform: PlotTransform::new(
rect,
min_auto_bounds,
center_axis.x,
center_axis.y,
),
last_click_pos_for_zoom: None,
});
let PlotMemory {
mut bounds_modified,
mut hovered_entry,
mut hidden_items,
last_plot_transform,
mut last_click_pos_for_zoom,
} = memory;
let mut plot_ui = PlotUi {
items: Vec::new(),
next_auto_color_idx: 0,
last_plot_transform,
response,
bounds_modifications: Vec::new(),
ctx: ui.ctx().clone(),
};
let inner = build_fn(&mut plot_ui);
let PlotUi {
mut items,
mut response,
last_plot_transform,
bounds_modifications,
..
} = plot_ui;
if show_background {
ui.painter()
.with_clip_rect(rect)
.add(epaint::RectShape::new(
rect,
Rounding::same(2.0),
ui.visuals().extreme_bg_color,
ui.visuals().widgets.noninteractive.bg_stroke,
));
}
let legend = legend_config
.and_then(|config| LegendWidget::try_new(rect, config, &items, &hidden_items));
if hovered_entry.is_some() {
show_x = false;
show_y = false;
}
items.retain(|item| !hidden_items.contains(item.name()));
if let Some(hovered_name) = &hovered_entry {
items
.iter_mut()
.filter(|entry| entry.name() == hovered_name)
.for_each(|entry| entry.highlight());
}
items.sort_by_key(|item| item.highlighted());
let mut bounds = *last_plot_transform.bounds();
let draw_cursors: Vec<Cursor> = if let Some((id, _)) = linked_cursors.as_ref() {
ui.memory_mut(|memory| {
let frames: &mut CursorLinkGroups = memory.data.get_temp_mut_or_default(Id::NULL);
let cursors = frames.0.entry(*id).or_default();
let index = cursors
.iter()
.enumerate()
.find(|(_, frame)| frame.id == plot_id)
.map(|(i, _)| i);
index.map(|index| cursors.drain(0..=index));
cursors
.iter()
.flat_map(|frame| frame.cursors.iter().copied())
.collect()
})
} else {
Vec::new()
};
if let Some((id, axes)) = linked_axes.as_ref() {
ui.memory_mut(|memory| {
let link_groups: &mut BoundsLinkGroups =
memory.data.get_temp_mut_or_default(Id::NULL);
if let Some(linked_bounds) = link_groups.0.get(id) {
if axes.x {
bounds.set_x(&linked_bounds.bounds);
bounds_modified.x = linked_bounds.bounds_modified.x;
}
if axes.y {
bounds.set_y(&linked_bounds.bounds);
bounds_modified.y = linked_bounds.bounds_modified.y;
}
};
});
};
if allow_double_click_reset && response.double_clicked() {
bounds_modified = false.into();
}
for modification in bounds_modifications {
match modification {
BoundsModification::Set(new_bounds) => {
bounds = new_bounds;
bounds_modified = true.into();
}
BoundsModification::Translate(delta) => {
bounds.translate(delta);
bounds_modified = true.into();
}
}
}
if !bounds_modified.x {
bounds.set_x(&min_auto_bounds);
}
if !bounds_modified.y {
bounds.set_y(&min_auto_bounds);
}
let auto_x = !bounds_modified.x && (!min_auto_bounds.is_valid_x() || auto_bounds.x);
let auto_y = !bounds_modified.y && (!min_auto_bounds.is_valid_y() || auto_bounds.y);
if auto_x || auto_y {
for item in &items {
let item_bounds = item.bounds();
if auto_x {
bounds.merge_x(&item_bounds);
}
if auto_y {
bounds.merge_y(&item_bounds);
}
}
if auto_x {
bounds.add_relative_margin_x(margin_fraction);
}
if auto_y {
bounds.add_relative_margin_y(margin_fraction);
}
}
let mut transform = PlotTransform::new(rect, bounds, center_axis.x, center_axis.y);
if let Some(data_aspect) = data_aspect {
if let Some((_, linked_axes)) = &linked_axes {
let change_x = linked_axes.y && !linked_axes.x;
transform.set_aspect_by_changing_axis(data_aspect as f64, change_x);
} else if auto_bounds.any() {
transform.set_aspect_by_expanding(data_aspect as f64);
} else {
transform.set_aspect_by_changing_axis(data_aspect as f64, false);
}
}
if allow_drag.any() && response.dragged_by(PointerButton::Primary) {
response = response.on_hover_cursor(CursorIcon::Grabbing);
let mut delta = -response.drag_delta();
if !allow_drag.x {
delta.x = 0.0;
}
if !allow_drag.y {
delta.y = 0.0;
}
transform.translate_bounds(delta);
bounds_modified = allow_drag;
}
let mut boxed_zoom_rect = None;
if allow_boxed_zoom {
if response.drag_started() && response.dragged_by(boxed_zoom_pointer) {
last_click_pos_for_zoom = response.hover_pos();
}
let box_start_pos = last_click_pos_for_zoom;
let box_end_pos = response.hover_pos();
if let (Some(box_start_pos), Some(box_end_pos)) = (box_start_pos, box_end_pos) {
if response.dragged_by(boxed_zoom_pointer) {
response = response.on_hover_cursor(CursorIcon::ZoomIn);
let rect = epaint::Rect::from_two_pos(box_start_pos, box_end_pos);
boxed_zoom_rect = Some((
epaint::RectShape::stroke(
rect,
0.0,
epaint::Stroke::new(4., Color32::DARK_BLUE),
), epaint::RectShape::stroke(
rect,
0.0,
epaint::Stroke::new(2., Color32::WHITE),
), ));
}
if response.drag_released() {
let box_start_pos = transform.value_from_position(box_start_pos);
let box_end_pos = transform.value_from_position(box_end_pos);
let new_bounds = PlotBounds {
min: [
box_start_pos.x.min(box_end_pos.x),
box_start_pos.y.min(box_end_pos.y),
],
max: [
box_start_pos.x.max(box_end_pos.x),
box_start_pos.y.max(box_end_pos.y),
],
};
if new_bounds.is_valid() {
transform.set_bounds(new_bounds);
bounds_modified = true.into();
}
last_click_pos_for_zoom = None;
}
}
}
let hover_pos = response.hover_pos();
if let Some(hover_pos) = hover_pos {
if allow_zoom.any() {
let mut zoom_factor = if data_aspect.is_some() {
Vec2::splat(ui.input(|i| i.zoom_delta()))
} else {
ui.input(|i| i.zoom_delta_2d())
};
if !allow_zoom.x {
zoom_factor.x = 1.0;
}
if !allow_zoom.y {
zoom_factor.y = 1.0;
}
if zoom_factor != Vec2::splat(1.0) {
transform.zoom(zoom_factor, hover_pos);
bounds_modified = allow_zoom;
}
}
if allow_scroll {
let scroll_delta = ui.input(|i| i.raw_scroll_delta);
if scroll_delta != Vec2::ZERO {
transform.translate_bounds(-scroll_delta);
bounds_modified = true.into();
}
}
}
let bounds = transform.bounds();
let x_axis_range = bounds.range_x();
let x_steps = Arc::new({
let input = GridInput {
bounds: (bounds.min[0], bounds.max[0]),
base_step_size: transform.dvalue_dpos()[0] * MIN_LINE_SPACING_IN_POINTS * 2.0,
};
(grid_spacers[0])(input)
});
let y_axis_range = bounds.range_y();
let y_steps = Arc::new({
let input = GridInput {
bounds: (bounds.min[1], bounds.max[1]),
base_step_size: transform.dvalue_dpos()[1] * MIN_LINE_SPACING_IN_POINTS * 2.0,
};
(grid_spacers[1])(input)
});
for mut widget in x_axis_widgets {
widget.range = x_axis_range.clone();
widget.transform = Some(transform);
widget.steps = x_steps.clone();
widget.ui(ui, Axis::X);
}
for mut widget in y_axis_widgets {
widget.range = y_axis_range.clone();
widget.transform = Some(transform);
widget.steps = y_steps.clone();
widget.ui(ui, Axis::Y);
}
for item in &mut items {
item.initialize(transform.bounds().range_x());
}
let prepared = PreparedPlot {
items,
show_x,
show_y,
label_formatter,
coordinates_formatter,
show_grid,
transform,
draw_cursor_x: linked_cursors.as_ref().is_some_and(|group| group.1.x),
draw_cursor_y: linked_cursors.as_ref().is_some_and(|group| group.1.y),
draw_cursors,
grid_spacers,
sharp_grid_lines,
clamp_grid,
};
let plot_cursors = prepared.ui(ui, &response);
if let Some(boxed_zoom_rect) = boxed_zoom_rect {
ui.painter().with_clip_rect(rect).add(boxed_zoom_rect.0);
ui.painter().with_clip_rect(rect).add(boxed_zoom_rect.1);
}
if let Some(mut legend) = legend {
ui.add(&mut legend);
hidden_items = legend.hidden_items();
hovered_entry = legend.hovered_entry_name();
}
if let Some((id, _)) = linked_cursors.as_ref() {
ui.memory_mut(|memory| {
let frames: &mut CursorLinkGroups = memory.data.get_temp_mut_or_default(Id::NULL);
let cursors = frames.0.entry(*id).or_default();
cursors.push(PlotFrameCursors {
id: plot_id,
cursors: plot_cursors,
});
});
}
if let Some((id, _)) = linked_axes.as_ref() {
ui.memory_mut(|memory| {
let link_groups: &mut BoundsLinkGroups =
memory.data.get_temp_mut_or_default(Id::NULL);
link_groups.0.insert(
*id,
LinkedBounds {
bounds: *transform.bounds(),
bounds_modified,
},
);
});
}
let memory = PlotMemory {
bounds_modified,
hovered_entry,
hidden_items,
last_plot_transform: transform,
last_click_pos_for_zoom,
};
memory.store(ui.ctx(), plot_id);
let response = if show_x || show_y {
response.on_hover_cursor(CursorIcon::Crosshair)
} else {
response
};
ui.advance_cursor_after_rect(complete_rect);
PlotResponse {
inner,
response,
transform,
}
}
}
fn axis_widgets(
show_axes: AxisBools,
plot_rect: Rect,
[x_axes, y_axes]: [&[AxisHints]; 2],
) -> [Vec<AxisWidget>; 2] {
let mut x_axis_widgets = Vec::<AxisWidget>::new();
let mut y_axis_widgets = Vec::<AxisWidget>::new();
struct NumWidgets {
left: usize,
top: usize,
right: usize,
bottom: usize,
}
let mut num_widgets = NumWidgets {
left: 0,
top: 0,
right: 0,
bottom: 0,
};
if show_axes.x {
for cfg in x_axes {
let size_y = Vec2::new(0.0, cfg.thickness(Axis::X));
let rect = match cfg.placement {
axis::Placement::LeftBottom => {
let off = num_widgets.bottom as f32;
num_widgets.bottom += 1;
Rect {
min: plot_rect.left_bottom() + size_y * off,
max: plot_rect.right_bottom() + size_y * (off + 1.0),
}
}
axis::Placement::RightTop => {
let off = num_widgets.top as f32;
num_widgets.top += 1;
Rect {
min: plot_rect.left_top() - size_y * (off + 1.0),
max: plot_rect.right_top() - size_y * off,
}
}
};
x_axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
}
}
if show_axes.y {
for cfg in y_axes {
let size_x = Vec2::new(cfg.thickness(Axis::Y), 0.0);
let rect = match cfg.placement {
axis::Placement::LeftBottom => {
let off = num_widgets.left as f32;
num_widgets.left += 1;
Rect {
min: plot_rect.left_top() - size_x * (off + 1.0),
max: plot_rect.left_bottom() - size_x * off,
}
}
axis::Placement::RightTop => {
let off = num_widgets.right as f32;
num_widgets.right += 1;
Rect {
min: plot_rect.right_top() + size_x * off,
max: plot_rect.right_bottom() + size_x * (off + 1.0),
}
}
};
y_axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
}
}
[x_axis_widgets, y_axis_widgets]
}
enum BoundsModification {
Set(PlotBounds),
Translate(Vec2),
}
pub struct PlotUi {
items: Vec<Box<dyn PlotItem>>,
next_auto_color_idx: usize,
last_plot_transform: PlotTransform,
response: Response,
bounds_modifications: Vec<BoundsModification>,
ctx: Context,
}
impl PlotUi {
fn auto_color(&mut self) -> Color32 {
let i = self.next_auto_color_idx;
self.next_auto_color_idx += 1;
let golden_ratio = (5.0_f32.sqrt() - 1.0) / 2.0; let h = i as f32 * golden_ratio;
Hsva::new(h, 0.85, 0.5, 1.0).into()
}
pub fn ctx(&self) -> &Context {
&self.ctx
}
pub fn plot_bounds(&self) -> PlotBounds {
*self.last_plot_transform.bounds()
}
pub fn set_plot_bounds(&mut self, plot_bounds: PlotBounds) {
self.bounds_modifications
.push(BoundsModification::Set(plot_bounds));
}
pub fn translate_bounds(&mut self, delta_pos: Vec2) {
self.bounds_modifications
.push(BoundsModification::Translate(delta_pos));
}
pub fn response(&self) -> &Response {
&self.response
}
#[deprecated = "Use plot_ui.response().hovered()"]
pub fn plot_hovered(&self) -> bool {
self.response.hovered()
}
#[deprecated = "Use plot_ui.response().clicked()"]
pub fn plot_clicked(&self) -> bool {
self.response.clicked()
}
#[deprecated = "Use plot_ui.response().secondary_clicked()"]
pub fn plot_secondary_clicked(&self) -> bool {
self.response.secondary_clicked()
}
pub fn pointer_coordinate(&self) -> Option<PlotPoint> {
let last_pos = self.ctx().input(|i| i.pointer.latest_pos())? - self.response.drag_delta();
let value = self.plot_from_screen(last_pos);
Some(value)
}
pub fn pointer_coordinate_drag_delta(&self) -> Vec2 {
let delta = self.response.drag_delta();
let dp_dv = self.last_plot_transform.dpos_dvalue();
Vec2::new(delta.x / dp_dv[0] as f32, delta.y / dp_dv[1] as f32)
}
pub fn transform(&self) -> &PlotTransform {
&self.last_plot_transform
}
pub fn screen_from_plot(&self, position: PlotPoint) -> Pos2 {
self.last_plot_transform.position_from_point(&position)
}
pub fn plot_from_screen(&self, position: Pos2) -> PlotPoint {
self.last_plot_transform.value_from_position(position)
}
pub fn line(&mut self, mut line: Line) {
if line.series.is_empty() {
return;
};
if line.stroke.color == Color32::TRANSPARENT {
line.stroke.color = self.auto_color();
}
self.items.push(Box::new(line));
}
pub fn stacked_line(&mut self, mut line: StackedLine) {
if line.series.is_empty() {
return;
};
if line.stroke.color == Color32::TRANSPARENT {
line.stroke.color = self.auto_color();
}
self.items.push(Box::new(line));
}
pub fn text(&mut self, text: Text) {
if text.text.is_empty() {
return;
};
self.items.push(Box::new(text));
}
pub fn points(&mut self, mut points: Points) {
if points.series.is_empty() {
return;
};
if points.color == Color32::TRANSPARENT {
points.color = self.auto_color();
}
self.items.push(Box::new(points));
}
}
pub struct GridInput {
pub bounds: (f64, f64),
pub base_step_size: f64,
}
#[derive(Debug, Clone, Copy)]
pub struct GridMark {
pub value: f64,
pub step_size: f64,
}
pub fn log_grid_spacer(log_base: i64) -> GridSpacer {
let log_base = log_base as f64;
let step_sizes = move |input: GridInput| -> Vec<GridMark> {
let smallest_visible_unit = next_power(input.base_step_size, log_base);
let step_sizes = [
smallest_visible_unit,
smallest_visible_unit * log_base,
smallest_visible_unit * log_base * log_base,
];
generate_marks(step_sizes, input.bounds)
};
Box::new(step_sizes)
}
pub fn uniform_grid_spacer(spacer: impl Fn(GridInput) -> [f64; 3] + 'static) -> GridSpacer {
let get_marks = move |input: GridInput| -> Vec<GridMark> {
let bounds = input.bounds;
let step_sizes = spacer(input);
generate_marks(step_sizes, bounds)
};
Box::new(get_marks)
}
struct PreparedPlot {
items: Vec<Box<dyn PlotItem>>,
show_x: bool,
show_y: bool,
label_formatter: LabelFormatter,
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
transform: PlotTransform,
show_grid: AxisBools,
grid_spacers: [GridSpacer; 2],
draw_cursor_x: bool,
draw_cursor_y: bool,
draw_cursors: Vec<Cursor>,
sharp_grid_lines: bool,
clamp_grid: bool,
}
impl PreparedPlot {
fn ui(self, ui: &mut Ui, response: &Response) -> Vec<Cursor> {
let mut axes_shapes = Vec::new();
if self.show_grid.x {
self.paint_grid(ui, &mut axes_shapes, Axis::X);
}
if self.show_grid.y {
self.paint_grid(ui, &mut axes_shapes, Axis::Y);
}
axes_shapes.sort_by(|(_, strength1), (_, strength2)| strength1.total_cmp(strength2));
let mut shapes = axes_shapes.into_iter().map(|(shape, _)| shape).collect();
let transform = &self.transform;
let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default());
plot_ui.set_clip_rect(*transform.frame());
for item in &self.items {
item.shapes(&mut plot_ui, transform, &mut shapes);
}
let hover_pos = response.hover_pos();
let cursors = if let Some(pointer) = hover_pos {
self.hover(ui, pointer, &mut shapes)
} else {
Vec::new()
};
let line_color = rulers_color(ui);
let mut draw_cursor = |cursors: &Vec<Cursor>, always| {
for &cursor in cursors {
match cursor {
Cursor::Horizontal { y } => {
if self.draw_cursor_y || always {
shapes.push(horizontal_line(
transform.position_from_point(&PlotPoint::new(0.0, y)),
&self.transform,
line_color,
));
}
}
Cursor::Vertical { x } => {
if self.draw_cursor_x || always {
shapes.push(vertical_line(
transform.position_from_point(&PlotPoint::new(x, 0.0)),
&self.transform,
line_color,
));
}
}
}
}
};
draw_cursor(&self.draw_cursors, false);
draw_cursor(&cursors, true);
let painter = ui.painter().with_clip_rect(*transform.frame());
painter.extend(shapes);
if let Some((corner, formatter)) = self.coordinates_formatter.as_ref() {
let hover_pos = response.hover_pos();
if let Some(pointer) = hover_pos {
let font_id = TextStyle::Monospace.resolve(ui.style());
let coordinate = transform.value_from_position(pointer);
let text = formatter.format(&coordinate, transform.bounds());
let padded_frame = transform.frame().shrink(4.0);
let (anchor, position) = match corner {
Corner::LeftTop => (Align2::LEFT_TOP, padded_frame.left_top()),
Corner::RightTop => (Align2::RIGHT_TOP, padded_frame.right_top()),
Corner::LeftBottom => (Align2::LEFT_BOTTOM, padded_frame.left_bottom()),
Corner::RightBottom => (Align2::RIGHT_BOTTOM, padded_frame.right_bottom()),
};
painter.text(position, anchor, text, font_id, ui.visuals().text_color());
}
}
cursors
}
fn paint_grid(&self, ui: &Ui, shapes: &mut Vec<(Shape, f32)>, axis: Axis) {
#![allow(clippy::collapsible_else_if)]
let Self {
transform,
grid_spacers,
clamp_grid,
..
} = self;
let iaxis = usize::from(axis);
let bounds = transform.bounds();
let value_cross = 0.0_f64.clamp(bounds.min[1 - iaxis], bounds.max[1 - iaxis]);
let input = GridInput {
bounds: (bounds.min[iaxis], bounds.max[iaxis]),
base_step_size: transform.dvalue_dpos()[iaxis] * MIN_LINE_SPACING_IN_POINTS,
};
let steps = (grid_spacers[iaxis])(input);
let clamp_range = clamp_grid.then(|| {
let mut tight_bounds = PlotBounds::NOTHING;
for item in &self.items {
let item_bounds = item.bounds();
tight_bounds.merge_x(&item_bounds);
tight_bounds.merge_y(&item_bounds);
}
tight_bounds
});
for step in steps {
let value_main = step.value;
if let Some(clamp_range) = clamp_range {
match axis {
Axis::X => {
if !clamp_range.range_x().contains(&value_main) {
continue;
};
}
Axis::Y => {
if !clamp_range.range_y().contains(&value_main) {
continue;
};
}
}
}
let value = match axis {
Axis::X => PlotPoint::new(value_main, value_cross),
Axis::Y => PlotPoint::new(value_cross, value_main),
};
let pos_in_gui = transform.position_from_point(&value);
let spacing_in_points = (transform.dpos_dvalue()[iaxis] * step.step_size).abs() as f32;
if spacing_in_points > MIN_LINE_SPACING_IN_POINTS as f32 {
let line_strength = remap_clamp(
spacing_in_points,
MIN_LINE_SPACING_IN_POINTS as f32..=300.0,
0.0..=1.0,
);
let line_color = color_from_strength(ui, line_strength);
let mut p0 = pos_in_gui;
let mut p1 = pos_in_gui;
p0[1 - iaxis] = transform.frame().min[1 - iaxis];
p1[1 - iaxis] = transform.frame().max[1 - iaxis];
if let Some(clamp_range) = clamp_range {
match axis {
Axis::X => {
p0.y = transform.position_from_point_y(clamp_range.min[1]);
p1.y = transform.position_from_point_y(clamp_range.max[1]);
}
Axis::Y => {
p0.x = transform.position_from_point_x(clamp_range.min[0]);
p1.x = transform.position_from_point_x(clamp_range.max[0]);
}
}
}
if self.sharp_grid_lines {
p0 = ui.painter().round_pos_to_pixels(p0);
p1 = ui.painter().round_pos_to_pixels(p1);
}
shapes.push((
Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)),
line_strength,
));
}
}
}
fn hover(&self, ui: &Ui, pointer: Pos2, shapes: &mut Vec<Shape>) -> Vec<Cursor> {
let Self {
transform,
show_x,
show_y,
label_formatter,
items,
..
} = self;
if !show_x && !show_y {
return Vec::new();
}
let interact_radius_sq: f32 = (16.0f32).powi(2);
let candidates = items.iter().filter_map(|item| {
let item = &**item;
let closest = item.find_closest(pointer, transform);
Some(item).zip(closest)
});
let closest = candidates
.min_by_key(|(_, elem)| elem.dist_sq.ord())
.filter(|(_, elem)| elem.dist_sq <= interact_radius_sq);
let mut cursors = Vec::new();
let plot = items::PlotConfig {
ui,
transform,
show_x: *show_x,
show_y: *show_y,
};
if let Some((item, elem)) = closest {
item.on_hover(elem, shapes, &mut cursors, &plot, label_formatter);
} else {
let value = transform.value_from_position(pointer);
items::rulers_at_value(
pointer,
value,
"",
&plot,
shapes,
&mut cursors,
label_formatter,
);
}
cursors
}
}
fn next_power(value: f64, base: f64) -> f64 {
assert_ne!(value, 0.0); base.powi(value.abs().log(base).ceil() as i32)
}
fn generate_marks(step_sizes: [f64; 3], bounds: (f64, f64)) -> Vec<GridMark> {
let mut steps = vec![];
fill_marks_between(&mut steps, step_sizes[0], bounds);
fill_marks_between(&mut steps, step_sizes[1], bounds);
fill_marks_between(&mut steps, step_sizes[2], bounds);
steps
}
fn fill_marks_between(out: &mut Vec<GridMark>, step_size: f64, (min, max): (f64, f64)) {
assert!(max > min);
let first = (min / step_size).ceil() as i64;
let last = (max / step_size).ceil() as i64;
let marks_iter = (first..last).map(|i| {
let value = (i as f64) * step_size;
GridMark { value, step_size }
});
out.extend(marks_iter);
}
pub fn format_number(number: f64, num_decimals: usize) -> String {
let is_integral = number as i64 as f64 == number;
if is_integral {
format!("{number:.0}")
} else {
format!("{:.*}", num_decimals.at_least(1), number)
}
}
pub fn color_from_strength(ui: &Ui, strength: f32) -> Color32 {
let bg = ui.visuals().extreme_bg_color;
let fg = ui.visuals().widgets.open.fg_stroke.color;
let mix = 0.5 * strength.sqrt();
Color32::from_rgb(
lerp((bg.r() as f32)..=(fg.r() as f32), mix) as u8,
lerp((bg.g() as f32)..=(fg.g() as f32), mix) as u8,
lerp((bg.b() as f32)..=(fg.b() as f32), mix) as u8,
)
}