-
-
Notifications
You must be signed in to change notification settings - Fork 531
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Need a more flexible Number widget #1128
Comments
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 #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 #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 :-) |
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.
Unfortunately not. In the dummy example I provided, the @jbednar thanks for the link to your proposal. Before that I had never tried 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.
This was discussed over at Bokeh bokeh/bokeh#6173 / bokeh/bokeh#7157 which led to PR bokeh/bokeh#8678 which implemented Spinner but AFAIK they dont work with floats. |
I've opened such an issue on Bokeh (bokeh/bokeh#9943); feel free to chime in there. |
@Jhsmit You can see in the docs that you can specify a |
Thanks, I think my problem with the spinner was that I didn't set the step properly. 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. |
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 |
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) |
That looks like it does what I would like to have. The bounds are given by the |
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 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 |
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 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.The text was updated successfully, but these errors were encountered: