Skip to content

Commit

Permalink
Customize Plot label and cursor texts (#1235)
Browse files Browse the repository at this point in the history
Co-authored-by: Emil Ernerfeldt <[email protected]>
  • Loading branch information
s-nie and emilk authored Feb 15, 2022
1 parent cfad289 commit 8f8eb5d
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 34 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand All @@ -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)).

Expand Down
14 changes: 7 additions & 7 deletions egui/src/widgets/plot/items/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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,
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -1380,7 +1380,7 @@ impl PlotItem for BarChart {
elem: ClosestElem,
shapes: &mut Vec<Shape>,
plot: &PlotConfig<'_>,
_: &CustomLabelFuncRef,
_: &LabelFormatter,
) {
let bar = &self.bars[elem.index];

Expand Down Expand Up @@ -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];

Expand Down Expand Up @@ -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 {
Expand All @@ -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!(
Expand Down
121 changes: 98 additions & 23 deletions egui/src/widgets/plot/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
label_formatter: impl Fn(&str, &Value) -> String + 'static,
) -> Self {
self.label_formatter = Some(Box::new(label_formatter));
self
}

/// Show the pointer coordinates in the plot.
pub fn coordinates_formatter(
mut self,
custom_label_func: impl Fn(&str, &Value) -> String + 'static,
position: Corner,
formatter: CoordinatesFormatter,
) -> Self {
self.custom_label_func = Some(Box::new(custom_label_func));
self.coordinates_formatter = Some((position, formatter));
self
}

/// Provide a function to customize the labels for the X axis.
/// 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
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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>) {
Expand All @@ -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());

Expand Down Expand Up @@ -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
};
Expand Down Expand Up @@ -982,7 +1057,7 @@ impl PreparedPlot {
transform,
show_x,
show_y,
custom_label_func,
label_formatter,
items,
..
} = self;
Expand Down Expand Up @@ -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);
}
}
}
4 changes: 4 additions & 0 deletions egui/src/widgets/plot/transform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 8f8eb5d

Please sign in to comment.