Skip to content

Commit d9d4a82

Browse files
authored
Version 0.4.2 (#46)
* Fix for eval_functions=True * Allow blank characters before "function" * Add "Loading..." below the table header * Remove Python 2 specific code * init_notebook_mode(all_interactive=False) restores the default HTML tables
1 parent ce1e7a8 commit d9d4a82

File tree

10 files changed

+898
-652
lines changed

10 files changed

+898
-652
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ exclude: >
55
(?x)^(
66
\.vscode/settings\.json|
77
demo/.*|
8-
tests/notebooks/.*|
8+
index.html
99
)$
1010
repos:
1111

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1+
0.4.2 (2022-01-07)
2+
==================
3+
4+
Fixed
5+
-----
6+
- Fix the HTML output when `eval_functions=True`
7+
- Display "Loading..." under the table header until the table is displayed with datatables.net
8+
- `init_notebook_mode(all_interactive=False)` restores the original Pandas HTML representation.
9+
110
0.4.1 (2022-01-06)
211
==================
312

413
Fixed
514
-------
6-
- Long column names don't overlap any more (#28)
15+
- Long column names don't overlap anymore (#28)
716

817

918
0.4.0 (2022-01-06)

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2019 Marc Wouts
3+
Copyright (c) 2019-2022 Marc Wouts
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Pandas DataFrames and Series as Interactive Tables
1+
# Pandas DataFrames and Series as Interactive DataTables
22

33
[![Pypi](https://img.shields.io/pypi/v/itables.svg)](https://pypi.python.org/pypi/itables)
44
![CI](https://github.com/mwouts/itables/workflows/CI/badge.svg)
@@ -9,7 +9,7 @@
99
[![Lab](https://img.shields.io/badge/Binder-JupyterLab-blue.svg)](https://mybinder.org/v2/gh/mwouts/itables/main?urlpath=lab/tree/README.md)
1010
<a class="github-button" href="https://github.com/mwouts/itables" data-icon="octicon-star" data-show-count="true" aria-label="Star mwouts/itables on GitHub">Star</a>
1111

12-
Turn pandas DataFrames and Series into interactive [datatables](https://datatables.net) in your notebooks with `import itables.interactive`:
12+
Turn pandas DataFrames and Series into interactive [datatables](https://datatables.net) in your notebooks!
1313

1414
![](https://raw.githubusercontent.com/mwouts/itables/main/demo/itables.gif)
1515

@@ -28,17 +28,16 @@ from itables import init_notebook_mode
2828
init_notebook_mode(all_interactive=True)
2929
```
3030

31+
Then any dataframe will be displayed as an interactive [datatables](https://datatables.net) table:
32+
3133
```python
3234
import world_bank_data as wb
3335

3436
df = wb.get_countries()
3537
df
3638
```
3739

38-
You don't see any table above? Please either open the [HTML export](https://mwouts.github.io/itables/) of this notebook, or run this README on [Binder](https://mybinder.org/v2/gh/mwouts/itables/main?urlpath=lab/tree/README.md)!
39-
40-
41-
Or display just one series or dataframe as an interactive table with the `show` function.
40+
If you want to display just one series or dataframe as an interactive table, use `itables.show`:
4241

4342
```python
4443
from itables import show
@@ -47,7 +46,11 @@ x = wb.get_series("SP.POP.TOTL", mrv=1, simplify_index=True)
4746
show(x)
4847
```
4948

50-
# Supported environments
49+
(NB: In Jupyter Notebook and Jupyter NBconvert, you need to call `init_notebook_mode()` before using `show`).
50+
51+
You don't see any table above? Please either open the [HTML export](https://mwouts.github.io/itables/) of this notebook, or run this README on [Binder](https://mybinder.org/v2/gh/mwouts/itables/main?urlpath=lab/tree/README.md)!
52+
53+
## Supported environments
5154

5255
`itables` has been tested in the following editors:
5356
- Jupyter Notebook
@@ -58,8 +61,20 @@ show(x)
5861
- PyCharm (for Jupyter Notebooks)
5962
- Nteract
6063

64+
## Table not loading?
65+
66+
If the table just says "Loading...", then maybe
67+
- You loaded a notebook that is not trusted (run "Trust Notebook" in View / Activate Command Palette)
68+
- Or you are offline?
69+
70+
At the moment `itables` does not have an [offline mode](https://github.com/mwouts/itables/issues/8). While the table data is embedded in the notebook, the `jquery` and `datatables.net` are loaded from a CDN, see our [require.config](https://github.com/mwouts/itables/blob/main/itables/javascript/load_datatables_connected.js) and our [table template](https://github.com/mwouts/itables/blob/main/itables/datatables_template.html), so an internet connection is required to display the tables.
71+
6172
# Advanced usage
6273

74+
As `itables` is mostly a wrapper for the Javascript [datatables.net](https://datatables.net/) library, you should be able to find help on the datatables.net [forum](https://datatables.net/forums/) and [examples](https://datatables.net/examples/index) for most formatting issues.
75+
76+
Below we give a few examples of how the datatables.net examples can be translated to Python with `itables`.
77+
6378
## Row sorting
6479

6580
Select the order in which the row are sorted with the [datatables' `order`](https://datatables.net/reference/option/order) argument. By default, the rows are sorted according to the first column (`order = [[0, 'asc']]`).
@@ -137,21 +152,25 @@ with pd.option_context("display.float_format", "${:,.2f}".format):
137152
show(pd.Series([i * math.pi for i in range(1, 6)]))
138153
```
139154

140-
## Advanced cell formatting
155+
## Advanced cell formatting with JS callbacks
156+
157+
You can use Javascript callbacks to set the cell or row style depending on the cell content.
141158

142-
Datatables allows to set the cell or row style depending on the cell content, with either the [createdRow](https://datatables.net/reference/option/createdRow) or [createdCell](https://datatables.net/reference/option/columns.createdCell) callback. For instance, if we want the cells with negative numbers to be colored in red, we can use the `columnDefs.createdCell` argument as follows:
159+
The example below, in which we color in red the cells with negative numbers, is directly inspired by the corresponding datatables.net [example](https://datatables.net/reference/option/columns.createdCell).
143160

144161
```python
145162
show(
146163
pd.DataFrame([[-1, 2, -3, 4, -5], [6, -7, 8, -9, 10]], columns=list("abcde")),
147164
columnDefs=[
148165
{
149166
"targets": "_all",
150-
"createdCell": """function (td, cellData, rowData, row, col) {
151-
if ( cellData < 0 ) {
167+
"createdCell": """
168+
function (td, cellData, rowData, row, col) {
169+
if (cellData < 0) {
152170
$(td).css('color', 'red')
153-
}
154-
}""",
171+
}
172+
}
173+
""",
155174
}
156175
],
157176
eval_functions=True,

index.html

Lines changed: 792 additions & 598 deletions
Large diffs are not rendered by default.

itables/datatables_template.html

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,32 +9,38 @@
99
overflow: hidden;
1010
} </style>
1111
<script type="module">
12-
// Load the eval_functions_js function if required
13-
// eval_functions_js
14-
1512
// Define the dt_args
1613
let dt_args = {};
1714

1815
// Define the table data
1916
const data = [];
20-
dt_args["data"] = data;
2117

2218
if (typeof require === 'undefined') {
2319
// TODO: This should become the default (use a simple import)
2420
// when the ESM version works independently of whether
2521
// require.js is there or not, see
2622
// https://datatables.net/forums/discussion/69066/esm-es6-module-support?
27-
const { default: $ } = await import("https://esm.sh/[email protected]");
28-
const { default: initDataTables } = await import("https://esm.sh/[email protected][email protected]");
23+
const {default: $} = await import("https://esm.sh/[email protected]");
24+
const {default: initDataTables} = await import("https://esm.sh/[email protected][email protected]");
2925

3026
initDataTables();
3127

28+
// Load and apply the eval_functions_js function to dt_args if required
29+
// eval_functions_js
30+
31+
dt_args["data"] = data;
32+
3233
// Display the table
3334
$(document).ready(function () {
3435
$('#table_id').DataTable(dt_args);
3536
});
3637
} else {
3738
require(["jquery", "datatables"], ($, datatables) => {
39+
// Load and apply the eval_functions_js function to dt_args if required
40+
// eval_functions_js
41+
42+
dt_args["data"] = data;
43+
3844
// Display the table
3945
$(document).ready(function () {
4046
$('#table_id').DataTable(dt_args);

itables/javascript.py

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import json
55
import logging
66
import os
7-
import re
87
import uuid
98
import warnings
109

@@ -17,15 +16,11 @@
1716

1817
from .downsample import downsample
1918

20-
try:
21-
unicode # Python 2
22-
except NameError:
23-
unicode = str # Python 3
24-
2519
logging.basicConfig()
2620
logger = logging.getLogger(__name__)
2721

2822
_DATATABLE_LOADED = False
23+
_ORIGINAL_DATAFRAME_REPR_HTML = pd.DataFrame._repr_html_
2924

3025

3126
def read_package_file(*path):
@@ -44,6 +39,10 @@ def init_notebook_mode(all_interactive=False):
4439
if all_interactive:
4540
pd.DataFrame._repr_html_ = _datatables_repr_
4641
pd.Series._repr_html_ = _datatables_repr_
42+
else:
43+
pd.DataFrame._repr_html_ = _ORIGINAL_DATAFRAME_REPR_HTML
44+
if hasattr(pd.Series, "_repr_html_"):
45+
del pd.Series._repr_html_
4746

4847
load_datatables(skip_if_already_loaded=False)
4948

@@ -68,7 +67,7 @@ def _formatted_values(df):
6867
continue
6968

7069
if x.dtype.kind == "O":
71-
formatted_df[col] = formatted_df[col].astype(unicode)
70+
formatted_df[col] = formatted_df[col].astype(str)
7271
continue
7372

7473
formatted_df[col] = np.array(fmt.format_array(x.values, None))
@@ -81,6 +80,21 @@ def _formatted_values(df):
8180
return formatted_df.values.tolist()
8281

8382

83+
def _table_header(df, table_id, show_index, classes):
84+
"""This function returns the HTML table header. Rows are not included."""
85+
thead = ""
86+
if show_index:
87+
thead = "<th></th>" * len(df.index.names)
88+
89+
for column in df.columns:
90+
thead += f"<th>{column}</th>"
91+
92+
loading = "<td>Loading... (need <a href=https://github.com/mwouts/itables/#table-not-loading>help</a>?)</td>"
93+
tbody = f"<tr>{loading}</tr>"
94+
95+
return f'<table id="{table_id}" class="{classes}"><thead>{thead}</thead><tbody>{tbody}</tbody></table>'
96+
97+
8498
def replace_value(template, pattern, value, count=1):
8599
"""Set the given pattern to the desired value in the template,
86100
after making sure that the pattern is found exactly once."""
@@ -130,15 +144,7 @@ def _datatables_repr_(df=None, tableId=None, **kwargs):
130144
if not showIndex:
131145
df = df.set_index(pd.RangeIndex(len(df.index)))
132146

133-
# Generate table head using pandas.to_html()
134-
pattern = re.compile(r".*<thead>(.*)</thead>", flags=re.MULTILINE | re.DOTALL)
135-
match = pattern.match(df.head(0).to_html())
136-
thead = match.groups()[0]
137-
if not showIndex:
138-
thead = thead.replace("<th></th>", "", 1)
139-
table_header = (
140-
f'<table id="{tableId}" class="{classes}"><thead>{thead}</thead></table>'
141-
)
147+
table_header = _table_header(df, tableId, showIndex, classes)
142148
output = replace_value(
143149
output,
144150
'<table id="table_id"><thead><tr><th>A</th></tr></thead></table>',
@@ -148,28 +154,23 @@ def _datatables_repr_(df=None, tableId=None, **kwargs):
148154

149155
# Export the DT args to JSON
150156
dt_args = json.dumps(kwargs)
157+
output = replace_value(output, "let dt_args = {};", f"let dt_args = {dt_args};")
151158

152159
# And load the eval_functions_js library if required
153160
if eval_functions:
154161
eval_functions_js = read_package_file("javascript", "eval_functions.js")
155162
output = replace_value(
156163
output,
157164
"// eval_functions_js",
158-
f"<script>\n{eval_functions_js}\n<script>",
165+
f"{eval_functions_js}\ndt_args = eval_functions(dt_args);",
166+
count=2,
159167
)
160-
output = replace_value(
161-
output,
162-
"let dt_args = {};",
163-
f"let dt_args = eval_functions({dt_args});",
168+
elif eval_functions is None and _any_function(kwargs):
169+
warnings.warn(
170+
"One of the arguments passed to datatables starts with 'function'. "
171+
"To evaluate this function, use the option 'eval_functions=True'. "
172+
"To silence this warning, use 'eval_functions=False'."
164173
)
165-
else:
166-
output = replace_value(output, "let dt_args = {};", f"let dt_args = {dt_args};")
167-
if eval_functions is None and _any_function(kwargs):
168-
warnings.warn(
169-
"One of the arguments passed to datatables starts with 'function'. "
170-
"To evaluate this function, use the option 'eval_functions=True'. "
171-
"To silence this warning, use 'eval_functions=False'."
172-
)
173174

174175
# Export the table data to JSON and include this in the HTML
175176
data = _formatted_values(df.reset_index() if showIndex else df)
@@ -181,7 +182,7 @@ def _datatables_repr_(df=None, tableId=None, **kwargs):
181182

182183
def _any_function(value):
183184
"""Does a value or nested value starts with 'function'?"""
184-
if isinstance(value, str) and value.startswith("function"):
185+
if isinstance(value, str) and value.lstrip().startswith("function"):
185186
return True
186187
elif isinstance(value, list):
187188
for nested_value in value:

itables/javascript/eval_functions.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
function eval_functions(map_or_text) {
22
if (typeof map_or_text === "string") {
3-
if (map_or_text.startsWith("function")) {
3+
if (map_or_text.trimStart().startsWith("function")) {
44
try {
55
// Note: parenthesis are required around the whole expression for eval to return a value!
66
// See https://stackoverflow.com/a/7399078/911298.

itables/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""ITables' version number"""
22

3-
__version__ = "0.4.1"
3+
__version__ = "0.4.2"

tests/test_init.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import pandas as pd
2+
3+
from itables import init_notebook_mode
4+
5+
6+
def test_init():
7+
assert not hasattr(pd.Series, "_repr_html_")
8+
9+
init_notebook_mode(all_interactive=True)
10+
assert hasattr(pd.Series, "_repr_html_")
11+
12+
init_notebook_mode(all_interactive=False)
13+
assert not hasattr(pd.Series, "_repr_html_")
14+
15+
# No pb if we do this twice
16+
init_notebook_mode(all_interactive=False)
17+
assert not hasattr(pd.Series, "_repr_html_")

0 commit comments

Comments
 (0)