Component for bi-directional communication with bokeh

I was working on a ML project with streamlit and I absolutely love it ! But one major drawback I found was that thereā€™s literally no possible way/workaround to have bi-directional communication with bokeh graphs. It becomes really a bottleneck when you are dealing with geo-data and you want the user to draw a polygon or something and process the selected data. I came across this issue last night and have managed to put together a component that does just that, its far from optimal solution and just a workaround but it sure does the job. I hope someone finds it useful :slight_smile:

Checkout the code example. And give the repo a star if you find it nice. :smile: GitHub - ash2shukla/streamlit-bokeh-events: A streamlit component for bi-directional communication with bokeh plots.

EDIT: changed repo link, package name to streamlit-bokeh-events, added debounce for event dispatch, added example.

12 Likes

Hey @ash2shukla, welcome to the community ! Short workaround code that does the job is my jam :llama: and you took the extra time to publish it thatā€™s awesome.

This looks great :smiley: I will definitely look into this ! Especially the bokeh.model.CustomJS part, I use a similar trick in my echarts component to pass JS code as string, and wonder about the difference between the CustomJS and my JsCode.

BTW, I think you should remove the event listeners on the componentDidUnmount just in case someone regularly recreates Bokeh Components.

Iā€™ve quickly written on doing this for Plotly.js and Iā€™d love to have a written comparison somewhere between JS events between Plotly.js and Bokeh.js to help people write their components as fast as you did.

Fanilo

1 Like

wonder about the difference between the CustomJS and my JsCode .

I think with CustomJS we can pass bokeh data sources as arguments and play with them more easily in JS code. I am not sure though. I am by no means a JS expert :slight_smile:

BTW, I think you should remove the event listeners on the componentDidUnmount just in case someone regularly recreates Bokeh Components.

Yes, definitely will do it.

Iā€™d love to have a written comparison somewhere between JS events between Plotly.js and Bokeh.js

Thatā€™s a great idea ! I will try to write a comparison between them, as I had to do some digging around myself to find this workaround, such write-up can save someoneā€™s precious time.

Thanks for your feedback @andfanilo :smiley:

1 Like

Dear @ash2shukla,

This is awesome!

Have you considered packaging it and adding it to the components library? I would love to play with this and Iā€™m sure other community members would too! Also, weā€™d love feedback on how that publishing process goes and how we could make it easier / more rewarding.

Thanks for creating this! :pray: :balloon:

4 Likes

Hey @Adrien_Treuille thanks for the feedback !
I had already packaged and published the same under name bokeh-plot-events in pypi. probably a better name is required ? Not sure how can I add it to the components library though. It will be awesome if you can share the process ?

For feed back on publishing: it was super smooth. Thanks for the awesome work you guys do ! :slight_smile:

EDIT: made changes as per instructions at Streamlit Components Launch šŸš€

What great timing! I just encountered what I think is a prime use-case for this!

I have two charts, one built in plotly and the other in bokeh. The bokeh is a scatterplot of the latest snapshot of an entire population, while the plotly is a historical view of a single constituent. Obviously Iā€™d like people to be able to select a point in bokeh and use that ID to draw the plotly chart.

I was thinking it would be easier to just rebuild the plotly chart in bokeh, but now it looks like maybe I donā€™t have to.

Well done.

Edit: Forgot to mention that I would have needed to build the bokeh chart with the full history of the entire population! Now I can load the history only as it becomes relevant.

5 Likes

Before Update:

After update:
after

You can see before the recent update, the selected points used to get highlighted and the remaining points used to get blur and this state was preserved until we select some different set of points, but after the update the selected points are highlighted only for few seconds and then the graph is getting reset (same thing happens when we zoom and select some points, it gets zoom out and no points are selected after few seconds).
Can please help me out with this @ash2shukla.

1 Like

Hey @AbhiD,

Some state magic should do the job, I whipped up some changes in the example,
The random example that I have given in repo will give weird result because the datasource will change every time the script will re-run, so just make sure you have a static data source.

Checkout this gist: https://gist.github.com/ash2shukla/bfccfdfea0061d63a6ba5ebf06df81e4
This gist has the provide_state decorator that I use: https://gist.github.com/ash2shukla/ff180d7fbe8ec3a0240f19f4452acde7
And this is the data.csv file: https://gist.github.com/ash2shukla/82ba6a264c8ede2d787a7a87a8ccbaf0

Same kind of logic can be applied to retain the zoom as well.

Hope it helps ! :slight_smile:

1 Like

Hi @ash2shukla
Can I use this for Bokeh tables as well?
I would like to get the value from a specific column in a row that the user selected.
Thanks

Hi @erezrot,

Yes you can use it with Data Tables as well starting v0.1.2, make sure you install 0.1.2 earlier versions will not work.
I have added a data table example, https://github.com/ash2shukla/streamlit-bokeh-events/blob/master/example/data_table.py, it will look like this,

Hope it helps !

Hi @ash2shukla

It worked like magic.
I really appreciate your help in resolving this problem.
Thank you

1 Like

Glad that It worked for you! Cheers!

1 Like

Hi @ash2shukla

Sorry for hijacking the thread a bit: Should it be possible to do consecutive selections with your component (preserving selection state using session_state) and also be able to reset the selection?

