diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d5137620b3..84b769614b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,11 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG ### Added ⭐ * `Event::Key` now has a `repeat` field that is set to `true` if the event was the result of a key-repeat ([#2435](https://github.com/emilk/egui/pull/2435)). * Add `Slider::drag_value_speed`, which lets you ask for finer precision when dragging the slider value rather than the actual slider. -* Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)). * Add `Memory::any_popup_open`, which returns true if any popup is currently open ([#2464](https://github.com/emilk/egui/pull/2464)). +* Add `Plot::clamp_grid` to only show grid where there is data ([#2480](https://github.com/emilk/egui/pull/2480)). + +### Changed 🔧 +* Improved plot grid appearance ([#2412](https://github.com/emilk/egui/pull/2412)). ### Fixed 🐛 * Expose `TextEdit`'s multiline flag to AccessKit ([#2448](https://github.com/emilk/egui/pull/2448)). diff --git a/crates/egui/src/widgets/plot/items/mod.rs b/crates/egui/src/widgets/plot/items/mod.rs index 138aed6e078..dfbcbd44fd5 100644 --- a/crates/egui/src/widgets/plot/items/mod.rs +++ b/crates/egui/src/widgets/plot/items/mod.rs @@ -34,6 +34,7 @@ pub(super) struct PlotConfig<'a> { pub(super) trait PlotItem { fn shapes(&self, ui: &mut Ui, transform: &ScreenTransform, shapes: &mut Vec); + /// For plot-items which are generated based on x values (plotting functions). fn initialize(&mut self, x_range: RangeInclusive); fn name(&self) -> &str; diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index 2978f0dd3d8..d46f2056e8a 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -291,8 +291,10 @@ pub struct Plot { legend_config: Option, show_background: bool, show_axes: [bool; 2], + grid_spacers: [GridSpacer; 2], sharp_grid_lines: bool, + clamp_grid: bool, } impl Plot { @@ -331,8 +333,10 @@ impl Plot { legend_config: None, show_background: true, show_axes: [true; 2], + grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)], sharp_grid_lines: true, + clamp_grid: false, } } @@ -557,6 +561,14 @@ impl Plot { self } + /// Clamp the grid to only be visible at the range of data where we have values. + /// + /// Default: `false`. + pub fn clamp_grid(mut self, clamp_grid: bool) -> Self { + self.clamp_grid = clamp_grid; + self + } + /// Expand bounds to include the given x value. /// For instance, to always show the y axis, call `plot.include_x(0.0)`. pub fn include_x(mut self, x: impl Into) -> Self { @@ -671,6 +683,8 @@ impl Plot { show_axes, linked_axes, linked_cursors, + + clamp_grid, grid_spacers, sharp_grid_lines, } = self; @@ -971,11 +985,12 @@ impl Plot { axis_formatters, show_axes, transform: transform.clone(), - grid_spacers, draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.link_x), draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.link_y), draw_cursors, + grid_spacers, sharp_grid_lines, + clamp_grid, }; let plot_cursors = prepared.ui(ui, &response); @@ -1297,11 +1312,13 @@ struct PreparedPlot { axis_formatters: [AxisFormatter; 2], show_axes: [bool; 2], transform: ScreenTransform, - grid_spacers: [GridSpacer; 2], draw_cursor_x: bool, draw_cursor_y: bool, draw_cursors: Vec, + + grid_spacers: [GridSpacer; 2], sharp_grid_lines: bool, + clamp_grid: bool, } impl PreparedPlot { @@ -1400,10 +1417,13 @@ impl PreparedPlot { shapes: &mut Vec<(Shape, f32)>, sharp_grid_lines: bool, ) { + #![allow(clippy::collapsible_else_if)] + let Self { transform, axis_formatters, grid_spacers, + clamp_grid, .. } = self; @@ -1417,7 +1437,6 @@ impl PreparedPlot { let font_id = TextStyle::Body.resolve(ui.style()); // Where on the cross-dimension to show the label values - let bounds = transform.bounds(); let value_cross = 0.0_f64.clamp(bounds.min[1 - axis], bounds.max[1 - axis]); let input = GridInput { @@ -1426,9 +1445,31 @@ impl PreparedPlot { }; let steps = (grid_spacers[axis])(input); + let clamp_range = clamp_grid.then(|| { + let mut tight_bounds = PlotBounds::NOTHING; + for item in &self.items { + let item_bounds = item.bounds(); + tight_bounds.merge_x(&item_bounds); + tight_bounds.merge_y(&item_bounds); + } + tight_bounds + }); + for step in steps { let value_main = step.value; + if let Some(clamp_range) = clamp_range { + if axis == 0 { + if !clamp_range.range_x().contains(&value_main) { + continue; + }; + } else { + if !clamp_range.range_y().contains(&value_main) { + continue; + }; + } + } + let value = if axis == 0 { PlotPoint::new(value_main, value_cross) } else { @@ -1451,11 +1492,23 @@ impl PreparedPlot { let mut p1 = pos_in_gui; p0[1 - axis] = transform.frame().min[1 - axis]; p1[1 - axis] = transform.frame().max[1 - axis]; + + if let Some(clamp_range) = clamp_range { + if axis == 0 { + p0.y = transform.position_from_point_y(clamp_range.min[1]); + p1.y = transform.position_from_point_y(clamp_range.max[1]); + } else { + p0.x = transform.position_from_point_x(clamp_range.min[0]); + p1.x = transform.position_from_point_x(clamp_range.max[0]); + } + } + if sharp_grid_lines { // Round to avoid aliasing p0 = ui.ctx().round_pos_to_pixels(p0); p1 = ui.ctx().round_pos_to_pixels(p1); } + shapes.push(( Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)), line_strength, diff --git a/crates/egui/src/widgets/plot/transform.rs b/crates/egui/src/widgets/plot/transform.rs index 51c63e896d8..ceff3a9a3cf 100644 --- a/crates/egui/src/widgets/plot/transform.rs +++ b/crates/egui/src/widgets/plot/transform.rs @@ -224,6 +224,7 @@ impl ScreenTransform { } } + /// ui-space rectangle. pub fn frame(&self) -> &Rect { &self.frame } @@ -263,18 +264,27 @@ impl ScreenTransform { } } - pub fn position_from_point(&self, value: &PlotPoint) -> Pos2 { - let x = remap( - value.x, + pub fn position_from_point_x(&self, value: f64) -> f32 { + remap( + value, self.bounds.min[0]..=self.bounds.max[0], (self.frame.left() as f64)..=(self.frame.right() as f64), - ); - let y = remap( - value.y, + ) as f32 + } + + pub fn position_from_point_y(&self, value: f64) -> f32 { + remap( + value, self.bounds.min[1]..=self.bounds.max[1], (self.frame.bottom() as f64)..=(self.frame.top() as f64), // negated y axis! - ); - pos2(x as f32, y as f32) + ) as f32 + } + + pub fn position_from_point(&self, value: &PlotPoint) -> Pos2 { + pos2( + self.position_from_point_x(value.x), + self.position_from_point_y(value.y), + ) } pub fn value_from_position(&self, pos: Pos2) -> PlotPoint { diff --git a/crates/egui_demo_lib/src/demo/plot_demo.rs b/crates/egui_demo_lib/src/demo/plot_demo.rs index ce834f54715..bb98a114415 100644 --- a/crates/egui_demo_lib/src/demo/plot_demo.rs +++ b/crates/egui_demo_lib/src/demo/plot_demo.rs @@ -755,6 +755,7 @@ impl ChartsDemo { Plot::new("Normal Distribution Demo") .legend(Legend::default()) .data_aspect(1.0) + .clamp_grid(true) .show(ui, |plot_ui| plot_ui.bar_chart(chart)) .response }