Skip to content

Commit

Permalink
Version 0.4.2 (#46)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mwouts authored Jan 7, 2022
1 parent ce1e7a8 commit d9d4a82
Show file tree
Hide file tree
Showing 10 changed files with 898 additions and 652 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ exclude: >
(?x)^(
\.vscode/settings\.json|
demo/.*|
tests/notebooks/.*|
index.html
)$
repos:

Expand Down
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
0.4.2 (2022-01-07)
==================

Fixed
-----
- Fix the HTML output when `eval_functions=True`
- Display "Loading..." under the table header until the table is displayed with datatables.net
- `init_notebook_mode(all_interactive=False)` restores the original Pandas HTML representation.

0.4.1 (2022-01-06)
==================

Fixed
-------
- Long column names don't overlap any more (#28)
- Long column names don't overlap anymore (#28)


0.4.0 (2022-01-06)
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2019 Marc Wouts
Copyright (c) 2019-2022 Marc Wouts

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
45 changes: 32 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Pandas DataFrames and Series as Interactive Tables
# Pandas DataFrames and Series as Interactive DataTables

[![Pypi](https://img.shields.io/pypi/v/itables.svg)](https://pypi.python.org/pypi/itables)
![CI](https://github.com/mwouts/itables/workflows/CI/badge.svg)
Expand All @@ -9,7 +9,7 @@
[![Lab](https://img.shields.io/badge/Binder-JupyterLab-blue.svg)](https://mybinder.org/v2/gh/mwouts/itables/main?urlpath=lab/tree/README.md)
<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>

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

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

Expand All @@ -28,17 +28,16 @@ from itables import init_notebook_mode
init_notebook_mode(all_interactive=True)
```

Then any dataframe will be displayed as an interactive [datatables](https://datatables.net) table:

```python
import world_bank_data as wb

df = wb.get_countries()
df
```

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)!


Or display just one series or dataframe as an interactive table with the `show` function.
If you want to display just one series or dataframe as an interactive table, use `itables.show`:

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

# Supported environments
(NB: In Jupyter Notebook and Jupyter NBconvert, you need to call `init_notebook_mode()` before using `show`).

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)!

## Supported environments

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

## Table not loading?

If the table just says "Loading...", then maybe
- You loaded a notebook that is not trusted (run "Trust Notebook" in View / Activate Command Palette)
- Or you are offline?

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.

# Advanced usage

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.

Below we give a few examples of how the datatables.net examples can be translated to Python with `itables`.

## Row sorting

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']]`).
Expand Down Expand Up @@ -137,21 +152,25 @@ with pd.option_context("display.float_format", "${:,.2f}".format):
show(pd.Series([i * math.pi for i in range(1, 6)]))
```

## Advanced cell formatting
## Advanced cell formatting with JS callbacks

You can use Javascript callbacks to set the cell or row style depending on the cell content.

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:
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).

```python
show(
pd.DataFrame([[-1, 2, -3, 4, -5], [6, -7, 8, -9, 10]], columns=list("abcde")),
columnDefs=[
{
"targets": "_all",
"createdCell": """function (td, cellData, rowData, row, col) {
if ( cellData < 0 ) {
"createdCell": """
function (td, cellData, rowData, row, col) {
if (cellData < 0) {
$(td).css('color', 'red')
}
}""",
}
}
""",
}
],
eval_functions=True,
Expand Down
1,390 changes: 792 additions & 598 deletions index.html

Large diffs are not rendered by default.

18 changes: 12 additions & 6 deletions itables/datatables_template.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,32 +9,38 @@
overflow: hidden;
} </style>
<script type="module">
// Load the eval_functions_js function if required
// eval_functions_js

// Define the dt_args
let dt_args = {};

// Define the table data
const data = [];
dt_args["data"] = data;

if (typeof require === 'undefined') {
// TODO: This should become the default (use a simple import)
// when the ESM version works independently of whether
// require.js is there or not, see
// https://datatables.net/forums/discussion/69066/esm-es6-module-support?
const { default: $ } = await import("https://esm.sh/jquery@3.5.0");
const { default: initDataTables } = await import("https://esm.sh/datatables.net@1.11.3?deps=jquery@3.5.0");
const {default: $} = await import("https://esm.sh/jquery@3.5.0");
const {default: initDataTables} = await import("https://esm.sh/datatables.net@1.11.3?deps=jquery@3.5.0");

initDataTables();

// Load and apply the eval_functions_js function to dt_args if required
// eval_functions_js

dt_args["data"] = data;

// Display the table
$(document).ready(function () {
$('#table_id').DataTable(dt_args);
});
} else {
require(["jquery", "datatables"], ($, datatables) => {
// Load and apply the eval_functions_js function to dt_args if required
// eval_functions_js

dt_args["data"] = data;

// Display the table
$(document).ready(function () {
$('#table_id').DataTable(dt_args);
Expand Down
61 changes: 31 additions & 30 deletions itables/javascript.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import json
import logging
import os
import re
import uuid
import warnings

Expand All @@ -17,15 +16,11 @@

from .downsample import downsample

try:
unicode # Python 2
except NameError:
unicode = str # Python 3

logging.basicConfig()
logger = logging.getLogger(__name__)

_DATATABLE_LOADED = False
_ORIGINAL_DATAFRAME_REPR_HTML = pd.DataFrame._repr_html_


def read_package_file(*path):
Expand All @@ -44,6 +39,10 @@ def init_notebook_mode(all_interactive=False):
if all_interactive:
pd.DataFrame._repr_html_ = _datatables_repr_
pd.Series._repr_html_ = _datatables_repr_
else:
pd.DataFrame._repr_html_ = _ORIGINAL_DATAFRAME_REPR_HTML
if hasattr(pd.Series, "_repr_html_"):
del pd.Series._repr_html_

load_datatables(skip_if_already_loaded=False)

Expand All @@ -68,7 +67,7 @@ def _formatted_values(df):
continue

if x.dtype.kind == "O":
formatted_df[col] = formatted_df[col].astype(unicode)
formatted_df[col] = formatted_df[col].astype(str)
continue

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


def _table_header(df, table_id, show_index, classes):
"""This function returns the HTML table header. Rows are not included."""
thead = ""
if show_index:
thead = "<th></th>" * len(df.index.names)

for column in df.columns:
thead += f"<th>{column}</th>"

loading = "<td>Loading... (need <a href=https://github.com/mwouts/itables/#table-not-loading>help</a>?)</td>"
tbody = f"<tr>{loading}</tr>"

return f'<table id="{table_id}" class="{classes}"><thead>{thead}</thead><tbody>{tbody}</tbody></table>'


def replace_value(template, pattern, value, count=1):
"""Set the given pattern to the desired value in the template,
after making sure that the pattern is found exactly once."""
Expand Down Expand Up @@ -130,15 +144,7 @@ def _datatables_repr_(df=None, tableId=None, **kwargs):
if not showIndex:
df = df.set_index(pd.RangeIndex(len(df.index)))

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

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

# And load the eval_functions_js library if required
if eval_functions:
eval_functions_js = read_package_file("javascript", "eval_functions.js")
output = replace_value(
output,
"// eval_functions_js",
f"<script>\n{eval_functions_js}\n<script>",
f"{eval_functions_js}\ndt_args = eval_functions(dt_args);",
count=2,
)
output = replace_value(
output,
"let dt_args = {};",
f"let dt_args = eval_functions({dt_args});",
elif eval_functions is None and _any_function(kwargs):
warnings.warn(
"One of the arguments passed to datatables starts with 'function'. "
"To evaluate this function, use the option 'eval_functions=True'. "
"To silence this warning, use 'eval_functions=False'."
)
else:
output = replace_value(output, "let dt_args = {};", f"let dt_args = {dt_args};")
if eval_functions is None and _any_function(kwargs):
warnings.warn(
"One of the arguments passed to datatables starts with 'function'. "
"To evaluate this function, use the option 'eval_functions=True'. "
"To silence this warning, use 'eval_functions=False'."
)

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

def _any_function(value):
"""Does a value or nested value starts with 'function'?"""
if isinstance(value, str) and value.startswith("function"):
if isinstance(value, str) and value.lstrip().startswith("function"):
return True
elif isinstance(value, list):
for nested_value in value:
Expand Down
2 changes: 1 addition & 1 deletion itables/javascript/eval_functions.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
function eval_functions(map_or_text) {
if (typeof map_or_text === "string") {
if (map_or_text.startsWith("function")) {
if (map_or_text.trimStart().startsWith("function")) {
try {
// Note: parenthesis are required around the whole expression for eval to return a value!
// See https://stackoverflow.com/a/7399078/911298.
Expand Down
2 changes: 1 addition & 1 deletion itables/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""ITables' version number"""

__version__ = "0.4.1"
__version__ = "0.4.2"
17 changes: 17 additions & 0 deletions tests/test_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import pandas as pd

from itables import init_notebook_mode


def test_init():
assert not hasattr(pd.Series, "_repr_html_")

init_notebook_mode(all_interactive=True)
assert hasattr(pd.Series, "_repr_html_")

init_notebook_mode(all_interactive=False)
assert not hasattr(pd.Series, "_repr_html_")

# No pb if we do this twice
init_notebook_mode(all_interactive=False)
assert not hasattr(pd.Series, "_repr_html_")

0 comments on commit d9d4a82

Please sign in to comment.