I have the following code that does mostly what I want it to do: make a scatter plot colored by a value, and a linked DynamicMap that shows an associated timeseries with each Point when you tap on it
import pandas as pd
import holoviews as hv
import panel as pn
from holoviews import streams
import numpy as np
hv.extension('bokeh')
df = pd.DataFrame(data = {'id':['a', 'b', 'c', 'd'], 'type':['d', 'd', 'h', 'h'], 'value':[1,2,3,4], 'x':range(4), 'y':range(4)})
points = hv.Points(data=df, kdims=['x', 'y'], vdims = ['id', 'type', 'value']).redim.range(value=(0,None))
options = hv.opts.Points(size = 10, color = 'value', tools = ['hover', 'tap'])
df_a = pd.DataFrame(data = {'id':['a']*5, 'hour':range(5), 'value':np.random.random(5)})
df_b = pd.DataFrame(data = {'id':['b']*5, 'hour':range(5), 'value':np.random.random(5)})
df_c = pd.DataFrame(data = {'id':['c']*10, 'hour':range(10), 'value':np.random.random(10)})
df_d = pd.DataFrame(data = {'id':['d']*10, 'hour':range(10), 'value':np.random.random(10)})
df_ts = pd.concat([df_a, df_b, df_c, df_d])
df_ts = df_ts.set_index(['id', 'hour'])
stream = hv.streams.Selection1D(source=points)
empty = hv.Curve(df_ts.loc['a']).opts(visible = False)
def tap_station(index):
if not index:
return empty
id = df.iloc[index[0]]['id']
return hv.Curve(df_ts.loc[id], label = str(id)).redim.range(value=(0,None))
ts_curve = hv.DynamicMap(tap_station, kdims=[], streams=[stream]).opts(framewise=True, show_grid=True)
pn.Row(points.opts(options), ts_curve)
However, there is one more thing I want to do: have the 'd' and 'h' Points have different marker shapes.
One thing I can do is change the options line to this:
options = hv.opts.Points(size = 10, color = 'value', tools = ['hover', 'tap'],
marker = hv.dim("type").categorize({'d': 'square', 'h':'circle'}))
But with this, holoviews doesn't show a legend that distinguishes between the two marker shapes
The other thing I can do is something like this:
df_d = df[df['type'] == 'd']
df_h = df[df['type'] == 'h']
d_points = hv.Points(data=df_d, kdims=['x', 'y'], vdims = ['id', 'type', 'value'], label = 'd').redim.range(value=(0,None)).opts(marker = 's')
h_points = hv.Points(data=df_h, kdims=['x', 'y'], vdims = ['id', 'type', 'value'], label = 'h').redim.range(value=(0,None)).opts(marker = 'o')
d_stream = hv.streams.Selection1D(source=d_points)
h_stream = hv.streams.Selection1D(source=h_points)
This gets me the legend I want, but then I'm not sure how to make a DynamicMap that is linked to both of those streams, and responds to clicks on both marker shapes.
Again, ultimately what I want is a Point plot with two marker shapes (based on type
), colored by value
, that responds to clicks by pulling up a timeseries plot to the right.
Thanks for any help!
You can use the rename()
method of Stream
to change the stream name, and then use two arguments of the tap_station()
function to receive the two streams, Here is the full code:
import pandas as pd
import holoviews as hv
import panel as pn
from holoviews import streams
import numpy as np
hv.extension('bokeh')
df = pd.DataFrame(data = {'id':['a', 'b', 'c', 'd'], 'type':['d', 'd', 'h', 'h'], 'value':[1,2,3,4], 'x':range(4), 'y':range(4)})
points = hv.Points(data=df, kdims=['x', 'y'], vdims = ['id', 'type', 'value']).redim.range(value=(0,None))
options = hv.opts.Points(size = 10, color = 'value', tools = ['hover', 'tap'],
marker = hv.dim("type").categorize({'d': 'square', 'h':'circle'}), show_legend=True)
df_d = df[df['type'] == 'd']
df_h = df[df['type'] == 'h']
d_points = hv.Points(data=df_d, kdims=['x', 'y'], vdims = ['id', 'type', 'value'], label = 'd').redim.range(value=(0,None)).opts(marker = 's')
h_points = hv.Points(data=df_h, kdims=['x', 'y'], vdims = ['id', 'type', 'value'], label = 'h').redim.range(value=(0,None)).opts(marker = 'o')
points = d_points * h_points
# rename the streams
stream_d = hv.streams.Selection1D(source=d_points).rename(index="index_d")
stream_h = hv.streams.Selection1D(source=h_points).rename(index="index_h")
df_a = pd.DataFrame(data = {'id':['a']*5, 'hour':range(5), 'value':np.random.random(5)})
df_b = pd.DataFrame(data = {'id':['b']*5, 'hour':range(5), 'value':np.random.random(5)})
df_c = pd.DataFrame(data = {'id':['c']*10, 'hour':range(10), 'value':np.random.random(10)})
df_d = pd.DataFrame(data = {'id':['d']*10, 'hour':range(10), 'value':np.random.random(10)})
df_ts = pd.concat([df_a, df_b, df_c, df_d])
df_ts = df_ts.set_index(['id', 'hour'])
empty = hv.Curve(df_ts.loc['a']).opts(visible = False)
# receive the two streams by different argument name
def tap_station(index_d, index_h):
if not index_d and not index_h:
return empty
elif index_d:
id = df_d.iloc[index_d[0]]['id']
elif index_h:
id = df_h.iloc[index_h[0]]['id']
return hv.Curve(df_ts.loc[id], label = str(id)).redim.range(value=(0,None))
# pass the two streams to DynamicMap
ts_curve = hv.DynamicMap(tap_station, kdims=[], streams=[stream_d, stream_h]).opts(framewise=True, show_grid=True)
pn.Row(points.opts(options), ts_curve)