I have a question regarding the ColumnDataSource
in a Bokeh 2.3.0 Server application.
Below is an example that tries to illustrate my question. Eventough it is a littlebit longer, I've spend a lot of effort making it as minimal but complete as possible.
So, there are at least two major ways of editing the data in ColumnDataSource
that I know will work.
First one is by using the 'index_way' (I don't know how to call this method correctly) by using source.data['my_column_name'][<numpy_like_array_indexing>] = 'my_new_value'
where <numpy_like_array_indexing>
can result in something like [0:10]
or [[True,False,True]]
, ect. to subset the data like a numpy array. This way, one can use the source.selected.indices
to index the data for example.
The second method is using the .patch()
function of ColumnDataSource
. Which the reference calls describes as Efficiently update data source columns at specific locations.
The third method I came accross in my code is when editing/changing a complete column in ColumnDataSource
like source.data['my_data_column_1'] = source.data['my_data_column_2']
. This way, I can set a data column to an already existing one.
My question is: Are they designed to behave differently? I found that changes using the 'index' method are not propagated or updated to the HoverTool, wheares for the other two methods, this seem to work.
This behavior can be seen in the following code example. When changing the first few samples in the plot, by selecting them with the selection tool and editing source.data['Label']
via label_selected_via_index()
the HoverTool does not show the correct and updated value of 'Label'. However, the change in the data was acutally performed, which can be seen by check_label()
which accesses and prints the first few samples of source.data['Label']
.
Changing the Label
Value with one of the other methods does indeed show the correct and updated value when hovering over the data.
import pandas as pd
from bokeh.plotting import figure, curdoc
from bokeh.models import ColumnDataSource, LinearColorMapper, Dropdown, Button, HoverTool
from bokeh.layouts import layout
import random
import time
plot_data = 'Value1'
LEN = 1000
df = pd.DataFrame({"ID":[i for i in range(LEN)],
"Value1":[random.random() for i in range(LEN)],
"Value2":[random.random() for i in range(LEN)],
"Color": [int(random.random()*10) for i in range(LEN)] })
df['plot_data'] = df[plot_data]
df['Label'] = "No Label Set"
df['Label_new_col'] = "Label was added"
source = ColumnDataSource(df)
cmap = LinearColorMapper(palette="Turbo256", low = 0, high = 3)
def make_tooltips():
return [('ID', '@ID'),
('Label', '@Label'),
(plot_data, f'@{plot_data}')]
tooltips = make_tooltips()
hover_tool = HoverTool(tooltips=tooltips)
plot1 = figure(plot_width=800, plot_height=250, tooltips=tooltips, tools='box_select')
plot1.add_tools(hover_tool)
circle = plot1.circle(x='ID', y='plot_data', source=source,
fill_color={"field":'Color', "transform":cmap},
line_color={"field":'Color', "transform":cmap})
def update_plot_data(event):
global plot_data
plot_data = event.item
source.data['plot_data'] = source.data[plot_data]
hover_tool.tooltips = make_tooltips()
dropdown = Dropdown(label='Change Value', menu=["Value1","Value2"])
dropdown.on_click(update_plot_data)
def label_selected_via_index(event):
t0 = time.time()
selected = source.selected.indices
source.data['Label'][0:10] = 'Label was added'
hover_tool.tooltips = make_tooltips()
source.selected.indices = []
print(f"Time needed for label_selected_via_index: {time.time()-t0:.5f}")
button_set_label1 = Button(label='Set Label via Index')
button_set_label1.on_click(label_selected_via_index)
def label_selected_via_patch(event):
t0 = time.time()
selected = source.selected.indices
patches = [(ind, 'Label was added') for ind in selected]
source.patch({'Label': patches})
hover_tool.tooltips = make_tooltips()
source.selected.indices = []
print(f"Time needed for label_selected_via_patch: {time.time()-t0:.5f}")
button_set_label2 = Button(label='Set Label via Patch')
button_set_label2.on_click(label_selected_via_patch)
def label_selected_via_new_col(event):
t0 = time.time()
selected = source.selected.indices
source.data['Label'] = source.data['Label_new_col']
hover_tool.tooltips = make_tooltips()
source.selected.indices = []
print(f"Time needed for label_selected_via_new_col: {time.time()-t0:.5f}")
button_set_label3 = Button(label='Set Label via New Column ')
button_set_label3.on_click(label_selected_via_new_col)
def check_label(event):
print(f"first 10 labels: {[l for l in source.data['Label'][0:10]]}")
button_label_check = Button(label='Check Label')
button_label_check.on_click(check_label)
layout_ = layout([[plot1],
[dropdown],
[button_set_label1 ,button_set_label2, button_set_label3],
[button_label_check]])
curdoc().add_root(layout_)
In my application, I have a lot of data and observed, that using .patch()
does take significantly longer than the indexing version or the replacement of a complete column. In my application, the indexing method needs less than a millisecond, while the patch method needs more than one seconds, which makes everything a little bit more laggy when interactively changing values. Basically, my application is somehow similar to the above example regarding the process of selecting samples in one plot and assigning a label via multiple buttons. Those labels are also shown in muliple plots via the tooltip, so this update is necessary for me.
Is there a way to A) Make the indexing version also updating the Hovertool? I prefer this method, because it is visually much faster or B) Make the .patch()
version somehow faster?
I hope I could make my problem somehow understandable and be thankful for any suggestions.
In the context of a Bokeh server app, it's worth keeping in mind, "what all actually needs to happen for a change to show up in the browser?" And the answer to that is roughly:
Pretty much Bokeh always handles the last three steps (modulo any actual bugs or TBD features). So the question really boils down to "what are the ways to signal a change" to Bokeh? Let's start from a position of describing what is available and intended (rather than starting from differences or what is not intended).
The number one, primary way to update a Bokeh object in order see a change in the browser is to assign an entire new value to a Bokeh property. If you do that, e.g. .prop_name = new_value
, literally including a "dot" and "equal sign", then Bokeh can auto-magically detect the change and send it to the browser. Here are a few examples:
plot.title.text = "New title" # updates the title
glyph.line_color = "red" # change a glyph's line color
slider.value = 10 # sets a slider's value
The examples above all show basic scalar (string, number) values, but this works just as well for more complicated values. Another extremely common example of this general mechanism is updating the entire .data
dict of a ColumnDataSource
source.data = {'x': [...], 'y': [...]} # new data for a glyph or table
That updates all the data in a CDS, so that e.g. a line glyph might re-draw itself.
Depending what you are doing, the size of your data, etc., updating the entire .data
dict may be expensive (due to serialization, de-serialization, network transit, etc). So there are some other ways that may be more efficient in specific cases.
The distinguishing characteristic above is that everything is a "whole" assignment, i.e. there are not mutating or in-place modifications. In a few cases, Bokeh can auto-magically handle in-place updates to mutable values. Without getting too into the weeds, by far the most important example of this is setting a single new column in a ColumnDataSource
by using standard Python dict indexing assignment on .data
:
source.data['x'] = [...] # Bokeh will automatically handle this
This is your Third method above. It works fine, but only for updating columns in a CDS .data
dict. This method only sends the one new column of data over the wire. As long as you only need to update one or a few columns in a large CDS, it is probably faster than assigning a new whole .data
value.
What does NOT work is basically any other kind of mutating, in-place assignment:
source.data['x'][100:200] = [...] # Bokeh does not automatically handle this
This is your First ("index") method above, and it is a non-starter. This kind of usage will not trigger any changes in the browsers.
The TLDR is that wrapping every CDS sequence in some custom class that overrides the standard getitem/setitem machinery just makes common usage too inefficient, and the trade-off cannot be justified. Bokeh will not auto-magically notice or do anything with with mutating assignments like this. (If you are purely in BokehJS JavaScript-side, then you can make in-place assignments like this and then manually call a change.emit()
to manually trigger updates, but that is only for the pure JS side of things).
Recognizing that sometimes even restricting CDS updates to a single column is still not efficient enough, the patch
and stream
methods were added to ColumnDataSource
.
These methods are for cases like:
I want to append few new values to the end of all my columns, instead of sending all the data again. (e.g for streaming new stock ticker or other sensor data efficiently)
I want to update a few specific values in the middle of my large time series or image, but only send the updated values and not re-send everything else.
This is your Second method and is typically much faster for small updates relative to total data size. As for patch
specifically, you can see e.g. the patch_app.py
example in the examples folder:
This example updates a three separate scatter, multi-line, and image plots all simultaneously at a 20Hz update rate. The checked-in version has fairly modest data sizes, but I tested it again locally by bumping all the data to 10–100x larger, and it still kept up. If you are seeing something different from patch (i.e. multi-second update times), then a complete Minimal Reproducing Example is needed to actually investigate.