Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit46a6025

Browse files
committed
BUGFIX: Bbox union and transform, better semantics
1 parent6ec39d5 commit46a6025

File tree

2 files changed

+154
-44
lines changed

2 files changed

+154
-44
lines changed

‎lib/matplotlib/tests/test_transforms.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,7 @@ def test_bbox_intersection():
471471
# r3 contains r2
472472
assert_bbox_eq(inter(r1,r3),r3)
473473
# no intersection
474-
assertinter(r1,r4)isNone
474+
assert_bbox_eq(inter(r1,r4),mtransforms.Bbox.null())
475475
# single point
476476
assert_bbox_eq(inter(r1,r5),bbox_from_ext(1,1,1,1))
477477

@@ -569,8 +569,10 @@ def test_log_transform():
569569

570570
deftest_nan_overlap():
571571
a=mtransforms.Bbox([[0,0], [1,1]])
572-
b=mtransforms.Bbox([[0,0], [1,np.nan]])
573-
assertnota.overlaps(b)
572+
withpytest.warns(RuntimeWarning,
573+
match="invalid value encountered in less"):
574+
b=mtransforms.Bbox([[0,0], [1,np.nan]])
575+
assertnota.overlaps(b)
574576

575577

576578
deftest_transform_angles():

‎lib/matplotlib/transforms.py

Lines changed: 149 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,10 @@ class BboxBase(TransformNode):
224224
is_bbox=True
225225
is_affine=True
226226

227+
@staticmethod
228+
def_empty_set_points():
229+
returnnp.array([[np.inf,np.inf], [-np.inf,-np.inf]])
230+
227231
ifDEBUG:
228232
@staticmethod
229233
def_check(points):
@@ -655,30 +659,60 @@ def rotated(self, radians):
655659
returnbbox
656660

657661
@staticmethod
658-
defunion(bboxes):
662+
defunion(bboxes,null_as_empty=True):
659663
"""Return a `Bbox` that contains all of the given *bboxes*."""
660664
ifnotlen(bboxes):
661665
raiseValueError("'bboxes' cannot be empty")
662-
# needed for 1.14.4 < numpy_version < 1.16
663-
# can remove once we are at numpy >= 1.16
664-
withnp.errstate(invalid='ignore'):
665-
x0=np.min([bbox.xminforbboxinbboxes])
666-
x1=np.max([bbox.xmaxforbboxinbboxes])
667-
y0=np.min([bbox.yminforbboxinbboxes])
668-
y1=np.max([bbox.ymaxforbboxinbboxes])
666+
ifnotnull_as_empty:
667+
cbook.warn_deprecated(
668+
3.4,message="Previously, Bboxs with negative width or "
669+
"height could cause surprising results when unioned. In order "
670+
"to fix this, any Bbox with negative width or height will be "
671+
"treated as Bbox.null() (the empty set) starting in the next "
672+
"point release. To upgrade to this behavior now, please pass "
673+
"null_as_empty=True.")
674+
# needed for 1.14.4 < numpy_version < 1.16
675+
# can remove once we are at numpy >= 1.16
676+
withnp.errstate(invalid='ignore'):
677+
x0=np.min([bbox.xminforbboxinbboxes])
678+
x1=np.max([bbox.xmaxforbboxinbboxes])
679+
y0=np.min([bbox.yminforbboxinbboxes])
680+
y1=np.max([bbox.ymaxforbboxinbboxes])
681+
else:
682+
# needed for 1.14.4 < numpy_version < 1.16
683+
# can remove once we are at numpy >= 1.16
684+
withnp.errstate(invalid='ignore'):
685+
x0=np.min([bbox.x0forbboxinbboxes])
686+
x1=np.max([bbox.x1forbboxinbboxes])
687+
y0=np.min([bbox.y0forbboxinbboxes])
688+
y1=np.max([bbox.y1forbboxinbboxes])
669689
returnBbox([[x0,y0], [x1,y1]])
670690

