bumplot 0.1.1__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bumplot-0.3.0/.github/workflows/doc.yaml +32 -0
- bumplot-0.3.0/.github/workflows/lint.yaml +30 -0
- {bumplot-0.1.1 → bumplot-0.3.0}/.github/workflows/pypi.yaml +1 -1
- {bumplot-0.1.1 → bumplot-0.3.0}/.github/workflows/tests.yaml +3 -4
- {bumplot-0.1.1 → bumplot-0.3.0}/.github/workflows/type.yaml +11 -5
- {bumplot-0.1.1 → bumplot-0.3.0}/.gitignore +4 -1
- {bumplot-0.1.1 → bumplot-0.3.0}/PKG-INFO +13 -19
- bumplot-0.1.1/docs/README.qmd → bumplot-0.3.0/README.md +6 -25
- bumplot-0.3.0/bumplot/__init__.py +5 -0
- {bumplot-0.1.1 → bumplot-0.3.0}/bumplot/_utils.py +12 -10
- {bumplot-0.1.1 → bumplot-0.3.0}/bumplot/bezier.py +13 -7
- bumplot-0.3.0/bumplot/main.py +124 -0
- bumplot-0.3.0/bumplot/opts.py +190 -0
- {bumplot-0.1.1 → bumplot-0.3.0}/bumplot.egg-info/PKG-INFO +13 -19
- {bumplot-0.1.1 → bumplot-0.3.0}/bumplot.egg-info/SOURCES.txt +8 -6
- bumplot-0.3.0/docs/advanced-usage.md +247 -0
- {bumplot-0.1.1 → bumplot-0.3.0}/docs/contributing.md +4 -2
- bumplot-0.3.0/docs/data/water-africa.csv +7 -0
- bumplot-0.3.0/docs/examples.md +226 -0
- {bumplot-0.1.1 → bumplot-0.3.0}/docs/index.md +19 -6
- bumplot-0.3.0/justfile +16 -0
- bumplot-0.3.0/pyproject.toml +57 -0
- bumplot-0.3.0/tests/test_main.py +195 -0
- bumplot-0.3.0/tests/test_opts.py +108 -0
- bumplot-0.3.0/zensical.toml +58 -0
- bumplot-0.1.1/.github/workflows/doc.yaml +0 -41
- bumplot-0.1.1/.github/workflows/lint.yaml +0 -28
- bumplot-0.1.1/Makefile +0 -17
- bumplot-0.1.1/README.md +0 -70
- bumplot-0.1.1/bumplot/__init__.py +0 -4
- bumplot-0.1.1/bumplot/main.py +0 -104
- bumplot-0.1.1/docs/examples.md +0 -151
- bumplot-0.1.1/mkdocs.yaml +0 -60
- bumplot-0.1.1/overrides/partials/footer.html +0 -7
- bumplot-0.1.1/pyproject.toml +0 -60
- bumplot-0.1.1/tests/test_main.py +0 -86
- {bumplot-0.1.1 → bumplot-0.3.0}/.coverage +0 -0
- {bumplot-0.1.1 → bumplot-0.3.0}/.pre-commit-config.yaml +0 -0
- {bumplot-0.1.1 → bumplot-0.3.0}/LICENSE +0 -0
- {bumplot-0.1.1 → bumplot-0.3.0}/bumplot.egg-info/dependency_links.txt +0 -0
- {bumplot-0.1.1 → bumplot-0.3.0}/bumplot.egg-info/requires.txt +0 -0
- {bumplot-0.1.1 → bumplot-0.3.0}/bumplot.egg-info/top_level.txt +0 -0
- {bumplot-0.1.1 → bumplot-0.3.0}/coverage-badge.svg +0 -0
- {bumplot-0.1.1 → bumplot-0.3.0}/docs/.gitignore +0 -0
- {bumplot-0.1.1 → bumplot-0.3.0}/docs/reference/bezier.md +0 -0
- {bumplot-0.1.1 → bumplot-0.3.0}/docs/reference/bumplot.md +0 -0
- {bumplot-0.1.1 → bumplot-0.3.0}/docs/stylesheets/style.css +0 -0
- /bumplot-0.1.1/README_files/figure-commonmark/cell-3-output-1.png → /bumplot-0.3.0/example.png +0 -0
- {bumplot-0.1.1 → bumplot-0.3.0}/setup.cfg +0 -0
- {bumplot-0.1.1 → bumplot-0.3.0}/tests/__init__.py +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
name: Documentation
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches:
|
|
5
|
+
- main
|
|
6
|
+
permissions:
|
|
7
|
+
contents: write
|
|
8
|
+
jobs:
|
|
9
|
+
deploy:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v6
|
|
13
|
+
- uses: actions/setup-python@v6
|
|
14
|
+
with:
|
|
15
|
+
python-version: 3.x
|
|
16
|
+
|
|
17
|
+
- name: Install uv
|
|
18
|
+
uses: astral-sh/setup-uv@v5
|
|
19
|
+
with:
|
|
20
|
+
enable-cache: true
|
|
21
|
+
|
|
22
|
+
- name: Install the project
|
|
23
|
+
run: uv sync --all-groups
|
|
24
|
+
|
|
25
|
+
- run: uv run zensical build --clean
|
|
26
|
+
|
|
27
|
+
- name: Deploy to GitHub Pages
|
|
28
|
+
uses: peaceiris/actions-gh-pages@v4
|
|
29
|
+
with:
|
|
30
|
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
31
|
+
publish_branch: gh-pages
|
|
32
|
+
publish_dir: ./site
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Check code formatting
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
check-formatting:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout repository
|
|
15
|
+
uses: actions/checkout@v6
|
|
16
|
+
with:
|
|
17
|
+
persist-credentials: false
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v5
|
|
21
|
+
with:
|
|
22
|
+
enable-cache: true
|
|
23
|
+
|
|
24
|
+
- name: Install the project
|
|
25
|
+
run: uv sync --all-groups
|
|
26
|
+
|
|
27
|
+
- name: Run Ruff formatter and linter
|
|
28
|
+
run: |
|
|
29
|
+
uv run ruff format --check .
|
|
30
|
+
uv run ruff check
|
|
@@ -10,13 +10,12 @@ jobs:
|
|
|
10
10
|
strategy:
|
|
11
11
|
matrix:
|
|
12
12
|
python-version:
|
|
13
|
-
- "3.
|
|
14
|
-
- "3.
|
|
15
|
-
- "3.13"
|
|
13
|
+
- "3.10"
|
|
14
|
+
- "3.15"
|
|
16
15
|
env:
|
|
17
16
|
UV_PYTHON: ${{ matrix.python-version }}
|
|
18
17
|
steps:
|
|
19
|
-
- uses: actions/checkout@
|
|
18
|
+
- uses: actions/checkout@v6
|
|
20
19
|
|
|
21
20
|
- name: Install uv
|
|
22
21
|
uses: astral-sh/setup-uv@v5
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
name: Check
|
|
1
|
+
name: Check static typing
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
4
6
|
pull_request:
|
|
5
|
-
branches:
|
|
6
|
-
- main
|
|
7
|
+
branches: [main]
|
|
7
8
|
|
|
8
9
|
jobs:
|
|
9
10
|
check-formatting:
|
|
@@ -11,7 +12,9 @@ jobs:
|
|
|
11
12
|
|
|
12
13
|
steps:
|
|
13
14
|
- name: Checkout repository
|
|
14
|
-
uses: actions/checkout@
|
|
15
|
+
uses: actions/checkout@v6
|
|
16
|
+
with:
|
|
17
|
+
persist-credentials: false
|
|
15
18
|
|
|
16
19
|
- name: Install uv
|
|
17
20
|
uses: astral-sh/setup-uv@v5
|
|
@@ -21,5 +24,8 @@ jobs:
|
|
|
21
24
|
- name: Install the project
|
|
22
25
|
run: uv sync --all-groups
|
|
23
26
|
|
|
24
|
-
- name:
|
|
27
|
+
- name: ty
|
|
25
28
|
run: uv run ty check
|
|
29
|
+
|
|
30
|
+
- name: pyrefly
|
|
31
|
+
run: uv run pyrefly check
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bumplot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Easily create and customize bump charts in Python
|
|
5
5
|
Author-email: Joseph Barbier <joseph.barbierdarnal@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
|
+
Project-URL: Documentation, https://y-sunflower.github.io/bumplot/
|
|
7
8
|
Project-URL: Homepage, https://y-sunflower.github.io/bumplot/
|
|
8
9
|
Project-URL: Issues, https://github.com/y-sunflower/bumplot/issues
|
|
9
|
-
Project-URL: Documentation, https://y-sunflower.github.io/bumplot/
|
|
10
10
|
Project-URL: Repository, https://github.com/y-sunflower/bumplot
|
|
11
|
-
Keywords: bump,bump
|
|
12
|
-
Classifier:
|
|
11
|
+
Keywords: bezier,bump,bump charts,bump plot,curve,data,matplotlib,visualization
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
|
-
Classifier:
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
15
|
Requires-Python: >=3.10
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
@@ -21,20 +21,13 @@ Dynamic: license-file
|
|
|
21
21
|
|
|
22
22
|
# `bumplot`: easily create and customize bump charts in Python
|
|
23
23
|
|
|
24
|
-
<!-- Automatically generated, uses README.qmd to modify README.md -->
|
|
25
|
-
|
|
26
24
|
<img src="https://github.com/JosephBARBIERDARNAL/static/blob/main/python-libs/bumplot/image.png?raw=true" alt="bumplot logo" align="right" width="180px"/>
|
|
27
25
|
|
|
28
|
-
`bumplot` is a small Python package made to facilitate the creation of
|
|
29
|
-
**bump charts** using matplotlib and Bézier curves. It has high
|
|
30
|
-
customization capabilities too!
|
|
26
|
+
`bumplot` is a small Python package made to facilitate the creation of **bump charts** using matplotlib and Bézier curves. It has high customization capabilities too!
|
|
31
27
|
|
|
32
|
-
Bump charts are useful when the focus is on comparing **relative
|
|
33
|
-
rankings**—who is ahead of whom—rather than the exact magnitude of the
|
|
34
|
-
differences.
|
|
28
|
+
Bump charts are useful when the focus is on comparing **relative rankings**—who is ahead of whom—rather than the exact magnitude of the differences.
|
|
35
29
|
|
|
36
|
-
Check out the [online
|
|
37
|
-
documentation](https://y-sunflower.github.io/bumplot/)
|
|
30
|
+
Check out the [online documentation](https://y-sunflower.github.io/bumplot/)
|
|
38
31
|
|
|
39
32
|
<br> <br>
|
|
40
33
|
|
|
@@ -70,7 +63,7 @@ ax.spines[["top", "right", "left", "bottom"]].set_visible(False)
|
|
|
70
63
|
ax.grid(alpha=0.4)
|
|
71
64
|
```
|
|
72
65
|
|
|
73
|
-

|
|
74
67
|
|
|
75
68
|
[See more examples](https://y-sunflower.github.io/bumplot/examples/)
|
|
76
69
|
|
|
@@ -78,14 +71,15 @@ ax.grid(alpha=0.4)
|
|
|
78
71
|
|
|
79
72
|
## Installation
|
|
80
73
|
|
|
81
|
-
|
|
74
|
+
```sh
|
|
75
|
+
pip install bumplot
|
|
76
|
+
```
|
|
82
77
|
|
|
83
78
|
<br><br>
|
|
84
79
|
|
|
85
80
|
## Features
|
|
86
81
|
|
|
87
|
-
- Automatic ranking of your data (with support for both `pandas` and
|
|
88
|
-
`polars`)
|
|
82
|
+
- Automatic ranking of your data (with support for both `pandas` and `polars`)
|
|
89
83
|
- Accept categorical data for x-axis
|
|
90
84
|
- Infinite customization capabilities
|
|
91
85
|
- Easy control of the curvature intensity
|
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
---
|
|
2
|
-
format: gfm
|
|
3
|
-
from: markdown-smart
|
|
4
|
-
default-image-extension: ""
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
<!-- Automatically generated, uses README.qmd to modify README.md -->
|
|
8
|
-
|
|
9
|
-
```{python}
|
|
10
|
-
# | include: false
|
|
11
|
-
# | echo: false
|
|
12
|
-
|
|
13
|
-
import matplotlib.pyplot as plt
|
|
14
|
-
|
|
15
|
-
plt.rcParams["figure.dpi"] = 200
|
|
16
|
-
```
|
|
17
|
-
|
|
18
1
|
# `bumplot`: easily create and customize bump charts in Python
|
|
19
2
|
|
|
20
3
|
<img src="https://github.com/JosephBARBIERDARNAL/static/blob/main/python-libs/bumplot/image.png?raw=true" alt="bumplot logo" align="right" width="180px"/>
|
|
@@ -25,14 +8,11 @@ Bump charts are useful when the focus is on comparing **relative rankings**—wh
|
|
|
25
8
|
|
|
26
9
|
Check out the [online documentation](https://y-sunflower.github.io/bumplot/)
|
|
27
10
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<br>
|
|
31
|
-
<br>
|
|
11
|
+
<br> <br>
|
|
32
12
|
|
|
33
13
|
## Get started
|
|
34
14
|
|
|
35
|
-
```
|
|
15
|
+
```python
|
|
36
16
|
import matplotlib.pyplot as plt
|
|
37
17
|
import pandas as pd
|
|
38
18
|
|
|
@@ -62,14 +42,15 @@ ax.spines[["top", "right", "left", "bottom"]].set_visible(False)
|
|
|
62
42
|
ax.grid(alpha=0.4)
|
|
63
43
|
```
|
|
64
44
|
|
|
45
|
+

|
|
46
|
+
|
|
65
47
|
[See more examples](https://y-sunflower.github.io/bumplot/examples/)
|
|
66
48
|
|
|
67
|
-
<br>
|
|
68
|
-
<br>
|
|
49
|
+
<br> <br>
|
|
69
50
|
|
|
70
51
|
## Installation
|
|
71
52
|
|
|
72
|
-
```
|
|
53
|
+
```sh
|
|
73
54
|
pip install bumplot
|
|
74
55
|
```
|
|
75
56
|
|
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
from typing import Any
|
|
2
2
|
|
|
3
3
|
import narwhals as nw
|
|
4
|
+
from narwhals import DataFrame
|
|
4
5
|
from narwhals.typing import IntoDataFrame
|
|
5
6
|
|
|
6
7
|
|
|
7
|
-
def
|
|
8
|
-
"""
|
|
9
|
-
Get the first n colors from matplotlib rcParams.
|
|
10
|
-
"""
|
|
11
|
-
colors: list[str] = plt.rcParams["axes.prop_cycle"].by_key()["color"][:n]
|
|
12
|
-
return colors
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def _ranked_df(df: IntoDataFrame, x: str, y_columns: list[str]):
|
|
8
|
+
def _ranked_df(df: IntoDataFrame, x: str, y_columns: list[str]) -> DataFrame[Any]:
|
|
16
9
|
"""
|
|
17
10
|
Convert a dataframe to a ranked version of it.
|
|
18
11
|
"""
|
|
@@ -26,3 +19,12 @@ def _ranked_df(df: IntoDataFrame, x: str, y_columns: list[str]):
|
|
|
26
19
|
)
|
|
27
20
|
|
|
28
21
|
return df_native_ranked
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _to_ordinal(n: int) -> str:
|
|
25
|
+
"""Convert number to ordinal string (1 -> '1st', 2 -> '2nd', etc.)"""
|
|
26
|
+
if 11 <= n % 100 <= 13:
|
|
27
|
+
suffix = "th"
|
|
28
|
+
else:
|
|
29
|
+
suffix = {1: "st", 2: "nd", 3: "rd"}.get(n % 10, "th")
|
|
30
|
+
return f"{n}{suffix}"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from matplotlib.path import Path
|
|
2
1
|
import numpy as np
|
|
2
|
+
from matplotlib.path import Path
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
def bezier_curve(
|
|
@@ -25,18 +25,24 @@ def bezier_curve(
|
|
|
25
25
|
vertices: List of (x, y) vertices including control points for the Bézier segments.
|
|
26
26
|
codes: Corresponding matplotlib Path codes for constructing the curve.
|
|
27
27
|
"""
|
|
28
|
-
vertices
|
|
29
|
-
codes
|
|
28
|
+
vertices = []
|
|
29
|
+
codes = []
|
|
30
30
|
|
|
31
|
-
vertices.append((x[0], y[0]))
|
|
32
|
-
codes.append(Path.MOVETO)
|
|
31
|
+
vertices.append((float(x[0]), float(y[0])))
|
|
32
|
+
codes.append(int(Path.MOVETO))
|
|
33
33
|
|
|
34
34
|
for i in range(1, len(x)):
|
|
35
35
|
x0, y0 = x[i - 1], y[i - 1]
|
|
36
36
|
x1, y1 = x[i], y[i]
|
|
37
37
|
dx = (x1 - x0) * force
|
|
38
38
|
|
|
39
|
-
vertices.extend(
|
|
40
|
-
|
|
39
|
+
vertices.extend(
|
|
40
|
+
[
|
|
41
|
+
(float(x0 + dx), float(y0)),
|
|
42
|
+
(float(x1 - dx), float(y1)),
|
|
43
|
+
(float(x1), float(y1)),
|
|
44
|
+
]
|
|
45
|
+
)
|
|
46
|
+
codes.extend([int(Path.CURVE4)] * 3)
|
|
41
47
|
|
|
42
48
|
return vertices, codes
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from itertools import cycle
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import numpy as np
|
|
7
|
+
from matplotlib import patches
|
|
8
|
+
from matplotlib.axes import Axes
|
|
9
|
+
from matplotlib.cbook import normalize_kwargs
|
|
10
|
+
from matplotlib.collections import PathCollection
|
|
11
|
+
from matplotlib.patches import PathPatch
|
|
12
|
+
from matplotlib.path import Path
|
|
13
|
+
from narwhals.typing import IntoDataFrame
|
|
14
|
+
|
|
15
|
+
from ._utils import _ranked_df, _to_ordinal
|
|
16
|
+
from .bezier import bezier_curve
|
|
17
|
+
from .opts import BumpOpts, _get_plot_kwargs, _get_scatter_kwargs
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def bumplot(
|
|
21
|
+
x: str,
|
|
22
|
+
y_columns: Iterable[str | tuple[str, BumpOpts]],
|
|
23
|
+
data: IntoDataFrame,
|
|
24
|
+
curve_force: float = 1,
|
|
25
|
+
invert_y_axis: bool = True,
|
|
26
|
+
colors: Iterable[str] | None = None,
|
|
27
|
+
plot_kwargs: dict[str, Any] | None = None,
|
|
28
|
+
scatter_kwargs: dict[str, Any] | None = None,
|
|
29
|
+
ax: Axes | None = None,
|
|
30
|
+
ordinal_labels: bool = False,
|
|
31
|
+
) -> tuple[Axes, dict[str, tuple[PathPatch, PathCollection]]]:
|
|
32
|
+
"""
|
|
33
|
+
Creates bump plot, or bump chart, from multiple numerical
|
|
34
|
+
columns.
|
|
35
|
+
|
|
36
|
+
It requires the data to be in wide format (e.g., one column
|
|
37
|
+
per line you want to plot).
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
x: colname of the x-axis variable
|
|
41
|
+
y_columns: colnames of the y-axis variables and their plotting options.
|
|
42
|
+
data: A dataframe
|
|
43
|
+
curve_force: Smoothing factor controlling curve tightness. Higher
|
|
44
|
+
values increase curvature by moving control points further away
|
|
45
|
+
from the anchors.
|
|
46
|
+
invert_y_axis: Whether to invert y axis
|
|
47
|
+
colors: An optional list of colors
|
|
48
|
+
plot_kwargs: Additional arguments passed to `patches.PathPatch()`
|
|
49
|
+
scatter_kwargs: Additional arguments passed to `scatter()`
|
|
50
|
+
ax: The matplotlib Axes used. Default to `plt.gca()`
|
|
51
|
+
ordinal_labels: If True, converts y-axis labels to ordinal numbers (1st, 2nd, 3rd, etc.)
|
|
52
|
+
Returns:
|
|
53
|
+
The matplotlib Axes with the bump plot
|
|
54
|
+
"""
|
|
55
|
+
plot_kwargs = {} if plot_kwargs is None else plot_kwargs
|
|
56
|
+
scatter_kwargs = {} if scatter_kwargs is None else scatter_kwargs
|
|
57
|
+
_plot_ax: Axes = ax if ax is not None else plt.gca()
|
|
58
|
+
colors_iterable = (
|
|
59
|
+
colors
|
|
60
|
+
if colors is not None
|
|
61
|
+
else plt.rcParams["axes.prop_cycle"].by_key()["color"]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
y_bumps: list[tuple[str, BumpOpts]] = [
|
|
65
|
+
(y, BumpOpts()) if isinstance(y, str) else y for y in y_columns
|
|
66
|
+
]
|
|
67
|
+
ranked = _ranked_df(data, x=x, y_columns=[y for y, _ in y_bumps])
|
|
68
|
+
x_values_raw: np.ndarray = np.ravel(ranked.select(x).to_numpy())
|
|
69
|
+
|
|
70
|
+
if np.issubdtype(x_values_raw.dtype, np.number):
|
|
71
|
+
x_values = x_values_raw
|
|
72
|
+
x_labels = x_values_raw
|
|
73
|
+
else:
|
|
74
|
+
uniques = list(dict.fromkeys(x_values_raw)) # preserves order
|
|
75
|
+
mapping = {val: i for i, val in enumerate(uniques)}
|
|
76
|
+
x_values = np.array([mapping[val] for val in x_values_raw], dtype=int)
|
|
77
|
+
x_labels = x_values_raw
|
|
78
|
+
|
|
79
|
+
artists = {}
|
|
80
|
+
for (name, bump_opts), color in zip(y_bumps, cycle(colors_iterable)):
|
|
81
|
+
y_values: np.ndarray = np.ravel(ranked.select(name).to_numpy())
|
|
82
|
+
vertices, codes = bezier_curve(
|
|
83
|
+
x=x_values,
|
|
84
|
+
y=y_values,
|
|
85
|
+
force=curve_force,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
path: Path = Path(vertices=vertices, codes=codes)
|
|
89
|
+
path_kwargs: dict[str, Any] = {"edgecolor": color}
|
|
90
|
+
path_kwargs.update(normalize_kwargs(plot_kwargs, PathPatch))
|
|
91
|
+
path_kwargs.update(_get_plot_kwargs(bump_opts))
|
|
92
|
+
patch: PathPatch = patches.PathPatch(path=path, facecolor="none", **path_kwargs)
|
|
93
|
+
_plot_ax.add_patch(patch)
|
|
94
|
+
|
|
95
|
+
artist_scatter_kwargs: dict[str, Any] = {"facecolor": color}
|
|
96
|
+
artist_scatter_kwargs.update(normalize_kwargs(scatter_kwargs, PathCollection))
|
|
97
|
+
artist_scatter_kwargs.update(_get_scatter_kwargs(bump_opts))
|
|
98
|
+
scatter = _plot_ax.scatter(
|
|
99
|
+
x_values,
|
|
100
|
+
y_values,
|
|
101
|
+
label=name,
|
|
102
|
+
**artist_scatter_kwargs,
|
|
103
|
+
)
|
|
104
|
+
artists[name] = (patch, scatter)
|
|
105
|
+
|
|
106
|
+
ticks: list[int] = list(range(1, len(y_bumps) + 1))
|
|
107
|
+
|
|
108
|
+
if invert_y_axis:
|
|
109
|
+
_plot_ax.invert_yaxis()
|
|
110
|
+
else:
|
|
111
|
+
ticks: list[int] = list(reversed(ticks))
|
|
112
|
+
|
|
113
|
+
_plot_ax.set_yticks(ticks=ticks)
|
|
114
|
+
|
|
115
|
+
labels = (
|
|
116
|
+
[_to_ordinal(tick) for tick in ticks]
|
|
117
|
+
if ordinal_labels
|
|
118
|
+
else [str(tick) for tick in ticks]
|
|
119
|
+
)
|
|
120
|
+
_plot_ax.set_yticklabels(labels)
|
|
121
|
+
|
|
122
|
+
_plot_ax.set_xticks(ticks=np.unique(x_values), labels=np.unique(x_labels))
|
|
123
|
+
|
|
124
|
+
return _plot_ax, artists
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from typing import Any, TypedDict, cast
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BumpOpts(TypedDict, total=False):
|
|
6
|
+
"""
|
|
7
|
+
Typed dictionary describing high-level styling options for a bump plot.
|
|
8
|
+
|
|
9
|
+
This type represents a user-facing, backend-agnostic set of visual options
|
|
10
|
+
that can be applied to both line (plot) and marker (scatter) artists.
|
|
11
|
+
|
|
12
|
+
All fields are optional. Keys are validated at runtime when constructed via
|
|
13
|
+
`opts()` to ensure only supported options are provided.
|
|
14
|
+
|
|
15
|
+
The options are later translated into Matplotlib-specific keyword arguments
|
|
16
|
+
via `_get_plot_kwargs()` and `_get_scatter_kwargs()`.
|
|
17
|
+
|
|
18
|
+
Notes
|
|
19
|
+
-----
|
|
20
|
+
- Keys prefixed with `line_` apply to line artists.
|
|
21
|
+
- Keys prefixed with `marker_` apply to scatter/marker artists.
|
|
22
|
+
- Some options (e.g. `clip_on` and `zorder`) apply to both.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
line_alpha: float
|
|
26
|
+
line_color: str
|
|
27
|
+
line_style: str
|
|
28
|
+
line_width: float
|
|
29
|
+
|
|
30
|
+
marker: str
|
|
31
|
+
marker_size: float
|
|
32
|
+
marker_alpha: float
|
|
33
|
+
marker_facecolor: str
|
|
34
|
+
marker_edgecolor: str
|
|
35
|
+
marker_edgewidth: float
|
|
36
|
+
|
|
37
|
+
clip_on: bool
|
|
38
|
+
zorder: int
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
PLOT_MAPPINGS: dict[str, str] = {
|
|
42
|
+
"line_alpha": "alpha",
|
|
43
|
+
"line_color": "edgecolor",
|
|
44
|
+
"line_style": "linestyle",
|
|
45
|
+
"line_width": "linewidth",
|
|
46
|
+
"clip_on": "clip_on",
|
|
47
|
+
"zorder": "zorder",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
SCATTER_MAPPINGS: dict[str, str] = {
|
|
51
|
+
"marker": "marker",
|
|
52
|
+
"marker_alpha": "alpha",
|
|
53
|
+
"marker_edgecolor": "edgecolor",
|
|
54
|
+
"marker_edgewidth": "linewidth",
|
|
55
|
+
"marker_facecolor": "facecolor",
|
|
56
|
+
"marker_size": "s",
|
|
57
|
+
"clip_on": "clip_on",
|
|
58
|
+
"zorder": "zorder",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# TODO: For >=Python 3.11 (https://docs.python.org/3/library/typing.html#typing.Unpack)
|
|
63
|
+
def opts(**kwargs: Any) -> BumpOpts:
|
|
64
|
+
"""
|
|
65
|
+
Construct a validated `BumpOpts` mapping from keyword arguments.
|
|
66
|
+
|
|
67
|
+
This function enforces that all provided keyword arguments correspond to
|
|
68
|
+
supported `BumpOpts` fields. Any unexpected keys result in a `TypeError`.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
**kwargs: Unpack[BumpOpts]
|
|
73
|
+
Keyword arguments corresponding to fields defined on `BumpOpts`.
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
BumpOpts
|
|
78
|
+
A typed dictionary containing the validated bump plot options.
|
|
79
|
+
|
|
80
|
+
Raises
|
|
81
|
+
------
|
|
82
|
+
TypeError
|
|
83
|
+
If one or more unsupported keyword arguments are provided.
|
|
84
|
+
|
|
85
|
+
Notes
|
|
86
|
+
-----
|
|
87
|
+
This function exists to provide runtime validation for `BumpOpts`,
|
|
88
|
+
which is otherwise only enforced statically by type checkers.
|
|
89
|
+
"""
|
|
90
|
+
unsupported_keys = kwargs.keys() - BumpOpts.__annotations__
|
|
91
|
+
if unsupported_keys:
|
|
92
|
+
unsupported_repr = ", ".join(map(repr, sorted(unsupported_keys)))
|
|
93
|
+
msg = f"BumpOpts got unexpected keyword argument(s): {unsupported_repr}"
|
|
94
|
+
raise TypeError(msg)
|
|
95
|
+
|
|
96
|
+
return BumpOpts(**kwargs)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def opts_from_color(color, /, **kwargs: Any) -> BumpOpts:
|
|
100
|
+
"""
|
|
101
|
+
Construct a `BumpOpts` mapping using a single color applied consistently
|
|
102
|
+
across line and marker components.
|
|
103
|
+
|
|
104
|
+
The provided color is applied to:
|
|
105
|
+
- `line_color`
|
|
106
|
+
- `marker_facecolor`
|
|
107
|
+
- `marker_edgecolor`
|
|
108
|
+
|
|
109
|
+
Any explicitly provided keyword arguments take precedence over the color-based
|
|
110
|
+
defaults.
|
|
111
|
+
|
|
112
|
+
Parameters
|
|
113
|
+
----------
|
|
114
|
+
color
|
|
115
|
+
The color to apply to line and marker components.
|
|
116
|
+
**kwargs
|
|
117
|
+
Additional or overriding bump plot options (passed to `opts`).
|
|
118
|
+
|
|
119
|
+
Returns
|
|
120
|
+
-------
|
|
121
|
+
BumpOpts
|
|
122
|
+
A validated bump plot options mapping.
|
|
123
|
+
|
|
124
|
+
See Also
|
|
125
|
+
--------
|
|
126
|
+
opts: Performs keyword validation and construction.
|
|
127
|
+
"""
|
|
128
|
+
overrides = ["line_color", "marker_facecolor", "marker_edgecolor"]
|
|
129
|
+
new_kwargs = {k: color for k in overrides} | kwargs
|
|
130
|
+
return opts(**new_kwargs)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _get_plot_kwargs(kwargs: BumpOpts, /) -> dict[str, Any]:
|
|
134
|
+
"""
|
|
135
|
+
Extract Matplotlib `plot` keyword arguments from a `BumpOpts` mapping.
|
|
136
|
+
|
|
137
|
+
This function translates bump-plot-specific option names into the equivalent
|
|
138
|
+
Matplotlib `Axes.plot` keyword arguments using `bumplot.opts.PLOT_MAPPINGS`.
|
|
139
|
+
|
|
140
|
+
Parameters
|
|
141
|
+
----------
|
|
142
|
+
kwargs: Unpack[BumpOpts]
|
|
143
|
+
A `BumpOpts` mapping containing zero or more plot-related options.
|
|
144
|
+
|
|
145
|
+
Returns
|
|
146
|
+
-------
|
|
147
|
+
dict[str, Any]
|
|
148
|
+
A dictionary of Matplotlib-compatible keyword arguments suitable for
|
|
149
|
+
`Axes.patches.PathPatch`.
|
|
150
|
+
|
|
151
|
+
Notes
|
|
152
|
+
-----
|
|
153
|
+
- Only keys present in both `kwargs` and `PLOT_MAPPINGS` are included.
|
|
154
|
+
"""
|
|
155
|
+
kwargs_mapping = cast(Mapping[str, Any], kwargs)
|
|
156
|
+
plot_kwargs: dict[str, Any] = {}
|
|
157
|
+
for bump_arg, mpl_arg in PLOT_MAPPINGS.items():
|
|
158
|
+
if bump_arg in kwargs_mapping:
|
|
159
|
+
plot_kwargs[mpl_arg] = kwargs_mapping[bump_arg]
|
|
160
|
+
return plot_kwargs
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _get_scatter_kwargs(kwargs: BumpOpts, /) -> dict[str, Any]:
|
|
164
|
+
"""
|
|
165
|
+
Extract Matplotlib `scatter` keyword arguments from a `BumpOpts` mapping.
|
|
166
|
+
|
|
167
|
+
This function translates bump-plot-specific option names into the equivalent
|
|
168
|
+
Matplotlib `Axes.scatter` keyword arguments using `bumplot.opts.SCATTER_MAPPINGS`.
|
|
169
|
+
|
|
170
|
+
Parameters
|
|
171
|
+
----------
|
|
172
|
+
kwargs: Unpack[BumpOpts]
|
|
173
|
+
A `BumpOpts` mapping containing zero or more plot-related options.
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
dict[str, Any]
|
|
178
|
+
A dictionary of Matplotlib-compatible keyword arguments suitable for
|
|
179
|
+
`Axes.scatter`.
|
|
180
|
+
|
|
181
|
+
Notes
|
|
182
|
+
-----
|
|
183
|
+
- Only keys present in both `kwargs` and `SCATTER_MAPPINGS` are included.
|
|
184
|
+
"""
|
|
185
|
+
kwargs_mapping = cast(Mapping[str, Any], kwargs)
|
|
186
|
+
scatter_kwargs: dict[str, Any] = {}
|
|
187
|
+
for bump_arg, mpl_arg in SCATTER_MAPPINGS.items():
|
|
188
|
+
if bump_arg in kwargs_mapping:
|
|
189
|
+
scatter_kwargs[mpl_arg] = kwargs_mapping[bump_arg]
|
|
190
|
+
return scatter_kwargs
|