Search code examples
pythonholoviews

Combining Pointdraw and Sample in Holoviews


I'm trying to combine Holoviews' Pointdraw functionality with its Sample functionality (I couldn't find a specific page, but it is shown in action here http://holoviews.org/gallery/demos/bokeh/mandelbrot_section.html)

Specifically, I want to have two subplots with interactivity. The one on the left shows a colormap, and the one on the right shows a sample (a linecut) of the colormap. This is achieved with .sample. Inside this right plot I'd like to have points that can be drawn, moved, and removed, typically done with pointdraw. I'd then also like to access their coordinates once I am done moving, which is possible when following the example from the documentation.

Now, I've got the two working independently, following the examples above. But when combined in the way that I have, this results in a plot that looks like this: enter image description here It has the elements I am looking for, except the points cannot be interacted with. This is somehow related to Holoviews' streams, but I am not sure how to solve it. Would anyone be able to help out?

The code that generates the above:

%%opts Points (color='color' size=10) [tools=['hover'] width=400 height=400] 
%%opts Layout [shared_datasource=True] Table (editable=True)

import param
import numpy as np
import holoviews as hv
hv.extension('bokeh', 'matplotlib')
from holoviews import streams

def lorentzian(x, x0, gamma):
    return 1/np.pi*1/2*gamma/((x-x0)**2+(1/2*gamma)**2)

xs = np.arange(0,4*np.pi,0.05)
ys = np.arange(0,4*np.pi,0.05)
data = hv.OrderedDict({'x': [2., 2., 2.], 'y': [0.5, 0.4, 0.2], 'color': ['red', 'green', 'blue']})

z = lorentzian(xs.reshape(len(xs),1),2*np.sin(ys.reshape(1,len(ys)))+5,1) + lorentzian(xs.reshape(len(xs),1),-2*np.sin(ys.reshape(1,len(ys)))+5,1)

def dispersions(f0):
    points = hv.Points(data, vdims=['color']).redim.range(x=(xs[0], xs[-1]), y=(np.min(z), np.max(z)))
    point_stream = streams.PointDraw(data=points.columns(), source=points, empty_value='black')
    image = hv.Image(z, bounds=(xs[0], ys[0], xs[-1], ys[-1]))
    return image* hv.VLine(x=f0) + image.sample(x=f0)*points

dmap = hv.DynamicMap(dispersions, kdims=['f0'])
dmap.redim.range(f0=(0,10)).redim.step(f0=(0.1))

I apologize for the weird function that we are plotting, I couldn't immediately come up with a simple one.


Solution

  • Based on your example it's not yet quite clear to me what you will be doing with the points but I do have some suggestions on structuring the code better.

    In general it is always better to compose plots from several separate DynamicMaps than creating a single DynamicMap that does everything. Not only is it more composable but you also get handles on the individual objects allowing you to set up streams to listen to changes on each component and most importantly it's more efficient, only the plots that need to be updated will be updated. In your example I'd split up the code as follows:

    def lorentzian(x, x0, gamma):
        return 1/np.pi*1/2*gamma/((x-x0)**2+(1/2*gamma)**2)
    
    xs = np.arange(0,4*np.pi,0.05)
    ys = np.arange(0,4*np.pi,0.05)
    data = hv.OrderedDict({'x': [2., 2., 2.], 'y': [0.5, 0.4, 0.2], 'color': ['red', 'green', 'blue']})
    
    points = hv.Points(data, vdims=['color']).redim.range(x=(xs[0], xs[-1]), y=(np.min(z), np.max(z)))
    image = hv.Image(z, bounds=(xs[0], ys[0], xs[-1], ys[-1]))
    
    z = lorentzian(xs.reshape(len(xs),1),2*np.sin(ys.reshape(1,len(ys)))+5,1) + lorentzian(xs.reshape(len(xs),1),-2*np.sin(ys.reshape(1,len(ys)))+5,1)
    taps = []
    
    def vline(f0):
        return hv.VLine(x=f0)
    
    def sample(f0):
        return image.sample(x=f0)
    
    dim = hv.Dimension('f0', step=0.1, range=(0,10))
    vline_dmap = hv.DynamicMap(vline, kdims=[dim])
    sample_dmap = hv.DynamicMap(sample, kdims=[dim])
    point_stream = streams.PointDraw(data=points.columns(), source=points, empty_value='black')
    
    (image * vline_dmap + sample_dmap * points)
    

    Since the Image and Points are not themselves dynamic there is no reason to put them inside the DynamicMap and the VLine and the sampled Curve are easily split out. The PointDraw stream doesn't do anything yet but you can now set that up as yet another DynamicMap which you can compose with the rest.