Skip to content

Commit 5b5a36e

Browse files
authored
Expand shape creation tutorial (#277)
1 parent 81bcd6a commit 5b5a36e

File tree

1 file changed

+146
-54
lines changed

1 file changed

+146
-54
lines changed

docs/tutorials/shape-creation.rst

Lines changed: 146 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,16 @@ This tutorial walks you through the process of creating a new shape
55
for use as a target in the morphing process.
66

77
.. contents:: Steps
8-
:depth: 2
8+
:depth: 1
99
:local:
1010
:backlinks: none
1111

1212
----
1313

14-
Create a class for the shape
15-
----------------------------
16-
17-
All Data Morph shapes are defined as classes inside the :mod:`.shapes` subpackage.
18-
In order to register a new target shape for the CLI, you will need to fork and clone
19-
`the Data Morph repository <https://github.com/stefmolin/data-morph>`_, and then add
20-
a class defining your shape.
21-
2214
Select the appropriate base class
23-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
15+
---------------------------------
2416

17+
All Data Morph shapes are defined as classes inside the :mod:`.shapes` subpackage.
2518
Data Morph uses a hierarchy of shapes that all descend from an abstract
2619
base class (:class:`.Shape`), which defines the basics of how a shape
2720
needs to behave (*i.e.*, it must have a ``distance()`` method and a
@@ -39,67 +32,61 @@ child classes:
3932
* If your shape is composed of points, inherit from :class:`.PointCollection`
4033
(*e.g.*, :class:`.Heart`).
4134
* If your shape isn't composed of lines or points you can inherit directly from
42-
:class:`.Shape` (*e.g.*, :class:`.Circle`). Note that in this case you must
43-
define both the ``distance()`` and ``plot()`` methods (this is done for your
35+
:class:`.Shape` (*e.g.*, :class:`.Circle`). Note that, in this case, you must
36+
define both the ``distance()`` and ``plot()`` methods (this is done for you
4437
if you inherit from :class:`.LineCollection` or :class:`.PointCollection`).
4538

4639
Define the scale and placement of the shape based on the dataset
47-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
40+
----------------------------------------------------------------
4841

4942
Each shape will be initialized with a :class:`.Dataset` instance. Use the dataset
5043
to determine where in the *xy*-plane the shape should be placed and also to scale it
51-
to the data. If you take a look at the existing shapes, you will see that they use
52-
various bits of information from the dataset, such as the automatically-calculated
53-
bounds (*e.g.*, :attr:`.Dataset.data_bounds`, which form the bounding box of the
54-
starting data, and :attr:`.Dataset.morph_bounds`, which define the limits of where
55-
the algorithm can move the points) or percentiles using the data itself (see
56-
:attr:`.Dataset.data`). For example, the :class:`.XLines` shape inherits from
57-
:class:`.LineCollection` and uses the morph bounds (:attr:`.Dataset.morph_bounds`)
58-
to calculate its position and scale:
44+
to the data. If you take a look at the code for the existing shapes, you will see
45+
that they use various bits of information from the dataset, such as the
46+
automatically-calculated bounds (*e.g.*, :attr:`.Dataset.data_bounds`, which form
47+
the bounding box of the starting data, and :attr:`.Dataset.morph_bounds`, which
48+
define the limits of where the algorithm can move the points) or percentiles using
49+
the data itself (see :attr:`.Dataset.data`). For example, the :class:`.XLines`
50+
shape inherits from :class:`.LineCollection` and uses the morph bounds
51+
(:attr:`.Dataset.morph_bounds`) to calculate its position and scale:
5952

6053
.. code:: python
6154
62-
class XLines(LineCollection):
55+
from data_morph.data.dataset import Dataset
56+
from data_morph.shapes.bases.line_collection import LineCollection
6357
64-
name = 'x'
58+
class XLines(LineCollection):
6559
66-
def __init__(self, dataset: Dataset) -> None:
67-
(xmin, xmax), (ymin, ymax) = dataset.morph_bounds
60+
def __init__(self, dataset: Dataset) -> None:
61+
(xmin, xmax), (ymin, ymax) = dataset.morph_bounds
6862
69-
super().__init__([[xmin, ymin], [xmax, ymax]], [[xmin, ymax], [xmax, ymin]])
63+
super().__init__([[xmin, ymin], [xmax, ymax]], [[xmin, ymax], [xmax, ymin]])
7064
7165
7266
Since we inherit from :class:`.LineCollection` here, we don't need to define
7367
the ``distance()`` and ``plot()`` methods (unless we want to override them).
74-
We do set the ``name`` attribute here since the default will result in
75-
a value of ``xlines`` and ``x`` makes more sense for use in the documentation
76-
(see :class:`.ShapeFactory`).
77-
78-
Register the shape
79-
------------------
80-
81-
For the ``data-morph`` CLI to find your shape, you need to register it with the
82-
:class:`.ShapeFactory`:
83-
84-
1. Add your shape class to the appropriate module inside the ``src/data_morph/shapes/``
85-
directory. Note that these correspond to the type of shape (*e.g.*, use
86-
``src/data_morph/shapes/points/<your_shape>.py`` for a new shape inheriting from
87-
:class:`.PointCollection`).
88-
2. Add your shape to ``__all__`` in that module's ``__init__.py`` (*e.g.*, use
89-
``src/data_morph/shapes/points/__init__.py`` for a new shape inheriting from
90-
:class:`.PointCollection`).
91-
3. Add an entry to the ``ShapeFactory._SHAPE_CLASSES`` tuple in
92-
``src/data_morph/shapes/factory.py``, preserving alphabetical order.
9368

9469
Test out the shape
9570
------------------
9671

9772
Defining how your shape should be generated from the input dataset will require
9873
a few iterations. Be sure to test out your shape on different datasets:
9974

100-
.. code:: console
75+
.. code:: python
76+
77+
from data_morph.data.loader import DataLoader
78+
from data_morph.morpher import DataMorpher
79+
80+
dataset = DataLoader.load_dataset('panda')
81+
target_shape = YourShape(dataset) # TODO replace with your class
82+
83+
morpher = DataMorpher(
84+
decimals=2,
85+
in_notebook=False, # whether you are running in a Jupyter Notebook
86+
output_dir='data_morph/output', # where you want the output to go
87+
)
10188
102-
$ data-morph --start-shape panda music soccer --target-shape <your shape>
89+
result = morpher.morph(start_shape=dataset, target_shape=target_shape)
10390
10491
Some shapes will work better on certain datasets, and that's fine. However,
10592
if your shape only works well on one of the built-in datasets (see the
@@ -110,12 +97,117 @@ if your shape only works well on one of the built-in datasets (see the
11097

11198
If you think that your shape would be a good addition to Data Morph, `create an issue
11299
<https://github.com/stefmolin/data-morph/issues>`_ in the Data Morph repository proposing
113-
its inclusion. Be sure to consult the `contributing guidelines
114-
<https://github.com/stefmolin/data-morph/blob/main/CONTRIBUTING.md>`_ before doing so.
100+
its inclusion. Be sure to consult the `contributing guidelines`_ before doing so.
101+
102+
If and only if you are given the go ahead, work through this section to contribute your
103+
shape.
104+
105+
1. Create a new module for your shape
106+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
107+
108+
.. note::
109+
If you haven't already, fork and clone `the Data Morph repository
110+
<https://github.com/stefmolin/data-morph>`_ and follow the instructions in the
111+
`contributing guidelines`_ to install Data Morph in editable mode and configure ``pre-commit``.
112+
113+
Save your shape in ``src/data_morph/shapes/<base>/<your_shape>.py``. In the case of
114+
the example in this tutorial (:class:`.XLines`), it inherits from :class:`.LineCollection`,
115+
and its module is called ``x_lines``, so the file is ``src/data_morph/shapes/lines/x_lines.py``.
116+
117+
Add type annotations and prepare a docstring for your shape following what the other
118+
shapes have. Be sure to change the plotting code in the docstring (in the
119+
``.. plot::`` block) to use your shape. Here's how the :mod:`.x_lines` module looks
120+
in the package:
121+
122+
.. literalinclude:: ../../src/data_morph/shapes/lines/x_lines.py
123+
:language: python
124+
125+
Notice that we set the ``name`` attribute here since the default will result in
126+
a value of ``xlines`` and ``x`` makes more sense for use in the documentation
127+
(see :class:`.ShapeFactory`). Check out some of the other modules inheriting from
128+
the same base as your shape to make sure you are following the project's conventions,
129+
such as using relative imports within the package.
130+
131+
.. note::
132+
If your shape inherits from :class:`.PointCollection`, try to create your shape with
133+
as few points as possible because each additional point requires another calculation
134+
per iteration of the morphing algorithm. Take a look at how many points existing
135+
shapes in the :mod:`.points` module use as a guideline.
136+
137+
At this point, your shape should pass all the ``pre-commit`` checks. If you haven't set up
138+
your development environment for Data Morph or aren't sure how to run these checks, please
139+
consult the `contributing guidelines`_.
140+
141+
2. Register the shape
142+
~~~~~~~~~~~~~~~~~~~~~
143+
144+
For the :doc:`Data Morph CLI <../cli>` to find your shape, you need to register it with the
145+
:class:`.ShapeFactory`:
146+
147+
1. Add your shape to ``__all__`` in the ``__init__.py`` closest to the module you
148+
created in the previous step (*e.g.*, use ``src/data_morph/shapes/lines/__init__.py``
149+
for a new shape inheriting from :class:`.LineCollection`).
150+
2. Add an entry to the ``ShapeFactory._SHAPE_CLASSES`` tuple in
151+
``src/data_morph/shapes/factory.py``, preserving alphabetical order.
152+
153+
3. Create test cases for the shape
154+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
155+
156+
Data Morph uses ``pytest`` for the test suite, and all tests are located in the ``tests/``
157+
directory, with a folder structure that mirrors the actual package. The test cases for
158+
your shape will go in ``tests/shapes/<base>/test_<your_shape>.py``. In the case of
159+
the example in this tutorial (:class:`.XLines`), it inherits from :class:`.LineCollection`,
160+
and its module is called ``x_lines``, so the test file is ``tests/shapes/lines/test_x_lines.py``.
161+
162+
There are test bases for each type of shape in ``tests/shapes/<base>/bases.py``, which
163+
handle most of the logic for running the tests. For shapes inheriting from
164+
:class:`.LineCollection`, this base is ``LinesModuleTestBase``, which can be used as follows:
165+
166+
.. literalinclude:: ../../tests/shapes/lines/test_x_lines.py
167+
:language: python
168+
169+
Note that the class variables provide the test cases for ``LinesModuleTestBase`` to use.
170+
To get ``distance_test_cases``, which is a tuple of test cases of the form
171+
``((x, y), expected_distance)``, for example, you will need to come up with a few points
172+
that have distance zero to the shape, and a few points that have a non-zero distance.
173+
You can come up with these by using the instantiated shape's ``distance()`` method, or
174+
by inspecting the instantiated shape's attributes like :attr:`.PointCollection.points`
175+
on shapes inheriting from :class:`.PointCollection`.
176+
177+
.. note::
178+
The :class:`.XLines` shape also defines its own test case to make sure that the lines
179+
form an X. It's only necessary to add additional test methods like this to test
180+
aspects not covered by the base class.
181+
182+
You should now be able to run the test suite with ``pytest``. Make sure your test cases pass
183+
before moving on. If you haven't set up your development environment for Data Morph or aren't
184+
sure how to run these checks, please consult the `contributing guidelines`_.
185+
186+
4. Confirm that your shape works via the CLI
187+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
188+
189+
Run the following on the command line replacing ``<your shape>`` with the value you set
190+
for the ``name`` attribute of your shape class to generate three animations:
191+
192+
.. code:: console
193+
194+
$ data-morph --start-shape panda music soccer --target-shape <your shape> --workers 3
195+
196+
Review the animations. Remember, some shapes will work better on certain datasets,
197+
and that's fine. However, if your shape only works well on one of the built-in datasets
198+
(see the :class:`.DataLoader`), then you need to keep tweaking your implementation.
199+
200+
.. tip::
201+
If you decide to run with multiple datasets, you can set ``--workers 0`` to run as
202+
many transformations in parallel as possible on your computer. In the above example,
203+
we only have three transformations, so ``--workers 3`` will run all three in parallel,
204+
assuming your machine has at least three CPU cores.
205+
206+
5. Submit your pull request
207+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
115208

116-
If and only if you are given the go ahead:
209+
If your shape works well on different datasets and your code passes all the checks and
210+
tests cases, you are ready to `make a pull request <https://github.com/stefmolin/data-morph/pulls>`_.
211+
If you aren't sure how to do this, please consult the `contributing guidelines`_.
117212

118-
1. Prepare a docstring for your shape following what the other shapes have.
119-
Be sure to change the plotting code in the docstring to use your shape.
120-
2. Add test cases for your shape to the ``tests/shapes/`` directory.
121-
3. Submit your pull request.
213+
.. _contributing guidelines: https://github.com/stefmolin/data-morph/blob/main/CONTRIBUTING.md

0 commit comments

Comments
 (0)