Skip to content

Commit a9bcafd

Browse files
jakirkhamjrbourbeau
authored andcommitted
Use ipytree in .tree representation (#450)
1 parent be63734 commit a9bcafd

File tree

7 files changed

+1125
-537
lines changed

7 files changed

+1125
-537
lines changed

docs/tutorial.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,8 @@ Groups also have the :func:`zarr.hierarchy.Group.tree` method, e.g.::
424424
├── bar (1000000,) int64
425425
└── baz (1000, 1000) float32
426426

427-
If you're using Zarr within a Jupyter notebook, calling ``tree()`` will generate an
427+
If you're using Zarr within a Jupyter notebook (requires
428+
`ipytree <https://github.com/QuantStack/ipytree>`_), calling ``tree()`` will generate an
428429
interactive tree representation, see the `repr_tree.ipynb notebook
429430
<http://nbviewer.jupyter.org/github/zarr-developers/zarr-python/blob/master/notebooks/repr_tree.ipynb>`_
430431
for more examples.

notebooks/repr_tree.ipynb

Lines changed: 1064 additions & 458 deletions
Large diffs are not rendered by default.

requirements_dev_optional.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# optional library requirements
22
bsddb3==6.2.6; sys_platform != 'win32'
33
lmdb==0.97; sys_platform != 'win32'
4+
# optional library requirements for Jupyter
5+
ipytree==0.1.3
46
# optional library requirements for services
57
# don't let pyup change pinning for azure-storage-blob, need to pin to older
68
# version to get compatibility with azure storage emulator on appveyor

setup.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
'setuptools>=38.6.0',
3131
'setuptools-scm>1.5.4'
3232
],
33+
extras_require={
34+
'jupyter': [
35+
'ipytree',
36+
],
37+
},
3338
python_requires='>=3.5',
3439
install_requires=dependencies,
3540
package_dir={'': '.'},

zarr/tests/test_hierarchy.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010

1111
import numpy as np
1212
import pytest
13+
14+
try:
15+
import ipytree
16+
except ImportError: # pragma: no cover
17+
ipytree = None
18+
1319
from numcodecs import Zlib
1420
from numpy.testing import assert_array_equal
1521

@@ -1199,11 +1205,10 @@ def _check_tree(g, expect_bytes, expect_text):
11991205
assert expect_text == str(g.tree())
12001206
expect_repr = expect_text
12011207
assert expect_repr == repr(g.tree())
1202-
# test _repr_html_ lightly
1203-
# noinspection PyProtectedMember
1204-
html = g.tree()._repr_html_().strip()
1205-
assert html.startswith('<link')
1206-
assert html.endswith('</script>')
1208+
if ipytree:
1209+
# noinspection PyProtectedMember
1210+
widget = g.tree()._ipython_display_()
1211+
isinstance(widget, ipytree.Tree)
12071212

12081213

12091214
def test_tree():

zarr/tests/test_util.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
from zarr.util import (guess_chunks, human_readable_size, info_html_report,
66
info_text_report, is_total_slice, normalize_chunks,
77
normalize_fill_value, normalize_order,
8-
normalize_resize_args, normalize_shape)
8+
normalize_resize_args, normalize_shape,
9+
tree_array_icon, tree_group_icon, tree_get_icon)
910

1011

1112
def test_normalize_shape():
@@ -152,3 +153,10 @@ def test_info_html_report():
152153
actual = info_html_report(items)
153154
assert '<table' == actual[:6]
154155
assert '</table>' == actual[-8:]
156+
157+
158+
def test_tree_get_icon():
159+
assert tree_get_icon("Array") == tree_array_icon
160+
assert tree_get_icon("Group") == tree_group_icon
161+
with pytest.raises(ValueError):
162+
tree_get_icon("Baz")

zarr/util.py

Lines changed: 33 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
import json
44
import math
55
import numbers
6-
import uuid
7-
from textwrap import TextWrapper, dedent
6+
from textwrap import TextWrapper
87

98
import numpy as np
109
from asciitree import BoxStyle, LeftAligned
@@ -399,83 +398,43 @@ def get_text(self, node):
399398
return node.get_text()
400399

401400

