flexx icon indicating copy to clipboard operation
flexx copied to clipboard

Question: Update Bokeh plots from PyComponent

Open juliusbierk opened this issue 5 years ago • 4 comments

Hi,

I am new to flexx, but really like it. In the bokeh-example, a line plot is updated using JS code. However, one of the strenghts of bokeh is that we can do numpy calculations and update the plots dynamically from Python. Is this possible in flexx?

For instance, this does not work:

from flexx import flx
import numpy as np
from bokeh.plotting import figure
from flexx import app, ui
from bokeh.models import ColumnDataSource

x = np.linspace(0, 6, 50)

data={'x': x, 'y': np.sin(x)}

source = ColumnDataSource(data)

p1 = figure(tools="pan,wheel_zoom,box_zoom,reset")
p1.toolbar.logo = None
p1.line(x='x', y='y', source=source)
p1.sizing_mode = 'scale_height'


class Example(app.PyComponent):
    def init(self):
        with ui.VSplit():
            self.plot = ui.BokehWidget.from_plot(p1)
            self.freq = flx.Slider(title='Frequency', max=2.0, value=1.0)

    @flx.reaction
    def update_plot(self):
        new_dict = {'x': x, 'y': np.sin(self.freq.value * x)}
        source.data = new_dict


if __name__ == '__main__':
    app = flx.App(Example)
    m = app.launch('chrome-browser')
    flx.run()

Even though source.data is updated successfully, the plot does not update as it would using just bokeh. Is there any simple way to get around this?

juliusbierk avatar Jan 11 '21 13:01 juliusbierk

Alternatively, I guess I would like to know if there is a way to replace the entire BokehWidget with a new BokehWidget? I guess this would be quite expensive, but would be an easy way to update the plot.

juliusbierk avatar Jan 11 '21 14:01 juliusbierk

Ah!, I found a hackish way to do it

from flexx import flx
import numpy as np
from bokeh.plotting import figure
from flexx import app, ui
from bokeh.models import ColumnDataSource
from bokeh.embed import components

x = np.linspace(0, 6, 50)

data={'x': x, 'y': np.sin(x)}

source = ColumnDataSource(data)

p1 = figure(tools="pan,wheel_zoom,box_zoom,reset")
p1.toolbar.logo = None
p1.line(x='x', y='y', source=source)
p1.sizing_mode = 'scale_height'


class Example(app.PyComponent):
    def init(self):
        with ui.VSplit():
            self.plot = ui.BokehWidget.from_plot(p1)
            self.freq = flx.Slider(title='Frequency', max=2.0, value=1.0)

    @flx.reaction
    def update_plot(self):
        new_dict = {'x': x, 'y': np.sin(self.freq.value * x)}
        source.data = new_dict

        script, div = components(p1)
        script = '\n'.join(script.strip().split('\n')[1:-1])
        self.plot.set_plot_components(
            dict(script=script, div=div, id=self.plot.id))


if __name__ == '__main__':
    app = flx.App(Example)
    m = app.launch('chrome-browser')  # for use during development
    flx.run()

juliusbierk avatar Jan 11 '21 14:01 juliusbierk

Yeah, I'm not sure if there is a way for Flexx to detect changes to source, but it's not done at the moment. You need to update the plot via Flexx' system. It could be worth considering adding an action to the BokehWidget to update the data...

almarklein avatar Jan 18 '21 10:01 almarklein

You can create a separate JS component that updates the data source of the plot.

class BokehUpdater(flx.JsComponent):
    @flx.action
    def update_plot(self, bw, x_data, y_data):
        print(bw.plot)
        for ren in bw.plot.model.renderers.values():
            if ren.data_source:
                ds = ren.data_source
                break
        if ds:
            ds.data.x= x_data
            ds.dasta.y = y_data

            ds.change.emit()

ds.data is your datasource Then from the Python side you can call:

self.bokeh_updater.update_plot(self.plot,x_data)

For your example above

class Example(app.PyComponent):
    def init(self):
        with ui.VSplit():
            self.plot = ui.BokehWidget.from_plot(p1)
            self.freq = flx.Slider(title='Frequency', max=2.0, value=1.0)
            self.bokeh_updater = BokehUpdater()

    @flx.reaction
    def update_plot(self):
        new_dict = {'x': x, 'y': np.sin(self.freq.value * x)}
        self.bokeh_updater.update_plot(self.plot,x, list(np.sin(self.freq.value * x)))

Hope this helps

amitdeliwala avatar Nov 05 '21 21:11 amitdeliwala