plot3 0.4.2__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plot3-0.5.0/PKG-INFO +308 -0
- plot3-0.5.0/README.md +275 -0
- plot3-0.5.0/plot3/__version__.py +1 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/build.py +3 -2
- {plot3-0.4.2 → plot3-0.5.0}/plot3/calculus.py +102 -24
- {plot3-0.4.2 → plot3-0.5.0}/plot3/contour.py +138 -26
- {plot3-0.4.2 → plot3-0.5.0}/plot3/expr.py +8 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/geoms.py +8 -2
- {plot3-0.4.2 → plot3-0.5.0}/plot3/ggplot.py +7 -6
- {plot3-0.4.2 → plot3-0.5.0}/plot3/static.py +88 -9
- {plot3-0.4.2 → plot3-0.5.0}/plot3/stats3d.py +104 -137
- plot3-0.5.0/plot3.egg-info/PKG-INFO +308 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_3d.py +29 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_contour.py +30 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_geom_function.py +16 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_math_layers.py +18 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_readme.py +14 -10
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_scales_api.py +18 -0
- {plot3-0.4.2 → plot3-0.5.0}/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.0}/LICENSE +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/__init__.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/aesexpr.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/compose.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/craft.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/encode.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/flip.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/function.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/io.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/jupyter.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/latexin.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/masking.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/mathtext.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/payload.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/remote.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/scales.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/scaling.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/special.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/stat2d.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/table.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/themes.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3/viewer.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3.egg-info/SOURCES.txt +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3.egg-info/dependency_links.txt +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3.egg-info/requires.txt +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/plot3.egg-info/top_level.txt +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/pyproject.toml +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/setup.cfg +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_2d_density.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_animation.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_annotate.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_api_public.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_array_backend.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_arrow.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_build_contract.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_everyday_ggplot2.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_expr.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_facets_compose.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_geoms_ds.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_ggplot2_extras2.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_ggplot2_parity.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_ggplot_extras.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_grammar.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_latex_input.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_lidar_scene.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_masking.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_mathtext.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_payload.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_payload_display.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_point_cloud.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_r_oracle_parity.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_remote_bridge.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_review_fixes.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_save_menu.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_scales_themes.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_special.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_stat2d.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_stats_semantic.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_step3_geoms.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_table_backends.py +0 -0
- {plot3-0.4.2 → plot3-0.5.0}/tests/test_titles_theme.py +0 -0
plot3-0.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plot3
|
|
3
|
+
Version: 0.5.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 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.0/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.0"
|
|
@@ -976,8 +976,8 @@ def expand_stat_geom(
|
|
|
976
976
|
indices = np.zeros((0, 3), dtype=np.int32)
|
|
977
977
|
colour_by = getattr(geom, "colour_by", "level")
|
|
978
978
|
map_kwargs: dict = {"x": "x", "y": "y", "z": "z"}
|
|
979
|
-
if colour_by == "level" and "
|
|
980
|
-
map_kwargs["colour"] = "
|
|
979
|
+
if colour_by == "level" and "level" in vertices.columns:
|
|
980
|
+
map_kwargs["colour"] = "level"
|
|
981
981
|
out = _Geom(
|
|
982
982
|
aes(**map_kwargs),
|
|
983
983
|
color=geom.const_color,
|
|
@@ -990,6 +990,7 @@ def expand_stat_geom(
|
|
|
990
990
|
out.wireframe = bool(getattr(geom, "wireframe", False))
|
|
991
991
|
out._indices = indices
|
|
992
992
|
out._iso_levels = used
|
|
993
|
+
out._colour_title = "level"
|
|
993
994
|
return out
|
|
994
995
|
return geom
|
|
995
996
|
|