Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Customize Plot label and cursor texts #1235

Merged
merged 9 commits into from
Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)).

### Changed 🔧
Expand All @@ -46,6 +47,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;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a future PR we could change these to return WidgetText instead, allowing users to color the output.

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