From 2b0b376b724741b85e2b3b80d019f4adf201c560 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 10 Oct 2017 20:44:56 -0700 Subject: [PATCH 1/2] Keep track of axes in interactive navigation. --- lib/matplotlib/backend_bases.py | 58 ++++++++++++--------------- lib/matplotlib/backends/backend_wx.py | 4 +- 2 files changed, 28 insertions(+), 34 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index cf7889678553..d9307bcc2e05 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2775,9 +2775,7 @@ class NavigationToolbar2(object): def __init__(self, canvas): self.canvas = canvas canvas.toolbar = self - # a dict from axes index to a list of view limits - self._views = cbook.Stack() - self._positions = cbook.Stack() # stack of subplot positions + self._nav_stack = cbook.Stack() self._xypress = None # the location and axis info at the time # of the press self._idPress = None @@ -2799,19 +2797,24 @@ def __init__(self, canvas): self.set_history_buttons() @partial(canvas.mpl_connect, 'draw_event') - def define_home(event): - self.push_current() - # The decorator sets `define_home` to the callback cid, so we can - # disconnect it after the first use. - canvas.mpl_disconnect(define_home) + def update_stack(event): + nav_info = self._nav_stack() + if nav_info is None: + # Define the true initial navigation info. + self.push_current() + else: + axes, views, positions = nav_info + if axes != self.canvas.figure.axes: + # An axes has been added or removed, so update the + # navigation info too. + self.push_current() def set_message(self, s): """Display a message on toolbar or in status bar.""" def back(self, *args): """move back up the view lim stack""" - self._views.back() - self._positions.back() + self._nav_stack.back() self.set_history_buttons() self._update_view() @@ -2830,15 +2833,13 @@ def remove_rubberband(self): def forward(self, *args): """Move forward in the view lim stack.""" - self._views.forward() - self._positions.forward() + self._nav_stack.forward() self.set_history_buttons() self._update_view() def home(self, *args): """Restore the original view.""" - self._views.home() - self._positions.home() + self._nav_stack.home() self.set_history_buttons() self._update_view() @@ -3015,16 +3016,13 @@ def _switch_off_zoom_mode(self, event): def push_current(self): """Push the current view limits and position onto the stack.""" - views = [] - pos = [] - for a in self.canvas.figure.get_axes(): - views.append(a._get_view()) - # Store both the original and modified positions - pos.append(( - a.get_position(True).frozen(), - a.get_position().frozen())) - self._views.push(views) - self._positions.push(pos) + axs = self.canvas.figure.axes + views = [ax._get_view() for ax in axs] + # Store both the original and modified positions. + positions = [ + (ax.get_position(True).frozen(), ax.get_position().frozen()) + for ax in axs] + self._nav_stack.push((axs, views, positions)) self.set_history_buttons() def release(self, event): @@ -3146,18 +3144,15 @@ def _update_view(self): position stack for each axes. """ - views = self._views() - if views is None: - return - pos = self._positions() - if pos is None: + nav_info = self._nav_stack() + if nav_info is None: return + axs, views, pos = nav_info for i, a in enumerate(self.canvas.figure.get_axes()): a._set_view(views[i]) # Restore both the original and modified positions a.set_position(pos[i][0], 'original') a.set_position(pos[i][1], 'active') - self.canvas.draw_idle() def save_figure(self, *args): @@ -3175,8 +3170,7 @@ def set_cursor(self, cursor): def update(self): """Reset the axes stack.""" - self._views.clear() - self._positions.clear() + self._nav_stack.clear() self.set_history_buttons() def zoom(self, *args): diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index c80c78486d2c..edf118ce3cfb 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -1703,8 +1703,8 @@ def set_message(self, s): self.statbar.set_function(s) def set_history_buttons(self): - can_backward = (self._views._pos > 0) - can_forward = (self._views._pos < len(self._views._elements) - 1) + can_backward = self._nav_stack._pos > 0 + can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1 self.EnableTool(self.wx_ids['Back'], can_backward) self.EnableTool(self.wx_ids['Forward'], can_forward) From cffeef08a7e35efc68a20ae14cfb641c343913d0 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 10 Oct 2017 21:03:37 -0700 Subject: [PATCH 2/2] Only keep weakrefs to the axes. --- lib/matplotlib/backend_bases.py | 38 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index d9307bcc2e05..8c8b9b90c0eb 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -46,6 +46,7 @@ import sys import time import warnings +from weakref import WeakKeyDictionary import numpy as np import matplotlib.cbook as cbook @@ -2799,15 +2800,11 @@ def __init__(self, canvas): @partial(canvas.mpl_connect, 'draw_event') def update_stack(event): nav_info = self._nav_stack() - if nav_info is None: - # Define the true initial navigation info. - self.push_current() - else: - axes, views, positions = nav_info - if axes != self.canvas.figure.axes: + if (nav_info is None # True initial navigation info. # An axes has been added or removed, so update the # navigation info too. - self.push_current() + or set(nav_info) != set(self.canvas.figure.axes)): + self.push_current() def set_message(self, s): """Display a message on toolbar or in status bar.""" @@ -3016,13 +3013,13 @@ def _switch_off_zoom_mode(self, event): def push_current(self): """Push the current view limits and position onto the stack.""" - axs = self.canvas.figure.axes - views = [ax._get_view() for ax in axs] - # Store both the original and modified positions. - positions = [ - (ax.get_position(True).frozen(), ax.get_position().frozen()) - for ax in axs] - self._nav_stack.push((axs, views, positions)) + self._nav_stack.push( + WeakKeyDictionary( + {ax: (ax._get_view(), + # Store both the original and modified positions. + (ax.get_position(True).frozen(), + ax.get_position().frozen())) + for ax in self.canvas.figure.axes})) self.set_history_buttons() def release(self, event): @@ -3143,16 +3140,17 @@ def _update_view(self): """Update the viewlim and position from the view and position stack for each axes. """ - nav_info = self._nav_stack() if nav_info is None: return - axs, views, pos = nav_info - for i, a in enumerate(self.canvas.figure.get_axes()): - a._set_view(views[i]) + # Retrieve all items at once to avoid any risk of GC deleting an Axes + # while in the middle of the loop below. + items = list(nav_info.items()) + for ax, (view, (pos_orig, pos_active)) in items: + ax._set_view(view) # Restore both the original and modified positions - a.set_position(pos[i][0], 'original') - a.set_position(pos[i][1], 'active') + ax.set_position(pos_orig, 'original') + ax.set_position(pos_active, 'active') self.canvas.draw_idle() def save_figure(self, *args):