hvplot icon indicating copy to clipboard operation
hvplot copied to clipboard

Specifying `widgets` or `widget_location` changes return type from holoviews to panel

Open scottyhq opened this issue 5 years ago • 8 comments

ALL software version info

hvplot 0.6 holoviews 1.13.5 bokeh 2.2.3

Description of expected behavior and the observed behavior

using default widgets seems to return a holoviews.core.spaces.DynamicMap, but specifying keywords widget_location or widgets returns panel.layout.base.Column (or Row). This leads to confusing errors when trying to compose elements or link streams. I imagine there are workarounds to get at the DynamicMap embedded within the panel layout, but I'm having trouble figuring it out.

Complete, minimal, self-contained example code that reproduces the issue

#minimal reproducible example
import xarray as xr
import hvplot.xarray
import holoviews as hv
from holoviews.streams import RangeXY

air_ds = xr.tutorial.open_dataset('air_temperature').load()
air = air_ds.air
print(type(air.hvplot.image()))
print(type(air.hvplot.image(widget_location='bottom')))
# <class 'holoviews.core.spaces.DynamicMap'>
# <class 'panel.layout.base.Column'>
bug1 composing elements
img = air.hvplot.image(widget_location='bottom')
point = hv.Points([(260, 40)])
img * point
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-13-b3246ce2d54e> in <module>
      2 img = air.hvplot.image(widget_location='bottom')
      3 point = hv.Points([(260, 40)])
----> 4 img * point

TypeError: unsupported operand type(s) for *: 'Column' and 'Points'
bug2 adding a stream
# bug 2 adding stream
img = air.hvplot.image(widget_location='bottom')
range_xy_stream = RangeXY(source=img)
img
/srv/conda/envs/notebook/lib/python3.8/site-packages/holoviews/plotting/plot.py in <listcomp>(.0)
    953             streams = [
    954                 s for src, streams in registry for s in streams
--> 955                 if src is source or (src._plot_id is not None and
    956                                      src._plot_id == source._plot_id)]
    957             cb_classes |= {(callbacks[type(stream)], stream) for stream in streams

AttributeError: 'Column' object has no attribute '_plot_id'

@jbednar or @philippjfr , it would be great to have an example of how to work around these issues by accessing the DynamicMap wrapped in panel.

scottyhq avatar Oct 28 '20 00:10 scottyhq

Looks like hvPlot is using Panel to provide an individual laid-out object with the widget in the right place, which should be documented somewhere if not already. In any case, you can drop back to specifying widget_location globally at the HoloViews level, so that you consistently get hv objects to work with instead:

import xarray as xr
import hvplot.xarray
import holoviews as hv
from holoviews.streams import RangeXY

hv.output(widget_location='bottom')

air_ds = xr.tutorial.open_dataset('air_temperature').load()
air_ds.air.hvplot.image()

image

The HoloViews output option doesn't take effect until the object gets rendered, so it will apply to all objects rendered until the option changes. The hvPlot version is tied to that specific object, but it does force that object to be a Panel object and not HoloViews, as you've observed. We'd probably need to add some lazy hv.output option storage on HoloViews objects to avoid having hvPlot do this...

jbednar avatar Oct 28 '20 00:10 jbednar

BTW, it's easy to access the underlying DynamicMap if you want, but that will bypass the Panel machinery that's putting the widgets at the bottom:

img = air_ds.air.hvplot.image(widget_location='bottom')
print(img)
dm = img[0].object

point = hv.Points([(260, 40)])
dm * point

image

jbednar avatar Oct 28 '20 00:10 jbednar

Well, ok, if you really do want to do it by indexing down to the DynamicMap, you can; you just have to go back and update the Panel object when you're done:

img = air_ds.air.hvplot.image(widget_location='bottom')
dm = img[0].object

point = hv.Points([(260, 40)])
img[0].object = dm * point
img

image

But I'd suggest the hv.output approach anyway; seems much less hassle unless you need to control the widget location per object!

jbednar avatar Oct 28 '20 00:10 jbednar

And on third thought, even if you did need to control it per object, I'd do that at the Panel level, since that's ultimately what's in charge of widgets and layouts:

import xarray as xr
import hvplot.xarray
import holoviews as hv
from holoviews.streams import RangeXY

air_ds = xr.tutorial.open_dataset('air_temperature').load()
img1 = air_ds.air.hvplot.image()
point = hv.Points([(260, 40)])

img2 = img1 * point

import panel as pn
pn.Column(pn.panel(img1, widget_location='top'),
          pn.panel(img2, widget_location='bottom'))

image

Surely one of those options will work for you. :-)

jbednar avatar Oct 28 '20 00:10 jbednar

Thanks @jbednar ! These are all great solutions. FWIW my docs journey first took me here https://hvplot.holoviz.org/user_guide/Widgets.html, but I never read into the link at the bottom to this helpful page https://panel.holoviz.org/reference/panes/HoloViews.html. So what I ended up trying was almost but not quite your solution 2: img[0] * point rather than the correct img[0].object * point

scottyhq avatar Oct 28 '20 02:10 scottyhq

This is still the case:

  • [ ] Document that the return type is a Panel object when one of the widgets options is set

hvPlot handles these options ["widgets", "widget_location", "widget_layout", "widget_type"] by passing them to pn.panel(), these are parameters of the HoloViews pane.

    widget_location = param.ObjectSelector(default='right_top', objects=[
        'left', 'bottom', 'right', 'top', 'top_left', 'top_right',
        'bottom_left', 'bottom_right', 'left_top', 'left_bottom',
        'right_top', 'right_bottom'], doc="""
        The layout of the plot and the widgets. The value refers to the
        position of the widgets relative to the plot.""")

    widget_layout = param.ObjectSelector(
        objects=[WidgetBox, Row, Column], constant=True, default=WidgetBox, doc="""
        The layout object to display the widgets in.""")

    widget_type = param.ObjectSelector(default='individual',
                                       objects=['individual', 'scrubber'], doc=""")
        Whether to generate individual widgets for each dimension or
        on global scrubber.""")

    widgets = param.Dict(default={}, doc="""
        A mapping from dimension name to a widget instance which will
        be used to override the default widgets.""")

@jlstevens do you think there would be a way to have HoloViews support these options?

maximlt avatar Oct 18 '22 23:10 maximlt

Yes we have discussed in the past to make renderer/output options another options group to let users control these settings more easily.

philippjfr avatar Oct 19 '22 07:10 philippjfr

Interesting! @jlstevens what's your opinion on this?

maximlt avatar Oct 19 '22 09:10 maximlt

This issue has been mentioned on HoloViz Discourse. There might be relevant details there:

https://discourse.holoviz.org/t/setting-additional-options-when-layout-is-generated/8601/6

holovizbot avatar Feb 25 '25 18:02 holovizbot