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,
|
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.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue