Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ wasm-pack publish
### Build for Python

```sh
uv sync --extra dev
uv sync --extra dev --extra widget
```

Build:
Expand Down
144 changes: 144 additions & 0 deletions bindings-python/notebooks/scatterplot_remote_widget.mo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import marimo

__generated_with = "0.18.4"
app = marimo.App(width="medium")


@app.cell
def _():
from pluot import render_to_image, render_to_svg
import numpy as np
import marimo as mo
import json
import zarr
return mo, zarr


@app.cell
def _():
from pluot import PluotWasmWidget
return (PluotWasmWidget,)


@app.cell
def _(PluotWasmWidget):
widget = PluotWasmWidget(
width=400,
height=400,
plot_id="my_plot",
plot_type="LayeredPlot",
store_name="my_store",
plot_params={
"layers": [
{
"layer_type": "PointLayer",
"layer_params": {
"layer_id": "points",
"data_unit_mode_x": "Data",
"data_unit_mode_y": "Data",
"point_radius_unit_mode_x": "Pixels",
"point_radius_unit_mode_y": "Pixels",
"point_shape_mode": "Circle",
"point_radius": 5.0,
"bounds": None,
"position_x": [0.1, 0.3, 0.5, 0.7, 0.9],
"position_y": [0.5, 0.2, 0.8, 0.3, 0.6],
"labels_vec": [0, 1, 2, 3, 4],
},
},
],
},
margin_top=20,
margin_right=20,
margin_bottom=40,
margin_left=40,
)
widget # display in a Jupyter cell
return


@app.cell
def _():
from obstore.store import HTTPStore
return (HTTPStore,)


@app.cell
def _(HTTPStore, zarr):
obs_store = HTTPStore.from_url("https://pub-adb3658c8ed642caa534fdc612cd1c0c.r2.dev/gaussian_quantiles.zarr")
store = zarr.storage.ObjectStore(obs_store, read_only=True)
return (store,)


@app.cell
def _(store, zarr):
arr = zarr.open_array(store=store, mode='r', path="/n_1000000/x_coords")
arr.shape
return


@app.cell
def _():
camera_view = [
0.15, 0.0, 0.0, 0.0,
0.0, 0.15, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0,
]
return (camera_view,)


@app.cell
def _(PluotWasmWidget, camera_view, point_radius_slider, store):
widget2 = PluotWasmWidget(
camera_matrix=camera_view, width=600, height=600, plot_id="test_store_instance", plot_type="LayeredPlot",
margin_left=100,
margin_bottom=100,
store=store,
plot_params=dict(
layers=[
dict(
layer_type="ZarrPointLayer",
layer_params=dict(
layer_id="zarr_layer",
data_unit_mode_x="Data",
data_unit_mode_y="Data",
point_radius_unit_mode_x="Pixels",
point_radius_unit_mode_y="Pixels",
point_shape_mode="Circle",
x_key="/n_1000000/x_coords",
y_key="/n_1000000/y_coords",
color_key="/n_1000000/class_labels",
point_radius=point_radius_slider.value,
)
),
dict(
layer_type="AxisLinearLayer",
layer_params=dict(
layer_id="left_axis",
position="Left"
)
),
dict(
layer_type="AxisLinearLayer",
layer_params=dict(
layer_id="bottom_axis",
position="Bottom"
)
)
]
),
)
widget2
return


@app.cell
def _(mo):
point_radius_slider = mo.ui.slider(start=1.0, stop=20.0, value=10.0)
point_radius_slider
return (point_radius_slider,)


if __name__ == "__main__":
app.run()
36 changes: 36 additions & 0 deletions bindings-python/pluot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,43 @@
from typing import TYPE_CHECKING

from .render import render, render_to_array, render_to_image, render_to_svg
from .log import get_logger
import logging
# TODO: remove this, leave to user to configure.
logger = get_logger()
logger.setLevel(logging.ERROR)

if TYPE_CHECKING:
from .widget_py import PluotPyWidget # noqa: F401
from .widget_wasm import PluotWasmWidget # noqa: F401

__all__ = [
"render",
"render_to_array",
"render_to_image",
"render_to_svg",
"PluotPyWidget",
"PluotWasmWidget",
]


