diff --git a/crates/egui/src/widgets/plot/axis.rs b/crates/egui/src/widgets/plot/axis.rs index 2c8c0aa82..c8ecdbf05 100644 --- a/crates/egui/src/widgets/plot/axis.rs +++ b/crates/egui/src/widgets/plot/axis.rs @@ -3,10 +3,15 @@ use std::{ ops::RangeInclusive, }; -use epaint::{Pos2, Rect, Stroke, TextShape}; +use epaint::{ + emath::{remap_clamp, round_to_decimals, NumExt}, + Color32, Pos2, Rect, Rgba, Shape, Stroke, TextShape, +}; use crate::{Response, Sense, TextStyle, Ui, Widget, WidgetText}; +use super::{transform::PlotTransform, GridMark, MIN_LINE_SPACING_IN_POINTS}; + pub(super) type AxisFormatterFn = fn(f64, &RangeInclusive) -> String; /// Axis specifier. @@ -87,7 +92,16 @@ impl AxisConfig { } fn default_formatter(tick: f64, _range: &RangeInclusive) -> String { - tick.to_string() + const MAX_DECIMALS: usize = 5; + if tick.abs() > 1e6 { + let tick_rounded = round_to_decimals(tick, MAX_DECIMALS); + format!("{:+e}", tick_rounded) + } else if tick.abs() < 1e-6 && tick != 0.0 { + format!("{:+e}", tick) + } else { + let tick_rounded = round_to_decimals(tick, MAX_DECIMALS); + format!("{}", tick_rounded) + } } pub(super) fn thickness(&self) -> f32 { @@ -101,49 +115,29 @@ impl AxisConfig { #[derive(Clone)] pub(super) struct AxisWidget { - config: AxisConfig, + pub(super) range: RangeInclusive, + pub(super) config: AxisConfig, pub(super) rect: Rect, + pub(super) transform: Option, + pub(super) steps: Vec, } impl AxisWidget { /// if `rect` as width or height == 0, is will be automatically calculated from ticks and text. pub(super) fn new(config: AxisConfig, rect: Rect) -> Self { - Self { config, rect } + Self { + range: (0.0..=0.0), + config, + rect, + transform: None, + steps: Vec::new(), + } } - - // fn calc_size(&mut self) { - // if self.rect.height() == 0.0 { - // if self.config.axis == Axis::X { - // // calculate height of x-axis label: ticks + label - // let y = self.rect.min.y; - // if self.config.label.is_empty() { - // self.rect.extend_with_y(y + LINE_HEIGHT); - // } - // else { - // self.rect.extend_with_y(y + 2.0*LINE_HEIGHT) - // } - // } - // } - // if self.rect.width() == 0.0 { - // if self.config.axis == Axis::Y { - // // calculate width of y-axis label: ticks + label - // if self.config.label.is_empty() { - // self.rect.extend_with_x(50.0); - // } - // else { - // self.rect.extend_with_x(100.0) - // } - // } - // } - // } - // pub(super) fn exact_size(mut self, rect: Rect) -> Self { - // self.rect = rect; - // self - // } } impl Widget for AxisWidget { fn ui(self, ui: &mut Ui) -> Response { + // --- add label --- let response = ui.allocate_rect(self.rect, Sense::click_and_drag()); if ui.is_rect_visible(response.rect) { let visuals = ui.style().visuals.clone(); @@ -199,7 +193,64 @@ impl Widget for AxisWidget { angle, }; ui.painter().add(shape); + + // --- add ticks --- + let font_id = TextStyle::Body.resolve(ui.style()); + let transform = match self.transform { + Some(t) => t, + None => return response, + }; + + for step in self.steps { + let text = (self.config.formatter)(step.value, &self.range); + if !text.is_empty() { + let spacing_in_points = (transform.dpos_dvalue()[self.config.axis as usize] + * step.step_size) + .abs() as f32; + + let line_alpha = remap_clamp( + spacing_in_points, + (MIN_LINE_SPACING_IN_POINTS as f32 * 2.0)..=300.0, + 0.0..=0.15, + ); + + if line_alpha > 0.0 { + let line_color = color_from_alpha(ui, line_alpha); + let galley = ui + .painter() + .layout_no_wrap(text, font_id.clone(), line_color); + let text_pos = match self.config.axis { + Axis::X => { + 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: self.rect.min.y, + } + } + Axis::Y => { + let projected_point = super::PlotPoint::new(0.0, step.value); + Pos2 { + x: self.rect.max.x - galley.size().x, + y: transform.position_from_point(&projected_point).y + - galley.size().y / 2.0, + } + } + }; + + ui.painter().add(Shape::galley(text_pos, galley)); + } + } + } } response } } + +fn color_from_alpha(ui: &Ui, alpha: f32) -> Color32 { + if ui.visuals().dark_mode { + Rgba::from_white_alpha(alpha).into() + } else { + Rgba::from_black_alpha((4.0 * alpha).at_most(1.0)).into() + } +} diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index 3a58e2668..df9935bb1 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -765,12 +765,7 @@ impl Plot { // Allocate the plot window. // let (rect, response) = ui.allocate_exact_size(size, Sense::drag()); - - for widget in axis_widgets { - ui.add(widget); - } - let mut response = ui.allocate_rect(complete_rect, Sense::drag()); - response.rect = plot_rect; + let response = ui.allocate_rect(plot_rect, Sense::drag()); let rect = plot_rect; // Load or initialize the memory. let plot_id = ui.make_persistent_id(id_source); @@ -1064,6 +1059,26 @@ impl Plot { } } + for mut widget in axis_widgets { + let axis = widget.config.axis; + let bounds = transform.bounds(); + let axis_range = match axis { + Axis::X => bounds.range_x(), + Axis::Y => bounds.range_y(), + }; + widget.range = axis_range; + let input = GridInput { + bounds: (bounds.min[axis as usize], bounds.max[axis as usize]), + base_step_size: transform.dvalue_dpos()[axis as usize] + * MIN_LINE_SPACING_IN_POINTS + * 2.0, + }; + let steps = (grid_spacers[axis as usize])(input); + widget.transform = Some(transform.clone()); + widget.steps = steps; + ui.add(widget); + } + // Initialize values from functions. for item in &mut items { item.initialize(transform.bounds().range_x()); @@ -1085,6 +1100,7 @@ impl Plot { sharp_grid_lines, clamp_grid, }; + let plot_cursors = prepared.ui(ui, &response); if let Some(boxed_zoom_rect) = boxed_zoom_rect { @@ -1139,7 +1155,7 @@ impl Plot { } else { response }; - + ui.advance_cursor_after_rect(complete_rect); PlotResponse { inner, response, @@ -1375,6 +1391,7 @@ pub struct GridInput { } /// One mark (horizontal or vertical line) in the background grid of a plot. +#[derive(Debug, Clone, Copy)] pub struct GridMark { /// X or Y value in the plot. pub value: f64, @@ -1452,10 +1469,11 @@ impl PreparedPlot { fn ui(self, ui: &mut Ui, response: &Response) -> Vec { let mut axes_shapes = Vec::new(); - for d in 0..2 { - if self.show_axes[d] { - self.paint_axis(ui, d, &mut axes_shapes, self.sharp_grid_lines); - } + if self.show_axes[Axis::X as usize] { + self.paint_axis(ui, Axis::X, &mut axes_shapes, self.sharp_grid_lines); + } + if self.show_axes[Axis::Y as usize] { + self.paint_axis(ui, Axis::Y, &mut axes_shapes, self.sharp_grid_lines); } // Sort the axes by strength so that those with higher strength are drawn in front. @@ -1536,7 +1554,7 @@ impl PreparedPlot { fn paint_axis( &self, ui: &Ui, - axis: usize, + axis: Axis, shapes: &mut Vec<(Shape, f32)>, sharp_grid_lines: bool, ) { @@ -1549,23 +1567,16 @@ impl PreparedPlot { .. } = self; - let bounds = transform.bounds(); - let _axis_range = match axis { - 0 => bounds.range_x(), - 1 => bounds.range_y(), - _ => panic!("Axis {} does not exist.", axis), - }; - - let _font_id = TextStyle::Body.resolve(ui.style()); - // Where on the cross-dimension to show the label values - let value_cross = 0.0_f64.clamp(bounds.min[1 - axis], bounds.max[1 - axis]); + let bounds = transform.bounds(); + let value_cross = + 0.0_f64.clamp(bounds.min[1 - axis as usize], bounds.max[1 - axis as usize]); let input = GridInput { - bounds: (bounds.min[axis], bounds.max[axis]), - base_step_size: transform.dvalue_dpos()[axis] * MIN_LINE_SPACING_IN_POINTS, + bounds: (bounds.min[axis as usize], bounds.max[axis as usize]), + base_step_size: transform.dvalue_dpos()[axis as usize] * MIN_LINE_SPACING_IN_POINTS, }; - let steps = (grid_spacers[axis])(input); + let steps = (grid_spacers[axis as usize])(input); let clamp_range = clamp_grid.then(|| { let mut tight_bounds = PlotBounds::NOTHING; @@ -1581,7 +1592,7 @@ impl PreparedPlot { let value_main = step.value; if let Some(clamp_range) = clamp_range { - if axis == 0 { + if axis == Axis::X { if !clamp_range.range_x().contains(&value_main) { continue; }; @@ -1592,14 +1603,14 @@ impl PreparedPlot { } } - let value = if axis == 0 { - PlotPoint::new(value_main, value_cross) - } else { - PlotPoint::new(value_cross, value_main) + let value = match axis { + Axis::X => PlotPoint::new(value_main, value_cross), + Axis::Y => PlotPoint::new(value_cross, value_main), }; let pos_in_gui = transform.position_from_point(&value); - let spacing_in_points = (transform.dpos_dvalue()[axis] * step.step_size).abs() as f32; + let spacing_in_points = + (transform.dpos_dvalue()[axis as usize] * step.step_size).abs() as f32; if spacing_in_points > MIN_LINE_SPACING_IN_POINTS as f32 { let line_strength = remap_clamp( @@ -1612,11 +1623,11 @@ impl PreparedPlot { let mut p0 = pos_in_gui; let mut p1 = pos_in_gui; - p0[1 - axis] = transform.frame().min[1 - axis]; - p1[1 - axis] = transform.frame().max[1 - axis]; + p0[1 - axis as usize] = transform.frame().min[1 - axis as usize]; + p1[1 - axis as usize] = transform.frame().max[1 - axis as usize]; if let Some(clamp_range) = clamp_range { - if axis == 0 { + if axis == Axis::X { p0.y = transform.position_from_point_y(clamp_range.min[1]); p1.y = transform.position_from_point_y(clamp_range.max[1]); } else {