plot3 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plot3-0.4.0/LICENSE +21 -0
- plot3-0.4.0/PKG-INFO +504 -0
- plot3-0.4.0/README.md +471 -0
- plot3-0.4.0/plot3/__init__.py +301 -0
- plot3-0.4.0/plot3/__version__.py +1 -0
- plot3-0.4.0/plot3/aesexpr.py +271 -0
- plot3-0.4.0/plot3/build.py +3948 -0
- plot3-0.4.0/plot3/calculus.py +1179 -0
- plot3-0.4.0/plot3/compose.py +285 -0
- plot3-0.4.0/plot3/contour.py +476 -0
- plot3-0.4.0/plot3/craft.py +142 -0
- plot3-0.4.0/plot3/encode.py +68 -0
- plot3-0.4.0/plot3/expr.py +1557 -0
- plot3-0.4.0/plot3/flip.py +245 -0
- plot3-0.4.0/plot3/function.py +1301 -0
- plot3-0.4.0/plot3/geoms.py +2558 -0
- plot3-0.4.0/plot3/ggplot.py +713 -0
- plot3-0.4.0/plot3/io.py +76 -0
- plot3-0.4.0/plot3/jupyter.py +514 -0
- plot3-0.4.0/plot3/latexin.py +616 -0
- plot3-0.4.0/plot3/masking.py +494 -0
- plot3-0.4.0/plot3/mathtext.py +842 -0
- plot3-0.4.0/plot3/payload.py +216 -0
- plot3-0.4.0/plot3/remote.py +220 -0
- plot3-0.4.0/plot3/scales.py +387 -0
- plot3-0.4.0/plot3/scaling.py +636 -0
- plot3-0.4.0/plot3/special.py +407 -0
- plot3-0.4.0/plot3/stat2d.py +1539 -0
- plot3-0.4.0/plot3/static.py +3760 -0
- plot3-0.4.0/plot3/stats3d.py +462 -0
- plot3-0.4.0/plot3/table.py +775 -0
- plot3-0.4.0/plot3/themes.py +104 -0
- plot3-0.4.0/plot3/viewer.py +3354 -0
- plot3-0.4.0/plot3.egg-info/PKG-INFO +504 -0
- plot3-0.4.0/plot3.egg-info/SOURCES.txt +78 -0
- plot3-0.4.0/plot3.egg-info/dependency_links.txt +1 -0
- plot3-0.4.0/plot3.egg-info/requires.txt +19 -0
- plot3-0.4.0/plot3.egg-info/top_level.txt +1 -0
- plot3-0.4.0/pyproject.toml +50 -0
- plot3-0.4.0/setup.cfg +4 -0
- plot3-0.4.0/tests/test_2d_density.py +130 -0
- plot3-0.4.0/tests/test_3d.py +263 -0
- plot3-0.4.0/tests/test_animation.py +641 -0
- plot3-0.4.0/tests/test_annotate.py +257 -0
- plot3-0.4.0/tests/test_api_public.py +69 -0
- plot3-0.4.0/tests/test_array_backend.py +189 -0
- plot3-0.4.0/tests/test_arrow.py +78 -0
- plot3-0.4.0/tests/test_build_contract.py +65 -0
- plot3-0.4.0/tests/test_contour.py +353 -0
- plot3-0.4.0/tests/test_everyday_ggplot2.py +137 -0
- plot3-0.4.0/tests/test_expr.py +126 -0
- plot3-0.4.0/tests/test_facets_compose.py +165 -0
- plot3-0.4.0/tests/test_geom_function.py +517 -0
- plot3-0.4.0/tests/test_geoms_ds.py +130 -0
- plot3-0.4.0/tests/test_ggplot2_extras2.py +110 -0
- plot3-0.4.0/tests/test_ggplot2_parity.py +139 -0
- plot3-0.4.0/tests/test_ggplot_extras.py +117 -0
- plot3-0.4.0/tests/test_grammar.py +99 -0
- plot3-0.4.0/tests/test_latex_input.py +249 -0
- plot3-0.4.0/tests/test_lidar_scene.py +100 -0
- plot3-0.4.0/tests/test_masking.py +185 -0
- plot3-0.4.0/tests/test_math_layers.py +310 -0
- plot3-0.4.0/tests/test_mathtext.py +270 -0
- plot3-0.4.0/tests/test_payload.py +153 -0
- plot3-0.4.0/tests/test_payload_display.py +116 -0
- plot3-0.4.0/tests/test_point_cloud.py +63 -0
- plot3-0.4.0/tests/test_r_oracle_parity.py +184 -0
- plot3-0.4.0/tests/test_readme.py +63 -0
- plot3-0.4.0/tests/test_remote_bridge.py +232 -0
- plot3-0.4.0/tests/test_review_fixes.py +150 -0
- plot3-0.4.0/tests/test_save_menu.py +252 -0
- plot3-0.4.0/tests/test_scales_api.py +230 -0
- plot3-0.4.0/tests/test_scales_themes.py +135 -0
- plot3-0.4.0/tests/test_special.py +175 -0
- plot3-0.4.0/tests/test_stat2d.py +232 -0
- plot3-0.4.0/tests/test_static_save.py +843 -0
- plot3-0.4.0/tests/test_stats_semantic.py +86 -0
- plot3-0.4.0/tests/test_step3_geoms.py +195 -0
- plot3-0.4.0/tests/test_table_backends.py +426 -0
- plot3-0.4.0/tests/test_titles_theme.py +106 -0
plot3-0.4.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rigoberto Leyva Salmeron
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
plot3-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plot3
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: ggplot2 grammar of graphics for Python: interactive WebGL figures and journal-ready PNG, SVG, and PDF
|
|
5
|
+
Author: Rigoberto Leyva Salmeron
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/rleyvasal/plot3
|
|
8
|
+
Project-URL: Changelog, https://github.com/rleyvasal/plot3/blob/main/CHANGELOG.md
|
|
9
|
+
Keywords: ggplot,ggplot2,grammar of graphics,visualization,plotting,three.js,dataframe
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Framework :: Jupyter
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: numpy>=1.24
|
|
19
|
+
Requires-Dist: pandas>=2.0
|
|
20
|
+
Provides-Extra: fast
|
|
21
|
+
Requires-Dist: contourpy>=1.0; extra == "fast"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
24
|
+
Requires-Dist: polars>=1.0; extra == "dev"
|
|
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
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# plot3
|
|
35
|
+
|
|
36
|
+
ggplot2's grammar of graphics for Python, drawn with WebGL (three.js) in the
|
|
37
|
+
notebook and saved as journal-ready PNG, SVG, or PDF.
|
|
38
|
+
|
|
39
|
+
```python notest
|
|
40
|
+
ggplot(df, aes(x="dose", y="response", colour="arm")) + geom_point() + geom_smooth(method="lm")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- **ggplot2 grammar**: `+` layers, `aes()`, geoms, stats, scales, facets,
|
|
44
|
+
themes, and `ggsave()`, with ggplot2's defaults and names.
|
|
45
|
+
- **Interactive 2D and 3D**: pan, zoom, hover, click a legend entry to hide
|
|
46
|
+
it, orbit 3D point clouds and surfaces, play animations, drag sliders.
|
|
47
|
+
- **Publication output**: `ggsave("fig.pdf", p, width=3.5, height=2.6,
|
|
48
|
+
units="in")` with real fonts, 300 dpi metadata, and `theme_bw` by default.
|
|
49
|
+
- **Maths built in**: plot `"y = x^2 + 1"`, implicit equations, surfaces,
|
|
50
|
+
densities (`dbeta`, `dnorm`), probability areas, tangents, and LaTeX.
|
|
51
|
+
- **Your data as it is**: pandas, Polars, tidy3, or NumPy arrays, locally
|
|
52
|
+
or on a remote GPU kernel (CRAFT / SolveIt).
|
|
53
|
+
|
|
54
|
+
Contents: [Install](#install) · [Quick start](#quick-start) ·
|
|
55
|
+
[Statistical plots](#statistical-plots) · [Annotation](#annotation) ·
|
|
56
|
+
[Scales](#scales) · [Themes and titles](#themes-and-titles) ·
|
|
57
|
+
[Facets and multi-panel figures](#facets-and-multi-panel-figures) ·
|
|
58
|
+
[Saving for a paper](#saving-for-a-paper) · [Functions and maths](#functions-and-maths) ·
|
|
59
|
+
[Animation and sliders](#animation-and-sliders) · [3D](#3d-and-point-clouds) ·
|
|
60
|
+
[Notebooks, SolveIt, CRAFT](#notebooks-solveit-and-craft) ·
|
|
61
|
+
[API reference](#api-reference) · [Differences from ggplot2](#differences-from-ggplot2)
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install "plot3[jupyter,export]"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The latest unreleased code installs straight from GitHub:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install "plot3[jupyter,export] @ git+https://github.com/rleyvasal/plot3"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
To work on plot3 itself:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
git clone https://github.com/rleyvasal/plot3 && cd plot3
|
|
79
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
80
|
+
pip install -e ".[dev,jupyter,export]"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
| Extra | Adds |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `export` | `cairosvg`: PNG with real fonts and PDF. Needs the Cairo C library (`brew install cairo`, `apt install libcairo2`). SVG needs nothing. |
|
|
86
|
+
| `fast` | `contourpy`: faster implicit-curve contours (matplotlib users have it) |
|
|
87
|
+
| `jupyter` | IPython integration (bare column names, `%plot3`) |
|
|
88
|
+
| `polars` | Polars tables |
|
|
89
|
+
|
|
90
|
+
Python 3.10 or newer; NumPy and pandas are the only required packages.
|
|
91
|
+
|
|
92
|
+
## Quick start
|
|
93
|
+
|
|
94
|
+
The examples below share this data:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
import numpy as np
|
|
98
|
+
import pandas as pd
|
|
99
|
+
from plot3 import *
|
|
100
|
+
|
|
101
|
+
rng = np.random.default_rng(1)
|
|
102
|
+
trial = pd.DataFrame({
|
|
103
|
+
"arm": rng.choice(["placebo", "low", "high"], 150),
|
|
104
|
+
"sex": rng.choice(["F", "M"], 150),
|
|
105
|
+
"dose": rng.uniform(0, 10, 150),
|
|
106
|
+
})
|
|
107
|
+
trial["response"] = (2 + 0.6 * trial.dose + 1.5 * (trial.arm == "high")
|
|
108
|
+
+ rng.normal(0, 1.2, 150))
|
|
109
|
+
sales = pd.DataFrame({"month": pd.date_range("2021-01-01", periods=36, freq="MS")})
|
|
110
|
+
sales["units"] = 100 + np.cumsum(rng.normal(1, 4, 36))
|
|
111
|
+
sales["lo"], sales["hi"] = sales.units - 8, sales.units + 8
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
A plot is data, an aesthetic mapping, and layers:
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
p = (ggplot(trial, aes(x="dose", y="response", colour="arm"))
|
|
118
|
+
+ geom_point(alpha=0.7)
|
|
119
|
+
+ geom_smooth(method="lm")
|
|
120
|
+
+ labs(title="Response by dose", x="Dose (mg)", y="Response"))
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
In a notebook, `p` on its own line displays the interactive figure. Save it:
|
|
124
|
+
|
|
125
|
+
```python notest
|
|
126
|
+
ggsave("response.pdf", p, width=3.5, height=2.6, units="in") # vector, journal column
|
|
127
|
+
ggsave("response.png", p, width=7, height=4, units="in", dpi=300)
|
|
128
|
+
ggsave("response.html", p) # interactive page
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
In notebooks, bare column names work too: `aes(x=dose, y=response)`.
|
|
132
|
+
Plain `.py` files use strings, as above.
|
|
133
|
+
|
|
134
|
+
## Statistical plots
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
# Distributions
|
|
138
|
+
ggplot(trial, aes(x="response", fill="sex")) + geom_histogram(bins=20) # stacked by group
|
|
139
|
+
(ggplot(trial, aes(x="response", y="after_stat(density)")) # density scale
|
|
140
|
+
+ geom_histogram(bins=20) + geom_density())
|
|
141
|
+
ggplot(trial, aes(x="response", fill="arm")) + geom_density(alpha=0.4)
|
|
142
|
+
ggplot(trial, aes(x="arm", y="response")) + geom_boxplot(outliers=False) + geom_jitter(width=0.15, height=0)
|
|
143
|
+
ggplot(trial, aes(x="arm", y="response", fill="arm")) + geom_violin()
|
|
144
|
+
ggplot(trial, aes(x="response", colour="arm")) + stat_ecdf()
|
|
145
|
+
ggplot(trial, aes(sample="response")) + geom_qq() + geom_qq_line()
|
|
146
|
+
|
|
147
|
+
# Counts and proportions
|
|
148
|
+
ggplot(trial, aes(x="arm", fill="sex")) + geom_bar() # stacked
|
|
149
|
+
ggplot(trial, aes(x="arm", fill="sex")) + geom_bar(position="dodge") # side by side
|
|
150
|
+
ggplot(trial, aes(x="arm", fill="sex")) + geom_bar(position="fill") # shares
|
|
151
|
+
|
|
152
|
+
# Means with uncertainty
|
|
153
|
+
(ggplot(trial, aes(x="arm", y="response", fill="sex"))
|
|
154
|
+
+ stat_summary(fun_data="mean_se", geom="col", position="dodge")
|
|
155
|
+
+ stat_summary(fun_data="mean_cl_normal", geom="errorbar", position="dodge", width=0.3))
|
|
156
|
+
|
|
157
|
+
# Trends and time series
|
|
158
|
+
ggplot(trial, aes(x="dose", y="response")) + geom_point() + geom_smooth() # loess + 95% band
|
|
159
|
+
(ggplot(sales, aes(x="month", y="units"))
|
|
160
|
+
+ geom_ribbon(aes(ymin="lo", ymax="hi"), alpha=0.25) + geom_line())
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
| Need | Use |
|
|
164
|
+
|---|---|
|
|
165
|
+
| Error bars from your own columns | `geom_errorbar(aes(ymin="mean - se", ymax="mean + se"))`, `geom_pointrange`, `geom_linerange`, `geom_crossbar`, `geom_errorbarh(aes(xmin=, xmax=))` |
|
|
166
|
+
| Summaries | `stat_summary(fun_data="mean_se" / "mean_cl_normal" / "mean_sdl" / "median_hilow")` |
|
|
167
|
+
| Heatmaps | `geom_tile(aes(x=, y=, fill=))` (or `geom_raster`), with `geom_text(aes(label=))` |
|
|
168
|
+
| Stacked areas | `geom_area(aes(fill=))`, `position="fill"` for shares |
|
|
169
|
+
| Steps, segments, rectangles | `geom_step()`, `geom_segment(aes(xend=, yend=), arrow=arrow())`, `geom_rect(aes(xmin=, xmax=, ymin=, ymax=))` |
|
|
170
|
+
| Horizontal layout | `+ coord_flip()` (bars, boxplots, densities, error bars) |
|
|
171
|
+
| Several datasets | `geom_rect(aes(...), data=periods)`: any layer can bring its own data |
|
|
172
|
+
| Points over grouped boxes | `geom_boxplot(aes(colour="sex"), outliers=False) + geom_point(position=position_jitterdodge())` |
|
|
173
|
+
| Labels beside points | `geom_text(position=position_nudge(y=0.3))`, or `nudge_y=` |
|
|
174
|
+
| Shapes and maps | `geom_polygon(aes(group="id", fill="region"))`, concave shapes included |
|
|
175
|
+
| Frequency lines | `geom_freqpoly(aes(colour="arm"), binwidth=0.5)` |
|
|
176
|
+
| Big scatters, 2D distributions | `geom_hex()`, `geom_bin_2d()`, `geom_count()`, `geom_density_2d()`, `geom_density_2d_filled()`, `stat_ellipse()` (95% by group) |
|
|
177
|
+
| Contours of a grid | `geom_contour(aes(x=, y=, z=))` |
|
|
178
|
+
|
|
179
|
+
`aes()` reads expressions over your columns, as ggplot2 does:
|
|
180
|
+
`aes(ymin="mean - se")`, `aes(y="log10(count)")`, `aes(colour="factor(cyl)")`,
|
|
181
|
+
`aes(colour="dose > 5")`, `aes(label="round(estimate, 2)")`. They use `+ - * /
|
|
182
|
+
^`, comparisons, and `log`, `log10`, `log2`, `exp`, `sqrt`, `abs`, `round`,
|
|
183
|
+
`floor`, `ceiling`, `factor`, `as.numeric`, `ifelse`, `pmin`, `pmax`, `mean`,
|
|
184
|
+
`median`, `sd`, `min`, `max`, `sum`; nothing else runs. The expression names
|
|
185
|
+
the axis or legend.
|
|
186
|
+
|
|
187
|
+
Rows with missing values are dropped, and plot3 says so: *Removed 3 rows
|
|
188
|
+
containing missing values (geom_point)*.
|
|
189
|
+
|
|
190
|
+
## Annotation
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
(ggplot(trial, aes(x="dose", y="response"))
|
|
194
|
+
+ geom_point()
|
|
195
|
+
+ geom_hline(yintercept=5, linetype="dashed")
|
|
196
|
+
+ geom_vline(xintercept=[2, 8], colour="firebrick")
|
|
197
|
+
+ geom_abline(slope=0.6, intercept=2)
|
|
198
|
+
+ annotate("rect", xmin=2, xmax=8, ymin=0, ymax=12)
|
|
199
|
+
+ annotate("label", x=9, y=1, label="safe range")
|
|
200
|
+
+ annotate("segment", x=1, y=10, xend=3, yend=8, arrow=arrow()))
|
|
201
|
+
|
|
202
|
+
means = trial.groupby("arm", as_index=False).response.mean()
|
|
203
|
+
(ggplot(means, aes(x="arm", y="response", label="response"))
|
|
204
|
+
+ geom_col() + geom_text(nudge_y=0.4)) # numbers print to 4 significant figures
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
`geom_text` / `geom_label` take `size` in millimetres (ggplot2's 3.88 mm
|
|
208
|
+
default), `hjust`, `vjust`, `nudge_x`, `nudge_y`, `fontface`, and
|
|
209
|
+
`check_overlap`. Line types are `"solid"`, `"dashed"`, `"dotted"`,
|
|
210
|
+
`"dotdash"`, `"longdash"`, `"twodash"`, R's numbers, or hex such as `"44"`.
|
|
211
|
+
|
|
212
|
+
Map more aesthetics: `aes(shape=)` (circle, triangle, square, diamond, plus,
|
|
213
|
+
cross), `aes(linetype=)`, `aes(size=)` (area), `aes(fill=)` for filled shapes
|
|
214
|
+
and `aes(colour=)` for points and lines.
|
|
215
|
+
|
|
216
|
+
## Scales
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
(ggplot(trial, aes(x="dose", y="response", colour="arm"))
|
|
220
|
+
+ geom_point()
|
|
221
|
+
+ scale_x_continuous("Dose (mg)", breaks=[0, 2.5, 5, 7.5, 10])
|
|
222
|
+
+ scale_y_continuous(limits=(0, None))
|
|
223
|
+
+ scale_colour_manual(values={"placebo": "grey50", "low": "#56B4E9", "high": "#D55E00"},
|
|
224
|
+
breaks=["placebo", "low", "high"],
|
|
225
|
+
labels=["Placebo", "Low dose", "High dose"], name="Arm"))
|
|
226
|
+
|
|
227
|
+
(ggplot(sales, aes(x="month", y="units"))
|
|
228
|
+
+ geom_line()
|
|
229
|
+
+ scale_x_date(date_breaks="6 months", date_labels="%b %Y")
|
|
230
|
+
+ scale_y_continuous(labels="dollar"))
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
| Scale | Functions |
|
|
234
|
+
|---|---|
|
|
235
|
+
| Position | `scale_x_continuous(name, limits, breaks, labels, trans="log10"/"reverse")`, `scale_x_discrete(limits, labels)`, `scale_x_date(date_breaks, date_labels)`, `scale_x_log10`, `scale_x_reverse`, `xlim`, `ylim`, `lims` (and the `y` versions) |
|
|
236
|
+
| Label formats | `"percent"`, `"comma"`, `"dollar"`, `"scientific"`, `"{:.1f} kg"`, a list, or a function |
|
|
237
|
+
| Discrete colour / fill | `scale_colour_hue` (ggplot2's default colours), `scale_colour_manual`, `scale_colour_brewer(palette="Set2")`, `scale_colour_viridis_d`, `scale_colour_grey`, `scale_colour_okabe_ito` (colour-blind safe), `scale_colour_identity` (the column holds colours) |
|
|
238
|
+
| Continuous colour / fill | `scale_colour_gradient(low, high)`, `scale_colour_gradient2(low, mid, high, midpoint)`, `scale_colour_gradientn(colours, values)`, `scale_colour_distiller(palette="RdBu")`, `scale_colour_viridis_c`, `scale_colour_continuous(trans="log10")` |
|
|
239
|
+
| Size / alpha | `scale_size(range=(4, 23))` (by area across the data's range, as ggplot2), `scale_size_area(max_size=)` (area from zero), `scale_alpha(range=(0.1, 1))` for `aes(alpha=)` |
|
|
240
|
+
| Shape / linetype | `scale_shape_manual`, `scale_linetype_manual` |
|
|
241
|
+
| Limits | `expand_limits(y=0)` makes an axis reach a value with no data there |
|
|
242
|
+
|
|
243
|
+
Every colour scale has `scale_fill_*` and `scale_color_*` names. Colours can
|
|
244
|
+
be CSS names (`"steelblue"`), R greys (`"grey50"`), `"rgb(1,2,3)"`, or hex.
|
|
245
|
+
Limits on a continuous axis drop the rows outside them and say how many.
|
|
246
|
+
|
|
247
|
+
## Themes and titles
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
(ggplot(trial, aes(x="arm", y="response", fill="arm"))
|
|
251
|
+
+ geom_boxplot()
|
|
252
|
+
+ labs(title="Response", subtitle="150 patients", caption="Source: simulated", tag="A")
|
|
253
|
+
+ theme_classic()
|
|
254
|
+
+ theme(legend_position="none", axis_text_x_angle=45, plot_title_hjust=0.5))
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Themes: `theme_bw` (default for saved files), `theme_classic`,
|
|
258
|
+
`theme_minimal`, `theme_void` (the data alone), `theme_light`, `theme_dark`
|
|
259
|
+
(default in the interactive viewer), and `theme_lidar` (driving scenes). Each takes `base_size` (points) and `base_family`. `theme()` sets
|
|
260
|
+
`legend_position` (`"right"`, `"bottom"`, `"none"`, or `(x, y)` inside the
|
|
261
|
+
panel), `legend_title=False`, `panel_grid=False`, `axis_text_x_angle`,
|
|
262
|
+
`plot_title_hjust`, `base_size`, and `base_family`. `theme_grey` (ggplot2's
|
|
263
|
+
grey panel) and `theme_linedraw` are there too.
|
|
264
|
+
|
|
265
|
+
ggplot2's elements work, with R's dotted names or underscores:
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
(ggplot(trial, aes(x="arm", y="response")) + geom_boxplot() + theme_bw()
|
|
269
|
+
+ theme(**{"axis.text.x": element_text(angle=45), "panel.grid": element_blank(),
|
|
270
|
+
"plot.title": element_text(hjust=0.5),
|
|
271
|
+
"panel.background": element_rect(fill="grey95")}))
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
`element_blank()` hides axis text, axis titles, the grid, the panel border,
|
|
275
|
+
or the legend title; colours in `element_text`, `element_line`, and
|
|
276
|
+
`element_rect` recolour text, grid, border, and backgrounds. Parts plot3
|
|
277
|
+
does not draw (minor grid, tick length) are accepted, and anything else it
|
|
278
|
+
cannot draw warns.
|
|
279
|
+
|
|
280
|
+
`ggtitle("Response", subtitle=)`, `xlab()`, and `ylab()` are shortcuts for
|
|
281
|
+
`labs()`. `guides(colour="none")` hides one legend (also `fill`, `size`,
|
|
282
|
+
`shape`, `linetype`) and keeps the others;
|
|
283
|
+
`guides(colour=guide_legend(title="Arm", reverse=True))` retitles or
|
|
284
|
+
reorders it. The `stat_*` spellings (`stat_smooth`, `stat_bin`,
|
|
285
|
+
`stat_count`, `stat_density`, `stat_function(fun=, args=)`) work too.
|
|
286
|
+
|
|
287
|
+
Zoom without dropping data with `coord_cartesian`: a smoother or boxplot is
|
|
288
|
+
still computed from every row, while `xlim()` and `scale_x_continuous(limits=)`
|
|
289
|
+
remove the rows outside first.
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
(ggplot(trial, aes(x="dose", y="response"))
|
|
293
|
+
+ geom_point() + geom_smooth(method="lm") + geom_rug(alpha=0.4)
|
|
294
|
+
+ coord_cartesian(xlim=(2, 6)))
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Facets and multi-panel figures
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
ggplot(trial, aes(x="dose", y="response")) + geom_point() + facet_wrap("arm")
|
|
301
|
+
ggplot(trial, aes(x="dose", y="response")) + geom_point() + facet_wrap("arm", labeller="label_both")
|
|
302
|
+
ggplot(trial, aes(x="dose", y="response")) + geom_point() + facet_wrap(vars("arm"))
|
|
303
|
+
(ggplot(trial, aes(x="dose", y="response", colour="arm"))
|
|
304
|
+
+ geom_point() + facet_grid("sex ~ arm")) # rows ~ columns
|
|
305
|
+
|
|
306
|
+
a = ggplot(trial, aes(x="dose", y="response", colour="arm")) + geom_point()
|
|
307
|
+
b = ggplot(trial, aes(x="arm", y="response", fill="arm")) + geom_boxplot() + theme(legend_position="none")
|
|
308
|
+
c = ggplot(sales, aes(x="month", y="units")) + geom_line()
|
|
309
|
+
fig = ((a | b) / c) + plot_annotation(title="Overview", tag_levels="A")
|
|
310
|
+
fig2 = (a | b) + plot_layout(widths=[2, 1])
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Facet panels share scales, colours, one legend, and one pair of axis titles,
|
|
314
|
+
as in ggplot2. `|` puts plots side by side, `/` stacks them, and
|
|
315
|
+
`tag_levels` is `"A"`, `"a"`, `"1"`, or `"I"`. Save a composed figure with
|
|
316
|
+
`ggsave` like any plot.
|
|
317
|
+
|
|
318
|
+
## Saving for a paper
|
|
319
|
+
|
|
320
|
+
```python notest
|
|
321
|
+
ggsave("fig1.pdf", p, width=3.5, height=2.6, units="in") # one column
|
|
322
|
+
ggsave("fig1.png", p, width=7, height=4.5, units="in", dpi=300) # two columns
|
|
323
|
+
ggsave("fig1.svg", p, width=18, height=12, units="cm", fontsize=9, family="Arial")
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
- PNG, SVG, and PDF draw the same picture with real fonts. PNGs carry their
|
|
327
|
+
DPI, so Word and journal portals size them correctly.
|
|
328
|
+
- Saved files use `theme_bw` unless you add a theme.
|
|
329
|
+
- Crowded category labels turn or thin automatically; set
|
|
330
|
+
`theme(axis_text_x_angle=)` to choose.
|
|
331
|
+
- Non-Latin text (東京, 서울, ✓) uses an installed font that has the glyphs.
|
|
332
|
+
- Notes such as *Removed 3 rows…* are printed, not drawn; `notes=True` draws them.
|
|
333
|
+
- Without the `export` extra, `.svg` works everywhere and `.png` falls back to
|
|
334
|
+
a built-in bitmap font.
|
|
335
|
+
|
|
336
|
+
The interactive viewer also has a **Save** button (HTML, SVG, PNG, video for
|
|
337
|
+
animations, copy to clipboard).
|
|
338
|
+
|
|
339
|
+
## Functions and maths
|
|
340
|
+
|
|
341
|
+
`geom_function` plots a formula with the same grammar as data. `^` is power
|
|
342
|
+
and `2x` means `2*x`.
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
ggplot() + geom_function("y = 2x + 2")
|
|
346
|
+
ggplot() + geom_function("y = a x^2 + b x + c", a=2, b=-3, c=1) # coefficients at the end
|
|
347
|
+
ggplot() + geom_function("x^2 + y^2 = 1") # implicit: a round circle
|
|
348
|
+
ggplot() + geom_function("z = sin(x) cos(y)", xlim=(-3, 3), ylim=(-3, 3)) # 3D surface, coloured by height
|
|
349
|
+
ggplot() + geom_function(r"y = \frac{\sin x}{x}") # LaTeX input
|
|
350
|
+
ggplot() + geom_function("r = 1 + cos(theta)") + coord_polar()
|
|
351
|
+
ggplot() + geom_function("y > x^2") # shaded region
|
|
352
|
+
ggplot(trial, aes(x="dose", y="response")) + geom_point() + geom_function("y = 2 + 0.6x")
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Distributions and probability areas (no SciPy needed):
|
|
356
|
+
|
|
357
|
+
```python
|
|
358
|
+
inf = float("inf")
|
|
359
|
+
ggplot() + geom_function("y = dbeta(x, 2, 5)") + area(0.2, 0.5) # P(0.2 ≤ X ≤ 0.5) = 0.546
|
|
360
|
+
ggplot() + geom_function("y = dnorm(x)") + area(-inf, -1.96) + area(1.96, inf)
|
|
361
|
+
ggplot() + geom_function("y = dt(x, 3)") + area(2.353, inf) # P(X ≥ 2.353) = 0.05
|
|
362
|
+
ggplot() + geom_function("y = x^3 - 3x") + tangent(at=1) + derivative()
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Formulas know `sin cos tan exp log ln sqrt abs floor ceil gamma lgamma beta
|
|
366
|
+
erf erfc`, the densities `dnorm dbeta dt dchisq dgamma dexp dunif dlnorm`, and
|
|
367
|
+
`pnorm qnorm pt qt pbeta pexp punif`. A density opens on its own support
|
|
368
|
+
(`dbeta` on [0, 1]). Poles (`1/x`, `tan x`) are clipped with a note; steep but
|
|
369
|
+
finite curves are not. Pass your own functions as keywords:
|
|
370
|
+
`geom_function("y = damp(x) sin(3x)", damp=my_damp)`, or a lambda.
|
|
371
|
+
|
|
372
|
+
## Animation and sliders
|
|
373
|
+
|
|
374
|
+
```python
|
|
375
|
+
ggplot() + geom_function("y = sin(x - t)") + transition_time(t=(0, 6.28)) # travelling wave
|
|
376
|
+
ggplot() + geom_function("y = dbeta(x, a, b)") + slider(a=(0.5, 5), b=(0.5, 5))
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Data animations follow `gganimate`:
|
|
380
|
+
|
|
381
|
+
```python notest
|
|
382
|
+
(ggplot(gapminder, aes(x="gdp", y="life", size="pop", colour="continent", group="country"))
|
|
383
|
+
+ geom_point() + scale_x_log10() + transition_time("year") + labs(title="{frame_time}"))
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
The viewer interpolates between frames in the browser, with play, pause,
|
|
387
|
+
scrubbing, speed, and video recording.
|
|
388
|
+
|
|
389
|
+
## 3D and point clouds
|
|
390
|
+
|
|
391
|
+
Map `z` on every layer for an orbit view. A point cloud with no colour of
|
|
392
|
+
its own is coloured by height (viridis), as lidar viewers draw it; map
|
|
393
|
+
`colour=` or set `colour="steelblue"` to change that.
|
|
394
|
+
|
|
395
|
+
```python notest
|
|
396
|
+
ggplot(lidar, aes(x="x", y="y", z="z")) + geom_point3d() # coloured by height
|
|
397
|
+
|
|
398
|
+
(ggplot(cloud, aes(x="x", y="y", z="z", colour="intensity"))
|
|
399
|
+
+ geom_point3d(size=0.008)
|
|
400
|
+
+ coord_3d(aspect="data", max_points=300_000)
|
|
401
|
+
+ scale_colour_viridis_c(option="turbo"))
|
|
402
|
+
|
|
403
|
+
ggplot(grid, aes(x="x", y="y", z="height", fill="height")) + geom_surface()
|
|
404
|
+
ggplot(points, aes(x="x", y="y", z="z")) + geom_isosurface(levels=[0.2, 0.5, 0.8])
|
|
405
|
+
read_bin("scan.pcd.bin") # nuScenes-style point clouds; remote=True under CRAFT
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
NumPy arrays use column positions: `ggplot(pts, aes(x=0, y=1, z=2, colour=3))`.
|
|
409
|
+
|
|
410
|
+
A driving scene, as autonomous-driving viewers draw it: points coloured by
|
|
411
|
+
height on black, detection boxes by class, and a chase camera behind the car.
|
|
412
|
+
|
|
413
|
+
```python notest
|
|
414
|
+
(ggplot(sweep, aes(x="x", y="y", z="z"))
|
|
415
|
+
+ geom_point3d()
|
|
416
|
+
+ geom_box3d(aes(length="l", width="w", height="h", angle="yaw", colour="class"),
|
|
417
|
+
data=boxes)
|
|
418
|
+
+ coord_3d(azim=180, elev=28, zoom=1.6)
|
|
419
|
+
+ theme_lidar())
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
`geom_box3d` takes each box's centre (`x`, `y`, `z`), its `length` along
|
|
423
|
+
the heading, `width`, `height`, and the heading `angle` in radians, as
|
|
424
|
+
nuScenes and KITTI store them. `coord_3d(elev=, azim=, zoom=)` sets where
|
|
425
|
+
the camera starts, in degrees as matplotlib's `view_init`.
|
|
426
|
+
|
|
427
|
+
The box keeps the data's proportions, except that a tall cloud (a helix, a
|
|
428
|
+
tree) is shortened to twice its width so it does not become a thin column;
|
|
429
|
+
`coord_3d(aspect="data")` keeps true proportions always, and
|
|
430
|
+
`aspect="equal"` draws a cube.
|
|
431
|
+
|
|
432
|
+
## Notebooks, SolveIt, and CRAFT
|
|
433
|
+
|
|
434
|
+
- **Jupyter / SolveIt**: bare column names and backticks work in `aes()`,
|
|
435
|
+
`facet_wrap()`, and `facet_grid()` (`aes(x=`First Name`)`). Toggle with
|
|
436
|
+
`enable_r_style()` / `disable_r_style()`. `.py` files keep quoted strings.
|
|
437
|
+
- **SolveIt** draws figures inline and hides their HTML from the model's
|
|
438
|
+
context (`autohide(False)` to opt out).
|
|
439
|
+
- **VS Code notebooks** block WebGL in output cells, so plot3 opens figures
|
|
440
|
+
in your browser (`PLOT3_DISPLAY=browser|iframe` to force a mode).
|
|
441
|
+
- **CRAFT / `%gpu`**: the same `ggplot(...)` code runs on the remote kernel;
|
|
442
|
+
stats run where the data lives and only a compact payload comes back.
|
|
443
|
+
|
|
444
|
+
```text
|
|
445
|
+
%run /path/to/plot3/plot3.py # loads plot3 locally and seeds the GPU kernel
|
|
446
|
+
%plot3 df x=wt y=mpg color=cyl # optional shortcut magic
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
## API reference
|
|
450
|
+
|
|
451
|
+
| Area | Functions |
|
|
452
|
+
|---|---|
|
|
453
|
+
| Figure | `ggplot(data, aes(...))`, `data >> ggplot(aes(...))`, `+`, `p.show()`, `p.save()`, `ggsave()` |
|
|
454
|
+
| Aesthetics | `aes(x, y, z, colour, fill, size, shape, linetype, group, label, ymin, ymax, xmin, xmax, xend, yend, sample)` |
|
|
455
|
+
| Points and lines | `geom_point`, `geom_jitter`, `geom_line`, `geom_path`, `geom_step`, `geom_segment`, `geom_text`, `geom_label` |
|
|
456
|
+
| Bars and areas | `geom_col`, `geom_bar`, `geom_histogram`, `geom_freqpoly`, `geom_area`, `geom_ribbon`, `geom_rect`, `geom_tile`/`geom_raster`, `geom_polygon` |
|
|
457
|
+
| Distributions | `geom_boxplot`, `geom_violin`, `geom_density`, `geom_qq`, `geom_qq_line`, `stat_ecdf`, `stat_summary` |
|
|
458
|
+
| 2D distributions | `geom_bin_2d`, `geom_hex`, `geom_count`, `geom_density_2d` / `stat_density_2d`, `geom_density_2d_filled`, `geom_contour`, `stat_ellipse` |
|
|
459
|
+
| Uncertainty and fits | `geom_errorbar`, `geom_errorbarh`, `geom_crossbar`, `geom_pointrange`, `geom_linerange`, `geom_smooth(method="loess"/"lm")` |
|
|
460
|
+
| Reference | `geom_hline`, `geom_vline`, `geom_abline`, `geom_rug`, `annotate("text"/"label"/"rect"/"segment"/"point")` |
|
|
461
|
+
| Positions | `position="stack"/"dodge"/"fill"/"identity"/"jitter"`, `position_dodge(width)`, `position_dodge2(padding)`, `position_stack()`, `position_fill()`, `position_jitter()`, `position_jitterdodge()`, `position_nudge()` |
|
|
462
|
+
| Functions | `geom_function`, `geom_vector_field`, `area`, `tangent`, `derivative` |
|
|
463
|
+
| Scales | see [Scales](#scales) |
|
|
464
|
+
| Coordinates | `coord_cartesian`, `coord_flip`, `coord_equal` / `coord_fixed`, `coord_polar`, `coord_3d` |
|
|
465
|
+
| Facets and layout | `facet_wrap`, `facet_grid`, `labeller="label_both"` / `labeller(var=dict)`, `p1 \| p2`, `p1 / p2`, `plot_layout(widths, heights, height)`, `plot_annotation` |
|
|
466
|
+
| Labels and themes | `labs(title, subtitle, caption, tag, x, y, colour, fill, alpha)`, `ggtitle`, `xlab`, `ylab`, `guides`, `theme_*`, `theme()`, `element_text`, `element_line`, `element_rect`, `element_blank` |
|
|
467
|
+
| Animation | `transition_time`, `transition_states`, `slider` |
|
|
468
|
+
| 3D | `geom_point3d`, `geom_surface`, `geom_isosurface`, `geom_box3d`, `stat_density_3d`, `read_bin` |
|
|
469
|
+
|
|
470
|
+
## Differences from ggplot2
|
|
471
|
+
|
|
472
|
+
- Python needs quotes outside notebooks: `aes(x="wt")`. In Jupyter and
|
|
473
|
+
SolveIt, `aes(x=wt)` works.
|
|
474
|
+
- `labs(x=None)` (or `labs(x="")`) removes a title, as ggplot2's `labs(x = NULL)`.
|
|
475
|
+
- `fill` colours filled shapes; points and lines use `colour`, as in ggplot2.
|
|
476
|
+
- Categories sort alphabetically (numbers numerically); use
|
|
477
|
+
`pd.Categorical` or `scale_x_discrete(limits=)` for your own order.
|
|
478
|
+
- Text `size` is in millimetres, as in ggplot2; `geom_point(size=)` is in
|
|
479
|
+
pixels in 2D.
|
|
480
|
+
- Saved files default to `theme_bw`, the interactive viewer to `theme_dark`.
|
|
481
|
+
On light themes, a layer with no colour of its own is drawn as ggplot2
|
|
482
|
+
draws it: black points and lines, grey bars, white boxes and violins. The
|
|
483
|
+
dark viewer uses its own blue instead.
|
|
484
|
+
- Building a figure prints nothing. `PLOT3_VERBOSE=1` prints each figure's
|
|
485
|
+
size in KB, for embedding in slides or pages with a size cap.
|
|
486
|
+
|
|
487
|
+
## Development
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
pytest -q # ~670 tests, including every example in this README
|
|
491
|
+
python examples/showcase_2d.py # 2D gallery in the browser
|
|
492
|
+
python examples/showcase_3d.py # 3D gallery
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
The code is in `plot3/`: `geoms.py` (grammar objects), `scaling.py`
|
|
496
|
+
(scale functions), `build.py` (stats to a figure spec), `stat2d.py` and
|
|
497
|
+
`flip.py` (statistical layers), `static.py` (PNG/SVG/PDF), `viewer.py` (the
|
|
498
|
+
WebGL viewer), `expr.py` / `function.py` / `calculus.py` (formulas),
|
|
499
|
+
`compose.py` (multi-panel figures). See [CHANGELOG.md](CHANGELOG.md) for
|
|
500
|
+
changes between versions.
|
|
501
|
+
|
|
502
|
+
## License
|
|
503
|
+
|
|
504
|
+
MIT. See [LICENSE](LICENSE).
|