Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]

env:
MPLBACKEND: Agg # non-interactive backend for matplotlib
Expand Down
Binary file modified docs/_static/panda-to-star-eased.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/_static/panda-to-star.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 16 additions & 2 deletions docs/quickstart.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,24 @@ This produces the following animation in the newly-created ``morphed_data`` dire
within your current working directory:

.. figure:: _static/panda-to-star.gif
:alt: Morphing the panda dataset into the star shape.
:alt: Morphing the panda dataset into the star shape with marginal plots.
:align: center

Morphing the panda :class:`.Dataset` into the star :class:`.Shape`.
Morphing the panda :class:`.Dataset` into the star :class:`.Shape` with marginal plots.

If you don't want the marginal plots (the histograms on the sides), you can use classic mode:

.. code:: console

$ data-morph --start-shape panda --target-shape star --classic

Animations generated in classic mode include only the scatter plot and the summary statistics:

.. figure:: _static/panda-to-star-classic.gif
:alt: Morphing the panda dataset into the star shape using classic mode.
:align: center

Morphing the panda :class:`.Dataset` into the star :class:`.Shape` using classic mode.

You can smooth the transition with the ``--ease`` or ``--ease-in`` and ``--ease-out`` flags.
The ``--freeze`` flag allows you to start the animation with the specified number of frames
Expand Down
2 changes: 1 addition & 1 deletion docs/tutorials/custom-datasets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Pass the path to the CSV file to use those points as the starting shape:

.. code:: console

$ data-morph --start-shape path/to/points.csv --target-shape wide_lines
$ data-morph --start-shape path/to/points.csv --target-shape wide_lines --classic

Here is an example animation generated from a custom dataset:

Expand Down
9 changes: 4 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,13 @@ authors = [
{ name = "Aaron Stevens", email = "bheklilr2@gmail.com" },
{ name = "Justin Matejka", email = "Justin.Matejka@Autodesk.com" },
]
requires-python = ">=3.9"
requires-python = ">=3.10"
classifiers = [
"Development Status :: 4 - Beta",
"Framework :: Matplotlib",
"Intended Audience :: Education",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand All @@ -45,9 +44,9 @@ dynamic = [
]

dependencies = [
"matplotlib>=3.7",
"numpy>=1.20",
"pandas>=1.2",
"matplotlib>=3.10",
"numpy>=1.23.0",
"pandas>=2.1",
"rich>=13.9.4",
]

Expand Down
2 changes: 1 addition & 1 deletion src/data_morph/bounds/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def _validate_2d(data: Iterable[Number], name: str) -> Iterable[Number]:
The validated data.
"""
if not (
isinstance(data, (tuple, list))
isinstance(data, tuple | list)
and len(data) == 2
and all(isinstance(x, Number) and not isinstance(x, bool) for x in data)
):
Expand Down
10 changes: 5 additions & 5 deletions src/data_morph/bounds/bounding_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def __init__(
if isinstance(inclusive, bool):
inclusive = [inclusive] * 2
if not (
isinstance(inclusive, (tuple, list))
isinstance(inclusive, tuple | list)
and len(inclusive) == 2
and all(isinstance(x, bool) for x in inclusive)
):
Expand All @@ -47,19 +47,19 @@ def __init__(
' or a single Boolean value'
)

self.x_bounds = (
self.x_bounds: Interval = (
x_bounds.clone()
if isinstance(x_bounds, Interval)
else Interval(x_bounds, inclusive[0])
)
"""Interval: The bounds for the x direction."""
"""The bounds for the x direction."""

self.y_bounds = (
self.y_bounds: Interval = (
y_bounds.clone()
if isinstance(y_bounds, Interval)
else Interval(y_bounds, inclusive[1])
)
"""Interval: The bounds for the y direction."""
"""The bounds for the y direction."""

self._bounds = (self.x_bounds, self.y_bounds)

Expand Down
12 changes: 12 additions & 0 deletions src/data_morph/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,16 @@ def generate_parser() -> argparse.ArgumentParser:
frame_group = parser.add_argument_group(
'Animation Configuration', description='Customize aspects of the animation.'
)
frame_group.add_argument(
'--classic',
default=False,
action='store_true',
help=(
'Whether to plot the original visualization, which consists of a scatter plot '
'and the summary statistics. Otherwise, marginal plots will be included in '
'addition to the classic plot.'
),
)
frame_group.add_argument(
'--ease',
default=False,
Expand Down Expand Up @@ -294,6 +304,7 @@ def _morph(
forward_only_animation=args.forward_only,
num_frames=100,
in_notebook=False,
classic=args.classic,
with_median=args.with_median,
)

Expand Down Expand Up @@ -409,6 +420,7 @@ def _serialize(args: argparse.Namespace, target_shapes: Sequence[str]) -> None:
forward_only_animation=args.forward_only,
num_frames=100,
in_notebook=False,
classic=args.classic,
with_median=args.with_median,
)

Expand Down
19 changes: 14 additions & 5 deletions src/data_morph/data/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import TYPE_CHECKING

import matplotlib.pyplot as plt
import numpy as np

from ..bounds.bounding_box import BoundingBox
from ..bounds.interval import Interval
Expand Down Expand Up @@ -55,19 +56,27 @@ def __init__(
self.data: pd.DataFrame = self._validate_data(data).pipe(
self._scale_data, scale
)
"""pandas.DataFrame: DataFrame containing columns x and y."""
"""DataFrame containing columns x and y."""

self.name: str = name
"""str: The name to use for the dataset."""
"""The name to use for the dataset."""

self.data_bounds: BoundingBox = self._derive_data_bounds()
"""BoundingBox: The bounds of the data."""
"""The bounds of the data."""

self.morph_bounds: BoundingBox = self._derive_morphing_bounds()
"""BoundingBox: The limits for the morphing process."""
"""The limits for the morphing process."""

self.plot_bounds: BoundingBox = self._derive_plotting_bounds()
"""BoundingBox: The bounds to use when plotting the morphed data."""
"""The bounds to use when plotting the morphed data."""

self.marginals: tuple[
tuple[np.ndarray, np.ndarray], tuple[np.ndarray, np.ndarray]
] = (
np.histogram(self.data.x, bins=30, range=self.plot_bounds.x_bounds),
np.histogram(self.data.y, bins=30, range=self.plot_bounds.y_bounds),
)
"""The counts per bin and bin boundaries for generating marginal plots."""

def __repr__(self) -> str:
return f'<{self.__class__.__name__} name={self.name} scaled={self._scaled}>'
Expand Down
12 changes: 12 additions & 0 deletions src/data_morph/morpher.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ class DataMorpher:
forward_only_animation : bool, default ``False``
Whether to generate the animation in the forward direction only.
By default, the animation will play forward and then reverse.
classic : bool, default ``False``
Whether to plot the original visualization, which consists of a scatter plot
and the summary statistics. When this is ``False``, marginal plots will be
included in addition to the classic plot.
with_median : bool, default ``False``
Whether to preserve the median in addition to the other summary statistics.
Note that this will be a little slower.
Expand All @@ -77,6 +81,7 @@ def __init__(
num_frames: int = 100,
keep_frames: bool = False,
forward_only_animation: bool = False,
classic: bool = False,
with_median: bool = False,
) -> None:
self._rng = np.random.default_rng(seed)
Expand Down Expand Up @@ -133,6 +138,7 @@ def __init__(

self._ProgressTracker = partial(DataMorphProgress, not self._in_notebook)

self._classic = classic
self._with_median = with_median

def _select_frames(
Expand Down Expand Up @@ -204,6 +210,8 @@ def _record_frames(
self,
data: pd.DataFrame,
bounds: BoundingBox,
marginals: tuple[tuple[np.ndarray, np.ndarray], tuple[np.ndarray, np.ndarray]]
| None,
base_file_name: str,
frame_number: str,
) -> None:
Expand All @@ -216,6 +224,8 @@ def _record_frames(
The DataFrame of the data for morphing.
bounds : BoundingBox
The plotting limits.
marginals : tuple[tuple[np.ndarray, np.ndarray], tuple[np.ndarray, np.ndarray]] | None
The counts per bin and bin boundaries for generating marginal plots.
base_file_name : str
The prefix to the file names for both the PNG and GIF files.
frame_number : str
Expand All @@ -228,6 +238,7 @@ def _record_frames(
decimals=self.decimals,
x_bounds=bounds.x_bounds,
y_bounds=bounds.y_bounds,
marginals=marginals,
with_median=self._with_median,
dpi=150,
)
Expand Down Expand Up @@ -456,6 +467,7 @@ def morph(
self._record_frames,
base_file_name=base_file_name,
bounds=start_shape.plot_bounds,
marginals=None if self._classic else start_shape.marginals,
)

frame_number_format = f'{{:0{len(str(iterations))}d}}'.format
Expand Down
6 changes: 4 additions & 2 deletions src/data_morph/plotting/animation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
import re
from functools import wraps
from pathlib import Path
from typing import TYPE_CHECKING, Callable
from typing import TYPE_CHECKING

from PIL import Image

if TYPE_CHECKING:
from collections.abc import Callable

from ..shapes.bases.shape import Shape


Expand Down Expand Up @@ -115,7 +117,7 @@ def wrapper(step: int | float) -> int | float:
int or float
The eased value at the current step, from 0.0 to 1.0.
"""
if not (isinstance(step, (int, float)) and 0 <= step <= 1):
if not (isinstance(step, int | float) and 0 <= step <= 1):
raise ValueError('Step must be an integer or float, between 0 and 1.')
return easing_function(step)

Expand Down
Loading
Loading