Browse Source

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)
pull/3925/head
Emil Ernerfeldt 9 months ago
committed by GitHub
parent
commit
527f4bfdf6
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 13
      crates/egui/src/painter.rs
  2. 204
      crates/egui_plot/src/axis.rs
  3. 353
      crates/egui_plot/src/lib.rs
  4. 9
      crates/egui_plot/src/memory.rs
  5. 13
      crates/emath/src/rot2.rs

13
crates/egui/src/painter.rs

@ -6,7 +6,7 @@ use crate::{
Color32, Context, FontId,
};
use epaint::{
text::{Fonts, Galley},
text::{Fonts, Galley, LayoutJob},
CircleShape, ClippedShape, RectShape, Rounding, Shape, Stroke,
};
@ -436,9 +436,18 @@ impl Painter {
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`].
///
/// 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.
///

204
crates/egui_plot/src/axis.rs

@ -1,9 +1,9 @@
use std::{fmt::Debug, ops::RangeInclusive, sync::Arc};
use egui::{
emath::{remap_clamp, round_to_decimals},
emath::{remap_clamp, round_to_decimals, Rot2},
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};
@ -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 {
#[inline]
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.
///
/// Used to configure axis label and ticks.
@ -211,16 +231,18 @@ impl AxisHints {
#[derive(Clone)]
pub(super) struct AxisWidget {
pub(super) range: RangeInclusive<f64>,
pub(super) hints: AxisHints,
pub(super) rect: Rect,
pub(super) transform: Option<PlotTransform>,
pub(super) steps: Arc<Vec<GridMark>>,
pub range: RangeInclusive<f64>,
pub hints: AxisHints,
/// The region where we draw the axis labels.
pub rect: Rect,
pub transform: Option<PlotTransform>,
pub steps: Arc<Vec<GridMark>>,
}
impl AxisWidget {
/// 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 {
range: (0.0..=0.0),
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());
if !ui.is_rect_visible(response.rect) {
return response;
return (response, 0.0);
}
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,
{
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,
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,
},
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,
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));
ui.painter()
.add(TextShape::new(text_pos, galley, text_color).with_angle(angle));
}
// --- add ticks ---
let font_id = TextStyle::Body.resolve(ui.style());
let Some(transform) = self.transform else {
return response;
return (response, 0.0);
};
let label_spacing = self.hints.label_spacing;
let mut thickness: f32 = 0.0;
// Add tick labels:
for step in self.steps.iter() {
let text = (self.hints.formatter)(*step, self.hints.digits, &self.range);
if !text.is_empty() {
@ -314,41 +342,61 @@ impl AxisWidget {
.layout_no_wrap(text, font_id.clone(), text_color);
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 => {
let y = match self.hints.placement {
Placement::LeftBottom => self.rect.min.y,
Placement::RightTop => self.rect.max.y - galley.size().y,
};
thickness = thickness.max(galley.size().y);
let projected_point = super::PlotPoint::new(step.value, 0.0);
Pos2 {
x: transform.position_from_point(&projected_point).x
- galley.size().x / 2.0,
y,
}
let center_x = transform.position_from_point(&projected_point).x;
let y = match VPlacement::from(self.hints.placement) {
VPlacement::Bottom => self.rect.min.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 => {
let x = match self.hints.placement {
Placement::LeftBottom => self.rect.max.x - galley.size().x,
Placement::RightTop => self.rect.min.x,
};
thickness = thickness.max(galley.size().x);
let projected_point = super::PlotPoint::new(0.0, step.value);
Pos2 {
x,
y: transform.position_from_point(&projected_point).y
- galley.size().y / 2.0,
}
let center_y = transform.position_from_point(&projected_point).y;
match HPlacement::from(self.hints.placement) {
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)
}
}

353
crates/egui_plot/src/lib.rs

@ -768,84 +768,29 @@ impl Plot {
.at_least(min_size.y);
vec2(width, height)
};
// Determine complete rect of widget.
let complete_rect = Rect {
min: pos,
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 = {
// 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
margin.shrink_rect(complete_rect)
};
let [mut x_axis_widgets, mut y_axis_widgets] =
axis_widgets(show_axes, plot_rect, [&x_axes, &y_axes]);
let plot_id = id.unwrap_or_else(|| ui.make_persistent_id(id_source));
// 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;
}
let ([x_axis_widgets, y_axis_widgets], plot_rect) = axis_widgets(
PlotMemory::load(ui.ctx(), plot_id).as_ref(), // TODO: avoid loading plot memory twice
show_axes,
complete_rect,
[&x_axes, &y_axes],
);
// Allocate the plot window.
let response = ui.allocate_rect(plot_rect, Sense::click_and_drag());
let rect = plot_rect;
// 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, rect, "Plot");
let memory = if reset {
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);
@ -860,24 +805,20 @@ impl Plot {
auto_bounds: default_auto_bounds,
hovered_item: None,
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,
x_axis_thickness: Default::default(),
y_axis_thickness: Default::default(),
});
let PlotMemory {
mut auto_bounds,
mut hovered_item,
mut hidden_items,
transform: last_plot_transform,
mut last_click_pos_for_zoom,
} = memory;
let last_plot_transform = mem.transform;
// Call the plot build function.
let mut plot_ui = PlotUi {
items: Vec::new(),
next_auto_color_idx: 0,
last_plot_transform,
last_auto_bounds: auto_bounds,
last_auto_bounds: mem.auto_bounds,
response,
bounds_modifications: Vec::new(),
ctx: ui.ctx().clone(),
@ -894,9 +835,9 @@ impl Plot {
// Background
if show_background {
ui.painter()
.with_clip_rect(rect)
.with_clip_rect(plot_rect)
.add(epaint::RectShape::new(
rect,
plot_rect,
Rounding::same(2.0),
ui.visuals().extreme_bg_color,
ui.visuals().widgets.noninteractive.bg_stroke,
@ -905,16 +846,16 @@ impl Plot {
// --- Legend ---
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.
if hovered_item.is_some() {
if mem.hovered_item.is_some() {
show_x = false;
show_y = false;
}
// 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.
if let Some(hovered_name) = &hovered_item {
if let Some(hovered_name) = &mem.hovered_item {
items
.iter_mut()
.filter(|entry| entry.name() == hovered_name)
@ -961,11 +902,11 @@ impl Plot {
if let Some(linked_bounds) = link_groups.0.get(id) {
if axes.x {
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 {
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.
if allow_double_click_reset && response.double_clicked() {
auto_bounds = true.into();
mem.auto_bounds = true.into();
}
// Apply bounds modifications.
@ -981,30 +922,32 @@ impl Plot {
match modification {
BoundsModification::Set(new_bounds) => {
bounds = new_bounds;
auto_bounds = false.into();
mem.auto_bounds = false.into();
}
BoundsModification::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) => {
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.
if auto_bounds.x {
if mem.auto_bounds.x {
bounds.set_x(&min_auto_bounds);
}
if auto_bounds.y {
if mem.auto_bounds.y {
bounds.set_y(&min_auto_bounds);
}
let auto_x = 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_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);
// Set bounds automatically based on content.
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
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);
mem.transform
.set_aspect_by_changing_axis(data_aspect as f64, change_x);
} 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 {
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 {
delta.y = 0.0;
}
transform.translate_bounds(delta);
auto_bounds = !allow_drag;
mem.transform.translate_bounds(delta);
mem.auto_bounds = !allow_drag;
}
// Zooming
@ -1061,9 +1006,9 @@ impl Plot {
// Save last click to allow boxed zooming
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
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();
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
@ -1085,8 +1030,8 @@ impl Plot {
}
// when the click is release perform the zoom
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 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),
@ -1098,11 +1043,11 @@ impl Plot {
],
};
if new_bounds.is_valid() {
transform.set_bounds(new_bounds);
auto_bounds = false.into();
mem.transform.set_bounds(new_bounds);
mem.auto_bounds = false.into();
}
// 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;
}
if zoom_factor != Vec2::splat(1.0) {
transform.zoom(zoom_factor, hover_pos);
auto_bounds = !allow_zoom;
mem.transform.zoom(zoom_factor, hover_pos);
mem.auto_bounds = !allow_zoom;
}
}
if allow_scroll {
let scroll_delta = ui.input(|i| i.smooth_scroll_delta);
if scroll_delta != Vec2::ZERO {
transform.translate_bounds(-scroll_delta);
auto_bounds = false.into();
mem.transform.translate_bounds(-scroll_delta);
mem.auto_bounds = false.into();
}
}
}
@ -1138,12 +1083,12 @@ impl Plot {
// --- transform initialized
// Add legend widgets to plot
let bounds = transform.bounds();
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: 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)
});
@ -1151,26 +1096,28 @@ impl Plot {
let y_steps = Arc::new({
let input = GridInput {
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)
});
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.transform = Some(transform);
widget.transform = Some(mem.transform);
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.transform = Some(transform);
widget.transform = Some(mem.transform);
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.
for item in &mut items {
item.initialize(transform.bounds().range_x());
item.initialize(mem.transform.bounds().range_x());
}
let prepared = PreparedPlot {
@ -1181,7 +1128,7 @@ impl Plot {
coordinates_formatter,
show_grid,
grid_spacing,
transform,
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,
@ -1193,14 +1140,18 @@ impl Plot {
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);
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);
hidden_items = legend.hidden_items();
hovered_item = legend.hovered_item_name();
mem.hidden_items = legend.hidden_items();
mem.hovered_item = legend.hovered_item_name();
}
if let Some((id, _)) = linked_cursors.as_ref() {
@ -1222,28 +1173,24 @@ impl Plot {
link_groups.0.insert(
*id,
LinkedBounds {
bounds: *transform.bounds(),
auto_bounds,
bounds: *mem.transform.bounds(),
auto_bounds: mem.auto_bounds,
},
);
});
}
let memory = PlotMemory {
auto_bounds,
hovered_item,
hidden_items,
transform,
last_click_pos_for_zoom,
};
memory.store(ui.ctx(), plot_id);
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,
@ -1252,77 +1199,115 @@ impl Plot {
}
}
/// Returns the rect left after adding axes.
fn axis_widgets(
mem: Option<&PlotMemory>,
show_axes: Vec2b,
plot_rect: Rect,
complete_rect: Rect,
[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 y_axis_widgets = Vec::<AxisWidget>::new();
// Widget count per border of plot in order left, top, right, bottom
struct NumWidgets {
left: usize,
top: usize,
right: usize,
bottom: usize,
}
let mut num_widgets = NumWidgets {
left: 0,
top: 0,
right: 0,
bottom: 0,
};
// Will shrink as we add more axes.
let mut rect_left = complete_rect;
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),
}
// We will fix this later, once we know how much space the y axes take up.
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 {
// If the labels took up too much space the previous frame, give them more space now:
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)
}
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,
}
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 {
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,
}
// We know this, since we've already allocated space for the x axes.
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 {
// If the labels took up too much space the previous frame, give them more space now:
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)
}
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),
}
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));
}
}
[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

9
crates/egui_plot/src/memory.rs

@ -1,3 +1,5 @@
use std::collections::BTreeMap;
use egui::{ahash, Context, Id, Pos2, Vec2b};
use crate::{PlotBounds, PlotTransform};
@ -23,6 +25,13 @@ pub struct PlotMemory {
/// Allows to remember the first click position when performing a boxed zoom
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 {

13
crates/emath/src/rot2.rs

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

Loading…
Cancel
Save