3

Short question

I want to capture coordinates by clicking different locations with a mouse on a Matplotlib figure inside a Jupyter Notebook. I want to use ipywidgets without using any Matplotlib magic command (like %matplotlib ipympl) to switch the backend and without using extra packages apart from Matplotlib, ipywidgets and Numpy.

Detailed explanation

I know how to achieve this using the ipympl package and the corresponding Jupyter magic command %matplotlib ipympl to switch the backend from inline to ipympl (see HERE).

After installing ipympl, e.g. with conda install ipympl, and switching to the ipympl backend, one can follow this procedure to capture mouse click coordinates in Matplotlib.

import matplotlib.pyplot as plt

# Function to store mouse-click coordinates
def onclick(event):
    x, y = event.xdata, event.ydata
    plt.plot(x, y, 'ro')
    xy.append((x, y))

# %%
# Start Matplotlib interactive mode
%matplotlib ipympl  

plt.plot([0, 1])
xy = []     # Initializes coordinates
plt.connect('button_press_event', onclick)

enter image description here

However, I find this switching back and forth between inline and ipympl backend quite confusing in a Notebook.

An alternative for interactive Matplotlib plotting in Jupyter Notebook is to use the ipywidgets package. For example, with the interact command one can easily create sliders for Matplotlib plots, without the need to switcxh backend. (see HERE).

from ipywidgets import interact
import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0, 2 * np.pi)

def update(w=1.0):
    plt.plot(np.sin(w * x))
    plt.show()

interact(update);

enter image description here

However, I have not found a way to use the ipywidgets package to capture (x,y) coordinates from mouse clicks, equivalent to my above example using ipympl.

6
  • Yes, but in your 'interactive' example with ipywidgets' interact() alone, you are only interacting via your mouse with the widget and so that is the mouse-responsive component, i.e., user interface. You want the mouse-responsive element in your idea to be the plot though, and I think you cannot use inline for that because it makes it non-responsive, i.e., there is no user interface for the plot. My understanding is the way the standard Matplotlib backend interacts with Jupyter notebooks, there's no way to detect clicks on it with inline. Commented Oct 11, 2024 at 15:13
  • @Wayne. Indeed, with my question, I am trying to understand whether it is possible. Either a positive or negative well-documented answer will be useful. Commented Oct 14, 2024 at 7:27
  • The Matplotlib doc explicitly states: To get the interactive functionality …, you must be using an interactive backend. The default backend in notebooks, the inline backend, is not. backend_inline renders the figure once and inserts a static image into the notebook when the cell is executed. Because the images are static, they cannot be panned / zoomed, take user input, or be updated from other cells. So you will need a different backend. Does this answer your question? Commented Oct 21, 2024 at 8:59
  • @simon thanks. In my example above with ipywidget one can change the Matplotlib plot interactively without changing backend. And what I need is possible e.g. with Plotly plotly.com/python/click-events. Is the same really impossible with Matplotlib? I am looking for an autoritative answer from someone expert with the Matplotlib and ipywidget internals. Commented Oct 21, 2024 at 9:45
  • 1
    @divenex I think Wayne already answered what is going on in interactively changing the plot without changing the backend. You don't want to interactively change the plot, however, you want to capture inputs from a static image (see again the doc that I cited above), so what you do with ipywidgets and what you want to achieve are two fundamentally different things. Also, what works with Plotly also does work with Matplotlib – if you change to an interactive backend. In any case, I hope someone will give you the authoritative answer that you are asking for. Commented Oct 21, 2024 at 9:57

2 Answers 2

2
+50

Short answer

Capturing mouse clicks on a non-interactive Matplotlib figure is not possible – that's what the interactive backends are for. If you want to avoid switching back and forth between non-interactive and interactive backends, maybe try the reverse approach: Rather than trying to get interactivity from non-interactive plots, use an interactive backend by default, and disable interactivity where it is not necessary.

Detailed answer

What Matplotlib says

Regarding interactivity, Matplotlib's documentation explicitly states (emphasis by me):

To get interactive figures in the 'classic' notebook or Jupyter lab, use the ipympl backend (must be installed separately) which uses the ipywidget framework.

