From 712cbf78c1af78c03df96c0f8a405911a420a536 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:00:17 +1100 Subject: [PATCH 1/3] Refactor shape files (#240) --- .pre-commit-config.yaml | 4 +- docs/tutorials/shape-creation.rst | 12 +- src/data_morph/shapes/factory.py | 69 ++- src/data_morph/shapes/lines.py | 268 ----------- src/data_morph/shapes/lines/__init__.py | 25 + src/data_morph/shapes/lines/diamond.py | 40 ++ src/data_morph/shapes/lines/high_lines.py | 41 ++ .../shapes/lines/horizontal_lines.py | 41 ++ src/data_morph/shapes/lines/rectangle.py | 37 ++ src/data_morph/shapes/lines/slant_down.py | 48 ++ src/data_morph/shapes/lines/slant_up.py | 48 ++ src/data_morph/shapes/lines/star.py | 55 +++ src/data_morph/shapes/lines/vertical_lines.py | 41 ++ src/data_morph/shapes/lines/wide_lines.py | 41 ++ src/data_morph/shapes/lines/x_lines.py | 34 ++ src/data_morph/shapes/points.py | 446 ------------------ src/data_morph/shapes/points/__init__.py | 20 + src/data_morph/shapes/points/club.py | 154 ++++++ src/data_morph/shapes/points/dots_grid.py | 41 ++ src/data_morph/shapes/points/heart.py | 54 +++ src/data_morph/shapes/points/parabola.py | 162 +++++++ src/data_morph/shapes/points/scatter.py | 66 +++ src/data_morph/shapes/points/spade.py | 119 +++++ src/data_morph/shapes/polygons.py | 124 ----- 24 files changed, 1123 insertions(+), 867 deletions(-) delete mode 100644 src/data_morph/shapes/lines.py create mode 100644 src/data_morph/shapes/lines/__init__.py create mode 100644 src/data_morph/shapes/lines/diamond.py create mode 100644 src/data_morph/shapes/lines/high_lines.py create mode 100644 src/data_morph/shapes/lines/horizontal_lines.py create mode 100644 src/data_morph/shapes/lines/rectangle.py create mode 100644 src/data_morph/shapes/lines/slant_down.py create mode 100644 src/data_morph/shapes/lines/slant_up.py create mode 100644 src/data_morph/shapes/lines/star.py create mode 100644 src/data_morph/shapes/lines/vertical_lines.py create mode 100644 src/data_morph/shapes/lines/wide_lines.py create mode 100644 src/data_morph/shapes/lines/x_lines.py delete mode 100644 src/data_morph/shapes/points.py create mode 100644 src/data_morph/shapes/points/__init__.py create mode 100644 src/data_morph/shapes/points/club.py create mode 100644 src/data_morph/shapes/points/dots_grid.py create mode 100644 src/data_morph/shapes/points/heart.py create mode 100644 src/data_morph/shapes/points/parabola.py create mode 100644 src/data_morph/shapes/points/scatter.py create mode 100644 src/data_morph/shapes/points/spade.py delete mode 100644 src/data_morph/shapes/polygons.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0da08462..99b26719 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: exclude: (\.(svg|png|pdf)$)|(CODE_OF_CONDUCT.md) - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.1 + rev: v0.8.0 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes] @@ -44,7 +44,7 @@ repos: files: tests/.* - repo: https://github.com/tox-dev/pyproject-fmt - rev: v2.4.3 + rev: v2.5.0 hooks: - id: pyproject-fmt args: [--keep-full-version, --no-print-diff] diff --git a/docs/tutorials/shape-creation.rst b/docs/tutorials/shape-creation.rst index 95dd877f..e388a153 100644 --- a/docs/tutorials/shape-creation.rst +++ b/docs/tutorials/shape-creation.rst @@ -82,10 +82,14 @@ Register the shape For the ``data-morph`` CLI to find your shape, you need to register it with the :class:`.ShapeFactory`: -1. Add your shape class to the appropriate file inside the ``src/data_morph/shapes/`` - directory. Note that the filenames correspond to the type of shape (*e.g.*, use - ``src/data_morph/shapes/points.py`` for a new shape inheriting from :class:`.PointCollection`). -2. Add an entry to the ``ShapeFactory._SHAPE_MAPPING`` dictionary in +1. Add your shape class to the appropriate module inside the ``src/data_morph/shapes/`` + directory. Note that these correspond to the type of shape (*e.g.*, use + ``src/data_morph/shapes/points/.py`` for a new shape inheriting from + :class:`.PointCollection`). +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``. Test out the shape diff --git a/src/data_morph/shapes/factory.py b/src/data_morph/shapes/factory.py index ca8be28d..ca2f1d23 100644 --- a/src/data_morph/shapes/factory.py +++ b/src/data_morph/shapes/factory.py @@ -9,8 +9,31 @@ from ..data.dataset import Dataset from ..plotting.style import plot_with_custom_style -from . import circles, lines, points, polygons from .bases.shape import Shape +from .circles import Bullseye, Circle, Rings +from .lines import ( + Diamond, + HighLines, + HorizontalLines, + Rectangle, + SlantDownLines, + SlantUpLines, + Star, + VerticalLines, + WideLines, + XLines, +) +from .points import ( + Club, + DotsGrid, + DownParabola, + Heart, + LeftParabola, + RightParabola, + Scatter, + Spade, + UpParabola, +) class ShapeFactory: @@ -34,28 +57,28 @@ class ShapeFactory: """ _SHAPE_MAPPING: dict = { - 'bullseye': circles.Bullseye, - 'circle': circles.Circle, - 'high_lines': lines.HighLines, - 'h_lines': lines.HorizontalLines, - 'slant_down': lines.SlantDownLines, - 'slant_up': lines.SlantUpLines, - 'v_lines': lines.VerticalLines, - 'wide_lines': lines.WideLines, - 'x': lines.XLines, - 'dots': points.DotsGrid, - 'down_parab': points.DownParabola, - 'heart': points.Heart, - 'left_parab': points.LeftParabola, - 'scatter': points.Scatter, - 'right_parab': points.RightParabola, - 'up_parab': points.UpParabola, - 'diamond': polygons.Diamond, - 'rectangle': polygons.Rectangle, - 'rings': circles.Rings, - 'star': polygons.Star, - 'club': points.Club, - 'spade': points.Spade, + '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, } AVAILABLE_SHAPES: list[str] = sorted(_SHAPE_MAPPING.keys()) diff --git a/src/data_morph/shapes/lines.py b/src/data_morph/shapes/lines.py deleted file mode 100644 index 0c89581b..00000000 --- a/src/data_morph/shapes/lines.py +++ /dev/null @@ -1,268 +0,0 @@ -"""Shapes that are lines of lines.""" - -import numpy as np - -from ..data.dataset import Dataset -from .bases.line_collection import LineCollection - - -class HighLines(LineCollection): - """ - Class for the high lines shape. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - from data_morph.data.loader import DataLoader - from data_morph.shapes.lines import HighLines - - _ = HighLines(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - """ - - def __init__(self, dataset: Dataset) -> None: - x_bounds = dataset.data_bounds.x_bounds - y_bounds = dataset.data_bounds.y_bounds - - offset = y_bounds.range / 5 - lower = y_bounds[0] + offset - upper = y_bounds[1] - offset - - super().__init__( - [[x_bounds[0], lower], [x_bounds[1], lower]], - [[x_bounds[0], upper], [x_bounds[1], upper]], - ) - - def __str__(self) -> str: - return 'high_lines' - - -class HorizontalLines(LineCollection): - """ - Class for the horizontal lines shape. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - from data_morph.data.loader import DataLoader - from data_morph.shapes.lines import HorizontalLines - - _ = HorizontalLines(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - """ - - def __init__(self, dataset: Dataset) -> None: - x_bounds = dataset.data_bounds.x_bounds - y_bounds = dataset.data_bounds.y_bounds - - super().__init__( - *[ - [[x_bounds[0], y], [x_bounds[1], y]] - for y in np.linspace(y_bounds[0], y_bounds[1], 5) - ] - ) - - def __str__(self) -> str: - return 'h_lines' - - -class SlantDownLines(LineCollection): - """ - Class for the slant down lines shape. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - from data_morph.data.loader import DataLoader - from data_morph.shapes.lines import SlantDownLines - - _ = SlantDownLines(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - """ - - def __init__(self, dataset: Dataset) -> None: - x_bounds = dataset.morph_bounds.x_bounds - y_bounds = dataset.morph_bounds.y_bounds - - xmin, xmax = x_bounds - xmid = xmin + x_bounds.range / 2 - x_offset = (xmid - xmin) / 2 - - ymin, ymax = y_bounds - ymid = ymin + y_bounds.range / 2 - y_offset = (ymid - ymin) / 2 - - super().__init__( - [[xmin, ymid], [xmid, ymin]], - [[xmin, ymid + y_offset], [xmid + x_offset, ymin]], - [[xmin, ymax], [xmax, ymin]], - [[xmin + x_offset, ymax], [xmax, ymin + y_offset]], - [[xmid, ymax], [xmax, ymid]], - ) - - def __str__(self) -> str: - return 'slant_down' - - -class SlantUpLines(LineCollection): - """ - Class for the slant up lines shape. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - from data_morph.data.loader import DataLoader - from data_morph.shapes.lines import SlantUpLines - - _ = SlantUpLines(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - """ - - def __init__(self, dataset: Dataset) -> None: - x_bounds = dataset.morph_bounds.x_bounds - y_bounds = dataset.morph_bounds.y_bounds - - xmin, xmax = x_bounds - xmid = xmin + x_bounds.range / 2 - x_offset = (xmid - xmin) / 2 - - ymin, ymax = y_bounds - ymid = ymin + y_bounds.range / 2 - y_offset = (ymid - ymin) / 2 - - super().__init__( - [[xmin, ymid], [xmid, ymax]], - [[xmin, ymin + y_offset], [xmid + x_offset, ymax]], - [[xmin, ymin], [xmax, ymax]], - [[xmin + x_offset, ymin], [xmax, ymid + y_offset]], - [[xmid, ymin], [xmax, ymid]], - ) - - def __str__(self) -> str: - return 'slant_up' - - -class VerticalLines(LineCollection): - """ - Class for the vertical lines shape. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - from data_morph.data.loader import DataLoader - from data_morph.shapes.lines import VerticalLines - - _ = VerticalLines(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - """ - - def __init__(self, dataset: Dataset) -> None: - x_bounds = dataset.data_bounds.x_bounds - y_bounds = dataset.data_bounds.y_bounds - - super().__init__( - *[ - [[x, y_bounds[0]], [x, y_bounds[1]]] - for x in np.linspace(x_bounds[0], x_bounds[1], 5) - ] - ) - - def __str__(self) -> str: - return 'v_lines' - - -class WideLines(LineCollection): - """ - Class for the wide lines shape. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - from data_morph.data.loader import DataLoader - from data_morph.shapes.lines import WideLines - - _ = WideLines(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - """ - - def __init__(self, dataset: Dataset) -> None: - x_bounds = dataset.data_bounds.x_bounds - y_bounds = dataset.data_bounds.y_bounds - - offset = x_bounds.range / 5 - lower = x_bounds[0] + offset - upper = x_bounds[1] - offset - - super().__init__( - [[lower, y_bounds[0]], [lower, y_bounds[1]]], - [[upper, y_bounds[0]], [upper, y_bounds[1]]], - ) - - def __str__(self) -> str: - return 'wide_lines' - - -class XLines(LineCollection): - """ - Class for the X shape consisting of two crossing, perpendicular lines. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - from data_morph.data.loader import DataLoader - from data_morph.shapes.lines import XLines - - _ = XLines(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - """ - - 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/lines/__init__.py b/src/data_morph/shapes/lines/__init__.py new file mode 100644 index 00000000..d1229fe7 --- /dev/null +++ b/src/data_morph/shapes/lines/__init__.py @@ -0,0 +1,25 @@ +"""Shapes made up of lines.""" + +from .diamond import Diamond +from .high_lines import HighLines +from .horizontal_lines import HorizontalLines +from .rectangle import Rectangle +from .slant_down import SlantDownLines +from .slant_up import SlantUpLines +from .star import Star +from .vertical_lines import VerticalLines +from .wide_lines import WideLines +from .x_lines import XLines + +__all__ = [ + 'Diamond', + 'HighLines', + 'HorizontalLines', + 'Rectangle', + 'SlantDownLines', + 'SlantUpLines', + 'Star', + 'VerticalLines', + 'WideLines', + 'XLines', +] diff --git a/src/data_morph/shapes/lines/diamond.py b/src/data_morph/shapes/lines/diamond.py new file mode 100644 index 00000000..9d92c617 --- /dev/null +++ b/src/data_morph/shapes/lines/diamond.py @@ -0,0 +1,40 @@ +"""Diamond shape.""" + +from ...data.dataset import Dataset +from ..bases.line_collection import LineCollection + + +class Diamond(LineCollection): + """ + Class for the diamond shape. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + import matplotlib.pyplot as plt + from data_morph.data.loader import DataLoader + from data_morph.shapes.polygons import Diamond + + _ = Diamond(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + """ + + def __init__(self, dataset: Dataset) -> None: + xmin, xmax = dataset.df.x.quantile([0.05, 0.95]) + ymin, ymax = dataset.df.y.quantile([0.05, 0.95]) + + xmid = (xmax + xmin) / 2 + ymid = (ymax + ymin) / 2 + + super().__init__( + [[xmin, ymid], [xmid, ymax]], + [[xmid, ymax], [xmax, ymid]], + [[xmax, ymid], [xmid, ymin]], + [[xmid, ymin], [xmin, ymid]], + ) diff --git a/src/data_morph/shapes/lines/high_lines.py b/src/data_morph/shapes/lines/high_lines.py new file mode 100644 index 00000000..37e62e2f --- /dev/null +++ b/src/data_morph/shapes/lines/high_lines.py @@ -0,0 +1,41 @@ +"""High lines shape.""" + +from ...data.dataset import Dataset +from ..bases.line_collection import LineCollection + + +class HighLines(LineCollection): + """ + Class for the high lines shape. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + from data_morph.data.loader import DataLoader + from data_morph.shapes.lines import HighLines + + _ = HighLines(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + """ + + def __init__(self, dataset: Dataset) -> None: + x_bounds = dataset.data_bounds.x_bounds + y_bounds = dataset.data_bounds.y_bounds + + offset = y_bounds.range / 5 + lower = y_bounds[0] + offset + upper = y_bounds[1] - offset + + super().__init__( + [[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 new file mode 100644 index 00000000..bcc8213a --- /dev/null +++ b/src/data_morph/shapes/lines/horizontal_lines.py @@ -0,0 +1,41 @@ +"""Horizontal lines shape.""" + +import numpy as np + +from ...data.dataset import Dataset +from ..bases.line_collection import LineCollection + + +class HorizontalLines(LineCollection): + """ + Class for the horizontal lines shape. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + from data_morph.data.loader import DataLoader + from data_morph.shapes.lines import HorizontalLines + + _ = HorizontalLines(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + """ + + def __init__(self, dataset: Dataset) -> None: + x_bounds = dataset.data_bounds.x_bounds + y_bounds = dataset.data_bounds.y_bounds + + super().__init__( + *[ + [[x_bounds[0], y], [x_bounds[1], y]] + 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/rectangle.py b/src/data_morph/shapes/lines/rectangle.py new file mode 100644 index 00000000..7a5d90f5 --- /dev/null +++ b/src/data_morph/shapes/lines/rectangle.py @@ -0,0 +1,37 @@ +"""Rectangle shape.""" + +from ...data.dataset import Dataset +from ..bases.line_collection import LineCollection + + +class Rectangle(LineCollection): + """ + Class for the rectangle shape. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + import matplotlib.pyplot as plt + from data_morph.data.loader import DataLoader + from data_morph.shapes.polygons import Rectangle + + _ = Rectangle(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + """ + + def __init__(self, dataset: Dataset) -> None: + xmin, xmax = dataset.df.x.quantile([0.1, 0.9]) + ymin, ymax = dataset.df.y.quantile([0.1, 0.9]) + + super().__init__( + [[xmin, ymin], [xmin, ymax]], + [[xmin, ymin], [xmax, ymin]], + [[xmax, ymin], [xmax, ymax]], + [[xmin, ymax], [xmax, ymax]], + ) diff --git a/src/data_morph/shapes/lines/slant_down.py b/src/data_morph/shapes/lines/slant_down.py new file mode 100644 index 00000000..7bb2991a --- /dev/null +++ b/src/data_morph/shapes/lines/slant_down.py @@ -0,0 +1,48 @@ +"""Slant down lines shape.""" + +from ...data.dataset import Dataset +from ..bases.line_collection import LineCollection + + +class SlantDownLines(LineCollection): + """ + Class for the slant down lines shape. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + from data_morph.data.loader import DataLoader + from data_morph.shapes.lines import SlantDownLines + + _ = SlantDownLines(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + """ + + def __init__(self, dataset: Dataset) -> None: + x_bounds = dataset.morph_bounds.x_bounds + y_bounds = dataset.morph_bounds.y_bounds + + xmin, xmax = x_bounds + xmid = xmin + x_bounds.range / 2 + x_offset = (xmid - xmin) / 2 + + ymin, ymax = y_bounds + ymid = ymin + y_bounds.range / 2 + y_offset = (ymid - ymin) / 2 + + super().__init__( + [[xmin, ymid], [xmid, ymin]], + [[xmin, ymid + y_offset], [xmid + x_offset, ymin]], + [[xmin, ymax], [xmax, ymin]], + [[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 new file mode 100644 index 00000000..76465523 --- /dev/null +++ b/src/data_morph/shapes/lines/slant_up.py @@ -0,0 +1,48 @@ +"""Slant up lines shape.""" + +from ...data.dataset import Dataset +from ..bases.line_collection import LineCollection + + +class SlantUpLines(LineCollection): + """ + Class for the slant up lines shape. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + from data_morph.data.loader import DataLoader + from data_morph.shapes.lines import SlantUpLines + + _ = SlantUpLines(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + """ + + def __init__(self, dataset: Dataset) -> None: + x_bounds = dataset.morph_bounds.x_bounds + y_bounds = dataset.morph_bounds.y_bounds + + xmin, xmax = x_bounds + xmid = xmin + x_bounds.range / 2 + x_offset = (xmid - xmin) / 2 + + ymin, ymax = y_bounds + ymid = ymin + y_bounds.range / 2 + y_offset = (ymid - ymin) / 2 + + super().__init__( + [[xmin, ymid], [xmid, ymax]], + [[xmin, ymin + y_offset], [xmid + x_offset, ymax]], + [[xmin, ymin], [xmax, ymax]], + [[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/star.py b/src/data_morph/shapes/lines/star.py new file mode 100644 index 00000000..be7ce305 --- /dev/null +++ b/src/data_morph/shapes/lines/star.py @@ -0,0 +1,55 @@ +"""Star shape.""" + +from ...data.dataset import Dataset +from ..bases.line_collection import LineCollection + + +class Star(LineCollection): + """ + Class for the star shape. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + import matplotlib.pyplot as plt + from data_morph.data.loader import DataLoader + from data_morph.shapes.polygons import Star + + _ = Star(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + """ + + def __init__(self, dataset: Dataset) -> None: + bounds = dataset.data_bounds.clone() + bounds.align_aspect_ratio() + + x_bounds = bounds.x_bounds + y_bounds = bounds.y_bounds + + xmin, xmax = x_bounds + ymin, ymax = y_bounds + + x_range = x_bounds.range + y_range = y_bounds.range + + pts = [ + [xmin, ymin + y_range * 0.625], + [xmin + x_range * 0.375, ymin + y_range * 0.625], + [xmin + x_range * 0.5, ymax], + [xmin + x_range * 0.625, ymin + y_range * 0.625], + [xmax, ymin + y_range * 0.625], + [xmin + x_range * 0.6875, ymin + y_range * 0.375], + [xmin + x_range * 0.8125, ymin], + [xmin + x_range * 0.5, ymin + y_range * 0.25], + [xmin + x_range * 0.1875, ymin], + [xmin + x_range * 0.3125, ymin + y_range * 0.375], + [xmin, ymin + y_range * 0.625], + ] + + super().__init__(*list(zip(pts[:-1], pts[1:]))) diff --git a/src/data_morph/shapes/lines/vertical_lines.py b/src/data_morph/shapes/lines/vertical_lines.py new file mode 100644 index 00000000..23b239d0 --- /dev/null +++ b/src/data_morph/shapes/lines/vertical_lines.py @@ -0,0 +1,41 @@ +"""Vertical lines shape.""" + +import numpy as np + +from ...data.dataset import Dataset +from ..bases.line_collection import LineCollection + + +class VerticalLines(LineCollection): + """ + Class for the vertical lines shape. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + from data_morph.data.loader import DataLoader + from data_morph.shapes.lines import VerticalLines + + _ = VerticalLines(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + """ + + def __init__(self, dataset: Dataset) -> None: + x_bounds = dataset.data_bounds.x_bounds + y_bounds = dataset.data_bounds.y_bounds + + super().__init__( + *[ + [[x, y_bounds[0]], [x, y_bounds[1]]] + 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 new file mode 100644 index 00000000..36cbe2a7 --- /dev/null +++ b/src/data_morph/shapes/lines/wide_lines.py @@ -0,0 +1,41 @@ +"""Wide lines shape.""" + +from ...data.dataset import Dataset +from ..bases.line_collection import LineCollection + + +class WideLines(LineCollection): + """ + Class for the wide lines shape. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + from data_morph.data.loader import DataLoader + from data_morph.shapes.lines import WideLines + + _ = WideLines(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + """ + + def __init__(self, dataset: Dataset) -> None: + x_bounds = dataset.data_bounds.x_bounds + y_bounds = dataset.data_bounds.y_bounds + + offset = x_bounds.range / 5 + lower = x_bounds[0] + offset + upper = x_bounds[1] - offset + + super().__init__( + [[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 new file mode 100644 index 00000000..14b367cb --- /dev/null +++ b/src/data_morph/shapes/lines/x_lines.py @@ -0,0 +1,34 @@ +"""X lines shape.""" + +from ...data.dataset import Dataset +from ..bases.line_collection import LineCollection + + +class XLines(LineCollection): + """ + Class for the X shape consisting of two crossing, perpendicular lines. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + from data_morph.data.loader import DataLoader + from data_morph.shapes.lines import XLines + + _ = XLines(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + """ + + 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.py b/src/data_morph/shapes/points.py deleted file mode 100644 index d4013d9d..00000000 --- a/src/data_morph/shapes/points.py +++ /dev/null @@ -1,446 +0,0 @@ -"""Shapes that are composed of points.""" - -import itertools -from numbers import Number - -import numpy as np - -from ..data.dataset import Dataset -from .bases.point_collection import PointCollection - - -class DotsGrid(PointCollection): - """ - Class representing a 3x3 grid of dots. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - from data_morph.data.loader import DataLoader - from data_morph.shapes.points import DotsGrid - - _ = DotsGrid(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - """ - - 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() - - xmid = (xhigh + xlow) / 2 - ymid = (yhigh + ylow) / 2 - - super().__init__( - *list(itertools.product([xlow, xmid, xhigh], [ylow, ymid, yhigh])) - ) - - def __str__(self) -> str: - return 'dots' - - -class DownParabola(PointCollection): - """ - Class for the down parabola shape. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - from data_morph.data.loader import DataLoader - from data_morph.shapes.points import DownParabola - - _ = DownParabola(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - """ - - def __init__(self, dataset: Dataset) -> None: - x_bounds = dataset.data_bounds.x_bounds - xmin, xmax = x_bounds - xmid = xmax - x_bounds.range / 2 - - x_offset = x_bounds.range / 10 - xmin += x_offset - xmax -= x_offset - - ymin, ymax = dataset.data_bounds.y_bounds - - poly = np.polynomial.Polynomial.fit([xmin, xmid, xmax], [ymin, ymax, ymin], 2) - - super().__init__(*np.stack(poly.linspace(), axis=1)) - - def __str__(self) -> str: - return 'down_parab' - - -class Heart(PointCollection): - """ - Class for the heart shape. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - from data_morph.data.loader import DataLoader - from data_morph.shapes.points import Heart - - _ = Heart(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - - Notes - ----- - The formula for the heart shape is inspired by - `Heart Curve `_: - - Weisstein, Eric W. "Heart Curve." From `MathWorld `_ - --A Wolfram Web Resource. https://mathworld.wolfram.com/HeartCurve.html - """ - - def __init__(self, dataset: Dataset) -> None: - x_bounds = dataset.data_bounds.x_bounds - y_bounds = dataset.data_bounds.y_bounds - - x_shift = sum(x_bounds) / 2 - y_shift = sum(y_bounds) / 2 - - t = np.linspace(-3, 3, num=80) - - x = 16 * np.sin(t) ** 3 - y = 13 * np.cos(t) - 5 * np.cos(2 * t) - 2 * np.cos(3 * t) - np.cos(4 * t) - - # scale by the half the widest width of the heart - scale_factor = (x_bounds[1] - x_shift) / 16 - - super().__init__( - *np.stack([x * scale_factor + x_shift, y * scale_factor + y_shift], axis=1) - ) - - -class LeftParabola(PointCollection): - """ - Class for the left parabola shape. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - from data_morph.data.loader import DataLoader - from data_morph.shapes.points import LeftParabola - - _ = LeftParabola(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - """ - - def __init__(self, dataset: Dataset) -> None: - y_bounds = dataset.data_bounds.y_bounds - ymin, ymax = y_bounds - ymid = ymax - y_bounds.range / 2 - - y_offset = y_bounds.range / 10 - ymin += y_offset - ymax -= y_offset - - xmin, xmax = dataset.data_bounds.x_bounds - - poly = np.polynomial.Polynomial.fit([ymin, ymid, ymax], [xmin, xmax, xmin], 2) - - super().__init__(*np.stack(poly.linspace()[::-1], axis=1)) - - def __str__(self) -> str: - return 'left_parab' - - -class RightParabola(PointCollection): - """ - Class for the right parabola shape. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - from data_morph.data.loader import DataLoader - from data_morph.shapes.points import RightParabola - - _ = RightParabola(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - """ - - def __init__(self, dataset: Dataset) -> None: - y_bounds = dataset.data_bounds.y_bounds - ymin, ymax = y_bounds - ymid = ymax - y_bounds.range / 2 - - y_offset = y_bounds.range / 10 - ymin += y_offset - ymax -= y_offset - - xmin, xmax = dataset.data_bounds.x_bounds - - poly = np.polynomial.Polynomial.fit([ymin, ymid, ymax], [xmax, xmin, xmax], 2) - - super().__init__(*np.stack(poly.linspace()[::-1], axis=1)) - - def __str__(self) -> str: - return 'right_parab' - - -class UpParabola(PointCollection): - """ - Class for the up parabola shape. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - from data_morph.data.loader import DataLoader - from data_morph.shapes.points import UpParabola - - _ = UpParabola(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - """ - - def __init__(self, dataset: Dataset) -> None: - x_bounds = dataset.data_bounds.x_bounds - xmin, xmax = x_bounds - xmid = xmax - x_bounds.range / 2 - - x_offset = x_bounds.range / 10 - xmin += x_offset - xmax -= x_offset - - ymin, ymax = dataset.data_bounds.y_bounds - - 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' - - -class Scatter(PointCollection): - """ - Class for the scatter shape: a cloud of randomly-scattered points. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - from data_morph.data.loader import DataLoader - from data_morph.shapes.points import Scatter - - _ = Scatter(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - """ - - def __init__(self, dataset: Dataset) -> None: - rng = np.random.default_rng(1) - center = (dataset.df.x.mean(), dataset.df.y.mean()) - points = [center] - max_radius = max(dataset.df.x.std(), dataset.df.y.std()) - for radius in np.linspace(max_radius // 5, max_radius, num=5): - for angle in np.linspace(0, 360, num=50, endpoint=False): - points.append( - ( - center[0] - + np.cos(angle) * radius - + rng.standard_normal() * max_radius, - center[1] - + np.sin(angle) * radius - + rng.standard_normal() * max_radius, - ) - ) - super().__init__(*points) - - self._alpha = 0.4 - - def distance(self, x: Number, y: Number) -> int: - """ - No-op that allows returns 0 so that all perturbations are accepted. - - Parameters - ---------- - x, y : int or float - Coordinates of a point in 2D space. - - Returns - ------- - int - Always returns 0 to allow for scattering of the points. - """ - return 0 - - -class Club(PointCollection): - """ - Class for the club shape. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - from data_morph.data.loader import DataLoader - from data_morph.shapes.points import Club - - _ = Club(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - """ - - def __init__(self, dataset: Dataset) -> None: - x_bounds = dataset.data_bounds.x_bounds - y_bounds = dataset.data_bounds.y_bounds - - x_shift = sum(x_bounds) / 2 - y_shift = sum(y_bounds) / 2 - scale_factor = min(x_bounds.range, y_bounds.range) / 75 - - # params for lobes - radius = 15 * scale_factor - top_lobe_y_offset = 18 * scale_factor - bottom_lobes_x_offset = 15 * scale_factor - bottom_lobes_y_offset = 9 * scale_factor - - t = np.linspace(0, (2 - 1 / 3) * np.pi, num=30) - - # top lobe - angle_offset = -1 / 3 * np.pi - x_top = radius * np.cos(t + angle_offset) - y_top = radius * np.sin(t + angle_offset) + top_lobe_y_offset - - # bottom left lobe - angle_offset = 1 / 3 * np.pi - x_bottom_left = radius * np.cos(t + angle_offset) - bottom_lobes_x_offset - y_bottom_left = radius * np.sin(t + angle_offset) - bottom_lobes_y_offset - - # bottom right lobe - angle_offset = np.pi - x_bottom_right = radius * np.cos(t + angle_offset) + bottom_lobes_x_offset - y_bottom_right = radius * np.sin(t + angle_offset) - bottom_lobes_y_offset - - x_lobes = [x_top, x_bottom_left, x_bottom_right] - y_lobes = [y_top, y_bottom_left, y_bottom_right] - - # params for the stem - stem_x_offset = 8 * scale_factor - stem_y_offset = 34 * scale_factor - stem_scaler = 0.35 / scale_factor - stem_x_pad = 1.5 * scale_factor - - # stem bottom - x_line = np.linspace(-stem_x_offset, stem_x_offset, num=8) - y_line = np.repeat(-stem_y_offset, 8) - - # left part of the stem - x_left = np.linspace(-(stem_x_offset - stem_x_pad), -stem_x_pad, num=6) - y_left = stem_scaler * np.power(x_left + stem_x_offset, 2) - stem_y_offset - - # right part of the stem - x_right = np.linspace(stem_x_pad, stem_x_offset - stem_x_pad, num=6) - y_right = stem_scaler * np.power(x_right - stem_x_offset, 2) - stem_y_offset - - x_stem = [x_line, x_left, x_right] - y_stem = [y_line, y_left, y_right] - - xs = x_shift + np.concatenate(x_lobes + x_stem) - ys = y_shift + np.concatenate(y_lobes + y_stem) - - super().__init__(*np.stack([xs, ys], axis=1)) - - -class Spade(PointCollection): - """ - Class for the spade shape. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - from data_morph.data.loader import DataLoader - from data_morph.shapes.points import Spade - - _ = Spade(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - """ - - def __init__(self, dataset: Dataset) -> None: - x_bounds = dataset.data_bounds.x_bounds - y_bounds = dataset.data_bounds.y_bounds - - x_shift = sum(x_bounds) / 2 - y_shift = sum(y_bounds) / 2 - - # graph upside-down heart - heart_points = Heart(dataset).points - heart_points[:, 1] = -heart_points[:, 1] + 2 * y_shift - - # line base - line_x = np.linspace(-6, 6, num=12) - line_y = np.repeat(-16, 12) - - # left wing - left_x = np.linspace(-6, 0, num=12) - left_y = 0.278 * np.power(left_x + 6, 2) - 16 - - # right wing - right_x = np.linspace(0, 6, num=12) - right_y = 0.278 * np.power(right_x - 6, 2) - 16 - - # shift and scale the base and wing - base_x = np.concatenate((line_x, left_x, right_x), axis=0) - base_y = np.concatenate((line_y, left_y, right_y), axis=0) - - # scale by the half the widest width of the spade - scale_factor = (x_bounds[1] - x_shift) / 16 - base_x = base_x * scale_factor + x_shift - base_y = base_y * scale_factor + y_shift - - # combine the base and the upside-down heart - x = np.concatenate((heart_points[:, 0], base_x), axis=0) - y = np.concatenate((heart_points[:, 1], base_y), axis=0) - - super().__init__(*np.stack([x, y], axis=1)) diff --git a/src/data_morph/shapes/points/__init__.py b/src/data_morph/shapes/points/__init__.py new file mode 100644 index 00000000..8609f4d4 --- /dev/null +++ b/src/data_morph/shapes/points/__init__.py @@ -0,0 +1,20 @@ +"""Shapes made up of points.""" + +from .club import Club +from .dots_grid import DotsGrid +from .heart import Heart +from .parabola import DownParabola, LeftParabola, RightParabola, UpParabola +from .scatter import Scatter +from .spade import Spade + +__all__ = [ + 'Club', + 'DotsGrid', + 'DownParabola', + 'Heart', + 'LeftParabola', + 'RightParabola', + 'Scatter', + 'Spade', + 'UpParabola', +] diff --git a/src/data_morph/shapes/points/club.py b/src/data_morph/shapes/points/club.py new file mode 100644 index 00000000..1ce579fb --- /dev/null +++ b/src/data_morph/shapes/points/club.py @@ -0,0 +1,154 @@ +"""Club shape.""" + +from numbers import Number + +import numpy as np + +from ...data.dataset import Dataset +from ..bases.point_collection import PointCollection + + +class Club(PointCollection): + """ + Class for the club shape. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + from data_morph.data.loader import DataLoader + from data_morph.shapes.points import Club + + _ = Club(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + """ + + def __init__(self, dataset: Dataset) -> None: + x_bounds = dataset.data_bounds.x_bounds + y_bounds = dataset.data_bounds.y_bounds + + x_shift = sum(x_bounds) / 2 + y_shift = sum(y_bounds) / 2 + scale_factor = min(x_bounds.range, y_bounds.range) / 75 + + x_lobes, y_lobes = self._get_lobes(scale_factor) + x_stem, y_stem = self._get_stem(scale_factor) + + xs = x_shift + np.concatenate(x_lobes + x_stem) + ys = y_shift + np.concatenate(y_lobes + y_stem) + + super().__init__(*np.stack([xs, ys], axis=1)) + + @staticmethod + def _get_arc( + r: Number, + t: np.ndarray, + angle_offset: np.float64, + x_offset: Number, + y_offset: Number, + ) -> tuple[np.ndarray, np.ndarray]: + """ + Get arc of a circle. + + Parameters + ---------- + r : Number + The radius of the circle. + t : numpy.ndarray + The values to sample at in radians. + angle_offset : numpy.float64 + Angle at which to start the arc in radians. + x_offset : Number + A constant value to shift the *x* coordinates by. + y_offset : Number + A constant value to shift the *y* coordinates by. + + Returns + ------- + tuple[numpy.ndarray, numpy.ndarray] + The *x* and *y* coordinates for the arc. + """ + x = r * np.cos(t + angle_offset) + x_offset + y = r * np.sin(t + angle_offset) + y_offset + return x, y + + @classmethod + def _get_lobes( + cls, scale_factor: Number + ) -> tuple[list[np.ndarray], list[np.ndarray]]: + """ + Get the lobes of the club. + + Parameters + ---------- + scale_factor : Number + The factor to scale up/down the radius of the arcs used to calculate the lobes. + + Returns + ------- + tuple[list[numpy.ndarray], list[numpy.ndarray]] + The *x* and *y* coordinates for the lobes. + """ + radius = 15 * scale_factor + top_lobe_y_offset = 18 * scale_factor + bottom_lobes_x_offset = 15 * scale_factor + bottom_lobes_y_offset = 9 * scale_factor + + t = np.linspace(0, (2 - 1 / 3) * np.pi, num=30) + + x_top, y_top = cls._get_arc(radius, t, -np.pi / 3, 0, top_lobe_y_offset) + x_bottom_left, y_bottom_left = cls._get_arc( + radius, t, np.pi / 3, -bottom_lobes_x_offset, -bottom_lobes_y_offset + ) + x_bottom_right, y_bottom_right = cls._get_arc( + radius, t, np.pi, bottom_lobes_x_offset, -bottom_lobes_y_offset + ) + + x_lobes = [x_top, x_bottom_left, x_bottom_right] + y_lobes = [y_top, y_bottom_left, y_bottom_right] + + return x_lobes, y_lobes + + @classmethod + def _get_stem( + cls, scale_factor: Number + ) -> tuple[list[np.ndarray], list[np.ndarray]]: + """ + Get the stem of the club. + + Parameters + ---------- + scale_factor : Number + The factor to scale up/down the stem. + + Returns + ------- + tuple[list[numpy.ndarray], list[numpy.ndarray]] + The *x* and *y* coordinates for the stem. + """ + stem_x_offset = 8 * scale_factor + stem_y_offset = 34 * scale_factor + stem_scaler = 0.35 / scale_factor + stem_x_pad = 1.5 * scale_factor + + # stem bottom + x_line = np.linspace(-stem_x_offset, stem_x_offset, num=8) + y_line = np.repeat(-stem_y_offset, 8) + + # left part of the stem + x_left = np.linspace(-(stem_x_offset - stem_x_pad), -stem_x_pad, num=6) + y_left = stem_scaler * np.power(x_left + stem_x_offset, 2) - stem_y_offset + + # right part of the stem + x_right = np.linspace(stem_x_pad, stem_x_offset - stem_x_pad, num=6) + y_right = stem_scaler * np.power(x_right - stem_x_offset, 2) - stem_y_offset + + x_stem = [x_line, x_left, x_right] + y_stem = [y_line, y_left, y_right] + + return x_stem, y_stem diff --git a/src/data_morph/shapes/points/dots_grid.py b/src/data_morph/shapes/points/dots_grid.py new file mode 100644 index 00000000..467a67ce --- /dev/null +++ b/src/data_morph/shapes/points/dots_grid.py @@ -0,0 +1,41 @@ +"""Dots grid shape.""" + +import itertools + +from ...data.dataset import Dataset +from ..bases.point_collection import PointCollection + + +class DotsGrid(PointCollection): + """ + Class representing a 3x3 grid of dots. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + from data_morph.data.loader import DataLoader + from data_morph.shapes.points import DotsGrid + + _ = DotsGrid(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + """ + + 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() + + xmid = (xhigh + xlow) / 2 + ymid = (yhigh + ylow) / 2 + + 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/heart.py b/src/data_morph/shapes/points/heart.py new file mode 100644 index 00000000..c8db9382 --- /dev/null +++ b/src/data_morph/shapes/points/heart.py @@ -0,0 +1,54 @@ +"""Heart shape.""" + +import numpy as np + +from ...data.dataset import Dataset +from ..bases.point_collection import PointCollection + + +class Heart(PointCollection): + """ + Class for the heart shape. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + from data_morph.data.loader import DataLoader + from data_morph.shapes.points import Heart + + _ = Heart(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + + Notes + ----- + The formula for the heart shape is inspired by + `Heart Curve `_: + + Weisstein, Eric W. "Heart Curve." From `MathWorld `_ + --A Wolfram Web Resource. https://mathworld.wolfram.com/HeartCurve.html + """ + + def __init__(self, dataset: Dataset) -> None: + x_bounds = dataset.data_bounds.x_bounds + y_bounds = dataset.data_bounds.y_bounds + + x_shift = sum(x_bounds) / 2 + y_shift = sum(y_bounds) / 2 + + t = np.linspace(-3, 3, num=80) + + x = 16 * np.sin(t) ** 3 + y = 13 * np.cos(t) - 5 * np.cos(2 * t) - 2 * np.cos(3 * t) - np.cos(4 * t) + + # scale by the half the widest width of the heart + scale_factor = (x_bounds[1] - x_shift) / 16 + + super().__init__( + *np.stack([x * scale_factor + x_shift, y * scale_factor + y_shift], axis=1) + ) diff --git a/src/data_morph/shapes/points/parabola.py b/src/data_morph/shapes/points/parabola.py new file mode 100644 index 00000000..dea94a99 --- /dev/null +++ b/src/data_morph/shapes/points/parabola.py @@ -0,0 +1,162 @@ +"""Parabola shapes.""" + +import numpy as np + +from ...data.dataset import Dataset +from ..bases.point_collection import PointCollection + + +class DownParabola(PointCollection): + """ + Class for the down parabola shape. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + from data_morph.data.loader import DataLoader + from data_morph.shapes.points import DownParabola + + _ = DownParabola(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + """ + + def __init__(self, dataset: Dataset) -> None: + x_bounds = dataset.data_bounds.x_bounds + xmin, xmax = x_bounds + xmid = xmax - x_bounds.range / 2 + + x_offset = x_bounds.range / 10 + xmin += x_offset + xmax -= x_offset + + ymin, ymax = dataset.data_bounds.y_bounds + + poly = np.polynomial.Polynomial.fit([xmin, xmid, xmax], [ymin, ymax, ymin], 2) + + super().__init__(*np.stack(poly.linspace(), axis=1)) + + def __str__(self) -> str: + return 'down_parab' + + +class LeftParabola(PointCollection): + """ + Class for the left parabola shape. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + from data_morph.data.loader import DataLoader + from data_morph.shapes.points import LeftParabola + + _ = LeftParabola(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + """ + + def __init__(self, dataset: Dataset) -> None: + y_bounds = dataset.data_bounds.y_bounds + ymin, ymax = y_bounds + ymid = ymax - y_bounds.range / 2 + + y_offset = y_bounds.range / 10 + ymin += y_offset + ymax -= y_offset + + xmin, xmax = dataset.data_bounds.x_bounds + + poly = np.polynomial.Polynomial.fit([ymin, ymid, ymax], [xmin, xmax, xmin], 2) + + super().__init__(*np.stack(poly.linspace()[::-1], axis=1)) + + def __str__(self) -> str: + return 'left_parab' + + +class RightParabola(PointCollection): + """ + Class for the right parabola shape. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + from data_morph.data.loader import DataLoader + from data_morph.shapes.points import RightParabola + + _ = RightParabola(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + """ + + def __init__(self, dataset: Dataset) -> None: + y_bounds = dataset.data_bounds.y_bounds + ymin, ymax = y_bounds + ymid = ymax - y_bounds.range / 2 + + y_offset = y_bounds.range / 10 + ymin += y_offset + ymax -= y_offset + + xmin, xmax = dataset.data_bounds.x_bounds + + poly = np.polynomial.Polynomial.fit([ymin, ymid, ymax], [xmax, xmin, xmax], 2) + + super().__init__(*np.stack(poly.linspace()[::-1], axis=1)) + + def __str__(self) -> str: + return 'right_parab' + + +class UpParabola(PointCollection): + """ + Class for the up parabola shape. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + from data_morph.data.loader import DataLoader + from data_morph.shapes.points import UpParabola + + _ = UpParabola(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + """ + + def __init__(self, dataset: Dataset) -> None: + x_bounds = dataset.data_bounds.x_bounds + xmin, xmax = x_bounds + xmid = xmax - x_bounds.range / 2 + + x_offset = x_bounds.range / 10 + xmin += x_offset + xmax -= x_offset + + ymin, ymax = dataset.data_bounds.y_bounds + + 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' diff --git a/src/data_morph/shapes/points/scatter.py b/src/data_morph/shapes/points/scatter.py new file mode 100644 index 00000000..a8f6ab11 --- /dev/null +++ b/src/data_morph/shapes/points/scatter.py @@ -0,0 +1,66 @@ +"""Scatter shape.""" + +from numbers import Number + +import numpy as np + +from ...data.dataset import Dataset +from ..bases.point_collection import PointCollection + + +class Scatter(PointCollection): + """ + Class for the scatter shape: a cloud of randomly-scattered points. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + from data_morph.data.loader import DataLoader + from data_morph.shapes.points import Scatter + + _ = Scatter(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + """ + + def __init__(self, dataset: Dataset) -> None: + rng = np.random.default_rng(1) + center = (dataset.df.x.mean(), dataset.df.y.mean()) + points = [center] + max_radius = max(dataset.df.x.std(), dataset.df.y.std()) + for radius in np.linspace(max_radius // 5, max_radius, num=5): + for angle in np.linspace(0, 360, num=50, endpoint=False): + points.append( + ( + center[0] + + np.cos(angle) * radius + + rng.standard_normal() * max_radius, + center[1] + + np.sin(angle) * radius + + rng.standard_normal() * max_radius, + ) + ) + super().__init__(*points) + + self._alpha = 0.4 + + def distance(self, x: Number, y: Number) -> int: + """ + No-op that allows returns 0 so that all perturbations are accepted. + + Parameters + ---------- + x, y : int or float + Coordinates of a point in 2D space. + + Returns + ------- + int + Always returns 0 to allow for scattering of the points. + """ + return 0 diff --git a/src/data_morph/shapes/points/spade.py b/src/data_morph/shapes/points/spade.py new file mode 100644 index 00000000..5acdefff --- /dev/null +++ b/src/data_morph/shapes/points/spade.py @@ -0,0 +1,119 @@ +"""Spade shape.""" + +from numbers import Number + +import numpy as np + +from ...data.dataset import Dataset +from ..bases.point_collection import PointCollection +from .heart import Heart + + +class Spade(PointCollection): + """ + Class for the spade shape. + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the panda dataset. + + from data_morph.data.loader import DataLoader + from data_morph.shapes.points import Spade + + _ = Spade(DataLoader.load_dataset('panda')).plot() + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + """ + + def __init__(self, dataset: Dataset) -> None: + x_bounds = dataset.data_bounds.x_bounds + y_bounds = dataset.data_bounds.y_bounds + + x_shift = sum(x_bounds) / 2 + y_shift = sum(y_bounds) / 2 + + # upside-down heart + heart_points = self._get_inverted_heart(dataset, y_shift) + + # base of the spade + base_x, base_y = self._get_base(x_bounds[1], x_shift, y_shift) + + # combine all points + x = np.concatenate((heart_points[:, 0], base_x), axis=0) + y = np.concatenate((heart_points[:, 1], base_y), axis=0) + + super().__init__(*np.stack([x, y], axis=1)) + + @staticmethod + def _get_inverted_heart(dataset: Dataset, y_shift: Number) -> np.ndarray: + """ + Get points for an inverted heart. + + Parameters + ---------- + dataset : Dataset + The starting dataset to morph into other shapes. + y_shift : Number + The constant value to shift the *y* up/down by. + + Returns + ------- + numpy.ndarray + The points for the upside-down heart. + + See Also + -------- + Heart : This shape is reused to calculate the spade. + """ + heart_points = Heart(dataset).points + heart_points[:, 1] = -heart_points[:, 1] + 2 * y_shift + return heart_points + + @staticmethod + def _get_base( + xmax: Number, x_shift: Number, y_shift: Number + ) -> tuple[np.ndarray, np.ndarray]: + """ + Get the base of the spade. + + Parameters + ---------- + xmax : Number + The maximum *x* value for the shape. + x_shift : Number + The constant value to shift the *x* left/right by. + y_shift : Number + The constant value to shift the *y* up/down by. + + Returns + ------- + tuple[numpy.ndarray, numpy.ndarray] + The *x* and *y* coordinates for the base of the spade. + """ + # line base + line_x = np.linspace(-6, 6, num=12) + line_y = np.repeat(-16, 12) + + # left wing + left_x = np.linspace(-6, 0, num=12) + left_y = 0.278 * np.power(left_x + 6, 2) - 16 + + # right wing + right_x = np.linspace(0, 6, num=12) + right_y = 0.278 * np.power(right_x - 6, 2) - 16 + + # shift and scale the base and wing + base_x = np.concatenate((line_x, left_x, right_x), axis=0) + base_y = np.concatenate((line_y, left_y, right_y), axis=0) + + # scale by the half the widest width of the spade + scale_factor = (xmax - x_shift) / 16 + + base_x = base_x * scale_factor + x_shift + base_y = base_y * scale_factor + y_shift + + return base_x, base_y diff --git a/src/data_morph/shapes/polygons.py b/src/data_morph/shapes/polygons.py deleted file mode 100644 index 44141bbb..00000000 --- a/src/data_morph/shapes/polygons.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Polygon shapes made from lines.""" - -from ..data.dataset import Dataset -from .bases.line_collection import LineCollection - - -class Diamond(LineCollection): - """ - Class for the diamond shape. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - import matplotlib.pyplot as plt - from data_morph.data.loader import DataLoader - from data_morph.shapes.polygons import Diamond - - _ = Diamond(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - """ - - def __init__(self, dataset: Dataset) -> None: - xmin, xmax = dataset.df.x.quantile([0.05, 0.95]) - ymin, ymax = dataset.df.y.quantile([0.05, 0.95]) - - xmid = (xmax + xmin) / 2 - ymid = (ymax + ymin) / 2 - - super().__init__( - [[xmin, ymid], [xmid, ymax]], - [[xmid, ymax], [xmax, ymid]], - [[xmax, ymid], [xmid, ymin]], - [[xmid, ymin], [xmin, ymid]], - ) - - -class Rectangle(LineCollection): - """ - Class for the rectangle shape. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - import matplotlib.pyplot as plt - from data_morph.data.loader import DataLoader - from data_morph.shapes.polygons import Rectangle - - _ = Rectangle(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - """ - - def __init__(self, dataset: Dataset) -> None: - xmin, xmax = dataset.df.x.quantile([0.1, 0.9]) - ymin, ymax = dataset.df.y.quantile([0.1, 0.9]) - - super().__init__( - [[xmin, ymin], [xmin, ymax]], - [[xmin, ymin], [xmax, ymin]], - [[xmax, ymin], [xmax, ymax]], - [[xmin, ymax], [xmax, ymax]], - ) - - -class Star(LineCollection): - """ - Class for the star shape. - - .. plot:: - :scale: 75 - :caption: - This shape is generated using the panda dataset. - - import matplotlib.pyplot as plt - from data_morph.data.loader import DataLoader - from data_morph.shapes.polygons import Star - - _ = Star(DataLoader.load_dataset('panda')).plot() - - Parameters - ---------- - dataset : Dataset - The starting dataset to morph into other shapes. - """ - - def __init__(self, dataset: Dataset) -> None: - bounds = dataset.data_bounds.clone() - bounds.align_aspect_ratio() - - x_bounds = bounds.x_bounds - y_bounds = bounds.y_bounds - - xmin, xmax = x_bounds - ymin, ymax = y_bounds - - x_range = x_bounds.range - y_range = y_bounds.range - - pts = [ - [xmin, ymin + y_range * 0.625], - [xmin + x_range * 0.375, ymin + y_range * 0.625], - [xmin + x_range * 0.5, ymax], - [xmin + x_range * 0.625, ymin + y_range * 0.625], - [xmax, ymin + y_range * 0.625], - [xmin + x_range * 0.6875, ymin + y_range * 0.375], - [xmin + x_range * 0.8125, ymin], - [xmin + x_range * 0.5, ymin + y_range * 0.25], - [xmin + x_range * 0.1875, ymin], - [xmin + x_range * 0.3125, ymin + y_range * 0.375], - [xmin, ymin + y_range * 0.625], - ] - - super().__init__(*list(zip(pts[:-1], pts[1:]))) From 0a7421a4bfc4f246c024abe45b3c067f99426dd1 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:35:58 +1100 Subject: [PATCH 2/3] Add ruff rules (#241) --- docs/conf.py | 2 +- pyproject.toml | 3 +- src/data_morph/bounds/bounding_box.py | 2 +- src/data_morph/data/dataset.py | 8 ++- src/data_morph/data/loader.py | 7 +- .../shapes/bases/line_collection.py | 4 +- .../shapes/bases/point_collection.py | 4 +- src/data_morph/shapes/bases/shape.py | 2 +- src/data_morph/shapes/circles.py | 8 ++- src/data_morph/shapes/factory.py | 3 +- tests/data/test_dataset.py | 2 +- tests/shapes/test_circles.py | 9 ++- tests/shapes/test_lines.py | 31 ++++----- tests/shapes/test_points.py | 69 +++++++++---------- tests/shapes/test_polygons.py | 9 ++- tests/test_morpher.py | 2 +- 16 files changed, 86 insertions(+), 79 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 555bb250..e3c8cc26 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,7 +13,7 @@ import data_morph sys.path.insert(0, str(Path().absolute())) -from post_build import determine_versions # noqa: E402 +from post_build import determine_versions project = 'Data Morph' current_year = dt.date.today().year diff --git a/pyproject.toml b/pyproject.toml index aaf5587a..7d36060a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,14 +102,13 @@ lint.select = [ "I", # isort "N", # pep8-naming "PTH", # flake8-use-pathlib + "RUF", # ruff-specific rules "SIM", # flake8-simplify "TRY", # tryceratops "UP", # pyupgrade "W", # pycodestyle warning ] lint.ignore = [ - "ANN101", # missing type annotation for self (will be removed in future ruff version) - "ANN102", # missing type annotation for cls in classmethod (will be removed in future ruff version) "E501", # line-too-long "TRY003", # avoid specifying long messages outside the exception class (revisit later and consider making custom exceptions) ] diff --git a/src/data_morph/bounds/bounding_box.py b/src/data_morph/bounds/bounding_box.py index b05f2c82..bbb30d18 100644 --- a/src/data_morph/bounds/bounding_box.py +++ b/src/data_morph/bounds/bounding_box.py @@ -97,7 +97,7 @@ def __eq__(self, other: BoundingBox) -> bool: def __repr__(self) -> str: return '\n' f' x={self.x_bounds}' '\n' f' y={self.y_bounds}' - def adjust_bounds(self, x: Number = None, y: Number = None) -> None: + def adjust_bounds(self, x: Number | None = None, y: Number | None = None) -> None: """ Adjust bounding box range. diff --git a/src/data_morph/data/dataset.py b/src/data_morph/data/dataset.py index 4a0ac297..aa26c834 100644 --- a/src/data_morph/data/dataset.py +++ b/src/data_morph/data/dataset.py @@ -1,5 +1,7 @@ """Class representing a dataset for morphing.""" +from __future__ import annotations + from numbers import Number import matplotlib.pyplot as plt @@ -39,13 +41,13 @@ class Dataset: Utility for creating :class:`Dataset` objects from CSV files. """ - _REQUIRED_COLUMNS = ['x', 'y'] + _REQUIRED_COLUMNS = ('x', 'y') def __init__( self, name: str, df: pd.DataFrame, - scale: Number = None, + scale: Number | None = None, ) -> None: self.df: pd.DataFrame = self._validate_data(df).pipe(self._scale_data, scale) """pandas.DataFrame: DataFrame containing columns x and y.""" @@ -181,7 +183,7 @@ def _validate_data(self, data: pd.DataFrame) -> pd.DataFrame: @plot_with_custom_style def plot( - self, ax: Axes = None, show_bounds: bool = True, title: str = 'default' + self, ax: Axes | None = None, show_bounds: bool = True, title: str = 'default' ) -> Axes: """ Plot the dataset and its bounds. diff --git a/src/data_morph/data/loader.py b/src/data_morph/data/loader.py index 064c099e..cb625824 100644 --- a/src/data_morph/data/loader.py +++ b/src/data_morph/data/loader.py @@ -1,9 +1,12 @@ """Load data for morphing.""" +from __future__ import annotations + from importlib.resources import files from itertools import zip_longest from numbers import Number from pathlib import Path +from typing import ClassVar import matplotlib.pyplot as plt import numpy as np @@ -41,7 +44,7 @@ class DataLoader: """ _DATA_PATH: str = 'data/starter_shapes/' - _DATASETS: dict = { + _DATASETS: ClassVar[dict[str, str]] = { 'bunny': 'bunny.csv', 'cat': 'cat.csv', 'dino': 'dino.csv', @@ -66,7 +69,7 @@ def __init__(self) -> None: def load_dataset( cls, dataset: str, - scale: Number = None, + scale: Number | None = None, ) -> Dataset: """ Load dataset. diff --git a/src/data_morph/shapes/bases/line_collection.py b/src/data_morph/shapes/bases/line_collection.py index 04f88c93..671f74ce 100644 --- a/src/data_morph/shapes/bases/line_collection.py +++ b/src/data_morph/shapes/bases/line_collection.py @@ -1,5 +1,7 @@ """Base class for shapes that are composed of lines.""" +from __future__ import annotations + from collections.abc import Iterable from numbers import Number @@ -97,7 +99,7 @@ def distance(self, x: Number, y: Number) -> float: ) @plot_with_custom_style - def plot(self, ax: Axes = None) -> Axes: + def plot(self, ax: Axes | None = None) -> Axes: """ Plot the shape. diff --git a/src/data_morph/shapes/bases/point_collection.py b/src/data_morph/shapes/bases/point_collection.py index 9103747e..ede45963 100644 --- a/src/data_morph/shapes/bases/point_collection.py +++ b/src/data_morph/shapes/bases/point_collection.py @@ -1,5 +1,7 @@ """Base class for shapes that are composed of points.""" +from __future__ import annotations + from collections.abc import Iterable from numbers import Number @@ -52,7 +54,7 @@ def distance(self, x: Number, y: Number) -> float: ) @plot_with_custom_style - def plot(self, ax: Axes = None) -> Axes: + def plot(self, ax: Axes | None = None) -> Axes: """ Plot the shape. diff --git a/src/data_morph/shapes/bases/shape.py b/src/data_morph/shapes/bases/shape.py index b629ce4b..36690f60 100644 --- a/src/data_morph/shapes/bases/shape.py +++ b/src/data_morph/shapes/bases/shape.py @@ -103,7 +103,7 @@ def _recursive_repr(self, attr: str | None = None) -> str: ) @abstractmethod - def plot(self, ax: Axes = None) -> Axes: + def plot(self, ax: Axes | None = None) -> Axes: """ Plot the shape. diff --git a/src/data_morph/shapes/circles.py b/src/data_morph/shapes/circles.py index 12bdb46b..ce665373 100644 --- a/src/data_morph/shapes/circles.py +++ b/src/data_morph/shapes/circles.py @@ -1,5 +1,7 @@ """Shapes that are circular in nature.""" +from __future__ import annotations + from numbers import Number import matplotlib.pyplot as plt @@ -33,7 +35,7 @@ class Circle(Shape): The radius of the circle. """ - def __init__(self, dataset: Dataset, radius: Number = None) -> None: + def __init__(self, dataset: Dataset, radius: Number | None = None) -> None: self.center: np.ndarray = dataset.df[['x', 'y']].mean().to_numpy() """numpy.ndarray: The (x, y) coordinates of the circle's center.""" @@ -63,7 +65,7 @@ def distance(self, x: Number, y: Number) -> float: ) @plot_with_custom_style - def plot(self, ax: Axes = None) -> Axes: + def plot(self, ax: Axes | None = None) -> Axes: """ Plot the shape. @@ -159,7 +161,7 @@ def distance(self, x: Number, y: Number) -> float: ) @plot_with_custom_style - def plot(self, ax: Axes = None) -> Axes: + def plot(self, ax: Axes | None = None) -> Axes: """ Plot the shape. diff --git a/src/data_morph/shapes/factory.py b/src/data_morph/shapes/factory.py index ca2f1d23..a0c8be23 100644 --- a/src/data_morph/shapes/factory.py +++ b/src/data_morph/shapes/factory.py @@ -2,6 +2,7 @@ from itertools import zip_longest from numbers import Number +from typing import ClassVar import matplotlib.pyplot as plt import numpy as np @@ -56,7 +57,7 @@ class ShapeFactory: The starting dataset to morph into other shapes. """ - _SHAPE_MAPPING: dict = { + _SHAPE_MAPPING: ClassVar[dict[str, type[Shape]]] = { 'bullseye': Bullseye, 'circle': Circle, 'high_lines': HighLines, diff --git a/tests/data/test_dataset.py b/tests/data/test_dataset.py index 5e2374a7..38d6e26c 100644 --- a/tests/data/test_dataset.py +++ b/tests/data/test_dataset.py @@ -62,7 +62,7 @@ def test_validate_data_fix_column_casing(self, starter_shapes_dir): df = pd.read_csv(starter_shapes_dir / 'dino.csv').rename(columns={'x': 'X'}) dataset = Dataset('dino', df) - assert not dataset.df[dataset._REQUIRED_COLUMNS].empty + assert not dataset.df[list(dataset._REQUIRED_COLUMNS)].empty @pytest.mark.bounds @pytest.mark.parametrize( diff --git a/tests/shapes/test_circles.py b/tests/shapes/test_circles.py index d45bdc32..9deb814c 100644 --- a/tests/shapes/test_circles.py +++ b/tests/shapes/test_circles.py @@ -1,7 +1,6 @@ """Test circles module.""" import re -from collections.abc import Iterable from numbers import Number import numpy as np @@ -16,7 +15,7 @@ class CirclesModuleTestBase: """Base for testing circle shapes.""" shape_name: str - distance_test_cases: Iterable[tuple[Iterable[Number], float]] + distance_test_cases: tuple[tuple[tuple[Number], float]] repr_regex: str @pytest.fixture(scope='class') @@ -40,7 +39,7 @@ class TestBullseye(CirclesModuleTestBase): """Test the Bullseye class.""" shape_name = 'bullseye' - distance_test_cases = [[(20, 50), 3.660254], [(10, 25), 9.08004]] + distance_test_cases = (((20, 50), 3.660254), ((10, 25), 9.08004)) repr_regex = ( r'^\n' r' circles=\n' @@ -61,7 +60,7 @@ class TestCircle(CirclesModuleTestBase): """Test the Circle class.""" shape_name = 'circle' - distance_test_cases = [[(20, 50), 10.490381], [(10, 25), 15.910168]] + distance_test_cases = (((20, 50), 10.490381), ((10, 25), 15.910168)) repr_regex = '^' + CIRCLE_REPR + '$' def test_is_circle(self, shape): @@ -79,7 +78,7 @@ class TestRings(CirclesModuleTestBase): """Test the Rings class.""" shape_name = 'rings' - distance_test_cases = [[(20, 50), 3.16987], [(10, 25), 9.08004]] + distance_test_cases = (((20, 50), 3.16987), ((10, 25), 9.08004)) repr_regex = ( r'^\n' r' circles=\n' diff --git a/tests/shapes/test_lines.py b/tests/shapes/test_lines.py index b20d860b..d285d82e 100644 --- a/tests/shapes/test_lines.py +++ b/tests/shapes/test_lines.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Iterable from numbers import Number import numpy as np @@ -15,9 +14,9 @@ class LinesModuleTestBase: """Base for testing line-based shapes.""" shape_name: str - distance_test_cases: Iterable[tuple[Iterable[Number], float]] + distance_test_cases: tuple[tuple[tuple[Number], float]] expected_line_count: int - expected_slopes: Iterable[Number] | Number + expected_slopes: tuple[Number] | Number @pytest.fixture(scope='class') def shape(self, shape_factory): @@ -67,7 +66,7 @@ class TestHighLines(ParallelLinesModuleTestBase): """Test the HighLines class.""" shape_name = 'high_lines' - distance_test_cases = [[(20, 50), 6.0], [(30, 60), 4.0]] + distance_test_cases = (((20, 50), 6.0), ((30, 60), 4.0)) expected_line_count = 2 expected_slopes = 0 @@ -76,7 +75,7 @@ class TestHorizontalLines(ParallelLinesModuleTestBase): """Test the HorizontalLines class.""" shape_name = 'h_lines' - distance_test_cases = [[(20, 50), 0.0], [(30, 60), 2.5]] + distance_test_cases = (((20, 50), 0.0), ((30, 60), 2.5)) expected_line_count = 5 expected_slopes = 0 @@ -85,7 +84,7 @@ class TestSlantDownLines(ParallelLinesModuleTestBase): """Test the SlantDownLines class.""" shape_name = 'slant_down' - distance_test_cases = [[(20, 50), 1.664101], [(30, 60), 0.554700]] + distance_test_cases = (((20, 50), 1.664101), ((30, 60), 0.554700)) expected_line_count = 5 expected_slopes = -1.5 @@ -94,7 +93,7 @@ class TestSlantUpLines(ParallelLinesModuleTestBase): """Test the SlantUpLines class.""" shape_name = 'slant_up' - distance_test_cases = [[(20, 50), 1.664101], [(30, 60), 1.109400]] + distance_test_cases = (((20, 50), 1.664101), ((30, 60), 1.109400)) expected_line_count = 5 expected_slopes = 1.5 @@ -103,7 +102,7 @@ class TestVerticalLines(ParallelLinesModuleTestBase): """Test the VerticalLines class.""" shape_name = 'v_lines' - distance_test_cases = [[(35, 60), 5.0], [(30, 60), 0.0]] + distance_test_cases = (((35, 60), 5.0), ((30, 60), 0.0)) expected_line_count = 5 expected_slopes = np.inf @@ -112,7 +111,7 @@ class TestWideLines(ParallelLinesModuleTestBase): """Test the WideLines class.""" shape_name = 'wide_lines' - distance_test_cases = [[(26, 50), 0], [(30, 60), 4.0]] + distance_test_cases = (((26, 50), 0), ((30, 60), 4.0)) expected_line_count = 2 expected_slopes = np.inf @@ -121,14 +120,14 @@ class TestXLines(LinesModuleTestBase): """Test the XLines class.""" shape_name = 'x' - distance_test_cases = [ - [(8, 83), 0], # edge of X line - [(20, 65), 0], # middle of X (intersection point) - [(19, 64), 0.277350], # off the X - [(10, 20), 27.073973], # off the X - ] + distance_test_cases = ( + ((8, 83), 0), # edge of X line + ((20, 65), 0), # middle of X (intersection point) + ((19, 64), 0.277350), # off the X + ((10, 20), 27.073973), # off the X + ) expected_line_count = 2 - expected_slopes = [-1.5, 1.5] + expected_slopes = (-1.5, 1.5) def test_lines_form_an_x(self, shape): """Test that the lines form an X.""" diff --git a/tests/shapes/test_points.py b/tests/shapes/test_points.py index 6d409464..61a109ff 100644 --- a/tests/shapes/test_points.py +++ b/tests/shapes/test_points.py @@ -1,6 +1,5 @@ """Test points module.""" -from collections.abc import Iterable from numbers import Number import numpy as np @@ -13,7 +12,7 @@ class PointsModuleTestBase: """Base for testing point-based shapes.""" shape_name: str - distance_test_cases: Iterable[tuple[Iterable[Number], float]] + distance_test_cases: tuple[tuple[tuple[Number], float]] @pytest.fixture(scope='class') def shape(self, shape_factory): @@ -33,7 +32,7 @@ class TestDotsGrid(PointsModuleTestBase): """Test the DotsGrid class.""" shape_name = 'dots' - distance_test_cases = [[(20, 50), 0.0], [(30, 60), 3.640055]] + distance_test_cases = (((20, 50), 0.0), ((30, 60), 3.640055)) expected_point_count = 9 def test_init(self, shape): @@ -72,21 +71,21 @@ class TestHeart(PointsModuleTestBase): """Test the Heart class.""" shape_name = 'heart' - distance_test_cases = [ - [(19.89946048, 54.82281916), 0.0], - [(10.84680454, 70.18556376), 0.0], - [(29.9971295, 67.66402445), 0.0], - [(27.38657942, 62.417184), 0.0], - [(20, 50), 4.567369], - [(10, 80), 8.564365], - ] + distance_test_cases = ( + ((19.89946048, 54.82281916), 0.0), + ((10.84680454, 70.18556376), 0.0), + ((29.9971295, 67.66402445), 0.0), + ((27.38657942, 62.417184), 0.0), + ((20, 50), 4.567369), + ((10, 80), 8.564365), + ) class TestScatter(PointsModuleTestBase): """Test the Scatter class.""" shape_name = 'scatter' - distance_test_cases = [[(20, 50), 0.0], [(30, 60), 0.0], [(-500, -150), 0.0]] + distance_test_cases = (((20, 50), 0.0), ((30, 60), 0.0), ((-500, -150), 0.0)) class ParabolaTestBase(PointsModuleTestBase): @@ -108,7 +107,7 @@ class TestDownParabola(ParabolaTestBase): """Test the DownParabola class.""" shape_name = 'down_parab' - distance_test_cases = [[(20, 50), 7.929688], [(30, 60), 3.455534]] + distance_test_cases = (((20, 50), 7.929688), ((30, 60), 3.455534)) positive_quadratic_term = False x_index = 0 y_index = 1 @@ -118,7 +117,7 @@ class TestLeftParabola(ParabolaTestBase): """Test the LeftParabola class.""" shape_name = 'left_parab' - distance_test_cases = [[(50, 20), 46.31798], [(10, 77), 0.0]] + distance_test_cases = (((50, 20), 46.31798), ((10, 77), 0.0)) positive_quadratic_term = False x_index = 1 y_index = 0 @@ -128,7 +127,7 @@ class TestRightParabola(ParabolaTestBase): """Test the RightParabola class.""" shape_name = 'right_parab' - distance_test_cases = [[(50, 20), 38.58756], [(10, 77), 7.740692]] + distance_test_cases = (((50, 20), 38.58756), ((10, 77), 7.740692)) positive_quadratic_term = True x_index = 1 y_index = 0 @@ -138,7 +137,7 @@ class TestUpParabola(ParabolaTestBase): """Test the UpParabola class.""" shape_name = 'up_parab' - distance_test_cases = [[(0, 0), 53.774155], [(30, 60), 5.2576809]] + distance_test_cases = (((0, 0), 53.774155), ((30, 60), 5.2576809)) positive_quadratic_term = True x_index = 0 y_index = 1 @@ -148,28 +147,28 @@ class TestClub(PointsModuleTestBase): """Test the Club class.""" shape_name = 'club' - distance_test_cases = [ - [(19.639387, 73.783711), 0.0], # top lobe - [(12.730310, 60.295844), 0.0], # bottom left lobe - [(27.630301, 60.920443), 0.0], # bottom right lobe - [(20.304761, 55.933333), 0.0], # top of stem - [(18.8, 57.076666), 0.0], # left part of stem - [(20.933333, 57.823333), 0.0], # right part of stem - [(0, 0), 58.717591], - [(20, 50), 5.941155], - [(10, 80), 10.288055], - ] + distance_test_cases = ( + ((19.639387, 73.783711), 0.0), # top lobe + ((12.730310, 60.295844), 0.0), # bottom left lobe + ((27.630301, 60.920443), 0.0), # bottom right lobe + ((20.304761, 55.933333), 0.0), # top of stem + ((18.8, 57.076666), 0.0), # left part of stem + ((20.933333, 57.823333), 0.0), # right part of stem + ((0, 0), 58.717591), + ((20, 50), 5.941155), + ((10, 80), 10.288055), + ) class TestSpade(PointsModuleTestBase): """Test the Spade class.""" shape_name = 'spade' - distance_test_cases = [ - [(19.97189615, 75.43271708), 0], - [(23.75, 55), 0], - [(11.42685318, 59.11304904), 0], - [(20, 75), 0.2037185], - [(0, 0), 57.350348], - [(10, 80), 10.968080], - ] + distance_test_cases = ( + ((19.97189615, 75.43271708), 0), + ((23.75, 55), 0), + ((11.42685318, 59.11304904), 0), + ((20, 75), 0.2037185), + ((0, 0), 57.350348), + ((10, 80), 10.968080), + ) diff --git a/tests/shapes/test_polygons.py b/tests/shapes/test_polygons.py index c97838c9..99a6c538 100644 --- a/tests/shapes/test_polygons.py +++ b/tests/shapes/test_polygons.py @@ -1,6 +1,5 @@ """Test polygons module.""" -from collections.abc import Iterable from numbers import Number import numpy as np @@ -13,7 +12,7 @@ class PolygonsModuleTestBase: """Base for testing polygon shapes.""" shape_name: str - distance_test_cases: Iterable[tuple[Iterable[Number], float]] + distance_test_cases: tuple[tuple[tuple[Number], float]] expected_line_count: int @pytest.fixture(scope='class') @@ -52,7 +51,7 @@ class TestDiamond(PolygonsModuleTestBase): """Test the Diamond class.""" shape_name = 'diamond' - distance_test_cases = [[(20, 50), 0.0], [(30, 60), 2.773501]] + distance_test_cases = (((20, 50), 0.0), ((30, 60), 2.773501)) expected_line_count = 4 def test_slopes(self, slopes): @@ -64,7 +63,7 @@ class TestRectangle(PolygonsModuleTestBase): """Test the Rectangle class.""" shape_name = 'rectangle' - distance_test_cases = [[(20, 50), 0.0], [(30, 60), 2.0]] + distance_test_cases = (((20, 50), 0.0), ((30, 60), 2.0)) expected_line_count = 4 def test_slopes(self, slopes): @@ -76,5 +75,5 @@ class TestStar(PolygonsModuleTestBase): """Test the Star class.""" shape_name = 'star' - distance_test_cases = [[(20, 50), 5.856516], [(30, 60), 3.709127]] + distance_test_cases = (((20, 50), 5.856516), ((30, 60), 3.709127)) expected_line_count = 10 diff --git a/tests/test_morpher.py b/tests/test_morpher.py index 01ad373b..b740d386 100644 --- a/tests/test_morpher.py +++ b/tests/test_morpher.py @@ -276,6 +276,6 @@ def test_freeze_animation_frames( # check that the images are indeed the same if write_images and freeze_for: assert len(image_hashes.keys()) == 1 - assert list(image_hashes.values())[0] == freeze_for + assert next(iter(image_hashes.values())) == freeze_for else: assert not image_hashes From e440ee76d2f6a4ceb06272928bd0303cbe3fe15d Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:59:48 +1100 Subject: [PATCH 3/3] Reduce redundancy in shape factory by adding names (#242) --- docs/tutorials/shape-creation.rst | 10 ++-- src/data_morph/shapes/bases/shape.py | 21 +++++++- src/data_morph/shapes/factory.py | 52 +++++++++++-------- src/data_morph/shapes/lines/high_lines.py | 5 +- .../shapes/lines/horizontal_lines.py | 5 +- src/data_morph/shapes/lines/slant_down.py | 5 +- src/data_morph/shapes/lines/slant_up.py | 5 +- src/data_morph/shapes/lines/vertical_lines.py | 5 +- src/data_morph/shapes/lines/wide_lines.py | 5 +- src/data_morph/shapes/lines/x_lines.py | 5 +- src/data_morph/shapes/points/dots_grid.py | 5 +- src/data_morph/shapes/points/parabola.py | 20 +++---- 12 files changed, 78 insertions(+), 65 deletions(-) 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'