-
Notifications
You must be signed in to change notification settings - Fork 88
Introduce type parameter for plot points #76
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
base: main
Are you sure you want to change the base?
Conversation
|
@lucasmerlin @emilk could you have a look at it? |
fffbf1d to
472cf91
Compare
|
@emilk Could you have a look at it again - I think I fixed everything now :-) |
472cf91 to
7d168be
Compare
7d168be to
dbb93ee
Compare
|
@emilk rebased onto main - ready for a re-review again :-) |
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.
Heading in the right direction. It seems like a fairly lightweight addition to the core logic. But as it stands there's a few issues:
- [Blocker] Easy-to-fix bug that causes
itemto always beNone - [Strong recommend] Remove the
Optionalwrapper from(Id, usize) - [Strong recommend] Refactor the demo to avoid the need for
ArcandMutexand make it easier to understand for newcomers.
Assuming the optional is dropped, here's an alternative approach that doesn't need Arc and Mutex. And maybe even more importantly, I think it's easier for the reader to understand the flow of the logic on first parse:
// ...
// Sine data.
let sine_points = (0..=500) // etc...
let sine_id = Id::new("sine_wave");
let sine_line = Line::new(
"sin(x)",
sine_points.iter().map(|p| [p.x, p.y]).collect::<Vec<_>>(),
)
.id(sine_id)
.color(Color32::from_rgb(200, 100, 100));
// Cosine data.
let cosine_points = (0..=500) // etc...
let cosine_id = Id::new("cosine_wave");
let cosine_line = Line::new(
"cos(x)",
cosine_points.iter().map(|p| [p.x, p.y]).collect::<Vec<_>>(),
)
.id(cosine_id)
.color(Color32::from_rgb(100, 200, 100));
// Damped sine data.
let damped_points = (0..=500) // etc...
let damped_id = Id::new("damped_wave");
let dampled_line = Line::new(
"e^(-0.5x) · sin(2x)",
damped_points.iter().map(|p| [p.x, p.y]).collect::<Vec<_>>(),
)
.id(damped_id)
.color(Color32::from_rgb(100, 100, 200));
// Store custom data for easy retrieval. Performance can be improved by caching this in the `UserdataDemo` struct.
let custom_data = HashMap::from([
(sine_id, sine_points),
(cosine_id, cosine_points),
(damped_id, damped_points),
]);
Plot::new("Userdata Plot Demo")
.legend(Legend::default().position(Corner::LeftTop))
.label_formatter(move |name, value, (id, index)| {
if let Some(points) = custom_data.get(&id)
&& let Some(point) = points.get(index)
{
format!(
"{}\nPosition: ({:.3}, {:.3})\nLabel: {}\nImportance: {:.1}%",
name,
value.x,
value.y,
point.custom_label,
point.importance * 100.0
)
} else {
format!("{}\n({:.3}, {:.3})", name, value.x, value.y)
}
})
.show(ui, |plot_ui| {
// Sine wave with custom data
plot_ui.line(sine_line);
// Cosine wave with custom data
plot_ui.line(cosine_line);
// Damped sine wave with custom data
plot_ui.line(dampled_line);
// ... the rest ...You could even include the Line objects in the HashMap, and loop over them in .show() instead of plot_ui.line(...) separately. But doesn't really offer a lot of value in this demo.
| } | ||
|
|
||
| /// Provide a function to customize the on-hover label for the x and y axis | ||
| /// ## `build_fn` parameter |
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.
Might need to rebase. I think the Plot struct and impl are in their own plot.rs file on main now.
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.
Also, I think this doc might be out of date?
It talks about a build_fn, but there's a single label_formatter Fn. The formatter fn does take a third parameter, but that parameter is an Id and index, which aren't documented.
| /// let line = Line::new("sin", sin); | ||
| /// Plot::new("my_plot").view_aspect(2.0) | ||
| /// .label_formatter(|name, value| { | ||
| /// .label_formatter(|name, value, _| { |
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.
Consider naming the third input here and using it to format! the final label.
Suggestion:
Plot::new("my_plot").view_aspect(2.0)
.label_formatter(|name, value, id_index| {
if !name.is_empty() {
if let Some((_id, index)) = id_index {
format!("{}_{}: {:.*}%", name, index, 1, value.y)
} else {
format!("{}: {:.*}%", name, 1, value.y)
}
} else {
"".to_owned()
}
})| let lock = custom_data_.lock(); | ||
| if let Some(points) = lock.get(&id) { | ||
| if let Some(point) = points.get(index) { | ||
| return format!( |
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.
When I run this demo, this branch is never executed. Might need some debugging here.

I haven't parsed it deeply yet, but I can see that the intention is that the .show() capture is called first, and populates custom_data. And then any hover event then calls the .label_formatter capture and queries custom_data_.
It makes me slightly nervous. Like, it makes sense that show() will be called before label_formatter, but that's not explicitly documented or guaranteed anywhere.
But I'm looking at this, and thinking about how I would debug it, and thinking it will be unpleasant. And that's usually not a good sign. I'm going to keep reading and wrap my head around the full (intended) flow here. And hopefully I can come up with a more constructive suggestion for an alternative design here.
| Plot::new("Userdata Plot Demo") | ||
| .legend(Legend::default().position(Corner::LeftTop)) | ||
| .label_formatter(move |name, value, item| { | ||
| if let Some((id, index)) = item { |
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.
I believe item is always None here when I run the demo
|
|
||
| let text = if let Some(custom_label) = label_formatter { | ||
| let label = custom_label(name, &value); | ||
| let label = custom_label(name, &value, None); |
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.
Should probably be
let label = custom_label(name, &value, None);
| let x_decimals = ((-scale[0].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6); | ||
| let y_decimals = ((-scale[1].abs().log10()).ceil().at_least(0.0) as usize).clamp(1, 6); | ||
| if plot.show_x && plot.show_y { | ||
| if let Some(custom_label) = 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.
This branch will never be true, since this is already in a branch in which label_formatter == None
|
|
||
| fn geometry(&self) -> PlotGeometry<'_> { | ||
| PlotGeometry::Points(self.origins.points()) | ||
| PlotGeometry::Points(self.origins.points(), Some(self.id())) |
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.
As far as I can see, self.id() is always available. See my comment in values.rs
|
|
||
| /// Point values (X-Y graphs) | ||
| Points(&'a [PlotPoint]), | ||
| Points(&'a [PlotPoint], Option<Id>), |
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.
I believe this can just be Id. And this would simplify a number of consumer sites, and most importantly the closure defined by the user for .label_formatter.

This pull request introduces a new "Userdata" demo panel to the plot demo, showcasing how custom per-point user data can be attached and accessed during label formatting. It also refactors the plot item geometry to include an optional
Id, allowing label formatters to receive both the item identifier and point index for richer tooltip customization. Several snapshot images are updated to reflect these demo and API changes.Userdata demo and API enhancements:
Userdatademo panel toPlotDemo, demonstrating how to associate custom information with each plotted point and display it in tooltips using the label formatter.PlotGeometry::Pointsto carry an optionalId, enabling identification of the plot item during hover interactions.(Id, index), allowing tooltips to display custom per-point data.Line,Polygon,Points,Arrows) to provide theirIdin the geometry for correct user data association.These changes make it easier to build interactive plots with custom per-point data and tooltips, and provide a clear demo for users to follow.
Reopening emilk/egui#4719