Skip to content

Commit fb93519

Browse files
committed
Support DataFrames in display class
1 parent ea0be03 commit fb93519

File tree

2 files changed

+146
-8
lines changed

2 files changed

+146
-8
lines changed

jupyterlab_table/__init__.py

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
from IPython.display import display, JSON
1+
from IPython.display import display, DisplayObject
22
import json
3+
import pandas as pd
4+
from .utils import prepare_data
35

46

57
# Running `npm run build` will create static resources in the static
@@ -23,22 +25,77 @@ def _jupyter_nbextension_paths():
2325

2426
# A display class that can be used within a notebook. E.g.:
2527
# from jupyterlab_table import JSONTable
26-
# JSONTable(data)
28+
# JSONTable(data, schema)
2729

28-
class JSONTable(JSON):
30+
class JSONTable(DisplayObject):
2931
"""A display class for displaying JSONTable visualizations in the Jupyter Notebook and IPython kernel.
3032
31-
JSONTable expects a JSON-able dict, not serialized JSON strings.
33+
JSONTable expects a JSON-able list, not serialized JSON strings.
3234
3335
Scalar types (None, number, string) are not allowed, only dict containers.
3436
"""
37+
# wrap data in a property, which warns about passing already-serialized JSON
38+
_data = None
39+
_schema = None
40+
def __init__(self, data=None, schema=None, url=None, filename=None, metadata=None):
41+
"""Create a JSON Table display object given raw data.
3542
36-
def _data_and_metadata(self):
37-
return self.data, self.metadata
38-
43+
Parameters
44+
----------
45+
data : list
46+
Not an already-serialized JSON string.
47+
Scalar types (None, number, string) are not allowed, only list containers.
48+
schema : dict
49+
JSON Table Schema. See http://frictionlessdata.io/guides/json-table-schema/.
50+
url : unicode
51+
A URL to download the data from.
52+
filename : unicode
53+
Path to a local file to load the data from.
54+
metadata: dict
55+
Specify extra metadata to attach to the json display object.
56+
"""
57+
self.schema = schema
58+
self.metadata = metadata
59+
super(JSONTable, self).__init__(data=data, url=url, filename=filename)
60+
61+
def _check_data(self):
62+
if self.data is not None and not isinstance(self.data, (list, pd.DataFrame)):
63+
raise TypeError("%s expects a JSONable list or pandas DataFrame, not %r" % (self.__class__.__name__, self.data))
64+
if self.schema is not None and not isinstance(self.schema, dict):
65+
raise TypeError("%s expects a JSONable dict, not %r" % (self.__class__.__name__, self.schema))
66+
67+
@property
68+
def data(self):
69+
return self._data
70+
71+
@property
72+
def schema(self):
73+
return self._schema
74+
75+
@data.setter
76+
def data(self, data):
77+
if isinstance(data, str):
78+
# warnings.warn("JSONTable expects JSON-able dict or list, not JSON strings")
79+
data = json.loads(data)
80+
self._data = data
81+
82+
@schema.setter
83+
def schema(self, schema):
84+
if isinstance(schema, str):
85+
# warnings.warn("JSONTable expects a JSON-able list, not JSON strings")
86+
schema = json.loads(schema)
87+
self._schema = schema
88+
3989
def _ipython_display_(self):
4090
bundle = {
41-
'application/vnd.dataresource+json': self.data,
91+
'application/vnd.dataresource+json': {
92+
'resources': [
93+
{
94+
'schema': self.schema,
95+
'data': prepare_data(self.data)
96+
}
97+
]
98+
},
4299
'text/plain': '<jupyterlab_table.JSONTable object>'
43100
}
44101
metadata = {

jupyterlab_table/utils.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import cgi
2+
import codecs
3+
import collections
4+
import os.path
5+
import pandas as pd
6+
7+
8+
def nested_update(d, u):
9+
"""Update nested dictionary d (in-place) with keys from u."""
10+
for k, v in u.items():
11+
if isinstance(v, collections.Mapping):
12+
d[k] = nested_update(d.get(k, {}), v)
13+
else:
14+
d[k] = v
15+
return d
16+
17+
18+
def abs_path(path):
19+
"""Make path absolute."""
20+
return os.path.join(
21+
os.path.dirname(os.path.abspath(__file__)),
22+
path)
23+
24+
25+
def get_content(path):
26+
"""Get content of file."""
27+
with codecs.open(abs_path(path), encoding='utf-8') as f:
28+
return f.read()
29+
30+
31+
def escape(string):
32+
"""Escape the string."""
33+
return cgi.escape(string, quote=True)
34+
35+
36+
def sanitize_dataframe(df):
37+
"""Sanitize a DataFrame to prepare it for serialization.
38+
39+
* Make a copy
40+
* Raise ValueError if it has a hierarchical index.
41+
* Convert categoricals to strings.
42+
* Convert np.int dtypes to Python int objects
43+
* Convert floats to objects and replace NaNs by None.
44+
* Convert DateTime dtypes into appropriate string representations
45+
"""
46+
import pandas as pd
47+
import numpy as np
48+
49+
df = df.copy()
50+
51+
if isinstance(df.index, pd.core.index.MultiIndex):
52+
raise ValueError('Hierarchical indices not supported')
53+
if isinstance(df.columns, pd.core.index.MultiIndex):
54+
raise ValueError('Hierarchical indices not supported')
55+
56+
for col_name, dtype in df.dtypes.iteritems():
57+
if str(dtype) == 'category':
58+
# XXXX: work around bug in to_json for categorical types
59+
# https://github.com/pydata/pandas/issues/10778
60+
df[col_name] = df[col_name].astype(str)
61+
elif np.issubdtype(dtype, np.integer):
62+
# convert integers to objects; np.int is not JSON serializable
63+
df[col_name] = df[col_name].astype(object)
64+
elif np.issubdtype(dtype, np.floating):
65+
# For floats, convert nan->None: np.float is not JSON serializable
66+
col = df[col_name].astype(object)
67+
df[col_name] = col.where(col.notnull(), None)
68+
elif str(dtype).startswith('datetime'):
69+
# Convert datetimes to strings
70+
# astype(str) will choose the appropriate resolution
71+
df[col_name] = df[col_name].astype(str).replace('NaT', '')
72+
return df
73+
74+
75+
def prepare_data(data=None):
76+
"""Prepare Plotly data from Pandas DataFrame."""
77+
78+
if isinstance(data, list):
79+
return data
80+
data = sanitize_dataframe(data)
81+
return data.to_dict(orient='records')

0 commit comments

Comments
 (0)