ggplot2-python 4.0.2.9000.post3__tar.gz → 4.0.2.9000.post4__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.post4}/PKG-INFO +49 -8
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/README.md +48 -7
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/__init__.py +6 -1
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/plot.py +172 -40
- ggplot2_python-4.0.2.9000.post4/ggplot2_py/protocols.py +202 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/save.py +8 -3
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/scale.py +55 -4
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/scales/__init__.py +25 -3
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/pyproject.toml +1 -1
- 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.post4}/.gitattributes +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/.gitignore +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/LICENSE +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/_compat.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/_defaults.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/_make_constructor.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/_plugins.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/_utils.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/aes.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/annotation.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/autoplot.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/coord.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/coords/__init__.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/datasets.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/draw_key.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/facet.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/fortify.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/geom.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/geoms/__init__.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/ggproto.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/guide.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/guide_axis.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/guide_colourbar.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/guide_legend.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/guides/__init__.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/labeller.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/labels.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/layer.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/layout.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/limits.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/plot_render.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/position.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/py.typed +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/qplot.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/resources/diamonds.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/resources/economics.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/resources/economics_long.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/resources/faithfuld.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/resources/luv_colours.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/resources/midwest.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/resources/mpg.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/resources/msleep.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/resources/presidential.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/resources/seals.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/resources/txhousing.csv +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/stat.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/stats/__init__.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/theme.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/theme_defaults.py +0 -0
- {ggplot2_python-4.0.2.9000.post3 → ggplot2_python-4.0.2.9000.post4}/ggplot2_py/theme_elements.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.post4
|
|
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
|
|----------------|-----------|-----------|
|
|
@@ -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
|
|----------------|-----------|-----------|
|
|
@@ -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.post4"
|
|
11
11
|
__r_commit__ = "c02c05a"
|
|
12
12
|
|
|
13
13
|
# ---------------------------------------------------------------------------
|
|
@@ -962,6 +962,11 @@ __all__ = [
|
|
|
962
962
|
"derive", "flip_data", "flipped_names", "has_flipped_aes",
|
|
963
963
|
# Plugin discovery
|
|
964
964
|
"discover_extensions", "list_extensions",
|
|
965
|
+
# Python-exclusive extension surface (README quickstart uses
|
|
966
|
+
# ``from ggplot2_py import *``; these would otherwise be invisible)
|
|
967
|
+
"ggplot_defaults", "BuildStage",
|
|
968
|
+
"GeomProtocol", "StatProtocol", "ScaleProtocol",
|
|
969
|
+
"CoordProtocol", "FacetProtocol", "PositionProtocol",
|
|
965
970
|
]
|
|
966
971
|
|
|
967
972
|
# ---------------------------------------------------------------------------
|
|
@@ -17,12 +17,14 @@ from __future__ import annotations
|
|
|
17
17
|
import contextlib
|
|
18
18
|
import contextvars
|
|
19
19
|
import copy
|
|
20
|
+
import inspect
|
|
20
21
|
import warnings
|
|
21
22
|
from functools import singledispatch
|
|
22
23
|
from typing import (
|
|
23
24
|
Any,
|
|
24
25
|
Callable,
|
|
25
26
|
Dict,
|
|
27
|
+
Iterable,
|
|
26
28
|
List,
|
|
27
29
|
Optional,
|
|
28
30
|
Sequence,
|
|
@@ -142,6 +144,14 @@ def ggplot_defaults(
|
|
|
142
144
|
that apply to all :func:`ggplot` calls within the ``with`` block,
|
|
143
145
|
without affecting code outside.
|
|
144
146
|
|
|
147
|
+
The context defaults are applied at the **end** of the :func:`ggplot`
|
|
148
|
+
factory, after the plot's intrinsic defaults
|
|
149
|
+
(``CoordCartesian(default=True)``, ``FacetNull()``, empty :class:`Theme`)
|
|
150
|
+
are set. The context-provided ``coord`` is marked ``default=True`` so a
|
|
151
|
+
subsequent ``plot + coord_X()`` replaces it silently (matching R's
|
|
152
|
+
``update_ggplot.Coord`` semantics on default coords —
|
|
153
|
+
``plot-construction.R:200-215``).
|
|
154
|
+
|
|
145
155
|
Parameters
|
|
146
156
|
----------
|
|
147
157
|
theme : Theme or dict, optional
|
|
@@ -184,6 +194,42 @@ def _get_context_defaults() -> Dict[str, Any]:
|
|
|
184
194
|
return _ggplot_context.get()
|
|
185
195
|
|
|
186
196
|
|
|
197
|
+
def _apply_context_defaults(p: "GGPlot") -> None:
|
|
198
|
+
"""Overlay scoped defaults from :func:`ggplot_defaults` onto *p*.
|
|
199
|
+
|
|
200
|
+
Called from the :func:`ggplot` factory **after** the intrinsic defaults
|
|
201
|
+
(``CoordCartesian(default=True)``, ``FacetNull()``, empty :class:`Theme`)
|
|
202
|
+
have been installed, so context values can override them cleanly.
|
|
203
|
+
|
|
204
|
+
Each ctx value is shallow-copied before assignment so callers can reuse
|
|
205
|
+
the same ``coord_fixed()`` / ``facet_wrap(...)`` instance across multiple
|
|
206
|
+
``ggplot()`` calls without us mutating their object.
|
|
207
|
+
|
|
208
|
+
For ``coord``, ``default = True`` is preserved on the copy so that a
|
|
209
|
+
later ``+ coord_X()`` replaces it silently (matches R's
|
|
210
|
+
``update_ggplot.Coord`` short-circuit on default coords —
|
|
211
|
+
``plot-construction.R:202``).
|
|
212
|
+
"""
|
|
213
|
+
ctx = _get_context_defaults()
|
|
214
|
+
if not ctx:
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
if "theme" in ctx:
|
|
218
|
+
new_theme = ctx["theme"]
|
|
219
|
+
p.theme = new_theme.copy() if hasattr(new_theme, "copy") else new_theme
|
|
220
|
+
|
|
221
|
+
if "coord" in ctx:
|
|
222
|
+
p.coordinates = copy.copy(ctx["coord"])
|
|
223
|
+
p.coordinates.default = True
|
|
224
|
+
|
|
225
|
+
if "facet" in ctx:
|
|
226
|
+
p.facet = copy.copy(ctx["facet"])
|
|
227
|
+
|
|
228
|
+
if "mapping" in ctx:
|
|
229
|
+
ctx_map = ctx["mapping"]
|
|
230
|
+
p.mapping = aes(**{**ctx_map, **p.mapping})
|
|
231
|
+
|
|
232
|
+
|
|
187
233
|
# ---------------------------------------------------------------------------
|
|
188
234
|
# GGPlot class
|
|
189
235
|
# ---------------------------------------------------------------------------
|
|
@@ -249,19 +295,11 @@ class GGPlot:
|
|
|
249
295
|
self._meta: Dict[str, Any] = {}
|
|
250
296
|
self._build_hooks: Dict[Tuple[str, str], List[Callable]] = {}
|
|
251
297
|
|
|
252
|
-
#
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if "coord" in ctx and self.coordinates is None:
|
|
258
|
-
self.coordinates = ctx["coord"]
|
|
259
|
-
if "facet" in ctx and self.facet is None:
|
|
260
|
-
self.facet = ctx["facet"]
|
|
261
|
-
if "mapping" in ctx:
|
|
262
|
-
# Merge: context defaults as base, explicit mapping overrides
|
|
263
|
-
merged = aes(**{**ctx["mapping"], **self.mapping})
|
|
264
|
-
self.mapping = merged
|
|
298
|
+
# NOTE: scoped-default application (``ggplot_defaults``) is intentionally
|
|
299
|
+
# NOT done here. It happens in the :func:`ggplot` factory via
|
|
300
|
+
# :func:`_apply_context_defaults` so that direct ``GGPlot(...)`` calls
|
|
301
|
+
# and ``_clone()`` (which uses ``copy.copy`` and skips ``__init__``) are
|
|
302
|
+
# unaffected by global context.
|
|
265
303
|
|
|
266
304
|
# ------------------------------------------------------------------
|
|
267
305
|
# Clone
|
|
@@ -303,9 +341,14 @@ class GGPlot:
|
|
|
303
341
|
A :class:`BuildStage` constant (e.g.
|
|
304
342
|
``BuildStage.COMPUTE_STAT``).
|
|
305
343
|
fn : callable
|
|
306
|
-
``fn(data, **ctx) ->
|
|
307
|
-
per-layer data list. Return a new list to replace it
|
|
308
|
-
``None``
|
|
344
|
+
``fn(data, **ctx) -> list_or_anything``. Receives the current
|
|
345
|
+
per-layer data list. Return a new ``list`` to replace it; any
|
|
346
|
+
non-list return (including ``None``) leaves the data unchanged.
|
|
347
|
+
``**ctx`` carries stage-specific context (``layout``, ``scales``,
|
|
348
|
+
``guides``, ``theme``) — see :class:`BuildStage` for the per-stage
|
|
349
|
+
table. Hook signatures are introspected, so you may declare
|
|
350
|
+
only the kwargs you want (``def fn(data, layout=None)``) or use
|
|
351
|
+
``**kw`` to receive everything.
|
|
309
352
|
|
|
310
353
|
Returns
|
|
311
354
|
-------
|
|
@@ -535,6 +578,11 @@ def ggplot(
|
|
|
535
578
|
p.coordinates.default = True
|
|
536
579
|
p.facet = FacetNull()
|
|
537
580
|
|
|
581
|
+
# Scoped defaults (``ggplot_defaults`` context manager) overlay the
|
|
582
|
+
# intrinsic defaults set above. No-op when no context is active, which
|
|
583
|
+
# preserves byte-level R parity for the bare ``ggplot(df, aes(...))`` case.
|
|
584
|
+
_apply_context_defaults(p)
|
|
585
|
+
|
|
538
586
|
# R parity: ``plot$labels`` stores ONLY user-set labels. The
|
|
539
587
|
# aesthetic-derived defaults (``x="carat"``, ``y="price"`` etc.)
|
|
540
588
|
# are computed lazily at render time by ``_setup_plot_labels``
|
|
@@ -596,14 +644,45 @@ class BuildStage:
|
|
|
596
644
|
These are used with :meth:`GGPlot.add_build_hook` to register
|
|
597
645
|
before/after callbacks on specific pipeline stages. This is a
|
|
598
646
|
**Python-exclusive** extension point — R's ggplot2 does not expose
|
|
599
|
-
hooks on individual build stages
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
647
|
+
hooks on individual build stages — but **every stage name here
|
|
648
|
+
corresponds to a real operation in R's** ``plot-build.R``
|
|
649
|
+
``ggplot_build.ggplot()`` method, in the same order.
|
|
650
|
+
|
|
651
|
+
Per-stage available ctx kwargs
|
|
652
|
+
------------------------------
|
|
653
|
+
The hook receives ``(data, **ctx)``. ``data`` is always the current
|
|
654
|
+
per-layer data list. ``**ctx`` carries side context whose set depends
|
|
655
|
+
on the stage:
|
|
656
|
+
|
|
657
|
+
===================== =================================================
|
|
658
|
+
Stage ctx keys
|
|
659
|
+
===================== =================================================
|
|
660
|
+
LAYER_DATA (none)
|
|
661
|
+
SETUP_LAYER (none)
|
|
662
|
+
SETUP_LAYOUT ``layout``
|
|
663
|
+
COMPUTE_AESTHETICS (none)
|
|
664
|
+
TRANSFORM_SCALES ``scales``
|
|
665
|
+
TRAIN_POSITION ``layout``, ``scales``
|
|
666
|
+
COMPUTE_STAT ``layout``
|
|
667
|
+
MAP_STAT (none)
|
|
668
|
+
COMPUTE_GEOM_1 (none)
|
|
669
|
+
COMPUTE_POSITION ``layout``
|
|
670
|
+
RETRAIN_POSITION ``layout``, ``scales``
|
|
671
|
+
SETUP_GUIDES ``layout``, ``guides``
|
|
672
|
+
TRAIN_NONPOSITION ``scales`` (the non-position ScalesList)
|
|
673
|
+
COMPUTE_GEOM_2 ``theme``
|
|
674
|
+
FINISH_STAT (none)
|
|
675
|
+
FINISH_DATA (none)
|
|
676
|
+
===================== =================================================
|
|
677
|
+
|
|
678
|
+
Hook signature flexibility — :func:`_run_hooks` introspects each hook
|
|
679
|
+
and forwards only ctx kwargs the hook can accept, so all of these
|
|
680
|
+
coexist::
|
|
681
|
+
|
|
682
|
+
p.add_build_hook("after", BuildStage.TRAIN_POSITION, lambda data: ...)
|
|
683
|
+
p.add_build_hook("after", BuildStage.TRAIN_POSITION, lambda data, **kw: ...)
|
|
684
|
+
p.add_build_hook("after", BuildStage.TRAIN_POSITION,
|
|
685
|
+
lambda data, layout=None, scales=None: ...)
|
|
607
686
|
"""
|
|
608
687
|
|
|
609
688
|
LAYER_DATA = "layer_data"
|
|
@@ -624,6 +703,26 @@ class BuildStage:
|
|
|
624
703
|
FINISH_DATA = "finish_data"
|
|
625
704
|
|
|
626
705
|
|
|
706
|
+
def _hook_accepts(hook: Callable, ctx_keys: Iterable[str]) -> Dict[str, bool]:
|
|
707
|
+
"""Return a ``{ctx_key: accepted}`` map for *hook*.
|
|
708
|
+
|
|
709
|
+
A key is accepted if the hook either declares it as a named parameter or
|
|
710
|
+
declares a ``**kwargs`` catch-all. Used by :func:`_run_hooks` to forward
|
|
711
|
+
only the ctx kwargs the hook actually wants — this is what lets
|
|
712
|
+
``lambda data: ...`` (old-style) and ``lambda data, **ctx: ...`` and
|
|
713
|
+
``def f(data, layout=None): ...`` all coexist without TypeErrors and
|
|
714
|
+
without resorting to ``try/except``.
|
|
715
|
+
"""
|
|
716
|
+
sig = inspect.signature(hook)
|
|
717
|
+
params = sig.parameters
|
|
718
|
+
has_var_keyword = any(
|
|
719
|
+
p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values()
|
|
720
|
+
)
|
|
721
|
+
if has_var_keyword:
|
|
722
|
+
return {k: True for k in ctx_keys}
|
|
723
|
+
return {k: (k in params) for k in ctx_keys}
|
|
724
|
+
|
|
725
|
+
|
|
627
726
|
def _run_hooks(
|
|
628
727
|
plot: GGPlot,
|
|
629
728
|
timing: str,
|
|
@@ -633,6 +732,21 @@ def _run_hooks(
|
|
|
633
732
|
) -> List[Any]:
|
|
634
733
|
"""Execute registered build hooks for (*timing*, *stage*).
|
|
635
734
|
|
|
735
|
+
Each hook is invoked as ``hook(data, **selected_ctx)`` where
|
|
736
|
+
``selected_ctx`` is the subset of *ctx* that the hook actually accepts
|
|
737
|
+
(named param or ``**kwargs``). This keeps three call styles working
|
|
738
|
+
in parallel:
|
|
739
|
+
|
|
740
|
+
* ``lambda data: ...`` — no ctx forwarded
|
|
741
|
+
* ``lambda data, **kw: ...`` — every ctx kwarg forwarded
|
|
742
|
+
* ``def f(data, layout=None, scales=None)`` — only matching kwargs
|
|
743
|
+
|
|
744
|
+
Hooks may return a new per-layer data list (an actual ``list``) to
|
|
745
|
+
replace the pipeline data, or any non-list value (including ``None``)
|
|
746
|
+
to leave it unchanged. The list-only test means hooks that accidentally
|
|
747
|
+
return a scalar (``True``, the value from ``log.setdefault``, etc.)
|
|
748
|
+
don't silently corrupt downstream stages.
|
|
749
|
+
|
|
636
750
|
Parameters
|
|
637
751
|
----------
|
|
638
752
|
plot : GGPlot
|
|
@@ -644,7 +758,8 @@ def _run_hooks(
|
|
|
644
758
|
data : list
|
|
645
759
|
Current per-layer data list.
|
|
646
760
|
**ctx
|
|
647
|
-
Additional context
|
|
761
|
+
Additional context. The set of keys available depends on the
|
|
762
|
+
stage — see :class:`BuildStage` for the per-stage table.
|
|
648
763
|
|
|
649
764
|
Returns
|
|
650
765
|
-------
|
|
@@ -654,9 +769,14 @@ def _run_hooks(
|
|
|
654
769
|
hooks = getattr(plot, "_build_hooks", None)
|
|
655
770
|
if not hooks:
|
|
656
771
|
return data
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
772
|
+
targets = hooks.get((timing, stage), [])
|
|
773
|
+
if not targets:
|
|
774
|
+
return data
|
|
775
|
+
for hook in targets:
|
|
776
|
+
wanted = _hook_accepts(hook, ctx.keys())
|
|
777
|
+
kwargs = {k: v for k, v in ctx.items() if wanted.get(k, False)}
|
|
778
|
+
result = hook(data, **kwargs)
|
|
779
|
+
if isinstance(result, list):
|
|
660
780
|
data = result
|
|
661
781
|
return data
|
|
662
782
|
|
|
@@ -776,9 +896,11 @@ def _build_ggplot(plot):
|
|
|
776
896
|
data = by_layer(lambda l, d: l.setup_layer(d, plot), layers, data, "setting up layer")
|
|
777
897
|
data = _h(plot, "after", S.SETUP_LAYER, data)
|
|
778
898
|
|
|
779
|
-
# --- Setup layout ---
|
|
899
|
+
# --- Setup layout --- (R: plot-build.R:62 ``layout$setup``)
|
|
780
900
|
layout = create_layout(plot.facet, plot.coordinates, getattr(plot, "layout", None))
|
|
901
|
+
data = _h(plot, "before", S.SETUP_LAYOUT, data, layout=layout)
|
|
781
902
|
data = layout.setup(data, plot.data if isinstance(plot.data, pd.DataFrame) else pd.DataFrame(), plot.plot_env)
|
|
903
|
+
data = _h(plot, "after", S.SETUP_LAYOUT, data, layout=layout)
|
|
782
904
|
|
|
783
905
|
# --- Compute aesthetics ---
|
|
784
906
|
data = _h(plot, "before", S.COMPUTE_AESTHETICS, data)
|
|
@@ -793,21 +915,25 @@ def _build_ggplot(plot):
|
|
|
793
915
|
# --- Setup plot labels ---
|
|
794
916
|
_setup_plot_labels(plot, layers, data)
|
|
795
917
|
|
|
796
|
-
# --- Transform scales ---
|
|
918
|
+
# --- Transform scales --- (R: plot-build.R:70 ``lapply(data, scales$transform_df)``)
|
|
919
|
+
data = _h(plot, "before", S.TRANSFORM_SCALES, data, scales=scales)
|
|
797
920
|
for i in range(len(data)):
|
|
798
921
|
if data[i] is not None and not data[i].empty:
|
|
799
922
|
data[i] = scales.transform_df(data[i])
|
|
923
|
+
data = _h(plot, "after", S.TRANSFORM_SCALES, data, scales=scales)
|
|
800
924
|
|
|
801
|
-
# --- Train and map positions ---
|
|
925
|
+
# --- Train and map positions --- (R: plot-build.R:77-78)
|
|
802
926
|
scale_x = scales.get_scales("x")
|
|
803
927
|
scale_y = scales.get_scales("y")
|
|
928
|
+
data = _h(plot, "before", S.TRAIN_POSITION, data, layout=layout, scales=scales)
|
|
804
929
|
layout.train_position(data, scale_x, scale_y)
|
|
805
930
|
data = layout.map_position(data)
|
|
931
|
+
data = _h(plot, "after", S.TRAIN_POSITION, data, layout=layout, scales=scales)
|
|
806
932
|
|
|
807
933
|
# --- Compute statistics ---
|
|
808
|
-
data = _h(plot, "before", S.COMPUTE_STAT, data)
|
|
934
|
+
data = _h(plot, "before", S.COMPUTE_STAT, data, layout=layout)
|
|
809
935
|
data = by_layer(lambda l, d: l.compute_statistic(d, layout), layers, data, "computing stat")
|
|
810
|
-
data = _h(plot, "after", S.COMPUTE_STAT, data)
|
|
936
|
+
data = _h(plot, "after", S.COMPUTE_STAT, data, layout=layout)
|
|
811
937
|
|
|
812
938
|
# --- Map statistics ---
|
|
813
939
|
data = _h(plot, "before", S.MAP_STAT, data)
|
|
@@ -834,20 +960,24 @@ def _build_ggplot(plot):
|
|
|
834
960
|
data = _h(plot, "after", S.COMPUTE_GEOM_1, data)
|
|
835
961
|
|
|
836
962
|
# --- Compute position ---
|
|
837
|
-
data = _h(plot, "before", S.COMPUTE_POSITION, data)
|
|
963
|
+
data = _h(plot, "before", S.COMPUTE_POSITION, data, layout=layout)
|
|
838
964
|
data = by_layer(lambda l, d: l.compute_position(d, layout), layers, data, "computing position")
|
|
839
|
-
data = _h(plot, "after", S.COMPUTE_POSITION, data)
|
|
965
|
+
data = _h(plot, "after", S.COMPUTE_POSITION, data, layout=layout)
|
|
840
966
|
|
|
841
|
-
# --- Reset and retrain position scales ---
|
|
967
|
+
# --- Reset and retrain position scales --- (R: plot-build.R:99-101)
|
|
842
968
|
scale_x = scales.get_scales("x")
|
|
843
969
|
scale_y = scales.get_scales("y")
|
|
970
|
+
data = _h(plot, "before", S.RETRAIN_POSITION, data, layout=layout, scales=scales)
|
|
844
971
|
layout.reset_scales()
|
|
845
972
|
layout.train_position(data, scale_x, scale_y)
|
|
846
973
|
layout.setup_panel_params()
|
|
847
974
|
data = layout.map_position(data)
|
|
975
|
+
data = _h(plot, "after", S.RETRAIN_POSITION, data, layout=layout, scales=scales)
|
|
848
976
|
|
|
849
|
-
# --- Setup panel guides ---
|
|
977
|
+
# --- Setup panel guides --- (R: plot-build.R:104 ``layout$setup_panel_guides``)
|
|
978
|
+
data = _h(plot, "before", S.SETUP_GUIDES, data, layout=layout, guides=plot.guides)
|
|
850
979
|
layout.setup_panel_guides(plot.guides, plot.layers)
|
|
980
|
+
data = _h(plot, "after", S.SETUP_GUIDES, data, layout=layout, guides=plot.guides)
|
|
851
981
|
|
|
852
982
|
# --- Complete theme ---
|
|
853
983
|
# R: plot@theme <- plot_theme(plot) (plot-build.R:107)
|
|
@@ -856,14 +986,16 @@ def _build_ggplot(plot):
|
|
|
856
986
|
# ``calc_element`` then returns ``None`` (no bg, no grid, no titles).
|
|
857
987
|
plot.theme = complete_theme(plot.theme)
|
|
858
988
|
|
|
859
|
-
# --- Train non-position scales and guides ---
|
|
989
|
+
# --- Train non-position scales and guides --- (R: plot-build.R:113 ``lapply(data, npscales$train_df)``)
|
|
860
990
|
npscales = scales.non_position_scales()
|
|
861
991
|
if npscales.n() > 0:
|
|
862
992
|
if hasattr(npscales, "set_palettes"):
|
|
863
993
|
npscales.set_palettes(plot.theme)
|
|
994
|
+
data = _h(plot, "before", S.TRAIN_NONPOSITION, data, scales=npscales)
|
|
864
995
|
for d in data:
|
|
865
996
|
if d is not None:
|
|
866
997
|
npscales.train_df(d)
|
|
998
|
+
data = _h(plot, "after", S.TRAIN_NONPOSITION, data, scales=npscales)
|
|
867
999
|
if plot.guides is not None and hasattr(plot.guides, "build"):
|
|
868
1000
|
plot.guides = plot.guides.build(npscales, plot.layers, plot.labels, data, plot.theme)
|
|
869
1001
|
for i in range(len(data)):
|
|
@@ -874,9 +1006,9 @@ def _build_ggplot(plot):
|
|
|
874
1006
|
plot.guides = plot.guides.get_custom()
|
|
875
1007
|
|
|
876
1008
|
# --- Compute geom 2 ---
|
|
877
|
-
data = _h(plot, "before", S.COMPUTE_GEOM_2, data)
|
|
1009
|
+
data = _h(plot, "before", S.COMPUTE_GEOM_2, data, theme=plot.theme)
|
|
878
1010
|
data = by_layer(lambda l, d: l.compute_geom_2(d, theme=plot.theme), layers, data, "setting up geom aesthetics")
|
|
879
|
-
data = _h(plot, "after", S.COMPUTE_GEOM_2, data)
|
|
1011
|
+
data = _h(plot, "after", S.COMPUTE_GEOM_2, data, theme=plot.theme)
|
|
880
1012
|
|
|
881
1013
|
# --- Finish statistics ---
|
|
882
1014
|
data = _h(plot, "before", S.FINISH_STAT, data)
|