Expand plot axes thickness to fit their labels (#3921)

Expand the plot axis thickness as the contained plot axis labels get
wider.

This fixes a problem where the plot labels would otherwise get clipped.


![plot-axis-expansion](https://github.com/emilk/egui/assets/1148717/4500a26e-4a11-401d-9e8e-2d98d02ef3b7)
This commit is contained in:
Emil Ernerfeldt 2024-01-30 12:45:27 +01:00 committed by GitHub
parent 01597fe1a1
commit 527f4bfdf6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 332 additions and 268 deletions

View File

@ -6,7 +6,7 @@ use crate::{
Color32, Context, FontId, Color32, Context, FontId,
}; };
use epaint::{ use epaint::{
text::{Fonts, Galley}, text::{Fonts, Galley, LayoutJob},
CircleShape, ClippedShape, RectShape, Rounding, Shape, Stroke, CircleShape, ClippedShape, RectShape, Rounding, Shape, Stroke,
}; };
@ -436,9 +436,18 @@ impl Painter {
self.fonts(|f| f.layout(text, font_id, color, f32::INFINITY)) self.fonts(|f| f.layout(text, font_id, color, f32::INFINITY))
} }
/// Lay out this text layut job in a galley.
///
/// Paint the results with [`Self::galley`].
#[inline]
#[must_use]
pub fn layout_job(&self, layout_job: LayoutJob) -> Arc<Galley> {
self.fonts(|f| f.layout_job(layout_job))
}
/// Paint text that has already been laid out in a [`Galley`]. /// Paint text that has already been laid out in a [`Galley`].
/// ///
/// You can create the [`Galley`] with [`Self::layout`]. /// You can create the [`Galley`] with [`Self::layout`] or [`Self::layout_job`].
/// ///
/// Any uncolored parts of the [`Galley`] (using [`Color32::PLACEHOLDER`]) will be replaced with the given color. /// Any uncolored parts of the [`Galley`] (using [`Color32::PLACEHOLDER`]) will be replaced with the given color.
/// ///

View File

