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 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 7 additions & 7 deletions egui/src/widgets/plot/items/mod.rs
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
125 changes: 102 additions & 23 deletions egui/src/widgets/plot/mod.rs
@@ -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,48 @@ 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_precision(precision: usize) -> Self {
s-nie marked this conversation as resolved.
Show resolved Hide resolved
Self {
function: Box::new(move |value, _| {
format!(
"x: {}\ny: {}",
emath::round_to_decimals(value.x, precision).to_string(),
emath::round_to_decimals(value.y, precision).to_string(),
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
emath::round_to_decimals(value.x, precision).to_string(),
emath::round_to_decimals(value.y, precision).to_string(),
format!("{:.*}", decimal_places, value.x),
format!("{:.*}", decimal_places, value.y),

Look at emath::round_to_decimals and you see why the old code was weird :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Haha I see, then we should also replace it down here right?

https://github.com/emilk/egui/blob/master/egui/src/widgets/plot/mod.rs#L952

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, no there it's different.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed in f6b2915

)
}),
}
}

fn format(&self, value: &Value, bounds: &PlotBounds) -> String {
(self.function)(value, bounds)
}
}

impl Default for CoordinatesFormatter {
fn default() -> Self {
Self::with_precision(3)
}
}

// ----------------------------------------------------------------------------

/// Information about the plot that has to persist between frames.
Expand Down Expand Up @@ -146,7 +182,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 +214,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 +322,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 +332,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 value range.
s-nie marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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 +442,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 +685,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 +905,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 +934,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 +962,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 +1026,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 +1061,7 @@ impl PreparedPlot {
transform,
show_x,
show_y,
custom_label_func,
label_formatter,
items,
..
} = self;
Expand Down Expand Up @@ -1012,10 +1091,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
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