datashader-browser 0.1.0a2__py3-none-any.whl

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.
@@ -0,0 +1,11 @@
1
+ """Run Datashader over bounded-memory streams in a browser Python runtime."""
2
+
3
+ from .streaming import Progress, StreamingCanvas, iter_csv_chunks, iter_parquet_batches
4
+
5
+ __all__ = [
6
+ "Progress",
7
+ "StreamingCanvas",
8
+ "iter_csv_chunks",
9
+ "iter_parquet_batches",
10
+ ]
11
+ __version__ = "0.1.0a2"
@@ -0,0 +1,272 @@
1
+ """Single-pass, bounded-memory Datashader aggregation.
2
+
3
+ The browser cannot assume that the complete dataset fits in its WebAssembly
4
+ heap. This module keeps one input chunk and Datashader's fixed-size aggregate
5
+ state resident at a time. It intentionally starts with point glyphs: they do
6
+ not require overlap between adjacent chunks and therefore provide a sound
7
+ foundation for later Arrow and DuckDB-Wasm sources.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Callable, Iterable, Iterator, Sequence
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+ import sys
16
+ from typing import Any
17
+
18
+ import pandas as pd
19
+
20
+ import datashader as ds
21
+ from datashader.compiler import compile_components
22
+ from datashader.core import _bypixel_sanitise
23
+ from datashader.glyphs import Point
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class Progress:
28
+ """Progress reported after an input chunk has been aggregated."""
29
+
30
+ chunks: int
31
+ rows: int
32
+
33
+
34
+ ProgressCallback = Callable[[Progress], None]
35
+ CancelCallback = Callable[[], bool]
36
+
37
+
38
+ def iter_csv_chunks(
39
+ path: str | Path,
40
+ *,
41
+ chunksize: int = 250_000,
42
+ columns: Iterable[str] | None = None,
43
+ **read_csv_kwargs: Any,
44
+ ) -> Iterator[pd.DataFrame]:
45
+ """Yield bounded-size DataFrames from a CSV in the browser filesystem.
46
+
47
+ Parameters mirror :func:`pandas.read_csv`. Only requested columns are
48
+ decoded, which is important in memory-constrained WebAssembly runtimes.
49
+ """
50
+
51
+ if chunksize <= 0:
52
+ raise ValueError("chunksize must be greater than zero")
53
+ usecols = None if columns is None else list(columns)
54
+ yield from pd.read_csv(
55
+ path,
56
+ chunksize=chunksize,
57
+ usecols=usecols,
58
+ **read_csv_kwargs,
59
+ )
60
+
61
+
62
+ def iter_parquet_batches(
63
+ source: Any,
64
+ *,
65
+ batch_size: int = 250_000,
66
+ columns: Sequence[str] | None = None,
67
+ row_groups: Sequence[int] | None = None,
68
+ use_threads: bool = False,
69
+ ) -> Iterator[Any]:
70
+ """Yield bounded-size Arrow record batches from a Parquet source.
71
+
72
+ ``source`` may be a path or any seekable file object accepted by
73
+ :class:`pyarrow.parquet.ParquetFile`. In JupyterLite/Xeus-Python this
74
+ includes files uploaded or mounted in the browser filesystem. PyArrow is
75
+ imported lazily so CSV-only users do not need the optional dependency.
76
+
77
+ Arrow batches are intentionally returned without first materializing a
78
+ complete Pandas DataFrame. :meth:`StreamingCanvas.points` converts one
79
+ batch at a time immediately before aggregation.
80
+ """
81
+
82
+ if batch_size <= 0:
83
+ raise ValueError("batch_size must be greater than zero")
84
+
85
+ try:
86
+ import pyarrow.parquet as pq
87
+ except ImportError as exc: # pragma: no cover - depends on optional install
88
+ raise ImportError(
89
+ "Parquet streaming requires PyArrow; install "
90
+ "datashader-browser[data]"
91
+ ) from exc
92
+
93
+ parquet = pq.ParquetFile(source)
94
+ yield from parquet.iter_batches(
95
+ batch_size=batch_size,
96
+ row_groups=None if row_groups is None else list(row_groups),
97
+ columns=None if columns is None else list(columns),
98
+ use_threads=use_threads,
99
+ )
100
+
101
+
102
+ class StreamingCanvas:
103
+ """A Datashader canvas that consumes Pandas or Arrow chunks.
104
+
105
+ Unlike ``datashader.Canvas.points``, the full source is never retained.
106
+ The current implementation requires explicit ranges so aggregation stays
107
+ single-pass. Use source metadata, a cheap bounds query, or a preparatory
108
+ pass to determine those ranges.
109
+ """
110
+
111
+ def __init__(
112
+ self,
113
+ *,
114
+ plot_width: int = 600,
115
+ plot_height: int = 600,
116
+ x_range: tuple[float, float],
117
+ y_range: tuple[float, float],
118
+ x_axis_type: str = "linear",
119
+ y_axis_type: str = "linear",
120
+ ) -> None:
121
+ self._canvas = ds.Canvas(
122
+ plot_width=plot_width,
123
+ plot_height=plot_height,
124
+ x_range=x_range,
125
+ y_range=y_range,
126
+ x_axis_type=x_axis_type,
127
+ y_axis_type=y_axis_type,
128
+ )
129
+
130
+ @property
131
+ def canvas(self) -> ds.Canvas:
132
+ """The underlying Datashader canvas."""
133
+
134
+ return self._canvas
135
+
136
+ def points(
137
+ self,
138
+ chunks: Iterable[Any],
139
+ x: str,
140
+ y: str,
141
+ *,
142
+ agg: Any | None = None,
143
+ on_progress: ProgressCallback | None = None,
144
+ cancelled: CancelCallback | None = None,
145
+ ):
146
+ """Aggregate point chunks while retaining only fixed-size state.
147
+
148
+ The aggregation uses Datashader's compiler directly, so binning and
149
+ reduction behavior match the native package rather than a separate
150
+ reimplementation.
151
+ """
152
+
153
+ reduction = ds.count() if agg is None else agg
154
+ glyph = Point(x, y)
155
+ iterator = iter(chunks)
156
+
157
+ try:
158
+ first = next(iterator)
159
+ except StopIteration as exc:
160
+ raise ValueError("chunks must contain at least one DataFrame") from exc
161
+
162
+ first, dshape = _prepare_chunk(first, glyph, reduction)
163
+ schema = dshape.measure
164
+ glyph.validate(schema)
165
+ reduction.validate(schema)
166
+ self._canvas.validate()
167
+
168
+ if reduction.uses_row_index(cuda=False, partitioned=False):
169
+ raise NotImplementedError(
170
+ "row-index reductions are not supported by the streaming points MVP"
171
+ )
172
+
173
+ (
174
+ create,
175
+ info,
176
+ append,
177
+ _combine,
178
+ finalize,
179
+ antialias_stage_2,
180
+ antialias_stage_2_funcs,
181
+ _column_names,
182
+ ) = compile_components(
183
+ reduction,
184
+ schema,
185
+ glyph,
186
+ antialias=False,
187
+ cuda=False,
188
+ partitioned=False,
189
+ )
190
+
191
+ extend = glyph._build_extend( # noqa: SLF001 - Datashader extension API
192
+ self._canvas.x_axis.mapper,
193
+ self._canvas.y_axis.mapper,
194
+ info,
195
+ append,
196
+ antialias_stage_2,
197
+ antialias_stage_2_funcs,
198
+ )
199
+
200
+ x_range = self._canvas.x_range
201
+ y_range = self._canvas.y_range
202
+ assert x_range is not None and y_range is not None
203
+ self._canvas.validate_ranges(x_range, y_range)
204
+
205
+ width = self._canvas.plot_width
206
+ height = self._canvas.plot_height
207
+ x_st = self._canvas.x_axis.compute_scale_and_translate(x_range, width)
208
+ y_st = self._canvas.y_axis.compute_scale_and_translate(y_range, height)
209
+ transform = x_st + y_st
210
+ bounds = x_range + y_range
211
+ bases = create((height, width))
212
+
213
+ processed_chunks = 0
214
+ processed_rows = 0
215
+
216
+ for chunk in _prepend(first, iterator):
217
+ if cancelled is not None and cancelled():
218
+ raise RuntimeError("aggregation cancelled")
219
+ prepared, chunk_dshape = _prepare_chunk(chunk, glyph, reduction)
220
+ if chunk_dshape.measure != schema:
221
+ raise TypeError(
222
+ "all chunks must have matching Datashader schemas; "
223
+ f"expected {schema}, received {chunk_dshape.measure}"
224
+ )
225
+ extend(bases, prepared, transform, bounds)
226
+ processed_chunks += 1
227
+ processed_rows += len(prepared)
228
+ if on_progress is not None:
229
+ on_progress(Progress(processed_chunks, processed_rows))
230
+
231
+ x_axis = self._canvas.x_axis.compute_index(x_st, width)
232
+ y_axis = self._canvas.y_axis.compute_index(y_st, height)
233
+ return finalize(
234
+ bases,
235
+ cuda=False,
236
+ coords={glyph.x_label: x_axis, glyph.y_label: y_axis},
237
+ dims=[glyph.y_label, glyph.x_label],
238
+ attrs={
239
+ "x_range": x_range,
240
+ "y_range": y_range,
241
+ "source_chunks": processed_chunks,
242
+ "source_rows": processed_rows,
243
+ },
244
+ )
245
+
246
+
247
+ def _prepare_chunk(chunk: Any, glyph: Point, reduction: Any):
248
+ if not isinstance(chunk, pd.DataFrame):
249
+ to_pandas = getattr(chunk, "to_pandas", None)
250
+ if to_pandas is None or not callable(to_pandas):
251
+ raise TypeError(
252
+ "each chunk must be a pandas DataFrame or Arrow-compatible "
253
+ f"record batch, received {type(chunk)!r}"
254
+ )
255
+ # PyArrow's direct Arrow-to-Pandas bridge can stall in the current
256
+ # Emscripten runtime. Building the bounded DataFrame from Python
257
+ # column values avoids that bridge while preserving the batch limit.
258
+ if sys.platform == "emscripten" and callable(getattr(chunk, "to_pydict", None)):
259
+ chunk = pd.DataFrame(chunk.to_pydict())
260
+ else:
261
+ chunk = to_pandas()
262
+ if not isinstance(chunk, pd.DataFrame):
263
+ raise TypeError(
264
+ "chunk.to_pandas() must return a pandas DataFrame, received "
265
+ f"{type(chunk)!r}"
266
+ )
267
+ return _bypixel_sanitise(chunk, glyph, reduction)
268
+
269
+
270
+ def _prepend(first: Any, rest: Iterator[Any]) -> Iterator[Any]:
271
+ yield first
272
+ yield from rest
@@ -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
+ ![Datashader rendering of 11.8 million January 2015 NYC taxi pickups](docs/assets/nyc-taxi-pickups.png)
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,6 @@
1
+ datashader_browser/__init__.py,sha256=2xa8RV0wxXtsyGjW0gJI-dGI4mQvcsKfGDR4yOR0oY0,296
2
+ datashader_browser/streaming.py,sha256=0Zy2C7798MGpFHNeA_sdoikL8m9pZdKKrlrPAOlCAaQ,9070
3
+ datashader_browser-0.1.0a2.dist-info/METADATA,sha256=dl4huRF2j2d5KlRLCQxsCrF-_RVZDDvI50ZorTaShz0,6648
4
+ datashader_browser-0.1.0a2.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
5
+ datashader_browser-0.1.0a2.dist-info/licenses/LICENSE,sha256=hTBVAxwAZaXkBLTae5MjdcULGYL7gclAjqJcCs0lHmE,1500
6
+ datashader_browser-0.1.0a2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.