plot3 0.4.2__tar.gz → 0.5.1__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 (84) hide show
  1. plot3-0.5.1/PKG-INFO +308 -0
  2. plot3-0.5.1/README.md +275 -0
  3. plot3-0.5.1/plot3/__version__.py +1 -0
  4. {plot3-0.4.2 → plot3-0.5.1}/plot3/build.py +21 -6
  5. {plot3-0.4.2 → plot3-0.5.1}/plot3/calculus.py +102 -24
  6. {plot3-0.4.2 → plot3-0.5.1}/plot3/contour.py +138 -26
  7. {plot3-0.4.2 → plot3-0.5.1}/plot3/expr.py +8 -0
  8. {plot3-0.4.2 → plot3-0.5.1}/plot3/geoms.py +8 -2
  9. {plot3-0.4.2 → plot3-0.5.1}/plot3/ggplot.py +7 -6
  10. {plot3-0.4.2 → plot3-0.5.1}/plot3/static.py +88 -9
  11. {plot3-0.4.2 → plot3-0.5.1}/plot3/stats3d.py +104 -137
  12. plot3-0.5.1/plot3.egg-info/PKG-INFO +308 -0
  13. {plot3-0.4.2 → plot3-0.5.1}/tests/test_3d.py +29 -0
  14. {plot3-0.4.2 → plot3-0.5.1}/tests/test_contour.py +30 -0
  15. {plot3-0.4.2 → plot3-0.5.1}/tests/test_geom_function.py +16 -0
  16. {plot3-0.4.2 → plot3-0.5.1}/tests/test_grammar.py +24 -0
  17. {plot3-0.4.2 → plot3-0.5.1}/tests/test_math_layers.py +18 -0
  18. {plot3-0.4.2 → plot3-0.5.1}/tests/test_readme.py +14 -10
  19. {plot3-0.4.2 → plot3-0.5.1}/tests/test_scales_api.py +18 -0
  20. {plot3-0.4.2 → plot3-0.5.1}/tests/test_static_save.py +34 -0
  21. plot3-0.4.2/PKG-INFO +0 -521
  22. plot3-0.4.2/README.md +0 -488
  23. plot3-0.4.2/plot3/__version__.py +0 -1
  24. plot3-0.4.2/plot3.egg-info/PKG-INFO +0 -521
  25. {plot3-0.4.2 → plot3-0.5.1}/LICENSE +0 -0
  26. {plot3-0.4.2 → plot3-0.5.1}/plot3/__init__.py +0 -0
  27. {plot3-0.4.2 → plot3-0.5.1}/plot3/aesexpr.py +0 -0
  28. {plot3-0.4.2 → plot3-0.5.1}/plot3/compose.py +0 -0
  29. {plot3-0.4.2 → plot3-0.5.1}/plot3/craft.py +0 -0
  30. {plot3-0.4.2 → plot3-0.5.1}/plot3/encode.py +0 -0
  31. {plot3-0.4.2 → plot3-0.5.1}/plot3/flip.py +0 -0
  32. {plot3-0.4.2 → plot3-0.5.1}/plot3/function.py +0 -0
  33. {plot3-0.4.2 → plot3-0.5.1}/plot3/io.py +0 -0
  34. {plot3-0.4.2 → plot3-0.5.1}/plot3/jupyter.py +0 -0
  35. {plot3-0.4.2 → plot3-0.5.1}/plot3/latexin.py +0 -0
  36. {plot3-0.4.2 → plot3-0.5.1}/plot3/masking.py +0 -0
  37. {plot3-0.4.2 → plot3-0.5.1}/plot3/mathtext.py +0 -0
  38. {plot3-0.4.2 → plot3-0.5.1}/plot3/payload.py +0 -0
  39. {plot3-0.4.2 → plot3-0.5.1}/plot3/remote.py +0 -0
  40. {plot3-0.4.2 → plot3-0.5.1}/plot3/scales.py +0 -0
  41. {plot3-0.4.2 → plot3-0.5.1}/plot3/scaling.py +0 -0
  42. {plot3-0.4.2 → plot3-0.5.1}/plot3/special.py +0 -0
  43. {plot3-0.4.2 → plot3-0.5.1}/plot3/stat2d.py +0 -0
  44. {plot3-0.4.2 → plot3-0.5.1}/plot3/table.py +0 -0
  45. {plot3-0.4.2 → plot3-0.5.1}/plot3/themes.py +0 -0
  46. {plot3-0.4.2 → plot3-0.5.1}/plot3/viewer.py +0 -0
  47. {plot3-0.4.2 → plot3-0.5.1}/plot3.egg-info/SOURCES.txt +0 -0
  48. {plot3-0.4.2 → plot3-0.5.1}/plot3.egg-info/dependency_links.txt +0 -0
  49. {plot3-0.4.2 → plot3-0.5.1}/plot3.egg-info/requires.txt +0 -0
  50. {plot3-0.4.2 → plot3-0.5.1}/plot3.egg-info/top_level.txt +0 -0
  51. {plot3-0.4.2 → plot3-0.5.1}/pyproject.toml +0 -0
  52. {plot3-0.4.2 → plot3-0.5.1}/setup.cfg +0 -0
  53. {plot3-0.4.2 → plot3-0.5.1}/tests/test_2d_density.py +0 -0
  54. {plot3-0.4.2 → plot3-0.5.1}/tests/test_animation.py +0 -0
  55. {plot3-0.4.2 → plot3-0.5.1}/tests/test_annotate.py +0 -0
  56. {plot3-0.4.2 → plot3-0.5.1}/tests/test_api_public.py +0 -0
  57. {plot3-0.4.2 → plot3-0.5.1}/tests/test_array_backend.py +0 -0
  58. {plot3-0.4.2 → plot3-0.5.1}/tests/test_arrow.py +0 -0
  59. {plot3-0.4.2 → plot3-0.5.1}/tests/test_build_contract.py +0 -0
  60. {plot3-0.4.2 → plot3-0.5.1}/tests/test_everyday_ggplot2.py +0 -0
  61. {plot3-0.4.2 → plot3-0.5.1}/tests/test_expr.py +0 -0
  62. {plot3-0.4.2 → plot3-0.5.1}/tests/test_facets_compose.py +0 -0
  63. {plot3-0.4.2 → plot3-0.5.1}/tests/test_geoms_ds.py +0 -0
  64. {plot3-0.4.2 → plot3-0.5.1}/tests/test_ggplot2_extras2.py +0 -0
  65. {plot3-0.4.2 → plot3-0.5.1}/tests/test_ggplot2_parity.py +0 -0
  66. {plot3-0.4.2 → plot3-0.5.1}/tests/test_ggplot_extras.py +0 -0
  67. {plot3-0.4.2 → plot3-0.5.1}/tests/test_latex_input.py +0 -0
  68. {plot3-0.4.2 → plot3-0.5.1}/tests/test_lidar_scene.py +0 -0
  69. {plot3-0.4.2 → plot3-0.5.1}/tests/test_masking.py +0 -0
  70. {plot3-0.4.2 → plot3-0.5.1}/tests/test_mathtext.py +0 -0
  71. {plot3-0.4.2 → plot3-0.5.1}/tests/test_payload.py +0 -0
  72. {plot3-0.4.2 → plot3-0.5.1}/tests/test_payload_display.py +0 -0
  73. {plot3-0.4.2 → plot3-0.5.1}/tests/test_point_cloud.py +0 -0
  74. {plot3-0.4.2 → plot3-0.5.1}/tests/test_r_oracle_parity.py +0 -0
  75. {plot3-0.4.2 → plot3-0.5.1}/tests/test_remote_bridge.py +0 -0
  76. {plot3-0.4.2 → plot3-0.5.1}/tests/test_review_fixes.py +0 -0
  77. {plot3-0.4.2 → plot3-0.5.1}/tests/test_save_menu.py +0 -0
  78. {plot3-0.4.2 → plot3-0.5.1}/tests/test_scales_themes.py +0 -0
  79. {plot3-0.4.2 → plot3-0.5.1}/tests/test_special.py +0 -0
  80. {plot3-0.4.2 → plot3-0.5.1}/tests/test_stat2d.py +0 -0
  81. {plot3-0.4.2 → plot3-0.5.1}/tests/test_stats_semantic.py +0 -0
  82. {plot3-0.4.2 → plot3-0.5.1}/tests/test_step3_geoms.py +0 -0
  83. {plot3-0.4.2 → plot3-0.5.1}/tests/test_table_backends.py +0 -0
  84. {plot3-0.4.2 → plot3-0.5.1}/tests/test_titles_theme.py +0 -0
