Skip to content

ENH Add a tooltip for the RocCurveDisplay #31860

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

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

jeremiedbb
Copy link
Member

@jeremiedbb jeremiedbb commented Jul 31, 2025

This PR proposes to add a tooltip along the curves of RocCurveDisplay displaying the fpr, tpr and threshold values.
line_tooltip_1

  • It switches from curve to curve when plotting results from cross validation. There's 1 matplotlib annotation per RocCurveDisplay instance.
line_tooltip_2 line_tooltip_3
  • It also switches from curve to curve when multiple displays share the same axes. Even if there are 2 annotations for the axes (one per display), only 1 is visible at once.
line_tooltip_4 line_tooltip_5
  • Regarding implementation, it's done through a Mixin class that is responsible for creating the annotation and handle the mouse events occurring on the curve. The mixin is in _plotting and uses generic variable names because it's not dedicated to RocCurveDisplay, but is instead ready to be used in other displays.

  • Currently, the tooltip will always be visible on hovering a curve. We could add a parameter to enable it or not. Not sure it's necessary but why not. Or maybe a global config ?

  • The tooltip is displayed in notebooks if %matplotlib widget is set. Otherwise, the plot works as usual, but the tooltip is not visible.

line_tooltip_6

cc/ @glemaitre

Copy link

github-actions bot commented Jul 31, 2025

✔️ Linting Passed

All linting checks passed. Your pull request is in excellent shape! ☀️

Generated for commit: 7f7e30e. Link to the linter CI: here

Comment on lines 294 to 297
# Set an attribute on the axes annotation to be able to keep only one visible
# at a time when there are multiple display instances that share an axes.
setattr(self.line_tooltip_, "_skl_line_tooltip", True)

Copy link
Member Author

@jeremiedbb jeremiedbb Jul 31, 2025

Choose a reason for hiding this comment

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

This is not ideal, we set an attribute on a matplotlib object. An alternative could be to have a class attribute that is a mapping from axes to something and reuse the same matplotlib annotation if a new display instance reuses the axes. But it's not very clean either...
Another alternative could be to subclass matplotlib.axes.Axes to add some metadata, but then the display.ax_ won't be a proper matplotlib.axes.Axes instance, so still not ideal...

Copy link
Member

Choose a reason for hiding this comment

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

won't be a proper matplotlib.axes.Axes instance, so still not ideal

Why do you think that would be a problem? Subclassing sounds a bit cleaner, unless the matplotlib documentation explicitly discourages subclassing for one reason or another.

Copy link
Member Author

Choose a reason for hiding this comment

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

I did the subclassing in aea3c92. (I meant subclassing Annotation). I also find it a bit cleaner. What I find problematic is not visible in this PR yet because I didn't document the new attribute. But it would be something like:

line_tooltip_ : matplotlib.text.Annotation instance  (or now) LineTooltip instance
    Annotation along the roc curve...

And that annotation is customizable using the Annotation API. A bit like we expose ax_, figure_, line_ for the user to be able to customize the plot. This is not clear if it's not documented as an Annotation object.

Maybe we can mention explicitly in the line_tooltip_ description that it's a subclass of Annotation and that's enough. What do you think ?

Comment on lines +362 to +366
# Compute an offset for the text depending on the quadrant where the cursor is
# to keep the tooltip somewhat inside the axes.
xlim, ylim = self.ax_.get_xlim(), self.ax_.get_ylim()
x_offset = 20 if x < (xlim[0] + xlim[1]) / 2 else -160
y_offset = 20 if y < (ylim[0] + ylim[1]) / 2 else -30
Copy link
Member Author

Choose a reason for hiding this comment

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

These hardcoded numbers are not perfect for all cases. We can have a constant offset and let the tooltip be outside the axes sometimes, or I can try to find a way to use the text length to have a matching offset.

@adrinjalali
Copy link
Member

@lucyleeow could you please maybe have a look?

@ogrisel
Copy link
Member

ogrisel commented Aug 4, 2025

Could you update the ROC curve display examples to set %matplotlib widget and see if it renders as expected in the HTML doc?

@lesteve
Copy link
Member

lesteve commented Aug 4, 2025

Agreed that it would be nice to see how this works inside a notebook with %matplotlib widget (need to install ipympl I think). Maybe I am wrong but I don't think there is a way for sphinx-gallery to capture the interactive widget so I am not sure it will work with our HTML doc.

@jeremiedbb jeremiedbb force-pushed the display-curve-hover branch from 011bd70 to d6d6998 Compare August 4, 2025 16:21
@jeremiedbb
Copy link
Member Author

Could you update the ROC curve display examples to set %matplotlib widget and see if it renders as expected in the HTML doc?

How can I add %matplotlib widget in an example and still make it run as a python script ?

@jeremiedbb
Copy link
Member Author

