1
0
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:
Johannes Schiffer
2022-11-11 22:21:55 +01:00
committed by JohannesProgrammiert
parent 26494597eb
commit ffb1a45651
2 changed files with 130 additions and 68 deletions

View File

@@ -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()
}
}

View File

@@ -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 {