Skip to content

Commit 60e1f24

Browse files
committed
New df property and setter, make some traits private
1 parent 194dce3 commit 60e1f24

File tree

5 files changed

+157
-116
lines changed

5 files changed

+157
-116
lines changed

docs/ipywidgets.md

Lines changed: 64 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -16,103 +16,102 @@ kernelspec:
1616

1717
ITables is available as a [Jupyter Widget](https://ipywidgets.readthedocs.io) since v2.2.
1818

19-
## Using `show`
19+
## The `ITable` widget
2020

21-
If you only want to _display_ the table, you **do not need**
22-
our Jupyter widget. The `show` function is enough!
21+
The `ITable` widget has a few dependencies that you can install with
22+
```bash
23+
pip install itables[widget]
24+
```
2325

24-
```{code-cell}
25-
import ipywidgets as widgets
26+
The `ITable` class accepts the same arguments as the `show` method, but
27+
the `df` argument is optional.
2628

27-
from itables import show
29+
```{code-cell}
2830
from itables.sample_dfs import get_dict_of_test_dfs
31+
from itables.widget import ITable
2932
30-
sample_dfs = get_dict_of_test_dfs()
31-
33+
df = get_dict_of_test_dfs()["int_float_str"]
3234
33-
def use_show_in_interactive_output(table_name: str):
34-
show(
35-
sample_dfs[table_name],
36-
caption=table_name,
37-
style="table-layout:auto;width:auto;float:left;caption-side:bottom",
38-
)
35+
table = ITable(df, selected_rows=[0, 2, 5], select=True)
36+
table
37+
```
3938

39+
## The `selected_rows` traits
4040

41-
table_selector = widgets.Dropdown(options=sample_dfs.keys(), value="int_float_str")
42-
out = widgets.interactive_output(
43-
use_show_in_interactive_output, {"table_name": table_selector}
44-
)
41+
The `selected_rows` attribute of the `ITable` object provides a view on the
42+
rows that have been selected in the table (remember to pass `select=True`
43+
to activate the row selection). You can use it to either retrieve
44+
or change the current row selection:
4545

46-
widgets.VBox([table_selector, out])
46+
```{code-cell}
47+
table.selected_rows
4748
```
4849

49-
```{tip}
50-
Jupyter widgets only work in a live notebook.
51-
Click on the rocket icon at the top of the page to run this demo in Binder.
50+
```{code-cell}
51+
table.selected_rows = [3, 4]
5252
```
5353

54-
## Using the ITable widget
55-
56-
The `ITable` widget has a few dependencies that you can install with
57-
```bash
58-
pip install itables[widget]
59-
```
54+
## The `df` property
6055

61-
The `ITable` class accepts the same arguments as the `show` method, but
62-
the `df` argument is optional.
56+
Use it to retrieve the table data:
6357

6458
```{code-cell}
65-
from itables.widget import ITable
66-
67-
table = ITable(selected_rows=[0, 2, 5, 99])
59+
table.df.iloc[table.selected_rows]
60+
```
6861

62+
or to update it
6963

70-
def update_selected_table(change):
71-
table_name = table_selector.value
72-
table.update(
73-
sample_dfs[table_name],
74-
caption=table_name,
75-
select=True,
76-
style="table-layout:auto;width:auto;float:left",
77-
)
64+
```{code-cell}
65+
table.df = df.head(6)
66+
```
7867

68+
```{tip}
69+
`ITable` will raise an `IndexError` if the `selected_rows` are not consistent with the
70+
updated data. If you need to update the two simultaneously, use `table.update(df, selected_rows=...)`, see below.
71+
```
7972

80-
# Update the table when the selector changes
81-
table_selector.observe(update_selected_table, "value")
73+
## The `caption`, `style` and `classes` traits
8274

83-
# Set the table to the initial table selected
84-
update_selected_table(None)
75+
You can update these traits from Python, e.g.
8576

86-
widgets.VBox([table_selector, table])
77+
```{code-cell}
78+
table.caption = "numbers and strings"
8779
```
8880

89-
## Get the selected rows
81+
## The `update` method
9082

91-
The `ITable` widget let you access the state of the table
92-
and in particular, it has an `.selected_rows` attribute
93-
that you can use to determine the rows that have been
94-
selected by the user (allow selection by passing `select=True`
95-
to the `ITable` widget).
83+
Last but not least, you can update the `ITable` arguments simultaneously using the `update` method:
9684

9785
```{code-cell}
98-
out = widgets.Output()
86+
table.update(df.head(20), selected_rows=[7, 8])
87+
```
9988

89+
## Limitations
10090

101-
def show_selected_rows(change):
102-
with out:
103-
out.clear_output()
104-
print("selected_rows: ", table.selected_rows)
91+
Compared to `show`, the `ITable` widget has the same limitations as the [streamlit component](streamlit.md#limitations),
92+
e.g. structured headers are not available, you can't pass JavaScript callback, etc.
10593

94+
The good news is that if you only want to _display_ the table, you **do not need**
95+
the `ITables` widget. Below is an example in which we use `show` to display a different
96+
table depending on the value of a drop-down component:
10697

107-
table.observe(show_selected_rows, "selected_rows")
98+
```python
99+
import ipywidgets as widgets
100+
from itables import show
101+
from itables.sample_dfs import get_dict_of_test_dfs
108102

109-
# Display the initial selection
110-
show_selected_rows(None)
103+
def use_show_in_interactive_output(table_name: str):
104+
show(
105+
sample_dfs[table_name],
106+
caption=table_name,
107+
)
111108

112-
out
113-
```
109+
sample_dfs = get_dict_of_test_dfs()
110+
table_selector = widgets.Dropdown(options=sample_dfs.keys(), value="int_float_str")
114111

115-
## Limitations
112+
out = widgets.interactive_output(
113+
use_show_in_interactive_output, {"table_name": table_selector}
114+
)
116115

117-
Compared to `show`, the `ITable` widget has the same limitations as the [streamlit component](streamlit.md#limitations),
118-
e.g. structured headers are not available, you can't pass JavaScript callback, etc.
116+
widgets.VBox([table_selector, out])
117+
```

docs/streamlit.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,3 @@ A sample application is available at https://to-html-datatable.streamlit.app (so
4949
<iframe src="https://to-html-datatable.streamlit.app?embed=true"
5050
style="height: 600px; width: 100%;"></iframe>
5151
```
52-
53-
## Future developments
54-
55-
ITables' Streamlit component might see the following developments in the future
56-
- Return the selected cells
57-
- Make the table editable (will require a DataTable [editor license](https://editor.datatables.net/purchase/))

packages/itables_anywidget/js/widget.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,31 +52,32 @@ function render({ model, el }: RenderContext<WidgetModel>) {
5252
function set_selected_rows_from_model() {
5353
// We use this variable to avoid triggering model updates!
5454
setting_selected_rows_from_model = true;
55-
DataTable.set_selected_rows(dt, model.get('filtered_row_count'), model.get('selected_rows'));
55+
DataTable.set_selected_rows(dt, model.get('_filtered_row_count'), model.get('selected_rows'));
5656
setting_selected_rows_from_model = false;
5757
};
5858

5959
function create_table(destroy = false) {
60-
let dt_args = model.get('dt_args');
6160
if (destroy) {
6261
dt.destroy();
6362
jQuery(table).empty();
6463
}
6564

65+
let dt_args = model.get('_dt_args');
66+
dt_args['data'] = model.get('_data');
67+
dt_args['columns'] = model.get('_columns');
6668
dt_args["fnInfoCallback"] = function (oSettings: any, iStart: number, iEnd: number, iMax: number, iTotal: number, sPre: string) {
67-
let msg = model.get("downsampling_warning");
69+
let msg = model.get("_downsampling_warning");
6870
if (msg)
69-
return sPre + ' (' + model.get("downsampling_warning") + ')';
71+
return sPre + ' (' + msg + ')';
7072
else
7173
return sPre;
7274
}
73-
dt_args['data'] = model.get('data');
7475
dt = new DataTable(table, dt_args);
76+
set_selected_rows_from_model();
7577
}
7678
create_table();
77-
set_selected_rows_from_model();
7879

79-
model.on('change:destroy_and_recreate', () => {
80+
model.on('change:_destroy_and_recreate', () => {
8081
create_table(true);
8182
});
8283

@@ -86,7 +87,7 @@ function render({ model, el }: RenderContext<WidgetModel>) {
8687
if (setting_selected_rows_from_model)
8788
return;
8889

89-
model.set('selected_rows', DataTable.get_selected_rows(dt, model.get('filtered_row_count')));
90+
model.set('selected_rows', DataTable.get_selected_rows(dt, model.get('_filtered_row_count')));
9091
model.save_changes();
9192
};
9293

src/itables/javascript.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -486,11 +486,15 @@ def get_itables_extension_arguments(df, caption=None, selected_rows=None, **kwar
486486
"Pandas style objects can't be used with the extension"
487487
)
488488

489+
if df is None:
490+
df = pd.DataFrame()
491+
489492
set_default_options(
490493
kwargs,
491494
use_to_html=False,
492-
context="the streamlit extension",
495+
context="the itable widget or streamlit extension",
493496
not_available=[
497+
"columns",
494498
"tags",
495499
"dt_url",
496500
"pre_dt_code",

src/itables/widget/__init__.py

Lines changed: 79 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import pathlib
33

44
import anywidget
5-
import pandas as pd
65
import traitlets
76

87
from itables.javascript import get_itables_extension_arguments
@@ -17,62 +16,106 @@ class ITable(anywidget.AnyWidget):
1716
_esm = pathlib.Path(__file__).parent / "static" / "widget.js"
1817
_css = pathlib.Path(__file__).parent / "static" / "widget.css"
1918

20-
data = traitlets.List(traitlets.List()).tag(sync=True)
21-
filtered_row_count = traitlets.Int().tag(sync=True)
22-
selected_rows = traitlets.List(traitlets.Int).tag(sync=True)
23-
destroy_and_recreate = traitlets.Int(0).tag(sync=True)
24-
19+
# public traits
2520
caption = traitlets.Unicode().tag(sync=True)
2621
classes = traitlets.Unicode().tag(sync=True)
2722
style = traitlets.Unicode().tag(sync=True)
28-
downsampling_warning = traitlets.Unicode().tag(sync=True)
29-
dt_args = traitlets.Dict().tag(sync=True)
23+
selected_rows = traitlets.List(traitlets.Int).tag(sync=True)
24+
25+
# private traits that relate to df or to the DataTable arguments
26+
# (use .update() to update them)
27+
_data = traitlets.List(traitlets.List()).tag(sync=True)
28+
_columns = traitlets.List(traitlets.Dict()).tag(sync=True)
29+
_filtered_row_count = traitlets.Int().tag(sync=True)
30+
_downsampling_warning = traitlets.Unicode().tag(sync=True)
31+
_dt_args = traitlets.Dict().tag(sync=True)
32+
_destroy_and_recreate = traitlets.Int(0).tag(sync=True)
3033

3134
def __init__(self, df=None, caption=None, selected_rows=None, **kwargs) -> None:
3235
super().__init__()
3336

34-
if df is None:
35-
df = pd.DataFrame()
36-
3737
dt_args, other_args = get_itables_extension_arguments(
3838
df, caption, selected_rows, **kwargs
3939
)
40-
self.data = dt_args.pop("data")
41-
self.dt_args = dt_args
40+
self._df = df
41+
self.caption = other_args.pop("caption") or ""
4242
self.classes = other_args.pop("classes")
4343
self.style = other_args.pop("style")
44-
self.caption = other_args.pop("caption") or ""
45-
self.downsampling_warning = other_args.pop("downsampling_warning") or ""
4644
self.selected_rows = other_args.pop("selected_rows") or []
47-
self.filtered_row_count = other_args.pop("filtered_row_count", 0)
45+
46+
self._data = dt_args.pop("data")
47+
self._columns = dt_args.pop("columns")
48+
self._dt_args = dt_args
49+
self._downsampling_warning = other_args.pop("downsampling_warning") or ""
50+
self._filtered_row_count = other_args.pop("filtered_row_count", 0)
4851
assert not other_args, other_args
4952

5053
def update(self, df=None, caption=None, selected_rows=None, **kwargs):
54+
"""
55+
Update either the table data, attributes, or the arguments passed
56+
to DataTable. Arguments that are not mentioned
57+
"""
58+
data_or_dt_args_changed = False
59+
for key, value in list(kwargs.items()):
60+
if value is None:
61+
data_or_dt_args_changed = True
62+
self._dt_args.pop(key, None)
63+
del kwargs[key]
64+
65+
if df is None:
66+
df = self._df
67+
if selected_rows is None:
68+
selected_rows = self.selected_rows
69+
if caption is None:
70+
caption = self.caption
71+
if "classes" not in kwargs:
72+
kwargs["classes"] = self.classes
73+
if "style" not in kwargs:
74+
kwargs["style"] = self.style
75+
5176
dt_args, other_args = get_itables_extension_arguments(
5277
df, caption, selected_rows, **kwargs
5378
)
5479

55-
if df is not None:
56-
data = dt_args.pop("data")
57-
self.downsampling_warning = other_args.pop("downsampling_warning") or ""
58-
self.filtered_row_count = other_args.pop("filtered_row_count", 0)
59-
if self.dt_args != dt_args:
60-
self.dt_args = dt_args
61-
if self.data != data:
62-
self.data = data
63-
else:
64-
data = dt_args.pop("data")
65-
if "columns" not in dt_args:
66-
dt_args["columns"] = self.dt_args["columns"]
67-
if self.dt_args != dt_args:
68-
self.dt_args = dt_args
69-
7080
self.classes = other_args.pop("classes")
7181
self.style = other_args.pop("style")
72-
self.caption = other_args.pop("caption") or ""
82+
self.caption = other_args.pop("caption")
83+
84+
if df is None:
85+
del dt_args["data"]
86+
del dt_args["columns"]
87+
88+
# Don't trigger an update if nor data nor the dt args changed
89+
data_or_dt_args_changed = data_or_dt_args_changed or self._update_dt_args(
90+
dt_args
91+
)
92+
else:
93+
self._df = df
94+
self._data = dt_args.pop("data")
95+
self._columns = dt_args.pop("columns")
96+
self._update_dt_args(dt_args)
97+
self._downsampling_warning = other_args.pop("downsampling_warning") or ""
98+
self._filtered_row_count = other_args.pop("filtered_row_count", 0)
99+
data_or_dt_args_changed = True
100+
101+
if data_or_dt_args_changed:
102+
self._destroy_and_recreate += 1
103+
104+
self.selected_rows = other_args.pop("selected_rows")
105+
106+
def _update_dt_args(self, dt_args):
107+
changed = False
108+
for key, value in dt_args.items():
109+
if key not in self._dt_args or (self._dt_args[key] != value):
110+
self._dt_args[key] = value
111+
changed = True
112+
113+
return changed
73114

74-
selected_rows = other_args.pop("selected_rows")
75-
if selected_rows is not None and self.selected_rows != selected_rows:
76-
self.selected_rows = selected_rows
115+
@property
116+
def df(self):
117+
return self._df
77118

78-
self.destroy_and_recreate += 1
119+
@df.setter
120+
def df(self, df):
121+
self.update(df)

0 commit comments

Comments
 (0)