@@ -164,14 +164,16 @@ def _render_html(
164164 html_style_tpl = self .template_html_style ,
165165 )
166166
167- def _render_latex (self , sparse_index : bool , sparse_columns : bool , ** kwargs ) -> str :
167+ def _render_latex (
168+ self , sparse_index : bool , sparse_columns : bool , clines : str | None , ** kwargs
169+ ) -> str :
168170 """
169171 Render a Styler in latex format
170172 """
171173 self ._compute ()
172174
173175 d = self ._translate (sparse_index , sparse_columns , blank = "" )
174- self ._translate_latex (d )
176+ self ._translate_latex (d , clines = clines )
175177
176178 self .template_latex .globals ["parse_wrap" ] = _parse_latex_table_wrapping
177179 self .template_latex .globals ["parse_table" ] = _parse_latex_table_styles
@@ -257,13 +259,19 @@ def _translate(
257259 head = self ._translate_header (sparse_cols , max_cols )
258260 d .update ({"head" : head })
259261
262+ # for sparsifying a MultiIndex and for use with latex clines
263+ idx_lengths = _get_level_lengths (
264+ self .index , sparse_index , max_rows , self .hidden_rows
265+ )
266+ d .update ({"index_lengths" : idx_lengths })
267+
260268 self .cellstyle_map : DefaultDict [tuple [CSSPair , ...], list [str ]] = defaultdict (
261269 list
262270 )
263271 self .cellstyle_map_index : DefaultDict [
264272 tuple [CSSPair , ...], list [str ]
265273 ] = defaultdict (list )
266- body = self ._translate_body (sparse_index , max_rows , max_cols )
274+ body = self ._translate_body (idx_lengths , max_rows , max_cols )
267275 d .update ({"body" : body })
268276
269277 ctx_maps = {
@@ -515,7 +523,7 @@ def _generate_index_names_row(self, iter: tuple, max_cols: int, col_lengths: dic
515523
516524 return index_names + column_blanks
517525
518- def _translate_body (self , sparsify_index : bool , max_rows : int , max_cols : int ):
526+ def _translate_body (self , idx_lengths : dict , max_rows : int , max_cols : int ):
519527 """
520528 Build each <tr> within table <body> as a list
521529
@@ -537,11 +545,6 @@ def _translate_body(self, sparsify_index: bool, max_rows: int, max_cols: int):
537545 body : list
538546 The associated HTML elements needed for template rendering.
539547 """
540- # for sparsifying a MultiIndex
541- idx_lengths = _get_level_lengths (
542- self .index , sparsify_index , max_rows , self .hidden_rows
543- )
544-
545548 rlabels = self .data .index .tolist ()
546549 if not isinstance (self .data .index , MultiIndex ):
547550 rlabels = [[x ] for x in rlabels ]
@@ -738,7 +741,7 @@ def _generate_body_row(
738741
739742 return index_headers + data
740743
741- def _translate_latex (self , d : dict ) -> None :
744+ def _translate_latex (self , d : dict , clines : str | None ) -> None :
742745 r"""
743746 Post-process the default render dict for the LaTeX template format.
744747
@@ -749,10 +752,10 @@ def _translate_latex(self, d: dict) -> None:
749752 or multirow sparsification (so that \multirow and \multicol work correctly).
750753 """
751754 index_levels = self .index .nlevels
752- visible_index_levels = index_levels - sum (self .hide_index_ )
755+ visible_index_level_n = index_levels - sum (self .hide_index_ )
753756 d ["head" ] = [
754757 [
755- {** col , "cellstyle" : self .ctx_columns [r , c - visible_index_levels ]}
758+ {** col , "cellstyle" : self .ctx_columns [r , c - visible_index_level_n ]}
756759 for c , col in enumerate (row )
757760 if col ["is_visible" ]
758761 ]
@@ -790,6 +793,39 @@ def _translate_latex(self, d: dict) -> None:
790793 body .append (row_body_headers + row_body_cells )
791794 d ["body" ] = body
792795
796+ # clines are determined from info on index_lengths and hidden_rows and input
797+ # to a dict defining which row clines should be added in the template.
798+ if clines not in [
799+ None ,
800+ "all;data" ,
801+ "all;index" ,
802+ "skip-last;data" ,
803+ "skip-last;index" ,
804+ ]:
805+ raise ValueError (
806+ f"`clines` value of { clines } is invalid. Should either be None or one "
807+ f"of 'all;data', 'all;index', 'skip-last;data', 'skip-last;index'."
808+ )
809+ elif clines is not None :
810+ data_len = len (row_body_cells ) if "data" in clines else 0
811+
812+ d ["clines" ] = defaultdict (list )
813+ visible_row_indexes : list [int ] = [
814+ r for r in range (len (self .data .index )) if r not in self .hidden_rows
815+ ]
816+ visible_index_levels : list [int ] = [
817+ i for i in range (index_levels ) if not self .hide_index_ [i ]
818+ ]
819+ for rn , r in enumerate (visible_row_indexes ):
820+ for lvln , lvl in enumerate (visible_index_levels ):
821+ if lvl == index_levels - 1 and "skip-last" in clines :
822+ continue
823+ idx_len = d ["index_lengths" ].get ((lvl , r ), None )
824+ if idx_len is not None : # i.e. not a sparsified entry
825+ d ["clines" ][rn + idx_len ].append (
826+ f"\\ cline{{{ lvln + 1 } -{ len (visible_index_levels )+ data_len } }}"
827+ )
828+
793829 def format (
794830 self ,
795831 formatter : ExtFormatter | None = None ,
0 commit comments