From 1d94315b385b9bd62a7e07bd09ede6da2712e49e Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 26 Oct 2025 17:36:38 -0300 Subject: [PATCH 1/8] Add shape diagnostic function --- src/data_morph/data/dataset.py | 5 +- src/data_morph/plotting/diagnostics.py | 79 ++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 src/data_morph/plotting/diagnostics.py diff --git a/src/data_morph/data/dataset.py b/src/data_morph/data/dataset.py index 48a31702..7572b434 100644 --- a/src/data_morph/data/dataset.py +++ b/src/data_morph/data/dataset.py @@ -201,6 +201,7 @@ def plot( ax: Axes | None = None, show_bounds: bool = True, title: str | None = 'default', + alpha: Number = 1, ) -> Axes: """ Plot the dataset and its bounds. @@ -214,6 +215,8 @@ def plot( title : str | ``None``, optional Title to use for the plot. The default will call ``str()`` on the Dataset. Pass ``None`` to leave the plot untitled. + alpha : Number, default ``1`` + The transparency to use for the points in the plot. Returns ------- @@ -225,7 +228,7 @@ def plot( fig.get_layout_engine().set(w_pad=0.2, h_pad=0.2) ax.axis('equal') - ax.scatter(self.data.x, self.data.y, s=2, color='black') + ax.scatter(self.data.x, self.data.y, s=2, color='black', alpha=alpha) ax.set(xlabel='', ylabel='', title=self if title == 'default' else title) if show_bounds: diff --git a/src/data_morph/plotting/diagnostics.py b/src/data_morph/plotting/diagnostics.py new file mode 100644 index 00000000..ad000caa --- /dev/null +++ b/src/data_morph/plotting/diagnostics.py @@ -0,0 +1,79 @@ +"""Diagnostic plot to visualize a shape superimposed on the dataset.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from ..plotting.style import plot_with_custom_style + +if TYPE_CHECKING: + from numbers import Number + + from matplotlib.axes import Axes + + from ..data.dataset import Dataset + from ..shapes.bases.shape import Shape + + +@plot_with_custom_style +def plot_shape_on_dataset( + dataset: Dataset, + shape: Shape, + show_bounds: bool = False, + alpha: Number = 0.25, +) -> Axes: + """ + Plot a shape superimposed on a dataset to evaluate heuristics. + + Parameters + ---------- + dataset : Dataset + The dataset that ``shape`` was instantiated with. + shape : Shape + The shape that was instantiated with ``dataset``. + show_bounds : bool, default ``False`` + Whether to include the dataset's bounds in the plot. + alpha : Number, default ``0.25`` + The transparency to use for the dataset's points. + + Returns + ------- + matplotlib.axes.Axes + The :class:`~matplotlib.axes.Axes` object containing the plot. + + Examples + -------- + + .. plot:: + :scale: 75 + :include-source: + :caption: + Visualization of the :class:`.Star` when calculated based on the + music :class:`.Dataset`, with the dataset's bounds. + + from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset + from data_morph.shapes.lines import Star + + dataset = DataLoader.load_dataset('music') + shape = Star(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=True, alpha=0.1) + + .. plot:: + :scale: 75 + :include-source: + :caption: + Visualization of the :class:`.Heart` when calculated based on the + music :class:`.Dataset`, without the dataset's bounds. + + from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset + from data_morph.shapes.points import Heart + + dataset = DataLoader.load_dataset('music') + shape = Heart(dataset) + plot_shape_on_dataset(dataset, shape, alpha=0.1) + """ + ax = dataset.plot(show_bounds=show_bounds, title=None, alpha=alpha) + shape.plot(ax=ax) + return ax From b1bd99bc9550b06278ab89780361dbd5fcb2c3d0 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 26 Oct 2025 17:37:06 -0300 Subject: [PATCH 2/8] Add shape diagnostic check to shape creation tutorial --- docs/tutorials/shape-creation.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/tutorials/shape-creation.rst b/docs/tutorials/shape-creation.rst index 52fa88e5..d45e297f 100644 --- a/docs/tutorials/shape-creation.rst +++ b/docs/tutorials/shape-creation.rst @@ -66,6 +66,26 @@ shape inherits from :class:`.LineCollection` and uses the morph bounds Since we inherit from :class:`.LineCollection` here, we don't need to define the ``distance()`` and ``plot()`` methods (unless we want to override them). +.. tip:: + You can use the :func:`.plot_shape_on_dataset` function to visualize your shape's + positioning relative to a given dataset: + + .. plot:: + :scale: 75 + :include-source: + :caption: + Visualization of how the :class:`.XLines` looks when calculated based on the + music :class:`.Dataset`, with the dataset's bounds. + + from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset + from data_morph.shapes.lines import XLines + + + dataset = DataLoader.load_dataset('music') + shape = XLines(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=True, alpha=0.1) + Test out the shape ------------------ From 0a08393de79d59243fc1d56303ab7c05acfa3d3c Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 26 Oct 2025 17:38:29 -0300 Subject: [PATCH 3/8] Use shape diagnostic function to display all shapes in the docs --- src/data_morph/shapes/circles/bullseye.py | 5 +++- src/data_morph/shapes/circles/circle.py | 5 +++- src/data_morph/shapes/circles/rings.py | 5 +++- src/data_morph/shapes/lines/diamond.py | 6 +++-- src/data_morph/shapes/lines/high_lines.py | 5 +++- .../shapes/lines/horizontal_lines.py | 5 +++- src/data_morph/shapes/lines/rectangle.py | 6 +++-- src/data_morph/shapes/lines/slant_down.py | 5 +++- src/data_morph/shapes/lines/slant_up.py | 5 +++- src/data_morph/shapes/lines/star.py | 6 +++-- 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/club.py | 5 +++- src/data_morph/shapes/points/dots_grid.py | 5 +++- src/data_morph/shapes/points/figure_eight.py | 23 +++++++++++++++++-- src/data_morph/shapes/points/heart.py | 5 +++- src/data_morph/shapes/points/parabola.py | 20 ++++++++++++---- src/data_morph/shapes/points/scatter.py | 5 +++- src/data_morph/shapes/points/spade.py | 5 +++- src/data_morph/shapes/points/spiral.py | 5 +++- 21 files changed, 113 insertions(+), 28 deletions(-) diff --git a/src/data_morph/shapes/circles/bullseye.py b/src/data_morph/shapes/circles/bullseye.py index b4fd6d09..0d5d6767 100644 --- a/src/data_morph/shapes/circles/bullseye.py +++ b/src/data_morph/shapes/circles/bullseye.py @@ -22,9 +22,12 @@ class Bullseye(Rings): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.circles import Bullseye - _ = Bullseye(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = Bullseye(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) See Also -------- diff --git a/src/data_morph/shapes/circles/circle.py b/src/data_morph/shapes/circles/circle.py index 1aee9e2a..0e83bcd9 100644 --- a/src/data_morph/shapes/circles/circle.py +++ b/src/data_morph/shapes/circles/circle.py @@ -28,9 +28,12 @@ class Circle(Shape): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.circles import Circle - _ = Circle(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = Circle(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) Parameters ---------- diff --git a/src/data_morph/shapes/circles/rings.py b/src/data_morph/shapes/circles/rings.py index bf571c20..807652cb 100644 --- a/src/data_morph/shapes/circles/rings.py +++ b/src/data_morph/shapes/circles/rings.py @@ -28,9 +28,12 @@ class Rings(Shape): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.circles import Rings - _ = Rings(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = Rings(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) Parameters ---------- diff --git a/src/data_morph/shapes/lines/diamond.py b/src/data_morph/shapes/lines/diamond.py index 1c74c1aa..c3cba1e2 100644 --- a/src/data_morph/shapes/lines/diamond.py +++ b/src/data_morph/shapes/lines/diamond.py @@ -13,11 +13,13 @@ class Diamond(LineCollection): :caption: This shape is generated using the panda dataset. - import matplotlib.pyplot as plt from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.lines import Diamond - _ = Diamond(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = Diamond(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) Parameters ---------- diff --git a/src/data_morph/shapes/lines/high_lines.py b/src/data_morph/shapes/lines/high_lines.py index 06ca74a4..047fcca3 100644 --- a/src/data_morph/shapes/lines/high_lines.py +++ b/src/data_morph/shapes/lines/high_lines.py @@ -14,9 +14,12 @@ class HighLines(LineCollection): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.lines import HighLines - _ = HighLines(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = HighLines(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) Parameters ---------- diff --git a/src/data_morph/shapes/lines/horizontal_lines.py b/src/data_morph/shapes/lines/horizontal_lines.py index 80be7772..66c7fbc2 100644 --- a/src/data_morph/shapes/lines/horizontal_lines.py +++ b/src/data_morph/shapes/lines/horizontal_lines.py @@ -16,9 +16,12 @@ class HorizontalLines(LineCollection): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.lines import HorizontalLines - _ = HorizontalLines(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = HorizontalLines(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) Parameters ---------- diff --git a/src/data_morph/shapes/lines/rectangle.py b/src/data_morph/shapes/lines/rectangle.py index d646190d..612f6a75 100644 --- a/src/data_morph/shapes/lines/rectangle.py +++ b/src/data_morph/shapes/lines/rectangle.py @@ -13,11 +13,13 @@ class Rectangle(LineCollection): :caption: This shape is generated using the panda dataset. - import matplotlib.pyplot as plt from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.lines import Rectangle - _ = Rectangle(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = Rectangle(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) Parameters ---------- diff --git a/src/data_morph/shapes/lines/slant_down.py b/src/data_morph/shapes/lines/slant_down.py index 931779a9..c4bd638c 100644 --- a/src/data_morph/shapes/lines/slant_down.py +++ b/src/data_morph/shapes/lines/slant_down.py @@ -14,9 +14,12 @@ class SlantDownLines(LineCollection): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.lines import SlantDownLines - _ = SlantDownLines(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = SlantDownLines(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) Parameters ---------- diff --git a/src/data_morph/shapes/lines/slant_up.py b/src/data_morph/shapes/lines/slant_up.py index 266c85a6..98bef938 100644 --- a/src/data_morph/shapes/lines/slant_up.py +++ b/src/data_morph/shapes/lines/slant_up.py @@ -14,9 +14,12 @@ class SlantUpLines(LineCollection): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.lines import SlantUpLines - _ = SlantUpLines(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = SlantUpLines(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) Parameters ---------- diff --git a/src/data_morph/shapes/lines/star.py b/src/data_morph/shapes/lines/star.py index 16ae1f19..333616ae 100644 --- a/src/data_morph/shapes/lines/star.py +++ b/src/data_morph/shapes/lines/star.py @@ -15,11 +15,13 @@ class Star(LineCollection): :caption: This shape is generated using the panda dataset. - import matplotlib.pyplot as plt from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.lines import Star - _ = Star(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = Star(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) Parameters ---------- diff --git a/src/data_morph/shapes/lines/vertical_lines.py b/src/data_morph/shapes/lines/vertical_lines.py index 4982588b..ea173662 100644 --- a/src/data_morph/shapes/lines/vertical_lines.py +++ b/src/data_morph/shapes/lines/vertical_lines.py @@ -16,9 +16,12 @@ class VerticalLines(LineCollection): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.lines import VerticalLines - _ = VerticalLines(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = VerticalLines(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) Parameters ---------- diff --git a/src/data_morph/shapes/lines/wide_lines.py b/src/data_morph/shapes/lines/wide_lines.py index c8df8032..d2dd88c0 100644 --- a/src/data_morph/shapes/lines/wide_lines.py +++ b/src/data_morph/shapes/lines/wide_lines.py @@ -14,9 +14,12 @@ class WideLines(LineCollection): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.lines import WideLines - _ = WideLines(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = WideLines(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) Parameters ---------- diff --git a/src/data_morph/shapes/lines/x_lines.py b/src/data_morph/shapes/lines/x_lines.py index 75a69f52..84314eb0 100644 --- a/src/data_morph/shapes/lines/x_lines.py +++ b/src/data_morph/shapes/lines/x_lines.py @@ -14,9 +14,12 @@ class XLines(LineCollection): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.lines import XLines - _ = XLines(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = XLines(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) Parameters ---------- diff --git a/src/data_morph/shapes/points/club.py b/src/data_morph/shapes/points/club.py index 6dfea021..9f689679 100644 --- a/src/data_morph/shapes/points/club.py +++ b/src/data_morph/shapes/points/club.py @@ -18,9 +18,12 @@ class Club(PointCollection): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.points import Club - _ = Club(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = Club(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) Parameters ---------- diff --git a/src/data_morph/shapes/points/dots_grid.py b/src/data_morph/shapes/points/dots_grid.py index b9327c20..e2277188 100644 --- a/src/data_morph/shapes/points/dots_grid.py +++ b/src/data_morph/shapes/points/dots_grid.py @@ -16,9 +16,12 @@ class DotsGrid(PointCollection): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.points import DotsGrid - _ = DotsGrid(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = DotsGrid(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.1) Parameters ---------- diff --git a/src/data_morph/shapes/points/figure_eight.py b/src/data_morph/shapes/points/figure_eight.py index c143380f..cc0c89a0 100644 --- a/src/data_morph/shapes/points/figure_eight.py +++ b/src/data_morph/shapes/points/figure_eight.py @@ -16,9 +16,12 @@ class FigureEight(PointCollection): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.points import FigureEight - _ = FigureEight(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = FigureEight(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) Parameters ---------- @@ -30,7 +33,23 @@ class FigureEight(PointCollection): Notes ----- This shape uses the formula for the `Lemniscate of Bernoulli - `_. + `_. It will orient itself + vertically or horizontally depending on which direction has a larger range in the + input dataset. For example, the panda dataset used above resulted in a horizontal + orientation, but the music dataset results in a vertical orientation: + + .. plot:: + :scale: 75 + :caption: + This shape is generated using the music dataset. + + from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset + from data_morph.shapes.points import FigureEight + + dataset = DataLoader.load_dataset('music') + shape = FigureEight(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.1) """ name = 'figure_eight' diff --git a/src/data_morph/shapes/points/heart.py b/src/data_morph/shapes/points/heart.py index 9570e91b..83381ac5 100644 --- a/src/data_morph/shapes/points/heart.py +++ b/src/data_morph/shapes/points/heart.py @@ -16,9 +16,12 @@ class Heart(PointCollection): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.points import Heart - _ = Heart(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = Heart(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) Parameters ---------- diff --git a/src/data_morph/shapes/points/parabola.py b/src/data_morph/shapes/points/parabola.py index f6a5dd24..ce159736 100644 --- a/src/data_morph/shapes/points/parabola.py +++ b/src/data_morph/shapes/points/parabola.py @@ -16,9 +16,12 @@ class DownParabola(PointCollection): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.points import DownParabola - _ = DownParabola(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = DownParabola(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) Parameters ---------- @@ -53,9 +56,12 @@ class LeftParabola(PointCollection): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.points import LeftParabola - _ = LeftParabola(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = LeftParabola(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) Parameters ---------- @@ -90,9 +96,12 @@ class RightParabola(PointCollection): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.points import RightParabola - _ = RightParabola(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = RightParabola(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) Parameters ---------- @@ -127,9 +136,12 @@ class UpParabola(PointCollection): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.points import UpParabola - _ = UpParabola(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = UpParabola(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) Parameters ---------- diff --git a/src/data_morph/shapes/points/scatter.py b/src/data_morph/shapes/points/scatter.py index 5d7b02de..e0500e0d 100644 --- a/src/data_morph/shapes/points/scatter.py +++ b/src/data_morph/shapes/points/scatter.py @@ -18,9 +18,12 @@ class Scatter(PointCollection): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.points import Scatter - _ = Scatter(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = Scatter(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.1) Parameters ---------- diff --git a/src/data_morph/shapes/points/spade.py b/src/data_morph/shapes/points/spade.py index 2fd59a71..c13d0c63 100644 --- a/src/data_morph/shapes/points/spade.py +++ b/src/data_morph/shapes/points/spade.py @@ -19,9 +19,12 @@ class Spade(PointCollection): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.points import Spade - _ = Spade(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = Spade(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.25) Parameters ---------- diff --git a/src/data_morph/shapes/points/spiral.py b/src/data_morph/shapes/points/spiral.py index 8deceaf2..7e18d4b7 100644 --- a/src/data_morph/shapes/points/spiral.py +++ b/src/data_morph/shapes/points/spiral.py @@ -16,9 +16,12 @@ class Spiral(PointCollection): This shape is generated using the panda dataset. from data_morph.data.loader import DataLoader + from data_morph.plotting.diagnostics import plot_shape_on_dataset from data_morph.shapes.points import Spiral - _ = Spiral(DataLoader.load_dataset('panda')).plot() + dataset = DataLoader.load_dataset('panda') + shape = Spiral(dataset) + plot_shape_on_dataset(dataset, shape, show_bounds=False, alpha=0.15) Parameters ---------- From 06c6e1cd379c008ac9dfcee3b91a9f469f6a2484 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 26 Oct 2025 17:48:34 -0300 Subject: [PATCH 4/8] Mention bounds tips --- docs/tutorials/shape-creation.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/shape-creation.rst b/docs/tutorials/shape-creation.rst index d45e297f..701e93f3 100644 --- a/docs/tutorials/shape-creation.rst +++ b/docs/tutorials/shape-creation.rst @@ -67,8 +67,10 @@ Since we inherit from :class:`.LineCollection` here, we don't need to define the ``distance()`` and ``plot()`` methods (unless we want to override them). .. tip:: - You can use the :func:`.plot_shape_on_dataset` function to visualize your shape's - positioning relative to a given dataset: + You can use the :func:`.plot_shape_on_dataset` function to visualize your + shape's positioning relative to a given dataset. Your shape can exceed the data + bounds (:attr:`.Dataset.data_bounds`); however, it should not exceed the morph + bounds (:attr:`.Dataset.morph_bounds`): .. plot:: :scale: 75 From 5dec40291d18f82af6d5100375d0884a4b1ae73d Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 26 Oct 2025 17:50:43 -0300 Subject: [PATCH 5/8] Update overrides --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 80bee93c..89a0f5b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -187,3 +187,6 @@ exclude = [ # don't report on checks for these '\.__repr__$', '\.__str__$', ] +override_SS05 = [ # allow docstrings to start with these words + '^Unambiguous ', +] From 30b2a0c03cea49ce782db2813e248f6c714c7ae8 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 26 Oct 2025 20:44:09 -0300 Subject: [PATCH 6/8] Add tests for the shape diagnostic function --- tests/plotting/test_diagnostics.py | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tests/plotting/test_diagnostics.py diff --git a/tests/plotting/test_diagnostics.py b/tests/plotting/test_diagnostics.py new file mode 100644 index 00000000..c3518d0f --- /dev/null +++ b/tests/plotting/test_diagnostics.py @@ -0,0 +1,46 @@ +"""Test the diagnostics module.""" + +import pytest +from matplotlib.axes import Axes +from matplotlib.patches import Rectangle + +from data_morph.data.loader import DataLoader +from data_morph.plotting.diagnostics import plot_shape_on_dataset +from data_morph.shapes.bases.line_collection import LineCollection +from data_morph.shapes.bases.point_collection import PointCollection +from data_morph.shapes.factory import ShapeFactory + + +@pytest.mark.parametrize( + ('dataset_name', 'shape_name', 'show_bounds', 'alpha'), + [ + ('panda', 'heart', True, 0.4), + ('music', 'rectangle', False, 0.25), + ('sheep', 'circle', False, 0.5), + ], +) +def test_plot_shape_on_dataset(dataset_name, shape_name, show_bounds, alpha): + """Test the plot_shape_on_dataset() function.""" + dataset = DataLoader.load_dataset(dataset_name) + shape = ShapeFactory(dataset).generate_shape(shape_name) + ax = plot_shape_on_dataset(dataset, shape, show_bounds, alpha) + + assert isinstance(ax, Axes) + assert not ax.get_title() + + assert ax.collections[0].get_alpha() == alpha + + points_expected = dataset.data.shape[0] + if isinstance(shape, PointCollection): + points_expected += shape.points.shape[0] + + points_plotted = sum( + collection.get_offsets().data.shape[0] for collection in ax.collections + ) + assert points_expected == points_plotted + + if isinstance(shape, LineCollection): + assert len(ax.lines) == len(shape.lines) + + if show_bounds: + assert sum(isinstance(patch, Rectangle) for patch in ax.patches) == 3 From 4067e5ea5b0b9187da9722b3e7ddecc1cc704a17 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 26 Oct 2025 20:53:24 -0300 Subject: [PATCH 7/8] Tweak captions --- docs/tutorials/shape-creation.rst | 2 +- src/data_morph/plotting/diagnostics.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/tutorials/shape-creation.rst b/docs/tutorials/shape-creation.rst index 701e93f3..424ceb3a 100644 --- a/docs/tutorials/shape-creation.rst +++ b/docs/tutorials/shape-creation.rst @@ -76,7 +76,7 @@ the ``distance()`` and ``plot()`` methods (unless we want to override them). :scale: 75 :include-source: :caption: - Visualization of how the :class:`.XLines` looks when calculated based on the + Visualization of the :class:`.XLines` shape when calculated based on the music :class:`.Dataset`, with the dataset's bounds. from data_morph.data.loader import DataLoader diff --git a/src/data_morph/plotting/diagnostics.py b/src/data_morph/plotting/diagnostics.py index ad000caa..4ef688b5 100644 --- a/src/data_morph/plotting/diagnostics.py +++ b/src/data_morph/plotting/diagnostics.py @@ -48,7 +48,7 @@ def plot_shape_on_dataset( :scale: 75 :include-source: :caption: - Visualization of the :class:`.Star` when calculated based on the + Visualization of the :class:`.Star` shape when calculated based on the music :class:`.Dataset`, with the dataset's bounds. from data_morph.data.loader import DataLoader @@ -63,7 +63,7 @@ def plot_shape_on_dataset( :scale: 75 :include-source: :caption: - Visualization of the :class:`.Heart` when calculated based on the + Visualization of the :class:`.Heart` shape when calculated based on the music :class:`.Dataset`, without the dataset's bounds. from data_morph.data.loader import DataLoader From eea8f3bd3af7d32af70fe867ebb20ce8915ed3a5 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 26 Oct 2025 20:57:47 -0300 Subject: [PATCH 8/8] Add remaining plot tests --- tests/data/test_dataset.py | 41 ++++++++++++++++++++++ tests/shapes/bases/test_line_collection.py | 12 +++++++ 2 files changed, 53 insertions(+) diff --git a/tests/data/test_dataset.py b/tests/data/test_dataset.py index cfcde4ff..1a327a21 100644 --- a/tests/data/test_dataset.py +++ b/tests/data/test_dataset.py @@ -1,7 +1,10 @@ """Test the dataset module.""" +import matplotlib.pyplot as plt import pandas as pd import pytest +from matplotlib.axes import Axes +from matplotlib.patches import Rectangle from numpy.testing import assert_equal from pandas.testing import assert_frame_equal @@ -108,3 +111,41 @@ def test_repr(self, scale): dataset = DataLoader.load_dataset('dino', scale=scale) assert repr(dataset) == (f'') + + @pytest.mark.parametrize( + ('ax', 'show_bounds', 'title', 'alpha'), + [ + (None, True, None, 1), + (None, True, 'Custom title', 0.75), + (plt.subplots()[1], False, 'default', 0.5), + ], + ) + def test_plot(self, ax, show_bounds, title, alpha): + """Test the plot() method.""" + dataset = DataLoader.load_dataset('dino') + ax = dataset.plot(ax=ax, show_bounds=show_bounds, title=title, alpha=alpha) + + assert isinstance(ax, Axes) + assert pytest.approx(ax.get_aspect()) == 1.0 + + if title is None: + assert not ax.get_title() + elif title == 'default': + assert ax.get_title() == repr(dataset) + else: + assert ax.get_title() == title + + assert ax.collections[0].get_alpha() == alpha + + points_expected = dataset.data.shape[0] + points_plotted = sum( + collection.get_offsets().data.shape[0] for collection in ax.collections + ) + assert points_expected == points_plotted + + if show_bounds: + assert sum(isinstance(patch, Rectangle) for patch in ax.patches) == 3 + assert ax.patches[0] != ax.patches[1] != ax.patches[2] + + assert len(labels := ax.texts) == 3 + assert all(label.get_text().endswith('BOUNDS') for label in labels) diff --git a/tests/shapes/bases/test_line_collection.py b/tests/shapes/bases/test_line_collection.py index 94f02a2f..3e3e5377 100644 --- a/tests/shapes/bases/test_line_collection.py +++ b/tests/shapes/bases/test_line_collection.py @@ -3,7 +3,9 @@ import itertools import re +import matplotlib.pyplot as plt import pytest +from matplotlib.axes import Axes from data_morph.shapes.bases.line_collection import LineCollection @@ -49,3 +51,13 @@ def test_repr(self, line_collection): ) is not None ) + + @pytest.mark.parametrize('existing_ax', [True, False]) + def test_plot(self, line_collection, existing_ax): + """Test the plot() method is working.""" + input_ax = plt.subplots()[1] if existing_ax else None + result_ax = line_collection.plot(input_ax) + + assert isinstance(result_ax, Axes) + assert len(result_ax.lines) == len(line_collection.lines) + assert pytest.approx(result_ax.get_aspect()) == 1.0