mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 21:00:03 -04:00
First Draft of axis labels outside of plotting window
This commit is contained in:
committed by
JohannesProgrammiert
parent
26494597eb
commit
ffb1a45651
@@ -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<f64>) -> String;
|
||||
|
||||
/// Axis specifier.
|
||||
@@ -87,7 +92,16 @@ impl AxisConfig {
|
||||
}
|
||||
|
||||
fn default_formatter(tick: f64, _range: &RangeInclusive<f64>) -> 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<f64>,
|
||||
pub(super) config: AxisConfig,
|
||||
pub(super) rect: Rect,
|
||||
pub(super) transform: Option<PlotTransform>,
|
||||
pub(super) steps: Vec<GridMark>,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Cursor> {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user