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