I am wondering if it is possible to dynamically change which column from a GeoPandas GeoDataFrame is shown in a geoplot. For example, if I have a GeoDataFrame with different columns representing Global data on different dates, how could I have an interactive slider which allows me to show data for a specific date in a geoplot? I see matplotlib.widgets has a slider, but I cannot figure out how to apply that to a GeoDataFrame and geoplot.
The ipywidgets.interact decorator is useful for quickly turning a function into an interactive widget
from ipywidgets import interact
# plot some GeoDataFrame, e.g. states
#interact(x=states.columns)
def on_trait_change(x):
states.plot(x)
I found it convenient to use interact to set up interactive widgets in combination with a function modifying the data/plot based on the parameters selected in the widgets. For demonstration I implemented a slider widget and a drop down menu widget. Dependent on your use case, you might need only one.
# import relevant modules
import geopandas as gpd
import ipywidgets
import numpy as np
# load a sample data set
world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
# set seed for reproducability
np.random.seed(0)
# generate 3 artifical columns: random proportions of the gdp_md_est column (logarithmized)
for date in ['date1', 'date2', 'date3']:
world[date] = np.log(world.gdp_md_est*np.random.rand(len(world)))
# function defining what should happen if the user selects a specific date and continent
def on_trait_change(date, continent):
df=world[world['continent'] == continent] # sub set data
df.plot(f'date{date}') # to plot for example column'date2'
# generating the interactive plot with two widgets
interact(on_trait_change, date=ipywidgets.widgets.IntSlider(min=1, max=3, value=2), continent=list(set(world.continent)))
Related
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 am trying to create a grid layout (say 2x2) of plots of the DynamicMap type using holoviews.
This is to be served as a Holoviews/Bokeh app.
After creating my dmaps I Lay them out using
layout = hv.Layout([dmap1, dmap2]).cols(2)
this layout generates a two plots side-by-side
The problem I am facing is that by default, the widgets for each Dynamic Map get clustered to the rightof the row , with no visual association to the plots (the maps toolbars also get combined into just one).
Moreover, If I pass more than two dmaps to the layout, I get an error:
TypeError: DynamicMap does not accept AdjointLayout type, data elements have to be a ('ViewableElement', 'NdMapping', 'Layout').
I am using the renderer in server mode:
renderer = hv.renderer('bokeh')
renderer = renderer.instance(mode='server')
In summary, I want to have a grid of more than 2 independently controlable dynamical plots.
There's a lot to unpack here, so let me start out answering this question:
the widgets for each Dynamic Map get clustered to the right of the row
If you want independent sets of widgets you will have to construct each set of widgets manually and compose the resulting bokeh models yourself. This example demonstrates this approach:
import numpy as np
import holoviews as hv
from bokeh.io import curdoc
from bokeh.layouts import row
renderer = hv.renderer('bokeh').instance(mode='server')
dmap1 = hv.DynamicMap(lambda x: hv.Curve(np.random.rand(10)), kdims='x').redim.range(x=(0,5))
dmap2 = hv.DynamicMap(lambda y: hv.Scatter(np.random.rand(10)), kdims='y').redim.range(x=(0,5))
widget1 = renderer.get_widget(dmap1, None, position='above').state
widget2 = renderer.get_widget(dmap2, None, position='above').state
r = row(widget1, widget2)
doc = curdoc()
doc.add_root(r)
We create two independent DynamicMaps, then use the renderer to generate separate plots and widgets and then compose them using the bokeh row layout. As you can see we can also define a position for the widgets so instead of laying them out on the right they are on top.
the maps toolbars also get combined into just one
In this recent PR a new merge_tools option was added to allow having separate toolbars in a single Layout.
Moreover, If I pass more than two dmaps to the layout, I get an error:
This is probably due to returning an adjoined object, which is not allowed inside a DynamicMap at the moment. Are you by any chance using the .hist method? If so try calling it on the DynamicMap instead of having the DynamicMap return an object with an adjoined Histogram.
I am trying to make interactive sliders with ipywidgets on jupyter notebook to change the data of a plot when a user changes a slider Widget, which is simple and we can find example codes easily. The problem that I have is twofold: 1) when there are many parameters (= variables,sliders) in my function to be displayed, sliders are vertically arranged so that it is getting hard to control them without scrolling the jupyter page. Is there any way to arrange sliders as I wish like m by n grid? 2) To pass a large number of integer-/float-valued sliders, I made a single dictionary to be passed to the function interactive. Here, the key (=slider/variable/parameter) names are displayed seemingly in random order. I tried to pass the dictionary after sorting by the key names beforehand, but it does not still resolve the issue.
I'd appreciate it if you can share any ideas.
def myfun(**var_dict):
v = [value for value in var_dict.values()]
return np.sum(v)
var_dict = {'var1':1,'var2':2,'var3':3,'var4':4}
w = interactive(myfun,**var_dict)
display(w)
ipywidgets interactive sliders
You will not be able to solve this using **kwargs. As stated in PEP468
"The only reason they [keyword arguments] don't work is because the interpreter throws that ordering information away."
"Starting in version 3.5 Python will preserve the order of keyword arguments as passed to a function"
So if you want to get this behavior you should either:
name your arguments when you use 'interactive':
from ipywidgets import interactive
from IPython.display import display
import numpy as np
def myfun(**kwargs):
return np.sum(list(kwargs.itervalues()))
w=interactive(myfun,var1=1,var2=2,var3=3,var4=4)
display(w)
or, if you really want to use a dict, as far as I know, the best is to build the widgets yourself, without the use of 'interactive'.
You could do this this way:
from IPython.display import display,clear_output
from ipywidgets import widgets
from collections import OrderedDict
var_dict = OrderedDict([('var1',1),('var2',2),('var3',3),('var4',4)])
def myfct(change,list_widgets):
clear_output()
print np.sum([widget.value for widget in list_widgets])
list_widgets=[]
# create the widgets
for name,value in var_dict.iteritems():
list_widgets.append(widgets.IntSlider(value=value,min=value-2,max=value+2,description=name))
# link the widgets with the function
for widget in list_widgets:
widget.observe(lambda change:myfct(change,list_widgets),names='value',type='change')
# group the widgets into a FlexBox
w = widgets.VBox(children=list_widgets)
# display the widgets
display(w)
Enjoy :-)
I am using the Ipython notebook, pandas library, and the bokeh plotting library and I have a function that generates a gridplot. I am trying to set up some checkboxes, with each checkbox corresponding to one of those plots and then update the gridplot with only the plots that have their corresponding checkboxes selected. There does not seem to be much support for the ipywidgets libray. This is my attempt so far; I am not sure how to pass the checkboxes I created to my function to update my gridplot though, so any help will be much appreciated. Thanks.
attributes = df.columns.tolist()
from ipywidgets import Checkbox, interact
from IPython.display import display
chk = [Checkbox(description=attributes[i]) for i in range(len(attributes))]
#this displays the checkboxes I created correctly
display(*chk)
#update plot takes in the names of the columns to be displayed and returns
#a gridplot containing all corresponding plots
#not sure about the part below though
interact(updatePlot,args=chk)
This displays the checkboxes and calls the updatePlot function as they're changed:
from ipywidgets import Checkbox, interact
from IPython.display import display
l = ["Dog", "Cat", "Mouse"]
chk = [Checkbox(description=a) for a in l]
def updatePlot(**kwargs):
print([(k,v) for k, v in kwargs.items()])
interact(updatePlot, **{c.description: c.value for c in chk})
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)