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. 
This commit is contained in:
parent
01597fe1a1
commit
527f4bfdf6
|
|
@ -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.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
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 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let plot_id = id.unwrap_or_else(|| ui.make_persistent_id(id_source));
|
||||
|
||||
// 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]);
|
||||
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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…
Reference in New Issue