Improve plot grid appearance (#2412)

* improve plot grid appearance

* update changelog

* Update crates/egui/src/widgets/plot/mod.rs

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>

* address review comments

* make lines a bit weaker

* move changelog entry

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
Sven Niederberger 2022-12-19 10:57:30 +01:00 committed by GitHub
parent f9728b88db
commit 7e9c7dac41
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 69 additions and 26 deletions

View file

@ -8,6 +8,7 @@ NOTE: [`epaint`](crates/epaint/CHANGELOG.md), [`eframe`](crates/eframe/CHANGELOG
### Added ⭐ ### 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)). * `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. * 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 `Memory::any_popup_open`, which returns true if any popup is currently open ([#2464](https://github.com/emilk/egui/pull/2464)).
### Fixed 🐛 ### Fixed 🐛

View file

@ -292,6 +292,7 @@ pub struct Plot {
show_background: bool, show_background: bool,
show_axes: [bool; 2], show_axes: [bool; 2],
grid_spacers: [GridSpacer; 2], grid_spacers: [GridSpacer; 2],
sharp_grid_lines: bool,
} }
impl Plot { impl Plot {
@ -331,6 +332,7 @@ impl Plot {
show_background: true, show_background: true,
show_axes: [true; 2], show_axes: [true; 2],
grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)], grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)],
sharp_grid_lines: true,
} }
} }
@ -617,6 +619,13 @@ impl Plot {
self 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. /// Resets the plot.
pub fn reset(mut self) -> Self { pub fn reset(mut self) -> Self {
self.reset = true; self.reset = true;
@ -663,6 +672,7 @@ impl Plot {
linked_axes, linked_axes,
linked_cursors, linked_cursors,
grid_spacers, grid_spacers,
sharp_grid_lines,
} = self; } = self;
// Determine the size of the plot in the UI // 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_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_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.link_y),
draw_cursors, draw_cursors,
sharp_grid_lines,
}; };
let plot_cursors = prepared.ui(ui, &response); let plot_cursors = prepared.ui(ui, &response);
@ -1290,18 +1301,30 @@ struct PreparedPlot {
draw_cursor_x: bool, draw_cursor_x: bool,
draw_cursor_y: bool, draw_cursor_y: bool,
draw_cursors: Vec<Cursor>, draw_cursors: Vec<Cursor>,
sharp_grid_lines: bool,
} }
impl PreparedPlot { impl PreparedPlot {
fn ui(self, ui: &mut Ui, response: &Response) -> Vec<Cursor> { fn ui(self, ui: &mut Ui, response: &Response) -> Vec<Cursor> {
let mut shapes = Vec::new(); let mut axes_shapes = Vec::new();
for d in 0..2 { for d in 0..2 {
if self.show_axes[d] { 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 transform = &self.transform;
let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default()); let mut plot_ui = ui.child_ui(*transform.frame(), Layout::default());
@ -1369,7 +1392,14 @@ impl PreparedPlot {
cursors cursors
} }
fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec<Shape>) { fn paint_axis(
&self,
ui: &Ui,
axis: usize,
other_axis_shown: bool,
shapes: &mut Vec<(Shape, f32)>,
sharp_grid_lines: bool,
) {
let Self { let Self {
transform, transform,
axis_formatters, axis_formatters,
@ -1408,29 +1438,35 @@ impl PreparedPlot {
let pos_in_gui = transform.position_from_point(&value); 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] * step.step_size).abs() as f32;
let line_alpha = remap_clamp( if spacing_in_points > MIN_LINE_SPACING_IN_POINTS as f32 {
spacing_in_points, let line_strength = remap_clamp(
(MIN_LINE_SPACING_IN_POINTS as f32)..=300.0, spacing_in_points,
0.0..=0.15, MIN_LINE_SPACING_IN_POINTS as f32..=300.0,
); 0.0..=1.0,
);
if line_alpha > 0.0 { let line_color = color_from_contrast(ui, line_strength);
let line_color = color_from_alpha(ui, line_alpha);
let mut p0 = pos_in_gui; let mut p0 = pos_in_gui;
let mut p1 = pos_in_gui; let mut p1 = pos_in_gui;
p0[1 - axis] = transform.frame().min[1 - axis]; p0[1 - axis] = transform.frame().min[1 - axis];
p1[1 - axis] = transform.frame().max[1 - axis]; p1[1 - axis] = transform.frame().max[1 - axis];
// Round to avoid aliasing if sharp_grid_lines {
p0 = ui.ctx().round_pos_to_pixels(p0); // Round to avoid aliasing
p1 = ui.ctx().round_pos_to_pixels(p1); p0 = ui.ctx().round_pos_to_pixels(p0);
shapes.push(Shape::line_segment([p0, p1], Stroke::new(1.0, line_color))); 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); const MIN_TEXT_SPACING: f32 = 40.0;
if spacing_in_points > MIN_TEXT_SPACING {
if text_alpha > 0.0 { let text_strength =
let color = color_from_alpha(ui, text_alpha); 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() { let text: String = if let Some(formatter) = axis_formatters[axis].as_deref() {
formatter(value_main, &axis_range) formatter(value_main, &axis_range)
@ -1438,8 +1474,11 @@ impl PreparedPlot {
emath::round_to_decimals(value_main, 5).to_string() // hack 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" // 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 galley = ui.painter().layout_no_wrap(text, font_id.clone(), color);
let mut text_pos = pos_in_gui + vec2(1.0, -galley.size().y); 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_most(transform.frame().max[1 - axis] - galley.size()[1 - axis] - 2.0)
.at_least(transform.frame().min[1 - axis] + 1.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 { fn color_from_contrast(ui: &Ui, contrast: f32) -> Color32 {
if ui.visuals().dark_mode { let bg = ui.visuals().extreme_bg_color;
Rgba::from_white_alpha(alpha).into() let fg = ui.visuals().widgets.open.fg_stroke.color;
} else { let mix = 0.5 * contrast.sqrt();
Rgba::from_black_alpha((4.0 * alpha).at_most(1.0)).into() 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,
)
} }
} }