mirror of
https://github.com/emilk/egui.git
synced 2026-09-02 23:00:04 -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,
|
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 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;
|
pub(super) type AxisFormatterFn = fn(f64, &RangeInclusive<f64>) -> String;
|
||||||
|
|
||||||
/// Axis specifier.
|
/// Axis specifier.
|
||||||
@@ -87,7 +92,16 @@ impl AxisConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_formatter(tick: f64, _range: &RangeInclusive<f64>) -> String {
|
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 {
|
pub(super) fn thickness(&self) -> f32 {
|
||||||
@@ -101,49 +115,29 @@ impl AxisConfig {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(super) struct AxisWidget {
|
pub(super) struct AxisWidget {
|
||||||
config: AxisConfig,
|
pub(super) range: RangeInclusive<f64>,
|
||||||
|
pub(super) config: AxisConfig,
|
||||||
pub(super) rect: Rect,
|
pub(super) rect: Rect,
|
||||||
|
pub(super) transform: Option<PlotTransform>,
|
||||||
|
pub(super) steps: Vec<GridMark>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AxisWidget {
|
impl AxisWidget {
|
||||||
/// if `rect` as width or height == 0, is will be automatically calculated from ticks and text.
|
/// 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 {
|
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 {
|
impl Widget for AxisWidget {
|
||||||
fn ui(self, ui: &mut Ui) -> Response {
|
fn ui(self, ui: &mut Ui) -> Response {
|
||||||
|
// --- add label ---
|
||||||
let response = ui.allocate_rect(self.rect, Sense::click_and_drag());
|
let response = ui.allocate_rect(self.rect, Sense::click_and_drag());
|
||||||
if ui.is_rect_visible(response.rect) {
|
if ui.is_rect_visible(response.rect) {
|
||||||
let visuals = ui.style().visuals.clone();
|
let visuals = ui.style().visuals.clone();
|
||||||
@@ -199,7 +193,64 @@ impl Widget for AxisWidget {
|
|||||||
angle,
|
angle,
|
||||||
};
|
};
|
||||||
ui.painter().add(shape);
|
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
|
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.
|
// Allocate the plot window.
|
||||||
// let (rect, response) = ui.allocate_exact_size(size, Sense::drag());
|
// let (rect, response) = ui.allocate_exact_size(size, Sense::drag());
|
||||||
|
let response = ui.allocate_rect(plot_rect, 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 rect = plot_rect;
|
let rect = plot_rect;
|
||||||
// Load or initialize the memory.
|
// Load or initialize the memory.
|
||||||
let plot_id = ui.make_persistent_id(id_source);
|
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.
|
// Initialize values from functions.
|
||||||
for item in &mut items {
|
for item in &mut items {
|
||||||
item.initialize(transform.bounds().range_x());
|
item.initialize(transform.bounds().range_x());
|
||||||
@@ -1085,6 +1100,7 @@ impl Plot {
|
|||||||
sharp_grid_lines,
|
sharp_grid_lines,
|
||||||
clamp_grid,
|
clamp_grid,
|
||||||
};
|
};
|
||||||
|
|
||||||
let plot_cursors = prepared.ui(ui, &response);
|
let plot_cursors = prepared.ui(ui, &response);
|
||||||
|
|
||||||
if let Some(boxed_zoom_rect) = boxed_zoom_rect {
|
if let Some(boxed_zoom_rect) = boxed_zoom_rect {
|
||||||
@@ -1139,7 +1155,7 @@ impl Plot {
|
|||||||
} else {
|
} else {
|
||||||
response
|
response
|
||||||
};
|
};
|
||||||
|
ui.advance_cursor_after_rect(complete_rect);
|
||||||
PlotResponse {
|
PlotResponse {
|
||||||
inner,
|
inner,
|
||||||
response,
|
response,
|
||||||
@@ -1375,6 +1391,7 @@ pub struct GridInput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// One mark (horizontal or vertical line) in the background grid of a plot.
|
/// One mark (horizontal or vertical line) in the background grid of a plot.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct GridMark {
|
pub struct GridMark {
|
||||||
/// X or Y value in the plot.
|
/// X or Y value in the plot.
|
||||||
pub value: f64,
|
pub value: f64,
|
||||||
@@ -1452,10 +1469,11 @@ impl PreparedPlot {
|
|||||||
fn ui(self, ui: &mut Ui, response: &Response) -> Vec<Cursor> {
|
fn ui(self, ui: &mut Ui, response: &Response) -> Vec<Cursor> {
|
||||||
let mut axes_shapes = Vec::new();
|
let mut axes_shapes = Vec::new();
|
||||||
|
|
||||||
for d in 0..2 {
|
if self.show_axes[Axis::X as usize] {
|
||||||
if self.show_axes[d] {
|
self.paint_axis(ui, Axis::X, &mut axes_shapes, self.sharp_grid_lines);
|
||||||
self.paint_axis(ui, d, &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.
|
// Sort the axes by strength so that those with higher strength are drawn in front.
|
||||||
@@ -1536,7 +1554,7 @@ impl PreparedPlot {
|
|||||||
fn paint_axis(
|
fn paint_axis(
|
||||||
&self,
|
&self,
|
||||||
ui: &Ui,
|
ui: &Ui,
|
||||||
axis: usize,
|
axis: Axis,
|
||||||
shapes: &mut Vec<(Shape, f32)>,
|
shapes: &mut Vec<(Shape, f32)>,
|
||||||
sharp_grid_lines: bool,
|
sharp_grid_lines: bool,
|
||||||
) {
|
) {
|
||||||
@@ -1549,23 +1567,16 @@ impl PreparedPlot {
|
|||||||
..
|
..
|
||||||
} = self;
|
} = 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
|
// 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 {
|
let input = GridInput {
|
||||||
bounds: (bounds.min[axis], bounds.max[axis]),
|
bounds: (bounds.min[axis as usize], bounds.max[axis as usize]),
|
||||||
base_step_size: transform.dvalue_dpos()[axis] * MIN_LINE_SPACING_IN_POINTS,
|
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 clamp_range = clamp_grid.then(|| {
|
||||||
let mut tight_bounds = PlotBounds::NOTHING;
|
let mut tight_bounds = PlotBounds::NOTHING;
|
||||||
@@ -1581,7 +1592,7 @@ impl PreparedPlot {
|
|||||||
let value_main = step.value;
|
let value_main = step.value;
|
||||||
|
|
||||||
if let Some(clamp_range) = clamp_range {
|
if let Some(clamp_range) = clamp_range {
|
||||||
if axis == 0 {
|
if axis == Axis::X {
|
||||||
if !clamp_range.range_x().contains(&value_main) {
|
if !clamp_range.range_x().contains(&value_main) {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -1592,14 +1603,14 @@ impl PreparedPlot {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let value = if axis == 0 {
|
let value = match axis {
|
||||||
PlotPoint::new(value_main, value_cross)
|
Axis::X => PlotPoint::new(value_main, value_cross),
|
||||||
} else {
|
Axis::Y => PlotPoint::new(value_cross, value_main),
|
||||||
PlotPoint::new(value_cross, value_main)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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 as usize] * step.step_size).abs() as f32;
|
||||||
|
|
||||||
if spacing_in_points > MIN_LINE_SPACING_IN_POINTS as f32 {
|
if spacing_in_points > MIN_LINE_SPACING_IN_POINTS as f32 {
|
||||||
let line_strength = remap_clamp(
|
let line_strength = remap_clamp(
|
||||||
@@ -1612,11 +1623,11 @@ impl PreparedPlot {
|
|||||||
|
|
||||||
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 as usize] = transform.frame().min[1 - axis as usize];
|
||||||
p1[1 - axis] = transform.frame().max[1 - axis];
|
p1[1 - axis as usize] = transform.frame().max[1 - axis as usize];
|
||||||
|
|
||||||
if let Some(clamp_range) = clamp_range {
|
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]);
|
p0.y = transform.position_from_point_y(clamp_range.min[1]);
|
||||||
p1.y = transform.position_from_point_y(clamp_range.max[1]);
|
p1.y = transform.position_from_point_y(clamp_range.max[1]);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user