@@ -5,23 +5,16 @@ This tutorial walks you through the process of creating a new shape
55for 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-
2214Select the appropriate base class
23- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
15+ ---------------------------------
2416
17+ All Data Morph shapes are defined as classes inside the :mod: `.shapes ` subpackage.
2518Data Morph uses a hierarchy of shapes that all descend from an abstract
2619base class (:class: `.Shape `), which defines the basics of how a shape
2720needs 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
4639Define the scale and placement of the shape based on the dataset
47- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
40+ ----------------------------------------------------------------
4841
4942Each shape will be initialized with a :class: `.Dataset ` instance. Use the dataset
5043to 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
7367the ``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
9469Test out the shape
9570------------------
9671
9772Defining how your shape should be generated from the input dataset will require
9873a 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,
10592if 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
11198If 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