Skip to content

Latest commit

 

History

History
282 lines (233 loc) · 8.91 KB

File metadata and controls

282 lines (233 loc) · 8.91 KB

pywidget examples

Static HTML (simplest)

class HelloWidget(PyWidget):
    def render(self, el, model):
        el.innerHTML = """
        <style>
            .hello-container {
                padding: 20px; border-radius: 12px;
                font-family: system-ui, sans-serif;
                background: #f0f4ff; color: #1a1a2e;
            }
            @media (prefers-color-scheme: dark) {
                .hello-container { background: #1a1a2e; color: #e0e4ff; }
            }
        </style>
        <div class="hello-container">
            <h2 style="margin:0 0 8px 0;">Hello from Pyodide!</h2>
            <p style="margin:0; opacity:0.75;">Rendered by Python in WebAssembly.</p>
        </div>
        """

Kernel → browser (read-only display with update)

class GreeterWidget(PyWidget):
    name = traitlets.Unicode("World").tag(sync=True)

    def render(self, el, model):
        name = model.get("name")
        el.innerHTML = f'<span style="font-size:24px; font-family:system-ui, sans-serif;">Hello, <strong>{name}</strong>!</span>'

    def update(self, el, model):
        render(el, model)

# Usage: GreeterWidget(name="Alice")

Browser → kernel (bidirectional counter)

class CounterWidget(PyWidget):
    count = traitlets.Int(0).tag(sync=True)

    def render(self, el, model):
        count = model.get("count")
        el.innerHTML = f"""
        <div style="font-family:system-ui, sans-serif; display:flex; align-items:center; gap:12px;">
            <button id="dec" style="padding:8px 16px; font-size:18px; cursor:pointer;
                border:1px solid currentColor; background:transparent; color:inherit;
                border-radius:6px;">−</button>
            <span id="display" style="font-size:24px; min-width:60px; text-align:center;">{count}</span>
            <button id="inc" style="padding:8px 16px; font-size:18px; cursor:pointer;
                border:1px solid currentColor; background:transparent; color:inherit;
                border-radius:6px;">+</button>
        </div>
        """
        def on_inc(event):
            new_val = model.get("count") + 1
            model.set("count", new_val)
            model.save_changes()
            el.querySelector("#display").textContent = str(new_val)

        def on_dec(event):
            new_val = model.get("count") - 1
            model.set("count", new_val)
            model.save_changes()
            el.querySelector("#display").textContent = str(new_val)

        el.querySelector("#inc").addEventListener("click", create_proxy(on_inc))
        el.querySelector("#dec").addEventListener("click", create_proxy(on_dec))

    def update(self, el, model):
        display = el.querySelector("#display")
        if display:
            display.textContent = str(model.get("count"))

NumPy computation (browser-side packages)

class StatsWidget(PyWidget):
    data = traitlets.List(traitlets.Float(), []).tag(sync=True)
    _py_packages = ["numpy"]

    def render(self, el, model):
        import numpy as np
        arr = np.array(list(model.get("data")))
        if arr.size == 0:
            el.innerHTML = '<p style="opacity:0.5;">No data.</p>'
            return
        el.innerHTML = f"""
        <table style="font-family:monospace; border-collapse:collapse;">
            <tr><td style="padding:4px 12px;">Mean:</td><td>{arr.mean():.4f}</td></tr>
            <tr><td style="padding:4px 12px;">Std:</td><td>{arr.std():.4f}</td></tr>
            <tr><td style="padding:4px 12px;">Min:</td><td>{arr.min():.4f}</td></tr>
            <tr><td style="padding:4px 12px;">Max:</td><td>{arr.max():.4f}</td></tr>
        </table>
        """

    def update(self, el, model):
        render(el, model)

Complex data via JSON (Pandas)

class SalesTable(PyWidget):
    sales_json = traitlets.Unicode("[]").tag(sync=True)
    _py_packages = ["pandas"]

    def render(self, el, model):
        import json
        import pandas as pd

        rows = json.loads(model.get("sales_json"))
        if not rows:
            el.innerHTML = '<p style="opacity:0.5;">No data.</p>'
            return
        df = pd.DataFrame(rows)
        summary = (
            df.groupby("region")
            .agg(total=("revenue", "sum"), avg=("revenue", "mean"), n=("revenue", "count"))
            .round(2).reset_index()
        )
        el.innerHTML = f"""
        <div style="font-family:system-ui, sans-serif; padding:16px;">
            <h3 style="margin:0 0 12px 0;">Sales Summary</h3>
            {summary.to_html(index=False, border=0)}
            <p style="opacity:0.5; font-size:13px; margin-top:8px;">{len(df)} rows aggregated</p>
        </div>
        """

    def update(self, el, model):
        render(el, model)

