Coverage for install/scipp/visualization/table.py: 78%
123 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-04-28 01:28 +0000
« prev ^ index » next coverage.py v7.4.0, created at 2024-04-28 01:28 +0000
1# SPDX-License-Identifier: BSD-3-Clause
2# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
4from typing import Dict, List, Union
6import numpy as np
8from .. import DataArray, Dataset, DType, Variable
9from ..typing import VariableLike
11CENTER = 'text-align: center;'
12LEFT_BORDER = 'border-left:1px solid #a9a9a9;'
13BOTTOM_BORDER = 'border-bottom:2px solid #a9a9a9;'
16def _string_in_cell(v: Variable) -> str:
17 if v.bins is not None:
18 return f'len={v.value.shape}'
19 if v.dtype not in (DType.float32, DType.float64):
20 return str(v.value)
21 if v.variance is None:
22 return f'{v.value:.3f}'
23 err = np.sqrt(v.variance)
24 if err == 0.0:
25 prec = 3
26 else:
27 try:
28 prec = max(3, -int(np.floor(np.log10(err))))
29 except OverflowError:
30 prec = 3 # happens when err is non-finite
31 return f'{v.value:.{prec}f}±{err:.{prec}f}'
34def _var_name_with_unit(name: str, var: Variable) -> str:
35 out = f'<span style="font-weight: bold;">{name}</span>'
36 unit = var.bins.unit if var.bins is not None else var.unit
37 if unit is not None:
38 out += ' [𝟙]' if unit == 'dimensionless' else f' [{unit}]' # noqa: RUF001
39 return out
42def _add_td_tags(cell_list: List[str], border: str = '') -> List[str]:
43 td = f' style="{border}"' if border else ''
44 td = f'<td{td}>'
45 return [f'{td}{cell}</td>' for cell in cell_list]
48def _make_variable_column(
49 name: str,
50 var: Variable,
51 indices: list,
52 need_bin_edge: bool,
53 is_bin_edge,
54 border: str = '',
55) -> List[str]:
56 head = [_var_name_with_unit(name, var)]
57 rows = []
58 for i in indices:
59 if i is None:
60 rows.append('...')
61 else:
62 rows.append(_string_in_cell(var[i]))
63 if need_bin_edge:
64 if is_bin_edge:
65 rows.append(_string_in_cell(var[-1]))
66 else:
67 rows.append('')
68 return _add_td_tags(head, border=border + BOTTOM_BORDER) + _add_td_tags(
69 rows, border=border
70 )
73def _make_data_array_table(
74 da: DataArray, indices: list, bin_edges: bool, no_left_border: bool = False
75) -> List[list]:
76 out = [
77 _make_variable_column(
78 name='',
79 var=da.data,
80 indices=indices,
81 need_bin_edge=bin_edges,
82 is_bin_edge=False,
83 border='' if no_left_border else LEFT_BORDER,
84 )
85 ]
87 for name, var in sorted(da.masks.items()):
88 out.append(
89 _make_variable_column(
90 name=name,
91 var=var,
92 indices=indices,
93 need_bin_edge=bin_edges,
94 is_bin_edge=False,
95 )
96 )
98 for name, var in sorted(da.deprecated_attrs.items()):
99 out.append(
100 _make_variable_column(
101 name=name,
102 var=var,
103 indices=indices,
104 need_bin_edge=bin_edges,
105 is_bin_edge=da.deprecated_attrs.is_edges(name),
106 )
107 )
109 return out
112def _make_entries_header(ds: Dataset) -> str:
113 out = '<tr>'
114 if ds.coords:
115 out += f'<th colspan="{len(ds.coords)}"></th>'
116 for name, da in sorted(ds.items()):
117 ncols = 1 + len(da.masks) + len(da.deprecated_attrs)
118 out += f'<th style="{CENTER}" colspan="{ncols}">{name}</th>'
119 out += '</tr>'
120 return out
123def _make_sections_header(ds: Dataset) -> str:
124 out = '<tr>'
125 if ds.coords:
126 out += f'<th style="{CENTER}" colspan="{len(ds.coords)}">Coordinates</th>'
127 for i, (_, da) in enumerate(sorted(ds.items())):
128 border = '' if (i == 0) and (not ds.coords) else LEFT_BORDER
129 out += f'<th style="{CENTER + border}">Data</th>'
130 if da.masks:
131 out += f'<th style="{CENTER}" colspan="{len(da.masks)}">Masks</th>'
132 if da.deprecated_attrs:
133 out += (
134 f'<th style="{CENTER}" '
135 f'colspan="{len(da.deprecated_attrs)}">Attributes</th>'
136 )
137 out += '</tr>'
138 return out
141def _to_html_table(header: str, body: List[list]) -> str:
142 out = '<table>' + header
143 ncols = len(body)
144 nrows = len(body[0])
145 for i in range(nrows):
146 out += '<tr>' + ''.join([body[j][i] for j in range(ncols)]) + '</tr>'
147 out += '</table>'
148 return out
151def _find_bin_edges(ds: Dataset) -> bool:
152 for key in ds.coords:
153 if ds.coords.is_edges(key):
154 return True
155 for da in ds.values():
156 for key in da.deprecated_attrs:
157 if da.deprecated_attrs.is_edges(key):
158 return True
159 return False
162def _strip_scalars_and_broadcast_masks(ds: Dataset) -> Dataset:
163 out = {}
164 for key, da in ds.items():
165 out[key] = DataArray(
166 data=da.data,
167 coords={
168 key: var for key, var in da.coords.items() if var.dims == da.data.dims
169 },
170 attrs={
171 key: var
172 for key, var in da.deprecated_attrs.items()
173 if var.dims == da.data.dims
174 },
175 masks={key: var.broadcast(sizes=da.sizes) for key, var in da.masks.items()},
176 )
177 return Dataset(out)
180def _to_dataset(obj: Union[VariableLike, dict]) -> Dataset:
181 if isinstance(obj, DataArray):
182 return Dataset({obj.name: obj})
183 if isinstance(obj, Variable):
184 return Dataset(data={"": obj})
185 if isinstance(obj, dict):
186 return Dataset(obj)
187 return obj
190def table(obj: Dict[str, Union[Variable, DataArray]], max_rows: int = 20):
191 """Create an HTML table from the contents of the supplied object.
193 Possible inputs are:
194 - Variable
195 - DataArray
196 - Dataset
197 - dict of Variable
198 - dict of DataArray
200 Inputs must be one-dimensional. Zero-dimensional data members, attributes and
201 coordinates are stripped. Zero-dimensional masks are broadcast.
203 Parameters
204 ----------
205 obj:
206 Input to be turned into a html table.
207 max_rows:
208 Maximum number of rows to display.
209 """
210 obj = _to_dataset(obj)
212 if obj.ndim != 1:
213 raise ValueError("Table can only be generated for one-dimensional objects.")
215 obj = _strip_scalars_and_broadcast_masks(obj)
217 # Limit the number of rows to be printed
218 size = obj.shape[0]
219 if size > max_rows:
220 half = int(max_rows / 2)
221 inds = [*range(half), None, *range(size - half, size)]
222 else:
223 inds = range(size)
225 bin_edges = _find_bin_edges(obj)
227 header = _make_sections_header(obj)
228 if len(obj) > 1:
229 header = _make_entries_header(obj) + header
231 # First attach coords
232 body = [
233 _make_variable_column(
234 name=name,
235 var=var,
236 indices=inds,
237 need_bin_edge=bin_edges,
238 is_bin_edge=obj.coords.is_edges(name),
239 )
240 for name, var in sorted(obj.coords.items())
241 ]
243 # Rest of the table from DataArrays
244 for i, (_, da) in enumerate(sorted(obj.items())):
245 body += _make_data_array_table(
246 da=da,
247 indices=inds,
248 bin_edges=bin_edges,
249 no_left_border=(i == 0) and (not obj.coords),
250 )
252 html = _to_html_table(header=header, body=body)
253 from IPython.display import HTML
255 return HTML(html)