bumplot 0.1.1__tar.gz → 0.2.1__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.1.1 → bumplot-0.2.1}/.github/workflows/tests.yaml +1 -2
- {bumplot-0.1.1 → bumplot-0.2.1}/.gitignore +2 -1
- {bumplot-0.1.1 → bumplot-0.2.1}/PKG-INFO +2 -2
- bumplot-0.2.1/bumplot/__init__.py +5 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/bumplot/_utils.py +9 -10
- bumplot-0.2.1/bumplot/main.py +131 -0
- bumplot-0.2.1/bumplot/opts.py +191 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/bumplot.egg-info/PKG-INFO +2 -2
- {bumplot-0.1.1 → bumplot-0.2.1}/bumplot.egg-info/SOURCES.txt +6 -2
- bumplot-0.2.1/docs/advanced-usage.md +231 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/docs/contributing.md +4 -2
- bumplot-0.2.1/docs/data/water-africa.csv +7 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/docs/examples.md +59 -20
- {bumplot-0.1.1 → bumplot-0.2.1}/docs/index.md +9 -4
- bumplot-0.1.1/Makefile → bumplot-0.2.1/justfile +0 -2
- {bumplot-0.1.1 → bumplot-0.2.1}/mkdocs.yaml +1 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/pyproject.toml +5 -2
- bumplot-0.2.1/tests/test_main.py +196 -0
- bumplot-0.2.1/tests/test_opts.py +108 -0
- bumplot-0.1.1/bumplot/__init__.py +0 -4
- bumplot-0.1.1/bumplot/main.py +0 -104
- bumplot-0.1.1/tests/test_main.py +0 -86
- {bumplot-0.1.1 → bumplot-0.2.1}/.coverage +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/.github/workflows/doc.yaml +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/.github/workflows/lint.yaml +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/.github/workflows/pypi.yaml +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/.github/workflows/type.yaml +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/.pre-commit-config.yaml +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/LICENSE +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/README.md +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/README_files/figure-commonmark/cell-3-output-1.png +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/bumplot/bezier.py +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/bumplot.egg-info/dependency_links.txt +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/bumplot.egg-info/requires.txt +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/bumplot.egg-info/top_level.txt +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/coverage-badge.svg +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/docs/.gitignore +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/docs/README.qmd +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/docs/reference/bezier.md +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/docs/reference/bumplot.md +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/docs/stylesheets/style.css +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/overrides/partials/footer.html +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/setup.cfg +0 -0
- {bumplot-0.1.1 → bumplot-0.2.1}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bumplot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
@@ -11,7 +11,7 @@ Project-URL: Repository, https://github.com/y-sunflower/bumplot
|
|
|
11
11
|
Keywords: bump,bump plot,bump charts,matplotlib,data,visualization,bezier,curve
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
|
-
Classifier: Development Status ::
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
15
|
Requires-Python: >=3.10
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
@@ -1,17 +1,7 @@
|
|
|
1
|
-
import matplotlib.pyplot as plt
|
|
2
|
-
|
|
3
1
|
import narwhals as nw
|
|
4
2
|
from narwhals.typing import IntoDataFrame
|
|
5
3
|
|
|
6
4
|
|
|
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
5
|
def _ranked_df(df: IntoDataFrame, x: str, y_columns: list[str]):
|
|
16
6
|
"""
|
|
17
7
|
Convert a dataframe to a ranked version of it.
|
|
@@ -26,3 +16,12 @@ def _ranked_df(df: IntoDataFrame, x: str, y_columns: list[str]):
|
|
|
26
16
|
)
|
|
27
17
|
|
|
28
18
|
return df_native_ranked
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _to_ordinal(n: int) -> str:
|
|
22
|
+
"""Convert number to ordinal string (1 -> '1st', 2 -> '2nd', etc.)"""
|
|
23
|
+
if 11 <= n % 100 <= 13:
|
|
24
|
+
suffix = "th"
|
|
25
|
+
else:
|
|
26
|
+
suffix = {1: "st", 2: "nd", 3: "rd"}.get(n % 10, "th")
|
|
27
|
+
return f"{n}{suffix}"
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
from collections import ChainMap
|
|
2
|
+
from itertools import cycle
|
|
3
|
+
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
from matplotlib.axes import Axes
|
|
6
|
+
from matplotlib.path import Path
|
|
7
|
+
import matplotlib.patches as patches
|
|
8
|
+
from matplotlib.cbook import normalize_kwargs
|
|
9
|
+
from matplotlib.collections import PathCollection
|
|
10
|
+
from matplotlib.patches import PathPatch
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from narwhals.typing import IntoDataFrame
|
|
15
|
+
|
|
16
|
+
from .bezier import bezier_curve
|
|
17
|
+
from ._utils import _ranked_df, _to_ordinal
|
|
18
|
+
from .opts import BumpOpts, _get_plot_kwargs, _get_scatter_kwargs
|
|
19
|
+
|
|
20
|
+
from typing import Any, Iterable, Tuple
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def bumplot(
|
|
24
|
+
x: str,
|
|
25
|
+
y_columns: Iterable[str | tuple[str, BumpOpts]],
|
|
26
|
+
data: IntoDataFrame,
|
|
27
|
+
curve_force: float = 1,
|
|
28
|
+
invert_y_axis: bool = True,
|
|
29
|
+
colors: Iterable[str] | None = None,
|
|
30
|
+
plot_kwargs: dict[str, Any] = {},
|
|
31
|
+
scatter_kwargs: dict[str, Any] = {},
|
|
32
|
+
ax: Axes | None = None,
|
|
33
|
+
ordinal_labels: bool = False,
|
|
34
|
+
) -> Tuple[Axes, dict[str, Tuple[PathPatch, PathCollection]]]:
|
|
35
|
+
"""
|
|
36
|
+
Creates bump plot, or bump chart, from multiple numerical
|
|
37
|
+
columns.
|
|
38
|
+
|
|
39
|
+
It requires the data to be in wide format (e.g., one column
|
|
40
|
+
per line you want to plot).
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
x: colname of the x-axis variable
|
|
44
|
+
y_columns: colnames of the y-axis variables and their plotting options.
|
|
45
|
+
data: A dataframe
|
|
46
|
+
curve_force: Smoothing factor controlling curve tightness. Higher
|
|
47
|
+
values increase curvature by moving control points further away
|
|
48
|
+
from the anchors.
|
|
49
|
+
invert_y_axis: Whether to invert y axis
|
|
50
|
+
colors: An optional list of colors
|
|
51
|
+
plot_kwargs: Additional arguments passed to `patches.PathPatch()`
|
|
52
|
+
scatter_kwargs: Additional arguments passed to `scatter()`
|
|
53
|
+
ax: The matplotlib Axes used. Default to `plt.gca()`
|
|
54
|
+
ordinal_labels: If True, converts y-axis labels to ordinal numbers (1st, 2nd, 3rd, etc.)
|
|
55
|
+
Returns:
|
|
56
|
+
The matplotlib Axes with the bump plot
|
|
57
|
+
"""
|
|
58
|
+
_plot_ax: Axes = ax if ax is not None else plt.gca()
|
|
59
|
+
colors_iterable = (
|
|
60
|
+
colors
|
|
61
|
+
if colors is not None
|
|
62
|
+
else plt.rcParams["axes.prop_cycle"].by_key()["color"]
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
y_bumps: list[tuple[str, BumpOpts]] = [
|
|
66
|
+
(y, BumpOpts()) if isinstance(y, str) else y for y in y_columns
|
|
67
|
+
]
|
|
68
|
+
ranked: IntoDataFrame = _ranked_df(data, x=x, y_columns=[y for y, _ in y_bumps])
|
|
69
|
+
x_values_raw: np.ndarray = np.ravel(ranked.select(x).to_numpy())
|
|
70
|
+
|
|
71
|
+
if np.issubdtype(x_values_raw.dtype, np.number):
|
|
72
|
+
x_values = x_values_raw
|
|
73
|
+
x_labels = x_values_raw
|
|
74
|
+
else:
|
|
75
|
+
uniques = list(dict.fromkeys(x_values_raw)) # preserves order
|
|
76
|
+
mapping = {val: i for i, val in enumerate(uniques)}
|
|
77
|
+
x_values = np.array([mapping[val] for val in x_values_raw], dtype=int)
|
|
78
|
+
x_labels = x_values_raw
|
|
79
|
+
|
|
80
|
+
artists = {}
|
|
81
|
+
for (name, bump_opts), color in zip(y_bumps, cycle(colors_iterable)):
|
|
82
|
+
y_values: np.ndarray = np.ravel(ranked.select(name).to_numpy())
|
|
83
|
+
vertices, codes = bezier_curve(
|
|
84
|
+
x=x_values,
|
|
85
|
+
y=y_values,
|
|
86
|
+
force=curve_force,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
path: Path = Path(vertices=vertices, codes=codes)
|
|
90
|
+
patch: PathPatch = patches.PathPatch(
|
|
91
|
+
path=path,
|
|
92
|
+
facecolor="none",
|
|
93
|
+
**ChainMap(
|
|
94
|
+
_get_plot_kwargs(bump_opts),
|
|
95
|
+
normalize_kwargs(plot_kwargs, PathPatch),
|
|
96
|
+
{"edgecolor": color},
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
_plot_ax.add_patch(patch)
|
|
100
|
+
|
|
101
|
+
scatter = _plot_ax.scatter(
|
|
102
|
+
x_values,
|
|
103
|
+
y_values,
|
|
104
|
+
label=name,
|
|
105
|
+
**ChainMap(
|
|
106
|
+
_get_scatter_kwargs(bump_opts),
|
|
107
|
+
normalize_kwargs(scatter_kwargs, PathCollection),
|
|
108
|
+
{"facecolor": color},
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
artists[name] = (patch, scatter)
|
|
112
|
+
|
|
113
|
+
ticks: list[int] = list(range(1, len(y_bumps) + 1))
|
|
114
|
+
|
|
115
|
+
if invert_y_axis:
|
|
116
|
+
_plot_ax.invert_yaxis()
|
|
117
|
+
else:
|
|
118
|
+
ticks: list[int] = list(reversed(ticks))
|
|
119
|
+
|
|
120
|
+
_plot_ax.set_yticks(ticks=ticks)
|
|
121
|
+
|
|
122
|
+
labels = (
|
|
123
|
+
[_to_ordinal(tick) for tick in ticks]
|
|
124
|
+
if ordinal_labels
|
|
125
|
+
else [str(tick) for tick in ticks]
|
|
126
|
+
)
|
|
127
|
+
_plot_ax.set_yticklabels(labels)
|
|
128
|
+
|
|
129
|
+
_plot_ax.set_xticks(ticks=np.unique(x_values), labels=np.unique(x_labels))
|
|
130
|
+
|
|
131
|
+
return _plot_ax, artists
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from typing import Any, TypedDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class BumpOpts(TypedDict, total=False):
|
|
5
|
+
"""
|
|
6
|
+
Typed dictionary describing high-level styling options for a bump plot.
|
|
7
|
+
|
|
8
|
+
This type represents a user-facing, backend-agnostic set of visual options
|
|
9
|
+
that can be applied to both line (plot) and marker (scatter) artists.
|
|
10
|
+
|
|
11
|
+
All fields are optional. Keys are validated at runtime when constructed via
|
|
12
|
+
`opts()` to ensure only supported options are provided.
|
|
13
|
+
|
|
14
|
+
The options are later translated into Matplotlib-specific keyword arguments
|
|
15
|
+
via `_get_plot_kwargs()` and `_get_scatter_kwargs()`.
|
|
16
|
+
|
|
17
|
+
Notes
|
|
18
|
+
-----
|
|
19
|
+
- Keys prefixed with `line_` apply to line artists.
|
|
20
|
+
- Keys prefixed with `marker_` apply to scatter/marker artists.
|
|
21
|
+
- Some options (e.g. `clip_on` and `zorder`) apply to both.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
line_alpha: float
|
|
25
|
+
line_color: str
|
|
26
|
+
line_style: str
|
|
27
|
+
line_width: float
|
|
28
|
+
|
|
29
|
+
marker: str
|
|
30
|
+
marker_size: float
|
|
31
|
+
marker_alpha: float
|
|
32
|
+
marker_facecolor: str
|
|
33
|
+
marker_edgecolor: str
|
|
34
|
+
marker_edgewidth: float
|
|
35
|
+
|
|
36
|
+
clip_on: bool
|
|
37
|
+
zorder: int
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
PLOT_MAPPINGS: dict[str, str] = {
|
|
41
|
+
"line_alpha": "alpha",
|
|
42
|
+
"line_color": "edgecolor",
|
|
43
|
+
"line_style": "linestyle",
|
|
44
|
+
"line_width": "linewidth",
|
|
45
|
+
"clip_on": "clip_on",
|
|
46
|
+
"zorder": "zorder",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
SCATTER_MAPPINGS: dict[str, str] = {
|
|
50
|
+
"marker": "marker",
|
|
51
|
+
"marker_alpha": "alpha",
|
|
52
|
+
"marker_edgecolor": "edgecolor",
|
|
53
|
+
"marker_edgewidth": "linewidth",
|
|
54
|
+
"marker_facecolor": "facecolor",
|
|
55
|
+
"marker_size": "s",
|
|
56
|
+
"clip_on": "clip_on",
|
|
57
|
+
"zorder": "zorder",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# TODO: For >=Python 3.11 (https://docs.python.org/3/library/typing.html#typing.Unpack)
|
|
62
|
+
def opts(**kwargs: Any) -> BumpOpts:
|
|
63
|
+
"""
|
|
64
|
+
Construct a validated `BumpOpts` mapping from keyword arguments.
|
|
65
|
+
|
|
66
|
+
This function enforces that all provided keyword arguments correspond to
|
|
67
|
+
supported `BumpOpts` fields. Any unexpected keys result in a `TypeError`.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
**kwargs: Unpack[BumpOpts]
|
|
72
|
+
Keyword arguments corresponding to fields defined on `BumpOpts`.
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
BumpOpts
|
|
77
|
+
A typed dictionary containing the validated bump plot options.
|
|
78
|
+
|
|
79
|
+
Raises
|
|
80
|
+
------
|
|
81
|
+
TypeError
|
|
82
|
+
If one or more unsupported keyword arguments are provided.
|
|
83
|
+
|
|
84
|
+
Notes
|
|
85
|
+
-----
|
|
86
|
+
This function exists to provide runtime validation for `BumpOpts`,
|
|
87
|
+
which is otherwise only enforced statically by type checkers.
|
|
88
|
+
"""
|
|
89
|
+
unsupported_keys = kwargs.keys() - BumpOpts.__annotations__
|
|
90
|
+
if unsupported_keys:
|
|
91
|
+
unsupported_repr = ", ".join(map(repr, sorted(unsupported_keys)))
|
|
92
|
+
msg = f"BumpOpts got unexpected keyword argument(s): {unsupported_repr}"
|
|
93
|
+
raise TypeError(msg)
|
|
94
|
+
|
|
95
|
+
return BumpOpts(**kwargs)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def opts_from_color(color, /, **kwargs: Any) -> BumpOpts:
|
|
99
|
+
"""
|
|
100
|
+
Construct a `BumpOpts` mapping using a single color applied consistently
|
|
101
|
+
across line and marker components.
|
|
102
|
+
|
|
103
|
+
The provided color is applied to:
|
|
104
|
+
- `line_color`
|
|
105
|
+
- `marker_facecolor`
|
|
106
|
+
- `marker_edgecolor`
|
|
107
|
+
|
|
108
|
+
Any explicitly provided keyword arguments take precedence over the color-based
|
|
109
|
+
defaults.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
color
|
|
114
|
+
The color to apply to line and marker components.
|
|
115
|
+
**kwargs
|
|
116
|
+
Additional or overriding bump plot options (passed to `opts`).
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
BumpOpts
|
|
121
|
+
A validated bump plot options mapping.
|
|
122
|
+
|
|
123
|
+
See Also
|
|
124
|
+
--------
|
|
125
|
+
opts: Performs keyword validation and construction.
|
|
126
|
+
"""
|
|
127
|
+
overrides = ["line_color", "marker_facecolor", "marker_edgecolor"]
|
|
128
|
+
new_kwargs = {k: color for k in overrides} | kwargs
|
|
129
|
+
return opts(**new_kwargs)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _get_plot_kwargs(kwargs: BumpOpts, /) -> dict[str, Any]:
|
|
133
|
+
"""
|
|
134
|
+
Extract Matplotlib `plot` keyword arguments from a `BumpOpts` mapping.
|
|
135
|
+
|
|
136
|
+
This function translates bump-plot-specific option names into the equivalent
|
|
137
|
+
Matplotlib `Axes.plot` keyword arguments using `bumplot.opts.PLOT_MAPPINGS`.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
kwargs: Unpack[BumpOpts]
|
|
142
|
+
A `BumpOpts` mapping containing zero or more plot-related options.
|
|
143
|
+
|
|
144
|
+
Returns
|
|
145
|
+
-------
|
|
146
|
+
dict[str, Any]
|
|
147
|
+
A dictionary of Matplotlib-compatible keyword arguments suitable for
|
|
148
|
+
`Axes.patches.PathPatch`.
|
|
149
|
+
|
|
150
|
+
Notes
|
|
151
|
+
-----
|
|
152
|
+
- Only keys present in both `kwargs` and `PLOT_MAPPINGS` are included.
|
|
153
|
+
"""
|
|
154
|
+
plot_kwargs = {}
|
|
155
|
+
for bump_arg, mpl_arg in PLOT_MAPPINGS.items():
|
|
156
|
+
try:
|
|
157
|
+
plot_kwargs[mpl_arg] = kwargs[bump_arg] # type: ignore[invalid-key]
|
|
158
|
+
except KeyError:
|
|
159
|
+
pass
|
|
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
|
+
scatter_kwargs = {}
|
|
186
|
+
for bump_arg, mpl_arg in SCATTER_MAPPINGS.items():
|
|
187
|
+
try:
|
|
188
|
+
scatter_kwargs[mpl_arg] = kwargs[bump_arg] # type: ignore[invalid-key]
|
|
189
|
+
except KeyError:
|
|
190
|
+
pass
|
|
191
|
+
return scatter_kwargs
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bumplot
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.1
|
|
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
|
|
@@ -11,7 +11,7 @@ Project-URL: Repository, https://github.com/y-sunflower/bumplot
|
|
|
11
11
|
Keywords: bump,bump plot,bump charts,matplotlib,data,visualization,bezier,curve
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
14
|
-
Classifier: Development Status ::
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
15
|
Requires-Python: >=3.10
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
License-File: LICENSE
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
.gitignore
|
|
3
3
|
.pre-commit-config.yaml
|
|
4
4
|
LICENSE
|
|
5
|
-
Makefile
|
|
6
5
|
README.md
|
|
7
6
|
coverage-badge.svg
|
|
7
|
+
justfile
|
|
8
8
|
mkdocs.yaml
|
|
9
9
|
pyproject.toml
|
|
10
10
|
.github/workflows/doc.yaml
|
|
@@ -17,6 +17,7 @@ bumplot/__init__.py
|
|
|
17
17
|
bumplot/_utils.py
|
|
18
18
|
bumplot/bezier.py
|
|
19
19
|
bumplot/main.py
|
|
20
|
+
bumplot/opts.py
|
|
20
21
|
bumplot.egg-info/PKG-INFO
|
|
21
22
|
bumplot.egg-info/SOURCES.txt
|
|
22
23
|
bumplot.egg-info/dependency_links.txt
|
|
@@ -24,12 +25,15 @@ bumplot.egg-info/requires.txt
|
|
|
24
25
|
bumplot.egg-info/top_level.txt
|
|
25
26
|
docs/.gitignore
|
|
26
27
|
docs/README.qmd
|
|
28
|
+
docs/advanced-usage.md
|
|
27
29
|
docs/contributing.md
|
|
28
30
|
docs/examples.md
|
|
29
31
|
docs/index.md
|
|
32
|
+
docs/data/water-africa.csv
|
|
30
33
|
docs/reference/bezier.md
|
|
31
34
|
docs/reference/bumplot.md
|
|
32
35
|
docs/stylesheets/style.css
|
|
33
36
|
overrides/partials/footer.html
|
|
34
37
|
tests/__init__.py
|
|
35
|
-
tests/test_main.py
|
|
38
|
+
tests/test_main.py
|
|
39
|
+
tests/test_opts.py
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
`bumplot` is designed to integrate seamlessly with matplotlib and allow you to control virtually everything.
|
|
2
|
+
|
|
3
|
+
Here you will find examples that show how to use `bumplot` in more complex, but also more realistic, cases based on real data.
|
|
4
|
+
|
|
5
|
+
These examples are also designed to be easy to reproduce: you should be able to simply copy and paste the code onto your computer.
|
|
6
|
+
|
|
7
|
+
<br>
|
|
8
|
+
<br>
|
|
9
|
+
|
|
10
|
+
## Water sources in Africa
|
|
11
|
+
|
|
12
|
+
```py
|
|
13
|
+
# mkdocs: render
|
|
14
|
+
import pandas as pd
|
|
15
|
+
import matplotlib.pyplot as plt
|
|
16
|
+
from pyfonts import set_default_font, load_google_font
|
|
17
|
+
|
|
18
|
+
from bumplot import bumplot, opts_from_color
|
|
19
|
+
|
|
20
|
+
url = "https://raw.githubusercontent.com/y-sunflower/bumplot/main/docs/data/water-africa.csv"
|
|
21
|
+
df = pd.read_csv(url)
|
|
22
|
+
|
|
23
|
+
font = load_google_font("Poppins")
|
|
24
|
+
set_default_font(font)
|
|
25
|
+
|
|
26
|
+
highlight = {
|
|
27
|
+
"Zambia": opts_from_color("#2a9d8f", zorder=2, line_width=3, marker_size=100),
|
|
28
|
+
"Tanzania": opts_from_color("#bb3e03", zorder=2, line_width=3, marker_size=100),
|
|
29
|
+
}
|
|
30
|
+
countries = [name for name in df.columns if not (name.startswith("decade"))]
|
|
31
|
+
|
|
32
|
+
fig, ax = plt.subplots(figsize=(12, 5))
|
|
33
|
+
_, bump_artists = bumplot(
|
|
34
|
+
x="decade",
|
|
35
|
+
y_columns=[(name, highlight.get(name, {})) for name in countries],
|
|
36
|
+
data=df,
|
|
37
|
+
curve_force=0.7,
|
|
38
|
+
ordinal_labels=True,
|
|
39
|
+
colors=["lightgray"]
|
|
40
|
+
)
|
|
41
|
+
ax.spines[["top", "right", "left", "bottom"]].set_visible(False)
|
|
42
|
+
ax.margins(x=0.01)
|
|
43
|
+
ax.tick_params(size=0)
|
|
44
|
+
ax.tick_params(axis="x", labelsize=14, pad=10)
|
|
45
|
+
ax.set_xticks(ax.get_xticks(), labels=[round(tick) for tick in ax.get_xticks()])
|
|
46
|
+
|
|
47
|
+
font_bold = load_google_font("Poppins", weight="bold")
|
|
48
|
+
for name, (_, scatter) in bump_artists.items():
|
|
49
|
+
_, last_y = scatter.get_offsets()[-1]
|
|
50
|
+
ax.text(
|
|
51
|
+
x=1,
|
|
52
|
+
y=last_y,
|
|
53
|
+
s=name,
|
|
54
|
+
size=11,
|
|
55
|
+
color=scatter.get_facecolor(),
|
|
56
|
+
va="center",
|
|
57
|
+
ha="left",
|
|
58
|
+
font=font_bold,
|
|
59
|
+
transform=ax.get_yaxis_transform(),
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
fig.text(
|
|
63
|
+
x=0.5,
|
|
64
|
+
y=0.98,
|
|
65
|
+
s="# of water sources installations in africa".upper(),
|
|
66
|
+
size=18,
|
|
67
|
+
ha="center",
|
|
68
|
+
va="top",
|
|
69
|
+
font=font_bold,
|
|
70
|
+
)
|
|
71
|
+
fig.text(
|
|
72
|
+
x=0.5,
|
|
73
|
+
y=0.91,
|
|
74
|
+
s="Ranking of each country based on the total number of water source installations for each decade",
|
|
75
|
+
size=10,
|
|
76
|
+
ha="center",
|
|
77
|
+
va="top",
|
|
78
|
+
color="grey",
|
|
79
|
+
font=load_google_font("Poppins", italic=True),
|
|
80
|
+
)
|
|
81
|
+
fig.text(
|
|
82
|
+
x=0.12,
|
|
83
|
+
y=0,
|
|
84
|
+
s="Made with bumplot\nGraphic: Joseph Barbier",
|
|
85
|
+
va="top",
|
|
86
|
+
color="grey",
|
|
87
|
+
font=load_google_font("Poppins", italic=True),
|
|
88
|
+
size=10,
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
<br>
|
|
93
|
+
<br>
|
|
94
|
+
|
|
95
|
+
## Global CO₂ Emissions Rankings
|
|
96
|
+
|
|
97
|
+
```py
|
|
98
|
+
# mkdocs: render
|
|
99
|
+
import pandas as pd
|
|
100
|
+
import matplotlib.pyplot as plt
|
|
101
|
+
from pyfonts import set_default_font, load_google_font
|
|
102
|
+
|
|
103
|
+
from bumplot import bumplot, opts_from_color
|
|
104
|
+
|
|
105
|
+
url = "https://raw.githubusercontent.com/owid/co2-data/master/owid-co2-data.csv"
|
|
106
|
+
df_raw = pd.read_csv(url)
|
|
107
|
+
countries = [
|
|
108
|
+
"United States",
|
|
109
|
+
"China",
|
|
110
|
+
"India",
|
|
111
|
+
"Russia",
|
|
112
|
+
"Japan",
|
|
113
|
+
"Germany",
|
|
114
|
+
"Iran",
|
|
115
|
+
"South Korea",
|
|
116
|
+
"Saudi Arabia",
|
|
117
|
+
"Indonesia",
|
|
118
|
+
]
|
|
119
|
+
years = [1930, 1940, 1950, 1960, 1970, 1980, 1990, 2000, 2010, 2020]
|
|
120
|
+
df = (
|
|
121
|
+
df_raw[df_raw["country"].isin(countries) & df_raw["year"].isin(years)][
|
|
122
|
+
["year", "country", "co2"]
|
|
123
|
+
]
|
|
124
|
+
.dropna()
|
|
125
|
+
.pivot(index="year", columns="country", values="co2")
|
|
126
|
+
.reset_index()
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
font = load_google_font("Inter")
|
|
130
|
+
set_default_font(font)
|
|
131
|
+
|
|
132
|
+
highlight = {
|
|
133
|
+
"China": opts_from_color(
|
|
134
|
+
"#ae2012",
|
|
135
|
+
marker_size=100,
|
|
136
|
+
marker_edgewidth=2,
|
|
137
|
+
zorder=10,
|
|
138
|
+
),
|
|
139
|
+
"United States": opts_from_color(
|
|
140
|
+
"#005f73",
|
|
141
|
+
marker_size=100,
|
|
142
|
+
marker_edgewidth=2,
|
|
143
|
+
zorder=10,
|
|
144
|
+
),
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
countries_to_plot = [col for col in df.columns if col != "year"]
|
|
148
|
+
|
|
149
|
+
fig, ax = plt.subplots(figsize=(10, 7))
|
|
150
|
+
_, bump_artists = bumplot(
|
|
151
|
+
x="year",
|
|
152
|
+
y_columns=[(name, highlight.get(name, {})) for name in countries_to_plot],
|
|
153
|
+
data=df,
|
|
154
|
+
curve_force=0.7,
|
|
155
|
+
ordinal_labels=True,
|
|
156
|
+
colors=["#D1D5DB"],
|
|
157
|
+
scatter_kwargs={
|
|
158
|
+
"edgecolor": "#D1D5DB",
|
|
159
|
+
"s": 40,
|
|
160
|
+
"linewidth": 1,
|
|
161
|
+
"facecolor": "#F3F4F6",
|
|
162
|
+
},
|
|
163
|
+
plot_kwargs={"linewidth": 1.5},
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
ax.spines[["top", "right", "left", "bottom"]].set_visible(False)
|
|
167
|
+
ax.tick_params(axis="x", labelsize=11, pad=4, colors="#4a4e69")
|
|
168
|
+
ax.tick_params(size=0)
|
|
169
|
+
|
|
170
|
+
font_medium = load_google_font("Inter", weight="medium")
|
|
171
|
+
for country in countries:
|
|
172
|
+
if country in bump_artists:
|
|
173
|
+
_, scatter = bump_artists[country]
|
|
174
|
+
_, last_y = scatter.get_offsets()[-1]
|
|
175
|
+
|
|
176
|
+
color = "#ae2012" if country == "China" else "#4a4e69"
|
|
177
|
+
weight = "bold" if country == "China" else "regular"
|
|
178
|
+
font_label = load_google_font("Inter", weight=weight)
|
|
179
|
+
|
|
180
|
+
ax.text(
|
|
181
|
+
x=1,
|
|
182
|
+
y=last_y,
|
|
183
|
+
s=country,
|
|
184
|
+
size=11,
|
|
185
|
+
color=color,
|
|
186
|
+
va="center",
|
|
187
|
+
ha="left",
|
|
188
|
+
font=font_label,
|
|
189
|
+
transform=ax.get_yaxis_transform(),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
font_bold = load_google_font("Inter", weight="bold")
|
|
193
|
+
fig.text(
|
|
194
|
+
x=0.5,
|
|
195
|
+
y=0.97,
|
|
196
|
+
s="The Rise of China's CO₂ Emissions",
|
|
197
|
+
size=20,
|
|
198
|
+
ha="center",
|
|
199
|
+
va="top",
|
|
200
|
+
font=font_bold,
|
|
201
|
+
color="#111827",
|
|
202
|
+
)
|
|
203
|
+
fig.text(
|
|
204
|
+
x=0.5,
|
|
205
|
+
y=0.92,
|
|
206
|
+
s="Ranking of top 10 emitting countries, 1930–2020",
|
|
207
|
+
size=11,
|
|
208
|
+
ha="center",
|
|
209
|
+
va="top",
|
|
210
|
+
color="#4a4e69",
|
|
211
|
+
)
|
|
212
|
+
fig.text(
|
|
213
|
+
x=0.1,
|
|
214
|
+
y=0.04,
|
|
215
|
+
s="Source: Our World in Data · CO₂ emissions dataset",
|
|
216
|
+
va="bottom",
|
|
217
|
+
ha="left",
|
|
218
|
+
color="#4a4e69",
|
|
219
|
+
size=8,
|
|
220
|
+
)
|
|
221
|
+
fig.text(
|
|
222
|
+
x=0.9,
|
|
223
|
+
y=0.04,
|
|
224
|
+
s="Made with bumplot",
|
|
225
|
+
va="bottom",
|
|
226
|
+
ha="right",
|
|
227
|
+
color="#4a4e69",
|
|
228
|
+
font=font_bold,
|
|
229
|
+
size=8,
|
|
230
|
+
)
|
|
231
|
+
```
|
|
@@ -37,16 +37,18 @@ You can now make changes to the package and start coding!
|
|
|
37
37
|
|
|
38
38
|
### Run the test
|
|
39
39
|
|
|
40
|
+
It's recommended to use [just](https://github.com/casey/just).
|
|
41
|
+
|
|
40
42
|
- Test that everything works correctly by running:
|
|
41
43
|
|
|
42
44
|
```bash
|
|
43
|
-
|
|
45
|
+
just test
|
|
44
46
|
```
|
|
45
47
|
|
|
46
48
|
### Preview documentation locally
|
|
47
49
|
|
|
48
50
|
```bash
|
|
49
|
-
|
|
51
|
+
just preview
|
|
50
52
|
```
|
|
51
53
|
|
|
52
54
|
## Push changes
|