Skip to content

Conversation

@bircni
Copy link
Contributor

@bircni bircni commented Feb 8, 2025

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:

  • Added a new Userdata demo panel to PlotDemo, demonstrating how to associate custom information with each plotted point and display it in tooltips using the label formatter.
  • Refactored PlotGeometry::Points to carry an optional Id, enabling identification of the plot item during hover interactions.
  • Updated the label formatter API to receive an optional tuple of (Id, index), allowing tooltips to display custom per-point data.
  • Modified all plot item implementations (Line, Polygon, Points, Arrows) to provide their Id in 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

@bircni
Copy link
Contributor Author

bircni commented May 4, 2025

@lucasmerlin @emilk could you have a look at it?
The issue has several upvotes

@bircni bircni force-pushed the plot-point-userdata branch 2 times, most recently from fffbf1d to 472cf91 Compare August 8, 2025 19:04
@bircni
Copy link
Contributor Author

bircni commented Aug 8, 2025

@emilk Could you have a look at it again - I think I fixed everything now :-)

@bircni bircni force-pushed the plot-point-userdata branch from 472cf91 to 7d168be Compare August 8, 2025 19:09
@bircni bircni requested a review from emilk August 10, 2025 14:40
@bircni bircni force-pushed the plot-point-userdata branch from 7d168be to dbb93ee Compare November 7, 2025 13:19
@bircni
Copy link
Contributor Author

bircni commented Nov 7, 2025

@emilk rebased onto main - ready for a re-review again :-)

@michalsustr michalsustr self-assigned this Nov 22, 2025
@michalsustr
Copy link
Collaborator

I like the idea of this PR but I am super confused. It seems the label formatter doesn't update the tooltip with this data?

{}\nPosition: ({:.3}, {:.3})\nLabel: {}\nImportance: {:.1}%

There is just label and (x,y) - no extra new stuff.
image

Copy link

@kitizz kitizz left a 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 item to always be None
  • [Strong recommend] Remove the Optional wrapper from (Id, usize)
  • [Strong recommend] Refactor the demo to avoid the need for Arc and Mutex and 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
Copy link

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.

Copy link

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, _| {
Copy link

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!(
Copy link

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.
Screenshot 2025-12-13 at 08 40 52

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 {
Copy link

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);
Copy link

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 {
Copy link

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()))
Copy link

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>),
Copy link

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

custom data for hover label

4 participants