0

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.

user129412
  • 765
  • 1
  • 6
  • 19

1 Answers1

3

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.

philippjfr
  • 3,997
  • 14
  • 15
  • Sorry, that was indeed vague in the question. I'd like to drag the points to the top of the peaks shown in the right panel. Depending on the Vline, there's either 1 or 2. I'd like to drag two of them to the outmost location of the two peaks, and the other to the mid-most location of the single peak. I'd use the .sample to switch between slices that have two peaks and a single peak. – user129412 Nov 12 '18 at 16:35
  • Although I somehow didn't see how to before, the restructuring makes a lot of sense. Thank you very much. In fact, the code now does exactly what I was looking for. I can drag the points, and then access their values with point_stream.data. – user129412 Nov 12 '18 at 16:42
  • If you just want to record the point locations you could add a subscriber to the PointDraw stream, such as ``point_stream.add_subscriber(record_points)``. If you need to record the locations for each value of ``f0`` you can query the current ``f0`` value on ``sample_dmap.callback.args``. – philippjfr Nov 12 '18 at 22:17