diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 7046f871841d..19b4bb328ac1 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -3071,21 +3071,10 @@ def set_xlim(self, left=None, right=None, emit=True, auto=False, **kw): 'invalid limits will be ignored.') left, right = self.xaxis.limit_range_for_scale(left, right) - self.viewLim.intervalx = (left, right) - if auto is not None: - self._autoscaleXon = bool(auto) - - if emit: - self.callbacks.process('xlim_changed', self) - # Call all of the other x-axes that are shared with this one - for other in self._shared_x_axes.get_siblings(self): - if other is not self: - other.set_xlim(self.viewLim.intervalx, - emit=False, auto=auto) - if (other.figure != self.figure and - other.figure.canvas is not None): - other.figure.canvas.draw_idle() - self.stale = True + # We force the use of `maxis.XAxis` so that subclasses have access + # to changing `viewLim` in this way (see Axes3D) + maxis.XAxis.set_view_interval(self.xaxis, left, right, ignore=True, + emit=emit, auto=auto) return left, right def get_xscale(self): @@ -3391,21 +3380,10 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False, **kw): 'invalid limits will be ignored.') bottom, top = self.yaxis.limit_range_for_scale(bottom, top) - self.viewLim.intervaly = (bottom, top) - if auto is not None: - self._autoscaleYon = bool(auto) - - if emit: - self.callbacks.process('ylim_changed', self) - # Call all of the other y-axes that are shared with this one - for other in self._shared_y_axes.get_siblings(self): - if other is not self: - other.set_ylim(self.viewLim.intervaly, - emit=False, auto=auto) - if (other.figure != self.figure and - other.figure.canvas is not None): - other.figure.canvas.draw_idle() - self.stale = True + # We force the use of `maxis.YAxis` so that subclasses have access + # to changing `viewLim` in this way (see Axes3D) + maxis.YAxis.set_view_interval(self.yaxis, bottom, top, ignore=True, + emit=emit, auto=auto) return bottom, top def get_yscale(self): diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index b3a7ffcc3ca3..36b9e3adaa8a 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -926,14 +926,15 @@ def get_view_interval(self): 'return the Interval instance for this axis view limits' raise NotImplementedError('Derived must override') - def set_view_interval(self, vmin, vmax, ignore=False): + def set_view_interval(self, vmin, vmax, ignore=False, emit=True, + auto=False): raise NotImplementedError('Derived must override') def get_data_interval(self): 'return the Interval instance for this axis data limits' raise NotImplementedError('Derived must override') - def set_data_interval(self): + def set_data_interval(self, vmin, vmax, ignore=False): '''set the axis data limits''' raise NotImplementedError('Derived must override') @@ -2062,18 +2063,53 @@ def get_view_interval(self): 'return the Interval instance for this axis view limits' return self.axes.viewLim.intervalx - def set_view_interval(self, vmin, vmax, ignore=False): + def set_view_interval(self, vmin, vmax, ignore=False, emit=True, + auto=False): """ - If *ignore* is *False*, the order of vmin, vmax - does not matter; the original axis orientation will - be preserved. In addition, the view limits can be - expanded, but will not be reduced. This method is - for mpl internal use; for normal use, see - :meth:`~matplotlib.axes.Axes.set_xlim`. + Set the x-axis view limits for the parent Axes instance. + + .. ACCEPTS: (vmin: float, vmax: float) + + Parameters + ---------- + vmin : scalar + The smaller value of viewLim. + + vmax : scalar + The higher value of viewLim. + + ignore : bool, optional + Whether we should take (vmin, vmax) literally (default: False). + If this is False, the order of vmin, vmax does not matter; the + original axis orientation will be preserved. + + emit : bool, optional + Whether to notify observers of limit change (default: True). + + auto : bool or None, optional + Whether to turn on autoscaling of this axis. True turns on, + False turns off (default action), None leaves unchanged. + + Notes + ----- + If `ignore` is False (default), the view limits can be expanded, but + will not be reduced. Otherwise, if `ignore` is True, the `vmin` value + may be greater than the `vmax` value, in which case the viewLim will + decrease from left to right. + + Examples + -------- + >>> set_view_interval(vmin, vmax, ignore=True) + + Sets the new view interval to (vmin, vmax). + + >>> set_view_interval(vmin, vmax) + Expands the view interval to the minimum of the current interval + and vmin, and the maximum of the current interval and vmax. """ if ignore: - self.axes.viewLim.intervalx = vmin, vmax + self.axes.viewLim.intervalx = (vmin, vmax) else: Vmin, Vmax = self.get_view_interval() if Vmin < Vmax: @@ -2082,6 +2118,19 @@ def set_view_interval(self, vmin, vmax, ignore=False): else: self.axes.viewLim.intervalx = (max(vmin, vmax, Vmin), min(vmin, vmax, Vmax)) + if auto is not None: + self.axes.set_autoscalex_on(bool(auto)) + if emit: + self.axes.callbacks.process('xlim_changed', self.axes) + # Call all of the other axes that are shared with this one + for other in self.axes.get_shared_x_axes().get_siblings(self.axes): + if other is not self.axes: + other.set_xlim(self.axes.viewLim.intervalx, emit=False, + auto=auto) + if (other.figure != self.axes.figure and + other.figure.canvas is not None): + other.figure.canvas.draw_idle() + self.axes.stale = True def get_minpos(self): return self.axes.dataLim.minposx @@ -2440,18 +2489,53 @@ def get_view_interval(self): 'return the Interval instance for this axis view limits' return self.axes.viewLim.intervaly - def set_view_interval(self, vmin, vmax, ignore=False): + def set_view_interval(self, vmin, vmax, ignore=False, emit=True, + auto=False): """ - If *ignore* is *False*, the order of vmin, vmax - does not matter; the original axis orientation will - be preserved. In addition, the view limits can be - expanded, but will not be reduced. This method is - for mpl internal use; for normal use, see - :meth:`~matplotlib.axes.Axes.set_ylim`. + Set the y-axis view limits for the parent Axes instance. + + .. ACCEPTS: (vmin: float, vmax: float) + + Parameters + ---------- + vmin : scalar + The smaller value of viewLim. + + vmax : scalar + The higher value of viewLim. + + ignore : bool, optional + Whether we should take (vmin, vmax) literally (default: False). + If this is False, the order of vmin, vmax does not matter; the + original axis orientation will be preserved. + + emit : bool, optional + Whether to notify observers of limit change (default: True). + + auto : bool or None, optional + Whether to turn on autoscaling of this axis. True turns on, + False turns off (default action), None leaves unchanged. + + Notes + ----- + If `ignore` is False (default), the view limits can be expanded, but + will not be reduced. Otherwise, if `ignore` is True, the `vmin` value + may be greater than the `vmax` value, in which case the viewLim will + decrease from left to right. + + Examples + -------- + >>> set_view_interval(vmin, vmax, ignore=True) + + Sets the new view interval to (vmin, vmax). + + >>> set_view_interval(vmin, vmax) + Expands the view interval to the minimum of the current interval + and vmin, and the maximum of the current interval and vmax. """ if ignore: - self.axes.viewLim.intervaly = vmin, vmax + self.axes.viewLim.intervaly = (vmin, vmax) else: Vmin, Vmax = self.get_view_interval() if Vmin < Vmax: @@ -2460,6 +2544,19 @@ def set_view_interval(self, vmin, vmax, ignore=False): else: self.axes.viewLim.intervaly = (max(vmin, vmax, Vmin), min(vmin, vmax, Vmax)) + if auto is not None: + self.axes.set_autoscaley_on(bool(auto)) + if emit: + self.axes.callbacks.process('ylim_changed', self.axes) + # Call all of the other axes that are shared with this one + for other in self.axes.get_shared_y_axes().get_siblings(self.axes): + if other is not self.axes: + other.set_ylim(self.axes.viewLim.intervaly, emit=False, + auto=auto) + if (other.figure != self.axes.figure and + other.figure.canvas is not None): + other.figure.canvas.draw_idle() + self.axes.stale = True self.stale = True def get_minpos(self): diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 5b808e820c76..cd67b49f4eb9 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -5134,6 +5134,29 @@ def test_remove_shared_axes_relim(): assert_array_equal(ax_lst[0][1].get_xlim(), orig_xlim) +def test_shared_axes_retick(): + # related to GitHub issue 8946 + # set_xticks should set shared axes limits + fig, ax_lst = plt.subplots(2, 2, sharex='all', sharey='all') + + data = np.random.randn(10) + data2 = np.random.randn(10) + index = range(10) + index2 = [x + 5.5 for x in index] + + ax_lst[0][0].scatter(index, data) + ax_lst[0][1].scatter(index2, data2) + + ax_lst[1][0].scatter(index, data) + ax_lst[1][1].scatter(index2, data2) + + ax_lst[0][0].set_xticks(range(-10, 20, 1)) + ax_lst[0][0].set_yticks(range(-10, 20, 1)) + + assert ax_lst[0][0].get_xlim() == ax_lst[0][1].get_xlim() + assert ax_lst[0][0].get_ylim() == ax_lst[1][0].get_ylim() + + def test_shared_axes_autoscale(): l = np.arange(-80, 90, 40) t = np.random.random_sample((l.size, l.size)) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 1bd3cf7c0a25..4e4b87b89964 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -642,22 +642,9 @@ def set_xlim3d(self, left=None, right=None, emit=True, auto=False, **kw): 'left=%s, right=%s') % (left, right)) left, right = mtransforms.nonsingular(left, right, increasing=False) left, right = self.xaxis.limit_range_for_scale(left, right) - self.xy_viewLim.intervalx = (left, right) - - if auto is not None: - self._autoscaleXon = bool(auto) - - if emit: - self.callbacks.process('xlim_changed', self) - # Call all of the other x-axes that are shared with this one - for other in self._shared_x_axes.get_siblings(self): - if other is not self: - other.set_xlim(self.xy_viewLim.intervalx, - emit=False, auto=auto) - if (other.figure != self.figure and - other.figure.canvas is not None): - other.figure.canvas.draw_idle() - self.stale = True + + self.xaxis.set_view_interval(left, right, ignore=True, emit=emit, + auto=auto) return left, right set_xlim = set_xlim3d @@ -694,22 +681,9 @@ def set_ylim3d(self, bottom=None, top=None, emit=True, auto=False, **kw): 'bottom=%s, top=%s') % (bottom, top)) bottom, top = mtransforms.nonsingular(bottom, top, increasing=False) bottom, top = self.yaxis.limit_range_for_scale(bottom, top) - self.xy_viewLim.intervaly = (bottom, top) - - if auto is not None: - self._autoscaleYon = bool(auto) - - if emit: - self.callbacks.process('ylim_changed', self) - # Call all of the other y-axes that are shared with this one - for other in self._shared_y_axes.get_siblings(self): - if other is not self: - other.set_ylim(self.xy_viewLim.intervaly, - emit=False, auto=auto) - if (other.figure != self.figure and - other.figure.canvas is not None): - other.figure.canvas.draw_idle() - self.stale = True + + self.yaxis.set_view_interval(bottom, top, ignore=True, emit=emit, + auto=auto) return bottom, top set_ylim = set_ylim3d @@ -746,22 +720,9 @@ def set_zlim3d(self, bottom=None, top=None, emit=True, auto=False, **kw): 'bottom=%s, top=%s') % (bottom, top)) bottom, top = mtransforms.nonsingular(bottom, top, increasing=False) bottom, top = self.zaxis.limit_range_for_scale(bottom, top) - self.zz_viewLim.intervalx = (bottom, top) - - if auto is not None: - self._autoscaleZon = bool(auto) - - if emit: - self.callbacks.process('zlim_changed', self) - # Call all of the other y-axes that are shared with this one - for other in self._shared_z_axes.get_siblings(self): - if other is not self: - other.set_zlim(self.zz_viewLim.intervalx, - emit=False, auto=auto) - if (other.figure != self.figure and - other.figure.canvas is not None): - other.figure.canvas.draw_idle() - self.stale = True + + self.zaxis.set_view_interval(bottom, top, ignore=True, emit=emit, + auto=auto) return bottom, top set_zlim = set_zlim3d @@ -952,6 +913,10 @@ def clabel(self, *args, **kwargs): """ return None + def get_shared_z_axes(self): + """Return a reference to the shared axes Grouper object for z axes.""" + return self._shared_z_axes + def view_init(self, elev=None, azim=None): """ Set the elevation and azimuth of the axes. @@ -2003,6 +1968,7 @@ def plot_trisurf(self, *args, **kwargs): xt = tri.x[triangles] yt = tri.y[triangles] zt = z[triangles] + verts = np.stack((xt, yt, zt), axis=-1) polyc = art3d.Poly3DCollection(verts, *args, **kwargs) @@ -2814,7 +2780,8 @@ def voxels(filled, **kwargs): def _broadcast_color_arg(color, name): if np.ndim(color) in (0, 1): # single color, like "red" or [1, 0, 0] - return np.broadcast_to(color, filled.shape + np.shape(color)) + return np.broadcast_to( + color, filled.shape + np.shape(color)) elif np.ndim(color) in (3, 4): # 3D array of strings, or 4D array with last axis rgb if np.shape(color)[:3] != filled.shape: diff --git a/lib/mpl_toolkits/mplot3d/axis3d.py b/lib/mpl_toolkits/mplot3d/axis3d.py index 4bfe8facbe46..c02e6eee52e5 100644 --- a/lib/mpl_toolkits/mplot3d/axis3d.py +++ b/lib/mpl_toolkits/mplot3d/axis3d.py @@ -450,7 +450,8 @@ def get_view_interval(self): """return the Interval instance for this 3d axis view limits""" return self.v_interval - def set_view_interval(self, vmin, vmax, ignore=False): + def set_view_interval(self, vmin, vmax, ignore=False, emit=True, + auto=False): if ignore: self.v_interval = vmin, vmax else: @@ -466,17 +467,203 @@ def get_tightbbox(self, renderer): # Use classes to look at different data limits + class XAxis(Axis): def get_data_interval(self): 'return the Interval instance for this axis data limits' return self.axes.xy_dataLim.intervalx + def set_view_interval(self, vmin, vmax, ignore=False, emit=True, + auto=False): + """ + Set the x-axis view limits for the parent Axes3D instance. + + .. ACCEPTS: (vmin: float, vmax: float) + + Parameters + ---------- + vmin : scalar + The smaller value of viewLim. + + vmax : scalar + The higher value of viewLim. + + ignore : bool, optional + Whether we should take (vmin, vmax) literally (default: False). + If this is False, the order of vmin, vmax does not matter; the + original axis orientation will be preserved. + + emit : bool, optional + Whether to notify observers of limit change (default: True). + + auto : bool or None, optional + Whether to turn on autoscaling of this axis. True turns on, + False turns off (default action), None leaves unchanged. + + Notes + ----- + If `ignore` is False (default), the view limits can be expanded, but + will not be reduced. Otherwise, if `ignore` is True, the `vmin` value + may be greater than the `vmax` value, in which case the viewLim will + decrease from left to right. + + Examples + -------- + >>> set_view_interval(vmin, vmax, ignore=True) + + Sets the new view interval to (vmin, vmax). + + >>> set_view_interval(vmin, vmax) + + Expands the view interval to the minimum of the current interval + and vmin, and the maximum of the current interval and vmax. + """ + Axis.set_view_interval(self, vmin, vmax, ignore=ignore, emit=emit, + auto=auto) + self.axes.xy_viewLim.intervalx = self.v_interval + if auto is not None: + self.axes.set_autoscalex_on(bool(auto)) + if emit: + self.axes.callbacks.process('xlim_changed', self.axes) + # Call all of the other x-axes that are shared with this one + for other in self.axes.get_shared_x_axes().get_siblings(self.axes): + if other is not self.axes: + other.set_xlim(self.v_interval, emit=False, auto=auto) + if (other.figure != self.axes.figure and + other.figure.canvas is not None): + other.figure.canvas.draw_idle() + self.axes.stale = True + + class YAxis(Axis): def get_data_interval(self): 'return the Interval instance for this axis data limits' return self.axes.xy_dataLim.intervaly + def set_view_interval(self, vmin, vmax, ignore=False, emit=True, + auto=False): + """ + Set the y-axis view limits for the parent Axes3D instance. + + .. ACCEPTS: (vmin: float, vmax: float) + + Parameters + ---------- + vmin : scalar + The smaller value of viewLim. + + vmax : scalar + The higher value of viewLim. + + ignore : bool, optional + Whether we should take (vmin, vmax) literally (default: False). + If this is False, the order of vmin, vmax does not matter; the + original axis orientation will be preserved. + + emit : bool, optional + Whether to notify observers of limit change (default: True). + + auto : bool or None, optional + Whether to turn on autoscaling of this axis. True turns on, + False turns off (default action), None leaves unchanged. + + Notes + ----- + If `ignore` is False (default), the view limits can be expanded, but + will not be reduced. Otherwise, if `ignore` is True, the `vmin` value + may be greater than the `vmax` value, in which case the viewLim will + decrease from left to right. + + Examples + -------- + >>> set_view_interval(vmin, vmax, ignore=True) + + Sets the new view interval to (vmin, vmax). + + >>> set_view_interval(vmin, vmax) + + Expands the view interval to the minimum of the current interval + and vmin, and the maximum of the current interval and vmax. + """ + Axis.set_view_interval(self, vmin, vmax, ignore=ignore, emit=emit, + auto=auto) + self.axes.xy_viewLim.intervaly = self.v_interval + if auto is not None: + self.axes.set_autoscaley_on(bool(auto)) + if emit: + self.axes.callbacks.process('ylim_changed', self.axes) + # Call all of the other x-axes that are shared with this one + for other in self.axes.get_shared_y_axes().get_siblings(self.axes): + if other is not self.axes: + other.set_ylim(self.v_interval, emit=False, auto=auto) + if (other.figure != self.axes.figure and + other.figure.canvas is not None): + other.figure.canvas.draw_idle() + self.axes.stale = True + + class ZAxis(Axis): def get_data_interval(self): 'return the Interval instance for this axis data limits' return self.axes.zz_dataLim.intervalx + + def set_view_interval(self, vmin, vmax, ignore=False, emit=True, + auto=False): + """ + Set the z-axis view limits for the parent Axes3D instance. + + .. ACCEPTS: (vmin: float, vmax: float) + + Parameters + ---------- + vmin : scalar + The smaller value of viewLim. + + vmax : scalar + The higher value of viewLim. + + ignore : bool, optional + Whether we should take (vmin, vmax) literally (default: False). + If this is False, the order of vmin, vmax does not matter; the + original axis orientation will be preserved. + + emit : bool, optional + Whether to notify observers of limit change (default: True). + + auto : bool or None, optional + Whether to turn on autoscaling of this axis. True turns on, + False turns off (default action), None leaves unchanged. + + Notes + ----- + If `ignore` is False (default), the view limits can be expanded, but + will not be reduced. Otherwise, if `ignore` is True, the `vmin` value + may be greater than the `vmax` value, in which case the viewLim will + decrease from left to right. + + Examples + -------- + >>> set_view_interval(vmin, vmax, ignore=True) + + Sets the new view interval to (vmin, vmax). + + >>> set_view_interval(vmin, vmax) + + Expands the view interval to the minimum of the current interval + and vmin, and the maximum of the current interval and vmax. + """ + Axis.set_view_interval(self, vmin, vmax, ignore=ignore, emit=emit, + auto=auto) + self.axes.zz_viewLim.intervalx = self.v_interval + if auto is not None: + self.axes.set_autoscalez_on(bool(auto)) + if emit: + self.axes.callbacks.process('zlim_changed', self.axes) + # Call all of the other x-axes that are shared with this one + for other in self.axes.get_shared_z_axes().get_siblings(self.axes): + if other is not self.axes: + other.set_zlim(self.v_interval, emit=False, auto=auto) + if (other.figure != self.axes.figure and + other.figure.canvas is not None): + other.figure.canvas.draw_idle() + self.axes.stale = True