Source code for data_morph.bounds.bounding_box
"""Class for working with two-dimensional bounds."""
from numbers import Number
from typing import Iterable, Union
from ._utils import _validate_2d
from .interval import Interval
[docs]
class BoundingBox:
"""
Class representing 2-dimensional range of numeric values.
Parameters
----------
x_bounds, y_bounds : Union[Interval, Iterable[numbers.Number]]
A 2-dimensional numeric iterable or an :class:`.Interval` object.
inclusive : bool, default ``False``
Whether the bounds include the endpoints. Default
is exclusive. If :class:`.Interval` objects are provided,
their settings are used.
"""
def __init__(
self,
x_bounds: Union[Interval, Iterable[Number]],
y_bounds: Union[Interval, Iterable[Number]],
inclusive: Iterable[bool] = False,
):
if x_bounds is None or y_bounds is None:
raise ValueError('BoundingBox requires bounds for both dimensions.')
if isinstance(inclusive, bool):
inclusive = [inclusive] * 2
if not (
isinstance(inclusive, (tuple, list))
and len(inclusive) == 2
and all(isinstance(x, bool) for x in inclusive)
):
raise ValueError(
'inclusive must be an iterable of 2 Boolean values'
' or a single Boolean value'
)
self.x_bounds = (
x_bounds.clone()
if isinstance(x_bounds, Interval)
else Interval(x_bounds, inclusive[0])
)
"""Interval: The bounds for the x direction."""
self.y_bounds = (
y_bounds.clone()
if isinstance(y_bounds, Interval)
else Interval(y_bounds, inclusive[1])
)
"""Interval: The bounds for the y direction."""
def __contains__(self, value: Iterable[Number]) -> bool:
"""
Add support for using the ``in`` operator to check whether
a two-dimensional point is in the bounding box.
Parameters
----------
value : Iterable[numbers.Number]
A two-dimensional point.
Returns
-------
bool
Whether ``value`` is contained in the bounding box.
"""
x, y = _validate_2d(value, 'input')
return x in self.x_bounds and y in self.y_bounds
def __eq__(self, other: 'BoundingBox') -> bool:
"""
Check whether two :class:`.BoundingBox` objects are equivalent.
Parameters
----------
other : BoundingBox
A :class:`.BoundingBox` object.
Returns
-------
bool
Whether the two :class:`.BoundingBox` objects are equivalent.
"""
if not isinstance(other, BoundingBox):
raise TypeError('Equality is only defined between BoundingBox objects.')
return self.x_bounds == other.x_bounds and self.y_bounds == other.y_bounds
def __repr__(self) -> str:
return '<BoundingBox>\n' f' x={self.x_bounds}' '\n' f' y={self.y_bounds}'
[docs]
def adjust_bounds(self, x: Number = None, y: Number = None) -> None:
"""
Adjust bounding box range.
Parameters
----------
x : numbers.Number, optional
The amount to change the x bound range by (half will be applied to each end).
y : numbers.Number, optional
The amount to change the y bound range by (half will be applied to each end).
See Also
--------
:meth:`.Interval.adjust_bounds` : Method that performs the adjustment.
"""
if x:
self.x_bounds.adjust_bounds(x)
if y:
self.y_bounds.adjust_bounds(y)
[docs]
def align_aspect_ratio(self) -> None:
"""Align the aspect ratio to 1:1."""
x_range, y_range = self.range
diff = x_range - y_range
if diff < 0:
self.adjust_bounds(x=-diff)
elif diff > 0:
self.adjust_bounds(y=diff)
@property
def aspect_ratio(self) -> Number:
"""
Calculate the aspect ratio of the bounding box.
Returns
-------
numbers.Number
The range in the x direction divided by the range in the y direction.
"""
x_range, y_range = self.range
return x_range / y_range
[docs]
def clone(self) -> 'BoundingBox':
"""
Clone this instance.
Returns
-------
BoundingBox
A new :class:`.BoundingBox` instance with the same bounds.
"""
return BoundingBox(
self.x_bounds.clone(),
self.y_bounds.clone(),
)
@property
def range(self) -> Iterable[Number]:
"""
Calculate the range (width) of the bounding box in each direction.
Returns
-------
Iterable[numbers.Number]
The range covered by the x and y bounds, respectively.
"""
return self.x_bounds.range, self.y_bounds.range