diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index ccffc6369e8..9c0f50f406d --- a/setup.py +++ b/setup.py @@ -147,4 +147,6 @@ def write_version_py(filename=None): tests_require=TESTS_REQUIRE, url=URL, packages=find_packages(), - package_data={'xarray': ['tests/data/*', 'plot/default_colormap.csv']}) + package_data={'xarray': ['static/*', + 'tests/data/*', + 'plot/default_colormap.csv']}) diff --git a/xarray/core/common.py b/xarray/core/common.py index 3bfcd484474..386f61f79a0 100644 --- a/xarray/core/common.py +++ b/xarray/core/common.py @@ -8,6 +8,7 @@ from .pycompat import basestring, suppress, dask_array_type, OrderedDict from . import dtypes from . import formatting +from . import formatting_html from . import ops from .utils import SortedKeysDict, not_implemented, Frozen @@ -99,6 +100,9 @@ def __array__(self, dtype=None): def __repr__(self): return formatting.array_repr(self) + def _repr_html_(self): + return formatting_html.array_repr(self) + def _iter(self): for n in range(len(self)): yield self[n] diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index 58847bb0086..3e2e9e5e792 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -20,6 +20,7 @@ from . import indexing from . import alignment from . import formatting +from . import formatting_html from . import duck_array_ops from .. import conventions from .alignment import align @@ -1169,6 +1170,9 @@ def to_zarr(self, store=None, mode='w-', synchronizer=None, group=None, def __unicode__(self): return formatting.dataset_repr(self) + def _repr_html_(self): + return formatting_html.dataset_repr(self) + def info(self, buf=None): """ Concise summary of a Dataset variables and attributes. diff --git a/xarray/core/formatting_html.py b/xarray/core/formatting_html.py new file mode 100644 index 00000000000..8b95e075d43 --- /dev/null +++ b/xarray/core/formatting_html.py @@ -0,0 +1,283 @@ +# coding: utf-8 + +import uuid +import pkg_resources +from functools import partial +from collections import OrderedDict + +from .formatting import format_array_flat + + +CSS_FILE_PATH = '/'.join(('static', 'css', 'style-jupyterlab.css')) +CSS_STYLE = (pkg_resources + .resource_string('xarray', CSS_FILE_PATH) + .decode('utf8')) + + +ICONS_SVG_PATH = '/'.join(('static', 'html', 'icons-svg-inline.html')) +ICONS_SVG = (pkg_resources + .resource_string('xarray', ICONS_SVG_PATH) + .decode('utf8')) + + +def format_dims(dims, coord_names): + if not dims: + return '' + + dim_css_map = {k: " class='xr-has-index'" if k in coord_names else '' + for k, v in dims.items()} + + dims_li = "".join("
  • {name}: {size}
  • " + .format(cssclass_idx=dim_css_map[k], name=k, size=v) + for k, v in dims.items()) + + return "".format(dims_li) + + +def format_values_preview(array, max_char=35): + pprint_str = format_array_flat(array, max_char) + + return "".join("{} ".format(s) + for s in pprint_str.split()) + + +def summarize_attrs(attrs): + attrs_li = "".join("
  • {} : {}
  • ".format(k, v) + for k, v in attrs.items()) + + return "".format(attrs_li) + + +def _icon(icon_name): + # icon_name should be defined in xarray/static/html/icon-svg-inline.html + return ("" + "" + "" + "" + .format(icon_name)) + + +def summarize_variable(name, var): + d = {} + + d['dims_str'] = '(' + ', '.join(dim for dim in var.dims) + ')' + + d['name'] = name + + if name in var.dims: + d['cssclass_idx'] = " class='xr-has-index'" + else: + d['cssclass_idx'] = "" + + d['dtype'] = var.dtype + + # "unique" ids required to expand/collapse subsections + d['attrs_id'] = 'attrs-' + str(uuid.uuid4()) + d['data_id'] = 'data-' + str(uuid.uuid4()) + + if len(var.attrs): + d['disabled'] = '' + d['attrs'] = summarize_attrs(var.attrs) + else: + d['disabled'] = 'disabled' + d['attrs'] = '' + + # TODO: no value preview if not in memory + d['preview'] = format_values_preview(var) + d['attrs_ul'] = summarize_attrs(var.attrs) + d['data_repr'] = repr(var.data) + + d['attrs_icon'] = _icon('icon-file-text2') + d['data_icon'] = _icon('icon-database') + + return ( + "
    {name}
    " + "
    {dims_str}
    " + "
    {dtype}
    " + "
    {preview}
    " + "" + "" + "" + "" + "
    {attrs_ul}
    " + "
    {data_repr}
    " + .format(**d)) + + +def summarize_vars(variables): + vars_li = "".join("
  • {}
  • " + .format(summarize_variable(k, v)) + for k, v in variables.items()) + + return "".format(vars_li) + + +def collapsible_section(name, inline_details=None, details=None, + n_items=None, enabled=True, collapsed=False): + d = {} + + # "unique" id to expand/collapse the section + d['id'] = 'section-' + str(uuid.uuid4()) + + if n_items is not None: + n_items_span = " ({})".format(n_items) + else: + n_items_span = '' + + d['title'] = "{}:{}".format(name, n_items_span) + + if n_items is not None and not n_items: + collapsed = True + + d['inline_details'] = inline_details or '' + d['details'] = details or '' + + d['enabled'] = '' if enabled else 'disabled' + d['collapsed'] = '' if collapsed else 'checked' + + if enabled: + d['tip'] = " title='Expand/collapse section'" + else: + d['tip'] = "" + + return ( + "" + "" + "
    {inline_details}
    " + "
    {details}
    " + .format(**d)) + + +def _mapping_section(mapping, name, details_func, + enabled=True, max_items_collapse=None): + n_items = len(mapping) + + if max_items_collapse is not None and n_items <= max_items_collapse: + collapsed = False + else: + collapsed = True + + return collapsible_section( + name, details=details_func(mapping), n_items=n_items, + enabled=enabled, collapsed=collapsed + ) + + +def dim_section(obj): + dim_list = format_dims(obj.dims, list(obj.coords)) + + return collapsible_section('Dimensions', inline_details=dim_list, + enabled=False, collapsed=True) + + +def array_section(obj): + d = {} + + # "unique" id to expand/collapse the section + d['id'] = 'section-' + str(uuid.uuid4()) + + # TODO: no value preview if not in memory + d['preview'] = format_values_preview(obj.values, max_char=70) + + d['data_repr'] = repr(obj.data) + + # TODO: maybe collapse section dep. on number of lines in data repr + d['collapsed'] = '' + + d['tip'] = "Show/hide data repr" + + return ( + "
    " + "" + "" + "
    {preview}
    " + "
    {data_repr}
    " + "
    " + .format(**d)) + + +coord_section = partial(_mapping_section, + name='Coordinates', details_func=summarize_vars, + max_items_collapse=25) + + +datavar_section = partial(_mapping_section, + name='Data variables', details_func=summarize_vars, + max_items_collapse=15) + + +attr_section = partial(_mapping_section, + name='Attributes', details_func=summarize_attrs, + max_items_collapse=10) + + +def _obj_repr(header_components, sections): + d = {} + + d['header'] = "
    {}
    ".format( + "".join(comp for comp in header_components)) + + d['icons'] = ICONS_SVG + d['style'] = "".format(CSS_STYLE) + + d['sections'] = "".join("
  • {}
  • ".format(s) + for s in sections) + + return ("
    " + "{icons}{style}" + "
    " + "{header}" + "" + "
    " + "
    " + .format(**d)) + + +def array_repr(arr): + dims = OrderedDict((k, v) for k, v in zip(arr.dims, arr.shape)) + + obj_type = "xarray.{}".format(type(arr).__name__) + + if hasattr(arr, 'name') and arr.name is not None: + arr_name = "'{}'".format(arr.name) + else: + arr_name = "" + + if hasattr(arr, 'coords'): + coord_names = list(arr.coords) + else: + coord_names = [] + + header_components = [ + "
    {}
    ".format(obj_type), + "
    {}
    ".format(arr_name), + format_dims(dims, coord_names) + ] + + sections = [] + + sections.append(array_section(arr)) + + if hasattr(arr, 'coords'): + sections.append(coord_section(arr.coords)) + + sections.append(attr_section(arr.attrs)) + + return _obj_repr(header_components, sections) + + +def dataset_repr(ds): + obj_type = "xarray.{}".format(type(ds).__name__) + + header_components = ["
    {}
    ".format(obj_type)] + + sections = [dim_section(ds), + coord_section(ds.coords), + datavar_section(ds.data_vars), + attr_section(ds.attrs)] + + return _obj_repr(header_components, sections) diff --git a/xarray/static/css/style-jupyterlab.css b/xarray/static/css/style-jupyterlab.css new file mode 100644 index 00000000000..f6c5968a3a8 --- /dev/null +++ b/xarray/static/css/style-jupyterlab.css @@ -0,0 +1,274 @@ +/* CSS stylesheet for displaying xarray objects in jupyterlab. + * + */ + +.xr-wrap { + min-width: 500px; + max-width: 700px; +} + +.xr-header { + padding-top: 6px; + padding-bottom: 6px; + margin-bottom: 4px; + border-bottom: solid 1px #ddd; +} + +.xr-header > div, +.xr-header > ul { + display: inline; + margin-top: 0; + margin-bottom: 0; +} + +.xr-obj-type, +.xr-array-name { + margin-left: 2px; + margin-right: 10px; +} + +.xr-obj-type { + color: #555; +} + +.xr-array-name { + color: #000; +} + +.xr-sections { + margin: 0 !important; + padding: 3px !important; + display: grid; + grid-template-columns: minmax(150px, auto) 0.5fr auto 1fr 20px 20px; +} + +.xr-section-item { + display: contents; +} + +.xr-section-item input { + display: none; +} + +.xr-section-item input:enabled + label { + cursor: pointer; +} + +.xr-section-summary { + grid-column: 1; + color: #555; + font-weight: 500; +} + +.xr-section-summary > span { + display: inline-block; + padding-left: 0.5em; +} + +.xr-section-summary-in + label:before { + display: inline-block; + content: '►'; + font-size: 11px; + width: 15px; + text-align: center; +} + +.xr-section-summary-in:disabled + label:before { + color: #ccc; +} + +.xr-section-summary-in:checked + label:before { + content: '▼'; +} + +.xr-section-summary-in:checked + label > span { + display: none; +} + +.xr-section-summary, +.xr-section-inline-details { + padding-top: 4px; + padding-bottom: 4px; +} + +.xr-section-inline-details { + grid-column: 2 / -1; +} + +.xr-section-details { + display: none; + grid-column: 1 / -1; + margin-bottom: 5px; +} + +.xr-section-summary-in:checked ~ .xr-section-details { + display: contents; +} + +.xr-array-wrap { + grid-column: 1 / -1; +} + +.xr-array-icon { + display: inline-block; + width: 15px !important; + vertical-align: top; + padding: 4px 0 2px 0 !important; +} + +.xr-array-in + label:before { + content: '➕'; +} + +.xr-array-in:checked + label:before { + content: '➖'; +} + +.xr-array-preview, +.xr-array-data { + padding: 5px 0 4px 8px !important; + margin: 0; +} + +.xr-array-data, +.xr-array-in:checked ~ .xr-array-preview { + display: none; +} + +.xr-array-in:checked ~ .xr-array-data, +.xr-array-preview { + display: inline-block; +} + +.xr-preview > span > span:nth-child(odd) { + color: rgba(0, 0, 0, .65); +} + +.xr-dim-list { + display: inline-block !important; + list-style: none; + padding: 0 !important; +} + +.xr-dim-list li { + display: inline-block; + padding: 0; + margin: 0; +} + +.xr-dim-list:before { + content: '('; +} + +.xr-dim-list:after { + content: ')'; +} + +.xr-dim-list li:not(:last-child):after { + content: ','; + padding-right: 5px; +} + +.xr-has-index { + text-decoration: underline; +} + +.xr-var-list { + display: contents; +} + +.xr-var-item { + display: contents; +} + +.xr-var-item > div, +.xr-var-item > label { + background-color: #fcfcfc; +} + +.xr-var-list > li:nth-child(odd) > div, +.xr-var-list > li:nth-child(odd) > label { + background-color: #efefef; +} + +.xr-var-name { + grid-column: 1; +} + +.xr-var-name > span { + padding-right: 10px; +} + +.xr-var-dims { + grid-column: 2; +} + +.xr-var-dtype { + grid-column: 3; + text-align: right; + padding-right: 10px; + color: #555; +} + +.xr-var-preview { + grid-column: 4; + color: #888; +} + +.xr-var-name > span, +.xr-var-dims, +.xr-var-dtype, +.xr-var-preview > span { + white-space: nowrap; + overflow-x: hidden; +} + +.xr-var-attrs, +.xr-var-data { + display: none; + background-color: #fff !important; + padding-bottom: 5px !important; +} + +.xr-var-attrs-in:checked ~ .xr-var-attrs, +.xr-var-data-in:checked ~ .xr-var-data { + display: block; +} + +.xr-var-item input + label { + color: #ccc; +} + +.xr-var-item input:enabled + label { + color: #555; +} + +.xr-var-item input:enabled + label:hover { + color: #000; +} + +.xr-var-name span, +.xr-var-data, +.xr-attrs { + padding-left: 25px !important; +} + +.xr-attrs, +.xr-var-attrs, +.xr-var-data { + grid-column: 1 / -1; +} + +.xr-attrs { + list-style: none !important; +} + +.icon { + display: inline-block; + vertical-align: middle; + width: 1em; + height: 1.5em !important; + stroke-width: 0; + stroke: currentColor; + fill: currentColor; +} diff --git a/xarray/static/html/icons-svg-inline.html b/xarray/static/html/icons-svg-inline.html new file mode 100644 index 00000000000..aa20a4f88b4 --- /dev/null +++ b/xarray/static/html/icons-svg-inline.html @@ -0,0 +1,17 @@ + + + +database + + + + + +file-text2 + + + + + + +