Metadata-Version: 2.4
Name: mpl_wrap
Version: 0.2.1
Summary: Matplotlib helper library for plotting wrapped, angular, or periodic data.
Author: Scott Shambaugh
Author-email: Scott Shambaugh <scottshambaugh@users.noreply.github.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Framework :: Matplotlib
Classifier: Operating System :: OS Independent
Classifier: Topic :: Scientific/Engineering :: Visualization
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Dist: matplotlib>=3.10.0
Requires-Dist: numpy>=1.26
Requires-Python: >=3.10, <4
Project-URL: Repository, https://github.com/scottshambaugh/mpl_wrap
Project-URL: Changelog, https://github.com/scottshambaugh/mpl_wrap/blob/main/CHANGELOG.md
Description-Content-Type: text/markdown

# mpl_wrap

[![PyPI](https://img.shields.io/pypi/v/mpl_wrap)](https://pypi.org/project/mpl-wrap/)
[![Python versions](https://img.shields.io/pypi/pyversions/mpl_wrap)](https://pypi.org/project/mpl-wrap/)
[![Builds](https://github.com/scottshambaugh/mpl_wrap/actions/workflows/builds.yml/badge.svg?branch=main)](https://github.com/scottshambaugh/mpl_wrap/actions/workflows/builds.yml)
[![Tests](https://github.com/scottshambaugh/mpl_wrap/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/scottshambaugh/mpl_wrap/actions/workflows/tests.yml)

Matplotlib helper functions for plotting **wrapped, angular, or periodic data**: angles,
phases, times of day, longitudes, and anything else that repeats or rotates.

Manually plotting this data can be tricky. Using a modulus such as `y % 360` is simple,
but introduces a few problems:
* Line jumps at the crossing points, and lines that stop short of the wrap boundaries at the crossing points
* Aliasing when data spans multiple crossings, obscuring the real underlying behavior
* Completely broken rendering for fill_between

`mpl_wrap` solves these issues, and provides simple functions to make plotting wrapped
data easy.

<p align="center">
  <img src="https://raw.githubusercontent.com/scottshambaugh/mpl_wrap/main/docs/wrapy_demo.png" alt="Unwrapped vs modulus vs mpl_wrap" width="600">
</p>

## Installation

```
pip install mpl_wrap
```

Or install from source:

```
git clone https://github.com/scottshambaugh/mpl_wrap.git
cd mpl_wrap
uv sync --group dev
```

## Basic Usage

```python
import numpy as np
import matplotlib.pyplot as plt
from mpl_wrap import set_wrap, plot_wrapped, fill_between_wrapped, errorbar_wrapped

t = np.linspace(0, 10, 500)
angle = 80.0 * t  # degrees
width = 5.0 + 4.0 * t  # degrees

fig, ax = plt.subplots()
set_wrap(ax, wrapy=(0, 360))  # helpers on ax now wrap y into (0, 360)
fill_between_wrapped(ax, t, angle - width, angle + width, alpha=0.3, label='uncertainty')
plot_wrapped(ax, t, angle, label='angle')
ax.set(xlabel="time (s)", ylabel="angle (deg)")
ax.legend()
```

<p align="center">
  <img src="https://raw.githubusercontent.com/scottshambaugh/mpl_wrap/main/docs/basic_usage.png" alt="Basic usage: wrapped angle with uncertainty band and error bars" width="600">
</p>

The helpers mirror their matplotlib counterparts, taking the target `Axes` as
the first argument plus optional `wrapx` / `wrapy` `(min, max)` windows:

| mpl_wrap                                 | mirrors           |
| ---------------------------------------- | ----------------- |
| `plot_wrapped(ax, x, y, ...)`            | `ax.plot`         |
| `scatter_wrapped(ax, x, y, ...)`         | `ax.scatter`      |
| `hlines_wrapped(ax, y, xmin, xmax)`      | `ax.hlines`       |
| `vlines_wrapped(ax, x, ymin, ymax)`      | `ax.vlines`       |
| `axhspan_wrapped(ax, ymin, ymax)`        | `ax.axhspan`      |
| `axvspan_wrapped(ax, xmin, xmax)`        | `ax.axvspan`      |
| `fill_between_wrapped(ax, x, y1, y2)`    | `ax.fill_between` |
| `fill_betweenx_wrapped(ax, y, x1, x2)`   | `ax.fill_betweenx`|
| `step_wrapped(ax, x, y, where=...)`      | `ax.step`         |
| `stairs_wrapped(ax, values, edges)`      | `ax.stairs`       |
| `errorbar_wrapped(ax, x, y, yerr, xerr)` | `ax.errorbar`     |

Each returns the same artist type as the method it mirrors, in the same `Axes`
container. The two span helpers return a *list* of `Rectangle`, since a band
across the seam is two rectangles.

Passing `wrapx=False` / `wrapy=False` disables wrapping for a single call (or
clears the stored window when passed to `set_wrap`), and `wrapx=True` /
`wrapy=True` requires the stored window.
`set_wrap` also sets the axis limits to the window by default, and ticks the
window evenly so that ticks land exactly on the window edges, at about the
automatic tick density. Opt out with `set_lims=False` / `edge_ticks=False`,
or pass `seam_lines=True` to mark the window edges with lines.

You must pass the original unwrapped data for these to work.
If your data is already wrapped, [`np.unwrap`](https://numpy.org/doc/stable/reference/generated/numpy.unwrap.html) may be able to recover that if it's sampled at a high enough rate.

### API

The helpers are free functions, and are also available as methods on an
`AxesWrap` axes. These three are equivalent:

```python
from mpl_wrap import set_wrap, plot_wrapped, wrap_axes

# 1. Free functions, on any existing axes
fig, ax = plt.subplots()
set_wrap(ax, wrapy=(0, 360))
plot_wrapped(ax, t, angle)

# 2. Methods on an AxesWrap axes, created with the "wrap" projection
fig, ax = plt.subplots(subplot_kw={"projection": "wrap"})
ax.set_wrap(wrapy=(0, 360))
ax.plot_wrapped(t, angle)

# 3. Methods on an existing axes, upgraded in place from Axes to AxesWrap
fig, ax = plt.subplots()
wrap_axes(ax, wrapy=(0, 360))
ax.plot_wrapped(t, angle)
```

The data processing is also exposed on its own: `wrap_line` and `wrap_points`
take data plus windows and return the wrapped arrays without plotting anything
(also available as `AxesWrap` methods).

### Wrapping x, y, or both

Both axes can be wrapped independently or together:

<p align="center">
  <img src="https://raw.githubusercontent.com/scottshambaugh/mpl_wrap/main/docs/circle_demo.png" alt="Circle wrapped in x, y, and both" width="500">
</p>

### Datetime data

Datetime data and windows work on either axis. Here a five-day series is wrapped
to show a time-of-day view:

```python
set_wrap(ax, wrapx=(t0, t0 + np.timedelta64(1, "D")))
plot_wrapped(ax, times, signal)
```

<p align="center">
  <img src="https://raw.githubusercontent.com/scottshambaugh/mpl_wrap/main/docs/datetime_demo.png" alt="Datetime wrapping" width="600">
</p>

### Radians

Windows that are multiples of π/2 such as `(0, 2 * np.pi)` or `(-np.pi, np.pi)` are automatically detected and labeled with ticks that are fractions of π.

<p align="center">
  <img src="https://raw.githubusercontent.com/scottshambaugh/mpl_wrap/main/docs/pi_demo.png" alt="pi demo" width="600">
</p>
