diff --git a/great_tables/_utils_render_latex.py b/great_tables/_utils_render_latex.py index 5b7806337..3bb3a3ec1 100644 --- a/great_tables/_utils_render_latex.py +++ b/great_tables/_utils_render_latex.py @@ -143,6 +143,21 @@ def create_table_start_l(data: GTData, use_longtable: bool) -> str: # Get the column alignments for the visible columns as a list of `col_defs` col_defs = [align[0] for align in data._boxhead._get_default_alignments()] + # Check if stub is present and determine layout + has_summary_rows = bool(data._summary_rows or data._summary_rows_grand) + stub_layout = data._stub._get_stub_layout( + has_summary_rows=has_summary_rows, options=data._options + ) + + # Determine if there's a stub column (rowname or group_label) + has_stub = len(stub_layout) > 0 + + # Build stub column definitions (left-aligned with separator) + stub_col_defs = "" + if has_stub: + # Add 'l' for each stub column, with a '|' separator after the last one + stub_col_defs = "l" * len(stub_layout) + "|" + # If a table width is specified, add an extra column # space to fill in enough space to match the width extra_sep = "" @@ -181,6 +196,7 @@ def create_table_start_l(data: GTData, use_longtable: bool) -> str: longtable_post_length if use_longtable else "", "\\begin{longtable}{" if use_longtable else hdr_tabular, extra_sep, + stub_col_defs, "".join(col_defs), "}", ] @@ -264,13 +280,30 @@ def create_columns_component_l(data: GTData) -> str: # Determine the finalized number of spanner rows spanner_row_count = _get_spanners_matrix_height(data=data, omit_columns_row=True) + # Check if stub is present and determine layout + has_summary_rows = bool(data._summary_rows or data._summary_rows_grand) + stub_layout = data._stub._get_stub_layout( + has_summary_rows=has_summary_rows, options=data._options + ) + + # Determine if there's a stub column (rowname or group_label) + has_stub = len(stub_layout) > 0 + + # Create stub header cells (empty space for each stub column) + stub_headers = [] + if has_stub: + stub_headers = [" "] * len(stub_layout) + # Get the column headings headings_labels = data._boxhead._get_default_column_labels() # Ensure that the heading labels are processed for LaTeX headings_labels = [_process_text(x, context="latex") for x in headings_labels] - table_col_headings = "".join(latex_heading_row(content=headings_labels)) + # Prepend stub headers to column headings + all_headings = stub_headers + headings_labels + + table_col_headings = "".join(latex_heading_row(content=all_headings)) if spanner_row_count > 0: boxhead = data._boxhead @@ -313,6 +346,11 @@ def create_columns_component_l(data: GTData) -> str: spanner_lines = [] span_accumulator = 0 + # Add empty cells for stub columns in spanner row + if has_stub: + spanner_labs.extend([" "] * len(stub_layout)) + span_accumulator = len(stub_layout) + for j, level_i_spanner_j in enumerate(level_i_spanners): if level_i_spanner_j is None: # Get the number of columns to span nothing @@ -385,29 +423,90 @@ def create_body_component_l(data: GTData) -> str: # Get the default column vars column_vars = data._boxhead._get_default_columns() + # Check if stub is present and determine layout + has_summary_rows = bool(data._summary_rows or data._summary_rows_grand) + stub_layout = data._stub._get_stub_layout( + has_summary_rows=has_summary_rows, options=data._options + ) + + # Determine what stub components are present + has_row_stub_column = "rowname" in stub_layout + has_group_stub_column = "group_label" in stub_layout + has_groups = len(data._stub.group_ids) > 0 + + # Get the stub column info if it exists + row_stub_var = data._boxhead._get_stub_column() + body_rows = [] ordered_index: list[tuple[int, GroupRowInfo | None]] = data._stub.group_indices_map() - for i, _ in ordered_index: + prev_group_info = None + first_group_added = False + + # Calculate total number of columns for multicolumn spanning in group headers + n_cols = len(column_vars) + len(stub_layout) + + for i, group_info in ordered_index: + # Handle row group labels + if has_groups and group_info is not None: + # Only create group row if this is first row of the group + if group_info is not prev_group_info: + group_label = group_info.defaulted_label() + + # Process the group label for LaTeX + group_label = _process_text(group_label, context="latex") + + # When group is shown as a column, we don't add a separate row + # Instead, it will be added as a cell in each data row + if not has_group_stub_column: + # Add midrule before group heading (except for first group, which already has + # one from column headers) then the group heading, then midrule after + if first_group_added: + group_row = f"\\midrule\\addlinespace[2.5pt]\n\\multicolumn{{{n_cols}}}{{l}}{{{group_label}}} \\\\[2.5pt] \n\\midrule\\addlinespace[2.5pt]" + else: + group_row = f"\\multicolumn{{{n_cols}}}{{l}}{{{group_label}}} \\\\[2.5pt] \n\\midrule\\addlinespace[2.5pt]" + first_group_added = True + body_rows.append(group_row) + + # Create data row cells body_cells: list[str] = [] - # Create a body row + # Add stub cells first (group_label column, then rowname column) + if has_group_stub_column and group_info is not None: + # Only show group label in first row of group + if group_info is prev_group_info: + # Use an empty cell for continuation rows in same group + body_cells.append("") + else: + # Get the group label from the group info + group_label = group_info.defaulted_label() + group_label = _process_text(group_label, context="latex") + + body_cells.append(group_label) + + if has_row_stub_column: + # Get the row name from the stub + rowname = _get_cell(tbl_data, i, row_stub_var.var) + rowname_str = str(rowname) + + body_cells.append(rowname_str) + + # Add data cells for colinfo in column_vars: cell_content = _get_cell(tbl_data, i, colinfo.var) cell_str: str = str(cell_content) body_cells.append(cell_str) - # When joining the body cells together, we need to ensure that each item is separated by - # an ampersand and that the row is terminated with a double backslash - body_cells = " & ".join(body_cells) + " \\\\" + # Join cells with ampersand and terminate with a double backslash + body_row_str = " & ".join(body_cells) + " \\\\" - body_rows.append("".join(body_cells)) + body_rows.append(body_row_str) - # When joining all the body rows together, we need to ensure that each row is separated by - # newline except for the last + prev_group_info = group_info + # Join all body rows with newlines all_body_rows = "\n".join(body_rows) return all_body_rows @@ -553,26 +652,6 @@ def _render_as_latex(data: GTData, use_longtable: bool = False, tbl_pos: str | N if data._styles: _not_implemented("Styles are not yet supported in LaTeX output.") - # Get list representation of stub layout - has_summary_rows = bool(data._summary_rows or data._summary_rows_grand) - stub_layout = data._stub._get_stub_layout( - has_summary_rows=has_summary_rows, options=data._options - ) - - # Throw exception if a stub is present in the table - if "rowname" in stub_layout or "group_label" in stub_layout: - raise NotImplementedError( - "The table stub (row names and/or row groups) are not yet supported in LaTeX output." - ) - - # Determine if row groups are used - has_groups = len(data._stub.group_ids) > 0 - - # Throw exception if row groups are used in LaTeX output (extra case where row - # groups are used but not in the stub) - if has_groups: - raise NotImplementedError("Row groups are not yet supported in LaTeX output.") - # Create a LaTeX fragment for the start of the table table_start = create_table_start_l(data=data, use_longtable=use_longtable) diff --git a/tests/__snapshots__/test_utils_render_latex.ambr b/tests/__snapshots__/test_utils_render_latex.ambr index 813970c3c..e382edead 100644 --- a/tests/__snapshots__/test_utils_render_latex.ambr +++ b/tests/__snapshots__/test_utils_render_latex.ambr @@ -63,6 +63,63 @@ ''' # --- +# name: test_snap_render_as_latex_groups_as_column + ''' + \begin{table}[!t] + + + \fontsize{12.0pt}{14.4pt}\selectfont + + \begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}l|rllrrrrl} + \toprule + & num & char & fctr & date & time & datetime & currency & row \\ + \midrule\addlinespace[2.5pt] + grp\_a & 0.1111 & apricot & one & 2015-01-15 & 13:35 & 2018-01-01 02:22 & 49.95 & row\_1 \\ + & 2.222 & banana & two & 2015-02-15 & 14:40 & 2018-02-02 14:33 & 17.95 & row\_2 \\ + & 33.33 & coconut & three & 2015-03-15 & 15:45 & 2018-03-03 03:44 & 1.39 & row\_3 \\ + & 444.4 & durian & four & 2015-04-15 & 16:50 & 2018-04-04 15:55 & 65100.0 & row\_4 \\ + grp\_b & 5550.0 & nan & five & 2015-05-15 & 17:55 & 2018-05-05 04:00 & 1325.81 & row\_5 \\ + & nan & fig & six & 2015-06-15 & nan & 2018-06-06 16:11 & 13.255 & row\_6 \\ + & 777000.0 & grapefruit & seven & nan & 19:10 & 2018-07-07 05:22 & nan & row\_7 \\ + & 8880000.0 & honeydew & eight & 2015-08-15 & 20:20 & nan & 0.44 & row\_8 \\ + \bottomrule + \end{tabular*} + + \end{table} + + ''' +# --- +# name: test_snap_render_as_latex_groups_only + ''' + \begin{table}[!t] + + + \fontsize{12.0pt}{14.4pt}\selectfont + + \begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}rllrrrrl} + \toprule + num & char & fctr & date & time & datetime & currency & row \\ + \midrule\addlinespace[2.5pt] + \multicolumn{8}{l}{grp\_a} \\[2.5pt] + \midrule\addlinespace[2.5pt] + 0.1111 & apricot & one & 2015-01-15 & 13:35 & 2018-01-01 02:22 & 49.95 & row\_1 \\ + 2.222 & banana & two & 2015-02-15 & 14:40 & 2018-02-02 14:33 & 17.95 & row\_2 \\ + 33.33 & coconut & three & 2015-03-15 & 15:45 & 2018-03-03 03:44 & 1.39 & row\_3 \\ + 444.4 & durian & four & 2015-04-15 & 16:50 & 2018-04-04 15:55 & 65100.0 & row\_4 \\ + \midrule\addlinespace[2.5pt] + \multicolumn{8}{l}{grp\_b} \\[2.5pt] + \midrule\addlinespace[2.5pt] + 5550.0 & nan & five & 2015-05-15 & 17:55 & 2018-05-05 04:00 & 1325.81 & row\_5 \\ + nan & fig & six & 2015-06-15 & nan & 2018-06-06 16:11 & 13.255 & row\_6 \\ + 777000.0 & grapefruit & seven & nan & 19:10 & 2018-07-07 05:22 & nan & row\_7 \\ + 8880000.0 & honeydew & eight & 2015-08-15 & 20:20 & nan & 0.44 & row\_8 \\ + \bottomrule + \end{tabular*} + + \end{table} + + ''' +# --- # name: test_snap_render_as_latex_longtable ''' \begingroup @@ -96,3 +153,112 @@ ''' # --- +# name: test_snap_render_as_latex_no_stub_no_groups + ''' + \begin{table}[!t] + + + \fontsize{12.0pt}{14.4pt}\selectfont + + \begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}rllrrrrll} + \toprule + num & char & fctr & date & time & datetime & currency & row & group \\ + \midrule\addlinespace[2.5pt] + 0.1111 & apricot & one & 2015-01-15 & 13:35 & 2018-01-01 02:22 & 49.95 & row\_1 & grp\_a \\ + 2.222 & banana & two & 2015-02-15 & 14:40 & 2018-02-02 14:33 & 17.95 & row\_2 & grp\_a \\ + 33.33 & coconut & three & 2015-03-15 & 15:45 & 2018-03-03 03:44 & 1.39 & row\_3 & grp\_a \\ + 444.4 & durian & four & 2015-04-15 & 16:50 & 2018-04-04 15:55 & 65100.0 & row\_4 & grp\_a \\ + 5550.0 & nan & five & 2015-05-15 & 17:55 & 2018-05-05 04:00 & 1325.81 & row\_5 & grp\_b \\ + nan & fig & six & 2015-06-15 & nan & 2018-06-06 16:11 & 13.255 & row\_6 & grp\_b \\ + 777000.0 & grapefruit & seven & nan & 19:10 & 2018-07-07 05:22 & nan & row\_7 & grp\_b \\ + 8880000.0 & honeydew & eight & 2015-08-15 & 20:20 & nan & 0.44 & row\_8 & grp\_b \\ + \bottomrule + \end{tabular*} + + \end{table} + + ''' +# --- +# name: test_snap_render_as_latex_stub_and_groups + ''' + \begin{table}[!t] + + + \fontsize{12.0pt}{14.4pt}\selectfont + + \begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}l|rllrrrr} + \toprule + & num & char & fctr & date & time & datetime & currency \\ + \midrule\addlinespace[2.5pt] + \multicolumn{8}{l}{grp\_a} \\[2.5pt] + \midrule\addlinespace[2.5pt] + row\_1 & 0.1111 & apricot & one & 2015-01-15 & 13:35 & 2018-01-01 02:22 & 49.95 \\ + row\_2 & 2.222 & banana & two & 2015-02-15 & 14:40 & 2018-02-02 14:33 & 17.95 \\ + row\_3 & 33.33 & coconut & three & 2015-03-15 & 15:45 & 2018-03-03 03:44 & 1.39 \\ + row\_4 & 444.4 & durian & four & 2015-04-15 & 16:50 & 2018-04-04 15:55 & 65100.0 \\ + \midrule\addlinespace[2.5pt] + \multicolumn{8}{l}{grp\_b} \\[2.5pt] + \midrule\addlinespace[2.5pt] + row\_5 & 5550.0 & nan & five & 2015-05-15 & 17:55 & 2018-05-05 04:00 & 1325.81 \\ + row\_6 & nan & fig & six & 2015-06-15 & nan & 2018-06-06 16:11 & 13.255 \\ + row\_7 & 777000.0 & grapefruit & seven & nan & 19:10 & 2018-07-07 05:22 & nan \\ + row\_8 & 8880000.0 & honeydew & eight & 2015-08-15 & 20:20 & nan & 0.44 \\ + \bottomrule + \end{tabular*} + + \end{table} + + ''' +# --- +# name: test_snap_render_as_latex_stub_and_groups_as_column + ''' + \begin{table}[!t] + + + \fontsize{12.0pt}{14.4pt}\selectfont + + \begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}ll|rllrrrr} + \toprule + & & num & char & fctr & date & time & datetime & currency \\ + \midrule\addlinespace[2.5pt] + grp\_a & row\_1 & 0.1111 & apricot & one & 2015-01-15 & 13:35 & 2018-01-01 02:22 & 49.95 \\ + & row\_2 & 2.222 & banana & two & 2015-02-15 & 14:40 & 2018-02-02 14:33 & 17.95 \\ + & row\_3 & 33.33 & coconut & three & 2015-03-15 & 15:45 & 2018-03-03 03:44 & 1.39 \\ + & row\_4 & 444.4 & durian & four & 2015-04-15 & 16:50 & 2018-04-04 15:55 & 65100.0 \\ + grp\_b & row\_5 & 5550.0 & nan & five & 2015-05-15 & 17:55 & 2018-05-05 04:00 & 1325.81 \\ + & row\_6 & nan & fig & six & 2015-06-15 & nan & 2018-06-06 16:11 & 13.255 \\ + & row\_7 & 777000.0 & grapefruit & seven & nan & 19:10 & 2018-07-07 05:22 & nan \\ + & row\_8 & 8880000.0 & honeydew & eight & 2015-08-15 & 20:20 & nan & 0.44 \\ + \bottomrule + \end{tabular*} + + \end{table} + + ''' +# --- +# name: test_snap_render_as_latex_stub_only + ''' + \begin{table}[!t] + + + \fontsize{12.0pt}{14.4pt}\selectfont + + \begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}l|rllrrrrl} + \toprule + & num & char & fctr & date & time & datetime & currency & group \\ + \midrule\addlinespace[2.5pt] + row\_1 & 0.1111 & apricot & one & 2015-01-15 & 13:35 & 2018-01-01 02:22 & 49.95 & grp\_a \\ + row\_2 & 2.222 & banana & two & 2015-02-15 & 14:40 & 2018-02-02 14:33 & 17.95 & grp\_a \\ + row\_3 & 33.33 & coconut & three & 2015-03-15 & 15:45 & 2018-03-03 03:44 & 1.39 & grp\_a \\ + row\_4 & 444.4 & durian & four & 2015-04-15 & 16:50 & 2018-04-04 15:55 & 65100.0 & grp\_a \\ + row\_5 & 5550.0 & nan & five & 2015-05-15 & 17:55 & 2018-05-05 04:00 & 1325.81 & grp\_b \\ + row\_6 & nan & fig & six & 2015-06-15 & nan & 2018-06-06 16:11 & 13.255 & grp\_b \\ + row\_7 & 777000.0 & grapefruit & seven & nan & 19:10 & 2018-07-07 05:22 & nan & grp\_b \\ + row\_8 & 8880000.0 & honeydew & eight & 2015-08-15 & 20:20 & nan & 0.44 & grp\_b \\ + \bottomrule + \end{tabular*} + + \end{table} + + ''' +# --- diff --git a/tests/test_utils_render_latex.py b/tests/test_utils_render_latex.py index e91203f01..6cad06887 100644 --- a/tests/test_utils_render_latex.py +++ b/tests/test_utils_render_latex.py @@ -494,20 +494,133 @@ def test_snap_render_as_latex_floating_table(snapshot): assert snapshot == latex_str -def test_render_as_latex_stub_raises(): +def test_render_as_latex_with_stub(): gt_tbl = GT(exibble, rowname_col="row") - with pytest.raises(NotImplementedError) as exc_info: - _render_as_latex(data=gt_tbl._build_data(context="latex")) + latex_str = _render_as_latex(data=gt_tbl._build_data(context="latex")) - assert ( - "The table stub (row names and/or row groups) are not yet supported in LaTeX output." - in exc_info.value.args[0] + # Check that stub column is present + assert "l|" in latex_str + + # Check that row names are present + assert "row\\_1" in latex_str + + # Check proper structuring with top and bottom rules + assert "\\toprule" in latex_str + assert "\\bottomrule" in latex_str + + +def test_render_as_latex_with_rowgroups(): + gt_tbl = GT(exibble, groupname_col="group") + latex_str = _render_as_latex(data=gt_tbl._build_data(context="latex")) + + # Row group headers should be present + assert "\\multicolumn" in latex_str + + # Check that row group names are present + assert "grp\\_a" in latex_str + assert "grp\\_b" in latex_str + + # Check for midrule associated with group headers + assert "\\midrule\\addlinespace[2.5pt]" in latex_str + + +def test_render_as_latex_with_stub_and_rowgroups(): + gt_tbl = GT(exibble, rowname_col="row", groupname_col="group") + latex_str = _render_as_latex(data=gt_tbl._build_data(context="latex")) + + # Check that stub column is present + assert "l|" in latex_str + + # Row group headers should be present + assert "\\multicolumn" in latex_str + + # Check that both row names and groups are present + assert "row\\_1" in latex_str + assert "grp\\_a" in latex_str + + +def test_render_as_latex_no_stub_no_groups(): + gt_tbl = GT(exibble) + latex_str = _render_as_latex(data=gt_tbl._build_data(context="latex")) + + # There should be no stub separator + assert "l|" not in latex_str + + # There should be no multicolumn command for row groups + assert "\\multicolumn" not in latex_str + + +def test_render_as_latex_groups_as_column(): + gt_tbl = GT(exibble, groupname_col="group").tab_options(row_group_as_column=True) + latex_str = _render_as_latex(data=gt_tbl._build_data(context="latex")) + + # There should be a stub separator for the group column + assert "l|" in latex_str + + # There should not be a multicolumn command + assert "\\multicolumn" not in latex_str + + # Group labels should appear in cells + assert "grp\\_a" in latex_str + assert "grp\\_b" in latex_str + + +def test_render_as_latex_stub_and_groups_as_column(): + gt_tbl = GT(exibble, rowname_col="row", groupname_col="group").tab_options( + row_group_as_column=True ) + latex_str = _render_as_latex(data=gt_tbl._build_data(context="latex")) + + # Should have two stub columns (group and rowname) + assert "ll|" in latex_str + # There should not be a multicolumn command + assert "\\multicolumn" not in latex_str -def test_render_as_latex_rowgroup_raises(): + # Both row names and groups should be present + assert "row\\_1" in latex_str + assert "grp\\_a" in latex_str + + +def test_snap_render_as_latex_stub_only(snapshot): + gt_tbl = GT(exibble, rowname_col="row") + latex_str = _render_as_latex(data=gt_tbl._build_data(context="latex")) + + assert snapshot == latex_str + + +def test_snap_render_as_latex_groups_only(snapshot): gt_tbl = GT(exibble, groupname_col="group") - with pytest.raises(NotImplementedError) as exc_info: - _render_as_latex(data=gt_tbl._build_data(context="latex")) + latex_str = _render_as_latex(data=gt_tbl._build_data(context="latex")) + + assert snapshot == latex_str + + +def test_snap_render_as_latex_stub_and_groups(snapshot): + gt_tbl = GT(exibble, rowname_col="row", groupname_col="group") + latex_str = _render_as_latex(data=gt_tbl._build_data(context="latex")) + + assert snapshot == latex_str - assert "Row groups are not yet supported in LaTeX output." in exc_info.value.args[0] + +def test_snap_render_as_latex_no_stub_no_groups(snapshot): + gt_tbl = GT(exibble) + latex_str = _render_as_latex(data=gt_tbl._build_data(context="latex")) + + assert snapshot == latex_str + + +def test_snap_render_as_latex_groups_as_column(snapshot): + gt_tbl = GT(exibble, groupname_col="group").tab_options(row_group_as_column=True) + latex_str = _render_as_latex(data=gt_tbl._build_data(context="latex")) + + assert snapshot == latex_str + + +def test_snap_render_as_latex_stub_and_groups_as_column(snapshot): + gt_tbl = GT(exibble, rowname_col="row", groupname_col="group").tab_options( + row_group_as_column=True + ) + latex_str = _render_as_latex(data=gt_tbl._build_data(context="latex")) + + assert snapshot == latex_str