I have some code going, but I donā€™t think I do it the right way and results look funny, too.

Hey @cwerner,

If you dont want to change the data source each time you can set refresh_on_update=False which will make your life a lot easier as you will not have to keep track of state manually. You can just use reset tool then to reset your selection like this,

import streamlit as st
from bokeh.models import ColumnDataSource, CustomJS
from bokeh.plotting import figure
import pandas as pd
import numpy as np
from streamlit_bokeh_events import streamlit_bokeh_events

@st.cache
def data():
    df = pd.DataFrame({"x": np.random.rand(500), "y": np.random.rand(500), "size": np.random.rand(500) * 10})
    return df

df = data()
source = ColumnDataSource(df)

st.subheader("Select Points From Map")

plot = figure( tools="lasso_select,reset", width=250, height=250)
plot.circle(x="x", y="y", size="size", source=source, alpha=0.6)

source.selected.js_on_change(
    "indices",
    CustomJS(
        args=dict(source=source),
        code="""
        document.dispatchEvent(
            new CustomEvent("TestSelectEvent", {detail: {indices: cb_obj.indices}})
        )
    """,
    ),
)

event_result = streamlit_bokeh_events(
    events="TestSelectEvent",
    bokeh_plot=plot,
    key="foo",
    debounce_time=100,
    refresh_on_update=False
)

# some event was thrown
if event_result is not None:
    # TestSelectEvent was thrown
    if "TestSelectEvent" in event_result:
        st.subheader("Selected Points' Pandas Stat summary")
        indices = event_result["TestSelectEvent"].get("indices", [])
        st.table(df.iloc[indices].describe())

st.subheader("Raw Event Data")
st.write(event_result)

Hope it helps ! :slight_smile:

PS. Also check this example https://github.com/ash2shukla/streamlit-bokeh-events/blob/master/example/retain_state.py this will also do the job but its meant to be used only if you want to refresh datasource on every update.

Thanks! I cleaned up the code and changed as suggested, however I have one problem:

I want to ā€œaddā€ to my selectionsā€¦ so a previous selection should be preserved until I click resetā€¦

Right now I always get new selections (without using state)

EDIT: just learned that I can have multiple selections with holding down the shift key. While not ideal, this might do the trick for the momentā€¦

Hey @cwerner,

Is this what you want to do ?

If so just hold ā€œshiftā€ to ā€œaddā€ to selections.

haha you beat me to it. :smiley:

1 Like

Yeah, was just told that this minute :wink:

This should do for now. Thanks very much for your help and the component!!!

1 Like

Can I ask some more?

So with refresh_on_update=False I get smooth plot interactions (and multi-select with SHIFT). Great!

However, Iā€™d need to change columns to plot with a streamlit selectbox. Does this require refresh_on_update=True and the SessionState handling?

@cwerner , well yes you do need to change it to refresh_on_update=True and it will be a little more complex than this.
I tried to create a simple example to see if this is what you are trying to achieve. Let me know if it does or doesnt work for you.

import streamlit as st
from bokeh.models import ColumnDataSource, CustomJS
from bokeh.plotting import figure
import pandas as pd
import numpy as np
from streamlit_bokeh_events import streamlit_bokeh_events
from state import provide_state
from collections import defaultdict

@st.cache
def data():
    df = pd.DataFrame({"x": np.random.rand(500), "y": np.random.rand(500), "z": np.random.rand(500), "size": np.random.rand(500) * 10})
    return df


@provide_state
def main(state):
    # create a map to hold selected points state across different plots
    state.selected_points_map = state.selected_points_map or defaultdict(list)

    x_field = st.selectbox("Select X", ["x", "y", "z"])
    y_field = st.selectbox("Select Y", ["x", "y", "z"])
    df = data()
    source = ColumnDataSource(df)

    # assign selected indices based on the current x_field, y_field 
    # to avoid carrying selected indices between different plots
    source.selected.indices = state.selected_points_map[(x_field, y_field)]

    st.subheader("Select Points From Map")

    plot = figure( tools="lasso_select,reset", width=250, height=250)
    plot.circle(x=x_field, y=y_field, size="size", source=source, alpha=0.6)
    
    source.selected.js_on_change(
        "indices",
        CustomJS(
            args=dict(source=source, x_field=x_field, y_field=y_field),
            code="""
            document.dispatchEvent(
                new CustomEvent("TestSelectEvent", {detail: {indices: cb_obj.indices, fields: [x_field, y_field]}})
            )
        """,
        ),
    )

    event_result = streamlit_bokeh_events(
        events="TestSelectEvent",
        bokeh_plot=plot,
        key="foo",
        debounce_time=100,
        refresh_on_update=True
    )

    # some event was thrown
    if event_result is not None:
        # TestSelectEvent was thrown
        if "TestSelectEvent" in event_result:
            st.subheader("Selected Points' Pandas Stat summary")
            # save selected indeces corresponding to current x_field, y_field in map
            key = tuple(event_result["TestSelectEvent"].get("fields"))
            value = event_result["TestSelectEvent"].get("indices", [])
            state.selected_points_map[key] = value 
    
    # show summary of current fields
    st.table(df.iloc[state.selected_points_map[(x_field, y_field)]].describe())

    st.subheader("Raw Event Data")
    st.write(event_result)
main()

Hope it helps ! :slight_smile: