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
Hey @ash2shukla, welcome to the community ! Short workaround code that does the job is my jam and you took the extra time to publish it that’s awesome.
This looks great 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.
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
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.
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.
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 !
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.
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.
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.
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.
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)
@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()
Thanks for stopping by! We use cookies to help us understand how you interact with our website.
By clicking “Accept all”, you consent to our use of cookies. For more information, please see our privacy policy.
Cookie settings
Strictly necessary cookies
These cookies are necessary for the website to function and cannot be switched off. They are usually only set in response to actions made by you which amount to a request for services, such as setting your privacy preferences, logging in or filling in forms.
Performance cookies
These cookies allow us to count visits and traffic sources so we can measure and improve the performance of our site. They help us understand how visitors move around the site and which pages are most frequently visited.
Functional cookies
These cookies are used to record your choices and settings, maintain your preferences over time and recognize you when you return to our website. These cookies help us to personalize our content for you and remember your preferences.
Targeting cookies
These cookies may be deployed to our site by our advertising partners to build a profile of your interest and provide you with content that is relevant to you, including showing you relevant ads on other websites.