@@ -173,181 +173,192 @@ def _translate(self):
173
173
BLANK_CLASS = "blank"
174
174
BLANK_VALUE = " "
175
175
176
- # mapping variables
177
- ctx = self .ctx # td css styles from apply() and applymap()
178
- cell_context = self .cell_context # td css classes from set_td_classes()
179
- cellstyle_map : DefaultDict [tuple [CSSPair , ...], list [str ]] = defaultdict (list )
180
-
181
- # copied attributes
182
- hidden_index = self .hidden_index
183
- hidden_columns = self .hidden_columns
184
-
185
176
# construct render dict
186
177
d = {
187
178
"uuid" : self .uuid ,
188
179
"table_styles" : _format_table_styles (self .table_styles or []),
189
180
"caption" : self .caption ,
190
181
}
191
182
183
+ head = self ._translate_header (
184
+ BLANK_CLASS , BLANK_VALUE , INDEX_NAME_CLASS , COL_HEADING_CLASS
185
+ )
186
+ d .update ({"head" : head })
187
+
188
+ self .cellstyle_map : DefaultDict [tuple [CSSPair , ...], list [str ]] = defaultdict (
189
+ list
190
+ )
191
+ body = self ._translate_body (DATA_CLASS , ROW_HEADING_CLASS )
192
+ d .update ({"body" : body })
193
+
194
+ cellstyle : list [dict [str , CSSList | list [str ]]] = [
195
+ {"props" : list (props ), "selectors" : selectors }
196
+ for props , selectors in self .cellstyle_map .items ()
197
+ ]
198
+ d .update ({"cellstyle" : cellstyle })
199
+
200
+ table_attr = self .table_attributes
201
+ use_mathjax = get_option ("display.html.use_mathjax" )
202
+ if not use_mathjax :
203
+ table_attr = table_attr or ""
204
+ if 'class="' in table_attr :
205
+ table_attr = table_attr .replace ('class="' , 'class="tex2jax_ignore ' )
206
+ else :
207
+ table_attr += ' class="tex2jax_ignore"'
208
+ d .update ({"table_attributes" : table_attr })
209
+
210
+ if self .tooltips :
211
+ d = self .tooltips ._translate (self .data , self .uuid , d )
212
+
213
+ return d
214
+
215
+ def _translate_header (
216
+ self , blank_class , blank_value , index_name_class , col_heading_class
217
+ ):
218
+ """
219
+ Build each <tr> within table <head>, using the structure:
220
+ +----------------------------+---------------+---------------------------+
221
+ | index_blanks ... | column_name_0 | column_headers (level_0) |
222
+ 1) | .. | .. | .. |
223
+ | index_blanks ... | column_name_n | column_headers (level_n) |
224
+ +----------------------------+---------------+---------------------------+
225
+ 2) | index_names (level_0 to level_n) ... | column_blanks ... |
226
+ +----------------------------+---------------+---------------------------+
227
+ """
192
228
# for sparsifying a MultiIndex
193
- idx_lengths = _get_level_lengths (self .index )
194
- col_lengths = _get_level_lengths (self .columns , hidden_columns )
229
+ col_lengths = _get_level_lengths (self .columns , self .hidden_columns )
195
230
196
- n_rlvls = self .data .index .nlevels
197
- n_clvls = self .data .columns .nlevels
198
- rlabels = self .data .index .tolist ()
199
231
clabels = self .data .columns .tolist ()
200
-
201
- if n_rlvls == 1 :
202
- rlabels = [[x ] for x in rlabels ]
203
- if n_clvls == 1 :
232
+ if self .data .columns .nlevels == 1 :
204
233
clabels = [[x ] for x in clabels ]
205
234
clabels = list (zip (* clabels ))
206
235
207
236
head = []
208
- for r in range (n_clvls ):
209
- # Blank for Index columns...
210
- row_es = [
211
- {
212
- "type" : "th" ,
213
- "value" : BLANK_VALUE ,
214
- "display_value" : BLANK_VALUE ,
215
- "is_visible" : not hidden_index ,
216
- "class" : " " .join ([BLANK_CLASS ]),
217
- }
218
- ] * (n_rlvls - 1 )
219
-
220
- # ... except maybe the last for columns.names
237
+ # 1) column headers
238
+ for r in range (self .data .columns .nlevels ):
239
+ index_blanks = [
240
+ _element ("th" , blank_class , blank_value , not self .hidden_index )
241
+ ] * (self .data .index .nlevels - 1 )
242
+
221
243
name = self .data .columns .names [r ]
222
- cs = [
223
- BLANK_CLASS if name is None else INDEX_NAME_CLASS ,
224
- f"level{ r } " ,
244
+ column_name = [
245
+ _element (
246
+ "th" ,
247
+ f"{ blank_class if name is None else index_name_class } level{ r } " ,
248
+ name if name is not None else blank_value ,
249
+ not self .hidden_index ,
250
+ )
225
251
]
226
- name = BLANK_VALUE if name is None else name
227
- row_es .append (
228
- {
229
- "type" : "th" ,
230
- "value" : name ,
231
- "display_value" : name ,
232
- "class" : " " .join (cs ),
233
- "is_visible" : not hidden_index ,
234
- }
235
- )
236
252
237
253
if clabels :
238
- for c , value in enumerate (clabels [r ]):
239
- es = {
240
- "type" : "th" ,
241
- "value" : value ,
242
- "display_value" : value ,
243
- "class" : f"{ COL_HEADING_CLASS } level{ r } col{ c } " ,
244
- "is_visible" : _is_visible (c , r , col_lengths ),
245
- }
246
- colspan = col_lengths .get ((r , c ), 0 )
247
- if colspan > 1 :
248
- es ["attributes" ] = f'colspan="{ colspan } "'
249
- row_es .append (es )
250
- head .append (row_es )
254
+ column_headers = [
255
+ _element (
256
+ "th" ,
257
+ f"{ col_heading_class } level{ r } col{ c } " ,
258
+ value ,
259
+ _is_visible (c , r , col_lengths ),
260
+ attributes = (
261
+ f'colspan="{ col_lengths .get ((r , c ), 0 )} "'
262
+ if col_lengths .get ((r , c ), 0 ) > 1
263
+ else ""
264
+ ),
265
+ )
266
+ for c , value in enumerate (clabels [r ])
267
+ ]
268
+ head .append (index_blanks + column_name + column_headers )
251
269
270
+ # 2) index names
252
271
if (
253
272
self .data .index .names
254
273
and com .any_not_none (* self .data .index .names )
255
- and not hidden_index
274
+ and not self . hidden_index
256
275
):
257
- index_header_row = []
276
+ index_names = [
277
+ _element (
278
+ "th" ,
279
+ f"{ index_name_class } level{ c } " ,
280
+ blank_value if name is None else name ,
281
+ True ,
282
+ )
283
+ for c , name in enumerate (self .data .index .names )
284
+ ]
258
285
259
- for c , name in enumerate (self .data .index .names ):
260
- cs = [INDEX_NAME_CLASS , f"level{ c } " ]
261
- name = "" if name is None else name
262
- index_header_row .append (
263
- {"type" : "th" , "value" : name , "class" : " " .join (cs )}
286
+ column_blanks = [
287
+ _element (
288
+ "th" ,
289
+ f"{ blank_class } col{ c } " ,
290
+ blank_value ,
291
+ c not in self .hidden_columns ,
264
292
)
293
+ for c in range (len (clabels [0 ]))
294
+ ]
295
+ head .append (index_names + column_blanks )
265
296
266
- index_header_row .extend (
267
- [
268
- {
269
- "type" : "th" ,
270
- "value" : BLANK_VALUE ,
271
- "class" : " " .join ([BLANK_CLASS , f"col{ c } " ]),
272
- }
273
- for c in range (len (clabels [0 ]))
274
- if c not in hidden_columns
275
- ]
276
- )
297
+ return head
277
298
278
- head .append (index_header_row )
279
- d .update ({"head" : head })
299
+ def _translate_body (self , data_class , row_heading_class ):
300
+ """
301
+ Build each <tr> in table <body> in the following format:
302
+ +--------------------------------------------+---------------------------+
303
+ | index_header_0 ... index_header_n | data_by_column |
304
+ +--------------------------------------------+---------------------------+
305
+
306
+ Also add elements to the cellstyle_map for more efficient grouped elements in
307
+ <style></style> block
308
+ """
309
+ # for sparsifying a MultiIndex
310
+ idx_lengths = _get_level_lengths (self .index )
311
+
312
+ rlabels = self .data .index .tolist ()
313
+ if self .data .index .nlevels == 1 :
314
+ rlabels = [[x ] for x in rlabels ]
280
315
281
316
body = []
282
317
for r , row_tup in enumerate (self .data .itertuples ()):
283
- row_es = []
284
- for c , value in enumerate (rlabels [r ]):
285
- rid = [
286
- ROW_HEADING_CLASS ,
287
- f"level{ c } " ,
288
- f"row{ r } " ,
289
- ]
290
- es = {
291
- "type" : "th" ,
292
- "is_visible" : (_is_visible (r , c , idx_lengths ) and not hidden_index ),
293
- "value" : value ,
294
- "display_value" : value ,
295
- "id" : "_" .join (rid [1 :]),
296
- "class" : " " .join (rid ),
297
- }
298
- rowspan = idx_lengths .get ((c , r ), 0 )
299
- if rowspan > 1 :
300
- es ["attributes" ] = f'rowspan="{ rowspan } "'
301
- row_es .append (es )
318
+ index_headers = [
319
+ _element (
320
+ "th" ,
321
+ f"{ row_heading_class } level{ c } row{ r } " ,
322
+ value ,
323
+ (_is_visible (r , c , idx_lengths ) and not self .hidden_index ),
324
+ id = f"level{ c } _row{ r } " ,
325
+ attributes = (
326
+ f'rowspan="{ idx_lengths .get ((c , r ), 0 )} "'
327
+ if idx_lengths .get ((c , r ), 0 ) > 1
328
+ else ""
329
+ ),
330
+ )
331
+ for c , value in enumerate (rlabels [r ])
332
+ ]
302
333
334
+ data = []
303
335
for c , value in enumerate (row_tup [1 :]):
304
- formatter = self ._display_funcs [(r , c )]
305
- row_dict = {
306
- "type" : "td" ,
307
- "value" : value ,
308
- "display_value" : formatter (value ),
309
- "is_visible" : (c not in hidden_columns ),
310
- "attributes" : "" ,
311
- }
312
-
313
- # only add an id if the cell has a style
314
- props : CSSList = []
315
- if self .cell_ids or (r , c ) in ctx :
316
- row_dict ["id" ] = f"row{ r } _col{ c } "
317
- props .extend (ctx [r , c ])
318
-
319
336
# add custom classes from cell context
320
337
cls = ""
321
- if (r , c ) in cell_context :
322
- cls = " " + cell_context [r , c ]
323
- row_dict ["class" ] = f"{ DATA_CLASS } row{ r } col{ c } { cls } "
324
-
325
- row_es .append (row_dict )
326
- if props : # (), [] won't be in cellstyle_map, cellstyle respectively
327
- cellstyle_map [tuple (props )].append (f"row{ r } _col{ c } " )
328
- body .append (row_es )
329
- d .update ({"body" : body })
330
-
331
- cellstyle : list [dict [str , CSSList | list [str ]]] = [
332
- {"props" : list (props ), "selectors" : selectors }
333
- for props , selectors in cellstyle_map .items ()
334
- ]
335
- d .update ({"cellstyle" : cellstyle })
338
+ if (r , c ) in self .cell_context :
339
+ cls = " " + self .cell_context [r , c ]
340
+
341
+ data_element = _element (
342
+ "td" ,
343
+ f"{ data_class } row{ r } col{ c } { cls } " ,
344
+ value ,
345
+ (c not in self .hidden_columns ),
346
+ attributes = "" ,
347
+ display_value = self ._display_funcs [(r , c )](value ),
348
+ )
336
349
337
- table_attr = self .table_attributes
338
- use_mathjax = get_option ("display.html.use_mathjax" )
339
- if not use_mathjax :
340
- table_attr = table_attr or ""
341
- if 'class="' in table_attr :
342
- table_attr = table_attr .replace ('class="' , 'class="tex2jax_ignore ' )
343
- else :
344
- table_attr += ' class="tex2jax_ignore"'
345
- d .update ({"table_attributes" : table_attr })
350
+ # only add an id if the cell has a style
351
+ if self .cell_ids or (r , c ) in self .ctx :
352
+ data_element ["id" ] = f"row{ r } _col{ c } "
353
+ if (r , c ) in self .ctx and self .ctx [r , c ]: # only add if non-empty
354
+ self .cellstyle_map [tuple (self .ctx [r , c ])].append (
355
+ f"row{ r } _col{ c } "
356
+ )
346
357
347
- if self .tooltips :
348
- d = self .tooltips ._translate (self .data , self .uuid , d )
358
+ data .append (data_element )
349
359
350
- return d
360
+ body .append (index_headers + data )
361
+ return body
351
362
352
363
def format (
353
364
self ,
@@ -502,6 +513,27 @@ def format(
502
513
return self
503
514
504
515
516
+ def _element (
517
+ html_element : str ,
518
+ html_class : str ,
519
+ value : Any ,
520
+ is_visible : bool ,
521
+ ** kwargs ,
522
+ ) -> dict :
523
+ """
524
+ Template to return container with information for a <td></td> or <th></th> element.
525
+ """
526
+ if "display_value" not in kwargs :
527
+ kwargs ["display_value" ] = value
528
+ return {
529
+ "type" : html_element ,
530
+ "value" : value ,
531
+ "class" : html_class ,
532
+ "is_visible" : is_visible ,
533
+ ** kwargs ,
534
+ }
535
+
536
+
505
537
def _get_level_lengths (index , hidden_elements = None ):
506
538
"""
507
539
Given an index, find the level length for each element.
0 commit comments