diff --git a/CHANGELOG.md b/CHANGELOG.md index ab5654621db4..2d5137620b3b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ 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)). ### Fixed 🐛 diff --git a/crates/egui/src/widgets/plot/mod.rs b/crates/egui/src/widgets/plot/mod.rs index 91c0fb666553..2978f0dd3d89 100644 --- a/crates/egui/src/widgets/plot/mod.rs +++ b/crates/egui/src/widgets/plot/mod.rs @@ -292,6 +292,7 @@ pub struct Plot { show_background: bool, show_axes: [bool; 2], grid_spacers: [GridSpacer; 2], + sharp_grid_lines: bool, } impl Plot { @@ -331,6 +332,7 @@ impl Plot { show_background: true, show_axes: [true; 2], grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)], + sharp_grid_lines: true, } } @@ -617,6 +619,13 @@ impl Plot { self } + /// Round grid positions to full pixels to avoid aliasing. Improves plot appearance but might have an + /// undesired effect when shifting the plot bounds. Enabled by default. + pub fn sharp_grid_lines(mut self, enabled: bool) -> Self { + self.sharp_grid_lines = enabled; + self + } + /// Resets the plot. pub fn reset(mut self) -> Self { self.reset = true; @@ -663,6 +672,7 @@ impl Plot { linked_axes, linked_cursors, grid_spacers, + sharp_grid_lines, } = self; // Determine the size of the plot in the UI @@ -965,6 +975,7 @@ impl Plot { 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, + sharp_grid_lines, }; let plot_cursors = prepared.ui(ui, &response); @@ -1290,18 +1301,30 @@ struct PreparedPlot { draw_cursor_x: bool, draw_cursor_y: bool, draw_cursors: Vec, + sharp_grid_lines: bool, } impl PreparedPlot { fn ui(self, ui: &mut Ui, response: &Response) -> Vec { - let mut shapes = Vec::new(); + let mut axes_shapes = Vec::new(); for d in 0..2 { if self.show_axes[d] { - self.paint_axis(ui, d, &mut shapes); + self.paint_axis( + ui, + d, + self.show_axes[1 - d], + &mut axes_shapes, + self.sharp_grid_lines, + ); } } + // Sort the axes by strength so that those with higher strength are drawn in front. + axes_shapes.sort_by(|(_, strength1), (_, strength2)| strength1.total_cmp(strength2)); + + let mut shapes = axes_shapes.into_iter().map(|(shape, _)| shape).collect(); + let transform = &self.transform; let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default()); @@ -1369,7 +1392,14 @@ impl PreparedPlot { cursors } - fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec) { + fn paint_axis( + &self, + ui: &Ui, + axis: usize, + other_axis_shown: bool, + shapes: &mut Vec<(Shape, f32)>, + sharp_grid_lines: bool, + ) { let Self { transform, axis_formatters, @@ -1408,29 +1438,35 @@ impl PreparedPlot { let pos_in_gui = transform.position_from_point(&value); let spacing_in_points = (transform.dpos_dvalue()[axis] * step.step_size).abs() as f32; - let line_alpha = remap_clamp( - spacing_in_points, - (MIN_LINE_SPACING_IN_POINTS as f32)..=300.0, - 0.0..=0.15, - ); + if spacing_in_points > MIN_LINE_SPACING_IN_POINTS as f32 { + let line_strength = remap_clamp( + spacing_in_points, + MIN_LINE_SPACING_IN_POINTS as f32..=300.0, + 0.0..=1.0, + ); - if line_alpha > 0.0 { - let line_color = color_from_alpha(ui, line_alpha); + let line_color = color_from_contrast(ui, line_strength); 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]; - // 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))); + 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, + )); } - let text_alpha = remap_clamp(spacing_in_points, 40.0..=150.0, 0.0..=0.4); - - if text_alpha > 0.0 { - let color = color_from_alpha(ui, text_alpha); + const MIN_TEXT_SPACING: f32 = 40.0; + if spacing_in_points > MIN_TEXT_SPACING { + let text_strength = + remap_clamp(spacing_in_points, MIN_TEXT_SPACING..=150.0, 0.0..=1.0); + let color = color_from_contrast(ui, text_strength); let text: String = if let Some(formatter) = axis_formatters[axis].as_deref() { formatter(value_main, &axis_range) @@ -1438,8 +1474,11 @@ impl PreparedPlot { emath::round_to_decimals(value_main, 5).to_string() // hack }; + // Skip origin label for y-axis if x-axis is already showing it (otherwise displayed twice) + let skip_origin_y = axis == 1 && other_axis_shown && value_main == 0.0; + // Custom formatters can return empty string to signal "no label at this resolution" - if !text.is_empty() { + if !text.is_empty() && !skip_origin_y { let galley = ui.painter().layout_no_wrap(text, font_id.clone(), color); let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y); @@ -1449,17 +1488,20 @@ impl PreparedPlot { .at_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0) .at_least(transform.frame().min[1 - axis] + 1.0); - shapes.push(Shape::galley(text_pos, galley)); + shapes.push((Shape::galley(text_pos, galley), text_strength)); } } } - 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() - } + fn color_from_contrast(ui: &Ui, contrast: f32) -> Color32 { + let bg = ui.visuals().extreme_bg_color; + let fg = ui.visuals().widgets.open.fg_stroke.color; + let mix = 0.5 * contrast.sqrt(); + Color32::from_rgb( + lerp((bg.r() as f32)..=(fg.r() as f32), mix) as u8, + lerp((bg.g() as f32)..=(fg.g() as f32), mix) as u8, + lerp((bg.b() as f32)..=(fg.b() as f32), mix) as u8, + ) } }