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.
Files changed (50) hide show
  1. bumplot-0.3.0/.github/workflows/doc.yaml +32 -0
  2. bumplot-0.3.0/.github/workflows/lint.yaml +30 -0
  3. {bumplot-0.1.1 → bumplot-0.3.0}/.github/workflows/pypi.yaml +1 -1
  4. {bumplot-0.1.1 → bumplot-0.3.0}/.github/workflows/tests.yaml +3 -4
  5. {bumplot-0.1.1 → bumplot-0.3.0}/.github/workflows/type.yaml +11 -5
  6. {bumplot-0.1.1 → bumplot-0.3.0}/.gitignore +4 -1
  7. {bumplot-0.1.1 → bumplot-0.3.0}/PKG-INFO +13 -19
  8. bumplot-0.1.1/docs/README.qmd → bumplot-0.3.0/README.md +6 -25
  9. bumplot-0.3.0/bumplot/__init__.py +5 -0
  10. {bumplot-0.1.1 → bumplot-0.3.0}/bumplot/_utils.py +12 -10
  11. {bumplot-0.1.1 → bumplot-0.3.0}/bumplot/bezier.py +13 -7
  12. bumplot-0.3.0/bumplot/main.py +124 -0
  13. bumplot-0.3.0/bumplot/opts.py +190 -0
  14. {bumplot-0.1.1 → bumplot-0.3.0}/bumplot.egg-info/PKG-INFO +13 -19
  15. {bumplot-0.1.1 → bumplot-0.3.0}/bumplot.egg-info/SOURCES.txt +8 -6
  16. bumplot-0.3.0/docs/advanced-usage.md +247 -0
  17. {bumplot-0.1.1 → bumplot-0.3.0}/docs/contributing.md +4 -2
  18. bumplot-0.3.0/docs/data/water-africa.csv +7 -0
  19. bumplot-0.3.0/docs/examples.md +226 -0
  20. {bumplot-0.1.1 → bumplot-0.3.0}/docs/index.md +19 -6
  21. bumplot-0.3.0/justfile +16 -0
  22. bumplot-0.3.0/pyproject.toml +57 -0
  23. bumplot-0.3.0/tests/test_main.py +195 -0
  24. bumplot-0.3.0/tests/test_opts.py +108 -0
  25. bumplot-0.3.0/zensical.toml +58 -0
  26. bumplot-0.1.1/.github/workflows/doc.yaml +0 -41
  27. bumplot-0.1.1/.github/workflows/lint.yaml +0 -28
  28. bumplot-0.1.1/Makefile +0 -17
  29. bumplot-0.1.1/README.md +0 -70
  30. bumplot-0.1.1/bumplot/__init__.py +0 -4
  31. bumplot-0.1.1/bumplot/main.py +0 -104
  32. bumplot-0.1.1/docs/examples.md +0 -151
  33. bumplot-0.1.1/mkdocs.yaml +0 -60
  34. bumplot-0.1.1/overrides/partials/footer.html +0 -7
  35. bumplot-0.1.1/pyproject.toml +0 -60
  36. bumplot-0.1.1/tests/test_main.py +0 -86
  37. {bumplot-0.1.1 → bumplot-0.3.0}/.coverage +0 -0
  38. {bumplot-0.1.1 → bumplot-0.3.0}/.pre-commit-config.yaml +0 -0
  39. {bumplot-0.1.1 → bumplot-0.3.0}/LICENSE +0 -0
  40. {bumplot-0.1.1 → bumplot-0.3.0}/bumplot.egg-info/dependency_links.txt +0 -0
  41. {bumplot-0.1.1 → bumplot-0.3.0}/bumplot.egg-info/requires.txt +0 -0
  42. {bumplot-0.1.1 → bumplot-0.3.0}/bumplot.egg-info/top_level.txt +0 -0
  43. {bumplot-0.1.1 → bumplot-0.3.0}/coverage-badge.svg +0 -0
  44. {bumplot-0.1.1 → bumplot-0.3.0}/docs/.gitignore +0 -0
  45. {bumplot-0.1.1 → bumplot-0.3.0}/docs/reference/bezier.md +0 -0
  46. {bumplot-0.1.1 → bumplot-0.3.0}/docs/reference/bumplot.md +0 -0
  47. {bumplot-0.1.1 → bumplot-0.3.0}/docs/stylesheets/style.css +0 -0
  48. /bumplot-0.1.1/README_files/figure-commonmark/cell-3-output-1.png → /bumplot-0.3.0/example.png +0 -0
  49. {bumplot-0.1.1 → bumplot-0.3.0}/setup.cfg +0 -0
  50. {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
@@ -17,7 +17,7 @@ jobs:
17
17
  runs-on: ubuntu-latest
18
18
 
19
19
  steps:
20
- - uses: actions/checkout@v4
20
+ - uses: actions/checkout@v6
21
21
  with:
22
22
  persist-credentials: false
23
23
  - name: Set up Python
@@ -10,13 +10,12 @@ jobs:
10
10
  strategy:
11
11
  matrix:
12
12
  python-version:
13
- - "3.11"
14
- - "3.12"
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@v4
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 Static Typing
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@v3
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: Run tests
27
+ - name: ty
25
28
  run: uv run ty check
29
+
30
+ - name: pyrefly
31
+ run: uv run pyrefly check
@@ -20,4 +20,7 @@ env/
20
20
 
21
21
  *.zip
22
22
  sandbox/
23
- scripts/release.sh
23
+ release.sh
24
+ .claude/
25
+
26
+ site/
@@ -1,17 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bumplot
3
- Version: 0.1.1
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 plot,bump charts,matplotlib,data,visualization,bezier,curve
12
- Classifier: Programming Language :: Python :: 3
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: Development Status :: 4 - Beta
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
- ![](README_files/figure-commonmark/cell-3-output-1.png)
66
+ ![A simple bump chart made with Python and bumplot](example.png)
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
- pip install bumplot
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
- ```{python}
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
+ ![A simple bump chart made with Python and bumplot](example.png)
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
 
@@ -0,0 +1,5 @@
1
+ from bumplot.main import bumplot
2
+ from bumplot.opts import opts, opts_from_color
3
+
4
+ __version__ = "0.3.0"
5
+ __all__ = ["bumplot", "opts", "opts_from_color"]
@@ -1,18 +1,11 @@
1
- import matplotlib.pyplot as plt
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 _get_first_n_colors(n=int) -> list[str]:
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: list = []
29
- codes: list = []
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([(x0 + dx, y0), (x1 - dx, y1), (x1, y1)])
40
- codes.extend([Path.CURVE4, Path.CURVE4, Path.CURVE4])
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