marimo
marimo copied to clipboard
Anywidget Observation: Trigger Cell on Specific Traitlet Changes only
Description
In my Jupyter notebook (left), I can observe changes to the blue_value traitlet and trigger actions accordingly. In my Marimo notebook (right), I want to replicate this behavior so that only changes to blue_value trigger the next cell execution, while changes to orange_value do not. After discussing this with @mscolnick on Discord, it seems Marimo currently does not have the fine-grained control needed for this behavior, so I’m opening this issue.
Suggested solution
a cell with print(w.blue_value) will not be triggered by changing w.orange_value
Alternative
No response
Additional context
Marimo code:
import marimo
__generated_with = "0.9.25"
app = marimo.App(width="medium")
@app.cell
def __():
import anywidget
import traitlets
import marimo as mo
class CounterWidget(anywidget.AnyWidget):
_esm = """
function render({ model, el }) {
const createButton = (label, color, valueKey) => {
let button = document.createElement("button");
button.innerHTML = `${label} is ${model.get(valueKey)}`;
Object.assign(button.style, { backgroundColor: color, color: "white", fontSize: "1.75rem", padding: "0.5rem 1rem", border: "none", borderRadius: "0.25rem", margin: "0.5rem" });
button.addEventListener("click", () => {
model.set(valueKey, model.get(valueKey) + 1);
model.save_changes();
});
model.on(`change:${valueKey}`, () => {
button.innerHTML = `${label} is ${model.get(valueKey)}`;
});
return button;
};
let container = document.createElement("div");
container.appendChild(createButton("Orange", "#ea580c", "orange_value"));
container.appendChild(createButton("Blue", "#2563eb", "blue_value"));
el.appendChild(container);
}
export default { render };
"""
orange_value = traitlets.Int(0).tag(sync=True)
blue_value = traitlets.Int(0).tag(sync=True)
return CounterWidget, anywidget, mo, traitlets
@app.cell
def __(CounterWidget, mo):
w =mo.ui.anywidget(CounterWidget(orange_value=42, blue_value=17))
w
return (w,)
@app.cell
def __(w):
import time
print(time.time())
print(w.blue_value)
return (time,)
@app.cell
def __():
return
if __name__ == "__main__":
app.run()
jupyter code:
import anywidget
import traitlets
class CounterWidget(anywidget.AnyWidget):
_esm = """
function render({ model, el }) {
const createButton = (label, color, valueKey) => {
let button = document.createElement("button");
button.innerHTML = `${label} is ${model.get(valueKey)}`;
Object.assign(button.style, { backgroundColor: color, color: "white", fontSize: "1.75rem", padding: "0.5rem 1rem", border: "none", borderRadius: "0.25rem", margin: "0.5rem" });
button.addEventListener("click", () => {
model.set(valueKey, model.get(valueKey) + 1);
model.save_changes();
});
model.on(`change:${valueKey}`, () => {
button.innerHTML = `${label} is ${model.get(valueKey)}`;
});
return button;
};
let container = document.createElement("div");
container.appendChild(createButton("Orange", "#ea580c", "orange_value"));
container.appendChild(createButton("Blue", "#2563eb", "blue_value"));
el.appendChild(container);
}
export default { render };
"""
orange_value = traitlets.Int(0).tag(sync=True)
blue_value = traitlets.Int(0).tag(sync=True)
w = CounterWidget(orange_value=42, blue_value=17)
w
from ipywidgets import widgets
from IPython.display import display
import time
output = widgets.Output()
def on_color_change(change):
output.clear_output(wait=True)
with output:
print(time.time())
print(w.blue_value)
w.observe(on_color_change, names='blue_value')
display(output)
Thanks for filing the detailed issue and the example! We definitely want to add fine-grained reactivity to anywidget, and haven't gotten to it yet.
As a workaround, you maybe be able to create state mo.state() in 2 separate cells. and also subscribe in another separate cell.
get_orange, set_orange = mo.state()
get_blue, set_blue = mo.state()
w =mo.ui.anywidget(CounterWidget(orange_value=42, blue_value=17))
# might need this in another cell
w.observe(set_orange, names='orange_value')
w.observe(set_blue, names='blue_value')
Then all references of get_blue and get_orange should be granular. (haven't tested this myself yet)
thanks a lot, here's the result of my testing:
am I using the state as it's supposed to be used here?
That looks mostly right. Can you try putting observe call in the same cell as the widget
thanks! That works!
Two notes:
What do I put as the initial state?
because when I put mo.state(0), I'll get this error in the observe cell on the first call
and second question:
Is there an indicator to see what cells did run again?
currently I'm using
print(time.time())
to see if the cell was run again, but maybe there is a better way.
What do I put as the initial state?
you can put whatever you want to initialize the state, but putting mo.state(0) means that get_state() the first iteration will return 0, so 0.new is not a thing. you can return None and check that for None or check that the response hasattr(get_state(), 'new').
Is there an indicator to see what cells did run again?
There is a status indicator on the right side of the cell for how long it took to run. If you hover over it, you can see when it was last run. We plan to later add a timeline/flame graph of previous cell runs as a sidebar helper.
you can return None and check that for None
That works!
Code Example
import time
w =mo.ui.anywidget(CounterWidget(orange_value=42, blue_value=17))
get_blue, set_blue = mo.state(None)
w.observe(set_blue, names='blue_value')
print(time.time())
w
current_blue = get_blue()
if current_blue is None:
print("Blue state is None (initial state)")
else:
print(f"New blue state is {current_blue.new}")
check that the response
hasattr(get_state(), 'new')
that works as well!
Code Example
import time
w =mo.ui.anywidget(CounterWidget(orange_value=42, blue_value=17))
get_blue, set_blue = mo.state(0)
w.observe(set_blue, names='blue_value')
print(time.time())
w
current_blue = get_blue()
if not hasattr(current_blue, 'new'):
print("Blue has no new state yet")
else:
print(f"New blue state is {current_blue.new}")