-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Changes from 3 commits
e70d66d
3e17949
c7d572f
d03c242
ad8431f
f6b2915
b24e0f1
8cc5363
a7ed33f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||||||||||
|
@@ -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; | ||||||||||
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(), | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Look at There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, no there it's different. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||||||||||
|
@@ -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, | ||||||||||
|
@@ -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, | ||||||||||
|
@@ -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 { | ||||||||||
|
@@ -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 | ||||||||||
} | ||||||||||
|
@@ -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, | ||||||||||
|
@@ -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(), | ||||||||||
|
@@ -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, | ||||||||||
|
@@ -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>) { | ||||||||||
|
@@ -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()); | ||||||||||
|
||||||||||
|
@@ -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 | ||||||||||
}; | ||||||||||
|
@@ -982,7 +1061,7 @@ impl PreparedPlot { | |||||||||
transform, | ||||||||||
show_x, | ||||||||||
show_y, | ||||||||||
custom_label_func, | ||||||||||
label_formatter, | ||||||||||
items, | ||||||||||
.. | ||||||||||
} = self; | ||||||||||
|
@@ -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); | ||||||||||
} | ||||||||||
} | ||||||||||
} |
There was a problem hiding this comment.
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.