plot3-0.5.1/PKG-INFO ADDED
@@ -0,0 +1,308 @@
1
+ Metadata-Version: 2.4
2
+ Name: plot3
3
+ Version: 0.5.1
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 for Python, plus what ggplot2 can't do: interactive 3D, animated
37
+ data, and plots typed straight from an equation.** Figures render with WebGL
38
+ in the notebook and save as journal-ready PNG, SVG, or PDF.
39
+
40
+ <table>
41
+ <tr>
42
+ <td width="50%"><img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/gapminder.gif" alt="Gapminder: life expectancy against GDP per capita, animated from 1952 to 2007"></td>
43
+ <td width="50%"><img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/lidar.gif" alt="A lidar driving scene with detection boxes, orbiting"></td>
44
+ </tr>
45
+ <tr>
46
+ <td><sub><code>+ transition_time("year")</code>: gapminder in one line</sub></td>
47
+ <td><sub><code>geom_point3d()</code> + <code>geom_box3d()</code>: a 220k-point lidar sweep with detection boxes</sub></td>
48
+ </tr>
49
+ <tr>
50
+ <td><img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/ripple.gif" alt="An animated ripple surface z = sin(r - t)"></td>
51
+ <td><img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/beta_slider.gif" alt="A beta density reshaping as two sliders move"></td>
52
+ </tr>
53
+ <tr>
54
+ <td><sub><code>geom_function("z = sin(sqrt(x^2 + y^2) - t)") + transition_time(t=(0, 6.28))</code></sub></td>
55
+ <td><sub><code>geom_function("y = dbeta(x, a, b)") + slider(a=…, b=…)</code></sub></td>
56
+ </tr>
57
+ </table>
58
+
59
+ ```bash
60
+ pip install "plot3[jupyter,export]"
61
+ ```
62
+
63
+ Contents: [Plot an equation](#plot-an-equation) ·
64
+ [Animate your data](#animate-your-data) · [3D and point clouds](#3d-and-point-clouds) ·
65
+ [One figure, two outputs](#one-figure-two-outputs) · [It's ggplot2](#its-ggplot2) ·
66
+ [Notebooks, SolveIt, CRAFT](#notebooks-solveit-and-craft) · [Install](#install) ·
67
+ [Full reference](docs/reference.md)
68
+
69
+ ## Plot an equation
70
+
71
+ Write the maths as you would on paper. `geom_function` works out whether
72
+ it's a curve, an implicit shape, a region, or a 3D surface, and picks a
73
+ sensible window.
74
+
75
+ ```python
76
+ from plot3 import *
77
+
78
+ (ggplot() + geom_function("y = sin(x)", xlim=(-4, 4)) # a function and
79
+ + geom_function("y = x - x^3/6", xlim=(-4, 4))) # its Taylor polynomial
80
+ ggplot() + geom_function("(x^2 + y^2 - 1)^3 = x^2 y^3") # implicit: a heart
81
+ ggplot() + geom_function("y > x^2", xlim=(-2, 2)) # shaded region
82
+ ggplot() + geom_function("r = 1 + cos(theta)") + coord_polar() # polar cardioid
83
+ ggplot() + geom_function(r"z = \frac{\sin\sqrt{x^2+y^2}}{\sqrt{x^2+y^2}}", # LaTeX in, 3D sombrero out
84
+ xlim=(-10, 10), ylim=(-10, 10))
85
+ ggplot() + geom_vector_field("dx = y, dy = -sin(x) - 0.3y", # a damped pendulum's
86
+ xlim=(-6, 6), ylim=(-3, 3), stream=True) # phase portrait
87
+ ```
88
+
89
+ <p>
90
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/fn_taylor.png" width="15%" alt="sin(x) and its Taylor polynomial">
91
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/fn_heart.png" width="15%" alt="Implicit heart curve">
92
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/fn_region.png" width="15%" alt="Shaded region y > x^2">
93
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/fn_cardioid.png" width="15%" alt="Polar cardioid">
94
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/fn_sombrero.png" width="15%" alt="3D sombrero surface">
95
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/fn_streamlines.png" width="15%" alt="Streamlines of a damped pendulum">
96
+ </p>
97
+
98
+ **Probability and calculus, with the answer printed.** No SciPy needed:
99
+
100
+ ```python
101
+ inf = float("inf")
102
+ ggplot() + geom_function("y = dbeta(x, 2, 5)") + area(0.2, 0.5) # labels P(0.2 ≤ X ≤ 0.5) = 0.546
103
+ ggplot() + geom_function("y = dnorm(x)") + area(-inf, -1.96) + area(1.96, inf)
104
+ ggplot() + geom_function("y = x^3 - 3x") + tangent(at=1) + derivative()
105
+ ```
106
+
107
+ **Turn any coefficient into a slider or an animation:**
108
+
109
+ ```python
110
+ ggplot() + geom_function("y = dbeta(x, a, b)") + slider(a=(0.5, 5), b=(0.5, 5))
111
+ ggplot() + geom_function("y = sin(x - t)") + transition_time(t=(0, 6.28)) # travelling wave
112
+ ```
113
+
114
+ Equations mix freely with data:
115
+ `ggplot(df, aes(x="dose", y="response")) + geom_point() + geom_function("y = 2 + 0.6x")`.
116
+ `^` is power and `2x` means `2*x`. All functions, densities, and rules are in
117
+ the [reference](docs/reference.md#functions-and-maths).
118
+
119
+ ## Animate your data
120
+
121
+ [gganimate](https://gganimate.com)'s grammar: add a transition and the plot
122
+ plays. The viewer interpolates between frames in the browser, with play,
123
+ pause, scrubbing, speed control, and one-click video export.
124
+
125
+ ```python notest
126
+ import pandas as pd
127
+ gapminder = pd.read_csv("https://raw.githubusercontent.com/kirenz/datasets/master/gapminder.csv")
128
+
129
+ (ggplot(gapminder, aes(x="gdpPercap", y="lifeExp", size="pop", colour="continent", group="country"))
130
+ + geom_point(alpha=0.7)
131
+ + scale_x_log10()
132
+ + transition_time("year")
133
+ + labs(title="{frame_time}", x="GDP per capita", y="Life expectancy"))
134
+ ```
135
+
136
+ `{frame_time}` in the title shows the current year as it plays.
137
+ `transition_states("phase")` steps through categories instead of time.
138
+
139
+ <details>
140
+ <summary>Run this without downloading anything (made-up gapminder-shaped data)</summary>
141
+
142
+ ```python
143
+ import pandas as pd
144
+
145
+ countries = {"Brazil": "Americas", "China": "Asia", "Egypt": "Africa",
146
+ "France": "Europe", "India": "Asia", "Mexico": "Americas"}
147
+ gapminder = pd.DataFrame([
148
+ {"country": country, "continent": continent, "year": year,
149
+ "gdpPercap": 800 * (1.03 + 0.01 * i) ** (year - 1952),
150
+ "lifeExp": 45 + 0.35 * (year - 1952) + 2 * i,
151
+ "pop": 2e7 * (1.02 + 0.002 * i) ** (year - 1952)}
152
+ for i, (country, continent) in enumerate(countries.items())
153
+ for year in range(1952, 2008, 5)
154
+ ])
155
+
156
+ (ggplot(gapminder, aes(x="gdpPercap", y="lifeExp", size="pop", colour="continent", group="country"))
157
+ + geom_point() + scale_x_log10() + transition_time("year") + labs(title="{frame_time}"))
158
+ ```
159
+
160
+ </details>
161
+
162
+ ## 3D and point clouds
163
+
164
+ Map `z` and the plot becomes a 3D scene you can orbit, zoom, and hover.
165
+ Hundreds of thousands of points stay smooth because they're drawn on the
166
+ GPU.
167
+
168
+ ```python
169
+ import numpy as np
170
+
171
+ rng = np.random.default_rng(0)
172
+ arm = rng.integers(0, 2, 20_000)
173
+ r = rng.gamma(2, 1.2, 20_000)
174
+ theta = r * 0.9 + arm * np.pi + rng.normal(0, 0.25, 20_000)
175
+ galaxy = pd.DataFrame({"x": r * np.cos(theta), "y": r * np.sin(theta),
176
+ "z": rng.normal(0, 0.15, 20_000), "r": r})
177
+
178
+ (ggplot(galaxy, aes(x="x", y="y", z="z", colour="r"))
179
+ + geom_point3d(size=0.01) + scale_colour_viridis_c(option="magma")
180
+ + coord_3d(elev=40) + theme_lidar() + guides(colour="none"))
181
+ ```
182
+
183
+ <p>
184
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/galaxy.gif" width="32%" alt="A 20,000-point spiral galaxy, orbiting">
185
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/peaks.png" width="32%" alt="The peaks surface coloured by height">
186
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/isosurface.png" width="32%" alt="Nested density isosurfaces of three clusters">
187
+ </p>
188
+
189
+ Surfaces, isosurfaces, and self-driving scenes use the same grammar:
190
+
191
+ ```python notest
192
+ ggplot(grid, aes(x="x", y="y", z="height", fill="height")) + geom_surface()
193
+ ggplot(points, aes(x="x", y="y", z="z")) + geom_isosurface(levels=[0.2, 0.5, 0.8])
194
+
195
+ (ggplot(sweep, aes(x="x", y="y", z="z")) # a nuScenes / KITTI lidar sweep
196
+ + geom_point3d() # coloured by height, as lidar viewers do
197
+ + geom_box3d(aes(length="l", width="w", height="h", angle="yaw", colour="class"), data=boxes)
198
+ + coord_3d(azim=180, elev=28, zoom=1.6) # chase camera behind the car
199
+ + theme_lidar())
200
+ ```
201
+
202
+ `read_bin("scan.pcd.bin")` loads nuScenes-style point clouds, and NumPy
203
+ arrays work by column position: `aes(x=0, y=1, z=2, colour=3)`. Camera,
204
+ aspect, and point-budget options are in the
205
+ [reference](docs/reference.md#api-reference) under `coord_3d`.
206
+
207
+ ## One figure, two outputs
208
+
209
+ The same object is an interactive figure in the notebook and a publication
210
+ figure on disk, with real fonts, correct DPI metadata, and `theme_bw` by
211
+ default.
212
+
213
+ ```python notest
214
+ ggsave("fig1.pdf", p, width=3.5, height=2.6, units="in") # vector, one journal column
215
+ ggsave("fig1.png", p, width=7, height=4.5, units="in", dpi=300) # two columns
216
+ ggsave("fig1.html", p) # interactive page to share
217
+ ```
218
+
219
+ ## It's ggplot2
220
+
221
+ If you know ggplot2, you already know plot3: `+` layers, `aes()`, geoms,
222
+ stats, scales, facets, themes, `labs()`, and patchwork's `|` and `/`, with
223
+ ggplot2's names and defaults.
224
+
225
+ ```python
226
+ rng = np.random.default_rng(1)
227
+ trial = pd.DataFrame({"arm": rng.choice(["placebo", "low", "high"], 150),
228
+ "dose": rng.uniform(0, 10, 150)})
229
+ trial["response"] = 2 + 0.6 * trial.dose + 1.5 * (trial.arm == "high") + rng.normal(0, 1.2, 150)
230
+
231
+ a = (ggplot(trial, aes(x="dose", y="response", colour="arm"))
232
+ + geom_point(alpha=0.7) + geom_smooth(method="lm"))
233
+ b = (ggplot(trial, aes(x="arm", y="response", fill="arm"))
234
+ + geom_violin() + geom_jitter(width=0.1, height=0) + theme(legend_position="none"))
235
+ fig = (a | b) + plot_annotation(title="Response by dose and arm", tag_levels="A")
236
+ ```
237
+
238
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/ggplot2_compose.png" alt="Two-panel figure: scatter with linear fits, and violins by arm">
239
+
240
+ Everything else you'd expect is there, including histograms, densities,
241
+ boxplots, bar positions, `stat_summary`, error bars, heatmaps, hexbins, 2D
242
+ densities, `facet_wrap` / `facet_grid`, Brewer, viridis, and Okabe–Ito
243
+ scales, date axes, `theme()` with `element_*()`, and `coord_flip` /
244
+ `coord_polar`. See the **[full reference](docs/reference.md)** for what
245
+ plot3 supports and how it differs from ggplot2, or use the
246
+ [ggplot2 docs](https://ggplot2.tidyverse.org/reference/) directly; the
247
+ names match.
248
+
249
+ In notebooks, bare column names work as in R: `aes(x=dose, y=response)`.
250
+
251
+ ## Notebooks, SolveIt, and CRAFT
252
+
253
+ - **Jupyter / SolveIt**: bare column names and backticks work in `aes()`,
254
+ `facet_wrap()`, and `facet_grid()` (`aes(x=`First Name`)`). Toggle with
255
+ `enable_r_style()` / `disable_r_style()`. `.py` files keep quoted strings.
256
+ - **SolveIt** draws figures inline and hides their HTML from the model's
257
+ context (`autohide(False)` to opt out).
258
+ - **VS Code notebooks** block WebGL in output cells, so plot3 opens figures
259
+ in your browser (`PLOT3_DISPLAY=browser|iframe` to force a mode). The page
260
+ is written to `.plot3_preview/latest.html` in the notebook's folder, or to
261
+ the system temp folder when that folder is read-only.
262
+ - **CRAFT / `%gpu`**: the same `ggplot(...)` code runs on the remote kernel;
263
+ stats run where the data lives and only a compact payload comes back.
264
+
265
+ ```text
266
+ %run /path/to/plot3/plot3.py # loads plot3 locally and seeds the GPU kernel
267
+ %plot3 df x=wt y=mpg color=cyl # optional shortcut magic
268
+ ```
269
+
270
+ ## Install
271
+
272
+ ```bash
273
+ pip install "plot3[jupyter,export]"
274
+ ```
275
+
276
+ Python 3.10 or newer; NumPy and pandas are the only required packages.
277
+
278
+ | Extra | Adds |
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 |
284
+
285
+ The latest unreleased code installs from GitHub with
286
+ `pip install "plot3[jupyter,export] @ git+https://github.com/rleyvasal/plot3"`.
287
+
288
+ ## Development
289
+
290
+ ```bash
291
+ git clone https://github.com/rleyvasal/plot3 && cd plot3
292
+ python3 -m venv .venv && source .venv/bin/activate
293
+ pip install -e ".[dev,jupyter,export]"
294
+ pytest -q # includes every example in this README and docs/reference.md
295
+ python examples/showcase_2d.py # 2D gallery in the browser
296
+ python examples/showcase_3d.py # 3D gallery
297
+ ```
298
+
299
+ The code is in `plot3/`: `geoms.py` (grammar objects), `scaling.py`
300
+ (scale functions), `build.py` (stats to a figure spec), `stat2d.py` and
301
+ `flip.py` (statistical layers), `static.py` (PNG/SVG/PDF), `viewer.py` (the
302
+ WebGL viewer), `expr.py` / `function.py` / `calculus.py` (formulas),
303
+ `compose.py` (multi-panel figures). See [CHANGELOG.md](CHANGELOG.md) for
304
+ changes between versions.
305
+
306
+ ## License
307
+
308
+ MIT. See [LICENSE](LICENSE).
plot3-0.5.1/README.md ADDED
@@ -0,0 +1,275 @@
1
+ # plot3
2
+
3
+ **ggplot2 for Python, plus what ggplot2 can't do: interactive 3D, animated
4
+ data, and plots typed straight from an equation.** Figures render with WebGL
5
+ in the notebook and save as journal-ready PNG, SVG, or PDF.
6
+
7
+ <table>
8
+ <tr>
9
+ <td width="50%"><img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/gapminder.gif" alt="Gapminder: life expectancy against GDP per capita, animated from 1952 to 2007"></td>
10
+ <td width="50%"><img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/lidar.gif" alt="A lidar driving scene with detection boxes, orbiting"></td>
11
+ </tr>
12
+ <tr>
13
+ <td><sub><code>+ transition_time("year")</code>: gapminder in one line</sub></td>
14
+ <td><sub><code>geom_point3d()</code> + <code>geom_box3d()</code>: a 220k-point lidar sweep with detection boxes</sub></td>
15
+ </tr>
16
+ <tr>
17
+ <td><img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/ripple.gif" alt="An animated ripple surface z = sin(r - t)"></td>
18
+ <td><img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/beta_slider.gif" alt="A beta density reshaping as two sliders move"></td>
19
+ </tr>
20
+ <tr>
21
+ <td><sub><code>geom_function("z = sin(sqrt(x^2 + y^2) - t)") + transition_time(t=(0, 6.28))</code></sub></td>
22
+ <td><sub><code>geom_function("y = dbeta(x, a, b)") + slider(a=…, b=…)</code></sub></td>
23
+ </tr>
24
+ </table>
25
+
26
+ ```bash
27
+ pip install "plot3[jupyter,export]"
28
+ ```
29
+
30
+ Contents: [Plot an equation](#plot-an-equation) ·
31
+ [Animate your data](#animate-your-data) · [3D and point clouds](#3d-and-point-clouds) ·
32
+ [One figure, two outputs](#one-figure-two-outputs) · [It's ggplot2](#its-ggplot2) ·
33
+ [Notebooks, SolveIt, CRAFT](#notebooks-solveit-and-craft) · [Install](#install) ·
34
+ [Full reference](docs/reference.md)
35
+
36
+ ## Plot an equation
37
+
38
+ Write the maths as you would on paper. `geom_function` works out whether
39
+ it's a curve, an implicit shape, a region, or a 3D surface, and picks a
40
+ sensible window.
41
+
42
+ ```python
43
+ from plot3 import *
44
+
45
+ (ggplot() + geom_function("y = sin(x)", xlim=(-4, 4)) # a function and
46
+ + geom_function("y = x - x^3/6", xlim=(-4, 4))) # its Taylor polynomial
47
+ ggplot() + geom_function("(x^2 + y^2 - 1)^3 = x^2 y^3") # implicit: a heart
48
+ ggplot() + geom_function("y > x^2", xlim=(-2, 2)) # shaded region
49
+ ggplot() + geom_function("r = 1 + cos(theta)") + coord_polar() # polar cardioid
50
+ ggplot() + geom_function(r"z = \frac{\sin\sqrt{x^2+y^2}}{\sqrt{x^2+y^2}}", # LaTeX in, 3D sombrero out
51
+ xlim=(-10, 10), ylim=(-10, 10))
52
+ ggplot() + geom_vector_field("dx = y, dy = -sin(x) - 0.3y", # a damped pendulum's
53
+ xlim=(-6, 6), ylim=(-3, 3), stream=True) # phase portrait
54
+ ```
55
+
56
+ <p>
57
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/fn_taylor.png" width="15%" alt="sin(x) and its Taylor polynomial">
58
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/fn_heart.png" width="15%" alt="Implicit heart curve">
59
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/fn_region.png" width="15%" alt="Shaded region y > x^2">
60
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/fn_cardioid.png" width="15%" alt="Polar cardioid">
61
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/fn_sombrero.png" width="15%" alt="3D sombrero surface">
62
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/fn_streamlines.png" width="15%" alt="Streamlines of a damped pendulum">
63
+ </p>
64
+
65
+ **Probability and calculus, with the answer printed.** No SciPy needed:
66
+
67
+ ```python
68
+ inf = float("inf")
69
+ ggplot() + geom_function("y = dbeta(x, 2, 5)") + area(0.2, 0.5) # labels P(0.2 ≤ X ≤ 0.5) = 0.546
70
+ ggplot() + geom_function("y = dnorm(x)") + area(-inf, -1.96) + area(1.96, inf)
71
+ ggplot() + geom_function("y = x^3 - 3x") + tangent(at=1) + derivative()
72
+ ```
73
+
74
+ **Turn any coefficient into a slider or an animation:**
75
+
76
+ ```python
77
+ ggplot() + geom_function("y = dbeta(x, a, b)") + slider(a=(0.5, 5), b=(0.5, 5))
78
+ ggplot() + geom_function("y = sin(x - t)") + transition_time(t=(0, 6.28)) # travelling wave
79
+ ```
80
+
81
+ Equations mix freely with data:
82
+ `ggplot(df, aes(x="dose", y="response")) + geom_point() + geom_function("y = 2 + 0.6x")`.
83
+ `^` is power and `2x` means `2*x`. All functions, densities, and rules are in
84
+ the [reference](docs/reference.md#functions-and-maths).
85
+
86
+ ## Animate your data
87
+
88
+ [gganimate](https://gganimate.com)'s grammar: add a transition and the plot
89
+ plays. The viewer interpolates between frames in the browser, with play,
90
+ pause, scrubbing, speed control, and one-click video export.
91
+
92
+ ```python notest
93
+ import pandas as pd
94
+ gapminder = pd.read_csv("https://raw.githubusercontent.com/kirenz/datasets/master/gapminder.csv")
95
+
96
+ (ggplot(gapminder, aes(x="gdpPercap", y="lifeExp", size="pop", colour="continent", group="country"))
97
+ + geom_point(alpha=0.7)
98
+ + scale_x_log10()
99
+ + transition_time("year")
100
+ + labs(title="{frame_time}", x="GDP per capita", y="Life expectancy"))
101
+ ```
102
+
103
+ `{frame_time}` in the title shows the current year as it plays.
104
+ `transition_states("phase")` steps through categories instead of time.
105
+
106
+ <details>
107
+ <summary>Run this without downloading anything (made-up gapminder-shaped data)</summary>
108
+
109
+ ```python
110
+ import pandas as pd
111
+
112
+ countries = {"Brazil": "Americas", "China": "Asia", "Egypt": "Africa",
113
+ "France": "Europe", "India": "Asia", "Mexico": "Americas"}
114
+ gapminder = pd.DataFrame([
115
+ {"country": country, "continent": continent, "year": year,
116
+ "gdpPercap": 800 * (1.03 + 0.01 * i) ** (year - 1952),
117
+ "lifeExp": 45 + 0.35 * (year - 1952) + 2 * i,
118
+ "pop": 2e7 * (1.02 + 0.002 * i) ** (year - 1952)}
119
+ for i, (country, continent) in enumerate(countries.items())
120
+ for year in range(1952, 2008, 5)
121
+ ])
122
+
123
+ (ggplot(gapminder, aes(x="gdpPercap", y="lifeExp", size="pop", colour="continent", group="country"))
124
+ + geom_point() + scale_x_log10() + transition_time("year") + labs(title="{frame_time}"))
125
+ ```
126
+
127
+ </details>
128
+
129
+ ## 3D and point clouds
130
+
131
+ Map `z` and the plot becomes a 3D scene you can orbit, zoom, and hover.
132
+ Hundreds of thousands of points stay smooth because they're drawn on the
133
+ GPU.
134
+
135
+ ```python
136
+ import numpy as np
137
+
138
+ rng = np.random.default_rng(0)
139
+ arm = rng.integers(0, 2, 20_000)
140
+ r = rng.gamma(2, 1.2, 20_000)
141
+ theta = r * 0.9 + arm * np.pi + rng.normal(0, 0.25, 20_000)
142
+ galaxy = pd.DataFrame({"x": r * np.cos(theta), "y": r * np.sin(theta),
143
+ "z": rng.normal(0, 0.15, 20_000), "r": r})
144
+
145
+ (ggplot(galaxy, aes(x="x", y="y", z="z", colour="r"))
146
+ + geom_point3d(size=0.01) + scale_colour_viridis_c(option="magma")
147
+ + coord_3d(elev=40) + theme_lidar() + guides(colour="none"))
148
+ ```
149
+
150
+ <p>
151
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/galaxy.gif" width="32%" alt="A 20,000-point spiral galaxy, orbiting">
152
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/peaks.png" width="32%" alt="The peaks surface coloured by height">
153
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/isosurface.png" width="32%" alt="Nested density isosurfaces of three clusters">
154
+ </p>
155
+
156
+ Surfaces, isosurfaces, and self-driving scenes use the same grammar:
157
+
158
+ ```python notest
159
+ ggplot(grid, aes(x="x", y="y", z="height", fill="height")) + geom_surface()
160
+ ggplot(points, aes(x="x", y="y", z="z")) + geom_isosurface(levels=[0.2, 0.5, 0.8])
161
+
162
+ (ggplot(sweep, aes(x="x", y="y", z="z")) # a nuScenes / KITTI lidar sweep
163
+ + geom_point3d() # coloured by height, as lidar viewers do
164
+ + geom_box3d(aes(length="l", width="w", height="h", angle="yaw", colour="class"), data=boxes)
165
+ + coord_3d(azim=180, elev=28, zoom=1.6) # chase camera behind the car
166
+ + theme_lidar())
167
+ ```
168
+
169
+ `read_bin("scan.pcd.bin")` loads nuScenes-style point clouds, and NumPy
170
+ arrays work by column position: `aes(x=0, y=1, z=2, colour=3)`. Camera,
171
+ aspect, and point-budget options are in the
172
+ [reference](docs/reference.md#api-reference) under `coord_3d`.
173
+
174
+ ## One figure, two outputs
175
+
176
+ The same object is an interactive figure in the notebook and a publication
177
+ figure on disk, with real fonts, correct DPI metadata, and `theme_bw` by
178
+ default.
179
+
180
+ ```python notest
181
+ ggsave("fig1.pdf", p, width=3.5, height=2.6, units="in") # vector, one journal column
182
+ ggsave("fig1.png", p, width=7, height=4.5, units="in", dpi=300) # two columns
183
+ ggsave("fig1.html", p) # interactive page to share
184
+ ```
185
+
186
+ ## It's ggplot2
187
+
188
+ If you know ggplot2, you already know plot3: `+` layers, `aes()`, geoms,
189
+ stats, scales, facets, themes, `labs()`, and patchwork's `|` and `/`, with
190
+ ggplot2's names and defaults.
191
+
192
+ ```python
193
+ rng = np.random.default_rng(1)
194
+ trial = pd.DataFrame({"arm": rng.choice(["placebo", "low", "high"], 150),
195
+ "dose": rng.uniform(0, 10, 150)})
196
+ trial["response"] = 2 + 0.6 * trial.dose + 1.5 * (trial.arm == "high") + rng.normal(0, 1.2, 150)
197
+
198
+ a = (ggplot(trial, aes(x="dose", y="response", colour="arm"))
199
+ + geom_point(alpha=0.7) + geom_smooth(method="lm"))
200
+ b = (ggplot(trial, aes(x="arm", y="response", fill="arm"))
201
+ + geom_violin() + geom_jitter(width=0.1, height=0) + theme(legend_position="none"))
202
+ fig = (a | b) + plot_annotation(title="Response by dose and arm", tag_levels="A")
203
+ ```
204
+
205
+ <img src="https://raw.githubusercontent.com/rleyvasal/plot3/main/docs/img/ggplot2_compose.png" alt="Two-panel figure: scatter with linear fits, and violins by arm">
206
+
207
+ Everything else you'd expect is there, including histograms, densities,
208
+ boxplots, bar positions, `stat_summary`, error bars, heatmaps, hexbins, 2D
209
+ densities, `facet_wrap` / `facet_grid`, Brewer, viridis, and Okabe–Ito
210
+ scales, date axes, `theme()` with `element_*()`, and `coord_flip` /
211
+ `coord_polar`. See the **[full reference](docs/reference.md)** for what
212
+ plot3 supports and how it differs from ggplot2, or use the
213
+ [ggplot2 docs](https://ggplot2.tidyverse.org/reference/) directly; the
214
+ names match.
215
+
216
+ In notebooks, bare column names work as in R: `aes(x=dose, y=response)`.
217
+
218
+ ## Notebooks, SolveIt, and CRAFT
219
+
220
+ - **Jupyter / SolveIt**: bare column names and backticks work in `aes()`,
221
+ `facet_wrap()`, and `facet_grid()` (`aes(x=`First Name`)`). Toggle with
222
+ `enable_r_style()` / `disable_r_style()`. `.py` files keep quoted strings.
223
+ - **SolveIt** draws figures inline and hides their HTML from the model's
224
+ context (`autohide(False)` to opt out).
225
+ - **VS Code notebooks** block WebGL in output cells, so plot3 opens figures
226
+ in your browser (`PLOT3_DISPLAY=browser|iframe` to force a mode). The page
227
+ is written to `.plot3_preview/latest.html` in the notebook's folder, or to
228
+ the system temp folder when that folder is read-only.
229
+ - **CRAFT / `%gpu`**: the same `ggplot(...)` code runs on the remote kernel;
230
+ stats run where the data lives and only a compact payload comes back.
231
+
232
+ ```text
233
+ %run /path/to/plot3/plot3.py # loads plot3 locally and seeds the GPU kernel
234
+ %plot3 df x=wt y=mpg color=cyl # optional shortcut magic
235
+ ```
236
+
237
+ ## Install
238
+
239
+ ```bash
240
+ pip install "plot3[jupyter,export]"
241
+ ```
242
+
243
+ Python 3.10 or newer; NumPy and pandas are the only required packages.
244
+
245
+ | Extra | Adds |
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 |
251
+
252
+ The latest unreleased code installs from GitHub with
253
+ `pip install "plot3[jupyter,export] @ git+https://github.com/rleyvasal/plot3"`.
254
+
255
+ ## Development
256
+
257
+ ```bash
258
+ git clone https://github.com/rleyvasal/plot3 && cd plot3
259
+ python3 -m venv .venv && source .venv/bin/activate
260
+ pip install -e ".[dev,jupyter,export]"
261
+ pytest -q # includes every example in this README and docs/reference.md
262
+ python examples/showcase_2d.py # 2D gallery in the browser
263
+ python examples/showcase_3d.py # 3D gallery
264
+ ```
265
+
266
+ The code is in `plot3/`: `geoms.py` (grammar objects), `scaling.py`
267
+ (scale functions), `build.py` (stats to a figure spec), `stat2d.py` and
268
+ `flip.py` (statistical layers), `static.py` (PNG/SVG/PDF), `viewer.py` (the
269
+ WebGL viewer), `expr.py` / `function.py` / `calculus.py` (formulas),
270
+ `compose.py` (multi-panel figures). See [CHANGELOG.md](CHANGELOG.md) for
271
+ changes between versions.
272
+
273
+ ## License
274
+
275
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1 @@
1
+ __version__ = "0.5.1"
@@ -440,6 +440,22 @@ def _apply_fill(mapping: dict, kind: str) -> dict:
440
440
  return out
441
441
 
442
442
 
443
+ def _layer_mapping(base_mapping, geom) -> dict:
444
+ """The plot's mapping with the layer's own on top, fill resolved per side.
445
+
446
+ A set colour (``geom_point(colour="black")`` under ``aes(colour=g)``)
447
+ replaces the inherited colour mapping for that layer, as in ggplot2, so
448
+ the layer neither recolours nor needs the column. Filled layers keep
449
+ it: there ``colour=`` is the outline.
450
+ """
451
+ kind = getattr(geom, "kind", None)
452
+ mapping = _apply_fill(dict(base_mapping), kind)
453
+ if getattr(geom, "const_color", None) is not None and kind not in _FILL_KINDS:
454
+ mapping.pop("color", None)
455
+ mapping.update(_apply_fill(dict(geom.mapping), kind))
456
+ return mapping
457
+
458
+
443
459
  _REF_KINDS = frozenset({"hline", "vline", "abline"})
444
460
  _MM_TO_PX = 96.0 / 25.4
445
461
 
@@ -571,8 +587,7 @@ def expand_stat_geom(
571
587
  layers = expand_vector_field(geom, transition, slider)
572
588
  return layers[0] if len(layers) == 1 else layers
573
589
  # Resolve fill per mapping, so a layer's own colour beats a base fill.
574
- mapping = _apply_fill(dict(base_mapping), geom.kind)
575
- mapping.update(_apply_fill(dict(geom.mapping), geom.kind))
590
+ mapping = _layer_mapping(base_mapping, geom)
576
591
  if geom.kind == "box3d":
577
592
  from plot3.stats3d import box3d_layers
578
593
 
@@ -976,8 +991,8 @@ def expand_stat_geom(
976
991
  indices = np.zeros((0, 3), dtype=np.int32)
977
992
  colour_by = getattr(geom, "colour_by", "level")
978
993
  map_kwargs: dict = {"x": "x", "y": "y", "z": "z"}
979
- if colour_by == "level" and "colour" in vertices.columns:
980
- map_kwargs["colour"] = "colour"
994
+ if colour_by == "level" and "level" in vertices.columns:
995
+ map_kwargs["colour"] = "level"
981
996
  out = _Geom(
982
997
  aes(**map_kwargs),
983
998
  color=geom.const_color,
@@ -990,6 +1005,7 @@ def expand_stat_geom(
990
1005
  out.wireframe = bool(getattr(geom, "wireframe", False))
991
1006
  out._indices = indices
992
1007
  out._iso_levels = used
1008
+ out._colour_title = "level"
993
1009
  return out
994
1010
  return geom
995
1011
 
@@ -1963,8 +1979,7 @@ def build_spec(g: ggplot) -> tuple[dict, list[tuple[str, str]]]:
1963
1979
  if getattr(geom, "_replace_mapping", False):
1964
1980
  m = dict(geom.mapping)
1965
1981
  else:
1966
- m = _apply_fill(dict(g.mapping), geom.kind)
1967
- m.update(_apply_fill(dict(geom.mapping), geom.kind))
1982
+ m = _layer_mapping(g.mapping, geom)
1968
1983
  if "x" not in m or "y" not in m:
1969
1984
  raise ValueError("aes(x=, y=) are required (bar/histogram/density supply y)")
1970
1985
  if geom.kind == "surface" and "z" not in m: