Say I have a class that holds some data and implements a function that returns a bokeh plot
import bokeh.plotting as bk
class Data():
def plot(self,**kwargs):
# do something to retrieve data
return bk.line(**kwargs)
Now I can instantiate multiple of these Data objects like exps and sets and create individual plots. If bk.hold() is set they'll, end up in one figure (which is basically what I want).
bk.output_notebook()
bk.figure()
bk.hold()
exps.scatter(arg1)
sets.plot(arg2)
bk.show()
Now I want aggregate these plots into a GridPlot() I can do it for the non overlayed single plots
bk.figure()
bk.hold(False)
g=bk.GridPlot(children=[[sets.plot(arg3),sets.plot(arg4)]])
bk.show(g)
but I don't know how I can overlay the scatter plots I had earlier as exps.scatter.
Is there any way to get a reference to the currently active figure like:
rows=[]
exps.scatter(arg1)
sets.plot(arg2)
af = bk.get_reference_to_figure()
rows.append(af) # append the active figure to rows list
bg.figure() # reset figure
gp = bk.GridPlot(children=[rows])
bk.show(gp)
As of Bokeh 0.7 the plotting.py interface has been changed to be more explicit and hopefully this will make things like this simpler and more clear. The basic change is that figure now returns an object, so you can just directly act on those objects without having to wonder what the "currently active" plot is:
p1 = figure(...)
p1.line(...)
p1.circle(...)
p2 = figure(...)
p2.rect(...)
gp = gridplot([p1, p2])
show(gp)
Almost all the previous code should work for now, but hold, curplot etc. are deprecated (and issue deprecation warnings if you run python with deprecation warnings enabled) and will be removed in a future release.
Ok apparently bk.curplot() does the trick
exps.scatter(arg1)
sets.plot(arg2)
p1 = bk.curplot()
bg.figure() # reset figure
exps.scatter(arg3)
sets.plot(arg4)
p2 = bk.curplot()
gp = bk.GridPlot(children=[[p1,p2])
bk.show(gp)
Related
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:
a change is detected (or signaled) in Python
a change event is serialized and sent over the network to a browser
the change event is deserialized by BokehJS
the change is applied and view in the browser is updates
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).
Direct Assignment to Properties
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.
"In-place" Special 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).
Dedicated APIs for Optimized Cases
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.
I've written a function which basically makes some calculations and returns a Bokeh plot object.
Then I'm calling that function to display some initial output to the user. After that I have a function which is there to check for updates.
I also have a Select, so the user can select option he/she wants. Finally, I'm updating the plot.
Here's the structure of the code:
plot = my_custom_function(dataset, 'input_parameter')
def update_plot(attr, old, new):
if new == 'some_other':
plot = my_custom_function(dataset, new)
else:
plot = my_custom_function(dataset, old)
select = Select(title='Charging Station', options=['the_first', 'some_other'], value='the_first')
select.on_change('value', update_plot)
layout = row(select, plot)
curdoc().add_root(layout)
The problem is, the chart is not updating? What is the problem?
There are a number of things to mention here:
First, are you running this with the Bokeh server, i.e. bokeh serve maypp.py? Real Python callbacks (e.g. with on_change) only work in the Bokeh server (the Bokeh server is the Python process that actually runs the callback code)
Your callback, as written, has no effect whatsoever. You assign to a local variable plot that only exists inside the callback function, and then disappears as soon as the function ends. You have not actually updated anything, so the entire callback is a no-op. What the callback needs to do is modify the plot you made earlier, e.g. by updating the existing data sources. A typical Bokeh app has a structure along the lines of:
source = ColumnDataSource(...)
p = figure(...)
p.line(..., source=source)
def update(attr, old, new):
source.data = some_new_data # Update the *existing* data source
p.title.text = "new title" # Update properties on *existing* objects
select = Select(...)
select.on_change('value', update)
All of the example apps in repository follow this kind of pattern.
The last thing to mention is that it is always 100% best practice to make the smallest change possible. I.e. you should update the .data for an existing data source, not replace entire data sources (or plots) with new ones. Bokeh is optimized for this kind of updating.
Sorry if this is a basic question, but I haven't been able to find an answer in the bokeh documentation. I want to be able to plot a bokeh plot without the long GlyphRenderer list displaying.
I have tried saving the p.hexbin line to a variable called 'test'. However, this new 'test' variable is being saved as a tuple and can no longer be used with the 'show()' function to display a bokeh plot. The example code I am using here is straight from the bokeh documentation site.
import numpy as np
from bokeh.models import HoverTool
from bokeh.plotting import figure, show
x = 2 + 2*np.random.standard_normal(500)
y = 2 + 2*np.random.standard_normal(500)
p = figure(match_aspect=True, tools="wheel_zoom,reset")
p.background_fill_color = '#440154'
p.grid.visible = False
p.hexbin(x, y, size=0.5, hover_color="pink", hover_alpha=0.8)
hover = HoverTool(tooltips=[("count", "#c"), ("(q,r)", "(#q, #r)")])
p.add_tools(hover)
show(p)
I only want the hexbin plot to display when I run the code, not the Glyph tuple.
I have tried saving the p.hexbin line to a variable called 'test'. However, this new 'test' variable is being saved as a tuple and can no longer be used with the 'show()' function to display a bokeh plot.
Printing outputs is standard Python behavior, there is nothing we can do about that. The function returns a list, so Python will print a list. The only thing to suppress that behavior, as you have noted, is to assign the output to a variable. However, since you don't care about its value, it can/should be ignored. There is no reason to pass it to show, you should continue to call show, on p, exactly the way you have been without any change:
rs = p.hexbin(x, y, size=0.5, hover_color="pink", hover_alpha=0.8)
show(p)
I'm trying to create a dashboard with two holoviews objects: a panel pn.widgets.Select object that contains a list of xarray variables, and a hvplot object that takes the selected variable on input, like this:
def hvmesh(var=None):
mesh = ds[var].hvplot.quadmesh(x='x', y='y', rasterize=True, crs=crs,
width=600, height=400, groupby=list(ds[var].dims[:-2]), cmap='jet')
return mesh
Here's what an example mesh looks like for a particular variable (one that has both time and height dimensions):
I would like to have the map update when I select a variable from the panel widget:
I tried to do this as a dynamic map, like this:
from holoviews.streams import Params
import holoviews as hv
var_stream = Params(var_select, ['value'], rename={'value': 'var'})
mesh = hv.DynamicMap(hvmesh, streams=[var_stream])
but when I try to display the map, I get:
Exception: Nesting a DynamicMap inside a DynamicMap is not supported.
It would seem a common need to select the variable for hvplot from a panel widget. What is the best way to accomplish this with pyviz?
In case it's useful, here is my full attempt Jupyter Notebook.
Because the groupby changes with each variable selected, a list of variables can not be passed to hvplot. So one solution is to just recreate the plot each time a new variable is selected. This works:
import holoviews as hv
from holoviews.streams import Params
def plot(var=None, tiles=None):
var = var or var_select.value
tiles = tiles or map_select.value
mesh = ds[var].hvplot.quadmesh(x='x', y='y', rasterize=True, crs=crs, title=var,
width=600, height=400, groupby=list(ds[var].dims[:-2]),
cmap='jet')
return mesh.opts(alpha=0.7) * tiles
def on_var_select(event):
var = event.obj.value
col[-1] = plot(var=var)
def on_map_select(event):
tiles = event.obj.value
col[-1] = plot(tiles=tiles)
var_select.param.watch(on_var_select, parameter_names=['value']);
map_select.param.watch(on_map_select, parameter_names=['value']);
col = pn.Column(var_select, map_select, plot(var_select.value) * tiles)
producing:
Here is the full notebook.
So there is a long answer and a short answer here. Let's start with the short answer, which is that there's no need to create a custom select widget for the data variable since hvPlot allows selecting between multiple data variables automatically, so if you change it to this:
rasterized_mesh = ds[time_vars].hvplot.quadmesh(
x='x', y='y', z=time_vars[::-1], crs=crs, width=600, height=400,
groupby=list(ds[var].dims[:-2]), rasterize=True, cmap='jet')
You will get a DynamicMap that lets you select the non-spatial dimensions and the data variable and you can now embed that in your panel, no extra work needed. If that's all you care about stop here as we're about to get into some of the internals to hopefully deliver a better understanding.
Let us assume for a minute hvPlot did not allow selecting between data variables, what would we do then? So the main thing you have to know is that HoloViews allows chaining DynamicMaps but does not allow nesting them. This can be a bit hard to wrap your head around but we'll break the problem down into multiple steps and then see how we can achieve what we want. So what is the chain of events that would give us our plot?
Select a data variable
Apply a groupby over the non-spatial dimensions
Apply rasterization to each QuadMesh
As you know, hvPlot takes care of steps 2. and 3. for us, so how can we inject step 1. before 2. and 3. In future we plan to add support for passing panel widgets directly into hvPlot, which means you'll be able to do it all in a single step. Since panel is still a very new project I'll be pointing out along the way how our APIs will eventually make this process trivial, but for now we'll have to stick with the relatively verbose workaround. In this case we have to rearrange the order of operations:
Apply a groupby over the non-spatial dimensions
Select a data variable
Apply rasterization to each QuadMesh
To start with we therefore select all data variables and skip the rasterization:
meshes = ds[time_vars].hvplot.quadmesh(
x='x', y='y', z=time_vars, crs=crs, width=600, height=400,
groupby=list(ds[var].dims[:-2]))
Now that we have a DynamicMap which contains all the data we might want to display we can apply the next operations. Here we will make use of the hv.util.Dynamic utility which can be used to chain operations on a DynamicMap while injecting stream values. In particular in this step we create a stream from the var_select widget which will be used to reindex the QuadMesh inside our meshes DynamicMap:
def select_var(obj, var):
return obj.clone(vdims=[var])
var_stream = Params(var_select, ['value'], rename={'value': 'var'})
var_mesh = hv.util.Dynamic(meshes, operation=select_var, streams=[var_select])
# Note starting in hv 1.12 you'll be able to replace this with
# var_mesh = meshes.map(select_var, streams=[var_select])
# And once param 2.0 is out we intend to support
# var_mesh = meshes.map(select_var, var=var_select.param.value)
Now we have a DynamicMap which responds to changes in the widget but are not yet rasterizing it so we can apply the rasterize operation manually:
rasterized_mesh = rasterize(var_mesh).opts(cmap='jet', width=600, height=400)
Now we have a DynamicMap which is linked to the Selection widget, applies the groupby and is rasterized, which we can now embed in the panel. Another approach hinted at by #jbednar above would be to do all of it in one step by making the hvPlot call not dynamic and doing the time and height level selection manually. I won't go through that here but it also a valid (if less efficient) approach.
As I hinted at above, eventually we also intend to have all hvPlot parameters become dynamic, which means you'll be able to do something like this to link a widget value to a hvPlot keyword argument:
ds[time_vars].hvplot.quadmesh(
x='x', y='y', z=var_select.param.value, rasterize=True, crs=crs,
width=600, height=400, groupby=list(ds[var].dims[:-2]), cmap='jet')
I'm creating a tool for geospatial visualization of economic data using Matplotlib and Basemap.
However, right now, the only way I thought of that gives me enough flexibility is to create a new basemap every time I want to change the data.
Here are the relevant parts of the code I'm using:
class WorldMapCanvas(FigureCanvas):
def __init__(self,data,country_data):
self.text_objects = {}
self.figure = Figure()
self.canvas = FigureCanvas(self.figure)
self.axes = self.figure.add_subplot(111)
self.data = data
self.country_data = country_data
#this draws the graph
super(WorldMapCanvas, self).__init__(Figure())
self.map = Basemap(projection='robin',lon_0=0,resolution='c', ax=self.axes)
self.country_info = self.map.readshapefile(
'shapefiles/world_country_admin_boundary_shapefile_with_fips_codes', 'world', drawbounds=True,linewidth=.3)
self.map.drawmapboundary(fill_color = '#85A6D9')
self.map.fillcontinents(color='white',lake_color='#85A6D9')
self.map.drawcoastlines(color='#6D5F47', linewidth=.3)
self.map.drawcountries(color='#6D5F47', linewidth=.3)
self.countrynames = []
for shapedict in self.map.world_info:
self.countrynames.append(shapedict['CNTRY_NAME'])
min_key = min(data, key=data.get)
max_key = max(data, key=data.get)
minv = data[min_key]
maxv = data[max_key]
for key in self.data.keys():
self.ColorCountry(key,self.GetCountryColor(data[key],minv,maxv))
self.canvas.draw()
How can I create these plots faster?
I couldn't think of a solution to avoid creating a map every time I run my code. I tried creating the canvas/figure outside of the class but it didn't make that much of a difference. The slowest call is the one that creates the Basemap and loads the shape data. Everything else runs quite fast.
Also, I tried saving the Basemap for future use but since I need new axes I couldn't get it to work. Maybe you can point me in the right direction on how to do this.
I'd like you to know that I'm using the canvas as a PySide QWidget and that I'm plotting different kinds of maps depending on the data, this is just one of them (another would be a map of Europe, for instance, or the US).
You can pickle and unpickle Basemap instances (there is an example of doing this in the basemap source) which might save you a fair chunk of time on the plot creation.
Additionally, it is probably worth seeing how long the shapefile reading is taking (you may want to pickle that too).
Finally, I would seriously consider investigating the option of updating country colours for data, rather than making a new figure each time.
HTH,