Matplotlib - Path Editor



A Path Editor is an application that allows users to interactively edit and manipulate paths in a graphical environment. In the context of Matplotlib, a Path Editor typically refers to a graphical user interface (GUI) application that facilitates the editing of paths defined using Matplotlib's Path class.

Before diving into the Path Editor, it's essential to understand the basics of Matplotlib paths. A Path is a fundamental object in Matplotlib that contains various elements like line segments, curves, and shapes within the matplotlib.patches module. Paths provide a versatile way to define complex outlines by specifying a series of commands such as moveto, lineto, and curveto.

Matplotlib offers a powerful Path class that serves as the foundation for creating and manipulating paths in visualizations.

Step by Step implementation

In this tutorial, we'll explore the Matplotlib Path Editor, a cross-GUI application that uses Matplotlib's event handling capabilities to edit and modify paths on the canvas interactively.

Creating the PathInteractor class

Create a path editor (PathInteractor) class to handle the interaction with the defined path. This class includes methods to toggle vertex markers (using the 't' key), drag vertices, and respond to mouse and key events.

Example

import matplotlib.pyplot as plt
import numpy as np

from matplotlib.backend_bases import MouseButton
from matplotlib.patches import PathPatch
from matplotlib.path import Path

class PathInteractor:

   showverts = True
   # max pixel distance to count as a vertex hit
   epsilon = 5  

   def __init__(self, pathpatch):
      # Initialization and event connections
      self.ax = pathpatch.axes
      canvas = self.ax.figure.canvas
      self.pathpatch = pathpatch
      self.pathpatch.set_animated(True)

      x, y = zip(*self.pathpatch.get_path().vertices)

      self.line, = ax.plot(
         x, y, marker='o', markerfacecolor='r', animated=True)

      self._ind = None  # the active vertex

      canvas.mpl_connect('draw_event', self.on_draw)
      canvas.mpl_connect('button_press_event', self.on_button_press)
      canvas.mpl_connect('key_press_event', self.on_key_press)
      canvas.mpl_connect('button_release_event', self.on_button_release)
      canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
      self.canvas = canvas

   def get_ind_under_point(self, event):
      # Return the index of the point closest to the event position or *None*
      xy = self.pathpatch.get_path().vertices
      xyt = self.pathpatch.get_transform().transform(xy)  # to display coords
      xt, yt = xyt[:, 0], xyt[:, 1]
      d = np.sqrt((xt - event.x)**2 + (yt - event.y)**2)
      ind = d.argmin()
      return ind if d[ind] < self.epsilon else None

   def on_draw(self, event):
      # Callback for draws.
      self.background = self.canvas.copy_from_bbox(self.ax.bbox)
      self.ax.draw_artist(self.pathpatch)
      self.ax.draw_artist(self.line)
      self.canvas.blit(self.ax.bbox)

   def on_button_press(self, event):
      # Callback for mouse button presses 
      if (event.inaxes is None
            or event.button != MouseButton.LEFT
            or not self.showverts):
         return
     self._ind = self.get_ind_under_point(event)

   def on_button_release(self, event):
      # Callback for mouse button releases 
      if (event.button != MouseButton.LEFT
            or not self.showverts):
         return
      self._ind = None

   def on_key_press(self, event):
      # Callback for key presses 
      if not event.inaxes:
         return
      if event.key == 't':
         self.showverts = not self.showverts
         self.line.set_visible(self.showverts)
         if not self.showverts:
            self._ind = None
      self.canvas.draw()

   def on_mouse_move(self, event):
      # Callback for mouse movements 
      if (self._ind is None
         or event.inaxes is None
         or event.button != MouseButton.LEFT
         or not self.showverts):
      return

      vertices = self.pathpatch.get_path().vertices

      vertices[self._ind] = event.xdata, event.ydata
      self.line.set_data(zip(*vertices))

      self.canvas.restore_region(self.background)
      self.ax.draw_artist(self.pathpatch)
      self.ax.draw_artist(self.line)
      self.canvas.blit(self.ax.bbox)

Event Handling and Canvas Interaction

The PathInteractor class connects various callbacks to canvas events, enabling users to interact with the defined path. These interactions include pressing and releasing mouse buttons, dragging vertices, and toggling vertex markers with key presses.

canvas.mpl_connect('draw_event', self.on_draw)
canvas.mpl_connect('button_press_event', self.on_button_press)
canvas.mpl_connect('key_press_event', self.on_key_press)
canvas.mpl_connect('button_release_event', self.on_button_release)
canvas.mpl_connect('motion_notify_event', self.on_mouse_move)

Defining and Visualizing a Path

Start by defining a predefined path, consisting of various path codes and vertices, which is created using the Matplotlib Path class. This path is then visualized on the canvas using a PathPatch instance, adding an interactive component to the plot.

fig, ax = plt.subplots()

pathdata = [
   (Path.MOVETO, (1.58, -2.57)),
   (Path.CURVE4, (0.35, -1.1)),
   (Path.CURVE4, (-1.75, 2.0)),
   (Path.CURVE4, (0.375, 2.0)),
   (Path.LINETO, (0.85, 1.15)),
   (Path.CURVE4, (2.2, 3.2)),
   (Path.CURVE4, (3, 0.05)),
   (Path.CURVE4, (2.0, -0.5)),
   (Path.CLOSEPOLY, (1.58, -2.57)),
]

codes, verts = zip(*pathdata)
path = Path(verts, codes)
patch = PathPatch(
   path, facecolor='green', edgecolor='yellow', alpha=0.5)
ax.add_patch(patch)

Running the Path Editor

Instantiate the PathInteractor class, set plot properties, and display the plot. Users can now interactively drag vertices, toggle vertex markers using the key "t", and observe real-time updates.

interactor = PathInteractor(patch)
ax.set_title('drag vertices to update path')
ax.set_xlim(-3, 4)
ax.set_ylim(-3, 4)

plt.show()

Example

Let’s see the complete example of the Matplotlib Path Editor.

import matplotlib.pyplot as plt
import numpy as np

from matplotlib.backend_bases import MouseButton
from matplotlib.patches import PathPatch
from matplotlib.path import Path


class PathInteractor:

   showverts = True
   # max pixel distance to count as a vertex hit
   epsilon = 5  

   def __init__(self, pathpatch):
      # Initialization and event connections
      self.ax = pathpatch.axes
      canvas = self.ax.figure.canvas
      self.pathpatch = pathpatch
      self.pathpatch.set_animated(True)

      x, y = zip(*self.pathpatch.get_path().vertices)

      self.line, = ax.plot(
         x, y, marker='o', markerfacecolor='r', animated=True)

      self._ind = None  # the active vertex

      canvas.mpl_connect('draw_event', self.on_draw)
      canvas.mpl_connect('button_press_event', self.on_button_press)
      canvas.mpl_connect('key_press_event', self.on_key_press)
      canvas.mpl_connect('button_release_event', self.on_button_release)
      canvas.mpl_connect('motion_notify_event', self.on_mouse_move)
      self.canvas = canvas

   def get_ind_under_point(self, event):
      # Return the index of the point closest to the event position or *None*
      xy = self.pathpatch.get_path().vertices
      xyt = self.pathpatch.get_transform().transform(xy)  # to display coords
      xt, yt = xyt[:, 0], xyt[:, 1]
      d = np.sqrt((xt - event.x)**2 + (yt - event.y)**2)
      ind = d.argmin()
         return ind if d[ind] < self.epsilon else None

   def on_draw(self, event):
      # Callback for draws.
      self.background = self.canvas.copy_from_bbox(self.ax.bbox)
      self.ax.draw_artist(self.pathpatch)
      self.ax.draw_artist(self.line)
      self.canvas.blit(self.ax.bbox)

   def on_button_press(self, event):
      # Callback for mouse button presses 
      if (event.inaxes is None
         or event.button != MouseButton.LEFT
         or not self.showverts):
         return
      self._ind = self.get_ind_under_point(event)

   def on_button_release(self, event):
      # Callback for mouse button releases 
      if (event.button != MouseButton.LEFT
         or not self.showverts):
         return
      self._ind = None

   def on_key_press(self, event):
      # Callback for key presses 
      if not event.inaxes:
         return
      if event.key == 't':
         self.showverts = not self.showverts
         self.line.set_visible(self.showverts)
         if not self.showverts:
            self._ind = None
      self.canvas.draw()

   def on_mouse_move(self, event):
      # Callback for mouse movements 
      if (self._ind is None
         or event.inaxes is None
         or event.button != MouseButton.LEFT
         or not self.showverts):
      return

      vertices = self.pathpatch.get_path().vertices

      vertices[self._ind] = event.xdata, event.ydata
      self.line.set_data(zip(*vertices))

      self.canvas.restore_region(self.background)
      self.ax.draw_artist(self.pathpatch)
      self.ax.draw_artist(self.line)
      self.canvas.blit(self.ax.bbox)

fig, ax = plt.subplots()

pathdata = [
   (Path.MOVETO, (1.58, -2.57)),
   (Path.CURVE4, (0.35, -1.1)),
   (Path.CURVE4, (-1.75, 2.0)),
   (Path.CURVE4, (0.375, 2.0)),
   (Path.LINETO, (0.85, 1.15)),
   (Path.CURVE4, (2.2, 3.2)),
   (Path.CURVE4, (3, 0.05)),
   (Path.CURVE4, (2.0, -0.5)),
   (Path.CLOSEPOLY, (1.58, -2.57)),
]

codes, verts = zip(*pathdata)
path = Path(verts, codes)
patch = PathPatch(
   path, facecolor='green', edgecolor='yellow', alpha=0.5)
ax.add_patch(patch)

interactor = PathInteractor(patch)
ax.set_title('drag vertices to update path')
ax.set_xlim(-3, 4)
ax.set_ylim(-3, 4)

plt.show()
Output

On executing the above program you will get the following figure. Press the "t" key on your keyboard. This action toggles the visibility of vertex markers on and off. When the vertex markers are visible (after pressing "t"), you can drag these markers with your mouse. Observe how dragging the vertices affects the shape of the path.

Path Editor

Watch the video below to observe how the path editor works here.

Path Editor gif
Advertisements