From 8896d903b68e47734f90952e969676645418df9b Mon Sep 17 00:00:00 2001 From: Lee Johnston Date: Mon, 17 Aug 2020 12:19:57 -0500 Subject: [PATCH 1/4] Fix autoscaling to exclude inifinite data limits when possible. Fix #18137 --- lib/matplotlib/axes/_base.py | 11 ++++++++++- lib/matplotlib/tests/test_axes.py | 9 +++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 045e68925766..d90ceb18e8d9 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2557,7 +2557,16 @@ def handle_single_axis(scale, autoscaleon, shared_axes, interval, dl.extend(y_finite) bb = mtransforms.BboxBase.union(dl) - x0, x1 = getattr(bb, interval) + # Issue 18137 + # bb can still have infinite limits, so instead compute + # finite limits for this 'axis' + x_values = [getattr(d, interval) for d in dl] + x_values = np.sort(np.unique(np.asarray(x_values).flatten())) + finite_x_values = np.extract(np.isfinite(x_values), x_values) + if finite_x_values.size >= 1: + x0, x1 = (finite_x_values.min(), finite_x_values.max()) + else: + x0, x1 = (-np.inf, np.inf) # If x0 and x1 are non finite, use the locator to figure out # default limits. locator = axis.get_major_locator() diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 84b13405af33..45fdcf5d6182 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -6480,3 +6480,12 @@ def test_relative_ticklabel_sizes(size): for name, axis in zip(['x', 'y'], [ax.xaxis, ax.yaxis]): for tick in axis.get_major_ticks(): assert tick.label1.get_size() == axis._get_tick_label_size(name) + + +def test_multiplot_autoscale(): + fig = plt.figure() + ax1, ax2 = fig.subplots(2, 1, sharex='all') + ax1.scatter([1, 2, 3, 4], [2, 3, 2, 3]) + ax2.axhspan(-5, 5) + xlim = ax1.get_xlim() + assert np.allclose(xlim, [0.5, 4.5]) From 1718f345f513a1ea8a0725333fdbcbe46352b8b4 Mon Sep 17 00:00:00 2001 From: Lee Johnston Date: Mon, 17 Aug 2020 14:21:42 -0500 Subject: [PATCH 2/4] Improve finite data limit algorithm efficiency --- lib/matplotlib/axes/_base.py | 32 ++++++++++---------------------- 1 file changed, 10 insertions(+), 22 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index d90ceb18e8d9..3c10af3ea813 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2541,27 +2541,15 @@ def handle_single_axis(scale, autoscaleon, shared_axes, interval, shared = shared_axes.get_siblings(self) dl = [ax.dataLim for ax in shared] # ignore non-finite data limits if good limits exist - finite_dl = [d for d in dl if np.isfinite(d).all()] - if len(finite_dl): - # if finite limits exist for at least one axis (and the - # other is infinite), restore the finite limits - x_finite = [d for d in dl - if (np.isfinite(d.intervalx).all() and - (d not in finite_dl))] - y_finite = [d for d in dl - if (np.isfinite(d.intervaly).all() and - (d not in finite_dl))] - - dl = finite_dl - dl.extend(x_finite) - dl.extend(y_finite) - - bb = mtransforms.BboxBase.union(dl) - # Issue 18137 - # bb can still have infinite limits, so instead compute - # finite limits for this 'axis' - x_values = [getattr(d, interval) for d in dl] - x_values = np.sort(np.unique(np.asarray(x_values).flatten())) + # issue 18137: The previous code would allow one subplot with + # inifinite data limits to 'clobber' other subplots with finite + # data limits. + x_values = [] + minpos_values = [] + for d in dl: + x_values.extend(getattr(d, interval)) + minpos_values.append(getattr(d, minpos)) + x_values = np.sort(x_values).flatten() finite_x_values = np.extract(np.isfinite(x_values), x_values) if finite_x_values.size >= 1: x0, x1 = (finite_x_values.min(), finite_x_values.max()) @@ -2587,7 +2575,7 @@ def handle_single_axis(scale, autoscaleon, shared_axes, interval, # Add the margin in figure space and then transform back, to handle # non-linear scales. - minpos = getattr(bb, minpos) + minpos = np.min(minpos_values) transform = axis.get_transform() inverse_trans = transform.inverted() x0, x1 = axis._scale.limit_range_for_scale(x0, x1, minpos) From 3aecf00e31a714cf679d4919d8367e60bbfb8314 Mon Sep 17 00:00:00 2001 From: Lee Johnston Date: Mon, 17 Aug 2020 18:47:30 -0500 Subject: [PATCH 3/4] Reduce the data limit logic some more --- lib/matplotlib/axes/_base.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 3c10af3ea813..98366f1844a6 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2539,20 +2539,19 @@ def handle_single_axis(scale, autoscaleon, shared_axes, interval, return # nothing to do... shared = shared_axes.get_siblings(self) - dl = [ax.dataLim for ax in shared] - # ignore non-finite data limits if good limits exist - # issue 18137: The previous code would allow one subplot with - # inifinite data limits to 'clobber' other subplots with finite - # data limits. + # Base autoscaling on finite data limits when there is at least one + # finite data limit among all the shared_axes and intervals. + # Also, find the minimum minpos for use in the margin calculation. x_values = [] - minpos_values = [] - for d in dl: - x_values.extend(getattr(d, interval)) - minpos_values.append(getattr(d, minpos)) - x_values = np.sort(x_values).flatten() - finite_x_values = np.extract(np.isfinite(x_values), x_values) - if finite_x_values.size >= 1: - x0, x1 = (finite_x_values.min(), finite_x_values.max()) + minimum_minpos = np.inf + for ax in shared: + x_values.extend(getattr(ax.dataLim, interval)) + minimum_minpos = np.min( + [minimum_minpos, getattr(ax.dataLim, minpos)] + ) + x_values = np.extract(np.isfinite(x_values), x_values) + if x_values.size >= 1: + x0, x1 = (x_values.min(), x_values.max()) else: x0, x1 = (-np.inf, np.inf) # If x0 and x1 are non finite, use the locator to figure out @@ -2575,10 +2574,9 @@ def handle_single_axis(scale, autoscaleon, shared_axes, interval, # Add the margin in figure space and then transform back, to handle # non-linear scales. - minpos = np.min(minpos_values) transform = axis.get_transform() inverse_trans = transform.inverted() - x0, x1 = axis._scale.limit_range_for_scale(x0, x1, minpos) + x0, x1 = axis._scale.limit_range_for_scale(x0, x1, minimum_minpos) x0t, x1t = transform.transform([x0, x1]) delta = (x1t - x0t) * margin if not np.isfinite(delta): From ee086d6337a57a83b5e0c3055097ef6cfe52b77e Mon Sep 17 00:00:00 2001 From: Lee Johnston Date: Mon, 17 Aug 2020 20:22:48 -0500 Subject: [PATCH 4/4] Improve minimum_minpos efficiency Co-authored-by: Elliott Sales de Andrade --- lib/matplotlib/axes/_base.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 98366f1844a6..282517cba817 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2546,9 +2546,8 @@ def handle_single_axis(scale, autoscaleon, shared_axes, interval, minimum_minpos = np.inf for ax in shared: x_values.extend(getattr(ax.dataLim, interval)) - minimum_minpos = np.min( - [minimum_minpos, getattr(ax.dataLim, minpos)] - ) + minimum_minpos = min(minimum_minpos, + getattr(ax.dataLim, minpos)) x_values = np.extract(np.isfinite(x_values), x_values) if x_values.size >= 1: x0, x1 = (x_values.min(), x_values.max())