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.
Files changed (80) hide show
  1. plot3-0.4.0/LICENSE +21 -0
  2. plot3-0.4.0/PKG-INFO +504 -0
  3. plot3-0.4.0/README.md +471 -0
  4. plot3-0.4.0/plot3/__init__.py +301 -0
  5. plot3-0.4.0/plot3/__version__.py +1 -0
  6. plot3-0.4.0/plot3/aesexpr.py +271 -0
  7. plot3-0.4.0/plot3/build.py +3948 -0
  8. plot3-0.4.0/plot3/calculus.py +1179 -0
  9. plot3-0.4.0/plot3/compose.py +285 -0
  10. plot3-0.4.0/plot3/contour.py +476 -0
  11. plot3-0.4.0/plot3/craft.py +142 -0
  12. plot3-0.4.0/plot3/encode.py +68 -0
  13. plot3-0.4.0/plot3/expr.py +1557 -0
  14. plot3-0.4.0/plot3/flip.py +245 -0
  15. plot3-0.4.0/plot3/function.py +1301 -0
  16. plot3-0.4.0/plot3/geoms.py +2558 -0
  17. plot3-0.4.0/plot3/ggplot.py +713 -0
  18. plot3-0.4.0/plot3/io.py +76 -0
  19. plot3-0.4.0/plot3/jupyter.py +514 -0
  20. plot3-0.4.0/plot3/latexin.py +616 -0
  21. plot3-0.4.0/plot3/masking.py +494 -0
  22. plot3-0.4.0/plot3/mathtext.py +842 -0
  23. plot3-0.4.0/plot3/payload.py +216 -0
  24. plot3-0.4.0/plot3/remote.py +220 -0
  25. plot3-0.4.0/plot3/scales.py +387 -0
  26. plot3-0.4.0/plot3/scaling.py +636 -0
  27. plot3-0.4.0/plot3/special.py +407 -0
  28. plot3-0.4.0/plot3/stat2d.py +1539 -0
  29. plot3-0.4.0/plot3/static.py +3760 -0
  30. plot3-0.4.0/plot3/stats3d.py +462 -0
  31. plot3-0.4.0/plot3/table.py +775 -0
  32. plot3-0.4.0/plot3/themes.py +104 -0
  33. plot3-0.4.0/plot3/viewer.py +3354 -0
  34. plot3-0.4.0/plot3.egg-info/PKG-INFO +504 -0
  35. plot3-0.4.0/plot3.egg-info/SOURCES.txt +78 -0
  36. plot3-0.4.0/plot3.egg-info/dependency_links.txt +1 -0
  37. plot3-0.4.0/plot3.egg-info/requires.txt +19 -0
  38. plot3-0.4.0/plot3.egg-info/top_level.txt +1 -0
  39. plot3-0.4.0/pyproject.toml +50 -0
  40. plot3-0.4.0/setup.cfg +4 -0
  41. plot3-0.4.0/tests/test_2d_density.py +130 -0
  42. plot3-0.4.0/tests/test_3d.py +263 -0
  43. plot3-0.4.0/tests/test_animation.py +641 -0
  44. plot3-0.4.0/tests/test_annotate.py +257 -0
  45. plot3-0.4.0/tests/test_api_public.py +69 -0
  46. plot3-0.4.0/tests/test_array_backend.py +189 -0
  47. plot3-0.4.0/tests/test_arrow.py +78 -0
  48. plot3-0.4.0/tests/test_build_contract.py +65 -0
  49. plot3-0.4.0/tests/test_contour.py +353 -0
  50. plot3-0.4.0/tests/test_everyday_ggplot2.py +137 -0
  51. plot3-0.4.0/tests/test_expr.py +126 -0
  52. plot3-0.4.0/tests/test_facets_compose.py +165 -0
  53. plot3-0.4.0/tests/test_geom_function.py +517 -0
  54. plot3-0.4.0/tests/test_geoms_ds.py +130 -0
  55. plot3-0.4.0/tests/test_ggplot2_extras2.py +110 -0
  56. plot3-0.4.0/tests/test_ggplot2_parity.py +139 -0
  57. plot3-0.4.0/tests/test_ggplot_extras.py +117 -0
  58. plot3-0.4.0/tests/test_grammar.py +99 -0
  59. plot3-0.4.0/tests/test_latex_input.py +249 -0
  60. plot3-0.4.0/tests/test_lidar_scene.py +100 -0
  61. plot3-0.4.0/tests/test_masking.py +185 -0
  62. plot3-0.4.0/tests/test_math_layers.py +310 -0
  63. plot3-0.4.0/tests/test_mathtext.py +270 -0
  64. plot3-0.4.0/tests/test_payload.py +153 -0
  65. plot3-0.4.0/tests/test_payload_display.py +116 -0
  66. plot3-0.4.0/tests/test_point_cloud.py +63 -0
  67. plot3-0.4.0/tests/test_r_oracle_parity.py +184 -0
  68. plot3-0.4.0/tests/test_readme.py +63 -0
  69. plot3-0.4.0/tests/test_remote_bridge.py +232 -0
  70. plot3-0.4.0/tests/test_review_fixes.py +150 -0
  71. plot3-0.4.0/tests/test_save_menu.py +252 -0
  72. plot3-0.4.0/tests/test_scales_api.py +230 -0
  73. plot3-0.4.0/tests/test_scales_themes.py +135 -0
  74. plot3-0.4.0/tests/test_special.py +175 -0
  75. plot3-0.4.0/tests/test_stat2d.py +232 -0
  76. plot3-0.4.0/tests/test_static_save.py +843 -0
  77. plot3-0.4.0/tests/test_stats_semantic.py +86 -0
  78. plot3-0.4.0/tests/test_step3_geoms.py +195 -0
  79. plot3-0.4.0/tests/test_table_backends.py +426 -0
  80. 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).