Component for bi-directional communication with bokeh

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.

4 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:


Hello, i am testing the data_table.py
however it ONly show 2 Row,
i cannot find have keywords in Doc that control the ROWs,
but scollable or frozen row only
can you give me a hints thanks!

df = pd.DataFrame({
        "x": [1, 2, 3, 4, 5, 6, 7],
        "y": [4, 5, 6, 7, 5, 6, 7],
})
p = DataTable(source=cds, columns=columns,selectable="checkbox",scroll_to_selection=True)
result = streamlit_bokeh_events(bokeh_plot=p, events="INDEX_SELECT", key="foo", refresh_on_update=False, debounce_time=0, override_height=100)

and its seem the table is Restricted to the canvas ? for unknow reason
I didnt add any modify code except the DF

Jeff

OK. i find the answer myself
keywords = “override_height=100”
thanks!

1 Like

sorry , another questoin ,
as in the above code, the table seems freeze,
without scroll bar, can i have some hints ,
thanks

Hi @apex_yu,

If you aren’t sure about your datatable height I’d recommend leaving the override_height argument.
I am unable to reproduce the table freezing issue at my end but I think if you will override view port height that’s less than the table height it will hide last few rows, which might look like a table freeze. For resolving this pass the same height as of the view port into the DataTable as a argument.
Check this GIF for this explanation in action,

Hope it helps!