ggplot2-python 4.0.2.9000.post3__tar.gz → 4.0.2.9000.post5__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.
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/PKG-INFO +54 -8
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/README.md +53 -7
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/__init__.py +16 -3
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/_compat.py +66 -9
- ggplot2_python-4.0.2.9000.post5/ggplot2_py/_env.py +160 -0
- ggplot2_python-4.0.2.9000.post3/ggplot2_py/guide_axis.py → ggplot2_python-4.0.2.9000.post5/ggplot2_py/_guide_axis.py +20 -9
- ggplot2_python-4.0.2.9000.post3/ggplot2_py/guide_colourbar.py → ggplot2_python-4.0.2.9000.post5/ggplot2_py/_guide_colourbar.py +1 -1
- ggplot2_python-4.0.2.9000.post3/ggplot2_py/guide_legend.py → ggplot2_python-4.0.2.9000.post5/ggplot2_py/_guide_legend.py +22 -27
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/coord.py +3 -3
- ggplot2_python-4.0.2.9000.post5/ggplot2_py/extension/__init__.py +365 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/facet.py +2 -2
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/geom.py +3 -6
- ggplot2_python-4.0.2.9000.post5/ggplot2_py/ggproto.py +653 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/guide.py +645 -39
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/layer.py +8 -3
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/plot.py +342 -45
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/plot_render.py +58 -551
- ggplot2_python-4.0.2.9000.post5/ggplot2_py/protocols.py +202 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/save.py +8 -3
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/scale.py +166 -82
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/scales/__init__.py +25 -3
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/theme_elements.py +107 -54
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/pyproject.toml +1 -1
- ggplot2_python-4.0.2.9000.post3/ggplot2_py/ggproto.py +0 -329
- ggplot2_python-4.0.2.9000.post3/ggplot2_py/protocols.py +0 -171
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/.gitattributes +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/.gitignore +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/LICENSE +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/_defaults.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/_make_constructor.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/_plugins.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/_utils.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/aes.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/annotation.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/autoplot.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/coords/__init__.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/datasets.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/draw_key.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/fortify.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/geoms/__init__.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/guides/__init__.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/labeller.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/labels.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/layout.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/limits.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/position.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/py.typed +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/qplot.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/diamonds.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/economics.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/economics_long.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/faithfuld.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/luv_colours.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/midwest.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/mpg.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/msleep.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/presidential.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/seals.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/resources/txhousing.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/stat.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/stats/__init__.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/theme.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post5}/ggplot2_py/theme_defaults.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ggplot2-python
|
|
3
|
-
Version: 4.0.2.9000.
|
|
3
|
+
Version: 4.0.2.9000.post5
|
|
4
4
|
Summary: Python port of the R ggplot2 package (tracks R ggplot2 4.0.2.9000)
|
|
5
5
|
Project-URL: Homepage, https://github.com/Bio-Babel/ggplot2-python
|
|
6
6
|
Project-URL: Repository, https://github.com/Bio-Babel/ggplot2-python
|
|
@@ -48,17 +48,17 @@ Requires-Dist: mkdocs-material; extra == 'docs'
|
|
|
48
48
|
Requires-Dist: mkdocstrings[python]; extra == 'docs'
|
|
49
49
|
Description-Content-Type: text/markdown
|
|
50
50
|
|
|
51
|
-
#
|
|
51
|
+
# ggplot2-python <a href="https://github.com/Bio-Babel/ggplot2-python"><img src="assets/ggplot2_py_logo.png" align="right" height="138" alt="ggplot2-python logo" /></a>
|
|
52
52
|
|
|
53
53
|
[](https://pypi.org/project/ggplot2-python/)
|
|
54
54
|
|
|
55
|
-
AI-assisted
|
|
55
|
+
AI-assisted port of the python **ggplot2** package — Create Elegant Data Visualisations Using the Grammar of Graphics.
|
|
56
56
|
|
|
57
57
|
## Overview
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
ggplot2-python implements the grammar of graphics in Python, faithfully porting R's ggplot2 using pandas DataFrames as the data container and a Cairo-based rendering backend. It supports 47 geoms, 32 stats, faceting, coordinate systems, themes, guides, and 130+ scales.
|
|
60
60
|
|
|
61
|
-
Beyond a direct port,
|
|
61
|
+
Beyond a direct port, ggplot2-python adds **Python-exclusive features** that extend the Grammar of Graphics with Python-native idioms while preserving full orthogonality of GOG components.
|
|
62
62
|
|
|
63
63
|
## Python-Exclusive Features
|
|
64
64
|
|
|
@@ -73,6 +73,7 @@ These capabilities have no R equivalent and leverage Python-specific language fe
|
|
|
73
73
|
| **Auto-registration** | `__init_subclass__` | `class GeomStar(Geom): ...` auto-registers; no manual wiring needed |
|
|
74
74
|
| **Protocol contracts** | `typing.Protocol` | `isinstance(my_geom, GeomProtocol)` — structural type checking for extensions |
|
|
75
75
|
| **Scoped defaults** | `contextvars.ContextVar` | `with ggplot_defaults(theme=theme_minimal()): ...` — thread-safe scoped defaults |
|
|
76
|
+
| **Functional composition** | `sum` / `reduce` over `__add__` | `sum(parts, start=ggplot(data))` — compose plots without the `+` operator, useful for programmatic plot construction |
|
|
76
77
|
|
|
77
78
|
## Installation
|
|
78
79
|
|
|
@@ -85,7 +86,7 @@ For a local development:
|
|
|
85
86
|
|
|
86
87
|
```bash
|
|
87
88
|
git clone https://github.com/Bio-Babel/ggplot2-python.git
|
|
88
|
-
cd
|
|
89
|
+
cd ggplot2-python
|
|
89
90
|
pip install -e ".[dev]"
|
|
90
91
|
```
|
|
91
92
|
|
|
@@ -145,6 +146,46 @@ with ggplot_defaults(theme=theme_minimal()):
|
|
|
145
146
|
# Outside: no defaults
|
|
146
147
|
```
|
|
147
148
|
|
|
149
|
+
### Functional composition with `sum()` / `reduce()` (Python-exclusive)
|
|
150
|
+
|
|
151
|
+
The `+` operator is the canonical ggplot2 syntax. Because `GGPlot.__add__` is defined and every component family is registered with
|
|
152
|
+
the `update_ggplot` singledispatch generic, **Python's iterable-composition
|
|
153
|
+
idioms also work directly** — useful for programmatic plot building, list
|
|
154
|
+
comprehensions, or just function-style code:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
# 1) `sum(parts, start=ggplot(data))` — the canonical function-style form
|
|
158
|
+
def fnplot(data, *parts):
|
|
159
|
+
return sum(parts, start=ggplot(data))
|
|
160
|
+
|
|
161
|
+
fnplot(
|
|
162
|
+
mpg,
|
|
163
|
+
aes(x="displ", y="hwy", colour="class"),
|
|
164
|
+
geom_point(),
|
|
165
|
+
geom_smooth(method="lm"),
|
|
166
|
+
facet_wrap("drv"),
|
|
167
|
+
theme_minimal(),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# 2) `sum` over an iterable, no helper needed:
|
|
171
|
+
sum(
|
|
172
|
+
[aes(x="displ", y="hwy"), geom_point(), theme_minimal()],
|
|
173
|
+
start=ggplot(mpg),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# 3) `functools.reduce` — the canonical Python composition operator:
|
|
177
|
+
from functools import reduce
|
|
178
|
+
from operator import add
|
|
179
|
+
reduce(add, [aes(x="displ", y="hwy"), geom_point(), theme_minimal()], ggplot(mpg))
|
|
180
|
+
|
|
181
|
+
# 4) List on the RHS of `+` — recursive add via the list-dispatch:
|
|
182
|
+
ggplot(mpg, aes("displ", "hwy")) + [geom_point(), geom_smooth(), theme_minimal()]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
> One caveat: Python's built-in `sum` has the signature
|
|
186
|
+
> `sum(iterable, /, start=0)` — it accepts *one* iterable plus an optional
|
|
187
|
+
> `start`, **not** variadic arguments. `sum(a, b, c, d)` raises `TypeError`;
|
|
188
|
+
|
|
148
189
|
## Tutorials
|
|
149
190
|
|
|
150
191
|
### User Tutorials
|
|
@@ -160,11 +201,11 @@ with ggplot_defaults(theme=theme_minimal()):
|
|
|
160
201
|
- [Build Hooks](tutorials/build_hooks.ipynb) — intercepting the 16-stage build pipeline
|
|
161
202
|
|
|
162
203
|
### Developer Guide
|
|
163
|
-
- [Developer Guide: Extending
|
|
204
|
+
- [Developer Guide: Extending ggplot2-python](tutorials/developer_guide.ipynb) — comprehensive guide covering ggproto system, custom Stat/Geom creation, Protocol contracts, singledispatch, hooks, auto-registration, context manager, and packaging
|
|
164
205
|
|
|
165
206
|
## Extension Architecture
|
|
166
207
|
|
|
167
|
-
|
|
208
|
+
ggplot2-python is designed as an **extensible platform**. The following table summarises all extension points:
|
|
168
209
|
|
|
169
210
|
| Extension point | Mechanism | How to use |
|
|
170
211
|
|----------------|-----------|-----------|
|
|
@@ -177,5 +218,10 @@ ggplot2_py is designed as an **extensible platform**. The following table summar
|
|
|
177
218
|
| Custom `+` types | `@update_ggplot.register(MyClass)` | Register any Python class for the `+` operator |
|
|
178
219
|
| Custom plot types | `@ggplot_build.register(MyPlot)` | Override the entire build pipeline |
|
|
179
220
|
| Build hooks | `plot.add_build_hook(timing, stage, fn)` | Intercept data at any pipeline stage |
|
|
221
|
+
| Per-plot `+` hooks | `register_pre_add_hook(plot, hook)` | Per-plot transformer fires on the next `+`; supports stateful, self-removing hooks (R: `+.<dynamic_class>`) |
|
|
222
|
+
| Plot-env constructor injection | `plot.plot_env.push({...})` | Override `find_scale` / `add_defaults` per-plot — install a custom `scale_<aes>_<type>` constructor (R: `find_global`) |
|
|
223
|
+
| Instance-as-parent ggproto | `ggproto("Name", instance, method=fn)` | Clone a Geom/Stat/Scale instance with method overrides (R: `ggproto(NULL, inst, method = fn)`) |
|
|
224
|
+
| Explicit method binding | `bind_method(obj, name, fn)` | Bind an arbitrary callable as a method when its first arg isn't named `self` |
|
|
225
|
+
| Extension toolkit | `from ggplot2_py.extension import …` | Pre-built helpers (`clone_layer`, `rename_aes_in_*`, `protect`, `palette_for_aes`, …) for cross-cutting extensions |
|
|
180
226
|
| Protocol validation | `isinstance(obj, GeomProtocol)` | Verify structural conformance |
|
|
181
227
|
| Scoped defaults | `with ggplot_defaults(theme=...):` | Thread-safe scoped defaults |
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ggplot2-python <a href="https://github.com/Bio-Babel/ggplot2-python"><img src="assets/ggplot2_py_logo.png" align="right" height="138" alt="ggplot2-python logo" /></a>
|
|
2
2
|
|
|
3
3
|
[](https://pypi.org/project/ggplot2-python/)
|
|
4
4
|
|
|
5
|
-
AI-assisted
|
|
5
|
+
AI-assisted port of the python **ggplot2** package — Create Elegant Data Visualisations Using the Grammar of Graphics.
|
|
6
6
|
|
|
7
7
|
## Overview
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
ggplot2-python implements the grammar of graphics in Python, faithfully porting R's ggplot2 using pandas DataFrames as the data container and a Cairo-based rendering backend. It supports 47 geoms, 32 stats, faceting, coordinate systems, themes, guides, and 130+ scales.
|
|
10
10
|
|
|
11
|
-
Beyond a direct port,
|
|
11
|
+
Beyond a direct port, ggplot2-python adds **Python-exclusive features** that extend the Grammar of Graphics with Python-native idioms while preserving full orthogonality of GOG components.
|
|
12
12
|
|
|
13
13
|
## Python-Exclusive Features
|
|
14
14
|
|
|
@@ -23,6 +23,7 @@ These capabilities have no R equivalent and leverage Python-specific language fe
|
|
|
23
23
|
| **Auto-registration** | `__init_subclass__` | `class GeomStar(Geom): ...` auto-registers; no manual wiring needed |
|
|
24
24
|
| **Protocol contracts** | `typing.Protocol` | `isinstance(my_geom, GeomProtocol)` — structural type checking for extensions |
|
|
25
25
|
| **Scoped defaults** | `contextvars.ContextVar` | `with ggplot_defaults(theme=theme_minimal()): ...` — thread-safe scoped defaults |
|
|
26
|
+
| **Functional composition** | `sum` / `reduce` over `__add__` | `sum(parts, start=ggplot(data))` — compose plots without the `+` operator, useful for programmatic plot construction |
|
|
26
27
|
|
|
27
28
|
## Installation
|
|
28
29
|
|
|
@@ -35,7 +36,7 @@ For a local development:
|
|
|
35
36
|
|
|
36
37
|
```bash
|
|
37
38
|
git clone https://github.com/Bio-Babel/ggplot2-python.git
|
|
38
|
-
cd
|
|
39
|
+
cd ggplot2-python
|
|
39
40
|
pip install -e ".[dev]"
|
|
40
41
|
```
|
|
41
42
|
|
|
@@ -95,6 +96,46 @@ with ggplot_defaults(theme=theme_minimal()):
|
|
|
95
96
|
# Outside: no defaults
|
|
96
97
|
```
|
|
97
98
|
|
|
99
|
+
### Functional composition with `sum()` / `reduce()` (Python-exclusive)
|
|
100
|
+
|
|
101
|
+
The `+` operator is the canonical ggplot2 syntax. Because `GGPlot.__add__` is defined and every component family is registered with
|
|
102
|
+
the `update_ggplot` singledispatch generic, **Python's iterable-composition
|
|
103
|
+
idioms also work directly** — useful for programmatic plot building, list
|
|
104
|
+
comprehensions, or just function-style code:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
# 1) `sum(parts, start=ggplot(data))` — the canonical function-style form
|
|
108
|
+
def fnplot(data, *parts):
|
|
109
|
+
return sum(parts, start=ggplot(data))
|
|
110
|
+
|
|
111
|
+
fnplot(
|
|
112
|
+
mpg,
|
|
113
|
+
aes(x="displ", y="hwy", colour="class"),
|
|
114
|
+
geom_point(),
|
|
115
|
+
geom_smooth(method="lm"),
|
|
116
|
+
facet_wrap("drv"),
|
|
117
|
+
theme_minimal(),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# 2) `sum` over an iterable, no helper needed:
|
|
121
|
+
sum(
|
|
122
|
+
[aes(x="displ", y="hwy"), geom_point(), theme_minimal()],
|
|
123
|
+
start=ggplot(mpg),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# 3) `functools.reduce` — the canonical Python composition operator:
|
|
127
|
+
from functools import reduce
|
|
128
|
+
from operator import add
|
|
129
|
+
reduce(add, [aes(x="displ", y="hwy"), geom_point(), theme_minimal()], ggplot(mpg))
|
|
130
|
+
|
|
131
|
+
# 4) List on the RHS of `+` — recursive add via the list-dispatch:
|
|
132
|
+
ggplot(mpg, aes("displ", "hwy")) + [geom_point(), geom_smooth(), theme_minimal()]
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
> One caveat: Python's built-in `sum` has the signature
|
|
136
|
+
> `sum(iterable, /, start=0)` — it accepts *one* iterable plus an optional
|
|
137
|
+
> `start`, **not** variadic arguments. `sum(a, b, c, d)` raises `TypeError`;
|
|
138
|
+
|
|
98
139
|
## Tutorials
|
|
99
140
|
|
|
100
141
|
### User Tutorials
|
|
@@ -110,11 +151,11 @@ with ggplot_defaults(theme=theme_minimal()):
|
|
|
110
151
|
- [Build Hooks](tutorials/build_hooks.ipynb) — intercepting the 16-stage build pipeline
|
|
111
152
|
|
|
112
153
|
### Developer Guide
|
|
113
|
-
- [Developer Guide: Extending
|
|
154
|
+
- [Developer Guide: Extending ggplot2-python](tutorials/developer_guide.ipynb) — comprehensive guide covering ggproto system, custom Stat/Geom creation, Protocol contracts, singledispatch, hooks, auto-registration, context manager, and packaging
|
|
114
155
|
|
|
115
156
|
## Extension Architecture
|
|
116
157
|
|
|
117
|
-
|
|
158
|
+
ggplot2-python is designed as an **extensible platform**. The following table summarises all extension points:
|
|
118
159
|
|
|
119
160
|
| Extension point | Mechanism | How to use |
|
|
120
161
|
|----------------|-----------|-----------|
|
|
@@ -127,5 +168,10 @@ ggplot2_py is designed as an **extensible platform**. The following table summar
|
|
|
127
168
|
| Custom `+` types | `@update_ggplot.register(MyClass)` | Register any Python class for the `+` operator |
|
|
128
169
|
| Custom plot types | `@ggplot_build.register(MyPlot)` | Override the entire build pipeline |
|
|
129
170
|
| Build hooks | `plot.add_build_hook(timing, stage, fn)` | Intercept data at any pipeline stage |
|
|
171
|
+
| Per-plot `+` hooks | `register_pre_add_hook(plot, hook)` | Per-plot transformer fires on the next `+`; supports stateful, self-removing hooks (R: `+.<dynamic_class>`) |
|
|
172
|
+
| Plot-env constructor injection | `plot.plot_env.push({...})` | Override `find_scale` / `add_defaults` per-plot — install a custom `scale_<aes>_<type>` constructor (R: `find_global`) |
|
|
173
|
+
| Instance-as-parent ggproto | `ggproto("Name", instance, method=fn)` | Clone a Geom/Stat/Scale instance with method overrides (R: `ggproto(NULL, inst, method = fn)`) |
|
|
174
|
+
| Explicit method binding | `bind_method(obj, name, fn)` | Bind an arbitrary callable as a method when its first arg isn't named `self` |
|
|
175
|
+
| Extension toolkit | `from ggplot2_py.extension import …` | Pre-built helpers (`clone_layer`, `rename_aes_in_*`, `protect`, `palette_for_aes`, …) for cross-cutting extensions |
|
|
130
176
|
| Protocol validation | `isinstance(obj, GeomProtocol)` | Verify structural conformance |
|
|
131
177
|
| Scoped defaults | `with ggplot_defaults(theme=...):` | Thread-safe scoped defaults |
|
|
@@ -7,7 +7,7 @@ approach to creating statistical visualizations.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
__version__ = "4.0.2.9000.
|
|
10
|
+
__version__ = "4.0.2.9000.post5"
|
|
11
11
|
__r_commit__ = "c02c05a"
|
|
12
12
|
|
|
13
13
|
# ---------------------------------------------------------------------------
|
|
@@ -23,7 +23,10 @@ from ggplot2_py.ggproto import (
|
|
|
23
23
|
ggproto,
|
|
24
24
|
ggproto_parent,
|
|
25
25
|
is_ggproto,
|
|
26
|
+
fetch_ggproto,
|
|
27
|
+
bind_method,
|
|
26
28
|
)
|
|
29
|
+
from ggplot2_py._env import PlotEnv
|
|
27
30
|
|
|
28
31
|
# ---------------------------------------------------------------------------
|
|
29
32
|
# Aesthetics
|
|
@@ -62,6 +65,8 @@ from ggplot2_py.plot import (
|
|
|
62
65
|
ggplotGrob,
|
|
63
66
|
ggplot_add,
|
|
64
67
|
add_gg,
|
|
68
|
+
register_pre_add_hook,
|
|
69
|
+
unregister_pre_add_hook,
|
|
65
70
|
get_last_plot,
|
|
66
71
|
set_last_plot,
|
|
67
72
|
last_plot,
|
|
@@ -734,7 +739,8 @@ __all__ = [
|
|
|
734
739
|
# Version
|
|
735
740
|
"__version__",
|
|
736
741
|
# Core
|
|
737
|
-
"GGProto", "ggproto", "ggproto_parent", "is_ggproto",
|
|
742
|
+
"GGProto", "ggproto", "ggproto_parent", "is_ggproto", "fetch_ggproto",
|
|
743
|
+
"bind_method", "PlotEnv",
|
|
738
744
|
"Waiver", "waiver", "is_waiver",
|
|
739
745
|
# Aesthetics
|
|
740
746
|
"aes", "after_stat", "after_scale", "stage", "vars",
|
|
@@ -745,7 +751,9 @@ __all__ = [
|
|
|
745
751
|
"Layer", "layer", "is_layer",
|
|
746
752
|
# Plot
|
|
747
753
|
"ggplot", "is_ggplot", "ggplot_build", "ggplot_gtable", "ggplotGrob",
|
|
748
|
-
"ggplot_add", "add_gg",
|
|
754
|
+
"ggplot_add", "add_gg",
|
|
755
|
+
"register_pre_add_hook", "unregister_pre_add_hook",
|
|
756
|
+
"get_last_plot", "set_last_plot", "last_plot",
|
|
749
757
|
"print_plot", "get_alt_text", "update_ggplot",
|
|
750
758
|
# Introspection
|
|
751
759
|
"get_layer_data", "get_layer_grob", "get_panel_scales",
|
|
@@ -962,6 +970,11 @@ __all__ = [
|
|
|
962
970
|
"derive", "flip_data", "flipped_names", "has_flipped_aes",
|
|
963
971
|
# Plugin discovery
|
|
964
972
|
"discover_extensions", "list_extensions",
|
|
973
|
+
# Python-exclusive extension surface (README quickstart uses
|
|
974
|
+
# ``from ggplot2_py import *``; these would otherwise be invisible)
|
|
975
|
+
"ggplot_defaults", "BuildStage",
|
|
976
|
+
"GeomProtocol", "StatProtocol", "ScaleProtocol",
|
|
977
|
+
"CoordProtocol", "FacetProtocol", "PositionProtocol",
|
|
965
978
|
]
|
|
966
979
|
|
|
967
980
|
# ---------------------------------------------------------------------------
|
|
@@ -8,6 +8,8 @@ ggplot2 relies on, adapted for idiomatic Python usage.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import importlib
|
|
11
|
+
import logging
|
|
12
|
+
import sys
|
|
11
13
|
import warnings
|
|
12
14
|
from typing import Any, NoReturn, Optional
|
|
13
15
|
|
|
@@ -41,6 +43,40 @@ __all__ = [
|
|
|
41
43
|
# ---------------------------------------------------------------------------
|
|
42
44
|
# CLI messaging helpers (rlang / cli replacements)
|
|
43
45
|
# ---------------------------------------------------------------------------
|
|
46
|
+
#
|
|
47
|
+
# R distinguishes three output channels:
|
|
48
|
+
#
|
|
49
|
+
# * ``cli::cli_abort`` — error stream (``stop``) → Python ``raise``
|
|
50
|
+
# * ``cli::cli_warn`` — warning stream (``warning``) → ``warnings.warn(..., UserWarning)``
|
|
51
|
+
# * ``cli::cli_inform``— message stream (``message``) → ``logging.INFO``
|
|
52
|
+
#
|
|
53
|
+
# In R these are three separate streams: ``suppressWarnings`` does not
|
|
54
|
+
# silence messages, ``suppressMessages`` does not silence warnings, and
|
|
55
|
+
# the test framework's ``expect_message`` / ``expect_warning`` discriminate
|
|
56
|
+
# between them. This Python port matches that split — informational
|
|
57
|
+
# output (e.g. ``ggsave``'s "Saving …", ``stat_bin``'s "using bins = 30")
|
|
58
|
+
# routes through the standard :mod:`logging` module at INFO level so
|
|
59
|
+
# pytest does not count it as a warning, ``warnings.catch_warnings``
|
|
60
|
+
# does not capture it, and ``logging.getLogger("ggplot2_py").setLevel(
|
|
61
|
+
# logging.WARNING)`` mirrors R's ``suppressMessages``.
|
|
62
|
+
|
|
63
|
+
#: Package-level logger. Library convention says "do not configure the
|
|
64
|
+
#: root logger" — instead we attach a single :class:`logging.StreamHandler`
|
|
65
|
+
#: to *this* logger and set :attr:`Logger.propagate` to ``False``, so the
|
|
66
|
+
#: handler runs even when the user has not called
|
|
67
|
+
#: :func:`logging.basicConfig` and ours never duplicates onto theirs.
|
|
68
|
+
_logger: logging.Logger = logging.getLogger("ggplot2_py")
|
|
69
|
+
if not _logger.handlers:
|
|
70
|
+
_handler = logging.StreamHandler(stream=sys.stderr)
|
|
71
|
+
# R's ``message()`` prints the bare message text (no level/timestamp
|
|
72
|
+
# prefix); mirror that to keep ggplot2_py output indistinguishable
|
|
73
|
+
# from the R original.
|
|
74
|
+
_handler.setFormatter(logging.Formatter("%(message)s"))
|
|
75
|
+
_handler.setLevel(logging.INFO)
|
|
76
|
+
_logger.addHandler(_handler)
|
|
77
|
+
_logger.setLevel(logging.INFO)
|
|
78
|
+
_logger.propagate = False
|
|
79
|
+
|
|
44
80
|
|
|
45
81
|
def cli_abort(
|
|
46
82
|
message: str,
|
|
@@ -81,7 +117,13 @@ def cli_warn(
|
|
|
81
117
|
call: Optional[str] = None,
|
|
82
118
|
**kwargs: Any,
|
|
83
119
|
) -> None:
|
|
84
|
-
"""Issue a ``UserWarning``
|
|
120
|
+
"""Issue a ``UserWarning`` — R *warning* stream equivalent.
|
|
121
|
+
|
|
122
|
+
Counterpart of :func:`cli_inform`: this function targets the
|
|
123
|
+
Python warning stream (matching R ``cli::cli_warn`` / ``warning()``),
|
|
124
|
+
while informational output goes through :mod:`logging` at INFO
|
|
125
|
+
level. Keeping the two streams separate preserves R's
|
|
126
|
+
``suppressWarnings`` / ``suppressMessages`` granularity.
|
|
85
127
|
|
|
86
128
|
Parameters
|
|
87
129
|
----------
|
|
@@ -105,24 +147,39 @@ def cli_inform(
|
|
|
105
147
|
call: Optional[str] = None,
|
|
106
148
|
**kwargs: Any,
|
|
107
149
|
) -> None:
|
|
108
|
-
"""Emit an informational message
|
|
150
|
+
"""Emit an informational message at ``logging.INFO`` level.
|
|
151
|
+
|
|
152
|
+
R analogue: ``cli::cli_inform`` / ``rlang::inform`` write to R's
|
|
153
|
+
*message* stream — informational output that is **distinct from**
|
|
154
|
+
R's *warning* stream (see :func:`cli_warn`). Python's natural
|
|
155
|
+
equivalent is the :mod:`logging` module at INFO level:
|
|
109
156
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
``suppressMessages
|
|
157
|
+
* pytest does not count INFO records as warnings (its
|
|
158
|
+
``--strict-warnings`` flag and "warnings summary" only track
|
|
159
|
+
:mod:`warnings`);
|
|
160
|
+
* ``warnings.catch_warnings`` does not capture them;
|
|
161
|
+
* the user opt-out parallels R's ``suppressMessages`` —
|
|
162
|
+
``logging.getLogger("ggplot2_py").setLevel(logging.WARNING)``
|
|
163
|
+
silences cli_inform output without affecting cli_warn.
|
|
164
|
+
|
|
165
|
+
By default the package logger has a :class:`logging.StreamHandler`
|
|
166
|
+
attached at INFO level (R-faithful: ``cli_inform`` messages are
|
|
167
|
+
visible to stderr unless explicitly suppressed).
|
|
115
168
|
|
|
116
169
|
Parameters
|
|
117
170
|
----------
|
|
118
171
|
message : str
|
|
119
|
-
Informational message.
|
|
172
|
+
Informational message. May contain ``{name}``-style placeholders.
|
|
120
173
|
call : str, optional
|
|
121
174
|
Name of the calling function (unused; matches R signature).
|
|
122
175
|
**kwargs : Any
|
|
123
176
|
Substitution values for placeholders in *message*.
|
|
124
177
|
"""
|
|
125
|
-
|
|
178
|
+
try:
|
|
179
|
+
formatted = message.format(**kwargs) if kwargs else message
|
|
180
|
+
except (KeyError, IndexError):
|
|
181
|
+
formatted = message
|
|
182
|
+
_logger.info(formatted)
|
|
126
183
|
|
|
127
184
|
|
|
128
185
|
# ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PlotEnv — layered namespace lookup for scale/guide constructors.
|
|
3
|
+
|
|
4
|
+
Port of R's ``find_global(name, env, mode = "function")`` (R ref:
|
|
5
|
+
``ggplot2/R/scale-type.R:39-54``). Quoting the R source:
|
|
6
|
+
|
|
7
|
+
Look for object first in parent environment and if not found, then in
|
|
8
|
+
ggplot2 namespace environment. This makes it possible to override
|
|
9
|
+
default scales by setting them in the parent environment.
|
|
10
|
+
|
|
11
|
+
In R, ``plot@plot_env`` is the environment where the plot was constructed
|
|
12
|
+
(typically ``parent.frame()`` at ``ggplot()`` call time), and a chain of
|
|
13
|
+
environments terminates in ``asNamespace("ggplot2")``. ggnewscale
|
|
14
|
+
exploits this by **injecting** ``scale_<bumped_aes>_<type>`` functions
|
|
15
|
+
into ``plot@plot_env`` (R ref:
|
|
16
|
+
``ggnewscale/R/rename-aes.R:87-104``), so that when build-time
|
|
17
|
+
default-scale resolution runs, the injected constructor wins.
|
|
18
|
+
|
|
19
|
+
In Python, the closest equivalent is a list-of-namespaces walked in
|
|
20
|
+
order, with the ``ggplot2_py.scales`` module as the implicit fallback
|
|
21
|
+
layer. :class:`PlotEnv` encapsulates that — call sites pass a
|
|
22
|
+
``PlotEnv`` to :func:`find_scale` / ``ScalesList.add_defaults`` /
|
|
23
|
+
``ScalesList.add_missing``, and lookups walk the user-pushed layers
|
|
24
|
+
first.
|
|
25
|
+
|
|
26
|
+
A *namespace* is anything that responds to attribute access **or**
|
|
27
|
+
mapping ``__getitem__``. ``dict``, ``types.ModuleType``,
|
|
28
|
+
``types.SimpleNamespace``, ``argparse.Namespace``, and arbitrary
|
|
29
|
+
objects implementing ``__getattr__`` all work.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from typing import Any, Callable, List, Optional
|
|
35
|
+
|
|
36
|
+
__all__ = ["PlotEnv"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PlotEnv:
|
|
40
|
+
"""Layered scale/guide-constructor lookup table for a plot.
|
|
41
|
+
|
|
42
|
+
Mirrors the *effective* behaviour of R's ``plot@plot_env`` + the
|
|
43
|
+
``find_global`` walk: lookups proceed through the user-pushed
|
|
44
|
+
layers in **last-pushed-first-checked** order (LIFO), then fall
|
|
45
|
+
back to the ``ggplot2_py.scales`` module.
|
|
46
|
+
|
|
47
|
+
Each layer may be any object that supports either attribute access
|
|
48
|
+
(``getattr(layer, name)``) or mapping access (``layer[name]``); the
|
|
49
|
+
first that yields a non-``None`` value wins.
|
|
50
|
+
|
|
51
|
+
Examples
|
|
52
|
+
--------
|
|
53
|
+
>>> env = PlotEnv()
|
|
54
|
+
>>> env.push({"scale_colour_continuous": my_factory})
|
|
55
|
+
>>> env.lookup("scale_colour_continuous") is my_factory
|
|
56
|
+
True
|
|
57
|
+
|
|
58
|
+
The implicit fallback to ``ggplot2_py.scales`` is intentionally
|
|
59
|
+
**not** included in :attr:`layers` so that :meth:`clone` produces
|
|
60
|
+
an env with the same user-visible chain.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, *layers: Any) -> None:
|
|
64
|
+
# Layers are stored in **push order**. Lookup walks them in
|
|
65
|
+
# reverse so that the most recently pushed layer wins, matching
|
|
66
|
+
# R's environment-chain semantics (the immediate parent is
|
|
67
|
+
# checked before more ancestral ones).
|
|
68
|
+
self._layers: List[Any] = [l for l in layers if l is not None]
|
|
69
|
+
|
|
70
|
+
# -- Mutation ----------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
def push(self, layer: Any) -> None:
|
|
73
|
+
"""Add a new lookup layer. Last-pushed wins on conflicts.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
layer : object
|
|
78
|
+
Any dict-like or attribute-bearing namespace.
|
|
79
|
+
"""
|
|
80
|
+
if layer is None:
|
|
81
|
+
return
|
|
82
|
+
self._layers.append(layer)
|
|
83
|
+
|
|
84
|
+
def pop(self) -> Optional[Any]:
|
|
85
|
+
"""Remove and return the most-recently-pushed layer, or ``None``
|
|
86
|
+
if the chain is empty.
|
|
87
|
+
"""
|
|
88
|
+
if not self._layers:
|
|
89
|
+
return None
|
|
90
|
+
return self._layers.pop()
|
|
91
|
+
|
|
92
|
+
# -- Query -------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
def lookup(self, name: str) -> Optional[Callable]:
|
|
95
|
+
"""Resolve *name* through the layer chain, then the
|
|
96
|
+
``ggplot2_py.scales`` fallback module.
|
|
97
|
+
|
|
98
|
+
Returns the first hit, or ``None`` if nothing matches.
|
|
99
|
+
|
|
100
|
+
Notes
|
|
101
|
+
-----
|
|
102
|
+
The fallback layer is searched **last** even when the lookup
|
|
103
|
+
chain is non-empty — matching R's
|
|
104
|
+
``c(env, list(as_namespace("ggplot2")))`` (R ref:
|
|
105
|
+
``scale-type.R:44``).
|
|
106
|
+
"""
|
|
107
|
+
for layer in reversed(self._layers):
|
|
108
|
+
val = _ns_get(layer, name)
|
|
109
|
+
if val is not None:
|
|
110
|
+
return val
|
|
111
|
+
# Fallback: the package's own scale-constructor module. Import
|
|
112
|
+
# lazily to avoid an import cycle with scale.py at load time —
|
|
113
|
+
# the cycle only matters during module construction; at call
|
|
114
|
+
# time every module is loaded so the import never raises.
|
|
115
|
+
from ggplot2_py import scales as _scales_mod
|
|
116
|
+
return getattr(_scales_mod, name, None)
|
|
117
|
+
|
|
118
|
+
def __contains__(self, name: str) -> bool:
|
|
119
|
+
return self.lookup(name) is not None
|
|
120
|
+
|
|
121
|
+
# -- Plumbing ----------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def clone(self) -> "PlotEnv":
|
|
124
|
+
"""Return a new :class:`PlotEnv` with the same layer chain.
|
|
125
|
+
|
|
126
|
+
The layers themselves are **not** deep-copied — mirrors R env
|
|
127
|
+
semantics where ``plot_clone`` does not snapshot the
|
|
128
|
+
environment contents.
|
|
129
|
+
"""
|
|
130
|
+
new = PlotEnv()
|
|
131
|
+
new._layers = list(self._layers)
|
|
132
|
+
return new
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def layers(self) -> List[Any]:
|
|
136
|
+
"""Read-only view of the layer chain (in push order)."""
|
|
137
|
+
return list(self._layers)
|
|
138
|
+
|
|
139
|
+
def __repr__(self) -> str:
|
|
140
|
+
return f"<PlotEnv layers={len(self._layers)}>"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _ns_get(layer: Any, name: str) -> Any:
|
|
144
|
+
"""Resolve *name* on *layer*, trying mapping then attribute access.
|
|
145
|
+
|
|
146
|
+
Returns the found value, or ``None`` if absent. Catches only the
|
|
147
|
+
natural "key absent" / "unsupported indexing" cases — anything else
|
|
148
|
+
propagates so real bugs surface (per the project rule against
|
|
149
|
+
over-broad fallback logic).
|
|
150
|
+
"""
|
|
151
|
+
# Mapping path (dict and dict-like). ``str``/``bytes`` are
|
|
152
|
+
# excluded because their ``__getitem__`` is positional and would
|
|
153
|
+
# raise ``TypeError`` for non-integer keys.
|
|
154
|
+
if hasattr(layer, "__getitem__") and not isinstance(layer, (str, bytes)):
|
|
155
|
+
try:
|
|
156
|
+
return layer[name]
|
|
157
|
+
except (KeyError, TypeError):
|
|
158
|
+
pass
|
|
159
|
+
# Attribute path (modules, SimpleNamespace, regular objects).
|
|
160
|
+
return getattr(layer, name, None)
|
|
@@ -55,8 +55,8 @@ _PT: float = 72.27 / 25.4
|
|
|
55
55
|
# Helpers
|
|
56
56
|
# ---------------------------------------------------------------------------
|
|
57
57
|
|
|
58
|
-
def _unit_to_cm(u: Unit) -> float:
|
|
59
|
-
"""Convert a Unit (possibly compound/sum) to cm
|
|
58
|
+
def _unit_to_cm(u: Unit, axis: str = "height") -> float:
|
|
59
|
+
"""Convert a Unit (possibly compound/sum) to cm along *axis*.
|
|
60
60
|
|
|
61
61
|
``convert_height/width`` can't handle compound ``"sum"`` units
|
|
62
62
|
without a viewport context. This helper decomposes them into
|
|
@@ -70,8 +70,17 @@ def _unit_to_cm(u: Unit) -> float:
|
|
|
70
70
|
|
|
71
71
|
The ``data`` element is itself a multi-element Unit with the
|
|
72
72
|
individual operands.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
u : Unit
|
|
77
|
+
Unit to convert.
|
|
78
|
+
axis : {"height", "width"}
|
|
79
|
+
Axis to measure along — needed for viewport-relative units
|
|
80
|
+
(``"npc"``) which resolve differently per axis in non-square
|
|
81
|
+
viewports. Compound sums propagate the axis to children.
|
|
73
82
|
"""
|
|
74
|
-
from grid_py import convert_height
|
|
83
|
+
from grid_py import convert_height, convert_width
|
|
75
84
|
|
|
76
85
|
units = getattr(u, "_units", None)
|
|
77
86
|
values = getattr(u, "_values", None)
|
|
@@ -79,6 +88,8 @@ def _unit_to_cm(u: Unit) -> float:
|
|
|
79
88
|
if units is None or values is None:
|
|
80
89
|
return 0.0
|
|
81
90
|
|
|
91
|
+
fn = convert_width if axis == "width" else convert_height
|
|
92
|
+
|
|
82
93
|
total_cm = 0.0
|
|
83
94
|
n = len(u)
|
|
84
95
|
for i in range(n):
|
|
@@ -88,7 +99,7 @@ def _unit_to_cm(u: Unit) -> float:
|
|
|
88
99
|
# The operands are stored as a multi-element Unit in data[i]
|
|
89
100
|
inner = data[i] if data and i < len(data) else None
|
|
90
101
|
if inner is not None and isinstance(inner, Unit):
|
|
91
|
-
total_cm += _unit_to_cm(inner)
|
|
102
|
+
total_cm += _unit_to_cm(inner, axis)
|
|
92
103
|
continue
|
|
93
104
|
|
|
94
105
|
# Skip context-dependent units we can't resolve statically
|
|
@@ -99,7 +110,7 @@ def _unit_to_cm(u: Unit) -> float:
|
|
|
99
110
|
val = float(values[i]) if i < len(values) else 0.0
|
|
100
111
|
leaf = Unit(val, unit_type)
|
|
101
112
|
try:
|
|
102
|
-
cm =
|
|
113
|
+
cm = fn(leaf, "cm", valueOnly=True)
|
|
103
114
|
total_cm += float(np.sum(cm))
|
|
104
115
|
except Exception:
|
|
105
116
|
pass
|
|
@@ -125,12 +136,12 @@ def _width_cm(x: Any) -> float:
|
|
|
125
136
|
# For compound (sum) units, convert_width returns bogus results;
|
|
126
137
|
# decompose and convert leaf-by-leaf instead.
|
|
127
138
|
if _has_sum_unit(u):
|
|
128
|
-
return _unit_to_cm(u)
|
|
139
|
+
return _unit_to_cm(u, "width")
|
|
129
140
|
try:
|
|
130
141
|
result = convert_width(u, "cm", valueOnly=True)
|
|
131
142
|
return float(np.sum(result))
|
|
132
143
|
except Exception:
|
|
133
|
-
return _unit_to_cm(u)
|
|
144
|
+
return _unit_to_cm(u, "width")
|
|
134
145
|
|
|
135
146
|
|
|
136
147
|
def _height_cm(x: Any) -> float:
|
|
@@ -143,12 +154,12 @@ def _height_cm(x: Any) -> float:
|
|
|
143
154
|
else:
|
|
144
155
|
return 0.0
|
|
145
156
|
if _has_sum_unit(u):
|
|
146
|
-
return _unit_to_cm(u)
|
|
157
|
+
return _unit_to_cm(u, "height")
|
|
147
158
|
try:
|
|
148
159
|
result = convert_height(u, "cm", valueOnly=True)
|
|
149
160
|
return float(np.sum(result))
|
|
150
161
|
except Exception:
|
|
151
|
-
return _unit_to_cm(u)
|
|
162
|
+
return _unit_to_cm(u, "height")
|
|
152
163
|
|
|
153
164
|
|
|
154
165
|
# ---------------------------------------------------------------------------
|
|
@@ -681,7 +681,7 @@ def assemble_colourbar(
|
|
|
681
681
|
gt = gtable_add_grob(gt, label_tree, t=1, l=1, clip="off", name="labels")
|
|
682
682
|
|
|
683
683
|
# Add title
|
|
684
|
-
from ggplot2_py.
|
|
684
|
+
from ggplot2_py._guide_legend import add_legend_title
|
|
685
685
|
gt = add_legend_title(gt, title_grob, position="top")
|
|
686
686
|
|
|
687
687
|
# Add padding
|