anytrellis 0.1.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.
- anytrellis-0.1.0/.gitignore +26 -0
- anytrellis-0.1.0/CHANGELOG.md +39 -0
- anytrellis-0.1.0/LICENSE +21 -0
- anytrellis-0.1.0/PKG-INFO +308 -0
- anytrellis-0.1.0/README.md +273 -0
- anytrellis-0.1.0/examples/jupyter/01_getting_started.ipynb +242 -0
- anytrellis-0.1.0/examples/jupyter/02_lattice_and_cleveland.ipynb +267 -0
- anytrellis-0.1.0/examples/jupyter/03_ggplot2_style.ipynb +442 -0
- anytrellis-0.1.0/examples/jupyter/04_seaborn_style.ipynb +263 -0
- anytrellis-0.1.0/examples/jupyter/05_vega_lite_style.ipynb +204 -0
- anytrellis-0.1.0/examples/jupyter/06_large_data.ipynb +129 -0
- anytrellis-0.1.0/examples/jupyter/07_linked_plots.ipynb +176 -0
- anytrellis-0.1.0/examples/marimo/01_getting_started.py +202 -0
- anytrellis-0.1.0/examples/marimo/02_lattice_and_cleveland.py +231 -0
- anytrellis-0.1.0/examples/marimo/03_ggplot2_style.py +390 -0
- anytrellis-0.1.0/examples/marimo/04_seaborn_style.py +231 -0
- anytrellis-0.1.0/examples/marimo/05_vega_lite_style.py +180 -0
- anytrellis-0.1.0/examples/marimo/06_large_data.py +113 -0
- anytrellis-0.1.0/examples/marimo/07_linked_plots.py +148 -0
- anytrellis-0.1.0/js/package-lock.json +2083 -0
- anytrellis-0.1.0/js/package.json +23 -0
- anytrellis-0.1.0/js/src/core/drawing.ts +366 -0
- anytrellis-0.1.0/js/src/core/layout.ts +275 -0
- anytrellis-0.1.0/js/src/core/renderers/canvas2d.ts +100 -0
- anytrellis-0.1.0/js/src/core/renderers/renderer.ts +78 -0
- anytrellis-0.1.0/js/src/core/renderers/select.ts +39 -0
- anytrellis-0.1.0/js/src/core/renderers/shaders.ts +167 -0
- anytrellis-0.1.0/js/src/core/renderers/webgl2.ts +176 -0
- anytrellis-0.1.0/js/src/core/renderers/webgpu.ts +234 -0
- anytrellis-0.1.0/js/src/core/scene.ts +295 -0
- anytrellis-0.1.0/js/src/core/selection.ts +214 -0
- anytrellis-0.1.0/js/src/core/ticks.ts +216 -0
- anytrellis-0.1.0/js/src/core/transport.ts +153 -0
- anytrellis-0.1.0/js/src/core/types.ts +173 -0
- anytrellis-0.1.0/js/src/core/view.ts +1347 -0
- anytrellis-0.1.0/js/src/standalone.ts +56 -0
- anytrellis-0.1.0/js/src/widget.css +375 -0
- anytrellis-0.1.0/js/src/widget.ts +174 -0
- anytrellis-0.1.0/js/test/core.test.ts +404 -0
- anytrellis-0.1.0/js/test/interact.mjs +130 -0
- anytrellis-0.1.0/js/test/legend.mjs +120 -0
- anytrellis-0.1.0/js/test/linked.mjs +127 -0
- anytrellis-0.1.0/js/test/live-open.mjs +81 -0
- anytrellis-0.1.0/js/test/live-select.mjs +76 -0
- anytrellis-0.1.0/js/test/live-zoom.mjs +66 -0
- anytrellis-0.1.0/js/test/notebooks.mjs +46 -0
- anytrellis-0.1.0/js/test/pages.py +36 -0
- anytrellis-0.1.0/js/test/select.mjs +114 -0
- anytrellis-0.1.0/js/test/visual.mjs +52 -0
- anytrellis-0.1.0/js/test/zoom.mjs +180 -0
- anytrellis-0.1.0/js/tsconfig.json +17 -0
- anytrellis-0.1.0/js/vitest.config.ts +4 -0
- anytrellis-0.1.0/pyproject.toml +125 -0
- anytrellis-0.1.0/src/anytrellis/__init__.py +13 -0
- anytrellis-0.1.0/src/anytrellis/codec.py +71 -0
- anytrellis-0.1.0/src/anytrellis/data.py +251 -0
- anytrellis-0.1.0/src/anytrellis/datasets/SOURCES.md +17 -0
- anytrellis-0.1.0/src/anytrellis/datasets/__init__.py +55 -0
- anytrellis-0.1.0/src/anytrellis/datasets/anscombe.csv +45 -0
- anytrellis-0.1.0/src/anytrellis/datasets/barley.csv +121 -0
- anytrellis-0.1.0/src/anytrellis/datasets/cars.csv +407 -0
- anytrellis-0.1.0/src/anytrellis/datasets/gapminder.csv +694 -0
- anytrellis-0.1.0/src/anytrellis/datasets/penguins.csv +345 -0
- anytrellis-0.1.0/src/anytrellis/datasets/seattle_weather.csv +1462 -0
- anytrellis-0.1.0/src/anytrellis/facets.py +284 -0
- anytrellis-0.1.0/src/anytrellis/layers.py +345 -0
- anytrellis-0.1.0/src/anytrellis/linking.py +210 -0
- anytrellis-0.1.0/src/anytrellis/plot.py +1629 -0
- anytrellis-0.1.0/src/anytrellis/py.typed +0 -0
- anytrellis-0.1.0/src/anytrellis/scales.py +115 -0
- anytrellis-0.1.0/src/anytrellis/selection.py +121 -0
- anytrellis-0.1.0/src/anytrellis/static/standalone.js +159 -0
- anytrellis-0.1.0/src/anytrellis/static/widget.css +1 -0
- anytrellis-0.1.0/src/anytrellis/static/widget.js +159 -0
- anytrellis-0.1.0/src/anytrellis/stats.py +733 -0
- anytrellis-0.1.0/src/anytrellis/styling.py +107 -0
- anytrellis-0.1.0/src/anytrellis/synthetic.py +77 -0
- anytrellis-0.1.0/src/anytrellis/widget.py +113 -0
- anytrellis-0.1.0/tests/test_examples.py +48 -0
- anytrellis-0.1.0/tests/test_linking.py +191 -0
- anytrellis-0.1.0/tests/test_trellis.py +915 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
.venv/
|
|
4
|
+
.venv-*/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.ruff_cache/
|
|
10
|
+
node_modules/
|
|
11
|
+
.DS_Store
|
|
12
|
+
.ipynb_checkpoints/
|
|
13
|
+
__marimo__/
|
|
14
|
+
|
|
15
|
+
# built JavaScript bundle (npm run build in js/)
|
|
16
|
+
src/anytrellis/static/
|
|
17
|
+
|
|
18
|
+
# visual checks (Playwright)
|
|
19
|
+
js/test-results/
|
|
20
|
+
js/playwright-report/
|
|
21
|
+
|
|
22
|
+
# personal experiments; not part of the project
|
|
23
|
+
_scratch/
|
|
24
|
+
|
|
25
|
+
# pages the example notebooks write when run (save_html)
|
|
26
|
+
examples/**/*.html
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to anytrellis. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow
|
|
5
|
+
[Semantic Versioning](https://semver.org/) (before 1.0, minor versions may change the API).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-11-07
|
|
10
|
+
|
|
11
|
+
The first release.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `Trellis`: one panel per level of the facet columns: a wrap (`wrap=`, `cols=`) or a
|
|
16
|
+
grid (`row=`, `col=`) that keeps empty cells, with shared, per-row, per-column or free
|
|
17
|
+
scales (`share_x=`, `share_y=`), linear or log (`x_scale=`, `y_scale=`).
|
|
18
|
+
- Marks: `line`, `points`, `bars` (stacked or dodged), `area`, `band`, `rect`, `text`,
|
|
19
|
+
`rule`, and summaries computed per panel in Python: `hist`, `box`, `density` and `smooth`
|
|
20
|
+
(loess or linear, with a confidence band).
|
|
21
|
+
- Data from Polars, pandas, PyArrow or DuckDB (through narwhals), or a dict of arrays;
|
|
22
|
+
numbers, times and categories, with category colours and legends.
|
|
23
|
+
- Drawing with WebGPU, WebGL2 or Canvas 2D, whichever the browser has; the data goes to the
|
|
24
|
+
browser once, as binary buffers, so millions of marks stay interactive.
|
|
25
|
+
- Zooming and panning (scroll, drag, one axis, box zoom), panels that share a scale
|
|
26
|
+
zooming together, `fit_y` to fit y to the data in view, undo and reset; one panel at a
|
|
27
|
+
time (`open`, `close`) and full screen.
|
|
28
|
+
- Selecting: a brush (`s`) in every panel or, with `link=`, in one panel and followed by
|
|
29
|
+
key; `select()`, `selection` and `selected()` from Python. Clicking legend entries
|
|
30
|
+
selects their colours.
|
|
31
|
+
- Linked plots: `link(a, b, ..., on=)` shares one selection between plots, histograms
|
|
32
|
+
filling to their selected share (crossfiltering).
|
|
33
|
+
- Tooltips with Python's values; `save_html` writes a page that opens without Python.
|
|
34
|
+
- Works in JupyterLab and marimo, and other anywidget hosts (Jupyter Notebook, VS Code).
|
|
35
|
+
- `anytrellis.datasets.load`: six small example datasets, with their sources and licences
|
|
36
|
+
in `datasets/SOURCES.md`; example notebooks for Jupyter and marimo.
|
|
37
|
+
|
|
38
|
+
[Unreleased]: https://github.com/mluttikh/anytrellis/compare/v0.1.0...HEAD
|
|
39
|
+
[0.1.0]: https://github.com/mluttikh/anytrellis/releases/tag/v0.1.0
|
anytrellis-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mluttikh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: anytrellis
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: GPU-accelerated trellis, facet and small-multiple plots for notebooks
|
|
5
|
+
Project-URL: Homepage, https://github.com/mluttikh/anytrellis
|
|
6
|
+
Project-URL: Repository, https://github.com/mluttikh/anytrellis
|
|
7
|
+
Project-URL: Issues, https://github.com/mluttikh/anytrellis/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/mluttikh/anytrellis/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: mluttikh <mluttikhof@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
License-File: src/anytrellis/datasets/SOURCES.md
|
|
13
|
+
Keywords: anywidget,facet,jupyter,lattice,marimo,small multiples,trellis,visualization,webgpu
|
|
14
|
+
Classifier: Development Status :: 3 - Alpha
|
|
15
|
+
Classifier: Framework :: Jupyter
|
|
16
|
+
Classifier: Framework :: Jupyter :: JupyterLab
|
|
17
|
+
Classifier: Intended Audience :: Developers
|
|
18
|
+
Classifier: Intended Audience :: Science/Research
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
26
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.12
|
|
29
|
+
Requires-Dist: anywidget>=0.9.13
|
|
30
|
+
Requires-Dist: narwhals>=1.30
|
|
31
|
+
Requires-Dist: numpy>=1.24
|
|
32
|
+
Provides-Extra: examples
|
|
33
|
+
Requires-Dist: polars>=1.30; extra == 'examples'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# anytrellis
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/anytrellis/)
|
|
39
|
+
[](https://github.com/mluttikh/anytrellis/actions/workflows/ci.yml)
|
|
40
|
+
|
|
41
|
+
Trellis, facet and small-multiple plots for Jupyter and marimo notebooks, drawn on the GPU
|
|
42
|
+
(WebGPU, WebGL2, or Canvas 2D on machines without a usable GPU).
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install anytrellis
|
|
46
|
+
pip install "anytrellis[examples]" # with Polars, for the example notebooks and datasets
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Status: early development (0.1). One panel per level of the facet columns, every panel
|
|
50
|
+
drawn with the same layers (lines, points, bars, areas, rects, text, histograms, box plots,
|
|
51
|
+
densities, reference rules), on shared scales unless
|
|
52
|
+
told otherwise. Every panel goes into one canvas, so a grid of hundreds of panels and
|
|
53
|
+
millions of marks stays interactive.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from anytrellis import Trellis
|
|
57
|
+
from anytrellis.synthetic import sensors
|
|
58
|
+
|
|
59
|
+
df = sensors() # a dict of columns; any Polars, pandas or PyArrow table works too
|
|
60
|
+
t = Trellis(df, wrap="station")
|
|
61
|
+
t.line(x="time", y="temp", color="sensor")
|
|
62
|
+
t.rule(y=0)
|
|
63
|
+
t # click it, then scroll to zoom; panels that share a scale zoom together
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
A grid crosses two facet columns, and keeps empty cells so rows and columns stay aligned:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from anytrellis.synthetic import sales
|
|
70
|
+
|
|
71
|
+
Trellis(sales(), row="store", col="product").bars(x="month", y="units")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Marks
|
|
75
|
+
|
|
76
|
+
Each mark is a method that adds a layer; layers draw in the order they were added.
|
|
77
|
+
|
|
78
|
+
| method | draws |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `line(x, y, color, group)` | a line per group (and colour) |
|
|
81
|
+
| `points(x, y, color, size)` | a point per row |
|
|
82
|
+
| `bars(x, y, color, position="stack"\|"dodge"\|"identity")` | a bar per row, from zero |
|
|
83
|
+
| `area(x, y, color, group, position="stack"\|"identity")` | the area under a line, stacked like bars |
|
|
84
|
+
| `band(x, ymin, ymax, color, group)` | a band between two lines (a range, an interval) |
|
|
85
|
+
| `rect(x, y, color, width=1, height=1)` / `rect(x, y, x2=, y2=)` | a cell per row (a heatmap), or a box between two corners |
|
|
86
|
+
| `text(x, y, text, color, size, align, dx, dy)` | a label per row |
|
|
87
|
+
| `rule(x=..., y=...)` | reference lines across every panel |
|
|
88
|
+
| `hist(x, color, bins=30 \| binwidth=, stat="count"\|"percent"\|"density")` | a histogram per panel, stacked by colour |
|
|
89
|
+
| `box(x, y, color, whis=1.5)` | box plots per position (and colour, side by side); sideways when y is the category |
|
|
90
|
+
| `density(x, color, fill=False, bw_adjust=1)` | a kernel density curve per panel and colour |
|
|
91
|
+
| `smooth(x, y, color, method="loess"\|"linear", se=True)` | a fitted line per panel and colour, with a confidence band |
|
|
92
|
+
|
|
93
|
+
A number column as `color` gets a continuous scale (and a gradient legend); text gets a
|
|
94
|
+
palette. On a category axis a rect is one band wide by default. A text label's `dx`/`dy`
|
|
95
|
+
are pixels, so labels keep their distance while zooming, and a label that would run past
|
|
96
|
+
the panel's edge goes to the other side of its point. `tooltip=[...]` adds columns to
|
|
97
|
+
what hovering shows (a box: to its outliers'); every mark but `text` and `rule` can be
|
|
98
|
+
selected.
|
|
99
|
+
|
|
100
|
+
`hist`, `box`, `density` and `smooth` summarise each panel's rows in Python, and are drawn
|
|
101
|
+
as bars, boxes, lines, bands and points. A histogram's bins are the same in every panel (round widths
|
|
102
|
+
by default), and so are a density's x values, so the panels compare. `stat="percent"`
|
|
103
|
+
and `"density"` scale each panel's counts. With `facet="exclude"` a summary covers the
|
|
104
|
+
other panels' rows, in grey: each panel against the rest. Hovering a box shows its
|
|
105
|
+
quartiles, its count and its whiskers' reach; a smooth line, its confidence interval.
|
|
106
|
+
`smooth` fits loess (local lines with tricube weights over a `span` share of the points)
|
|
107
|
+
or least squares, and its band is the `level` (95%) interval for the mean. Brushing selects whole bins and boxes, and
|
|
108
|
+
`t.selected("hist")` gives the rows they stand for.
|
|
109
|
+
|
|
110
|
+
## Facets and scales
|
|
111
|
+
|
|
112
|
+
- `Trellis(df, row=..., col=...)`: a grid; `Trellis(df, wrap=..., cols=4)`: a wrap. Each
|
|
113
|
+
takes one column or several (their combinations are the levels).
|
|
114
|
+
- `share_x=` / `share_y=` (or `t.share(x=..., y=...)`): `True` (all panels, the default),
|
|
115
|
+
`"row"`, `"col"` or `False` (each panel its own). Axes are drawn only where a panel's
|
|
116
|
+
scale differs from its neighbour's.
|
|
117
|
+
- `x_scale="log"` / `y_scale="log"` (or `t.scale(y="log")`): a log10 scale, with ticks at
|
|
118
|
+
decades (and 2 and 5 between when they fit). Values of 0 or less are left out; marks
|
|
119
|
+
drawn from zero (bars, areas, histograms, densities) don't go on a log y. Summaries are
|
|
120
|
+
computed on the log scale, as in ggplot2: a histogram's bins are equally wide on it.
|
|
121
|
+
- `t.order("site", by="median", of="yield")`: panels ordered by a summary of each level.
|
|
122
|
+
- `t.order("month", ["Jan", "Feb", ...])`: any category column in the given order: panels,
|
|
123
|
+
bands on an axis and colours. Text is sorted otherwise; a pandas Categorical or a
|
|
124
|
+
Polars Enum keeps its declared order.
|
|
125
|
+
- `line(..., group=["station", "sensor"])`: one line per combination of the columns.
|
|
126
|
+
- A layer whose data lacks the facet columns is drawn in every panel; `facet="exclude"`
|
|
127
|
+
draws, in each panel, the other panels' data (grey context behind it).
|
|
128
|
+
|
|
129
|
+
A wrap whose last row is not full shows its legend in the empty cells there when it fits,
|
|
130
|
+
and under the plot otherwise (also while one panel is open); `Trellis(..., legend="bottom")`
|
|
131
|
+
keeps it under the plot.
|
|
132
|
+
|
|
133
|
+
## Interaction
|
|
134
|
+
|
|
135
|
+
Click the plot, then: scroll to zoom x (Shift + scroll: y), drag to pan, `r` to reset.
|
|
136
|
+
Zoom and pan apply to every panel that shares the scale. Hovering shows the nearest mark's
|
|
137
|
+
values and a crosshair across the panels that share x.
|
|
138
|
+
|
|
139
|
+
**One axis.** Over an axis, scrolling zooms that axis alone and dragging pans it;
|
|
140
|
+
double-clicking it resets it. An axis stands for its whole scale: with `share_y="row"` the
|
|
141
|
+
left axis of a row zooms that row.
|
|
142
|
+
|
|
143
|
+
**Box zoom.** Shift+drag draws a box (or switch plain drag to it: `z`, or the toolbar). The
|
|
144
|
+
box follows the drag: mostly sideways zooms x alone, mostly up or down y alone, otherwise
|
|
145
|
+
both. While you draw it, the panels it will also zoom show where.
|
|
146
|
+
|
|
147
|
+
**Fit y.** `y` (or the toolbar) fits every y scale to the data in view, and keeps fitting
|
|
148
|
+
it as you zoom or pan x; a shared y fits all its panels together, so they stay comparable.
|
|
149
|
+
Zooming y by hand stops it. `Trellis(..., fit_y=True)` starts with it on.
|
|
150
|
+
|
|
151
|
+
`u` (or Cmd/Ctrl+Z) undoes a zoom, one step at a time.
|
|
152
|
+
|
|
153
|
+
**Selecting.** `s` (or the toolbar) makes drag a brush, shaped like a zoom box: x, y or
|
|
154
|
+
both. It selects the marks it meets, in every panel, and the rest fade: the same range
|
|
155
|
+
compared across panels. Drag the brush to move it; a click or Esc clears it.
|
|
156
|
+
|
|
157
|
+
Click a colour's entry in the legend to select its marks (Shift, Cmd or Ctrl+click adds
|
|
158
|
+
or removes more; a click on the only one chosen clears). With a brush too, a mark must
|
|
159
|
+
meet both: brush a range, then pick a colour in it. Marks coloured by another column, or by
|
|
160
|
+
none, fade.
|
|
161
|
+
|
|
162
|
+
With `Trellis(..., link="country")` the brush selects in the panel you draw it in, and
|
|
163
|
+
then every row sharing its link values, in all panels and layers that have the column:
|
|
164
|
+
brush countries in 1955 and follow them through the years. From Python:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
t.select(x=(2, 4), y=(60, 80)) # a time axis: dates; a category axis: categories
|
|
168
|
+
t.select(x=(0, 3), panel="1955") # in one panel (with link: then followed)
|
|
169
|
+
t.select(color=["Asia", "Africa"]) # the legend's entries: these colours' marks
|
|
170
|
+
t.selection # {"x": (2, 4), "y": (60, 80), "color": None, "panel": None}
|
|
171
|
+
t.selected() # the selected rows, as the table you gave
|
|
172
|
+
t.select() # clear
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
A brush drawn in the browser reaches Python when you let go, so `t.selected()` in the next
|
|
176
|
+
cell gives its rows. A point is selected inside the brush, a line or area segment with
|
|
177
|
+
either end inside, a bar or rect overlapping it, a box when its body or lines are met;
|
|
178
|
+
grey context is never selected.
|
|
179
|
+
|
|
180
|
+
**Linked plots.** `link(a, b, ...)` gives plots one selection (crossfiltering): brush in
|
|
181
|
+
one and the others show the same rows, following the brush as it moves. Points and lines of
|
|
182
|
+
those rows stay bright and the rest fade, a histogram's bars fill to the share of their
|
|
183
|
+
count that is selected, and a box lights up when any of its rows is. A brush in another plot
|
|
184
|
+
of the link replaces the first; a click or Esc in any of them clears it.
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from anytrellis import link
|
|
188
|
+
|
|
189
|
+
scatter = Trellis(cars).points(x="weight", y="mpg")
|
|
190
|
+
power = Trellis(cars).hist(x="horsepower")
|
|
191
|
+
link(scatter, power) # the same table: the same rows
|
|
192
|
+
link(by_year, lives, on="country") # any tables: the rows sharing a country
|
|
193
|
+
power.selected() # the rows the link's selection picks
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Without `on=`, the plots must draw the same table (the same object): a filtered copy is
|
|
197
|
+
another table, so link it `on=` a key column. Python relays the selection, so the plots are
|
|
198
|
+
linked while the kernel runs, not in a page from `save_html`. `unlink()` the returned link
|
|
199
|
+
to set them apart.
|
|
200
|
+
|
|
201
|
+
**One panel at a time.** Double-click a panel (or press Enter over it) to see it alone on
|
|
202
|
+
the whole plot; Page Up and Page Down (or ‹ ›) step to the previous and next panel at the
|
|
203
|
+
same zoom, and Backspace, Esc, a double-click or *All panels* goes back. The zoom stays
|
|
204
|
+
linked: back in the grid, the panels that share its scales show what the opened one
|
|
205
|
+
showed. From Python: `t.open("station 3")` (a grid's panel: `t.open("north", "s1")`),
|
|
206
|
+
`t.close()`, `t.focused`, and `t.panels()` lists the names.
|
|
207
|
+
|
|
208
|
+
**Full screen.** The toolbar's button or `f` shows the plot on the whole screen (`f` or Esc
|
|
209
|
+
leaves). Browsers allow it only from a click or a key, so not from Python; where a page
|
|
210
|
+
forbids it (some notebook frames, iPhone Safari) the button is not shown.
|
|
211
|
+
|
|
212
|
+
| key | |
|
|
213
|
+
|---|---|
|
|
214
|
+
| `f` | full screen, and back |
|
|
215
|
+
| `r`, `0` | reset the zoom |
|
|
216
|
+
| `u`, Cmd/Ctrl+Z | undo the last zoom |
|
|
217
|
+
| `z` | drag draws a zoom box, and back (Shift+drag: the other) |
|
|
218
|
+
| `y` | fit y to the data in view, and stop |
|
|
219
|
+
| `s` | drag draws a selection brush, and back |
|
|
220
|
+
| Esc | clear the selection (else: back to all panels) |
|
|
221
|
+
| click a legend entry | select its colour (Shift+click: add or remove one) |
|
|
222
|
+
| Enter | open the panel under the pointer |
|
|
223
|
+
| Page Up / Page Down | previous / next panel |
|
|
224
|
+
| Backspace | back to all panels |
|
|
225
|
+
|
|
226
|
+
These keys stay in the plot, so JupyterLab's own shortcuts don't also act on them.
|
|
227
|
+
|
|
228
|
+
## Sharing
|
|
229
|
+
|
|
230
|
+
`t.save_html("plot.html")` writes a page with the data inside, which opens without Python.
|
|
231
|
+
|
|
232
|
+
Licence: MIT.
|
|
233
|
+
|
|
234
|
+
## Examples
|
|
235
|
+
|
|
236
|
+
Notebooks in [`examples/jupyter`](https://github.com/mluttikh/anytrellis/tree/main/examples/jupyter)
|
|
237
|
+
(JupyterLab, VS Code) and the same in
|
|
238
|
+
[`examples/marimo`](https://github.com/mluttikh/anytrellis/tree/main/examples/marimo)
|
|
239
|
+
(`marimo edit examples/marimo/01_getting_started.py`):
|
|
240
|
+
|
|
241
|
+
| notebook | what it shows |
|
|
242
|
+
|---|---|
|
|
243
|
+
| `01_getting_started` | wrap, grid, free scales, layers and context, `rule`, `order`, `save_html` |
|
|
244
|
+
| `02_lattice_and_cleveland` | Cleveland's barley dot plot, Anscombe's quartet, conditioning on two variables, small multiples instead of an animation, following countries with linked brushing |
|
|
245
|
+
| `03_ggplot2_style` | `facet_wrap`, `facet_grid`, gghighlight, `scales = "free_y"`, stacked and dodged bars |
|
|
246
|
+
| `04_seaborn_style` | `relplot` (scatter and line, `col_wrap`), `FacetGrid` histograms, `catplot(kind="bar")` |
|
|
247
|
+
| `05_vega_lite_style` | `row` / `column` / `facet` channels, `resolve_scale`, a trellis of grouped bars |
|
|
248
|
+
| `06_large_data` | a million points, a year of hourly readings, a hundred panels |
|
|
249
|
+
| `07_linked_plots` | crossfiltering a scatter, histograms and boxes; plots linked by a key column |
|
|
250
|
+
|
|
251
|
+
Their data comes with the package: `from anytrellis.datasets import load; load("barley")`
|
|
252
|
+
(anscombe, barley, cars, gapminder, penguins, seattle_weather; from the UCI Machine
|
|
253
|
+
Learning Repository and vega-datasets; their sources and licences are in
|
|
254
|
+
[`SOURCES.md`](https://github.com/mluttikh/anytrellis/blob/main/src/anytrellis/datasets/SOURCES.md);
|
|
255
|
+
the gapminder data is based on free material from [GAPMINDER.ORG](https://www.gapminder.org/),
|
|
256
|
+
CC-BY LICENSE).
|
|
257
|
+
The notebooks are written once in `scripts/build_examples.py`, which
|
|
258
|
+
writes both sets: run `uv run python scripts/build_examples.py` after changing it.
|
|
259
|
+
|
|
260
|
+
## Development
|
|
261
|
+
|
|
262
|
+
```sh
|
|
263
|
+
uv sync # builds js/ into src/anytrellis/static
|
|
264
|
+
uv run pytest # Python: spec, geometry, scales, protocol
|
|
265
|
+
cd js && npm test && npm run typecheck # TypeScript: ticks, layout, picking
|
|
266
|
+
npm run build # after changing js/src
|
|
267
|
+
node test/visual.mjs <dir of save_html pages> # screenshots per backend
|
|
268
|
+
node test/interact.mjs <page.html> # hover, linked zoom, pan, reset, dark
|
|
269
|
+
node test/zoom.mjs <page.html> # one axis, box zoom, fit y, undo
|
|
270
|
+
node test/select.mjs <page.html> [dir] [renderer] # brushing, moving, clearing
|
|
271
|
+
node test/legend.mjs <page.html> [dir] [renderer] # clicking legend entries
|
|
272
|
+
node test/notebooks.mjs jupyter|marimo <url> <out> # a live notebook: run it, screenshot plots
|
|
273
|
+
node test/linked.mjs <url of 07 in JupyterLab> # linked plots: brushing across plots
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Pages force a backend with `?renderer=webgpu|webgl2|canvas2d` in their URL;
|
|
277
|
+
`uv run python js/test/pages.py <dir>` writes the pages the browser checks run on.
|
|
278
|
+
|
|
279
|
+
CI (`.github/workflows/ci.yml`) runs on every push to `main` and every pull request: ruff
|
|
280
|
+
and ty, the TypeScript checks, pytest on Python 3.12 to 3.15 (Linux; macOS and Windows on
|
|
281
|
+
3.15), the select, legend and zoom checks in headless Chromium (WebGL2 and Canvas 2D), and
|
|
282
|
+
builds the sdist and wheel, checked by `scripts/check_dist.py` (contents, version, a clean
|
|
283
|
+
install that draws a plot).
|
|
284
|
+
|
|
285
|
+
## Releasing
|
|
286
|
+
|
|
287
|
+
Releases go to PyPI from GitHub Actions by
|
|
288
|
+
[trusted publishing](https://docs.pypi.org/trusted-publishers/): no API token is stored.
|
|
289
|
+
|
|
290
|
+
Once, before the first release:
|
|
291
|
+
|
|
292
|
+
1. On [pypi.org](https://pypi.org/manage/account/publishing/), add a pending publisher:
|
|
293
|
+
project `anytrellis`, owner `mluttikh`, repository `anytrellis`, workflow
|
|
294
|
+
`release.yml`, environment `pypi`. For dry runs, do the same on
|
|
295
|
+
[test.pypi.org](https://test.pypi.org/manage/account/publishing/) with environment
|
|
296
|
+
`testpypi`.
|
|
297
|
+
2. On GitHub (Settings > Environments), the environments `pypi` and `testpypi`; giving
|
|
298
|
+
`pypi` a required reviewer makes every release wait for an approval.
|
|
299
|
+
|
|
300
|
+
Each release:
|
|
301
|
+
|
|
302
|
+
1. Set the version in `pyproject.toml`, and in `CHANGELOG.md` move *Unreleased* under a
|
|
303
|
+
heading `## [x.y.z] - date` (and update the links at the bottom).
|
|
304
|
+
2. Optionally, a dry run: Actions > Release > Run workflow publishes to TestPyPI
|
|
305
|
+
(`pip install -i https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ anytrellis`).
|
|
306
|
+
3. Commit, tag and push: `git tag vx.y.z && git push origin main vx.y.z`. The release
|
|
307
|
+
workflow runs CI on the tag, checks that the tag matches the version, publishes to PyPI
|
|
308
|
+
and makes a GitHub release with the changelog's section and the files.
|