8000 Merge pull request #18642 from QuLogic/collection-datalim · matplotlib/matplotlib@777cba6 · GitHub
[go: up one dir, main page]

Skip to content

Commit 777cba6

Browse files
authored
Merge pull request #18642 from QuLogic/collection-datalim
Propagate minpos from Collections to Axes.datalim
2 parents a7c5e6f + e85ea1b commit 777cba6

File tree

6 files changed

+90
-14
lines changed

6 files changed

+90
-14
lines changed

lib/matplotlib/axes/_base.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1999,7 +1999,16 @@ def add_collection(self, collection, autolim=True):
19991999
# Make sure viewLim is not stale (mostly to match
20002000
# pre-lazy-autoscale behavior, which is not really better).
20012001
self._unstale_viewLim()
2002-
self.update_datalim(collection.get_datalim(self.transData))
2002+
datalim = collection.get_datalim(self.transData)
2003+
points = datalim.get_points()
2004+
if not np.isinf(datalim.minpos).all():
2005+
# By definition, if minpos (minimum positive value) is set
2006+
8000 # (i.e., non-inf), then min(points) <= minpos <= max(points),
2007+
# and minpos would be superfluous. However, we add minpos to
2008+
# the call so that self.dataLim will update its own minpos.
2009+
# This ensures that log scales see the correct minimum.
2010+
points = np.concatenate([points, [datalim.minpos]])
2011+
self.update_datalim(points)
20032012

20042013
self.stale = True
20052014
return collection

lib/matplotlib/collections.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -274,11 +274,11 @@ def get_datalim(self, transData):
274274
# can properly have the axes limits set by their shape +
275275
# offset. LineCollections that have no offsets can
276276
# also use this algorithm (like streamplot).
277-
result = mpath.get_path_collection_extents(
278-
transform.get_affine(), paths, self.get_transforms(),
277+
return mpath.get_path_collection_extents(
278+
transform.get_affine() - transData, paths,
279+
self.get_transforms(),
279280
transOffset.transform_non_affine(offsets),
280281
transOffset.get_affine().frozen())
281-
return result.transformed(transData.inverted())
282282
if not self._offsetsNone:
283283
# this is for collections that have their paths (shapes)
284284
# in physical, axes-relative, or figure-relative units
@@ -290,9 +290,9 @@ def get_datalim(self, transData):
290290
# note A-B means A B^{-1}
291291
offsets = np.ma.masked_invalid(offsets)
292292
if not offsets.mask.all():
293-
points = np.row_stack((offsets.min(axis=0),
294-
offsets.max(axis=0)))
295-
return transforms.Bbox(points)
293+
bbox = transforms.Bbox.null()
294+
bbox.update_from_data_xy(offsets)
295+
return bbox
296296
return transforms.Bbox.null()
297297

298298
def get_window_extent(self, renderer):

lib/matplotlib/path.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,6 +1069,7 @@ def get_path_collection_extents(
10691069
from .transforms import Bbox
10701070
if len(paths) == 0:
10711071
raise ValueError("No paths provided")
1072-
return Bbox.from_extents(*_path.get_path_collection_extents(
1072+
extents, minpos = _path.get_path_collection_extents(
10731073
master_transform, paths, np.atleast_3d(transforms),
1074-
offsets, offset_transform))
1074+
offsets, offset_transform)
1075+
return Bbox.from_extents(*extents, minpos=minpos)

lib/matplotlib/tests/test_collections.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import matplotlib.transforms as mtransforms
1212
from matplotlib.collections import (Collection, LineCollection,
1313
EventCollection, PolyCollection)
14-
from matplotlib.testing.decorators import image_comparison
14+
from matplotlib.testing.decorators import check_figures_equal, image_comparison
1515

1616

1717
def generate_EventCollection_plot():
@@ -301,6 +301,32 @@ def test_add_collection():
301301
assert ax.dataLim.bounds == bounds
302302

303303

304+
@pytest.mark.style('mpl20')
305+
@check_figures_equal(extensions=['png'])
306+
def test_collection_log_datalim(fig_test, fig_ref):
307+
# Data limits should respect the minimum x/y when using log scale.
308+
x_vals = [4.38462e-6, 5.54929e-6, 7.02332e-6, 8.88889e-6, 1.12500e-5,
309+
1.42383e-5, 1.80203e-5, 2.28070e-5, 2.88651e-5, 3.65324e-5,
310+
4.62363e-5, 5.85178e-5, 7.40616e-5, 9.37342e-5, 1.18632e-4]
311+
y_vals = [0.0, 0.1, 0.182, 0.332, 0.604, 1.1, 2.0, 3.64, 6.64, 12.1, 22.0,
312+
39.6, 71.3]
313+
314+
x, y = np.meshgrid(x_vals, y_vals)
315+
x = x.flatten()
316+
y = y.flatten()
317+
318+
ax_test = fig_test.subplots()
319+
ax_test.set_xscale('log')
320+
ax_test.set_yscale('log')
321+
ax_test.margins = 0
322+
ax_test.scatter(x, y)
323+
324+
ax_ref = fig_ref.subplots()
325+
ax_ref.set_xscale('log')
326+
ax_ref.set_yscale('log')
327+
ax_ref.plot(x, y, marker="o", ls="")
328+
329+
304330
def test_quiver_limits():
305331
ax = plt.axes()
306332
x, y = np.arange(8), np.arange(10)

lib/matplotlib/transforms.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -808,13 +808,26 @@ def from_bounds(x0, y0, width, height):
808808
return Bbox.from_extents(x0, y0, x0 + width, y0 + height)
809809

810810
@staticmethod
811-
def from_extents(*args):
811+
def from_extents(*args, minpos=None):
812812
"""
813813
Create a new Bbox from *left*, *bottom*, *right* and *top*.
814814
815815
The *y*-axis increases upwards.
816+
817+
Parameters
818+
----------
819+
left, bottom, right, top : float
820+
The four extents of the bounding box.
821+
822+
minpos : float or None
823+
If this is supplied, the Bbox will have a minimum positive value
824+
set. This is useful when dealing with logarithmic scales and other
825+
scales where negative bounds result in floating point errors.
816826
"""
817-
return Bbox(np.reshape(args, (2, 2)))
827+
bbox = Bbox(np.reshape(args, (2, 2)))
828+
if minpos is not None:
829+
bbox._minpos[:] = minpos
830+
return bbox
818831

819832
def __format__(self, fmt):
820833
return (
@@ -953,14 +966,35 @@ def bounds(self, bounds):
953966

954967
@property
955968
def minpos(self):
969+
"""
970+
The minimum positive value in both directions within the Bbox.
971+
972+
This is useful when dealing with logarithmic scales and other scales
973+
where negative bounds result in floating point errors, and will be used
974+
as the minimum extent instead of *p0*.
975+
"""
956976
return self._minpos
957977

958978
@property
959979
def minposx(self):
980+
"""
981+
The minimum positive value in the *x*-direction within the Bbox.
982+
983+
This is useful when dealing with logarithmic scales and other scales
984+
where negative bounds result in floating point errors, and will be used
985+
as the minimum *x*-extent instead of *x0*.
986+
"""
960987
return self._minpos[0]
961988

962989
@property
963990
def minposy(self):
991+
"""
992+
The minimum positive value in the *y*-direction within the Bbox.
993+
994+
This is useful when dealing with logarithmic scales and other scales
995+
where negative bounds result in floating point errors, and will be used
996+
as the minimum *y*-extent instead of *y0*.
997+
"""
964998
return self._minpos[1]
965999

9661000
def get_points(self):

src/_path_wrapper.cpp

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,8 @@ static PyObject *Py_update_path_extents(PyObject *self, PyObject *args, PyObject
250250
"NNi", outextents.pyobj(), outminpos.pyobj(), changed);
251251
}
252252

253-
const char *Py_get_path_collection_extents__doc__ = "get_path_collection_extents(";
253+
const char *Py_get_path_collection_extents__doc__ = "get_path_collection_extents("
254+
"master_transform, paths, transforms, offsets, offset_transform)";
254255

255256
static PyObject *Py_get_path_collection_extents(PyObject *self, PyObject *args, PyObject *kwds)
256257
{
@@ -295,7 +296,12 @@ static PyObject *Py_get_path_collection_extents(PyObject *self, PyObject *args,
295296
extents(1, 0) = e.x1;
296297
extents(1, 1) = e.y1;
297298

298-
return extents.pyobj();
299+
npy_intp minposdims[] = { 2 };
300+
numpy::array_view<double, 1> minpos(minposdims);
301+
minpos(0) = e.xm;
302+
minpos(1) = e.ym;
303+
304+
return Py_BuildValue("NN", extents.pyobj(), minpos.pyobj());
299305
}
300306

301307
const char *Py_point_in_path_collection__doc__ =

0 commit comments

Comments
 (0)
0