Open maximlt opened 4 years ago
Hi @maximlt
For a contribution the first thing would be to search (google, npm, alternative frameworks like ipywidgets, streamlit, dash, bokeh or shiny or ?) and identify suitable candidates for widgets. I don't know exactly what the criteria are but I guess one is bundle size.
Then you could add a pull request and implement the python side of the widget.
A way to get started is to identify a similar Panel widget and its panel widget .py and bokeh model .py file. Then implement something similar for the new widget. See
If there are questions reach out on gitter where the developers hang around.
A simpler, python only alternative is to use the upcoming web component functionality https://github.com/holoviz/panel/pull/1122.
The caveat is that the widgets created don't support jslinking and export to a static html file because there is no js implementation. But they work very well with a server or in a notebook.
In the pull request https://github.com/holoviz/panel/pull/1122 you will find lots of examples of implementations of the wired js widgets.
I dream of Panel also supporting
You could seach (google, npm, https://www.webcomponents.org/, material or vaadin) for a suitable component and then try implementing it.
There are some jupyter notebooks in the pull request that tries to explain how to do this.
And implementing a web component, python only widget is also a big stepping stone towards implementing a more traditional Panel widget that support jslinking and export to a static site.
So if you feel Panel would be even more awesome with more widgets its just to get started :-)
The instantiated Spinner has a step value of 1 which means that it's not possible to interactivaly set that widget value to a decimal number.
You should be able to type in any number you like already, right?
In any case, how I think numeric widgets should work is to have both soft and hard bounds available, with a slider covering up to the soft bound but allowing extension to the hard bound (if any). See my proposal for this for ipywidgets, which was never implemented. I think this approach gives maximum utility and flexibility, especially when combined with interact-like guessing of initial soft bounds if none are provided. I'm not sure how hard this would be to achieve with Bokeh widgets, but I do think we should do it in Bokeh rather than add a separate library just for this case.
Thank you both for you answers, that helps me a lot to understand what's going on.
You should be able to type in any number you like already, right?
Unfortunately not. In the dummy example I provided, the Spinner
widget gets a default step
value of 1
(see here) It's not possible to enter 2.95
for instance, the number gets rounded to 3
.
@jbednar thanks for the link to your proposal. Before that I had never tried ipywidgets
and decided to try its FloatSlider
for the first time. I found out that its behaviour is very close to what I'd like to get, the widget numeric value is displayed in a entry box, so the value can be set either through that entry or with the slider. I believe your proposal is right, having both softbounds and hardbounds is required for numeric values, as in theory most quantities have an open-ended bound (temperature, weight, etc.).
It'd be really nice if Bokeh could implement this feature. Till then, I'll probably use the following workaround.
import param
import panel as pn
class Dummy(param.Parameterized):
amount = param.Number(default=1,step=0.01, bounds=(0, None), softbounds=(0, 50))
dm = Dummy()
fs = pn.Param(dm.param.amount, widgets={"amount": {"show_value": False}})
s = pn.Param(dm.param.amount, widgets={"amount": {"type": pn.widgets.Spinner, "name": "", "width":80}})
numeric_input = pn.Row(fs, s)
numeric_input.app()
@jbednar Do you think I should open a feature request on the GitHub page of Bokeh instead of here?
I'm currently using LiteralInput for this but its too flexible because users can enter strings or out-of-bounds values.
Ideally I'd like a bounded field where you can type values like '1.23e-5' or '1', '124.45', and which doesnt accept anything thats out of bounds.
pn.param.LiteralInputTyped
almost does the job except for bounds.
This was discussed over at Bokeh https://github.com/bokeh/bokeh/issues/6173 / https://github.com/bokeh/bokeh/pull/7157 which led to PR https://github.com/bokeh/bokeh/pull/8678 which implemented Spinner but AFAIK they dont work with floats.
@maximlt Do you think I should open a feature request on the GitHub page of Bokeh instead of here?
I've opened such an issue on Bokeh (https://github.com/bokeh/bokeh/issues/9943); feel free to chime in there.
@Jhsmit You can see in the docs that you can specify a type
attribute of a LiteralInput
widget (https://panel.holoviz.org/reference/widgets/LiteralInput.html#widgets-gallery-literalinput). It doesn't allow you to specify bounds though.
Thanks, I think my problem with the spinner was that I didn't set the step properly. With this set sufficiently low its doing mostly what I want.
Some suggestions:
These are mostly personal preferences, but having control over these would be nice.
Yes actually I find the spinner to be quite a weird widget, I'd rather have a more powerful LiteralInput widget, which can turn into a Spinner if I set a step.
Yes actually I find the spinner to be quite a weird widget, I'd rather have a more powerful LiteralInput widget, which can turn into a Spinner if I set a step.
Same here! The spinner doesn't work for our applications. This is the only reason why we hesitate to use panels for our tools!
Thanks for all the great comments in here. I agree with what @maximlt said, 90% of my use cases would ideally require fine-tuned in/output of numbers. For the time being, I made a PR that allows specifying LiteralInput
(or anything else) for a param.Number
even if no step
or bounds
are specified.
If I understand correctly you are looking for a widget like this? If yes I could start a PR to discuss a better name and API for it
Nice! Something like that would work for me, assuming:
The extra text display in the second display is just for demonstation of the link between the widget and an other panel (in this case a statictext) Here the the biderectionnal is implicit because I display 2 times the same widget => one panel instance (s) and 2 bokeh models 1 for each display
That looks like it does what I would like to have. The bounds are given by the hard_end
and hard_start
parameters?
I agree with @jbednar that the width is important, it should not look out of place if you put it in a column with selectors or other widgets. Along those lines, would it be possible to not show the slider? It is a nice functionality to have but if space is limited and high accuracy is needed, I might want to leave it out.
Ok so this is another issue personally if you don't want the slider you can try something like this (code to put in input.py of panel.panel.widgets):
class NumericInput(Widget):
value = param.Number(default=0, allow_None=True, bounds=[None, None])
placeholder = param.Number(default=None)
start = param.Number(default=None, allow_None=True)
end = param.Number(default=None, allow_None=True)
_widget_type = _BkTextInput
formatter = param.Parameter(default=None)
_rename = {'formatter': None, 'start': None, 'end': None}
def _bound_value(self, value):
if self.start is not None:
value = max(value, self.start)
if self.end is not None:
value = min(value, self.end)
return value
def _format_value(self, value):
if self.formatter is not None:
value = self.formatter.format(value)
else:
value = str(value)
return value
def _process_param_change(self, msg):
msg.pop('formatter', None)
if 'start' in msg:
start = msg.pop('start')
self.param.value.bounds[0] = start
if 'end' in msg:
end = msg.pop('end')
self.param.value.bounds[1] = end
if 'value' in msg and msg['value'] is not None:
msg['value'] = self._format_value(self.value)
if 'placeholder' in msg and msg['placeholder'] is not None:
msg['placeholder'] = self._format_value(self.placeholder)
return msg
def _process_property_change(self, msg):
if 'value' in msg and msg['value'] is not None:
try:
value = float(msg['value'])
msg['value'] = self._bound_value(value)
if msg['value'] != value:
self.param.trigger('value')
except ValueError:
msg.pop('value')
if 'placeholder' in msg and msg['placeholder'] is not None:
try:
msg['placeholder'] = self._format_value(float(msg['placeholder']))
except ValueError:
msg.pop('placeholder')
return msg
Great, that does exactly what I was looking for.
I'd like to be able to this as well:
class A(param.Parameterized):
my_number = param.Number(default=10)
def panel(self):
return pn.Param(self.param, widgets={'my_number': {'type': pn.widgets.input.NumericInput, 'formatter': None}})
Which requires some changes in param.py
as well because currently although if you specify this widget if the param is a Number
you get a spinner regardless.
This should probably be in a separate PR?
By the way why doesnt your spinner show the spinning buttons in your example?
I need to hover it to make the spinning button appear the html5 input number look is browser dependant and concerning the mapping of param and widgets it should be address in https://github.com/holoviz/panel/pull/1322
That looks indeed a lot better than what we have now! 👍 I need to give it a proper try. One question, is it possible to set the slider step?
@xavArtley I've modified the NumericInput
example you provided by copying parts of Literalnput
such that now it does display a name from the name
parameter. I don't understand why this code does not show a name and your example does not, however but hey it works.
class NumericInput(pn.widgets.input.Widget):
"""
NumericInput allows input of floats with bounds
"""
type = param.ClassSelector(default=None, class_=(type, tuple),
is_instance=True)
value = param.Number(default=None)
start = param.Number(default=None, allow_None=True)
end = param.Number(default=None, allow_None=True)
_rename = {'name': 'title', 'type': None, 'serializer': None, 'start': None, 'end': None}
_source_transforms = {'value': """JSON.parse(value.replace(/'/g, '"'))"""}
_target_transforms = {'value': """JSON.stringify(value).replace(/,/g, ", ").replace(/:/g, ": ")"""}
_widget_type = _BkTextInput
def __init__(self, **params):
super(NumericInput, self).__init__(**params)
self._state = ''
self._validate(None)
self._callbacks.append(self.param.watch(self._validate, 'value'))
def _validate(self, event):
if self.type is None: return
new = self.value
if not isinstance(new, self.type) and new is not None:
if event:
self.value = event.old
types = repr(self.type) if isinstance(self.type, tuple) else self.type.__name__
raise ValueError('LiteralInput expected %s type but value %s '
'is of type %s.' %
(types, new, type(new).__name__))
def _bound_value(self, value):
if self.start is not None:
value = max(value, self.start)
if self.end is not None:
value = min(value, self.end)
return value
def _process_property_change(self, msg):
if 'value' in msg and msg['value'] is not None:
try:
value = float(msg['value'])
msg['value'] = self._bound_value(value)
if msg['value'] != value:
self.param.trigger('value')
except ValueError:
msg.pop('value')
if 'placeholder' in msg and msg['placeholder'] is not None:
try:
msg['placeholder'] = self._format_value(float(msg['placeholder']))
except ValueError:
msg.pop('placeholder')
return msg
def _process_param_change(self, msg):
msg = super(NumericInput, self)._process_param_change(msg)
if 'start' in msg:
start = msg.pop('start')
self.param.value.bounds[0] = start
if 'end' in msg:
end = msg.pop('end')
self.param.value.bounds[1] = end
if 'value' in msg:
value = '' if msg['value'] is None else msg['value']
value = as_unicode(value)
msg['value'] = value
msg['title'] = self.name
return msg
The problem
When I create a Param-based app that declares a
Number
object with an open-ended bound, the default widget instantiated by panel is aSpinner
. See the code to reproduce that behaviour.This was an unexepected behaviour to me as the
Spinner
widget isn't even introduced on the widgets page.The instantiated
Spinner
has astep
value of 1 which means that it's not possible to interactivaly set that widget value to a decimal number.It is however possible to programmatically set its value to a decimal number. But now the widget doesn't display the exact value but the value rounded according to that step of 1.
What would be really nice to have
In the case of a number that doesn't have any hard-bounds, I'd rather not get a
Spinner
but a more flexible widget, possibly aNumberInput
. This widget would be similar to theTextInput
widget, it would however accept only numeric (int, float) as input.Ideally, a parameter
format
would allow to control how the number is formatted by the widget. The following code would instantiate aNumberInput
widget that could display values such as2.00 €
or2.15 €
.In the end, the
Spinner
widget isn't that bad, and theIntSlider
andFloatSlider
widgets are also useful. I believe though that a more flexible widget would come in handy quite frequently, this is at least what I've observed with the few apps I've built so far.Something else I've tried
I thought the
TextInput
widget would provide me with a simple workaround. Unfortunately, that failed.Bonus Thanks a lot for
panel
, it's a great library! If I can be of any help in implementing this feature (Python only, sorry), please guide me through this work.