Effective Redrawing Matplotlib

I use Matplotlib so the user can select interesting data points using mouseclicks using a very similar method for this answer.

Effectively, a scatter plot is displayed above the heat map image, and mouse clicks can add or remove scatter points.

My data is drawn in the background using pcolormesh() , so when I update the canvas using axis.figure.canvas.draw() , both the scatter points and the background heat map are redrawn. Given the size of the heatmap, this is too slow for a convenient interface.

Is there a way to selectively redraw only scatter points without redrawing the background?

Code example:

 points = [] # Holds a list of (x,y) scatter points def onclick(event): # Click event handler to add points points.append( (event.x, event.y) ) ax.figure.canvas.draw() fig = plt.figure() ax = plt.figure() # Plot the background ax.pcolormesh(heatmap_data) # Add the listener to handle clicks cid = fig.canvas.mpl_connect("button_press_event", onclick) plt.show() 
+5
source share
1 answer

Of course! What you want is beating. If you did not write gui, you can simplify some of them using matplotlib.animation , but you will need to process it directly if you want something to be interactive.

In matplotlib terms, you need a combination of fig.canvas.copy_from_bbox , and then call fig.canvas.restore_region(background) , ax.draw_artist(what_you_want_to_draw) and fig.canvas.blit :

 background = fig.canvas.copy_from_bbox(ax.bbox) for x, y in user_interactions: fig.canvas.restore_region(background) points.append([x, y]) scatter.set_offsets(points) ax.draw_artist(scatter) fig.canvas.blit(ax.bbox) 

Simple Blitting Example: Adding Points

In your case, if you add only points, you can actually skip saving and restoring the background. However, if you go along this route, you will encounter some minor changes to the plot due to the repeated intersection of smoothing points on top of each other.

In any case, here is the simplest example of the type of thing you want. This only applies to adding points and skips saving and restoring the background, as I mentioned above:

 import matplotlib.pyplot as plt import numpy as np def main(): fig, ax = plt.subplots() ax.pcolormesh(np.random.random((100, 100)), cmap='gray') ClickToDrawPoints(ax).show() class ClickToDrawPoints(object): def __init__(self, ax): self.ax = ax self.fig = ax.figure self.xy = [] self.points = ax.scatter([], [], s=200, color='red', picker=20) self.fig.canvas.mpl_connect('button_press_event', self.on_click) def on_click(self, event): if event.inaxes is None: return self.xy.append([event.xdata, event.ydata]) self.points.set_offsets(self.xy) self.ax.draw_artist(self.points) self.fig.canvas.blit(self.ax.bbox) def show(self): plt.show() main() 

Sometimes Simple is Too Easy

However, let's say we wanted right-clicks to delete a point.

In this case, we should be able to restore the background without redrawing it.

Good, all is well and good. We will use something similar to the pseudo-code fragment that I mentioned at the beginning of the answer.

However, there is a caveat: if the image size changes, we need to update the background. Similarly, if the axes are lit interactively, we need to update the background. Basically, you need to update the background at any time when the chart is drawn.

Pretty soon you will need to be pretty hard.


More complicated: add / drag / drop points

Here is a general example of the kind of "forest" that you are completing.

This is somewhat inefficient, as the plot is obtained twice. (e.g. panning will be slow). This can be circumvented, but I will leave these examples at a different time.

This allows you to add points, drag and drop points, and delete points. To add / drag a point after interactively zooming / panning, click again to enlarge / pan the tool in the toolbar to disable it.

This is a rather complicated example, but I hope it gives an idea of ​​the type of structure that is usually assembled for interactive drawing / dragging / editing / deleting matplotlib artists without redrawing the entire graph.

 import numpy as np import matplotlib.pyplot as plt class DrawDragPoints(object): """ Demonstrates a basic example of the "scaffolding" you need to efficiently blit drawable/draggable/deleteable artists on top of a background. """ def __init__(self): self.fig, self.ax = self.setup_axes() self.xy = [] self.tolerance = 10 self._num_clicks = 0 # The artist we'll be modifying... self.points = self.ax.scatter([], [], s=200, color='red', picker=self.tolerance, animated=True) connect = self.fig.canvas.mpl_connect connect('button_press_event', self.on_click) self.draw_cid = connect('draw_event', self.grab_background) def setup_axes(self): """Setup the figure/axes and plot any background artists.""" fig, ax = plt.subplots() # imshow would be _much_ faster in this case, but let deliberately # use something slow... ax.pcolormesh(np.random.random((1000, 1000)), cmap='gray') ax.set_title('Left click to add/drag a point\nRight-click to delete') return fig, ax def on_click(self, event): """Decide whether to add, delete, or drag a point.""" # If we're using a tool on the toolbar, don't add/draw a point... if self.fig.canvas.toolbar._active is not None: return contains, info = self.points.contains(event) if contains: i = info['ind'][0] if event.button == 1: self.start_drag(i) elif event.button == 3: self.delete_point(i) else: self.add_point(event) def update(self): """Update the artist for any changes to self.xy.""" self.points.set_offsets(self.xy) self.blit() def add_point(self, event): self.xy.append([event.xdata, event.ydata]) self.update() def delete_point(self, i): self.xy.pop(i) self.update() def start_drag(self, i): """Bind mouse motion to updating a particular point.""" self.drag_i = i connect = self.fig.canvas.mpl_connect cid1 = connect('motion_notify_event', self.drag_update) cid2 = connect('button_release_event', self.end_drag) self.drag_cids = [cid1, cid2] def drag_update(self, event): """Update a point that being moved interactively.""" self.xy[self.drag_i] = [event.xdata, event.ydata] self.update() def end_drag(self, event): """End the binding of mouse motion to a particular point.""" for cid in self.drag_cids: self.fig.canvas.mpl_disconnect(cid) def safe_draw(self): """Temporarily disconnect the draw_event callback to avoid recursion""" canvas = self.fig.canvas canvas.mpl_disconnect(self.draw_cid) canvas.draw() self.draw_cid = canvas.mpl_connect('draw_event', self.grab_background) def grab_background(self, event=None): """ When the figure is resized, hide the points, draw everything, and update the background. """ self.points.set_visible(False) self.safe_draw() # With most backends (eg TkAgg), we could grab (and refresh, in # self.blit) self.ax.bbox instead of self.fig.bbox, but Qt4Agg, and # some others, requires us to update the _full_ canvas, instead. self.background = self.fig.canvas.copy_from_bbox(self.fig.bbox) self.points.set_visible(True) self.blit() def blit(self): """ Efficiently update the figure, without needing to redraw the "background" artists. """ self.fig.canvas.restore_region(self.background) self.ax.draw_artist(self.points) self.fig.canvas.blit(self.fig.bbox) def show(self): plt.show() DrawDragPoints().show() 
+11
source

Source: https://habr.com/ru/post/1216184/


All Articles