402-
def tree_html_sublist(node, root=False, expand=False):
403-
result = ''
404-
data_jstree = '{"type": "%s"}' % node.get_type()
405-
if root or (expand is True) or (isinstance(expand, int) and node.depth < expand):
406-
css_class = 'jstree-open'
407-
else:
408-
css_class = ''
409-
result += "<li data-jstree='{}' class='{}'>".format(data_jstree, css_class)
410-
result += '<span>{}</span>'.format(node.get_text())
411-
children = node.get_children()
412-
if children:
413-
result += '<ul>'
414-
for c in children:
415-
result += tree_html_sublist(c, expand=expand)
416-
result += '</ul>'
417-
result += '</li>'
418-
return result
401+
tree_group_icon = 'folder'
402+
tree_array_icon = 'table'
419403

420404

421-
def tree_html(group, expand, level):
405+
def tree_get_icon(stype):
406+
if stype == "Array":
407+
return tree_array_icon
408+
elif stype == "Group":
409+
return tree_group_icon
410+
else:
411+
raise ValueError("Unknown type: %s" % stype)
422412

423-
result = ''
424413

425-
# include CSS for jstree default theme
426-
css_url = '//cdnjs.cloudflare.com/ajax/libs/jstree/3.3.3/themes/default/style.min.css'
427-
result += '<link rel="stylesheet" href="{}"/>'.format(css_url)
414+
def tree_widget_sublist(node, root=False, expand=False):
415+
import ipytree
428416

429-
# construct the tree as HTML nested lists
430-
node_id = uuid.uuid4()
431-
result += '<div id="{}" class="zarr-tree">'.format(node_id)
432-
result += '<ul>'
433-
root = TreeNode(group, level=level)
434-
result += tree_html_sublist(root, root=True, expand=expand)
435-
result += '</ul>'
436-
result += '</div>'
437-
438-
# construct javascript
439-
result += dedent("""
440-
<script>
441-
if (!require.defined('jquery')) {
442-
require.config({
443-
paths: {
444-
jquery: '//cdnjs.cloudflare.com/ajax/libs/jquery/1.12.1/jquery.min'
445-
},
446-
});
447-
}
448-
if (!require.defined('jstree')) {
449-
require.config({
450-
paths: {
451-
jstree: '//cdnjs.cloudflare.com/ajax/libs/jstree/3.3.3/jstree.min'
452-
},
453-
});
454-
}
455-
require(['jstree'], function() {
456-
$('#%s').jstree({
457-
types: {
458-
Group: {
459-
icon: "%s"
460-
},
461-
Array: {
462-
icon: "%s"
463-
}
464-
},
465-
plugins: ["types"]
466-
});
467-
});
468-
</script>
469-
""" % (node_id, tree_group_icon, tree_array_icon))
417+
result = ipytree.Node()
418+
result.icon = tree_get_icon(node.get_type())
419+
if root or (expand is True) or (isinstance(expand, int) and node.depth < expand):
420+
result.opened = True
421+
else:
422+
result.opened = False
423+
result.name = node.get_text()
424+
result.nodes = [tree_widget_sublist(c, expand=expand) for c in node.get_children()]
425+
result.disabled = True
470426

471427
return result
472428

473429

474-
tree_group_icon = 'fa fa-folder'
475-
tree_array_icon = 'fa fa-table'
476-
# alternatives...
477-
# tree_group_icon: 'jstree-folder'
478-
# tree_array_icon: 'jstree-file'
430+
def tree_widget(group, expand, level):
431+
import ipytree
432+
433+
result = ipytree.Tree()
434+
root = TreeNode(group, level=level)
435+
result.add_node(tree_widget_sublist(root, root=True, expand=expand))
436+
437+
return result
479438

480439

481440
class TreeViewer(object):
@@ -531,8 +490,10 @@ def __unicode__(self):
531490
def __repr__(self):
532491
return self.__unicode__()
533492

534-
def _repr_html_(self):
535-
return tree_html(self.group, expand=self.expand, level=self.level)
493+
def _ipython_display_(self):
494+
tree = tree_widget(self.group, expand=self.expand, level=self.level)
495+
tree._ipython_display_()
496+
return tree
536497

537498

538499
def check_array_shape(param, array, shape):

0 commit comments

Comments
 (0)