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

1# SPDX-License-Identifier: BSD-3-Clause 

2# Copyright (c) 2023 Scipp contributors (https://github.com/scipp) 

3 

4from typing import Dict, List, Union 

5 

6import numpy as np 

7 

8from .. import DataArray, Dataset, DType, Variable 

9from ..typing import VariableLike 

10 

11CENTER = 'text-align: center;' 

12LEFT_BORDER = 'border-left:1px solid #a9a9a9;' 

13BOTTOM_BORDER = 'border-bottom:2px solid #a9a9a9;' 

14 

15 

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

32 

33 

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 

40 

41 

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] 

46 

47 

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 ) 

71 

72 

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 ] 

86 

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 ) 

97 

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 ) 

108 

109 return out 

110 

111 

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 

121 

122 

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 

139 

140 

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 

149 

150 

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 

160 

161 

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) 

178 

179 

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 

188 

189 

190def table(obj: Dict[str, Union[Variable, DataArray]], max_rows: int = 20): 

191 """Create an HTML table from the contents of the supplied object. 

192 

193 Possible inputs are: 

194 - Variable 

195 - DataArray 

196 - Dataset 

197 - dict of Variable 

198 - dict of DataArray 

199 

200 Inputs must be one-dimensional. Zero-dimensional data members, attributes and 

201 coordinates are stripped. Zero-dimensional masks are broadcast. 

202 

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) 

211 

212 if obj.ndim != 1: 

213 raise ValueError("Table can only be generated for one-dimensional objects.") 

214 

215 obj = _strip_scalars_and_broadcast_masks(obj) 

216 

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) 

224 

225 bin_edges = _find_bin_edges(obj) 

226 

227 header = _make_sections_header(obj) 

228 if len(obj) > 1: 

229 header = _make_entries_header(obj) + header 

230 

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 ] 

242 

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 ) 

251 

252 html = _to_html_table(header=header, body=body) 

253 from IPython.display import HTML 

254 

255 return HTML(html)