def __getattr__(name: str):
if name == "PluotPyWidget":
try:
from .widget_py import PluotPyWidget
except ImportError as e:
raise ImportError(
"PluotPyWidget requires the 'widget' extra. "
"Install with: pip install pluot[widget]"
) from e
return PluotPyWidget
if name == "PluotWasmWidget":
try:
from .widget_wasm import PluotWasmWidget
except ImportError as e:
raise ImportError(
"PluotWasmWidget requires the 'widget' extra. "
"Install with: pip install pluot[widget]"
) from e
return PluotWasmWidget
raise AttributeError(f"module 'pluot' has no attribute {name!r}")
14 changes: 12 additions & 2 deletions bindings-python/pluot/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,27 @@ def parse_kwargs(kwargs):
new_kwargs["plot_params"] = replace_arr_with_key(kwargs["plot_params"], GLOBAL_STORES[memory_store_name])
return new_kwargs

_RENDER_DEFAULTS = dict(timeout=None, wait_for_store_gets=True, cache_enabled=True, device_pixel_ratio=1.0, format="Raster", aspect_ratio_mode="Contain", aspect_ratio_alignment_mode="Center", view_mode="2d", pickable=False, svg_compression_enabled=False, svg_include_document=True)

async def render(**kwargs):
"""Render to raw bytes."""
# We wrap the internal function here to be able to provide types, docstrings, etc.
new_kwargs = parse_kwargs(kwargs)

merged_params = dict(timeout=None, wait_for_store_gets=True, cache_enabled=True, device_pixel_ratio=1.0, format="Raster", aspect_ratio_mode="Contain", aspect_ratio_alignment_mode="Center", view_mode="2d", pickable=False, svg_compression_enabled=False, svg_include_document=True)
merged_params.update(new_kwargs)
merged_params = {**_RENDER_DEFAULTS, **new_kwargs}

result = await render_py(**merged_params)
return result

async def render_raw(**kwargs):
"""Render to raw bytes, bypassing parse_kwargs.

The caller is responsible for ensuring ``store_name`` is already registered
in ``GLOBAL_STORES`` before calling this function.
"""
merged_params = {**_RENDER_DEFAULTS, **kwargs}
return await render_py(**merged_params)

async def render_to_array(**kwargs):
"""Render to a NumPy array, with shape (height, width, RGBA)."""
width = kwargs["width"]
Expand Down
68 changes: 68 additions & 0 deletions bindings-python/pluot/sync_store.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Copied from zarr-python
# Reference: https://github.com/zarr-developers/zarr-python/blob/fe229107f9915f05817f7a664d3550695ff9ca44/src/zarr/testing/stateful.py#L438

import builtins
from typing import Any
import zarr
from zarr.abc.store import Store
from zarr.core.buffer import Buffer, BufferPrototype


class SyncStoreWrapper(zarr.core.sync.SyncMixin):
def __init__(self, store: Store) -> None:
"""Synchronous Store wrapper

This class holds synchronous methods that map to async methods of Store classes.
The synchronous wrapper is needed because hypothesis' stateful testing infra does
not support asyncio so we redefine sync versions of the Store API.
https://github.com/HypothesisWorks/hypothesis/issues/3712#issuecomment-1668999041
"""
self.store = store

@property
def read_only(self) -> bool:
return self.store.read_only

def set(self, key: str, data_buffer: Buffer) -> None:
return self._sync(self.store.set(key, data_buffer))

def list(self) -> builtins.list[str]:
return self._sync_iter(self.store.list())

def get(self, key: str, prototype: BufferPrototype, **kwargs) -> Buffer | None:
return self._sync(self.store.get(key, prototype=prototype, **kwargs))

def get_partial_values(
self, key_ranges: builtins.list[Any], prototype: BufferPrototype
) -> builtins.list[Buffer | None]:
return self._sync(self.store.get_partial_values(prototype=prototype, key_ranges=key_ranges))

def delete(self, path: str) -> None:
return self._sync(self.store.delete(path))

def is_empty(self, prefix: str) -> bool:
return self._sync(self.store.is_empty(prefix=prefix))

def clear(self) -> None:
return self._sync(self.store.clear())

def exists(self, key: str) -> bool:
return self._sync(self.store.exists(key))

def list_dir(self, prefix: str) -> None:
raise NotImplementedError

def list_prefix(self, prefix: str) -> None:
raise NotImplementedError

@property
def supports_listing(self) -> bool:
return self.store.supports_listing

@property
def supports_writes(self) -> bool:
return self.store.supports_writes

@property
def supports_deletes(self) -> bool:
return self.store.supports_deletes
Loading
Loading