panel icon indicating copy to clipboard operation
panel copied to clipboard

Future of `pn.interact`?

Open maximlt opened this issue 1 year ago • 10 comments

@Azaya89 has been working on modernizing the Attractors example on examples.holoviz.org. One of its notebooks makes use of pn.interact.

pn.interact has been soft-deprecated in Panel:

  • It is no longer included in the toctree. I could not find when this has been done, perhaps for Panel 1.0
  • Note however that if you search for pn.interact you can still find the How-Tos that described it and the API reference
  • Discourse shows that people still use it (1, 2)

I wanted to start a discussion to see:

  1. Whether pn.interact should really be deprecated,
  2. If so, how exactly? Is removing it from the toctree enough?

The work done by @Azaya89 has forced us to think about how to replace pn.interact with other Panel constructs. The example starts by defining the function that is meant to be interacted with and a dict ps of colormaps meant to be passed to the function.

from colorcet import palette_n
ps ={k:p[::-1] for k,p in palette_n.items()}

def clifford_plot(a=1.9, b=1.9, c=1.9, d=0.8, n=1000000, colormap=ps['kbc']):
    cvs = ds.Canvas(plot_width=600, plot_height=600)
    xs, ys = clifford_trajectory(a, b, c, d, 0, 0, n)
    agg = cvs.points(pd.DataFrame({'x':xs, 'y':ys}), 'x', 'y')
    return tf.shade(agg, cmap=colormap)

And it just calls pn.interact.

pn.interact(clifford_plot, n=(1,20000000), colormap=ps)

Which immediately generates this app: image

The suggested alternative implementation to pn.interact(...) is as follows.

widgets = {
    'a': pn.widgets.FloatSlider(value=1.9, end=2.0, step=0.1, name='a'),
    'b': pn.widgets.FloatSlider(value=1.9, end=2.0, step=0.1, name='b'),
    'c': pn.widgets.FloatSlider(value=1.9, end=2.0, step=0.1, name='c'),
    'd': pn.widgets.FloatSlider(value=0.8, end=1.0, step=0.1, name='d'),
    'n': pn.widgets.IntSlider(value=10000000, start=1000, end=20000000, step=100, name='n'),
    'colormap': pn.widgets.Select(value=ps['bmw'], options=ps, name='colormap'),
}

attr = pn.bind(clifford_plot, **widgets)
pn.Column(*widgets.values(), attr)

With this approach, the widgets must all explicitly be defined, pn.bind is used to create a bound function, and pn.Column is used to layout the widgets and the output of the bound function.

While this isn't too complicated Panel code, I think it's still a pretty big gap for users who aren't too familiar with Panel or with Python in general, users that could easily get started with pn.interact. To understand/read the code above they need to know about:

  • Panel's widget API
  • pn.bind (binding a function is quite an abstract term, and its similarity with functools.partial won't help the non-advanced Python user)
  • Dictionary unpacking
  • Dictionary .values() method
  • Iterable unpacking
  • Panel's layout API

Of course, the dictionary creation and unpacking could be replaced by a more explicit and simpler approach, at the expense of making the code longer.

So I wonder:

  • Is it fine for Panel not to provide an entry point for the very beginner users who need a simple way to explore the results of a function in a notebook? The answer may just be say, use ipywidgets.interact or the slightly more complex but more powerful constructs Panel offers.
  • Isn't pn.rx the more natural replacement for pn.interact? The example could be written without having to create any intermediate variable pn.rx(clifford_plot)(a=pn.widgets.FloatSlider(...), ...)
  • Shouldn't there be an API that simplifies creating widgets like pn.interact does? When using hvplot.interactive I have sometimes wanted a simpler way to declare a widget compared to the more explicit but longer to type pn.widgets.<Type>(**w_args). Perhaps Widget(<value_supported_by_interact>, name=...). cc @jbednar who I think has already asked for something like this.

maximlt avatar Apr 15 '24 08:04 maximlt

Some of the arguments for deprecating pn.interact.

  • Its just one more api to maintain
  • Its just one more api to explain. For example we already have a lot to explain our users in our tutorials. Should we add another tutorial explaining pn.interact?
  • We saw very little usage for example in discourse.
  • pn.interact seems simple and easy to use when looking at a working example. In practice its often not that simple and easy to use.
  • Our users should quickly transition away from pn.interact anyway.

MarcSkovMadsen avatar Apr 15 '24 14:04 MarcSkovMadsen

Would it make sense to make pn.Param wrap this or pn.ParamFunction?

Related: https://github.com/holoviz/panel/issues/6464

ahuang11 avatar Apr 15 '24 15:04 ahuang11

Would it make sense to make pn.Param wrap this or pn.ParamFunction?

Could you explain what you're suggesting.

philippjfr avatar Apr 15 '24 17:04 philippjfr

Functionality of pn.interact migrated to pn.Param so you can pass Param classes, Python functions (where the params are auto determined by type and positional args), widgets, etc.

import panel as pn
pn.extension()
button = pn.widgets.Button(name='Click me')

def a_function(x: int, y: int, z:int = 10):
    return x + y + z

pn.Param([a_function, button], parameters=["x", "y", "clicks"], show_name=False)

Or, on second thought, I don't really know what combining multiple objects will do, so actually maybe not combining them, but just be able to wrap a function.

import panel as pn
pn.extension()

def a_function(x: int, y: int, z:int = 10):
    return x + y + z

pn.Param(a_function, parameters=["x", "y"], show_name=False)

ahuang11 avatar Apr 15 '24 17:04 ahuang11

Thanks for making that proposal explicit. That said, I'm -100 on overloading pn.Param like that 😃

philippjfr avatar Apr 15 '24 17:04 philippjfr

Sorry I guess I should have considered ParamFunction too. There I could sort of see it but still think it could be quite unexpected.

philippjfr avatar Apr 15 '24 17:04 philippjfr

For what it's worth, I find Maxim's alternative with pn.bind easier to understand than the pn.interact code. I guess it is less elegant and more verbose though.

Coderambling avatar Apr 15 '24 21:04 Coderambling

For completeness, we discussed two other alternatives. Possible today (though untested):

from panel.widgets.widget import widget as w

widgets = dict(a=w(1.9), b=w(1.9), c=w(1.9), d=w(0.8),
               n=pn.widgets.IntSlider(default=1000000, range=(1,20000000)),
               colormap=w(ps['kbc']))

pn.bind(clifford_plot, **widgets)

(shorter than explicitly listing all widgets, but more mysterious, and requires a lot of explanation). Or, if we add a function to inspect the function signature the way interact does, but use bind (or .rx):

widgets = pn.widgets(clifford_plot, n=(1,20000000), colormap=ps)
pn.Column(pn.bind(clifford_plot, **widgets), widgets)

Still pretty obscure, and not easy to see how to motify it, but at least avoids adding a fundamentally new API like interact.

I guess one final option: keep interact, but explicitly turn it into a macro, built up out of other calls like shown here. Then if we introduce it, we can say that it's nothing special, just a handy wrapper that's useful in a notebook or command line. Today we can't say that, but I don't think it's a major job to rewrite it using separate functions for synthesizing widgets from values (already useful for pn.rx) plus pn.bind or pn.rx. That might be my vote unless someone comes up with something better!

jbednar avatar Apr 16 '24 01:04 jbednar

Hm. I don't have a complete picture of this, but from my perspective, there are already so many parts to Panel that soft-deprecating something (in combination with the macro approach?) seems to be a positive thing. As long as there are good alternatives of course.

Coderambling avatar Apr 16 '24 20:04 Coderambling

Definitely. I think we all agree that we don't think pn.interact fits in, and we do want to reduce the visible API surface of panel, particularly for new users. But we don't seem to have come up with a very good alternative to pn.interact. :-(

jbednar avatar Apr 17 '24 00:04 jbednar