marimo-team / marimo

A reactive notebook for Python — run reproducible experiments, execute as a script, deploy as an app, and version with git.
https://marimo.io
Apache License 2.0
5.31k stars 155 forks source link

Anywidget.send does not update the widget #1690

Open crazycapivara opened 3 days ago

crazycapivara commented 3 days ago

Describe the bug

Anywidget.send does not update the widget.

I tried to render maplibre widget which is based on anywidget. The initial rendering works, but if I try to update the widget which internally uses Anywidget.send method nothing happens. In th Js Developer console I can see that the custom message is not send.

Environment

{
  "marimo": "0.6.23",
  "OS": "Linux",
  "OS Version": "5.15.0-113-generic",
  "Processor": "x86_64",
  "Python Version": "3.9.19",
  "Binaries": {
    "Browser": "--",
    "Node": "v20.10.0"
  },
  "Requirements": {
    "click": "8.1.7",
    "importlib-resources": "missing",
    "jedi": "0.19.1",
    "markdown": "3.6",
    "pymdown-extensions": "10.8.1",
    "pygments": "2.17.2",
    "tomlkit": "0.12.5",
    "uvicorn": "0.29.0",
    "starlette": "0.37.2",
    "websockets": "12.0",
    "typing-extensions": "4.11.0",
    "black": "24.4.2"
  }
}

Code to reproduce

import marimo

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

@app.cell
def __():
    import marimo as mo

    return (mo,)

@app.cell
def __():
    from maplibre.controls import NavigationControl, ScaleControl
    from maplibre.ipywidget import MapOptions, MapWidget

    return MapOptions, MapWidget, NavigationControl, ScaleControl

@app.cell
def __():
    deck_grid_layer = {
        "@@type": "GridLayer",
        "id": "GridLayer",
        "data": "https://raw.githubusercontent.com/visgl/deck.gl-data/master/website/sf-bike-parking.json",
        "extruded": True,
        "getPosition": "@@=COORDINATES",
        "getColorWeight": "@@=SPACES",
        "getElevationWeight": "@@=SPACES",
        "elevationScale": 4,
        "cellSize": 200,
        "pickable": True,
    }
    return (deck_grid_layer,)

@app.cell
def __(MapOptions):
    map_options = MapOptions(
        center=(-122.4, 37.74),
        zoom=12,
        hash=True,
        pitch=40,
    )
    return (map_options,)

@app.cell
def __(MapWidget, NavigationControl, deck_grid_layer, map_options):
    m = MapWidget(map_options)
    m.use_message_queue(False)
    m.add_control(NavigationControl())
    m.add_deck_layers([deck_grid_layer])
    m
    return (m,)

@app.cell
def __(m):
    m.clicked
    return

@app.cell
def __(m):
    m.zoom
    return

@app.cell
def __(m):
    m.center
    return

@app.cell
def __(ScaleControl, m):
    m.add_control(ScaleControl())
    return

@app.cell
def __():
    return

if __name__ == "__main__":
    app.run()
crazycapivara commented 3 days ago

Same for using marimo.ui.anywidget.

mscolnick commented 3 days ago

To bring anywidget into marimo's reactive system, you'll need to wrap it in mo.ui.anywidget

m = MapWidget(map_options)
m.use_message_queue(False)
m.add_control(NavigationControl())
m.add_deck_layers([deck_grid_layer])
map = mo.ui.anywidget(m)
map

In order for the downstream cell's to re-render based off the reactive changes from w, you'll need to pick the values off that instead of m.

map.value["clicked"]

or you can force the dependency between cells by doing something like:

map.widget.clicked

We can look into making anywidget more tightly integrated without having to wrap marimo.ui.anywidget

crazycapivara commented 3 days ago

Hi, yes I know and I first did it this way and w.value works fine but I need it the other way round. I need to update the widget itself from Python, e. g. to update layer properties after it was initially rendered. In the example I provided I wanted to add a ScaleControl after the widget was rendered. This is done via Anywidget.send method.

See sending custom messages.

Maybe only model.on("change:my_value") is supported?

mscolnick commented 3 days ago

Oh sorry - I read this fast and started with your re-production code, which didn't have .send - but i see now, its from add_control(ScaleControl()).

I can look into getting .send working, but this feels like mutating state which usually does not play nicely in a reactive environment.

mscolnick commented 3 days ago

I can see the value of .send in other contexts (button click or conditionals). We can look into supporting this, but as of now, we don't have any existing mechanism for python widgets to send data to their frontend counterpart after rendering (we do if they request it, as an RPC), but not a plain bi-directional connection.