Maybe the easiest way to try it yourself is to fetch my branch and run one of these:

  • single curve
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import RocCurveDisplay
import matplotlib.pyplot as plt
 
X, y = make_classification(n_samples=200, class_sep=0.5)
clf = LogisticRegression().fit(X, y)
RocCurveDisplay.from_estimator(clf, X, y)
plt.show()
  • cv results
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.metrics import RocCurveDisplay
from sklearn.model_selection import cross_validate
from sklearn.linear_model import LogisticRegression

X, y = make_classification(n_samples=200, class_sep=0.5, random_state=0)
clf = LogisticRegression()
cv_results = cross_validate(
    clf, X, y, cv=3, return_estimator=True, return_indices=True
)
RocCurveDisplay.from_cv_results(cv_results, X, y)
plt.show()
  • several displays
from sklearn.metrics import RocCurveDisplay
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
import matplotlib.pyplot as plt

X, y = make_classification(n_samples=200, class_sep=0.5)
X_, y_ = make_classification(n_samples=200, class_sep=0.85)
clf = LogisticRegression().fit(X, y)
clf2 = LogisticRegression().fit(X_, y_)
disp = RocCurveDisplay.from_estimator(clf, X, y)
RocCurveDisplay.from_estimator(clf2, X_, y_, ax=disp.ax_)
plt.show()

@ogrisel
Copy link
Member

ogrisel commented Aug 5, 2025

How can I add %matplotlib widget in an example and still make it run as a python script?

That's a good question, I spoke too quickly because I am used to working with jupytext/jupyterbook on other projects. For the scikit-learn doc, we would probably need non-trivial changes sphinx-gallery to allow for this.

I know that jupytext for instance can convert python script with commented out lines that start with # %magic command into a notebook with the uncommented %magic command in the generated .ipynb file before rendering it with jupyter. Something similar would have to be implemented in sphinx gallery.

contain values for a single curve. If plotting multiple curves, list should be
of same length as `fpr` and `tpr`.
Only used to display the threshold values along the curve as a tooltip. If None,
only the fpr and tpr values are displayed.
Copy link
Member

Choose a reason for hiding this comment

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

I think this docstring should mention that it's necessary to install the ipympl package and use the %matplotlib widget magic to use this feature in jupyter notebooks.

Copy link
Member

Choose a reason for hiding this comment

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

The user guide could also benefit from a new paragraph to explain how to enable such interactive tooltips.

@ogrisel
Copy link
Member

ogrisel commented Aug 5, 2025

BTW, I tried in a VS Code interactive window with %matplotlib widget after installing the ipympl package, and it works great.

@ogrisel
Copy link
Member

ogrisel commented Aug 5, 2025

The mixin is in _plotting and uses generic variable names because it's not dedicated to RocCurveDisplay, but is instead ready to be used in other displays.

Can you try to add it to at least one other display (e.g. PR curve) to check that the design is generic enough to be reused?

@jeremiedbb
Copy link
Member Author

Can you try to add it to at least one other display (e.g. PR curve) to check that the design is generic enough to be reused?

Done in e916a77.

@jeremiedbb
Copy link
Member Author

That's a good question, I spoke too quickly because I am used to working with jupytext/jupyterbook on other projects. For the scikit-learn doc, we would probably need non-trivial changes sphinx-gallery to allow for this.

I know that jupytext for instance can convert python script with commented out lines that start with # %magic command into a notebook with the uncommented %magic command in the generated .ipynb file before rendering it with jupyter. Something similar would have to be implemented in sphinx gallery.

Looks like there's a way but in the SG config file (see d0735d5). It's not visible in the HTML rendered examples but is there in the generated notebook. In all generated notebooks actually.

I wanted to check the jupyterlite button from the generated example but the tooltip doesn't show up :(

Also I didn't find where to request ipympl to be installed for jupyterlite. @lesteve can you show me ?

@jeremiedbb
Copy link
Member Author

By the way, sphing-gallery supports examples using plotly (see https://sphinx-gallery.github.io/dev/auto_plotly_examples/plot_0_plotly.html for instance) so it might be possible to have support for matplotlib interactive plots at some point, we'll see.

@lucyleeow
Copy link
Member

What would be required for an interactive plot to be rendered - i.e. what would the rst look like?

Plotly simply uses the capture_repr configuration - essentially the following rst header:

.. raw:: html

        <div class="output_subarea output_html rendered_html output_result">

followed by the html of the figure.

Note that matplotlib animations are also supported. Depending on what is set as the format in config, either raw html is embedded (similar to above) or a video file is saved and a video directive (I think this needs the sphinxcontrib-video extension) is used.
See: https://github.com/sphinx-gallery/sphinx-gallery/blob/df6ca880069a891b565fb62598a036f21d55ed27/sphinx_gallery/scrapers.py#L233

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

Successfully merging this pull request may close these issues.

5 participants