Bug summary
CompositeGenericTransform.contains_branch_separately() only checks the second component (_b) of the composite transform, completely ignoring the first component (_a). This means if a composed transform is A + B and the target transform is a subtree of A, it returns (False, False) instead of the correct result.
Code for reproduction
A composite transform A + B where the query transform is found in component A:
import matplotlib.transforms as mtransforms
from matplotlib.transforms import Transform, BlendedGenericTransform
# Two independent non-affine transforms
class NonAffine(Transform):
input_dims = 2
output_dims = 2
is_separable = True
is_affine = False
def transform_non_affine(self, values): return values
tn1 = NonAffine()
tn2 = NonAffine()
# Blended transform: _x=tn1, _y=tn2
blend = BlendedGenericTransform(tn1, tn2)
# Composite: _a=blend, _b=tn1
composite = blend + tn1
# tn1 is a subtree of _a.x, but NOT of _b
x, y = composite.contains_branch_separately(tn1)
print(f'x={x}, y={y}') # Got: False, False. Expected: True, False
Actual outcome
contains_branch_separately returns (False, False).
Expected outcome
contains_branch_separately should return (True, False) since tn1 is in the _a.x branch.
Buggy code location
https://github.com/matplotlib/matplotlib/blob/main/lib/matplotlib/transforms.py#L2450-L2457
def contains_branch_separately(self, other_transform):
if self.output_dims != 2:
raise ValueError('contains_branch_separately only supports '
'transforms with 2 output dimensions')
if self == other_transform:
return (True, True)
return self._b.contains_branch_separately(other_transform) # <-- only _b!
The fix should check both children:
def contains_branch_separately(self, other_transform):
if self.output_dims != 2:
raise ValueError('contains_branch_separately only supports '
'transforms with 2 output dimensions')
if self == other_transform:
return (True, True)
a_result = self._a.contains_branch_separately(other_transform)
b_result = self._b.contains_branch_separately(other_transform)
return (a_result[0] or b_result[0], a_result[1] or b_result[1])
Impact
This bug affects any code that uses contains_branch_separately on composite transforms, including autoscaling and axis limit updates in axes/_base.py, axes/_axes.py, collections.py, and contour.py. When a composite transform's first component contains transData, the method would incorrectly return False, potentially causing autoscaling to not trigger.
Matplotlib version
main branch (all versions with this code).
Note: CompositeAffine2D does NOT have this bug because it doesn't override contains_branch_separately and inherits the base Transform implementation which uses contains_branch -> _iter_break_from_left_to_right, which correctly iterates both children.
Bug summary
CompositeGenericTransform.contains_branch_separately()only checks the second component (_b) of the composite transform, completely ignoring the first component (_a). This means if a composed transform isA + Band the target transform is a subtree ofA, it returns(False, False)instead of the correct result.Code for reproduction
A composite transform
A + Bwhere the query transform is found in componentA:Actual outcome
contains_branch_separatelyreturns(False, False).Expected outcome
contains_branch_separatelyshould return(True, False)sincetn1is in the_a.xbranch.Buggy code location
https://github.com/matplotlib/matplotlib/blob/main/lib/matplotlib/transforms.py#L2450-L2457
The fix should check both children:
Impact
This bug affects any code that uses
contains_branch_separatelyon composite transforms, including autoscaling and axis limit updates inaxes/_base.py,axes/_axes.py,collections.py, andcontour.py. When a composite transform's first component containstransData, the method would incorrectly returnFalse, potentially causing autoscaling to not trigger.Matplotlib version
main branch (all versions with this code).
Note:
CompositeAffine2Ddoes NOT have this bug because it doesn't overridecontains_branch_separatelyand inherits the baseTransformimplementation which usescontains_branch->_iter_break_from_left_to_right, which correctly iterates both children.