Skip to content

Commit 27f5e22

Browse files
DPeterKpelson
authored andcommitted
Add _repr_html_ for Iris cubes (#2918)
Add cube _repr_html_ functionality
1 parent 2b4f1b3 commit 27f5e22

File tree

5 files changed

+822
-0
lines changed

5 files changed

+822
-0
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
* Added ``repr_html`` functionality to the :class:`~iris.cube.Cube` to provide
2+
a rich html representation of cubes in Jupyter notebooks.

lib/iris/cube.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2103,6 +2103,11 @@ def __repr__(self):
21032103
return "<iris 'Cube' of %s>" % self.summary(shorten=True,
21042104
name_padding=1)
21052105

2106+
def _repr_html_(self):
2107+
from iris.experimental.representation import CubeRepresentation
2108+
representer = CubeRepresentation(self)
2109+
return representer.repr_html()
2110+
21062111
def __iter__(self):
21072112
raise TypeError('Cube is not iterable')
21082113

Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
# (C) British Crown Copyright 2018, Met Office
2+
#
3+
# This file is part of Iris.
4+
#
5+
# Iris is free software: you can redistribute it and/or modify it under
6+
# the terms of the GNU Lesser General Public License as published by the
7+
# Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# Iris is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with Iris. If not, see <http://www.gnu.org/licenses/>.
17+
18+
"""
19+
Definitions of how Iris objects should be represented.
20+
21+
"""
22+
23+
from __future__ import (absolute_import, division, print_function)
24+
from six.moves import (filter, input, map, range, zip) # noqa
25+
26+
import re
27+
28+
29+
class CubeRepresentation(object):
30+
"""
31+
Produce representations of a :class:`~iris.cube.Cube`.
32+
33+
This includes:
34+
35+
* ``_html_repr_``: a representation of the cube as an html object,
36+
available in Jupyter notebooks. Specifically, this is presented as an
37+
html table.
38+
39+
"""
40+
41+
_template = """
42+
<style>
43+
a.iris {{
44+
text-decoration: none !important;
45+
}}
46+
table.iris {{
47+
white-space: pre;
48+
border: 1px solid;
49+
border-color: #9c9c9c;
50+
font-family: monaco, monospace;
51+
}}
52+
th.iris {{
53+
background: #303f3f;
54+
color: #e0e0e0;
55+
border-left: 1px solid;
56+
border-color: #9c9c9c;
57+
font-size: 1.05em;
58+
min-width: 50px;
59+
max-width: 125px;
60+
}}
61+
tr.iris :first-child {{
62+
border-right: 1px solid #9c9c9c !important;
63+
}}
64+
td.iris-title {{
65+
background: #d5dcdf;
66+
border-top: 1px solid #9c9c9c;
67+
font-weight: bold;
68+
}}
69+
.iris-word-cell {{
70+
text-align: left !important;
71+
white-space: pre;
72+
}}
73+
.iris-subheading-cell {{
74+
padding-left: 2em !important;
75+
}}
76+
.iris-inclusion-cell {{
77+
padding-right: 1em !important;
78+
}}
79+
.iris-panel-body {{
80+
padding-top: 0px;
81+
}}
82+
.iris-panel-title {{
83+
padding-left: 3em;
84+
}}
85+
.iris-panel-title {{
86+
margin-top: 7px;
87+
}}
88+
</style>
89+
<table class="iris" id="{id}">
90+
{header}
91+
{shape}
92+
{content}
93+
</table>
94+
"""
95+
96+
def __init__(self, cube):
97+
self.cube = cube
98+
self.cube_id = id(self.cube)
99+
self.cube_str = str(self.cube)
100+
101+
self.str_headings = {
102+
'Dimension coordinates:': None,
103+
'Auxiliary coordinates:': None,
104+
'Derived coordinates:': None,
105+
'Scalar coordinates:': None,
106+
'Attributes:': None,
107+
'Cell methods:': None,
108+
}
109+
self.dim_desc_coords = ['Dimension coordinates:',
110+
'Auxiliary coordinates:',
111+
'Derived coordinates:']
112+
113+
# Important content that summarises a cube is defined here.
114+
self.shapes = self.cube.shape
115+
self.scalar_cube = self.shapes == ()
116+
self.ndims = self.cube.ndim
117+
118+
self.name = self.cube.name().title().replace('_', ' ')
119+
self.names = self._dim_names()
120+
self.units = self.cube.units
121+
122+
def _get_dim_names(self):
123+
"""
124+
Get dimension-describing coordinate names, or '--' if no coordinate]
125+
describes the dimension.
126+
127+
Note: borrows from `cube.summary`.
128+
129+
"""
130+
# Create a set to contain the axis names for each data dimension.
131+
dim_names = list(range(len(self.cube.shape)))
132+
133+
# Add the dim_coord names that participate in the associated data
134+
# dimensions.
135+
for dim in range(len(self.cube.shape)):
136+
dim_coords = self.cube.coords(contains_dimension=dim,
137+
dim_coords=True)
138+
if dim_coords:
139+
dim_names[dim] = dim_coords[0].name()
140+
else:
141+
dim_names[dim] = '--'
142+
return dim_names
143+
144+
def _dim_names(self):
145+
if self.scalar_cube:
146+
dim_names = ['(scalar cube)']
147+
else:
148+
dim_names = self._get_dim_names()
149+
return dim_names
150+
151+
def _get_lines(self):
152+
return self.cube_str.split('\n')
153+
154+
def _get_bits(self, bits):
155+
"""
156+
Parse the body content (`bits`) of the cube string in preparation for
157+
being converted into table rows.
158+
159+
"""
160+
left_indent = re.split(r'\w+', bits[1])[0]
161+
162+
# Get heading indices within the printout.
163+
start_inds = []
164+
for hdg in self.str_headings.keys():
165+
heading = '{}{}'.format(left_indent, hdg)
166+
try:
167+
start_ind = bits.index(heading)
168+
except ValueError:
169+
continue
170+
else:
171+
start_inds.append(start_ind)
172+
# Mark the end of the file.
173+
start_inds.append(0)
174+
175+
# Retrieve info for each heading from the printout.
176+
for i0, i1 in zip(start_inds[:-1], start_inds[1:]):
177+
str_heading_name = bits[i0].strip()
178+
if i1 != 0:
179+
content = bits[i0 + 1: i1]
180+
else:
181+
content = bits[i0 + 1:]
182+
self.str_headings[str_heading_name] = content
183+
184+
def _make_header(self):
185+
"""
186+
Make the table header. This is similar to the summary of the cube,
187+
but does not include dim shapes. These are included on the next table
188+
row down, and produced with `make_shapes_row`.
189+
190+
"""
191+
# Header row.
192+
tlc_template = \
193+
'<th class="iris iris-word-cell">{self.name} ({self.units})</th>'
194+
top_left_cell = tlc_template.format(self=self)
195+
cells = ['<tr class="iris">', top_left_cell]
196+
for dim_name in self.names:
197+
cells.append(
198+
'<th class="iris iris-word-cell">{}</th>'.format(dim_name))
199+
cells.append('</tr>')
200+
return '\n'.join(cell for cell in cells)
201+
202+
def _make_shapes_row(self):
203+
"""Add a row to show data / dimensions shape."""
204+
title_cell = \
205+
'<td class="iris-word-cell iris-subheading-cell">Shape</td>'
206+
cells = ['<tr class="iris">', title_cell]
207+
for shape in self.shapes:
208+
cells.append(
209+
'<td class="iris iris-inclusion-cell">{}</td>'.format(shape))
210+
cells.append('</td>')
211+
return '\n'.join(cell for cell in cells)
212+
213+
def _make_row(self, title, body=None, col_span=0):
214+
"""
215+
Produce one row for the table body; i.e.
216+
<tr><td>Coord name</td><td>x</td><td>-</td>...</tr>
217+
218+
`body` contains the content for each cell not in the left-most (title)
219+
column.
220+
If None, indicates this row is a title row (see below).
221+
`title` contains the row heading. If `body` is None, indicates
222+
that the row contains a sub-heading;
223+
e.g. 'Dimension coordinates:'.
224+
`col_span` indicates how many columns the string should span.
225+
226+
"""
227+
row = ['<tr class="iris">']
228+
template = ' <td{html_cls}>{content}</td>'
229+
if body is None:
230+
# This is a title row.
231+
# Strip off the trailing ':' from the title string.
232+
title = title.strip()[:-1]
233+
row.append(
234+
template.format(html_cls=' class="iris-title iris-word-cell"',
235+
content=title))
236+
# Add blank cells for the rest of the rows.
237+
for _ in range(self.ndims):
238+
row.append(template.format(html_cls=' class="iris-title"',
239+
content=''))
240+
else:
241+
# This is not a title row.
242+
# Deal with name of coord/attr etc. first.
243+
sub_title = '\t{}'.format(title)
244+
row.append(template.format(
245+
html_cls=' class="iris-word-cell iris-subheading-cell"',
246+
content=sub_title))
247+
# One further item or more than that?
248+
if col_span != 0:
249+
html_cls = ' class="{}" colspan="{}"'.format('iris-word-cell',
250+
col_span)
251+
row.append(template.format(html_cls=html_cls, content=body))
252+
else:
253+
# "Inclusion" - `x` or `-`.
254+
for itm in body:
255+
row.append(template.format(
256+
html_cls=' class="iris-inclusion-cell"',
257+
content=itm))
258+
row.append('</tr>')
259+
return row
260+
261+
def _make_content(self):
262+
elements = []
263+
for k, v in self.str_headings.items():
264+
if v is not None:
265+
# Add the sub-heading title.
266+
elements.extend(self._make_row(k))
267+
for line in v:
268+
# Add every other row in the sub-heading.
269+
if k in self.dim_desc_coords:
270+
body = re.findall(r'[\w-]+', line)
271+
title = body.pop(0)
272+
colspan = 0
273+
else:
274+
split_point = line.index(':')
275+
title = line[:split_point].strip()
276+
body = line[split_point + 2:].strip()
277+
colspan = self.ndims
278+
elements.extend(
279+
self._make_row(title, body=body, col_span=colspan))
280+
return '\n'.join(element for element in elements)
281+
282+
def repr_html(self):
283+
"""The `repr` interface for Jupyter."""
284+
# Deal with the header first.
285+
header = self._make_header()
286+
287+
# Check if we have a scalar cube.
288+
if self.scalar_cube:
289+
shape = ''
290+
# We still need a single content column!
291+
self.ndims = 1
292+
else:
293+
shape = self._make_shapes_row()
294+
295+
# Now deal with the rest of the content.
296+
lines = self._get_lines()
297+
# If we only have a single line `cube_str` we have no coords / attrs!
298+
# We need to handle this case specially.
299+
if len(lines) == 1:
300+
content = ''
301+
else:
302+
self._get_bits(lines)
303+
content = self._make_content()
304+
305+
return self._template.format(header=header,
306+
id=self.cube_id,
307+
shape=shape,
308+
content=content)

0 commit comments

Comments
 (0)