Plot: Add auto_bounds_x
, auto_bounds_y
and reset
(#2029)
* Make double click reset to the initial view * Allow forcing auto bounds * Allow plots to be reset * Rename `modified` to `bounds_modified`
This commit is contained in:
parent
5e91a3033d
commit
53ff83737b
2 changed files with 92 additions and 62 deletions
|
@ -73,25 +73,15 @@ impl Default for CoordinatesFormatter {
|
|||
const MIN_LINE_SPACING_IN_POINTS: f64 = 6.0; // TODO(emilk): large enough for a wide label
|
||||
|
||||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[derive(Clone)]
|
||||
struct AutoBounds {
|
||||
#[derive(Copy, Clone)]
|
||||
struct AxisBools {
|
||||
x: bool,
|
||||
y: bool,
|
||||
}
|
||||
|
||||
impl AutoBounds {
|
||||
fn from_bool(val: bool) -> Self {
|
||||
AutoBounds { x: val, y: val }
|
||||
}
|
||||
|
||||
fn any(&self) -> bool {
|
||||
self.x || self.y
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for AutoBounds {
|
||||
impl From<bool> for AxisBools {
|
||||
fn from(val: bool) -> Self {
|
||||
AutoBounds::from_bool(val)
|
||||
AxisBools { x: val, y: val }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,10 +89,11 @@ impl From<bool> for AutoBounds {
|
|||
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
|
||||
#[derive(Clone)]
|
||||
struct PlotMemory {
|
||||
auto_bounds: AutoBounds,
|
||||
/// Indicates if the user has modified the bounds, for example by moving or zooming,
|
||||
/// or if the bounds should be calculated based by included point or auto bounds.
|
||||
bounds_modified: AxisBools,
|
||||
hovered_entry: Option<String>,
|
||||
hidden_items: ahash::HashSet<String>,
|
||||
min_auto_bounds: PlotBounds,
|
||||
last_screen_transform: ScreenTransform,
|
||||
/// Allows to remember the first click position when performing a boxed zoom
|
||||
last_click_pos_for_zoom: Option<Pos2>,
|
||||
|
@ -268,6 +259,7 @@ pub struct Plot {
|
|||
allow_zoom: bool,
|
||||
allow_drag: bool,
|
||||
allow_scroll: bool,
|
||||
auto_bounds: AxisBools,
|
||||
min_auto_bounds: PlotBounds,
|
||||
margin_fraction: Vec2,
|
||||
allow_boxed_zoom: bool,
|
||||
|
@ -281,6 +273,8 @@ pub struct Plot {
|
|||
data_aspect: Option<f32>,
|
||||
view_aspect: Option<f32>,
|
||||
|
||||
reset: bool,
|
||||
|
||||
show_x: bool,
|
||||
show_y: bool,
|
||||
label_formatter: LabelFormatter,
|
||||
|
@ -303,6 +297,7 @@ impl Plot {
|
|||
allow_zoom: true,
|
||||
allow_drag: true,
|
||||
allow_scroll: true,
|
||||
auto_bounds: false.into(),
|
||||
min_auto_bounds: PlotBounds::NOTHING,
|
||||
margin_fraction: Vec2::splat(0.05),
|
||||
allow_boxed_zoom: true,
|
||||
|
@ -316,6 +311,8 @@ impl Plot {
|
|||
data_aspect: None,
|
||||
view_aspect: None,
|
||||
|
||||
reset: false,
|
||||
|
||||
show_x: true,
|
||||
show_y: true,
|
||||
label_formatter: None,
|
||||
|
@ -402,7 +399,7 @@ impl Plot {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set the side margin as a fraction of the plot size.
|
||||
/// Set the side margin as a fraction of the plot size. Only used for auto bounds.
|
||||
///
|
||||
/// For instance, a value of `0.1` will add 10% space on both sides.
|
||||
pub fn set_margin_fraction(mut self, margin_fraction: Vec2) -> Self {
|
||||
|
@ -556,6 +553,18 @@ impl Plot {
|
|||
self
|
||||
}
|
||||
|
||||
/// Expand bounds to fit all items across the x axis, including values given by `include_x`.
|
||||
pub fn auto_bounds_x(mut self) -> Self {
|
||||
self.auto_bounds.x = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Expand bounds to fit all items across the y axis, including values given by `include_y`.
|
||||
pub fn auto_bounds_y(mut self) -> Self {
|
||||
self.auto_bounds.y = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Show a legend including all named items.
|
||||
pub fn legend(mut self, legend: Legend) -> Self {
|
||||
self.legend_config = Some(legend);
|
||||
|
@ -592,6 +601,12 @@ impl Plot {
|
|||
self
|
||||
}
|
||||
|
||||
/// Resets the plot.
|
||||
pub fn reset(mut self) -> Self {
|
||||
self.reset = true;
|
||||
self
|
||||
}
|
||||
|
||||
/// Interact with and add items to the plot and finally draw it.
|
||||
pub fn show<R>(self, ui: &mut Ui, build_fn: impl FnOnce(&mut PlotUi) -> R) -> InnerResponse<R> {
|
||||
self.show_dyn(ui, Box::new(build_fn))
|
||||
|
@ -611,6 +626,7 @@ impl Plot {
|
|||
allow_drag,
|
||||
allow_boxed_zoom,
|
||||
boxed_zoom_pointer_button: boxed_zoom_pointer,
|
||||
auto_bounds,
|
||||
min_auto_bounds,
|
||||
margin_fraction,
|
||||
width,
|
||||
|
@ -624,6 +640,7 @@ impl Plot {
|
|||
coordinates_formatter,
|
||||
axis_formatters,
|
||||
legend_config,
|
||||
reset,
|
||||
show_background,
|
||||
show_axes,
|
||||
linked_axes,
|
||||
|
@ -661,11 +678,19 @@ impl Plot {
|
|||
// Load or initialize the memory.
|
||||
let plot_id = ui.make_persistent_id(id_source);
|
||||
ui.ctx().check_for_id_clash(plot_id, rect, "Plot");
|
||||
let mut memory = PlotMemory::load(ui.ctx(), plot_id).unwrap_or_else(|| PlotMemory {
|
||||
auto_bounds: (!min_auto_bounds.is_valid()).into(),
|
||||
let memory = if reset {
|
||||
if let Some(axes) = linked_axes.as_ref() {
|
||||
axes.bounds.set(None);
|
||||
};
|
||||
|
||||
None
|
||||
} else {
|
||||
PlotMemory::load(ui.ctx(), plot_id)
|
||||
}
|
||||
.unwrap_or_else(|| PlotMemory {
|
||||
bounds_modified: false.into(),
|
||||
hovered_entry: None,
|
||||
hidden_items: Default::default(),
|
||||
min_auto_bounds,
|
||||
last_screen_transform: ScreenTransform::new(
|
||||
rect,
|
||||
min_auto_bounds,
|
||||
|
@ -675,24 +700,12 @@ impl Plot {
|
|||
last_click_pos_for_zoom: None,
|
||||
});
|
||||
|
||||
// If the min bounds changed, recalculate everything.
|
||||
if min_auto_bounds != memory.min_auto_bounds {
|
||||
memory = PlotMemory {
|
||||
auto_bounds: (!min_auto_bounds.is_valid()).into(),
|
||||
hovered_entry: None,
|
||||
min_auto_bounds,
|
||||
..memory
|
||||
};
|
||||
memory.clone().store(ui.ctx(), plot_id);
|
||||
}
|
||||
|
||||
let PlotMemory {
|
||||
mut auto_bounds,
|
||||
mut bounds_modified,
|
||||
mut hovered_entry,
|
||||
mut hidden_items,
|
||||
last_screen_transform,
|
||||
mut last_click_pos_for_zoom,
|
||||
..
|
||||
} = memory;
|
||||
|
||||
// Call the plot build function.
|
||||
|
@ -774,52 +787,51 @@ impl Plot {
|
|||
if let Some(linked_bounds) = axes.get() {
|
||||
if axes.link_x {
|
||||
bounds.set_x(&linked_bounds);
|
||||
// Turn off auto bounds to keep it from overriding what we just set.
|
||||
auto_bounds.x = false;
|
||||
// Mark the axis as modified to prevent it from being changed.
|
||||
bounds_modified.x = true;
|
||||
}
|
||||
if axes.link_y {
|
||||
bounds.set_y(&linked_bounds);
|
||||
// Turn off auto bounds to keep it from overriding what we just set.
|
||||
auto_bounds.y = false;
|
||||
// Mark the axis as modified to prevent it from being changed.
|
||||
bounds_modified.y = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Allow double clicking to reset to automatic bounds.
|
||||
// Allow double clicking to reset to the initial bounds.
|
||||
if response.double_clicked_by(PointerButton::Primary) {
|
||||
auto_bounds = true.into();
|
||||
bounds_modified = false.into();
|
||||
}
|
||||
|
||||
if !bounds.is_valid() {
|
||||
auto_bounds = true.into();
|
||||
// Reset bounds to initial bounds if we haven't been modified.
|
||||
if !bounds_modified.x {
|
||||
bounds.set_x(&min_auto_bounds);
|
||||
}
|
||||
if !bounds_modified.y {
|
||||
bounds.set_y(&min_auto_bounds);
|
||||
}
|
||||
|
||||
let auto_x = !bounds_modified.x && (!min_auto_bounds.is_valid_x() || auto_bounds.x);
|
||||
let auto_y = !bounds_modified.y && (!min_auto_bounds.is_valid_y() || auto_bounds.y);
|
||||
|
||||
// Set bounds automatically based on content.
|
||||
if auto_bounds.any() {
|
||||
if auto_bounds.x {
|
||||
bounds.set_x(&min_auto_bounds);
|
||||
}
|
||||
|
||||
if auto_bounds.y {
|
||||
bounds.set_y(&min_auto_bounds);
|
||||
}
|
||||
|
||||
if auto_x || auto_y {
|
||||
for item in &items {
|
||||
let item_bounds = item.bounds();
|
||||
|
||||
if auto_bounds.x {
|
||||
if auto_x {
|
||||
bounds.merge_x(&item_bounds);
|
||||
}
|
||||
if auto_bounds.y {
|
||||
if auto_y {
|
||||
bounds.merge_y(&item_bounds);
|
||||
}
|
||||
}
|
||||
|
||||
if auto_bounds.x {
|
||||
if auto_x {
|
||||
bounds.add_relative_margin_x(margin_fraction);
|
||||
}
|
||||
|
||||
if auto_bounds.y {
|
||||
if auto_y {
|
||||
bounds.add_relative_margin_y(margin_fraction);
|
||||
}
|
||||
}
|
||||
|
@ -840,7 +852,7 @@ impl Plot {
|
|||
if allow_drag && response.dragged_by(PointerButton::Primary) {
|
||||
response = response.on_hover_cursor(CursorIcon::Grabbing);
|
||||
transform.translate_bounds(-response.drag_delta());
|
||||
auto_bounds = false.into();
|
||||
bounds_modified = true.into();
|
||||
}
|
||||
|
||||
// Zooming
|
||||
|
@ -887,7 +899,7 @@ impl Plot {
|
|||
};
|
||||
if new_bounds.is_valid() {
|
||||
transform.set_bounds(new_bounds);
|
||||
auto_bounds = false.into();
|
||||
bounds_modified = true.into();
|
||||
}
|
||||
// reset the boxed zoom state
|
||||
last_click_pos_for_zoom = None;
|
||||
|
@ -904,14 +916,14 @@ impl Plot {
|
|||
};
|
||||
if zoom_factor != Vec2::splat(1.0) {
|
||||
transform.zoom(zoom_factor, hover_pos);
|
||||
auto_bounds = false.into();
|
||||
bounds_modified = true.into();
|
||||
}
|
||||
}
|
||||
if allow_scroll {
|
||||
let scroll_delta = ui.input().scroll_delta;
|
||||
if scroll_delta != Vec2::ZERO {
|
||||
transform.translate_bounds(-scroll_delta);
|
||||
auto_bounds = false.into();
|
||||
bounds_modified = true.into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -961,10 +973,9 @@ impl Plot {
|
|||
}
|
||||
|
||||
let memory = PlotMemory {
|
||||
auto_bounds,
|
||||
bounds_modified,
|
||||
hovered_entry,
|
||||
hidden_items,
|
||||
min_auto_bounds,
|
||||
last_screen_transform: transform,
|
||||
last_click_pos_for_zoom,
|
||||
};
|
||||
|
|
|
@ -40,10 +40,26 @@ impl PlotBounds {
|
|||
&& self.max[1].is_finite()
|
||||
}
|
||||
|
||||
pub fn is_finite_x(&self) -> bool {
|
||||
self.min[0].is_finite() && self.max[0].is_finite()
|
||||
}
|
||||
|
||||
pub fn is_finite_y(&self) -> bool {
|
||||
self.min[1].is_finite() && self.max[1].is_finite()
|
||||
}
|
||||
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.is_finite() && self.width() > 0.0 && self.height() > 0.0
|
||||
}
|
||||
|
||||
pub fn is_valid_x(&self) -> bool {
|
||||
self.is_finite_x() && self.width() > 0.0
|
||||
}
|
||||
|
||||
pub fn is_valid_y(&self) -> bool {
|
||||
self.is_finite_y() && self.height() > 0.0
|
||||
}
|
||||
|
||||
pub fn width(&self) -> f64 {
|
||||
self.max[0] - self.min[0]
|
||||
}
|
||||
|
@ -181,8 +197,11 @@ pub(crate) struct ScreenTransform {
|
|||
impl ScreenTransform {
|
||||
pub fn new(frame: Rect, mut bounds: PlotBounds, x_centered: bool, y_centered: bool) -> Self {
|
||||
// Make sure they are not empty.
|
||||
if !bounds.is_valid() {
|
||||
bounds = PlotBounds::new_symmetrical(1.0);
|
||||
if !bounds.is_valid_x() {
|
||||
bounds.set_x(&PlotBounds::new_symmetrical(1.0));
|
||||
}
|
||||
if !bounds.is_valid_y() {
|
||||
bounds.set_y(&PlotBounds::new_symmetrical(1.0));
|
||||
}
|
||||
|
||||
// Scale axes so that the origin is in the center.
|
||||
|
|
Loading…
Reference in a new issue