Repository: MultiQC/MultiQC (2025-08-19T10:03:29Z)

Issue:
Bulk sample renaming not working as expected 
### Description of bug

A regression has been observed while trying to use bulk sample renaming in reports. The [sampleinfo.txt](https://github.com/user-attachments/files/21614836/sampleinfo.txt) file containing columns "MultiQC Names", "Original Sample Name" and "id_Original Sample Name" is not correctly parsed in report for interactive renaming in the report. While the values from the file appear to be there in the renaming tool (metadata is visible) but by default "Original Sample Name" is reflecting in the report but not changing on using "MultiQC Names" and "id_Original Sample Name" tabs.

**This issue was not present in MultiQC v1.14, where the same sampleinfo file worked as expected**. By default report is showing "MultiQC Names" and names changing interactively on using other two tabs. 

 I also tried version 1.21 and 1.29 but was not working.

Here is the multiqc command that I am using:
_**multiqc --interactive --ignore "multiqc" -f -n project_name -c /path/to/yaml/config_multiqc_submission.yaml -o /path/to/project/multiqc_output/ /path/to/project/samples/*/*/dragen.4.4.4/ /path/to/project/configs --sample-names /path/to/sampleinfo/sampleinfo.txt**_

Note: This is just test data

### File that triggers the error

[sampleinfo.txt](https://github.com/user-attachments/files/21614836/sampleinfo.txt)

### MultiQC Error log

```console
/// MultiQC 🔍 v1.30

            config | Loading config settings from: /path/to/project/config_multiqc_submission.yaml
       file_search | Search path: /path/to/sample1/dragen.4.4.4
       file_search | Search path: /path/to/sample2/dragen.4.4.4
       file_search | Search path: /path/to/project/configs

    custom_content | software: Found 1 sample (html)
    custom_content | qcflags: Found 5 samples (PlotType.HEATMAP)
    custom_content | dragen-wgs-pipeline-description: Found 1 sample (html)
    custom_content | dragen-wgs-workflow: Found 1 sample (html)
   mapping_metrics | sum of unique/duplicate/unmapped reads not matching total, skipping mapping/duplicates percentages plot for: 0201330541 (LAA0018456_0201330541_HHC7NDSX7_4) 
   mapping_metrics | sum of unique/duplicate/unmapped reads not matching total, skipping mapping/duplicates percentages plot for: 0201330541 (LAA0018456_0201330541_HHCFHDSX7_3) 
   mapping_metrics | sum of unique/duplicate/unmapped reads not matching total, skipping mapping/duplicates percentages plot for: 0201330541 (LAA0018456_0201330541_HHCFHDSX7_1) 
   mapping_metrics | sum of unique/duplicate/unmapped reads not matching total, skipping mapping/duplicates percentages plot for: 0201330541 (LAA0018456_0201330541_HHCFHDSX7_2) 
   mapping_metrics | sum of unique/duplicate/unmapped reads not matching total, skipping mapping/duplicates percentages plot for: 0201330571 (LAA0018544_0201330571_HHC7NDSX7_4) 
   mapping_metrics | sum of unique/duplicate/unmapped reads not matching total, skipping mapping/duplicates percentages plot for: 0201330571 (LAA0018544_0201330571_HHCFHDSX7_3) 
   mapping_metrics | sum of unique/duplicate/unmapped reads not matching total, skipping mapping/duplicates percentages plot for: 0201330571 (LAA0018544_0201330571_HHCFHDSX7_1) 
   mapping_metrics | sum of unique/duplicate/unmapped reads not matching total, skipping mapping/duplicates percentages plot for: 0201330571 (LAA0018544_0201330571_HHCFHDSX7_2) 
            dragen | Found 2 add_mapping_metrics reports
            dragen | Found 2 add_vc_metrics reports
            dragen | Found 2 add_ploidy_estimation_metrics reports
            dragen | Found 2 collect_overall_mean_cov_data reports
            violin | All values are NaN or Inf for metric: Mean/med autosomal coverage
            violin | All values are NaN or Inf for metric: Mean/med autosomal coverage
            violin | All values are NaN or Inf for metric: Mean/med autosomal coverage
            violin | All values are NaN or Inf for metric: Mean/med autosomal coverage
            dragen | Found 2 add_coverage_metrics reports
            dragen | Found 2 add_coverage_hist reports
            dragen | Found 2 add_coverage_per_contig reports
            dragen | Found 2 add_fragment_length_hist reports
            dragen | Found 2 add_gc_metrics_hist reports
            dragen | Found 2 add_trimmer_metrics reports
            dragen | Found 2 add_time_metrics reports
            dragen | Found 0 add_rna_metrics reports
            dragen | Found 0 add_rna_transcript_coverage reports
            dragen | Found 0 add_sc_rna_metrics reports
            dragen | Found 0 add_sc_atac_metrics reports
     dragen_fastqc | Found 2 reports
     write_results | Rendering plots. Export plots to formats png, svg, pdf is requested, so it might take a while. To disable plot export, set `export_plots: false` in config, or remove the `--export-plots` command line flag

              plot | Batch plot export timed out after 30s. Only 0 of 90 plots were exported. This is likely due to a known issue in Kaleido. The remaining plots will be skipped but the report will continue to generate.
              plot | Some plot exports failed: 0/90 completed
              plot | Failed to export the following PNG images: /tmp/tmpxxxxxxx/multiqc_plots/png/[...many filenames omitted for brevity...]

     write_results | Data        : /path/to/multiqc/output_data_folder   (overwritten)
     write_results | Report      : /path/to/multiqc/report.html          (overwritten)
     write_results | Plots       : /path/to/multiqc/plots_folder         (overwritten)
           multiqc | MultiQC complete
```

### Before submitting

- [x] I have read the [troubleshooting documentation](https://docs.seqera.io/multiqc/usage/troubleshooting/).
- [x] I am using the latest release of MultiQC.
- [x] I have included a full MultiQC log, not truncated.
- [x] I have attached an input file (**.zip** if necessary) that triggers the error.


Discussion:
Thank you for reporting the issue! This PR fixes the buttons behaviour. Let me know if it works!
https://github.com/MultiQC/MultiQC/pull/3300
Thanks a lot, Vlad..!  I just tested the fix and the sample renaming tabs now work as expected within report. Really appreciate the quick response and patch on this. It clears up the issue I was running into. 



Patch:
diff --git a/multiqc/report.py b/multiqc/report.py
index 4a109ff39a..a8a674ece1 100644
--- a/multiqc/report.py
+++ b/multiqc/report.py
@@ -1069,6 +1069,8 @@ def multiqc_dump_json(data_dir: Path):
             "title",
             "version",
             "output_dir",
+            "sample_names_rename",
+            "sample_names_rename_buttons",
         ],
     }
     for pymod, names in export_vars.items():
diff --git a/multiqc/templates/default/assets/js/toolbox.js b/multiqc/templates/default/assets/js/toolbox.js
index b71e2f389f..9bda73a4a0 100644
--- a/multiqc/templates/default/assets/js/toolbox.js
+++ b/multiqc/templates/default/assets/js/toolbox.js
@@ -792,13 +792,26 @@ $(function () {
   if (!has_rename_filters && mqc_config.sample_names_rename && mqc_config.sample_names_rename.length > 0) {
     let mqc_renamesamples_idx = 300;
 
-    // Add each pattern
+    // Check which button is initially active to determine which column to use
+    const activeButton = $(".mqc_sname_switches.active");
+    let activeIndex = 0;
+    if (activeButton.length > 0) {
+      activeIndex = parseInt(activeButton.data("index")) || 0;
+    }
+
+    // If index 0 is active, don't apply any rename filters (show original names)
+    if (activeIndex === 0) {
+      // Don't add any filters, just return
+      return;
+    }
+
+    // For other indices, add rename patterns using the correct column
     for (let i = 0; i < mqc_config.sample_names_rename.length; i++) {
       const pattern = mqc_config.sample_names_rename[i];
-      if (!Array.isArray(pattern) || pattern.length < 2) continue;
+      if (!Array.isArray(pattern) || pattern.length <= activeIndex) continue;
 
       const from_text = pattern[0];
-      const to_text = pattern[1];
+      const to_text = pattern[activeIndex];
 
       // Add to the filters list
       $("#mqc_renamesamples_filters").append(


Test patch:
diff --git a/tests/test_sample_renaming.py b/tests/test_sample_renaming.py
new file mode 100644
index 0000000000..b9dcc076b7
--- /dev/null
+++ b/tests/test_sample_renaming.py
@@ -0,0 +1,219 @@
+"""Test that the sample renaming functionality works as expected."""
+
+import tempfile
+import os
+import subprocess
+import json
+from pathlib import Path
+
+import pytest
+
+from multiqc import config
+from multiqc.core.update_config import ClConfig, update_config
+
+
+@pytest.fixture
+def sample_names_file():
+    """Create a temporary sample names file for testing."""
+    content = """MultiQC Names\tOriginal Sample Name\tid_Original Sample Name
+Sample_A\tOriginal_A\tID_A
+Sample_B\tOriginal_B\tID_B
+Sample_C\tOriginal_C\tID_C"""
+
+    with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
+        f.write(content)
+        f.flush()
+        yield f.name
+    os.unlink(f.name)
+
+
+def test_load_sample_names_file(sample_names_file):
+    """Test that sample names file is loaded correctly."""
+    # Reset config
+    config.reset()
+
+    # Load the sample names file
+    config.load_sample_names(Path(sample_names_file))
+
+    # Check that the config was updated correctly
+    assert len(config.sample_names_rename_buttons) == 3
+    assert config.sample_names_rename_buttons == ["MultiQC Names", "Original Sample Name", "id_Original Sample Name"]
+
+    assert len(config.sample_names_rename) == 3
+    assert config.sample_names_rename[0] == ["Sample_A", "Original_A", "ID_A"]
+    assert config.sample_names_rename[1] == ["Sample_B", "Original_B", "ID_B"]
+    assert config.sample_names_rename[2] == ["Sample_C", "Original_C", "ID_C"]
+
+
+def test_sample_names_config_integration():
+    """Test that sample names config is properly integrated via update_config."""
+    with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
+        f.write("Button1\tButton2\nSample1\tAltName1\nSample2\tAltName2")
+        f.flush()
+
+        try:
+            # Reset config
+            config.reset()
+
+            # Use update_config to load sample names
+            update_config(cfg=ClConfig(sample_names=f.name))
+
+            # Verify the config was updated
+            assert len(config.sample_names_rename_buttons) == 2
+            assert config.sample_names_rename_buttons == ["Button1", "Button2"]
+            assert len(config.sample_names_rename) == 2
+            assert config.sample_names_rename[0] == ["Sample1", "AltName1"]
+            assert config.sample_names_rename[1] == ["Sample2", "AltName2"]
+        finally:
+            os.unlink(f.name)
+
+
+def test_sample_names_cmdline_integration(tmp_path):
+    """Test that sample renaming works via command line interface with custom content."""
+
+    # Create custom content with known sample names
+    custom_content = "#id: test_custom\n#plot_type: table\n#section_name: Test Custom Data\nSample_Name\tValue1\tValue2\nSample1\t100\t200\nSample2\t150\t250\nSample3\t120\t220"
+
+    custom_file = tmp_path / "test_data_mqc.txt"
+    with open(custom_file, "w") as f:
+        f.write(custom_content)
+
+    # Create sample names file that maps the actual sample names
+    sample_names_content = "MultiQC Names\tOriginal Sample Name\tid_Original Sample Name\nSample1\tOriginal_Sample_1\tID_Sample_1\nSample2\tOriginal_Sample_2\tID_Sample_2\nSample3\tOriginal_Sample_3\tID_Sample_3"
+
+    sample_names_file = tmp_path / "sample_names.txt"
+    with open(sample_names_file, "w") as f:
+        f.write(sample_names_content)
+
+    # Run MultiQC with sample names file
+    cmd = ["multiqc", str(tmp_path), "--sample-names", str(sample_names_file), "--filename", "test_report", "--force"]
+
+    result = subprocess.run(cmd, cwd=tmp_path, capture_output=True, text=True)
+
+    # Check that MultiQC ran successfully
+    assert result.returncode == 0, f"MultiQC failed with: {result.stderr}"
+
+    # Check that report was generated
+    assert (tmp_path / "test_report.html").exists()
+    assert (tmp_path / "test_report_data").exists()
+
+    # Check that the data file contains the sample rename configuration
+    with open(tmp_path / "test_report_data" / "multiqc_data.json", "r") as f:
+        data = json.load(f)
+
+    # Verify that sample_names_rename config is present in the report data
+    assert "config_sample_names_rename" in data
+    assert "config_sample_names_rename_buttons" in data
+
+    # Verify the content matches what we expect
+    assert data["config_sample_names_rename_buttons"] == [
+        "MultiQC Names",
+        "Original Sample Name",
+        "id_Original Sample Name",
+    ]
+    assert len(data["config_sample_names_rename"]) == 3
+
+    # Verify actual mapping content
+    assert data["config_sample_names_rename"][0] == ["Sample1", "Original_Sample_1", "ID_Sample_1"]
+    assert data["config_sample_names_rename"][1] == ["Sample2", "Original_Sample_2", "ID_Sample_2"]
+    assert data["config_sample_names_rename"][2] == ["Sample3", "Original_Sample_3", "ID_Sample_3"]
+
+    # Verify that HTML report contains sample renaming buttons
+    with open(tmp_path / "test_report.html", "r") as f:
+        html_content = f.read()
+
+    # Check that sample rename buttons are present
+    assert "Change sample names:" in html_content
+    assert "MultiQC Names" in html_content
+    assert "Original Sample Name" in html_content
+    assert "id_Original Sample Name" in html_content
+    assert "mqc_sname_switches" in html_content
+
+    # Check that the initial state shows the correct sample names in the table content
+    # When "MultiQC Names" is active (index 0), it should show Sample1, Sample2, Sample3
+    import re
+
+    # Extract the table content (the actual displayed data)
+    table_matches = re.findall(r'<span class="th-sample-name"[^>]*>([^<]+)</span>', html_content)
+    assert len(table_matches) == 3, f"Expected 3 sample names in table, got {len(table_matches)}: {table_matches}"
+
+    # Verify that the table shows the original sample names (Sample1, Sample2, Sample3)
+    assert "Sample1" in table_matches
+    assert "Sample2" in table_matches
+    assert "Sample3" in table_matches
+
+    # Verify that the renamed versions are NOT displayed in the table content
+    assert "Original_Sample_1" not in table_matches
+    assert "ID_Sample_1" not in table_matches
+
+    # The config data should still contain the mapping (this is expected for the buttons to work)
+    assert '"sample_names_rename"' in html_content
+    assert '"Original_Sample_1"' in html_content  # This should be in the config, not the table
+
+
+def test_sample_names_malformed_file():
+    """Test handling of malformed sample names files."""
+
+    # Test file with inconsistent columns
+    malformed_content = """Header1\tHeader2\tHeader3
+Sample1\tAlt1\tID1
+Sample2\tAlt2
+Sample3\tAlt3\tID3\tExtra"""
+
+    with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
+        f.write(malformed_content)
+        f.flush()
+
+        try:
+            config.reset()
+            config.load_sample_names(Path(f.name))
+
+            # Should still load header and all lines (but warn about inconsistent columns)
+            assert len(config.sample_names_rename_buttons) == 3
+            # All lines should be loaded but some may be malformed
+            assert len(config.sample_names_rename) == 3  # All three lines loaded, despite warnings
+
+        finally:
+            os.unlink(f.name)
+
+
+def test_sample_names_empty_file():
+    """Test handling of empty sample names file."""
+
+    with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
+        f.write("")
+        f.flush()
+
+        try:
+            config.reset()
+            config.load_sample_names(Path(f.name))
+
+            # Should have empty rename configs
+            assert len(config.sample_names_rename_buttons) == 0
+            assert len(config.sample_names_rename) == 0
+
+        finally:
+            os.unlink(f.name)
+
+
+def test_sample_names_single_column_file():
+    """Test that files with single columns are handled correctly."""
+
+    content = """OnlyHeader
+Sample1
+Sample2"""
+
+    with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
+        f.write(content)
+        f.flush()
+
+        try:
+            config.reset()
+            config.load_sample_names(Path(f.name))
+
+            # Single column files shouldn't be processed as rename configs
+            assert len(config.sample_names_rename_buttons) == 0
+            assert len(config.sample_names_rename) == 0
+
+        finally:
+            os.unlink(f.name)