And further down:

The default backend in notebooks, the inline backend, is not [interactive]. backend_inline renders the figure once and inserts a static image into the notebook when the cell is executed. Because the images are static, they cannot be panned / zoomed, take user input, or be updated from other cells.

I guess that should make the situation pretty clear.

Interactivity with ipywidgets

As you noted, you can interact with (static) Matplotlib figures using ipywidgets. What happens there, however, is the following: The widgets (e.g. the slider that you show) are interactive, while the figure is still not. So "interactivity" in this context means interacting with a widget that then triggers the re-rendering of a static image. This use case and setup is fundamentally different from trying to interactively capture inputs from a static image.

Proposed approach

What I would suggest is:

  1. Install ipympl, as it is meant to be used for your purpose.
  2. If you want to avoid switching back and forth between backends, set your interactive backend once for your notebook, and disable interactive features in plots where you don't need them. Following Matplotlib's "comprehensive ipympl example", the display() function can be used for this purpose.

Altogether, this could look as follows in code:

%matplotlib widget
# Alternatively: %matplotlib ipympl
import matplotlib.pyplot as plt
import numpy as np

# Provide some dummy data
x = np.linspace(-10, 10, num=10000)
y1 = x ** 2
y2 = x ** 3

# Plot `y1` in an interactive plot
def on_click(event):
    plt.plot(event.xdata, event.ydata, "ro")

plt.connect("button_press_event", on_click)
plt.plot(x, y1)

# Plot `y2` in a 'static' plot
with plt.ioff():
    plt.figure()  # Create new figure for 2nd plot
    plt.plot(x, y2)
    display(plt.gcf())  # Display without interactivity

The resulting notebook would look as follows: screenshot of resulting notebook

Semi-off-topic: "interactive mode"

You might have noticed that display() is used in connection with ioff() for the static figure here. And although ioff() is documented as the function to, quote, disable interactive mode, it is not the one that is responsible for disabling click capturing etc. here. In this context, "interactive" refers to yet another concept, which is explained with the isinteractive() function; namely,

… whether plots are updated after every plotting command. The interactive mode is mainly useful if you build plots from the command line and want to see the effect of each command while you are building the figure.

In the given example, we don't want plotting commands to have immediate effects on the output, because this would mean that already the figure() and plot() calls would render the figure (with all its interaction capabilities in our original sense!), rather than only rendering it (as a static image) when we call display(). Moreover, we would get two outputs of our figure: one (interactive) plot because of the figure() and plot() calls, one (static) plot because of the display() call. To suppress the first one, we use an ioff() context.

Sign up to request clarification or add additional context in comments.

1 Comment

I accepted this answer because I am reasonably convinced that what I want to achieve is not possible with Matplotlib. I do not think the proposed approach is more convenient than switching backend, which is what I was trying to avoid.
1

As far as I know, using ipywidgets alone might not work because interact() is for interacting with widgets, not capturing clicks on plots directly. You’re right to explore the issue, I think it's not possible.

In a Jupyter Notebook with the default inline backend, the plot becomes non-interactive. To make the plot itself responsive to clicks, you would need to switch backends, for example, using ipympl (interactive backend). Inline rendering won't capture mouse events.

there's is an example:

# pip install ipympl      oviously

%matplotlib widget
import matplotlib.pyplot as plt

fig, ax = plt.subplots()

def onclick(event):
    print(f'Coordinates: ({event.xdata}, {event.ydata})')

fig.canvas.mpl_connect('button_press_event', onclick)
plt.show()

This lets you click on the plot and capture the coordinates interactively! and like I said I'm not sure, but I personally think the answer to your question is No. Not directly but we can walk around.

2 Comments

Please suggest the explicit %matplotlib ipympl and not the version that works because of older approaches before the dust had settled. See here where it state this pretty much. The explicit %matplotlib ipympl helps the uninitiated understand better how they need to install ipympl for it all to work.
@Wayne But it is also the current Matplotlib doc that suggests: If ipympl is installed use the magic %matplotlib widget to select and enable it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.