Image rendering (Pillow + base64)

class FractalWidget(PyWidget):
    center_x = traitlets.Float(-0.5).tag(sync=True)
    center_y = traitlets.Float(0.0).tag(sync=True)
    zoom = traitlets.Float(1.0).tag(sync=True)
    _py_packages = ["numpy", "Pillow"]

    def render(self, el, model):
        import base64, io
        import numpy as np
        from PIL import Image

        def compute(cx, cy, z, w=400, h=400, max_iter=100):
            r = 3.0 / z
            real = np.linspace(cx - r/2, cx + r/2, w)
            imag = np.linspace(cy - r/2, cy + r/2, h)
            c = real[np.newaxis,:] + 1j * imag[:,np.newaxis]
            z_arr = np.zeros_like(c)
            iters = np.zeros(c.shape, dtype=np.int32)
            mask = np.ones(c.shape, dtype=bool)
            for i in range(max_iter):
                z_arr[mask] = z_arr[mask]**2 + c[mask]
                escaped = mask & (np.abs(z_arr) > 2.0)
                iters[escaped] = i + 1
                mask &= ~escaped
            return iters

        cx, cy, z = model.get("center_x"), model.get("center_y"), model.get("zoom")
        iters = compute(cx, cy, z)
        norm = iters.astype(np.float64) / 100
        rgb = np.stack([
            (np.sin(norm*np.pi*2)*127+128).astype(np.uint8),
            (np.sin(norm*np.pi*2+2.094)*127+128).astype(np.uint8),
            (np.sin(norm*np.pi*2+4.189)*127+128).astype(np.uint8),
        ], axis=-1)
        rgb[iters == 0] = 0

        buf = io.BytesIO()
        Image.fromarray(rgb, "RGB").save(buf, format="PNG")
        b64 = base64.b64encode(buf.getvalue()).decode("ascii")

        el.innerHTML = f'<img src="data:image/png;base64,{b64}" style="cursor:crosshair;" />'

        def on_click(event):
            r = 3.0 / z
            new_cx = (cx - r/2) + (event.offsetX / 400) * r
            new_cy = (cy - r/2) + (event.offsetY / 400) * r
            model.set("center_x", float(new_cx))
            model.set("center_y", float(new_cy))
            model.save_changes()

        el.querySelector("img").addEventListener("click", create_proxy(on_click))

    def update(self, el, model):
        render(el, model)

Slider input (HTML range element)

class SliderWidget(PyWidget):
    value = traitlets.Float(50.0).tag(sync=True)

    def render(self, el, model):
        val = model.get("value")
        el.innerHTML = f"""
        <div style="font-family:system-ui, sans-serif; padding:16px;">
            <input id="slider" type="range" min="0" max="100" step="1"
                   value="{val}" style="width:200px;" />
            <span id="label">{val:.0f}</span>
        </div>
        """
        def on_input(event):
            new_val = float(event.target.value)
            el.querySelector("#label").textContent = f"{new_val:.0f}"
            model.set("value", new_val)
            model.save_changes()

        el.querySelector("#slider").addEventListener("input", create_proxy(on_input))

    def update(self, el, model):
        val = model.get("value")
        slider = el.querySelector("#slider")
        if slider:
            slider.value = str(val)
            el.querySelector("#label").textContent = f"{val:.0f}"

Jupyter usage

import traitlets
from pywidget import PyWidget

class MyWidget(PyWidget):
    value = traitlets.Int(0).tag(sync=True)
    def render(self, el, model):
        # ... render logic ...
        pass
    def update(self, el, model):
        render(el, model)

# Display directly — works in Jupyter Lab, Notebook, and Colab
MyWidget(value=42)

marimo usage

import marimo as mo
import traitlets
from pywidget import PyWidget

class MyWidget(PyWidget):
    value = traitlets.Int(0).tag(sync=True)
    def render(self, el, model):
        # ... render logic ...
        pass
    def update(self, el, model):
        render(el, model)

widget = mo.ui.anywidget(MyWidget(value=42))
widget  # display in cell

# In another cell, read state reactively:
# widget.value["value"]

String-based alternative

For simple widgets or dynamically generated code, set _py_render directly. Method extraction is skipped when this attribute is set.

class SimpleWidget(PyWidget):
    _py_render = """
def render(el, model):
    el.innerHTML = "<h2>Hello from Pyodide!</h2>"
"""