posit-dev / py-shinywidgets

Render ipywidgets inside a PyShiny app
MIT License
46 stars 5 forks source link

`.observe()` method doesn't re-execute on trait change #70

Closed mfesan36 closed 1 year ago

mfesan36 commented 1 year ago

I cannot find a way to force pyshiny capture interactions that happen on the plot. To be more clear, when I select a portion of plot by interval selector (see code below) the observe method gets ignored. If you click on top chart an interval selector will appear, you can adjust the width by moving your mouse and fix the interval by a double click:

import numpy as np
import bqplot.pyplot as plt
from bqplot.interacts import FastIntervalSelector

from shiny import *
from shinywidgets import *

y1, y2 = np.random.randn(2, 200).cumsum(axis=1)  # two simple random walks

app_ui = ui.page_fluid(
    ui.layout_sidebar(
        ui.panel_sidebar(
            # ui.input_action_button('calc_btn',label='Go',width='100px'),
        ),
        ui.panel_main(
            output_widget("bqplot_line"),
            output_widget("bqplot_scat"),
            ui.output_text_verbatim('txt'),
        ),
    ),
    title="Interval Selector Test",
)

def server(input: Inputs, output: Outputs, session: Session):

    selected_date_range = reactive.Value(('1900','1900'))

    time_series_fig = plt.figure()
    line = plt.plot([y1, y2])

    scat_fig = plt.figure(  title="Scatter of time series slice selected by the interval selector")
    # set the x and y attributes to the y values of line.y
    scat = plt.scatter(*line.y, colors=["red"], stroke="black")

    # define a callback for the interval selector
    def update_scatter(*args):
        # get the start and end indices of the interval
        start_ix, end_ix = line.selected[0], line.selected[-1]
        start_ix, end_ix = reactive_read(line,'selected')
        selected_date_range.set((start_ix, end_ix))
        # update the x and y attributes of the scatter by slicing line.y
        with scat.hold_sync():
            scat.x, scat.y = line.y[:, start_ix:end_ix]

    # register the callback with line.selected trait
    line.observe(update_scatter, "selected")

    # create a fast interval selector by passing in the X scale and the line mark on which the selector operates
    intsel = FastIntervalSelector(marks=[line], scale=line.scales["x"])
    time_series_fig.interaction = intsel  # set the interval selector on the figure

    # register_widget("intsel", intsel)
    register_widget("bqplot_line", time_series_fig)
    register_widget("bqplot_scat", scat_fig)

    @output
    @render.text
    def txt():
        return f'{selected_date_range()}'

app = App(app_ui, server,debug=True)
cpsievert commented 1 year ago

I cannot find a way to force pyshiny capture interactions that happen on the plot. To be more clear, when I select a portion of plot by interval selector (see code below) the observe method gets ignored.

At least currently, the .observe() method on widget objects don't work the way you might expect them to in shinywidgets (i.e., .observe() callback won't re-execute when widget attributes change).

However, by combining Shiny's @reactive.Effect() with shinywidgets reactive_read() you can effectively "observe" (i.e., re-execute a callback function) whenever particular traits (and/or reactive values) change.

Seems as though this is the sort of server-side logic you're looking for?

def server(input: Inputs, output: Outputs, session: Session):

    time_series_fig = plt.figure()
    line = plt.plot([y1, y2])

    time_series_fig.interaction = FastIntervalSelector(marks=[line], scale=line.scales["x"])
    register_widget("bqplot_line", time_series_fig)

    scat_fig = plt.figure(
        title="Scatter of time series slice selected by the interval selector"
    )
    register_widget("bqplot_scat", scat_fig)

    scat = plt.scatter([], [], colors=["red"], stroke="black")

    @reactive.Effect()
    def _():
        selected = reactive_read(time_series_fig.interaction, "selected")
        if selected is None:
            return

        idx = np.arange(int(selected[0]), int(selected[1]))
        with scat.hold_sync():
            scat.x = line.y[0][idx]
            scat.y = line.y[1][idx]