datashader-browser 0.1.0a2__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.
- datashader_browser-0.1.0a2/.github/workflows/pages.yml +49 -0
- datashader_browser-0.1.0a2/.github/workflows/release.yml +48 -0
- datashader_browser-0.1.0a2/.github/workflows/test.yml +16 -0
- datashader_browser-0.1.0a2/.gitignore +10 -0
- datashader_browser-0.1.0a2/LICENSE +28 -0
- datashader_browser-0.1.0a2/PKG-INFO +162 -0
- datashader_browser-0.1.0a2/README.md +133 -0
- datashader_browser-0.1.0a2/ROADMAP.md +35 -0
- datashader_browser-0.1.0a2/content/README.md +10 -0
- datashader_browser-0.1.0a2/content/nyc-taxi-browser.ipynb +119 -0
- datashader_browser-0.1.0a2/content/streaming-parquet.ipynb +101 -0
- datashader_browser-0.1.0a2/content/streaming-points.ipynb +72 -0
- datashader_browser-0.1.0a2/docs/architecture.md +111 -0
- datashader_browser-0.1.0a2/docs/assets/nyc-taxi-pickups.png +0 -0
- datashader_browser-0.1.0a2/docs/research.md +78 -0
- datashader_browser-0.1.0a2/environment.yml +26 -0
- datashader_browser-0.1.0a2/examples/nyc_taxi.py +123 -0
- datashader_browser-0.1.0a2/pyproject.toml +48 -0
- datashader_browser-0.1.0a2/release-dist/.gitignore +1 -0
- datashader_browser-0.1.0a2/requirements-build.txt +4 -0
- datashader_browser-0.1.0a2/src/datashader_browser/__init__.py +11 -0
- datashader_browser-0.1.0a2/src/datashader_browser/streaming.py +272 -0
- datashader_browser-0.1.0a2/tests/test_streaming.py +132 -0
- datashader_browser-0.1.0a2/uv.lock +1352 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
name: Build browser demo
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
branches: [main]
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
pages: write
|
|
11
|
+
id-token: write
|
|
12
|
+
|
|
13
|
+
concurrency:
|
|
14
|
+
group: pages
|
|
15
|
+
cancel-in-progress: true
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
build:
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
- uses: mamba-org/setup-micromamba@v2
|
|
23
|
+
with:
|
|
24
|
+
init-shell: none
|
|
25
|
+
- uses: actions/setup-python@v5
|
|
26
|
+
with:
|
|
27
|
+
python-version: "3.12"
|
|
28
|
+
cache: pip
|
|
29
|
+
- run: pip install -r requirements-build.txt
|
|
30
|
+
- name: Build JupyterLite site
|
|
31
|
+
run: >-
|
|
32
|
+
jupyter lite build
|
|
33
|
+
--XeusAddon.environment_file=environment.yml
|
|
34
|
+
--contents content
|
|
35
|
+
--output-dir dist
|
|
36
|
+
- uses: actions/upload-pages-artifact@v3
|
|
37
|
+
with:
|
|
38
|
+
path: dist
|
|
39
|
+
|
|
40
|
+
deploy:
|
|
41
|
+
environment:
|
|
42
|
+
name: github-pages
|
|
43
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
needs: build
|
|
46
|
+
steps:
|
|
47
|
+
- name: Deploy to GitHub Pages
|
|
48
|
+
id: deployment
|
|
49
|
+
uses: actions/deploy-pages@v4
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
name: Build release distributions
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: astral-sh/setup-uv@v6
|
|
18
|
+
with:
|
|
19
|
+
enable-cache: true
|
|
20
|
+
- name: Verify tag matches package version
|
|
21
|
+
run: test "$(uv run python -c 'from datashader_browser import __version__; print(__version__)')" = "${GITHUB_REF_NAME#v}"
|
|
22
|
+
- name: Build wheel and source distribution
|
|
23
|
+
run: uv build --out-dir release-dist
|
|
24
|
+
- name: Validate package metadata
|
|
25
|
+
run: uv run --with twine twine check release-dist/*
|
|
26
|
+
- uses: actions/upload-artifact@v4
|
|
27
|
+
with:
|
|
28
|
+
name: python-distributions
|
|
29
|
+
path: release-dist/
|
|
30
|
+
|
|
31
|
+
publish:
|
|
32
|
+
name: Publish release to PyPI
|
|
33
|
+
needs: build
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
environment:
|
|
36
|
+
name: pypi
|
|
37
|
+
url: https://pypi.org/p/datashader-browser
|
|
38
|
+
permissions:
|
|
39
|
+
id-token: write
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/download-artifact@v4
|
|
42
|
+
with:
|
|
43
|
+
name: python-distributions
|
|
44
|
+
path: release-dist/
|
|
45
|
+
- name: Publish package distributions to PyPI
|
|
46
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
47
|
+
with:
|
|
48
|
+
packages-dir: release-dist/
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: astral-sh/setup-uv@v6
|
|
13
|
+
with:
|
|
14
|
+
enable-cache: true
|
|
15
|
+
- run: uv sync --extra test --extra data
|
|
16
|
+
- run: uv run pytest
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Sumaiya Islam
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: datashader-browser
|
|
3
|
+
Version: 0.1.0a2
|
|
4
|
+
Summary: Bounded-memory Datashader pipelines for Python running in the browser
|
|
5
|
+
Project-URL: Homepage, https://github.com/SuMayaBee/datashader-browser
|
|
6
|
+
Project-URL: Issues, https://github.com/SuMayaBee/datashader-browser/issues
|
|
7
|
+
Project-URL: Repository, https://github.com/SuMayaBee/datashader-browser
|
|
8
|
+
Author: Sumaiya Islam
|
|
9
|
+
License-Expression: BSD-3-Clause
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: datashader<0.20,>=0.19.1
|
|
22
|
+
Requires-Dist: pandas>=2.2
|
|
23
|
+
Provides-Extra: data
|
|
24
|
+
Requires-Dist: pillow>=11; extra == 'data'
|
|
25
|
+
Requires-Dist: pyarrow>=21; extra == 'data'
|
|
26
|
+
Provides-Extra: test
|
|
27
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# datashader-browser
|
|
31
|
+
|
|
32
|
+
Run Datashader-powered visualizations entirely in a browser with Python and
|
|
33
|
+
WebAssembly—without assuming that the complete dataset fits in browser memory.
|
|
34
|
+
|
|
35
|
+
This is an early, independent project. It is not yet an official HoloViz
|
|
36
|
+
project and is not affiliated with or endorsed by the Datashader maintainers.
|
|
37
|
+
|
|
38
|
+
[Try the live browser demo](https://sumayabee.github.io/datashader-browser/).
|
|
39
|
+
Open `streaming-points.ipynb` and run all cells.
|
|
40
|
+
|
|
41
|
+
## What works now
|
|
42
|
+
|
|
43
|
+
The first vertical slice is a bounded-memory point aggregator:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
import datashader as ds
|
|
47
|
+
import datashader.transfer_functions as tf
|
|
48
|
+
|
|
49
|
+
from datashader_browser import StreamingCanvas, iter_csv_chunks, iter_parquet_batches
|
|
50
|
+
|
|
51
|
+
source = iter_csv_chunks(
|
|
52
|
+
"points.csv",
|
|
53
|
+
chunksize=250_000,
|
|
54
|
+
columns=["longitude", "latitude", "value"],
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
canvas = StreamingCanvas(
|
|
58
|
+
plot_width=1000,
|
|
59
|
+
plot_height=600,
|
|
60
|
+
x_range=(-180, 180),
|
|
61
|
+
y_range=(-90, 90),
|
|
62
|
+
)
|
|
63
|
+
aggregate = canvas.points(source, "longitude", "latitude", agg=ds.mean("value"))
|
|
64
|
+
image = tf.shade(aggregate)
|
|
65
|
+
|
|
66
|
+
# A local Parquet file can yield projected Arrow record batches directly.
|
|
67
|
+
parquet_source = iter_parquet_batches(
|
|
68
|
+
"points.parquet", columns=["longitude", "latitude"], batch_size=250_000
|
|
69
|
+
)
|
|
70
|
+
aggregate = canvas.points(parquet_source, "longitude", "latitude", agg=ds.count())
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Only one input chunk and Datashader's fixed-size aggregate buffers are live at
|
|
74
|
+
once. The implementation calls Datashader's own compiler, so point binning and
|
|
75
|
+
reductions match native Datashader instead of being reimplemented.
|
|
76
|
+
|
|
77
|
+
Currently verified reductions are `count`, `sum`, `mean`, `min`, and `max`.
|
|
78
|
+
Explicit `x_range` and `y_range` values are required to preserve a true
|
|
79
|
+
single-pass pipeline.
|
|
80
|
+
|
|
81
|
+
## NYC Taxi real-data verification
|
|
82
|
+
|
|
83
|
+
The streaming accumulator successfully rendered all **11,842,094 pickup
|
|
84
|
+
locations** in HoloViz's transformed
|
|
85
|
+
[January 2015 NYC Taxi dataset](https://examples.holoviz.org/gallery/nyc_taxi/nyc_taxi.html).
|
|
86
|
+
The 385 MB Parquet file was decoded as 48 Arrow/Pandas batches of at most
|
|
87
|
+
250,000 rows; the complete table was never materialized as one DataFrame.
|
|
88
|
+
|
|
89
|
+

|
|
90
|
+
|
|
91
|
+
One warm-cache Linux run produced the 1200-by-700 image in 3.24 seconds with
|
|
92
|
+
514 MiB peak process RSS. Runtime and memory will vary by machine. Reproduce it
|
|
93
|
+
with:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
uv sync --extra data
|
|
97
|
+
uv run python examples/nyc_taxi.py --download
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This benchmark validates `StreamingCanvas` against a real large dataset on
|
|
101
|
+
native Python. The browser build now includes an experimental PyArrow Parquet
|
|
102
|
+
reader: open `nyc-taxi-browser.ipynb`, place the source file in the browser
|
|
103
|
+
filesystem, and it streams projected Arrow record batches through the same
|
|
104
|
+
accumulator. Full 385 MB end-to-end browser timing and memory results are still
|
|
105
|
+
an explicit validation task, not a claim made by this repository.
|
|
106
|
+
|
|
107
|
+
## Browser runtime
|
|
108
|
+
|
|
109
|
+
The initial deployment target is JupyterLite with the Xeus-Python WebAssembly
|
|
110
|
+
kernel. Its environment is described in [`environment.yml`](environment.yml)
|
|
111
|
+
and uses the experimental Numba and llvmlite packages from emscripten-forge.
|
|
112
|
+
The kernel executes in a Web Worker, keeping aggregation off the browser UI
|
|
113
|
+
thread. The Parquet-capable build is currently about 256 MB because PyArrow
|
|
114
|
+
brings compiled Arrow and compression dependencies; this is an engineering
|
|
115
|
+
baseline, not the final delivery architecture. The first visit therefore needs
|
|
116
|
+
a substantial download, while subsequent use benefits from the browser cache.
|
|
117
|
+
|
|
118
|
+
Browser verification: `streaming-parquet.ipynb` has been run end-to-end in the
|
|
119
|
+
Xeus-Python WebAssembly kernel. It created a 57.2 MB, two-million-row Parquet
|
|
120
|
+
file in the browser filesystem, streamed it through 20 Arrow batches, checked
|
|
121
|
+
the final row count, and rendered the shaded aggregate with no browser console
|
|
122
|
+
errors. The full 385 MB NYC Taxi browser benchmark remains a separate task.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
uv sync --extra test
|
|
126
|
+
uv run pytest
|
|
127
|
+
|
|
128
|
+
# Build the browser site (requires the build dependencies below).
|
|
129
|
+
uv pip install -r requirements-build.txt
|
|
130
|
+
uv run jupyter lite build \
|
|
131
|
+
--XeusAddon.environment_file=environment.yml \
|
|
132
|
+
--contents content \
|
|
133
|
+
--output-dir dist
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Large-data strategy
|
|
137
|
+
|
|
138
|
+
“In the browser” does not mean that an unlimited dataset can be copied into a
|
|
139
|
+
Pandas DataFrame. The project uses three execution levels:
|
|
140
|
+
|
|
141
|
+
1. **Stream:** scan CSV or local Parquet/Arrow batches through a bounded-memory Datashader
|
|
142
|
+
accumulator. Total row count affects time, not peak aggregate memory.
|
|
143
|
+
2. **Prune:** use Parquet row groups, spatial partitions, and DuckDB-Wasm to
|
|
144
|
+
read only the viewport and required columns.
|
|
145
|
+
3. **Tile:** for billion-row interactive datasets, fetch spatial partitions or
|
|
146
|
+
precomputed multiresolution aggregates instead of rescanning the source on
|
|
147
|
+
every zoom.
|
|
148
|
+
|
|
149
|
+
See [Architecture](docs/architecture.md), [Research](docs/research.md), and the
|
|
150
|
+
[roadmap](ROADMAP.md) for the engineering decisions and current limitations.
|
|
151
|
+
|
|
152
|
+
## Status
|
|
153
|
+
|
|
154
|
+
This repository is an alpha-stage research implementation. The WebAssembly
|
|
155
|
+
Numba/llvmlite stack works in current demonstrations but its patches have not
|
|
156
|
+
yet been fully upstreamed. Production use should wait for reproducible browser
|
|
157
|
+
builds, compatibility CI, and measured browser memory limits.
|
|
158
|
+
|
|
159
|
+
## License
|
|
160
|
+
|
|
161
|
+
BSD-3-Clause. Datashader is a separate BSD-3-Clause project owned by its
|
|
162
|
+
respective copyright holders and contributors.
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# datashader-browser
|
|
2
|
+
|
|
3
|
+
Run Datashader-powered visualizations entirely in a browser with Python and
|
|
4
|
+
WebAssembly—without assuming that the complete dataset fits in browser memory.
|
|
5
|
+
|
|
6
|
+
This is an early, independent project. It is not yet an official HoloViz
|
|
7
|
+
project and is not affiliated with or endorsed by the Datashader maintainers.
|
|
8
|
+
|
|
9
|
+
[Try the live browser demo](https://sumayabee.github.io/datashader-browser/).
|
|
10
|
+
Open `streaming-points.ipynb` and run all cells.
|
|
11
|
+
|
|
12
|
+
## What works now
|
|
13
|
+
|
|
14
|
+
The first vertical slice is a bounded-memory point aggregator:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import datashader as ds
|
|
18
|
+
import datashader.transfer_functions as tf
|
|
19
|
+
|
|
20
|
+
from datashader_browser import StreamingCanvas, iter_csv_chunks, iter_parquet_batches
|
|
21
|
+
|
|
22
|
+
source = iter_csv_chunks(
|
|
23
|
+
"points.csv",
|
|
24
|
+
chunksize=250_000,
|
|
25
|
+
columns=["longitude", "latitude", "value"],
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
canvas = StreamingCanvas(
|
|
29
|
+
plot_width=1000,
|
|
30
|
+
plot_height=600,
|
|
31
|
+
x_range=(-180, 180),
|
|
32
|
+
y_range=(-90, 90),
|
|
33
|
+
)
|
|
34
|
+
aggregate = canvas.points(source, "longitude", "latitude", agg=ds.mean("value"))
|
|
35
|
+
image = tf.shade(aggregate)
|
|
36
|
+
|
|
37
|
+
# A local Parquet file can yield projected Arrow record batches directly.
|
|
38
|
+
parquet_source = iter_parquet_batches(
|
|
39
|
+
"points.parquet", columns=["longitude", "latitude"], batch_size=250_000
|
|
40
|
+
)
|
|
41
|
+
aggregate = canvas.points(parquet_source, "longitude", "latitude", agg=ds.count())
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Only one input chunk and Datashader's fixed-size aggregate buffers are live at
|
|
45
|
+
once. The implementation calls Datashader's own compiler, so point binning and
|
|
46
|
+
reductions match native Datashader instead of being reimplemented.
|
|
47
|
+
|
|
48
|
+
Currently verified reductions are `count`, `sum`, `mean`, `min`, and `max`.
|
|
49
|
+
Explicit `x_range` and `y_range` values are required to preserve a true
|
|
50
|
+
single-pass pipeline.
|
|
51
|
+
|
|
52
|
+
## NYC Taxi real-data verification
|
|
53
|
+
|
|
54
|
+
The streaming accumulator successfully rendered all **11,842,094 pickup
|
|
55
|
+
locations** in HoloViz's transformed
|
|
56
|
+
[January 2015 NYC Taxi dataset](https://examples.holoviz.org/gallery/nyc_taxi/nyc_taxi.html).
|
|
57
|
+
The 385 MB Parquet file was decoded as 48 Arrow/Pandas batches of at most
|
|
58
|
+
250,000 rows; the complete table was never materialized as one DataFrame.
|
|
59
|
+
|
|
60
|
+

|
|
61
|
+
|
|
62
|
+
One warm-cache Linux run produced the 1200-by-700 image in 3.24 seconds with
|
|
63
|
+
514 MiB peak process RSS. Runtime and memory will vary by machine. Reproduce it
|
|
64
|
+
with:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
uv sync --extra data
|
|
68
|
+
uv run python examples/nyc_taxi.py --download
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This benchmark validates `StreamingCanvas` against a real large dataset on
|
|
72
|
+
native Python. The browser build now includes an experimental PyArrow Parquet
|
|
73
|
+
reader: open `nyc-taxi-browser.ipynb`, place the source file in the browser
|
|
74
|
+
filesystem, and it streams projected Arrow record batches through the same
|
|
75
|
+
accumulator. Full 385 MB end-to-end browser timing and memory results are still
|
|
76
|
+
an explicit validation task, not a claim made by this repository.
|
|
77
|
+
|
|
78
|
+
## Browser runtime
|
|
79
|
+
|
|
80
|
+
The initial deployment target is JupyterLite with the Xeus-Python WebAssembly
|
|
81
|
+
kernel. Its environment is described in [`environment.yml`](environment.yml)
|
|
82
|
+
and uses the experimental Numba and llvmlite packages from emscripten-forge.
|
|
83
|
+
The kernel executes in a Web Worker, keeping aggregation off the browser UI
|
|
84
|
+
thread. The Parquet-capable build is currently about 256 MB because PyArrow
|
|
85
|
+
brings compiled Arrow and compression dependencies; this is an engineering
|
|
86
|
+
baseline, not the final delivery architecture. The first visit therefore needs
|
|
87
|
+
a substantial download, while subsequent use benefits from the browser cache.
|
|
88
|
+
|
|
89
|
+
Browser verification: `streaming-parquet.ipynb` has been run end-to-end in the
|
|
90
|
+
Xeus-Python WebAssembly kernel. It created a 57.2 MB, two-million-row Parquet
|
|
91
|
+
file in the browser filesystem, streamed it through 20 Arrow batches, checked
|
|
92
|
+
the final row count, and rendered the shaded aggregate with no browser console
|
|
93
|
+
errors. The full 385 MB NYC Taxi browser benchmark remains a separate task.
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
uv sync --extra test
|
|
97
|
+
uv run pytest
|
|
98
|
+
|
|
99
|
+
# Build the browser site (requires the build dependencies below).
|
|
100
|
+
uv pip install -r requirements-build.txt
|
|
101
|
+
uv run jupyter lite build \
|
|
102
|
+
--XeusAddon.environment_file=environment.yml \
|
|
103
|
+
--contents content \
|
|
104
|
+
--output-dir dist
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Large-data strategy
|
|
108
|
+
|
|
109
|
+
“In the browser” does not mean that an unlimited dataset can be copied into a
|
|
110
|
+
Pandas DataFrame. The project uses three execution levels:
|
|
111
|
+
|
|
112
|
+
1. **Stream:** scan CSV or local Parquet/Arrow batches through a bounded-memory Datashader
|
|
113
|
+
accumulator. Total row count affects time, not peak aggregate memory.
|
|
114
|
+
2. **Prune:** use Parquet row groups, spatial partitions, and DuckDB-Wasm to
|
|
115
|
+
read only the viewport and required columns.
|
|
116
|
+
3. **Tile:** for billion-row interactive datasets, fetch spatial partitions or
|
|
117
|
+
precomputed multiresolution aggregates instead of rescanning the source on
|
|
118
|
+
every zoom.
|
|
119
|
+
|
|
120
|
+
See [Architecture](docs/architecture.md), [Research](docs/research.md), and the
|
|
121
|
+
[roadmap](ROADMAP.md) for the engineering decisions and current limitations.
|
|
122
|
+
|
|
123
|
+
## Status
|
|
124
|
+
|
|
125
|
+
This repository is an alpha-stage research implementation. The WebAssembly
|
|
126
|
+
Numba/llvmlite stack works in current demonstrations but its patches have not
|
|
127
|
+
yet been fully upstreamed. Production use should wait for reproducible browser
|
|
128
|
+
builds, compatibility CI, and measured browser memory limits.
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
BSD-3-Clause. Datashader is a separate BSD-3-Clause project owned by its
|
|
133
|
+
respective copyright holders and contributors.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Roadmap
|
|
2
|
+
|
|
3
|
+
## 0.1: bounded-memory correctness
|
|
4
|
+
|
|
5
|
+
- Point glyphs over iterables of Pandas chunks.
|
|
6
|
+
- Count, sum, mean, min, and max parity with Datashader 0.19.1.
|
|
7
|
+
- Progress and cancellation hooks.
|
|
8
|
+
- Xeus-Python/JupyterLite environment using emscripten-forge Numba.
|
|
9
|
+
- Browser build and deployment CI.
|
|
10
|
+
|
|
11
|
+
## 0.2: browser data plane
|
|
12
|
+
|
|
13
|
+
- Local Parquet record-batch source via PyArrow in the Xeus-Python worker.
|
|
14
|
+
- DuckDB-Wasm worker for local and remote Parquet.
|
|
15
|
+
- Column projection and viewport predicates.
|
|
16
|
+
- Transferable buffers between the data worker and Python worker.
|
|
17
|
+
- Generation IDs so obsolete pan/zoom jobs are cancelled or ignored.
|
|
18
|
+
|
|
19
|
+
## 0.3: indexed exploration
|
|
20
|
+
|
|
21
|
+
- Spatially partitioned Parquet manifest format.
|
|
22
|
+
- Partition pruning by canvas bounds.
|
|
23
|
+
- Cache recent partitions in OPFS.
|
|
24
|
+
- Progressive low-resolution then full-resolution rendering.
|
|
25
|
+
|
|
26
|
+
## Later
|
|
27
|
+
|
|
28
|
+
- Lines with one-row partition overlap.
|
|
29
|
+
- Categorical aggregation with explicit category budgets.
|
|
30
|
+
- Panel/Bokeh integration outside the JupyterLab shell.
|
|
31
|
+
- Optional GPU aggregation experiments after CPU parity and memory behavior are
|
|
32
|
+
characterized.
|
|
33
|
+
|
|
34
|
+
Polygons, trimesh, quadmesh, Dask, CUDA, and arbitrary row-index reductions are
|
|
35
|
+
outside the initial scope.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Datashader Browser Lab
|
|
2
|
+
|
|
3
|
+
Start with `streaming-parquet.ipynb` to create, stream, and render a Parquet
|
|
4
|
+
dataset entirely in this browser tab. `streaming-points.ipynb` is the smaller
|
|
5
|
+
in-memory-generated example. `nyc-taxi-browser.ipynb` contains the full
|
|
6
|
+
11.8-million-row HoloViz NYC Taxi workflow and instructions for supplying its
|
|
7
|
+
385 MB source file.
|
|
8
|
+
|
|
9
|
+
The Xeus-Python kernel and all Python packages are WebAssembly assets served by
|
|
10
|
+
the static site; no Python server is involved.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cells": [
|
|
3
|
+
{
|
|
4
|
+
"cell_type": "markdown",
|
|
5
|
+
"metadata": {},
|
|
6
|
+
"source": [
|
|
7
|
+
"# NYC Taxi: 11.8 million points in browser Python\n",
|
|
8
|
+
"\n",
|
|
9
|
+
"This notebook renders all January 2015 NYC Taxi pickup locations from the 385 MB HoloViz Parquet dataset. Upload or mount `nyc_taxi_wide.parq` in JupyterLite, or enable the download cell below. Parquet decoding and Datashader aggregation are bounded by the selected batch size; the complete table is never constructed as one DataFrame."
|
|
10
|
+
]
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"cell_type": "code",
|
|
14
|
+
"execution_count": null,
|
|
15
|
+
"metadata": {},
|
|
16
|
+
"outputs": [],
|
|
17
|
+
"source": [
|
|
18
|
+
"from pathlib import Path\n",
|
|
19
|
+
"from urllib.request import urlopen\n",
|
|
20
|
+
"\n",
|
|
21
|
+
"import pyarrow.parquet as pq\n",
|
|
22
|
+
"import datashader.transfer_functions as tf\n",
|
|
23
|
+
"\n",
|
|
24
|
+
"from datashader_browser import StreamingCanvas, iter_parquet_batches\n",
|
|
25
|
+
"\n",
|
|
26
|
+
"DATA_URL = 'https://datasets.holoviz.org/nyc_taxi/v1/nyc_taxi_wide.parq'\n",
|
|
27
|
+
"DATA_PATH = Path('nyc_taxi_wide.parq')\n",
|
|
28
|
+
"X_RANGE = (-8_256_000, -8_209_000)\n",
|
|
29
|
+
"Y_RANGE = (4_964_000, 4_990_000)"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"cell_type": "markdown",
|
|
34
|
+
"metadata": {},
|
|
35
|
+
"source": [
|
|
36
|
+
"Set `DOWNLOAD = True` to fetch the public dataset. Mounting or uploading it through JupyterLite avoids repeating this 385 MB download on subsequent sessions."
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"cell_type": "code",
|
|
41
|
+
"execution_count": null,
|
|
42
|
+
"metadata": {},
|
|
43
|
+
"outputs": [],
|
|
44
|
+
"source": [
|
|
45
|
+
"DOWNLOAD = False\n",
|
|
46
|
+
"\n",
|
|
47
|
+
"if DOWNLOAD and not DATA_PATH.exists():\n",
|
|
48
|
+
" with urlopen(DATA_URL) as response, DATA_PATH.open('wb') as output:\n",
|
|
49
|
+
" while block := response.read(1024 * 1024):\n",
|
|
50
|
+
" output.write(block)\n",
|
|
51
|
+
"\n",
|
|
52
|
+
"if not DATA_PATH.exists():\n",
|
|
53
|
+
" raise FileNotFoundError(\n",
|
|
54
|
+
" f'{DATA_PATH} is missing. Upload/mount it in JupyterLite or set DOWNLOAD=True.'\n",
|
|
55
|
+
" )\n",
|
|
56
|
+
"\n",
|
|
57
|
+
"parquet = pq.ParquetFile(DATA_PATH)\n",
|
|
58
|
+
"print(f'{parquet.metadata.num_rows:,} rows; {DATA_PATH.stat().st_size / 1_000_000:.1f} MB')"
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"cell_type": "code",
|
|
63
|
+
"execution_count": null,
|
|
64
|
+
"metadata": {},
|
|
65
|
+
"outputs": [],
|
|
66
|
+
"source": [
|
|
67
|
+
"def report(progress):\n",
|
|
68
|
+
" if progress.chunks % 10 == 0 or progress.rows == parquet.metadata.num_rows:\n",
|
|
69
|
+
" print(f'aggregated {progress.rows:,}/{parquet.metadata.num_rows:,} rows')\n",
|
|
70
|
+
"\n",
|
|
71
|
+
"canvas = StreamingCanvas(\n",
|
|
72
|
+
" plot_width=1_200,\n",
|
|
73
|
+
" plot_height=700,\n",
|
|
74
|
+
" x_range=X_RANGE,\n",
|
|
75
|
+
" y_range=Y_RANGE,\n",
|
|
76
|
+
")\n",
|
|
77
|
+
"aggregate = canvas.points(\n",
|
|
78
|
+
" iter_parquet_batches(\n",
|
|
79
|
+
" DATA_PATH,\n",
|
|
80
|
+
" batch_size=250_000,\n",
|
|
81
|
+
" columns=['pickup_x', 'pickup_y'],\n",
|
|
82
|
+
" ),\n",
|
|
83
|
+
" 'pickup_x',\n",
|
|
84
|
+
" 'pickup_y',\n",
|
|
85
|
+
" on_progress=report,\n",
|
|
86
|
+
")\n",
|
|
87
|
+
"aggregate.attrs"
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"cell_type": "code",
|
|
92
|
+
"execution_count": null,
|
|
93
|
+
"metadata": {},
|
|
94
|
+
"outputs": [],
|
|
95
|
+
"source": [
|
|
96
|
+
"assert aggregate.attrs['source_rows'] == parquet.metadata.num_rows\n",
|
|
97
|
+
"image = tf.shade(\n",
|
|
98
|
+
" aggregate,\n",
|
|
99
|
+
" cmap=['#111827', '#1d4ed8', '#06b6d4', '#facc15', '#fff7ed'],\n",
|
|
100
|
+
" how='eq_hist',\n",
|
|
101
|
+
")\n",
|
|
102
|
+
"tf.set_background(image, '#050814').to_pil()"
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
],
|
|
106
|
+
"metadata": {
|
|
107
|
+
"kernelspec": {
|
|
108
|
+
"display_name": "Python (XPython)",
|
|
109
|
+
"language": "python",
|
|
110
|
+
"name": "xpython"
|
|
111
|
+
},
|
|
112
|
+
"language_info": {
|
|
113
|
+
"name": "python",
|
|
114
|
+
"version": "3.13"
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
"nbformat": 4,
|
|
118
|
+
"nbformat_minor": 5
|
|
119
|
+
}
|