diff --git a/docs/tutorials/shape-creation.rst b/docs/tutorials/shape-creation.rst index e388a153..9290693f 100644 --- a/docs/tutorials/shape-creation.rst +++ b/docs/tutorials/shape-creation.rst @@ -61,18 +61,18 @@ to calculate its position and scale: class XLines(LineCollection): + name = 'x' + def __init__(self, dataset: Dataset) -> None: xmin, xmax = dataset.morph_bounds.x_bounds ymin, ymax = dataset.morph_bounds.y_bounds super().__init__([[xmin, ymin], [xmax, ymax]], [[xmin, ymax], [xmax, ymin]]) - def __str__(self) -> str: - return 'x' Since we inherit from :class:`.LineCollection` here, we don't need to define the ``distance()`` and ``plot()`` methods (unless we want to override them). -We do override the ``__str__()`` method here since the default will result in +We do set the ``name`` attribute here since the default will result in a value of ``xlines`` and ``x`` makes more sense for use in the documentation (see :class:`.ShapeFactory`). @@ -89,8 +89,8 @@ For the ``data-morph`` CLI to find your shape, you need to register it with the 2. Add your shape to ``__all__`` in that module's ``__init__.py`` (*e.g.*, use ``src/data_morph/shapes/points/__init__.py`` for a new shape inheriting from :class:`.PointCollection`). -3. Add an entry to the ``ShapeFactory._SHAPE_MAPPING`` dictionary in - ``src/data_morph/shapes/factory.py``. +3. Add an entry to the ``ShapeFactory._SHAPE_CLASSES`` tuple in + ``src/data_morph/shapes/factory.py``, preserving alphabetical order. Test out the shape ------------------ diff --git a/src/data_morph/shapes/bases/shape.py b/src/data_morph/shapes/bases/shape.py index 36690f60..7d06c41d 100644 --- a/src/data_morph/shapes/bases/shape.py +++ b/src/data_morph/shapes/bases/shape.py @@ -13,6 +13,21 @@ class Shape(ABC): """Abstract base class for a shape.""" + name: str | None = None + """The display name for the shape, if the lowercased class name is not desired.""" + + @classmethod + def get_name(cls) -> str: + """ + Get the name of the shape. + + Returns + ------- + str + The name of the shape. + """ + return cls.name or cls.__name__.lower() + def __repr__(self) -> str: """ Return string representation of the shape. @@ -32,8 +47,12 @@ def __str__(self) -> str: ------- str The human-readable string representation of the shape. + + See Also + -------- + get_name : This calls the :meth:`.get_name` class method. """ - return self.__class__.__name__.lower() + return self.get_name() @abstractmethod def distance(self, x: Number, y: Number) -> float: diff --git a/src/data_morph/shapes/factory.py b/src/data_morph/shapes/factory.py index a0c8be23..d5975c63 100644 --- a/src/data_morph/shapes/factory.py +++ b/src/data_morph/shapes/factory.py @@ -57,33 +57,39 @@ class ShapeFactory: The starting dataset to morph into other shapes. """ + _SHAPE_CLASSES: tuple[type[Shape]] = ( + Bullseye, + Circle, + Club, + Diamond, + DotsGrid, + DownParabola, + Heart, + HighLines, + HorizontalLines, + LeftParabola, + Rectangle, + RightParabola, + Rings, + Scatter, + SlantDownLines, + SlantUpLines, + Spade, + Star, + UpParabola, + VerticalLines, + WideLines, + XLines, + ) + """New shape classes must be registered here.""" + _SHAPE_MAPPING: ClassVar[dict[str, type[Shape]]] = { - 'bullseye': Bullseye, - 'circle': Circle, - 'high_lines': HighLines, - 'h_lines': HorizontalLines, - 'slant_down': SlantDownLines, - 'slant_up': SlantUpLines, - 'v_lines': VerticalLines, - 'wide_lines': WideLines, - 'x': XLines, - 'dots': DotsGrid, - 'down_parab': DownParabola, - 'heart': Heart, - 'left_parab': LeftParabola, - 'scatter': Scatter, - 'right_parab': RightParabola, - 'up_parab': UpParabola, - 'diamond': Diamond, - 'rectangle': Rectangle, - 'rings': Rings, - 'star': Star, - 'club': Club, - 'spade': Spade, + shape_cls.get_name(): shape_cls for shape_cls in _SHAPE_CLASSES } + """Mapping of shape display names to classes.""" AVAILABLE_SHAPES: list[str] = sorted(_SHAPE_MAPPING.keys()) - """list[str]: The list of available shapes, which can be visualized with + """The list of available shapes, which can be visualized with :meth:`.plot_available_shapes`.""" def __init__(self, dataset: Dataset) -> None: diff --git a/src/data_morph/shapes/lines/high_lines.py b/src/data_morph/shapes/lines/high_lines.py index 37e62e2f..20c4354c 100644 --- a/src/data_morph/shapes/lines/high_lines.py +++ b/src/data_morph/shapes/lines/high_lines.py @@ -24,6 +24,8 @@ class HighLines(LineCollection): The starting dataset to morph into other shapes. """ + name = 'high_lines' + def __init__(self, dataset: Dataset) -> None: x_bounds = dataset.data_bounds.x_bounds y_bounds = dataset.data_bounds.y_bounds @@ -36,6 +38,3 @@ def __init__(self, dataset: Dataset) -> None: [[x_bounds[0], lower], [x_bounds[1], lower]], [[x_bounds[0], upper], [x_bounds[1], upper]], ) - - def __str__(self) -> str: - return 'high_lines' diff --git a/src/data_morph/shapes/lines/horizontal_lines.py b/src/data_morph/shapes/lines/horizontal_lines.py index bcc8213a..37ea3fb2 100644 --- a/src/data_morph/shapes/lines/horizontal_lines.py +++ b/src/data_morph/shapes/lines/horizontal_lines.py @@ -26,6 +26,8 @@ class HorizontalLines(LineCollection): The starting dataset to morph into other shapes. """ + name = 'h_lines' + def __init__(self, dataset: Dataset) -> None: x_bounds = dataset.data_bounds.x_bounds y_bounds = dataset.data_bounds.y_bounds @@ -36,6 +38,3 @@ def __init__(self, dataset: Dataset) -> None: for y in np.linspace(y_bounds[0], y_bounds[1], 5) ] ) - - def __str__(self) -> str: - return 'h_lines' diff --git a/src/data_morph/shapes/lines/slant_down.py b/src/data_morph/shapes/lines/slant_down.py index 7bb2991a..7392275e 100644 --- a/src/data_morph/shapes/lines/slant_down.py +++ b/src/data_morph/shapes/lines/slant_down.py @@ -24,6 +24,8 @@ class SlantDownLines(LineCollection): The starting dataset to morph into other shapes. """ + name = 'slant_down' + def __init__(self, dataset: Dataset) -> None: x_bounds = dataset.morph_bounds.x_bounds y_bounds = dataset.morph_bounds.y_bounds @@ -43,6 +45,3 @@ def __init__(self, dataset: Dataset) -> None: [[xmin + x_offset, ymax], [xmax, ymin + y_offset]], [[xmid, ymax], [xmax, ymid]], ) - - def __str__(self) -> str: - return 'slant_down' diff --git a/src/data_morph/shapes/lines/slant_up.py b/src/data_morph/shapes/lines/slant_up.py index 76465523..e13563ba 100644 --- a/src/data_morph/shapes/lines/slant_up.py +++ b/src/data_morph/shapes/lines/slant_up.py @@ -24,6 +24,8 @@ class SlantUpLines(LineCollection): The starting dataset to morph into other shapes. """ + name = 'slant_up' + def __init__(self, dataset: Dataset) -> None: x_bounds = dataset.morph_bounds.x_bounds y_bounds = dataset.morph_bounds.y_bounds @@ -43,6 +45,3 @@ def __init__(self, dataset: Dataset) -> None: [[xmin + x_offset, ymin], [xmax, ymid + y_offset]], [[xmid, ymin], [xmax, ymid]], ) - - def __str__(self) -> str: - return 'slant_up' diff --git a/src/data_morph/shapes/lines/vertical_lines.py b/src/data_morph/shapes/lines/vertical_lines.py index 23b239d0..74f72587 100644 --- a/src/data_morph/shapes/lines/vertical_lines.py +++ b/src/data_morph/shapes/lines/vertical_lines.py @@ -26,6 +26,8 @@ class VerticalLines(LineCollection): The starting dataset to morph into other shapes. """ + name = 'v_lines' + def __init__(self, dataset: Dataset) -> None: x_bounds = dataset.data_bounds.x_bounds y_bounds = dataset.data_bounds.y_bounds @@ -36,6 +38,3 @@ def __init__(self, dataset: Dataset) -> None: for x in np.linspace(x_bounds[0], x_bounds[1], 5) ] ) - - def __str__(self) -> str: - return 'v_lines' diff --git a/src/data_morph/shapes/lines/wide_lines.py b/src/data_morph/shapes/lines/wide_lines.py index 36cbe2a7..f919e2a6 100644 --- a/src/data_morph/shapes/lines/wide_lines.py +++ b/src/data_morph/shapes/lines/wide_lines.py @@ -24,6 +24,8 @@ class WideLines(LineCollection): The starting dataset to morph into other shapes. """ + name = 'wide_lines' + def __init__(self, dataset: Dataset) -> None: x_bounds = dataset.data_bounds.x_bounds y_bounds = dataset.data_bounds.y_bounds @@ -36,6 +38,3 @@ def __init__(self, dataset: Dataset) -> None: [[lower, y_bounds[0]], [lower, y_bounds[1]]], [[upper, y_bounds[0]], [upper, y_bounds[1]]], ) - - def __str__(self) -> str: - return 'wide_lines' diff --git a/src/data_morph/shapes/lines/x_lines.py b/src/data_morph/shapes/lines/x_lines.py index 14b367cb..edf550dd 100644 --- a/src/data_morph/shapes/lines/x_lines.py +++ b/src/data_morph/shapes/lines/x_lines.py @@ -24,11 +24,10 @@ class XLines(LineCollection): The starting dataset to morph into other shapes. """ + name = 'x' + def __init__(self, dataset: Dataset) -> None: xmin, xmax = dataset.morph_bounds.x_bounds ymin, ymax = dataset.morph_bounds.y_bounds super().__init__([[xmin, ymin], [xmax, ymax]], [[xmin, ymax], [xmax, ymin]]) - - def __str__(self) -> str: - return 'x' diff --git a/src/data_morph/shapes/points/dots_grid.py b/src/data_morph/shapes/points/dots_grid.py index 467a67ce..ddec20ce 100644 --- a/src/data_morph/shapes/points/dots_grid.py +++ b/src/data_morph/shapes/points/dots_grid.py @@ -26,6 +26,8 @@ class DotsGrid(PointCollection): The starting dataset to morph into other shapes. """ + name = 'dots' + def __init__(self, dataset: Dataset) -> None: xlow, xhigh = dataset.df.x.quantile([0.05, 0.95]).tolist() ylow, yhigh = dataset.df.y.quantile([0.05, 0.95]).tolist() @@ -36,6 +38,3 @@ def __init__(self, dataset: Dataset) -> None: super().__init__( *list(itertools.product([xlow, xmid, xhigh], [ylow, ymid, yhigh])) ) - - def __str__(self) -> str: - return 'dots' diff --git a/src/data_morph/shapes/points/parabola.py b/src/data_morph/shapes/points/parabola.py index dea94a99..ec2b0d41 100644 --- a/src/data_morph/shapes/points/parabola.py +++ b/src/data_morph/shapes/points/parabola.py @@ -26,6 +26,8 @@ class DownParabola(PointCollection): The starting dataset to morph into other shapes. """ + name = 'down_parab' + def __init__(self, dataset: Dataset) -> None: x_bounds = dataset.data_bounds.x_bounds xmin, xmax = x_bounds @@ -41,9 +43,6 @@ def __init__(self, dataset: Dataset) -> None: super().__init__(*np.stack(poly.linspace(), axis=1)) - def __str__(self) -> str: - return 'down_parab' - class LeftParabola(PointCollection): """ @@ -65,6 +64,8 @@ class LeftParabola(PointCollection): The starting dataset to morph into other shapes. """ + name = 'left_parab' + def __init__(self, dataset: Dataset) -> None: y_bounds = dataset.data_bounds.y_bounds ymin, ymax = y_bounds @@ -80,9 +81,6 @@ def __init__(self, dataset: Dataset) -> None: super().__init__(*np.stack(poly.linspace()[::-1], axis=1)) - def __str__(self) -> str: - return 'left_parab' - class RightParabola(PointCollection): """ @@ -104,6 +102,8 @@ class RightParabola(PointCollection): The starting dataset to morph into other shapes. """ + name = 'right_parab' + def __init__(self, dataset: Dataset) -> None: y_bounds = dataset.data_bounds.y_bounds ymin, ymax = y_bounds @@ -119,9 +119,6 @@ def __init__(self, dataset: Dataset) -> None: super().__init__(*np.stack(poly.linspace()[::-1], axis=1)) - def __str__(self) -> str: - return 'right_parab' - class UpParabola(PointCollection): """ @@ -143,6 +140,8 @@ class UpParabola(PointCollection): The starting dataset to morph into other shapes. """ + name = 'up_parab' + def __init__(self, dataset: Dataset) -> None: x_bounds = dataset.data_bounds.x_bounds xmin, xmax = x_bounds @@ -157,6 +156,3 @@ def __init__(self, dataset: Dataset) -> None: poly = np.polynomial.Polynomial.fit([xmin, xmid, xmax], [ymax, ymin, ymax], 2) super().__init__(*np.stack(poly.linspace(), axis=1)) - - def __str__(self) -> str: - return 'up_parab'