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.
Files changed (81) hide show
  1. anytrellis-0.1.0/.gitignore +26 -0
  2. anytrellis-0.1.0/CHANGELOG.md +39 -0
  3. anytrellis-0.1.0/LICENSE +21 -0
  4. anytrellis-0.1.0/PKG-INFO +308 -0
  5. anytrellis-0.1.0/README.md +273 -0
  6. anytrellis-0.1.0/examples/jupyter/01_getting_started.ipynb +242 -0
  7. anytrellis-0.1.0/examples/jupyter/02_lattice_and_cleveland.ipynb +267 -0
  8. anytrellis-0.1.0/examples/jupyter/03_ggplot2_style.ipynb +442 -0
  9. anytrellis-0.1.0/examples/jupyter/04_seaborn_style.ipynb +263 -0
  10. anytrellis-0.1.0/examples/jupyter/05_vega_lite_style.ipynb +204 -0
  11. anytrellis-0.1.0/examples/jupyter/06_large_data.ipynb +129 -0
  12. anytrellis-0.1.0/examples/jupyter/07_linked_plots.ipynb +176 -0
  13. anytrellis-0.1.0/examples/marimo/01_getting_started.py +202 -0
  14. anytrellis-0.1.0/examples/marimo/02_lattice_and_cleveland.py +231 -0
  15. anytrellis-0.1.0/examples/marimo/03_ggplot2_style.py +390 -0
  16. anytrellis-0.1.0/examples/marimo/04_seaborn_style.py +231 -0
  17. anytrellis-0.1.0/examples/marimo/05_vega_lite_style.py +180 -0
  18. anytrellis-0.1.0/examples/marimo/06_large_data.py +113 -0
  19. anytrellis-0.1.0/examples/marimo/07_linked_plots.py +148 -0
  20. anytrellis-0.1.0/js/package-lock.json +2083 -0
  21. anytrellis-0.1.0/js/package.json +23 -0
  22. anytrellis-0.1.0/js/src/core/drawing.ts +366 -0
  23. anytrellis-0.1.0/js/src/core/layout.ts +275 -0
  24. anytrellis-0.1.0/js/src/core/renderers/canvas2d.ts +100 -0
  25. anytrellis-0.1.0/js/src/core/renderers/renderer.ts +78 -0
  26. anytrellis-0.1.0/js/src/core/renderers/select.ts +39 -0
  27. anytrellis-0.1.0/js/src/core/renderers/shaders.ts +167 -0
  28. anytrellis-0.1.0/js/src/core/renderers/webgl2.ts +176 -0
  29. anytrellis-0.1.0/js/src/core/renderers/webgpu.ts +234 -0
  30. anytrellis-0.1.0/js/src/core/scene.ts +295 -0
  31. anytrellis-0.1.0/js/src/core/selection.ts +214 -0
  32. anytrellis-0.1.0/js/src/core/ticks.ts +216 -0
  33. anytrellis-0.1.0/js/src/core/transport.ts +153 -0
  34. anytrellis-0.1.0/js/src/core/types.ts +173 -0
  35. anytrellis-0.1.0/js/src/core/view.ts +1347 -0
  36. anytrellis-0.1.0/js/src/standalone.ts +56 -0
  37. anytrellis-0.1.0/js/src/widget.css +375 -0
  38. anytrellis-0.1.0/js/src/widget.ts +174 -0
  39. anytrellis-0.1.0/js/test/core.test.ts +404 -0
  40. anytrellis-0.1.0/js/test/interact.mjs +130 -0
  41. anytrellis-0.1.0/js/test/legend.mjs +120 -0
  42. anytrellis-0.1.0/js/test/linked.mjs +127 -0
  43. anytrellis-0.1.0/js/test/live-open.mjs +81 -0
  44. anytrellis-0.1.0/js/test/live-select.mjs +76 -0
  45. anytrellis-0.1.0/js/test/live-zoom.mjs +66 -0
  46. anytrellis-0.1.0/js/test/notebooks.mjs +46 -0
  47. anytrellis-0.1.0/js/test/pages.py +36 -0
  48. anytrellis-0.1.0/js/test/select.mjs +114 -0
  49. anytrellis-0.1.0/js/test/visual.mjs +52 -0
  50. anytrellis-0.1.0/js/test/zoom.mjs +180 -0
  51. anytrellis-0.1.0/js/tsconfig.json +17 -0
  52. anytrellis-0.1.0/js/vitest.config.ts +4 -0
  53. anytrellis-0.1.0/pyproject.toml +125 -0
  54. anytrellis-0.1.0/src/anytrellis/__init__.py +13 -0
  55. anytrellis-0.1.0/src/anytrellis/codec.py +71 -0
  56. anytrellis-0.1.0/src/anytrellis/data.py +251 -0
  57. anytrellis-0.1.0/src/anytrellis/datasets/SOURCES.md +17 -0
  58. anytrellis-0.1.0/src/anytrellis/datasets/__init__.py +55 -0
  59. anytrellis-0.1.0/src/anytrellis/datasets/anscombe.csv +45 -0
  60. anytrellis-0.1.0/src/anytrellis/datasets/barley.csv +121 -0
  61. anytrellis-0.1.0/src/anytrellis/datasets/cars.csv +407 -0
  62. anytrellis-0.1.0/src/anytrellis/datasets/gapminder.csv +694 -0
  63. anytrellis-0.1.0/src/anytrellis/datasets/penguins.csv +345 -0
  64. anytrellis-0.1.0/src/anytrellis/datasets/seattle_weather.csv +1462 -0
  65. anytrellis-0.1.0/src/anytrellis/facets.py +284 -0
  66. anytrellis-0.1.0/src/anytrellis/layers.py +345 -0
  67. anytrellis-0.1.0/src/anytrellis/linking.py +210 -0
  68. anytrellis-0.1.0/src/anytrellis/plot.py +1629 -0
  69. anytrellis-0.1.0/src/anytrellis/py.typed +0 -0
  70. anytrellis-0.1.0/src/anytrellis/scales.py +115 -0
  71. anytrellis-0.1.0/src/anytrellis/selection.py +121 -0
  72. anytrellis-0.1.0/src/anytrellis/static/standalone.js +159 -0
  73. anytrellis-0.1.0/src/anytrellis/static/widget.css +1 -0
  74. anytrellis-0.1.0/src/anytrellis/static/widget.js +159 -0
  75. anytrellis-0.1.0/src/anytrellis/stats.py +733 -0
  76. anytrellis-0.1.0/src/anytrellis/styling.py +107 -0
  77. anytrellis-0.1.0/src/anytrellis/synthetic.py +77 -0
  78. anytrellis-0.1.0/src/anytrellis/widget.py +113 -0
  79. anytrellis-0.1.0/tests/test_examples.py +48 -0
  80. anytrellis-0.1.0/tests/test_linking.py +191 -0
  81. 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
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/anytrellis)](https://pypi.org/project/anytrellis/)
39
+ [![CI](https://github.com/mluttikh/anytrellis/actions/workflows/ci.yml/badge.svg)](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.