plot3 0.6.1__tar.gz → 0.6.3__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.
- {plot3-0.6.1 → plot3-0.6.3}/PKG-INFO +36 -25
- {plot3-0.6.1 → plot3-0.6.3}/README.md +33 -16
- plot3-0.6.3/plot3/__version__.py +1 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/build.py +45 -2
- {plot3-0.6.1 → plot3-0.6.3}/plot3/contour.py +2 -2
- {plot3-0.6.1 → plot3-0.6.3}/plot3/ggplot.py +3 -3
- {plot3-0.6.1 → plot3-0.6.3}/plot3/static.py +9 -7
- {plot3-0.6.1 → plot3-0.6.3}/plot3.egg-info/PKG-INFO +36 -25
- {plot3-0.6.1 → plot3-0.6.3}/plot3.egg-info/requires.txt +1 -10
- {plot3-0.6.1 → plot3-0.6.3}/pyproject.toml +1 -4
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_contour.py +1 -1
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_grammar.py +42 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_static_save.py +2 -2
- plot3-0.6.1/plot3/__version__.py +0 -1
- {plot3-0.6.1 → plot3-0.6.3}/LICENSE +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/__init__.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/aesexpr.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/calculus.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/compose.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/craft.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/encode.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/expr.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/flip.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/function.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/geoms.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/io.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/jupyter.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/latexin.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/masking.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/mathtext.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/payload.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/remote.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/scales.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/scaling.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/special.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/stat2d.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/stats3d.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/table.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/themes.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3/viewer.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3.egg-info/SOURCES.txt +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3.egg-info/dependency_links.txt +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/plot3.egg-info/top_level.txt +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/setup.cfg +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_2d_density.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_3d.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_animation.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_annotate.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_api_public.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_array_backend.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_arrow.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_build_contract.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_everyday_ggplot2.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_expr.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_facets_compose.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_geom_function.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_geoms_ds.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_ggplot2_extras2.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_ggplot2_parity.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_ggplot_extras.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_latex_input.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_lidar_scene.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_masking.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_math_layers.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_mathtext.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_payload.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_payload_display.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_point_cloud.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_r_oracle_parity.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_readme.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_remote_bridge.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_review_fixes.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_save_menu.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_scales_api.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_scales_themes.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_special.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_stat2d.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_stats_semantic.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_step3_geoms.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_table_backends.py +0 -0
- {plot3-0.6.1 → plot3-0.6.3}/tests/test_titles_theme.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plot3
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
4
4
|
Summary: ggplot2 grammar of graphics for Python: interactive WebGL figures and journal-ready PNG, SVG, and PDF
|
|
5
5
|
Author: Rigoberto Leyva Salmeron
|
|
6
6
|
License-Expression: MIT
|
|
@@ -17,18 +17,12 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: numpy>=1.24
|
|
19
19
|
Requires-Dist: pandas>=2.0
|
|
20
|
-
Provides-Extra:
|
|
21
|
-
Requires-Dist:
|
|
20
|
+
Provides-Extra: pdf
|
|
21
|
+
Requires-Dist: cairosvg>=2.7; extra == "pdf"
|
|
22
22
|
Provides-Extra: dev
|
|
23
23
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
24
24
|
Requires-Dist: polars>=1.0; extra == "dev"
|
|
25
25
|
Requires-Dist: contourpy>=1.0; extra == "dev"
|
|
26
|
-
Provides-Extra: jupyter
|
|
27
|
-
Requires-Dist: ipython>=8.0; extra == "jupyter"
|
|
28
|
-
Provides-Extra: polars
|
|
29
|
-
Requires-Dist: polars>=1.0; extra == "polars"
|
|
30
|
-
Provides-Extra: export
|
|
31
|
-
Requires-Dist: cairosvg>=2.7; extra == "export"
|
|
32
26
|
Dynamic: license-file
|
|
33
27
|
|
|
34
28
|
# plot3
|
|
@@ -57,7 +51,7 @@ in the notebook and save as journal-ready PNG, SVG, or PDF.
|
|
|
57
51
|
</table>
|
|
58
52
|
|
|
59
53
|
```bash
|
|
60
|
-
pip install
|
|
54
|
+
pip install plot3
|
|
61
55
|
```
|
|
62
56
|
|
|
63
57
|
Contents: [Plot an equation](#plot-an-equation) ·
|
|
@@ -224,18 +218,27 @@ ggplot2's names and defaults.
|
|
|
224
218
|
|
|
225
219
|
```python
|
|
226
220
|
rng = np.random.default_rng(1)
|
|
227
|
-
|
|
221
|
+
arms = ["placebo", "low", "high"]
|
|
222
|
+
trial = pd.DataFrame({"arm": pd.Categorical(rng.choice(arms, 150), categories=arms),
|
|
228
223
|
"dose": rng.uniform(0, 10, 150)})
|
|
229
|
-
|
|
224
|
+
slope = trial.arm.map({"placebo": 0.05, "low": 0.35, "high": 0.7}).astype(float)
|
|
225
|
+
trial["response"] = 2 + slope * trial.dose + rng.normal(0, 0.9, 150)
|
|
230
226
|
|
|
231
227
|
a = (ggplot(trial, aes(x="dose", y="response", colour="arm"))
|
|
232
|
-
+ geom_point(alpha=0.7) + geom_smooth(method="lm")
|
|
228
|
+
+ geom_point(alpha=0.7) + geom_smooth(method="lm", se=False)
|
|
229
|
+
+ scale_colour_hue() + theme_grey() + theme(legend_position="bottom"))
|
|
233
230
|
b = (ggplot(trial, aes(x="arm", y="response", fill="arm"))
|
|
234
|
-
+ geom_violin() + geom_jitter(width=0.1, height=0)
|
|
235
|
-
|
|
231
|
+
+ geom_violin() + geom_jitter(width=0.1, height=0)
|
|
232
|
+
+ scale_fill_hue() + theme_grey() + theme(legend_position="none"))
|
|
233
|
+
fig = ((a | b) + plot_layout(widths=[3, 2])
|
|
234
|
+
+ plot_annotation(title="Response by dose and arm", tag_levels="A"))
|
|
236
235
|
```
|
|
237
236
|
|
|
238
|
-
|
|
237
|
+
`scale_colour_hue()` and `theme_grey()` are ggplot2's own defaults, so the
|
|
238
|
+
figure looks as it would from R. plot3's defaults are `theme_bw()` for saved
|
|
239
|
+
files and `theme_dark()` in the viewer.
|
|
240
|
+
|
|
241
|
+
<img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/ggplot2_look.png" alt="Two-panel figure in ggplot2's grey theme: response against dose with one fitted line per arm, and violins of response by arm">
|
|
239
242
|
|
|
240
243
|
Everything else you'd expect is there, including histograms, densities,
|
|
241
244
|
boxplots, bar positions, `stat_summary`, error bars, heatmaps, hexbins, 2D
|
|
@@ -270,27 +273,35 @@ In notebooks, bare column names work as in R: `aes(x=dose, y=response)`.
|
|
|
270
273
|
## Install
|
|
271
274
|
|
|
272
275
|
```bash
|
|
273
|
-
pip install
|
|
276
|
+
pip install plot3
|
|
274
277
|
```
|
|
275
278
|
|
|
276
279
|
Python 3.10 or newer; NumPy and pandas are the only required packages.
|
|
280
|
+
Notebooks, pandas, and Polars tables work with no extras.
|
|
281
|
+
|
|
282
|
+
For PDF, and PNG with real fonts, add cairosvg and the Cairo library. SVG
|
|
283
|
+
needs neither.
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
pip install cairosvg
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
brew install cairo
|
|
291
|
+
```
|
|
277
292
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
| `export` | `cairosvg`: PNG with real fonts and PDF. Needs the Cairo C library (`brew install cairo`, `apt install libcairo2`). SVG needs nothing. |
|
|
281
|
-
| `fast` | `contourpy`: faster implicit-curve contours (matplotlib users have it) |
|
|
282
|
-
| `jupyter` | IPython integration (bare column names, `%plot3`) |
|
|
283
|
-
| `polars` | Polars tables |
|
|
293
|
+
(`apt install libcairo2` on Debian and Ubuntu.) `pip install "plot3[pdf]"`
|
|
294
|
+
installs plot3 and cairosvg together.
|
|
284
295
|
|
|
285
296
|
The latest unreleased code installs from GitHub with
|
|
286
|
-
`pip install "plot3
|
|
297
|
+
`pip install "plot3 @ git+https://github.com/rleyvasal/plot3"`.
|
|
287
298
|
|
|
288
299
|
## Development
|
|
289
300
|
|
|
290
301
|
```bash
|
|
291
302
|
git clone https://github.com/rleyvasal/plot3 && cd plot3
|
|
292
303
|
python3 -m venv .venv && source .venv/bin/activate
|
|
293
|
-
pip install -e ".[dev
|
|
304
|
+
pip install -e ".[dev]"
|
|
294
305
|
pytest -q # includes every example in this README and docs/reference.md
|
|
295
306
|
python examples/showcase_2d.py # 2D gallery in the browser
|
|
296
307
|
python examples/showcase_3d.py # 3D gallery
|
|
@@ -24,7 +24,7 @@ in the notebook and save as journal-ready PNG, SVG, or PDF.
|
|
|
24
24
|
</table>
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
pip install
|
|
27
|
+
pip install plot3
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
Contents: [Plot an equation](#plot-an-equation) ·
|
|
@@ -191,18 +191,27 @@ ggplot2's names and defaults.
|
|
|
191
191
|
|
|
192
192
|
```python
|
|
193
193
|
rng = np.random.default_rng(1)
|
|
194
|
-
|
|
194
|
+
arms = ["placebo", "low", "high"]
|
|
195
|
+
trial = pd.DataFrame({"arm": pd.Categorical(rng.choice(arms, 150), categories=arms),
|
|
195
196
|
"dose": rng.uniform(0, 10, 150)})
|
|
196
|
-
|
|
197
|
+
slope = trial.arm.map({"placebo": 0.05, "low": 0.35, "high": 0.7}).astype(float)
|
|
198
|
+
trial["response"] = 2 + slope * trial.dose + rng.normal(0, 0.9, 150)
|
|
197
199
|
|
|
198
200
|
a = (ggplot(trial, aes(x="dose", y="response", colour="arm"))
|
|
199
|
-
+ geom_point(alpha=0.7) + geom_smooth(method="lm")
|
|
201
|
+
+ geom_point(alpha=0.7) + geom_smooth(method="lm", se=False)
|
|
202
|
+
+ scale_colour_hue() + theme_grey() + theme(legend_position="bottom"))
|
|
200
203
|
b = (ggplot(trial, aes(x="arm", y="response", fill="arm"))
|
|
201
|
-
+ geom_violin() + geom_jitter(width=0.1, height=0)
|
|
202
|
-
|
|
204
|
+
+ geom_violin() + geom_jitter(width=0.1, height=0)
|
|
205
|
+
+ scale_fill_hue() + theme_grey() + theme(legend_position="none"))
|
|
206
|
+
fig = ((a | b) + plot_layout(widths=[3, 2])
|
|
207
|
+
+ plot_annotation(title="Response by dose and arm", tag_levels="A"))
|
|
203
208
|
```
|
|
204
209
|
|
|
205
|
-
|
|
210
|
+
`scale_colour_hue()` and `theme_grey()` are ggplot2's own defaults, so the
|
|
211
|
+
figure looks as it would from R. plot3's defaults are `theme_bw()` for saved
|
|
212
|
+
files and `theme_dark()` in the viewer.
|
|
213
|
+
|
|
214
|
+
<img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/ggplot2_look.png" alt="Two-panel figure in ggplot2's grey theme: response against dose with one fitted line per arm, and violins of response by arm">
|
|
206
215
|
|
|
207
216
|
Everything else you'd expect is there, including histograms, densities,
|
|
208
217
|
boxplots, bar positions, `stat_summary`, error bars, heatmaps, hexbins, 2D
|
|
@@ -237,27 +246,35 @@ In notebooks, bare column names work as in R: `aes(x=dose, y=response)`.
|
|
|
237
246
|
## Install
|
|
238
247
|
|
|
239
248
|
```bash
|
|
240
|
-
pip install
|
|
249
|
+
pip install plot3
|
|
241
250
|
```
|
|
242
251
|
|
|
243
252
|
Python 3.10 or newer; NumPy and pandas are the only required packages.
|
|
253
|
+
Notebooks, pandas, and Polars tables work with no extras.
|
|
254
|
+
|
|
255
|
+
For PDF, and PNG with real fonts, add cairosvg and the Cairo library. SVG
|
|
256
|
+
needs neither.
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
pip install cairosvg
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
brew install cairo
|
|
264
|
+
```
|
|
244
265
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
| `export` | `cairosvg`: PNG with real fonts and PDF. Needs the Cairo C library (`brew install cairo`, `apt install libcairo2`). SVG needs nothing. |
|
|
248
|
-
| `fast` | `contourpy`: faster implicit-curve contours (matplotlib users have it) |
|
|
249
|
-
| `jupyter` | IPython integration (bare column names, `%plot3`) |
|
|
250
|
-
| `polars` | Polars tables |
|
|
266
|
+
(`apt install libcairo2` on Debian and Ubuntu.) `pip install "plot3[pdf]"`
|
|
267
|
+
installs plot3 and cairosvg together.
|
|
251
268
|
|
|
252
269
|
The latest unreleased code installs from GitHub with
|
|
253
|
-
`pip install "plot3
|
|
270
|
+
`pip install "plot3 @ git+https://github.com/rleyvasal/plot3"`.
|
|
254
271
|
|
|
255
272
|
## Development
|
|
256
273
|
|
|
257
274
|
```bash
|
|
258
275
|
git clone https://github.com/rleyvasal/plot3 && cd plot3
|
|
259
276
|
python3 -m venv .venv && source .venv/bin/activate
|
|
260
|
-
pip install -e ".[dev
|
|
277
|
+
pip install -e ".[dev]"
|
|
261
278
|
pytest -q # includes every example in this README and docs/reference.md
|
|
262
279
|
python examples/showcase_2d.py # 2D gallery in the browser
|
|
263
280
|
python examples/showcase_3d.py # 3D gallery
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.6.3"
|
|
@@ -762,7 +762,7 @@ def expand_stat_geom(
|
|
|
762
762
|
if len(group_cols) > 1:
|
|
763
763
|
out_row[colour_col] = key_tuple[1]
|
|
764
764
|
outlier_rows.append(out_row)
|
|
765
|
-
frame = pd.DataFrame(rows)
|
|
765
|
+
frame = _keep_categories(pd.DataFrame(rows), data, [colour_col])
|
|
766
766
|
box_width = float(getattr(geom, "width", 0.75))
|
|
767
767
|
dodge_levels = None
|
|
768
768
|
if len(group_cols) > 1 and not frame.empty:
|
|
@@ -920,7 +920,7 @@ def expand_stat_geom(
|
|
|
920
920
|
elif colour_col == xcol:
|
|
921
921
|
row[colour_col] = x_key
|
|
922
922
|
rows.append(row)
|
|
923
|
-
frame = pd.DataFrame(rows)
|
|
923
|
+
frame = _keep_categories(pd.DataFrame(rows), data, [colour_col])
|
|
924
924
|
if frame.empty:
|
|
925
925
|
frame = pd.DataFrame(columns=["x", "y", "group"])
|
|
926
926
|
# Force categorical x scale labels via a helper frame for domains:
|
|
@@ -1423,6 +1423,48 @@ def _legend_position_spec(value):
|
|
|
1423
1423
|
|
|
1424
1424
|
|
|
1425
1425
|
|
|
1426
|
+
def _categorical_order(g, resolved, levels) -> list:
|
|
1427
|
+
"""Colour levels in the order of a categorical colour column, as ggplot2.
|
|
1428
|
+
|
|
1429
|
+
Stats (density, histogram, smooth, summaries) rebuild their data, so
|
|
1430
|
+
their levels arrive sorted as text. When the plot's colour column is a
|
|
1431
|
+
``pd.Categorical``, its categories set the order (and so the colours);
|
|
1432
|
+
levels it does not list keep their place after them.
|
|
1433
|
+
"""
|
|
1434
|
+
data = getattr(g, "data", None)
|
|
1435
|
+
if data is None:
|
|
1436
|
+
return list(levels)
|
|
1437
|
+
columns = dict.fromkeys(m.get("color") for _geom, m in resolved if isinstance(m.get("color"), str))
|
|
1438
|
+
for column in columns:
|
|
1439
|
+
if not has_column(data, column):
|
|
1440
|
+
continue
|
|
1441
|
+
source = materialize_columns(data, [column])[column]
|
|
1442
|
+
if not isinstance(source.dtype, pd.CategoricalDtype):
|
|
1443
|
+
continue
|
|
1444
|
+
wanted = [str(c) for c in source.cat.categories]
|
|
1445
|
+
present = [str(level) for level in levels]
|
|
1446
|
+
first = [c for c in wanted if c in present]
|
|
1447
|
+
return first + [level for level in present if level not in first]
|
|
1448
|
+
return list(levels)
|
|
1449
|
+
|
|
1450
|
+
|
|
1451
|
+
def _keep_categories(frame: pd.DataFrame, data, columns) -> pd.DataFrame:
|
|
1452
|
+
"""Give a stat's output columns the category order of the source column.
|
|
1453
|
+
|
|
1454
|
+
Box and violin stats build their output from plain rows, which turns
|
|
1455
|
+
``pd.Categorical(..., categories=["placebo", "low", "high"])`` into
|
|
1456
|
+
text, sorted alphabetically: the fill colours then disagree with the
|
|
1457
|
+
points of the same column in another panel or layer.
|
|
1458
|
+
"""
|
|
1459
|
+
for column in dict.fromkeys(c for c in columns if c):
|
|
1460
|
+
if column not in frame.columns or not has_column(data, column):
|
|
1461
|
+
continue
|
|
1462
|
+
source = materialize_columns(data, [column])[column]
|
|
1463
|
+
if isinstance(source.dtype, pd.CategoricalDtype):
|
|
1464
|
+
frame[column] = pd.Categorical(frame[column], categories=list(source.cat.categories))
|
|
1465
|
+
return frame
|
|
1466
|
+
|
|
1467
|
+
|
|
1426
1468
|
def _forced_levels(g, ccats) -> list:
|
|
1427
1469
|
"""Colour levels shared by every facet panel, so a group keeps its colour."""
|
|
1428
1470
|
forced = getattr(g, "_force_color_levels", None)
|
|
@@ -2826,6 +2868,7 @@ def build_spec(g: ggplot) -> tuple[dict, list[tuple[str, str]]]:
|
|
|
2826
2868
|
# viridis_d scale, else the theme's colours extended past eight groups.
|
|
2827
2869
|
cat_colours: list[str] = []
|
|
2828
2870
|
if color_scale is not None and color_scale[0] == "cat":
|
|
2871
|
+
color_scale[1] = _categorical_order(g, resolved, color_scale[1])
|
|
2829
2872
|
user_scale = getattr(g, "colour_scale", None)
|
|
2830
2873
|
if user_scale is not None and user_scale.kind == "continuous":
|
|
2831
2874
|
raise ValueError(
|
|
@@ -115,7 +115,7 @@ def _backend_name() -> str:
|
|
|
115
115
|
if _load_contourpy() is None:
|
|
116
116
|
raise ValueError(
|
|
117
117
|
"PLOT3_CONTOUR_BACKEND=contourpy but contourpy>=1.0 is not "
|
|
118
|
-
"installed. Install it with: pip install
|
|
118
|
+
"installed. Install it with: pip install contourpy"
|
|
119
119
|
)
|
|
120
120
|
return "contourpy"
|
|
121
121
|
if _load_contourpy() is None:
|
|
@@ -346,7 +346,7 @@ def _contour_contourpy(
|
|
|
346
346
|
if contourpy is None:
|
|
347
347
|
raise ValueError(
|
|
348
348
|
"PLOT3_CONTOUR_BACKEND=contourpy but contourpy>=1.0 is not "
|
|
349
|
-
"installed. Install it with: pip install
|
|
349
|
+
"installed. Install it with: pip install contourpy"
|
|
350
350
|
)
|
|
351
351
|
masked = np.ma.masked_invalid(np.asarray(field, dtype=np.float64))
|
|
352
352
|
generator = contourpy.contour_generator(
|
|
@@ -517,7 +517,7 @@ class ggplot:
|
|
|
517
517
|
suffix writes the standalone HTML viewer. ``width`` and ``height``
|
|
518
518
|
are pixels unless ``units`` is ``"in"``, ``"cm"``, or ``"mm"``.
|
|
519
519
|
``dpi`` is the PNG resolution for a physical size (default 300).
|
|
520
|
-
PDF needs
|
|
520
|
+
PDF needs cairosvg and the Cairo C library. ``.svg``
|
|
521
521
|
needs neither.
|
|
522
522
|
"""
|
|
523
523
|
suffix = Path(path).suffix.lower()
|
|
@@ -683,8 +683,8 @@ def ggsave(
|
|
|
683
683
|
"""Save ``plot``.
|
|
684
684
|
|
|
685
685
|
``ggsave("fig.png", p)`` and ``ggsave("fig.svg", p)`` write a static
|
|
686
|
-
image. ``ggsave("fig.pdf", p)`` writes a PDF when
|
|
687
|
-
installed.
|
|
686
|
+
image. ``ggsave("fig.pdf", p)`` writes a PDF when cairosvg is
|
|
687
|
+
installed (``plot3[pdf]``). It also needs the Cairo C library; ``.svg`` writes
|
|
688
688
|
the same drawing with no extra dependencies. Any other suffix,
|
|
689
689
|
including ``.html``, writes the standalone viewer. The plot and the
|
|
690
690
|
filename can be passed in either order.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
PNG, SVG, and PDF replay one list of drawing commands. SVG is text.
|
|
4
4
|
PNG and PDF are that same SVG rendered by cairosvg when the optional
|
|
5
|
-
``plot3[
|
|
5
|
+
``plot3[pdf]`` extra (cairosvg) is installed, so the three files share one
|
|
6
6
|
drawing and one font. That extra needs the Cairo C library as well as
|
|
7
7
|
the Python package (``libcairo2``, or the GTK runtime on Windows).
|
|
8
8
|
Without cairosvg, PNG falls back to a zlib RGB file and a built-in
|
|
@@ -42,7 +42,7 @@ _UNIT_INCH = {
|
|
|
42
42
|
}
|
|
43
43
|
_EXPORT_HINT = (
|
|
44
44
|
"ggsave() needs the Cairo C library to write PDF with a journal font. "
|
|
45
|
-
"Install
|
|
45
|
+
"Install it with: pip install cairosvg. "
|
|
46
46
|
"Windows and minimal Linux images also need the Cairo library itself "
|
|
47
47
|
"(for example the libcairo2 package, or the GTK runtime). "
|
|
48
48
|
"ggsave('fig.svg', plot) writes the same drawing with no extra dependencies."
|
|
@@ -196,8 +196,8 @@ def save_static(
|
|
|
196
196
|
``"cm"`` and ``"mm"``) with ``dpi`` (default 300) sets a physical
|
|
197
197
|
page. Layout stays in CSS pixels (96 per inch) so type keeps its
|
|
198
198
|
size, and cairosvg rasterizes that SVG at ``dpi``. PDF and a
|
|
199
|
-
journal-font PNG need the Cairo C library (``pip install
|
|
200
|
-
|
|
199
|
+
journal-font PNG need cairosvg and the Cairo C library (``pip install
|
|
200
|
+
cairosvg``). ``.svg`` needs nothing extra.
|
|
201
201
|
"""
|
|
202
202
|
path = Path(path)
|
|
203
203
|
suffix = path.suffix.lower()
|
|
@@ -251,7 +251,7 @@ def save_static(
|
|
|
251
251
|
if fallback:
|
|
252
252
|
print(
|
|
253
253
|
"plot3: PNG used the built-in font. "
|
|
254
|
-
"pip install
|
|
254
|
+
"pip install cairosvg for Helvetica."
|
|
255
255
|
)
|
|
256
256
|
return str(path)
|
|
257
257
|
|
|
@@ -2196,13 +2196,15 @@ def _paint_legend(commands, origin, metrics, theme, fonts) -> None:
|
|
|
2196
2196
|
surface = theme.get("surface") or "#0b1020"
|
|
2197
2197
|
grid = theme.get("grid") or "#1c2742"
|
|
2198
2198
|
if _rgb(grid) == _rgb(surface):
|
|
2199
|
-
grid
|
|
2199
|
+
# theme_grey / theme_classic: white grid on a white page. ggplot2
|
|
2200
|
+
# draws no legend box, so neither do we (not a dark one instead).
|
|
2201
|
+
grid = None
|
|
2200
2202
|
tick = fonts[0]
|
|
2201
2203
|
row_h = metrics["row_h"]
|
|
2202
2204
|
lx, ly = origin
|
|
2203
2205
|
# Opaque, so a legend inside the panel does not fade the marks under it.
|
|
2204
2206
|
commands.append((
|
|
2205
|
-
"rect", lx, ly, metrics["w"], metrics["h"], surface, grid, 1, 1.0,
|
|
2207
|
+
"rect", lx, ly, metrics["w"], metrics["h"], surface, grid, 1 if grid else 0, 1.0,
|
|
2206
2208
|
))
|
|
2207
2209
|
cursor = ly + 6
|
|
2208
2210
|
for row in metrics["rows"]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: plot3
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
4
4
|
Summary: ggplot2 grammar of graphics for Python: interactive WebGL figures and journal-ready PNG, SVG, and PDF
|
|
5
5
|
Author: Rigoberto Leyva Salmeron
|
|
6
6
|
License-Expression: MIT
|
|
@@ -17,18 +17,12 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: numpy>=1.24
|
|
19
19
|
Requires-Dist: pandas>=2.0
|
|
20
|
-
Provides-Extra:
|
|
21
|
-
Requires-Dist:
|
|
20
|
+
Provides-Extra: pdf
|
|
21
|
+
Requires-Dist: cairosvg>=2.7; extra == "pdf"
|
|
22
22
|
Provides-Extra: dev
|
|
23
23
|
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
24
24
|
Requires-Dist: polars>=1.0; extra == "dev"
|
|
25
25
|
Requires-Dist: contourpy>=1.0; extra == "dev"
|
|
26
|
-
Provides-Extra: jupyter
|
|
27
|
-
Requires-Dist: ipython>=8.0; extra == "jupyter"
|
|
28
|
-
Provides-Extra: polars
|
|
29
|
-
Requires-Dist: polars>=1.0; extra == "polars"
|
|
30
|
-
Provides-Extra: export
|
|
31
|
-
Requires-Dist: cairosvg>=2.7; extra == "export"
|
|
32
26
|
Dynamic: license-file
|
|
33
27
|
|
|
34
28
|
# plot3
|
|
@@ -57,7 +51,7 @@ in the notebook and save as journal-ready PNG, SVG, or PDF.
|
|
|
57
51
|
</table>
|
|
58
52
|
|
|
59
53
|
```bash
|
|
60
|
-
pip install
|
|
54
|
+
pip install plot3
|
|
61
55
|
```
|
|
62
56
|
|
|
63
57
|
Contents: [Plot an equation](#plot-an-equation) ·
|
|
@@ -224,18 +218,27 @@ ggplot2's names and defaults.
|
|
|
224
218
|
|
|
225
219
|
```python
|
|
226
220
|
rng = np.random.default_rng(1)
|
|
227
|
-
|
|
221
|
+
arms = ["placebo", "low", "high"]
|
|
222
|
+
trial = pd.DataFrame({"arm": pd.Categorical(rng.choice(arms, 150), categories=arms),
|
|
228
223
|
"dose": rng.uniform(0, 10, 150)})
|
|
229
|
-
|
|
224
|
+
slope = trial.arm.map({"placebo": 0.05, "low": 0.35, "high": 0.7}).astype(float)
|
|
225
|
+
trial["response"] = 2 + slope * trial.dose + rng.normal(0, 0.9, 150)
|
|
230
226
|
|
|
231
227
|
a = (ggplot(trial, aes(x="dose", y="response", colour="arm"))
|
|
232
|
-
+ geom_point(alpha=0.7) + geom_smooth(method="lm")
|
|
228
|
+
+ geom_point(alpha=0.7) + geom_smooth(method="lm", se=False)
|
|
229
|
+
+ scale_colour_hue() + theme_grey() + theme(legend_position="bottom"))
|
|
233
230
|
b = (ggplot(trial, aes(x="arm", y="response", fill="arm"))
|
|
234
|
-
+ geom_violin() + geom_jitter(width=0.1, height=0)
|
|
235
|
-
|
|
231
|
+
+ geom_violin() + geom_jitter(width=0.1, height=0)
|
|
232
|
+
+ scale_fill_hue() + theme_grey() + theme(legend_position="none"))
|
|
233
|
+
fig = ((a | b) + plot_layout(widths=[3, 2])
|
|
234
|
+
+ plot_annotation(title="Response by dose and arm", tag_levels="A"))
|
|
236
235
|
```
|
|
237
236
|
|
|
238
|
-
|
|
237
|
+
`scale_colour_hue()` and `theme_grey()` are ggplot2's own defaults, so the
|
|
238
|
+
figure looks as it would from R. plot3's defaults are `theme_bw()` for saved
|
|
239
|
+
files and `theme_dark()` in the viewer.
|
|
240
|
+
|
|
241
|
+
<img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/ggplot2_look.png" alt="Two-panel figure in ggplot2's grey theme: response against dose with one fitted line per arm, and violins of response by arm">
|
|
239
242
|
|
|
240
243
|
Everything else you'd expect is there, including histograms, densities,
|
|
241
244
|
boxplots, bar positions, `stat_summary`, error bars, heatmaps, hexbins, 2D
|
|
@@ -270,27 +273,35 @@ In notebooks, bare column names work as in R: `aes(x=dose, y=response)`.
|
|
|
270
273
|
## Install
|
|
271
274
|
|
|
272
275
|
```bash
|
|
273
|
-
pip install
|
|
276
|
+
pip install plot3
|
|
274
277
|
```
|
|
275
278
|
|
|
276
279
|
Python 3.10 or newer; NumPy and pandas are the only required packages.
|
|
280
|
+
Notebooks, pandas, and Polars tables work with no extras.
|
|
281
|
+
|
|
282
|
+
For PDF, and PNG with real fonts, add cairosvg and the Cairo library. SVG
|
|
283
|
+
needs neither.
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
pip install cairosvg
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
```bash
|
|
290
|
+
brew install cairo
|
|
291
|
+
```
|
|
277
292
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
| `export` | `cairosvg`: PNG with real fonts and PDF. Needs the Cairo C library (`brew install cairo`, `apt install libcairo2`). SVG needs nothing. |
|
|
281
|
-
| `fast` | `contourpy`: faster implicit-curve contours (matplotlib users have it) |
|
|
282
|
-
| `jupyter` | IPython integration (bare column names, `%plot3`) |
|
|
283
|
-
| `polars` | Polars tables |
|
|
293
|
+
(`apt install libcairo2` on Debian and Ubuntu.) `pip install "plot3[pdf]"`
|
|
294
|
+
installs plot3 and cairosvg together.
|
|
284
295
|
|
|
285
296
|
The latest unreleased code installs from GitHub with
|
|
286
|
-
`pip install "plot3
|
|
297
|
+
`pip install "plot3 @ git+https://github.com/rleyvasal/plot3"`.
|
|
287
298
|
|
|
288
299
|
## Development
|
|
289
300
|
|
|
290
301
|
```bash
|
|
291
302
|
git clone https://github.com/rleyvasal/plot3 && cd plot3
|
|
292
303
|
python3 -m venv .venv && source .venv/bin/activate
|
|
293
|
-
pip install -e ".[dev
|
|
304
|
+
pip install -e ".[dev]"
|
|
294
305
|
pytest -q # includes every example in this README and docs/reference.md
|
|
295
306
|
python examples/showcase_2d.py # 2D gallery in the browser
|
|
296
307
|
python examples/showcase_3d.py # 3D gallery
|
|
@@ -25,11 +25,8 @@ dependencies = [
|
|
|
25
25
|
]
|
|
26
26
|
|
|
27
27
|
[project.optional-dependencies]
|
|
28
|
-
|
|
28
|
+
pdf = ["cairosvg>=2.7"]
|
|
29
29
|
dev = ["pytest>=7.0", "polars>=1.0", "contourpy>=1.0"]
|
|
30
|
-
jupyter = ["ipython>=8.0"]
|
|
31
|
-
polars = ["polars>=1.0"]
|
|
32
|
-
export = ["cairosvg>=2.7"]
|
|
33
30
|
|
|
34
31
|
[project.urls]
|
|
35
32
|
Homepage = "https://github.com/rleyvasal/plot3"
|
|
@@ -265,7 +265,7 @@ def test_version_floor_and_backend_selection(monkeypatch):
|
|
|
265
265
|
assert _backend_name() == "numpy"
|
|
266
266
|
monkeypatch.setenv("PLOT3_CONTOUR_BACKEND", "contourpy")
|
|
267
267
|
_reset_contour_backend()
|
|
268
|
-
with pytest.raises(ValueError, match=r"
|
|
268
|
+
with pytest.raises(ValueError, match=r"pip install contourpy"):
|
|
269
269
|
_contour_lines(np.linspace(-1, 1, 4), np.linspace(-1, 1, 4), np.zeros((4, 4)), 0.0)
|
|
270
270
|
|
|
271
271
|
fake = types.ModuleType("contourpy")
|
|
@@ -135,3 +135,45 @@ def test_geom_smooth_takes_a_linetype():
|
|
|
135
135
|
assert [layer.get("dash") for layer in spec["layers"] if layer["kind"] == "line"] == [[4.0, 4.0]]
|
|
136
136
|
with pytest.raises(ValueError):
|
|
137
137
|
geom_smooth(linetype="wavy")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@pytest.mark.parametrize("kind", ["violin", "boxplot", "density", "histogram", "bar", "smooth", "summary"])
|
|
141
|
+
def test_categorical_colour_order_survives_stats(kind):
|
|
142
|
+
# pd.Categorical(categories=["placebo", "low", "high"]) sets the colour
|
|
143
|
+
# order for every geom, as in ggplot2; stats used to sort it as text, so
|
|
144
|
+
# a violin and a scatter of the same column disagreed on colours.
|
|
145
|
+
import numpy as np
|
|
146
|
+
import pandas as pd
|
|
147
|
+
|
|
148
|
+
from plot3 import (aes, geom_bar, geom_boxplot, geom_density, geom_histogram,
|
|
149
|
+
geom_smooth, geom_violin, ggplot, stat_summary)
|
|
150
|
+
from plot3.build import build_spec
|
|
151
|
+
|
|
152
|
+
rng = np.random.default_rng(0)
|
|
153
|
+
arms = ["placebo", "low", "high"]
|
|
154
|
+
df = pd.DataFrame({"arm": pd.Categorical(rng.choice(arms, 90), categories=arms),
|
|
155
|
+
"x": rng.uniform(0, 10, 90), "y": rng.normal(size=90),
|
|
156
|
+
"sex": rng.choice(["F", "M"], 90)})
|
|
157
|
+
layer = {
|
|
158
|
+
"violin": (aes(x="arm", y="y", fill="arm"), geom_violin()),
|
|
159
|
+
"boxplot": (aes(x="sex", y="y", fill="arm"), geom_boxplot()),
|
|
160
|
+
"density": (aes(x="y", colour="arm"), geom_density()),
|
|
161
|
+
"histogram": (aes(x="y", fill="arm"), geom_histogram(bins=10)),
|
|
162
|
+
"bar": (aes(x="sex", fill="arm"), geom_bar()),
|
|
163
|
+
"smooth": (aes(x="x", y="y", colour="arm"), geom_smooth(method="lm")),
|
|
164
|
+
"summary": (aes(x="sex", y="y", colour="arm"), stat_summary()),
|
|
165
|
+
}[kind]
|
|
166
|
+
spec, _ = build_spec(ggplot(df, layer[0]) + layer[1])
|
|
167
|
+
assert spec["color"]["cats"] == arms
|
|
168
|
+
assert [entry["label"] for entry in spec["legend"]] == arms
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_theme_grey_legend_has_no_dark_frame(tmp_path):
|
|
172
|
+
import pandas as pd
|
|
173
|
+
|
|
174
|
+
from plot3 import aes, geom_point, ggplot, ggsave, theme_grey
|
|
175
|
+
|
|
176
|
+
df = pd.DataFrame({"x": [1, 2], "y": [1, 2], "g": ["a", "b"]})
|
|
177
|
+
out = tmp_path / "g.svg"
|
|
178
|
+
ggsave(str(out), ggplot(df, aes(x="x", y="y", colour="g")) + geom_point() + theme_grey())
|
|
179
|
+
assert 'stroke="#4d4d4d"' not in out.read_text()
|
|
@@ -438,7 +438,7 @@ def test_inches_and_dpi_set_the_png_page(tmp_path, monkeypatch, capsys):
|
|
|
438
438
|
rgb = _read_png(path)
|
|
439
439
|
assert rgb.shape == (1200, 2100, 3)
|
|
440
440
|
assert tuple(rgb[0, 0]) == (255, 255, 255)
|
|
441
|
-
assert "
|
|
441
|
+
assert "pip install cairosvg" in capsys.readouterr().out
|
|
442
442
|
|
|
443
443
|
cm = tmp_path / "cm.png"
|
|
444
444
|
ggsave(cm, fig, width=2.54, height=2.54, units="cm", dpi=100)
|
|
@@ -466,7 +466,7 @@ def test_pdf_without_cairosvg_names_the_extra(tmp_path, monkeypatch):
|
|
|
466
466
|
from plot3 import static
|
|
467
467
|
|
|
468
468
|
monkeypatch.setattr(static, "_load_cairosvg", lambda: None)
|
|
469
|
-
with pytest.raises(RuntimeError, match=r"
|
|
469
|
+
with pytest.raises(RuntimeError, match=r"pip install cairosvg") as caught:
|
|
470
470
|
ggsave(tmp_path / "fig.pdf", _curve())
|
|
471
471
|
message = str(caught.value)
|
|
472
472
|
assert "Cairo" in message
|
plot3-0.6.1/plot3/__version__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.6.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|