#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
mod axis;
mod items;
mod legend;
mod memory;
mod plot_ui;
mod transform;
use std::{cmp::Ordering, ops::RangeInclusive, sync::Arc};
use ahash::HashMap;
use egui::{
epaint, remap_clamp, vec2, Align2, Color32, CursorIcon, Id, Layout, NumExt, PointerButton,
Pos2, Rangef, Rect, Response, Rounding, Sense, Shape, Stroke, TextStyle, Ui, Vec2, Vec2b,
WidgetText,
};
use emath::Float as _;
pub use crate::{
axis::{Axis, AxisHints, HPlacement, Placement, VPlacement},
items::{
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, ClosestElem, HLine, Line, LineStyle,
MarkerShape, Orientation, PlotConfig, PlotGeometry, PlotImage, PlotItem, PlotPoint,
PlotPoints, Points, Polygon, Text, VLine,
},
legend::{Corner, Legend},
memory::PlotMemory,
plot_ui::PlotUi,
transform::{PlotBounds, PlotTransform},
};
use axis::AxisWidget;
use items::{horizontal_line, rulers_color, vertical_line};
use legend::LegendWidget;
type LabelFormatterFn<'a> = dyn Fn(&str, &PlotPoint) -> String + 'a;
pub type LabelFormatter<'a> = Option<Box<LabelFormatterFn<'a>>>;
type GridSpacerFn<'a> = dyn Fn(GridInput) -> Vec<GridMark> + 'a;
type GridSpacer<'a> = Box<GridSpacerFn<'a>>;
type CoordinatesFormatterFn<'a> = dyn Fn(&PlotPoint, &PlotBounds) -> String + 'a;
pub struct CoordinatesFormatter<'a> {
function: Box<CoordinatesFormatterFn<'a>>,
}
impl<'a> CoordinatesFormatter<'a> {
pub fn new(function: impl Fn(&PlotPoint, &PlotBounds) -> String + 'a) -> 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)
}
}
#[derive(Copy, Clone, PartialEq)]
pub 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,
auto_bounds: Vec2b,
}
#[derive(Default, Clone)]
struct BoundsLinkGroups(HashMap<Id, LinkedBounds>);
pub struct PlotResponse<R> {
pub inner: R,
pub response: Response,
pub transform: PlotTransform,
pub hovered_plot_item: Option<Id>,
}
pub struct Plot<'a> {
id_source: Id,
id: Option<Id>,
center_axis: Vec2b,
allow_zoom: Vec2b,
allow_drag: Vec2b,
allow_scroll: Vec2b,
allow_double_click_reset: bool,
allow_boxed_zoom: bool,
default_auto_bounds: Vec2b,
min_auto_bounds: PlotBounds,
margin_fraction: Vec2,
boxed_zoom_pointer_button: PointerButton,
linked_axes: Option<(Id, Vec2b)>,
linked_cursors: Option<(Id, Vec2b)>,
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<'a>,
coordinates_formatter: Option<(Corner, CoordinatesFormatter<'a>)>,
x_axes: Vec<AxisHints<'a>>, y_axes: Vec<AxisHints<'a>>, legend_config: Option<Legend>,
show_background: bool,
show_axes: Vec2b,
show_grid: Vec2b,
grid_spacing: Rangef,
grid_spacers: [GridSpacer<'a>; 2],
sharp_grid_lines: bool,
clamp_grid: bool,
sense: Sense,
}
impl<'a> Plot<'a> {
pub fn new(id_source: impl std::hash::Hash) -> Self {
Self {
id_source: Id::new(id_source),
id: None,
center_axis: false.into(),
allow_zoom: true.into(),
allow_drag: true.into(),
allow_scroll: true.into(),
allow_double_click_reset: true,
allow_boxed_zoom: true,
default_auto_bounds: true.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![AxisHints::new(Axis::X)],
y_axes: vec![AxisHints::new(Axis::Y)],
legend_config: None,
show_background: true,
show_axes: true.into(),
show_grid: true.into(),
grid_spacing: Rangef::new(8.0, 300.0),
grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)],
sharp_grid_lines: true,
clamp_grid: false,
sense: egui::Sense::click_and_drag(),
}
}
#[inline]
pub fn id(mut self, id: Id) -> Self {
self.id = Some(id);
self
}
#[inline]
pub fn data_aspect(mut self, data_aspect: f32) -> Self {
self.data_aspect = Some(data_aspect);
self
}
#[inline]
pub fn view_aspect(mut self, view_aspect: f32) -> Self {
self.view_aspect = Some(view_aspect);
self
}
#[inline]
pub fn width(mut self, width: f32) -> Self {
self.min_size.x = width;
self.width = Some(width);
self
}
#[inline]
pub fn height(mut self, height: f32) -> Self {
self.min_size.y = height;
self.height = Some(height);
self
}
#[inline]
pub fn min_size(mut self, min_size: Vec2) -> Self {
self.min_size = min_size;
self
}
#[inline]
pub fn show_x(mut self, show_x: bool) -> Self {
self.show_x = show_x;
self
}
#[inline]
pub fn show_y(mut self, show_y: bool) -> Self {
self.show_y = show_y;
self
}
#[inline]
pub fn center_x_axis(mut self, on: bool) -> Self {
self.center_axis.x = on;
self
}
#[inline]
pub fn center_y_axis(mut self, on: bool) -> Self {
self.center_axis.y = on;
self
}
#[inline]
pub fn allow_zoom<T>(mut self, on: T) -> Self
where
T: Into<Vec2b>,
{
self.allow_zoom = on.into();
self
}
#[inline]
pub fn allow_scroll<T>(mut self, on: T) -> Self
where
T: Into<Vec2b>,
{
self.allow_scroll = on.into();
self
}
#[inline]
pub fn allow_double_click_reset(mut self, on: bool) -> Self {
self.allow_double_click_reset = on;
self
}
#[inline]
pub fn set_margin_fraction(mut self, margin_fraction: Vec2) -> Self {
self.margin_fraction = margin_fraction;
self
}
#[inline]
pub fn allow_boxed_zoom(mut self, on: bool) -> Self {
self.allow_boxed_zoom = on;
self
}
#[inline]
pub fn boxed_zoom_pointer_button(mut self, boxed_zoom_pointer_button: PointerButton) -> Self {
self.boxed_zoom_pointer_button = boxed_zoom_pointer_button;
self
}
#[inline]
pub fn allow_drag<T>(mut self, on: T) -> Self
where
T: Into<Vec2b>,
{
self.allow_drag = on.into();
self
}
pub fn label_formatter(
mut self,
label_formatter: impl Fn(&str, &PlotPoint) -> String + 'a,
) -> Self {
self.label_formatter = Some(Box::new(label_formatter));
self
}
pub fn coordinates_formatter(
mut self,
position: Corner,
formatter: CoordinatesFormatter<'a>,
) -> Self {
self.coordinates_formatter = Some((position, formatter));
self
}
#[inline]
pub fn x_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec<GridMark> + 'a) -> Self {
self.grid_spacers[0] = Box::new(spacer);
self
}
#[inline]
pub fn y_grid_spacer(mut self, spacer: impl Fn(GridInput) -> Vec<GridMark> + 'a) -> Self {
self.grid_spacers[1] = Box::new(spacer);
self
}
#[inline]
pub fn grid_spacing(mut self, grid_spacing: impl Into<Rangef>) -> Self {
self.grid_spacing = grid_spacing.into();
self
}
#[inline]
pub fn clamp_grid(mut self, clamp_grid: bool) -> Self {
self.clamp_grid = clamp_grid;
self
}
#[inline]
pub fn sense(mut self, sense: Sense) -> Self {
self.sense = sense;
self
}
#[inline]
pub fn include_x(mut self, x: impl Into<f64>) -> Self {
self.min_auto_bounds.extend_with_x(x.into());
self
}
#[inline]
pub fn include_y(mut self, y: impl Into<f64>) -> Self {
self.min_auto_bounds.extend_with_y(y.into());
self
}
#[inline]
pub fn auto_bounds(mut self, auto_bounds: Vec2b) -> Self {
self.default_auto_bounds = auto_bounds;
self
}
#[deprecated = "Use `auto_bounds` instead"]
#[inline]
pub fn auto_bounds_x(mut self) -> Self {
self.default_auto_bounds.x = true;
self
}
#[deprecated = "Use `auto_bounds` instead"]
#[inline]
pub fn auto_bounds_y(mut self) -> Self {
self.default_auto_bounds.y = true;
self
}
#[inline]
pub fn legend(mut self, legend: Legend) -> Self {
self.legend_config = Some(legend);
self
}
#[inline]
pub fn show_background(mut self, show: bool) -> Self {
self.show_background = show;
self
}
#[inline]
pub fn show_axes(mut self, show: impl Into<Vec2b>) -> Self {
self.show_axes = show.into();
self
}
#[inline]
pub fn show_grid(mut self, show: impl Into<Vec2b>) -> Self {
self.show_grid = show.into();
self
}
#[allow(clippy::fn_params_excessive_bools)] #[inline]
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(),
Vec2b {
x: link_x,
y: link_y,
},
));
self
}
#[allow(clippy::fn_params_excessive_bools)] #[inline]
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(),
Vec2b {
x: link_x,
y: link_y,
},
));
self
}
#[inline]
pub fn sharp_grid_lines(mut self, enabled: bool) -> Self {
self.sharp_grid_lines = enabled;
self
}
#[inline]
pub fn reset(mut self) -> Self {
self.reset = true;
self
}
#[inline]
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
}
#[inline]
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
}
#[inline]
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
}
#[inline]
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(GridMark, &RangeInclusive<f64>) -> String + 'a,
) -> 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(GridMark, &RangeInclusive<f64>) -> String + 'a,
) -> Self {
if let Some(main) = self.y_axes.first_mut() {
main.formatter = Arc::new(fmt);
}
self
}
#[inline]
pub fn y_axis_min_width(mut self, min_width: f32) -> Self {
if let Some(main) = self.y_axes.first_mut() {
main.min_thickness = min_width;
}
self
}
#[inline]
#[deprecated = "Use `y_axis_min_width` instead"]
pub fn y_axis_width(self, digits: usize) -> Self {
self.y_axis_min_width(12.0 * digits as f32)
}
#[inline]
pub fn custom_x_axes(mut self, hints: Vec<AxisHints<'a>>) -> Self {
self.x_axes = hints;
self
}
#[inline]
pub fn custom_y_axes(mut self, hints: Vec<AxisHints<'a>>) -> Self {
self.y_axes = hints;
self
}
pub fn show<R>(
self,
ui: &mut Ui,
build_fn: impl FnOnce(&mut PlotUi) -> R + 'a,
) -> PlotResponse<R> {
self.show_dyn(ui, Box::new(build_fn))
}
#[allow(clippy::too_many_lines)] #[allow(clippy::type_complexity)] fn show_dyn<R>(
self,
ui: &mut Ui,
build_fn: Box<dyn FnOnce(&mut PlotUi) -> R + 'a>,
) -> PlotResponse<R> {
let Self {
id_source,
id,
center_axis,
allow_zoom,
allow_drag,
allow_scroll,
allow_double_click_reset,
allow_boxed_zoom,
boxed_zoom_pointer_button,
default_auto_bounds,
min_auto_bounds,
margin_fraction,
width,
height,
mut 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,
grid_spacing,
linked_axes,
linked_cursors,
clamp_grid,
grid_spacers,
sharp_grid_lines,
sense,
} = self;
let allow_zoom = allow_zoom.and(ui.is_enabled());
let allow_drag = allow_drag.and(ui.is_enabled());
let allow_scroll = allow_scroll.and(ui.is_enabled());
let pos = ui.available_rect_before_wrap().min;
min_size.x = min_size.x.at_least(1.0);
min_size.y = min_size.y.at_least(1.0);
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 plot_id = id.unwrap_or_else(|| ui.make_persistent_id(id_source));
let ([x_axis_widgets, y_axis_widgets], plot_rect) = axis_widgets(
PlotMemory::load(ui.ctx(), plot_id).as_ref(), show_axes,
complete_rect,
[&x_axes, &y_axes],
);
let response = ui.allocate_rect(plot_rect, sense);
ui.ctx().check_for_id_clash(plot_id, plot_rect, "Plot");
let mut mem = if reset {
if let Some((name, _)) = linked_axes.as_ref() {
ui.data_mut(|data| {
let link_groups: &mut BoundsLinkGroups = 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 {
auto_bounds: default_auto_bounds,
hovered_legend_item: None,
hidden_items: Default::default(),
transform: PlotTransform::new(plot_rect, min_auto_bounds, center_axis.x, center_axis.y),
last_click_pos_for_zoom: None,
x_axis_thickness: Default::default(),
y_axis_thickness: Default::default(),
});
let last_plot_transform = mem.transform;
let mut plot_ui = PlotUi {
ctx: ui.ctx().clone(),
items: Vec::new(),
next_auto_color_idx: 0,
last_plot_transform,
last_auto_bounds: mem.auto_bounds,
response,
bounds_modifications: Vec::new(),
};
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(plot_rect)
.add(epaint::RectShape::new(
plot_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(plot_rect, config, &items, &mem.hidden_items));
if mem.hovered_legend_item.is_some() {
show_x = false;
show_y = false;
}
items.retain(|item| !mem.hidden_items.contains(item.name()));
if let Some(hovered_name) = &mem.hovered_legend_item {
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.data_mut(|data| {
let frames: &mut CursorLinkGroups = 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.data_mut(|data| {
let link_groups: &mut BoundsLinkGroups = 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);
mem.auto_bounds.x = linked_bounds.auto_bounds.x;
}
if axes.y {
bounds.set_y(&linked_bounds.bounds);
mem.auto_bounds.y = linked_bounds.auto_bounds.y;
}
};
});
};
if allow_double_click_reset && response.double_clicked() {
mem.auto_bounds = true.into();
}
for modification in bounds_modifications {
match modification {
BoundsModification::Set(new_bounds) => {
bounds = new_bounds;
mem.auto_bounds = false.into();
}
BoundsModification::Translate(delta) => {
let delta = (delta.x as f64, delta.y as f64);
bounds.translate(delta);
mem.auto_bounds = false.into();
}
BoundsModification::AutoBounds(new_auto_bounds) => {
mem.auto_bounds = new_auto_bounds;
}
BoundsModification::Zoom(zoom_factor, center) => {
bounds.zoom(zoom_factor, center);
mem.auto_bounds = false.into();
}
}
}
if mem.auto_bounds.x {
bounds.set_x(&min_auto_bounds);
}
if mem.auto_bounds.y {
bounds.set_y(&min_auto_bounds);
}
let auto_x = mem.auto_bounds.x && (!min_auto_bounds.is_valid_x() || default_auto_bounds.x);
let auto_y = mem.auto_bounds.y && (!min_auto_bounds.is_valid_y() || default_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);
}
}
mem.transform = PlotTransform::new(plot_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;
mem.transform.set_aspect_by_changing_axis(
data_aspect as f64,
if change_x { Axis::X } else { Axis::Y },
);
} else if default_auto_bounds.any() {
mem.transform.set_aspect_by_expanding(data_aspect as f64);
} else {
mem.transform
.set_aspect_by_changing_axis(data_aspect as f64, Axis::Y);
}
}
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;
}
mem.transform
.translate_bounds((delta.x as f64, delta.y as f64));
mem.auto_bounds = mem.auto_bounds.and(!allow_drag);
}
let mut boxed_zoom_rect = None;
if allow_boxed_zoom {
if response.drag_started() && response.dragged_by(boxed_zoom_pointer_button) {
mem.last_click_pos_for_zoom = response.hover_pos();
}
let box_start_pos = mem.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_button) {
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_stopped() {
let box_start_pos = mem.transform.value_from_position(box_start_pos);
let box_end_pos = mem.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() {
mem.transform.set_bounds(new_bounds);
mem.auto_bounds = false.into();
}
mem.last_click_pos_for_zoom = None;
}
}
}
if let (true, Some(hover_pos)) = (
response.contains_pointer,
ui.input(|i| i.pointer.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) {
mem.transform.zoom(zoom_factor, hover_pos);
mem.auto_bounds = mem.auto_bounds.and(!allow_zoom);
}
}
if allow_scroll.any() {
let mut scroll_delta = ui.input(|i| i.smooth_scroll_delta);
if !allow_scroll.x {
scroll_delta.x = 0.0;
}
if !allow_scroll.y {
scroll_delta.y = 0.0;
}
if scroll_delta != Vec2::ZERO {
mem.transform
.translate_bounds((-scroll_delta.x as f64, -scroll_delta.y as f64));
mem.auto_bounds = false.into();
}
}
}
let bounds = mem.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: mem.transform.dvalue_dpos()[0].abs() * grid_spacing.min as f64,
};
(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: mem.transform.dvalue_dpos()[1].abs() * grid_spacing.min as f64,
};
(grid_spacers[1])(input)
});
for (i, mut widget) in x_axis_widgets.into_iter().enumerate() {
widget.range = x_axis_range.clone();
widget.transform = Some(mem.transform);
widget.steps = x_steps.clone();
let (_response, thickness) = widget.ui(ui, Axis::X);
mem.x_axis_thickness.insert(i, thickness);
}
for (i, mut widget) in y_axis_widgets.into_iter().enumerate() {
widget.range = y_axis_range.clone();
widget.transform = Some(mem.transform);
widget.steps = y_steps.clone();
let (_response, thickness) = widget.ui(ui, Axis::Y);
mem.y_axis_thickness.insert(i, thickness);
}
for item in &mut items {
item.initialize(mem.transform.bounds().range_x());
}
let prepared = PreparedPlot {
items,
show_x,
show_y,
label_formatter,
coordinates_formatter,
show_grid,
grid_spacing,
transform: mem.transform,
draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.1.x),
draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.1.y),
draw_cursors,
grid_spacers,
sharp_grid_lines,
clamp_grid,
};
let (plot_cursors, hovered_plot_item) = prepared.ui(ui, &response);
if let Some(boxed_zoom_rect) = boxed_zoom_rect {
ui.painter()
.with_clip_rect(plot_rect)
.add(boxed_zoom_rect.0);
ui.painter()
.with_clip_rect(plot_rect)
.add(boxed_zoom_rect.1);
}
if let Some(mut legend) = legend {
ui.add(&mut legend);
mem.hidden_items = legend.hidden_items();
mem.hovered_legend_item = legend.hovered_item_name();
}
if let Some((id, _)) = linked_cursors.as_ref() {
ui.data_mut(|data| {
let frames: &mut CursorLinkGroups = 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.data_mut(|data| {
let link_groups: &mut BoundsLinkGroups = data.get_temp_mut_or_default(Id::NULL);
link_groups.0.insert(
*id,
LinkedBounds {
bounds: *mem.transform.bounds(),
auto_bounds: mem.auto_bounds,
},
);
});
}
let transform = mem.transform;
mem.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,
hovered_plot_item,
}
}
}
fn axis_widgets<'a>(
mem: Option<&PlotMemory>,
show_axes: Vec2b,
complete_rect: Rect,
[x_axes, y_axes]: [&'a [AxisHints<'a>]; 2],
) -> ([Vec<AxisWidget<'a>>; 2], Rect) {
let mut x_axis_widgets = Vec::<AxisWidget<'_>>::new();
let mut y_axis_widgets = Vec::<AxisWidget<'_>>::new();
let mut rect_left = complete_rect;
if show_axes.x {
let initial_x_range = complete_rect.x_range();
for (i, cfg) in x_axes.iter().enumerate().rev() {
let mut height = cfg.thickness(Axis::X);
if let Some(mem) = mem {
height = height.max(mem.x_axis_thickness.get(&i).copied().unwrap_or_default());
}
let rect = match VPlacement::from(cfg.placement) {
VPlacement::Bottom => {
let bottom = rect_left.bottom();
*rect_left.bottom_mut() -= height;
let top = rect_left.bottom();
Rect::from_x_y_ranges(initial_x_range, top..=bottom)
}
VPlacement::Top => {
let top = rect_left.top();
*rect_left.top_mut() += height;
let bottom = rect_left.top();
Rect::from_x_y_ranges(initial_x_range, top..=bottom)
}
};
x_axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
}
}
if show_axes.y {
let plot_y_range = rect_left.y_range();
for (i, cfg) in y_axes.iter().enumerate().rev() {
let mut width = cfg.thickness(Axis::Y);
if let Some(mem) = mem {
width = width.max(mem.y_axis_thickness.get(&i).copied().unwrap_or_default());
}
let rect = match HPlacement::from(cfg.placement) {
HPlacement::Left => {
let left = rect_left.left();
*rect_left.left_mut() += width;
let right = rect_left.left();
Rect::from_x_y_ranges(left..=right, plot_y_range)
}
HPlacement::Right => {
let right = rect_left.right();
*rect_left.right_mut() -= width;
let left = rect_left.right();
Rect::from_x_y_ranges(left..=right, plot_y_range)
}
};
y_axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
}
}
let mut plot_rect = rect_left;
if plot_rect.width() <= 0.0 || plot_rect.height() <= 0.0 {
y_axis_widgets.clear();
x_axis_widgets.clear();
plot_rect = complete_rect;
}
for widget in &mut x_axis_widgets {
widget.rect = Rect::from_x_y_ranges(plot_rect.x_range(), widget.rect.y_range());
}
([x_axis_widgets, y_axis_widgets], plot_rect)
}
enum BoundsModification {
Set(PlotBounds),
Translate(Vec2),
AutoBounds(Vec2b),
Zoom(Vec2, PlotPoint),
}
pub struct GridInput {
pub bounds: (f64, f64),
pub base_step_size: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GridMark {
pub value: f64,
pub step_size: f64,
}
pub fn log_grid_spacer(log_base: i64) -> GridSpacer<'static> {
let log_base = log_base as f64;
let step_sizes = move |input: GridInput| -> Vec<GridMark> {
if input.base_step_size.abs() < f64::EPSILON {
return Vec::new();
}
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<'a>(spacer: impl Fn(GridInput) -> [f64; 3] + 'a) -> GridSpacer<'a> {
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<'a> {
items: Vec<Box<dyn PlotItem>>,
show_x: bool,
show_y: bool,
label_formatter: LabelFormatter<'a>,
coordinates_formatter: Option<(Corner, CoordinatesFormatter<'a>)>,
transform: PlotTransform,
show_grid: Vec2b,
grid_spacing: Rangef,
grid_spacers: [GridSpacer<'a>; 2],
draw_cursor_x: bool,
draw_cursor_y: bool,
draw_cursors: Vec<Cursor>,
sharp_grid_lines: bool,
clamp_grid: bool,
}
impl<'a> PreparedPlot<'a> {
fn ui(self, ui: &mut Ui, response: &Response) -> (Vec<Cursor>, Option<Id>) {
let mut axes_shapes = Vec::new();
if self.show_grid.x {
self.paint_grid(ui, &mut axes_shapes, Axis::X, self.grid_spacing);
}
if self.show_grid.y {
self.paint_grid(ui, &mut axes_shapes, Axis::Y, self.grid_spacing);
}
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.new_child(
egui::UiBuilder::new()
.max_rect(*transform.frame())
.layout(Layout::default()),
);
plot_ui.set_clip_rect(transform.frame().intersect(ui.clip_rect()));
for item in &self.items {
item.shapes(&plot_ui, transform, &mut shapes);
}
let hover_pos = response.hover_pos();
let (cursors, hovered_item_id) = if let Some(pointer) = hover_pos {
self.hover(ui, pointer, &mut shapes)
} else {
(Vec::new(), None)
};
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, hovered_item_id)
}
fn paint_grid(&self, ui: &Ui, shapes: &mut Vec<(Shape, f32)>, axis: Axis, fade_range: Rangef) {
#![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].abs() * fade_range.min as f64,
};
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 <= fade_range.min {
continue; }
let line_strength = remap_clamp(spacing_in_points, fade_range, 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>, Option<Id>) {
let Self {
transform,
show_x,
show_y,
label_formatter,
items,
..
} = self;
if !show_x && !show_y {
return (Vec::new(), None);
}
let interact_radius_sq = ui.style().interaction.interact_radius.powi(2);
let candidates = items
.iter()
.filter(|entry| entry.allow_hover())
.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 plot = items::PlotConfig {
ui,
transform,
show_x: *show_x,
show_y: *show_y,
};
let mut cursors = Vec::new();
let hovered_plot_item_id = if let Some((item, elem)) = closest {
item.on_hover(elem, shapes, &mut cursors, &plot, label_formatter);
item.id()
} else {
let value = transform.value_from_position(pointer);
items::rulers_at_value(
pointer,
value,
"",
&plot,
shapes,
&mut cursors,
label_formatter,
);
None
};
(cursors, hovered_plot_item_id)
}
}
fn next_power(value: f64, base: f64) -> f64 {
debug_assert_ne!(value, 0.0, "Bad input"); 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.sort_by(|a, b| cmp_f64(a.value, b.value));
let min_step = step_sizes.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let eps = 0.1 * min_step;
let mut deduplicated: Vec<GridMark> = Vec::with_capacity(steps.len());
for step in steps {
if let Some(last) = deduplicated.last_mut() {
if (last.value - step.value).abs() < eps {
if last.step_size < step.step_size {
*last = step;
}
continue;
}
}
deduplicated.push(step);
}
deduplicated
}
#[test]
fn test_generate_marks() {
fn approx_eq(a: &GridMark, b: &GridMark) -> bool {
(a.value - b.value).abs() < 1e-10 && a.step_size == b.step_size
}
let gm = |value, step_size| GridMark { value, step_size };
let marks = generate_marks([0.01, 0.1, 1.0], (2.855, 3.015));
let expected = vec![
gm(2.86, 0.01),
gm(2.87, 0.01),
gm(2.88, 0.01),
gm(2.89, 0.01),
gm(2.90, 0.1),
gm(2.91, 0.01),
gm(2.92, 0.01),
gm(2.93, 0.01),
gm(2.94, 0.01),
gm(2.95, 0.01),
gm(2.96, 0.01),
gm(2.97, 0.01),
gm(2.98, 0.01),
gm(2.99, 0.01),
gm(3.00, 1.),
gm(3.01, 0.01),
];
let mut problem = None;
if marks.len() != expected.len() {
problem = Some(format!(
"Different lengths: got {}, expected {}",
marks.len(),
expected.len()
));
}
for (i, (a, b)) in marks.iter().zip(&expected).enumerate() {
if !approx_eq(a, b) {
problem = Some(format!("Mismatch at index {i}: {a:?} != {b:?}"));
break;
}
}
if let Some(problem) = problem {
panic!("Test failed: {problem}. Got: {marks:#?}, expected: {expected:#?}");
}
}
fn cmp_f64(a: f64, b: f64) -> Ordering {
match a.partial_cmp(&b) {
Some(ord) => ord,
None => a.is_nan().cmp(&b.is_nan()),
}
}
fn fill_marks_between(out: &mut Vec<GridMark>, step_size: f64, (min, max): (f64, f64)) {
debug_assert!(min <= max, "Bad plot bounds: min: {min}, max: {max}");
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 base_color = ui.visuals().text_color();
base_color.gamma_multiply(strength.sqrt())
}