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.
- plot3-0.5.1/PKG-INFO +308 -0
- plot3-0.5.1/README.md +275 -0
- plot3-0.5.1/plot3/__version__.py +1 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/build.py +21 -6
- {plot3-0.4.2 → plot3-0.5.1}/plot3/calculus.py +102 -24
- {plot3-0.4.2 → plot3-0.5.1}/plot3/contour.py +138 -26
- {plot3-0.4.2 → plot3-0.5.1}/plot3/expr.py +8 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/geoms.py +8 -2
- {plot3-0.4.2 → plot3-0.5.1}/plot3/ggplot.py +7 -6
- {plot3-0.4.2 → plot3-0.5.1}/plot3/static.py +88 -9
- {plot3-0.4.2 → plot3-0.5.1}/plot3/stats3d.py +104 -137
- plot3-0.5.1/plot3.egg-info/PKG-INFO +308 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_3d.py +29 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_contour.py +30 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_geom_function.py +16 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_grammar.py +24 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_math_layers.py +18 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_readme.py +14 -10
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_scales_api.py +18 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_static_save.py +34 -0
- plot3-0.4.2/PKG-INFO +0 -521
- plot3-0.4.2/README.md +0 -488
- plot3-0.4.2/plot3/__version__.py +0 -1
- plot3-0.4.2/plot3.egg-info/PKG-INFO +0 -521
- {plot3-0.4.2 → plot3-0.5.1}/LICENSE +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/__init__.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/aesexpr.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/compose.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/craft.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/encode.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/flip.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/function.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/io.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/jupyter.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/latexin.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/masking.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/mathtext.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/payload.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/remote.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/scales.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/scaling.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/special.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/stat2d.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/table.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/themes.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3/viewer.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3.egg-info/SOURCES.txt +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3.egg-info/dependency_links.txt +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3.egg-info/requires.txt +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/plot3.egg-info/top_level.txt +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/pyproject.toml +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/setup.cfg +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_2d_density.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_animation.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_annotate.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_api_public.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_array_backend.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_arrow.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_build_contract.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_everyday_ggplot2.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_expr.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_facets_compose.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_geoms_ds.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_ggplot2_extras2.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_ggplot2_parity.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_ggplot_extras.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_latex_input.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_lidar_scene.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_masking.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_mathtext.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_payload.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_payload_display.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_point_cloud.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_r_oracle_parity.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_remote_bridge.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_review_fixes.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_save_menu.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_scales_themes.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_special.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_stat2d.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_stats_semantic.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_step3_geoms.py +0 -0
- {plot3-0.4.2 → plot3-0.5.1}/tests/test_table_backends.py +0 -0
- {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 =
|
|
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 "
|
|
980
|
-
map_kwargs["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 =
|
|
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:
|