671691
@staticmethod
672-
defintersection(bbox1,bbox2):
692+
defintersection(bbox1,bbox2,null_as_empty=True):
673693
"""
674694
Return the intersection of *bbox1* and *bbox2* if they intersect, or
675695
None if they don't.
676696
"""
677-
x0=np.maximum(bbox1.xmin,bbox2.xmin)
678-
x1=np.minimum(bbox1.xmax,bbox2.xmax)
679-
y0=np.maximum(bbox1.ymin,bbox2.ymin)
680-
y1=np.minimum(bbox1.ymax,bbox2.ymax)
681-
returnBbox([[x0,y0], [x1,y1]])ifx0<=x1andy0<=y1elseNone
697+
ifnotnull_as_empty:
698+
cbook.warn_deprecated(
699+
3.4,message="Previously, Bboxs with negative width or "
700+
"height could cause surprising results under intersection. To "
701+
"fix this, any Bbox with negative width or height will be "
702+
"treated as Bbox.null() (the empty set) starting in the next "
703+
"point release. To upgrade to this behavior now, please pass "
704+
"null_as_empty=True.")
705+
#TODO: should probably be nanmax...
706+
x0=np.maximum(bbox1.xmin,bbox2.xmin)
707+
x1=np.minimum(bbox1.xmax,bbox2.xmax)
708+
y0=np.maximum(bbox1.ymin,bbox2.ymin)
709+
y1=np.minimum(bbox1.ymax,bbox2.ymax)
710+
else:
711+
x0=np.maximum(bbox1.x0,bbox2.x0)
712+
x1=np.minimum(bbox1.x1,bbox2.x1)
713+
y0=np.maximum(bbox1.y0,bbox2.y0)
714+
y1=np.minimum(bbox1.y1,bbox2.y1)
715+
returnBbox([[x0,y0], [x1,y1]])
682716

683717

684718
classBbox(BboxBase):
@@ -708,16 +742,16 @@ class Bbox(BboxBase):
708742
709743
**Create from collections of points**
710744
711-
The "empty" object for accumulating Bboxs is the null bbox, which is a
712-
stand-in for the empty set.
745+
The "empty" object for accumulating Bboxs is the null bbox, which
746+
represents the empty set.
713747
714748
>>> Bbox.null()
715749
Bbox([[inf, inf], [-inf, -inf]])
716750
717751
Adding points to the null bbox will give you the bbox of those points.
718752
719753
>>> box = Bbox.null()
720-
>>> box.update_from_data_xy([[1, 1]])
754+
>>> box.update_from_data_xy([[1, 1]], ignore=False)
721755
>>> box
722756
Bbox([[1.0, 1.0], [1.0, 1.0]])
723757
>>> box.update_from_data_xy([[2, 3], [3, 2]], ignore=False)
@@ -736,33 +770,54 @@ class Bbox(BboxBase):
736770
default value of ``ignore`` can be changed at any time by code with
737771
access to your Bbox, for example using the method `~.Bbox.ignore`.
738772
739-
**Properties of the ``null`` bbox**
773+
**Create from a set of constraints**
740774
741-
.. note::
775+
The null object for accumulating Bboxs from constraints is the entire plane
776+
777+
>>> Bbox.unbounded()
778+
Bbox([[-inf, -inf], [inf, inf]])
779+
780+
By repeatedly intersecting Bboxs, we can refine the Bbox as needed
781+
782+
>>> constraints = Bbox.unbounded()
783+
>>> for box in [Bbox([[0, 0], [1, 1]]), Bbox([[-1, 1], [1, 1]])]:
784+
... constraints = Bbox.intersection(box, constraints)
785+
>>> constraints
786+
Bbox([[0.0, 1.0], [1.0, 1.0]])
787+
788+
**Algebra of Bboxs**
742789
743-
The current behavior of `Bbox.null()` may be surprising as it does
744-
not have all of the properties of the "empty set", and as such does
745-
not behave like a "zero" object in the mathematical sense. We may
746-
change that in the future (with a deprecation period).
790+
The family of all BBoxs forms a ring of sets, once we include the empty set
791+
(`Bbox.null`) and the full space (`Bbox.unbounded`).
747792
748-
The null bbox is the identity for intersections
793+
The unbounded bbox is the identity for intersections (the "multiplicative"
794+
identity)
749795
750-
>>> Bbox.intersection(Bbox([[1, 1], [3, 7]]), Bbox.null())
796+
>>> Bbox.intersection(Bbox([[1, 1], [3, 7]]), Bbox.unbounded())
751797
Bbox([[1.0, 1.0], [3.0, 7.0]])
752798
753-
exceptwithitself, where itreturns thefull space.
799+
and unionwiththe unbounded Bbox alwaysreturns theunbounded Bbox
754800
755-
>>> Bbox.intersection(Bbox.null(), Bbox.null())
801+
>>> Bbox.union([Bbox([[0, 0], [0, 0]]), Bbox.unbounded()])
756802
Bbox([[-inf, -inf], [inf, inf]])
757803
758-
A union containing null will always return the full space (not the other
759-
set!)
804+
The null Bbox is the identity for unions (the "additive" identity)
760805
761-
>>> Bbox.union([Bbox([[0, 0], [0, 0]]), Bbox.null()])
762-
Bbox([[-inf, -inf], [inf, inf]])
806+
>>> Bbox.union([Bbox.null(), Bbox([[1, 1], [3, 7]])])
807+
Bbox([[1.0, 1.0], [3.0, 7.0]])
808+
809+
and intersection with the null Bbox always returns the null Bbox
810+
811+
>>> Bbox.intersection(Bbox.null(), Bbox.unbounded())
812+
Bbox([[inf, inf], [-inf, -inf]])
813+
814+
.. note::
815+
816+
In order to ensure that there is a unique "empty set", all empty Bboxs
817+
are automatically converted to ``Bbox([[inf, inf], [-inf, -inf]])``.
763818
"""
764819

765-
def__init__(self,points,**kwargs):
820+
def__init__(self,points,null_as_empty=True,**kwargs):
766821
"""
767822
Parameters
768823
----------
@@ -774,6 +829,18 @@ def __init__(self, points, **kwargs):
774829
ifpoints.shape!= (2,2):
775830
raiseValueError('Bbox points must be of the form '
776831
'"[[x0, y0], [x1, y1]]".')
832+
ifnp.any(np.diff(points,axis=0)<0):
833+
ifnull_as_empty:
834+
points=self._empty_set_points()
835+
else:
836+
cbook.warn_deprecated(
837+
3.4,message="In order to guarantee that Bbox union and "
838+
"intersection can work correctly, Bboxs with negative "
839+
"width or height will always be converted to Bbox.null "
840+
"starting in the next point release. To silence this "
841+
"warning, explicitly pass explicitly set null_as_empty to "
842+
"True to enable this behavior now.")
843+
self._null_as_empty=null_as_empty
777844
self._points=points
778845
self._minpos=np.array([np.inf,np.inf])
779846
self._ignore=True
@@ -793,32 +860,48 @@ def invalidate(self):
793860
TransformNode.invalidate(self)
794861

795862
@staticmethod
796-
defunit():
863+
defunit(null_as_empty=True):
797864
"""Create a new unit `Bbox` from (0, 0) to (1, 1)."""
798-
returnBbox([[0,0], [1,1]])
865+
returnBbox([[0,0], [1,1]],null_as_empty=null_as_empty)
799866

800867
@staticmethod
801-
defnull():
868+
defnull(null_as_empty=True):
802869
"""Create a new null `Bbox` from (inf, inf) to (-inf, -inf)."""
803-
returnBbox([[np.inf,np.inf], [-np.inf,-np.inf]])
870+
returnBbox(Bbox._empty_set_points(),null_as_empty=null_as_empty)
804871

805872
@staticmethod
806-
deffrom_bounds(x0,y0,width,height):
873+
defunbounded(null_as_empty=True):
874+
"""Create a new unbounded `Bbox` from (-inf, -inf) to (inf, inf)."""
875+
returnBbox([[-np.inf,-np.inf], [np.inf,np.inf]],
876+
null_as_empty=null_as_empty)
877+
878+
@staticmethod
879+
deffrom_bounds(x0,y0,width,height,null_as_empty=True):
807880
"""
808881
Create a new `Bbox` from *x0*, *y0*, *width* and *height*.
809882
810-
*width*and *height*may benegative.
883+
If*width*or *height*arenegative, `Bbox.null` is returned.
811884
"""
812-
returnBbox.from_extents(x0,y0,x0+width,y0+height)
885+
ifwidth<0orheight<0ornp.isnan(width)ornp.isnan(height):
886+
ifnull_as_empty:
887+
returnBbox.null()
888+
else:
889+
cbook.warn_deprecated(
890+
3.4,message="As of the next point release, "
891+
"Bbox.from_bounds will return the null Bbox if *width* or "
892+
"*height* are negative. To enable this behavior now, pass "
893+
"null_as_empty=True.")
894+
returnBbox.from_extents(x0,y0,x0+width,y0+height,
895+
null_as_empty=null_as_empty)
813896

814897
@staticmethod
815-
deffrom_extents(*args):
898+
deffrom_extents(*args,null_as_empty=True):
816899
"""
817900
Create a new Bbox from *left*, *bottom*, *right* and *top*.
818901
819902
The *y*-axis increases upwards.
820903
"""
821-
returnBbox(np.reshape(args, (2,2)))
904+
returnBbox(np.reshape(args, (2,2)),null_as_empty=null_as_empty)
822905

823906
def__format__(self,fmt):
824907
return (
@@ -910,41 +993,57 @@ def update_from_data_xy(self, xy, ignore=None, updatex=True, updatey=True):
910993
@BboxBase.x0.setter
911994
defx0(self,val):
912995
self._points[0,0]=val
996+
ifself._null_as_emptyandself.x0>self.x1:
997+
self._points=self._empty_set_points()
913998
self.invalidate()
914999

9151000
@BboxBase.y0.setter
9161001
defy0(self,val):
9171002
self._points[0,1]=val
1003+
ifself._null_as_emptyandself.y0>self.y1:
1004+
self._points=self._empty_set_points()
9181005
self.invalidate()
9191006

9201007
@BboxBase.x1.setter
9211008
defx1(self,val):
9221009
self._points[1,0]=val
1010+
ifself._null_as_emptyandself.x0>self.x1:
1011+
self._points=self._empty_set_points()
9231012
self.invalidate()
9241013

9251014
@BboxBase.y1.setter
9261015
defy1(self,val):
9271016
self._points[1,1]=val
1017+
ifself._null_as_emptyandself.y0>self.y1:
1018+
self._points=self._empty_set_points()
9281019
self.invalidate()
9291020

9301021
@BboxBase.p0.setter
9311022
defp0(self,val):
9321023
self._points[0]=val
1024+
ifself._null_as_emptyand (self.y0>self.y1orself.x0>self.x1):
1025+
self._points=self._empty_set_points()
9331026
self.invalidate()
9341027

9351028
@BboxBase.p1.setter
9361029
defp1(self,val):
9371030
self._points[1]=val
1031+
ifself._null_as_emptyand (self.y0>self.y1orself.x0>self.x1):
1032+
self._points=self._empty_set_points()
9381033
self.invalidate()
9391034

9401035
@BboxBase.intervalx.setter
9411036
defintervalx(self,interval):
9421037
self._points[:,0]=interval
1038+
ifself._null_as_emptyandself.x0>self.x1:
1039+
self._points=self._empty_set_points()
9431040
self.invalidate()
9441041

9451042
@BboxBase.intervaly.setter
9461043
defintervaly(self,interval):
9471044
self._points[:,1]=interval
1045+
ifself._null_as_emptyandself.y0>self.y1:
1046+
self._points=self._empty_set_points()
9481047
self.invalidate()
9491048

9501049
@BboxBase.bounds.setter
@@ -953,6 +1052,9 @@ def bounds(self, bounds):
9531052
points=np.array([[l,b], [l+w,b+h]],float)
9541053
ifnp.any(self._points!=points):
9551054
self._points=points
1055+
ifself._null_as_emptyand \
1056+
(self.y0>self.y1orself.x0>self.x1):
1057+
self._points=self._empty_set_points()
9561058
self.invalidate()
9571059

9581060
@property
@@ -983,6 +1085,9 @@ def set_points(self, points):
9831085
"""
9841086
ifnp.any(self._points!=points):
9851087
self._points=points
1088+
ifself._null_as_empty \
1089+
and (self.y0>self.y1orself.x0>self.x1):
1090+
self._points=self._empty_set_points()
9861091
self.invalidate()
9871092

9881093
defset(self,other):
@@ -991,6 +1096,9 @@ def set(self, other):
9911096
"""
9921097
ifnp.any(self._points!=other.get_points()):
9931098
self._points=other.get_points()
1099+
ifself._null_as_empty \
1100+
and (self.y0>self.y1orself.x0>self.x1):
1101+
self._points=self._empty_set_points()
9941102
self.invalidate()
9951103

9961104
defmutated(self):

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp