Skip to content

Commit

Permalink
Implement Widget.from_values
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Jan 21, 2025
1 parent 2c51805 commit f959255
Show file tree
Hide file tree
Showing 10 changed files with 259 additions and 39 deletions.
8 changes: 8 additions & 0 deletions doc/how_to/components/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ How to access Pane Type.
How to access and set widget values.
:::

:::{grid-item-card} {octicon}`telescope;2.5em;sd-mr-1 sd-animate-grow50` Construct Widget from Data
:link: widget_values
:link-type: doc

How to automatically infer widget parameters from data values.
:::

:::{grid-item-card} {octicon}`file-diff;2.5em;sd-mr-1 sd-animate-grow50` Add or Remove Components
:link: add_remove
:link-type: doc
Expand All @@ -42,5 +49,6 @@ How to add or remove components from a panel.
construct_panes
pane_type
widget_values
widget_from_values
add_remove
```
2 changes: 1 addition & 1 deletion doc/how_to/interactivity/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Discover how to bind parameters, widgets, and bound functions to components.
:link: hvplot_interactive
:link-type: doc

How to use `hvplot.interactive` with widgets to make your data workflows interactive
How to use `param.rx` with widgets to make your data workflows interactive
:::

::::
Expand Down
37 changes: 25 additions & 12 deletions panel/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,31 @@ def dataframe():
}, index=[1, 2, 3], columns=['int', 'float', 'str'])


@pytest.fixture
def df_mixed():
df = pd.DataFrame({
'int': [1, 2, 3, 4],
'float': [3.14, 6.28, 9.42, -2.45],
'str': ['A', 'B', 'C', 'D'],
'bool': [True, True, True, False],
'date': [dt.date(2019, 1, 1), dt.date(2020, 1, 1), dt.date(2020, 1, 10), dt.date(2019, 1, 10)],
'datetime': [dt.datetime(2019, 1, 1, 10), dt.datetime(2020, 1, 1, 12), dt.datetime(2020, 1, 10, 13), dt.datetime(2020, 1, 15, 13)]
}, index=['idx0', 'idx1', 'idx2', 'idx3'])
return df


@pytest.fixture
def df_multiindex(df_mixed):
df_mi = df_mixed.copy()
df_mi.index = pd.MultiIndex.from_tuples([
('group0', 'subgroup0'),
('group0', 'subgroup1'),
('group1', 'subgroup0'),
('group1', 'subgroup1'),
], names=['groups', 'subgroups'])
return df_mi


@pytest.fixture
def hv_bokeh():
import holoviews as hv
Expand Down Expand Up @@ -544,18 +569,6 @@ def eh(exception):
config.exception_handler = old_eh


@pytest.fixture
def df_mixed():
df = pd.DataFrame({
'int': [1, 2, 3, 4],
'float': [3.14, 6.28, 9.42, -2.45],
'str': ['A', 'B', 'C', 'D'],
'bool': [True, True, True, False],
'date': [dt.date(2019, 1, 1), dt.date(2020, 1, 1), dt.date(2020, 1, 10), dt.date(2019, 1, 10)],
'datetime': [dt.datetime(2019, 1, 1, 10), dt.datetime(2020, 1, 1, 12), dt.datetime(2020, 1, 10, 13), dt.datetime(2020, 1, 15, 13)]
}, index=['idx0', 'idx1', 'idx2', 'idx3'])
return df

@pytest.fixture
def df_strings():
descr = [
Expand Down
12 changes: 0 additions & 12 deletions panel/tests/ui/widgets/test_tabulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,18 +71,6 @@ def df_mixed_as_string():
"""


@pytest.fixture
def df_multiindex(df_mixed):
df_mi = df_mixed.copy()
df_mi.index = pd.MultiIndex.from_tuples([
('group0', 'subgroup0'),
('group0', 'subgroup1'),
('group1', 'subgroup0'),
('group1', 'subgroup1'),
], names=['groups', 'subgroups'])
return df_mi


def count_per_page(count: int, page_size: int):
"""
>>> count_per_page(12, 7)
Expand Down
78 changes: 78 additions & 0 deletions panel/tests/widgets/test_select.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import numpy as np
import pandas as pd
import pytest

from panel.layout import GridBox, Row
Expand Down Expand Up @@ -39,6 +40,32 @@ def test_select_text_option_with_equality(widget):
select.value = 'DEF'
assert select.value == 'DEF'

def test_select_from_list(document, comm):
select = Select.from_values(['A', 'B', 'A', 'B', 'C'])

assert select.options == ['A', 'B', 'C']
assert select.value == 'A'

def test_select_from_array(document, comm):
select = Select.from_values(np.array(['A', 'B', 'A', 'B', 'C']))

assert select.options == ['A', 'B', 'C']
assert select.value == 'A'

def test_select_from_index(document, comm):
select = Select.from_values(pd.Index(['A', 'B', 'A', 'B', 'C'], name='index'))

assert select.options == ['A', 'B', 'C']
assert select.value == 'A'
assert select.name == 'index'

def test_select_from_series(document, comm):
select = Select.from_values(pd.Series(['A', 'B', 'A', 'B', 'C'], name='Series'))

assert select.options == ['A', 'B', 'C']
assert select.value == 'A'
assert select.name == 'Series'

def test_select(document, comm):
opts = {'A': 'a', '1': 1}
select = Select(options=opts, value=opts['1'], name='Select')
Expand Down Expand Up @@ -275,6 +302,31 @@ def test_nested_select_defaults(document, comm):
assert select._max_depth == 3


def test_nested_select_from_multi_index(df_multiindex):
select = NestedSelect.from_values(df_multiindex.index)

assert select.options == {
'group0': ['subgroup0', 'subgroup1'],
'group1': ['subgroup0', 'subgroup1'],
}
assert select.value == {'groups': 'group0', 'subgroups': 'subgroup0'}
assert select._max_depth == 2
assert select.levels == ['groups', 'subgroups']

def test_nested_select_from_index():
select = NestedSelect.from_values(pd.Index(['A', 'B', 'A', 'B', 'C'], name='index'))

assert select.options == ['A', 'B', 'C']
assert select.value == {'index': 'A'}
assert select._max_depth == 1

def test_nested_select_from_series():
select = NestedSelect.from_values(pd.Series(['A', 'B', 'A', 'B', 'C'], name='Series'))

assert select.options == ['A', 'B', 'C']
assert select.value == {'Series': 'A'}
assert select._max_depth == 1

def test_nested_select_init_value(document, comm):
options = {
"Andrew": {
Expand Down Expand Up @@ -856,6 +908,32 @@ def test_select_disabled_options_set_value_and_disabled_options(options, size, d
assert widget.disabled_options == [10]


def test_multi_select_from_list():
select = MultiSelect.from_values(['A', 'B', 'A', 'B', 'C'])

assert select.options == ['A', 'B', 'C']
assert select.value == []

def test_multi_select_from_array():
select = MultiSelect.from_values(np.array(['A', 'B', 'A', 'B', 'C']))

assert select.options == ['A', 'B', 'C']
assert select.value == []

def test_multi_select_from_index():
select = MultiSelect.from_values(pd.Index(['A', 'B', 'A', 'B', 'C'], name='index'))

assert select.options == ['A', 'B', 'C']
assert select.value == []
assert select.name == 'index'

def test_multi_select_from_series(document, comm):
select = MultiSelect.from_values(pd.Series(['A', 'B', 'A', 'B', 'C'], name='Series'))

assert select.options == ['A', 'B', 'C']
assert select.value == []
assert select.name == 'Series'

def test_multi_select(document, comm):
select = MultiSelect(options={'A': 'A', '1': 1, 'C': object},
value=[object, 1], name='Select')
Expand Down
46 changes: 46 additions & 0 deletions panel/tests/widgets/test_slider.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from datetime import date, datetime

import numpy as np
import pandas as pd
import pytest

from bokeh.models import (
Expand All @@ -15,6 +16,28 @@
)


def test_float_slider_from_list():
slider = FloatSlider.from_values([1.1, 2.2])

assert slider.start == 1.1
assert slider.end == 2.2
assert slider.value == 1.1

def test_float_slider_from_array():
slider = FloatSlider.from_values(np.array([1.1, 2.2]))

assert slider.start == 1.1
assert slider.end == 2.2
assert slider.value == 1.1

def test_float_slider_from_series():
slider = FloatSlider.from_values(pd.Series([1.1, 2.2], name='Series'))

assert slider.start == 1.1
assert slider.end == 2.2
assert slider.value == 1.1
assert slider.name == 'Series'

def test_float_slider(document, comm):

slider = FloatSlider(start=0.1, end=0.5, value=0.4, name='Slider')
Expand Down Expand Up @@ -84,6 +107,29 @@ def test_int_slider(document, comm):
assert widget.value == 2



def test_range_slider_from_list():
slider = RangeSlider.from_values([1.1, 2.2])

assert slider.start == 1.1
assert slider.end == 2.2
assert slider.value == (1.1, 2.2)

def test_range_slider_from_array():
slider = RangeSlider.from_values(np.array([1.1, 2.2]))

assert slider.start == 1.1
assert slider.end == 2.2
assert slider.value == (1.1, 2.2)

def test_range_slider_from_series():
slider = RangeSlider.from_values(pd.Series([1.1, 2.2], name='Series'))

assert slider.start == 1.1
assert slider.end == 2.2
assert slider.value == (1.1, 2.2)
assert slider.name == 'Series'

def test_range_slider(document, comm):

slider = RangeSlider(start=0., end=3, value=(0, 3), name='Slider')
Expand Down
10 changes: 10 additions & 0 deletions panel/util/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,16 @@ def safe_next():
break
yield value

def unique_iterator(seq):
"""
Returns an iterator containing all non-duplicate elements
in the input sequence.
"""
seen = set()
for item in seq:
if item not in seen:
seen.add(item)
yield item

def prefix_length(a: str, b: str) -> int:
"""
Expand Down
41 changes: 41 additions & 0 deletions panel/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
TYPE_CHECKING, Any, ClassVar, TypeVar,
)

import numpy as np
import param # type: ignore

from bokeh.models import ImportedStyleSheet, Tooltip
Expand All @@ -21,6 +22,7 @@
from .._param import Margin
from ..layout.base import Row
from ..reactive import Reactive
from ..util import unique_iterator
from ..viewable import Layoutable, Viewable

if TYPE_CHECKING:
Expand Down Expand Up @@ -69,6 +71,45 @@ def from_param(cls: type[T], parameter: param.Parameter, **params) -> T:
)
return layout[0]

@classmethod
def _infer_params(cls, values, **params):
if 'name' not in params and getattr(values, 'name', None):
params['name'] = values.name
if 'start' in cls.param and 'start' not in params:
params['start'] = np.nanmin(values)
if 'end' in cls.param and 'end' not in params:
params['end'] = np.nanmax(values)
if 'options' in cls.param and 'options' not in params:
if isinstance(values, dict):
params['options'] = values
else:
params['options'] = list(unique_iterator(values))
if 'value' not in params:
p = cls.param['value']
if isinstance(p, param.Tuple):
params['value'] = (params['start'], params['end'])
elif 'start' in params:
params['value'] = params['start']
elif ('options' in params and not isinstance(p, (param.List, param.ListSelector))
and not getattr(cls, '_allows_none', False)):
params['value'] = params['options'][0]
return params

@classmethod
def from_values(cls, values, **params):
"""
Creates an instance of this Widget where the parameters are
inferred from the data.
Arguments
---------
values: Iterable
The values to infer the parameters from.
params: dict
Additional parameters to pass to the widget.
"""
return cls(**cls._infer_params(values, **params))

@property
def rx(self):
return self.param.value.rx
Expand Down
Loading

0 comments on commit f959255

Please sign in to comment.