Customize Plot label and cursor texts (#1235)
Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
This commit is contained in:
parent
cfad28936d
commit
8f8eb5d4a9
5 changed files with 124 additions and 34 deletions
|
@ -24,6 +24,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
|
|||
* Added linked axis support for plots via `plot::LinkedAxisGroup` ([#1184](https://github.com/emilk/egui/pull/1184)).
|
||||
* Added `Response::on_hover_text_at_pointer` as a convenience akin to `Response::on_hover_text` ([1179](https://github.com/emilk/egui/pull/1179)).
|
||||
* Added `ui.weak(text)`.
|
||||
* Added plot pointer coordinates with `Plot::coordinates_formatter`. ([#1235](https://github.com/emilk/egui/pull/1235)).
|
||||
* Added `Slider::step_by` ([1255](https://github.com/emilk/egui/pull/1225)).
|
||||
* Added ability to scroll an UI into view without specifying an alignment ([1247](https://github.com/emilk/egui/pull/1247))
|
||||
|
||||
|
@ -47,6 +48,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
|
|||
* Replaced Frame's `margin: Vec2` with `margin: Margin`, allowing for different margins on opposing sides ([#1219](https://github.com/emilk/egui/pull/1219)).
|
||||
* `Plot::highlight` now takes a `bool` argument ([#1159](https://github.com/emilk/egui/pull/1159)).
|
||||
* `ScrollArea::show` now returns a `ScrollAreaOutput`, so you might need to add `.inner` after the call to it ([#1166](https://github.com/emilk/egui/pull/1166)).
|
||||
* Renamed `Plot::custom_label_func` to `Plot::label_formatter` ([#1235](https://github.com/emilk/egui/pull/1235)).
|
||||
* Tooltips that don't fit the window don't flicker anymore ([#1240](https://github.com/emilk/egui/pull/1240)).
|
||||
* `Areas::layer_id_at` ignores non interatable layers (i.e. Tooltips) ([#1240](https://github.com/emilk/egui/pull/1240)).
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ use epaint::Mesh;
|
|||
|
||||
use crate::*;
|
||||
|
||||
use super::{CustomLabelFuncRef, PlotBounds, ScreenTransform};
|
||||
use super::{LabelFormatter, PlotBounds, ScreenTransform};
|
||||
use rect_elem::*;
|
||||
use values::{ClosestElem, PlotGeometry};
|
||||
|
||||
|
@ -66,7 +66,7 @@ pub(super) trait PlotItem {
|
|||
elem: ClosestElem,
|
||||
shapes: &mut Vec<Shape>,
|
||||
plot: &PlotConfig<'_>,
|
||||
custom_label_func: &CustomLabelFuncRef,
|
||||
label_formatter: &LabelFormatter,
|
||||
) {
|
||||
let points = match self.geometry() {
|
||||
PlotGeometry::Points(points) => points,
|
||||
|
@ -89,7 +89,7 @@ pub(super) trait PlotItem {
|
|||
let pointer = plot.transform.position_from_value(&value);
|
||||
shapes.push(Shape::circle_filled(pointer, 3.0, line_color));
|
||||
|
||||
rulers_at_value(pointer, value, self.name(), plot, shapes, custom_label_func);
|
||||
rulers_at_value(pointer, value, self.name(), plot, shapes, label_formatter);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1380,7 +1380,7 @@ impl PlotItem for BarChart {
|
|||
elem: ClosestElem,
|
||||
shapes: &mut Vec<Shape>,
|
||||
plot: &PlotConfig<'_>,
|
||||
_: &CustomLabelFuncRef,
|
||||
_: &LabelFormatter,
|
||||
) {
|
||||
let bar = &self.bars[elem.index];
|
||||
|
||||
|
@ -1522,7 +1522,7 @@ impl PlotItem for BoxPlot {
|
|||
elem: ClosestElem,
|
||||
shapes: &mut Vec<Shape>,
|
||||
plot: &PlotConfig<'_>,
|
||||
_: &CustomLabelFuncRef,
|
||||
_: &LabelFormatter,
|
||||
) {
|
||||
let box_plot = &self.boxes[elem.index];
|
||||
|
||||
|
@ -1643,7 +1643,7 @@ pub(super) fn rulers_at_value(
|
|||
name: &str,
|
||||
plot: &PlotConfig<'_>,
|
||||
shapes: &mut Vec<Shape>,
|
||||
custom_label_func: &CustomLabelFuncRef,
|
||||
label_formatter: &LabelFormatter,
|
||||
) {
|
||||
let line_color = rulers_color(plot.ui);
|
||||
if plot.show_x {
|
||||
|
@ -1663,7 +1663,7 @@ pub(super) fn rulers_at_value(
|
|||
let scale = plot.transform.dvalue_dpos();
|
||||
let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||
let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).at_most(6);
|
||||
if let Some(custom_label) = custom_label_func {
|
||||
if let Some(custom_label) = label_formatter {
|
||||
custom_label(name, &value)
|
||||
} else if plot.show_x && plot.show_y {
|
||||
format!(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//! Simple plotting library.
|
||||
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use std::{cell::RefCell, ops::RangeInclusive, rc::Rc};
|
||||
|
||||
use crate::*;
|
||||
use epaint::ahash::AHashSet;
|
||||
|
@ -20,12 +20,44 @@ mod items;
|
|||
mod legend;
|
||||
mod transform;
|
||||
|
||||
type CustomLabelFunc = dyn Fn(&str, &Value) -> String;
|
||||
type CustomLabelFuncRef = Option<Box<CustomLabelFunc>>;
|
||||
|
||||
type AxisFormatterFn = dyn Fn(f64) -> String;
|
||||
type LabelFormatterFn = dyn Fn(&str, &Value) -> String;
|
||||
type LabelFormatter = Option<Box<LabelFormatterFn>>;
|
||||
type AxisFormatterFn = dyn Fn(f64, &RangeInclusive<f64>) -> String;
|
||||
type AxisFormatter = Option<Box<AxisFormatterFn>>;
|
||||
|
||||
/// Specifies the coordinates formatting when passed to [`Plot::coordinates_formatter`].
|
||||
pub struct CoordinatesFormatter {
|
||||
function: Box<dyn Fn(&Value, &PlotBounds) -> String>,
|
||||
}
|
||||
|
||||
impl CoordinatesFormatter {
|
||||
/// Create a new formatter based on the pointer coordinate and the plot bounds.
|
||||
pub fn new(function: impl Fn(&Value, &PlotBounds) -> String + 'static) -> Self {
|
||||
Self {
|
||||
function: Box::new(function),
|
||||
}
|
||||
}
|
||||
|
||||
/// Show a fixed number of decimal places.
|
||||
pub fn with_decimals(num_decimals: usize) -> Self {
|
||||
Self {
|
||||
function: Box::new(move |value, _| {
|
||||
format!("x: {:.d$}\ny: {:.d$}", value.x, value.y, d = num_decimals)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn format(&self, value: &Value, bounds: &PlotBounds) -> String {
|
||||
(self.function)(value, bounds)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for CoordinatesFormatter {
|
||||
fn default() -> Self {
|
||||
Self::with_decimals(3)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
/// Information about the plot that has to persist between frames.
|
||||
|
@ -146,7 +178,8 @@ pub struct Plot {
|
|||
|
||||
show_x: bool,
|
||||
show_y: bool,
|
||||
custom_label_func: CustomLabelFuncRef,
|
||||
label_formatter: LabelFormatter,
|
||||
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
|
||||
axis_formatters: [AxisFormatter; 2],
|
||||
legend_config: Option<Legend>,
|
||||
show_background: bool,
|
||||
|
@ -177,7 +210,8 @@ impl Plot {
|
|||
|
||||
show_x: true,
|
||||
show_y: true,
|
||||
custom_label_func: None,
|
||||
label_formatter: None,
|
||||
coordinates_formatter: None,
|
||||
axis_formatters: [None, None], // [None; 2] requires Copy
|
||||
legend_config: None,
|
||||
show_background: true,
|
||||
|
@ -284,7 +318,7 @@ impl Plot {
|
|||
/// });
|
||||
/// let line = Line::new(Values::from_values_iter(sin));
|
||||
/// Plot::new("my_plot").view_aspect(2.0)
|
||||
/// .custom_label_func(|name, value| {
|
||||
/// .label_formatter(|name, value| {
|
||||
/// if !name.is_empty() {
|
||||
/// format!("{}: {:.*}%", name, 1, value.y).to_string()
|
||||
/// } else {
|
||||
|
@ -294,34 +328,50 @@ impl Plot {
|
|||
/// .show(ui, |plot_ui| plot_ui.line(line));
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn custom_label_func(
|
||||
pub fn label_formatter(
|
||||
mut self,
|
||||
custom_label_func: impl Fn(&str, &Value) -> String + 'static,
|
||||
label_formatter: impl Fn(&str, &Value) -> String + 'static,
|
||||
) -> Self {
|
||||
self.custom_label_func = Some(Box::new(custom_label_func));
|
||||
self.label_formatter = Some(Box::new(label_formatter));
|
||||
self
|
||||
}
|
||||
|
||||
/// Provide a function to customize the labels for the X axis.
|
||||
/// Show the pointer coordinates in the plot.
|
||||
pub fn coordinates_formatter(
|
||||
mut self,
|
||||
position: Corner,
|
||||
formatter: CoordinatesFormatter,
|
||||
) -> Self {
|
||||
self.coordinates_formatter = Some((position, formatter));
|
||||
self
|
||||
}
|
||||
|
||||
/// Provide a function to customize the labels for the X axis based on the current visible value range.
|
||||
///
|
||||
/// This is useful for custom input domains, e.g. date/time.
|
||||
///
|
||||
/// If axis labels should not appear for certain values or beyond a certain zoom/resolution,
|
||||
/// the formatter function can return empty strings. This is also useful if your domain is
|
||||
/// discrete (e.g. only full days in a calendar).
|
||||
pub fn x_axis_formatter(mut self, func: impl Fn(f64) -> String + 'static) -> Self {
|
||||
pub fn x_axis_formatter(
|
||||
mut self,
|
||||
func: impl Fn(f64, &RangeInclusive<f64>) -> String + 'static,
|
||||
) -> Self {
|
||||
self.axis_formatters[0] = Some(Box::new(func));
|
||||
self
|
||||
}
|
||||
|
||||
/// Provide a function to customize the labels for the Y axis.
|
||||
/// Provide a function to customize the labels for the Y axis based on the current value range.
|
||||
///
|
||||
/// This is useful for custom value representation, e.g. percentage or units.
|
||||
///
|
||||
/// If axis labels should not appear for certain values or beyond a certain zoom/resolution,
|
||||
/// the formatter function can return empty strings. This is also useful if your Y values are
|
||||
/// discrete (e.g. only integers).
|
||||
pub fn y_axis_formatter(mut self, func: impl Fn(f64) -> String + 'static) -> Self {
|
||||
pub fn y_axis_formatter(
|
||||
mut self,
|
||||
func: impl Fn(f64, &RangeInclusive<f64>) -> String + 'static,
|
||||
) -> Self {
|
||||
self.axis_formatters[1] = Some(Box::new(func));
|
||||
self
|
||||
}
|
||||
|
@ -388,7 +438,8 @@ impl Plot {
|
|||
view_aspect,
|
||||
mut show_x,
|
||||
mut show_y,
|
||||
custom_label_func,
|
||||
label_formatter,
|
||||
coordinates_formatter,
|
||||
axis_formatters,
|
||||
legend_config,
|
||||
show_background,
|
||||
|
@ -630,7 +681,8 @@ impl Plot {
|
|||
items,
|
||||
show_x,
|
||||
show_y,
|
||||
custom_label_func,
|
||||
label_formatter,
|
||||
coordinates_formatter,
|
||||
axis_formatters,
|
||||
show_axes,
|
||||
transform: transform.clone(),
|
||||
|
@ -849,7 +901,8 @@ struct PreparedPlot {
|
|||
items: Vec<Box<dyn PlotItem>>,
|
||||
show_x: bool,
|
||||
show_y: bool,
|
||||
custom_label_func: CustomLabelFuncRef,
|
||||
label_formatter: LabelFormatter,
|
||||
coordinates_formatter: Option<(Corner, CoordinatesFormatter)>,
|
||||
axis_formatters: [AxisFormatter; 2],
|
||||
show_axes: [bool; 2],
|
||||
transform: ScreenTransform,
|
||||
|
@ -877,7 +930,24 @@ impl PreparedPlot {
|
|||
self.hover(ui, pointer, &mut shapes);
|
||||
}
|
||||
|
||||
ui.painter().sub_region(*transform.frame()).extend(shapes);
|
||||
let painter = ui.painter().sub_region(*transform.frame());
|
||||
painter.extend(shapes);
|
||||
|
||||
if let Some((corner, formatter)) = self.coordinates_formatter.as_ref() {
|
||||
if let Some(pointer) = response.hover_pos() {
|
||||
let font_id = TextStyle::Monospace.resolve(ui.style());
|
||||
let coordinate = transform.value_from_position(pointer);
|
||||
let text = formatter.format(&coordinate, transform.bounds());
|
||||
let padded_frame = transform.frame().shrink(4.0);
|
||||
let (anchor, position) = match corner {
|
||||
Corner::LeftTop => (Align2::LEFT_TOP, padded_frame.left_top()),
|
||||
Corner::RightTop => (Align2::RIGHT_TOP, padded_frame.right_top()),
|
||||
Corner::LeftBottom => (Align2::LEFT_BOTTOM, padded_frame.left_bottom()),
|
||||
Corner::RightBottom => (Align2::RIGHT_BOTTOM, padded_frame.right_bottom()),
|
||||
};
|
||||
painter.text(position, anchor, text, font_id, ui.visuals().text_color());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_axis(&self, ui: &Ui, axis: usize, shapes: &mut Vec<Shape>) {
|
||||
|
@ -888,6 +958,11 @@ 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());
|
||||
|
||||
|
@ -947,7 +1022,7 @@ impl PreparedPlot {
|
|||
let color = color_from_alpha(ui, text_alpha);
|
||||
|
||||
let text: String = if let Some(formatter) = axis_formatters[axis].as_deref() {
|
||||
formatter(value_main)
|
||||
formatter(value_main, &axis_range)
|
||||
} else {
|
||||
emath::round_to_decimals(value_main, 5).to_string() // hack
|
||||
};
|
||||
|
@ -982,7 +1057,7 @@ impl PreparedPlot {
|
|||
transform,
|
||||
show_x,
|
||||
show_y,
|
||||
custom_label_func,
|
||||
label_formatter,
|
||||
items,
|
||||
..
|
||||
} = self;
|
||||
|
@ -1012,10 +1087,10 @@ impl PreparedPlot {
|
|||
};
|
||||
|
||||
if let Some((item, elem)) = closest {
|
||||
item.on_hover(elem, shapes, &plot, custom_label_func);
|
||||
item.on_hover(elem, shapes, &plot, label_formatter);
|
||||
} else {
|
||||
let value = transform.value_from_position(pointer);
|
||||
items::rulers_at_value(pointer, value, "", &plot, shapes, custom_label_func);
|
||||
items::rulers_at_value(pointer, value, "", &plot, shapes, label_formatter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -120,6 +120,10 @@ impl PlotBounds {
|
|||
self.min[0]..=self.max[0]
|
||||
}
|
||||
|
||||
pub(crate) fn range_y(&self) -> RangeInclusive<f64> {
|
||||
self.min[1]..=self.max[1]
|
||||
}
|
||||
|
||||
pub(crate) fn make_x_symmetrical(&mut self) {
|
||||
let x_abs = self.min[0].abs().max(self.max[0].abs());
|
||||
self.min[0] = -x_abs;
|
||||
|
|
|
@ -2,8 +2,9 @@ use std::f64::consts::TAU;
|
|||
|
||||
use egui::*;
|
||||
use plot::{
|
||||
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, Corner, HLine, Legend, Line, LineStyle,
|
||||
MarkerShape, Plot, PlotImage, Points, Polygon, Text, VLine, Value, Values,
|
||||
Arrows, Bar, BarChart, BoxElem, BoxPlot, BoxSpread, CoordinatesFormatter, Corner, HLine,
|
||||
Legend, Line, LineStyle, MarkerShape, Plot, PlotImage, Points, Polygon, Text, VLine, Value,
|
||||
Values,
|
||||
};
|
||||
|
||||
#[derive(PartialEq)]
|
||||
|
@ -14,6 +15,7 @@ struct LineDemo {
|
|||
circle_center: Pos2,
|
||||
square: bool,
|
||||
proportional: bool,
|
||||
coordinates: bool,
|
||||
line_style: LineStyle,
|
||||
}
|
||||
|
||||
|
@ -26,6 +28,7 @@ impl Default for LineDemo {
|
|||
circle_center: Pos2::new(0.0, 0.0),
|
||||
square: false,
|
||||
proportional: true,
|
||||
coordinates: true,
|
||||
line_style: LineStyle::Solid,
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +44,7 @@ impl LineDemo {
|
|||
square,
|
||||
proportional,
|
||||
line_style,
|
||||
coordinates,
|
||||
..
|
||||
} = self;
|
||||
|
||||
|
@ -76,6 +80,8 @@ impl LineDemo {
|
|||
.on_hover_text("Always keep the viewport square.");
|
||||
ui.checkbox(proportional, "Proportional data axes")
|
||||
.on_hover_text("Tick are the same size on both axes.");
|
||||
ui.checkbox(coordinates, "Show coordinates")
|
||||
.on_hover_text("Can take a custom formatting function.");
|
||||
|
||||
ComboBox::from_label("Line style")
|
||||
.selected_text(line_style.to_string())
|
||||
|
@ -151,6 +157,9 @@ impl Widget for &mut LineDemo {
|
|||
if self.proportional {
|
||||
plot = plot.data_aspect(1.0);
|
||||
}
|
||||
if self.coordinates {
|
||||
plot = plot.coordinates_formatter(Corner::LeftBottom, CoordinatesFormatter::default());
|
||||
}
|
||||
plot.show(ui, |plot_ui| {
|
||||
plot_ui.line(self.circle());
|
||||
plot_ui.line(self.sin());
|
||||
|
@ -595,7 +604,7 @@ impl ChartsDemo {
|
|||
.name("Set 4")
|
||||
.stack_on(&[&chart1, &chart2, &chart3]);
|
||||
|
||||
let mut x_fmt: fn(f64) -> String = |val| {
|
||||
let mut x_fmt: fn(f64, &std::ops::RangeInclusive<f64>) -> String = |val, _range| {
|
||||
if val >= 0.0 && val <= 4.0 && is_approx_integer(val) {
|
||||
// Only label full days from 0 to 4
|
||||
format!("Day {}", val)
|
||||
|
@ -605,7 +614,7 @@ impl ChartsDemo {
|
|||
}
|
||||
};
|
||||
|
||||
let mut y_fmt: fn(f64) -> String = |val| {
|
||||
let mut y_fmt: fn(f64, &std::ops::RangeInclusive<f64>) -> String = |val, _range| {
|
||||
let percent = 100.0 * val;
|
||||
|
||||
if is_approx_integer(percent) && !is_approx_zero(percent) {
|
||||
|
|
Loading…
Reference in a new issue