Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Anywidget Observation: Trigger Cell on Specific Traitlet Changes only #2976

Open
kolibril13 opened this issue Nov 26, 2024 · 6 comments
Open
Labels
enhancement New feature or request

Comments

@kolibril13
Copy link

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.

Image

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)
@kolibril13 kolibril13 added the enhancement New feature or request label Nov 26, 2024
@mscolnick
Copy link
Contributor

mscolnick commented Nov 26, 2024

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)

@kolibril13
Copy link
Author

thanks a lot, here's the result of my testing:
Image

am I using the state as it's supposed to be used here?

@mscolnick
Copy link
Contributor

That looks mostly right. Can you try putting observe call in the same cell as the widget

@kolibril13
Copy link
Author

thanks! That works!
Image
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
Image

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.

@mscolnick
Copy link
Contributor

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.

@kolibril13
Copy link
Author

you can return None and check that for None

That works!
Image

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!

Image

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}")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants