1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-30 13:20:05 -04:00

egui_plot: customizable spacing of grid and axis label spacing (#3896)

This lets users specify the spacing of the grid lines and the axis
labels, as well as when these start to fade out.

New:
* `AxisHints::new_x/new_y` (replaces `::default()`)
* `AxisHints::label_spacing`
* `Plot::grid_spacing`
This commit is contained in:
Emil Ernerfeldt
2024-01-26 13:36:49 +01:00
committed by GitHub
parent 6b0782c96b
commit 5d0bc2bf7d
3 changed files with 239 additions and 190 deletions

View File

@@ -79,10 +79,6 @@ impl Default for CoordinatesFormatter {
// ----------------------------------------------------------------------------
const MIN_LINE_SPACING_IN_POINTS: f64 = 6.0; // TODO(emilk): large enough for a wide label
// ----------------------------------------------------------------------------
/// Indicates a vertical or horizontal cursor line in plot coordinates.
#[derive(Copy, Clone, PartialEq)]
enum Cursor {
@@ -175,7 +171,9 @@ pub struct Plot {
legend_config: Option<Legend>,
show_background: bool,
show_axes: Vec2b,
show_grid: Vec2b,
grid_spacing: Rangef,
grid_spacers: [GridSpacer; 2],
sharp_grid_lines: bool,
clamp_grid: bool,
@@ -213,12 +211,14 @@ impl Plot {
show_y: true,
label_formatter: None,
coordinates_formatter: None,
x_axes: vec![Default::default()],
y_axes: vec![Default::default()],
x_axes: vec![AxisHints::new(Axis::X)],
y_axes: vec![AxisHints::new(Axis::Y)],
legend_config: None,
show_background: true,
show_axes: true.into(),
show_grid: true.into(),
grid_spacing: Rangef::new(8.0, 300.0),
grid_spacers: [log_grid_spacer(10), log_grid_spacer(10)],
sharp_grid_lines: true,
clamp_grid: false,
@@ -453,6 +453,17 @@ impl Plot {
self
}
/// Set when the grid starts showing.
///
/// When grid lines are closer than the given minimum, they will be hidden.
/// When they get further apart they will fade in, until the reaches the given maximum,
/// at which point they are fully opaque.
#[inline]
pub fn grid_spacing(mut self, grid_spacing: impl Into<Rangef>) -> Self {
self.grid_spacing = grid_spacing.into();
self
}
/// Clamp the grid to only be visible at the range of data where we have values.
///
/// Default: `false`.
@@ -624,12 +635,12 @@ impl Plot {
/// Specify custom formatter for ticks on the main X-axis.
///
/// Arguments of `fmt`:
/// * raw tick value as `f64`.
/// * the grid mark to format
/// * maximum requested number of characters per tick label.
/// * currently shown range on this axis.
pub fn x_axis_formatter(
mut self,
fmt: impl Fn(f64, usize, &RangeInclusive<f64>) -> String + 'static,
fmt: impl Fn(GridMark, usize, &RangeInclusive<f64>) -> String + 'static,
) -> Self {
if let Some(main) = self.x_axes.first_mut() {
main.formatter = Arc::new(fmt);
@@ -640,12 +651,12 @@ impl Plot {
/// Specify custom formatter for ticks on the main Y-axis.
///
/// Arguments of `fmt`:
/// * raw tick value as `f64`.
/// * the grid mark to format
/// * maximum requested number of characters per tick label.
/// * currently shown range on this axis.
pub fn y_axis_formatter(
mut self,
fmt: impl Fn(f64, usize, &RangeInclusive<f64>) -> String + 'static,
fmt: impl Fn(GridMark, usize, &RangeInclusive<f64>) -> String + 'static,
) -> Self {
if let Some(main) = self.y_axes.first_mut() {
main.formatter = Arc::new(fmt);
@@ -723,6 +734,7 @@ impl Plot {
show_background,
show_axes,
show_grid,
grid_spacing,
linked_axes,
linked_cursors,
@@ -827,7 +839,7 @@ impl Plot {
}
// Allocate the plot window.
let response = ui.allocate_rect(plot_rect, Sense::drag());
let response = ui.allocate_rect(plot_rect, Sense::click_and_drag());
let rect = plot_rect;
// Load or initialize the memory.
@@ -1131,7 +1143,7 @@ impl Plot {
let x_steps = Arc::new({
let input = GridInput {
bounds: (bounds.min[0], bounds.max[0]),
base_step_size: transform.dvalue_dpos()[0] * MIN_LINE_SPACING_IN_POINTS * 2.0,
base_step_size: transform.dvalue_dpos()[0].abs() * grid_spacing.min as f64,
};
(grid_spacers[0])(input)
});
@@ -1139,7 +1151,7 @@ impl Plot {
let y_steps = Arc::new({
let input = GridInput {
bounds: (bounds.min[1], bounds.max[1]),
base_step_size: transform.dvalue_dpos()[1] * MIN_LINE_SPACING_IN_POINTS * 2.0,
base_step_size: transform.dvalue_dpos()[1].abs() * grid_spacing.min as f64,
};
(grid_spacers[1])(input)
});
@@ -1168,6 +1180,7 @@ impl Plot {
label_formatter,
coordinates_formatter,
show_grid,
grid_spacing,
transform,
draw_cursor_x: linked_cursors.as_ref().map_or(false, |group| group.1.x),
draw_cursor_y: linked_cursors.as_ref().map_or(false, |group| group.1.y),
@@ -1565,6 +1578,8 @@ pub struct GridInput {
///
/// Computed as the ratio between the diagram's bounds (in plot coordinates) and the viewport
/// (in frame/window coordinates), scaled up to represent the minimal possible step.
///
/// Always positive.
pub base_step_size: f64,
}
@@ -1634,6 +1649,7 @@ struct PreparedPlot {
// axis_formatters: [AxisFormatter; 2],
transform: PlotTransform,
show_grid: Vec2b,
grid_spacing: Rangef,
grid_spacers: [GridSpacer; 2],
draw_cursor_x: bool,
draw_cursor_y: bool,
@@ -1648,10 +1664,10 @@ impl PreparedPlot {
let mut axes_shapes = Vec::new();
if self.show_grid.x {
self.paint_grid(ui, &mut axes_shapes, Axis::X);
self.paint_grid(ui, &mut axes_shapes, Axis::X, self.grid_spacing);
}
if self.show_grid.y {
self.paint_grid(ui, &mut axes_shapes, Axis::Y);
self.paint_grid(ui, &mut axes_shapes, Axis::Y, self.grid_spacing);
}
// Sort the axes by strength so that those with higher strength are drawn in front.
@@ -1728,7 +1744,7 @@ impl PreparedPlot {
cursors
}
fn paint_grid(&self, ui: &Ui, shapes: &mut Vec<(Shape, f32)>, axis: Axis) {
fn paint_grid(&self, ui: &Ui, shapes: &mut Vec<(Shape, f32)>, axis: Axis, fade_range: Rangef) {
#![allow(clippy::collapsible_else_if)]
let Self {
transform,
@@ -1746,7 +1762,7 @@ impl PreparedPlot {
let input = GridInput {
bounds: (bounds.min[iaxis], bounds.max[iaxis]),
base_step_size: transform.dvalue_dpos()[iaxis] * MIN_LINE_SPACING_IN_POINTS,
base_step_size: transform.dvalue_dpos()[iaxis].abs() * fade_range.min as f64,
};
let steps = (grid_spacers[iaxis])(input);
@@ -1786,44 +1802,42 @@ impl PreparedPlot {
let pos_in_gui = transform.position_from_point(&value);
let spacing_in_points = (transform.dpos_dvalue()[iaxis] * step.step_size).abs() as f32;
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 spacing_in_points <= fade_range.min {
continue; // Too close together
}
let line_color = color_from_strength(ui, line_strength);
let line_strength = remap_clamp(spacing_in_points, fade_range, 0.0..=1.0);
let mut p0 = pos_in_gui;
let mut p1 = pos_in_gui;
p0[1 - iaxis] = transform.frame().min[1 - iaxis];
p1[1 - iaxis] = transform.frame().max[1 - iaxis];
let line_color = color_from_strength(ui, line_strength);
if let Some(clamp_range) = clamp_range {
match 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]);
}
Axis::Y => {
p0.x = transform.position_from_point_x(clamp_range.min[0]);
p1.x = transform.position_from_point_x(clamp_range.max[0]);
}
let mut p0 = pos_in_gui;
let mut p1 = pos_in_gui;
p0[1 - iaxis] = transform.frame().min[1 - iaxis];
p1[1 - iaxis] = transform.frame().max[1 - iaxis];
if let Some(clamp_range) = clamp_range {
match 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]);
}
Axis::Y => {
p0.x = transform.position_from_point_x(clamp_range.min[0]);
p1.x = transform.position_from_point_x(clamp_range.max[0]);
}
}
if self.sharp_grid_lines {
// Round to avoid aliasing
p0 = ui.painter().round_pos_to_pixels(p0);
p1 = ui.painter().round_pos_to_pixels(p1);
}
shapes.push((
Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)),
line_strength,
));
}
if self.sharp_grid_lines {
// Round to avoid aliasing
p0 = ui.painter().round_pos_to_pixels(p0);
p1 = ui.painter().round_pos_to_pixels(p1);
}
shapes.push((
Shape::line_segment([p0, p1], Stroke::new(1.0, line_color)),
line_strength,
));
}
}
@@ -1932,12 +1946,6 @@ pub fn format_number(number: f64, num_decimals: usize) -> String {
/// Determine a color from a 0-1 strength value.
pub fn color_from_strength(ui: &Ui, strength: f32) -> Color32 {
let bg = ui.visuals().extreme_bg_color;
let fg = ui.visuals().widgets.open.fg_stroke.color;
let mix = 0.5 * strength.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,
)
let base_color = ui.visuals().text_color();
base_color.gamma_multiply(strength.sqrt())
}