Advanced#

Here we demonstrate some more advanced uses of mpltoolbox.

Callbacks#

Every tool in the mpltoolbox comes with entry points for adding callbacks to different events. The different events are:

  • on_create: called when drawing the shape (rectangle, line or polygon) is complete

  • on_change: called when the shape has changed in any way (position, size…)

  • on_remove: called when the shape is deleted (middle-click, or Ctrl + left-click)

  • on_vertex_press: called when a vertex is clicked (left-click)

  • on_vertex_move: called when a vertex is moved

  • on_vertex_release: called when the mouse button is released after clicking a vertex

  • on_drag_press: called when the entire shape (rectangle, line, etc..) is right clicked to initiate drag

  • on_drag_move: called for every movement during shape drag (right-click and hold)

  • on_drag_release: called when the shape is released after drag

Below is a couple of examples on how these callbacks are used.

[1]:
%matplotlib widget
import numpy as np
import mpltoolbox as tbx
import matplotlib.pyplot as plt

plt.ioff()
[1]:
<contextlib.ExitStack at 0x7f8938c46080>

Example 1: Add markers to slice 3d cube#

We first make some three-dimensional data:

[2]:
N = 200
M = 300
L = 100
xx = np.arange(N, dtype=np.float64)
yy = np.arange(M, dtype=np.float64)
zz = np.arange(L, dtype=np.float64)
x, y, z = np.meshgrid(xx, yy, zz, indexing="ij")
b = N / 20.0
c = M / 2.0
d = L / 2.0
r = np.sqrt(((x - c) / b) ** 2 + ((y - c) / b) ** 2 + ((z - d) / b) ** 2)
a = 10.0 * np.sin(r) + np.random.random([N, M, L])

Create a figure to display the first z slice of the data as a two-dimensional image, as well as an empty subplot below:

[3]:
fig, ax = plt.subplots(2, 1, figsize=(7, 7))
fig.canvas.header_visible = False
ax[0].imshow(a[..., 0], interpolation="none", origin="lower")
ax[0].set(xlabel='x', ylabel='y')
ax[1].set(xlabel='z')
fig.tight_layout()

Then we add a Points tool where:

  • When a dot is added on the image, a line is created in the lower panel, showing a one-dimensional z slice at the location of the marker

  • When a dot is moved, the z line is updated accordingly

  • When a dot is removed, remove the corresponding z profile

[4]:
def make_line(new_point):
    (line,) = ax[1].plot(a[int(new_point.y), int(new_point.x), :])
    new_point.associated = line


def update_line(new_point):
    new_point.associated.set_ydata(a[int(new_point.y), int(new_point.x), :])


def remove_line(point):
    point.associated.remove()


points = tbx.Points(ax=ax[0], mec="white")
points.on_create(make_line)
points.on_change(update_line)
points.on_remove(remove_line)
[6]:
fig.canvas
[6]:

Example 2: Histogram inside a rectangular region#

In the second example, we use the Ractangles tool to draw rectangles on the same 2d image. This defines a region of interest, inside which the data will be histogrammed and displayed on the lower panel.

[7]:
fig2, ax2 = plt.subplots(2, 1, figsize=(7, 7))
fig2.canvas.header_visible = False
fig2.tight_layout()
ax2[0].imshow(a[..., 0], interpolation="none", origin="lower")


def make_hist(new_rectangle):
    xy = new_rectangle.xy
    ix0 = int(xy[0])
    iy0 = int(xy[1])
    ix1 = int(xy[0] + new_rectangle.width)
    iy1 = int(xy[1] + new_rectangle.height)
    n, bins, patches = ax2[1].hist(
        a[min(iy0, iy1) : max(iy0, iy1), min(ix0, ix1) : max(ix0, ix1), :].ravel(),
        histtype="step",
        lw=1.5,
        color=new_rectangle.edgecolor,
    )
    new_rectangle.associated = patches


def update_hist(new_rectangle):
    for patch in new_rectangle.associated:
        patch.remove()
    make_hist(new_rectangle)


def remove_hist(rectangle):
    for patch in rectangle.associated:
        patch.remove()


rects = tbx.Rectangles(ax=ax2[0], facecolor=(0, 0, 0, 0.3))
rects.on_create(make_hist)
rects.on_change(update_hist)
rects.on_remove(remove_hist)
[9]:
fig2.canvas
[9]:

Example 3: Multiple callbacks#

It is also possible to add multiple callbacks by calling the on_* methods multiple times:

[10]:
fig, ax = plt.subplots(2, 1, figsize=(7, 7))
fig.canvas.header_visible = False
ax[0].set(xlabel='x', ylabel='y')
ax[1].set(xlabel='z')
fig.tight_layout()
ax[0].imshow(a[..., 0], interpolation="none", origin="lower")


def make_line(p):
    (line,) = ax[1].plot(a[int(p.y), int(p.y), :])
    p.associated = line


def update_line(p):
    p.associated.set_ydata(a[int(p.y), int(p.y), :])


def make_text(p):
    t = ax[1].text(50.0, np.random.random() * 10, f"x={int(p.x)}; y={int(p.y)}")
    p.text = t


def update_text(p):
    p.text.set_text(f"x={int(p.x)}; y={int(p.y)}")


points = tbx.Points(ax=ax[0], mec="white")
points.on_create(make_line)
points.on_create(make_text)
points.on_change(update_line)
points.on_change(update_text)
[12]:
fig.canvas
[12]:

Programmatic control#

It can sometimes be useful to programmatically add or remove artists on the figure, instead of using the mouse. This can either be if a very accurate position is required, or for software testing purposes.

Simulating clicks#

Every tool has a click() method that can be used to simulate a mouse click event. It accepts two numbers for the x and y coordinates of the event, as well as an optional button argument that can be used to change which mouse button is used. The different values for button are: 1 for left-click (default), 2 for middle-click, 3 for right-click.

In the following example, we will add lines to a figure using the click method.

[13]:
fig, ax = plt.subplots(dpi=96)

ax.set_xlim(0, 100)
ax.set_ylim(0, 100)

lines = tbx.Lines(ax=ax, n=2)
[14]:
lines.click(x=20, y=40)  # first line vertex
lines.click(x=80, y=70)  # second line vertex

lines.click(x=30, y=10)
lines.click(x=40, y=90)

lines.click(x=50, y=50)
lines.click(x=85, y=15)
[15]:
fig.canvas
[15]:

Removing artists#

To remove an artist from a figure, the remove method accepts

  • an integer index, in which case the artist with the corresponding position in the list of children will be removed

  • an artist (using tool.children will give a list of all artists the tool is responsible for)

  • a string, which should be the id (uuid) of the artist to be removed

[17]:
lines.remove(0)  # remove the first line that was added
lines.remove(lines.children[-1])  # remove the last line that was added

fig.canvas
[17]: