From 9d421c4b06b646391731ec7dd7d0703e40b84222 Mon Sep 17 00:00:00 2001 From: Stefanie Molin <24376333+stefmolin@users.noreply.github.com> Date: Sun, 14 May 2023 19:15:13 -0400 Subject: [PATCH] Add heart target shape. --- src/data_morph/shapes/factory.py | 1 + src/data_morph/shapes/points.py | 48 ++++++++++++++++++++++++++++++++ tests/shapes/test_points.py | 16 ++++++++++- 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/data_morph/shapes/factory.py b/src/data_morph/shapes/factory.py index caa0cb6b..c4b4c600 100644 --- a/src/data_morph/shapes/factory.py +++ b/src/data_morph/shapes/factory.py @@ -44,6 +44,7 @@ class ShapeFactory: 'x': lines.XLines, 'dots': points.DotsGrid, 'down_parab': points.DownParabola, + 'heart': points.Heart, 'left_parab': points.LeftParabola, 'scatter': points.Scatter, 'right_parab': points.RightParabola, diff --git a/src/data_morph/shapes/points.py b/src/data_morph/shapes/points.py index 3692cc11..158be2a8 100644 --- a/src/data_morph/shapes/points.py +++ b/src/data_morph/shapes/points.py @@ -83,6 +83,54 @@ 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. diff --git a/tests/shapes/test_points.py b/tests/shapes/test_points.py index f3de859f..842d831d 100644 --- a/tests/shapes/test_points.py +++ b/tests/shapes/test_points.py @@ -25,7 +25,7 @@ def test_distance(self, shape, test_point, expected_distance): Test the distance() method parametrized by distance_test_cases (see conftest.py). """ - assert pytest.approx(shape.distance(*test_point)) == expected_distance + assert pytest.approx(shape.distance(*test_point), abs=1e-5) == expected_distance class TestDotsGrid(PointsModuleTestBase): @@ -67,6 +67,20 @@ def test_points_form_symmetric_grid(self, shape): assert row_midpoint == middle_row[point][1] +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], + ] + + class TestScatter(PointsModuleTestBase): """Test the Scatter class."""