From b819cb13445a49c39204e9f8f8689975e8a7ef75 Mon Sep 17 00:00:00 2001 From: clintval Date: Wed, 9 Sep 2026 21:17:54 -0400 Subject: [PATCH 1/2] fix: order secondary tables by their level order Tables named by levels used to appear in the order MultiQC ran the modules that first fed them, which put a one-row cohort table above the library table. They now follow the order of the levels in the config, and a table that received no rows is left out. --- README.md | 2 +- multiqc_pivot/pivot.py | 10 +++++----- tests/test_pivot.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c32f460..c65fd1c 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ And your report will look like: | `levels` | An ordered list; the first level whose `match` is found in a sample name wins. Required. | | `levels[].match` | A regular expression searched in the sample name. Named captures are available to `label`. | | `levels[].label` | A format string built from the captures of `match`. Columns of matching rows are renamed with it and folded onto the group row; the row itself stays beneath. Omit it, and omit `table`, to fold the row's columns onto the group row unchanged. | -| `levels[].table` | The name of a table that receives matching rows instead of General Statistics. Rows keep their grouping, so paired reads stay nested under their library. | +| `levels[].table` | The name of a table that receives matching rows instead of General Statistics. Rows keep their grouping, so paired reads stay nested under their library. Tables sit directly under General Statistics in the order their levels are listed. | | `column_title` | How a pivoted column is titled. `{label}` is the label as written, `{Label}` has its first letter upper-cased, `{title}` is the module's title. Default `{Label} {title}`. | | `label_order` | Labels in the order their column blocks should appear. Labels not listed follow in order of first appearance. | | `tables` | Presentation of the tables named by `levels[].table`, currently a `description` each. | diff --git a/multiqc_pivot/pivot.py b/multiqc_pivot/pivot.py index 2d23ec3..5ef430b 100644 --- a/multiqc_pivot/pivot.py +++ b/multiqc_pivot/pivot.py @@ -211,13 +211,14 @@ def pivot(rows: Rows, headers: Headers, settings: SamplePivotConfig) -> PivotRes Rows that match a labelled level have their declared columns renamed after the label and copied onto the group's row; the original rows stay beneath it so the group can still be expanded. Rows that match an unlabelled level are folded onto the group's row as they are. Rows that match - a level with a table are moved into that table with their grouping intact. Rows that match no + a level with a table are moved into that table with their grouping intact; the tables come back + in the order their levels are listed, without the ones that received no rows. Rows that match no level, and columns a module did not declare a header for, are left alone. """ placement = Placement(settings.label_order) out_rows: Rows = {} out_headers: Headers = {} - tables: dict[str, Table] = {} + tables = {level.table: Table() for level in settings.levels if level.table is not None} for section, rows_by_group in rows.items(): section_headers = headers.get(section, {}) pivoted = SectionPivot(section_headers, settings, placement) @@ -227,15 +228,14 @@ def pivot(rows: Rows, headers: Headers, settings: SamplePivotConfig) -> PivotRes if route is None: pivoted.keep(group, row) elif isinstance(route, Moved): - table = tables.setdefault(route.table, Table()) - table.add(section, group, row, section_headers) + tables[route.table].add(section, group, row, section_headers) elif route.label is None: pivoted.fold(route.group, row) else: pivoted.fold_labelled(route.group, route.label, row) out_rows[section] = pivoted.finish() out_headers[section] = pivoted.new_headers - return PivotResult(out_rows, out_headers, tables) + return PivotResult(out_rows, out_headers, {n: t for n, t in tables.items() if t.rows}) def _fold(target: RowData, key: ColumnKeyT, value: ExtValueT | None, group: str) -> None: diff --git a/tests/test_pivot.py b/tests/test_pivot.py index 356adb3..a3d61e9 100644 --- a/tests/test_pivot.py +++ b/tests/test_pivot.py @@ -158,6 +158,34 @@ def test_table_levels_move_rows_with_their_grouping() -> None: assert table.headers == headers +def test_tables_follow_level_order_not_module_order() -> None: + settings = SETTINGS.model_copy( + update={ + "levels": [ + Level(match=r"\.library\.", table="Library statistics"), + Level(match=r"^cohort\.", table="Cohort annotation"), + Level(match=r"\.nothing\.", table="Never filled"), + ] + } + ) + snpeff, fastqc = SectionKey("snpeff"), SectionKey("fastqc") + rows = { + snpeff: section(row("cohort.somatic", variants=1234)), + fastqc: section(row("101.tissueA.library.L1", dups=70.0)), + } + headers = { + snpeff: {ColumnKey("variants"): header("Variants")}, + fastqc: {ColumnKey("dups"): header("Dups")}, + } + + result = pivot(rows, headers, settings) + + assert list(result.tables) == ["Library statistics", "Cohort annotation"] + assert result.tables["Cohort annotation"].rows == { + snpeff: section(row("cohort.somatic", variants=1234)) + } + + def test_unmatched_rows_are_untouched() -> None: other = SectionKey("other") rows = {other: section(row("control", median=1))} From 66f79d6ee3617f2ec31b795ce0a82d0284cad684 Mon Sep 17 00:00:00 2001 From: clintval Date: Wed, 9 Sep 2026 21:18:09 -0400 Subject: [PATCH 2/2] chore(release): bump to 0.1.1 --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dfe41b0..147e9d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "multiqc-pivot" -version = "0.1.0" +version = "0.1.1" description = "A MultiQC plugin that folds related samples into one General Statistics row per group with labelled metric columns." readme = "README.md" authors = [{ name = "Clint Valentine", email = "valentine.clint@gmail.com" }] diff --git a/uv.lock b/uv.lock index 3b69de7..25e63dc 100644 --- a/uv.lock +++ b/uv.lock @@ -701,7 +701,7 @@ wheels = [ [[package]] name = "multiqc-pivot" -version = "0.1.0" +version = "0.1.1" source = { editable = "." } dependencies = [ { name = "multiqc" },