@ -1,9 +1,9 @@
use std::{fmt::Debug, ops::RangeInclusive, sync::Arc}; use std::{fmt::Debug, ops::RangeInclusive, sync::Arc};
use egui::{ use egui::{
emath::{remap_clamp, round_to_decimals}, emath::{remap_clamp, round_to_decimals, Rot2},
epaint::TextShape, epaint::TextShape,
Pos2, Rangef, Rect, Response, Sense, Shape, TextStyle, Ui, WidgetText, Pos2, Rangef, Rect, Response, Sense, TextStyle, Ui, Vec2, WidgetText,
}; };
use super::{transform::PlotTransform, GridMark}; use super::{transform::PlotTransform, GridMark};
@ -64,6 +64,16 @@ impl From<HPlacement> for Placement {
} }
} }
impl From<Placement> for HPlacement {
#[inline]
fn from(placement: Placement) -> Self {
match placement {
Placement::LeftBottom => Self::Left,
Placement::RightTop => Self::Right,
}
}
}
impl From<VPlacement> for Placement { impl From<VPlacement> for Placement {
#[inline] #[inline]
fn from(placement: VPlacement) -> Self { fn from(placement: VPlacement) -> Self {
@ -74,6 +84,16 @@ impl From<VPlacement> for Placement {
} }
} }
impl From<Placement> for VPlacement {
#[inline]
fn from(placement: Placement) -> Self {
match placement {
Placement::LeftBottom => Self::Bottom,
Placement::RightTop => Self::Top,
}
}
}
/// Axis configuration. /// Axis configuration.
/// ///
/// Used to configure axis label and ticks. /// Used to configure axis label and ticks.
@ -211,16 +231,18 @@ impl AxisHints {
#[derive(Clone)] #[derive(Clone)]
pub(super) struct AxisWidget { pub(super) struct AxisWidget {
pub(super) range: RangeInclusive<f64>, pub range: RangeInclusive<f64>,
pub(super) hints: AxisHints, pub hints: AxisHints,
pub(super) rect: Rect,
pub(super) transform: Option<PlotTransform>, /// The region where we draw the axis labels.
pub(super) steps: Arc<Vec<GridMark>>, pub rect: Rect,
pub transform: Option<PlotTransform>,
pub steps: Arc<Vec<GridMark>>,
} }
impl AxisWidget { impl AxisWidget {
/// if `rect` as width or height == 0, is will be automatically calculated from ticks and text. /// if `rect` as width or height == 0, is will be automatically calculated from ticks and text.
pub(super) fn new(hints: AxisHints, rect: Rect) -> Self { pub fn new(hints: AxisHints, rect: Rect) -> Self {
Self { Self {
range: (0.0..=0.0), range: (0.0..=0.0),
hints, hints,
@ -230,70 +252,76 @@ impl AxisWidget {
} }
} }
pub fn ui(self, ui: &mut Ui, axis: Axis) -> Response { /// Returns the actual thickness of the axis.
pub fn ui(self, ui: &mut Ui, axis: Axis) -> (Response, f32) {
let response = ui.allocate_rect(self.rect, Sense::hover()); let response = ui.allocate_rect(self.rect, Sense::hover());
if !ui.is_rect_visible(response.rect) { if !ui.is_rect_visible(response.rect) {
return response; return (response, 0.0);
} }
let visuals = ui.style().visuals.clone(); let visuals = ui.style().visuals.clone();
let text = self.hints.label;
let galley = text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Body);
let text_color = visuals
.override_text_color
.unwrap_or_else(|| ui.visuals().text_color());
let angle: f32 = match axis {
Axis::X => 0.0,
Axis::Y => -std::f32::consts::TAU * 0.25,
};
// select text_pos and angle depending on placement and orientation of widget
let text_pos = match self.hints.placement {
Placement::LeftBottom => match axis {
Axis::X => {
let pos = response.rect.center_bottom();
Pos2 {
x: pos.x - galley.size().x / 2.0,
y: pos.y - galley.size().y * 1.25,
}
}
Axis::Y => {
let pos = response.rect.left_center();
Pos2 {
x: pos.x,
y: pos.y + galley.size().x / 2.0,
}
}
},
Placement::RightTop => match axis {
Axis::X => {
let pos = response.rect.center_top();
Pos2 {
x: pos.x - galley.size().x / 2.0,
y: pos.y + galley.size().y * 0.25,
}
}
Axis::Y => {
let pos = response.rect.right_center();
Pos2 {
x: pos.x - galley.size().y * 1.5,
y: pos.y + galley.size().x / 2.0,
}
}
},
};
ui.painter() {
.add(TextShape::new(text_pos, galley, text_color).with_angle(angle)); let text = self.hints.label;
let galley = text.into_galley(ui, Some(false), f32::INFINITY, TextStyle::Body);
let text_color = visuals
.override_text_color
.unwrap_or_else(|| ui.visuals().text_color());
let angle: f32 = match axis {
Axis::X => 0.0,
Axis::Y => -std::f32::consts::TAU * 0.25,
};
// select text_pos and angle depending on placement and orientation of widget
let text_pos = match self.hints.placement {
Placement::LeftBottom => match axis {
Axis::X => {
let pos = response.rect.center_bottom();
Pos2 {
x: pos.x - galley.size().x / 2.0,
y: pos.y - galley.size().y * 1.25,
}
}
Axis::Y => {
let pos = response.rect.left_center();
Pos2 {
x: pos.x,
y: pos.y + galley.size().x / 2.0,
}
}
},
Placement::RightTop => match axis {
Axis::X => {
let pos = response.rect.center_top();
Pos2 {
x: pos.x - galley.size().x / 2.0,
y: pos.y + galley.size().y * 0.25,
}
}
Axis::Y => {
let pos = response.rect.right_center();
Pos2 {
x: pos.x - galley.size().y * 1.5,
y: pos.y + galley.size().x / 2.0,
}
}
},
};
ui.painter()
.add(TextShape::new(text_pos, galley, text_color).with_angle(angle));
}
// --- add ticks ---
let font_id = TextStyle::Body.resolve(ui.style()); let font_id = TextStyle::Body.resolve(ui.style());
let Some(transform) = self.transform else { let Some(transform) = self.transform else {
return response; return (response, 0.0);
}; };
let label_spacing = self.hints.label_spacing; let label_spacing = self.hints.label_spacing;
let mut thickness: f32 = 0.0;
// Add tick labels:
for step in self.steps.iter() { for step in self.steps.iter() {
let text = (self.hints.formatter)(*step, self.hints.digits, &self.range); let text = (self.hints.formatter)(*step, self.hints.digits, &self.range);
if !text.is_empty() { if !text.is_empty() {
@ -314,41 +342,61 @@ impl AxisWidget {
.layout_no_wrap(text, font_id.clone(), text_color); .layout_no_wrap(text, font_id.clone(), text_color);
if spacing_in_points < galley.size()[axis as usize] { if spacing_in_points < galley.size()[axis as usize] {
continue; // the galley won't fit continue; // the galley won't fit (likely too wide on the X axis).
} }
let text_pos = match axis { match axis {
Axis::X => { Axis::X => {
let y = match self.hints.placement { thickness = thickness.max(galley.size().y);
Placement::LeftBottom => self.rect.min.y,
Placement::RightTop => self.rect.max.y - galley.size().y,
};
let projected_point = super::PlotPoint::new(step.value, 0.0); let projected_point = super::PlotPoint::new(step.value, 0.0);
Pos2 { let center_x = transform.position_from_point(&projected_point).x;
x: transform.position_from_point(&projected_point).x let y = match VPlacement::from(self.hints.placement) {
- galley.size().x / 2.0, VPlacement::Bottom => self.rect.min.y,
y, VPlacement::Top => self.rect.max.y - galley.size().y,
} };
let pos = Pos2::new(center_x - galley.size().x / 2.0, y);
ui.painter().add(TextShape::new(pos, galley, text_color));
} }
Axis::Y => { Axis::Y => {
let x = match self.hints.placement { thickness = thickness.max(galley.size().x);
Placement::LeftBottom => self.rect.max.x - galley.size().x,
Placement::RightTop => self.rect.min.x,
};
let projected_point = super::PlotPoint::new(0.0, step.value); let projected_point = super::PlotPoint::new(0.0, step.value);
Pos2 { let center_y = transform.position_from_point(&projected_point).y;
x,
y: transform.position_from_point(&projected_point).y match HPlacement::from(self.hints.placement) {
- galley.size().y / 2.0, HPlacement::Left => {
} let angle = 0.0; // TODO: allow users to rotate text
if angle == 0.0 {
let x = self.rect.max.x - galley.size().x;
let pos = Pos2::new(x, center_y - galley.size().y / 2.0);
ui.painter().add(TextShape::new(pos, galley, text_color));
} else {
let right = Pos2::new(
self.rect.max.x,
center_y - galley.size().y / 2.0,
);
let width = galley.size().x;
let left =
right - Rot2::from_angle(angle) * Vec2::new(width, 0.0);
ui.painter().add(
TextShape::new(left, galley, text_color).with_angle(angle),
);
}
}
HPlacement::Right => {
let x = self.rect.min.x;
let pos = Pos2::new(x, center_y - galley.size().y / 2.0);
ui.painter().add(TextShape::new(pos, galley, text_color));
}
};
} }
}; };
ui.painter()
.add(Shape::galley(text_pos, galley, text_color));
} }
} }
response (response, thickness)
} }
} }

View File

@ -768,84 +768,29 @@ impl Plot {
.at_least(min_size.y); .at_least(min_size.y);
vec2(width, height) vec2(width, height)
}; };
// Determine complete rect of widget. // Determine complete rect of widget.
let complete_rect = Rect { let complete_rect = Rect {
min: pos, min: pos,
max: pos + size, max: pos + size,
}; };
// Next we want to create this layout.
// Indices are only examples.
//
// left right
// +---+---------x----------+ +
// | | X-axis 3 |
// | +--------------------+ top
// | | X-axis 2 |
// +-+-+--------------------+-+-+
// |y|y| |y|y|
// |-|-| |-|-|
// |A|A| |A|A|
// y|x|x| Plot Window |x|x|
// |i|i| |i|i|
// |s|s| |s|s|
// |1|0| |2|3|
// +-+-+--------------------+-+-+
// | X-axis 0 | |
// +--------------------+ | bottom
// | X-axis 1 | |
// + +--------------------+---+
//
let mut plot_rect: Rect = { let plot_id = id.unwrap_or_else(|| ui.make_persistent_id(id_source));
// Calcuclate the space needed for each axis labels.
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);
}
}
}
}
// determine plot rectangle let ([x_axis_widgets, y_axis_widgets], plot_rect) = axis_widgets(
margin.shrink_rect(complete_rect) PlotMemory::load(ui.ctx(), plot_id).as_ref(), // TODO: avoid loading plot memory twice
}; show_axes,
complete_rect,
let [mut x_axis_widgets, mut y_axis_widgets] = [&x_axes, &y_axes],
axis_widgets(show_axes, plot_rect, [&x_axes, &y_axes]); );
// If too little space, remove axis widgets
if plot_rect.width() <= 0.0 || plot_rect.height() <= 0.0 {
y_axis_widgets.clear();
x_axis_widgets.clear();
plot_rect = complete_rect;
}
// Allocate the plot window. // Allocate the plot window.
let response = ui.allocate_rect(plot_rect, Sense::click_and_drag()); let response = ui.allocate_rect(plot_rect, Sense::click_and_drag());
let rect = plot_rect;
// Load or initialize the memory. // Load or initialize the memory.
let plot_id = id.unwrap_or_else(|| ui.make_persistent_id(id_source)); ui.ctx().check_for_id_clash(plot_id, plot_rect, "Plot");
ui.ctx().check_for_id_clash(plot_id, rect, "Plot");
let memory = if reset { let mut mem = if reset {
if let Some((name, _)) = linked_axes.as_ref() { if let Some((name, _)) = linked_axes.as_ref() {
ui.data_mut(|data| { ui.data_mut(|data| {
let link_groups: &mut BoundsLinkGroups = data.get_temp_mut_or_default(Id::NULL); let link_groups: &mut BoundsLinkGroups = data.get_temp_mut_or_default(Id::NULL);
@ -860,24 +805,20 @@ impl Plot {
auto_bounds: default_auto_bounds, auto_bounds: default_auto_bounds,
hovered_item: None, hovered_item: None,
hidden_items: Default::default(), hidden_items: Default::default(),
transform: PlotTransform::new(rect, min_auto_bounds, center_axis.x, center_axis.y), transform: PlotTransform::new(plot_rect, min_auto_bounds, center_axis.x, center_axis.y),
last_click_pos_for_zoom: None, last_click_pos_for_zoom: None,
x_axis_thickness: Default::default(),
y_axis_thickness: Default::default(),
}); });
let PlotMemory { let last_plot_transform = mem.transform;
mut auto_bounds,
mut hovered_item,
mut hidden_items,
transform: last_plot_transform,
mut last_click_pos_for_zoom,
} = memory;
// Call the plot build function. // Call the plot build function.
let mut plot_ui = PlotUi { let mut plot_ui = PlotUi {
items: Vec::new(), items: Vec::new(),
next_auto_color_idx: 0, next_auto_color_idx: 0,
last_plot_transform, last_plot_transform,
last_auto_bounds: auto_bounds, last_auto_bounds: mem.auto_bounds,
response, response,
bounds_modifications: Vec::new(), bounds_modifications: Vec::new(),
ctx: ui.ctx().clone(), ctx: ui.ctx().clone(),
@ -894,9 +835,9 @@ impl Plot {
// Background // Background
if show_background { if show_background {
ui.painter() ui.painter()
.with_clip_rect(rect) .with_clip_rect(plot_rect)
.add(epaint::RectShape::new( .add(epaint::RectShape::new(
rect, plot_rect,
Rounding::same(2.0), Rounding::same(2.0),
ui.visuals().extreme_bg_color, ui.visuals().extreme_bg_color,
ui.visuals().widgets.noninteractive.bg_stroke, ui.visuals().widgets.noninteractive.bg_stroke,
@ -905,16 +846,16 @@ impl Plot {
// --- Legend --- // --- Legend ---
let legend = legend_config let legend = legend_config
.and_then(|config| LegendWidget::try_new(rect, config, &items, &hidden_items)); .and_then(|config| LegendWidget::try_new(plot_rect, config, &items, &mem.hidden_items));
// Don't show hover cursor when hovering over legend. // Don't show hover cursor when hovering over legend.
if hovered_item.is_some() { if mem.hovered_item.is_some() {
show_x = false; show_x = false;
show_y = false; show_y = false;
} }
// Remove the deselected items. // Remove the deselected items.
items.retain(|item| !hidden_items.contains(item.name())); items.retain(|item| !mem.hidden_items.contains(item.name()));
// Highlight the hovered items. // Highlight the hovered items.
if let Some(hovered_name) = &hovered_item { if let Some(hovered_name) = &mem.hovered_item {
items items
.iter_mut() .iter_mut()
.filter(|entry| entry.name() == hovered_name) .filter(|entry| entry.name() == hovered_name)
@ -961,11 +902,11 @@ impl Plot {
if let Some(linked_bounds) = link_groups.0.get(id) { if let Some(linked_bounds) = link_groups.0.get(id) {
if axes.x { if axes.x {
bounds.set_x(&linked_bounds.bounds); bounds.set_x(&linked_bounds.bounds);
auto_bounds.x = linked_bounds.auto_bounds.x; mem.auto_bounds.x = linked_bounds.auto_bounds.x;
} }
if axes.y { if axes.y {
bounds.set_y(&linked_bounds.bounds); bounds.set_y(&linked_bounds.bounds);
auto_bounds.y = linked_bounds.auto_bounds.y; mem.auto_bounds.y = linked_bounds.auto_bounds.y;
} }
}; };
}); });
@ -973,7 +914,7 @@ impl Plot {
// Allow double-clicking to reset to the initial bounds. // Allow double-clicking to reset to the initial bounds.
if allow_double_click_reset && response.double_clicked() { if allow_double_click_reset && response.double_clicked() {
auto_bounds = true.into(); mem.auto_bounds = true.into();
} }
// Apply bounds modifications. // Apply bounds modifications.
@ -981,30 +922,32 @@ impl Plot {
match modification { match modification {
BoundsModification::Set(new_bounds) => { BoundsModification::Set(new_bounds) => {
bounds = new_bounds; bounds = new_bounds;
auto_bounds = false.into(); mem.auto_bounds = false.into();
} }
BoundsModification::Translate(delta) => { BoundsModification::Translate(delta) => {
bounds.translate(delta); bounds.translate(delta);
auto_bounds = false.into(); mem.auto_bounds = false.into();
}
BoundsModification::AutoBounds(new_auto_bounds) => {
mem.auto_bounds = new_auto_bounds;
} }
BoundsModification::AutoBounds(new_auto_bounds) => auto_bounds = new_auto_bounds,
BoundsModification::Zoom(zoom_factor, center) => { BoundsModification::Zoom(zoom_factor, center) => {
bounds.zoom(zoom_factor, center); bounds.zoom(zoom_factor, center);
auto_bounds = false.into(); mem.auto_bounds = false.into();
} }
} }
} }
// Reset bounds to initial bounds if they haven't been modified. // Reset bounds to initial bounds if they haven't been modified.
if auto_bounds.x { if mem.auto_bounds.x {
bounds.set_x(&min_auto_bounds); bounds.set_x(&min_auto_bounds);
} }
if auto_bounds.y { if mem.auto_bounds.y {
bounds.set_y(&min_auto_bounds); bounds.set_y(&min_auto_bounds);
} }
let auto_x = auto_bounds.x && (!min_auto_bounds.is_valid_x() || default_auto_bounds.x); let auto_x = mem.auto_bounds.x && (!min_auto_bounds.is_valid_x() || default_auto_bounds.x);
let auto_y = auto_bounds.y && (!min_auto_bounds.is_valid_y() || default_auto_bounds.y); let auto_y = mem.auto_bounds.y && (!min_auto_bounds.is_valid_y() || default_auto_bounds.y);
// Set bounds automatically based on content. // Set bounds automatically based on content.
if auto_x || auto_y { if auto_x || auto_y {
@ -1027,17 +970,19 @@ impl Plot {
} }
} }
let mut transform = PlotTransform::new(rect, bounds, center_axis.x, center_axis.y); mem.transform = PlotTransform::new(plot_rect, bounds, center_axis.x, center_axis.y);
// Enforce aspect ratio // Enforce aspect ratio
if let Some(data_aspect) = data_aspect { if let Some(data_aspect) = data_aspect {
if let Some((_, linked_axes)) = &linked_axes { if let Some((_, linked_axes)) = &linked_axes {
let change_x = linked_axes.y && !linked_axes.x; let change_x = linked_axes.y && !linked_axes.x;
transform.set_aspect_by_changing_axis(data_aspect as f64, change_x); mem.transform
.set_aspect_by_changing_axis(data_aspect as f64, change_x);
} else if default_auto_bounds.any() { } else if default_auto_bounds.any() {
transform.set_aspect_by_expanding(data_aspect as f64); mem.transform.set_aspect_by_expanding(data_aspect as f64);
} else { } else {
transform.set_aspect_by_changing_axis(data_aspect as f64, false); mem.transform
.set_aspect_by_changing_axis(data_aspect as f64, false);
} }
} }
@ -1051,8 +996,8 @@ impl Plot {
if !allow_drag.y { if !allow_drag.y {
delta.y = 0.0; delta.y = 0.0;
} }
transform.translate_bounds(delta); mem.transform.translate_bounds(delta);
auto_bounds = !allow_drag; mem.auto_bounds = !allow_drag;
} }
// Zooming // Zooming
@ -1061,9 +1006,9 @@ impl Plot {
// Save last click to allow boxed zooming // Save last click to allow boxed zooming
if response.drag_started() && response.dragged_by(boxed_zoom_pointer_button) { if response.drag_started() && response.dragged_by(boxed_zoom_pointer_button) {
// it would be best for egui that input has a memory of the last click pos because it's a common pattern // it would be best for egui that input has a memory of the last click pos because it's a common pattern
last_click_pos_for_zoom = response.hover_pos(); mem.last_click_pos_for_zoom = response.hover_pos();
} }
let box_start_pos = last_click_pos_for_zoom; let box_start_pos = mem.last_click_pos_for_zoom;
let box_end_pos = response.hover_pos(); let box_end_pos = response.hover_pos();
if let (Some(box_start_pos), Some(box_end_pos)) = (box_start_pos, box_end_pos) { if let (Some(box_start_pos), Some(box_end_pos)) = (box_start_pos, box_end_pos) {
// while dragging prepare a Shape and draw it later on top of the plot // while dragging prepare a Shape and draw it later on top of the plot
@ -1085,8 +1030,8 @@ impl Plot {
} }
// when the click is release perform the zoom // when the click is release perform the zoom
if response.drag_released() { if response.drag_released() {
let box_start_pos = transform.value_from_position(box_start_pos); let box_start_pos = mem.transform.value_from_position(box_start_pos);
let box_end_pos = transform.value_from_position(box_end_pos); let box_end_pos = mem.transform.value_from_position(box_end_pos);
let new_bounds = PlotBounds { let new_bounds = PlotBounds {
min: [ min: [
box_start_pos.x.min(box_end_pos.x), box_start_pos.x.min(box_end_pos.x),
@ -1098,11 +1043,11 @@ impl Plot {
], ],
}; };
if new_bounds.is_valid() { if new_bounds.is_valid() {
transform.set_bounds(new_bounds); mem.transform.set_bounds(new_bounds);
auto_bounds = false.into(); mem.auto_bounds = false.into();
} }
// reset the boxed zoom state // reset the boxed zoom state
last_click_pos_for_zoom = None; mem.last_click_pos_for_zoom = None;
} }
} }
} }
@ -1122,15 +1067,15 @@ impl Plot {
zoom_factor.y = 1.0; zoom_factor.y = 1.0;
} }
if zoom_factor != Vec2::splat(1.0) { if zoom_factor != Vec2::splat(1.0) {
transform.zoom(zoom_factor, hover_pos); mem.transform.zoom(zoom_factor, hover_pos);
auto_bounds = !allow_zoom; mem.auto_bounds = !allow_zoom;
} }
} }
if allow_scroll { if allow_scroll {
let scroll_delta = ui.input(|i| i.smooth_scroll_delta); let scroll_delta = ui.input(|i| i.smooth_scroll_delta);
if scroll_delta != Vec2::ZERO { if scroll_delta != Vec2::ZERO {
transform.translate_bounds(-scroll_delta); mem.transform.translate_bounds(-scroll_delta);
auto_bounds = false.into(); mem.auto_bounds = false.into();
} }
} }
} }
@ -1138,12 +1083,12 @@ impl Plot {
// --- transform initialized // --- transform initialized
// Add legend widgets to plot // Add legend widgets to plot
let bounds = transform.bounds(); let bounds = mem.transform.bounds();
let x_axis_range = bounds.range_x(); let x_axis_range = bounds.range_x();
let x_steps = Arc::new({ let x_steps = Arc::new({
let input = GridInput { let input = GridInput {
bounds: (bounds.min[0], bounds.max[0]), bounds: (bounds.min[0], bounds.max[0]),
base_step_size: transform.dvalue_dpos()[0].abs() * grid_spacing.min as f64, base_step_size: mem.transform.dvalue_dpos()[0].abs() * grid_spacing.min as f64,
}; };
(grid_spacers[0])(input) (grid_spacers[0])(input)
}); });
@ -1151,26 +1096,28 @@ impl Plot {
let y_steps = Arc::new({ let y_steps = Arc::new({
let input = GridInput { let input = GridInput {
bounds: (bounds.min[1], bounds.max[1]), bounds: (bounds.min[1], bounds.max[1]),
base_step_size: transform.dvalue_dpos()[1].abs() * grid_spacing.min as f64, base_step_size: mem.transform.dvalue_dpos()[1].abs() * grid_spacing.min as f64,
}; };
(grid_spacers[1])(input) (grid_spacers[1])(input)
}); });
for mut widget in x_axis_widgets { for (i, mut widget) in x_axis_widgets.into_iter().enumerate() {
widget.range = x_axis_range.clone(); widget.range = x_axis_range.clone();
widget.transform = Some(transform); widget.transform = Some(mem.transform);
widget.steps = x_steps.clone(); widget.steps = x_steps.clone();
widget.ui(ui, Axis::X); let (_response, thickness) = widget.ui(ui, Axis::X);
mem.x_axis_thickness.insert(i, thickness);
} }
for mut widget in y_axis_widgets { for (i, mut widget) in y_axis_widgets.into_iter().enumerate() {
widget.range = y_axis_range.clone(); widget.range = y_axis_range.clone();
widget.transform = Some(transform); widget.transform = Some(mem.transform);
widget.steps = y_steps.clone(); widget.steps = y_steps.clone();
widget.ui(ui, Axis::Y); let (_response, thickness) = widget.ui(ui, Axis::Y);
mem.y_axis_thickness.insert(i, thickness);
} }
// Initialize values from functions. // Initialize values from functions.
for item in &mut items { for item in &mut items {
item.initialize(transform.bounds().range_x()); item.initialize(mem.transform.bounds().range_x());
} }
let prepared = PreparedPlot { let prepared = PreparedPlot {
@ -1181,7 +1128,7 @@ impl Plot {
coordinates_formatter, coordinates_formatter,
show_grid, show_grid,
grid_spacing, grid_spacing,
transform, transform: mem.transform,
draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.1.x), 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_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.1.y),
draw_cursors, draw_cursors,
@ -1193,14 +1140,18 @@ impl Plot {
let plot_cursors = prepared.ui(ui, &response); let plot_cursors = prepared.ui(ui, &response);
if let Some(boxed_zoom_rect) = boxed_zoom_rect { if let Some(boxed_zoom_rect) = boxed_zoom_rect {
ui.painter().with_clip_rect(rect).add(boxed_zoom_rect.0); ui.painter()
ui.painter().with_clip_rect(rect).add(boxed_zoom_rect.1); .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 { if let Some(mut legend) = legend {
ui.add(&mut legend); ui.add(&mut legend);
hidden_items = legend.hidden_items(); mem.hidden_items = legend.hidden_items();
hovered_item = legend.hovered_item_name(); mem.hovered_item = legend.hovered_item_name();
} }
if let Some((id, _)) = linked_cursors.as_ref() { if let Some((id, _)) = linked_cursors.as_ref() {
@ -1222,28 +1173,24 @@ impl Plot {
link_groups.0.insert( link_groups.0.insert(
*id, *id,
LinkedBounds { LinkedBounds {
bounds: *transform.bounds(), bounds: *mem.transform.bounds(),
auto_bounds, auto_bounds: mem.auto_bounds,
}, },
); );
}); });
} }
let memory = PlotMemory { let transform = mem.transform;
auto_bounds, mem.store(ui.ctx(), plot_id);
hovered_item,
hidden_items,
transform,
last_click_pos_for_zoom,
};
memory.store(ui.ctx(), plot_id);
let response = if show_x || show_y { let response = if show_x || show_y {
response.on_hover_cursor(CursorIcon::Crosshair) response.on_hover_cursor(CursorIcon::Crosshair)
} else { } else {
response response
}; };
ui.advance_cursor_after_rect(complete_rect); ui.advance_cursor_after_rect(complete_rect);
PlotResponse { PlotResponse {
inner, inner,
response, response,
@ -1252,77 +1199,115 @@ impl Plot {
} }
} }
/// Returns the rect left after adding axes.
fn axis_widgets( fn axis_widgets(
mem: Option<&PlotMemory>,
show_axes: Vec2b, show_axes: Vec2b,
plot_rect: Rect, complete_rect: Rect,
[x_axes, y_axes]: [&[AxisHints]; 2], [x_axes, y_axes]: [&[AxisHints]; 2],
) -> [Vec<AxisWidget>; 2] { ) -> ([Vec<AxisWidget>; 2], Rect) {
// Next we want to create this layout.
// Indices are only examples.
//
// left right
// +---+---------x----------+ +
// | | X-axis 3 |
// | +--------------------+ top
// | | X-axis 2 |
// +-+-+--------------------+-+-+
// |y|y| |y|y|
// |-|-| |-|-|
// |A|A| |A|A|
// y|x|x| Plot Window |x|x|
// |i|i| |i|i|
// |s|s| |s|s|
// |1|0| |2|3|
// +-+-+--------------------+-+-+
// | X-axis 0 | |
// +--------------------+ | bottom
// | X-axis 1 | |
// + +--------------------+---+
//
let mut x_axis_widgets = Vec::<AxisWidget>::new(); let mut x_axis_widgets = Vec::<AxisWidget>::new();
let mut y_axis_widgets = Vec::<AxisWidget>::new(); let mut y_axis_widgets = Vec::<AxisWidget>::new();
// Widget count per border of plot in order left, top, right, bottom // Will shrink as we add more axes.
struct NumWidgets { let mut rect_left = complete_rect;
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 { if show_axes.x {
for cfg in x_axes { // We will fix this later, once we know how much space the y axes take up.
let size_y = Vec2::new(0.0, cfg.thickness(Axis::X)); let initial_x_range = complete_rect.x_range();
let rect = match cfg.placement {
axis::Placement::LeftBottom => { for (i, cfg) in x_axes.iter().enumerate().rev() {
let off = num_widgets.bottom as f32; let mut height = cfg.thickness(Axis::X);
num_widgets.bottom += 1; if let Some(mem) = mem {
Rect { // If the labels took up too much space the previous frame, give them more space now:
min: plot_rect.left_bottom() + size_y * off, height = height.max(mem.x_axis_thickness.get(&i).copied().unwrap_or_default());
max: plot_rect.right_bottom() + size_y * (off + 1.0), }
}
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)
} }
axis::Placement::RightTop => { VPlacement::Top => {
let off = num_widgets.top as f32; let top = rect_left.top();
num_widgets.top += 1; *rect_left.top_mut() += height;
Rect { let bottom = rect_left.top();
min: plot_rect.left_top() - size_y * (off + 1.0), Rect::from_x_y_ranges(initial_x_range, top..=bottom)
max: plot_rect.right_top() - size_y * off,
}
} }
}; };
x_axis_widgets.push(AxisWidget::new(cfg.clone(), rect)); x_axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
} }
} }
if show_axes.y { if show_axes.y {
for cfg in y_axes { // We know this, since we've already allocated space for the x axes.
let size_x = Vec2::new(cfg.thickness(Axis::Y), 0.0); let plot_y_range = rect_left.y_range();
let rect = match cfg.placement {
axis::Placement::LeftBottom => { for (i, cfg) in y_axes.iter().enumerate().rev() {
let off = num_widgets.left as f32; let mut width = cfg.thickness(Axis::Y);
num_widgets.left += 1; if let Some(mem) = mem {
Rect { // If the labels took up too much space the previous frame, give them more space now:
min: plot_rect.left_top() - size_x * (off + 1.0), width = width.max(mem.y_axis_thickness.get(&i).copied().unwrap_or_default());
max: plot_rect.left_bottom() - size_x * off, }
}
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)
} }
axis::Placement::RightTop => { HPlacement::Right => {
let off = num_widgets.right as f32; let right = rect_left.right();
num_widgets.right += 1; *rect_left.right_mut() -= width;
Rect { let left = rect_left.right();
min: plot_rect.right_top() + size_x * off, Rect::from_x_y_ranges(left..=right, plot_y_range)
max: plot_rect.right_bottom() + size_x * (off + 1.0),
}
} }
}; };
y_axis_widgets.push(AxisWidget::new(cfg.clone(), rect)); y_axis_widgets.push(AxisWidget::new(cfg.clone(), rect));
} }
} }
[x_axis_widgets, y_axis_widgets] let mut plot_rect = rect_left;
// If too little space, remove axis widgets
if plot_rect.width() <= 0.0 || plot_rect.height() <= 0.0 {
y_axis_widgets.clear();
x_axis_widgets.clear();
plot_rect = complete_rect;
}
// Bow that we know the final x_range of the plot_rect,
// assign it to the x_axis_widgets (they are currently too wide):
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)
} }
/// User-requested modifications to the plot bounds. We collect them in the plot build function to later apply /// User-requested modifications to the plot bounds. We collect them in the plot build function to later apply

View File

@ -1,3 +1,5 @@
use std::collections::BTreeMap;
use egui::{ahash, Context, Id, Pos2, Vec2b}; use egui::{ahash, Context, Id, Pos2, Vec2b};
use crate::{PlotBounds, PlotTransform}; use crate::{PlotBounds, PlotTransform};
@ -23,6 +25,13 @@ pub struct PlotMemory {
/// Allows to remember the first click position when performing a boxed zoom /// Allows to remember the first click position when performing a boxed zoom
pub(crate) last_click_pos_for_zoom: Option<Pos2>, pub(crate) last_click_pos_for_zoom: Option<Pos2>,
/// The thickness of each of the axes the previous frame.
///
/// This is used in the next frame to make the axes thicker
/// in order to fit the labels, if necessary.
pub(crate) x_axis_thickness: BTreeMap<usize, f32>,
pub(crate) y_axis_thickness: BTreeMap<usize, f32>,
} }
impl PlotMemory { impl PlotMemory {

View File

@ -28,6 +28,7 @@ pub struct Rot2 {
/// Identity rotation /// Identity rotation
impl Default for Rot2 { impl Default for Rot2 {
/// Identity rotation /// Identity rotation
#[inline]
fn default() -> Self { fn default() -> Self {
Self { s: 0.0, c: 1.0 } Self { s: 0.0, c: 1.0 }
} }
@ -39,29 +40,35 @@ impl Rot2 {
/// Angle is clockwise in radians. /// Angle is clockwise in radians.
/// A 𝞃/4 = 90° rotation means rotating the X axis to the Y axis. /// A 𝞃/4 = 90° rotation means rotating the X axis to the Y axis.
#[inline]
pub fn from_angle(angle: f32) -> Self { pub fn from_angle(angle: f32) -> Self {
let (s, c) = angle.sin_cos(); let (s, c) = angle.sin_cos();
Self { s, c } Self { s, c }
} }
#[inline]
pub fn angle(self) -> f32 { pub fn angle(self) -> f32 {
self.s.atan2(self.c) self.s.atan2(self.c)
} }
/// The factor by which vectors will be scaled. /// The factor by which vectors will be scaled.
#[inline]
pub fn length(self) -> f32 { pub fn length(self) -> f32 {
self.c.hypot(self.s) self.c.hypot(self.s)
} }
#[inline]
pub fn length_squared(self) -> f32 { pub fn length_squared(self) -> f32 {
self.c.powi(2) + self.s.powi(2) self.c.powi(2) + self.s.powi(2)
} }
#[inline]
pub fn is_finite(self) -> bool { pub fn is_finite(self) -> bool {
self.c.is_finite() && self.s.is_finite() self.c.is_finite() && self.s.is_finite()
} }
#[must_use] #[must_use]
#[inline]
pub fn inverse(self) -> Self { pub fn inverse(self) -> Self {
Self { Self {
s: -self.s, s: -self.s,
@ -70,6 +77,7 @@ impl Rot2 {
} }
#[must_use] #[must_use]
#[inline]
pub fn normalized(self) -> Self { pub fn normalized(self) -> Self {
let l = self.length(); let l = self.length();
let ret = Self { let ret = Self {
@ -95,6 +103,7 @@ impl std::fmt::Debug for Rot2 {
impl std::ops::Mul<Self> for Rot2 { impl std::ops::Mul<Self> for Rot2 {
type Output = Self; type Output = Self;
#[inline]
fn mul(self, r: Self) -> Self { fn mul(self, r: Self) -> Self {
/* /*
|lc -ls| * |rc -rs| |lc -ls| * |rc -rs|
@ -111,6 +120,7 @@ impl std::ops::Mul<Self> for Rot2 {
impl std::ops::Mul<Vec2> for Rot2 { impl std::ops::Mul<Vec2> for Rot2 {
type Output = Vec2; type Output = Vec2;
#[inline]
fn mul(self, v: Vec2) -> Vec2 { fn mul(self, v: Vec2) -> Vec2 {
Vec2 { Vec2 {
x: self.c * v.x - self.s * v.y, x: self.c * v.x - self.s * v.y,
@ -123,6 +133,7 @@ impl std::ops::Mul<Vec2> for Rot2 {
impl std::ops::Mul<Rot2> for f32 { impl std::ops::Mul<Rot2> for f32 {
type Output = Rot2; type Output = Rot2;
#[inline]
fn mul(self, r: Rot2) -> Rot2 { fn mul(self, r: Rot2) -> Rot2 {
Rot2 { Rot2 {
c: self * r.c, c: self * r.c,
@ -135,6 +146,7 @@ impl std::ops::Mul<Rot2> for f32 {
impl std::ops::Mul<f32> for Rot2 { impl std::ops::Mul<f32> for Rot2 {
type Output = Self; type Output = Self;
#[inline]
fn mul(self, r: f32) -> Self { fn mul(self, r: f32) -> Self {
Self { Self {
c: self.c * r, c: self.c * r,
@ -147,6 +159,7 @@ impl std::ops::Mul<f32> for Rot2 {
impl std::ops::Div<f32> for Rot2 { impl std::ops::Div<f32> for Rot2 {
type Output = Self; type Output = Self;
#[inline]
fn div(self, r: f32) -> Self { fn div(self, r: f32) -> Self {
Self { Self {
c: self.c / r, c: self.c / r,