From 9af50f13029e17241d0361eb11c318c6c248fff1 Mon Sep 17 00:00:00 2001 From: ljwharbers Date: Wed, 15 Jul 2026 12:36:14 +0200 Subject: [PATCH 01/17] feat: add LRSOMATICREPORT as the final pipeline step Wraps the lrsomatic_report R/Quarto tool (added as a git submodule) in a new local module that renders a self-contained per-sample HTML report from the pipeline's key outputs (VEP-annotated somatic SNVs, Severus SVs, ASCAT copy number, QC). Runs last, gated by --skip_report, so future dependents (e.g. a real Wakhan integration) can hook in without restructuring. Every report input is optional and joined by plain sample-id keys (not full meta maps) so missing/skipped upstream steps degrade gracefully instead of breaking the join. The module's environment.yml was verified against the actual R code (not just the tool's README) and trimmed to what's really used; a Wave container was built and validated with a real end-to-end render. Co-Authored-By: Claude Sonnet 5 --- .gitmodules | 3 + assets/lrsomatic_report | 1 + conf/modules.config | 10 ++ docs/output.md | 24 +++- docs/usage.md | 8 ++ modules/local/lrsomaticreport/environment.yml | 19 +++ modules/local/lrsomaticreport/main.nf | 112 ++++++++++++++++++ modules/local/lrsomaticreport/meta.yml | 87 ++++++++++++++ .../local/lrsomaticreport/tests/main.nf.test | 67 +++++++++++ .../lrsomaticreport/tests/main.nf.test.snap | 47 ++++++++ nextflow.config | 5 + nextflow_schema.json | 23 ++++ workflows/lrsomatic.nf | 107 +++++++++++++++++ 13 files changed, 511 insertions(+), 2 deletions(-) create mode 100644 .gitmodules create mode 160000 assets/lrsomatic_report create mode 100644 modules/local/lrsomaticreport/environment.yml create mode 100644 modules/local/lrsomaticreport/main.nf create mode 100644 modules/local/lrsomaticreport/meta.yml create mode 100644 modules/local/lrsomaticreport/tests/main.nf.test create mode 100644 modules/local/lrsomaticreport/tests/main.nf.test.snap diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..c95b45f7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "assets/lrsomatic_report"] + path = assets/lrsomatic_report + url = https://github.com/ljwharbers/lrsomatic_report.git diff --git a/assets/lrsomatic_report b/assets/lrsomatic_report new file mode 160000 index 00000000..2868c9fe --- /dev/null +++ b/assets/lrsomatic_report @@ -0,0 +1 @@ +Subproject commit 2868c9fec2f42bb6ed5ffcf41b41fe54c0185443 diff --git a/conf/modules.config b/conf/modules.config index 9f7d3c34..d4b90fad 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -558,6 +558,16 @@ process { ] } + withName : '.*:LRSOMATICREPORT' { + ext.prefix = { "${meta.id}" } + ext.args = { params.report_gene_panel ? "--gene-panel ${params.report_gene_panel}" : '' } + publishDir = [ + path: { "${params.outdir}/${meta.id}/report" }, + mode: params.publish_dir_mode, + saveAs: { filename -> filename.equals('versions.yml') ? null : filename } + ] + } + withName : '.*:WGET' { ext.args = { [ diff --git a/docs/output.md b/docs/output.md index 61d82b28..cb1d8c38 100644 --- a/docs/output.md +++ b/docs/output.md @@ -38,7 +38,8 @@ The pipeline produces per-sample output directories. Two modes exist depending o │ ├── vep │ │ ├── somatic │ │ └── SVs -│ └── wakhan +│ ├── wakhan +│ └── report ``` **Paired tumor + normal sample**: @@ -81,7 +82,8 @@ The pipeline produces per-sample output directories. Two modes exist depending o │ │ ├── germline │ │ ├── somatic │ │ └── SVs -│ └── wakhan +│ ├── wakhan +│ └── report ├── pipeline_info └── multiqc ``` @@ -516,6 +518,24 @@ Phased variant calls produced by Longphase. Present in all samples. +### `report` + +
+Output files + +``` +├── report +│ ├── {sample}_report.html +``` + +| File | Description | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `{sample}_report.html` | Self-contained per-sample HTML report ([lrsomatic_report](https://github.com/ljwharbers/lrsomatic_report)): circos plot, small/structural variant tables, ASCAT copy-number summary, and QC. Any section whose upstream data is unavailable (e.g. a skipped tool) shows a "not available" notice instead. | + +
+ +This is the final step of the pipeline, run after SNV/SV calling, ASCAT, and QC. Disable it with `--skip_report`. + ### `multiqc`
diff --git a/docs/usage.md b/docs/usage.md index ce5cec0e..5cb134d7 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -151,6 +151,7 @@ If you want to run with a CHM13 reference without using `--genome CHM13` (for ex | `--skip_modcall` | A boolean to skip modkit methylation calling. Default = `false` | | `--skip_modkit` | A boolean to skip the modkit pileup step. Default = `false` | | `--skip_whatshapstats` | A boolean to skip WhatsHap phasing statistics. Default = `false` | +| `--skip_report` | A boolean to skip the final per-sample HTML report. Default = `false` | #### LONGPHASE options: @@ -208,6 +209,13 @@ If you want to run with a CHM13 reference without using `--genome CHM13` (for ex | ---------------------- | ------------------------------------------------------------------------------------ | | `--severus_minsupport` | Minimum number of supporting reads required for SEVERUS to call an SV. Default = `3` | +#### Report Options + +| Parameter | Description | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--report_src` | Path to the [lrsomatic_report](https://github.com/ljwharbers/lrsomatic_report) repository (bin/, R/, templates/, assets/). Default = `${projectDir}/assets/lrsomatic_report` | +| `--report_gene_panel` | Gene panel for the report: a builtin panel name (e.g. `lymphoid`) or a path to a TSV file with a `gene` column. Default = `null` | + #### WAKHAN Options | Parameter | Description | diff --git a/modules/local/lrsomaticreport/environment.yml b/modules/local/lrsomaticreport/environment.yml new file mode 100644 index 00000000..340f3116 --- /dev/null +++ b/modules/local/lrsomaticreport/environment.yml @@ -0,0 +1,19 @@ +--- +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/environment-schema.json +channels: + - conda-forge + - bioconda +dependencies: + - "conda-forge::r-base=4.4.*" + - "conda-forge::quarto=1.5.*" + - "conda-forge::r-data.table" + - "conda-forge::r-dplyr" + - "conda-forge::r-dt" + - "conda-forge::r-htmltools" + - "conda-forge::r-optparse" + - "conda-forge::r-quarto" + - "conda-forge::r-yaml" + - "conda-forge::r-ggplot2" + - "conda-forge::r-svglite" + - "conda-forge::r-circlize" + - "conda-forge::r-knitr" diff --git a/modules/local/lrsomaticreport/main.nf b/modules/local/lrsomaticreport/main.nf new file mode 100644 index 00000000..4efde26b --- /dev/null +++ b/modules/local/lrsomaticreport/main.nf @@ -0,0 +1,112 @@ +process LRSOMATICREPORT { + tag "$meta.id" + label 'process_medium' + + conda "${moduleDir}/environment.yml" + // Built via the Wave containers API from this module's environment.yml (frozen build). + container "community.wave.seqera.io/library/r-base_quarto_r-data.table_r-dplyr_pruned:f1d36670d940c971" + + input: + // All per-sample report inputs are optional (path may be `[]` if the corresponding + // upstream tool was skipped or produced no output for this sample); the report tool + // renders a "not available" notice for any missing section. + tuple val(meta), path(vep_somatic), path(severus_vcf), path(somatic_vcf), path(ascat_files), path(qc_tumor_files), path(qc_normal_files) + path(report_src) // staged lrsomatic_report repo (bin/, R/, templates/, assets/) + + output: + tuple val(meta), path("*_report.html"), emit: report + // No CLI version flag is provided by the tool; footer literal is "LRSomatic report v1.0" (templates/per_sample.qmd) + tuple val("${task.process}"), val('lrsomatic_report'), val("1.0"), topic: versions, emit: versions_lrsomaticreport + + when: + task.ext.when == null || task.ext.when + + script: + def args = task.ext.args ?: '' + def prefix = task.ext.prefix ?: "${meta.id}" + def sex = meta.sex ?: 'male' + // matched (T/N) samples publish under variants/clairs/; tumor-only samples under variants/clairsto/ + // -- this only controls the report tool's run-mode detection/labelling, see locate_outputs.R + def somatic_dir = meta.paired_data ? 'variants/clairs' : 'variants/clairsto' + + def link_vep = vep_somatic ? """ + mkdir -p sample_dir/vep/somatic + ln -s "\$PWD/${vep_somatic}" "sample_dir/vep/somatic/${prefix}_SOMATIC_VEP.vcf.gz" + """ : '' + + def link_severus = severus_vcf ? """ + mkdir -p sample_dir/variants/severus/somatic_SVs + ln -s "\$PWD/${severus_vcf}" "sample_dir/variants/severus/somatic_SVs/severus_somatic.vcf.gz" + """ : '' + + def link_somatic = somatic_vcf ? """ + mkdir -p sample_dir/${somatic_dir} + ln -s "\$PWD/${somatic_vcf}" "sample_dir/${somatic_dir}/somatic.vcf.gz" + """ : '' + + def ascat_file_list = ascat_files ? ascat_files.join(' ') : '' + def link_ascat = ascat_files ? """ + mkdir -p sample_dir/ascat + for f in ${ascat_file_list}; do ln -s "\$PWD/\$f" "sample_dir/ascat/\$f"; done + """ : '' + + def qc_tumor_file_list = qc_tumor_files ? qc_tumor_files.join(' ') : '' + def link_qc_tumor = qc_tumor_files ? """ + mkdir -p sample_dir/qc/tumor/mosdepth sample_dir/qc/tumor/cramino_aln sample_dir/qc/tumor/samtools + for f in ${qc_tumor_file_list}; do + case "\$f" in + *.mosdepth.*.txt) ln -s "\$PWD/\$f" "sample_dir/qc/tumor/mosdepth/\$f" ;; + *_cramino.txt) ln -s "\$PWD/\$f" "sample_dir/qc/tumor/cramino_aln/\$f" ;; + *.flagstat|*.stats) ln -s "\$PWD/\$f" "sample_dir/qc/tumor/samtools/\$f" ;; + esac + done + """ : '' + + def qc_normal_file_list = qc_normal_files ? qc_normal_files.join(' ') : '' + def link_qc_normal = qc_normal_files ? """ + mkdir -p sample_dir/qc/normal/mosdepth sample_dir/qc/normal/cramino_aln sample_dir/qc/normal/samtools + for f in ${qc_normal_file_list}; do + case "\$f" in + *.mosdepth.*.txt) ln -s "\$PWD/\$f" "sample_dir/qc/normal/mosdepth/\$f" ;; + *_cramino.txt) ln -s "\$PWD/\$f" "sample_dir/qc/normal/cramino_aln/\$f" ;; + *.flagstat|*.stats) ln -s "\$PWD/\$f" "sample_dir/qc/normal/samtools/\$f" ;; + esac + done + """ : '' + + """ + # Quarto/Deno write a cache dir under \$HOME; point it at the task work dir + # (always writable) rather than relying on the container's \$HOME being bound. + export HOME=\$PWD + + # The Wave/conda-built container doesn't auto-source conda's activation hooks + # (e.g. quarto needs QUARTO_SHARE_PATH); source them if present. Some hooks + # (e.g. gcc_linux-64) reference \$CONDA_PREFIX under `set -u`, so export it first. + export CONDA_PREFIX=/opt/conda + for f in /opt/conda/etc/conda/activate.d/*.sh; do + [ -f "\$f" ] && source "\$f" + done + + mkdir -p sample_dir + ${link_vep} + ${link_severus} + ${link_somatic} + ${link_ascat} + ${link_qc_tumor} + ${link_qc_normal} + + Rscript ${report_src}/bin/render_report.R \\ + --sample-dir sample_dir \\ + --sample-id ${prefix} \\ + --sex ${sex} \\ + --reference auto \\ + --output ${prefix}_report.html \\ + ${args} + """ + + stub: + def prefix = task.ext.prefix ?: "${meta.id}" + """ + touch ${prefix}_report.html + """ +} diff --git a/modules/local/lrsomaticreport/meta.yml b/modules/local/lrsomaticreport/meta.yml new file mode 100644 index 00000000..017df4b2 --- /dev/null +++ b/modules/local/lrsomaticreport/meta.yml @@ -0,0 +1,87 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/nf-core/modules/master/modules/meta-schema.json +name: "lrsomaticreport" +description: Render a self-contained per-sample HTML report (circos plot, small/structural variant tables, ASCAT copy-number, QC) from the pipeline's key final outputs, using the lrsomatic_report R/Quarto tool. +keywords: + - report + - quarto + - somatic + - long-read +tools: + - "lrsomatic_report": + description: "Standalone R/Quarto reporting tool for the LRSomatic pipeline" + homepage: "https://github.com/ljwharbers/lrsomatic_report" + documentation: "https://github.com/ljwharbers/lrsomatic_report/blob/main/README.md" + tool_dev_url: "https://github.com/ljwharbers/lrsomatic_report" + doi: "" + licence: null + identifier: null + +input: + - - meta: + type: map + description: | + Groovy Map containing sample information, e.g. `[ id:'sample1' ]` + - vep_somatic: + type: file + description: VEP-annotated somatic small-variant VCF (SOMATIC_VEP output), or `[]` if VEP was skipped + pattern: "*.vcf.gz" + - severus_vcf: + type: file + description: Severus somatic structural-variant VCF, or `[]` if not available + pattern: "*.vcf.gz" + - somatic_vcf: + type: file + description: Final somatic small-variant VCF (ClairS/ClairS-TO/DeepSomatic or consensus), or `[]` if not available + pattern: "*.vcf.gz" + - ascat_files: + type: file + description: Collected ASCAT copy-number output files (segments_raw, purityploidy, diagnostic PNGs), or `[]` if ASCAT was skipped + - qc_tumor_files: + type: file + description: Collected tumor QC files (mosdepth summary/dist, cramino, samtools stats/flagstat), or `[]` if QC was skipped + - qc_normal_files: + type: file + description: Collected normal-sample QC files (matched mode only), or `[]` for tumor-only samples or if QC was skipped + - - report_src: + type: directory + description: Staged lrsomatic_report repository (bin/, R/, templates/, assets/), shared across all samples + +output: + report: + - - meta: + type: map + description: | + Groovy Map containing sample information, e.g. `[ id:'sample1' ]` + - "*_report.html": + type: file + description: Self-contained per-sample HTML report + pattern: "*_report.html" + versions_lrsomaticreport: + - - "${task.process}": + type: string + description: The name of the process + - "lrsomatic_report": + type: string + description: The name of the tool + - "1.0": + type: string + description: | + Manually pinned version (the tool has no CLI version flag; the report + footer literal is "LRSomatic report v1.0", templates/per_sample.qmd) + +topics: + versions: + - - ${task.process}: + type: string + description: The name of the process + - lrsomatic_report: + type: string + description: The name of the tool + - "1.0": + type: string + description: Manually pinned version (tool has no CLI version flag) + +authors: + - "@ljwharbers" +maintainers: + - "@ljwharbers" diff --git a/modules/local/lrsomaticreport/tests/main.nf.test b/modules/local/lrsomaticreport/tests/main.nf.test new file mode 100644 index 00000000..fc9c7f7c --- /dev/null +++ b/modules/local/lrsomaticreport/tests/main.nf.test @@ -0,0 +1,67 @@ +nextflow_process { + + name "Test Process LRSOMATICREPORT" + script "../main.nf" + process "LRSOMATICREPORT" + + tag "modules" + tag "modules_local" + tag "lrsomaticreport" + + test("no optional inputs - stub") { + + options "-stub" + + when { + process { + """ + input[0] = [ + [ id:'test', paired_data: null, sex: 'male' ], + [], // vep_somatic + [], // severus_vcf + [], // somatic_vcf + [], // ascat_files + [], // qc_tumor_files + [] // qc_normal_files + ] + input[1] = file("${projectDir}/assets/lrsomatic_report", checkIfExists: true) + """ + } + } + + then { + assert process.success + assertAll( + { assert snapshot(process.out).match() } + ) + } + + } + + test("no optional inputs - real render") { + + when { + process { + """ + input[0] = [ + [ id:'test', paired_data: null, sex: 'male' ], + [], // vep_somatic + [], // severus_vcf + [], // somatic_vcf + [], // ascat_files + [], // qc_tumor_files + [] // qc_normal_files + ] + input[1] = file("${projectDir}/assets/lrsomatic_report", checkIfExists: true) + """ + } + } + + then { + assert process.success + assert process.out.report.get(0).get(1).endsWith("_report.html") + } + + } + +} diff --git a/modules/local/lrsomaticreport/tests/main.nf.test.snap b/modules/local/lrsomaticreport/tests/main.nf.test.snap new file mode 100644 index 00000000..bc2974ad --- /dev/null +++ b/modules/local/lrsomaticreport/tests/main.nf.test.snap @@ -0,0 +1,47 @@ +{ + "no optional inputs - stub": { + "content": [ + { + "0": [ + [ + { + "id": "test", + "paired_data": null, + "sex": "male" + }, + "test_report.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "1": [ + [ + "LRSOMATICREPORT", + "lrsomatic_report", + "1.0" + ] + ], + "report": [ + [ + { + "id": "test", + "paired_data": null, + "sex": "male" + }, + "test_report.html:md5,d41d8cd98f00b204e9800998ecf8427e" + ] + ], + "versions_lrsomaticreport": [ + [ + "LRSOMATICREPORT", + "lrsomatic_report", + "1.0" + ] + ] + } + ], + "timestamp": "2026-07-15T10:08:15.629341585", + "meta": { + "nf-test": "0.9.4", + "nextflow": "26.04.3" + } + } +} \ No newline at end of file diff --git a/nextflow.config b/nextflow.config index f8db398c..c3ac0e9a 100644 --- a/nextflow.config +++ b/nextflow.config @@ -59,6 +59,7 @@ params { skip_modkit = false use_gpu = false skip_whatshapstats = false + skip_report = false // minimap2 options minimap2_ont_model = null @@ -86,6 +87,10 @@ params { // Wakhan options wakhan_chroms = null + // Report options + report_src = "${projectDir}/assets/lrsomatic_report" + report_gene_panel = null + //TODO: // Once iGenomes is udpated we can update our iGenomes.config to automatically assign genome version // and allele/loci(/gc/rt) files. For now they need to be specified for anything else but GRCh38 and CHM13 diff --git a/nextflow_schema.json b/nextflow_schema.json index ccb39258..75fa71fc 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -303,6 +303,22 @@ } } }, + "report_options": { + "title": "Report options", + "type": "object", + "description": "Options for the final per-sample HTML report", + "default": "", + "properties": { + "report_src": { + "type": "string", + "description": "Path to the lrsomatic_report repository (bin/, R/, templates/, assets/)" + }, + "report_gene_panel": { + "type": "string", + "description": "Gene panel for the report: a builtin panel name (e.g. 'lymphoid') or path to a TSV with a 'gene' column" + } + } + }, "skip_options": { "title": "Skip options", "type": "object", @@ -364,6 +380,10 @@ "use_gpu": { "type": "boolean", "description": "Use GPU for supported tools (e.g. DeepVariant, DeepSomatic, Clair3)" + }, + "skip_report": { + "type": "boolean", + "description": "Skip the final per-sample HTML report" } } }, @@ -552,6 +572,9 @@ { "$ref": "#/$defs/wakhan_options" }, + { + "$ref": "#/$defs/report_options" + }, { "$ref": "#/$defs/skip_options" }, diff --git a/workflows/lrsomatic.nf b/workflows/lrsomatic.nf index 211552df..1c4ed69d 100644 --- a/workflows/lrsomatic.nf +++ b/workflows/lrsomatic.nf @@ -26,6 +26,7 @@ include { ASCAT } from '../modules/nf-core/ascat/mai include { SEVERUS } from '../modules/nf-core/severus/main.nf' include { METAEXTRACT } from '../modules/local/metaextract/main' include { WAKHAN } from '../modules/local/wakhan/main' +include { LRSOMATICREPORT } from '../modules/local/lrsomaticreport/main' include { FIBERTOOLSRS_PREDICTM6A } from '../modules/local/fibertoolsrs/predictm6a' include { FIBERTOOLSRS_FIRE } from '../modules/local/fibertoolsrs/fire' include { FIBERTOOLSRS_NUCLEOSOMES } from '../modules/local/fibertoolsrs/nucleosomes' @@ -706,6 +707,8 @@ workflow LRSOMATIC { } + ch_somatic_vep_vcf = channel.empty() + if (!params.skip_vep) { // @@ -756,6 +759,8 @@ workflow LRSOMATIC { vep_custom, vep_custom_tbi ) + + ch_somatic_vep_vcf = SOMATIC_VEP.out.vcf } // Build SEVERUS input by combining tumor-only and T/N paired samples with phased germline VCFs @@ -829,6 +834,7 @@ workflow LRSOMATIC { ch_nanoplot_post_txt = channel.empty() + ch_cramino_post_txt = channel.empty() if (!params.skip_qc && !params.skip_cramino) { @@ -841,6 +847,8 @@ workflow LRSOMATIC { CRAMINO_POST ( ch_minimap_bam ) + ch_cramino_post_txt = CRAMINO_POST.out.txt + if (!params.skip_nanoplot) { // @@ -918,6 +926,8 @@ workflow LRSOMATIC { // Output: .png plots, .segments, .purity_ploidy -- copy number results // + ch_ascat_files = channel.empty() + if (!params.skip_ascat) { // ASCAT expects [normal, tumor] order; rearrange from severus_input [tumor, normal] order severus_input @@ -939,6 +949,13 @@ workflow LRSOMATIC { ) ch_versions = ch_versions.mix(ASCAT.out.versions) + + // Collect all ASCAT copy-number files (segments_raw, purityploidy, diagnostic PNGs) per sample + // for the final report module -- it globs by suffix, so exact grouping doesn't matter. + ch_ascat_files = ASCAT.out.segments_raw + .mix(ASCAT.out.purityploidy, ASCAT.out.png) + .groupTuple() + // ch_ascat_files: [meta, [file, file, ...]] } // @@ -969,6 +986,96 @@ workflow LRSOMATIC { ) } + // + // MODULE: LRSOMATICREPORT (label: process_medium) + // Final step: render a per-sample HTML report from the key analytical outputs + // (VEP-annotated somatic SNVs, Severus somatic SVs, ASCAT copy number, QC). + // Every input is optional -- the report tool shows a "not available" notice for + // any section whose file is missing, so joins below use `remainder: true` and a + // plain String (tumor sample id) as the join key throughout, to avoid relying on + // exact Groovy-map equality across differently-stripped meta values. + // + // Known simplification: `ch_somatic_vcf` is the FINAL somatic small-variant VCF + // (single caller, or consensus if multiple somatic callers were combined). It is + // staged under variants/clairs/ (matched) or variants/clairsto/ (tumor-only) purely + // to drive the report tool's run-mode detection and its per-caller VAF column; if a + // consensus of multiple callers was used, that VAF column will not reflect a single + // real caller. The VEP-based variant table (the primary source) is unaffected. + // + + if (!params.skip_report) { + + // Canonical per-report-row identity: keyed on the tumor sample's own id (also + // used by severus_input/ascat_ch/ch_somatic_vcf), carrying the definitive meta + // to attach to the final module call. + severus_input + .map { meta, _tumor_bam, _tumor_bai, _normal_bam, _normal_bai, _phased_vcf, _phased_tbi -> + return [meta.id, meta] + } + .set { report_id_meta } + // report_id_meta: [id, meta] + + ch_somatic_vep_vcf + .map { meta, vcf -> [meta.id, vcf] } + .set { report_vep_ch } + + SEVERUS.out.somatic_vcf + .map { meta, vcf -> [meta.id, vcf] } + .set { report_severus_ch } + + ch_somatic_vcf + .map { meta, vcf, _tbi -> [meta.id, vcf] } + .set { report_somatic_ch } + + ch_ascat_files + .map { meta, files -> [meta.id, files] } + .set { report_ascat_ch } + + // Tumor-side QC: keyed by the sample's own id, which for tumor rows is already the report id + ch_mosdepth_summary + .mix(ch_mosdepth_global, ch_cramino_post_txt, ch_bam_stats, ch_bam_flagstat) + .filter { meta, _f -> meta.type == 'tumor' } + .map { meta, f -> [meta.id, f] } + .groupTuple() + .set { report_qc_tumor_ch } + + // Normal-side QC (matched mode only): re-key from the normal's own id to the + // tumor's id via meta.paired_data ("the tumor ID for normals", see branching comment above) + ch_mosdepth_summary + .mix(ch_mosdepth_global, ch_cramino_post_txt, ch_bam_stats, ch_bam_flagstat) + .filter { meta, _f -> meta.type == 'normal' } + .map { meta, f -> [meta.paired_data, f] } + .groupTuple() + .set { report_qc_normal_ch } + + report_id_meta + .join(report_vep_ch, remainder: true) + .join(report_severus_ch, remainder: true) + .join(report_somatic_ch, remainder: true) + .join(report_ascat_ch, remainder: true) + .join(report_qc_tumor_ch, remainder: true) + .join(report_qc_normal_ch, remainder: true) + .filter { _id, meta, _vep, _severus, _somatic, _ascat, _qc_t, _qc_n -> meta != null } + .map { _id, meta, vep, severus, somatic, ascat, qc_t, qc_n -> + return [ + meta, + vep ?: [], + severus ?: [], + somatic ?: [], + ascat ?: [], + qc_t ?: [], + qc_n ?: [] + ] + } + .set { report_input_ch } + // report_input_ch: [meta, vep_somatic, severus_vcf, somatic_vcf, ascat_files, qc_tumor_files, qc_normal_files] + + LRSOMATICREPORT ( + report_input_ch, + file(params.report_src) + ) + } + // // Collate software versions from two sources: // 1. ch_versions (classic path): version YAML files emitted by modules From c8029227de4c5682a57e417ed91eac0ece26f3ed Mon Sep 17 00:00:00 2001 From: ljwharbers Date: Thu, 16 Jul 2026 19:21:19 +0200 Subject: [PATCH 02/17] fix: resolve five bugs blocking LRSOMATICREPORT CI (PR #176) CI's docker/singularity 25.04.0 jobs regressed vs. dev after adding the report step. Chasing the failures end-to-end (real render, not just config) surfaced five distinct bugs: 1. workflows/lrsomatic.nf: normal-sample QC was keyed by the boolean meta.paired_data instead of meta.id, so the join produced a malformed remainder tuple and crashed the pipeline for any matched tumor/normal pair reaching the report step. Also fixed misleading comments (paired_data is not a sample id). 2. modules/local/lrsomaticreport/main.nf: qc_tumor_files/qc_normal_files were staged flat. mosdepth/samtools default to a meta.id-only prefix, so a matched pair's tumor and normal QC files share a name and collided in the task work dir. Fixed via stageAs subdirectories (qc_tumor/*, qc_normal/*) with basename-based destination linking. 3. modules/local/lrsomaticreport/environment.yml: missing r-r.utils, needed by data.table::fread() to read a gzipped VCF directly -- only surfaced once a real Severus VCF reached the render step. Required rebuilding the Wave container (new frozen tag 4506737a6b63b769); the container directive now follows this codebase's existing dual-engine pattern (singularity blob URL + docker tag, e.g. modules/local/bcftools/view/main.nf) since the frozen singularity artifact is SIF/ORAS-native, not a portable OCI image. 4. assets/lrsomatic_report submodule (re-pinned to fdf2a0a): parse_severus_vcf's fread() errored instead of returning zero rows when a sample's Severus VCF has no variant records at all (skip landing exactly on the last line). Fixed upstream with tryCatch. 5. modules/local/lrsomaticreport/main.nf: report_src was staged via a shared symlink (same fixed path for every sample), and Quarto renders in-place next to the .qmd. Concurrent per-sample tasks raced on that single physical directory ("cannot open file per_sample.qmd"). Fixed with stageInMode 'copy' for an isolated copy per task. Snapshot regenerated (additive: LRSOMATICREPORT versions entry + sample*/report/*_report.html); tests/.nftignore excludes the Quarto-rendered HTML's unstable content, matching multiqc/nanoplot. Co-Authored-By: Claude Sonnet 5 --- assets/lrsomatic_report | 2 +- modules/local/lrsomaticreport/environment.yml | 1 + modules/local/lrsomaticreport/main.nf | 36 +++++++++++++------ tests/.nftignore | 1 + tests/default.nf.test.snap | 17 ++++++--- workflows/lrsomatic.nf | 11 +++--- 6 files changed, 48 insertions(+), 20 deletions(-) diff --git a/assets/lrsomatic_report b/assets/lrsomatic_report index 2868c9fe..fdf2a0a8 160000 --- a/assets/lrsomatic_report +++ b/assets/lrsomatic_report @@ -1 +1 @@ -Subproject commit 2868c9fec2f42bb6ed5ffcf41b41fe54c0185443 +Subproject commit fdf2a0a82cebace13d3fb92f20b09e61dd26d6c1 diff --git a/modules/local/lrsomaticreport/environment.yml b/modules/local/lrsomaticreport/environment.yml index 340f3116..641d45d2 100644 --- a/modules/local/lrsomaticreport/environment.yml +++ b/modules/local/lrsomaticreport/environment.yml @@ -17,3 +17,4 @@ dependencies: - "conda-forge::r-svglite" - "conda-forge::r-circlize" - "conda-forge::r-knitr" + - "conda-forge::r-r.utils" diff --git a/modules/local/lrsomaticreport/main.nf b/modules/local/lrsomaticreport/main.nf index 4efde26b..035045c4 100644 --- a/modules/local/lrsomaticreport/main.nf +++ b/modules/local/lrsomaticreport/main.nf @@ -1,16 +1,28 @@ process LRSOMATICREPORT { tag "$meta.id" label 'process_medium' + // Quarto renders in-place next to the .qmd it's given (render_report.R's own + // post-render step relies on this). report_src is a single fixed path shared + // by every sample's task, so the default symlink staging would have all + // concurrent per-sample renders reading/writing the same physical + // templates/ directory at once; force a private copy per task instead. + stageInMode 'copy' conda "${moduleDir}/environment.yml" // Built via the Wave containers API from this module's environment.yml (frozen build). - container "community.wave.seqera.io/library/r-base_quarto_r-data.table_r-dplyr_pruned:f1d36670d940c971" + container "${workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container + ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/e0/e0d4fabb2f79dcc0d3446f1bda84507eb52ac21ebea75fd29ee5b1b26c61ee34/data' + : 'community.wave.seqera.io/library/r-base_quarto_r-data.table_r-dplyr_pruned:4506737a6b63b769'}" input: // All per-sample report inputs are optional (path may be `[]` if the corresponding // upstream tool was skipped or produced no output for this sample); the report tool // renders a "not available" notice for any missing section. - tuple val(meta), path(vep_somatic), path(severus_vcf), path(somatic_vcf), path(ascat_files), path(qc_tumor_files), path(qc_normal_files) + // qc_tumor_files/qc_normal_files are staged into distinct subdirectories: + // mosdepth/samtools default to a `${meta.id}`-only prefix (see conf/modules.config), + // so for a matched T/N pair (same meta.id) the tumor and normal QC files are + // identically named -- staging both lists flat would collide. + tuple val(meta), path(vep_somatic), path(severus_vcf), path(somatic_vcf), path(ascat_files), path(qc_tumor_files, stageAs: 'qc_tumor/*'), path(qc_normal_files, stageAs: 'qc_normal/*') path(report_src) // staged lrsomatic_report repo (bin/, R/, templates/, assets/) output: @@ -50,14 +62,17 @@ process LRSOMATICREPORT { for f in ${ascat_file_list}; do ln -s "\$PWD/\$f" "sample_dir/ascat/\$f"; done """ : '' + // $f includes the 'qc_tumor/' staging subdirectory (see stageAs above); the + // destination link name uses just the basename. def qc_tumor_file_list = qc_tumor_files ? qc_tumor_files.join(' ') : '' def link_qc_tumor = qc_tumor_files ? """ mkdir -p sample_dir/qc/tumor/mosdepth sample_dir/qc/tumor/cramino_aln sample_dir/qc/tumor/samtools for f in ${qc_tumor_file_list}; do - case "\$f" in - *.mosdepth.*.txt) ln -s "\$PWD/\$f" "sample_dir/qc/tumor/mosdepth/\$f" ;; - *_cramino.txt) ln -s "\$PWD/\$f" "sample_dir/qc/tumor/cramino_aln/\$f" ;; - *.flagstat|*.stats) ln -s "\$PWD/\$f" "sample_dir/qc/tumor/samtools/\$f" ;; + fname=\$(basename "\$f") + case "\$fname" in + *.mosdepth.*.txt) ln -s "\$PWD/\$f" "sample_dir/qc/tumor/mosdepth/\$fname" ;; + *_cramino.txt) ln -s "\$PWD/\$f" "sample_dir/qc/tumor/cramino_aln/\$fname" ;; + *.flagstat|*.stats) ln -s "\$PWD/\$f" "sample_dir/qc/tumor/samtools/\$fname" ;; esac done """ : '' @@ -66,10 +81,11 @@ process LRSOMATICREPORT { def link_qc_normal = qc_normal_files ? """ mkdir -p sample_dir/qc/normal/mosdepth sample_dir/qc/normal/cramino_aln sample_dir/qc/normal/samtools for f in ${qc_normal_file_list}; do - case "\$f" in - *.mosdepth.*.txt) ln -s "\$PWD/\$f" "sample_dir/qc/normal/mosdepth/\$f" ;; - *_cramino.txt) ln -s "\$PWD/\$f" "sample_dir/qc/normal/cramino_aln/\$f" ;; - *.flagstat|*.stats) ln -s "\$PWD/\$f" "sample_dir/qc/normal/samtools/\$f" ;; + fname=\$(basename "\$f") + case "\$fname" in + *.mosdepth.*.txt) ln -s "\$PWD/\$f" "sample_dir/qc/normal/mosdepth/\$fname" ;; + *_cramino.txt) ln -s "\$PWD/\$f" "sample_dir/qc/normal/cramino_aln/\$fname" ;; + *.flagstat|*.stats) ln -s "\$PWD/\$f" "sample_dir/qc/normal/samtools/\$fname" ;; esac done """ : '' diff --git a/tests/.nftignore b/tests/.nftignore index a1de7635..f84bfd22 100644 --- a/tests/.nftignore +++ b/tests/.nftignore @@ -27,3 +27,4 @@ pipeline_info/*.{html,json,txt,yml} */qc/{tumor,normal}/mosdepth/*.txt */variants/deepsomatic/*.{vcf.gz,vcf.gz.tbi} */variants/deepvariant/*.{vcf.gz,vcf.gz.tbi} +*/report/*.html diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap index c3ba2e30..e2c044de 100644 --- a/tests/default.nf.test.snap +++ b/tests/default.nf.test.snap @@ -46,6 +46,9 @@ "LONGPHASE_PHASE_SOMATIC": { "longphase": "2.0.1" }, + "LRSOMATICREPORT": { + "lrsomatic_report": 1.0 + }, "METAEXTRACT": { "samtools": 1.21 }, @@ -265,6 +268,8 @@ "sample1/qc/whatshap_stats/sample1_whatshap_stats.gtf", "sample1/qc/whatshap_stats/sample1_whatshap_stats.log", "sample1/qc/whatshap_stats/sample1_whatshap_stats.tsv", + "sample1/report", + "sample1/report/sample1_report.html", "sample1/variants", "sample1/variants/clair3", "sample1/variants/clair3/merge_output.vcf.gz", @@ -380,6 +385,8 @@ "sample2/qc/whatshap_stats/sample2_whatshap_stats.gtf", "sample2/qc/whatshap_stats/sample2_whatshap_stats.log", "sample2/qc/whatshap_stats/sample2_whatshap_stats.tsv", + "sample2/report", + "sample2/report/sample2_report.html", "sample2/variants", "sample2/variants/clair3", "sample2/variants/clair3/merge_output.vcf.gz", @@ -462,6 +469,8 @@ "sample3/qc/whatshap_stats/sample3_whatshap_stats.gtf", "sample3/qc/whatshap_stats/sample3_whatshap_stats.log", "sample3/qc/whatshap_stats/sample3_whatshap_stats.tsv", + "sample3/report", + "sample3/report/sample3_report.html", "sample3/variants", "sample3/variants/clairsto", "sample3/variants/clairsto/germline.vcf.gz", @@ -561,10 +570,10 @@ "breakpoint_clusters_list.tsv:md5,0c0ce62e329f8de492487e8414c30a50" ] ], + "timestamp": "2026-07-16T19:16:20.692019944", "meta": { - "nf-test": "0.9.3", - "nextflow": "26.04.1" - }, - "timestamp": "2026-06-01T15:01:21.469856129" + "nf-test": "0.9.4", + "nextflow": "26.04.3" + } } } \ No newline at end of file diff --git a/workflows/lrsomatic.nf b/workflows/lrsomatic.nf index 1c4ed69d..62d7b2a1 100644 --- a/workflows/lrsomatic.nf +++ b/workflows/lrsomatic.nf @@ -541,8 +541,8 @@ workflow LRSOMATIC { ch_index_minimap .branch { meta, _bams, _bais -> - paired: meta.paired_data // meta.paired_data is the normal sample ID for tumors, or the tumor ID for normals - tumor_only: !meta.paired_data // meta.paired_data is null/false for tumor-only samples + paired: meta.paired_data // meta.paired_data is true for both tumor and normal rows of a matched pair + tumor_only: !meta.paired_data // meta.paired_data is false for tumor-only samples } .set { branched_minimap } @@ -1039,12 +1039,13 @@ workflow LRSOMATIC { .groupTuple() .set { report_qc_tumor_ch } - // Normal-side QC (matched mode only): re-key from the normal's own id to the - // tumor's id via meta.paired_data ("the tumor ID for normals", see branching comment above) + // Normal-side QC (matched mode only): tumor and normal rows of a matched pair + // share the same meta.id (see branching comment above), so this is already + // keyed by the report id -- no re-keying needed. ch_mosdepth_summary .mix(ch_mosdepth_global, ch_cramino_post_txt, ch_bam_stats, ch_bam_flagstat) .filter { meta, _f -> meta.type == 'normal' } - .map { meta, f -> [meta.paired_data, f] } + .map { meta, f -> [meta.id, f] } .groupTuple() .set { report_qc_normal_ch } From f9c1d3d2481d273114a59299d24e828a2d65512f Mon Sep 17 00:00:00 2001 From: ljwharbers Date: Fri, 17 Jul 2026 09:30:03 +0200 Subject: [PATCH 03/17] fix: use docker-context Wave build for LRSOMATICREPORT container The previously pinned tag (4506737a6b63b769) was built during a singularity.enabled=true Wave session, which only produces a Singularity-native SIF artifact -- Docker CI's docker|25.04.0 job failed to pull it ("Encountered remote application/vnd.sylabs.sif.config.v1+json (unknown) when fetching"). A second Wave freeze build under a docker-context session (docker.enabled=true, wave.strategy=['conda']) produced tag 9d12b9297c3c4d38, a genuine OCI image (verified via `skopeo inspect --raw`: application/vnd.oci.image.manifest.v1+json with real tar+gzip layers). The dual-engine container directive now uses this new tag for the docker branch; the singularity branch's blob URL is unchanged (already confirmed working). Co-Authored-By: Claude Sonnet 5 --- modules/local/lrsomaticreport/main.nf | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/local/lrsomaticreport/main.nf b/modules/local/lrsomaticreport/main.nf index 035045c4..5df515e6 100644 --- a/modules/local/lrsomaticreport/main.nf +++ b/modules/local/lrsomaticreport/main.nf @@ -9,10 +9,13 @@ process LRSOMATICREPORT { stageInMode 'copy' conda "${moduleDir}/environment.yml" - // Built via the Wave containers API from this module's environment.yml (frozen build). + // Built via the Wave containers API from this module's environment.yml (frozen + // build). Two separate Wave builds were needed: a singularity.enabled=true + // session only produces a Singularity-native SIF artifact (blob URL below), + // while a docker.enabled=true session produces a genuine OCI image (plain tag). container "${workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/e0/e0d4fabb2f79dcc0d3446f1bda84507eb52ac21ebea75fd29ee5b1b26c61ee34/data' - : 'community.wave.seqera.io/library/r-base_quarto_r-data.table_r-dplyr_pruned:4506737a6b63b769'}" + : 'community.wave.seqera.io/library/r-base_quarto_r-data.table_r-dplyr_pruned:9d12b9297c3c4d38'}" input: // All per-sample report inputs are optional (path may be `[]` if the corresponding From 4df21e1d5da8e5298a565bf38d939561ba2df8c6 Mon Sep 17 00:00:00 2001 From: ljwharbers Date: Fri, 17 Jul 2026 11:17:14 +0200 Subject: [PATCH 04/17] fix: replace stageInMode copy with explicit cp -rL for report_src stageInMode 'copy' (added to fix a race condition where concurrent per-sample Quarto renders collided on a shared symlinked report_src directory) has a real bug in Nextflow 25.04.0 for directory-type path inputs under the docker executor: docker|latest-everything passed but docker|25.04.0 failed with "cannot open file lrsomatic_report/bin/render_report.R: No such file or directory" -- the copied directory came out incomplete. Replaced the process-level directive with a plain `cp -rL` in the script body itself. This is pure shell with no Nextflow-version dependency, and fixes the same underlying problem: each task now dereferences report_src into its own private, task-local copy before Quarto renders in-place next to the .qmd. Co-Authored-By: Claude Sonnet 5 --- modules/local/lrsomaticreport/main.nf | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/local/lrsomaticreport/main.nf b/modules/local/lrsomaticreport/main.nf index 5df515e6..bac01ca9 100644 --- a/modules/local/lrsomaticreport/main.nf +++ b/modules/local/lrsomaticreport/main.nf @@ -1,12 +1,6 @@ process LRSOMATICREPORT { tag "$meta.id" label 'process_medium' - // Quarto renders in-place next to the .qmd it's given (render_report.R's own - // post-render step relies on this). report_src is a single fixed path shared - // by every sample's task, so the default symlink staging would have all - // concurrent per-sample renders reading/writing the same physical - // templates/ directory at once; force a private copy per task instead. - stageInMode 'copy' conda "${moduleDir}/environment.yml" // Built via the Wave containers API from this module's environment.yml (frozen @@ -114,7 +108,13 @@ process LRSOMATICREPORT { ${link_qc_tumor} ${link_qc_normal} - Rscript ${report_src}/bin/render_report.R \\ + # Quarto renders in-place next to the .qmd it's given (render_report.R's own + # post-render step relies on this). report_src is a single fixed path shared + # by every sample's task, so dereferencing it into a private, task-local copy + # avoids concurrent per-sample renders colliding on the same physical directory. + cp -rL "${report_src}" report_src_local + + Rscript report_src_local/bin/render_report.R \\ --sample-dir sample_dir \\ --sample-id ${prefix} \\ --sex ${sex} \\ From 95bb782e8beceb98b8c7b752dff6230c5fb8d3b3 Mon Sep 17 00:00:00 2001 From: Luuk Harbers Date: Wed, 12 Aug 2026 11:28:57 +0200 Subject: [PATCH 05/17] feat: vendor lrsomatic_report v1.1.0 and wire the report to SV VEP + Wakhan The `assets/lrsomatic_report` submodule could not reach anyone. `nextflow run IntGenomicsLab/lrsomatic` clones the pipeline repo but not its submodules, and CI checks out without `submodules: recursive` -- so the gitlink resolved to an empty directory for end users and for every CI run, which is what has been failing PR #176. Replace it with the upstream tree as real tracked files (bin/, R/, templates/, assets/, LICENSE, README.md; ~565 KB), recorded in assets/lrsomatic_report/VENDORED.md. `--report_src` stays, now as an override for a local checkout rather than a required setup step. Dependencies stay in the Wave multi-package container, rebuilt from the module's environment.yml after adding r-base64enc (used by R/utils.R embed_png(), listed in the upstream recipe, missing here). The tool is at v1.1.0, several releases past the pin. Rewire accordingly: - Drop the symlink tree that faked variants/clairs vs variants/clairsto so the old CLI could infer run mode. v1.1.0 derives the mode from whether normal-side QC is present and discovers files recursively by base name, so staging is now flat plus three fixed locations (qc/tumor, qc/normal, wakhan). - Drop `cp -rL` of report_src: render_report.R copies templates/ and assets/ into a task-local ._render itself, so the shared source dir is never written. - Feed the phased somatic VCF rather than the pre-phasing caller VCF, at the path the tool looks for it. The VAF/depth/phase-set columns now come from the same file VEP annotated instead of a possibly-consensus VCF. - Add SV_VEP.out.vcf, the tool's primary SV annotation source. - Add the Wakhan outputs it renders. Its per-solution plots all share one base name, so WAKHAN gains a `solution_dirs` output and the directories are staged whole rather than the files individually. - Export TMPDIR into the task work dir alongside HOME. Quarto's Deno runtime creates a session dir under TMPDIR and dies with "Read-only file system (os error 30): tmpdir" wherever the container's /tmp is not writable. The module test suite previously passed `checkIfExists` on a directory that existed but was empty, which is why it never caught any of this. It now has a stub test and a real-render test with a VEP somatic VCF, so a broken container, an incomplete tool tree or CLI drift all fail loudly. Refs #133 Co-Authored-By: Claude Opus 5 --- .gitattributes | 1 + .gitmodules | 3 - .pre-commit-config.yaml | 2 + .prettierignore | 2 + CHANGELOG.md | 8 + CITATIONS.md | 4 + assets/lrsomatic_report | 1 - assets/lrsomatic_report/LICENSE | 21 + assets/lrsomatic_report/R/circos.R | 266 +++ assets/lrsomatic_report/R/locate_outputs.R | 123 ++ assets/lrsomatic_report/R/parse_ascat.R | 64 + assets/lrsomatic_report/R/parse_qc.R | 111 ++ assets/lrsomatic_report/R/parse_severus.R | 229 +++ .../lrsomatic_report/R/parse_smallvariants.R | 404 +++++ assets/lrsomatic_report/R/references.R | 72 + assets/lrsomatic_report/R/sections.R | 14 + assets/lrsomatic_report/R/sections/sv.R | 64 + assets/lrsomatic_report/R/sections/whatshap.R | 56 + assets/lrsomatic_report/R/utils.R | 235 +++ assets/lrsomatic_report/README.md | 172 ++ assets/lrsomatic_report/VENDORED.md | 57 + .../assets/gene_lists/README.md | 20 + .../assets/gene_lists/lymphoid.tsv | 74 + .../assets/references/hg38/chrom_lengths.tsv | 25 + .../assets/references/hg38/cytobands.tsv | 1549 +++++++++++++++++ .../assets/references/t2t/chrom_lengths.tsv | 25 + .../assets/references/t2t/cytobands.tsv | 862 +++++++++ .../assets/styles/_fonts.scss | 85 + .../assets/styles/report.scss | 951 ++++++++++ assets/lrsomatic_report/bin/render_report.R | 158 ++ .../lrsomatic_report/templates/per_sample.qmd | 292 ++++ .../templates/sections/_ascat.qmd | 109 ++ .../templates/sections/_circos.qmd | 75 + .../templates/sections/_gene_filter.qmd | 36 + .../templates/sections/_header.qmd | 63 + .../templates/sections/_qc.qmd | 72 + .../templates/sections/_smallvariants.qmd | 39 + .../templates/sections/_sv.qmd | 47 + .../templates/sections/_whatshap.qmd | 48 + docs/output.md | 17 +- docs/usage.md | 27 +- modules/local/lrsomaticreport/environment.yml | 1 + modules/local/lrsomaticreport/main.nf | 128 +- modules/local/lrsomaticreport/meta.yml | 29 +- .../local/lrsomaticreport/tests/main.nf.test | 25 +- .../lrsomaticreport/tests/main.nf.test.snap | 22 +- .../tests/test_SOMATIC_VEP.vcf.gz | Bin 0 -> 620 bytes modules/local/wakhan/main.nf | 5 + nextflow_schema.json | 4 +- tests/clair_only.nf.test.snap | 13 + tests/consensus.nf.test.snap | 9 + tests/deep_only.nf.test.snap | 9 + tests/default.nf.test.snap | 2 +- tests/union.nf.test.snap | 9 + workflows/lrsomatic.nf | 62 +- 55 files changed, 6678 insertions(+), 123 deletions(-) delete mode 100644 .gitmodules delete mode 160000 assets/lrsomatic_report create mode 100644 assets/lrsomatic_report/LICENSE create mode 100644 assets/lrsomatic_report/R/circos.R create mode 100644 assets/lrsomatic_report/R/locate_outputs.R create mode 100644 assets/lrsomatic_report/R/parse_ascat.R create mode 100644 assets/lrsomatic_report/R/parse_qc.R create mode 100644 assets/lrsomatic_report/R/parse_severus.R create mode 100644 assets/lrsomatic_report/R/parse_smallvariants.R create mode 100644 assets/lrsomatic_report/R/references.R create mode 100644 assets/lrsomatic_report/R/sections.R create mode 100644 assets/lrsomatic_report/R/sections/sv.R create mode 100644 assets/lrsomatic_report/R/sections/whatshap.R create mode 100644 assets/lrsomatic_report/R/utils.R create mode 100644 assets/lrsomatic_report/README.md create mode 100644 assets/lrsomatic_report/VENDORED.md create mode 100644 assets/lrsomatic_report/assets/gene_lists/README.md create mode 100644 assets/lrsomatic_report/assets/gene_lists/lymphoid.tsv create mode 100644 assets/lrsomatic_report/assets/references/hg38/chrom_lengths.tsv create mode 100644 assets/lrsomatic_report/assets/references/hg38/cytobands.tsv create mode 100644 assets/lrsomatic_report/assets/references/t2t/chrom_lengths.tsv create mode 100644 assets/lrsomatic_report/assets/references/t2t/cytobands.tsv create mode 100644 assets/lrsomatic_report/assets/styles/_fonts.scss create mode 100644 assets/lrsomatic_report/assets/styles/report.scss create mode 100755 assets/lrsomatic_report/bin/render_report.R create mode 100644 assets/lrsomatic_report/templates/per_sample.qmd create mode 100644 assets/lrsomatic_report/templates/sections/_ascat.qmd create mode 100644 assets/lrsomatic_report/templates/sections/_circos.qmd create mode 100644 assets/lrsomatic_report/templates/sections/_gene_filter.qmd create mode 100644 assets/lrsomatic_report/templates/sections/_header.qmd create mode 100644 assets/lrsomatic_report/templates/sections/_qc.qmd create mode 100644 assets/lrsomatic_report/templates/sections/_smallvariants.qmd create mode 100644 assets/lrsomatic_report/templates/sections/_sv.qmd create mode 100644 assets/lrsomatic_report/templates/sections/_whatshap.qmd create mode 100644 modules/local/lrsomaticreport/tests/test_SOMATIC_VEP.vcf.gz diff --git a/.gitattributes b/.gitattributes index 7a2dabc2..1cdf81cf 100644 --- a/.gitattributes +++ b/.gitattributes @@ -2,3 +2,4 @@ *.nf.test linguist-language=nextflow modules/nf-core/** linguist-generated subworkflows/nf-core/** linguist-generated +assets/lrsomatic_report/** linguist-vendored diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index c95b45f7..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "assets/lrsomatic_report"] - path = assets/lrsomatic_report - url = https://github.com/ljwharbers/lrsomatic_report.git diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f51e1a28..7795beca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,6 +15,7 @@ repos: .*ro-crate-metadata.json$| modules/(?!local/).*| subworkflows/(?!local/).*| + assets/lrsomatic_report/.*| .*\.snap$ )$ - id: end-of-file-fixer @@ -23,6 +24,7 @@ repos: .*ro-crate-metadata.json$| modules/(?!local/).*| subworkflows/(?!local/).*| + assets/lrsomatic_report/.*| .*\.snap$ )$ - repo: https://github.com/seqeralabs/nf-lint-pre-commit diff --git a/.prettierignore b/.prettierignore index 63cde500..1d1daf74 100644 --- a/.prettierignore +++ b/.prettierignore @@ -12,3 +12,5 @@ bin/ ro-crate-metadata.json modules/nf-core/ subworkflows/nf-core/ +# Vendored upstream tool source -- see assets/lrsomatic_report/VENDORED.md +assets/lrsomatic_report/ diff --git a/CHANGELOG.md b/CHANGELOG.md index a6e62e36..081058a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### `Added` + +- [#176](https://github.com/IntGenomicsLab/lrsomatic/pull/176) - Added `LRSOMATICREPORT` as the final pipeline step: a self-contained per-sample HTML report covering small variants, structural variants, copy number and QC. Skip it with `--skip_report`; choose the gene panel selected on load with `--report_gene_panel` (@ljwharbers). +- [#176](https://github.com/IntGenomicsLab/lrsomatic/pull/176) - Vendored the [lrsomatic_report](https://github.com/ljwharbers/lrsomatic_report) v1.1.0 tool source at `assets/lrsomatic_report`, so `nextflow run IntGenomicsLab/lrsomatic` ships it without a submodule checkout (@ljwharbers). +- [#176](https://github.com/IntGenomicsLab/lrsomatic/pull/176) - Added a `solution_dirs` output to the WAKHAN module so its per-solution copy-number plots can be staged downstream (@ljwharbers). + ## v1.1.0 - [2026-04-28] ### `Added` diff --git a/CITATIONS.md b/CITATIONS.md index e13600d0..b33a40b0 100644 --- a/CITATIONS.md +++ b/CITATIONS.md @@ -50,6 +50,10 @@ > Lin JH, Chen LC, Yu SC, Huang YT. LongPhase: an ultra-fast chromosome-scale phasing algorithm for small and large variants. Bioinformatics. 2022 Apr 28;38(9):2452-2455. doi: 10.1093/bioinformatics/btac126. PubMed PMID: 35253834; PubMed Central PMCID: PMC9048675. +- [lrsomatic_report](https://github.com/ljwharbers/lrsomatic_report) + + > Standalone R/Quarto reporting tool that renders the pipeline's final per-sample HTML report. https://github.com/ljwharbers/lrsomatic_report + - [minimap2](https://pubmed.ncbi.nlm.nih.gov/29750242/) > Li H. Minimap2: pairwise alignment for nucleotide sequences. Bioinformatics. 2018 Sep 15;34(18):3094-3100. doi: 10.1093/bioinformatics/bty191. PubMed PMID: 29750242; PubMed Central PMCID: PMC6137996. diff --git a/assets/lrsomatic_report b/assets/lrsomatic_report deleted file mode 160000 index fdf2a0a8..00000000 --- a/assets/lrsomatic_report +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fdf2a0a82cebace13d3fb92f20b09e61dd26d6c1 diff --git a/assets/lrsomatic_report/LICENSE b/assets/lrsomatic_report/LICENSE new file mode 100644 index 00000000..8d542e4f --- /dev/null +++ b/assets/lrsomatic_report/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Luuk Harbers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/assets/lrsomatic_report/R/circos.R b/assets/lrsomatic_report/R/circos.R new file mode 100644 index 00000000..f1d210ca --- /dev/null +++ b/assets/lrsomatic_report/R/circos.R @@ -0,0 +1,266 @@ +suppressPackageStartupMessages({ + library(circlize) + library(data.table) +}) + +# Colour palettes — keep in sync with --circos-* in assets/styles/report.scss + +# SBS-6 SNV palette (SigProfiler/COSMIC standard, softened slightly toward report ink/paper) +SNV_COLOURS = c( + "C>A" = "#2EBAED", + "C>G" = "#1b1e22", + "C>T" = "#b3402f", + "T>A" = "#c7c2b8", + "T>C" = "#ADCC54", + "T>G" = "#F0D0CE" +) + +# SV colours — saturated, hue-matched to --sv-* table tokens +SV_COLOURS = c( + INS = "#cf5b46", + DEL = "#2f6db3", + INV = "#c08a1e", + DUP = "#3f7d4e" +) + +SV_YPOS = c(INS = 1.0, DEL = 0.66, INV = 0.33, DUP = 0.05) + +# CNV colours — tied to the report spine +CNV_COLOURS = c( + major = "#b3402f", # brick = "more" + minor = "#0d5c75", # teal = "less" + total = "#1b1e22" # ink +) + +# BND/translocation link colour +BND_COLOUR = "#8a5fa3" + +# Classify SNV into 6 SBS categories (C/T-ref normalised) +.classify_mut = function(ref, alt) { + comp = c(A = "T", T = "A", C = "G", G = "C") + ref = toupper(ref); alt = toupper(alt) + use_comp = !(ref %in% c("C", "T")) + norm_ref = ifelse(use_comp, comp[ref], ref) + norm_alt = ifelse(use_comp, comp[alt], alt) + paste0(norm_ref, ">", norm_alt) +} + +# Draw a circos plot and write it to output_path (SVG or PNG depending on extension) +# +# @param snv_data data.table: chrom, pos, ref, alt (single-base SNVs only) +# @param sv_nontrans data.table from parse_severus_vcf()$nontrans +# @param sv_trans data.table from parse_severus_vcf()$translocations +# @param cnv_data data.table from parse_ascat_segments() +# @param cytobands data.frame: chrom, start, end, name, stain +# @param chrom_lengths named integer vector (chrom → bp length) +# @param chromosomes character vector of chroms to plot +# @param output_path path to the output file +draw_circos = function(snv_data = NULL, + sv_nontrans = NULL, + sv_trans = NULL, + cnv_data = NULL, + cytobands, + chrom_lengths, + chromosomes, + output_path) { + + # Filter cytobands and lengths to displayed chromosomes + cyto_filt = cytobands[cytobands$chrom %in% chromosomes, ] + lens_filt = chrom_lengths[names(chrom_lengths) %in% chromosomes] + lens_filt = lens_filt[chromosomes[chromosomes %in% names(lens_filt)]] + + # Prepare SNV data + if (!is.null(snv_data) && nrow(snv_data) > 0) { + snv = as.data.table(snv_data)[nchar(ref) == 1 & nchar(alt) == 1] + snv = snv[chrom %in% chromosomes] + snv[, mut_cat := .classify_mut(ref, alt)] + snv[, circos_col := SNV_COLOURS[mut_cat]] + snv[is.na(circos_col), circos_col := "#AAAAAA"] + } else { + snv = data.table(chrom = character(), pos = integer(), + mut_cat = character(), circos_col = character()) + } + + # Prepare SV (non-BND) data + if (!is.null(sv_nontrans) && nrow(sv_nontrans) > 0) { + sv_nt = as.data.table(sv_nontrans)[chrom %in% chromosomes] + } else { + sv_nt = data.table(chrom = character(), pos = integer(), end = integer(), + svtype = character(), circos_pos = numeric(), circos_col = character()) + } + + # Prepare translocation (BND) data + if (!is.null(sv_trans) && nrow(sv_trans) > 0) { + sv_tr = as.data.table(sv_trans)[chrom %in% chromosomes & chrom2 %in% chromosomes] + } else { + sv_tr = data.table(chrom = character(), pos = integer(), + chrom2 = character(), pos2 = integer()) + } + + # Prepare CNV data + if (!is.null(cnv_data) && nrow(cnv_data) > 0) { + cnv = as.data.table(cnv_data)[chr %in% chromosomes] + cnv = cnv[order(chr, startpos)] + } else { + cnv = data.table(chr = character(), startpos = integer(), endpos = integer(), + major_cn = numeric(), minor_cn = numeric(), total_cn = numeric()) + } + + # Open device + ext = tolower(tools::file_ext(output_path)) + if (ext == "svg") { + svglite::svglite(output_path, width = 8, height = 8) + } else { + png(output_path, width = 2400, height = 2400, res = 300) + } + + plot.new() + circos.clear() + + n_chr = length(chromosomes) + gap_degrees = c(rep(1, n_chr - 1), 5) + + circos.par( + "start.degree" = 90, + "gap.degree" = gap_degrees, + "track.margin" = c(0.008, 0.008), + "cell.padding" = c(0, 0, 0, 0) + ) + + # Build cytobands list as expected by circos.initializeWithIdeogram + cyto_list = list( + df = cyto_filt, + chromosome = chromosomes[chromosomes %in% unique(cyto_filt$chrom)], + chr.len = lens_filt + ) + + circos.initializeWithIdeogram(cyto_list$df, + chromosome.index = cyto_list$chromosome, + labels.cex = 0.7) + + # Pre-compute jitter once so it varies per chromosome but stays reproducible + set.seed(42) + + # ---- Track 1: SNV dots (coloured by mutation category) ------------------ + circos.trackPlotRegion( + factors = chromosomes, + ylim = c(0, 1), + bg.border = "#d8d3c8", + bg.col = rep(c("#fcfbf7", "#f6f4ee"), length.out = n_chr), + track.height = 0.13, + panel.fun = function(region, value, ...) { + chr = get.cell.meta.data("sector.index") + sub_snv = snv[chrom == chr] + if (nrow(sub_snv) == 0) return(invisible(NULL)) + y_jitter = runif(nrow(sub_snv), 0.05, 0.95) + circos.points( + x = sub_snv$pos, + y = y_jitter, + col = sub_snv$circos_col, + pch = 19, + cex = 0.15 + ) + } + ) + + # ---- Track 2: Non-BND SVs (DEL/DUP/INV/INS as horizontal segments) ----- + circos.trackPlotRegion( + factors = chromosomes, + ylim = c(0, 1), + bg.border = "#d8d3c8", + bg.col = rep(c("#f6f4ee", "#fcfbf7"), length.out = n_chr), + track.height = 0.11, + panel.fun = function(region, value, ...) { + chr = get.cell.meta.data("sector.index") + sub_sv = sv_nt[chrom == chr & !is.na(circos_pos)] + if (nrow(sub_sv) == 0) return(invisible(NULL)) + for (i in seq_len(nrow(sub_sv))) { + x1 = sub_sv$pos[i] + x2 = if (!is.na(sub_sv$end[i]) && sub_sv$end[i] > x1) sub_sv$end[i] else x1 + 1L + circos.segments( + x0 = x1, x1 = x2, + y0 = sub_sv$circos_pos[i], y1 = sub_sv$circos_pos[i], + col = sub_sv$circos_col[i], + lwd = 2 + ) + } + } + ) + + # Y-axis labels for SV track + tryCatch( + circos.yaxis( + side = "left", + at = c(0.05, 0.33, 0.66, 1.0), + labels = c("DUP", "INV", "DEL", "INS"), + track.index = 3, + sector.index = chromosomes[1], + labels.niceFacing = TRUE, + labels.cex = 0.35 + ), + error = function(e) NULL + ) + + # ---- Track 3: ASCAT copy-number ----------------------------------------- + circos.trackPlotRegion( + factors = chromosomes, + ylim = c(0, 4), + bg.border = "#d8d3c8", + bg.col = rep(c("#fcfbf7", "#f6f4ee"), length.out = n_chr), + track.height = 0.17, + panel.fun = function(region, value, ...) { + chr = get.cell.meta.data("sector.index") + sub_cnv = cnv[chr == get.cell.meta.data("sector.index")] + if (nrow(sub_cnv) == 0) return(invisible(NULL)) + + xmax = lens_filt[chr] + if (!is.na(xmax)) { + for (y_ref in c(1, 2, 3, 4)) { + circos.lines(c(0, xmax), c(y_ref, y_ref), + col = "#d8d3c8", lwd = 0.3, lty = "dotted") + } + } + + circos.yaxis( + side = "left", + at = c(0, 1, 2, 3, 4), + labels = c("0", "1", "2", "3", "4+"), + sector.index = chromosomes[1], + labels.niceFacing = TRUE, + labels.cex = 0.30 + ) + + for (i in seq_len(nrow(sub_cnv))) { + xl = sub_cnv$startpos[i]; xr = sub_cnv$endpos[i] + maj = sub_cnv$major_cn[i] + circos.rect(xl, maj + 0.02, xr, maj + 0.12, + col = CNV_COLOURS["major"], border = CNV_COLOURS["major"], lwd = 0.05) + min_cn = sub_cnv$minor_cn[i] + circos.rect(xl, min_cn - 0.12, xr, min_cn - 0.02, + col = CNV_COLOURS["minor"], border = CNV_COLOURS["minor"], lwd = 0.05) + tot = sub_cnv$total_cn[i] + circos.rect(xl, tot - 0.03, xr, tot + 0.03, + col = CNV_COLOURS["total"], border = CNV_COLOURS["total"], lwd = 0.05) + } + } + ) + + # ---- Translocation links (BND) in the centre ---------------------------- + if (nrow(sv_tr) > 0) { + for (i in seq_len(nrow(sv_tr))) { + tryCatch( + circos.link( + sector.index1 = sv_tr$chrom[i], point1 = sv_tr$pos[i], + sector.index2 = sv_tr$chrom2[i], point2 = sv_tr$pos2[i], + col = adjustcolor(BND_COLOUR, alpha.f = 0.5), + lwd = 0.8 + ), + error = function(e) NULL + ) + } + } + + circos.clear() + dev.off() + invisible(output_path) +} diff --git a/assets/lrsomatic_report/R/locate_outputs.R b/assets/lrsomatic_report/R/locate_outputs.R new file mode 100644 index 00000000..68b2ca99 --- /dev/null +++ b/assets/lrsomatic_report/R/locate_outputs.R @@ -0,0 +1,123 @@ +# Discover the per-tool output files for a sample run. Discovery is recursive +# under sample_dir: the pipeline may dump inputs flat rather than in a fixed +# directory tree, so files are matched by their distinctive filename suffix. +# Returns a named list; any missing optional file is NULL. + +locate_outputs = function(sample_dir, sample_id) { + d = sample_dir # shorthand + + # First recursive hit under `root` matching a filename pattern + find1 = function(pattern, root = d) { + hits = list.files(root, pattern = pattern, recursive = TRUE, full.names = TRUE) + if (length(hits) > 0) hits[1] else NULL + } + + # Same, but excluding anything under a normal/ subtree (tumor-side QC) + find1_tumor = function(pattern) { + hits = list.files(d, pattern = pattern, recursive = TRUE, full.names = TRUE) + hits = hits[!grepl("/normal/", hits)] + if (length(hits) > 0) hits[1] else NULL + } + + # The mirror of find1_tumor: normal-side files, wherever the pipeline puts them. + # This has moved (a top-level normal/ historically, qc/normal/ today), so match on + # the path component rather than rooting the search at a fixed directory. + find1_normal = function(pattern) { + hits = list.files(d, pattern = pattern, recursive = TRUE, full.names = TRUE) + hits = hits[grepl("/normal/", hits)] + if (length(hits) > 0) hits[1] else NULL + } + + # --- small variants ------------------------------------------------------- + vep_somatic = find1("_SOMATIC_VEP\\.vcf\\.gz$") + + # VAF, depth and phasing come from the VCF that VEP annotated, not from a separate + # caller VCF. Preferred is the phased somatic VCF, which is what the pipeline feeds to + # VEP and which additionally carries FORMAT/PS. Runs predating variants/phased/ fall back + # to the raw ClairS(-TO) output, matched on the containing directory because the + # basename ("somatic.vcf.gz") is shared across callers. The clairs*/ fallback may return + # several paths — parse_caller_vcf() stacks them. + somatic_vaf_vcf = { + phased = file.path(d, "variants", "phased", "somatic_smallvariants.vcf.gz") + if (file.exists(phased)) phased else { + vcfs = list.files(d, pattern = "\\.vcf\\.gz$", recursive = TRUE, full.names = TRUE) + named = vcfs[grepl("/clairs(to)?/somatic\\.vcf\\.gz$", vcfs)] + any_c = vcfs[grepl("/clairs(to)?/", vcfs) & !grepl("/germline\\.vcf\\.gz$", vcfs)] + if (length(named) > 0) named[1] else if (length(any_c) > 0) any_c else NULL + } + } + + # --- structural variants --------------------------------------------------- + # Severus paths are now located by R/sections/sv.R (section-module contract). + + # --- ASCAT ---------------------------------------------------------------- + ascat_segments_raw = find1("\\.segments_raw\\.txt$") + ascat_purityploidy = find1("\\.purityploidy\\.txt$") + ascat_plots = list( + profile = find1("\\.tumour\\.ASCATprofile\\.png$"), + rawprofile = find1("\\.tumour\\.rawprofile\\.png$"), + sunrise = find1("\\.tumour\\.sunrise\\.png$"), + aspcf = find1("\\.tumour\\.ASPCF\\.png$"), + before_gc = find1("\\.before_correction\\..*\\.tumour\\.tumour\\.png$"), + after_gc = find1("\\.after_correction_gc.*\\.tumour\\.tumour\\.png$"), + tumour_sep = find1("^tumorSep.*\\.tumour\\.png$") + ) + + # --- QC (tumor side) -------------------------------------------------------- + mosdepth_summary = find1_tumor("\\.mosdepth\\.summary\\.txt$") + mosdepth_dist = find1_tumor("\\.mosdepth\\.global\\.dist\\.txt$") + cramino_aln = find1_tumor("_cramino\\.txt$") + flagstat = find1_tumor("\\.flagstat$") + samtools_stats = find1_tumor("\\.stats$") + + # --- Normal-side QC (matched mode only) ----------------------------------- + normal_mosdepth_summary = find1_normal("\\.mosdepth\\.summary\\.txt$") + normal_mosdepth_dist = find1_normal("\\.mosdepth\\.global\\.dist\\.txt$") + normal_cramino = find1_normal("_cramino\\.txt$") + normal_flagstat = find1_normal("\\.flagstat$") + normal_samtools_stats = find1_normal("\\.stats$") + + # Driven by what was actually found rather than by directory layout or run mode: + # the QC section renders a tumour/normal comparison only if there is normal data. + has_normal = !is.null(normal_mosdepth_summary) || !is.null(normal_cramino) + + # Run mode is derived from the same evidence rather than declared by the caller; its + # only consumer is the hero badge in templates/sections/_header.qmd. + mode = if (has_normal) "matched" else "tumour-only" + + # --- Wakhan (optional) ----------------------------------------------------- + wakhan_dir = file.path(d, "wakhan") + has_wakhan = dir.exists(wakhan_dir) + wakhan_solutions = if (has_wakhan) { + f = file.path(wakhan_dir, "solutions_ranks.tsv") + if (file.exists(f)) f else NULL + } else NULL + wakhan_heatmap = if (has_wakhan) { + hits = Sys.glob(file.path(wakhan_dir, "*heatmap_ploidy_purity.html")) + if (length(hits) > 0) hits[1] else NULL + } else NULL + + list( + mode = mode, + vep_somatic = vep_somatic, + somatic_vaf_vcf = somatic_vaf_vcf, + ascat_segments = ascat_segments_raw, + ascat_purityploidy = ascat_purityploidy, + mosdepth_summary = mosdepth_summary, + mosdepth_dist = mosdepth_dist, + cramino = cramino_aln, + flagstat = flagstat, + samtools_stats = samtools_stats, + has_normal = has_normal, + ascat_plots = ascat_plots, + normal_mosdepth_summary = normal_mosdepth_summary, + normal_mosdepth_dist = normal_mosdepth_dist, + normal_cramino = normal_cramino, + normal_flagstat = normal_flagstat, + normal_samtools_stats = normal_samtools_stats, + has_wakhan = has_wakhan, + wakhan_dir = wakhan_dir, + wakhan_solutions = wakhan_solutions, + wakhan_heatmap = wakhan_heatmap + ) +} diff --git a/assets/lrsomatic_report/R/parse_ascat.R b/assets/lrsomatic_report/R/parse_ascat.R new file mode 100644 index 00000000..2c93ef7a --- /dev/null +++ b/assets/lrsomatic_report/R/parse_ascat.R @@ -0,0 +1,64 @@ +suppressPackageStartupMessages({ + library(data.table) +}) + +# Parse ASCAT raw segments (segments_raw.txt) +# Columns: sample, chr, startpos, endpos, nMajor, nMinor, nAraw, nBraw +parse_ascat_segments = function(segments_file) { + if (is.null(segments_file) || !file.exists(segments_file)) return(NULL) + dt = fread(segments_file, sep = "\t", header = TRUE) + + # Normalise column names to lowercase + setnames(dt, tolower(names(dt))) + + # Add chr prefix if missing + dt[, chr := ensure_chr_prefix(as.character(chr))] + + # Column names after tolower(): naraw, nbraw + dt[, total_cn := pmin(naraw + nbraw, 4)] + dt[, major_cn := pmin(naraw, 4)] + dt[, minor_cn := pmin(nbraw, 4)] + + dt +} + +# Parse ASCAT purity/ploidy file +# Columns: AberrantCellFraction, Ploidy +parse_ascat_purityploidy = function(pp_file) { + if (is.null(pp_file) || !file.exists(pp_file)) return(list(purity = NA_real_, ploidy = NA_real_)) + dt = fread(pp_file, sep = "\t", header = TRUE) + setnames(dt, tolower(names(dt))) + list( + purity = round(as.numeric(dt$aberrantcellfraction[1]), 3), + ploidy = round(as.numeric(dt$ploidy[1]), 3) + ) +} + +# Parse Wakhan's ranked purity/ploidy solutions table (wakhan/solutions_ranks.tsv). +# Columns: repository_name, dna_purity, cell_purity, ploidy, confidence, solution_rank +parse_wakhan_solutions = function(tsv_file) { + if (is.null(tsv_file) || !file.exists(tsv_file)) return(NULL) + dt = fread(tsv_file, sep = "\t", header = TRUE) + if (nrow(dt) == 0) return(NULL) + setorder(dt, solution_rank) + dt +} + +# Locate each solution's whole-genome copy-number + breakpoints plot +# (wakhan/solution_/..._genome_copynumbers_breakpoints.html). Solution +# directories are aliased two ways (solution_/ and a duplicate +# __/ directory) — solution_/ is tried +# first to avoid picking up the duplicate. +locate_wakhan_cn_plots = function(wakhan_dir, solutions_dt) { + if (is.null(wakhan_dir) || is.null(solutions_dt) || nrow(solutions_dt) == 0) return(list()) + out = lapply(seq_len(nrow(solutions_dt)), function(i) { + row = solutions_dt[i] + sdir = file.path(wakhan_dir, paste0("solution_", row$solution_rank)) + if (!dir.exists(sdir)) sdir = file.path(wakhan_dir, row$repository_name) + if (!dir.exists(sdir)) return(NULL) + hits = list.files(sdir, pattern = "genome_copynumbers_breakpoints\\.html$", full.names = TRUE) + if (length(hits) == 0) return(NULL) + list(rank = row$solution_rank, purity = row$cell_purity, ploidy = row$ploidy, plot = hits[1]) + }) + Filter(Negate(is.null), out) +} diff --git a/assets/lrsomatic_report/R/parse_qc.R b/assets/lrsomatic_report/R/parse_qc.R new file mode 100644 index 00000000..b95f90a3 --- /dev/null +++ b/assets/lrsomatic_report/R/parse_qc.R @@ -0,0 +1,111 @@ +suppressPackageStartupMessages({ + library(data.table) +}) + +# Parse mosdepth summary (*.mosdepth.summary.txt) +# Returns list: mean_depth, total_row (the "total" row from mosdepth) +parse_mosdepth_summary = function(summary_file) { + if (is.null(summary_file) || !file.exists(summary_file)) { + return(list(mean_depth = NA_real_, table = data.table())) + } + dt = fread(summary_file, sep = "\t", header = TRUE) + setnames(dt, tolower(names(dt))) + total_row = dt[chrom == "total"] + mean_depth = if (nrow(total_row) > 0) total_row$mean[1] else NA_real_ + + # Keep per-chromosome rows (exclude region-level and total) + chr_rows = dt[grepl("^chr", chrom) & !grepl("_region", chrom)] + total_length = if (nrow(total_row) > 0) total_row$length[1] else NA_real_ + total_bases = if (nrow(total_row) > 0) total_row$bases[1] else NA_real_ + list(mean_depth = round(mean_depth, 2), total_length = total_length, total_bases = total_bases, table = chr_rows) +} + +# Parse mosdepth global distribution (*.mosdepth.global.dist.txt) +# Returns data.table with columns: chrom, coverage, fraction +parse_mosdepth_dist = function(dist_file) { + if (is.null(dist_file) || !file.exists(dist_file)) return(NULL) + dt = fread(dist_file, sep = "\t", header = FALSE, + col.names = c("chrom", "coverage", "fraction")) + dt +} + +# Parse cramino alignment report +# Returns list: n50, yield_gb, mapped_pct, n_reads +parse_cramino = function(cramino_file) { + if (is.null(cramino_file) || !file.exists(cramino_file)) { + return(list(n50 = NA_real_, yield_gb = NA_real_, + mapped_pct = NA_real_, n_reads = NA_integer_)) + } + lines = readLines(cramino_file, warn = FALSE) + get_val = function(pattern) { + hit = grep(pattern, lines, value = TRUE, ignore.case = TRUE) + if (length(hit) == 0) return(NA_character_) + trimws(sub(paste0(".*", pattern, "\\s*"), "", hit[1], ignore.case = TRUE)) + } + + # Cramino outputs key\tvalue pairs + dt = tryCatch( + fread(cramino_file, sep = "\t", header = FALSE, col.names = c("key", "value"), fill = TRUE), + error = function(e) NULL + ) + if (is.null(dt)) return(list(n50 = NA_real_, yield_gb = NA_real_, + mapped_pct = NA_real_, n_reads = NA_integer_)) + + get_field = function(pattern) { + row = dt[grepl(pattern, key, ignore.case = TRUE)] + if (nrow(row) == 0) NA_character_ else as.character(row$value[1]) + } + + list( + n50 = suppressWarnings(as.numeric(get_field("N50"))), + yield_gb = suppressWarnings(as.numeric(get_field("Yield"))), + mapped_pct = suppressWarnings(as.numeric(sub("%", "", get_field("% from total")))), + n_reads = suppressWarnings(as.integer(get_field("Number of reads"))) + ) +} + +# Parse samtools flagstat +# Returns a named list of counts (total, mapped, ...) +parse_flagstat = function(flagstat_file) { + if (is.null(flagstat_file) || !file.exists(flagstat_file)) return(list()) + lines = readLines(flagstat_file, warn = FALSE) + out = list() + for (line in lines) { + count = suppressWarnings(as.integer(sub(" .*", "", trimws(line)))) + if (grepl("in total", line)) out$total = count + if (grepl("mapped \\(", line)) out$mapped = count + if (grepl("paired in seq", line)) out$paired = count + if (grepl("secondary", line)) out$secondary = count + if (grepl("supplementary", line)) out$supplementary = count + if (grepl("duplicate", line)) out$duplicate = count + } + out +} + +# Parse samtools stats (*.stats) — SN summary lines only (long-read relevant) +# Returns list of summary metrics; NULL if file missing. +parse_samtools_stats = function(stats_file) { + if (is.null(stats_file) || !file.exists(stats_file)) return(NULL) + lines = readLines(stats_file, warn = FALSE) + sn = lines[startsWith(lines, "SN\t")] + get_sn = function(key) { + hit = grep(paste0("^SN\t", key, ":\t"), sn, value = TRUE) + if (length(hit) == 0) return(NA_real_) + suppressWarnings(as.numeric(trimws(sub(paste0("^SN\t", key, ":\t([^\t#]+).*"), "\\1", hit[1])))) + } + reads_total = get_sn("raw total sequences") + reads_mapped = get_sn("reads mapped") + mapped_pct = if (!is.na(reads_total) && reads_total > 0) + round(reads_mapped / reads_total * 100, 2) else NA_real_ + list( + reads_total = reads_total, + reads_mapped = reads_mapped, + mapped_pct = mapped_pct, + total_length = get_sn("total length"), + bases_mapped = get_sn("bases mapped \\(cigar\\)"), + error_rate = get_sn("error rate"), + avg_length = get_sn("average length"), + max_length = get_sn("maximum length"), + avg_quality = get_sn("average quality") + ) +} diff --git a/assets/lrsomatic_report/R/parse_severus.R b/assets/lrsomatic_report/R/parse_severus.R new file mode 100644 index 00000000..170b3316 --- /dev/null +++ b/assets/lrsomatic_report/R/parse_severus.R @@ -0,0 +1,229 @@ +suppressPackageStartupMessages({ + library(data.table) +}) + +# Parse Severus somatic VCF for circos plot data. +# Returns list: $translocations (BND records) and $nontrans (DEL/DUP/INV/INS) +parse_severus_vcf = function(vcf_file) { + if (is.null(vcf_file) || !file.exists(vcf_file)) { + return(list(translocations = data.table(), nontrans = data.table())) + } + + con = gzfile(vcf_file, "rb") + skip_n = 0L + repeat { + line = readLines(con, n = 1, warn = FALSE) + if (length(line) == 0) break + if (startsWith(line, "#CHROM")) break + skip_n = skip_n + 1L + } + close(con) + + # fread() errors (rather than returning 0 rows) when skip lands exactly on + # the last line of the file, i.e. a VCF with no variant records at all. + dt = tryCatch( + fread(vcf_file, skip = skip_n + 1L, sep = "\t", header = FALSE, + select = 1:8, + col.names = c("CHROM", "POS", "ID", "REF", "ALT", "QUAL", "FILTER", "INFO")), + error = function(e) data.table() + ) + if (nrow(dt) == 0) { + return(list(translocations = data.table(), nontrans = data.table())) + } + + dt[, CHROM := ensure_chr_prefix(CHROM)] + + # Extract INFO sub-fields + .info_val = function(info_vec, key) { + pattern = paste0("(?:^|;)", key, "=([^;]+)") + m = regmatches(info_vec, regexpr(pattern, info_vec, perl = TRUE)) + ifelse(nchar(m) > 0, sub(paste0(".*="), "", m), NA_character_) + } + + dt[, SVTYPE := .info_val(INFO, "SVTYPE")] + dt[grepl("END=", INFO, fixed = TRUE), END := as.integer(.info_val(INFO[grepl("END=", INFO, fixed = TRUE)], "END"))] + dt[grepl("SVLEN=", INFO, fixed = TRUE), SVLEN := as.integer(.info_val(INFO[grepl("SVLEN=", INFO, fixed = TRUE)], "SVLEN"))] + + # BND partner chromosome/position from ALT field + # ALT format examples: "N[chr7:24547089[" or "]chr7:24547089]N" + dt[SVTYPE == "BND", CHROM2 := { + m = regmatches(ALT, regexpr("chr[^:]+", ALT, perl = TRUE)) + ifelse(nchar(m) > 0, m, NA_character_) + }] + dt[SVTYPE == "BND", POS2 := as.integer(regmatches(ALT, regexpr("(?<=:)\\d+", ALT, perl = TRUE)))] + + # Insertions have no END — use POS + dt[SVTYPE == "INS" | is.na(END), END := POS] + + # Colours and y-positions for non-BND SV track + SV_COL = c(INS = "#f97e02", DEL = "#020272", INV = "#e7cc02", DUP = "#e41a1c") + SV_YPOS = c(INS = 1.0, DEL = 0.66, INV = 0.33, DUP = 0.05) + dt[SVTYPE %in% names(SV_COL), circos_col := SV_COL[SVTYPE]] + dt[SVTYPE %in% names(SV_YPOS), circos_pos := SV_YPOS[SVTYPE]] + + translocations = dt[SVTYPE == "BND" & !is.na(CHROM2) & !is.na(POS2), + .(chrom = CHROM, pos = POS, chrom2 = CHROM2, pos2 = POS2)] + + nontrans = dt[SVTYPE != "BND", + .(chrom = CHROM, pos = POS, end = END, svtype = SVTYPE, + svlen = SVLEN, circos_pos, circos_col)] + + list(translocations = translocations, nontrans = nontrans) +} + +# Parse the somatic Severus VCF into one row per SV (id, svtype, coords, length, VAF). +# Used as the input to build_sv_table_from_vep() — a lighter-weight companion to +# parse_severus_vcf() above, which instead returns circos-ready translocation/non-BND tracks. +parse_severus_somatic_records = function(vcf_file) { + if (is.null(vcf_file) || !file.exists(vcf_file)) return(data.table()) + + con = gzfile(vcf_file, "rb") + skip_n = 0L + repeat { + line = readLines(con, n = 1, warn = FALSE) + if (length(line) == 0) break + if (startsWith(line, "#CHROM")) break + skip_n = skip_n + 1L + } + close(con) + + dt = tryCatch( + fread(vcf_file, skip = skip_n + 1L, sep = "\t", header = FALSE, select = 1:10, + col.names = c("CHROM", "POS", "ID", "REF", "ALT", "QUAL", "FILTER", "INFO", + "FORMAT", "SAMPLE1")), + error = function(e) data.table() + ) + if (nrow(dt) == 0) return(data.table()) + + dt[, CHROM := ensure_chr_prefix(CHROM)] + + .info_val = function(info_vec, key) { + pattern = paste0("(?:^|;)", key, "=([^;]+)") + m = regmatches(info_vec, regexpr(pattern, info_vec, perl = TRUE)) + ifelse(nchar(m) > 0, sub(paste0(".*="), "", m), NA_character_) + } + + dt[, SVTYPE := .info_val(INFO, "SVTYPE")] + dt[grepl("END=", INFO, fixed = TRUE), END := as.integer(.info_val(INFO[grepl("END=", INFO, fixed = TRUE)], "END"))] + dt[grepl("SVLEN=", INFO, fixed = TRUE), SVLEN := as.integer(.info_val(INFO[grepl("SVLEN=", INFO, fixed = TRUE)], "SVLEN"))] + + # Insertions and BNDs have no END — use POS + dt[SVTYPE %in% c("INS", "BND") | is.na(END), END := POS] + + # VAF from FORMAT/SAMPLE1 (format string is uniform for Severus output, but split + # format-group by format-group defensively, as in parse_caller_vcf()) + fmt_groups = unique(dt$FORMAT) + vaf_list = vector("numeric", nrow(dt)) + for (fmt in fmt_groups) { + idx_rows = which(dt$FORMAT == fmt) + fields = strsplit(fmt, ":", fixed = TRUE)[[1]] + vaf_idx = match("VAF", fields) + split_s = strsplit(dt$SAMPLE1[idx_rows], ":", fixed = TRUE) + vaf_list[idx_rows] = if (!is.na(vaf_idx)) { + vapply(split_s, function(x) + if (length(x) >= vaf_idx) suppressWarnings(as.numeric(x[vaf_idx])) else NA_real_, + numeric(1)) + } else NA_real_ + } + dt[, VAF := vaf_list] + + dt[, .(id = ID, svtype = SVTYPE, chrom = CHROM, start = POS, end = END, + sv_len = SVLEN, vaf = VAF)] +} + +# Build the SV display table by joining VEP CSQ gene annotations (from the SV VEP VCF, +# `parse_vep_vcf()` from R/parse_smallvariants.R) onto the somatic Severus SVs by locus. +# This is the primary path when a VEP SV VCF is available; build_sv_table() below (fed by +# the gene-annotated TSV) is the fallback for pipelines that don't produce a VEP SV VCF. +build_sv_table_from_vep = function(somatic_vcf, vep_sv_vcf) { + somatic = parse_severus_somatic_records(somatic_vcf) + if (nrow(somatic) == 0) return(data.table()) + + vep = parse_vep_vcf(vep_sv_vcf) + if (is.null(vep) || nrow(vep) == 0) { + somatic[, `:=`(gene_hits = NA_character_, consequence = NA_character_, impact = NA_character_)] + return(somatic[, .(id, gene_hits, svtype, chrom, start, end, sv_len, vaf, consequence, impact)]) + } + + # Keep the highest-impact annotation per locus, and collapse all distinct gene symbols + # hit at that locus into one comma-joined column. + impact_rank = c(HIGH = 1L, MODERATE = 2L, LOW = 3L, MODIFIER = 4L) + vep[, impact_rank := impact_rank[impact]] + vep[is.na(impact_rank), impact_rank := 5L] + setorder(vep, impact_rank) + + agg = vep[, .( + gene_hits = paste(unique(symbol[!is.na(symbol) & nzchar(symbol)]), collapse = ","), + consequence = consequence[1], + impact = impact[1] + ), by = .(chrom, pos)] + agg[!nzchar(gene_hits), gene_hits := NA_character_] + + merged = merge(somatic, agg, by.x = c("chrom", "start"), by.y = c("chrom", "pos"), all.x = TRUE) + merged[, .(id, gene_hits, svtype, chrom, start, end, sv_len, vaf, consequence, impact)] +} + +# Parse the gene-annotated Severus TSV (filtered_SV2/SV_filtered_with_gene_annotations.tsv) +parse_severus_gene_tsv = function(tsv_file) { + if (is.null(tsv_file) || !file.exists(tsv_file)) return(NULL) + dt = fread(tsv_file, sep = "\t", header = TRUE, fill = TRUE) + setnames(dt, toupper(names(dt))) + + if ("START_CHROM" %in% names(dt)) dt[, START_CHROM := ensure_chr_prefix(START_CHROM)] + if ("END_CHROM" %in% names(dt)) dt[, END_CHROM := ensure_chr_prefix(END_CHROM)] + + # Gene column: prefer NHL hits + gene_col = if ("NHL_GENE_HITS" %in% names(dt)) "NHL_GENE_HITS" + else if ("COSMIC_GENE_HITS" %in% names(dt)) "COSMIC_GENE_HITS" + else NULL + dt[, gene_hits := if (!is.null(gene_col)) get(gene_col) else NA_character_] + dt +} + +# Build the SV display table. +# gene_panel: character vector of HGNC symbols to keep, or NULL to return all SVs (one row each). +build_sv_table = function(sv_tsv, gene_panel = NULL) { + if (is.null(sv_tsv) || nrow(sv_tsv) == 0) return(data.table()) + + sv_tsv = copy(sv_tsv) + + # When no panel is supplied, return one row per SV without explosion + if (is.null(gene_panel)) { + display_cols = intersect( + c("ID", "SVTYPE", "DETAILED_TYPE", + "START_CHROM", "START_POS", "END_CHROM", "END_POS", + "SV_LEN", "VAF", "NHL_GENE_HITS", "COSMIC_GENE_HITS", + "NHL_NEAREST_GENE_HITS_1MBWINDOW"), + names(sv_tsv) + ) + return(sv_tsv[, ..display_cols]) + } + + # Panel-filtered path: explode multi-gene gene_hits, filter, return one row per gene×SV + sv_tsv[, .ridx := .I] + + sv_long = sv_tsv[, { + raw = as.character(gene_hits[1]) + genes = unique(trimws(unlist(strsplit(raw, "[;,]+")))) + genes = genes[nchar(genes) > 0 & genes != "-" & toupper(genes) != "NA"] + if (length(genes) == 0) genes = NA_character_ + list(gene = genes) + }, by = .ridx] + + sv_long = merge(sv_long, sv_tsv, by = ".ridx") + sv_long[, .ridx := NULL] + sv_tsv[, .ridx := NULL] + + # Filter by gene panel (always, even if panel is empty) + sv_long = sv_long[!is.na(gene) & gene %in% gene_panel] + if (nrow(sv_long) == 0) return(data.table()) + + display_cols = intersect( + c("gene", "ID", "SVTYPE", "DETAILED_TYPE", + "START_CHROM", "START_POS", "END_CHROM", "END_POS", + "SV_LEN", "VAF", "NHL_GENE_HITS", "COSMIC_GENE_HITS", + "NHL_NEAREST_GENE_HITS_1MBWINDOW"), + names(sv_long) + ) + sv_long[, ..display_cols] +} diff --git a/assets/lrsomatic_report/R/parse_smallvariants.R b/assets/lrsomatic_report/R/parse_smallvariants.R new file mode 100644 index 00000000..f0c837b5 --- /dev/null +++ b/assets/lrsomatic_report/R/parse_smallvariants.R @@ -0,0 +1,404 @@ +suppressPackageStartupMessages({ + library(data.table) + library(dplyr) +}) + +# Values of LRSomatic's INFO/CALLER tag that denote a somatic caller. The pipeline's +# *_SOMATIC_VEP.vcf.gz is a merged multi-caller VCF in which germline callers +# (deepvariant, clair3) supply the overwhelming majority of records, so the somatic +# table has to be filtered on this tag. ClairS is tagged "clairs" in matched mode and +# "clairs-to" in tumour-only mode. +SOMATIC_CALLERS = c("clairs", "clairs-to", "clairsto", "deepsomatic") + +# Derive dbsnp/cosmic columns from a VEP "Existing_variation" column (semicolon- or +# comma-joined list of IDs, e.g. "rs123&COSV456"). Shared by parse_vep_text/parse_vep_vcf. +derive_dbsnp_cosmic = function(dt) { + dt[, dbsnp := sub("(rs[0-9]+).*", "\\1", existing)] + dt[!grepl("^rs", dbsnp, perl = TRUE), dbsnp := NA_character_] + + dt[, cosmic := sub(".*(COS[VM][0-9]+).*", "\\1", existing)] + dt[!grepl("^COS", cosmic, perl = TRUE), cosmic := NA_character_] + dt +} + +# Dispatch to the right VEP parser based on actual file contents — both forms ship as +# "*_SOMATIC_VEP.vcf.gz" so the filename alone doesn't tell you which one you have. +# - VEP default text output: "##"-commented header, column line starts with "#Uploaded_variation" +# - genuine VCF w/ CSQ INFO field: "##fileformat=VCFv4.2", column line starts with "#CHROM" +parse_vep = function(vep_file) { + if (is.null(vep_file) || !file.exists(vep_file)) return(NULL) + + con = gzfile(vep_file, "rb") + is_vcf = FALSE + repeat { + line = readLines(con, n = 1, warn = FALSE) + if (length(line) == 0) break + if (startsWith(line, "#Uploaded_variation")) break + if (startsWith(line, "#CHROM")) { is_vcf = TRUE; break } + } + close(con) + + if (is_vcf) parse_vep_vcf(vep_file) else parse_vep_text(vep_file) +} + +# Parse the VEP default text output (tab-delimited, ##-commented header, NOT a VCF). +# Returns a data.table with one row per consequence per variant. +parse_vep_text = function(vep_file) { + if (is.null(vep_file) || !file.exists(vep_file)) return(NULL) + + # Count meta-lines (start with ##) to find the column-header line + con = gzfile(vep_file, "rb") + skip_n = 0L + repeat { + line = readLines(con, n = 1, warn = FALSE) + if (length(line) == 0) break + if (startsWith(line, "#Uploaded_variation")) break + skip_n = skip_n + 1L + } + close(con) + + dt = tryCatch( + fread(vep_file, skip = skip_n, sep = "\t", header = TRUE, + col.names = function(x) gsub("^#", "", x)), + error = function(e) { + message("Failed to parse VEP file: ", conditionMessage(e)) + NULL + } + ) + if (is.null(dt) || nrow(dt) == 0) return(NULL) + + setnames(dt, old = "Uploaded_variation", new = "variant_id", skip_absent = TRUE) + setnames(dt, old = "Gene", new = "gene_id", skip_absent = TRUE) + setnames(dt, old = "Consequence", new = "consequence", skip_absent = TRUE) + + # Coordinates and alleles both come from variant_id ("chr1_3506_A/G") wherever it has + # that canonical shape, which is what VEP synthesises for VCF input without an ID. + # Mixing the two sources is not safe: for an insertion reported in VEP's dash form, + # Location's start is one base left of the position variant_id names + # ("chr1_197488_-/G" has Location "chr1:197487-197488"), which then fails to join to + # anything. Location remains the fallback for rows carrying a real VCF ID instead. + vid = "^.+_[0-9]+_[^_]+/[^_]+$" + dt[, from_vid := grepl(vid, variant_id)] + + dt[, chrom := ifelse(from_vid, sub("_[0-9]+_[^_]+$", "", variant_id), + sub(":.*", "", Location))] + dt[, pos := as.integer(ifelse(from_vid, sub(".*_([0-9]+)_[^_]+$", "\\1", variant_id), + sub(".*:(\\d+).*", "\\1", Location)))] + dt[, chrom := ensure_chr_prefix(chrom)] + + dt[, ref := sub(".*_([^/]+)/.*", "\\1", variant_id)] + dt[, alt := sub(".*/", "", variant_id)] + + # Parse VEP Extra key=value field + dt[, symbol := extract_extra_key(Extra, "SYMBOL")] + dt[, impact := extract_extra_key(Extra, "IMPACT")] + dt[, existing := extract_extra_key(Extra, "Existing_variation")] + dt[, sift := extract_extra_key(Extra, "SIFT")] + dt[, polyphen := extract_extra_key(Extra, "PolyPhen")] + dt[, hgvsp := extract_extra_key(Extra, "HGVSp")] + + # dbSNP / COSMIC IDs, derived from Existing_variation + dt = derive_dbsnp_cosmic(dt) + + # No per-variant caller in the text format; keep the column for contract parity + # with parse_vep_vcf(). + dt[, caller := NA_character_] + + dt +} + +# Parse a genuine VCF carrying VEP annotation in a CSQ INFO field (VEP run with --vcf, +# as opposed to the default text output handled by parse_vep_text()). +# Returns a data.table with one row per gene/transcript annotation per variant, using the +# same column contract as parse_vep_text(): chrom, pos, ref, alt, symbol, gene_id, +# consequence, impact, hgvsp, existing, dbsnp, cosmic, sift, polyphen, caller. +parse_vep_vcf = function(vep_file) { + if (is.null(vep_file) || !file.exists(vep_file)) return(NULL) + + # Skip header to #CHROM, capturing the CSQ field order from its INFO meta-line + # (e.g. "...Format: Allele|Consequence|IMPACT|SYMBOL|Gene|...") and noting whether + # the file carries a per-record CALLER tag. + con = gzfile(vep_file, "rb") + skip_n = 0L + csq_format = NULL + has_caller_info = FALSE + repeat { + line = readLines(con, n = 1, warn = FALSE) + if (length(line) == 0) break + if (startsWith(line, "##INFO= 0) csq_format = strsplit(sub("^Format: ", "", m), "|", fixed = TRUE)[[1]] + } + if (startsWith(line, "##INFO= 1) { + parts = lapply(vcf_file, parse_caller_vcf, caller_name = caller_name) + parts = parts[!vapply(parts, is.null, logical(1))] + return(if (length(parts) > 0) rbindlist(parts) else NULL) + } + if (!file.exists(vcf_file)) return(NULL) + + # Count header lines + con = gzfile(vcf_file, "rb") + skip_n = 0L + repeat { + line = readLines(con, n = 1, warn = FALSE) + if (length(line) == 0) break + if (startsWith(line, "#CHROM")) break + skip_n = skip_n + 1L + } + close(con) + + # Read up to 10 columns (standard VCF single-sample layout) + col_names = c("CHROM", "POS", "ID", "REF", "ALT", "QUAL", "FILTER", "INFO", "FORMAT", "SAMPLE1") + dt = fread(vcf_file, skip = skip_n + 1L, sep = "\t", header = FALSE, + select = 1:10, col.names = col_names) + if (nrow(dt) == 0) return(NULL) + + dt[, CHROM := ensure_chr_prefix(CHROM)] + + # Extract AF, DP, GT and PS from the FORMAT + SAMPLE1 columns. + # Work format-group by format-group to avoid splitting every single row redundantly. + fmt_groups = unique(dt$FORMAT) + vaf_list = rep(NA_real_, nrow(dt)) + dp_list = rep(NA_integer_, nrow(dt)) + gt_list = rep(NA_character_, nrow(dt)) + ps_list = rep(NA_character_, nrow(dt)) + + for (fmt in fmt_groups) { + idx_rows = which(dt$FORMAT == fmt) + fields = strsplit(fmt, ":", fixed = TRUE)[[1]] + split_s = strsplit(dt$SAMPLE1[idx_rows], ":", fixed = TRUE) + + # One FORMAT field, by name, across this group's rows + field = function(name) { + i = match(name, fields) + if (is.na(i)) return(rep(NA_character_, length(split_s))) + vapply(split_s, function(x) if (length(x) >= i) x[i] else NA_character_, + character(1)) + } + + vaf_list[idx_rows] = suppressWarnings(as.numeric(field("AF"))) + dp_list[idx_rows] = suppressWarnings(as.integer(field("DP"))) + gt_list[idx_rows] = field("GT") + ps_list[idx_rows] = field("PS") + } + + # Unphased records carry "." for PS and a "/"-separated GT. Blank the placeholders so + # the report shows an empty cell rather than a bare ".". + ps_list[!is.na(ps_list) & ps_list == "."] = NA_character_ + gt_list[!is.na(gt_list) & gt_list %in% c(".", "./.")] = NA_character_ + + data.table(chrom = dt$CHROM, pos = dt$POS, ref = dt$REF, alt = dt$ALT, + vaf = vaf_list, dp = dp_list, gt = gt_list, ps = ps_list, + caller = caller_name) +} + +# Canonical variant key, used to join VEP annotation rows to the VCF they came from. +# +# VEP always reports an indel one base to the right of the VCF anchor, and writes the +# alleles either as the raw VCF pair or in its own trimmed form with a dash for the empty +# side — which of the two depends on the VEP version: +# +# VCF record VEP Uploaded_variation allele notation +# chr1 1871654 TG T chr1_1871655_TG/T raw +# chr1 14553006 G GA chr1_14553007_G/GA raw +# chr1 192936 GAATA G chr1_192937_AATA/- trimmed + dash +# chr1 197487 A AG chr1_197488_-/G trimmed + dash +# +# All four reconcile in one space: trimmed alleles (anchor base dropped, empty side +# written "-") at the VCF anchor position + 1. SNVs and equal-length MNVs have no anchor +# base and are keyed verbatim. +# +# This relies on `pos` coming from VEP's variant_id, which is consistently the shifted +# position — the Location column is not (see parse_vep_text()). +# +# `space` is "vcf" for records read from a VCF, "vep" for rows read from VEP output. +variant_key = function(chrom, pos, ref, alt, space = c("vcf", "vep")) { + space = match.arg(space) + ref = toupper(as.character(ref)); alt = toupper(as.character(alt)) + pos = as.integer(pos) + + trim = function(x) { t = substr(x, 2L, nchar(x)); ifelse(t == "", "-", t) } + + dash = ref == "-" | alt == "-" # already trimmed by VEP + is_indel = dash | nchar(ref) != nchar(alt) + + # Raw allele pairs still need the anchor base dropped; dash forms are already trimmed. + need_trim = is_indel & !dash + + # Only the VCF side needs shifting — VEP has already done it. + key_pos = ifelse(space == "vcf" & is_indel, pos + 1L, pos) + key_ref = ifelse(need_trim, trim(ref), ref) + key_alt = ifelse(need_trim, trim(alt), alt) + + paste(chrom, key_pos, key_ref, key_alt, sep = "|") +} + +# Classify SNV into 6 SBS mutation categories (C/T-ref normalised) +classify_mut = function(ref, alt) { + comp = c(A = "T", T = "A", C = "G", G = "C") + ref = toupper(ref); alt = toupper(alt) + use_comp = !(ref %in% c("C", "T")) + norm_ref = ifelse(use_comp, comp[ref], ref) + norm_alt = ifelse(use_comp, comp[alt], alt) + paste0(norm_ref, ">", norm_alt) +} + +# Build the small-variant display table: canonical rows from the VEP annotation, with +# VAF / depth / phasing joined from the VCF that VEP annotated (see `somatic_vaf_vcf` in +# locate_outputs.R). +# gene_panel: character vector of HGNC symbols to keep, or NULL to return all variants. +build_variant_table = function(vep_data, vaf_data, gene_panel = NULL) { + if (is.null(vep_data) || nrow(vep_data) == 0) return(NULL) + + # Impact ranking for deduplication + impact_rank = c(HIGH = 1L, MODERATE = 2L, LOW = 3L, MODIFIER = 4L) + vep_data[, impact_rank := impact_rank[impact]] + vep_data[is.na(impact_rank), impact_rank := 5L] + + # Filter to gene panel (by gene symbol or Ensembl ID fallback) + if (!is.null(gene_panel)) { + if (length(gene_panel) > 0) { + vep_data = vep_data[symbol %in% gene_panel | gene_id %in% gene_panel] + } else { + vep_data = vep_data[FALSE] # Empty panel → empty result + } + } + if (nrow(vep_data) == 0) return(data.table()) + + # Keep best consequence per variant×gene (lowest impact rank) + key_cols = c("chrom", "pos", "ref", "alt", "symbol") + setorder(vep_data, impact_rank) + vep_data = unique(vep_data, by = key_cols) + + # Join VEP rows to the VCF they were produced from, via the canonical key that + # reconciles the two sides' indel representations (see variant_key()). + vep_data[, join_key := variant_key(chrom, pos, ref, alt, space = "vep")] + + if (!is.null(vaf_data) && nrow(vaf_data) > 0) { + vdt = vaf_data[, .(join_key = variant_key(chrom, pos, ref, alt, space = "vcf"), + vaf, dp, gt, ps)] + vdt = unique(vdt, by = "join_key") + vep_data = merge(vep_data, vdt, by = "join_key", all.x = TRUE) + } else { + vep_data[, `:=`(vaf = NA_real_, dp = NA_integer_, + gt = NA_character_, ps = NA_character_)] + } + + # Which caller reported each variant. A merged multi-caller VEP VCF states this outright + # in INFO/CALLER; the VEP text format carries no per-variant caller, leaving this empty. + if ("caller" %in% names(vep_data) && any(!is.na(vep_data$caller))) { + vep_data[, callers := caller] + } else { + vep_data[, callers := ""] + } + + # Mutation category for SNVs + vep_data[nchar(ref) == 1 & nchar(alt) == 1, + mut_cat := classify_mut(ref, alt)] + + display_cols = c("symbol", "chrom", "pos", "ref", "alt", + "consequence", "impact", "hgvsp", + "vaf", "dp", "gt", "ps", + "callers", "cosmic", "dbsnp", "sift", "polyphen") + display_cols = display_cols[display_cols %in% names(vep_data)] + vep_data[, ..display_cols] +} diff --git a/assets/lrsomatic_report/R/references.R b/assets/lrsomatic_report/R/references.R new file mode 100644 index 00000000..c0fdcb28 --- /dev/null +++ b/assets/lrsomatic_report/R/references.R @@ -0,0 +1,72 @@ +suppressPackageStartupMessages({ + library(data.table) +}) + +# Load cytobands for a given reference; returns data.frame suitable for circlize +load_cytobands = function(reference, assets_dir) { + ref = tolower(reference) + path = file.path(assets_dir, "references", ref, "cytobands.tsv") + if (!file.exists(path)) stop("No cytobands for reference '", ref, "': ", path) + dt = fread(path, header = FALSE, sep = "\t", + col.names = c("chrom", "start", "end", "name", "stain")) + as.data.frame(dt) +} + +# Load chromosome lengths; returns named integer vector (name = chrom, value = length) +load_chrom_lengths = function(reference, assets_dir) { + ref = tolower(reference) + path = file.path(assets_dir, "references", ref, "chrom_lengths.tsv") + if (!file.exists(path)) stop("No chrom_lengths for reference '", ref, "': ", path) + dt = fread(path, header = FALSE, sep = "\t", col.names = c("chrom", "length")) + setNames(as.integer(dt$length), dt$chrom) +} + +# Auto-detect reference genome from VCF/VEP header lines. +# Checks: ##contig length (VCF), ## assembly version (VEP text), ## genome_build. +# T2T CHM13v2: chr1 = 248387328 +# GRCh38: chr1 = 248956422 +detect_reference = function(vcf_file) { + if (!file.exists(vcf_file)) { + message("Cannot auto-detect reference: file not found, defaulting to t2t") + return("t2t") + } + con = gzfile(vcf_file, "rb") + on.exit(close(con)) + header_lines = character(0) + for (i in seq_len(2000)) { + line = tryCatch(readLines(con, n = 1, warn = FALSE), error = function(e) character(0)) + if (length(line) == 0 || !startsWith(line, "##")) break + header_lines = c(header_lines, line) + } + + # 1. Check VEP "## assembly version" line + asm_line = grep("assembly version|genome_build|assembly=", header_lines, + value = TRUE, ignore.case = TRUE) + if (length(asm_line) > 0) { + asm = tolower(paste(asm_line, collapse = " ")) + if (grepl("t2t|chm13", asm)) return("t2t") + if (grepl("grch38|hg38|38", asm)) return("hg38") + } + + # 2. Check ##contig chr1 length (standard VCF) + contig_chr1 = grep("ID=chr1[^0-9].*length=|ID=1[^0-9].*length=", + header_lines, value = TRUE, perl = TRUE) + if (length(contig_chr1) > 0) { + len = as.integer(sub(".*length=([0-9]+).*", "\\1", contig_chr1[1])) + if (!is.na(len)) { + if (abs(len - 248387328L) < 1000L) return("t2t") + if (abs(len - 248956422L) < 1000L) return("hg38") + } + } + + message("Could not determine reference from file headers, defaulting to t2t") + "t2t" +} + +# Build the chromosome list for plotting based on sex +chromosomes_for_sex = function(sex) { + sex = tolower(trimws(sex)) + autosomes = paste0("chr", 1:22) + if (sex %in% c("male", "xy")) c(autosomes, "chrX", "chrY") + else c(autosomes, "chrX") +} diff --git a/assets/lrsomatic_report/R/sections.R b/assets/lrsomatic_report/R/sections.R new file mode 100644 index 00000000..d778dc0a --- /dev/null +++ b/assets/lrsomatic_report/R/sections.R @@ -0,0 +1,14 @@ +# Section-module contract: each report section registers a descriptor with +# id, title, locate(sample_dir, sample_id), and parse(inputs, section_data). +# See CLAUDE.md "Section-module contract" for the recipe to add a new section. + +SECTIONS = list() + +register_section = function(descriptor) { + SECTIONS[[descriptor$id]] <<- descriptor +} + +# Standard "nothing to show" notice used by section presentation shims. +section_notice = function(msg) { + tags$div(class = "alert alert-info", msg) +} diff --git a/assets/lrsomatic_report/R/sections/sv.R b/assets/lrsomatic_report/R/sections/sv.R new file mode 100644 index 00000000..54bc6380 --- /dev/null +++ b/assets/lrsomatic_report/R/sections/sv.R @@ -0,0 +1,64 @@ +# Structural variants section. Reference implementation of the section-module +# contract (see R/sections.R and CLAUDE.md). Keyed by caller so a second SV +# caller can be added later without touching the plumbing below. + +register_section(list( + id = "sv", + title = "Structural variants", + + locate = function(sample_dir, sample_id) { + d = sample_dir + + find1 = function(pattern) { + hits = list.files(d, pattern = pattern, recursive = TRUE, full.names = TRUE) + if (length(hits) > 0) hits[1] else NULL + } + + severus_vcf = find1("^severus_somatic\\.vcf\\.gz$") + severus_gene_tsv = find1("^SV_filtered_with_gene_annotations\\.tsv$") + # VEP SV VCF (CSQ-annotated) is the more commonly produced annotation source; the + # gene-annotated TSV above is a fallback for pipelines that produce it instead. + severus_vep_vcf = find1("_SV_VEP\\.vcf\\.gz$") + + list(callers = list( + severus = list(vcf = severus_vcf, gene_tsv = severus_gene_tsv, vep_vcf = severus_vep_vcf) + )) + }, + + parse = function(inputs, section_data) { + tabs = list() + circ = list(nontrans = data.table(), translocations = data.table()) + any_annotation = FALSE + + for (nm in names(inputs$callers)) { + caller_inputs = inputs$callers[[nm]] + v = parse_severus_vcf(caller_inputs$vcf) + + # VEP SV VCF is the primary annotation source; the gene-annotated TSV (not produced + # by most pipelines) is a fallback for samples that have it instead. + if (!is.null(caller_inputs$vep_vcf)) { + t = build_sv_table_from_vep(caller_inputs$vcf, caller_inputs$vep_vcf) + } else { + g = parse_severus_gene_tsv(caller_inputs$gene_tsv) + t = build_sv_table(g, gene_panel = NULL) + } + if (!is.null(t) && nrow(t) > 0) { + any_annotation = TRUE + t[, caller := nm] + tabs[[nm]] = t + } + # Circos tracks are drawn from raw breakpoints, not the gene table; + # with a single caller today, last-write-wins is a no-op. + circ$nontrans = v$nontrans + circ$translocations = v$translocations + } + + tbl = if (length(tabs) > 0) rbindlist(tabs, fill = TRUE) else data.table() + + list( + table = tbl, + circos = circ, + annotation_found = any_annotation + ) + } +)) diff --git a/assets/lrsomatic_report/R/sections/whatshap.R b/assets/lrsomatic_report/R/sections/whatshap.R new file mode 100644 index 00000000..de9f8595 --- /dev/null +++ b/assets/lrsomatic_report/R/sections/whatshap.R @@ -0,0 +1,56 @@ +# Phasing section. Reads the WhatsHap phasing statistics the pipeline writes to +# qc/whatshap_stats/. See R/sections.R and CLAUDE.md for the section-module contract. +# +# Note these are *germline* phasing statistics: the pipeline runs WHATSHAP_STATS on the +# phased germline VCF, so every row's file_name is germline_smallvariants.vcf.gz. + +register_section(list( + id = "whatshap", + title = "Phasing", + + locate = function(sample_dir, sample_id) { + d = sample_dir + + find1 = function(pattern) { + hits = list.files(d, pattern = pattern, recursive = TRUE, full.names = TRUE) + if (length(hits) > 0) hits[1] else NULL + } + + # qc/whatshap_stats/ is not tumour/normal-scoped, unlike the rest of qc/, so a plain + # recursive match is correct here. + list(stats_tsv = find1("_whatshap_stats\\.tsv$")) + }, + + parse = function(inputs, section_data) { + f = inputs$stats_tsv + if (is.null(f) || !file.exists(f)) return(NULL) + + dt = tryCatch( + fread(f, sep = "\t", header = TRUE), + error = function(e) { + message("Failed to parse WhatsHap stats: ", conditionMessage(e)) + NULL + } + ) + if (is.null(dt) || nrow(dt) == 0) return(NULL) + + # The header line is "#sample\tchromosome\t..." — fread keeps the leading "#". + setnames(dt, sub("^#", "", names(dt))) + if (!"chromosome" %in% names(dt)) { + message("WhatsHap stats has no 'chromosome' column; skipping section") + return(NULL) + } + + # bp_per_block_sum exceeds .Machine$integer.max and reads as integer64, which DT + # renders badly. Widen every integer64 column to double. + for (col in names(dt)) { + if (inherits(dt[[col]], "integer64")) dt[, (col) := as.numeric(get(col))] + } + + list( + per_chrom = dt[chromosome != "ALL"], + all = if (any(dt$chromosome == "ALL")) as.list(dt[chromosome == "ALL"][1]) else NULL, + vcf = if ("file_name" %in% names(dt)) dt$file_name[1] else NA_character_ + ) + } +)) diff --git a/assets/lrsomatic_report/R/utils.R b/assets/lrsomatic_report/R/utils.R new file mode 100644 index 00000000..127e040f --- /dev/null +++ b/assets/lrsomatic_report/R/utils.R @@ -0,0 +1,235 @@ +suppressPackageStartupMessages({ + library(data.table) +}) + +ensure_chr_prefix = function(x) { + ifelse(startsWith(x, "chr"), x, paste0("chr", x)) +} + +strip_chr_prefix = function(x) { + sub("^chr", "", x) +} + +# Parse VEP "Extra" key=value semicolon-delimited field into a named character vector +parse_extra_kv = function(extra_string) { + if (is.na(extra_string) || extra_string == "" || extra_string == "-") return(character(0)) + pairs = strsplit(extra_string, ";", fixed = TRUE)[[1]] + kv = strsplit(pairs, "=", fixed = TRUE) + keys = vapply(kv, `[`, character(1), 1) + vals = vapply(kv, function(x) if (length(x) >= 2) paste(x[-1], collapse = "=") else "", character(1)) + setNames(vals, keys) +} + +# Vectorised: extract one key from VEP Extra column for each row +extract_extra_key = function(extra_vec, key) { + vapply(extra_vec, function(x) { + kv = parse_extra_kv(x) + if (key %in% names(kv)) kv[[key]] else NA_character_ + }, character(1), USE.NAMES = FALSE) +} + +# Load a gene panel TSV or plain text file; returns character vector of gene symbols +load_gene_panel = function(path) { + if (!file.exists(path)) stop("Gene panel file not found: ", path) + dt = tryCatch( + fread(path, header = TRUE, sep = "\t", fill = TRUE), + error = function(e) fread(path, header = FALSE, sep = "\t", fill = TRUE) + ) + gene_col = if ("gene" %in% tolower(names(dt))) names(dt)[tolower(names(dt)) == "gene"][1] else names(dt)[1] + unique(dt[[gene_col]]) +} + +# Filter a data frame to rows where the gene column matches the panel. +# panel_genes = NULL means "no panel" and returns dt untouched; an empty +# character vector is a genuinely empty panel and filters everything out. +filter_by_gene_panel = function(dt, panel_genes, gene_col = "gene") { + if (is.null(panel_genes)) return(dt) + dt[dt[[gene_col]] %in% panel_genes, ] +} + +# Is a --gene-panel argument the "no filtering" sentinel? +is_no_gene_panel = function(panel_arg) { + is.null(panel_arg) || length(panel_arg) != 1 || is.na(panel_arg) || + identical(tolower(trimws(panel_arg)), "none") +} + +# Resolve a --gene-panel arg: the "none" sentinel (no filtering, returns NULL), +# a builtin name ("lymphoid"), or a path to a TSV. A value that is neither is an +# error rather than a silent fall-through to unfiltered output. +resolve_gene_panel = function(panel_arg, assets_dir) { + if (is_no_gene_panel(panel_arg)) return(NULL) + builtin_path = file.path(assets_dir, "gene_lists", paste0(panel_arg, ".tsv")) + if (file.exists(builtin_path)) return(load_gene_panel(builtin_path)) + if (file.exists(panel_arg)) return(load_gene_panel(panel_arg)) + stop("Gene panel not found (tried builtin '", panel_arg, "' and as file path)") +} + +# Load all gene panels from assets/gene_lists/*.tsv +# Returns a named list (name = panel name, value = character vector of gene symbols) +load_all_gene_panels = function(assets_dir) { + tsv_files = Sys.glob(file.path(assets_dir, "gene_lists", "*.tsv")) + if (length(tsv_files) == 0) return(list()) + panels = lapply(tsv_files, load_gene_panel) + names(panels) = tools::file_path_sans_ext(basename(tsv_files)) + panels +} + +# Format a number for human-readable display +fmt_bp = function(x) { + x = as.numeric(x) + ifelse(abs(x) >= 1e6, paste0(round(x / 1e6, 1), " Mb"), + ifelse(abs(x) >= 1e3, paste0(round(x / 1e3, 1), " kb"), + paste0(x, " bp"))) +} + +# Embed a local PNG file as a self-contained base64 img tag +embed_png = function(path, max_width = "900px") { + if (is.null(path) || !file.exists(path)) return(NULL) + b64 = base64enc::base64encode(path) + htmltools::tags$img( + src = paste0("data:image/png;base64,", b64), + style = paste0("max-width:", max_width, "; display:block; margin:auto;") + ) +} + +# Build a data: URI for a Wakhan Plotly HTML file, with a small responsive-resize +# script injected before so the plot fills the iframe's width instead of +# rendering at Plotly's fixed native layout.width (which causes horizontal scroll +# inside the iframe). Runs on the iframe's own `load` event so it fires after +# Plotly.newPlot() has already drawn the figure. +wakhan_plot_datauri = function(path) { + html = paste(readLines(path, warn = FALSE), collapse = "\n") + # Wakhan's Plotly divs carry an inline fixed width/height (e.g. style=\"width:1380px\") + # set by Plotly at export time, in addition to a fixed layout.width. autosize/relayout + # alone resizes against that fixed div, so the div's own inline size must be cleared + # to 100% first, then relayout({autosize:true}) + Plots.resize() recomputes against + # the now-flexible container (i.e. the iframe). + resize_script = " + +" + if (grepl("", html, fixed = TRUE)) { + html = sub("", paste0(resize_script, ""), html, fixed = TRUE) + } else { + html = paste0(html, resize_script) + } + paste0("data:text/html;base64,", base64enc::base64encode(charToRaw(html))) +} + +# Embed a self-contained HTML file (e.g. a standalone Plotly plot) as an inline iframe. +embed_html_iframe = function(path, height = "780px") { + if (is.null(path) || !file.exists(path)) return(NULL) + htmltools::tags$iframe( + src = wakhan_plot_datauri(path), + style = paste0("width:100%; height:", height, "; border:none;") + ) +} + +# Render Wakhan's ranked copy-number plots as a self-contained tab widget (not a +# Quarto .panel-tabset): Quarto's panel-tabset relies on Pandoc parsing `####` +# ATX headings out of a results='asis' stream, which breaks when raw iframe HTML +# for one rank is emitted immediately before the next rank's heading (Pandoc +# absorbs the heading into the preceding raw-HTML block, so only the first tab +# ever registers). This widget also defers loading: only the first pane's +# iframe gets a real `src`; the rest carry `data-src` and are populated on +# first click, so hidden ranks' plotly.js payloads aren't parsed at page load. +render_wakhan_cn_tabs = function(plots) { + if (length(plots) == 0) return(NULL) + + ids = paste0("wakhan-cn-pane-", seq_along(plots)) + + buttons = lapply(seq_along(plots), function(i) { + p = plots[[i]] + htmltools::tags$button( + class = if (i == 1) "wakhan-cn-tab active" else "wakhan-cn-tab", + `data-target` = ids[i], + paste0("Rank ", p$rank, " — purity ", p$purity, ", ploidy ", p$ploidy) + ) + }) + + panes = lapply(seq_along(plots), function(i) { + p = plots[[i]] + uri = wakhan_plot_datauri(p$plot) + iframe = if (i == 1) { + htmltools::tags$iframe(src = uri, style = "width:100%; height:780px; border:none;") + } else { + htmltools::tags$iframe(`data-src` = uri, style = "width:100%; height:780px; border:none;") + } + htmltools::tags$div( + class = if (i == 1) "wakhan-cn-pane active" else "wakhan-cn-pane", + id = ids[i], + iframe + ) + }) + + htmltools::tagList( + htmltools::tags$style(" + .wakhan-cn-tabs__nav { display:flex; flex-wrap:wrap; gap:6px; margin-bottom:10px; } + .wakhan-cn-tab { + border:1px solid var(--color-border, #ccc); background:var(--color-bg, #fff); + border-radius:5px; padding:5px 10px; font-size:0.85rem; cursor:pointer; + } + .wakhan-cn-tab.active { background:var(--color-primary, #333); color:#fff; } + .wakhan-cn-pane { display:none; } + .wakhan-cn-pane.active { display:block; } + "), + htmltools::tags$div(class = "wakhan-cn-tabs__nav", buttons), + htmltools::tags$div(class = "wakhan-cn-tabs__panes", panes), + htmltools::tags$script(htmltools::HTML(" + document.querySelectorAll('.wakhan-cn-tabs__nav').forEach(function (nav) { + nav.querySelectorAll('.wakhan-cn-tab').forEach(function (btn) { + btn.addEventListener('click', function () { + const container = nav.nextElementSibling; + nav.querySelectorAll('.wakhan-cn-tab').forEach(function (b) { b.classList.remove('active'); }); + btn.classList.add('active'); + container.querySelectorAll('.wakhan-cn-pane').forEach(function (p) { p.classList.remove('active'); }); + const pane = document.getElementById(btn.dataset.target); + pane.classList.add('active'); + const iframe = pane.querySelector('iframe[data-src]'); + if (iframe) { + iframe.src = iframe.dataset.src; + iframe.removeAttribute('data-src'); + } + }); + }); + }); + ")) + ) +} + +# Compute coding TMB from a variant_table produced by build_variant_table(). +# consequence column may be comma-joined (e.g. "frameshift_variant,splice_region_variant"). +# denominator_mb: coding Mb used as divisor (default 30 Mb — canonical clinical denominator). +compute_tmb = function(variant_table, denominator_mb = 30) { + nonsyn_terms = c( + "missense_variant", "frameshift_variant", "stop_gained", "stop_lost", + "start_lost", "inframe_insertion", "inframe_deletion", + "splice_acceptor_variant", "splice_donor_variant", "protein_altering_variant" + ) + if (is.null(variant_table) || nrow(variant_table) == 0) { + return(list(n_nonsyn = NA_integer_, tmb = NA_real_, denominator_mb = denominator_mb)) + } + is_nonsyn = vapply(variant_table$consequence, function(csq) { + if (is.na(csq) || csq == "") return(FALSE) + any(trimws(unlist(strsplit(csq, ","))) %in% nonsyn_terms) + }, logical(1)) + n_nonsyn = sum(is_nonsyn, na.rm = TRUE) + list( + n_nonsyn = n_nonsyn, + tmb = round(n_nonsyn / denominator_mb, 2), + denominator_mb = denominator_mb + ) +} diff --git a/assets/lrsomatic_report/README.md b/assets/lrsomatic_report/README.md new file mode 100644 index 00000000..6d5efe0d --- /dev/null +++ b/assets/lrsomatic_report/README.md @@ -0,0 +1,172 @@ +# lrsomatic_report + +Standalone reporting tool for the [LRSomatic](https://github.com/nf-core/lrsomatic) Nextflow pipeline. Generates a self-contained HTML report per sample with: + +- **Summary header**: purity, ploidy, coverage, N50, variant counts +- **Circos plot**: somatic SNVs (6-class SBS colours), non-BND SVs, ASCAT copy number, translocation links +- **Interactive variant table**: VEP-annotated somatic small variants, optionally filtered to a gene panel, with VAF, depth and phasing +- **Interactive SV table**: Severus structural variants annotated with gene overlaps, sharing the same gene-panel filter +- **Phasing**: per-chromosome WhatsHap statistics (germline) +- **QC details**: mosdepth coverage, samtools flagstat, cramino read stats + +## Quick start + +```bash +S=/path/to/sample-dir + +Rscript bin/render_report.R \ + --sample-dir $S \ + --sample-id SAMPLE_ID \ + --sex male \ + --reference auto # auto-detects t2t vs hg38 from VCF headers +``` + +The output file `SAMPLE_ID_report.html` will be written to the current directory. + +Matched and tumour-only runs take the same command: the run mode and every input file +are discovered from the sample directory. + +Tables render unfiltered. Add `--gene-panel lymphoid` (or a path to your own TSV) to have a +panel selected when the report opens — see [Gene panels](#gene-panels). + +## All options + +``` +--sample-dir Path to the sample output directory (required) +--sample-id Sample identifier (default: directory name) +--reference t2t | hg38 | auto (default: auto) +--sex male | female | XY | XX (required) +--gene-panel none | builtin panel name (e.g. lymphoid) | path to a custom TSV + (default: none — tables render unfiltered) +--output Output HTML path (default: _report.html in current dir) +--title Report title +``` + +> **Changed in v1.1.0:** +> - `--mode` and `--somatic-vcf` were removed. Run mode is derived from whether normal-side +> QC is present, and the VCF supplying VAF is now discovered (see below), so neither needs +> to be declared. Scripts passing them will fail on an unknown option. +> - `--gene-panel` now defaults to `none` instead of `lymphoid`: reports open unfiltered +> unless a panel is asked for. Pass `--gene-panel lymphoid` to restore the old default. + +## Gene panels + +Reports are **unfiltered by default**. `--gene-panel` only chooses which panel is selected when +the report opens; the rendered HTML always contains every variant and every builtin panel, so a +reader can switch panels (or paste a custom gene list) in the browser without re-rendering. + +Built-in panels live in `assets/gene_lists/`. Each is a TSV with a `gene` column (HGNC symbols). + +| Panel | Description | +|---|---| +| `lymphoid` | ~70 recurrently mutated genes in B-cell lymphomas (DLBCL, FL, CLL, MCL, BL, MALT) | + +```bash +--gene-panel lymphoid # open with the builtin lymphoid panel applied +--gene-panel /path/to/my_genes.tsv # must have a 'gene' column or be a single-column file +``` + +A `--gene-panel` value that is neither `none`, a builtin name, nor an existing file is an error — +a typo will not silently produce an unfiltered report. + +## Expected input layout + +The `--sample-dir` must be the root of a single-sample LRSomatic output. Files are discovered +**recursively** by their distinctive filename suffix, so they can be nested in any directory +structure underneath it — for example: + +``` +/ +├── *_SOMATIC_VEP.vcf.gz VEP-annotated somatic small variants +├── variants/phased/somatic_smallvariants.vcf.gz VAF / depth / phasing source +├── severus_somatic.vcf.gz Severus SV calls +├── *_SV_VEP.vcf.gz VEP-annotated SVs +├── *.segments_raw.txt, *.purityploidy.txt ASCAT +├── *.mosdepth.summary.txt, *.mosdepth.global.dist.txt mosdepth (tumour) +├── *_cramino.txt, *.flagstat, *.stats cramino / samtools (tumour) +├── qc/whatshap_stats/*_whatshap_stats.tsv phasing statistics (germline) +├── wakhan/ Wakhan copy-number solutions +└── **/normal/** same QC file set, normal side + (matched mode; e.g. qc/normal/) +``` + +Normal-side QC is picked up from any `normal/` directory in the tree, wherever the pipeline +nests it, and is also what determines the run mode. + +**Small variants come from the VEP annotation only.** `*_SOMATIC_VEP.vcf.gz` defines the +variant set; VAF, depth, genotype and phase set are joined from the VCF that VEP annotated — +`variants/phased/somatic_smallvariants.vcf.gz`. Runs predating `variants/phased/` fall back to +`variants/clairs{,to}/somatic.vcf.gz` (then any non-germline VCF in those directories), which +yields VAF and depth but no phase set. If none is found the table still renders, without +those columns. + +VEP writes indels at a different position and sometimes in a different allele notation than +the VCF it was given, so the join is made on a normalised key — see `variant_key()` in +`R/parse_smallvariants.R`. + +`*_SOMATIC_VEP.vcf.gz` ships in two formats. Usually it is VEP *default text output* despite +the `.vcf.gz` name. If VEP was run with `--vcf` it is a genuine VCF with a `CSQ` field, and +may be a *merged* multi-caller VCF carrying germline calls (DeepVariant, Clair3) alongside +somatic ones, tagged in `INFO/CALLER`; the report then keeps only `PASS` records from a +somatic caller (ClairS, ClairS-TO, DeepSomatic). Both formats are detected automatically. + +Missing files are handled gracefully: the corresponding report section shows a "not available" notice. + +### Not covered: methylation + +There is no methylation section. The only methylation output the pipeline publishes is +`methylation//modkit_pileup/.bed.gz` — measured at 38–42 GB gzipped per sample, +unfiltered and with no tabix index, which cannot be read at render time. + +`modkit pileup` already runs with `--bgzf`, so emitting a `tabix -p bed` index next to the +`.bed.gz` would be enough to unblock this: region queries on an indexed pileup measured +~0.16 s per Mb, making a binned genome-wide profile or per-locus lookup practical. + +## Supported references + +| `--reference` | Cytobands source | chr1 length | +|---|---|---| +| `t2t` | CHM13v2.0 | 248,387,328 bp | +| `hg38` | GRCh38 (UCSC) | 248,956,422 bp | + +Auto-detection reads `##contig` lines from the VEP somatic VCF. + +## R package requirements + +Install in your R environment if missing: + +```r +install.packages(c("data.table", "dplyr", "tidyr", "DT", "htmltools", + "optparse", "quarto", "yaml", "ggplot2", "svglite")) +BiocManager::install(c("circlize", "ComplexHeatmap", "GenomicRanges")) +# paletteer, prismatic are optional (not required by this version) +``` + +Tested with R 4.4.1 and Quarto 1.5.57. + +## Repository structure + +``` +lrsomatic_report/ +├── bin/render_report.R CLI entrypoint +├── R/ +│ ├── utils.R Shared helpers (gene panel, Extra-field parser) +│ ├── references.R Cytoband + chrom-length loading, reference auto-detection +│ ├── locate_outputs.R Discover per-tool output files in a sample directory +│ ├── parse_smallvariants.R VEP text + raw caller VCF parsers; build variant table +│ ├── parse_severus.R Severus VCF + gene TSV parsers; build SV table +│ ├── parse_ascat.R ASCAT segments + purity/ploidy parsers +│ ├── parse_qc.R Mosdepth, cramino, flagstat parsers +│ └── circos.R draw_circos() — generates the circos SVG +├── templates/per_sample.qmd Quarto template (HTML report) +├── assets/ +│ ├── references/{t2t,hg38}/ Cytobands + chrom lengths (bundled, no network needed) +│ └── gene_lists/ lymphoid.tsv + README +└── tests/ Unit tests (testthat) +``` + +## Roadmap + +- **v2**: Cohort report (oncoprint, recurrence tables across multiple samples) +- **v2**: Nextflow module wrapping this CLI as a final pipeline step +- **v2**: Wakhan haplotype-resolved copy-number integration diff --git a/assets/lrsomatic_report/VENDORED.md b/assets/lrsomatic_report/VENDORED.md new file mode 100644 index 00000000..e81ee1d7 --- /dev/null +++ b/assets/lrsomatic_report/VENDORED.md @@ -0,0 +1,57 @@ +# Vendored: lrsomatic_report + +This directory is a **vendored copy** of the standalone report tool, not a git submodule. +Do not edit it here — fix upstream, tag a release, and re-sync. + +| | | +|---|---| +| Upstream | | +| Release | `v1.1.0` (`9d660a77d5f23f92e1f7ff34f85da7956f445009`) | +| Vendored commit | `d17a636aeb3f79462b7f58db9102f4030941195b` (`main`) | +| License | MIT (see `LICENSE`) | + +The vendored commit is two chore commits ahead of the `v1.1.0` tag. Neither changes +behaviour: `b4cc7620` scrubs real sample identifiers out of the README and the +`--sample-id` help string, `d17a636a` drops a development helper script. Vendoring the +tag itself would publish those identifiers in this repository. + +## Why vendored rather than a submodule + +`nextflow run IntGenomicsLab/lrsomatic` clones the pipeline repository but does **not** +fetch git submodules, so a gitlink here would be an empty directory for every user who +did not hand-clone with `--recurse-submodules` — and for CI, whose checkout steps do not +pass `submodules: recursive`. Real tracked files work for both. + +The tool's *dependencies* (R, Quarto and its R packages) are handled separately, by the +Wave multi-package container declared in `modules/local/lrsomaticreport/main.nf` and +built from that module's `environment.yml`. + +## What is included + +Only what `bin/render_report.R` needs at run time: + +``` +bin/ R/ templates/ assets/ LICENSE README.md +``` + +Upstream `docs/`, `tests/` and `recipe/` are deliberately excluded — the same set marked +`export-ignore` in the upstream `.gitattributes`. + +## Re-syncing on the next upstream release + +```bash +TAG=v1.2.0 +git clone --depth 1 --branch "$TAG" https://github.com/ljwharbers/lrsomatic_report.git /tmp/lrr +rm -rf assets/lrsomatic_report/{bin,R,templates,assets,LICENSE,README.md} +cp -a /tmp/lrr/{bin,R,templates,assets,LICENSE,README.md} assets/lrsomatic_report/ +# then update the table above, and: +# - modules/local/lrsomaticreport/environment.yml if upstream recipe/meta.yaml gained a dependency +# - modules/local/lrsomaticreport/main.nf container digest + the hard-coded version topic +# - modules/local/lrsomaticreport/meta.yml the same version string +``` + +Rebuild the container after any `environment.yml` change so the image and the file agree: + +```bash +wave --conda-file modules/local/lrsomaticreport/environment.yml --freeze --await +``` diff --git a/assets/lrsomatic_report/assets/gene_lists/README.md b/assets/lrsomatic_report/assets/gene_lists/README.md new file mode 100644 index 00000000..8a0714f7 --- /dev/null +++ b/assets/lrsomatic_report/assets/gene_lists/README.md @@ -0,0 +1,20 @@ +# Gene Panel Lists + +Each file is a TSV with a required `gene` column (HGNC symbol) and optional metadata columns (`panel`, `notes`). + +To supply a custom panel at render time: + +```bash +Rscript bin/render_report.R \ + --sample-dir /path/to/sample \ + --sample-id MySample \ + --gene-panel /path/to/my_genes.tsv +``` + +The minimal format of a custom panel file is one gene symbol per line (no header needed if there is only one column, but a TSV with a `gene` header is preferred). + +## Bundled panels + +| File | Contents | +|---|---| +| `lymphoid.tsv` | ~70 recurrently mutated genes in B-cell lymphomas (DLBCL, FL, MCL, CLL, BL, MALT) | diff --git a/assets/lrsomatic_report/assets/gene_lists/lymphoid.tsv b/assets/lrsomatic_report/assets/gene_lists/lymphoid.tsv new file mode 100644 index 00000000..c0098a53 --- /dev/null +++ b/assets/lrsomatic_report/assets/gene_lists/lymphoid.tsv @@ -0,0 +1,74 @@ +gene panel notes +MYC lymphoid Proto-oncogene, BCL translocations +BCL2 lymphoid Anti-apoptotic; t(14;18) in FL/DLBCL +BCL6 lymphoid Transcription factor; t(3;14) in DLBCL +TP53 lymphoid Tumour suppressor +CDKN2A lymphoid Cell cycle regulator (p16/p14ARF) +MYD88 lymphoid TLR signaling adaptor; L265P hotspot +CD79B lymphoid BCR co-receptor signaling +EZH2 lymphoid Histone methyltransferase; Y641/A677/A687 hotspots +KMT2D lymphoid Histone methyltransferase (MLL4) +CREBBP lymphoid Acetyltransferase; loss-of-function in FL/DLBCL +EP300 lymphoid Acetyltransferase +CARD11 lymphoid NF-kB signaling scaffold +TNFAIP3 lymphoid A20; NF-kB negative regulator +B2M lymphoid HLA class I; immune evasion +CD58 lymphoid Immune evasion +HLA-A lymphoid Immune evasion +HLA-B lymphoid Immune evasion +HLA-C lymphoid Immune evasion +FOXO1 lymphoid Transcription factor; BCL6 target +GNA13 lymphoid G-protein; germinal center exit +RB1 lymphoid Tumour suppressor; cell cycle +CCND1 lymphoid Cyclin D1; t(11;14) in MCL +CCND3 lymphoid Cyclin D3; DLBCL hotspot +SOX11 lymphoid MCL marker +ATM lymphoid DNA damage response; CLL/MCL +SF3B1 lymphoid Splicing factor; CLL +NOTCH1 lymphoid Notch pathway; CLL +BIRC3 lymphoid IAP; NF-kB; CLL +BTK lymphoid BCR kinase; ibrutinib target +DTX1 lymphoid Notch pathway effector +SGK1 lymphoid Kinase; germinal center +PIM1 lymphoid Kinase; BCR/TLR signaling +PCLO lymphoid Pepe-scaffold; recurrently mutated +FAT1 lymphoid Tumour suppressor; Hippo pathway +FAT3 lymphoid Tumour suppressor +SPEN lymphoid Transcriptional repressor +MEF2B lymphoid Transcription factor; FL/DLBCL +KLHL6 lymphoid BCR signaling ubiquitin adaptor +SMARCA4 lymphoid Chromatin remodeling +IRF4 lymphoid Transcription factor; MYC target +ARID1A lymphoid SWI/SNF chromatin remodeling +HIST1H1E lymphoid Linker histone H1; DLBCL +DUSP2 lymphoid MAP kinase phosphatase +BTG1 lymphoid Anti-proliferative; DLBCL +CIITA lymphoid MHC class II transactivator +CXCR4 lymphoid Chemokine receptor; CLL/WM +RHOA lymphoid Rho GTPase; AITL G17V hotspot +SYK lymphoid BCR/FcR signaling kinase +PRKCB lymphoid Protein kinase C beta +KRAS lymphoid RAS signaling +NRAS lymphoid RAS signaling +BRAF lymphoid MAPK kinase; HCL V600E +MAP2K1 lymphoid ERK signaling +PIK3CA lymphoid PI3K catalytic subunit alpha +PIK3CD lymphoid PI3K catalytic subunit delta +PTEN lymphoid PI3K pathway tumour suppressor +ID3 lymphoid BL; inhibits E-proteins/TCF3 +TCF3 lymphoid BL; E-box transcription factor +BCL11A lymphoid Transcription factor; lymphoma +SAMHD1 lymphoid dNTP hydrolase; CLL +RPS15 lymphoid Ribosomal; CLL +FBXW7 lymphoid Ubiquitin E3 ligase +TBL1XR1 lymphoid Transcription corepressor +DDX3X lymphoid RNA helicase; Burkitt/DLBCL +SETD2 lymphoid H3K36 methyltransferase +GEF1 lymphoid Guanine nucleotide exchange +LYN lymphoid Src family kinase; BCR signaling +CD19 lymphoid BCR coreceptor; therapy target +CD20 lymphoid Rituximab target (MS4A1) +MS4A1 lymphoid CD20; B-cell surface marker +TNFRSF14 lymphoid Immune checkpoint; FL +BCL10 lymphoid CBM complex; NF-kB +MALT1 lymphoid Paracaspase; NF-kB; MALT lymphoma diff --git a/assets/lrsomatic_report/assets/references/hg38/chrom_lengths.tsv b/assets/lrsomatic_report/assets/references/hg38/chrom_lengths.tsv new file mode 100644 index 00000000..bbd5557d --- /dev/null +++ b/assets/lrsomatic_report/assets/references/hg38/chrom_lengths.tsv @@ -0,0 +1,25 @@ +chr1 248956422 +chr2 242193529 +chr3 198295559 +chr4 190214555 +chr5 181538259 +chr6 170805979 +chr7 159345973 +chr8 145138636 +chr9 138394717 +chr10 133797422 +chr11 135086622 +chr12 133275309 +chr13 114364328 +chr14 107043718 +chr15 101991189 +chr16 90338345 +chr17 83257441 +chr18 80373285 +chr19 58617616 +chr20 64444167 +chr21 46709983 +chr22 50818468 +chrX 156040895 +chrY 57227415 +chrM 16569 diff --git a/assets/lrsomatic_report/assets/references/hg38/cytobands.tsv b/assets/lrsomatic_report/assets/references/hg38/cytobands.tsv new file mode 100644 index 00000000..0dc94f07 --- /dev/null +++ b/assets/lrsomatic_report/assets/references/hg38/cytobands.tsv @@ -0,0 +1,1549 @@ +chr1 0 2300000 p36.33 gneg +chr1 2300000 5300000 p36.32 gpos25 +chr1 5300000 7100000 p36.31 gneg +chr1 7100000 9100000 p36.23 gpos25 +chr1 9100000 12500000 p36.22 gneg +chr1 12500000 15900000 p36.21 gpos50 +chr1 15900000 20100000 p36.13 gneg +chr1 20100000 23600000 p36.12 gpos25 +chr1 23600000 27600000 p36.11 gneg +chr1 27600000 29900000 p35.3 gpos25 +chr1 29900000 32300000 p35.2 gneg +chr1 32300000 34300000 p35.1 gpos25 +chr1 34300000 39600000 p34.3 gneg +chr1 39600000 43700000 p34.2 gpos25 +chr1 43700000 46300000 p34.1 gneg +chr1 46300000 50200000 p33 gpos75 +chr1 50200000 55600000 p32.3 gneg +chr1 55600000 58500000 p32.2 gpos50 +chr1 58500000 60800000 p32.1 gneg +chr1 60800000 68500000 p31.3 gpos50 +chr1 68500000 69300000 p31.2 gneg +chr1 69300000 84400000 p31.1 gpos100 +chr1 84400000 87900000 p22.3 gneg +chr1 87900000 91500000 p22.2 gpos75 +chr1 91500000 94300000 p22.1 gneg +chr1 94300000 99300000 p21.3 gpos75 +chr1 99300000 101800000 p21.2 gneg +chr1 101800000 106700000 p21.1 gpos100 +chr1 106700000 111200000 p13.3 gneg +chr1 111200000 115500000 p13.2 gpos50 +chr1 115500000 117200000 p13.1 gneg +chr1 117200000 120400000 p12 gpos50 +chr1 120400000 121700000 p11.2 gneg +chr1 121700000 123400000 p11.1 acen +chr1 123400000 125100000 q11 acen +chr1 125100000 143200000 q12 gvar +chr1 143200000 147500000 q21.1 gneg +chr1 147500000 150600000 q21.2 gpos50 +chr1 150600000 155100000 q21.3 gneg +chr1 155100000 156600000 q22 gpos50 +chr1 156600000 159100000 q23.1 gneg +chr1 159100000 160500000 q23.2 gpos50 +chr1 160500000 165500000 q23.3 gneg +chr1 165500000 167200000 q24.1 gpos50 +chr1 167200000 170900000 q24.2 gneg +chr1 170900000 173000000 q24.3 gpos75 +chr1 173000000 176100000 q25.1 gneg +chr1 176100000 180300000 q25.2 gpos50 +chr1 180300000 185800000 q25.3 gneg +chr1 185800000 190800000 q31.1 gpos100 +chr1 190800000 193800000 q31.2 gneg +chr1 193800000 198700000 q31.3 gpos100 +chr1 198700000 207100000 q32.1 gneg +chr1 207100000 211300000 q32.2 gpos25 +chr1 211300000 214400000 q32.3 gneg +chr1 214400000 223900000 q41 gpos100 +chr1 223900000 224400000 q42.11 gneg +chr1 224400000 226800000 q42.12 gpos25 +chr1 226800000 230500000 q42.13 gneg +chr1 230500000 234600000 q42.2 gpos50 +chr1 234600000 236400000 q42.3 gneg +chr1 236400000 243500000 q43 gpos75 +chr1 243500000 248956422 q44 gneg +chr10 0 3000000 p15.3 gneg +chr10 3000000 3800000 p15.2 gpos25 +chr10 3800000 6600000 p15.1 gneg +chr10 6600000 12200000 p14 gpos75 +chr10 12200000 17300000 p13 gneg +chr10 17300000 18300000 p12.33 gpos75 +chr10 18300000 18400000 p12.32 gneg +chr10 18400000 22300000 p12.31 gpos75 +chr10 22300000 24300000 p12.2 gneg +chr10 24300000 29300000 p12.1 gpos50 +chr10 29300000 31100000 p11.23 gneg +chr10 31100000 34200000 p11.22 gpos25 +chr10 34200000 38000000 p11.21 gneg +chr10 38000000 39800000 p11.1 acen +chr10 39800000 41600000 q11.1 acen +chr10 41600000 45500000 q11.21 gneg +chr10 45500000 48600000 q11.22 gpos25 +chr10 48600000 51100000 q11.23 gneg +chr10 51100000 59400000 q21.1 gpos100 +chr10 59400000 62800000 q21.2 gneg +chr10 62800000 68800000 q21.3 gpos100 +chr10 68800000 73100000 q22.1 gneg +chr10 73100000 75900000 q22.2 gpos50 +chr10 75900000 80300000 q22.3 gneg +chr10 80300000 86100000 q23.1 gpos100 +chr10 86100000 87700000 q23.2 gneg +chr10 87700000 91100000 q23.31 gpos75 +chr10 91100000 92300000 q23.32 gneg +chr10 92300000 95300000 q23.33 gpos50 +chr10 95300000 97500000 q24.1 gneg +chr10 97500000 100100000 q24.2 gpos50 +chr10 100100000 101200000 q24.31 gneg +chr10 101200000 103100000 q24.32 gpos25 +chr10 103100000 104000000 q24.33 gneg +chr10 104000000 110100000 q25.1 gpos100 +chr10 110100000 113100000 q25.2 gneg +chr10 113100000 117300000 q25.3 gpos75 +chr10 117300000 119900000 q26.11 gneg +chr10 119900000 121400000 q26.12 gpos50 +chr10 121400000 125700000 q26.13 gneg +chr10 125700000 128800000 q26.2 gpos50 +chr10 128800000 133797422 q26.3 gneg +chr10_GL383545v1_alt 0 179254 gneg +chr10_GL383546v1_alt 0 309802 gneg +chr10_KI270824v1_alt 0 181496 gneg +chr10_KI270825v1_alt 0 188315 gneg +chr11 0 2800000 p15.5 gneg +chr11 2800000 11700000 p15.4 gpos50 +chr11 11700000 13800000 p15.3 gneg +chr11 13800000 16900000 p15.2 gpos50 +chr11 16900000 22000000 p15.1 gneg +chr11 22000000 26200000 p14.3 gpos100 +chr11 26200000 27200000 p14.2 gneg +chr11 27200000 31000000 p14.1 gpos75 +chr11 31000000 36400000 p13 gneg +chr11 36400000 43400000 p12 gpos100 +chr11 43400000 48800000 p11.2 gneg +chr11 48800000 51000000 p11.12 gpos75 +chr11 51000000 53400000 p11.11 acen +chr11 53400000 55800000 q11 acen +chr11 55800000 60100000 q12.1 gpos75 +chr11 60100000 61900000 q12.2 gneg +chr11 61900000 63600000 q12.3 gpos25 +chr11 63600000 66100000 q13.1 gneg +chr11 66100000 68700000 q13.2 gpos25 +chr11 68700000 70500000 q13.3 gneg +chr11 70500000 75500000 q13.4 gpos50 +chr11 75500000 77400000 q13.5 gneg +chr11 77400000 85900000 q14.1 gpos100 +chr11 85900000 88600000 q14.2 gneg +chr11 88600000 93000000 q14.3 gpos100 +chr11 93000000 97400000 q21 gneg +chr11 97400000 102300000 q22.1 gpos100 +chr11 102300000 103000000 q22.2 gneg +chr11 103000000 110600000 q22.3 gpos100 +chr11 110600000 112700000 q23.1 gneg +chr11 112700000 114600000 q23.2 gpos50 +chr11 114600000 121300000 q23.3 gneg +chr11 121300000 124000000 q24.1 gpos50 +chr11 124000000 127900000 q24.2 gneg +chr11 127900000 130900000 q24.3 gpos50 +chr11 130900000 135086622 q25 gneg +chr11_GL383547v1_alt 0 154407 gneg +chr11_JH159136v1_alt 0 200998 gneg +chr11_JH159137v1_alt 0 191409 gneg +chr11_KI270721v1_random 0 100316 gneg +chr11_KI270826v1_alt 0 186169 gneg +chr11_KI270827v1_alt 0 67707 gneg +chr11_KI270829v1_alt 0 204059 gneg +chr11_KI270830v1_alt 0 177092 gneg +chr11_KI270831v1_alt 0 296895 gneg +chr11_KI270832v1_alt 0 210133 gneg +chr11_KI270902v1_alt 0 106711 gneg +chr11_KI270903v1_alt 0 214625 gneg +chr11_KI270927v1_alt 0 218612 gneg +chr12 0 3200000 p13.33 gneg +chr12 3200000 5300000 p13.32 gpos25 +chr12 5300000 10000000 p13.31 gneg +chr12 10000000 12600000 p13.2 gpos75 +chr12 12600000 14600000 p13.1 gneg +chr12 14600000 19800000 p12.3 gpos100 +chr12 19800000 21100000 p12.2 gneg +chr12 21100000 26300000 p12.1 gpos100 +chr12 26300000 27600000 p11.23 gneg +chr12 27600000 30500000 p11.22 gpos50 +chr12 30500000 33200000 p11.21 gneg +chr12 33200000 35500000 p11.1 acen +chr12 35500000 37800000 q11 acen +chr12 37800000 46000000 q12 gpos100 +chr12 46000000 48700000 q13.11 gneg +chr12 48700000 51100000 q13.12 gpos25 +chr12 51100000 54500000 q13.13 gneg +chr12 54500000 56200000 q13.2 gpos25 +chr12 56200000 57700000 q13.3 gneg +chr12 57700000 62700000 q14.1 gpos75 +chr12 62700000 64700000 q14.2 gneg +chr12 64700000 67300000 q14.3 gpos50 +chr12 67300000 71100000 q15 gneg +chr12 71100000 75300000 q21.1 gpos75 +chr12 75300000 79900000 q21.2 gneg +chr12 79900000 86300000 q21.31 gpos100 +chr12 86300000 88600000 q21.32 gneg +chr12 88600000 92200000 q21.33 gpos100 +chr12 92200000 95800000 q22 gneg +chr12 95800000 101200000 q23.1 gpos75 +chr12 101200000 103500000 q23.2 gneg +chr12 103500000 108600000 q23.3 gpos50 +chr12 108600000 111300000 q24.11 gneg +chr12 111300000 111900000 q24.12 gpos25 +chr12 111900000 113900000 q24.13 gneg +chr12 113900000 116400000 q24.21 gpos50 +chr12 116400000 117700000 q24.22 gneg +chr12 117700000 120300000 q24.23 gpos50 +chr12 120300000 125400000 q24.31 gneg +chr12 125400000 128700000 q24.32 gpos50 +chr12 128700000 133275309 q24.33 gneg +chr12_GL383549v1_alt 0 120804 gneg +chr12_GL383550v2_alt 0 169178 gneg +chr12_GL383551v1_alt 0 184319 gneg +chr12_GL383552v1_alt 0 138655 gneg +chr12_GL383553v2_alt 0 152874 gneg +chr12_GL877875v1_alt 0 167313 gneg +chr12_GL877876v1_alt 0 408271 gneg +chr12_KI270833v1_alt 0 76061 gneg +chr12_KI270834v1_alt 0 119498 gneg +chr12_KI270835v1_alt 0 238139 gneg +chr12_KI270836v1_alt 0 56134 gneg +chr12_KI270837v1_alt 0 40090 gneg +chr12_KI270904v1_alt 0 572349 gneg +chr13 0 4600000 p13 gvar +chr13 4600000 10100000 p12 stalk +chr13 10100000 16500000 p11.2 gvar +chr13 16500000 17700000 p11.1 acen +chr13 17700000 18900000 q11 acen +chr13 18900000 22600000 q12.11 gneg +chr13 22600000 24900000 q12.12 gpos25 +chr13 24900000 27200000 q12.13 gneg +chr13 27200000 28300000 q12.2 gpos25 +chr13 28300000 31600000 q12.3 gneg +chr13 31600000 33400000 q13.1 gpos50 +chr13 33400000 34900000 q13.2 gneg +chr13 34900000 39500000 q13.3 gpos75 +chr13 39500000 44600000 q14.11 gneg +chr13 44600000 45200000 q14.12 gpos25 +chr13 45200000 46700000 q14.13 gneg +chr13 46700000 50300000 q14.2 gpos50 +chr13 50300000 54700000 q14.3 gneg +chr13 54700000 59000000 q21.1 gpos100 +chr13 59000000 61800000 q21.2 gneg +chr13 61800000 65200000 q21.31 gpos75 +chr13 65200000 68100000 q21.32 gneg +chr13 68100000 72800000 q21.33 gpos100 +chr13 72800000 74900000 q22.1 gneg +chr13 74900000 76700000 q22.2 gpos50 +chr13 76700000 78500000 q22.3 gneg +chr13 78500000 87100000 q31.1 gpos100 +chr13 87100000 89400000 q31.2 gneg +chr13 89400000 94400000 q31.3 gpos100 +chr13 94400000 97500000 q32.1 gneg +chr13 97500000 98700000 q32.2 gpos25 +chr13 98700000 101100000 q32.3 gneg +chr13 101100000 104200000 q33.1 gpos100 +chr13 104200000 106400000 q33.2 gneg +chr13 106400000 109600000 q33.3 gpos100 +chr13 109600000 114364328 q34 gneg +chr13_KI270838v1_alt 0 306913 gneg +chr13_KI270839v1_alt 0 180306 gneg +chr13_KI270840v1_alt 0 191684 gneg +chr13_KI270841v1_alt 0 169134 gneg +chr13_KI270842v1_alt 0 37287 gneg +chr13_KI270843v1_alt 0 103832 gneg +chr14 0 3600000 p13 gvar +chr14 3600000 8000000 p12 stalk +chr14 8000000 16100000 p11.2 gvar +chr14 16100000 17200000 p11.1 acen +chr14 17200000 18200000 q11.1 acen +chr14 18200000 24100000 q11.2 gneg +chr14 24100000 32900000 q12 gpos100 +chr14 32900000 34800000 q13.1 gneg +chr14 34800000 36100000 q13.2 gpos50 +chr14 36100000 37400000 q13.3 gneg +chr14 37400000 43000000 q21.1 gpos100 +chr14 43000000 46700000 q21.2 gneg +chr14 46700000 50400000 q21.3 gpos100 +chr14 50400000 53600000 q22.1 gneg +chr14 53600000 55000000 q22.2 gpos25 +chr14 55000000 57600000 q22.3 gneg +chr14 57600000 61600000 q23.1 gpos75 +chr14 61600000 64300000 q23.2 gneg +chr14 64300000 67400000 q23.3 gpos50 +chr14 67400000 69800000 q24.1 gneg +chr14 69800000 73300000 q24.2 gpos50 +chr14 73300000 78800000 q24.3 gneg +chr14 78800000 83100000 q31.1 gpos100 +chr14 83100000 84400000 q31.2 gneg +chr14 84400000 89300000 q31.3 gpos100 +chr14 89300000 91400000 q32.11 gneg +chr14 91400000 94200000 q32.12 gpos25 +chr14 94200000 95800000 q32.13 gneg +chr14 95800000 100900000 q32.2 gpos50 +chr14 100900000 102700000 q32.31 gneg +chr14 102700000 103500000 q32.32 gpos50 +chr14 103500000 107043718 q32.33 gneg +chr14_GL000009v2_random 0 201709 gneg +chr14_GL000194v1_random 0 191469 gneg +chr14_GL000225v1_random 0 211173 gneg +chr14_KI270722v1_random 0 194050 gneg +chr14_KI270723v1_random 0 38115 gneg +chr14_KI270724v1_random 0 39555 gneg +chr14_KI270725v1_random 0 172810 gneg +chr14_KI270726v1_random 0 43739 gneg +chr14_KI270844v1_alt 0 322166 gneg +chr14_KI270845v1_alt 0 180703 gneg +chr14_KI270846v1_alt 0 1351393 gneg +chr14_KI270847v1_alt 0 1511111 gneg +chr15 0 4200000 p13 gvar +chr15 4200000 9700000 p12 stalk +chr15 9700000 17500000 p11.2 gvar +chr15 17500000 19000000 p11.1 acen +chr15 19000000 20500000 q11.1 acen +chr15 20500000 25500000 q11.2 gneg +chr15 25500000 27800000 q12 gpos50 +chr15 27800000 30000000 q13.1 gneg +chr15 30000000 30900000 q13.2 gpos50 +chr15 30900000 33400000 q13.3 gneg +chr15 33400000 39800000 q14 gpos75 +chr15 39800000 42500000 q15.1 gneg +chr15 42500000 43300000 q15.2 gpos25 +chr15 43300000 44500000 q15.3 gneg +chr15 44500000 49200000 q21.1 gpos75 +chr15 49200000 52600000 q21.2 gneg +chr15 52600000 58800000 q21.3 gpos75 +chr15 58800000 59000000 q22.1 gneg +chr15 59000000 63400000 q22.2 gpos25 +chr15 63400000 66900000 q22.31 gneg +chr15 66900000 67000000 q22.32 gpos25 +chr15 67000000 67200000 q22.33 gneg +chr15 67200000 72400000 q23 gpos25 +chr15 72400000 74900000 q24.1 gneg +chr15 74900000 76300000 q24.2 gpos25 +chr15 76300000 78000000 q24.3 gneg +chr15 78000000 81400000 q25.1 gpos50 +chr15 81400000 84700000 q25.2 gneg +chr15 84700000 88500000 q25.3 gpos50 +chr15 88500000 93800000 q26.1 gneg +chr15 93800000 98000000 q26.2 gpos50 +chr15 98000000 101991189 q26.3 gneg +chr15_GL383554v1_alt 0 296527 gneg +chr15_GL383555v2_alt 0 388773 gneg +chr15_KI270727v1_random 0 448248 gneg +chr15_KI270848v1_alt 0 327382 gneg +chr15_KI270849v1_alt 0 244917 gneg +chr15_KI270850v1_alt 0 430880 gneg +chr15_KI270851v1_alt 0 263054 gneg +chr15_KI270852v1_alt 0 478999 gneg +chr15_KI270905v1_alt 0 5161414 gneg +chr15_KI270906v1_alt 0 196384 gneg +chr16 0 7800000 p13.3 gneg +chr16 7800000 10400000 p13.2 gpos50 +chr16 10400000 12500000 p13.13 gneg +chr16 12500000 14700000 p13.12 gpos50 +chr16 14700000 16700000 p13.11 gneg +chr16 16700000 21200000 p12.3 gpos50 +chr16 21200000 24200000 p12.2 gneg +chr16 24200000 28500000 p12.1 gpos50 +chr16 28500000 35300000 p11.2 gneg +chr16 35300000 36800000 p11.1 acen +chr16 36800000 38400000 q11.1 acen +chr16 38400000 47000000 q11.2 gvar +chr16 47000000 52600000 q12.1 gneg +chr16 52600000 56000000 q12.2 gpos50 +chr16 56000000 57300000 q13 gneg +chr16 57300000 66600000 q21 gpos100 +chr16 66600000 70800000 q22.1 gneg +chr16 70800000 72800000 q22.2 gpos50 +chr16 72800000 74100000 q22.3 gneg +chr16 74100000 79200000 q23.1 gpos75 +chr16 79200000 81600000 q23.2 gneg +chr16 81600000 84100000 q23.3 gpos50 +chr16 84100000 87000000 q24.1 gneg +chr16 87000000 88700000 q24.2 gpos25 +chr16 88700000 90338345 q24.3 gneg +chr16_GL383556v1_alt 0 192462 gneg +chr16_GL383557v1_alt 0 89672 gneg +chr16_KI270728v1_random 0 1872759 gneg +chr16_KI270853v1_alt 0 2659700 gneg +chr16_KI270854v1_alt 0 134193 gneg +chr16_KI270855v1_alt 0 232857 gneg +chr16_KI270856v1_alt 0 63982 gneg +chr17 0 3400000 p13.3 gneg +chr17 3400000 6500000 p13.2 gpos50 +chr17 6500000 10800000 p13.1 gneg +chr17 10800000 16100000 p12 gpos75 +chr17 16100000 22700000 p11.2 gneg +chr17 22700000 25100000 p11.1 acen +chr17 25100000 27400000 q11.1 acen +chr17 27400000 33500000 q11.2 gneg +chr17 33500000 39800000 q12 gpos50 +chr17 39800000 40200000 q21.1 gneg +chr17 40200000 42800000 q21.2 gpos25 +chr17 42800000 46800000 q21.31 gneg +chr17 46800000 49300000 q21.32 gpos25 +chr17 49300000 52100000 q21.33 gneg +chr17 52100000 59500000 q22 gpos75 +chr17 59500000 60200000 q23.1 gneg +chr17 60200000 63100000 q23.2 gpos75 +chr17 63100000 64600000 q23.3 gneg +chr17 64600000 66200000 q24.1 gpos50 +chr17 66200000 69100000 q24.2 gneg +chr17 69100000 72900000 q24.3 gpos75 +chr17 72900000 76800000 q25.1 gneg +chr17 76800000 77200000 q25.2 gpos25 +chr17 77200000 83257441 q25.3 gneg +chr17_GL000205v2_random 0 185591 gneg +chr17_GL000258v2_alt 0 1821992 gneg +chr17_GL383563v3_alt 0 375691 gneg +chr17_GL383564v2_alt 0 133151 gneg +chr17_GL383565v1_alt 0 223995 gneg +chr17_GL383566v1_alt 0 90219 gneg +chr17_JH159146v1_alt 0 278131 gneg +chr17_JH159147v1_alt 0 70345 gneg +chr17_JH159148v1_alt 0 88070 gneg +chr17_KI270729v1_random 0 280839 gneg +chr17_KI270730v1_random 0 112551 gneg +chr17_KI270857v1_alt 0 2877074 gneg +chr17_KI270858v1_alt 0 235827 gneg +chr17_KI270859v1_alt 0 108763 gneg +chr17_KI270860v1_alt 0 178921 gneg +chr17_KI270861v1_alt 0 196688 gneg +chr17_KI270862v1_alt 0 391357 gneg +chr17_KI270907v1_alt 0 137721 gneg +chr17_KI270908v1_alt 0 1423190 gneg +chr17_KI270909v1_alt 0 325800 gneg +chr17_KI270910v1_alt 0 157099 gneg +chr18 0 2900000 p11.32 gneg +chr18 2900000 7200000 p11.31 gpos50 +chr18 7200000 8500000 p11.23 gneg +chr18 8500000 10900000 p11.22 gpos25 +chr18 10900000 15400000 p11.21 gneg +chr18 15400000 18500000 p11.1 acen +chr18 18500000 21500000 q11.1 acen +chr18 21500000 27500000 q11.2 gneg +chr18 27500000 35100000 q12.1 gpos100 +chr18 35100000 39500000 q12.2 gneg +chr18 39500000 45900000 q12.3 gpos75 +chr18 45900000 50700000 q21.1 gneg +chr18 50700000 56200000 q21.2 gpos75 +chr18 56200000 58600000 q21.31 gneg +chr18 58600000 61300000 q21.32 gpos50 +chr18 61300000 63900000 q21.33 gneg +chr18 63900000 69100000 q22.1 gpos100 +chr18 69100000 71000000 q22.2 gneg +chr18 71000000 75400000 q22.3 gpos25 +chr18 75400000 80373285 q23 gneg +chr18_GL383567v1_alt 0 289831 gneg +chr18_GL383568v1_alt 0 104552 gneg +chr18_GL383569v1_alt 0 167950 gneg +chr18_GL383570v1_alt 0 164789 gneg +chr18_GL383571v1_alt 0 198278 gneg +chr18_GL383572v1_alt 0 159547 gneg +chr18_KI270863v1_alt 0 167999 gneg +chr18_KI270864v1_alt 0 111737 gneg +chr18_KI270911v1_alt 0 157710 gneg +chr18_KI270912v1_alt 0 174061 gneg +chr19 0 6900000 p13.3 gneg +chr19 6900000 12600000 p13.2 gpos25 +chr19 12600000 13800000 p13.13 gneg +chr19 13800000 16100000 p13.12 gpos25 +chr19 16100000 19900000 p13.11 gneg +chr19 19900000 24200000 p12 gvar +chr19 24200000 26200000 p11 acen +chr19 26200000 28100000 q11 acen +chr19 28100000 31900000 q12 gvar +chr19 31900000 35100000 q13.11 gneg +chr19 35100000 37800000 q13.12 gpos25 +chr19 37800000 38200000 q13.13 gneg +chr19 38200000 42900000 q13.2 gpos25 +chr19 42900000 44700000 q13.31 gneg +chr19 44700000 47500000 q13.32 gpos25 +chr19 47500000 50900000 q13.33 gneg +chr19 50900000 53100000 q13.41 gpos25 +chr19 53100000 55800000 q13.42 gneg +chr19 55800000 58617616 q13.43 gpos25 +chr19_GL000209v2_alt 0 177381 gneg +chr19_GL383573v1_alt 0 385657 gneg +chr19_GL383574v1_alt 0 155864 gneg +chr19_GL383575v2_alt 0 170222 gneg +chr19_GL383576v1_alt 0 188024 gneg +chr19_GL949746v1_alt 0 987716 gneg +chr19_GL949747v2_alt 0 729520 gneg +chr19_GL949748v2_alt 0 1064304 gneg +chr19_GL949749v2_alt 0 1091841 gneg +chr19_GL949750v2_alt 0 1066390 gneg +chr19_GL949751v2_alt 0 1002683 gneg +chr19_GL949752v1_alt 0 987100 gneg +chr19_GL949753v2_alt 0 796479 gneg +chr19_KI270865v1_alt 0 52969 gneg +chr19_KI270866v1_alt 0 43156 gneg +chr19_KI270867v1_alt 0 233762 gneg +chr19_KI270868v1_alt 0 61734 gneg +chr19_KI270882v1_alt 0 248807 gneg +chr19_KI270883v1_alt 0 170399 gneg +chr19_KI270884v1_alt 0 157053 gneg +chr19_KI270885v1_alt 0 171027 gneg +chr19_KI270886v1_alt 0 204239 gneg +chr19_KI270887v1_alt 0 209512 gneg +chr19_KI270888v1_alt 0 155532 gneg +chr19_KI270889v1_alt 0 170698 gneg +chr19_KI270890v1_alt 0 184499 gneg +chr19_KI270891v1_alt 0 170680 gneg +chr19_KI270914v1_alt 0 205194 gneg +chr19_KI270915v1_alt 0 170665 gneg +chr19_KI270916v1_alt 0 184516 gneg +chr19_KI270917v1_alt 0 190932 gneg +chr19_KI270918v1_alt 0 123111 gneg +chr19_KI270919v1_alt 0 170701 gneg +chr19_KI270920v1_alt 0 198005 gneg +chr19_KI270921v1_alt 0 282224 gneg +chr19_KI270922v1_alt 0 187935 gneg +chr19_KI270923v1_alt 0 189352 gneg +chr19_KI270929v1_alt 0 186203 gneg +chr19_KI270930v1_alt 0 200773 gneg +chr19_KI270931v1_alt 0 170148 gneg +chr19_KI270932v1_alt 0 215732 gneg +chr19_KI270933v1_alt 0 170537 gneg +chr19_KI270938v1_alt 0 1066800 gneg +chr1_GL383518v1_alt 0 182439 gneg +chr1_GL383519v1_alt 0 110268 gneg +chr1_GL383520v2_alt 0 366580 gneg +chr1_KI270706v1_random 0 175055 gneg +chr1_KI270707v1_random 0 32032 gneg +chr1_KI270708v1_random 0 127682 gneg +chr1_KI270709v1_random 0 66860 gneg +chr1_KI270710v1_random 0 40176 gneg +chr1_KI270711v1_random 0 42210 gneg +chr1_KI270712v1_random 0 176043 gneg +chr1_KI270713v1_random 0 40745 gneg +chr1_KI270714v1_random 0 41717 gneg +chr1_KI270759v1_alt 0 425601 gneg +chr1_KI270760v1_alt 0 109528 gneg +chr1_KI270761v1_alt 0 165834 gneg +chr1_KI270762v1_alt 0 354444 gneg +chr1_KI270763v1_alt 0 911658 gneg +chr1_KI270764v1_alt 0 50258 gneg +chr1_KI270765v1_alt 0 185285 gneg +chr1_KI270766v1_alt 0 256271 gneg +chr1_KI270892v1_alt 0 162212 gneg +chr2 0 4400000 p25.3 gneg +chr2 4400000 6900000 p25.2 gpos50 +chr2 6900000 12000000 p25.1 gneg +chr2 12000000 16500000 p24.3 gpos75 +chr2 16500000 19000000 p24.2 gneg +chr2 19000000 23800000 p24.1 gpos75 +chr2 23800000 27700000 p23.3 gneg +chr2 27700000 29800000 p23.2 gpos25 +chr2 29800000 31800000 p23.1 gneg +chr2 31800000 36300000 p22.3 gpos75 +chr2 36300000 38300000 p22.2 gneg +chr2 38300000 41500000 p22.1 gpos50 +chr2 41500000 47500000 p21 gneg +chr2 47500000 52600000 p16.3 gpos100 +chr2 52600000 54700000 p16.2 gneg +chr2 54700000 61000000 p16.1 gpos100 +chr2 61000000 63900000 p15 gneg +chr2 63900000 68400000 p14 gpos50 +chr2 68400000 71300000 p13.3 gneg +chr2 71300000 73300000 p13.2 gpos50 +chr2 73300000 74800000 p13.1 gneg +chr2 74800000 83100000 p12 gpos100 +chr2 83100000 91800000 p11.2 gneg +chr2 91800000 93900000 p11.1 acen +chr2 93900000 96000000 q11.1 acen +chr2 96000000 102100000 q11.2 gneg +chr2 102100000 105300000 q12.1 gpos50 +chr2 105300000 106700000 q12.2 gneg +chr2 106700000 108700000 q12.3 gpos25 +chr2 108700000 112200000 q13 gneg +chr2 112200000 118100000 q14.1 gpos50 +chr2 118100000 121600000 q14.2 gneg +chr2 121600000 129100000 q14.3 gpos50 +chr2 129100000 131700000 q21.1 gneg +chr2 131700000 134300000 q21.2 gpos25 +chr2 134300000 136100000 q21.3 gneg +chr2 136100000 141500000 q22.1 gpos100 +chr2 141500000 143400000 q22.2 gneg +chr2 143400000 147900000 q22.3 gpos100 +chr2 147900000 149000000 q23.1 gneg +chr2 149000000 149600000 q23.2 gpos25 +chr2 149600000 154000000 q23.3 gneg +chr2 154000000 158900000 q24.1 gpos75 +chr2 158900000 162900000 q24.2 gneg +chr2 162900000 168900000 q24.3 gpos75 +chr2 168900000 177100000 q31.1 gneg +chr2 177100000 179700000 q31.2 gpos50 +chr2 179700000 182100000 q31.3 gneg +chr2 182100000 188500000 q32.1 gpos75 +chr2 188500000 191100000 q32.2 gneg +chr2 191100000 196600000 q32.3 gpos75 +chr2 196600000 202500000 q33.1 gneg +chr2 202500000 204100000 q33.2 gpos50 +chr2 204100000 208200000 q33.3 gneg +chr2 208200000 214500000 q34 gpos100 +chr2 214500000 220700000 q35 gneg +chr2 220700000 224300000 q36.1 gpos75 +chr2 224300000 225200000 q36.2 gneg +chr2 225200000 230100000 q36.3 gpos100 +chr2 230100000 234700000 q37.1 gneg +chr2 234700000 236400000 q37.2 gpos50 +chr2 236400000 242193529 q37.3 gneg +chr20 0 5100000 p13 gneg +chr20 5100000 9200000 p12.3 gpos75 +chr20 9200000 12000000 p12.2 gneg +chr20 12000000 17900000 p12.1 gpos75 +chr20 17900000 21300000 p11.23 gneg +chr20 21300000 22300000 p11.22 gpos25 +chr20 22300000 25700000 p11.21 gneg +chr20 25700000 28100000 p11.1 acen +chr20 28100000 30400000 q11.1 acen +chr20 30400000 33500000 q11.21 gneg +chr20 33500000 35800000 q11.22 gpos25 +chr20 35800000 39000000 q11.23 gneg +chr20 39000000 43100000 q12 gpos75 +chr20 43100000 43500000 q13.11 gneg +chr20 43500000 47800000 q13.12 gpos25 +chr20 47800000 51200000 q13.13 gneg +chr20 51200000 56400000 q13.2 gpos75 +chr20 56400000 57800000 q13.31 gneg +chr20 57800000 59700000 q13.32 gpos50 +chr20 59700000 64444167 q13.33 gneg +chr20_GL383577v2_alt 0 128386 gneg +chr20_KI270869v1_alt 0 118774 gneg +chr20_KI270870v1_alt 0 183433 gneg +chr20_KI270871v1_alt 0 58661 gneg +chr21 0 3100000 p13 gvar +chr21 3100000 7000000 p12 stalk +chr21 7000000 10900000 p11.2 gvar +chr21 10900000 12000000 p11.1 acen +chr21 12000000 13000000 q11.1 acen +chr21 13000000 15000000 q11.2 gneg +chr21 15000000 22600000 q21.1 gpos100 +chr21 22600000 25500000 q21.2 gneg +chr21 25500000 30200000 q21.3 gpos75 +chr21 30200000 34400000 q22.11 gneg +chr21 34400000 36400000 q22.12 gpos50 +chr21 36400000 38300000 q22.13 gneg +chr21 38300000 41200000 q22.2 gpos50 +chr21 41200000 46709983 q22.3 gneg +chr21_GL383578v2_alt 0 63917 gneg +chr21_GL383579v2_alt 0 201197 gneg +chr21_GL383580v2_alt 0 74653 gneg +chr21_GL383581v2_alt 0 116689 gneg +chr21_KI270872v1_alt 0 82692 gneg +chr21_KI270873v1_alt 0 143900 gneg +chr21_KI270874v1_alt 0 166743 gneg +chr22 0 4300000 p13 gvar +chr22 4300000 9400000 p12 stalk +chr22 9400000 13700000 p11.2 gvar +chr22 13700000 15000000 p11.1 acen +chr22 15000000 17400000 q11.1 acen +chr22 17400000 21700000 q11.21 gneg +chr22 21700000 23100000 q11.22 gpos25 +chr22 23100000 25500000 q11.23 gneg +chr22 25500000 29200000 q12.1 gpos50 +chr22 29200000 31800000 q12.2 gneg +chr22 31800000 37200000 q12.3 gpos50 +chr22 37200000 40600000 q13.1 gneg +chr22 40600000 43800000 q13.2 gpos50 +chr22 43800000 48100000 q13.31 gneg +chr22 48100000 49100000 q13.32 gpos50 +chr22 49100000 50818468 q13.33 gneg +chr22_GL383582v2_alt 0 162811 gneg +chr22_GL383583v2_alt 0 96924 gneg +chr22_KB663609v1_alt 0 74013 gneg +chr22_KI270731v1_random 0 150754 gneg +chr22_KI270732v1_random 0 41543 gneg +chr22_KI270733v1_random 0 179772 gneg +chr22_KI270734v1_random 0 165050 gneg +chr22_KI270735v1_random 0 42811 gneg +chr22_KI270736v1_random 0 181920 gneg +chr22_KI270737v1_random 0 103838 gneg +chr22_KI270738v1_random 0 99375 gneg +chr22_KI270739v1_random 0 73985 gneg +chr22_KI270875v1_alt 0 259914 gneg +chr22_KI270876v1_alt 0 263666 gneg +chr22_KI270877v1_alt 0 101331 gneg +chr22_KI270878v1_alt 0 186262 gneg +chr22_KI270879v1_alt 0 304135 gneg +chr22_KI270928v1_alt 0 176103 gneg +chr2_GL383521v1_alt 0 143390 gneg +chr2_GL383522v1_alt 0 123821 gneg +chr2_GL582966v2_alt 0 96131 gneg +chr2_KI270715v1_random 0 161471 gneg +chr2_KI270716v1_random 0 153799 gneg +chr2_KI270767v1_alt 0 161578 gneg +chr2_KI270768v1_alt 0 110099 gneg +chr2_KI270769v1_alt 0 120616 gneg +chr2_KI270770v1_alt 0 136240 gneg +chr2_KI270771v1_alt 0 110395 gneg +chr2_KI270772v1_alt 0 133041 gneg +chr2_KI270773v1_alt 0 70887 gneg +chr2_KI270774v1_alt 0 223625 gneg +chr2_KI270775v1_alt 0 138019 gneg +chr2_KI270776v1_alt 0 174166 gneg +chr2_KI270893v1_alt 0 161218 gneg +chr2_KI270894v1_alt 0 214158 gneg +chr3 0 2800000 p26.3 gpos50 +chr3 2800000 4000000 p26.2 gneg +chr3 4000000 8100000 p26.1 gpos50 +chr3 8100000 11600000 p25.3 gneg +chr3 11600000 13200000 p25.2 gpos25 +chr3 13200000 16300000 p25.1 gneg +chr3 16300000 23800000 p24.3 gpos100 +chr3 23800000 26300000 p24.2 gneg +chr3 26300000 30800000 p24.1 gpos75 +chr3 30800000 32000000 p23 gneg +chr3 32000000 36400000 p22.3 gpos50 +chr3 36400000 39300000 p22.2 gneg +chr3 39300000 43600000 p22.1 gpos75 +chr3 43600000 44100000 p21.33 gneg +chr3 44100000 44200000 p21.32 gpos50 +chr3 44200000 50600000 p21.31 gneg +chr3 50600000 52300000 p21.2 gpos25 +chr3 52300000 54400000 p21.1 gneg +chr3 54400000 58600000 p14.3 gpos50 +chr3 58600000 63800000 p14.2 gneg +chr3 63800000 69700000 p14.1 gpos50 +chr3 69700000 74100000 p13 gneg +chr3 74100000 79800000 p12.3 gpos75 +chr3 79800000 83500000 p12.2 gneg +chr3 83500000 87100000 p12.1 gpos75 +chr3 87100000 87800000 p11.2 gneg +chr3 87800000 90900000 p11.1 acen +chr3 90900000 94000000 q11.1 acen +chr3 94000000 98600000 q11.2 gvar +chr3 98600000 100300000 q12.1 gneg +chr3 100300000 101200000 q12.2 gpos25 +chr3 101200000 103100000 q12.3 gneg +chr3 103100000 106500000 q13.11 gpos75 +chr3 106500000 108200000 q13.12 gneg +chr3 108200000 111600000 q13.13 gpos50 +chr3 111600000 113700000 q13.2 gneg +chr3 113700000 117600000 q13.31 gpos75 +chr3 117600000 119300000 q13.32 gneg +chr3 119300000 122200000 q13.33 gpos75 +chr3 122200000 124100000 q21.1 gneg +chr3 124100000 126100000 q21.2 gpos25 +chr3 126100000 129500000 q21.3 gneg +chr3 129500000 134000000 q22.1 gpos25 +chr3 134000000 136000000 q22.2 gneg +chr3 136000000 139000000 q22.3 gpos25 +chr3 139000000 143100000 q23 gneg +chr3 143100000 149200000 q24 gpos100 +chr3 149200000 152300000 q25.1 gneg +chr3 152300000 155300000 q25.2 gpos50 +chr3 155300000 157300000 q25.31 gneg +chr3 157300000 159300000 q25.32 gpos50 +chr3 159300000 161000000 q25.33 gneg +chr3 161000000 167900000 q26.1 gpos100 +chr3 167900000 171200000 q26.2 gneg +chr3 171200000 176000000 q26.31 gpos75 +chr3 176000000 179300000 q26.32 gneg +chr3 179300000 183000000 q26.33 gpos75 +chr3 183000000 184800000 q27.1 gneg +chr3 184800000 186300000 q27.2 gpos25 +chr3 186300000 188200000 q27.3 gneg +chr3 188200000 192600000 q28 gpos75 +chr3 192600000 198295559 q29 gneg +chr3_GL000221v1_random 0 155397 gneg +chr3_GL383526v1_alt 0 180671 gneg +chr3_JH636055v2_alt 0 173151 gneg +chr3_KI270777v1_alt 0 173649 gneg +chr3_KI270778v1_alt 0 248252 gneg +chr3_KI270779v1_alt 0 205312 gneg +chr3_KI270780v1_alt 0 224108 gneg +chr3_KI270781v1_alt 0 113034 gneg +chr3_KI270782v1_alt 0 162429 gneg +chr3_KI270783v1_alt 0 109187 gneg +chr3_KI270784v1_alt 0 184404 gneg +chr3_KI270895v1_alt 0 162896 gneg +chr3_KI270924v1_alt 0 166540 gneg +chr3_KI270934v1_alt 0 163458 gneg +chr3_KI270935v1_alt 0 197351 gneg +chr3_KI270936v1_alt 0 164170 gneg +chr3_KI270937v1_alt 0 165607 gneg +chr4 0 4500000 p16.3 gneg +chr4 4500000 6000000 p16.2 gpos25 +chr4 6000000 11300000 p16.1 gneg +chr4 11300000 15000000 p15.33 gpos50 +chr4 15000000 17700000 p15.32 gneg +chr4 17700000 21300000 p15.31 gpos75 +chr4 21300000 27700000 p15.2 gneg +chr4 27700000 35800000 p15.1 gpos100 +chr4 35800000 41200000 p14 gneg +chr4 41200000 44600000 p13 gpos50 +chr4 44600000 48200000 p12 gneg +chr4 48200000 50000000 p11 acen +chr4 50000000 51800000 q11 acen +chr4 51800000 58500000 q12 gneg +chr4 58500000 65500000 q13.1 gpos100 +chr4 65500000 69400000 q13.2 gneg +chr4 69400000 75300000 q13.3 gpos75 +chr4 75300000 78000000 q21.1 gneg +chr4 78000000 81500000 q21.21 gpos50 +chr4 81500000 83200000 q21.22 gneg +chr4 83200000 86000000 q21.23 gpos25 +chr4 86000000 87100000 q21.3 gneg +chr4 87100000 92800000 q22.1 gpos75 +chr4 92800000 94200000 q22.2 gneg +chr4 94200000 97900000 q22.3 gpos75 +chr4 97900000 100100000 q23 gneg +chr4 100100000 106700000 q24 gpos50 +chr4 106700000 113200000 q25 gneg +chr4 113200000 119900000 q26 gpos75 +chr4 119900000 122800000 q27 gneg +chr4 122800000 127900000 q28.1 gpos50 +chr4 127900000 130100000 q28.2 gneg +chr4 130100000 138500000 q28.3 gpos100 +chr4 138500000 140600000 q31.1 gneg +chr4 140600000 145900000 q31.21 gpos25 +chr4 145900000 147500000 q31.22 gneg +chr4 147500000 150200000 q31.23 gpos25 +chr4 150200000 154600000 q31.3 gneg +chr4 154600000 160800000 q32.1 gpos100 +chr4 160800000 163600000 q32.2 gneg +chr4 163600000 169200000 q32.3 gpos100 +chr4 169200000 171000000 q33 gneg +chr4 171000000 175400000 q34.1 gpos75 +chr4 175400000 176600000 q34.2 gneg +chr4 176600000 182300000 q34.3 gpos100 +chr4 182300000 186200000 q35.1 gneg +chr4 186200000 190214555 q35.2 gpos25 +chr4_GL000008v2_random 0 209709 gneg +chr4_GL000257v2_alt 0 586476 gneg +chr4_GL383527v1_alt 0 164536 gneg +chr4_GL383528v1_alt 0 376187 gneg +chr4_KI270785v1_alt 0 119912 gneg +chr4_KI270786v1_alt 0 244096 gneg +chr4_KI270787v1_alt 0 111943 gneg +chr4_KI270788v1_alt 0 158965 gneg +chr4_KI270789v1_alt 0 205944 gneg +chr4_KI270790v1_alt 0 220246 gneg +chr4_KI270896v1_alt 0 378547 gneg +chr4_KI270925v1_alt 0 555799 gneg +chr5 0 4400000 p15.33 gneg +chr5 4400000 6300000 p15.32 gpos25 +chr5 6300000 9900000 p15.31 gneg +chr5 9900000 15000000 p15.2 gpos50 +chr5 15000000 18400000 p15.1 gneg +chr5 18400000 23300000 p14.3 gpos100 +chr5 23300000 24600000 p14.2 gneg +chr5 24600000 28900000 p14.1 gpos100 +chr5 28900000 33800000 p13.3 gneg +chr5 33800000 38400000 p13.2 gpos25 +chr5 38400000 42500000 p13.1 gneg +chr5 42500000 46100000 p12 gpos50 +chr5 46100000 48800000 p11 acen +chr5 48800000 51400000 q11.1 acen +chr5 51400000 59600000 q11.2 gneg +chr5 59600000 63600000 q12.1 gpos75 +chr5 63600000 63900000 q12.2 gneg +chr5 63900000 67400000 q12.3 gpos75 +chr5 67400000 69100000 q13.1 gneg +chr5 69100000 74000000 q13.2 gpos50 +chr5 74000000 77600000 q13.3 gneg +chr5 77600000 82100000 q14.1 gpos50 +chr5 82100000 83500000 q14.2 gneg +chr5 83500000 93000000 q14.3 gpos100 +chr5 93000000 98900000 q15 gneg +chr5 98900000 103400000 q21.1 gpos100 +chr5 103400000 105100000 q21.2 gneg +chr5 105100000 110200000 q21.3 gpos100 +chr5 110200000 112200000 q22.1 gneg +chr5 112200000 113800000 q22.2 gpos50 +chr5 113800000 115900000 q22.3 gneg +chr5 115900000 122100000 q23.1 gpos100 +chr5 122100000 127900000 q23.2 gneg +chr5 127900000 131200000 q23.3 gpos100 +chr5 131200000 136900000 q31.1 gneg +chr5 136900000 140100000 q31.2 gpos25 +chr5 140100000 145100000 q31.3 gneg +chr5 145100000 150400000 q32 gpos75 +chr5 150400000 153300000 q33.1 gneg +chr5 153300000 156300000 q33.2 gpos50 +chr5 156300000 160500000 q33.3 gneg +chr5 160500000 169000000 q34 gpos100 +chr5 169000000 173300000 q35.1 gneg +chr5 173300000 177100000 q35.2 gpos25 +chr5 177100000 181538259 q35.3 gneg +chr5_GL000208v1_random 0 92689 gneg +chr5_GL339449v2_alt 0 1612928 gneg +chr5_GL383530v1_alt 0 101241 gneg +chr5_GL383531v1_alt 0 173459 gneg +chr5_GL383532v1_alt 0 82728 gneg +chr5_GL949742v1_alt 0 226852 gneg +chr5_KI270791v1_alt 0 195710 gneg +chr5_KI270792v1_alt 0 179043 gneg +chr5_KI270793v1_alt 0 126136 gneg +chr5_KI270794v1_alt 0 164558 gneg +chr5_KI270795v1_alt 0 131892 gneg +chr5_KI270796v1_alt 0 172708 gneg +chr5_KI270897v1_alt 0 1144418 gneg +chr5_KI270898v1_alt 0 130957 gneg +chr6 0 2300000 p25.3 gneg +chr6 2300000 4200000 p25.2 gpos25 +chr6 4200000 7100000 p25.1 gneg +chr6 7100000 10600000 p24.3 gpos50 +chr6 10600000 11600000 p24.2 gneg +chr6 11600000 13400000 p24.1 gpos25 +chr6 13400000 15200000 p23 gneg +chr6 15200000 25200000 p22.3 gpos75 +chr6 25200000 27100000 p22.2 gneg +chr6 27100000 30500000 p22.1 gpos50 +chr6 30500000 32100000 p21.33 gneg +chr6 32100000 33500000 p21.32 gpos25 +chr6 33500000 36600000 p21.31 gneg +chr6 36600000 40500000 p21.2 gpos25 +chr6 40500000 46200000 p21.1 gneg +chr6 46200000 51800000 p12.3 gpos100 +chr6 51800000 53000000 p12.2 gneg +chr6 53000000 57200000 p12.1 gpos100 +chr6 57200000 58500000 p11.2 gneg +chr6 58500000 59800000 p11.1 acen +chr6 59800000 62600000 q11.1 acen +chr6 62600000 62700000 q11.2 gneg +chr6 62700000 69200000 q12 gpos100 +chr6 69200000 75200000 q13 gneg +chr6 75200000 83200000 q14.1 gpos50 +chr6 83200000 84200000 q14.2 gneg +chr6 84200000 87300000 q14.3 gpos50 +chr6 87300000 92500000 q15 gneg +chr6 92500000 98900000 q16.1 gpos100 +chr6 98900000 100000000 q16.2 gneg +chr6 100000000 105000000 q16.3 gpos100 +chr6 105000000 114200000 q21 gneg +chr6 114200000 117900000 q22.1 gpos75 +chr6 117900000 118100000 q22.2 gneg +chr6 118100000 125800000 q22.31 gpos100 +chr6 125800000 126800000 q22.32 gneg +chr6 126800000 130000000 q22.33 gpos75 +chr6 130000000 130900000 q23.1 gneg +chr6 130900000 134700000 q23.2 gpos50 +chr6 134700000 138300000 q23.3 gneg +chr6 138300000 142200000 q24.1 gpos75 +chr6 142200000 145100000 q24.2 gneg +chr6 145100000 148500000 q24.3 gpos75 +chr6 148500000 152100000 q25.1 gneg +chr6 152100000 155200000 q25.2 gpos50 +chr6 155200000 160600000 q25.3 gneg +chr6 160600000 164100000 q26 gpos50 +chr6 164100000 170805979 q27 gneg +chr6_GL000250v2_alt 0 4672374 gneg +chr6_GL000251v2_alt 0 4795265 gneg +chr6_GL000252v2_alt 0 4604811 gneg +chr6_GL000253v2_alt 0 4677643 gneg +chr6_GL000254v2_alt 0 4827813 gneg +chr6_GL000255v2_alt 0 4606388 gneg +chr6_GL000256v2_alt 0 4929269 gneg +chr6_GL383533v1_alt 0 124736 gneg +chr6_KB021644v2_alt 0 185823 gneg +chr6_KI270758v1_alt 0 76752 gneg +chr6_KI270797v1_alt 0 197536 gneg +chr6_KI270798v1_alt 0 271782 gneg +chr6_KI270799v1_alt 0 152148 gneg +chr6_KI270800v1_alt 0 175808 gneg +chr6_KI270801v1_alt 0 870480 gneg +chr6_KI270802v1_alt 0 75005 gneg +chr7 0 2800000 p22.3 gneg +chr7 2800000 4500000 p22.2 gpos25 +chr7 4500000 7200000 p22.1 gneg +chr7 7200000 13700000 p21.3 gpos100 +chr7 13700000 16500000 p21.2 gneg +chr7 16500000 20900000 p21.1 gpos100 +chr7 20900000 25500000 p15.3 gneg +chr7 25500000 27900000 p15.2 gpos50 +chr7 27900000 28800000 p15.1 gneg +chr7 28800000 34900000 p14.3 gpos75 +chr7 34900000 37100000 p14.2 gneg +chr7 37100000 43300000 p14.1 gpos75 +chr7 43300000 45400000 p13 gneg +chr7 45400000 49000000 p12.3 gpos75 +chr7 49000000 50500000 p12.2 gneg +chr7 50500000 53900000 p12.1 gpos75 +chr7 53900000 58100000 p11.2 gneg +chr7 58100000 60100000 p11.1 acen +chr7 60100000 62100000 q11.1 acen +chr7 62100000 67500000 q11.21 gneg +chr7 67500000 72700000 q11.22 gpos50 +chr7 72700000 77900000 q11.23 gneg +chr7 77900000 86700000 q21.11 gpos100 +chr7 86700000 88500000 q21.12 gneg +chr7 88500000 91500000 q21.13 gpos75 +chr7 91500000 93300000 q21.2 gneg +chr7 93300000 98400000 q21.3 gpos75 +chr7 98400000 104200000 q22.1 gneg +chr7 104200000 104900000 q22.2 gpos50 +chr7 104900000 107800000 q22.3 gneg +chr7 107800000 115000000 q31.1 gpos75 +chr7 115000000 117700000 q31.2 gneg +chr7 117700000 121400000 q31.31 gpos75 +chr7 121400000 124100000 q31.32 gneg +chr7 124100000 127500000 q31.33 gpos75 +chr7 127500000 129600000 q32.1 gneg +chr7 129600000 130800000 q32.2 gpos25 +chr7 130800000 132900000 q32.3 gneg +chr7 132900000 138500000 q33 gpos50 +chr7 138500000 143400000 q34 gneg +chr7 143400000 148200000 q35 gpos75 +chr7 148200000 152800000 q36.1 gneg +chr7 152800000 155200000 q36.2 gpos25 +chr7 155200000 159345973 q36.3 gneg +chr7_GL383534v2_alt 0 119183 gneg +chr7_KI270803v1_alt 0 1111570 gneg +chr7_KI270804v1_alt 0 157952 gneg +chr7_KI270805v1_alt 0 209988 gneg +chr7_KI270806v1_alt 0 158166 gneg +chr7_KI270807v1_alt 0 126434 gneg +chr7_KI270808v1_alt 0 271455 gneg +chr7_KI270809v1_alt 0 209586 gneg +chr7_KI270899v1_alt 0 190869 gneg +chr8 0 2300000 p23.3 gneg +chr8 2300000 6300000 p23.2 gpos75 +chr8 6300000 12800000 p23.1 gneg +chr8 12800000 19200000 p22 gpos100 +chr8 19200000 23500000 p21.3 gneg +chr8 23500000 27500000 p21.2 gpos50 +chr8 27500000 29000000 p21.1 gneg +chr8 29000000 36700000 p12 gpos75 +chr8 36700000 38500000 p11.23 gneg +chr8 38500000 39900000 p11.22 gpos25 +chr8 39900000 43200000 p11.21 gneg +chr8 43200000 45200000 p11.1 acen +chr8 45200000 47200000 q11.1 acen +chr8 47200000 51300000 q11.21 gneg +chr8 51300000 51700000 q11.22 gpos75 +chr8 51700000 54600000 q11.23 gneg +chr8 54600000 60600000 q12.1 gpos50 +chr8 60600000 61300000 q12.2 gneg +chr8 61300000 65100000 q12.3 gpos50 +chr8 65100000 67100000 q13.1 gneg +chr8 67100000 69600000 q13.2 gpos50 +chr8 69600000 72000000 q13.3 gneg +chr8 72000000 74600000 q21.11 gpos100 +chr8 74600000 74700000 q21.12 gneg +chr8 74700000 83500000 q21.13 gpos75 +chr8 83500000 85900000 q21.2 gneg +chr8 85900000 92300000 q21.3 gpos100 +chr8 92300000 97900000 q22.1 gneg +chr8 97900000 100500000 q22.2 gpos25 +chr8 100500000 105100000 q22.3 gneg +chr8 105100000 109500000 q23.1 gpos75 +chr8 109500000 111100000 q23.2 gneg +chr8 111100000 116700000 q23.3 gpos100 +chr8 116700000 118300000 q24.11 gneg +chr8 118300000 121500000 q24.12 gpos50 +chr8 121500000 126300000 q24.13 gneg +chr8 126300000 130400000 q24.21 gpos50 +chr8 130400000 135400000 q24.22 gneg +chr8 135400000 138900000 q24.23 gpos75 +chr8 138900000 145138636 q24.3 gneg +chr8_KI270810v1_alt 0 374415 gneg +chr8_KI270811v1_alt 0 292436 gneg +chr8_KI270812v1_alt 0 282736 gneg +chr8_KI270813v1_alt 0 300230 gneg +chr8_KI270814v1_alt 0 141812 gneg +chr8_KI270815v1_alt 0 132244 gneg +chr8_KI270816v1_alt 0 305841 gneg +chr8_KI270817v1_alt 0 158983 gneg +chr8_KI270818v1_alt 0 145606 gneg +chr8_KI270819v1_alt 0 133535 gneg +chr8_KI270820v1_alt 0 36640 gneg +chr8_KI270821v1_alt 0 985506 gneg +chr8_KI270822v1_alt 0 624492 gneg +chr8_KI270900v1_alt 0 318687 gneg +chr8_KI270901v1_alt 0 136959 gneg +chr8_KI270926v1_alt 0 229282 gneg +chr9 0 2200000 p24.3 gneg +chr9 2200000 4600000 p24.2 gpos25 +chr9 4600000 9000000 p24.1 gneg +chr9 9000000 14200000 p23 gpos75 +chr9 14200000 16600000 p22.3 gneg +chr9 16600000 18500000 p22.2 gpos25 +chr9 18500000 19900000 p22.1 gneg +chr9 19900000 25600000 p21.3 gpos100 +chr9 25600000 28000000 p21.2 gneg +chr9 28000000 33200000 p21.1 gpos100 +chr9 33200000 36300000 p13.3 gneg +chr9 36300000 37900000 p13.2 gpos25 +chr9 37900000 39000000 p13.1 gneg +chr9 39000000 40000000 p12 gpos50 +chr9 40000000 42200000 p11.2 gneg +chr9 42200000 43000000 p11.1 acen +chr9 43000000 45500000 q11 acen +chr9 45500000 61500000 q12 gvar +chr9 61500000 65000000 q13 gneg +chr9 65000000 69300000 q21.11 gpos25 +chr9 69300000 71300000 q21.12 gneg +chr9 71300000 76600000 q21.13 gpos50 +chr9 76600000 78500000 q21.2 gneg +chr9 78500000 81500000 q21.31 gpos50 +chr9 81500000 84300000 q21.32 gneg +chr9 84300000 87800000 q21.33 gpos50 +chr9 87800000 89200000 q22.1 gneg +chr9 89200000 91200000 q22.2 gpos25 +chr9 91200000 93900000 q22.31 gneg +chr9 93900000 96500000 q22.32 gpos25 +chr9 96500000 99800000 q22.33 gneg +chr9 99800000 105400000 q31.1 gpos100 +chr9 105400000 108500000 q31.2 gneg +chr9 108500000 112100000 q31.3 gpos25 +chr9 112100000 114900000 q32 gneg +chr9 114900000 119800000 q33.1 gpos75 +chr9 119800000 123100000 q33.2 gneg +chr9 123100000 127500000 q33.3 gpos25 +chr9 127500000 130600000 q34.11 gneg +chr9 130600000 131100000 q34.12 gpos25 +chr9 131100000 133100000 q34.13 gneg +chr9 133100000 134500000 q34.2 gpos25 +chr9 134500000 138394717 q34.3 gneg +chr9_GL383539v1_alt 0 162988 gneg +chr9_GL383540v1_alt 0 71551 gneg +chr9_GL383541v1_alt 0 171286 gneg +chr9_GL383542v1_alt 0 60032 gneg +chr9_KI270717v1_random 0 40062 gneg +chr9_KI270718v1_random 0 38054 gneg +chr9_KI270719v1_random 0 176845 gneg +chr9_KI270720v1_random 0 39050 gneg +chr9_KI270823v1_alt 0 439082 gneg +chrM 0 16569 gneg +chrUn_GL000195v1 0 182896 gneg +chrUn_GL000213v1 0 164239 gneg +chrUn_GL000214v1 0 137718 gneg +chrUn_GL000216v2 0 176608 gneg +chrUn_GL000218v1 0 161147 gneg +chrUn_GL000219v1 0 179198 gneg +chrUn_GL000220v1 0 161802 gneg +chrUn_GL000224v1 0 179693 gneg +chrUn_GL000226v1 0 15008 gneg +chrUn_KI270302v1 0 2274 gneg +chrUn_KI270303v1 0 1942 gneg +chrUn_KI270304v1 0 2165 gneg +chrUn_KI270305v1 0 1472 gneg +chrUn_KI270310v1 0 1201 gneg +chrUn_KI270311v1 0 12399 gneg +chrUn_KI270312v1 0 998 gneg +chrUn_KI270315v1 0 2276 gneg +chrUn_KI270316v1 0 1444 gneg +chrUn_KI270317v1 0 37690 gneg +chrUn_KI270320v1 0 4416 gneg +chrUn_KI270322v1 0 21476 gneg +chrUn_KI270329v1 0 1040 gneg +chrUn_KI270330v1 0 1652 gneg +chrUn_KI270333v1 0 2699 gneg +chrUn_KI270334v1 0 1368 gneg +chrUn_KI270335v1 0 1048 gneg +chrUn_KI270336v1 0 1026 gneg +chrUn_KI270337v1 0 1121 gneg +chrUn_KI270338v1 0 1428 gneg +chrUn_KI270340v1 0 1428 gneg +chrUn_KI270362v1 0 3530 gneg +chrUn_KI270363v1 0 1803 gneg +chrUn_KI270364v1 0 2855 gneg +chrUn_KI270366v1 0 8320 gneg +chrUn_KI270371v1 0 2805 gneg +chrUn_KI270372v1 0 1650 gneg +chrUn_KI270373v1 0 1451 gneg +chrUn_KI270374v1 0 2656 gneg +chrUn_KI270375v1 0 2378 gneg +chrUn_KI270376v1 0 1136 gneg +chrUn_KI270378v1 0 1048 gneg +chrUn_KI270379v1 0 1045 gneg +chrUn_KI270381v1 0 1930 gneg +chrUn_KI270382v1 0 4215 gneg +chrUn_KI270383v1 0 1750 gneg +chrUn_KI270384v1 0 1658 gneg +chrUn_KI270385v1 0 990 gneg +chrUn_KI270386v1 0 1788 gneg +chrUn_KI270387v1 0 1537 gneg +chrUn_KI270388v1 0 1216 gneg +chrUn_KI270389v1 0 1298 gneg +chrUn_KI270390v1 0 2387 gneg +chrUn_KI270391v1 0 1484 gneg +chrUn_KI270392v1 0 971 gneg +chrUn_KI270393v1 0 1308 gneg +chrUn_KI270394v1 0 970 gneg +chrUn_KI270395v1 0 1143 gneg +chrUn_KI270396v1 0 1880 gneg +chrUn_KI270411v1 0 2646 gneg +chrUn_KI270412v1 0 1179 gneg +chrUn_KI270414v1 0 2489 gneg +chrUn_KI270417v1 0 2043 gneg +chrUn_KI270418v1 0 2145 gneg +chrUn_KI270419v1 0 1029 gneg +chrUn_KI270420v1 0 2321 gneg +chrUn_KI270422v1 0 1445 gneg +chrUn_KI270423v1 0 981 gneg +chrUn_KI270424v1 0 2140 gneg +chrUn_KI270425v1 0 1884 gneg +chrUn_KI270429v1 0 1361 gneg +chrUn_KI270435v1 0 92983 gneg +chrUn_KI270438v1 0 112505 gneg +chrUn_KI270442v1 0 392061 gneg +chrUn_KI270448v1 0 7992 gneg +chrUn_KI270465v1 0 1774 gneg +chrUn_KI270466v1 0 1233 gneg +chrUn_KI270467v1 0 3920 gneg +chrUn_KI270468v1 0 4055 gneg +chrUn_KI270507v1 0 5353 gneg +chrUn_KI270508v1 0 1951 gneg +chrUn_KI270509v1 0 2318 gneg +chrUn_KI270510v1 0 2415 gneg +chrUn_KI270511v1 0 8127 gneg +chrUn_KI270512v1 0 22689 gneg +chrUn_KI270515v1 0 6361 gneg +chrUn_KI270516v1 0 1300 gneg +chrUn_KI270517v1 0 3253 gneg +chrUn_KI270518v1 0 2186 gneg +chrUn_KI270519v1 0 138126 gneg +chrUn_KI270521v1 0 7642 gneg +chrUn_KI270522v1 0 5674 gneg +chrUn_KI270528v1 0 2983 gneg +chrUn_KI270529v1 0 1899 gneg +chrUn_KI270530v1 0 2168 gneg +chrUn_KI270538v1 0 91309 gneg +chrUn_KI270539v1 0 993 gneg +chrUn_KI270544v1 0 1202 gneg +chrUn_KI270548v1 0 1599 gneg +chrUn_KI270579v1 0 31033 gneg +chrUn_KI270580v1 0 1553 gneg +chrUn_KI270581v1 0 7046 gneg +chrUn_KI270582v1 0 6504 gneg +chrUn_KI270583v1 0 1400 gneg +chrUn_KI270584v1 0 4513 gneg +chrUn_KI270587v1 0 2969 gneg +chrUn_KI270588v1 0 6158 gneg +chrUn_KI270589v1 0 44474 gneg +chrUn_KI270590v1 0 4685 gneg +chrUn_KI270591v1 0 5796 gneg +chrUn_KI270593v1 0 3041 gneg +chrUn_KI270741v1 0 157432 gneg +chrUn_KI270742v1 0 186739 gneg +chrUn_KI270743v1 0 210658 gneg +chrUn_KI270744v1 0 168472 gneg +chrUn_KI270745v1 0 41891 gneg +chrUn_KI270746v1 0 66486 gneg +chrUn_KI270747v1 0 198735 gneg +chrUn_KI270748v1 0 93321 gneg +chrUn_KI270749v1 0 158759 gneg +chrUn_KI270750v1 0 148850 gneg +chrUn_KI270751v1 0 150742 gneg +chrUn_KI270752v1 0 27745 gneg +chrUn_KI270753v1 0 62944 gneg +chrUn_KI270754v1 0 40191 gneg +chrUn_KI270755v1 0 36723 gneg +chrUn_KI270756v1 0 79590 gneg +chrUn_KI270757v1 0 71251 gneg +chrX 0 4400000 p22.33 gneg +chrX 4400000 6100000 p22.32 gpos50 +chrX 6100000 9600000 p22.31 gneg +chrX 9600000 17400000 p22.2 gpos50 +chrX 17400000 19200000 p22.13 gneg +chrX 19200000 21900000 p22.12 gpos50 +chrX 21900000 24900000 p22.11 gneg +chrX 24900000 29300000 p21.3 gpos100 +chrX 29300000 31500000 p21.2 gneg +chrX 31500000 37800000 p21.1 gpos100 +chrX 37800000 42500000 p11.4 gneg +chrX 42500000 47600000 p11.3 gpos75 +chrX 47600000 50100000 p11.23 gneg +chrX 50100000 54800000 p11.22 gpos25 +chrX 54800000 58100000 p11.21 gneg +chrX 58100000 61000000 p11.1 acen +chrX 61000000 63800000 q11.1 acen +chrX 63800000 65400000 q11.2 gneg +chrX 65400000 68500000 q12 gpos50 +chrX 68500000 73000000 q13.1 gneg +chrX 73000000 74700000 q13.2 gpos50 +chrX 74700000 76800000 q13.3 gneg +chrX 76800000 85400000 q21.1 gpos100 +chrX 85400000 87000000 q21.2 gneg +chrX 87000000 92700000 q21.31 gpos100 +chrX 92700000 94300000 q21.32 gneg +chrX 94300000 99100000 q21.33 gpos75 +chrX 99100000 103300000 q22.1 gneg +chrX 103300000 104500000 q22.2 gpos50 +chrX 104500000 109400000 q22.3 gneg +chrX 109400000 117400000 q23 gpos75 +chrX 117400000 121800000 q24 gneg +chrX 121800000 129500000 q25 gpos100 +chrX 129500000 131300000 q26.1 gneg +chrX 131300000 134500000 q26.2 gpos25 +chrX 134500000 138900000 q26.3 gneg +chrX 138900000 141200000 q27.1 gpos75 +chrX 141200000 143000000 q27.2 gneg +chrX 143000000 148000000 q27.3 gpos100 +chrX 148000000 156040895 q28 gneg +chrX_KI270880v1_alt 0 284869 gneg +chrX_KI270881v1_alt 0 144206 gneg +chrX_KI270913v1_alt 0 274009 gneg +chrY 0 300000 p11.32 gneg +chrY 300000 600000 p11.31 gpos50 +chrY 600000 10300000 p11.2 gneg +chrY 10300000 10400000 p11.1 acen +chrY 10400000 10600000 q11.1 acen +chrY 10600000 12400000 q11.21 gneg +chrY 12400000 17100000 q11.221 gpos50 +chrY 17100000 19600000 q11.222 gneg +chrY 19600000 23800000 q11.223 gpos50 +chrY 23800000 26600000 q11.23 gneg +chrY 26600000 57227415 q12 gvar +chrY_KI270740v1_random 0 37240 gneg +chr8_KZ208915v1_fix 0 6367528 gneg +chr15_KN538374v1_fix 0 4998962 gneg +chr15_KQ031389v1_alt 0 2365364 gneg +chr16_KV880768v1_fix 0 1927115 gneg +chr12_KZ208916v1_fix 0 1046838 gneg +chr14_KZ208920v1_fix 0 690932 gneg +chr7_KZ208913v1_alt 0 680662 gneg +chr5_KV575244v1_fix 0 673059 gneg +chr7_KZ208912v1_fix 0 589656 gneg +chr1_KV880763v1_alt 0 551020 gneg +chr12_KN538369v1_fix 0 541038 gneg +chr2_KQ983256v1_alt 0 535088 gneg +chr2_KQ031384v1_fix 0 481245 gneg +chr7_KV880765v1_fix 0 468267 gneg +chr1_KQ031383v1_fix 0 467143 gneg +chr1_KN538360v1_fix 0 460100 gneg +chr3_KN196475v1_fix 0 451168 gneg +chr4_KV766193v1_alt 0 420675 gneg +chr10_KN538367v1_fix 0 420164 gneg +chr3_KN538364v1_fix 0 415308 gneg +chr3_KV766192v1_fix 0 411654 gneg +chr18_KQ090028v1_fix 0 407387 gneg +chr19_KQ458386v1_fix 0 405389 gneg +chr3_KQ031385v1_fix 0 373699 gneg +chr19_KN196484v1_fix 0 370917 gneg +chr2_KN538363v1_fix 0 365499 gneg +chr5_KV575243v1_alt 0 362221 gneg +chr13_KN538372v1_fix 0 356766 gneg +chr1_KQ458383v1_alt 0 349938 gneg +chr9_KN196479v1_fix 0 330164 gneg +chr1_KZ208906v1_fix 0 330031 gneg +chr6_KQ031387v1_fix 0 320750 gneg +chr12_KQ759760v1_fix 0 315610 gneg +chr3_KN196476v1_fix 0 305979 gneg +chr1_KN538361v1_fix 0 305542 gneg +chr19_KV575249v1_alt 0 293522 gneg +chr17_KV766196v1_fix 0 281919 gneg +chr1_KQ983255v1_alt 0 278659 gneg +chr10_KN196480v1_fix 0 277797 gneg +chr17_KV766198v1_alt 0 276292 gneg +chr6_KN196478v1_fix 0 268330 gneg +chr16_KQ090027v1_alt 0 267463 gneg +chr8_KV880767v1_fix 0 265876 gneg +chr10_KQ090021v1_fix 0 264545 gneg +chr17_KV766197v1_alt 0 246895 gneg +chr6_KQ090016v1_fix 0 245716 gneg +chr6_KZ208911v1_fix 0 242796 gneg +chr19_KV575250v1_alt 0 241058 gneg +chr4_KQ090015v1_alt 0 236512 gneg +chr4_KQ983257v1_fix 0 230434 gneg +chr19_KV575256v1_alt 0 223118 gneg +chr1_KQ458384v1_alt 0 212205 gneg +chr12_KN196482v1_fix 0 211377 gneg +chrY_KZ208924v1_fix 0 209722 gneg +chr2_KN538362v1_fix 0 208149 gneg +chr13_KN538371v1_fix 0 206320 gneg +chr4_KQ983258v1_alt 0 205407 gneg +chr18_KQ458385v1_alt 0 205101 gneg +chr11_KN538368v1_alt 0 203552 gneg +chr11_KQ759759v1_fix 0 196940 gneg +chrX_KV766199v1_alt 0 188004 gneg +chr1_KN196472v1_fix 0 186494 gneg +chr10_KQ090020v1_alt 0 185507 gneg +chr11_KQ090022v1_fix 0 181958 gneg +chr2_KZ208907v1_alt 0 181658 gneg +chr7_KQ031388v1_fix 0 179932 gneg +chr19_KV575252v1_alt 0 178197 gneg +chr3_KZ208909v1_alt 0 175849 gneg +chr12_KZ208918v1_alt 0 174808 gneg +chr22_KQ458388v1_alt 0 174749 gneg +chr14_KZ208919v1_alt 0 171798 gneg +chr19_KV575259v1_alt 0 171263 gneg +chr19_KV575247v1_alt 0 170206 gneg +chr16_KQ031390v1_alt 0 169136 gneg +chr13_KQ090024v1_alt 0 168146 gneg +chr19_KV575248v1_alt 0 168131 gneg +chr19_KV575253v1_alt 0 166713 gneg +chr1_KN196473v1_fix 0 166200 gneg +chr1_KZ208904v1_alt 0 166136 gneg +chr3_KQ031386v1_fix 0 165718 gneg +chr8_KZ208914v1_fix 0 165120 gneg +chr19_KV575246v1_alt 0 163926 gneg +chr9_KQ090018v1_alt 0 163882 gneg +chr4_KQ090014v1_alt 0 163749 gneg +chr19_KV575255v1_alt 0 161095 gneg +chr19_KV575251v1_alt 0 159285 gneg +chr8_KV880766v1_fix 0 156998 gneg +chr19_KV575258v1_alt 0 156965 gneg +chr22_KN196485v1_alt 0 156562 gneg +chr22_KQ458387v1_alt 0 155930 gneg +chr17_KV575245v1_fix 0 154723 gneg +chr22_KN196486v1_alt 0 153027 gneg +chr13_KN538373v1_fix 0 148762 gneg +chr19_KV575260v1_alt 0 145691 gneg +chr22_KQ759761v1_alt 0 145162 gneg +chr7_KV880764v1_fix 0 142129 gneg +chr1_KQ458382v1_alt 0 141019 gneg +chr11_KV766195v1_fix 0 140877 gneg +chr2_KZ208908v1_alt 0 140361 gneg +chr1_KZ208905v1_alt 0 140355 gneg +chr6_KV766194v1_fix 0 139427 gneg +chr5_KN196477v1_alt 0 139087 gneg +chr5_KZ208910v1_alt 0 135987 gneg +chr9_KQ090019v1_alt 0 134099 gneg +chr13_KQ090025v1_alt 0 123480 gneg +chr1_KN196474v1_fix 0 122022 gneg +chr12_KQ090023v1_alt 0 109323 gneg +chr11_KN196481v1_fix 0 108875 gneg +chrY_KN196487v1_fix 0 101150 gneg +chr22_KQ759762v1_fix 0 101037 gneg +chr19_KV575257v1_alt 0 100553 gneg +chr19_KV575254v1_alt 0 99845 gneg +chr18_KZ208922v1_fix 0 93070 gneg +chr4_KQ090013v1_alt 0 90922 gneg +chr12_KN538370v1_fix 0 86533 gneg +chr10_KN538366v1_fix 0 85284 gneg +chr6_KQ090017v1_alt 0 82315 gneg +chr16_KZ208921v1_alt 0 78609 gneg +chr12_KZ208917v1_fix 0 64689 gneg +chr16_KQ090026v1_alt 0 59016 gneg +chrY_KZ208923v1_fix 0 48370 gneg +chr13_KN196483v1_fix 0 35455 gneg +chr10_KN538365v1_fix 0 14347 gneg +chr16_KZ559113v1_fix 0 480415 gneg +chr11_KZ559108v1_fix 0 305244 gneg +chr3_KZ559103v1_alt 0 302885 gneg +chr11_KZ559110v1_alt 0 301637 gneg +chr11_KZ559109v1_fix 0 279644 gneg +chr18_KZ559115v1_fix 0 230843 gneg +chr3_KZ559102v1_alt 0 197752 gneg +chr3_KZ559105v1_alt 0 195063 gneg +chr11_KZ559111v1_alt 0 181167 gneg +chr7_KZ559106v1_alt 0 172555 gneg +chr3_KZ559101v1_alt 0 164041 gneg +chr18_KZ559116v1_alt 0 163186 gneg +chr12_KZ559112v1_alt 0 154139 gneg +chr17_KZ559114v1_alt 0 116753 gneg +chr3_KZ559104v1_fix 0 105527 gneg +chr8_KZ559107v1_alt 0 103072 gneg +chr1_KZ559100v1_fix 0 44955 gneg +chr15_ML143371v1_fix 0 5500449 gneg +chr21_ML143377v1_fix 0 519485 gneg +chr19_ML143376v1_fix 0 493165 gneg +chr22_ML143378v1_fix 0 461303 gneg +chr10_ML143354v1_fix 0 454963 gneg +chr22_ML143380v1_fix 0 412368 gneg +chr13_ML143366v1_fix 0 409912 gneg +chrX_ML143381v1_fix 0 403128 gneg +chr14_ML143367v1_fix 0 399183 gneg +chr15_ML143372v1_fix 0 396515 gneg +chr15_ML143370v1_fix 0 369264 gneg +chr4_ML143345v1_fix 0 341066 gneg +chr12_ML143361v1_fix 0 297568 gneg +chr10_ML143355v1_fix 0 292944 gneg +chr4_ML143349v1_fix 0 276109 gneg +chr16_ML143373v1_fix 0 270967 gneg +chr11_ML143358v1_fix 0 270122 gneg +chr14_ML143368v1_alt 0 264228 gneg +chr7_ML143352v1_fix 0 254759 gneg +chr4_ML143344v1_fix 0 235734 gneg +chr11_ML143359v1_fix 0 217075 gneg +chr3_ML143343v1_alt 0 215443 gneg +chr12_ML143362v1_fix 0 192531 gneg +chr4_ML143347v1_fix 0 176674 gneg +chr11_ML143360v1_fix 0 170928 gneg +chr11_ML143357v1_fix 0 165419 gneg +chr13_ML143364v1_fix 0 158944 gneg +chr2_ML143341v1_fix 0 145975 gneg +chr17_ML143374v1_fix 0 137908 gneg +chr4_ML143348v1_fix 0 125549 gneg +chr15_ML143369v1_fix 0 97763 gneg +chr5_ML143350v1_fix 0 89956 gneg +chr2_ML143342v1_fix 0 84043 gneg +chr6_ML143351v1_fix 0 73265 gneg +chrX_ML143383v1_fix 0 68192 gneg +chr13_ML143365v1_fix 0 65394 gneg +chr17_ML143375v1_fix 0 56695 gneg +chr4_ML143346v1_fix 0 53476 gneg +chr11_ML143356v1_fix 0 45257 gneg +chrX_ML143382v1_fix 0 28824 gneg +chr9_ML143353v1_fix 0 25408 gneg +chrX_ML143385v1_fix 0 17435 gneg +chrX_ML143384v1_fix 0 14678 gneg +chr22_ML143379v1_fix 0 12295 gneg +chr13_ML143363v1_fix 0 7309 gneg +chr5_MU273354v1_fix 0 2101585 gneg +chr1_MU273333v1_fix 0 1572686 gneg +chr15_MU273374v1_fix 0 1154574 gneg +chr21_MU273391v1_fix 0 1020778 gneg +chr2_MU273342v1_fix 0 955087 gneg +chrY_MU273398v1_fix 0 865743 gneg +chr1_MU273331v1_alt 0 847441 gneg +chr14_MU273373v1_fix 0 722645 gneg +chrX_MU273395v1_alt 0 619716 gneg +chr9_MU273366v1_fix 0 569668 gneg +chr17_MU273380v1_fix 0 538541 gneg +chr2_MU273338v1_alt 0 535251 gneg +chr1_MU273330v1_alt 0 516764 gneg +chr5_MU273355v1_fix 0 508332 gneg +chr2_MU273339v1_alt 0 500581 gneg +chr2_MU273343v1_fix 0 489404 gneg +chr9_MU273365v1_fix 0 482250 gneg +chr3_MU273348v1_fix 0 475876 gneg +chr3_MU273346v1_fix 0 469342 gneg +chr7_MU273358v1_alt 0 464417 gneg +chr11_MU273369v1_fix 0 434831 gneg +chr2_MU273337v1_alt 0 431782 gneg +chr8_MU273362v1_fix 0 429744 gneg +chr6_MU273357v1_alt 0 383128 gneg +chr17_MU273378v1_alt 0 372839 gneg +chr20_MU273389v1_fix 0 355731 gneg +chr11_MU273370v1_fix 0 344606 gneg +chr9_MU273364v1_fix 0 340717 gneg +chr21_MU273390v1_fix 0 336752 gneg +chr1_MU273332v1_alt 0 335159 gneg +chr16_MU273377v1_fix 0 334997 gneg +chr19_MU273384v1_fix 0 333754 gneg +chrX_MU273397v1_alt 0 330493 gneg +chr4_MU273349v1_alt 0 308682 gneg +chr5_MU273356v1_alt 0 302485 gneg +chr3_MU273347v1_fix 0 301310 gneg +chrX_MU273396v1_alt 0 294119 gneg +chr2_MU273340v1_alt 0 284971 gneg +chr20_MU273388v1_fix 0 273725 gneg +chr11_MU273368v1_alt 0 261194 gneg +chr1_MU273336v1_fix 0 250447 gneg +chr2_MU273344v1_fix 0 244725 gneg +chr17_MU273379v1_fix 0 234878 gneg +chr19_MU273386v1_fix 0 226166 gneg +chr1_MU273335v1_fix 0 211934 gneg +chr1_MU273334v1_fix 0 210426 gneg +chr5_MU273353v1_fix 0 208405 gneg +chr8_MU273363v1_fix 0 207371 gneg +chr4_MU273351v1_fix 0 205691 gneg +chr11_KQ759759v2_fix 0 204999 gneg +chr15_MU273375v1_alt 0 204007 gneg +chr10_MU273367v1_fix 0 196262 gneg +chr21_MU273392v1_fix 0 189707 gneg +chr17_MU273382v1_fix 0 187626 gneg +chr2_MU273345v1_fix 0 174385 gneg +chr17_MU273383v1_fix 0 172609 gneg +chr8_MU273359v1_fix 0 150302 gneg +chr17_MU273381v1_fix 0 144689 gneg +chrX_MU273394v1_fix 0 140567 gneg +chr19_MU273385v1_fix 0 137818 gneg +chr11_MU273371v1_fix 0 122722 gneg +chr2_MU273341v1_fix 0 120381 gneg +chr4_MU273350v1_fix 0 113364 gneg +chr8_MU273361v1_fix 0 106905 gneg +chr12_MU273372v1_fix 0 104537 gneg +chr22_KQ759762v2_fix 0 101040 gneg +chr19_MU273387v1_alt 0 89211 gneg +chr16_MU273376v1_fix 0 87715 gneg +chrX_MU273393v1_fix 0 68810 gneg +chr8_MU273360v1_fix 0 39290 gneg +chr5_MU273352v1_fix 0 34400 gneg diff --git a/assets/lrsomatic_report/assets/references/t2t/chrom_lengths.tsv b/assets/lrsomatic_report/assets/references/t2t/chrom_lengths.tsv new file mode 100644 index 00000000..fe82530e --- /dev/null +++ b/assets/lrsomatic_report/assets/references/t2t/chrom_lengths.tsv @@ -0,0 +1,25 @@ +chr1 248387328 +chr2 242696752 +chr3 201105948 +chr4 193574945 +chr5 182045439 +chr6 172126628 +chr7 160567428 +chr8 146259331 +chr9 150617247 +chr10 134758134 +chr11 135127769 +chr12 133324548 +chr13 113566686 +chr14 101161492 +chr15 99753195 +chr16 96330374 +chr17 84276897 +chr18 80542538 +chr19 61707364 +chr20 66210255 +chr21 45090682 +chr22 51324926 +chrX 154259566 +chrY 62460029 +chrM 16569 diff --git a/assets/lrsomatic_report/assets/references/t2t/cytobands.tsv b/assets/lrsomatic_report/assets/references/t2t/cytobands.tsv new file mode 100644 index 00000000..33e192b1 --- /dev/null +++ b/assets/lrsomatic_report/assets/references/t2t/cytobands.tsv @@ -0,0 +1,862 @@ +chr1 0 1735965 p36.33 gneg +chr1 1735965 4816989 p36.32 gpos25 +chr1 4816989 6629068 p36.31 gneg +chr1 6629068 8634052 p36.23 gpos25 +chr1 8634052 12044143 p36.22 gneg +chr1 12044143 15341266 p36.21 gpos50 +chr1 15341266 19923637 p36.13 gneg +chr1 19923637 23434574 p36.12 gpos25 +chr1 23434574 27441306 p36.11 gneg +chr1 27441306 29743244 p35.3 gpos25 +chr1 29743244 32157918 p35.2 gneg +chr1 32157918 34161711 p35.1 gpos25 +chr1 34161711 39468378 p34.3 gneg +chr1 39468378 43570492 p34.2 gpos25 +chr1 43570492 46177222 p34.1 gneg +chr1 46177222 50078974 p33 gpos75 +chr1 50078974 55482505 p32.3 gneg +chr1 55482505 58377946 p32.2 gpos50 +chr1 58377946 60678767 p32.1 gneg +chr1 60678767 68377386 p31.3 gpos50 +chr1 68377386 69177517 p31.2 gneg +chr1 69177517 84240464 p31.1 gpos100 +chr1 84240464 87743231 p22.3 gneg +chr1 87743231 91344645 p22.2 gpos75 +chr1 91344645 94148241 p22.1 gneg +chr1 94148241 99148340 p21.3 gpos75 +chr1 99148340 101649187 p21.2 gneg +chr1 101649187 106737488 p21.1 gpos100 +chr1 106737488 111214805 p13.3 gneg +chr1 111214805 115511187 p13.2 gpos50 +chr1 115511187 117210493 p13.1 gneg +chr1 117210493 120413269 p12 gpos50 +chr1 120413269 121796048 p11.2 gneg +chr1 121796048 124048267 p11.1 acen +chr1 124048267 126300487 q11 acen +chr1 126300487 142241659 q12 gvar +chr1 142241659 147308041 q21.1 gneg +chr1 147308041 149724009 q21.2 gpos50 +chr1 149724009 154239365 q21.3 gneg +chr1 154239365 155736378 q22 gpos50 +chr1 155736378 158237044 q23.1 gneg +chr1 158237044 159637083 q23.2 gpos50 +chr1 159637083 164846435 q23.3 gneg +chr1 164846435 166547042 q24.1 gpos50 +chr1 166547042 170256335 q24.2 gneg +chr1 170256335 172358116 q24.3 gpos75 +chr1 172358116 175455343 q25.1 gneg +chr1 175455343 179655381 q25.2 gpos50 +chr1 179655381 185154686 q25.3 gneg +chr1 185154686 190146214 q31.1 gpos100 +chr1 190146214 193148937 q31.2 gneg +chr1 193148937 197959914 q31.3 gpos100 +chr1 197959914 206365243 q32.1 gneg +chr1 206365243 210545587 q32.2 gpos25 +chr1 210545587 213639518 q32.3 gneg +chr1 213639518 223089723 q41 gpos100 +chr1 223089723 223588804 q42.11 gneg +chr1 223588804 225987880 q42.12 gpos25 +chr1 225987880 229880334 q42.13 gneg +chr1 229880334 233990393 q42.2 gpos50 +chr1 233990393 235800112 q42.3 gneg +chr1 235800112 242911804 q43 gpos75 +chr1 242911804 248387328 q44 gneg +chr2 0 4423386 p25.3 gneg +chr2 4423386 6921497 p25.2 gpos50 +chr2 6921497 12028815 p25.1 gneg +chr2 12028815 16531703 p24.3 gpos75 +chr2 16531703 19032774 p24.2 gneg +chr2 19032774 23835087 p24.1 gpos75 +chr2 23835087 27743068 p23.3 gneg +chr2 27743068 29843598 p23.2 gpos25 +chr2 29843598 31845058 p23.1 gneg +chr2 31845058 36306629 p22.3 gpos75 +chr2 36306629 38306895 p22.2 gneg +chr2 38306895 41509232 p22.1 gpos50 +chr2 41509232 47505052 p21 gneg +chr2 47505052 52596301 p16.3 gpos100 +chr2 52596301 54694000 p16.2 gneg +chr2 54694000 61005834 p16.1 gpos100 +chr2 61005834 63907559 p15 gneg +chr2 63907559 68410570 p14 gpos50 +chr2 68410570 71311026 p13.3 gneg +chr2 71311026 73312998 p13.2 gpos50 +chr2 73312998 74808844 p13.1 gneg +chr2 74808844 83100333 p12 gpos100 +chr2 83100333 92333543 p11.2 gneg +chr2 92333543 93503283 p11.1 acen +chr2 93503283 94673023 q11.1 acen +chr2 94673023 102558292 q11.2 gneg +chr2 102558292 105761211 q12.1 gpos50 +chr2 105761211 107161426 q12.2 gneg +chr2 107161426 109160598 q12.3 gpos25 +chr2 109160598 112626870 q13 gneg +chr2 112626870 118533173 q14.1 gpos50 +chr2 118533173 122035284 q14.2 gneg +chr2 122035284 129528325 q14.3 gpos50 +chr2 129528325 132134645 q21.1 gneg +chr2 132134645 134739485 q21.2 gpos25 +chr2 134739485 136544463 q21.3 gneg +chr2 136544463 141946261 q22.1 gpos100 +chr2 141946261 143848187 q22.2 gneg +chr2 143848187 148350462 q22.3 gpos100 +chr2 148350462 149450376 q23.1 gneg +chr2 149450376 150050446 q23.2 gpos25 +chr2 150050446 154452841 q23.3 gneg +chr2 154452841 159362096 q24.1 gpos75 +chr2 159362096 163356865 q24.2 gneg +chr2 163356865 169374490 q24.3 gpos75 +chr2 169374490 177582181 q31.1 gneg +chr2 177582181 180183173 q31.2 gpos50 +chr2 180183173 182589080 q31.3 gneg +chr2 182589080 188988809 q32.1 gpos75 +chr2 188988809 191589136 q32.2 gneg +chr2 191589136 197084040 q32.3 gpos75 +chr2 197084040 202981065 q33.1 gneg +chr2 202981065 204581908 q33.2 gpos50 +chr2 204581908 208679779 q33.3 gneg +chr2 208679779 214984516 q34 gpos100 +chr2 214984516 221185014 q35 gneg +chr2 221185014 224783144 q36.1 gpos75 +chr2 224783144 225681833 q36.2 gneg +chr2 225681833 230582566 q36.3 gpos100 +chr2 230582566 235189048 q37.1 gneg +chr2 235189048 236890330 q37.2 gpos50 +chr2 236890330 242696752 q37.3 gneg +chr3 0 2794029 p26.3 gpos50 +chr3 2794029 3995951 p26.2 gneg +chr3 3995951 8091216 p26.1 gpos50 +chr3 8091216 11595822 p25.3 gneg +chr3 11595822 13200348 p25.2 gpos25 +chr3 13200348 16301213 p25.1 gneg +chr3 16301213 23804776 p24.3 gpos100 +chr3 23804776 26302605 p24.2 gneg +chr3 26302605 30802486 p24.1 gpos75 +chr3 30802486 32002957 p23 gneg +chr3 32002957 36401366 p22.3 gpos50 +chr3 36401366 39312902 p22.2 gneg +chr3 39312902 43615563 p22.1 gpos75 +chr3 43615563 44115566 p21.33 gneg +chr3 44115566 44215563 p21.32 gpos50 +chr3 44215563 50629881 p21.31 gneg +chr3 50629881 52332899 p21.2 gpos25 +chr3 52332899 54433863 p21.1 gneg +chr3 54433863 58640379 p14.3 gpos50 +chr3 58640379 63843624 p14.2 gneg +chr3 63843624 69736880 p14.1 gpos50 +chr3 69736880 74141615 p13 gneg +chr3 74141615 79855975 p12.3 gpos75 +chr3 79855975 83556432 p12.2 gneg +chr3 83556432 87174355 p12.1 gpos75 +chr3 87174355 91738002 p11.2 gneg +chr3 91738002 94076514 p11.1 acen +chr3 94076514 96415026 q11.1 acen +chr3 96415026 101303688 q11.2 gvar +chr3 101303688 103005343 q12.1 gneg +chr3 103005343 103905942 q12.2 gpos25 +chr3 103905942 105816831 q12.3 gneg +chr3 105816831 109218980 q13.11 gpos75 +chr3 109218980 110919781 q13.12 gneg +chr3 110919781 114320814 q13.13 gpos50 +chr3 114320814 116421198 q13.2 gneg +chr3 116421198 120319753 q13.31 gpos75 +chr3 120319753 122019706 q13.32 gneg +chr3 122019706 124919592 q13.33 gpos75 +chr3 124919592 126826138 q21.1 gneg +chr3 126826138 128832394 q21.2 gpos25 +chr3 128832394 132244475 q21.3 gneg +chr3 132244475 136745163 q22.1 gpos25 +chr3 136745163 138745773 q22.2 gneg +chr3 138745773 141741001 q22.3 gpos25 +chr3 141741001 145847398 q23 gneg +chr3 145847398 151950769 q24 gpos100 +chr3 151950769 155067896 q25.1 gneg +chr3 155067896 158073984 q25.2 gpos50 +chr3 158073984 160074579 q25.31 gneg +chr3 160074579 162074354 q25.32 gpos50 +chr3 162074354 163774734 q25.33 gneg +chr3 163774734 170683909 q26.1 gpos100 +chr3 170683909 173984304 q26.2 gneg +chr3 173984304 178794975 q26.31 gpos75 +chr3 178794975 182103836 q26.32 gneg +chr3 182103836 185805095 q26.33 gpos75 +chr3 185805095 187615802 q27.1 gneg +chr3 187615802 189115680 q27.2 gpos25 +chr3 189115680 191017553 q27.3 gneg +chr3 191017553 195295941 q28 gpos75 +chr3 195295941 201105948 q29 gneg +chr4 0 4469440 p16.3 gneg +chr4 4469440 5971735 p16.2 gpos25 +chr4 5971735 11276065 p16.1 gneg +chr4 11276065 14981780 p15.33 gpos50 +chr4 14981780 17682307 p15.32 gneg +chr4 17682307 21281575 p15.31 gpos75 +chr4 21281575 27685358 p15.2 gneg +chr4 27685358 35768949 p15.1 gpos100 +chr4 35768949 41173799 p14 gneg +chr4 41173799 44566953 p13 gpos50 +chr4 44566953 49705154 p12 gneg +chr4 49705154 52452474 p11 acen +chr4 52452474 55199795 q11 acen +chr4 55199795 61991213 q12 gneg +chr4 61991213 68936809 q13.1 gpos100 +chr4 68936809 72734812 q13.2 gneg +chr4 72734812 78640131 q13.3 gpos75 +chr4 78640131 81340837 q21.1 gneg +chr4 81340837 84829707 q21.21 gpos50 +chr4 84829707 86530115 q21.22 gneg +chr4 86530115 89329480 q21.23 gpos25 +chr4 89329480 90429379 q21.3 gneg +chr4 90429379 96127729 q22.1 gpos75 +chr4 96127729 97515472 q22.2 gneg +chr4 97515472 101214669 q22.3 gpos75 +chr4 101214669 103415497 q23 gneg +chr4 103415497 110005081 q24 gpos50 +chr4 110005081 116508718 q25 gneg +chr4 116508718 123205172 q26 gpos75 +chr4 123205172 126104099 q27 gneg +chr4 126104099 131203026 q28.1 gpos50 +chr4 131203026 133404460 q28.2 gneg +chr4 133404460 141823705 q28.3 gpos100 +chr4 141823705 143919702 q31.1 gneg +chr4 143919702 149216051 q31.21 gpos25 +chr4 149216051 150824125 q31.22 gneg +chr4 150824125 153521681 q31.23 gpos25 +chr4 153521681 157931811 q31.3 gneg +chr4 157931811 164150415 q32.1 gpos100 +chr4 164150415 166947399 q32.2 gneg +chr4 166947399 172559730 q32.3 gpos100 +chr4 172559730 174357423 q33 gneg +chr4 174357423 178738596 q34.1 gpos75 +chr4 178738596 179939101 q34.2 gneg +chr4 179939101 185641813 q34.3 gpos100 +chr4 185641813 189540361 q35.1 gneg +chr4 189540361 193574945 q35.2 gpos25 +chr5 0 4327607 p15.33 gneg +chr5 4327607 6228676 p15.32 gpos25 +chr5 6228676 9839807 p15.31 gneg +chr5 9839807 14939449 p15.2 gpos50 +chr5 14939449 18401838 p15.1 gneg +chr5 18401838 23407694 p14.3 gpos100 +chr5 23407694 24705214 p14.2 gneg +chr5 24705214 29005224 p14.1 gpos100 +chr5 29005224 33919872 p13.3 gneg +chr5 33919872 38649069 p13.2 gpos25 +chr5 38649069 42755507 p13.1 gneg +chr5 42755507 47039134 p12 gpos50 +chr5 47039134 48317879 p11 acen +chr5 48317879 49596625 q11.1 acen +chr5 49596625 60418219 q11.2 gneg +chr5 60418219 64420173 q12.1 gpos75 +chr5 64420173 64720135 q12.2 gneg +chr5 64720135 68222153 q12.3 gpos75 +chr5 68222153 69922486 q13.1 gneg +chr5 69922486 74481328 q13.2 gpos50 +chr5 74481328 78082577 q13.3 gneg +chr5 78082577 82584664 q14.1 gpos50 +chr5 82584664 83988771 q14.2 gneg +chr5 83988771 93484055 q14.3 gpos100 +chr5 93484055 99403284 q15 gneg +chr5 99403284 103908183 q21.1 gpos100 +chr5 103908183 105604211 q21.2 gneg +chr5 105604211 110710493 q21.3 gpos100 +chr5 110710493 112710455 q22.1 gneg +chr5 112710455 114312885 q22.2 gpos50 +chr5 114312885 116412397 q22.3 gneg +chr5 116412397 122616882 q23.1 gpos100 +chr5 122616882 128419736 q23.2 gneg +chr5 128419736 131718996 q23.3 gpos100 +chr5 131718996 137422646 q31.1 gneg +chr5 137422646 140625098 q31.2 gpos25 +chr5 140625098 145634538 q31.3 gneg +chr5 145634538 150936541 q32 gpos75 +chr5 150936541 153833148 q33.1 gneg +chr5 153833148 156818817 q33.2 gpos50 +chr5 156818817 161028563 q33.3 gneg +chr5 161028563 169535680 q34 gpos100 +chr5 169535680 173840079 q35.1 gneg +chr5 173840079 177643208 q35.2 gpos25 +chr5 177643208 182045439 q35.3 gneg +chr6 0 2163637 p25.3 gneg +chr6 2163637 4069276 p25.2 gpos25 +chr6 4069276 6969513 p25.1 gneg +chr6 6969513 10467733 p24.3 gpos50 +chr6 10467733 11468370 p24.2 gneg +chr6 11468370 13273264 p24.1 gpos25 +chr6 13273264 15073193 p23 gneg +chr6 15073193 25065737 p22.3 gpos75 +chr6 25065737 26968681 p22.2 gneg +chr6 26968681 30364188 p22.1 gpos50 +chr6 30364188 31953196 p21.33 gneg +chr6 31953196 33321362 p21.32 gpos25 +chr6 33321362 36420643 p21.31 gneg +chr6 36420643 40327879 p21.2 gpos25 +chr6 40327879 46035128 p21.1 gneg +chr6 46035128 51643048 p12.3 gpos100 +chr6 51643048 52839576 p12.2 gneg +chr6 52839576 57039025 p12.1 gpos100 +chr6 57039025 58286706 p11.2 gneg +chr6 58286706 59672548 p11.1 acen +chr6 59672548 61058390 q11.1 acen +chr6 61058390 63845066 q11.2 gneg +chr6 63845066 70378649 q12 gpos100 +chr6 70378649 76377151 q13 gneg +chr6 76377151 84423242 q14.1 gpos50 +chr6 84423242 85423294 q14.2 gneg +chr6 85423294 88508804 q14.3 gpos50 +chr6 88508804 93711666 q15 gneg +chr6 93711666 100074369 q16.1 gpos100 +chr6 100074369 101173694 q16.2 gneg +chr6 101173694 106176013 q16.3 gpos100 +chr6 106176013 115383115 q21 gneg +chr6 115383115 119084265 q22.1 gpos75 +chr6 119084265 119285071 q22.2 gneg +chr6 119285071 126988674 q22.31 gpos100 +chr6 126988674 127988742 q22.32 gneg +chr6 127988742 131194763 q22.33 gpos75 +chr6 131194763 132095087 q23.1 gneg +chr6 132095087 135888233 q23.2 gpos50 +chr6 135888233 139488489 q23.3 gneg +chr6 139488489 143392072 q24.1 gpos75 +chr6 143392072 146292619 q24.2 gneg +chr6 146292619 149696304 q24.3 gpos75 +chr6 149696304 153301308 q25.1 gneg +chr6 153301308 156401943 q25.2 gpos50 +chr6 156401943 161852984 q25.3 gneg +chr6 161852984 165464819 q26 gpos50 +chr6 165464819 172126628 q27 gneg +chr7 0 2913569 p22.3 gneg +chr7 2913569 4616674 p22.2 gpos25 +chr7 4616674 7319206 p22.1 gneg +chr7 7319206 13832042 p21.3 gpos100 +chr7 13832042 16629717 p21.2 gneg +chr7 16629717 21036083 p21.1 gpos100 +chr7 21036083 25635438 p15.3 gneg +chr7 25635438 28037762 p15.2 gpos50 +chr7 28037762 28937489 p15.1 gneg +chr7 28937489 35040677 p14.3 gpos75 +chr7 35040677 37240409 p14.2 gneg +chr7 37240409 43458167 p14.1 gpos75 +chr7 43458167 45560768 p13 gneg +chr7 45560768 49160858 p12.3 gpos75 +chr7 49160858 50661224 p12.2 gneg +chr7 50661224 54061627 p12.1 gpos75 +chr7 54061627 60414372 p11.2 gneg +chr7 60414372 62064435 p11.1 acen +chr7 62064435 63714499 q11.1 acen +chr7 63714499 68720114 q11.21 gneg +chr7 68720114 73918537 q11.22 gpos50 +chr7 73918537 79151921 q11.23 gneg +chr7 79151921 87949894 q21.11 gpos100 +chr7 87949894 89750628 q21.12 gneg +chr7 89750628 92747879 q21.13 gpos75 +chr7 92747879 94542054 q21.2 gneg +chr7 94542054 99630796 q21.3 gpos75 +chr7 99630796 105514255 q22.1 gneg +chr7 105514255 106214835 q22.2 gpos50 +chr7 106214835 109118351 q22.3 gneg +chr7 109118351 116314793 q31.1 gpos75 +chr7 116314793 119015389 q31.2 gneg +chr7 119015389 122715324 q31.31 gpos75 +chr7 122715324 125416714 q31.32 gneg +chr7 125416714 128811644 q31.33 gpos75 +chr7 128811644 130913119 q32.1 gneg +chr7 130913119 132117533 q32.2 gpos25 +chr7 132117533 134221510 q32.3 gneg +chr7 134221510 139809728 q33 gpos50 +chr7 139809728 144755427 q34 gneg +chr7 144755427 149379963 q35 gpos75 +chr7 149379963 153973252 q36.1 gneg +chr7 153973252 156375514 q36.2 gpos25 +chr7 156375514 160567428 q36.3 gneg +chr8 0 2084125 p23.3 gneg +chr8 2084125 6054502 p23.2 gpos75 +chr8 6054502 13066163 p23.1 gneg +chr8 13066163 19465021 p22 gpos100 +chr8 19465021 23774881 p21.3 gneg +chr8 23774881 27777347 p21.2 gpos50 +chr8 27777347 29278158 p21.1 gneg +chr8 29278158 36976053 p12 gpos75 +chr8 36976053 38776976 p11.23 gneg +chr8 38776976 40177151 p11.22 gpos25 +chr8 40177151 44215832 p11.21 gneg +chr8 44215832 45270456 p11.1 acen +chr8 45270456 46325080 q11.1 acen +chr8 46325080 51673061 q11.21 gneg +chr8 51673061 52073547 q11.22 gpos75 +chr8 52073547 54977464 q11.23 gneg +chr8 54977464 61023743 q12.1 gpos50 +chr8 61023743 61723740 q12.2 gneg +chr8 61723740 65525777 q12.3 gpos50 +chr8 65525777 67526469 q13.1 gneg +chr8 67526469 70029827 q13.2 gpos50 +chr8 70029827 72435109 q13.3 gneg +chr8 72435109 75029437 q21.11 gpos100 +chr8 75029437 75129430 q21.12 gneg +chr8 75129430 83931590 q21.13 gpos75 +chr8 83931590 87017433 q21.2 gneg +chr8 87017433 93425090 q21.3 gpos100 +chr8 93425090 99025482 q22.1 gneg +chr8 99025482 101626041 q22.2 gpos25 +chr8 101626041 106227505 q22.3 gneg +chr8 106227505 110628554 q23.1 gpos75 +chr8 110628554 112228628 q23.2 gneg +chr8 112228628 117826624 q23.3 gpos100 +chr8 117826624 119428452 q24.11 gneg +chr8 119428452 122630227 q24.12 gpos50 +chr8 122630227 127427152 q24.13 gneg +chr8 127427152 131526643 q24.21 gpos50 +chr8 131526643 136517853 q24.22 gneg +chr8 136517853 140019529 q24.23 gpos75 +chr8 140019529 146259331 q24.3 gneg +chr9 0 2202472 p24.3 gneg +chr9 2202472 4603652 p24.2 gpos25 +chr9 4603652 9006617 p24.1 gneg +chr9 9006617 14209493 p23 gpos75 +chr9 14209493 16611825 p22.3 gneg +chr9 16611825 18513245 p22.2 gpos25 +chr9 18513245 19913875 p22.1 gneg +chr9 19913875 25610359 p21.3 gpos100 +chr9 25610359 28010599 p21.2 gneg +chr9 28010599 33218700 p21.1 gpos100 +chr9 33218700 36322011 p13.3 gneg +chr9 36322011 37923910 p13.2 gpos25 +chr9 37923910 39013701 p13.1 gneg +chr9 39013701 40014198 p12 gpos50 +chr9 40014198 44951775 p11.2 gneg +chr9 44951775 46267185 p11.1 acen +chr9 46267185 47582595 q11 acen +chr9 47582595 76694047 q12 gvar +chr9 76694047 77166639 q13 gneg +chr9 77166639 81466639 q21.11 gpos25 +chr9 81466639 83449676 q21.12 gneg +chr9 83449676 88756405 q21.13 gpos50 +chr9 88756405 90657183 q21.2 gneg +chr9 90657183 93652149 q21.31 gpos50 +chr9 93652149 96450330 q21.32 gneg +chr9 96450330 99953385 q21.33 gpos50 +chr9 99953385 101362780 q22.1 gneg +chr9 101362780 103366144 q22.2 gpos25 +chr9 103366144 106067989 q22.31 gneg +chr9 106067989 108671778 q22.32 gpos25 +chr9 108671778 111971620 q22.33 gneg +chr9 111971620 117574632 q31.1 gpos100 +chr9 117574632 120669232 q31.2 gneg +chr9 120669232 124271384 q31.3 gpos25 +chr9 124271384 127092454 q32 gneg +chr9 127092454 131993831 q33.1 gpos75 +chr9 131993831 135297230 q33.2 gneg +chr9 135297230 139706950 q33.3 gpos25 +chr9 139706950 142804928 q34.11 gneg +chr9 142804928 143308266 q34.12 gpos25 +chr9 143308266 145314130 q34.13 gneg +chr9 145314130 146716800 q34.2 gpos25 +chr9 146716800 150617247 q34.3 gneg +chr10 0 2999810 p15.3 gneg +chr10 2999810 3803899 p15.2 gpos25 +chr10 3803899 6600140 p15.1 gneg +chr10 6600140 12211271 p14 gpos75 +chr10 12211271 17318428 p13 gneg +chr10 17318428 18319354 p12.33 gpos75 +chr10 18319354 18419356 p12.32 gneg +chr10 18419356 22319034 p12.31 gpos75 +chr10 22319034 24318538 p12.2 gneg +chr10 24318538 29331681 p12.1 gpos50 +chr10 29331681 31131269 p11.23 gneg +chr10 31131269 34228529 p11.22 gpos25 +chr10 34228529 39633793 p11.21 gneg +chr10 39633793 40649191 p11.1 acen +chr10 40649191 41664589 q11.1 acen +chr10 41664589 46381004 q11.21 gneg +chr10 46381004 49449041 q11.22 gpos25 +chr10 49449041 51948621 q11.23 gneg +chr10 51948621 60254318 q21.1 gpos100 +chr10 60254318 63658144 q21.2 gneg +chr10 63658144 69669152 q21.3 gpos100 +chr10 69669152 73971392 q22.1 gneg +chr10 73971392 76774110 q22.2 gpos50 +chr10 76774110 81169099 q22.3 gneg +chr10 81169099 86980090 q23.1 gpos100 +chr10 86980090 88583858 q23.2 gneg +chr10 88583858 91983671 q23.31 gpos75 +chr10 91983671 93182363 q23.32 gneg +chr10 93182363 96179180 q23.33 gpos50 +chr10 96179180 98380193 q24.1 gneg +chr10 98380193 100984294 q24.2 gpos50 +chr10 100984294 102083356 q24.31 gneg +chr10 102083356 103986242 q24.32 gpos25 +chr10 103986242 104887391 q24.33 gneg +chr10 104887391 110983867 q25.1 gpos100 +chr10 110983867 113991312 q25.2 gneg +chr10 113991312 118194477 q25.3 gpos75 +chr10 118194477 120797529 q26.11 gneg +chr10 120797529 122296091 q26.12 gpos50 +chr10 122296091 126582419 q26.13 gneg +chr10 126582419 129725246 q26.2 gpos50 +chr10 129725246 134758134 q26.3 gneg +chr11 0 2889344 p15.5 gneg +chr11 2889344 11789203 p15.4 gpos50 +chr11 11789203 13892569 p15.3 gneg +chr11 13892569 16997372 p15.2 gpos50 +chr11 16997372 22120532 p15.1 gneg +chr11 22120532 26340879 p14.3 gpos100 +chr11 26340879 27340466 p14.2 gneg +chr11 27340466 31136090 p14.1 gpos75 +chr11 31136090 36542448 p13 gneg +chr11 36542448 43555411 p12 gpos100 +chr11 43555411 48958062 p11.2 gneg +chr11 48958062 51035789 p11.12 gpos75 +chr11 51035789 52743313 p11.11 acen +chr11 52743313 54450838 q11 acen +chr11 54450838 60051280 q12.1 gpos75 +chr11 60051280 61888804 q12.2 gneg +chr11 61888804 63589288 q12.3 gpos25 +chr11 63589288 66094130 q13.1 gneg +chr11 66094130 68706120 q13.2 gpos25 +chr11 68706120 70520423 q13.3 gneg +chr11 70520423 75429506 q13.4 gpos50 +chr11 75429506 77332929 q13.5 gneg +chr11 77332929 85836684 q14.1 gpos100 +chr11 85836684 88519134 q14.2 gneg +chr11 88519134 92928863 q14.3 gpos100 +chr11 92928863 97406624 q21 gneg +chr11 97406624 102302118 q22.1 gpos100 +chr11 102302118 103003764 q22.2 gneg +chr11 103003764 110610246 q22.3 gpos100 +chr11 110610246 112710438 q23.1 gneg +chr11 112710438 114610447 q23.2 gpos50 +chr11 114610447 121325059 q23.3 gneg +chr11 121325059 124028615 q24.1 gpos50 +chr11 124028615 127933253 q24.2 gneg +chr11 127933253 130935758 q24.3 gpos50 +chr11 130935758 135127769 q25 gneg +chr12 0 3215744 p13.33 gneg +chr12 3215744 5306179 p13.32 gpos25 +chr12 5306179 9886155 p13.31 gneg +chr12 9886155 12469144 p13.2 gpos75 +chr12 12469144 14477435 p13.1 gneg +chr12 14477435 19678338 p12.3 gpos100 +chr12 19678338 20978622 p12.2 gneg +chr12 20978622 26172544 p12.1 gpos100 +chr12 26172544 27471672 p11.23 gneg +chr12 27471672 30374680 p11.22 gpos50 +chr12 30374680 34620838 p11.21 gneg +chr12 34620838 35911664 p11.1 acen +chr12 35911664 37202490 q11 acen +chr12 37202490 45959545 q12 gpos100 +chr12 45959545 48662027 q13.11 gneg +chr12 48662027 51062917 q13.12 gpos25 +chr12 51062917 54466577 q13.13 gneg +chr12 54466577 56167601 q13.2 gpos25 +chr12 56167601 57668365 q13.3 gneg +chr12 57668365 62678775 q14.1 gpos75 +chr12 62678775 64679035 q14.2 gneg +chr12 64679035 67279394 q14.3 gpos50 +chr12 67279394 71079501 q15 gneg +chr12 71079501 75274601 q21.1 gpos75 +chr12 75274601 79878671 q21.2 gneg +chr12 79878671 86280942 q21.31 gpos100 +chr12 86280942 88581935 q21.32 gneg +chr12 88581935 92177463 q21.33 gpos100 +chr12 92177463 95779143 q22 gneg +chr12 95779143 101161710 q23.1 gpos75 +chr12 101161710 103460805 q23.2 gneg +chr12 103460805 108574599 q23.3 gpos50 +chr12 108574599 111279651 q24.11 gneg +chr12 111279651 111876909 q24.12 gpos25 +chr12 111876909 113875849 q24.13 gneg +chr12 113875849 116381153 q24.21 gpos50 +chr12 116381153 117687226 q24.22 gneg +chr12 117687226 120287197 q24.23 gpos50 +chr12 120287197 125408188 q24.31 gneg +chr12 125408188 128730796 q24.32 gpos50 +chr12 128730796 133324548 q24.33 gneg +chr13 0 5751447 p13 gvar +chr13 5751447 9368750 p12 stalk +chr13 9368750 15547593 p11.2 gvar +chr13 15547593 16522942 p11.1 acen +chr13 16522942 17498291 q11 acen +chr13 17498291 21797623 q12.11 gneg +chr13 21797623 24108235 q12.12 gpos25 +chr13 24108235 26420720 q12.13 gneg +chr13 26420720 27522230 q12.2 gpos25 +chr13 27522230 30823418 q12.3 gneg +chr13 30823418 32617334 q13.1 gpos50 +chr13 32617334 34118269 q13.2 gneg +chr13 34118269 38718394 q13.3 gpos75 +chr13 38718394 43819777 q14.11 gneg +chr13 43819777 44420051 q14.12 gpos25 +chr13 44420051 45921251 q14.13 gneg +chr13 45921251 49520618 q14.2 gpos50 +chr13 49520618 53916044 q14.3 gneg +chr13 53916044 58219746 q21.1 gpos100 +chr13 58219746 61020124 q21.2 gneg +chr13 61020124 64418827 q21.31 gpos75 +chr13 64418827 67322320 q21.32 gneg +chr13 67322320 72021474 q21.33 gpos100 +chr13 72021474 74123996 q22.1 gneg +chr13 74123996 75923712 q22.2 gpos50 +chr13 75923712 77725243 q22.3 gneg +chr13 77725243 86303655 q31.1 gpos100 +chr13 86303655 88604095 q31.2 gneg +chr13 88604095 93603590 q31.3 gpos100 +chr13 93603590 96706317 q32.1 gneg +chr13 96706317 97909939 q32.2 gpos25 +chr13 97909939 100315089 q32.3 gneg +chr13 100315089 103418692 q33.1 gpos100 +chr13 103418692 105620405 q33.2 gneg +chr13 105620405 108828306 q33.3 gpos100 +chr13 108828306 113566686 q34 gneg +chr14 0 2077628 p13 gvar +chr14 2077628 2840421 p12 stalk +chr14 2840421 10092112 p11.2 gvar +chr14 10092112 11400261 p11.1 acen +chr14 11400261 12708411 q11.1 acen +chr14 12708411 18298312 q11.2 gneg +chr14 18298312 27096896 q12 gpos100 +chr14 27096896 28999171 q13.1 gneg +chr14 28999171 30288797 q13.2 gpos50 +chr14 30288797 31589055 q13.3 gneg +chr14 31589055 37191028 q21.1 gpos100 +chr14 37191028 40898832 q21.2 gneg +chr14 40898832 44606207 q21.3 gpos100 +chr14 44606207 47808271 q22.1 gneg +chr14 47808271 49205176 q22.2 gpos25 +chr14 49205176 51806957 q22.3 gneg +chr14 51806957 55806586 q23.1 gpos75 +chr14 55806586 58507715 q23.2 gneg +chr14 58507715 61607123 q23.3 gpos50 +chr14 61607123 64007668 q24.1 gneg +chr14 64007668 67506518 q24.2 gpos50 +chr14 67506518 73008616 q24.3 gneg +chr14 73008616 77312020 q31.1 gpos100 +chr14 77312020 78613483 q31.2 gneg +chr14 78613483 83523641 q31.3 gpos100 +chr14 83523641 85625240 q32.11 gneg +chr14 85625240 88427273 q32.12 gpos25 +chr14 88427273 90031477 q32.13 gneg +chr14 90031477 95135433 q32.2 gpos50 +chr14 95135433 96936346 q32.31 gneg +chr14 96936346 97736032 q32.32 gpos50 +chr14 97736032 101161492 q32.33 gneg +chr15 0 2484618 p13 gvar +chr15 2484618 4728636 p12 stalk +chr15 4728636 16678794 p11.2 gvar +chr15 16678794 17186630 p11.1 acen +chr15 17186630 17694466 q11.1 acen +chr15 17694466 23236963 q11.2 gneg +chr15 23236963 25542506 q12 gpos50 +chr15 25542506 27791711 q13.1 gneg +chr15 27791711 28693986 q13.2 gpos50 +chr15 28693986 31196487 q13.3 gneg +chr15 31196487 37605195 q14 gpos75 +chr15 37605195 40306040 q15.1 gneg +chr15 40306040 41107300 q15.2 gpos25 +chr15 41107300 42307677 q15.3 gneg +chr15 42307677 47008200 q21.1 gpos75 +chr15 47008200 50408265 q21.2 gneg +chr15 50408265 56602018 q21.3 gpos75 +chr15 56602018 56802078 q22.1 gneg +chr15 56802078 61204679 q22.2 gpos25 +chr15 61204679 64721821 q22.31 gneg +chr15 64721821 64821874 q22.32 gpos25 +chr15 64821874 65021911 q22.33 gneg +chr15 65021911 70217331 q23 gpos25 +chr15 70217331 72769881 q24.1 gneg +chr15 72769881 74171079 q24.2 gpos25 +chr15 74171079 75862542 q24.3 gneg +chr15 75862542 79264071 q25.1 gpos50 +chr15 79264071 82452409 q25.2 gneg +chr15 82452409 86254599 q25.3 gpos50 +chr15 86254599 91561911 q26.1 gneg +chr15 91561911 95765652 q26.2 gpos50 +chr15 95765652 99753195 q26.3 gneg +chr16 0 7831635 p13.3 gneg +chr16 7831635 10435669 p13.2 gpos50 +chr16 10435669 12537038 p13.13 gneg +chr16 12537038 14730876 p13.12 gpos50 +chr16 14730876 16712187 p13.11 gneg +chr16 16712187 21130950 p12.3 gpos50 +chr16 21130950 24476520 p12.2 gneg +chr16 24476520 28752507 p12.1 gpos50 +chr16 28752507 35848286 p11.2 gneg +chr16 35848286 36838903 p11.1 acen +chr16 36838903 37829521 q11.1 acen +chr16 37829521 52219471 q11.2 gvar +chr16 52219471 58397876 q12.1 gneg +chr16 58397876 61794998 q12.2 gpos50 +chr16 61794998 63095972 q13 gneg +chr16 63095972 72394295 q21 gpos100 +chr16 72394295 76611172 q22.1 gneg +chr16 76611172 78617811 q22.2 gpos50 +chr16 78617811 79917729 q22.3 gneg +chr16 79917729 85256228 q23.1 gpos75 +chr16 85256228 87661662 q23.2 gneg +chr16 87661662 90166083 q23.3 gpos50 +chr16 90166083 93070234 q24.1 gneg +chr16 93070234 94769136 q24.2 gpos25 +chr16 94769136 96330374 q24.3 gneg +chr17 0 3288940 p13.3 gneg +chr17 3288940 6400012 p13.2 gpos50 +chr17 6400012 10707908 p13.1 gneg +chr17 10707908 16002309 p12 gpos75 +chr17 16002309 23892419 p11.2 gneg +chr17 23892419 25689679 p11.1 acen +chr17 25689679 27486939 q11.1 acen +chr17 27486939 34446012 q11.2 gneg +chr17 34446012 40663561 q12 gpos50 +chr17 40663561 41063885 q21.1 gneg +chr17 41063885 43657141 q21.2 gpos25 +chr17 43657141 47661458 q21.31 gneg +chr17 47661458 50163080 q21.32 gpos25 +chr17 50163080 52967757 q21.33 gneg +chr17 52967757 60368167 q22 gpos75 +chr17 60368167 61068949 q23.1 gneg +chr17 61068949 63970246 q23.2 gpos75 +chr17 63970246 65469760 q23.3 gneg +chr17 65469760 67070785 q24.1 gpos50 +chr17 67070785 69976574 q24.2 gneg +chr17 69976574 73787784 q24.3 gpos75 +chr17 73787784 77692007 q25.1 gneg +chr17 77692007 78093613 q25.2 gpos25 +chr17 78093613 84276897 q25.3 gneg +chr18 0 3055502 p11.32 gneg +chr18 3055502 7363114 p11.31 gpos50 +chr18 7363114 8663156 p11.23 gneg +chr18 8663156 11062106 p11.22 gpos25 +chr18 11062106 15965699 p11.21 gneg +chr18 15965699 18449624 p11.1 acen +chr18 18449624 20933550 q11.1 acen +chr18 20933550 27694024 q11.2 gneg +chr18 27694024 35291319 q12.1 gpos100 +chr18 35291319 39695477 q12.2 gneg +chr18 39695477 46090953 q12.3 gpos75 +chr18 46090953 50901745 q21.1 gneg +chr18 50901745 56402692 q21.2 gpos75 +chr18 56402692 58801152 q21.31 gneg +chr18 58801152 61503246 q21.32 gpos50 +chr18 61503246 64105093 q21.33 gneg +chr18 64105093 69316653 q22.1 gpos100 +chr18 69316653 71221701 q22.2 gneg +chr18 71221701 75629292 q22.3 gpos25 +chr18 75629292 80542538 q23 gneg +chr19 0 6889318 p13.3 gneg +chr19 6889318 12724341 p13.2 gpos25 +chr19 12724341 13926177 p13.13 gneg +chr19 13926177 16234072 p13.12 gpos25 +chr19 16234072 20037734 p13.11 gneg +chr19 20037734 25817676 p12 gvar +chr19 25817676 27792923 p11 acen +chr19 27792923 29768171 q11 acen +chr19 29768171 34418519 q12 gvar +chr19 34418519 37644588 q13.11 gneg +chr19 37644588 40601536 q13.12 gpos25 +chr19 40601536 41002157 q13.13 gneg +chr19 41002157 45718957 q13.2 gpos25 +chr19 45718957 47524747 q13.31 gneg +chr19 47524747 50330819 q13.32 gpos25 +chr19 50330819 53989630 q13.33 gneg +chr19 53989630 56182556 q13.41 gpos25 +chr19 56182556 58899358 q13.42 gneg +chr19 58899358 61707364 q13.43 gpos25 +chr20 0 5139422 p13 gneg +chr20 5139422 9242915 p12.3 gpos75 +chr20 9242915 12043277 p12.2 gneg +chr20 12043277 17951106 p12.1 gpos75 +chr20 17951106 21359369 p11.23 gneg +chr20 21359369 22357561 p11.22 gpos25 +chr20 22357561 26925852 p11.21 gneg +chr20 26925852 28012753 p11.1 acen +chr20 28012753 29099655 q11.1 acen +chr20 29099655 35226553 q11.21 gneg +chr20 35226553 37520975 q11.22 gpos25 +chr20 37520975 40730034 q11.23 gneg +chr20 40730034 44834038 q12 gpos75 +chr20 44834038 45233263 q13.11 gneg +chr20 45233263 49539094 q13.12 gpos25 +chr20 49539094 52970605 q13.13 gneg +chr20 52970605 58177493 q13.2 gpos75 +chr20 58177493 59578060 q13.31 gneg +chr20 59578060 61484264 q13.32 gpos50 +chr20 61484264 66210255 q13.33 gneg +chr21 0 3084882 p13 gvar +chr21 3084882 5633495 p12 stalk +chr21 5633495 10962853 p11.2 gvar +chr21 10962853 11134529 p11.1 acen +chr21 11134529 11306205 q11.1 acen +chr21 11306205 13355188 q11.2 gneg +chr21 13355188 20956835 q21.1 gpos100 +chr21 20956835 23857586 q21.2 gneg +chr21 23857586 28565933 q21.3 gpos75 +chr21 28565933 32782056 q22.11 gneg +chr21 32782056 34782581 q22.12 gpos50 +chr21 34782581 36683433 q22.13 gneg +chr21 36683433 39588333 q22.2 gpos50 +chr21 39588333 45090682 q22.3 gneg +chr22 0 4770731 p13 gvar +chr22 4770731 5743502 p12 stalk +chr22 5743502 12788180 p11.2 gvar +chr22 12788180 14249622 p11.1 acen +chr22 14249622 15711065 q11.1 acen +chr22 15711065 22113480 q11.21 gneg +chr22 22113480 23522872 q11.22 gpos25 +chr22 23522872 25961147 q11.23 gneg +chr22 25961147 29663505 q12.1 gpos50 +chr22 29663505 32264007 q12.2 gneg +chr22 32264007 37659920 q12.3 gpos50 +chr22 37659920 41071957 q13.1 gneg +chr22 41071957 44282889 q13.2 gpos50 +chr22 44282889 48592476 q13.31 gneg +chr22 48592476 49604335 q13.32 gpos50 +chr22 49604335 51324926 q13.33 gneg +chrX 0 3944795 p22.33 gneg +chrX 3944795 5652276 p22.32 gpos50 +chrX 5652276 9182594 p22.31 gneg +chrX 9182594 16982597 p22.2 gpos50 +chrX 16982597 18782737 p22.13 gneg +chrX 18782737 21483312 p22.12 gpos50 +chrX 21483312 24484237 p22.11 gneg +chrX 24484237 28892068 p21.3 gpos100 +chrX 28892068 31098156 p21.2 gneg +chrX 31098156 37203573 p21.1 gpos100 +chrX 37203573 41906024 p11.4 gneg +chrX 41906024 47009888 p11.3 gpos75 +chrX 47009888 49417662 p11.23 gneg +chrX 49417662 54090881 p11.22 gpos25 +chrX 54090881 57820107 p11.21 gneg +chrX 57820107 59373565 p11.1 acen +chrX 59373565 60927025 q11.1 acen +chrX 60927025 63825591 q11.2 gneg +chrX 63825591 66933457 q12 gpos50 +chrX 66933457 71433508 q13.1 gneg +chrX 71433508 73133628 q13.2 gpos50 +chrX 73133628 75233853 q13.3 gneg +chrX 75233853 83828518 q21.1 gpos100 +chrX 83828518 85427981 q21.2 gneg +chrX 85427981 91150234 q21.31 gpos100 +chrX 91150234 92745170 q21.32 gneg +chrX 92745170 97541699 q21.33 gpos75 +chrX 97541699 101746979 q22.1 gneg +chrX 101746979 102931146 q22.2 gpos50 +chrX 102931146 107847498 q22.3 gneg +chrX 107847498 115776569 q23 gpos75 +chrX 115776569 120099927 q24 gneg +chrX 120099927 127818782 q25 gpos100 +chrX 127818782 129624103 q26.1 gneg +chrX 129624103 132825159 q26.2 gpos25 +chrX 132825159 137210433 q26.3 gneg +chrX 137210433 139517370 q27.1 gpos75 +chrX 139517370 141308643 q27.2 gneg +chrX 141308643 146265526 q27.3 gpos100 +chrX 146265526 154259566 q28 gneg +chrY 0 127375 p11.32 gneg +chrY 127375 454123 p11.31 gpos50 +chrY 454123 10565750 p11.2 gneg +chrY 10565750 10724418 p11.1 acen +chrY 10724418 10883085 q11.1 acen +chrY 10883085 13307633 q11.21 gneg +chrY 13307633 18006518 q11.221 gpos50 +chrY 18006518 20506564 q11.222 gneg +chrY 20506564 25062557 q11.223 gpos50 +chrY 25062557 27449937 q11.23 gneg +chrY 27449937 62460029 q12 gvar diff --git a/assets/lrsomatic_report/assets/styles/_fonts.scss b/assets/lrsomatic_report/assets/styles/_fonts.scss new file mode 100644 index 00000000..fe61a495 --- /dev/null +++ b/assets/lrsomatic_report/assets/styles/_fonts.scss @@ -0,0 +1,85 @@ +/* AUTO-GENERATED: base64-embedded latin-subset webfonts. Do not edit by hand. */ + +@font-face { + + font-family: 'Spectral'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAAFmYABEAAAAA35wAAFkzAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGoIaG90KHJY0BmAAhGAIgWQJnAwRCAqCgnSB5WULhFoAATYCJAOJMAQgBYQqB4tSDIFvG4nNB8TTThC/24H2tqO2qmcHYreDd1xtn69g4yo26A6H1POp2eD//7ykEce20XYPKT2pglWSgqlNrRA7dtAQlOdpxtImryunsKJSCBWMutP2CD11EztZeEhe6E80NxU4etbHjg1h42ER65IZ7XxfeIsyOhebzTZ9UIiucTp/fxT+85XBF5vNHqk+h1hCgbdE2DKOtQ3DgzvtP1uepzgyrWKGlSkj9IEt6rWETPEtnze6DCjj1oORHa0jV558ef77tf5d+9ye7pl5PwCIklGYlI8wL1HI5ckBlSe8P0/bfO/fcUSKiFHYjdHYrJirMHKK0at0HbJi2W2vTatKtTKzqrq6Z4bEQpT8OmtG8iACUihGWAdhX3v2ycAH/L3vecPzc/rPSghcSpMTh5jeuBC7MYEYFiRoVaitrhQqPq+YbE71d1aZUllF3+/0UlfVkiXbY+IBtAeAe5ahap8g/PSTZDdLDvPjn+ePQZ37/mSTjqZW2mxZFS7hAmdZmRRtf+iB+/nux5JgoMMAyiow3u/8VVu0pVrlctWaKTtKGP0ck+NnXx6BBA4m2+w/BAqLlMSRgCdIRemKMu9wvMkX+NovNnKPHbjFzylfODgEkS5agiUacje18fvVSn4BTUEV7SPOioWNiFwn53teLBWdndam0uGl1zvlb9lOmejNDkQ7LCMF8N/f4CLe7/u2rGpYea+KydmaRERZYgIk3tUF03uQOTGd/R1+w0bVflrXSxVgPP/0o1mbtfQBerNDEJvoX0IAAk0rNiHnxMwbM4wxgwj3+5wNPfyEOULKFTAFY2s1ed2Dec1+lbJQH0noA2+E/6/ulYcpZRWu+so+doECKFMR9IpSvvOHOcOsbbpyGAxY245QXsxQD51EAl6NNCAAgUDof2581vYxhKJg6lfvyVgJC02/Z1qz8zOZd5tbyg6t52hdWA4heR4js9k6myvkqOGoiwq1yKM0IXHN2FarQhiQGInn/6aldL/+jYs2be0O6+NpfO98Tqlrp8Iwv9wLMrJmpPvekbTr0telVJS2o5FO9c5J30vhrQBkO7BWnBcAKwprDYfw4ABUWIJYcF5gOMjfm6q1/++SEkDZHoh0oBzpmIuGDiHVLuqb2X3/L3bxdwkCuyQFLEgKWFAy84EgLVGrBAKgTYIKlHOULkq+yxWJVQRFHyRdTrnornNRXumyvdSFNP1VAsHYGKY5RM1j2lCkSNx+3/lm9Skz0s8cCcYrhBFGvYx73KGE+ds6RMyNrC13X+o1EVlEJEgYQpBBZJel7bg51F5ZI0ACxfH32M/y143eJnGvdAsoFhSUgRnElr/kPd83nXyHscyylS0UN8+hvQa4pzLjnxREPrA28D9bYqnUVNTUwqj5c+ebExizwEDQMLmpFMlYqZDJTzsdYYLBQBw8SEAGGTODLDA9smINOSpS5lB0vmmsOE3QSzX66QADdJDBlgZFBYq0iioU1SplllLmKWVBjrIoD11m+ZaIAGs4QcykzZqwBcFvswwCS+6SUSeerE4F2Qs1pQUgezmtXAsyNKB3ExDkjptKtSDfB9zEDt1dCCr9p0BgWDnaxud3PDQQHJiLCYFZRMA1ONxwQw85IMVIQ5d/WrNV77O3nnvodhauZuu8kw7bazs9nRUWmeO86SYarZxWrnRD9TcHWl0XtLEumLE57DCk5cw3LCY5POmm50si+jL8JtNNp6uHt0nnihm6HI4rMBkNk8upLst5iVlW5RzBQajAFGPNEldgrDQ9Krvu1gEX4WCcboz/ReSKwcHFIyAkIiZhwJCMMROmzFiwYs2RUrRYcRIkSpJMradeNPpK1c8AgwyWpoBWBQbYE9gZ2JZ3bw5726YCzLdwO2iC+qa9xZVZ8Ybxn0aHmCkNPiXIIMDffmn01XsvPXbXdReddtR+O222zipLzFNrslXGqlQsX6bhBtJQQyt2ENoIRAeQhPXvMBZAPAfiGRl6MzIzJ8t0GI2bDL9yhGhmcMtajMHlVOJ2HISmOA2X4wZ8iFnGyrLrRl05qzET5+J8Y9QdcWgs57QUHLylY1XiICXdQzg470ZgTMDPPFqO4CDTVzxkVo8G086jiSkro61pjcsyvmDgNkpIcujiIyGycBPFm5fGlogMGtVBlMU70kdnzoiWRafeQ2Wi2rlTFRiNjxZRc8sAhsgTWn0VoAaz4DGzYRiH11npKtpkwZ6B3xy1VWCD0fFJ2irZ0GCv30+/EW9csBShZLxHzPWZzwKkMFJUj9TWyGPmTgyELJv3v1Dss3YJqpNAPyW50cE2NNWRlLYR66byJFO1+ncSzhtDjmF1ejYl1a0ELLllnzaXqU49H1pTjB6RkL3RJD4OvfXTDEYy6mgJT8DMPURAnLdS/vzHv8Tm1vEr4xr26qyz3gZ6m2y2xVbbbLfDTvsddNgxJ5xxzkXXvffBR5989sVX33z3Q3085KMICCd16hA6a9Fqd2MEgAEzEjC5gHEFjCdgXIGIJwmxL2XbrxwHlO2gchxSM8cUnVB0WilnlXJNjV1Xvtcq9F4tfVCBjyrzSS18VnNf1MBXlfimpr6riR8qUp8zrHgAsMIfABjRAGBFPADEkZJOwsrzio5QbkLTUyRcWcqGzDCbGWZHKDRiSUxYDiflO6PEOSUuRjxZEUcQmzCXDdiY5SxmERuxjAUsiSgpl6cgMzQwp9rZcyniIBFW6jdo1JTW7evWSazHDuxMKA3VGoGmGpcgvO2vXI0ICN5kq2uBBtv8z/hIEQx9wJZug8M7qXUjtVusU1ahdpuz832A1v2JIHVung2L2ptTdo+4khZ31Tnmemf4ruplIjT8KfvtCCGDhnKiGTZBRf0BhSI0/kklNZk0Au2slKC8h6Yp3mmireBiSwITUPAiZFEYhRVBPsyhcLlnt1JF0NFmUnbS+RHLWUV6ywClO/Hf0dGl4nPk7t78xapdyufFG4F1/6lAiImgvf1ECaD0UoSOWUVsKImqHbKwHVM3p5siTXmzI3ZISdBRvCSqqAAlbAvastrKzZ9ZllBBHU0fG+ES84oFrKiwttU6U4/2fMbJmQA2v4+xrSrfDBHPGjsThjC9nvf2b+xs8kY9WQ5spbpFH01VhOsQRuyVs216Uqe99rqCbdHAOnS6DbWndcm78+7zTElUNt/4TM+G6dCz9NEdRuZuv7H6Y9edcOPYjtU2WIfcfJMKsJo3tYrE+spU842RXYN06SKIth4plz/D56EwIpVBFNqiAYdSbQrksU0727xxzSTa5vonn5sz5VEgTJLGtnVbUylD8N+AcTk0snofrEO7L4dPuVIRFKLObFndjtcP/0kRkqW8mgraWfyRKhZ9sVlU2kVAWlWWlDaz2n89UYYfbbNFeMH59duTs06jl2eCNn3fBsfJQJTPTlV8L2itY/N7V8B6JkRtzU1rY4uNZuNnZo1wgP96nZXDvohjTZc1ijqliko2wm4pTybxGKxlauSbbHnNn0YAtY2b3Ck1x/3kTlOQqPG1oDAaTdpjeJfsH+20R9fehNMVG/GofpBumJWawe0nRlMe4+/aXSmiBCPjHJIOhWXC+ygJkzBYL5l6lwpXHhNBNyQAr1tq5aKgh+ifTyFZFKqbIMKzP8E9RUpIiJB5YqDTenHDeR081c5NjdX3K35EjanJjo6rolIKjrGKa59dtqEgZXuIlAdFF2DbdjD9STICcz2miCWpAMRF8TcyA4t5Y9MKJKgIB7Yi7pc16bBVUxJ8WkOTDXSOQ9SX0CDr7cba57lkC4G4yqdR0NF8n2TaQ895QpvjzxbdLAu4ovc8L6af3dv5m54mL3SKnrPoVErzwSCH50K22PlkjK9lSwRh49/cqhdKXQQWEHL6IH77aDkZGVQMSDLL84GcbYU8zenRxxfSIf43e2Ln+5ZS7e1JPj4nJeBDnz0LKIz136OJ3/u9Kfr+29du7MFLT8r1Fcfdn5HKC1eLbkPvz6ZfH3SqIX5KI/TI2s23KxwtPb18N/fjYPCr3M3Bt/zON/O8YvBTZi9Td6BJncFe7x8x+yMon6XiFx/KHWJQ5JxlcaKZ+9J77zTxaB+bdW9Dvb0Dtoi9KrvUG1Mxr4lQ17zNpP1yhtqN49t7huZuT4OHCxzg1evAG7fHGCPaqqGh0PS0VH4X4EuQyQc/Xt6iRoXU+VC4hLh3bbokkM6HI6VW3xZxZR6yNHpXXUQ6Oy8pyjZ2ujzFbiwcJKa3dcdW6lozyfatsRMXVTrwBe/DlWMulXnWkH5+WSLNZaLMCAn2aA57nNznry92yZpzoSmSquWwCly44nTGJ6etnUtbx+rr/HmRjaaen9YwCBPFbPVSEig++NrdSwy2EuGlPtGVdLRIkplVMbOlIEW3/ob+uvnjeFiW5FU/6ojR95y3N93wq8eP24E4N/qNgCPiemoLPtUFfMJJcy+LG/hDc+TQBDQIBqkVuZEpWTASHbP62lyeePfZOs71cfCpcnW2DWRLCm14YRQg+WvxtBPP/iqP/lLCrfzrnaMv3mkMu+lnrEane8FZEedN+dWHUTe2UK9Z+yQ20DO1A/+cxGvo46U9GxX/KPxCCRiNNcfvC/Tzroxl9h1LMGyXlDnK1DIJuI1UdYfTEw2ILHirlI94L9y/CDGcgN1MYKvOBjX0DpvwphwX4WJc4i59s/zBLXPFNcvjIhdWxUc+rM40sDUQEzssiIIwdc0YLH2KP7+QBMOZIRciMizumwUKCPdKU2BMRQSn0TyQE3FJ8fCZ4C3A8cnwxQUMsAUyEkRMjsZIUs0Hk9k5yHguNojnDsEDlS0owPbZjfkABwA7cUTdHMy0Zcce5w7gCbDJNLZm/T+sF29uAHuy4c/JyN0/Zkg84N1xP6mnns2TEdB4ke/4M5uzI2XPAPb94hsQMMbly5IfMQt4lIszYWxpjnY0W/Ubi4H9MPDY3UtzYdoFLSv8nvr+EQojIyLEE81aDAkzzGQGzQTdE6nlwBm0EDyQzB0lBJXtOYLzsyuAw4SL4M5DeKf5NtRy5UbnjhIEsO14dgJw0N8wB3MSaTSYI+5sjzwQQi7HfPDSBFbgk7CysKQXEqwS0sLry9EOsXSGuXPMXdC3BUuXK16QznrRn3EJ1nGXxylg9PXoVKWZ1zUDet5SqwYoH2hSbmYLercKBEEtuD0cX22e3TzTPEvo8gL5tjSU+PcFN9xQWAz6F4cXFYLufze+Dkx8eVBOGox+uSijAMoT1nIup+8y9NWKwlLo/3peDL3fxsTJQDiyE9szl9nMMMUEzjdfC4+AiISUjJzJkvHCrhosHO4M7HZ07KJpEVmART/Or6Pq+eG5ns4yfDKsGVQDhrpbsA5ahBDtkZtYhP5k8hD4PjvTP4Gowk0xtEptNTjRgvTXFZuoWrFcIw3UWzKVCEq+3DlSJEKkJkCo4yFS4iCSYyGSYiASoyPioyJiglHipSZc9VbH750St3dy7N5JMXsnRvNjIDSviSE/G0OCQLe8BHq5N8/+Sv1wGScXtOzpgexvsbN8A55lUUUJREDV3XIMLLcn68AOOcoD6Wj2/0pOGcf9J19PybRp16FTl24EDBKkNIr0pmoD+1laOhSwPilEVxdfle2QSlt0W/H1F9TAqs4dgPvmwQn36QTbcN89S1LUbCnC5aqt5jmtNeZ1MvM5XL1F34/mYPRxzWNz9WZ9n5ixGao1m+HqTfqeHF8MTmaxuHqjvsuDTz+oTdNctcE8++itu/onKa5er+8H7bWOY5PwnnUQpOM2hUZaJZdVlWSBr7ZFq59++e2Pv/75r7XX7muUMO7sqdvkWhWd2C45gnajD+qzTdAQErxj61qtTa9ztfr2uN1ujz+crmtLm9yyW6ipGrn+nc91t47X5lpUE0tbDRUrZ2lKWNQqxvrczv4sy9g0JxRN6EFDq9duOm67VeYYr1S2/scqkrUrIPpaCqRcR01KspuUaBIYAsVgkcBMI8AoAqNWkK0ITloBTqCwuVaAqxFYhBTgaQWWoQX5WoGVkNLIRG0dw55wPMI5IWyiiXIqJdEIM40AowiMWkG2IjhpBTiBwuZaAa5GYBFSgKcVWIYW4OslnMFH4KEWQS0ST1NnBw9t1Llgsc5H4TnB6bUp9qRwrVhyEyHXhak/V7p5kkun/X6VnSHfLzE22c+4zVPXbzRAA5ihGtXp4R7BtQUkgWQVDyTQrC2gvf9Y5UhQsNXDaK2MuxOgt8wg/Dzpl+7Pm6uIpHIiuvpXNRaoF/Q4cMZT52q+oWB07sfALcDu3sdz7/VSwVmw/UWa978aDDukd1vAFwH5uJYA2xIAFXj3hihIFbrU/w4Xi8IiB9biJViUwUYabbydzrjijjeadOMwyTpZl+t5vez4yXZs3+5LbKSZy7Ycy2W5La/Lp/Kn7GwvL/61/7vxcqG8uxuwFgVvIaINkWaMdXY766q73mrOJ4k64T8DqEZxpm2gMFFYzNlXEexyNvQhL4F+aVnHpD6y50b3MaXQZ+G/b8LOsKUKctPfDuI/95r/G6qroeouGsKY0dsDzpDgfLgb7pv2mOl08CZ8Nf5Xf7J+RrF/+K/QbLTbLXfioqldTjvjYjyos8E6h6ylSxosGtrkirMOO5eB4yY674gLSSKld9e9jNx31CVTXXXDgWSumZZJSqBRk0kZu+yUGRrcdDA2MsiBzSL58uMvQKCgBoBSiFBhwkWIUmW8IsVKlCpTHgYqVKsx2hhjTTBOsEprrO7fyiILWsec+KOK3CEWYhnVH8o8kK81KiCHoY7of2L9n9FAntDL4+Zj0M1qU3+Tu5FGCROCL1f/I6dKLwKab42uX/O/ybTag/xqnOeo+kpEi3770gU2X2X1ptqUf69ek/J25bTZaWo545+EDgivvlXg7SkYj28p3v3ynCzYeAlTpNdnUotncmAgmMiusZbwE+FYmhEInIMrScBSnpakEAsBb0cBGA6E4fkZV9MlQnCZDmuXq9VjUn7bBzUlZR0Hsuv9kVbUdXAsH69atedojGaPrmJ0vR7iRje6ESchPPg9HYNILSPWQ3D69CzMokDiLNpgAH9ck6VfYyPq1+2kpbj3dqrN9Mtvzz1NehM+XL/zIelcTHQuXhSZxeLARpImhRg3YAMF9CMBU6T32EOKLzisJMKUCW6GZqGgeqcAqVHjhPDkCdcHn7UVk7hCcuEsnlphIhg95cGfSxiikriWEkFaJAOK05IcaEFmcQAlDiVQUke5hI21d+O5pAi7wMjbs3Rw4IK4NRMHfApRyluMfGeHXnYilf4/zXoe1Ktk6bMPn0z8ol7UKoh3npnJj+TTukpKYIrFCLTcSE5MqRMQ2rEi6WFK8h2itNvMFBB0osHJKQv+5gM2MSuvEZrNxhSMqQHn1j2wCELqQERpmrEwcfGjZBbMrhQXDKDrkC2MYM1aEIELUbCg1thaGl1PkMk0hoxbZEBkCQd6D/x0aNPWiNUHtqSJTCu3/DceK2lhUwDT5BtNYqddndKaxE+qVTdkCwYBZyycshCkuNDmtJI2QQuDSDbp4O03n9lPo7eoH2XfQW7LZ//26ylo7Ma7h/SQwam/K46ykRRH2zW2dmoum2ds9SMZCyhYYxnGZDJz6R32Y9bOyn3vlGbEEBUluSYqTfa0ILppipZNKSfMAC1pSEjYjSTKZdM+ZjL09Junow4tDdVKXSlFCiCcRNNZUGeWZpxzmfB5KJaU7LXa/CgWN7VkgAx+UTLUHEERR1GaE1CGYyjLcZTjRJTnJFTgZFTkUs1tSB0SKOa333zfqkg+ukZYh3a4pgYlv9W7U2slf8+0EmZoeLotJOXoBEywNJWVVIiYSIgqUSPqRINoEi2SRLwHrCMX9XsGRhm19NW6tAo5WY+6gQFCim/KznimDAH0qKolcVkW+OwFmckCGtGjlY3GLfGcONxlHjutf6TJamOYsheNT4DnCTRNMcNG2Ls2STykzPMFITgEAG42ZTlPop/yM2UGrKNPI0Q4rl9CJeeoLq6XXMgESALivf5LZBqs26iKSYi7pvV1eu9VtEf4XTN8+6sbF5F0zj1IPVcgBAwfQWZXwh/KWuiPqbLYSz0F1tMCJ7KuQ2U2QNHxKpngn6OlP974AyaLJaPa02JG3cDtUIgUK4akCzUn25aFoUd/nMZgDIOQMw5V7AajYRYGXlrVcBRMJSTp4rLZTIXdD5eh0Nn1BmyZMJ8EWChKmFuUablNsuQNZz5BSQM1xaWiD/8mJz649TyxkKwvj0zdGlTElA5gHarp34mQqGjGBecYL77CulgjnxjCq2GcQdHMYa//wtBiZiS9VDWm2PfvH6QK0pWFXzppY/eWOoAwWJVNQW46pP9kvqjHulZ5EL0idMLDBb+YTyMfmcha8Sk+BlduOfEEXHsqHUC1G5gWMkIsxoqDSXRqQEBTLVUgFKntFAK4KXJ3NipZdpN8hqzUZh27prH9R9GTGVKDcbbquiwbS5t6q+Gi82ZYFdnxXZjCUW9lJyUlQ4EvNb/99RE6XVnyXbPdyblqvUYG0g1yzspNdHS2fKXbS0HQCip0KS5Z67gDsmPe9ja5x4iL4OSCsIMUaQQQdZBiKjgFcQcp0Qgg6eCmKdkBgRMy0oyUyQLIMlKulyDIM1IhC6DIWCu5xklOOVxTpRFA1UGqqeBU1B2kRiOApoNRKzycyO1wQ50sgC5j1GMhQ5+RBlkAQ8YNI9k+hRUyGk/GewOrO0GwNgW/sEaoOhtoRZTmAi8EWi5UuHGs3HCs3fDahhXv2JbutBNoL9jh0RCBoxuBkxuBsxuBS4lC0FWgm2B3NETi4Ubi6Ubi5UbiXQJ9BPoKq36vfuS9w330vaL5Kxah4i61szG3GTQoxm2vK2tVsD+3+qL92T0AYe82A2RPQF0KshnY6CWALe8CzLYFbRUc+AtgGZVmPBXThBskdSY9YLJ/3PAEkX4HzEjudtKgMmr13mPJNMy1YHsmFVvDwTUpZGCE5ZgE7KTAIP2aslv1EyQtCp0ANRaxDCQBQjtzwEnregF4O9RpIqxORKDyCDF0EmHoxZz8MYKtYF/3KV0+2WuiUZzNcz/Gtl/Gc3/s12XVVvZ1bgsJVZRxn4S265yG+rWiRsMI5bKUvt9qOmweN7txU47jm7BdBOUpMAH/PIlz3Sz7el661rAT+eiJIuEjx+krxXnIwm8cx3WB4J/bTQZ+ur5cfhjV/Wl5f+r7ZcSbu1hWvFveR/fJ7Xi+6Xsxbzq7XXWozLFSYtvVvqwqpzbMBAiJTLDzgZ0Gz3O1dr3uNJJHlR/0rRzLh2v91kl/re1x+0EqeX3zN8Zws3X2wApcstsFX1bn837AuOxr/lAgiWrJCDi2jFpt0S+j1wAMOb+il4DLGDkjT1rtMLbfsIIvChN/45j3BwnZAzlyBkCv1zpC2ZwDg0QbrgIjVMD+DpAlYOXPnJfxGhT4znS76hec0kPQog8G9oFj+qXmv1WrYvQkkh490rl1SUwh/Z1yEONqLV4Es64VeAPB5wbiVcynAjQveYtKYS64MQphd9BNxbyjRv40oXKd9FkKEiDaSjD8b1KR80UgF7hZoHjE3+a2hnTz+PxM/W+EXYmPTPZZVBCgwGwJoLAjZVmeT0ZCb7IOuJc102rZpwKqct8duTaSocpoVUlpGKfmVZy9/RqzeM8IvxIwC+zezVn0RVicruGLk+mmgPKM6gKeFhBFuNUCoHG0pm6bC8X0PLxnX4f0/Pvwha8MX/skQn0YojlEuKwdMxGWacTK6uAQ+70xI+8HQYVcQt7dXZogZCTwLg9589IEnq9XdHdNUzaXhR20U8ZFaNTnR8qiFRS7Rn9yGPnejMDYb/+Oqyn2K4XCQ5kQU4JjSFxDsKpTiPwZCBTfIRV8fcAqv9z1sK5AhlpoH9jI5KzircwhqV6Jyh7zU9P93JnqYo93OWvlneQhSyaiM1Nt76YWglpT8l0Mh4CzkBMtKlcJUH86B/ZgrSYOWGr1YXyJIqlLDkhaFj0GQQx9+U0ssxJ0+UlwU9dm9wHE0OaLazy3iwAkpw9GW4eX4ZlYmv3cqBdc2JL1CrauwvJ6ZpTjthgZAdF8QEBqhQ6z6xBw0ui1qyABLdRc9volBnoZKMWhqoTZHgSBbtTEUpU073v9gQf5nXgCfMuGH11xKTIsNrIEaI4FgfXsqgywTk3sj9AehDszVPYJ18mq2fgsjVq5iObScrh2yosxFihV/XPx45b9+GKhO10gUDpwfhZ2BzuXXS+g6qByss/yZ9OHYM9dQ692Br/4lcbNcrlETji14gnIR1SlkGdfV8Kpk3qQelDTaK0DjUeCzQDBzO7s9TDEmd44CFW5ykDIzv/GeljHEbXIXoLRDV+RtPvQJeA72/R+3skzsITIs8IhkrVMbWrIAJPKgqYFMYd7wxIIzL2c3Yci58g/LD4oT2gMsoyODClXVL4gzyC4zN113WsHcQ1Qdo6mQ/A5udknKcbzQ040Npy86GTfVTnmLTgyCZku7inka56kBxDQPWwMm/3tYRs+0kWjusFCgiqouOeYWW/iRR919FNuCD5lmw5Lt+TKtShCBZhLlw4B8vGwx1EQ0QIYMNabuyruyfALsSh/9erJxGrL15B7MYla/+bAkL44wdfHyr8gmHUFjI0hpgMEtbalsiduR4ks8oeDispgFHHk22tyUj7IMc9z6E4+JuwjrQsk0VbcKV+c322bcA5+9lUa3aMJnspuk4A+VJeLpTJAlirL0+IH4RlppPVrlZkvdbCu69DWEfJVF3GvH7EmD3hdBYV5sNFM9SXCGGwwsUZxDHkRfu6KPmjAeTM3zI2u3o4Lat/iQtQoxJVHxUrGqKuff1JpRhf59lOrrd/E5Vti0S4IlQwl5uAOOcsSYUit5oBo3SoWA3/zH/IX/ym1XOMS7HUTBOsTeBTfI71EF/GhGoQmTuoYTHW6HQKCbx1jo0UWehNSy0A1DIJcS5g0cSdg4sU+QD1hhZiYfChcF1kENzEQ5Q9IzX6veO5BjMsQFMnRGwpKG94bxV70Gq28NhnZ5LDmzJh+Xc6eBz1pe6oC2PvzizQu2i/2lit3waF/ppytI9yDBQ0zcwekKjsKp2f3hr+17vh2MSzqw0TvLsFdw7UMZqKk6r1eGzDBQGg6f1q7OL+J7eSoI7r5gruaH0EQBVrLY7SX4uutvCKVJ2b9apUJ6XHv+VPB5Psa223tynxsXXJl2fRaiUNwMI5knBkog2iLoEQmCrx1lMMpSGMJGWg8wSSMQ/ReuyjDxLgwDrGGrBkELraQ8MKFc+WYN3VAcSP8Q6sIVVCLaw0RHaM1BiPyuffjKcIgg9MoT7YsV0qkwMQ8LayEr6exmQbkIvXpoWiObWhkfFPf9gI7jDdYGogZ5I9FPdHoKzGPaAR3yLSCMunT3bDfidJpnYIMe0ddvbjO2wgQNHHPHNxXjCgQ7iqvch+U4hmo/NZj71HEDsNz1+V53OOC8mkPNxwunroCaKp+uyqMNdXmd48qrf2znTNBhIa5uPDKwS8ooRCh2KgdIiGdZhNNtujGi4nFjlxHfp3iAnGWLeogg+sIghoyeAh1CYPkGM9H4+ROINE8sT8eBT6u3SNyXlcY2y+Qe244j0TJ5MrK/MA7QpgqMYmfU9HmfMT4Ma1r6V8mFX6AcjqcD0nDxCt+QOFsMEf5UpwJzobZCCq2pvvHo+Ek+ptYaeF7yS9zT9quLWLRuxixjzMH4wKrBZSo1n0eE88JhkhVFRjYZ0YQaYfaU2t94BgYSUBipsp845UTmTHyRMf5nNvLRF/im1sZcaUztHTw5dIDTdhT4twVtBuF+RAqC7zDT50RflgyWJnXcOSwOSR0DBUZsALjUrBr9icQvKI7VP+hPGkw6UOj71F2vkhDokoL5zu7WHhSTZmo3HWBU1q8bjMw8LxJtQfR4/lpa3nEYeomssYCvyDqQqphFyMtb/GAQ13WOkhhSeqzPsQm+/W0L3SUig72YcDnnJlghtqZotONpK1D/o5MelGz1fnUHtqknH+xeOwIdgNXLmArGNhxgYL8xx/hx7A6XIWd9hA+xmTR14tueEPeW+1M2atz1EE63axnAdMkabtLIbXBRoe/prasFd48cbgYqkahw9h3ytNBhwouLt1A6C+hP/qjGTJHpdkFUJT2LY7EtqhHq6mQ9eIRsjnhK0iLANlLUIyi0y/GB0sNaODnedfuoRr2CWH1UvQTYtjO1M98IzV+KKEmvl2s5f/Jp/tf2DcUZ0j211D9p+TvI/DLv0lKvi1cmCVM7k/MqZWDeY9LpHtv+ib8z/Tte6jvZaCIVcL1g54D567UnPydoxeS+PBCcK/yWPydkqcQriFiZLT+apQxKfRO+dhGQ3C+J6oyIhyjqpEPucYQvBA1iP+IJv/o3GhGWJORW0gWsL5FwcibgHyqBDXOfXJ+aZdRhlnWZRKh4KjRdG+h9zKyN8UYysDVftm+/9oQLmCDDNe362lTEtZYEOV802O9HiRgLj49AN87jG13arSBlAdXXzI4jRgDmSnquD0rf6r1RMqNk0ZxEqiacDpodmfsyaVQbk8qtbI+aJ7Wkptu9Gt9Ja2nig7hKxZfl3JcHX7rjHSdeUazt1zupGyJTsKVvLdmMM3lMuAa470aqXmZmGFmc9NcfZyajQsb4sPdlkmpd+d2WYVf3OJdwDPut8fr9SlwF+gTW/t1+T/AhW18KG9f17g7T5fjH0Ld/dLoXTx3CZQ4SZXNaMf7IpYZbY2zdSF9qP/7X7O34xIzUX14nmFe++TKBsecoX6EQpqzv1oKWCAg3/JtqqCeGHIg7ovG4EfKRzEYsLeYPT1PCQ0OU96kz5MKzbtIs7AsxSfVQIR168Q8Sw4crth0+XKWUC+Hkij5YiVieJyfC34CTuqrP/hxNw2cVouOqwD1MP/38AHwyKx+FyJw0nnMOzfqH9Q/kqQ4AA6Y72wciIqgn75Y6fqoJbdTRSCpBkmYkxCDnTM+XThECFCgUpTTffD80BAR6FgCJU3R1NVqpgRDmsl1tVM0wUNocihIwDbfbEo5lGPiOcuy1pjdlLarLWRriMs9+/1qGL80VW8KpqYrE7E/cjCAs+YCZ7wmYPLVlrt29sjqyaPtIUumaG5ddEWdtR8uccvMgVkvcWpKh5BY5NfbSYPv6s45nuL4m6Ad1JmJmEKJy8cpXiE4cVLl+1+oCShZ7VtzRMRntISdgf6Qs760D9qMs42+5jEhBq1Q4Rcu8HHCMUTA976JF8WaDHkstBnYhl0SsCUdUvlhL/vLqF6AaFRwwKNJlnn3gw6Y3x3aAG403wChDd38dhjoN0OaqUb/lLZwJf9ke5PaxgxskIX48gpkcKMQvAUaUaTNO2WyUd2Cym/x8gETJAWH9qAP7U3wwU2wFMoTkVAnc2Eh4IKUYMKqgp3f+O7+hlW5KiUA9wDCO92qPvANc/ibHkwQFl4tvKNw8oAYxAQFyy+tuUSrheAecHK//Pu21CcCCuDno3pubL5O9RYAzW1QA//1g9TJA1QQ4qH2PNH9QacIwG3gZF+T/13dAPIMI+145f0AF37p2CF/F/h6VO+qC52EfuV0WMd1+oT8a9OeXu2hGAfcAi9uPdrN3DfxCmM1jjxli6Q+l780E1ta29/bMK6V5stjue0sTCiNH4P8hdmaPpn8pC75qOMvZ7RuK9jgjPjjbcG1tW07VRP9JFrANoi72+IK1r0ldKVsok1Wl22MHl+HsjR8XylXUqju3jJcff/qcHUoc8N3CUX59lF563umlzW3NXZ/kBqMgsHd5fvcoECj0DN2xoqp02Z3v9+moz3tuHeNSJ6Oevd/Pp4eyO+RRoe4a0hNFLfgMQ9zqDA9iOslMmqqhfpY2m7Gtc8ncLjQASAS/LyrcFeAK5vHinN4I1f98dLvP2lDwcwrvC2G4roCny2sjOjkTUilffRgmzN99fax+fAEZtLIULFivlQ4PuY/CBv1uiZXxD1mjCug+qcAA+QNqZYmtPhrVlhZkagOM+6p8zHTmtzKXsgrqpbX2dP2CUOciMp0yK2GdhqKz8XRwd4Z6OEeen1fRTikgScwQa6UO4OTmJQPtY0Z1xyxjxnkclbUDMvL0LFlrJBPbqM/41CYkTmXdyescQ45W6TqQ2TAyRoYVURpYmk2WFmtldPHYFDUgt1TWK1lKYdpimLKoCAT1P+5jbZmQ4kI/5o3OhJA3HJT5RBdtDYdNZC6/iCuLh6mtj6sblZ4pBIZEhOqS61966CQwYW4/2KlUjkSE61lS5S6LzjsgFr3hZerc7i1Fp9Nwq+yNBUW+JRZIXCyIiC+NgaiEI9AZovYbCInCzwDu/4VfuVTHAOUu0DLVgsjw2Ckr6gt3L7aMjtdcbf/alt6JRO3iCwIaTm4OTt3KWed1nReyUpHvC2uXppHK+ejKfAleLFq9OnHLWAbYdXxczEMkOEB19N+r3irATrdK7GrvdiKH4vrLMVFwQKtWyBVeyQf51f7pEpLMMgCe/4WHuXR8HmiteB3MO3LtZNZ/8iLQECLlJisSlkR8j3AhIW8HXbzELfbPLjD4bWmO8yDlSHeZpZy9xCiMgOlMSLhniG2fKVNdXLBb8DJAleMzzMF3eihw2gbr8Pm0A06HGIB1MBtu1vKaf8xw0h1OBFl/AZC9IfoCB9NqIyQ6fXnmDDM6eVNPuGiQPOcTDDjQu9QSIOflH1L82r7e1DATrASHjFwsiTuPqkgKIviHGWWXSah7adzpKkXcREWkF2DfLw49hgMiewRLWeUThU1ZWqVYokrIJJ6747BPqPXUNmNdMa+9i+HIkqFyhnmeG9BgQGt0hEpruFVDcMMn6+YLy77jEn45bwCc0sltP0IhYXzZD8kE90SkBUY+dSbq8fFAIcdKAThW28f7/kbcECQgRr1GtX6rMDNAReBn3tkpejCFoF+da8KpgpZ0wUP9kQMX/pY2bGSNUswTyAnAefc9xeduyl5BU6g+4yOD0W7wC7gnNi15uoc8AdYhB42o2YyegXQMC8VSLwM1IR/CxrfFrg44Bzwc/6vOMJzkVmKoeo7UN9XWc52/w2HEb6f3aMZhCveWnAhC5Sar+SHkhG4TePWQt4us17ncdWSG8huwWM+5v3C9CD+zCjp1dr6bYAGt+Y0D0u1trvzyGaX21orPVy4v0oJ9yg6N/n/FvFZcQ4tb/ECN+GHP/xPvVzInzXibA1q6NiGAbJqXdIxh7x0r+SXtE3EfntAc4z2svplIe4CW+AbVkqFSkehi7j1jWcd6GUmVdgUnbyEbW/0aNix++rz3Ux8Gp3/4vWR6HfsuvrtNIJqR3Od7Oabmra2onZY6xja1S+3JriNKf7ee7KWMo6ur20q8j+2OnTgaUdMGaNquY3OaIPMVpuN6Dl7OsZvYFL25P3VR8Bwb1AgUyM49hgs6f5ja94BDvUi+1fyryrmFCFtslQmVbu0mSB3Hpbr6Si3dDijzqFDzW4y7xdqzBzwBoZ1XEwyRO3UpCEa7L6CVd7mcyMrrRMHI/ue+JZW6UUK05CJqmz1j2xteaazC5HmCbfeAGrRcQ9N9NqdWYvDHdyE72TVXiLsnEUcR6nDRfd24g9sEtRdCnWPI+6YxViLm7MXnrsXP7/qW0SraO2lad2EuhUPvirWJlFZ33Oi+X97SPieNwQXqgid7nrLdTyR/vAYGX/grOGLX3ALfpB32xX3Rk3pEwrlSSnF8XxcemfandJubeP3pU8roKdT7hZ1V7ooYio1CU+LsHz+f7AII+Bje/b/p8fCLD1iulwo2KSKT3QweJ8kFdHb6YxThdH5nojMjHf+KiCBgjW9sQUVVHhHN/pFuW2CngYeA6hJPPp7OPLiLeapArZv+a9zGyxF9Itm1RTcFhYDgmagLDrpGZ4/JlV520fwMcdeM5xdsIwe8KRMWDbTqwO0BXTMtTR6TyPGcJYWkly8Zr1uIrikz2S6JCdOLB8Rx/VSeCakUWo1pxSKuMaTG9jGKo4MuPUZsAn00JHkL+m1WEAjgVcRjbC1TB6j6fKFTrKNxAhib3rS3b6e/06nHO4LIP0W/Wc6+VAnEcKPeiB423xN0Kc8+LP5qkXfej/bKzxg6N4qAcU7ZK8He21vD0Fnyax2TW1WfDqBGL/l7X3NfaSlfrlmubrqGH+E8o1miYZZukhzXiM8UW3C2y69x6tLlYhe611Y5/WypnxBn0f/QUKnP+tuV6q56vaUKSTonx4l39rovLKgjt5tnCNy5jOhrnxnp2EOvbtugaPX1pRPqeNMsnVNOlo2NjC2zNe/C8UcneS73ZUMBa0Sip+CcDluddnldat2cbgUhBIQW0PBJKWMJt0Ki0g+opsnmEwkThZwiQjJJ4K3SmllrdyumEu24gCij4RwZ/BIbqJrhYxRrlEupgR4F+92+yV/Z7ZSEsRtjye5SpdoNJQl012P9xNXEAd2/HrQ9a7z3YO/5nSg9RuFF+3YogVTWzJn93PJzT9urCI5vTqNMaCb6eYobJFmhVH4eoQFrZ3ann12j1Paemd3NdETMqjNvnwXYKks0UbZgFvVJ9boqaWtP66n5i3MLCtru6MuGnZTYkiJVD5ZpMEbHdXP1G8hSMCfq8eMyy6MRuZlKgwL2joWmJPNc5LIwoRPPbjCqjYuxtX4tCLed2aLaBBSmVLIKZO+qyRpqIOSLJaTlTGHrJPHBpOtS9LB1Y0ZZFqnN+5sYa6/3GcEb45QpaeQC8dwzu7aSmRLmLlNK6HETk1NrXpKMKSeXFPbqQnQocmhoAfo7xekZsUi86qqpKCHpqi3RlNrbtjQjNWSa7ZY5RW78ksZwnjxxzhzIIPEfFU6srhPHGAUdnz9YoTdZ8+ZjM0OOyXkzHZ7kxkuu2OTTaY7+gq1LGnQy1IVKr0BghaKYAveX66NhU33m5hS+B6TdK3TMHhDYaJxLLHhbqGoTzwUR+qjlP56fOhq9Pjr9mOFfZ5V8mPl12PoGvQd5mEXujp+rOJYGo1B39iNzkOfzj9a3Uuq+gp16F903/wtZXE6Q0Wnc8XCwmtlWjkW4J6haAocZ+F8PBZXVLu3THYN9bovFMKysZibPfzzfD/onuu6x325xeIL6nS+oCXbrD8oJiWEyjNUMiJQ7c51R+ePNls+z6lHSNc8QrYruqJOd6S86Ejz57qYMoNCgT+n2x/b04PEJ3AYReGoYnAa6/cPGWRzSz57iePUfTgzmnUWGjy5osZj8AmMUK47Cu/H9IlEnOyumfk2i2r0ttzoOZjSxDfxcLAkViGHYW1ubM2ficSVtItEPrd+U0IQxONL9voFkS8RREziEBUpVveT0U4D1SlBUo2/UKWbWDz9/yKOk/IvXW748+uWGY1l5BWbj+XIFF3vFMezonmtdMqFHQ9PHi7Dx5yE4JBoxD1mtNMPEBh4/RWE/lN7RjiTIdUXArqTwMtnLGp0uPE44FWFJDTfpZBCvIsV9kvNcKxDAocRa4ojM3lEpQNODs/WoWQGxLFoPbxeyZ4dCPzr/xMvvLiq3hC5/l0wcYx7EkjDh+bAc4bPgYctPLcJ/A2e9Cv5uPX9/DZwBvBnndfCs4fPhmcfSsMg3B8KDUoYugc1W3oGVftCgyoNPYNy1rWDqpUMoZswLlJfulKflDBFLuLYaEPZSkNSvgZk+S8/jgtfd8kbE3wg0LET2aFT7em7U4TGsMq0eWwQ/5hCr3Wr0gyVtUahjmvYv9V4goPbw/5/DWkAulTyDuRsFSzhku/gQ7lOt9UVtHr90RqjAi6vCHptKDnh84YRIcIdksCGtEnttoxY6ZNpk+koaCtpaPSrnZmRuswZkOXPANTaC/LVE0ITZiT5YNJ/ex4F//BzAjJ4vCUaDavo+D/znU1bMNFVsaC3KlZVJjzcv291nQB9AqE7HDUqZUKHgXyD7eaRoaHNO450TqVzxvMXfAjGbI+JtMaYRpI2GLRNHdbDf3ds2lX8LxrZHl3YwGmAIHXzev1w9+Sadbb29JA9LK4nuj6wzbQdqxZAUHm70OZVMWrmZ/+n6mFUWSualCZDrdgQVIq9niURqS9ltThTHfIuNpU2bOivRkH5QgVsp60vd3t1+g8VfFZQvidv1GJ1LcQZz1/4IbZsVVykMcaDKjbmH2S3jAwNbdlxuNNyBdFGDJwGKlQ8sLZ2Ff/7OAydOB6E4iuG1ZuFdYhORmDR2Bacoygg1PTPdJFw0QWL/r2wRmrFjiO1qNjLq1RZElZAIw91J629FPpyOnVqUl/8IdZ0pZcjNrusCsqtZVG3U/3R92LWhGHrIdfPCgwHdJf4WcJsrogrnqzjMDGwTGrVFSe7vNr+H+KINY9go0cl5W+FZ5QM5szyM1LN2M7y9/BnoOgnPoWvvBT7amX7KjNnsqU2HJRyQPGaYAwlgvj8bxSccNGtZzHHUPkiicWIaA5CsclEoGnBnIkEhosQTR7/KUBndin4DCe/dV35N1Rc7XwifxntnrRILS1zVi43Z79K9KeIY/79lA7eRigsqpVSfz1k2MHguP8Q6j0aMd3/7wk3X1vpGIhg3ivjfskmseSlpV+wn3Jms6lSCgHakMoTpUSgIoQYrdr1XO+3WcErcfFVSDLpzy2abkPzF9OYtiLIQEOouFC3uhfc+HHWyZhbsMDT8KOHdYePmpxxZObXOr2O+bR7rhleblqO8NzHg9/1K+/MtK9csXLlLl6UTCe0YkVGzof+pkM8yMuPYsGXR4yw01yXg9u/fClSaf7MUx7U8SWzFpYxTygMHltQz7ecY5TSnkh15rdHt2vaWNY1me6M6Hb59qOaJWTNrGEDXXfpszrFoD8sPzpc994f9qbmaOz6fzn1v+vaaotGyzet8li0LeYirAd7vSeK4adKeWiBHTnFXbX6rsJ5ewzpg0xOcXh97YR+O8fKRLf4ZDumfuy09k4iWwMdYAvhNysKW9/3vV0r2E7l8ajbBcJVq8RVNTyMWIytCpYeSzHm+9CQEbVxnbvBL7rH+Zfprg3WyMyppqBVGgukhfNwC3rWaiLizpwQvAT9+AyXbTRmxIgED3wWCBxpk2QF3FDGNgLmT+NxlxnedYZ8Nu/J9euxt04+dg5dKlhoLNqoKc7mN3oZ8CMclyv1KuzZqIhSJNKspzKq4dHUJWjanvYyESlp0z8kYf89SnGgE+EJTMTEYLMf9MRvaa6guc8PZVHljtGDYo99cpvyz0IsrvMkKnQiemJAkWTbcLqImLDqX1JcJVmv5BQjCnnyQHTtS/GoyUkSZ5zkWdrrxzogcRxJFqmM64S0Q20dQhMSk5me9mBq+l1w5LrcK76xv14PPZjxy8fp6lOUC8jxhkHdEfo6xLQ8umtAkWTLQKqRmJjVtb3DufNGffGI1oc8d6Ic1xOZgVI8KSP0zhVO7XN6qaLsq7WtOQM6Hx4ty+4YZmiPJ5m9khddyfGiU6bHwrwGY27imcsrfrvfvpGnnoX3WbYcNTHTEW9+/jstLf9wiKy1zZao18PqRI7ngZ4A0ZuWDJZnV3+9/JTscqvHnPlj2VsHvSH7iu+C0KNVnu35S1Ds48B2bsDdIxa2VweKN2sym7Vb9rqQdeOfl07fpCl+HNjUgZZlyVzqy/NH9TDM8HmKeeKyoyz8L9cUmFtxQoXRr1U67nqtgOSxe9XjqmFYd2k+TVcakONe1P7toyounjww6NcfhJKnDUrwI2r3ovZSxH7C6sIAk5m5gWzt7O7Vzm189/ndF7IlPs8b2YyHuxgvORhsIYg2j2a+x04xWnaJbus6sYW+uACdfcOwuMaXBUI/JsPnTJacTun52bELm+GbuWFml1l5+MzMOwKnHI/6VnXh/dIRYmJ0Z63VQ+2sYmvnaOg+r/sCthjjamsdqp3V8No5cdgCKFgwJu/Ol/jq8N9QO8fG7gvGLrXB1w4IaMWYv3a7H4+HJ1kcs1g8KRq6L7b9tBteZ2ksZemkBdjGmvFirm3xpa5e7beTrv2IyJWd7t20l10Yu8ixsYtNba3Q1tLSKdmSUXZT2Qpgywl3sQ7ktKCh7tckst6kuSAZN74G3MmdAO4WGrvFRdxqbJUtnQ7/PePkRp8PK4y7WrtcLTX3HGrtcT5r7jN2nxO4H/dvGt06mZi3/8I75t3fzXuZ90+bD86aD380HzEf/2c++dd8+qf5jPn8D/PFTKfj5X+xqT3OX1o6pBNtuEHapUM6pcvt7o4gKMShzlhopAHsop1NynC6CKpAJuholS5OnNa7q7V9Owmlko/2bdv13O9/pq39oEaUA/j3/e4Atvng/+/fDnjwyugVAEy9e/8wnQ0AbAbqhzacpIckqlbArFzRGnIB0jhXwpTA4/DGTfCza2O+oGuy+Ow+u73+rR6C95/6sOwZFeQSBXiNtWfk0WQG6rNpQH1dSzidSFrmAmSnrNXT3OwsQsjVhrObru2/gWwJcwVuABdHQ1A9DFmu4+/mts2SJcez1DG1k8v+k7/2nqlCubVkXrnvGQHG6T91Sf4uIj/nWw7eB3MHWskjm+YUztDa6fva1NTmNBpvzbHvjuzlnKbbIBnGlG44Xyqh8dWqyPvdx/nIoAfezleCHupKt0Xs9lW9Jptmy0QflnnZurl8x/tIRV4+StU9TKVvGqB4jglUd9YK3Z0C1Pq1mqj/SVPpeO29eR+/ujezztYY9iA/VR2DBTuRr1ecVALyjO07g/xctfG9/DMOYSK/OA817O0/kYuXryK3NquORVi+qzj/I9RosqRBbbY+TfCmVtUkUk9ilOGRj9Ti1ROqCRD3P7aV+6aAcROsqgHysAAgMxWVEHfvSfOkvPbqHgLxEjb4DR66/o//Mqo3tUy5fKz2XllM7btWM00cAcqgDUbaEjNOBo3XQfWgBAw2982Fuo4cy+HwoGuFhdKIQ3Q9BHwM/BS5kAQNE9QwO5qYK5fq+sAIRtMB3cRwkhEjVulhvklImAAfA0Rwcuag7zF2LC8FYLIoTWWyRBSyAG1anS8X6rrTYeSILEyxI9zCTzDjGvJuAHesHeA71ySeLIqcU9R2Fc/oKsj5ui4YBJNMJM1GGwqBgbKCDQvQtnFGMlDD4XNNCJoTl5gWZFaypY//E/a9wxmIXPwnNTLrPzGUsLIhScUNj/tyC5ntQa61RZYeFxp03+0ekqi52yVGdAB6osbazi1CeaKudqCLnJ0gPMO2pa2s7QRkeZ5Fkh7b3g1SadGIFkCHTjAeXx77Vjg3GJ+/0W1Tuy5UtKm/TvBMXSrBMJKGl/SwUjbm21f7mm6VuBxJQQlENdcI4UZ/qAAfg3jJbKET4PGOYEx4cqWuMd2xd1SCYyIaLbMNUKA/Z8vLYmsvXxZLkKMC4hTyjqTE4QHOa2l9ivZ8RvpezdwB9aJJKLrFez5G+jnPZgrb54JIkUYbEQhx5xGCOfbACL+tgoP59AHoku6FEXfgrfadS/dMCzx/Oj+8ocn2BnkxkxYqPbozPrERQO+WI+oB+vRdQYUrl6sW1dYuRcIGlsvPYdOilGaYF5tkscSBwMV9hrRQWOts0XJ4AkNaJSbtVbc2l8qOZ5kZvBf97keamkPuGAxG/ZFuoECekC8i58YxQ57BOXzZWw6g69Kc5aBBozOeXKzrJi8h2eMw0nj3GC437VBfCH/gHtB2mZwqW+blsb10F5XFiidVKPamOGUcxvCBirNTTAzKRwJiaJNw96ZGXOYcegFgGMoqqYhvpuYp+thRhviqYnH8KQbDuYMK6oQIW2N2N4A0nNBTTiqVfgjH63q2io2MrP31Qo4aEWDsDEBNSHeCKGJhE1DDrN9UhvN19dKp4EEsUaWhjncclhogpi3mxGM6eGf2RE8tSyhRoAmJKdDUhnlhcgBOMr6HOPcSo5phsPGgdqTnYM3A8kxqVl4ftkfPEYIlr/1CobK0iVzaW2F85JyGZAsDxhn3U0mmBp8rQcdlDDnY5RZhPZVOiHxISx5KpsMxrxvcTgCzC8ziQUYbPVb5b7jvdk0RjR6Nj4RQ8Ce3SxZ0DqE+ukm5E9Pcj7D1410BN6wyRAxLIL0OBaz7C5twkvvBc/I6hsW9YqXyRDJ6K8Pbrf/JI07vPboWfBcMGzEvbmWXnL3ECe5aI911xe518vq5F1KAdE2tU5oS7xz2u3HoW5VJQRAnPtD7pFiv9pmsKMkiBkgLHxN0IogNpfRtd4RNKeQVnCzrMpWCgxbNhjB/7I+56p+aOkfKiHO05ljs34IkQmunMOhe4YU6YP4wXXUHPH5k00yk46AtnY2AqlZqbXZHFs9NwboTjX11oJSYiFgG/VateFACPnHN49pRAZOHqrS6S8aIGOeUZZ7IlG7us/SvAPP+V0CSEpe1bftMPdgFg3DQzIxGmMnN8r8IMeq0gRwirXS/G7tetBhHRTkGxLdeKM+5NM4Tx1984IRx9bI0ZdmMxZR4oZU5lkDRKd9BQpl1P4hGJ2KbHqWqffU4Kqis0QuKo9ytoZR42Uv21wxnJV5s6BPzxsb0amUnSDu8rCcs823yBaIRhc6LK+j9CBSUDSeL5AE+B4mbuNX9AIrGfL6LI9jDbAOK6uwj0cgGtGn3PXcGSZ0cItOTX+VHmwFy1eWQUb/iSZUXM9S1N6pj8PEDdHF2wLb1plaWQOkWEMFy+ILgMp/14lL77YnoEvN0WeSMorYlLmjlcHqjQnEETZQUcECrZ5pxyjeILPNoklZZRcgYKynH45pfE7LUY3sIXyTh+WijDCCTRi6D0VUjoTNloTB7tTHGLGWs2xvjp3YL+vI4EeI8mDbmohEMbCFqsj6EwIZS1MPINGTFTSp3xpb55f74sFrcv5fveRr0cdiUeFuvZEbEnU1eaXz8UAhOMEST2S8+bAKzuJ4XFAQ5LkKwp3bv8OOMd0JLn/bpYATmpIlpnAFgwI7tdGQZ+FyHuYGvQR94dYjYZkrz/Y42jRF/QldTYjgvbBP1PHr2uCrwf9vs3Grop7rPxBb8qlZkVjPoYnIACjqlHF7Nj7UG4mZEC5z3qsIuTNB+nOxiICmOpVWxF/VQ4tyKXKJHKj/5S/l6uAEDX0hLGzVIqDtJeJah5sXszH+8i2DvEFEJkKM7j+LZKZMCjrcPl3kyui9UmmBnWCqwGrBRM+NW5voZ5BaGvJVt4e6EoeXShU351RvUQ7Ez6gyN0wJLytCn+ylj3xO572pYjpcCXJPaD5hxdtGuhB7jsdS24edBsRCrAM7Ta/skwxNHuvcDrSb2GcBEnhtlwIBBr2WqHxwjAh51O+HnFwwRCDAgBQmwngXg1wvMtUVO0sxh8Hj1bYZSMIoGwj8Qruo/9+zTb2/UUUR3T+z+4Z7I8iwRFa+oLrgUQTsLz8UhQINo+6bPQMvTRxhnTDfqq6i4qPVmFhZqw0O8MDPbndSkxMF9IcBOv1pYRinRyooVVhsEfAVTu1r9y54NGtFRWyHDsVaiPf4vFFeSyp1ypHw6OV9i1byKcJWpWolmMDNi7a1ahp0aZhqL2rY4+tkxD02Qa7P53/HyWNLbk1p6GjRK9YsYpGMSnwgU7dvLhtDEm0jFvYBtmoFio9qOhigtNmWXfZ5pJGXJFKyyrvM69dzIYJZa48GyelEjlqeLZb74vL4s74/302HQbVMqy7/7O3qdbScVHzp76x7tVrwJT+VLpgNiGZkHsGsiJpsEwtZUWYpyTXY4pDVJpdcTfrloZIr+X84uRwI/MWDfY4VJ+1fAr/i1JEbru4EqxytzWcQo4RdBYZT3xH7mmU0AMxtzbOvZXzGwcfP/qrLMYfMynD0N9vVzTpinbbKpUoyAaW/dQPbwIZVsUaudwVjmvdPh4/gxT2Y/7JtqdWAU5ZZ4Vtc3pq7adT8DyoB5excMAh47fI/X6BhkgpBWhfwBDvR7ejVJLqcLtG4pt3cxYW+9F3x5ut5NRre18Q8cnEhQvPWy3nR8j1rE/vz8aXh4wxiU0cQdZoQpLVryaNnAKEN0uFdn3SWy4nEd/TnEEHDycAjZNSGXkct0cqD5hrwXV7AHCWQGedtvt2/QowhNYcbZA341LSgXR05moF2VEUDIrtP2X4CBz3VVGDxFgfbpcrxRH0r6OJT54DRjkCnaDk0QN64UmCR1O+thxW4ztJuAHr26p13P4typGWhHGgKoC9JituzhdttIZRFHVrTKK4ok3jsVA+25s4AQy2m/JHgdoGH9SXKrXZ8XVtL5LbcKrmFSwyM8Y0ZzngUaGwQm2tYAVE4iHXMtGjU3isnR0BxB41WdRFbenl3i239x59uFSHarb6q5QYNwx4gp5ct+1ehW8PCg7n1Ws3tWAP5v+tNuW48S4f/l9/ejUyoNa9ioe+ET15JByDq2Nhkgr9DXa70m6GALUDSfxawMuS5ZBxU02W+YO3Kfa3hZnw2oXI5+WuuJgiCNGiEMtJ1jAYsTQEAEeyH3hcnRHGC6KsZyDD2Ud/0QXpXX7ch5UPgJK4eahKAxbDfIhk46jx0hR59v/xX3lUmPcYE1CC7/sxxYGDVshfKC6CpVlnC6jyv7d5jASpXU5YJT7rhZOQgLcJdHBwqXEdyO5Man+mcieTxpNPC57H+WPyiVFIblxIQltz2qwFd+vhYKt68VBnUho8o8NBZxJjOA9yB7Aeq/dkrJixe8ErySbFfBfiCnjEltqPvos3fxh1YSV+Sv3ZACN5lGY+b7OWZapo8uux5/Qoc5wKRniZkzmDk4eCAcTM5BQNKgnsy/NYiqZFftMy97OjLws3AWBdSpr+S8nsVdUA4Iw1A3lM6JnEoMGb5ogpxfAK17uWF4IO0oIeWGEBCmgZNA978zBJp4LFscRe8tUYjYoyAEyDg2VGFNGi7zbe45gF0tikKBm8Rc0qooZtLnsPCmXlxD/1aQXBeQLkQBmJg7SKWqUm6Sc8tjgJ10tV7sTX9rcCia8G9dxNt17R2vhr3wXG7BegxS/2CCTF58k5R4ri2Vz+vDnG/rWYgwE3clbJXBuwLE7phnBM9cBxd647mLPZ0tWxUr+fdtGrtPP0OWjYr/ig5AkM7cWo6uebel9kTFH7Sm9VIOlhCGfv0IujzCsOjL14raimJULfqLCqQrasW6gCoDlM/788PxYTm/nudR97R4cYoDxuf6pu74D9S/PRKCIyVblraj1Wz1BMKCtiWQJXJLHeiqvi0IUKuChHUOws3ZRHDLfPH9eHvcL+fDTv+qHu2ODcu22tGLreCvwtd3nnVeGU84sk9UlFCUfwrBeZIPzOpJX0SpsckjPuezJo/T4jeZdsn3LmJ1/tw0BFvm4/vz98t382iW23XUQQ5vB8znTz5YvfhZBg2fv+naIqHbGOoNGv31pyDTiXs2hSEszfuFEstTW0QRRmw+Wi9tyntpbrTrtahx/sVwmC+tQs24P2AfHFcwni9HxsKA+ohH3ddllkoOajg8mljYQAX2bY4O8bXHbG096ZkjNG5dHR7mqF6WJiqbsZg/9oB/GDh/0zmO9L/tEJLIEwyUcUvWmksy64NczXqjKf5ZOF1cH6Isf8NlBUTVj8ZFzV+KLH6Cg46TG6zoJ+YHF8u8eRLW7bqfV6/HlRzHqeF53Upe/Dv0/NpIqBAubPEIX+IeUyo8rvejrRIXE1opz6g9a4y28cwY4tAJPbFx3O8Es8sdmMmlMNQGLJ36flEk+L+c7QIeH+/q1JIXsK02wb1vQ/InYfgKYj1HYhQ6B4cEhl61oWqetAO8aGvdmq67lwcfwdT0IJzQvFdsy/lN2Mplgjqsb8+Wg964evP58ni7GkOG33pQuje8dzhZrrQw+mj8KlZUxw5kFe0kb4oSpxp5HFS14tkfDprWyppS7rn0OBZAcc8jm9LFR8ydny3mgLtwfjPfv/6APfbqYqBSN9vw6Qtxogo8F3FGNxSkyZamEzhufgqwQmydL7cKruECN8F8DyaZ7Pck8Va+yvaGNVJ8a3KDOw2tygt8s69C0HkkJ06fHAOwwIJFEtlgSCCPlzfcReQzipGm+TOXEN5Bo2NJ0fQwxUZ0xMaqQIeq6xYjY59hpmoIYelw6yy6y6HdU3rXXwkdZTppIgerO5VFIrCXXVAPr0/pXZI/U3S8GWasbOjmN/VznjTxGqw3ijZSwqP4DpqM2uEwum2JyDWZBxWmpEkvdE2lsp6F8CmEybg44cy2iEHoYBDdMQlUm5XqZq9UE30nR1A3zCEO5wNlw7zoMGtRnE/EzSdEtp0qeEJ3PCTIPf+UzfTYu/JoXXtaHXONWRd/NgJpDjlEny5VVjvNeS39iJAFkjfcNODYADRJXURr6U1a+eQ27E7srsPr62nAsXlU+4tVzTvUnI1KCD8hQOEPPVlY51xzGBy5vPzF/xk/QOL/TiaXVd/ksiwpIYUUfUU7rtrzDbrgxYfuwA0OeFDqpUymam9wXH1icWKVeQWIsH3g3jixOGNueh9nggkrMd3k8eJ+zJ3MVw6RiJIh6ax853aO9nxbl3l65W/qRWQG9Mnqyh5eT+wNzO5+tMNRNAynFGWNlfSU2zkM1UPncqUC41kmldvJ93iaiK3gjEbfxLqSFCUTUMTmgX/j0Dm6+eTwAZ1ByVydkAbIMpVz0suj7mLdGCHRenqRReaEpKayOAZgdhEIAv2Srxvj8OX6V+Pb6miszT4YtdSHOtqkmJxnj+uAAv38BRidbP1B3Axb8XpVKqMoNpTMsqSUnDJZ8nq5ELKRq8CB+deEHzJnCzg/N5o0R+4NCuiXdncg7cXori15ubiUHT85k49Mg5gkHMnzfsy4ig5qOtp310T4tPufHXy7c7PKAjZguWXXBAShC1q2ai1ZAb0ylxwPLnt6DyEbm7bPjp+ns7mNn4JQNpdBoAhQCE3gy+pAD7Rg2xBi48owfROLNyMaP9f7WZ/NuakOxxYdj03i4yN0umipqG6Lajm7IMdOIz87X1y2JuGMVwmyBoeNFqmJp3QR4oJpaA+cQCG9RgoewZUe5YKHFVFQGMWLfb5fH8euUnnKXQk9CxsacYdqaScr12K7sPXj+3IbqLlA+ewo3biKZ6u6BYSXqLDLD97djo50bz3jNvc++lwnFVpkpAwA4zlxZSjTJ8f9zbOh65qF3vPU+kZ732tdbtugd79seZwAFnuCUdwWMZkuUoIOJf08fuRdLhEn3+/mukwT0KHTa474Uvvilfnz/RdXt+aw3Xx7c36yF7vDXx6O0eNp9i8HZD7plFtrTSSvJKGhi7WWzbQq2csh7WFM03XZNZtNrbctOJD5LksmC3gu79zfBT6+t6L3PwW34nO+ujw7PTrstp8f13PrMXsx16BmWkZRQ33w7n47nyBqIvRq5hh2o0y56mbaU7cmZeg4M2S7aGovnaN2PODAYUSZ3BB2eQn+7vf2G2eFqLT66efXl/Mi3IHqlIOVB8ESYkpEoAnM4nhqukF7wkQJMPERG3h/e34s93ns2qMD7i23/H/AbX18W7I7/vT0eHd7dVknNxf/AKQJdUOt6/JtZE3JKvqcuSfKX3xeXp4f7m+uL87PTtfL+jkyRhBa7Fe2MNQBOD1KtMl7BqYyAeNxRebxP1jPrANCjZ3LkKraB6UDEK5AesO0mmgum/ToWAFTfZ8x80LxJwyrGmCQLDDN9ID4WEWP5s+XEQ167ppRR4ZYXSbIVRBDEV5raEKroe+HwzUMK6XaqC+m39VKWbjncA3KGYtDv2WQ5eMSWGPQoH2SRQFoyeZz+2/A7fXucn8Jn3H6fGM1VywO5bnIkOCPH2CTp60tJ8xmI37Wnr4nK4tl6aXIm2cgZwssNs7H6T7fe2a69Bn/VtJbqf/y/EamRU8bN/ow/FQ+p157+gN27Lut+pe/edbQG1u+PgI83h/dHt/CV3zVcpXI7phbfmo2zBxY8LBX8v23t+uJH93nFCVvWcEyzsmVChrl23S/9zADGN7y0bra6di3hTrr44645b/16qUavtU1auzMk+OqPHOKEizxNUThydkq2n1onig/UXNnQO6N++m9WhXflUwP2pQkvND3XS0zKze0O7ICAB1IitBkOJgGsasv5LpoYd8/YkYVoHhRkD9y6Kg43SqJdidpyXf2kwCo2b662G7eXh+LELsOTKvIrZJkfTbLVH/CY4vSB0tITkgzR+SZryhfC+t89/EOj/e8XgYtFNeyh7N6Ca7nERD7HkJ4lzYi1x6n7zMt0VTAlkxA/bu8YtFrfx56Qk7osUhRav4xyn2KPkTEMG4jVYdmo6GiPYVlX5nVniPJZU/zmI+G4Y31EP7dhoAFiGyel7pAHTk8w5NDyavkhJAM7KPnGfRRfAbvhWJHBpKAey7E70RAoJL40ejc4BFi229FYuor4PNLR/yA7y93Tu6qzvCcgQNMCECAlFOTyDSevRUir7Bxl5MJs2deCdL69wYPk6vUv0xnt4D5WvCiaNsIjLz6WyHLLKDVlG0mqGavOhPjrRt8lrhKn8vM2oJLJ+ORUrqf87LgXl4wW28Jq4PV/MJsPtBfbo/m6PWCPIQKCjApoIR3zS+cstmjyRwoCMPoghAFCVp71v6f05Sz6WduOufQo2eug2JYUqaOY16uebhNgGNmdmssx+IRw+YyAe7eyrmNXDoguHGEFUPWnf6+3stEDEbW6ndpFCq3R1OfPs4hhPJdzWJv/83kAW6LgCqGFJpKKHtCZvnrM5tD1gUhQqwsBy526hhgifHdW8qjHptrYXQHBH8R0sV9KWG16CVb0iQP5bIVp1pPumv02iihv8l9gwrAEgInIjNn643hTRWgd9nW3JuETTMdz1NNThXcyduoO0+9RlMRmBj3wlBtwMkc2CvenabrPmoTQ1rZO+BH4CEcggjN044hnyKw7i92SyJqCcUPT8j08q+HBn7KlDKEEQIRCTsQ2CAJzrCHoDWUQhBc3pf1qCmmGEQlAq+e8h4c+zEwGDPfjpMMm/MQOcflFAG/Ice9/YyExo3+3iIacVLIipI1mBW9q7u/SThF7b4PsKIKzf6oXjdsq857tUJAIGxm55WJEGhmNqQpeNTUd9kr1XOP1Xqf4mAJKOYbinWeIKad8QG8xieG87EHZGwGGj8SUp9VMrOfpA0IyJ7OksjDRoy01VyQ9dYuARoCc9koOHABrCbLRiNbraMJkY+jKUo7D6arjmZJNdCYJIEmMlJwq3IxfrEyIbys6EyzaGWTaQNrCuTJKLCyZp5lTakcfStbRiwpldbUQ99NC+WJUWRrpv5WOhvl8V1PwZcnb94ChFJIp9nSVOg1RXLlSZ5NwVklH56Uxfzpz4N4CJApQDoXrpPbm0YvKqEhQZPkqLCOnKa02YwVUWw0rCa2Jk+O3ADVl3caRCHVQdYe+Ky3sbRIfmE+EYXofqq5sWNl+/RRj+TIqzh+hXSeMhQp5FWAryJTRU3eDVFrZHN7ycddECdX1mv5egNlTyAhQIVCGuw5cOTEmQtXbtx58OTVz9RJnuxDKfjDZZdrrtgtXYYFMiuAG7Jcdd0dN91yOwayPXDXPXvkWGivxx56JNcP3zRQiROvSyJVzcKs3vooWnjF+xtgoEEGG2KoYfLlGaVQAa11ipRESC2L7r8ylaoqRA5ykQfVRqsxxjhjrbfPTBOMN9Ek9Y77rlGsuZ56Zp7nXnn9MkzLy+Ptg6AYTpASqUxOKZQqdSBHJYKQHSRoG9na4LAjDjrkokt4WFGE47QzttlOKtIIX+0X44AZLuATiBBluJGEpqtlZ7ZZ5qijw8QW21TTLGNIxohcA8Qbw5xFvPAjiDCiwBFHEmlkkUcRZVRRRzMNpzdpftFFH0OMMcUcS6yxpTz2OOKMK+4g8cQbn5MlTNnpA4dDpMlizY5SSBPNmg29U5aXaApukhJKOJFEna1n4qlIZRLO1TeppJNJVarrzzh/WeXoJAETJmlUIZpOiN9DXYKoN1BXKzoOEecH6qGAuhbG+CAdFIPKytQzGT2cfYWWw6P3OVkrEfCKJxlbnZYgRsZw6bVOeV+fORQN1/0Sx54DFCEhZNxbCCF6YYOw9KteeV/Pld+rCRaMScQFwESPXLFW7BmpEk/rOlevAqRPAElZBeYKH20PoSg8SrMSXakJXnA2TPjsAZyP/0M+lWNeoiuRrm4Sxm8IpXckRVXEiH2N0hWjUam1MEK9sSYqXoGXZmPe33SeqDU2tRkoADERIb4fwjMKqJIItUa1YCjBP72uIpMa5rnKJO4C5UDRC3ENmHblPSxEuQzsSBIysDMqaElledv7semZFfGABWicBctYSwmwtDpYAmKpj1haAyyBZCugisEDIKAglgIICgCAbCmopQBQAFErqrrpKLw+JRXptOKx45NSrBgX4UpJL1yAqxmBM5x74SpwdYDDQHtdV89jwy5BmcwLSY8V2PbRNg5SvoCr96r3EZx32V7yjyomfn0fsI8/lpsHV92MDnBi57nSk0vyWc1DYu4bLaNsYk6FvPwJiqj2qh0jg3PQzgSz2VaKjj+OS303PcvPkrPx2ZWo6Sw2fsVGK+dFnQEKviYKnPhh0vzzqvfi/DcaZXKNxqMyLJLgxKfm+3yrDRyXJ8Ih6rl/UhmcvFRRkgKqtVJjYLANihl7VihyfZXuaas+92WNJn/MM8c81Q0AAA==) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + + font-family: 'IBM Plex Sans'; + font-style: normal; + font-weight: 400; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,d09GMgABAAAAALKQABgAAAABgPQAALIPAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGoZPG4KgchyEYj9IVkFShDM/TVZBUmIGYD9TVEFUgTInKgCEfCsiCCoJnwMvahEMCoG5GIGaWwuEHgAwgctGATYCJAOIOAQgBYhUB4htDIEoW/BtkUDYxm6DT0uK9BSTBOrrfMcXUTDdHLzcNqpH5LpZHig7SiDoDmCP9J7cZf////8vSCYyZndBk6QtpSAgMFTZ76GSGcyDUrBRMjbKJXqkiqQRkQfLmlqPEalMNGrN/aOh5zYvtKL0ERT87BJCS4kSm5FtcusUO0VUhGDpFiYY6UTJE0KwdUbW6SgwFweVVhV/VDrhJunycnYaj925acHEqz5C8psn7TxLBe/jdC8fKLY0O0IRYcLLhExVUHz9Yv+IGpgxv+g5i+0GdvNf2Rd+fGGxQIWTXxuegzTIk1yFnd8Vg3KZFmmQXVZRd3GRDTsPNLR1Pl//hOQwfDMiYMhOU4m9iudfRco8kE2jY1E7kQzVeiTEpWQqBCl0xMeitsMeDdtg+BbaslFFXqCw1Wy71sA6aOf/pKyqZLjU+YKTCdplJ6N15awdMj5saFAYHRTOPWlIPSx1efNWKWmnqp2DNdPh5DdaipJjHPEr2D0ex3jueGDGpSu5WGWV2z1MvfQn76538hDYgMuAIZmW9nElz5MvD9/z+5a/9rnP6HZovIGHSqCnZ2hUw1ATYsaIE6aRIUZ1xIg6P19sQsSJODEnU8SJE/GJ6P553k3/vMt7D7iiCDwQUJCKEH4UQUAyOnUHB+DMbmtNp2aYdGy7M4eaxiy7HLPTNbKIiGMCyl9pVudKeHq8yDeTn2ThQIteYDqyanUH+G32TsBCUaeooESktMALWsInCIpVm1Gr0nWcf38Rrdttu3TVV7Wqi0XPQJfl0p0TG0QOyc1uOey2a9ll0yV2iTe6fF/ywRuEQ9IVDhEJ4nujS8QuyUsQa3fw7bIiR24tX+Rql9zbJcUub+lqK54kPVPcNv6BqKlpkU6bW4kj2XyR4pOJAghywiUAWEry/0XbfNGFsLHdBoIhJUv+kIuMoLkdfSxuYco7QdBgyC41YzV0o7FIz8fF9j7wkZxBGvEZBRiladNMAkFNFDa94XWzf0mwYIGAN4jVaU9c9+/DcsPyv89/E53+eCJKaelhElwPAh74F+byHaTX5SAHGbQdkmK9GYeWoEaoP2+ufLUMoc3/JcLrJROnt/be1ysQ7ksWaNgeMpDAliwZaHj9/2zvyobWmpaOpubQWW1Sp0Z0EZsGmyY2yCIPWeRFJnITeJH75e4HYsFkf0Dc1FbU2hCv/OeBQAkdy0+haowcW8g951+n/94YBXQlXQFZbIFJtiMnL/CJYSpQqtdl6DT0dFg+ALYPyQG0E7NsQ7xkemjaVjxITvbuf5EgL4PrCrl+Scxnzic/Bvg3J3pp27HhEUxHaBzrW9azpB91/r+y4wA/Qtg+DPPfJuXe33aYu7dz144fAJNLATuJSbKtI4usQaeWkEWThJAEwiBwoTjBJRbBaU5yd9y3PiwM+zvz3zD/D9sP65W2bDesd5/O/FkWrMgrWUYMky51NyMflIEyrwf8BFwhlf/PXL/3XlPbdpkiWiR/QjLMlyPiGfpFYZX5RVcQkyIW9MjlGNRuRy6n9R/m2jb2Jh+xP9FLXBULBcjR4mk8B59HJKupsszoCHrbXYB3A1Y8xowuia9/p7krWcnMrl5dsgMHyGF8YEuZuD4fqwzWr9TfQ2T//5dZ3vd+iPV/VXW3urs6KYUzU5sTnwVsAiCr/WyDbY2NjEaHO+iYGVlmzhpmZ8kJqwEbAbb9H7heff7XLO+u5ecrJKs6W6Fqb2PMEC5/nsB+oHGBoSRHmU+n/f8Aa4SWE9txssklgqL6UNRUk6Rs9U//e6AKqHpAvHuXgxuzgIeH92+fWECJ5XLugljb7bO9Jd8lLQ0o/1M1IzdFrivNwOFjfmdoXYesFbkxfXAdAMVx0eXSvrvfVQ8XKXeX/v9eNd2+yy9qQHEKwfGcIVxJ28yh3JmylzTFvSqt7nPeu+9//v/+BchPkJRQVEBiCkBMAQhKA5KamB8EYpCUfIaa2l3oqnFrH6TkUFOh4tKltDrOOV6ltbpZue3iZVZ2Vsk6i523WeVkWXdZ7bNf5ngX+P/ar7Tz+8zLnT8LHURHJEyPayUj5ODS260OoosikKAAdWxUyqc8kN1aIUwqQtnowPP/pvpvO5f72547dMClGkByeh3fYqPn1zdzDxwws3/S/KQBHCTCiaADSThR6huScv4xVbZLly5NIZ6cnq22AOLjcP5/NntL6xpllh11GauNr4wtO0gchU7Clv7MsUYy9MjUGjin9bHGX3NOtUylMTAtZMDR8us2lbnmzwJSEAFGlG20G6WbRAuP50fNS0gtN24rqkbU6kXUfdz3+d8wNT9I0F5O5Um1Go+qagj7lzL4h1BKiOKwGY2rvd3eftf7/73VmZn7dck8Ih9xRETEtrVWSilLKa1te57wvyG0e//PQUc4hISIiPPR0NQQweP8BM2TMr7arS9TjBHGGCOEuKzLXF3GtEyAKp0KxZwF2an3a1xj6f+gpomvzfFz9yfWeCCgoiyCQkRtIaLWqohM0GbOdBGxxRtqfHxXZbWCrpzZd8WMEeuQHC9PYBFqx45AaAQraqe9m8gu7QifcSH86C+sKFHHQoCqj0L5rrD3dTIQOUD5H0jxrt+uPzXWh3F1ejXb7e83j8tzJD8JxGR2ebwt9LRSK53pqqpqOM01POeh1wMKUrCjEgP8BIAwVGgww4QIBAly4lKcuawVsIQlz/coVIK8dXO/CQYryU5xMmCUCg1k1GfjmZvcOQ2yYrRpYkUlUkGmJXQJqqKWJZV2jdfvWlq45z1515ObF5+Ou8WXNnOX3bf2dhTCB2NGxScicmwRxQMyZCCcAA5J0SxAkCmQhs7r6ZPTjvI+m3xlzZ4zxUVVVlXXVKlarf/f1pxeuBaQrlOdMlXqtBLsw8xk2GVu1a/cHD+e3n5wCIECIIQFx0YAHCQKjSDB4AAqCGEVx3ExbDQ8eswM2fPO5MydV/byVyhV/upv1fjPPYVXKsGC5ODDSaMIky8awSDZOSiu8kLL27exHfRpR0/92X9kHC/DRjXm+kkiFVSoFGX6qVKtRkNRmq8QfMEb3xPPRBEhJpGiiZVYi604FCmuihaaoFahulJVhUrV1EodqlJX9XO6WXtLLLVgmTWzVr2xZ97QK+2v9vAaQCD84Np5ZL7kOzhZrebgopZX+yAQ9zUBHyZuyo7CNjR/bpNUy+t5F0XQR3QA2Py3EALosN7HyO079x+Scv3jR+9gvXn0xtvp7njs8SPkSEBOKQI5Wp21OgDKp26/cfSIlE67TzPaumI/hQoQZO9nIaBmAz8yNcWBgsYLPrav6Gjr/kBCcIHA4WIDBqt719rxL+PL8Kys5BUuf2u68fINNlCmV2rr+TKylBV3MRRT0RVlCSm1uC/pJDOUjoRzP964Y8/FaI3aiIQvzOELaagiNAD+5BqfqwZ82ZwmTWpV252kiqg8h1VWl44hgSqGS8dQQFWuLYMwUKI5Bqq43KLau+Z7D5aI2t5erz50eApn6WO0Ly4d4zesG40crxTbenJIJ7FVUgblx17IW0YV6hi+FPQXOmNQM9WcRJ/GbNXDt9WKcOGsDb1nialskTylZuC0XLoyfVwSK9ebw8o6llNdreJ9P2jSjevO7topP8pW6yC7x/Upqv3Nbc2fpcjDtY077To6HmNP4NR+9ZBpmUZkY/l2zIPyhuYY/XygIe5vTLlYJ0V8Wsvh6t0LOPkrb9dx1usB+Cpz6aC/rHLMGU0mUwMv22EKW6K/q6qbBR9RtD1dTqOv48C6+BBibGGs+A44yOC44+Kd7CtMzq4wO3+oRZt2CT5bsczgiuWGP+8KwcD6pnPYicflfueDRf+tH/0G/NVf6MfwV3C36y/e2dZQ7+nfC5926DZuMz2MDJpuvHyDZessXbJYtUW7sZv54U9i4wwdMli0QbMxm+hgaCAtjhfEcqKRsGl3PS6hgzAJ7Rp4GeC48nYZ6S9oQBlc2gcEcmWbWN/jE9gM0oInLjwLTJxQcDqbxivZZ20zuQHyAyrBeREBjXfBtdAszPQxWLamjrSnuLzMALKlAKeTiCSFJz9ukRktCrDAIIxmuRSqBlQVI7WCOvHpki/kRX94PHTmZRZf1Kss1ESzEUvKfCm8AQq8LGqiWdYgi+5D1Cww+ohEU6DoJQJlUaGbngM9Z6UmB0+MHIKisFRHwk/RJt8AMSFpIpWi0Kp+OOT2W48Tlv6hYQi5zaSiBJCyQ0v0oiIL8jupiyAE9vTDroKwBRPgYsQKEcdqiV0OMDqk2jLvOy/Rh9qs8plhhS5yeQIBNBAAgfA7Lg5Wa3eJxUNAQAuKPikgXpStODjaG53MD6VPZPBPyj+V/n4i0Kd7TJ/4nC1jF1ZOxyKUS913yT8YwpYsFdglA4uNDSVLEdpGGwk94hEi22zHQIKJ9cSaDqu6JH1myeSTpCzZkJxCsReao1Ccx7RceVhuZuQqA3vKFyIHIlsIaNjQsGckvxdSQaEUFlKRFFdYsL5AJPyMkLU3u/4fkeLR3/bu2npZnc//bGjt5jwbdvQ1ZfX36lloeVtL9UVcOHEl4Tiii1g+FRAOW0DE8DEWpzyktdZdQ7F8v1wlHoeNCAgWIh3cfVRGh41c8v4/zaYftTlw6qR5UI7zpheSD8zZzN8QhRbSV9BiPRkFwMmOGOSzAw2zxe2o+fSd4QMn3swPRh9hoSCDAJj9Pnzp6aFVxgYbueTdnim+1puV8UMcuv2L8ujwon7zq3T7eAEKrbCs38SuZxXfl6VjqVn43bxlAJOncAinJxZsmlmBuy4ekJfg0Lx13x5f/RzcfWYit+ju+r9XIUvR9+YYIsD+HuXkfPdDFszJOVWHPl3wWXaUbn6g2I89cfjS13s36eBWz6IILPdtRS/hk79ed9jIJa/MAjmRaNy7HMzaEx9bgpMfW6UP6KyGX2puYPgr5C4Fz92J09ON2qZrH5rKf/F7c5olkGXmd27dYSOXvIslyS/Mpsevhru13OnrgNGv//2jPMARY+mJMA7aIyYa5liTKXhoqh5PmgbHazFI54HAHQNd0rOE4zKAjVzyLpaAI8M2fa/u4prnSWGfCXB0o8kCNod51uvS+PvjpWf6LJEBbOSSd7EkI2y06ebVDBVG+DqP17jBiZNG02MgiPSETc17VunauAB2rLBsXsRRchVfUqmkH7zHu2nm5QZ3DBfzTB8e+y+1MZ4b7eEKPO6WAs+Xro4t2nQV8tje9cnn4MTMFqbKuN7mVXurFAU9MfTolscl8PGf2NQN7mwM2/dRWTDH3loZzh7+Nh+Cpi+vzILinANHdNjIJW/hBhHbKdDL6frrvOcjX/1+9NBqGuy+gKUE//gkm2SQt/ux49cGwn14vebzxmoT6ZVm8kvtat5JGMNI0afRv3mYLrsCbY9eegu0+X54XIsQPAZqcjK3U+wdjhNnFOUKdXTvgjr2H8twCLwC0fi/2PazRN31sCU40vMQGH0npDhP4m6utneNjFGtvD9Oep5rn5HRYbuYQ3nsCDsOnOYazMOnOZLBna+QQmqT9eLYY2SzjXK2/3SEtTnVEgmxLh99ZoyUUchTaYuNXbvFxidODDQ1OJm//08k9jZd2qfMgtESMwdXlXfYy8o03EbmqNf61gzhjTmC+Fci41NCuKvn4WDR9Pt+PGjel55uUuklyd9hM4L0eEG2Yxs3chzF9rPEJo/bptx2I9xGeYwpu+pTFhEWIwhTw2ZnFSpUqFDloARlk9vUnIIKFSpU64FM28OxBL7i6+UTlDGXkpUZ2Q7boBy2ZwhzqJjbV4pEVNSoyjrr8dfEM7+1YtPztW5DPf3sHNhvMJcoVqidyly+eJA/MXdlUmHj9MczjOop0LZY/IRrurkiIPg4l87PC66Xp9IcHRFrxh2HgOYal2G6ZndTTVZ8RvuslGrGq2+kjkiypTowbDY24SLMLlRpLE1EKUTuK9yY2xCSAWMhB6JA2a2lz9cvyHh00SpwbUVJcX1mElCz0u4f6xeR+rgMXk21ovaBxpC8tOvMccOW3B+14/W5K+aKogpKWW0nZBcRuce3mc9MrSYeL1wZfM1e5n3z+xRzJjIMtb/0LFVTtTSVIY525SOH5VRLY6nCDXqd48vwo6hdj7S5l6a2tdV8PWKLbXZMriDz8ExotUPr2y/fkvPTovmziaN/KiOtMn/FCsPQkJo/J3EjlCVj90s9w2IAPf3S83Bfu/41L65sWMvkROH3nRSPvHHSq+KxER9CpNelZ5AruMo4kMyW52ScbPNveCh2ov2dio7DV1hrWpxTB5tWWX5zFiaEGeVPN8uMHmNVbC7sCZba4vjj0zfG8FsQ+BG/byOxMzb/fHPt6yRckfPXaKuwMkljrIN46ZhvnODG55xdLzBbJ39hsu6epofR4zi0HrDzMrREVV/OZt1ex3URR7EWj0mXj1ASVx3rhGPUJvFvqJwV8b3Bla17ZrJhNJQSz+DPmII5XZa4UHXaDwOCBO5nEjVJztgu4VX2O98c9SFfz0Uj8wZef90LGTRb3bT1Nt/T40j7pFyQXynDiIX5P9omXMdwusbsW69Q2TyeXr/YQT39UQuKTlfsfamZQoUdXVGueeot3/5p/rq5xMallliuo2OYJj/BZ7jvY/xS8u5MMGJjly+koWDLYJvHfHg23KefS2U8v+OQ3vRtWs/byCTyOlmLORttbucV1r0edLRJOjJ6AXGdNKa55/LrPMTY/HFN8WKOaMoB6yPuOqT8xELMjxddSyD+gziwzSnLq8PXsTWhns9zaxeO574Aefbry001YEYSeUz+p5eLPJr0fe5jFqbdynEfdWXQAiXac5mKOfRPqyxU0LLRVM6ieNhoiqzHL3zVuC4Irr3nNOcG2+LPW8x4XDGPU2rdhiUY10dmi+xF3uGRPIvvcsWqmhO87rW6eKdguSj4U7IGPETt8lG4demOWVmutliGHFl7XCJ1Q6xoDK+znguTNmQQjUQMFLGAyUTcyzp2ZxIKs3kOWFeEXyWlSt8Rlo6jgRvER/yOUXFsntlPeB5hvWAlZowkjIGQgrK7OIOSYB3FC6QyXL0VylEa4xyWZHldFhrpmWKW4Wx37UTJFYxHauUvymw67inUR0pCTBgNyYKiSLY2wxG4WVj2fMWcAmTXoeJfh6/ztfgtotl63n4G5hgzxw9xpqnf132lIGKGrntcwjEDmMyTK/ZsJWZDwwpmRpTPJQZTCLXyG3Kvf8ej/gbcNuHyYyzrnudr4yNUVh6OSLliX3tFGrCWdf90VLuPRv5pXv+Q8xDaFq2C+pjPnxoPX2VVVOnYQ+75BmoHX/JbP0bvlXLWtIS3/VWiF3z4ISksVSp8/zTWj9d0r+JKjmVNa8wN6TuG9DSumJA5xtAq2RA9mB1+u3t1F1zZpE/qpdmBGdp2ZtTj+lrq2vZa5kYLpM2sTTe4X8L4oP+FqUnym9jcE3IWKtWZ/6TQGLzq28N6RNpxCibvFwvrLwQH+S2gVTwjHpmOu+02A5poH5xPvM/sbrfU9m0F4IUCfOYzL6qOus3Om/mqbGP7nAmIIOWtEqQJy0Yx4u8iM1W428I6N0tjj+1RcRFXvWaUcCGN9u5HB/gd4QADZi1T4qNnVBcyCz8n53H6A6Yhbo5JwqIgphqlbPsWTtzUZpvl76p/aMVsJvPKrn41r13YW8cjvH4tY1hHY1JTI8ch79En1y3QiOfzGOfNqNX2k2c0aqOUSOJwl9pJyw0UY8hu9n5dPAujYR9uWhoxpSXM7EHkJ5vZYvBbzyfhO6kegVY+wgkdAMt3Iv9fnLtt1CbnjPm57fYq38omy4vnmZLHUOWxG/0WyvU5563yWi6/lglqA/GscornIWPZbcvG4IVTeZhnMCKQb8AyTGZfmTeJIq/Zk/5waCjWN0xqFfNTabV4pSoooMs05OU7lTW8TZf1GZJoHjtH8646+6rqknqebSrIWqjKU0XiTtEHuT+n54pwEdEnDsik2VhIvkpONZ6dSRxhKLK0WDjFkBvpHjJqOkfQ0FGI6XwPvXhRzPlLJLDSfQUFLFVFvdoo/wMeoQe/ZiH9kJ3Oj+/5B9o97DPdNus17HkXubwGMY+Bwm63AOuIbsw5S68DftVkvsBv5mnP382yl4En6p+z+LI5+r8zqzewOk6T6FFymAbnG/GRT0PQ1k3YZmuIXm3TG6Jr9HFnjbZuotZ//Omgz/2k2y92y/GhRac907NnzHbID446ORVLr3r/85XVvb2jBdJyeqO1Ubqf9F90TcvuY5A04QeaKbbfeauvmf8vsGikftKf6UejumV1xZfywK/9gKtOyN+BULP0PUXGBIHDFjjGcQggOK2UC5gCiGYQgAF48oLWEvgv8dvf/gEHhjco6PxJ1SM/sEHFE6WIV2rwIL5UUuRPWlEGNHp0coDHH7NlJQbHOZ4WuGgfAzAQV5ePy1BvHx/g4wAdCteC+2rbBAAFaEUkQHqXRlB6WBzbFMkBAPMa/VusM7OFSe33b+05o0j+k1nikqvUxiUWSOkvPzClWlVKfGMLHsRba1x/0gFlQZNHJwd4XIcfWGk41HGeI1/JJEjV53omqyQZFjKIZFBvHxeQ08TFmp/xbqYcL4nZdLv/iH+170ef2jvF/d4Nq4rre1e/q9X5s1P4GfnesawvcIyzn7ObbOs2hwUqB78HyqznrERLuTmuYFXBcNMJQ6z+gN6pe6sLak9qQfWej/0Vq/qUN5SHZD/LYsV7xEmieqFTsE4w70uA9zvvNB1ArSE+hqcDD+JyHpOF0zgHOET2LeYBZiOz8ALnGq5+/5HHae8Un5oROo5j3JBgROgfxT+x2kl/8X4Utcz3o381Puu5h7GU95tOhNfhbzqcSfDmFNJp5SG+jcVJea25JURDzqLsKfi7S5xF/b+cjk5y1YlU+e+QM/tn9MzH6WX7c62IJkjwa19QY7xC1N6ttBKtZrLWejbFys9WqkzOa1NwxNvlvR6F15Xr64GNCDjmIQS49gIEHzyPtP0YQkEXdvIiJ7V3H1fBSBGKuSlJpFaRsdw2C9/vJSiYNRncNyRnxjFmhvjBlPezzNntcSyHq0MSstGxFb72s251cMwwkJERk5CSkVuEsISKmoYWAASBIVCY3WF5AoUo46pAY7A4vNAdWe2ELrq96HgJLnNFV+IxQ/zuwNZ87fBASapB9zrDDpV3V0RRMFLkz9J0HpZl3TWTgwO7hWfVEDF89DAKMEcmUW1q14xpo76EKv0q0FMpaYcajJ4z+rG+VAr//sy9qMIwqJERk5CSkVucsYxo1ayVhhYABIEhUJjdYXkyhShRgcZgcXj7lI4wMbNw5sJVEReL8BJc5oou3k+EUHtvQQJ1QodTcstbYs4YUDBSxEbil/xPpMkBV89yaz35+sVloyF0avetf4Uqqu0vtYktsdZyog8f5e2+s9LqytsXi1WbS91aX7wRLbnFY+KqQRSLw5ZGRU1DCwCCwBAozG6fPK5ClKFVoDFYHN4+kCNMzCycuXD1cz6/wFe+eWP8XeRs/z/K4TfcWaMN9u/29XT++Af2t0XmCNeoudKk/B8U2NIVO/VyMKRau1yQfaTQ31OKj8y2+dBnKQ7v9z5E3Vbr1Xbs2Gcndind9mIf9DPAIEMMe/GYl+AyV3SdvIEs7ODLRwTY96hr2HMKKVDQFznJ9fu4CjpFvJYmDmG01pevwN45j4cPi8DfcwcFYI5MrVRbiJOgOCIGfTIMGrbFl7M7+9UL96c/B/v6Oh+qKUcY0ciISUjJyC2gtIKahhYABIEhUJg8kwIlKtAYLA5vnyztpeuDoERx4qTMLJy5cBXOlf6fGhDw7pNInu+5DR08yBv2osFvAQWjjVjtnvNxUilYK1nEqH8F9rwgttsRlFtfRVOubqCFtwdjtutRY+ZNbMBqtlJa4mCH4WEjIyYhJSO3OG8FNQ0tAAgCQ6Awu5PyoApxHO/D43CCz0eteTMkL5jdqwPUafQUlPaA6Z0TqO4JdMrODMcNQfYMXCfSWChOa7fHo8Okec+Rcqa0l5qyc9tWnLZn31WK9D0kLkkXGTEJKRm5xXlLqKhpaAFAEBgChclDK1CiAo3B4vCWoCOZndBFt64041JJu22vuty1EVXCbvu1DFErYvCUHN41TQRm55+8XotqO2jg7W8uVcSol4MB7Npe8jpSyop/091CwNq5c7e7Trrb2MNe9rGfAx4KYe2C7UqHnUoX3fY69kE/AwwyxLCuECY8Q42/T0j7VQVxu5SmLbmn+KriNrPjLxT43RyZLKN6CbN0W59dTcyJPixzY61IfGKIJNS7ukwtMdeyHX9BGxwgNDJiElIycrtZeQiFKFGBxmBxePso7iN6EI6cHB0nmFk4c+Eq28UcXoLLXNGV6e4yBNjysOSZt5+PAgWNANFBDrZ9Nlb7DNzY/m0KjCMuuhWwkuI9/8wyJmyyWZbWNSnDA0ZCTEJKRm4xawkVNQ0tAAgCQ6AweVAFSlSgMVgcXgQdaeyELrq9mMxLcJkrulL+R7mI1LIZSi5dRLKjUQ36w2x42004iWUvLjeDyLadwR/2iwNCUTB1LATbHka0JqP3xRnLOAmZ/6kUf/NBMgyhR7tlXuwC7bYXncu82CayrlRb0lzpabPtiEE/GzEk89XTWcDt2JpcegwiiGiYw0iJSUjJyC3iWga1atZKQ+t9Ah+y+ig88eyrI5gSCggMgcLswskTK0SJCjQGi8Pb53ZfhwfhyMnRcYKZhTMXrnf/AUKAzS9SNG/+dFF+KDAHYcpGytfd8nkysi2vIIe3/IdwNhE8uumbFFkWXTTJ1aDNfihGeDBrPN13TfNrlegkKrfOhmeMjJiElIzch8M+whPPdjvk5xWiRAUag8Xh3Uf0AEdO399/iBAJKofSFx4js8y7UQ16Zie5yMmOFuJ+OAhTlPiJWQhCHyiY2gi1n48QrcmMNmLVcl6SWiCE3ZqTkYLVAzLU5peQJxd0MRxs/rSIWZHNdo4Eg2qbNajbsTa7BY+YTr1oiD0bKWnMX3fT81jgzU+QgNl8lnVBoCC0YTIjJSYhJSO3SGAZ3apZKw2t95l8SO+j8MSzr45gSiggMAQKswvnphS3wo43OQqUqEBjsDi8fTXu03sQjpwcHSeYWThz4aoOdJV4hWjHgDBZP70PATY8AjomU3MoqkFvOLyYmQ3c4yDM+gKCkSL/IIHjgqfaja+qHo8gbzg8KIdKt3Lk1gwjEhxU20GTmNtFvnZw87pTL7Mx2b1sAhsOM6Oid86zrUPkYD3sLHaddLexh73sYz8HPBiRQx5uHhGOcswTaayEKqo95VgDpznDWc5x3rqU1mMDNkITzbTQSpvtWe2owE6hi257HfugnwEGGWJYVyl3UMQYGFUVWPeSAuv/KzbohnMZL8OdQr925Jr1SJHf64yZ3tetHerWPbNb96pu+dFu5Nstsy9TM+TaiiZZaXtG1p16edyIWjuHGX/9f7Hw9SgJYP0XWcHwLx08ySEPN4+IR+GYJ8JZaVRR7SnHGjjNGc5yjvPWhbYeG7ARmmimhVbabI9uRyY7hS667XXsg34GGGSIYV1ZdyiGX7xSQVo2U2u3fgcFRlZVB95ec5Y4h8svvzAjHUV29dvPlOs6upGfdGt/mYcGBus2Gup2qc2OzUa4HEz4C+elhXbOZ+swpqSunbuOudvYw172sZ8DnghhJVRRbd1h67EBG6GJZlpopc2OcHZCF926Av0xis0fNcnXXIkCCQW9Z8PDHzvJk2WBDwfhr/0yHiY+JPAVcZDA7VHDvrnm18I5t/ZeEXCYtG5Hdem1Ale/oFhTHjv1cjCAtX8TMA+fOmOuudKOqN34bexH5tnplF3A3cYe9rKP/RzgIIc4zBGOcswTOayEKqo9mYxT1HgaznCWc5y3NpZ1ia0X27Ejq53YpXTbi33QzwCDDDGsK/eEvFp25TWNB/Rohz5f2cxjbYsgsjBvvvp4nAyyMh8gZzylRJ+BGkWRPmpuhAb7MQvDPoyapjTbwgLqay8w/JLqNRzgAAc4wAEO9PZEFCuhimprd1gn1osN2AhNNNNCK212xLUTuujWFe6xOGzUmrM4ALhN0IvhhxAXCdxI31nzVuFgxj0OXo6vEnk5PhvUiBjM47MXuYavO/XylAFcdZuE9DFN6O3HZHae8cRWK5Uqqq11rDtgvdhwxkZoopkWWmmz/ZgdIewUuui217EP+hlgkCGGdQX9p8sRYHgQOi6zFHI/+guqQZ9nwysehLMf0OGqgpEi/yCBV7/MOHnNWwszPDgUDxoNCtUMy3RXDfZUFrKhQ+vlwcrJULfiQd3wYE+gIktipGTkFmuWh62atdLQAoAgMAQKsxN5dIUoUYHGYHF49/k8wJHT+L+Y7b+R5YqNOGDWYBQXI7jKihnvmJtsZEkDzmrjvNlLU/GB5a6e0gFPw7A+/PYoG1OIeHl0cbiYWl1ol1OT+PDV1I/F7HpqabkI3E19P/eAH2StTZ0obPic9DQyyQDbxSyw5rzsJucDK8ezg6EfcDSc5op5csinvN/D/nhKHvqfmCjabPBq9aMUv1+vFAw+3bZ9lhu3aWVYjRHEJKRk5BZYQkVNQ+t9Qb4i6ISAwBAozM2sW9jxJq9VgRIVaAz2xIUf99KP8FwVzyOwdYfejOaLCet5mrEajuPENTi5Nk+hdDxdNB6bcZyV83Ng+qj52m28qit9HPvjsM/Cpmaxdgbi7lDsestlDF1OYxw9zGV3Gvx6j0PAq3dC4H6TkA+goBv2ctlRnBcrzuEqUEUkycuDpKn+MNmtwV/IQRreqdjaRFajRkMMeg3LxZh4PB6s+DJTckjKzGsIErrVkIZ1cU7yfvCoFEi9mI4p78vAdjELrJ4k++M5iUEp+66Io+E0d8o8uZhP2XhS4bwWnr/zdcmeWqUZQ1Jl6X0+m5OpelvY90Vs22o5gKwMqjFMaSTEJKRk5BaVWUJFTUMLAILAECjM7rC8aIUoBYXGYHXgS+8dew4cOTEyMbNw5sLVzxfmF/jKN2/XNvjcMY1SY98aLDCD4fwsvOKcuXpzvuG/uAB69ls0YD8PEjUGehjpFDLRLeuABMrOOq4HHx7BLG9z4688bJTRg3BWXvH2KONE6bUiWz5tYDqnZ+8i5BjofWxediGWg5l+5ZKHtysfFgjULpAasemxKOlBUZyQqtNlH+JB/1kDZKQ57E2S505vfOSf+wK3vc/NkuGbiWDpSWndA71hpam+X5Q93lZrjfBYgBusuGgMFY7zegKsQsRHe7tMV/tYMzDzLCqYw4sv+ccvCpqXHY6dAblJkoNFT9eaYsLctYAVh/CGnPa959eu2L8CS999TS3/Fw0jG5UmmmmhlTZdsz9PISJJJVD+JQkWWP5byAwSoTrp9mE2WDQNJ73Xj6viqSGIZz/HM7gbQvR+Gx8sPSVUFCwrdBTtk0HxgmGmTJSvNRGPrCigyLYbF/nYCajlWAtKQ0xTT2N4x/2FkXLZJ4WpNObV8s9L2F1i3efO0O7K525lD3vZx34OeDCohyJ6uHlEOcoxj0fnxFOV1Mv+RRXd0tFObr/kQShfRr76glmhgcAQKMwuops1t8KON2URKtAYLA5vX4ojTMwsnLlwVU1cNd0HMQYC5hcKwcny6Q4cePFu3Kd4TIsWCIeXvlPqWrSMjquAjU461FlvEj8rdpyadKbu2njJS18QEzHow95Jvx5fY+j9+AYkjGxYsNFoopkWWmmzfdZe6KOfAQYZYhjXuPJlRW5GA1DQt3mYxZeEZI+CThGNpGk0YVPN+PZRbsYG0IR/oKaiAEsLRI5U02hrIQb9bMrMh80Jj5GQz3O8g1s9BIc5wlGOeTKKp5o1cNozcJZznPfDcH5kfMwn1kW0HhuC2ghNNNNCK222h7YX+uhngEGGGNaV6S6LGSzs6az2DQ+XvIzIkw2a6bLmRvz3aEODTZiB5eSMJid/3cXbWcDUp6ukXHxcFe+3bSViIQnPlvaT+EPj+C4qNCn+4q77XWNtN/W7rxbuvVytrudgTQtm/rkVvPDyd48XT4KYEvUYS7L+EEmabeVKXeRq9c8NL7qPNnxyKPaPgW2I9OInKHONRdkxO2tlI87HE+TyYR7T/NuEpNhNIv9anJ3n+Aqdqz2kmbYHWVrN9pZDjm7f4oHPvqLNiy6Wpt4cS88CL7pnlcacsVMvg4HSyvjRgPgssynTajDnXw6wYBxvQWXn/SCT9EE4w9BGRkxCSkZuGdeXBHZr8ogKU6ICjcHi8PZB3SfwIBw5OTpOMLNw5sJVjBfzeQkuc0UXVyi2jeFk35f0tLTu56tIk5k0U9ZyM0tadaEIHLDgR7gk2fhzI1aGcGaxCHZCUHT3NRnVKG0iNTgHY1oV9mw/x6g8Vzq+WCqmznqxYc1Go4lmWmilzY7ddkIX3br2rT8StJx/E7RvPxUpUNAIEF1KUuW36M5YGni7YBIDY0eKyRX9tlaWJfTz1e0sYMH9hLTqHlbQSRkeMDJiElIycoszllBR09ACgCAwBAqTB1WgjKsCjcHi8PoOV+j/G+FMYMJlSYmyANLw5++2AZ5zXjGY/U6l8Pz1KjHzD6jCzJtnBEzr44bnNWAUzP4Rz9TlGJj1p/gW1Pzdy1QWnIO7n22qptMLh1nMzBUJOM+fFwvexref+wh+Z14AzIv45ScPpAoA0wjTSgRp1jJFnihbmFDz9krsmvIyKUvp4bTMqaB1+PMOK863yV+UzuMjeNfNuyereXdf7CY3ZsaZurs5A23Xj1axKB5gFUiV18zcw6a7v5ypKVH0UfP2XFc0wqJ3jIDcRVHGwGNzXy7dv44ShwQKGgGigziVN8deLiUCSTpyeKeUQw7mNcyXy8CGUqS2ttQT6Fkl6VBs6qVJbIpzUvrhODmtBEIKpM5pmM70izJDXHVGWyNzk+MPFMXsqMGfcIDTXN88u5ifyOMKv5918NwrNoDZR5WCufdR9lF8ucL0RkJMQkpGblGBJVTUNLQAIAgMgcLc1OcWdrzJUVCkH1BoDBaHty/UESZmFs5cuL521JHk8EI39wfd3G+38xqrPDq+xQP/54J27vlVLNTG+TAf/7TAQshrLXaV9wM5KSHrbty09v4swlS2nPwy+syeq3OFmCwo+Eop3N8q+/m1nTW4SsI98LumrC/V8y+45CKb9ZGi7FeY2c+OpafwDdTsWcGzbxiUMDVMdx89yWzdOLf0XmaF6urNeZZ5wbrMckWVRbXFL6KL9dQOKcowTlhOcbmVnOOp2M851xc9n5ICBX2Rk9aVgTvzQDnmZ+S9QlHgKxRFDWYWJxjCYqS98yZ3RQQ1P6zIz7H1InNHS1BzrM2avimGUY8IpNblLlG9J0V+M+23mLUsX5XwfbZPsYL2GYnpea/krFmFlAek3k/LlPdLz5xmMK9KFGxmynEW3L4kWz6cg+l/AjtZ+50c4DR31jw5n5/MphTUm4V6UhRsy+1LQymfLKNrf8Q2WNg7Y2VYqxHEJKRk5E1RsYSKmobWe46f/Ela5uUaeBU4QkQGhkBhbi7DLex4k1+PAiUq0Bgs7thHc1R/VY4wMbNw5sJ1vKkX44yW+uYogVaBpp6PrVKgfSmQJj76qpmKc/bEfDlv5i3l/6TyYjpPryoZsFjOVVJuoJ//syjknQM1iWDwgiu/C++Z4gTk2vZAvIW8PL08nTS7dAb5T5nZxYTqOi0xq9csKU0kpGXjJa0DcSjOlMlKfnfakLbtxNDnDA++O0//W8EzLi/wtKHnVdTmGZe3MH2vg7sZl7sZl/OMl2yz97ihjr94IxJ8WYv+0bDILiCgO2T6hTatXE5jMv5XQ19nRgOSwPT7SEvj8C3T1BylvBIjzuCOuQtGy2sAvky/ikDtvJm9KoVdzrj93UyBQXBvEm/NgKmfNKtnrzDXzO9nrOJtje/F+HfyuzBi0QA75Lv3shM1TsxlkztdyCyIBbj7Egw+s8qXRKWyJA8soxNfd462xjO5mbhvxaqjQqw3Rc/AcF708pQvZhr5g9PGuux0FQoiyvhY9GfHXCBmFjh4APqaDfrPfPUSjj4UjDbCw4sMQ6R4WcnUU1JQvYIMNf1v/VcthM8R4HBQZEt0J6P1tummqDHV28GM6uobsv0n2nsJoJxmzSoAat0QMyEpqXjpkTMXypRbGwu2nkJVrwj0FGb6lwvTFRobTnOnzNOLBWD63yga19UVRJrIlAZyGb3G0++D5+Mm74yVQTLDlEZCTEJKRm5RkGU1Vs1aaWgBQBAYAoXZHZYHVYiyRBVoDBb3xlfRrbn97uc1Wue114r61NJYpN0Frl6xhKfcEe1VCh5sbHHfx0hSczPL1FXmvP4W8ZkzXJmiUyWTttykowUCKNNdZaXE2phkg0xzj6XWjpl4FWx1+xTfphV3JzDlDnaYYelulDhN7eNL5y7LUx+1papw8CsXY/s4ofJudotiOT8Z3T2mlzxurhbnR/ok+XfbLYiLDoGnkeIxMIk0S7pyNNBO+dNlvsLh3NJ1wi1bsaSc/OZVtp3TFzDxEUsye/+DvIx3jZacDp5yNOMv4kMm709307ltFuMyR3Z/Hp78e3xwNy/EZPH9QJFeDwzW4GJLm2I0XejIe15DYFro3BdZPvrTmqNWDAYuKE3nuCrgLHfVgY2Ggx4tY5njbTGRh0jVzdTbdJvp8hqCGU3cxLRu4O154FwsenVeqA7N4jSl449jZ1Q6ES+tHzv7mC/Bdr/GqGV7FynSZ/44lk96UyfOB/Q3F0Zp8bVegOodmHwvJuLj/aRgckch15foLNriyZtza18iq+bJkwZkmYiV3oGKM/4n51ADF82D4gXFl/RbCRRjsJRveQfLenwoYp5wy4YZzO14LKejg+abY2Bin3FqcWI0TLoPp7SZrrrzFCKN91Ol0sNSyR8b9E12qsOGo30HhWsX+QluFaoAhapoQYaN2B/TjpKkmrAV95GOzC+FtFy3zI68sau/HAvpdvlArvKHrsTSU6bJEdOJPtjaphvi0YE+KqXhrQFsF3NQk25gx4HTXGfMQyS7POlJxeBKX//KsSCfYQVGSkxCSkZukdgyq1WzVhpa7yP6iqATAgJDoDC7EG7KcivseJOjQIkKNAaLO/axmvXFOcLEzMKZy8k1UkbSUROi9CzBwOfXWiaSk7fz/Wkw8aiZ2vnh2ZxbzQUymc9b0Wbenhd9vPY37WBuThUVS5+Pqz3p9rMg5Qvme7WddJ664Y8wzQukoaGHLRNZt01JVsXlhMtG3iQ93NSipzE2TlyKutu+kfXIWZm9INLC+EVv1UHhTX5SMSW1DP8SXrofxh30zvHQ7iE2bx8OORY8lS5p6jPwNi4AL+NR1Ej+0YE6pMVRhBwX+EkWX7QTyNHx7/WRggmTWMD4b0rku050SUvgScWg+pARMP7T3KjueBT0PZNHHMNxnHOcB5+LNIEjhyErqk9oXOPwzNRF1vuYGsmU8DO8ZiNbsgPkG82GdxuF3iZO0cYdL2NW9uuXBWz7laIqFo17xucT20jYD2jnpWdYURmG7aId9E3hNLczj6Z4lTArfd+rLGHF7IxlLdYdth4bztsITTTTQittukJsRjRFMmzMcTYmPIwxzsRhMkR+UHmKlbwt0SW0yxiyUNNF/QjZc/jZ4RoNFVyfE70XskBctgT9e5/Iz/icBdV/0K7v2CFJqahuFPjJ/gdZl0SL2s2lmQ/3y/JRB2GkkpfuDBUXZ/piscXFUFuKubafZ7GpmBebvo27XoXn/mWF39+KzXmNl+DpIzvoL898CTz55jT6UaXayfSp7zX+qi4WeEzcouIcO9DietTaWJL1vSbOo//LfUcL9NQGnHqs15lOXa/K98uoNQl6rzOF6fnYNOjbb2bKWXOOxrsf9xJfp1/KAnCRHYproqYtL0+SmIj4JU3RtGezpVrIBJ+g6+1ThungVPZr3fmFaV2J6alxSntNUPY0Zcvs3cL03vxu+b3fNRuiLt486uWrRq/w5xhLA5WeUm1tP1+/gGJWmDTw/iYodyJypEFBI0B0Jc7ekvwB/juhdz8t6L5pskKj7y6qPundIipv7TS6bFw25Gh75sRSKVTnhcL0xsWG0/xlC/W4DB53EvnHWe9gZXHeEipqGloAEASGQGHKBCrQGCwO7y2N8o5FnMdhmAg7pIfUEshdH7uGnkFzXM2jejso0H3TAOh3XKjDxUZgFPfQXffbb1itSimJ9pfdL+CuHEbJVOheGcaG8U31c/eXRdZ5M857iqz7w8J0ndnSYuKts+3ZThTYzwptboCGhbTnGeH7TUD6dvBgRA7MAiiH1nqkQUHfZIOyHoc9rcVFwUgRg3j4r15FglWWiABlHkXCltGA7hYtKDMZs1P0LKB5RELWR1ClV5P8giRSTlJjdJaNUGmNXwFsF7Mw427L/nAOdbecna5bxdFwmjs15pXw05dsWY27YtuC5QrbyiCNYWIjISYhJSO3SGkJFTUNLQAIAkOgMLvDyoJUgsZgcXj3JXqAIydvhEmSb6NVmQx7nBZhii1Md2GBRi3QU6B0FbfU9dXHsDabLwot9ctug1DsHNPFSPb3swBc9t8dfBrTP8OU5zV0DrDYITHDWMbeznxxOe7n1h0FgXNnq/BbpwulcosdLt24S7l5S2HG3TzDHT9vO0LjXqSVVgqtR2Uebrx9VBWa8ALamRXo+EiBCafkwvSnJ5DmNt4lT8jc20dZk2xXUYFTc/7JS1/sOMkKOD6wGzzDTmrc0sS4J2yzqSuS7cXVZ79NNS+br9vZi5KBtgM09fYwJX3aTHsfVscEGm991GgSNV4PnKhngdqs5rd1rQ7upnlzlLJ1jYd/0jAZYxrXxxomyiSmTNE5s8lZmGvm31G38mb6bQr0BFojugKtN2+hB4f0rUqdxkSmpj/G2hhtMuSSTOMW1TEVuQ6X+lrSN7s6HZy0cUGz/mIRet8K1F+1Ldr7JPpFA2GVgcODhsY9tf70TlfX+lPX+lPX/FGnZTq9o2t9tQZzePEl//hb5MbTYyLwxwj9Hn2Iu0yqt58QolYafvNR6na1X3etk5NenbAyM8SmxxhjnIk0VX+JR4macvPpMZU5TDN/Tw0vZfnwX8vCtJ4749f/kr1qYDoDbYfsLdQHjI+S52oiaQbXptzE3vtxvGSGSZb1j63qP4K9q3h0NQLqW7WA5jvrsBkglqi12gn5y8kcFYrblQHM7Pk5uP573n3zjIWFJnCkKKFYdd1pR2H8SWPStkf5JLVKddkc4tpC+472lapYp4VjR07JtbcLv/HJ6oC1j4blxqQo48QSbwd2tde6huR545P4k3TKOJNqb29qfNksp8rN8UmFOvzcKjdXoPGkhVTrCjQ+W6T4+jL6RrUtimOLtkf8KqDR9brMEjFMohAtC0rdCjR6ihTZYCrHG8jUKr7M86WYRooy3VD7xjQeG8a6UdXQq2r2B1eY+tML7PAl8Dw75EZK4Tc2FvDMBVSXLpOKUOkCBYCWVeHOFNT+da8iZFcavXMIygn92nfpyRQbdE+BMlpEeCzHQaDcROCMokv1z+MC9bcXXM0ZwAdFjunUuxctaZegfu6/gbq+nwmvHGLGlBwWnLJbgpCylRUq66O6pbVLUFathdRKpvLi0vxAskXSK7raK0VXomPDaa5jnh7WYOofvq7M9Q+LrIoqoJK+7L70mI6TWeSgtGrWYkNq118tmPlXwklLOPKaHh3gCA0EhkBhdmeUpagEjcHi8OMNYfqswIV3hksjV+NFWXhFV3/6Slnfb1Jjid1z7dNAmQllLm/mJwJen9atC6PNYk4s988vs4Klh/pIumdBrqQzibR+x1ROXj+NvdT8vdGhz7FLnYHXSVcKXc9CGAKZkZ1+qll/FW+Hlolgp06U4nwfQfhJglzbVUOn0rKxbuoo1VUdXflJVy525SeXNN3UFrTvUNsT6NRU9Bo+/L9/nxZcarHrafYPd9/XcK9Blr4eJrNdQk3AZ1QW4vCIh8q/97H7wi+7+1ycMlq7wVOuDhkjUxp/vcoEt2K3SYBTs8fTicrNOPMsjHOWvOUUnzOjplWVIH9bDnQx+TTGp7FCt1NKPenq8HnHyMxsZxktXVzs9QecAdOd/+rirT2BztmOBbp4Xk+g4qw/e1GpT5L/80+oc90ZuMV2KJZcv2KTg4OQx51UV+gZq3HKl3AlzPjr0dITYCBrxsC0tlo2/Tnwrxa/1/T392v6cQF+uUziWqVc5h7no7jF5BL44itAGh8/TfmMgQXQtQTeHag+o7Rkk4WvrjBqz/gunRpIZwvWEvgy4UWBKexMl54FWrLzVWOAN/mN71PuMwpNJtHnBN+bdNqp/+myMbr29Smv82l97I+khVql+Y87dCP32WO/+3m4Hl7mtl5js6Spa0Y35U2y8fszeuLHc5zHehoP0ShsmBt035/M+q87tO9v175Yb6/X18vqQRWovZ5v8OlH/3Y80zimWrD5/nFuyO7XgMAe925OpLRGEauQA41vstBRb111e+THEd2HuXq+mpdt6SukslbKKq6KrnC50qFqClzySlrcTU+i4bZdzVn4AfnfjURzWJBoDQEbSBATMyFCkdTENHTkYsRRMjKKYmKlliiNjp1dHCcngwIFjIqsFm+ttczWW8+iVJkEO52wTLWT0p1Sa5U67Zw+022dXheUcHEpNSKgDMvTCHbgBmUgMaNKAjvBTXAb3AX3wSp1YOecXACOg5MgAjFIQAoykIMCkABplURIjCRIimRIjhTIC0CmjszoODqJSmSggE1gRnbgBF4QYE2kjXVAD/TBAAzBCIzBBEzBDMzBAlwF10GExViCpViG5ViByczAyTUyTURj18UhVi0bTLbVFFZSR86mOne4gMuMxMRaJY42tkSl42JGPyHVUGRitcWEX2eng4rIIYYEUsggh4JEQq5FQmQAoMAmbIMTq8ZT6MJyXQfpCvSgDwMYwgjGMIEpzGAOC/mPTUgfypE+1z/SgQoVqhTZLlF7Dvs4Dw6vKKNjqpzl1bvCB3zCN/zAH/vHZxyoxtAEPRIJxJBAChnkUJAUrXg7sfELcATq/hYGYWUBbhDgg/x14sgPfPiKWn5yHhUqVKhQoer1kKlHhyd4VtIB4WgQFzW7anbBIi9TFDbHbyFWHTcXfWUB9GEAQxjBGCYwhRnMYUGn2QIddNBxIiJ3YHgNeTxIz/6Uo7Z+YdWssoIfO4PCtgGzDCvN15xwF6YaU43lZ1B4IOT4SRBjNRd3hFTSmckQIgg4TcmTSFLpjEpTSkBS1aEmJT0FmyY+W1l/H2r9hhhXdl5F1IuFxGY9h8GKQtSAij9UqAMDx+H91lGYR0hbWVa4wau4S+4VjJTgFhiDCZiKz8DHP/LTlbAW3QAbeUKiAXLthhzEqnMQSFmiPCUKDpF14QoQVKKWy7cNpdxhux6mUp6RMmPotVGGJHRIXy9f5AjzROrSpSheBz9RhNjFUrWESmqGkXn42agIZRojxyjN5IsXqPrA1qSTs0OKQyqlRZ2Y5SOdrhIr9kiahbQGquBKv6TUkEcrHeEyiDAk5AOkNYCP/UXmQkKsgguSlTj060wTUdkQGQhNVSk6d+JawhfUlxy+DF+Br8LXkB6fAjNZPqhzw6lUE9B05OOEIu9gejoSn9ohBvRtWZSy2x7G92jnMjrbKJXDdszTtntg9el2fL1AH/TDAAzCEAzHJiiXhGTQ5M+D+Dyqwwzxp8EEeXNOLg4Cp2ZpRqV8ulueZwgX1l41heAcGzS2xtHY9RyV3/2UAj+t0E8pqqTPvWEWbuRWGLKCT9TXoFEE/7WGxtIYTRyVICwej9j5QUVkc/8IwsFFTZzGRpGW6igDVC4xdaTfE+v6/gkENg4hjkTD1VINZaEnmJ/ZYbmXzjZIk2vyNclpWxAK8T2lrr/+zqNW3nDkcXey3DT62OvF3nHrzY+lPXLizlEqErE6FFAgKNxQicbBcOOdwaM84NCE1HVJGLMMymaMe3TcyYBRRS3qIS8WhOc3f/jTX2pizWselyMkLmcqEYR+qNUYNBmljIIVnB2NJtciNAUpmaLlZkJSc0kCT7fq0vogiNKEMhWus6ORcjMHCYrqW1kYIZJP9Cd/Dv+v/qBfqKv+rAkxALUz04KOZyMuyTgzitKOF75ywjIPgbYHYDSVKivEchmKM1Oh+2cEUA8Rqwv9I6YhKktSc1kRgs42QE22+LQSC92/OtwJ088tFgADAN+XjZg0uAoYgiyQObc85NJqyep6lwaChKJsFzGLgkvNIXltCikb5KUnCApAjpsToDn4ebrkNHFFGzF4gt+JGlaMhZqVmXR6LJqKcjoJBCazUBUOW61cblxc9rPBDxsmS4z35w9nlCozp5RZLsZLquAbAVOKBAuLQInFZ9a0DS7NSnJNpBZyHYvD5bRleO12BfAaQY5f7+b/qNKK65qilRVIVUNz1f7VorG6CIOpgIyQ/4MCidJircWetnJbzO33n89QflP5gZLDhsPmI8YjvkHPWXdJX+97o3pr/HDhetGBuEyuba4d7o3uTe697lPtru9k1w4sBqzBpZAlNBcWRjZFtsa0sYX4VMKUMCem08oMK6PJ6HOinCZnyYvzU3lrQV2q0+WSsqQuK8q6ijdYrpqrl/K8EIuni3csN0u+7C8Hy/1jtNo/no7vXZusTdd12k9PrT+zobOTG3sbT22m+T1bpGxXpyekPTsFXa2fzpH5DzEjAGRw5+VeUX+uVjaGeN7fDFaHzv0fqLbunKCFYvmsDQ4C1YRjQ3HiyA1jBYnAqVtuPk6FTjO7TV3IWL3e//szsP9YmqjzdK1MjIPWBuUDpel5/YcOxHfcF8YWUTrrVb1UUDUGbgaWAGDP1vu39VHlRM771x+oPSYy2oh/A5RaeR/Qhu73wDrwAVxgGXT3zbccDw64+4bjt4QU8UG1fMChgLxks2K50qwUJ4oQC4JiCNyLFbEet1rJ2WJl6RqOX1FW+5fHz1C9/BJL4yCgHL8kYjcvdgkVi1wiY4zRAU8duhemv6cEADre+Nkw8YHTDy1541kaNxJXGZYYRDR+stxSbb+HUHgMVrToWk/orb3mbh4jr5xotPEmzjbsQ6Ca2y6qcapO1UB7Wntme057QXtNe6S9vX2uz5/94t6yiuZM/Jfo/5dGm9FKidZ50svOZGXkluOOHID17mnhPhPaU9qDT8rq9toP8SbIBCBvA8g/AP8amjyczEkH4J+/Af/8+vdZb/xbYAOAy8fknOSVbMnl9R8//foBr68buWnkIJgSfQhZwC3AKPBs+0Hshb7YXkyhib60sT4e8u8B0bBGth3e9bhEWdbbYLvn2SRLV2yVTK96xWvWSbLRYx72rDS3EFhIFBobxyOe8KjnIt5qb3nHm97zpKdc94zNCr0RSKmvbfWiUR43XfW01/3AbYsi//Fv/7VWswaNWjVp0abdVz73hS9169Cpy2d6DOjVZ0i/lwy64qJLLnO5oNZeu+y23x77HHDQMYcdcVS1EypVOeR9Z5xS45zTXnbWJz70kY/V+UC9Yef96hc/+00Ou1wOTnkIoGCw36APNN/gIShKI/70v686riPuQpcaXnkfwo/YlA4gs787sti+HP9+QwT/g3oCzc7PVq1cMXPG9L7eaVOnTJ40ccL4cWPHjB7V093V2dHe1toyckRzU2NDfV1tTXVVZUV5tKw0Ei4JBYuRIr/PW+hxu5wOuw3mUt7xXiyFg/m7qJSJreHSUDt7R9HDWR7K3Md6mfVrwy5H0Kdi98+k7BprvWEbDYHJ8nRaHGj3peWZKjvAj2HxxfbPef+0Fiq15Ylk+kVEz+p5mGx8R0CZj2LUjZPX2pgZaOOTkDYhoCoToKLmUAJHlpLaJLzv3c2e59jlPKPj2LIMeWNSAG7fRdjalsfVcr4NyniTQMee2b3mBtGYWsBbI7/DTAJJHEedN7BWfHi8TQG8SwRPcu9EtM9iS5io/0zYaacFTiPJ+6b3T2AeJrab7URQlwQ+rSadP63XMkkA+NOG/EpIWavj0+oyF4xruo4wwKc63pZN5VM1CnAjZqL7tILYDxkkCw1CfHngQ1hGwaQUTfEi0DKmaYHAlgXBArN+DQR1Sd4PIAQI9UM4Qafc3MslVwc61DGVKU34U/0qPb8jQIhw1sUy5lih1RJfsXjHy6QTBPDlFxQY1wq5W0EwmYqVnfVGVzS5n6hq+S0GgelFNO3O4bVststEOwdYpbmNgGSG4VDwhp+Zo0jwWvhc8AaDsd44vnsMNWs/SXho2KcFC/RcmZJka1+z01rP5H50as+p4FTdUMkiXDwWY+DI3T5XoT7IYNKo3TjSvyJpdZjiNUxFpaMaYUfPgUUPnr6DCZnMt2Xb3iF5sS2JYQeR6CTeZXu2vc5j/gbgIsBrLrmk1nvtco8I+kTGZuQjeNdbvNbIRMckBnXW+lY+1KYywU7XHcfunyZ97E5uJGHYSmqGaXhGxX782KJrDVt9fU0x3PXYVikcqzkqNBtKj7QUZ1IjHGlQHHsKJglQ51e4rvGzL2rR+JBYnHMJnZfKiPSjsRlvNO+DnehutnPDsopZy2QENg33qOyPnqMbDp3AvR84MIvGZxx7asRweWHeK0veNEzrJJeDJp6sqLZxsppYGnh1CUPFZpSYY1VMEhxJeZMmmyP1KKTQl6H2aHDEMbs6tQUw+UAGYUVUn83ihMEp79IXiig6pDzhx3yWBWGbbSv3I1Z0o2GyPb0TEewVkvSM9WDqzW7VNi7F3qzkkjjj6ElzOhIbsxYkm5KFokLBGxUKrOpYQ+DGaBQVC00unbSFr/5aKBq1fsyG3pM28BuzXZEh2x3GyUOKnYLlRvYAfVUAqTHCWMwTVd3GEFwFT+2uGI475zgOwVtVWRyQTPQEFRhklDIlW58VJ4QRhTbUECw/v5MFFW3C/uDgyg2dZ/hGnf0B+4sMmOpfiXy2VFBFcnai2aEA7F3k+Ceg52oIJZLK42tROO3S/nkZk6j/rMtJAcOtCVATVNLJ95lrnU+itPhhBRsewdqeiZiUUHCWu1rh7OizyTaESQI10kJsa+8WC5mYLCKR7Q91totXLTJKJO+oxSxkgXPGAX/HSLP9pnSxk4+BisNnTuh87dGzCEOUWOdSeMJCNGxPA0msPXZ6TEc+ZZmm93cvI8KBERdYNgWlILakS/qBjcXP2QMxYKJ5rItot/6uV1TyyOcXk/XmTHOFbgKXnPT+AjT5oYeYuNGRPw1IY8ixw+xYcEAQmrPeUaK+5k86nWg4N3JwkO18mgHKioS6Uy/bFfwJ0OCeZUCNTOWjIhstdt3HwSKwIMbzcyLjFl0vKDS/x/GvBBiIhxsWjkyEvsVFhiirWI1Nl01/GLylc08EsCF9mkdBsJDCbrWvmKW8hsymM7m68CR4VZ6kLphqBvbV4djtg2gHyd26kkKUBcljgBf0Zl8guAhMUBILKDraIZKW6EKoha/gtHTie2DX4BRTaw80NcxUX41Zi6X9a4MzGquNUvYtlOQj+FZyKGGxbpXDUCmXgpjchC+kERTJTGi1nFUFj/5WaxGCzzxKMTAcmvR6eJaYugDQw73QRppBKDUTPeWVXZ1UeNgVMgZRaHmATo8tvkmbhXgsktLDlPAt383MZMRJKAouZCadzW3pQKaAIx9DOaiWrb93dFP1KF/VuYw2qYlsPwaZCYDOHMvLhFx/pHDbbba7L0BQTpxxIYFWPaODu8s3+cLoC8OlzEbZ1AXHJTuUft4DuPLr//YGASgilDp3J3ASNrUQ2Dy7ZJG7yOoVO1J/rLjvv5CvCFL0f1sCjG1lT8r83mRFUr8Hzjon1/7asXSfFncPyel9BMtQhFvhEeVtYgotDVxUTTtjR/z/+hzGaqnw/tnEeHf1WrigfEu2JZ3LGEuMLCEwKZnxjzyw9bX3X9gq+eazt085F8p5sZHnZM1z8bZ9JFojmTjOxUZ4CkNs4t5vDA5ynlohM3XOttVizFpSakmZs6sL/HYgzSbScUCBg/zBiiFtd/NHm+FJgvPX0haYM3bbq+GLLRZRfVdyka99OIiuyLqig8fSSn5m8Y9SLba/9j4IYStafhMu4L3bfJ4/uh5qWBj3XuQmbKwfXweiahEZJWbpPt3Ddgmjj8+l6jzgM8uiQ3JRt6oP8TcnoGEGG/59zKfE+NVKrpJEAfHhKmpgF4h6NNSyQ2xoQsRAtKJT3UdTV3Q3lIds6IleF63Rh5DDhfM/UN8AYDCzGEx46V8Hj0J1UtgjTp28EtqFiE6xMAYDGlozEUKk2QjUoyl0Zm94InRtGlW+14nArshQmlM0V+4AukmS1JpeJyk3o1VQZbZzrMlc2Ztmk1JKe4HVYNpPqyohTlNOdVE8jAoCOkwCsXZu18a0SJtdfTmk3A1GXK08t6pjRpojS68vtMGkS9kdJJy9ToltnOC0sMFWD2lcEOvzB5FBqHEBLoM4kWG872U63u95y+OSyL1c+nOWXM5aBB/VaG5JLWoTntVJQecMIh/eXCY6tUSx9r6naP9yLpm3TUVjqjkaFsh2VHqRlKS3rHuX9C6beQdb+nIasSq8cdNI5DqWUjh8UOzoW2G9y56yHDua7IgdkVuKpXJXNJrIuUOELMcaU7MWzWlC/UjdKy5llbp/qCYsmEo/qfIP47iCInACcRJt4xRJDNVHZgx/FO1Tq076nFiAxXpsAcBj5HaTBNKxfLjCrBGzGDmhExMYkidyIQ6Agh22+Gza0Hso8rw50wlMrNptkqL5Whw/zleFxTfJHbIQF9czo/MUL+EE7Ip81jXeA1hCywOhFogwlC/YJuQKBhkIbL/1TV1u4w6GdAEeCDC8FuAVGs4wjijECh2uc4GR3ldBkXN5nJAhIdrniYCLjPLiP4rI+QBs95diOB9H7RGvj6fr4tOztCV0hugHyIZrkHKRww64HNs51oDDCWXnKXIifI1FPYGNYZklTwIPvQRZEWZySeCTfzBn3Enb/DbyFMuaLGoJDGlZdklNqf6inf0ILX82W2/W6MmK87bSp8xAZDW7E1Xdx4VUFyPWNP8CxEiYfj6p44j8vxSyf968Mm3RSpR3S+wi4sdGFqjXUZut/w+saLzNCUGDrBQ8Ib4bF68B0pqkZF/zXoInzk7BLxVs8s4RIZtelvUSLq52UcAKkVSjXEk5zhbZvSNiAUX2aCzsr69LfDEPON5OEobBk4cGz3uFhHlJJc9ovzNHHVjPjhiVjuWOqwmUdB0BLpzWMT0SSaRdnOK05Cr4nnBhQraStmuRw5Zo+624yA6hAEpwTiTkldZm+5dVjk3Ck9BHoEHOipUP5fTzkwFPS4JszM94msBKXsLkAIJCnUpF5GpSbZDnLn8O2cKwQXY7wQ+Fz0fCer1vrVC9KzRCQGN1/bdbjMLePL/pAaNYbccJ4tFQ9B/Pa3xP+a39RglyCJM5tsSEKhLkQOLXObJZ9n3+Ni7pAfI0Ya/HYRNnsu2UEWQPfwcuBqqxg2uAoKQUWmSfYSRZZ172qUuM1Ds/T18qfe5tp03tOvpP3NwA57fhN5OHiLGtmQShi6vZYiAvI+wA3AwfpU91CbhnRvtE7fPK56Yl/C4sU15p9R4WFLigPwbBhnZ8Ys0R9/Ja4LGJmO5HrJ9afG5aSB9frklvQo9IULNIIZ2uadOu4XOX3IB08YVqpxrBQNv4VLXOmub5xycdve4qxPiBeLt6ovehUyzmfANlGCC8wDmcju1JBUcFI1tFezOrN1ab37NWM91oowkY+ukTTJ7UN/4SKgqs8KBnYn8dyZfS0W7EAfOcF/pks5+dr6r5EBu6H2fNuVlM7Nd4pkYzS4IxqOcokIrQ56xn9XdtyEOGTDyk2qQWFLvf2ZfIGLXNzzRIdv74Ue9mmMvK8cP8xm5+BUZOJ72QH3zGxg56Ka9IHz8v3XKeShmohHNcQsillB/WOeFJGF+C6BhbpRhdGwv78WmAW1+MHQwCR6MP0Vt7X9ObE/AAOYNIiv7iPP3JRAELO9P3gyrQF8OzvuRKQ3XqysWXV2YOKSrlrlAlIljoD8nF9YeXUSY/uDK/7YE8o9LdV1gkF9UcWUebfP/yuNb74rSORy4LzWRPbn/DPX2IQfHb2rpeTx3tlF11lgAqoVJbpiHoNu5Qy7SWLQAYa24SHdJ5jPsFT6ArBoQCzC6ssYr04pd6tIbvRD5ZfgcoQ/b7rMUiNFyPUKuBIjDt823a4YcBI3eryqu2KpBQ2TYYIZ8vrDis40EVarkrWi+7TshGgEFtPvmcY5kySn6pOjTJLpPWQVaZMCTJNiI3smwEUl7mm+KnFo8ybT4XonX+gZWibGLV0Y3jZdH+heNywrJDc6FZ+ih/rZ9cFNG67EJa9hndr0yvVFlFlLhIv0FPWbYwvxwL8BXkldT0LnE55sH0Zr6DH/vDd+jzHWEFIyM5WVaZLQIYHqKr1VEQhieZZXKzKOYrV0v1IRMYzxOK62YMMtzftI4d53zr4jVtdRp049qN9j3uagWnjeW5UoBbvIELvncRrGPKI+niVYXhO01i5h2j3MqigoHWv/Z3Iog8YZmTRoYvl5oayQ/T+w8fLIJ335In/kP8OwJY8WSMcNKddjvX84kE2eSiXqJA72YOZBxyqa2kpulyBLohlbSJLTxe8PSUt3aT+1mDhUJGjkxtNLXe8R4VW0G6er1tmSLpzB9vGnRasExeN/NmCw9gWNkeWSWnmyNyMa+vxw3E15a9oD2wnzuhGxSye51UVeYEkSHX7uO0gpPnNBPfW9Ik5f7F5+p1vzan2JgjY6HDKwotp2yTZzjlCM9ydh7T913Rje7uNkj988e7viHDcCBB0BvFeJrdH0OB540LbpwqZWsyfg6BJClwJ3cJ6yjDgRNaumDdk02pBrPbLmsMGJyysxfLLnPDjiZNp7yeLzRovewKuHxyQK5eVyVJeVh85u4TUM+4eu7663ia4BwYZwxTTygvpchx+oM0F7iBayUpa1aw2kW2SVZBzOLVDlnDUcrAGai8becfO7JQ5u4ACVjLFUevkUU0Fs4f0GGt5+TqzbVeKEyrBw4ZozTchSQDExfeCvO78aTtTVpfNhm+dBds3chUru6uTCk5x6dFul6IJxedjPQoDgD36Nq9MADWKtIfFKdd1hi5riIQpkvm26eEs6rQCifUnVxmDWJHVgcXkhVazieHWt2cxMKhD1SNrvtSWWSbdI2sy+t6VVumbbZkqt7UBhFIHygo0r6s2ko5rlP7c/f6mWXLNqEcCpivuyMlJ99WINtMoI3KsVqo5eLryCBaz1CYvhtHnEYRPapb/A6HDkXW2E0B8ZbdFD7kle9ncFlk8n35PCz7eXgQy9p1EU2cuNhA1fDX9mZ4e7EHtVEy60lGAds31+3jAdvu8QQGIDs8XhSV4xpWVlNOyriTNG50bX1DdNjQd8b6jNN5xWsvT3KvfQWJKebozkhNXpVcqqcuo6a9EVHHUSjxjxkfC6XqQKTILpNYK+muekX+gEn8xvreXqqHo42lgTz9nE3N4Cs2nj1WzD3DJxrXIug2p0RlLrfIxwaK5GPonTCrPGIpGvGhb+hZx0y00yT9AAsoRKASaKqFU3zbp0RLPWKVJWqRjykKyMfCQhaVU9KGRvr0oAeD49Ig86yedJpmOsPKxLaMWTxhgvzDqcUTBRAr7TuzQGKzB8ESwB6yySCRa8WMamtG9U9M6bvtwcrCwg1W7ZhAADh9+8BQjVnV4WB35QC+ErvMLtLN6m2y2L8XWDksWBRPwoppwE1Wx0kERhpV+VZtIdzW3waJk7R4TfhBCrZSi3y1pO+5/FNhbKSTyMlpQt+U+9UqiTck1OUlOhLJR6geuJp2lAzwlBJfSKhPWbJ+xo6c7FOM1sqg3QlRbht2zanOoUuMTsDBO4Ao9inywAjLZP8ThO/BrDf7Yn+updOe++E2rZ3c4VdE+ZLwFoAKna+svtzlM0K6Ze/UZXpaPWnCBMKYzJTMMQRi4/pr2miZgPah8cjUGT5EpbLqlPD4j1nfNdnFt31jy9CFocWrq06ev5pftSPOXXiM8reNd5K52LfGjdvsQ/sQdPx3BkWx1Bb7g9tk9ZaU1xFUwXpKdy5/TYRUkJbSJ6DJck7JkwcpGws1WnVjAgHLqRJ6FICwxe8NOAP3mMUc2HF4JWQgQxd8iJ494bw+1A3irBnKU+MnfOEGymbV8QdDBWZWZJBP8UkEtzAEwjOsrUotmn/DwZMp5nLJI9jT77qou6SeRqeXb85QOrzxFrZcpmOx6K96ipNtdr1cZCtmkwS1vFOspmJYSmQSK+0LO+COMU2kpQj6tlln8Za3HvRL9+npdT40iRk9hPKFP43fxZjaSj9tu+2zUYXqX9V/xJr+MP9qs/5asN0plX/k/+qahaqxvRKujhWstr1i4E+ijnyqrk+hZp4KH34zEJZPP4tadJaE3zSh3Ht3/J4Nn7N5sJ/dcjrbN748uwhD2rsAYI7Lgyca+FMVuc0l0EUieBX/O3DSvD70nhTEDPz+byJadJASrKkKKD2emjjqn5uPC7XiUp9SeR94e5mpOcWgM05pmJcEt7+g3e2n0Cn9d2mSALZkos+6pqfHumEiUhIa7aXaAS5UbLZEaqeNaBxROy4ChEG7lUe1TyjOiA4+DE1xQ/09PVD/FHdouWOFLqSQVUKdkKwqJNetyIjfMbjDGXxodVr/Ccr2I4PrnO6HEqeERKtHna41+4qnBRzrerod66YXhcsnIjQA5I0uL7AaoxpFPdQBKeortVaZlyYFFFC/83EDZ7ihyoAMIsoqJTKoQnLganE1fDkMuVUDIEYvD4bFen91KAV2pa+epVLrLcCu5xnv7wWrWx68s9TbsaiaMdfeNKD4+AWkHydG3u08XHSwvfclMAUDThEeIwcD3/VgdN1bJAt3d4Tp+Y6B+XhZ70f9b0dgsOrbg1SFjfQF7Ot6eiynysijhrDTWKpR1kHdkLK+NN9oKMsH/Fy6rkyTsR8ZzCoISoUlJqMwDL9HBV/C77+k3Z1HVVLn3aUp3kXhBt4ehs0X3xMs+F8L059kzYZVrSF3UrF4dTiYu08gHH8VjEGROGR222qA1LWKihWCDbbwTvqlgYpFxphXXTeH5UZXoc1A+/+CH+Y9DUlELpVJizQKQG+7tuHJ0YDICLi1Gtr51QPzAFyxgutSmrXRZgWJ1fZrO9L+qzMNHxpA9lkpEbvIZLKc7rzWfcjqOmlfW0E0OqJA3FeXUV1/byDSWeHGYAoxXRWhjNzo9oGw06tFx5rRIW8l4jWjUHpUoTd4YCAaKS34tGj5VJL8Vf1LsG4PPvYAvvK3ojt1dUeYscaD4kcyn22uHPCihkNR+c+GlOJwvncvYUxy/uBeysgVYIuai2XrOjtl65uRIqQJAeGXWtdUXGTPt+jIjUAnSG6YEvlqqw4jYCeQcPw/nAzGeedX8vi9G9HHdvW2H6JkOpzngn9dTbbUyZN1BTaL2QpcdeZ9r3SGJQBYoZBFDGp2GPQOHd9GObbYbYMt8vh8UK1RA+LQb7lFXppGX8HVeLSAxz8RHbTyeXaVQRdpVtgMCH1z9Nk2WG52ugBjgslldQagId9sv5Nxd+KjOrgOefZ+O+1g/D0wKc9IjI/3y5mGqEJVAYL5DY06p85Pn+55ts0sL7A5XVAS5HTZpNUhODFnMk2ItTeaNaOLCoUznR3BYP9Ih6q5ERo73GdJvvCGbyhTKCutoLaxQQvbxWYa3SJJkNAtZpr46RCwEiNJHfxprnPnnHkCrHeEUl2qtXmQ3xOqzYjB113e2Ggdn8FHqhN+Lzg2bcOjWem1O7wWPV7/7HbcC+yIBdSoUlXQbUGbocwBnu7mVDc13P3dLw8Efkjb8Mp0NJg1o+9/ztlnvCkdvtirOi9FA1UopCGVkucwK+QqvlcsEbr8QrXcyeJbRWI+YFcdW73tTmowdxZoa872xMxcVD1NUMdNzoz/Qf18ls5Nzl2+UWSnV13GDJ7d/dcBTmq+s8CxRW+837Wzrvzo+AmVJ/Y2j7L18htLjYsbIVqnGyqQpEJozm6x2CuRVDldkkqvRCweeRkNQlLOjjyJpLJIXOKbNtsetPfIikvEoyC9TdhTVNwltiV9ybjVTB5HDH3HVImUPqUo34HGBDEJnES0G51wL/Fr99cJWZvewG8SZmIpVwgpxN8o1MvEFMLppJnd1NNEHFdSswn4zd6pFGTiiFdwq8/bc+y43geUK0QcT1K9CfjV0akQZuKIp3HznuCeT+/0F9kJ7w4un9rsNrk9M5fFximmOw56rCDu2OGS89kqLpzFH0v5jchJmng5a/2xjHW+MlfwNunPbs6TL4wRsRTRmtUIIjk60rPJjyqzFeAUZoNKB1r3O3IHVY6gkO0juBzyp3quBc8pHSGhyRQWS4t0jK4tPsayeTXRA/lVkh3+WXx3lX+mcDkchEUrPgor3ImBmdLdCe8q3PEwQnBXIC8J4M2Ub2aHm8/l/D0nxMzeNG1TQQW6Ik3/6rumZOJshDuZ/lwwkl/IHyl4zpjERYT3W8qojYfTDm3qob6tIPE4HDOJ0Uif/JDUnk2H0g5vHEWpQvCtsKcVjbcndRIoapoF3BkAktGevwY8CZPlsIgZMpsth41yBfTg8FYYpDCDbWAzwqTVRfNhbD4vxcnZ4DQw4eRysBqF/aZL+BElk8ElbL0oSBB91/UtS/Mbl9D7Q+KfXkD0hibVOJTxGK5ZKJVms1DHe11H2PlPeS0P3vxZ0fCaXhsYp2JbBN/lzTaNu9LxovMGm8Hv3rctgam+qVK9WPZPHVIrwmp8JyMhlRoEAawD24bN7k2KrgyVrGtrK1m/MhgtXxEk4b3XrQhF9YWjIWBsoaHwzu/TCp8OaXFTYbCpJBxsLJRbJHELtxtJgltCi+A7AdX30I8BZZInPHVYpYrq9KoystWP1jK9ThX1ColBAQfmZnE5sNgkFkE+h2+3xNk+7tUIeXheTjROiogEXokUO5iUlTSIlap2bpA/jIjuiMLHH0iyC8A+FNBnyBZPGhZAB/oXC03frtqG5Ro6KYmcKs/ZUiRWzhCoPHKbLY9bzKL/vGXxA4sg32S3GHeaog7u0ewkwl0SewNJ+23I4a9AEBtyIC+7QrhZYBJsFgoOS44pwoMWidtHPDIZcNookbo8Dp/30xtC7WRy+wnyO4OfOtaZX+DEff4QxixMnynDW+eyYsMdY3xjY4015mLFhh5460xZekIn40I/MnPWFMOc3K2TR4secKT+C4/ZXDaFV5p2JvQX9rtYtsuDNvQg1nypI5i04RGrT/J5EYUgrsRqWkSspWknkKChoGIi3m90X/0K/OoqqobLxF0DryX4V7qHdjSEer8sj1O558rWbFhmiAkpIW+1+2T68cyszON0xs7bpXfSe5+jhMyutp18Sn1Y3vjaJ1uQkjMJG5LC2LtCMpdO2vDM5O0m4ZsBYIQwTzgCuNaekOmBvpgPXkuQjeJDWcvr6j42Nb1LtXI41nq3WR/31vLC9vETs3hWHWy2Llx36+6HUi/tgJvCyVXz2zuq55UkN8ENN3xm7z9f8sB5tXOzVviLzbd3/q5VBPgzeOClk/ixJRMC1nU9PZLTD5wSXYRhBSvMHDBkMfsqy7HuYQAmR5qDAYYluyvLvdYwHDLz3WZbGVeScrx2McnNT8khRmDb+gyAZ1KX16vsnnpD5pOZDlG+2WVzsNPNa7+HvyXEYjXC8HXMSIrLb+pOrE7svty5/tKWU2zqqZuiC2ACV2wjz+TQ96y9HwFSwjK+U5Gf72sUObztBbKL0xCVwenRi5i/7pxVaksNy7h2lckQaZbS4hftW1y0eB+wrxI9Zg1ay/foMNt10Yc9KzaDZFZ8U2ho38mrJ1/PHA9C1PKkGRj2uoxnN0k5Z/hE79IZdZA3v/5eXfVSDgudM/vqzYDaz8YE41R3/BxdYVkRLJeCVUxHvTR/QC+7V/ROE9XDeiv3DhAXfOFlKa0VZsWS/Q0pzUz1RK2Ilf7XwJ0gUGI2oxfTl8gTSVXUvNvpZO7b3bNzrmRlLEjDLVt+MNlSmdi4sGvCmMWcKc8TPPmrXG3ihSVR3cT6AgCut+R3ud2W02GBZRbFE9kqqPF9Yf59SPbAKowdfz8blz1O8IIBS6xSBvRCoBOQ1CrZh18Y54VnJscM7UFmy7vBeTlfWIumEcL5i9Ysnp1wxOiO74t0ECPqPD6c8+38rAXs4mn6Em3stHVVYyRpefPnsslUQaMuNj+VYsnWzXPncp+C+WP6zCB3UYCnGF+txbJUvi1rlW50HFJpBMEas7rd+XtHttwmco7prWN9I9Quc1UWI4Ziwb2Ktc+4EgGXfHbW3dkWSpMUrHD7YzAHrQ7qkNuX6uvuAgwsb6jQQJDl9Nsj2Cjh7/2TxFpiv+VIKwBkyNAun3LR/fuKtf7iH3UXgrUDfzpDDwEn8E/Iub0tZV/JSVtg3KzwxSeD7UWFsryphZgORP9Z2+LyVcGp9HvNXt5i8qB+m22eDVVhFUEiT1/cB1QaWVORudIUPwfCMxZIt8FKs/wQu+vl5CN9ZjxLv4TawE4zv2DLxhs/23EIjiNVzpmLRkY51pJTSdqvO7SjoftNScr7flvOtu/zetrGN3Y3tE2w7EcGkUVfLuaOCht90NvF782jyKlz79GgTUO+C38HNGmpy1kt+X4pWMrEIvyAikNYJ6wuqLq0zNqu225LoRqnXMCTWWQ8QfCvuwzQjpO3JMeFS5R19dGC6mz1OHmadmrjEC43joij0mWjiw68NJgsBqlAaVlb6SR10rhsTxaawCeTRt9Yn3BdpnHIRMwfXqLwuVyZR2fJ/577x825dHZlXhypiS1SJNsN9s+VG/8rfqORak6dRk89RaEPyA8m9aFv6uhKyKDOd8Kg6kzOF3dG0NkIBUMtYhP/2KFOuUg5ZAUcbkAlDFsAFAKKvIEfpV+fYikAgyrfDX0qNdIxjKkM+mIGhr6Es8YOLDX9D1SPSlOmHbfCDoOYeYGL/sFFEYAKkwlUCCiuH9CaE0yxwwD9xCASEglEBp0/VOXTdrJbcpNzW9isicOiExlGiUoQNlszEaAyj8N72UqrSalxg+k2Dve8TsY0elyZEHdhTmCAnQkKlskelZjsZPx+lpRgR02BdRtOJoy9FjUD6p6TgJkugXEucyfIHE+uu0t89s2lS8+Lg4upmMon4JOnQ10cXO/z5M+wCbfWXUP0v5qQlJK2+3H89d/pyJf+/5v96nfV2glgDki41EQYg8fi30Findu4LrIDuAyP1fea1uQj27OZduq101Xum/e61gUnu+c5J2mPBQUFVM9fNNpfHirN6G9rjJmKiFnEx7nB7xeGMEBYlYVlFPTolB31qKz0pKxVKcv+UhxTYY9h8y8ZrhcYXilP9Zm1d4FedMrYwv70qGDpyIm3tA3c7YRVeCx+FYE4C3/MsxLia8JNa+KQ7UtY9Z6+KxJhYQc6HIimHuWZ41+5cx+DOeDTlZsbxssuwgnbw5j8LxNm45Pxs5lq7Dp37LpIkkC4vkSUd0li/nN6UurGrasrLhA8gO9aPAzQLIRTCfG1qFa7y5i+K3IaJ+aNrtkl54knfLXys9TsjMD4R86Dw3cgJ9Aa7n3IgWv37HrkAw8M7Zx3z7OveLSHs+DTW77xDQOcvOR8+Isw7FyUfqAKJR86AK10YKH0SCTWbCgZbAF1RAJwvLYinbY6S49g8lZSnaOnX5HYu/plkGu55Hf1MVBAwcWGkinbFrY5dUL73s37kN5Or6e30m+5Bp3dfKW30+vc8uMMlt/s4PS285vOG3ZrPtRf+DYFz71leju9nt7iGmHZzU96O73OLcKzmzeuo4h3JmUh98/zd81pVH93J/80MO45e6cms5D7h7to4p3XZSH3D3dZ2v77KmLineEs5P5J7/I38fHO/Czk/uEuCe07/3R3GawqiVEfmRshzSrv8t08VuEj/aprf9m+tut2k1tT7VIvPgBJBU5v+M3b0O/qfwBHPWDz+L7coZhnFXgERgcWn0e/XBAXcqjclh4Pc5fFx8bPaekX6jzLaO+K5oiLGWWh8ctv3r0gHlUGLY+XYgTd1kZe+r3yMdGJl/TqJtbiFTGG6scAvRyi9MobDuDXtvL7sZi5tstbB0TnDh5wz2xBzF9xUVPeQA7RorxnyeWXOI6zd7cDOdGJ0ituCvJA/oOeD4aBclt1GLb1ZZNgV1ZMI9hxSxm0Yba5NhT2v+W0sAW7rzcOCUvUrclWNMSDDSEpD3lTNc3K3CHakPX/zHBRiIDVcEgX9Vt7Jol0MCjbZaq7VTvKIIyA9PDMLX3vIar1cWPvEZpob1++h6nZ/va+Le2zrcLmBZj+EL/7+8c2JAm+4KTvxaG7HJqVrKwubBcS9vKFhC8ktXI69Za7HaFMh1CWk9Zh+rbbhJDH3qTNaTwdAyzopbi9nMNqpGz0MTXUwlKF5kpY0eMRS+WO0Lxwc70Fvpj1pMOuIOUPA40WaVJ+snsSk7ZErKpvK/svr7n6yu5SU2qz5UwPodjimBoz47GWuTKMwbKYWqwu8RjBeisOOPsWDX78cbdDHX3j5vHxjz902aCqdHQsg2LkjoPQeJX5qxS/o81ZdH3dtmPilf6HH0alcWABw5m3vvRfMY5q7kYvBFEZJDCpdDddMkvsFupqzyl1YRlRv8oXgZQwgYmYBgwjvW6Rj4bOpBijGD0LOkzTUC2ryhtKa6lFI5yFmIhJTHnJM9YxYzfCq3Dkxu2d7o+9Wsg6JzenpDtSKM8LF7HkUM9ZjRpIeTaOAi2uApcddhOoGB354rhUM/BgO+6ILWbKxcHgqGRM1AmGw1Yw3eJwYkFsjOili20/g0OXkgqFB5CqicQ1jFULl7NUu3Tk+hoxVX2XrAzY5VqufZwqRstr0mQSGYoxPswhROdsJOmOMjNSxgVGSsXWdo7Gpg6OF4EIWgilk7lu0FJ2Z8hsa839rfJrTBfDyyFfDWkYq6VaWoA5JDV+w9PpMKD8G6E9xioR91sB15Rc3sx55ujqyG6F/7E455VXTL4wTes0941j08J6nWO6+lDihP7u4qau7WEMq8XMJvBJU2ZgmzxZlbuG9MzQ7YWuUJJZSWW+r5NFSd3cCJRiy97Vs1NKc65lv01Hb0GlQ8FZ7lEjgGHvIAWQR7PcJpJWZWTO0uSbAY2Leh7HEEYzYlW96ao4OO0Pq6glS1ZTeol+FPk8XkReB02P4ul1xVhVIsO26QHnLz6Zwg0nlKQKo/eo997JFcn5AWgF6Oc4yboGt+AfgADu9KQaLdZMmCKOvTLjoe8gh8z97rnjcDmfPqKxvRUmpbSA0tgMYd0QuDTwmzlASHpdaCj2QvPKamB9kikuBCIO9oBV6KyufpDPHCslCOxlAhMlDLbl6ZLOsZxLFPQCodfGsFKWOTRxUc/sPs1jBkOQg8m8gMNRA9s0SX/KRLucWvTc0KP+bwPXhvrP3zGt9Hc/gZ4beN5f9Nwbz9ZLUmq7y8JP25XRjoUEFkbGKEQIcPSorsWxw1Cfn4Mn68UihRO4y0nZHVZcy2GzVMRQN9a6od0m92dgIPdPH9DWeNJBuX+hlBCajBL6X5lN1dg73nT1UMN1TaZnpxHODk9Zndipi8vFYUAtybL1bd0oLfRSoR8vcutFTrO8aabO7Zix3cVB1ur5ubUsa96z+fxezdiHly+co8vNubem8Fp19GWFMhry+lH3ERC9ooPWu+i8Wumff54OVfUCfzhEWsPXrrD267bChAfJJIwv8g0T+lsjsf1O0W2oaOaDBcKDHxG3NiLJ2i+O5mkArftZ1rbAqDegetr8WuKyp8EXgrSuRCG+7FcJw4RIVJZHE7it+8craIm1zqHTJfsIdL2RvPwGTjOALVHq9x6msUOH9vmyBONTCXnL0aSY2OtZ6XdaCoySUXW6tAbcn3jTLLXqi06hayhHV1KUk5NMgAAREsTfsW6F67vng2HCsWbJF/ubs/0e7s/oEEmK+1dRmxPVIMZK8GrU+ChijX8XBv3H8Rey/V0+HNFRV+WkXa8rIuunygpxSUIrJAu9tudKiFRzX4rM6D0/1+lxd2NuKtO23NuuMhsgNdOsi1SrVLbfNzuOR9cdmyY7rlaczWUj3sEhSf2X2LrcM+T198Vsikd0yBrqvCMjhCSaoiSLl0WOxfLfJJP0fpe1KOF0KToxiMlzcSPWQjQiSbzjDQzh/qxCDtKJnhQsROMmIKCLIidb743SyN1fDbCZpjKhS3mXS3qrq6Lvz8xaw3FRuBuLBR6E2T8IlAOFUBnWWlbwRfrSoFLJTO/LYjX/kxpFwqKI60KfBtAV0AXoVVZPy30fhBUSB82OlvyL3BhJKQlZeeCpCcmGt5rrNx5PpYQyBMJ3lTZIrHRim0cLuIjg9kNT0TcobUy+NQr5bg75vCy3YKKUpDnvygZoIU7AiQR4RYLuNHSp6zpZt2rIFt4SDiRxzhJxRwobYCg4w8aITsZjva5zP6dqUSzAPMuSYMCjJOETKn7Vaep7DG4hPKr8FZ5K35MvbR5MMSeihVfqbHZxcaaoep37qJBGb8bj8WnitDiPlD8uR7V3mtV2DJEJVuIa2EkdaTG7YVno5V2f1aG5luv39OpFe2uumgdvlv7yizVZRN8F9MJQb14YmPvFWy7rNe/w6mGhrUP9gp9NyruThZLuinXpi2nQ9Yn9UmX7paXD/V5rxQxeOshAGF/4jB6NlXvMi6xW+N6FhMRHzssFrWwWDR7jpqzqa9hIbvtT864wSjEM0zI+9SMhzuMQ0ILloaJ7WRe67O2kpZ3PfsEsadtO+11CivPNBUaxow54TWfP7m92pbtcg7S+ucvzxs19vBR27hHYykjZwmu9NClHMBiO9rQDhb3kt3hSN8VGdF2KxTtNuEwyu7Acu6HRxZLUWBgf445OTJywltDdUnp21K5uJcbKVrCNsJml6t2zKdtExnHWt7+hYl1+KWnr/DyKmMHLa1JetqVTw24OZ5MsHvEp8xrSS/AjYjfWnW465wtW75K50zYcHHWaxVcx+0sN88NkexV1vAMdbwMttiu3xeGk0wFmA2bbXEcDwEXOl3SKBv9u0BVit8InNpKpXMaH5IsMJcsRSG7cCxOj4iUvOMtyzeia3TL+QSLXSylxVij4MUl6gahHHyKW0gm3zSbRm9M4d/214ZHGAhozJCUPYYoDirFEUovk12v1B05ELtoRfa1EWRECkhMqHV174oBmpjKDySC4Z7y1LpRF58d0s1lv7pL2mxcbeLypvWWePA9LdtP6nw4WZk+VFyYlIRSlThA107utUM5y1wxjuBnksomIhdQ3NSQE8+K/fJEvs8h55I4AQzOmRovLWZ+EpuRiyglutTIYuL3Rug0O2cdUCh1H6R1A4rdK/R9/Fa+iiHFeSLutRmdLpqbQmF8zMX8iwPwJW60Xx5fHYnUMnxRifs3w4E47ehBZK1rTVmUg5k8QzK8CMA+5VbatDjrgTzIxv0bL+WALK6CEHwBLDJ2WRJJEwfIea+c+1480a/056BS0CegcCPTjaRx7cxrFTfc0i9FLgC4Ne041Rt2JZ8e9H5lkgN4mFI1kDcE50aVhPct9hGWmfMXAwMDxIHhIJBRwvukSWWbXQZUsR9eyUyfGU+7ZS+ANU3pAn7DB1lDv6idCACDUT156Cwf2lu2vFjBX5tplk9JIDXHxkwrxvf6wtXTZ0sKNozvDzljP8sLuN9+xD1z95KPfjoCJ78CceHgPcUZaxCGcSjoXLLPLkOTVHGGtuH5lxDRb2wmTK0TAGlsit5V1p1p0Yh8xsGf29SW5k/VQFOxET9arSOfaQbfJPPVfBHuAgr2H38eCMGHQfyG6RiSNrBu1tvmRZD19GPJDwRPiJG51GQP7iD1kP99qBXrAp/WoG6nvu4WEld/7/NAxmhw5bBn1bddBZI/YBevnv0FrxGstKS9PKN2WuDW33pEmapDndV0U1oAbF1Q5nMZMALF0w/7/4AICQGCAX7ESAPAbAEj4Cte0AxwlMVBBuarcXq7VO3m1dbXHalPmvz23/qD6rgM2VjUOnVcnd5FmbnNcc9t9df9/0Na8J/KMPGfzTvuzxLrEKf4tP4SWuTriw6QVgYu3kfwz7E1eia+nXqP+LHmFvW9dAgX9Mv1XKfeFHOePcKdiP3Y6dW96PO3f/RWfhzbM4KPpTelj0rdnDzOb2I+zg5y/fnhgeG/j18b1xioewvMSQrg7Y3fOCUfiq5mTM5flMdksmkM344P4CvyrI/UtU/pUTh39c/aLY33l3y2fChu7ak99jXCs7VKLtEgbzUGJCHE08UviLeLfucm5qtyy3Jbcn/JEeX15t0nNpK2kF2QUOZ1MIYvIerKD/JzSRPmD8ozygYqlEqh8qoYKUxFqNXUsdSZ1OXUjdT/1FE1NA2nX6D76JvoB+mn6JfrP9If014w4RiaDzpAyTAw3YzLjDRNgeplR5gjmWOZM5nLmRuZ+5inmReZPzAfMVywMawJrDutPNp0tZZvYbnaE3cjeyr7DSeDYONs4Z7h4bj23j/uW5+bN4e3mPeZ7+C38AUGSgCtYI3gu+PQ+oZAktAu7hAeEp0UoESwKi0aKpovWiH4T48Q0ca24RTxKPFk8S7xYfEj8k4Qt6ZCckLyTmqR+aa10ovRr6T2ZXDZRVumn/3uPB9CnogAPjUCDEvUS547yse8g7yL0u1nooZ9o9V8FdaLWhmPy78jaLloaIblu+VElz09yDK3TcEcAYbFLPN7ZnskFGxvXEDDGtS7b/vzbqPLiKZWFNLC5pI5UII0FKF19V32owHqo1xnFnyfHjFO3odCMsiwKi8g4zZKbWvbY3EAs8FvE7hmuaVR4Un1tAHgr3iim5WOCxFOo86SyWQYDZfibIlyh4nZ7Vem6NfS2KKRMAoIX3mssjimqrVEKhZ6Y6l3p92jR9UfGdAfbtz4DxBj5QLbjfp8p9CLViqYtAHpuyy5dxb1UL36CVVelRaFA0kW8yCrmijmLTu5K71uk8GYZcYr1/Uv+ABgYOYPIuD+u6paWDAGGHl3pvdgUL5fsvpdNGd7uFcATnt082SHx6bgndxERrajb7PX7kyCM+b183p+HaayjDkY6KnIwqGFi5NBa7gq+l0d35dTja+dpWa5BHg1nW+20HXk1F83leHY45DZ3jAtPiYfzsZJWHTApSLRCW7Q3I4cS8mskl9lmdyWbhseaGNBBV/REzc4qXC01fZcB3vg1uvjRI6ybyRgsMZVt5eaqmnVflCj8BKusJas3ODnsMRpWpTweoiP5Kdmbp8KqmPngCK8D3EcFQrlU0SrQVVVKs7897rpy+dA7Cu/jwoSeOXhcR8nFmYNHGicXcJu5MA27CK1ZjFzRqHIyHKBLzYotlJvuWxxI5TtsadwHfo5UcIBnLxwRaEQBBthXlL257zvlMCzmqAEGKWRtaCjiUJWLhLIYs4IN0pvu97JwAuyoHiGUCUnCcJHntaCzC6F4tArMO1jCHEf3a1MChg3auRdgngOEhAhBhM4CQaH+/8v6ob6/Nrf5wWzMAK6vhNwbz4GNq+a9uVhTt/rxokH5KhqnIu5Bye9Cxn7sy6q4VFxWda48ReGhEmjbEwiNe09ZiXa9zCg4CRKs6JTiM2u9XW+hG7/FyE8H5c8P3e6RWEDG6X4PCaerbhgF93FPVaMKIZgFMqu1YlCKWtsJUKl/AJ0kT9TjXFlAhS4VraFcRGJuT2O9No+3krvZiFrUnrw3w8kqN5YqkxeqM3nCVGmjIl15GCOfsFJQaBJ2oCg1NgoSvlKvFxrbaNbi4jqOp/ks8wKJ5yFXamZr4BkSrLmXAliQg7kEYnGP7pSxjeKvch/xNmglpHIGVhwlVw5PmQKhtKP7oqj5kRcZ1Y7EVDXtg4TQ7fpCeROMUzzwu3EWDtvN7sh2oFNGlMbALCrIk8WqDpUhdCO4wGACMh7YxZ3bBMBFRHqNuT0lBL79yHsD9MhMkkOHBuBOFqfilSFsI+gHbwT3RFTCWsfzztkU2CJQXrPakY6g+woYGiBloj5QWmU00x5AH83iTYqoybSfBBmuzaQqgCqJDWMnvawLK4exAPSVBYhwn/pKsb18lGf9PG+fZchaQ2MlZEApZY9+v+kCgE4ED7yRA9cTspSvtJ5fJQe41OwrFKsTqB4zPfLBJmw6fD8e5DwIRr3A0nPAx6nN1od8JMGqYti/AjRrNaGAotdi9q76C4tCKb8TwEkHlf/ng+exo6RB0WL7HoYetwnlZcDvV1Zcjup3YS1z1v6BBlo/3Tm9/kPXo+/elNk2AFS1fumonhV5VGFCdpgyd/Lo4J0a8F8M86BfpVtsO8alvihA8o8sZF0a3CKLkF3uANZVz+L2CnM4pGC+Qy6pDBPQ2Hm/aEwtXWRts0AR/HL1l1TKKiaOZ8T3RVLyEg+Uihu67GRsXqb2KgdWI0opqKoSYlsShkVy92HuDh2u9H3ONM0LyWqLPz4fHJpbyNqdzijrhCWOqdwK/bY4W0w8LV2tAghTikpCmJyaQihb4n6k66HIdSL+8pfVjKwDeZ141r8uEddDPgUNS1aDa0KX+odFLyWQYYHZDEF4dhFSQb40Yy/Nh3uSzSc1SwMB6r5eUk5XszEg0I/GTMzh+agpzTntb6UhXLPNodJVrlRotir5omK+RgaVqA4QKuSGFRRH0VIHrR/eGDOXz3fVqdpufSXUwf+LpWCbof26tdv/rRUmOOD0G9nEdAcDzaKUh2i5FGAUvyNtmtW7D2tnCx0kbU4zwywNA9tOku5lHgRbbFTmIGsyxU+Xu6riFhFEuAO6YdvH5SjvTCWlPAdHsh3NyQpuIKhcN1E6j7+ArJ+DE7TslL/f1WGtyBoqqipDGryEMIHuudQM1YgigC6OTsPR4QE2auEQTzGzhBR2R5OE5Y2B5UUYQIX2ACXXJ1o8anAA/jIDcTHuylo0vlvwaVwAayNU83Zm8KJ87kxM3ZQZhx2WGkTgm+JPD99SQTim8ehnzl5vYEBXncZs757btIitspjSy87PHVLEL7HdURqcfkG4XB8vivxhcngDeyXQXv8zLuYxedUKBKSQuuVJ3bB4VrolfqX8EQ702iMJkGHkvVKNoowYZuPbvQfFOWyWhQLxEYJhiI8a79OweWrgfYupT8M4Yqove++TFNujzvZAAMJhCn8oZP+4wFkkfw56yFDw/zfP/vk/N8KDT7detlUDbT8FnafnQuXzzeYsLtMJMpB9K5SHKj9qz0N3yd24Da0118uNVsRgFSgwouDf3/Z+1L3LWOy5DjD0lePicWbZJPvALBAZ8gMePCnwW0Nwy+atUBk+GMwEd56cq4lfHCn2lthKfavCdh1rteNS+a7LRjME+OZWxvbJEhxOxmH5tk3bfnmd5ikmfAPMvk2xpbzPl7UNyaQ71913YB9//9bzHwab8Thb5fzkXUBBk9LgfKIVsXE4dob89IcfDDSt3bnNoee5l+sLlc5L+LYCPKyu4TxoUbGxtndc/kuJBEgTaQuGgbR/3RCzLwYP7I3/+tkz5Z6jTn8OVp+qHbM2Pqieojb9hrpSDwoo3/LkJxj4MTfw94XMi1qnCrqIEUahst0XR1sLKVk+O37i2g+Xv8Q1GYLLoPWuEQxeAfvHN89+nHg/t7TYrIND9yLRqIRBTJML2Gu1XKPZHIotLZew0lGB3cAvgsU5+oNNxFMDvg4peBEH6Ubi0vgJjBomtdnvuvklfFPtbKlRnhcHG+SS73IG34JQtrxAGw7FyiyZ82XpMiK60DKczRTARdsWAh+MNQDC3lgYleQ2mJaCy9rXtJt9cqxOty61bm7meBigMwPQDR2VrIXINfbQCmY3BLtF13GuZUKQ41pAhNM4g9Qj/RFKP2mqrjD17qjfHEgZWFLpOKHpx+daVpheRWAq4uv4HXoTWJg4XdnLdefvxhkqEoDw6j0MQwwd5rfdGrorTgPb08pPiftArD9k1z+BFG8kbQGCUWiHWqtTJ4eX8k49VPQHhmuLIdDP0eDhkVFwuRBovxmwuzdoCssd+fXVbMKQKlK3GAYRvkrm3nLRpkMrctspwg5Wl+eH9eccZUW+0ab2AjpSRMr+mk5T944r/blXEbsKUF2eHTLPaSti1wcK/p6vyhGHrGpTgqOrFm7o3Ohtsks6bcbVapp+pradnoFXIsCF/L0nlhODCImbbPVoNpJGM1dN9G+vA9qk2NFPZl+s8nnE0lTIEsc0LDXUjpdPw3F2lSSTyqU/CEnWx1WgYOSoZLGgjbHJy6L4+/ZM2ssMsB5cvFrfIM+Kb9AeZgrcnXCx+HfY/xpz6VNNMMWQCx3d0nj0u0EMjgHtqOjOJtvCx656NKh0bAYDmifFI/3Ebl+172cE+BDkefV5sQ3dyWv50blq5QRvj36BONyZUVCLtRrXJJz0hbzIlVezBolL9ebtUs/CW/JjIGsyqYyrQ9Uw7GULmUfnXUh1rn1g46fmHvbzCGyIN3jsZ+AlZIu1RERtV3oFGDbGtzY9rk6NcnDDoD2m6m0jFOo88C6vAF8iUSQOJA6Rwe7Pq7jC7GDpkzEyV9heMwRCwkNz6H4tPMwPtI4m31DEXi/1j63XBBGuxadf5TTxDA8U06CXfF+zQc7velDeVCh+kWhwyZK250F9G3NUJruhF3653OnDR+bOf/v7Ozt7s/Iou3jWnfEe9hNUNziWejLzquJ0JOLqFfdu3nxqNyMJq/EV8nhjPnqzAcC30X/Xrt0Yf9bWVj/DFeywm2nGXB0EZVnq9DQnTFCoaWJpPW3cTDQcAFFWhmAclwgGHyr0/aG3YPkVvPOpfBP82kC/W5GmCGxKiHIwsxzNYjj961LrZzhoXzVo80/mZ+7F/riEEPgEvPaqJdbquH0WV2zzMQtbi+UJpVC60PkJw6gBYgSW/lyuKNO+J1Wq4D+87RCuPaTQz+t816/b9+Z50dnSWFwKO7MRdUJ/KHmsylZEKwD9dqsf+3DQsnGxfS1axEirXlx4RBhha/+3a61gwwcyEZV331fq9Sa3CTwmxlv3u6y8e9B+by8IM7uNtd/opuU78UCW3k2dh6+b3+bWeCEnNpYqStN4of2+YSxJ72mggXbQ++lyH1iedecu+Aj6F/0cFBjkH0pEwX4QWXnT7nu9e8Twctz7edblawV/TP6yIXAxeDtFrQnHRsV6f2z3m4ByVHCHc6LRr1Y9AsOipRVb2sGEhdgnamPWZGHnf2z52NDpoyRn+MBQN3o/tW4M6ol1wtR1taGzNosdczCcyyl7VDEzbXvwBbw8PCzkRl+wG6h0plboMY1b1rF3dDcEZXcrvAaqokLTiTtsrIEUk6OLa9kiQS7Vp8hUOypmQRE7tveT4wxrkDTtLb117KiQYbMhsAK7nyJ/J3JFNRSF+wLIsZw9wWRr9E3wSiW5mzjr/SRJ203xQBG/TFq8MELrJ/TGg4/Wr/XX0OJ3vJGUMe8mUVI270MSFSsWpnjjtMaTpKQUea0FY9hd1O6iSswmdriIroofeOC/BeEPcO73mvOA7H0EfRvx21vnP9wJt6/JF11VntK7IH4yFi8uwn/ZQATR+suxmvv9Tsro0R/BVMsLx8mnlCgrNNtqRrlgcHDB1OlbMTCwoBfVAsMtvdmFHxJhI5eXmei1y+XuN3X240hSyqPJLTF9vyLoPXBYBL0LDHtH7X+fGSwygPxlJMThX/oFrr9+/tZ+ofA/WIFAR15v5dcNzyeHk15h3ncZu29uAt+oyiJYxAgs7btiJPmiZuZQVHy96B+srIImScAx8/srgFUeeLen33FpW6+ujln4Q4wbuw7d5bAu3B0mEVsgC9XTV0TZt4H8azoqf5+2Hs49bvd74LwFDK3BEde1irgKvKBMowzp+2e1gAL/jW8D1Cwh2c6QY8UlzwH/FOixpBTj09yFBj4lTlTUb7cSjnqhG4QNgRiaGsLA31stTY+Bc2/H4zHJlWxb7lWLdr11w+X1sSzq9+aFhFzkesO+TndNZ+f04v/j84TUFusDS+Y2ZvOrSKGhxoztzl7Z/vKXac/Jcvdr178/O7s6hlEYp6FXFD39fN4qXq4We+qhGJKwpSGvVUrvkx/ALn45wUTuLCc5PRd3RGyYfTEwXX9lqofzl2o26yavT62JeTKfQuaMLAq+1D39R7FlcrNqHVRq9SSl0JPaeq2xFDmh9BEH1KJ25YgzUHpVreFVxB5JIM1pnaJ8P8lotWi5O7ZpXG7bnpxBNBYmcW5zQYaNg/KnZcFiO6ncRpnKdkofnFHcTrKfnl+HMuic72jFET5cb/Nx18Zmx8ta5J31IK7vR6FbrTGsSQh+iLN8uQXxRjqJpWoaskHToygzQmI7BM6FUbBVDZpRgpX7/KCLatBWuTsbEDJF5oIvQHZ1rr9r4syCfv5g96GvLN8axTEAa8sJC2f8S+/OE/6vDeFvzfghSM1/9/9vC3eqD2CW1o1Bpw+5SGakqoMh0o7kf4jP0SDCr+DV0twTFRhty1dpTtZcORDRWoyy222cYC729DcE6nCmVOhA4JhA00yHap0Cr1a2vcdTcDnBH64InK1jaQmjDx+y1PS3mEQ8R9M4u33IjcPa5dxlLPIZB1FEzQHbNEL9kDNaR0KtVlfpmAj6jKJFtdWXYq1BGbqtBFppQprOp34YF3jMqFbEC5MFvmidMY1+mD3CFllDFx7V0LzYv5UOIGVEqMUQed/9koTbzlrBf3/dddQ0mhQLTUMl8b1WoAcL0oiQ1GiDx31W9SypPUqs41gUrFlc2QZPHg0HO9jbfldF9X4IGAbJD/jL33Ka3BYO5PVXt35/R+sIh627wBVa0WbwFOo6oQa6LsGIAC9TRRHiSUwBhQ+lCC5MMgAHdyQElwsXSuQNGYgG7I70H/M+GIcxTKEfphtmYCQL1lP3FuOZDKxZOIvHO1/CU+MiQTJKhcyBDjpvn9SYwuDBLH3w+IFUwKVTCnB46UR8sfGgzgDnI2/owyuME3fnE6VGN3vBppSNElx2aXSrw0fgRIuZTFIbktaTTdB961yWtTpt32zoimohEIS3Ly7jwVy1wlXK+SBCYJlIrQxZGcWj5raqcKxPevQ6Fokd5FyLsUl/QG7O+kVFKtpXoSEdUKmaoSv/eeWzXrveVk3dskwqcNY1Q3WS6jKaHk7ljh+i44nsaofgKGZZOGW8IIjsNZgauXF5yhDAmiFb5CtEXY4Gp1KLJWIxC9U9TMA0iSvbh3KdEU02g7dl6eDV1+rdlkCrRV1zfAKnt+7UFNcHT69UavXzYeg3J60L90t+OTUQQm50mSTsUcoYiKbUmsWHIaIExeBycaPlOVs8yNFT9wxWwrVveTl2PC3qfbHeC+81AatJaNakKj/peS6dKqUo6BLhETEm1k7w6snwEM4pvc9vpa1uvxKGaLZhiLuLSjH1SouUscqx9jr81qhAxyjyeS+NfdqiXihbW3h4GGqhFFLfS3Se/zVCZgxcK7Q0LLQsnekgQb3Wx7cmEn4tTqa+1JxNOTZiq10ZI+qSuoWZDCLEM89TZEW1JJoK1ylB7S3XdZyXI1NTU3V4sCG+0Ws1/azcfwB9J+bAwZseBnIMvrAjpF2FCGm5apJEjISox8ch8NFUlkTnwFbvIIWQSdvdbzlRg6tqE+oPbqPRw3xD3uxqk2eS9o9S9W/DIWmlxmFDSxd5DHLw5BNlw0XNIokrnpGI80dSoIzQLePSDqRWEbKOz4NxHEcguJjWeoCKYGpUwQTDDS2p7CFsKx8hkdUcgRpgtCgYOxZGBBIlxDijYVode6Do+t7xEMnAS1+L1WAqRlEOIkEkzCz5v5EzH6F7joDF5aEKuqMFRsSLeAf2hNY6kZFO71+p9FBe6G/s1PlbxiBodALMq13unhAAHfRRsnSDkqXldo01gCVi841vt0+paNs45UkiqZbI+sDu5DYIV20IOttWc+h7aFoyA/25gCLvAL+l3Tki5sCTN2FwakhAWN4Q0+d67cAHf8HRnUOYbGrbUo60DwpPzbQeXPbyZct2HZasyy4x1Ldbp5I7lHzPmrpuWDs4B7CbOm4ZRg21sv4pnCsEPbURDQvfehyNJPz4uFdK4A2Y1xfU4BsqirzwyHYiG8Ctp2K8wyl0sKNZ8qu1htKOZG0mvalvQ9blkTcIRAK1b/cjKioV1akh77Ul3oojJtCbOq6vd1QnQ+eGOllGWO3UBHCBG3PFtGDGyOaeQmA8lO9CBfyGYtzx3LTfn7sNLJCFDn/Y8UY+nRC/reqyy6IG7bT8vPbyaHBXq921ZMXx0nIzrGY/+KvfABaL6ZsD4G+XbQRLIUdh3q5OHftAhTherrUGfpU9ffFj3TSIEBkH72JHr/3HqCLo2sm1yzyQcPMbRgAyzjZmkathvTVdjbJQc6Os8Kz9tQFc7T6teKj8zOsrL/v8AmTYfDoRXhocECrxsdZF7XDfBnRk2DcjO+NAO4Vp9TnMnc7BEoB07KiD3eQd6s8xifncJy3/pqfF3prgSpJLcxp0/XrQLk8PYhBvXfyI4we3G8W2fGsJR8E7B/nBOrWHuNKi9rooQl5eFFvHtLk3y5dyAm7gdCIO8Bu9UQnuvmCw46Ds2r6cjkd4IHmpE+1yOvHhlx/LGvRvgHf5/yi+adhpcTsZD0eH9bM8xhLc5OERoFHAD0yvWxnP5KnDX2gCQQTNOmnVlS2ghu7vgrf/r/ySTfTgd1JHMR4gy6vteqgJcrykoCARJtR7KKe9PbronNI9b30qwougzVpOG4iWlHL+vsXuUmWsVDfknGPrWBkhh2LbD5WBjpeodLY0HiLxClH6sGHEy3zUqVVXlowOioGU1hOJxG5Zp4ArO71dzGGdeNAv5atJKcdKvFgoHgiYJJHwyYT6ds7fKHNJD49zmCmPtZw7sbSeP1/2BvLinvebW8CVI0cOWtROqJZI3duJ6nUg40SlMnYWc+sk4++PLdLVSA+ztCwLFmsIwazIj0iGpZ6qeFZ86wjDTwhdAlMsaI/CyLKtzj9h5Moa0MqN3O1uVO+2BJ7A581R1g/XGi2wFO36HF62coYMcxmO9wXC6SDvWPARLpmWESefiNvEUURL3cg34E1OjS0ecYgSxxuz4WhUJ02RjJXP8n6mQQsh2NYCK9EqOxq9WwQTWCT513ZPYiSKuPVbsLw8OdThCg/MgV/kTRTHGx6rPNjcKg6Y+xGAAEownqyRpeqzJjU+WdO01uZGrdbo/2M1ANUhaTuQsO6KxuGnTP9U1vapXDiOFNNXtW890niGcTqogfNg85p9DJq5xZ8MJ8bEdcgYXKrLH+KCcHEYR4HAuJJ8XA3GOXs4LviMWqPpnn2jSVWzESYCKoiSJAp8H5OFae5ydWcN3QH8iJR4PCcqWCWDFOP3W9+RGnq2VBybw2QfGzVsrV+gGMZHkmz/zcURI5y5uC4E501tk+qPLpCIRK/6GaP6CB7TnlrcPXtPTOf5pkyH/c/vC47DrjLe37nxzMaK/mbltz/sCfr3hfnTDBCL2LntmplgKRa51fcMxk8GTCs7a1Myj6p5Ytz2pxrdyjLoETL8PWI+YahfvbzWLqt9NZjokdrfCLz/e+DbXdC20XxNEbN4XLIthqjebocL0u3urqfPJMtDM4tiZdgHaZsCn7I1CPTF5ebigoYyfnhwsd6Z6VK1eaIf1g1QCFjujExNYXgTIS2Y8IEbE1eIbJknsh1155e1+W5k0Ey3pxH+3HXOCUAZ08U7i9O7wd2ge88FjSJt4Rt+inDWenrirtx4F0Xkta83Vx+BP53qZGrqxzEZJN8huRTS9XTv/AmGmDEBuk/3/ZLhHPDivG11umcn7vDhfELvpOmQj8thKYXKVmqQliUtrs+rFK1xwL/zllTDmNq6temPWuyJGrEBJUgvg4Ze1XRJEJMJOGdUb5QOisPf+eu7rsd91+MEnrTHrmsbugR5PD4wQA8vHv+QPK6cZzSJ8mJpWjZEbCoGimbZXhDI1XRNXNvQVCpVAlsYmU8HWS5UZEmJiG9bDlY2hI7NelGq+8M9UWY1FyrBx55ry0rHspT9RdQfB6WUhuM3d+IJp1MdhIYo6SQKqUd8y8NQU8SMcxF4ru2emDTTGweG5MeO5pceVY4OJREJgmxYBg9pU6yggzFCJAP+qYbOhSq18hI8QcuB82xA04W5/v954rHHfkWNBngFtdbiSTckY7apvzL2Dr+P5l54Q23KwukLI8FW678jkwb9oRpIFiZjkrm0HYWsCNQNs9fUeqZBh0Yr10br7L/qzXbbcKtMRW7cZus95fzXBL0l6/Hyq7sa+A8taAMQ+AQys3enuBNRNnZTQ8xa2WpmSlp1XKcdjVvEDpodFHtyfFqtaXWTvE5mAuxuDgXCk2IOU7bH0kpwnCwx5+mEqozvDUeUZXFfhYd1odXLsy4QRWxQUQHBDAUz0X1j8dFg4oEpC0iLML7LSiCHZLXGLjeeiaoWbuWrXVgC+APemni3KcumeTlv+rnUm8bRg9Qo2LBzt4XmM75CW9Warwsfn82scEx2MYBTTxpCcY8QA049lkNjDRU5RBqYhd42hISkoggJCHWjiDfjbB1dvFuX+aFl3fVxV6frjbZOa4PgZmQYW2HXeTBL+/a5u3jeroX1YnVVoRfYHmuL8w4aqnDl6bGiDXR85VWbTlclIasNhTaMmXzCPIbe7nSNF7yqTsW2vlPmVu9dRKBUJtEcRBVhWbvmlO7SmUR0VavY4sUkQf9tHRW1+87VYdZHInNvfvB1rfwuPfIGye9NRONf7eUELAFMJoD6wwmc4yaexjNr4027je/bZG+3+CC27lIuCwPszrvNqdMtuXS33+FIUmMMvqnMzvah++dLWjvewQRvmyPTYwKzLOKkR2efoCgMdpJkhpL+8baA6jTAJLM0ayq/1P7MQXyFB7KTKktbdotLArWEQD+amQgtQajgF5Eo676ZzT0mCO+/cerApKi8TKvuwnQVQxxCVsyWFTKqyKkhvaizZUdutUMdsFuRHkcLEjdE2N5FvrG9w++ltl4dxWdSdiBEuGnvOxfBDq/lbpJgGIsdx9E95iKpQZ2xyI7x9yIDwCcyMUo/HCJI+nY1EcTfJupgCqaiqjtzqb+x+qs55CzyhHFxhYwP2hUAvRmI6E0C0Rxe/iuxN8mWWRvw03yzyZl2NfKOwKgFWXgfZIZtHNfHgiCoP+z7v4J1hYS4rLxs9kLrCaD5LBOJ7z1fhQiGL6aIn7zyygf/fvzxp7+X5eskh1j3ffALoqxTtZetBt8bjXp8wwpy9Ycjll6pr7fERPAnu9Zljlkffu1yyYbdH0z6T2fgathIpH0+9p/79/e2uh2XXN/qNm/mTfbi3y9mShswYG58EG3kZ2xCTbyR3bHWyD7hTHbSRC6+tSd2ldvIdMYwNzK0HOdO1GOKmbDpkt6mjz1wGViMY8FW719PflL/1wtfKaf//lZ3jSl8GTJtdq28+UM3++D3hCyRqJixZd67aN9eYcIWJdRX2QNytjbCcAz/Xpi5ioYnuP5IOllSZDIcapXLRv503j66vRoUis1++cuZjh7mMBc4pY5XyWRfr5ZE8SZftpohltykqJJpKELJUEshdkXPo8BzkPHDHTt8zXddDyDGjIsSzMxBcPCbpiyFguanHkmBlceOQk3owlhV5cuhxxXMHZn1JV/WdG96cvXomRyfWTh9Qo+BfwLUFJ6ewMkZ0DSI/c3YPoAz85WbOhyfAf8Up97JpAhcwAstd7SEEG/SpmvzK/MfRz+eg5+6e6FI7oPN/mAFD4EVDGH8zBC2QGnnPvocHNlcTZPg+/AiOLjQwiNQlcIFOLS5noagsnNAJ+G8T7fq1eYOcTa2AsH14vpprGijKOCnJXlQg3chRry0GqtnvNd3WVHnR5q0Fu7uimR4lb1KUnFnWsBaN2gGygzo8V1QcpCsbnU6mrwzPEy2bjp2GgQjiK0RqySGVbSaF5ygZldz/GOaelPH9tW3rsHfmRwein7orIvktM4AhrmHfvdyJTHPLEzZu2hsfCc2fXuT/2l1/jtQQyM8+Xh8e8Fi1KiX54cO6MYl9bqaeZl2g3/4hWiH/hmUk+/YlW68YDdce2HJynq3kZtg7sT9AayY1YmswhA/cOiVspTIM9PcBHp3HqGkQQEegZrsCJ5orIB8q2Qhi2ntf/lP4NNWe9/myh294XXjDnt24QR8WPxXEnCdwYNez3Gxq2xropF3tcXMN7p/L0MT9czij2Lwt+z0ZINX8Tzz6mZOmbuslAivUuQ2oyIt2wbf0nSDq+mas9TocrN8PCI2kxsDNOBXXlYqULkrRVYDpuU5PxGF3JxEHq4Z2GWFjMoTfxiMZNF6wDfAinb1t/Sq0I4z3j7bZMPx1TW0IAgVx5poUbYQIqJdyqkIoyARiCKdFjMU7sPDaJb7dYFiAwMdXjGHC+hdRjybGkKlPWl0yZScUsBtuM3avXEYQDq63ok/YumasQ2Xe3ZK1OLYVq2W+HB1+uYC7R+i0QSSCQevAbiqy3tbvfTTDze3RIyNe/Lm+v2bmJMfZvMlZGR2bYY+ksRhc4rCCy46qK5FSZJlFmldiu8RCDFpmGPwXla4HJelmCr19vs2JGpVcIwG7fR7dzDnBx/8iytuVvBZHScraPdcw/3qXatVq1br+fJ3d6g02r2ZwRzE781jJx2merifFy8dTx9JuLWYv645Z6ei1h3XZnOFs+jvq7tuoMu0vhGfB/2bIT7/GtPpdW+ApcMnU3QgkRAoRSRXBTmjLuQe7EbN2JxIuS5c4xS896RX4as3atPDNcq7hbPCsZ22wCq1o1CyFsqWhBzxGhHcQvuGZbU6ujW4ucnAMt343hv9cOKO1XU2ApZmcRNuz/XyKBpCbfP42k9UTBa4ysX1tXRAc4VEEygf20jU5L3LAuVsrpCnnkuhyzhvLPlFjhYWTLCGKTZ7IuIwSl8cJajGcFL0/+5cUmiFfOj+5apUOD4azM/OFnX9vmCy+XXSGzMG/CorikwQL/Lv6o0eGVKeNPOZruKYjaCl1y/iue/e/QP6bl/zfbW2630EqkhkUm8gVk4Q1rHMIgw+qF9azdXchZfbe6+65sEolMP7VE2/P5eVvVoeuBAOXJxqN7mkv1AZljhv0iNkb5DIxBZKIEazkOaBXbQJbRwu8xw9b81N8wGpAtZ6lhu+lsYCVERCYh6vVk5U07JEnmyB2uNep299xyLLpW06mAXUAy4MUsfBunTgjfRQhqsc1CXiqoMa6jXt0Kcc4hP9N32H+X5reebnR38/ugwQTCGvGNnT+QPx0WSFRaMVNadLertWai+PRmMqRGN6J35Tkzdf3SJ5tLugjVm7+CDRLrrWor7KasxG6Zwu523FLbClIwWYdBSEnLpx/0u5Xv/zE/+JadDU93ukN8/+XhNvgkuo6owzH8xOE5sHm4DT4a4AW9OdnBeHZYLTpZ3pP12ZlzcNlxO1ZLJ8T43XH9MToiLJDfQ6FQuPZ86i9MbBZwG8jTuF1jxGK78IeB9fhsN6WBuvjqyV5o4kWd3Zkn4voaydK62nur6A7bspxF6Pu7rFWtuwihBdLX0qHh4I3rEzui4btdo0tVsgBnaKDK0vfC0hMHgxEBBurGOTkqDcp2rVjAM8VoX24V4W7+TphZ+SVm700Ms11Z5dtARmRvrwdPMBErlOdt7/vdz/rhjxvXab5F3l0gaaenXmOzAIxbyEUeYmYY+WhNTfPB9iG9suGDc8Mthju9GKxWaTAemppLJOYkoo0+OpXoKOiucqjVfHzLJ4hyLplASdGzXvHPfO9fvDBtTZPeA64F14d7Y3eVwP02MYOYamqeWJs5IItcrLEkg6CK9uOTayp/O9nudIsbrZUWtSlq6jNa0Qp62dvmHYv0C8aEAjTJlMUL7xJscb9kCFcAJgImJbWOQn2sBxMb/Dm8KeoH9fL+BEr0tJSsY3e6a1jFJWrQJ3sgl0va33KBnmFBNLUS73Ft6peydLrn8dOMQOvKiRsvdZodpY21KtQ7eHO2Gr/YNBwjGbhKyk1ep0ULMHdcprsGbCwDjwsy5umxYx1Lq/3LZ3PI9e9wiOhu3molZTU1q01IulHLyob4zocU06vbh0rb1Aboe2qzLg7QHmvuoEeckc0a+zud374t5BRyJrFuWMrS2nv+TN9x3Qw2iq/WVYcqNHjbybA1trV9rkM8POVszgHGL64diicBFrTW66xkus9+J7urmDln4OSPnoHO5OdXmFP1efMQqh5SOfnaJgzmYKzfw48D8qxe0DD/DX30D8OfS+KuKg/vmDiqLIoyHnlx/HR/VoKc0jlF4uD+Stoe7ikxYC6AZ0QHvWaZ/Jkrq8cMlUZbyjhGXQmSRZQvzc2a2mMJRmX9dhfqXKrbcGtp1P5Qp4KBcWnHHq7hboOpGU4QbEqM8J6LuJ4610MGicI+8gvFsbzhrX1/YBkRSnhXfLEYyXbcoSjitowKdhpjRapPKryQPDWDSbFAqHdetZEoal67khIJGj9rGOxPT+Chcowo5UiKCk36OXA2lnr3f36vQ0fKoPTmRSL86WbnQ6c58WoHQUYCbMnKGiCh3jQ3lHPixvZoaxAN4rXTkIQ/RAxGhpam1BZds/PhHybHdiEQYfcQs9nOzkv25FMrbSlIl7qOLQtFqFDn2a5Op3clixiQ7z73VHz34y8O4Z50oS+TwI72FG1AjT1NYwD7b741PGVDXjk0USOFoE42RnvlQkP+12u+h5D2bCsmS0HvT7Nj5HQCTfUDigG8YtcSGtj5Dh+Q89OvWb0YGGKXeUbxq00OCfv1qPpaQVdmA5eEuj8MaktP2WdTE9D8FKxAqtuEPLIx9St4T0REURcAhXRA6ejiGmxiItEvlXSFgzwnq6RjYMpQvb8jQOLXt5oe3QDR2ENWzo+Mi2pTVq9o30SdbUCbU0t6U9d2dZDX6XW6MGQ8yUTNzp8dNuQkxnrXMX4awfEk0y98eQWP5OJ1wweEcqZip1HJ9u8ikPXds0jeAX7uu6uYtSvwDkfbQPby6B5lNQBrzmIuj+QyMGLWOqxN4F5+PT+egT+7+e/7w18rmWsvq4BL+ATGMWdlt+Kvca8cQGb45UlEewtBd+4xvhsFA48HkUHvsmAksEjYd98+YN2mOpz+8FPwf2RWcMeqBz2lmsbPvTSaI50sLRroUyfBg6u8CtSRTFGm5YNU3OKc7yaOO1XJlQbjmu8jAe9VmAuIjX37sgYbKBkmO7FiDjg7zaBe5lumiEsqj5UDBu0yXF+MUZDV3LIttizSyl27ZEIZfOFp83Vkv4s4KH8mLUygDGxHBo+dXJQuwLLLqIRoJFbJU/2DmDmhHB4CZ3doAu6s3Z5D43zgFgAJMi+IS3j4woenU0D9G5nwdyPtqHNzfRnU8BEbBNcx+tnggHpAdb83zSIiDOdrenQ7bJ1i40V9kSMcqXDoi7RqoElgEXbdmFMNnXyszRSMbY9ac3FotV6PO2mJSDQv2e4wUVZo6yGPhQITP6Z69fdp/MjvCjg6Cz5eXkdPBaOo3obc3Z5yV5NLpOA7SOOaRW1H1wrhZq+37517Ec5zu1EorD7jhLGyMO1CLyu3FuEYxALIwX3AGZaWOBAX66aElXC5LBQDsZqXc/JRncJZ1qFT+dzoqjAH7wj1n5EoVQxqKLCoyXrFO5TkeIcQE8UluiKBFVfChGcSAQMJJbIxwD3cgliV/R889ur4Xhr5bairSSn61PcvnFjx6gKD0ixfFEd9WkC1WCEOHBG/bnlhagIwL6hnFiGLqLB1f24uLc4DPbWZ6GxmxCVx2hRbSwNCotsmm2swPXiCjte/T7RciMtO44TYseclgamhbZNJXRgavCLlv7zLe+3jpNehaY5nFsbqNjPxekfbQPb86Dpp8Ac26lhLphDLL64h7ftgtDYxQ4ngwne6q6Z6NQiNA3tWFEmKsfLaTNZkbYTDKjL1MaDterwgAdBR6NkTVumHumYVYf30XpmUIRX38O0UP+TWnNK8BI4hwLvyIM90z2eOBLobyZFilg0ScNA9kGgn10zSh02rlHQ6S9fDvzNp8ZFS2yJGoTQ4qpNRmSYDt9EJoYG+MDB28a5Y7jIWvaB5okLBgBFaR0rOrbfnKRLwAmekq3WG+7T5xENG+RLAe64d8DYW7q9dppd3/1t3r3uJ+B3a3fbf4yWUapM/XsNLU6cNaqiToW0Ba/ARY7leoKiiDmgQensaCUSKJIvxNly6rlkaByArRDeT3HTIeYMnwQQJ+I884l3DE1Wxi+MmJDE1unNsvioOAc8djKvZ5sWXJ8uSpWjs1hnyLKRrOs6ZD2+HHWIG9sWan1CEbV18pE6dCJBRtjxyfrGU/Ub1YlXFfBqEK+VtxDLb3DRSXo1GmcV8JGOa47oGuIyeNgg2GzHDvhZwgciERNn29O3WARmpPY1rJt+o4ESiosD2xT8Rgp45tnbzrHmRGLx2B2MRqpvxvgXwHr/WG4KvTxbL6Njt0HfOz+LFD3P7w9Aaolt71en3mYlutqVlWlIYyfsqzTO7hCpU29TQm2f9rqwDmwde7qOi/jej8NL3oe6oMkO9wOeCYQOHYoyqzlan2R+oxFFxfCR9Jxwo0cyOhMXVHHNXa0teJxjeXvrumiSKp/mLoor+Ph827VG3oQS6pTNPQ/KnxC00VSZ6HF+kyMKFwtqz8qxK1dkqyNp7KHvM1ssgA3bVr2KlxD9PzclnZ7aoUPGH/XAVJ1ifUZraRdcUsmvRxtoA6m+CmhFPdVNK0bzmjYpkkG9B2uIi+FcSOJp185kCF/K/zqXHSe7bmNAjJ6ig8c0opwPvF7G18cBqtrjMHms+iW47oP2JZivt3eypX2qyfSSqkRoxhSvNUGS6GmLWkQxVKWarcvTSKfDgsQZtriQNObnkDkEEFyyQx8dMCKAsn9RrGwLsF4zxajHDoYmIu5nXmWzhQUgQzz1kQQgqnMszND+jnUEmZ+R+KcDDu5/s0/FLC3pm6a2A79umMtjGaMmZzNslFr89ywicZVcd7sIxhi6mmFcuRbLu/64u0TSTdVNuRg6RgqCVRTScDC95cAR8IjiYm8cJWiGl5ZIk7ujBdYDhsYhOM81uc4Hsf+SveggwIWRupFzx2zWcgcyImfAFMsmgqhyDNWyTansijqODX7YbXUi0008s+wcFpS34k+NMzf1c48BsDG0UGJPjal2n8kFALrBm7QbiinqdaWY9nlcpoqsBz7if2yeaSEEvX1aN1Os4hDLVmKSBEpXdxSREqVIlJEPh6Km5O6q+MAOrDKwKbomGMJqDwRAPldYYEKMlPoFmYpE5jvZNp46P7loTNfffXvv1+e3bv0LObj1CmYTHVhIGCbirFNNRaqM3vTuHauFYYAICUFm4TjJsT41jtifv9iy7p1+639XueWL37vXWLq2aV7z57u+O/MmaHL94ekw+NzB/bsGbAGpuiBc4/NTuDikrApKZmBYCvXbjtQCyKVlV7Lm9FIgRlh6/afeid6bkHMKDb9QZlFFZhONi1EMy8iy+OoEEnVKnLr6XELc2wr5eJF5OmNuWSC9qbvzkXZ7lXUZYsQwlsQcyjLFmEdUz7ZW2BfwjBsq3hcF+izJLwyg4tZmXP7Adz04daagUVYHJwhZxoBBmXLQN6rUYDo6Z1InMyWFGWaxHEeMTF69MFXogu1EQE1XHQCyJ+uA53v9yzfEXMxNnbg/q+DNnXS9feFhC0ACigvsQy2HyhE46bzqeV+/Ci9Tqvu3z9FdMT+/mUCNfPMlmK8s3Nddfd+2sduaf3AL2wDbB0u9DnNRICl545p76ACJjuv7nsWs6EbZ+o6FLrGmzZ11qO+Vh1gFewmuE2MAdUBCvYKryZZ2DZr2ALqeMMwb5g9F0WzyFqG08Ukn3vVKhgW3iu8l43fP8XSssqRrexXVY84R+48V4OwQWYt4lHaqwhWb7DprZ3qBKk7DqLUHZsmDSFFyJxlL9ZtU7F8VpLkzCtndopSV1HiCRbWwm9vL2uN2vc8FzFeqdNp7AskY/YJIvrp19+SzqCUh/T3P35bVOjmMm45L64bj07abl52HXVqhD2knDnzu8cbyGGjLFYOet5FWEbKYxzKQz9eZ/n3YumiqJSQlhsIMS2PYsWZ51WqqHOWhAv5KOozvQ6TSj1AgpSR5eBAx71TBWM5HuVYNVxTU1e1uaeZEnlmtatTXvZcqkB1uiQlwAZ78MzqqQEKrM3HpUl1rycTrIqoMmJq2yvO9MwxwMGBvTBf/KWDmJihYrrOLvH1NYy5rDs7j4VuBkSsSQKTkgSBShkaFrOgNOvp2+9XqXdY1V2H0ONgxFdzJ/s3RH6zUwE7F2EAs4ooUFp7iWXp9Zw3uG2TiA+10Hk0VEf5qCijtaDtV+5amHKarAU6Ptz/fLt3eNtgLbbls/5a7J5nr6/FZmPba3v/zTdfLv9olDXRJKthZ6ul7ffLluyC0ZeAR1pPkHURYOxA+RRZlLYtA0FG3MvZWl7M9Wo3hCpQqMYK1W2F2ZcafBWs+c6TAho0RihUM4TKVqg2ClVXYfYRijLEA+epo4wcjUoQ96S6T54FtPKm99R0Pn/WVu608pWnRR8Y/P4DD1H13KMrcDVcXd7MXbAtc1VTJSS9xchQT94uAa0qxpheULC9cTb9n5rbwSuBHNltCg72RaAHiI9HyeIIBSOo/pyjt0TAKkL33StOsK90wpvKJDOMM77k67l8giTeNebhqDGcOuEbgSNwmqeTxZygPIUqVqgGCpWu0AuWbfTBZRdx5dx5ah5yu6MuobohxN8pxDcjVHEJEwtacu1tath13EnMw6qujEdTy9IZrQWKLkYSpWOt1em1fDdKx1rMbLCrEqVjLYirrjI+CDqhIIBqzYnV75v+veca/lhC1RXAvavqq4Afnq7Y1N53z1qYRUADAghQi3/9gerKCtuLFq52L9ETl6iKRGxJYRoyBfGKYbA0M4tcRQkVU0Sx8nIF/pTVkSi9CiLNVzU1FA1J6XaBTW9seb3yPdHxMMXhcMRjwgpgVvxFHD8RhMc95Q1hZZ8NpcXKcnpWthEk6ugtG/SIRe6LOyLKS+mLFw2VUH0V6uu10MYeKg4HZGm5j6R8aHWpwlQvY8om0uIUXLaLLqvdF2CGVFgKWsgOJU9p5RA9IuOzaGIsjLRbdMVz8QTKCJVZMWQGM1S1ZI7G+3FRG0d/uvRobHgDfowKqYOH3yi61VII0ffHEhGcXtdpwi8s7PJYKcW22NB6XxSRiIi+juu9dY5M9dnWaYmh83KNxSmrzGOpOKzhoyuHqeKAe4n9YmNSkh8tj/NmBkSXy+GCCe6vTEbl+sFeiwkBezzmyZq+J7WEBTGUbVaWPPeVQpayQVRZRpEbUwlf9YjMFX8Mgxt9/gRz9CYpqzfgdRNwQTzWqRuq87EKijwSTnNIzBsEYkokqz3IXIqdIwDmkwsIISyxVwxmLk0lBqq5HMOccgnE3jYoiHFi4wmqWCtKkKaZWC/6MV8XdpHxwmtNzxUNJad6kqqy6/0uJ60sXIHReBvO1NfAXeis3wNfsj99IVzJFEgNEFAP/fA3+KBzOW64nOmUq9CXtJ6/ZPNt4Iv7GpxcxfdQdQVGgOhRgGJHQkIZVIPY2tuLhQDmRA0WojYOeFgpG9oJ8pLUjoSUoXaWlfFnO2lpeNop8vi8naaNg6eGsIS7LQJ0xUG0F5zY3V5Rha69hgmmvS7W9fYGbKG9KUoXtfzoaO/E09LeJdre9m6hXmnvoZPf3osjq72PSmzlQo6rLqpczKtyCRdaGhsdlcv9V67wWbnST/sqWBRbfRSJWhP1+F0bgoHrbFHmUZutssOjSj3lMTkbKm+ipYo+VMlePZmmk2RztMwmL8iziG6n30S3KBWjL42i8tbNZZ6CgfJsIBtKLvWssqE0qsrKXuAJT9qxL42mUpfoDuXNFK5pebbRfE/mPZt7VZQZdLwW1rrVSgZRjD6m3KJb7Qh/nO3xUnFRVk9kL55dmiwfKndNP+FZpS1arnChcmu1JduTilkcE6P4X3pUokfVrEE1mR66t8G+bM/uUanenj3ZFlu9qDwaIRU2r6CyoeTg1Vb1qyTyVqkiI2cop+3B0nOJT8ZB2i8pb+E+TdGj2vaIlSpNugzOtAVK5ifnVNkbOZtMvgKFH5gfrFHrLGjpG0TT0FpC51+WihFLL46B8/+uiZlFAqtlllthpXvd534izH9ahaZSS1tHV4+PIfW4g6zS3UOSIAtgHHHUIYd97RsnnbLdDhuhbUAXSmaypVyl4SovxiWocN8Ms8w1xzz/t0ZvkCNFHIsqOM8s9VyFamjsDTNt8rm+ZNPFW+G0JiOM1KxVi3XaPNSuU5cO3+k2ymg9KcYYa7wJxllvov1me2qSKaaa7LFBZw2ptluNPRapTfVAnTPOOe/Chxzl9U3V8K9bbK99nrjhpka3PLIy9LdwDU+Ix+vj6wfiCUSIRKZQaZcxSs64vji74H5wGYt1ehrsM+JweXyBUFMklki1tHV09WT6Boa35C83MmfeIVmij6q+f2QwwGgY0h+p6Hl9GBLXGlgnfHx9SPqv7Dwg1wlenboI9KcLwtKWbSvr5kw0jCazpS3bVtbMGg4/9V37cPM1OR7erhefLwLm1DdbxfC14W04/VklwIwPI1haGw2gZbNJvnycfXEVBjcj91eE++ibSOjl+x9phEk8PF/ffOGyTYoEmXb6lbySqIweLo5t45PCLfchy798MT1xVeT1SW9ohuRhyKk8/AG8d/8A1IdGV//ydus6yaqsKvD6ftx9+WjjfnG53pjTLPevNKP+pWbi1/I4L+Vh/0LT759ruv0zTad/qokNaC7kXs5yyiEvMucXAgK4gPgGGm6/8XvnPoLKXuBTK+tdSp/W8PoloVnfSEJ8lX97+RJW7GjIeVOXXx24ZPlElFXf+JizqZMN7GQbyoCwWw==) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + + font-family: 'IBM Plex Sans'; + font-style: normal; + font-weight: 500; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + + font-family: 'IBM Plex Sans'; + font-style: normal; + font-weight: 600; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + + font-family: 'IBM Plex Sans'; + font-style: normal; + font-weight: 700; + font-stretch: 100%; + font-display: swap; + src: url(data:font/woff2;base64,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) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + + font-family: 'IBM Plex Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(data:font/woff2;base64,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) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + + font-family: 'IBM Plex Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url(data:font/woff2;base64,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) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +@font-face { + + font-family: 'IBM Plex Mono'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url(data:font/woff2;base64,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) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/assets/lrsomatic_report/assets/styles/report.scss b/assets/lrsomatic_report/assets/styles/report.scss new file mode 100644 index 00000000..444572b7 --- /dev/null +++ b/assets/lrsomatic_report/assets/styles/report.scss @@ -0,0 +1,951 @@ +// ============================================================ +// LRSomatic Report — custom SCSS theme +// Aesthetic: "laboratory precision meets scientific-journal editorial" +// · Spectral (serif) for display / section headings +// · IBM Plex Sans for body +// · IBM Plex Mono for all numeric + genomic-coordinate data +// Fonts are base64-embedded (assets/styles/_fonts.scss) so the +// report stays fully self-contained and offline-legible. +// +// Section structure follows Quarto convention: +// /*-- scss:defaults --*/ Bootstrap variable overrides (resolved before theme) +// /*-- scss:rules --*/ Custom CSS rules (resolved after theme) +// ============================================================ + +/*-- scss:defaults --*/ + +// ── Core palette — warm paper + ink + a single confident accent ── +$report-primary: #0d5c75; // deep cyan-teal — clinical spine accent +$report-primary-light: #128aa6; // brighter teal +$report-primary-muted: #e8f1f4; // pale teal wash — quiet fills + +$report-success: #3f7d4e; // muted clinical green +$report-warning: #a9781a; // ochre / amber +$report-danger: #b3402f; // brick red + +$report-text: #1b1e22; // warm near-black ink +$report-text-muted: #6a6f76; +$report-bg: #f6f4ee; // warm archival paper +$report-surface: #ffffff; +$report-border: #e4e0d6; // warm hairline + +// ── Bootstrap SCSS variable overrides ───────────────────────── +$primary: $report-primary; +$success: $report-success; +$warning: $report-warning; +$danger: $report-danger; +$body-bg: $report-bg; +$body-color: $report-text; +$border-color: $report-border; +$link-color: $report-primary; + +// Typography — embedded webfonts, graceful system fallbacks +$font-family-sans-serif: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; +$font-family-monospace: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace; +$headings-font-family: "Spectral", Georgia, "Times New Roman", serif; + +$font-size-base: 0.92rem; +$headings-color: $report-text; +$headings-font-weight: 600; +$line-height-base: 1.6; + +$border-radius: 8px; +$border-radius-sm: 5px; +$border-radius-lg: 12px; + +/*-- scss:rules --*/ + +@import "fonts"; + +// ============================================================ +// CSS custom properties — light mode +// ============================================================ +:root { + --color-primary: #{$report-primary}; + --color-primary-light: #{$report-primary-light}; + --color-primary-muted: #{$report-primary-muted}; + --color-success: #{$report-success}; + --color-warning: #{$report-warning}; + --color-danger: #{$report-danger}; + --color-text: #{$report-text}; + --color-text-muted: #{$report-text-muted}; + --color-bg: #{$report-bg}; + --color-surface: #{$report-surface}; + --color-surface-alt: #faf8f2; + --color-border: #{$report-border}; + + --font-sans: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + --font-mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace; + --font-display: "Spectral", Georgia, "Times New Roman", serif; + + --card-radius: 12px; + --card-shadow: 0 1px 2px rgba(27, 30, 34, 0.05), 0 8px 24px -16px rgba(27, 30, 34, 0.18); + --card-shadow-hover: 0 2px 6px rgba(27, 30, 34, 0.08), 0 18px 40px -20px rgba(27, 30, 34, 0.28); + + // Impact colour tokens (values match formatStyle in _smallvariants.qmd) + --impact-high: #f7e3df; + --impact-moderate: #f6edd6; + --impact-low: #e4efe3; + --impact-modifier: #f3f1ea; + + // SV-type colour tokens (values match formatStyle in _severus.qmd) + --sv-del: #dbeafe; + --sv-dup: #dcfce7; + --sv-inv: #fef9c3; + --sv-ins: #fee2e2; + --sv-bnd: #f3e8ff; + + // Circos ring palette — keep in sync with colour constants in R/circos.R + // SNV (SBS-6, softened toward report ink/brick/paper) + --circos-snv-ca: #2EBAED; + --circos-snv-cg: #1b1e22; + --circos-snv-ct: #b3402f; + --circos-snv-ta: #c7c2b8; + --circos-snv-tc: #ADCC54; + --circos-snv-tg: #F0D0CE; + // SV (hue-matched to --sv-* table tokens) + --circos-sv-ins: #cf5b46; + --circos-sv-del: #2f6db3; + --circos-sv-inv: #c08a1e; + --circos-sv-dup: #3f7d4e; + // CNV (tied to report spine) + --circos-cnv-major: #b3402f; + --circos-cnv-minor: #0d5c75; + --circos-cnv-total: #1b1e22; + // BND/translocation + --circos-bnd: #8a5fa3; + + // Metric-card accent colours + --metric-purity: #{$report-danger}; + --metric-ploidy: #{$report-warning}; + --metric-coverage: #{$report-primary}; + --metric-n50: #{$report-success}; + --metric-snvs: #6d4b8a; + --metric-svs: #2c4a6e; + --metric-panel-vars: #{$report-primary-light}; + --metric-panel-svs: #17877a; + --metric-tmb: #{$report-success}; + --metric-error: #{$report-text-muted}; + --metric-phased: #4a6d8a; +} + +// ============================================================ +// Dark mode — warm-neutral ink +// ============================================================ +@media (prefers-color-scheme: dark) { + :root { + --color-primary: #3fb0cc; + --color-primary-light: #5cc7e0; + --color-primary-muted: #16323d; + --color-success: #5fb874; + --color-warning: #d6a23c; + --color-danger: #e0715f; + --color-text: #e8e6df; + --color-text-muted: #9aa0a6; + --color-bg: #14161a; + --color-surface: #1d2025; + --color-surface-alt: #21252b; + --color-border: #31363d; + --card-shadow: 0 1px 2px rgba(0,0,0,0.4), 0 10px 30px -18px rgba(0,0,0,0.7); + --card-shadow-hover: 0 2px 6px rgba(0,0,0,0.5), 0 20px 44px -20px rgba(0,0,0,0.8); + // Circos legend swatches that would be invisible on dark bg — lighten to match --color-text + --circos-snv-cg: #e8e6df; + --circos-cnv-total: #e8e6df; + } + + body { background: var(--color-bg) !important; color: var(--color-text) !important; } + + // Bootstrap alert boxes — default light text is unreadable on dark bg + .alert { + background: var(--color-surface) !important; + color: var(--color-text) !important; + border-color: var(--color-border) !important; + } + .alert-info { border-left: 3px solid var(--color-primary) !important; } + .alert-warning { border-left: 3px solid var(--color-warning) !important; } + + // DT dark-mode reset (overrides DT inline backgrounds) + table.dataTable tbody tr { + background: var(--color-surface) !important; + td { + background-color: var(--color-surface) !important; + color: var(--color-text) !important; + border-color: var(--color-border) !important; + } + &:hover td { background-color: var(--color-primary-muted) !important; } + } + + .callout { background: var(--color-surface) !important; border-color: var(--color-border) !important; } + .callout-header { background: transparent !important; } + .quarto-title-block, .quarto-title-meta { color: var(--color-text) !important; } + #TOC, .sidebar-navigation { background: transparent !important; } +} + +// ============================================================ +// Page atmosphere — warm paper with a whisper of grain +// ============================================================ +body { + font-family: var(--font-sans); + font-feature-settings: "ss01", "cv05"; + background-color: var(--color-bg); + background-image: + radial-gradient(1200px 520px at 78% -8%, rgba(18, 138, 166, 0.10), transparent 60%), + radial-gradient(900px 480px at -6% 4%, rgba(179, 64, 47, 0.05), transparent 55%), + url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxNjAnIGhlaWdodD0nMTYwJz48ZmlsdGVyIGlkPSduJz48ZmVUdXJidWxlbmNlIHR5cGU9J2ZyYWN0YWxOb2lzZScgYmFzZUZyZXF1ZW5jeT0nMC44NScgbnVtT2N0YXZlcz0nMicgc3RpdGNoVGlsZXM9J3N0aXRjaCcvPjxmZUNvbG9yTWF0cml4IHR5cGU9J3NhdHVyYXRlJyB2YWx1ZXM9JzAnLz48L2ZpbHRlcj48cmVjdCB3aWR0aD0nMTAwJScgaGVpZ2h0PScxMDAlJyBmaWx0ZXI9J3VybCgjbiknIG9wYWNpdHk9JzAuMDI1Jy8+PC9zdmc+"); + background-attachment: fixed, fixed, fixed; + counter-reset: section-counter; +} + +@media (prefers-color-scheme: dark) { + body { + background-image: + radial-gradient(1200px 520px at 78% -8%, rgba(63, 176, 204, 0.10), transparent 60%), + radial-gradient(900px 480px at -6% 4%, rgba(224, 113, 95, 0.06), transparent 55%), + url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxNjAnIGhlaWdodD0nMTYwJz48ZmlsdGVyIGlkPSduJz48ZmVUdXJidWxlbmNlIHR5cGU9J2ZyYWN0YWxOb2lzZScgYmFzZUZyZXF1ZW5jeT0nMC44NScgbnVtT2N0YXZlcz0nMicgc3RpdGNoVGlsZXM9J3N0aXRjaCcvPjxmZUNvbG9yTWF0cml4IHR5cGU9J3NhdHVyYXRlJyB2YWx1ZXM9JzAnLz48L2ZpbHRlcj48cmVjdCB3aWR0aD0nMTAwJScgaGVpZ2h0PScxMDAlJyBmaWx0ZXI9J3VybCgjbiknIG9wYWNpdHk9JzAuMDQnLz48L3N2Zz4="); + } +} + +p { line-height: 1.65; } + +// ============================================================ +// Masthead — Quarto's auto title block becomes a slim eyebrow +// ============================================================ +.quarto-title-block .quarto-title .title, +header#title-block-header .title { + font-family: var(--font-sans); + font-size: 0.74rem !important; + font-weight: 600; + letter-spacing: 0.22em; + text-transform: uppercase; + color: var(--color-primary); + margin-bottom: 0.15rem; +} + +.quarto-title-block .quarto-title-meta, +header#title-block-header .quarto-title-meta { + font-family: var(--font-mono); + font-size: 0.7rem; + letter-spacing: 0.04em; + color: var(--color-text-muted); + text-transform: uppercase; +} +.quarto-title-meta-heading { display: none; } + +// ============================================================ +// Hero — sample identity band +// ============================================================ +.report-hero { + position: relative; + margin: 0.4rem 0 2.2rem; + padding: 1.6rem 0 1.5rem; + border-bottom: 1px solid var(--color-border); + + &::after { + content: ""; + position: absolute; + left: 0; bottom: -1px; + width: 88px; height: 3px; + background: linear-gradient(90deg, var(--color-primary), var(--color-primary-light)); + border-radius: 3px; + } +} + +.report-hero__eyebrow { + font-family: var(--font-mono); + font-size: 0.72rem; + font-weight: 500; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--color-text-muted); + margin-bottom: 0.35rem; +} + +.report-hero__title { + font-family: var(--font-display); + font-weight: 600; + font-size: clamp(2.1rem, 4.5vw, 3.1rem); + line-height: 1.04; + letter-spacing: -0.01em; + color: var(--color-text); + margin: 0 0 0.85rem; + word-break: break-word; +} + +.report-hero__badges { display: flex; flex-wrap: wrap; gap: 7px; } + +// ============================================================ +// Section headings — editorial numbered rules +// ============================================================ +h2 { + font-family: var(--font-display); + font-weight: 600; + font-size: 1.6rem; + letter-spacing: -0.005em; + color: var(--color-text); + margin-top: 2.8rem; + margin-bottom: 1.1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--color-border); + display: flex; + align-items: baseline; + gap: 0.7rem; + + &::before { + counter-increment: section-counter; + content: counter(section-counter, decimal-leading-zero); + font-family: var(--font-mono); + font-size: 0.82rem; + font-weight: 600; + color: var(--color-primary); + letter-spacing: 0.04em; + transform: translateY(-0.15em); + flex: none; + } +} + +h3 { + font-family: var(--font-sans); + font-weight: 600; + font-size: 1.05rem; + letter-spacing: 0.01em; + color: var(--color-text); +} + +code, pre { font-family: var(--font-mono); } +code:not(pre code) { + background: var(--color-primary-muted); + color: var(--color-primary); + padding: 0.08em 0.38em; + border-radius: 4px; + font-size: 0.86em; +} + +// Section intro blockquotes (e.g. circos legend) → quiet note panel +blockquote { + border-left: 3px solid var(--color-primary); + background: var(--color-surface); + margin: 0 0 1.4rem; + padding: 0.85rem 1.1rem; + border-radius: 0 8px 8px 0; + font-size: 0.88rem; + color: var(--color-text-muted); + box-shadow: var(--card-shadow); + + p { margin: 0; } + strong { color: var(--color-text); } +} + +// ============================================================ +// Badges (run mode / reference / sex) +// ============================================================ +.report-badge { + display: inline-flex; + align-items: center; + padding: 4px 12px; + border-radius: 999px; + font-family: var(--font-mono); + font-size: 0.66rem; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + + &.is-matched { + color: var(--color-success); + background: color-mix(in srgb, var(--color-success) 12%, transparent); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-success) 35%, transparent); + } + &.is-tumour-only { + color: var(--color-warning); + background: color-mix(in srgb, var(--color-warning) 12%, transparent); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-warning) 35%, transparent); + } + &.is-meta { + color: var(--color-text-muted); + background: color-mix(in srgb, var(--color-text-muted) 10%, transparent); + box-shadow: inset 0 0 0 1px var(--color-border); + } +} + +// ============================================================ +// Metric cards grid +// ============================================================ +.metric-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(168px, 1fr)); + gap: 14px; + margin: 18px 0 30px; +} + +.metric-card { + position: relative; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--card-radius); + padding: 15px 16px 14px; + box-shadow: var(--card-shadow); + overflow: hidden; + transition: box-shadow 0.18s ease, transform 0.18s ease, border-color 0.18s ease; + + // animated entrance + opacity: 0; + transform: translateY(8px); + animation: metric-rise 0.5s cubic-bezier(0.22, 0.61, 0.36, 1) forwards; + + // accent keyline (left) — colour set per modifier below + &::before { + content: ""; + position: absolute; + left: 0; top: 0; bottom: 0; + width: 3px; + background: var(--_accent, var(--color-primary)); + opacity: 0.85; + transition: width 0.18s ease; + } + + &:hover { + box-shadow: var(--card-shadow-hover); + transform: translateY(-3px); + border-color: color-mix(in srgb, var(--_accent, var(--color-primary)) 45%, var(--color-border)); + &::before { width: 5px; } + } + + &.metric-purity { --_accent: var(--metric-purity); } + &.metric-ploidy { --_accent: var(--metric-ploidy); } + &.metric-coverage { --_accent: var(--metric-coverage); } + &.metric-n50 { --_accent: var(--metric-n50); } + &.metric-snvs { --_accent: var(--metric-snvs); } + &.metric-svs { --_accent: var(--metric-svs); } + &.metric-panel-vars { --_accent: var(--metric-panel-vars); } + &.metric-panel-svs { --_accent: var(--metric-panel-svs); } + &.metric-tmb { --_accent: var(--metric-tmb); } + &.metric-error { --_accent: var(--metric-error); } + &.metric-phased { --_accent: var(--metric-phased); } + + // staggered reveal + &:nth-child(1) { animation-delay: 0.02s; } + &:nth-child(2) { animation-delay: 0.06s; } + &:nth-child(3) { animation-delay: 0.10s; } + &:nth-child(4) { animation-delay: 0.14s; } + &:nth-child(5) { animation-delay: 0.18s; } + &:nth-child(6) { animation-delay: 0.22s; } + &:nth-child(7) { animation-delay: 0.26s; } + &:nth-child(8) { animation-delay: 0.30s; } + &:nth-child(9) { animation-delay: 0.34s; } + &:nth-child(10){ animation-delay: 0.38s; } + &:nth-child(11){ animation-delay: 0.42s; } +} + +@keyframes metric-rise { + to { opacity: 1; transform: translateY(0); } +} + +.metric-card__label { + display: flex; + align-items: center; + gap: 6px; + font-family: var(--font-sans); + font-size: 0.64rem; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--color-text-muted); + margin-bottom: 7px; + + &::before { + content: ""; + width: 6px; height: 6px; + border-radius: 50%; + background: var(--_accent, var(--color-primary)); + flex: none; + } + + // Opt-out of uppercase for a single character (e.g. the trailing "s" in "SNVs") + .lc { text-transform: none; } +} + +.metric-card__value { + font-family: var(--font-mono); + font-size: 1.5rem; + font-weight: 500; + font-feature-settings: "tnum" 1; + color: var(--color-text); + line-height: 1.1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.metric-card__subtitle { + font-family: var(--font-mono); + font-size: 0.66rem; + color: var(--color-text-muted); + margin-top: 5px; +} + +@media (prefers-reduced-motion: reduce) { + .metric-card { animation: none; opacity: 1; transform: none; } +} + +// ============================================================ +// Panel controls (gene-panel selector) +// ============================================================ +.panel-controls { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 12px; + padding: 12px 16px; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--card-radius); + box-shadow: var(--card-shadow); +} + +.panel-controls__label { + font-weight: 600; + font-size: 0.8rem; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--color-text-muted); + white-space: nowrap; +} + +.panel-controls__select { + padding: 6px 12px; + border-radius: 6px; + border: 1px solid var(--color-border); + font-family: var(--font-sans); + font-size: 0.85rem; + font-weight: 500; + background: var(--color-bg); + color: var(--color-text); + cursor: pointer; + transition: border-color 0.15s, box-shadow 0.15s; + + &:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 22%, transparent); + } +} + +.panel-controls__count { + font-family: var(--font-mono); + font-size: 0.76rem; + color: var(--color-text-muted); + margin-left: auto; +} + +.panel-controls__custom-area { + padding: 12px 16px; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--card-radius); + margin-bottom: 14px; + box-shadow: var(--card-shadow); + + label { + font-weight: 600; + font-size: 0.78rem; + letter-spacing: 0.03em; + text-transform: uppercase; + color: var(--color-text-muted); + display: block; + margin-bottom: 8px; + } + + textarea { + width: 440px; + max-width: 100%; + font-family: var(--font-mono); + font-size: 0.82rem; + padding: 10px; + border: 1px solid var(--color-border); + border-radius: 6px; + background: var(--color-bg); + color: var(--color-text); + resize: vertical; + + &:focus { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 22%, transparent); + } + } +} + +// ============================================================ +// DT tables +// ============================================================ +.datatables, div.dataTables_wrapper { + border-radius: var(--card-radius); + overflow: hidden; + border: 1px solid var(--color-border); + background: var(--color-surface); + box-shadow: var(--card-shadow); + padding: 4px 12px 10px; +} + +table.dataTable { + border-collapse: collapse !important; + border-spacing: 0 !important; + font-family: var(--font-sans); + + thead th { + background: var(--color-surface-alt) !important; + color: var(--color-text-muted) !important; + border-bottom: 1.5px solid var(--color-border) !important; + font-family: var(--font-sans); + font-size: 0.7rem; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + padding: 10px 12px !important; + white-space: nowrap; + } + + thead input, thead select { + border: 1px solid var(--color-border) !important; + border-radius: 5px !important; + font-family: var(--font-mono) !important; + font-size: 0.72rem !important; + padding: 3px 7px !important; + background: var(--color-bg) !important; + color: var(--color-text) !important; + font-weight: 400; + text-transform: none; + letter-spacing: 0; + } + + tbody { + tr td { + font-size: 0.8rem; + padding: 7px 12px !important; + border-bottom: 1px solid var(--color-border) !important; + color: var(--color-text) !important; + } + tr td:first-child { font-weight: 500; } + tr.odd td { background: var(--color-surface) !important; } + tr.even td { background: var(--color-surface-alt) !important; } + tr:hover td { background: var(--color-primary-muted) !important; } + } +} + +// DT Buttons toolbar +div.dt-buttons { + margin-bottom: 10px !important; + + button.dt-button { + background: var(--color-surface) !important; + border: 1px solid var(--color-border) !important; + border-radius: 6px !important; + color: var(--color-text-muted) !important; + font-family: var(--font-sans) !important; + font-size: 0.72rem !important; + font-weight: 600 !important; + letter-spacing: 0.04em; + text-transform: uppercase; + padding: 5px 14px !important; + margin-right: 5px !important; + box-shadow: none !important; + transition: background 0.15s, border-color 0.15s, color 0.15s !important; + + &:hover { + background: var(--color-primary-muted) !important; + border-color: var(--color-primary) !important; + color: var(--color-primary) !important; + } + } +} + +.dataTables_info, +.dataTables_length, +.dataTables_paginate { + font-family: var(--font-mono) !important; + font-size: 0.74rem !important; + color: var(--color-text-muted) !important; +} + +.dataTables_paginate .paginate_button.current { + border-radius: 5px !important; + color: var(--color-primary) !important; + font-weight: 600; +} + +.dataTables_filter input { + border: 1px solid var(--color-border) !important; + border-radius: 6px !important; + font-family: var(--font-mono) !important; + font-size: 0.76rem !important; + padding: 4px 10px !important; + background: var(--color-bg) !important; + color: var(--color-text) !important; + + &:focus { + outline: none; + border-color: var(--color-primary) !important; + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 20%, transparent) !important; + } +} + +// ============================================================ +// Tabset (ASCAT panel-tabset) +// ============================================================ +.panel-tabset > .nav-tabs { + border-bottom: 1px solid var(--color-border); + gap: 2px; + + .nav-link { + font-family: var(--font-sans); + font-size: 0.82rem; + font-weight: 600; + letter-spacing: 0.02em; + color: var(--color-text-muted); + border: none; + border-bottom: 2px solid transparent; + border-radius: 0; + padding: 8px 14px; + background: transparent; + transition: color 0.15s, border-color 0.15s; + + &:hover { color: var(--color-primary); } + &.active { + color: var(--color-primary); + background: transparent; + border-bottom-color: var(--color-primary); + } + } +} + +// ============================================================ +// TOC / sidebar +// ============================================================ +#TOC { + font-family: var(--font-sans); + font-size: 0.8rem; + padding-right: 14px; + + ul { list-style: none; padding-left: 0; } + li { margin: 1px 0; } + + a { + color: var(--color-text-muted); + text-decoration: none; + display: block; + padding: 4px 10px; + border-radius: 6px; + border-left: 2px solid transparent; + transition: color 0.12s, background 0.12s, border-color 0.12s; + + &:hover { color: var(--color-primary); background: var(--color-primary-muted); } + &.active { + color: var(--color-primary); + font-weight: 600; + border-left-color: var(--color-primary); + background: color-mix(in srgb, var(--color-primary) 8%, transparent); + } + } +} + +.sidebar nav[role="doc-toc"] > h2, +#toc-title { + font-family: var(--font-sans); + font-size: 0.66rem; + font-weight: 600; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--color-text-muted); + border: none; + padding: 0 0 0.4rem 10px; + margin: 0; +} + +// ============================================================ +// Circos — lightbox plate + editorial HTML legend +// ============================================================ +.circos-container { + background: #fcfbf7; + border: 1px solid #d8d3c8; + border-radius: var(--card-radius); + padding: 26px; + display: inline-block; + box-shadow: var(--card-shadow), inset 0 0 0 1px rgba(255,255,255,0.6); + text-align: center; +} + +@media (prefers-color-scheme: dark) { + .circos-container { + // Lightbox stays light — only the frame adapts to signal intentionality + border-color: #4a4540; + box-shadow: 0 0 0 1px rgba(0,0,0,0.5), 0 8px 32px -8px rgba(0,0,0,0.7); + } +} + +.circos-eyebrow { + font-family: var(--font-mono); + font-size: 0.68rem; + font-weight: 500; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--color-text-muted); + margin-bottom: 0.6rem; +} + +// Legend below the plate +.circos-legend { + display: flex; + flex-wrap: wrap; + gap: 16px 28px; + margin-top: 1rem; + padding-top: 0.85rem; + border-top: 1px solid var(--color-border); + font-family: var(--font-mono); + font-size: 0.68rem; +} + +.circos-legend__group { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 110px; +} + +.circos-legend__title { + font-size: 0.6rem; + font-weight: 600; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--color-text-muted); + margin-bottom: 3px; +} + +.circos-legend__item { + display: flex; + align-items: center; + gap: 7px; + color: var(--color-text); + line-height: 1.3; +} + +// SNV swatches — round dots +.circos-swatch--dot { + width: 9px; + height: 9px; + border-radius: 50%; + flex-shrink: 0; + background: var(--_swatch); +} + +// SV / CNV swatches — short horizontal bar +.circos-swatch--bar { + width: 18px; + height: 3px; + border-radius: 2px; + flex-shrink: 0; + background: var(--_swatch); +} + +// BND/translocation — curved arc glyph via border trick +.circos-swatch--arc { + width: 14px; + height: 7px; + border-radius: 14px 14px 0 0; + border: 2px solid var(--_swatch); + border-bottom: none; + flex-shrink: 0; + background: transparent; +} + +@media print { + .circos-legend { border-top-color: #ccc; } + .circos-eyebrow { color: #666; } +} + +// ============================================================ +// Callouts +// ============================================================ +.callout { + border-radius: var(--card-radius) !important; + border: 1px solid var(--color-border) !important; + border-left-width: 3px !important; + font-size: 0.88rem !important; + box-shadow: var(--card-shadow); +} + +.callout .callout-title, +.callout-header { + font-family: var(--font-sans); + font-weight: 600; + letter-spacing: 0.01em; +} + +// horizontal rules between sections — quiet, the numbered h2 carries the break +hr { + border: none; + border-top: 1px solid var(--color-border); + opacity: 0.5; + margin: 2.4rem 0 0; +} + +// Footer line +main > p:last-child em, +.quarto-document-content > p:last-child em { + font-family: var(--font-mono); + font-style: normal; + font-size: 0.72rem; + letter-spacing: 0.03em; + color: var(--color-text-muted); +} + +// ============================================================ +// Max-width guard for full-page-layout +// ============================================================ +.page-full .quarto-title-block, +.page-full > .column-body { + max-width: 1400px; +} + +// ============================================================ +// Print stylesheet +// ============================================================ +@media print { + #TOC, .sidebar-navigation, .quarto-sidebar, + .dt-buttons, div.dt-buttons, .panel-controls, + #custom-gene-panel, .dataTables_filter, .dataTables_length, + .dataTables_paginate, .dataTables_info { display: none !important; } + + body { + background: #fff !important; + color: #000 !important; + background-image: none !important; + } + + .dataTables_scrollBody { + height: auto !important; + max-height: none !important; + overflow: visible !important; + } + + .datatables, div.dataTables_wrapper { box-shadow: none !important; } + + table.dataTable { + thead th { background: #f0f0ec !important; color: #000 !important; } + tbody tr td { color: #000 !important; } + } + + .metric-card { + break-inside: avoid; + box-shadow: none !important; + border: 1px solid #ddd !important; + opacity: 1 !important; + transform: none !important; + animation: none !important; + } + + .metric-grid { grid-template-columns: repeat(5, 1fr) !important; } + .report-hero { border-color: #ccc; } + + h2 { page-break-after: avoid; } + section { page-break-inside: avoid; } +} diff --git a/assets/lrsomatic_report/bin/render_report.R b/assets/lrsomatic_report/bin/render_report.R new file mode 100755 index 00000000..c1293969 --- /dev/null +++ b/assets/lrsomatic_report/bin/render_report.R @@ -0,0 +1,158 @@ +#!/usr/bin/env Rscript +suppressPackageStartupMessages({ + library(optparse) + library(quarto) + library(yaml) +}) + +# Locate the repository root relative to this script. normalizePath() must be +# applied to the script *file* path (resolving a bin/ symlink to its real +# target) before taking dirname() -- doing it the other way around resolves +# the symlink's containing directory instead, which is a no-op when that +# directory isn't itself a symlink (e.g. $PREFIX/bin from the bioconda recipe). +script_file = normalizePath(sub("--file=", "", commandArgs()[grep("--file=", commandArgs())])) +repo_dir = normalizePath(file.path(dirname(script_file), "..")) + +# Source helpers (needed for detect_reference and locate_outputs below) +source(file.path(repo_dir, "R/utils.R")) +source(file.path(repo_dir, "R/references.R")) +source(file.path(repo_dir, "R/locate_outputs.R")) + +# ---- CLI argument parsing ----------------------------------------------- +option_list = list( + make_option("--sample-dir", type = "character", default = NULL, + help = "Path to the sample output directory (required)"), + make_option("--sample-id", type = "character", default = NULL, + help = "Sample identifier, e.g. SAMPLE_ID (required)"), + make_option("--reference", type = "character", default = "auto", + help = "Reference genome: t2t | hg38 | auto (default: auto)"), + make_option("--sex", type = "character", default = NULL, + help = "Biological sex: male | female | XY | XX (required)"), + make_option("--gene-panel", type = "character", default = "none", + help = "Gene panel applied on load: none | builtin name (lymphoid) | path to TSV (default: none, i.e. unfiltered)"), + make_option("--output", type = "character", default = NULL, + help = "Output HTML path (default: _report.html in current dir)"), + make_option("--title", type = "character", default = NULL, + help = "Report title (default: 'LRSomatic Report – ')") +) + +opt = parse_args(OptionParser(option_list = option_list)) + +# ---- Validate required arguments ---------------------------------------- +abort = function(...) { cat("ERROR:", ..., "\n"); quit(status = 1) } + +if (is.null(opt[["sample-dir"]])) abort("--sample-dir is required") +if (is.null(opt[["sex"]])) abort("--sex is required") + +sample_dir = normalizePath(opt[["sample-dir"]], mustWork = TRUE) +sample_id = if (!is.null(opt[["sample-id"]])) opt[["sample-id"]] else basename(sample_dir) +sex = tolower(trimws(opt[["sex"]])) +sex = switch(sex, xy = "male", xx = "female", sex) # normalise XY/XX + +gene_panel = opt[["gene-panel"]] +output = if (!is.null(opt[["output"]])) opt[["output"]] else + file.path(getwd(), paste0(sample_id, "_report.html")) +title = if (!is.null(opt[["title"]])) opt[["title"]] else + paste0("LRSomatic Report – ", sample_id) + +# ---- Load all available gene panels ---------------------------------------- +# The rendered report always ships every builtin panel so the reader can switch +# panels client-side; --gene-panel only decides which one is selected on load. +# "__all__" is the sentinel the report's JS uses for "no filter" — it must stay +# in sync with templates/sections/_gene_filter.qmd and the search hook in +# templates/per_sample.qmd. +all_panels = load_all_gene_panels(file.path(repo_dir, "assets")) +default_panel = if (is_no_gene_panel(gene_panel)) { + gene_panel = "none" + "__all__" +} else if (file.exists(file.path(repo_dir, "assets", "gene_lists", + paste0(gene_panel, ".tsv")))) { + gene_panel +} else if (file.exists(gene_panel)) { + # A user-supplied TSV: register it alongside the builtins so it can be + # selected on load (and switched away from and back to) in the report. + nm = tools::file_path_sans_ext(basename(gene_panel)) + if (nm %in% names(all_panels)) nm = paste0(nm, "-custom") + all_panels[[nm]] = load_gene_panel(gene_panel) + # Absolute, because the template resolves it again from Quarto's own working + # directory (the copied template dir), not from where this script was invoked. + gene_panel = normalizePath(gene_panel) + nm +} else { + abort(paste0("--gene-panel not found: tried builtin '", gene_panel, + "' and as a file path. Use 'none' for no filtering.")) +} + +# ---- Locate per-tool outputs --------------------------------------------- +message("Locating outputs in: ", sample_dir) +outputs = locate_outputs(sample_dir, sample_id) +message("Run mode: ", outputs$mode) +message("VEP somatic: ", ifelse(is.null(outputs$vep_somatic), "NOT FOUND", outputs$vep_somatic)) +message("Somatic VAF VCF: ", ifelse(is.null(outputs$somatic_vaf_vcf), "NOT FOUND", + paste(outputs$somatic_vaf_vcf, collapse = ", "))) +message("ASCAT segments: ", ifelse(is.null(outputs$ascat_segments), "NOT FOUND", outputs$ascat_segments)) + +# ---- Auto-detect reference ----------------------------------------------- +reference = opt[["reference"]] +if (reference == "auto") { + # Reuse already-resolved paths rather than a fixed vep/somatic/* glob + vep_file = outputs$vep_somatic + sv_file = if (is.null(vep_file)) { + hits = list.files(sample_dir, pattern = "severus_somatic\\.vcf\\.gz$", recursive = TRUE, full.names = TRUE) + if (length(hits) > 0) hits[1] else NA_character_ + } else NA_character_ + probe = if (!is.null(vep_file)) vep_file else if (!is.na(sv_file)) sv_file else NA_character_ + reference = if (!is.na(probe)) detect_reference(probe) else "t2t" + message("Auto-detected reference: ", reference) +} +reference = tolower(reference) + +# ---- Render the Quarto template ----------------------------------------- +# Copy templates/ and assets/ into a writable working directory: repo_dir's +# own templates/ may be read-only (e.g. inside a container), and Quarto +# writes intermediate files next to the .qmd during render. +work = file.path(getwd(), "._render") +unlink(work, recursive = TRUE) +dir.create(work, recursive = TRUE) +invisible(file.copy(file.path(repo_dir, "templates"), work, recursive = TRUE)) +invisible(file.copy(file.path(repo_dir, "assets"), work, recursive = TRUE)) +template = file.path(work, "templates", "per_sample.qmd") +if (!file.exists(template)) abort("Quarto template not found: ", template) + +message("Rendering report to: ", output) +quarto::quarto_render( + input = template, + output_file = basename(output), + output_format = "html", + execute_params = list( + sample_id = sample_id, + sample_dir = sample_dir, + reference = reference, + sex = sex, + gene_panel = gene_panel, + default_panel = default_panel, + all_panels = all_panels, + title = title, + repo_dir = repo_dir, + outputs = outputs + ), + quiet = FALSE +) + +# Move output if Quarto wrote it next to the template +rendered = file.path(dirname(template), basename(output)) +if (file.exists(rendered)) { + dest = normalizePath(output, mustWork = FALSE) + src = normalizePath(rendered, mustWork = FALSE) + if (src != dest) { + ok = file.copy(rendered, output, overwrite = TRUE) + if (ok) file.remove(rendered) + } +} +unlink(work, recursive = TRUE) + +if (file.exists(output)) { + message("Report written to: ", output) +} else { + abort("Rendering completed but output file not found at: ", output) +} diff --git a/assets/lrsomatic_report/templates/per_sample.qmd b/assets/lrsomatic_report/templates/per_sample.qmd new file mode 100644 index 00000000..33f058fd --- /dev/null +++ b/assets/lrsomatic_report/templates/per_sample.qmd @@ -0,0 +1,292 @@ +--- +title: "LRSomatic — per-sample genomics report" +date: today +format: + html: + self-contained: true + toc: true + toc-depth: 3 + toc-location: left + theme: [flatly, ../assets/styles/report.scss] + code-fold: true + page-layout: full +params: + sample_id: "SAMPLE" + sample_dir: "" + reference: "t2t" + sex: "female" + gene_panel: "none" # "none" | builtin panel name | path to a TSV + default_panel: "__all__" # panel selected on load; "__all__" = no filter + all_panels: NULL # named list from load_all_gene_panels() + title: "LRSomatic Report" + repo_dir: "." + outputs: NULL # named list from locate_outputs() +--- + +```{r setup, include=FALSE} +knitr::opts_chunk$set(echo = FALSE, message = FALSE, warning = FALSE) +suppressPackageStartupMessages({ + library(data.table) + library(dplyr) + library(DT) + library(htmltools) + library(ggplot2) +}) + +repo_dir = params$repo_dir +source(file.path(repo_dir, "R/utils.R")) +source(file.path(repo_dir, "R/references.R")) +source(file.path(repo_dir, "R/locate_outputs.R")) +source(file.path(repo_dir, "R/parse_smallvariants.R")) +source(file.path(repo_dir, "R/parse_severus.R")) +source(file.path(repo_dir, "R/parse_ascat.R")) +source(file.path(repo_dir, "R/parse_qc.R")) +source(file.path(repo_dir, "R/circos.R")) + +source(file.path(repo_dir, "R/sections.R")) +for (f in list.files(file.path(repo_dir, "R/sections"), pattern = "\\.R$", full.names = TRUE)) { + source(f) +} + +outputs = params$outputs +sample_id = params$sample_id +sample_dir = params$sample_dir + +# Section-module contract: each registered section owns its own file +# discovery (locate) and parsing (parse). See CLAUDE.md. +SECTION_DATA = list() +for (s in SECTIONS) { + SECTION_DATA[[s$id]] = s$parse(s$locate(sample_dir, sample_id), SECTION_DATA) +} + +# Load reference data +cytobands = load_cytobands(params$reference, file.path(repo_dir, "assets")) +chrom_lens = load_chrom_lengths(params$reference, file.path(repo_dir, "assets")) +chromosomes = chromosomes_for_sex(params$sex) +chromosomes = chromosomes[chromosomes %in% unique(cytobands$chrom)] + +# Load the gene panel selected on load (NULL when --gene-panel none, i.e. no +# filtering). Only the "Panel variants"/"Panel SVs" summary cards use it — the +# tables themselves are built unfiltered and filtered client-side. +panel_arg = params$gene_panel +panel_genes = tryCatch( + resolve_gene_panel(panel_arg, file.path(repo_dir, "assets")), + error = function(e) { message("Gene panel error: ", e$message); NULL } +) + +# Parse ASCAT +ascat_segments = parse_ascat_segments(outputs$ascat_segments) +ascat_pp = parse_ascat_purityploidy(outputs$ascat_purityploidy) + +# Parse Wakhan +wakhan_solutions = parse_wakhan_solutions(outputs$wakhan_solutions) +wakhan_cn_plots = locate_wakhan_cn_plots(outputs$wakhan_dir, wakhan_solutions) + +# Parse QC +qc_mosdepth = parse_mosdepth_summary(outputs$mosdepth_summary) +qc_mosdist = parse_mosdepth_dist(outputs$mosdepth_dist) +qc_cramino = parse_cramino(outputs$cramino) +qc_flagstat = parse_flagstat(outputs$flagstat) +samtools_stats = parse_samtools_stats(outputs$samtools_stats) +qc_normal_mosdepth = parse_mosdepth_summary(outputs$normal_mosdepth_summary) +qc_normal_cramino = parse_cramino(outputs$normal_cramino) +qc_normal_flagstat = parse_flagstat(outputs$normal_flagstat) +qc_normal_samtools_stats = parse_samtools_stats(outputs$normal_samtools_stats) + +# Parse VEP + raw callers +vep_data = parse_vep(outputs$vep_somatic) + +vaf_data = parse_caller_vcf(outputs$somatic_vaf_vcf, "somatic") + +variant_table = build_variant_table(vep_data, vaf_data, gene_panel = NULL) +tmb_info = compute_tmb(variant_table) + +# SNV data for circos (from VEP file — contains all somatic variants) +snv_circos = NULL +if (!is.null(vep_data) && nrow(vep_data) > 0) { + snv_circos = unique(vep_data[, .(chrom, pos, ref, alt)]) +} + +# Draw circos to temp file, then embed as base64 +circos_tmp = tempfile(fileext = ".svg") +tryCatch({ + draw_circos( + snv_data = snv_circos, + sv_nontrans = SECTION_DATA$sv$circos$nontrans, + sv_trans = SECTION_DATA$sv$circos$translocations, + cnv_data = ascat_segments, + cytobands = cytobands, + chrom_lengths = chrom_lens, + chromosomes = chromosomes, + output_path = circos_tmp + ) +}, error = function(e) { + message("Circos plot failed: ", e$message) + circos_tmp <<- NULL +}) + +# Summary counts +sv_table = SECTION_DATA$sv$table +n_snv = if (!is.null(vep_data)) nrow(unique(vep_data[, .(chrom, pos, ref, alt)])) else NA_integer_ +n_sv = if (!is.null(SECTION_DATA$sv)) + nrow(SECTION_DATA$sv$circos$nontrans) + nrow(SECTION_DATA$sv$circos$translocations) else NA_integer_ +# No panel selected on load → the panel cards have nothing to count, so they read +# "N/A" rather than a "0" that looks like "no panel genes hit". +have_panel = length(panel_genes) > 0 +n_panel_vars = if (!have_panel) NA_integer_ else + if (!is.null(variant_table)) + sum(variant_table$symbol %in% panel_genes, na.rm = TRUE) else 0L +# Gene-hits column differs by annotation source: "gene_hits" (VEP path) vs +# "NHL_GENE_HITS" (gene-annotated TSV fallback path) +sv_gene_col = intersect(c("gene_hits", "NHL_GENE_HITS"), names(sv_table)) +n_panel_svs = if (!have_panel) NA_integer_ else + if (!is.null(sv_table) && nrow(sv_table) > 0 && length(sv_gene_col) > 0) + sum(vapply(sv_table[[sv_gene_col[1]]], function(h) + any(trimws(unlist(strsplit(as.character(h), "[;,]+"))) %in% panel_genes), + logical(1)), na.rm = TRUE) else 0L +``` + +```{r panel-js-data, results='asis'} +all_p = if (!is.null(params$all_panels) && length(params$all_panels) > 0) + params$all_panels else list() +js_panels = paste0( + "const GENE_PANELS = {", + paste(vapply(names(all_p), function(nm) { + genes_json = paste0('"', all_p[[nm]], '"', collapse = ", ") + paste0('"', nm, '": new Set([', genes_json, '])') + }, character(1)), collapse = ",\n"), + "};\n", + 'const DEFAULT_PANEL = "', params$default_panel, '";\n' +) +cat("\n", sep = "") +``` + +{{< include sections/_header.qmd >}} + +--- + +{{< include sections/_circos.qmd >}} + +--- + +{{< include sections/_ascat.qmd >}} + +--- + +{{< include sections/_gene_filter.qmd >}} + +--- + +{{< include sections/_smallvariants.qmd >}} + +--- + +{{< include sections/_sv.qmd >}} + +--- + +{{< include sections/_whatshap.qmd >}} + +--- + +{{< include sections/_qc.qmd >}} + +--- + +*Report generated `r format(Sys.time(), "%Y-%m-%d %H:%M")` · LRSomatic report v1.1.0* + +```{=html} + +``` diff --git a/assets/lrsomatic_report/templates/sections/_ascat.qmd b/assets/lrsomatic_report/templates/sections/_ascat.qmd new file mode 100644 index 00000000..350cefc5 --- /dev/null +++ b/assets/lrsomatic_report/templates/sections/_ascat.qmd @@ -0,0 +1,109 @@ +## Copy number profile + +```{r cn-setup} +has_any_ascat_plot = !is.null(outputs$ascat_plots) && + any(vapply(outputs$ascat_plots, function(x) !is.null(x) && file.exists(x), logical(1))) +``` + +```{r cn-outer-tabset-open, results='asis'} +cat("::: {.panel-tabset}\n\n") +cat("### ASCAT\n\n") +``` + +```{r ascat-unavailable} +if (!has_any_ascat_plot) { + htmltools::div(class = "alert alert-info", + "No ASCAT plots found for this sample — ASCAT may have failed or was not run.") +} +``` + +```{r ascat-inner-tabset-open, results='asis', eval=has_any_ascat_plot} +cat("::: {.panel-tabset}\n\n") +cat("#### Fitted CN profile\n\n") +``` + +```{r ascat-profile, eval=has_any_ascat_plot} +p = embed_png(outputs$ascat_plots$profile) +if (!is.null(p)) p else htmltools::p("Plot not produced — ASCAT may have failed for this sample.") +``` + +```{r ascat-inner-rawprofile-header, results='asis', eval=has_any_ascat_plot} +cat("\n#### Raw logR + BAF\n\n") +``` + +```{r ascat-rawprofile, eval=has_any_ascat_plot} +p = embed_png(outputs$ascat_plots$aspcf) +if (!is.null(p)) p else htmltools::p("Plot not produced — ASCAT may have failed for this sample.") +``` + +```{r ascat-inner-sunrise-header, results='asis', eval=has_any_ascat_plot} +cat("\n#### Sunrise (purity × ploidy)\n\n") +``` + +```{r ascat-sunrise, eval=has_any_ascat_plot} +p = embed_png(outputs$ascat_plots$sunrise) +if (!is.null(p)) p else htmltools::p("Plot not produced — ASCAT may have failed for this sample.") +``` + +```{r ascat-inner-tabset-close, results='asis', eval=has_any_ascat_plot} +cat("\n:::\n\n") +``` + +```{r ascat-diag-open, results='asis', eval=has_any_ascat_plot} +cat('::: {.callout-note collapse="true" title="Diagnostic plots"}\n\n') +``` + +```{r ascat-diagnostics, eval=has_any_ascat_plot} +plots = list( + "Pre-GC correction" = embed_png(outputs$ascat_plots$before_gc, max_width = "700px"), + "Post-GC correction" = embed_png(outputs$ascat_plots$after_gc, max_width = "700px"), + "Tumour separation" = embed_png(outputs$ascat_plots$tumour_sep, max_width = "700px") +) +htmltools::tagList(lapply(names(plots), function(nm) { + if (is.null(plots[[nm]])) return(NULL) + htmltools::tagList(htmltools::tags$p(htmltools::tags$strong(nm)), plots[[nm]]) +})) +``` + +```{r ascat-diag-close, results='asis', eval=has_any_ascat_plot} +cat("\n:::\n\n") +``` + +```{r cn-wakhan-tab-header, results='asis'} +cat("\n### Wakhan\n\n") +``` + +```{r wakhan-status} +if (!outputs$has_wakhan) { + section_notice("Wakhan was not run for this sample.") +} else if (is.null(wakhan_solutions) && is.null(outputs$wakhan_heatmap) && length(wakhan_cn_plots) == 0) { + section_notice("Wakhan output directory found, but no recognised solutions table or plots inside it.") +} +``` + +```{r wakhan-solutions-table} +if (!is.null(wakhan_solutions)) { + DT::datatable( + wakhan_solutions, + rownames = FALSE, + options = list(dom = "t", pageLength = nrow(wakhan_solutions)) + ) +} +``` + +```{r wakhan-heatmap} +p = embed_html_iframe(outputs$wakhan_heatmap, height = "600px") +if (!is.null(p)) htmltools::tagList(htmltools::tags$p(htmltools::tags$strong("Ploidy × purity solutions")), p) +``` + +```{r wakhan-cn-plots-header, results='asis', eval=length(wakhan_cn_plots) > 0} +cat('\n

Genome copy number + breakpoints

\n\n') +``` + +```{r wakhan-cn-plots, eval=length(wakhan_cn_plots) > 0} +render_wakhan_cn_tabs(wakhan_cn_plots) +``` + +```{r cn-outer-tabset-close, results='asis'} +cat("\n:::\n\n") +``` diff --git a/assets/lrsomatic_report/templates/sections/_circos.qmd b/assets/lrsomatic_report/templates/sections/_circos.qmd new file mode 100644 index 00000000..59cb5a89 --- /dev/null +++ b/assets/lrsomatic_report/templates/sections/_circos.qmd @@ -0,0 +1,75 @@ +## Circos overview + +```{r circos-plot, fig.align='center'} +#| echo: false +if (!is.null(circos_tmp) && file.exists(circos_tmp)) { + + img_b64 = base64enc::base64encode(circos_tmp) + + # HTML legend — four groups, swatches use --circos-* CSS vars + snv_items = list( + list(label = "C→A", var = "--circos-snv-ca"), + list(label = "C→G", var = "--circos-snv-cg"), + list(label = "C→T", var = "--circos-snv-ct"), + list(label = "T→A", var = "--circos-snv-ta"), + list(label = "T→C", var = "--circos-snv-tc"), + list(label = "T→G", var = "--circos-snv-tg") + ) + sv_items = list( + list(label = "INS", var = "--circos-sv-ins"), + list(label = "DEL", var = "--circos-sv-del"), + list(label = "INV", var = "--circos-sv-inv"), + list(label = "DUP", var = "--circos-sv-dup") + ) + cnv_items = list( + list(label = "Major", var = "--circos-cnv-major"), + list(label = "Minor", var = "--circos-cnv-minor"), + list(label = "Total", var = "--circos-cnv-total") + ) + + make_items = function(items, swatch_type) { + lapply(items, function(x) { + tags$div(class = "circos-legend__item", + tags$span(class = paste0("circos-swatch--", swatch_type), + style = paste0("--_swatch: var(", x$var, ")")), + x$label + ) + }) + } + + tagList( + tags$p(class = "circos-eyebrow", + "Tracks (outer → inner): ideogram · SNV (SBS-6) · SV · copy number"), + tags$div(class = "circos-container", + tags$img(src = paste0("data:image/svg+xml;base64,", img_b64), + style = "max-width:710px; display:block; margin:auto;") + ), + tags$div(class = "circos-legend", + tags$div(class = "circos-legend__group", + tags$div(class = "circos-legend__title", "SNV type"), + make_items(snv_items, "dot") + ), + tags$div(class = "circos-legend__group", + tags$div(class = "circos-legend__title", "Structural variants"), + make_items(sv_items, "bar") + ), + tags$div(class = "circos-legend__group", + tags$div(class = "circos-legend__title", "Copy number"), + make_items(cnv_items, "bar") + ), + tags$div(class = "circos-legend__group", + tags$div(class = "circos-legend__title", "Translocation"), + tags$div(class = "circos-legend__item", + tags$span(class = "circos-swatch--arc", + style = "--_swatch: var(--circos-bnd)"), + "BND link" + ) + ) + ) + ) + +} else { + tags$div(class = "alert alert-warning", + "Circos plot could not be generated. Check that ASCAT and Severus output files are present.") +} +``` diff --git a/assets/lrsomatic_report/templates/sections/_gene_filter.qmd b/assets/lrsomatic_report/templates/sections/_gene_filter.qmd new file mode 100644 index 00000000..bb790269 --- /dev/null +++ b/assets/lrsomatic_report/templates/sections/_gene_filter.qmd @@ -0,0 +1,36 @@ +## Gene panel filter + +_Applies to both the small-variant and structural-variant tables below. Tables are +unfiltered unless a panel is selected here._ + +```{r gene-filter-ui, results='asis'} +# "__all__" (no filter) is listed first and is the default selection unless the +# render was given a --gene-panel. Every builtin panel stays selectable either way. +all_p = if (!is.null(params$all_panels) && length(params$all_panels) > 0) + params$all_panels else list() +sel = function(value) if (identical(value, params$default_panel)) " selected" else "" + +panel_opts = paste0( + '\n') +for (nm in names(all_p)) { + label = paste0(toupper(substr(nm, 1, 1)), substr(nm, 2, nchar(nm))) + panel_opts = paste0(panel_opts, + '\n') +} +panel_opts = paste0(panel_opts, + '\n') + +cat(paste0(' +
+ + + +
+ +')) +``` diff --git a/assets/lrsomatic_report/templates/sections/_header.qmd b/assets/lrsomatic_report/templates/sections/_header.qmd new file mode 100644 index 00000000..f32e5dc5 --- /dev/null +++ b/assets/lrsomatic_report/templates/sections/_header.qmd @@ -0,0 +1,63 @@ +```{r report-hero} +mode_css = if (outputs$mode == "matched") "is-matched" else "is-tumour-only" +tags$header( + class = "report-hero", + div(class = "report-hero__eyebrow", "Somatic variant profile"), + div(class = "report-hero__title", params$sample_id), + div(class = "report-hero__badges", + tags$span(class = paste("report-badge", mode_css), toupper(outputs$mode)), + tags$span(class = "report-badge is-meta", toupper(params$reference)), + tags$span(class = "report-badge is-meta", toupper(params$sex)) + ) +) +``` + +```{r summary-cards} +fmt_val = function(x, digits = 2) { + if (is.na(x)) return("N/A") + if (is.numeric(x) && !is.integer(x)) return(round(x, digits)) + as.character(x) +} + +card = function(label, value, css_class = "", subtitle = NULL) { + div( + class = paste("metric-card", css_class), + div(class = "metric-card__label", label), + div(class = "metric-card__value", value), + if (!is.null(subtitle)) + div(class = "metric-card__subtitle", subtitle) + ) +} + +# Keep acronym uppercase but let the trailing plural "s" stay lowercase +lc_plural = function(text) { + tags$span(substr(text, 1, nchar(text) - 1L), tags$span(class = "lc", "s")) +} + +div(class = "metric-grid", + card("Purity", fmt_val(ascat_pp$purity), "metric-purity"), + card("Ploidy", fmt_val(ascat_pp$ploidy), "metric-ploidy"), + card("Mean coverage", paste0(fmt_val(qc_mosdepth$mean_depth), "×"), "metric-coverage"), + card("Read N50", if (!is.na(qc_cramino$n50)) fmt_bp(qc_cramino$n50) else "N/A", "metric-n50"), + card(lc_plural("Somatic SNVs"), fmt_val(n_snv, 0), "metric-snvs"), + card(lc_plural("Somatic SVs"), fmt_val(n_sv, 0), "metric-svs"), + card("Panel variants",fmt_val(n_panel_vars, 0), "metric-panel-vars"), + card(lc_plural("Panel SVs"), fmt_val(n_panel_svs, 0), "metric-panel-svs"), + card("Coding TMB", + if (!is.na(tmb_info$tmb)) fmt_val(tmb_info$tmb) else "N/A", + "metric-tmb", + subtitle = if (!is.na(tmb_info$n_nonsyn)) + paste0("mut/Mb · ", tmb_info$n_nonsyn, " non-syn / ", tmb_info$denominator_mb, " Mb") + else if (!is.na(tmb_info$tmb)) "mut/Mb" else NULL), + card("Error rate", + if (!is.null(samtools_stats) && !is.na(samtools_stats$error_rate)) + paste0(formatC(samtools_stats$error_rate * 100, format = "f", digits = 3), "%") else "N/A", + "metric-error"), + card("Phased variants", + { w = SECTION_DATA$whatshap$all + if (!is.null(w) && !is.na(w$phased_fraction)) + paste0(fmt_val(w$phased_fraction * 100, 1), "%") else "N/A" }, + "metric-phased", + subtitle = "germline, WhatsHap") +) +``` diff --git a/assets/lrsomatic_report/templates/sections/_qc.qmd b/assets/lrsomatic_report/templates/sections/_qc.qmd new file mode 100644 index 00000000..ed5f8698 --- /dev/null +++ b/assets/lrsomatic_report/templates/sections/_qc.qmd @@ -0,0 +1,72 @@ +## QC details + +::: {.callout-note collapse="true"} +### Coverage summary + +```{r coverage-table, results='asis'} +show_normal_cov = outputs$has_normal && !is.null(qc_normal_mosdepth) && nrow(qc_normal_mosdepth$table) > 0 +if (show_normal_cov) cat("**Tumour**\n\n") +if (!is.null(qc_mosdepth$table) && nrow(qc_mosdepth$table) > 0) { + print(DT::datatable(qc_mosdepth$table, rownames = FALSE, + options = list(pageLength = 30, dom = "ft", scrollY = "300px"))) +} else { + cat("Mosdepth summary not available.\n") +} +if (show_normal_cov) { + cat("\n\n**Normal**\n\n") + print(DT::datatable(qc_normal_mosdepth$table, rownames = FALSE, + options = list(pageLength = 30, dom = "ft", scrollY = "300px"))) +} +``` +::: + +::: {.callout-note collapse="true"} +### Alignment statistics (samtools flagstat) + +```{r flagstat-table, results='asis'} +show_normal_fs = outputs$has_normal && length(qc_normal_flagstat) > 0 +if (show_normal_fs) cat("**Tumour**\n\n") +if (length(qc_flagstat) > 0) { + fs = data.frame(metric = names(qc_flagstat), value = unlist(qc_flagstat)) + print(knitr::kable(fs, row.names = FALSE)) +} else { + cat("Flagstat file not available.\n") +} +if (show_normal_fs) { + cat("\n\n**Normal**\n\n") + fs_n = data.frame(metric = names(qc_normal_flagstat), value = unlist(qc_normal_flagstat)) + print(knitr::kable(fs_n, row.names = FALSE)) +} +``` +::: + +::: {.callout-note collapse="true"} +### Read quality + alignment statistics + +```{r cramino-table, results='asis'} +make_quality_df = function(cr, st) { + rows = list() + rows[["N50 (bp)"]] = fmt_val(cr$n50, 0) + rows[["Yield (Gb)"]] = fmt_val(cr$yield_gb) + rows[["% mapped"]] = fmt_val(cr$mapped_pct) + rows[["# reads"]] = fmt_val(cr$n_reads, 0) + if (!is.null(st)) { + rows[["Avg read length (bp)"]] = fmt_val(st$avg_length, 0) + rows[["Max read length (bp)"]] = fmt_val(st$max_length, 0) + rows[["Avg base quality"]] = fmt_val(st$avg_quality) + rows[["Bases mapped (Gb)"]] = fmt_val(st$bases_mapped / 1e9) + rows[["Error rate"]] = if (!is.na(st$error_rate)) + paste0(formatC(st$error_rate * 100, format = "f", digits = 3), "%") else "N/A" + } + data.frame(metric = names(rows), value = unlist(rows), row.names = NULL) +} + +show_normal_cr = outputs$has_normal +if (show_normal_cr) cat("**Tumour**\n\n") +print(knitr::kable(make_quality_df(qc_cramino, samtools_stats), row.names = FALSE)) +if (show_normal_cr) { + cat("\n\n**Normal**\n\n") + print(knitr::kable(make_quality_df(qc_normal_cramino, qc_normal_samtools_stats), row.names = FALSE)) +} +``` +::: diff --git a/assets/lrsomatic_report/templates/sections/_smallvariants.qmd b/assets/lrsomatic_report/templates/sections/_smallvariants.qmd new file mode 100644 index 00000000..d0089ebc --- /dev/null +++ b/assets/lrsomatic_report/templates/sections/_smallvariants.qmd @@ -0,0 +1,39 @@ +## Small variants + +```{r small-variant-table} +if (!is.null(variant_table) && nrow(variant_table) > 0) { + # Format the VAF as a percentage + dt_display = copy(variant_table) + if ("vaf" %in% names(dt_display)) { + dt_display[, vaf := round(vaf * 100, 1)] + setnames(dt_display, "vaf", "VAF%") + } + + DT::datatable( + dt_display, + rownames = FALSE, + filter = "top", + elementId = "snv-table", + extensions = c("Buttons", "Scroller"), + options = list( + dom = "Bfrtip", + buttons = c("copy", "csv"), + scrollX = TRUE, + scrollY = "400px", + scroller = TRUE, + deferRender = TRUE, + pageLength = 25, + columnDefs = list(list(className = "dt-left", targets = "_all")), + initComplete = JS("function() { window.snvTableElem = this.api().table().node(); }") + ) + ) |> + DT::formatStyle( + columns = "impact", + target = "cell", + backgroundColor = DT::styleEqual( + c("HIGH", "MODERATE", "LOW", "MODIFIER"), + c("#f7e3df", "#f6edd6", "#e4efe3", "#f3f1ea") + ) + ) +} +``` diff --git a/assets/lrsomatic_report/templates/sections/_sv.qmd b/assets/lrsomatic_report/templates/sections/_sv.qmd new file mode 100644 index 00000000..129f10a4 --- /dev/null +++ b/assets/lrsomatic_report/templates/sections/_sv.qmd @@ -0,0 +1,47 @@ +## Structural variants in gene panel + +```{r sv-info} +if (is.null(sv_table) || nrow(sv_table) == 0) { + section_notice( + if (!isTRUE(SECTION_DATA$sv$annotation_found)) "SV annotation file (VEP or gene TSV) not found." + else "No somatic structural variants detected." + ) +} +``` + +```{r sv-table} +if (!is.null(sv_table) && nrow(sv_table) > 0) { + # Column casing differs by annotation source: "svtype" (VEP path) vs "SVTYPE" (TSV path) + svtype_col = intersect(c("svtype", "SVTYPE"), names(sv_table)) + + dtbl = DT::datatable( + sv_table, + rownames = FALSE, + filter = "top", + elementId = "sv-table", + extensions = c("Buttons", "Scroller"), + options = list( + dom = "Bfrtip", + buttons = c("copy", "csv"), + scrollX = TRUE, + scrollY = "350px", + scroller = TRUE, + deferRender = TRUE, + pageLength = 25, + initComplete = JS("function() { window.svTableElem = this.api().table().node(); }") + ) + ) + if (length(svtype_col) > 0) { + dtbl = dtbl |> + DT::formatStyle( + columns = svtype_col[1], + target = "cell", + backgroundColor = DT::styleEqual( + c("DEL", "DUP", "INV", "INS", "BND"), + c("#dbeafe", "#dcfce7", "#fef9c3", "#fee2e2", "#f3e8ff") + ) + ) + } + dtbl +} +``` diff --git a/assets/lrsomatic_report/templates/sections/_whatshap.qmd b/assets/lrsomatic_report/templates/sections/_whatshap.qmd new file mode 100644 index 00000000..db40b7bd --- /dev/null +++ b/assets/lrsomatic_report/templates/sections/_whatshap.qmd @@ -0,0 +1,48 @@ +## Phasing + +```{r whatshap-info} +whatshap = SECTION_DATA[["whatshap"]] +if (is.null(whatshap)) { + section_notice("WhatsHap phasing statistics not found.") +} +``` + +```{r whatshap-table} +if (!is.null(whatshap) && nrow(whatshap$per_chrom) > 0) { + show_cols = c("chromosome", "variants", "heterozygous_variants", "phased", "unphased", + "singletons", "blocks", "phased_fraction", "bp_per_block_median", + "block_n50") + show_cols = show_cols[show_cols %in% names(whatshap$per_chrom)] + + wt = whatshap$per_chrom[, ..show_cols] + if ("phased_fraction" %in% names(wt)) { + wt[, phased_fraction := round(phased_fraction * 100, 1)] + setnames(wt, "phased_fraction", "phased%") + } + + htmltools::tagList( + htmltools::p( + htmltools::tags$em( + sprintf("Germline phasing statistics, from %s.", + if (!is.na(whatshap$vcf)) whatshap$vcf else "the phased germline VCF") + ) + ), + DT::datatable( + wt, + rownames = FALSE, + filter = "top", + extensions = c("Buttons", "Scroller"), + options = list( + dom = "Bfrtip", + buttons = c("copy", "csv"), + scrollX = TRUE, + scrollY = "350px", + scroller = TRUE, + deferRender = TRUE, + pageLength = 25, + columnDefs = list(list(className = "dt-left", targets = "_all")) + ) + ) + ) +} +``` diff --git a/docs/output.md b/docs/output.md index cb1d8c38..519ffb0a 100644 --- a/docs/output.md +++ b/docs/output.md @@ -528,13 +528,22 @@ Phased variant calls produced by Longphase. Present in all samples. │ ├── {sample}_report.html ``` -| File | Description | -| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `{sample}_report.html` | Self-contained per-sample HTML report ([lrsomatic_report](https://github.com/ljwharbers/lrsomatic_report)): circos plot, small/structural variant tables, ASCAT copy-number summary, and QC. Any section whose upstream data is unavailable (e.g. a skipped tool) shows a "not available" notice instead. | +| File | Description | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `{sample}_report.html` | Self-contained per-sample HTML report ([lrsomatic_report](https://github.com/ljwharbers/lrsomatic_report)): circos plot, small/structural variant tables, copy-number summary, and QC. Any section whose upstream data is unavailable (e.g. a skipped tool) shows a "not available" notice instead. |
-This is the final step of the pipeline, run after SNV/SV calling, ASCAT, and QC. Disable it with `--skip_report`. +This is the final step of the pipeline, run after SNV/SV calling, ASCAT, WAKHAN and QC. Disable it with `--skip_report`. + +Sections: + +- **Small variants** — the VEP-annotated somatic SNVs/indels, with VAF, depth and phase set taken from the phased somatic VCF that VEP annotated. Unfiltered by default; see `--report_gene_panel` in [usage](usage.md#report-options) for panel filtering. +- **Structural variants** — SEVERUS breakpoints, annotated from the VEP SV VCF (`{sample}_SV_VEP.vcf.gz`). Skipping VEP leaves the SV table unannotated but still drawn on the circos plot. +- **Copy number** — ASCAT purity/ploidy plus its diagnostic plots, and, when WAKHAN ran, its ranked purity/ploidy solutions with the interactive per-solution genome copy-number/breakpoint plots and the ploidy/purity heatmap. +- **QC** — mosdepth, cramino and samtools statistics; for a matched tumour/normal pair both sides are shown side by side. + +The report is one self-contained file — plots and tables are embedded, so it can be copied or emailed on its own. ### `multiqc` diff --git a/docs/usage.md b/docs/usage.md index 5cb134d7..852b554e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -211,10 +211,29 @@ If you want to run with a CHM13 reference without using `--genome CHM13` (for ex #### Report Options -| Parameter | Description | -| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `--report_src` | Path to the [lrsomatic_report](https://github.com/ljwharbers/lrsomatic_report) repository (bin/, R/, templates/, assets/). Default = `${projectDir}/assets/lrsomatic_report` | -| `--report_gene_panel` | Gene panel for the report: a builtin panel name (e.g. `lymphoid`) or a path to a TSV file with a `gene` column. Default = `null` | +| Parameter | Description | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `--report_src` | Override the report tool source tree (bin/, R/, templates/, assets/). Not needed for normal runs: a copy of [lrsomatic_report](https://github.com/ljwharbers/lrsomatic_report) ships inside the pipeline. Point it at a local checkout to render with an unreleased version of the tool. Default = `${projectDir}/assets/lrsomatic_report` | +| `--report_gene_panel` | Gene panel selected when the report opens. One of `none` (no filtering), a builtin panel name (e.g. `lymphoid`), or a path to a TSV file with a `gene` column. Default = `null`, i.e. unfiltered | + +Gene panel filtering is a view, not a filter on the data: every builtin panel is embedded in +the rendered report and the reader can switch between them (or back to the unfiltered table) +in the browser. `--report_gene_panel` only decides which one is selected on load. A custom +panel is a tab-separated file with a header row containing at least a `gene` column: + +```tsv +gene panel note +TP53 mypanel Tumour suppressor +KRAS mypanel Oncogene +``` + +```bash +nextflow run IntGenomicsLab/lrsomatic \ + -profile \ + --input samplesheet.csv \ + --outdir results \ + --report_gene_panel /path/to/mypanel.tsv +``` #### WAKHAN Options diff --git a/modules/local/lrsomaticreport/environment.yml b/modules/local/lrsomaticreport/environment.yml index 641d45d2..c13bee5c 100644 --- a/modules/local/lrsomaticreport/environment.yml +++ b/modules/local/lrsomaticreport/environment.yml @@ -6,6 +6,7 @@ channels: dependencies: - "conda-forge::r-base=4.4.*" - "conda-forge::quarto=1.5.*" + - "conda-forge::r-base64enc" - "conda-forge::r-data.table" - "conda-forge::r-dplyr" - "conda-forge::r-dt" diff --git a/modules/local/lrsomaticreport/main.nf b/modules/local/lrsomaticreport/main.nf index bac01ca9..7946353e 100644 --- a/modules/local/lrsomaticreport/main.nf +++ b/modules/local/lrsomaticreport/main.nf @@ -3,13 +3,17 @@ process LRSOMATICREPORT { label 'process_medium' conda "${moduleDir}/environment.yml" - // Built via the Wave containers API from this module's environment.yml (frozen - // build). Two separate Wave builds were needed: a singularity.enabled=true - // session only produces a Singularity-native SIF artifact (blob URL below), - // while a docker.enabled=true session produces a genuine OCI image (plain tag). + // Dependencies only (R, Quarto and the tool's R packages), built via the Wave + // containers API from this module's environment.yml (frozen build). The tool + // itself is vendored at assets/lrsomatic_report -- see VENDORED.md there. + // Two separate Wave builds are needed: `wave --singularity` produces a + // Singularity-native SIF artifact (the oras:// reference), while the default + // build produces a genuine OCI image (the plain tag). Rebuild both whenever + // environment.yml changes: + // wave --conda-file modules/local/lrsomaticreport/environment.yml --freeze --await [--singularity] container "${workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container - ? 'https://community-cr-prod.seqera.io/docker/registry/v2/blobs/sha256/e0/e0d4fabb2f79dcc0d3446f1bda84507eb52ac21ebea75fd29ee5b1b26c61ee34/data' - : 'community.wave.seqera.io/library/r-base_quarto_r-data.table_r-dplyr_pruned:9d12b9297c3c4d38'}" + ? 'oras://community.wave.seqera.io/library/r-base_quarto_r-base64enc_r-data.table_pruned:dc62d809aa6fd497' + : 'community.wave.seqera.io/library/r-base_quarto_r-base64enc_r-data.table_pruned:c1049dbaf31bf178'}" input: // All per-sample report inputs are optional (path may be `[]` if the corresponding @@ -19,13 +23,14 @@ process LRSOMATICREPORT { // mosdepth/samtools default to a `${meta.id}`-only prefix (see conf/modules.config), // so for a matched T/N pair (same meta.id) the tumor and normal QC files are // identically named -- staging both lists flat would collide. - tuple val(meta), path(vep_somatic), path(severus_vcf), path(somatic_vcf), path(ascat_files), path(qc_tumor_files, stageAs: 'qc_tumor/*'), path(qc_normal_files, stageAs: 'qc_normal/*') - path(report_src) // staged lrsomatic_report repo (bin/, R/, templates/, assets/) + tuple val(meta), path(vep_somatic), path(sv_vep), path(severus_vcf), path(somatic_vcf), path(ascat_files), path(qc_tumor_files, stageAs: 'qc_tumor/*'), path(qc_normal_files, stageAs: 'qc_normal/*'), path(wakhan_files, stageAs: 'wakhan/*') + path(report_src) // lrsomatic_report source tree (bin/, R/, templates/, assets/) output: tuple val(meta), path("*_report.html"), emit: report - // No CLI version flag is provided by the tool; footer literal is "LRSomatic report v1.0" (templates/per_sample.qmd) - tuple val("${task.process}"), val('lrsomatic_report'), val("1.0"), topic: versions, emit: versions_lrsomaticreport + // No CLI version flag is provided by the tool; keep in sync with the vendored + // release recorded in assets/lrsomatic_report/VENDORED.md + tuple val("${task.process}"), val('lrsomatic_report'), val("1.1.0"), topic: versions, emit: versions_lrsomaticreport when: task.ext.when == null || task.ext.when @@ -34,63 +39,33 @@ process LRSOMATICREPORT { def args = task.ext.args ?: '' def prefix = task.ext.prefix ?: "${meta.id}" def sex = meta.sex ?: 'male' - // matched (T/N) samples publish under variants/clairs/; tumor-only samples under variants/clairsto/ - // -- this only controls the report tool's run-mode detection/labelling, see locate_outputs.R - def somatic_dir = meta.paired_data ? 'variants/clairs' : 'variants/clairsto' - def link_vep = vep_somatic ? """ - mkdir -p sample_dir/vep/somatic - ln -s "\$PWD/${vep_somatic}" "sample_dir/vep/somatic/${prefix}_SOMATIC_VEP.vcf.gz" - """ : '' - - def link_severus = severus_vcf ? """ - mkdir -p sample_dir/variants/severus/somatic_SVs - ln -s "\$PWD/${severus_vcf}" "sample_dir/variants/severus/somatic_SVs/severus_somatic.vcf.gz" + // Discovery (R/locate_outputs.R, R/sections/sv.R) is recursive under sample_dir and + // matches on the *base name*, so anything identified by a distinctive filename suffix + // can be linked flat: the VEP somatic VCF (*_SOMATIC_VEP.vcf.gz), the VEP SV VCF + // (*_SV_VEP.vcf.gz), the Severus SV VCF (severus_somatic.vcf.gz) and every ASCAT file + // (*.segments_raw.txt, *.purityploidy.txt, the diagnostic PNGs). + def flat_inputs = [vep_somatic, sv_vep, severus_vcf, ascat_files].flatten().findAll { f -> f } + def link_flat = flat_inputs ? """ + for f in ${flat_inputs.join(' ')}; do ln -s "\$PWD/\$f" "sample_dir/\$f"; done """ : '' + // The VAF/depth/phasing source is the exception: locate_outputs() looks for it at the + // literal path variants/phased/somatic_smallvariants.vcf.gz before falling back to a + // caller-specific directory, so this one file needs its canonical name and location. def link_somatic = somatic_vcf ? """ - mkdir -p sample_dir/${somatic_dir} - ln -s "\$PWD/${somatic_vcf}" "sample_dir/${somatic_dir}/somatic.vcf.gz" - """ : '' - - def ascat_file_list = ascat_files ? ascat_files.join(' ') : '' - def link_ascat = ascat_files ? """ - mkdir -p sample_dir/ascat - for f in ${ascat_file_list}; do ln -s "\$PWD/\$f" "sample_dir/ascat/\$f"; done - """ : '' - - // $f includes the 'qc_tumor/' staging subdirectory (see stageAs above); the - // destination link name uses just the basename. - def qc_tumor_file_list = qc_tumor_files ? qc_tumor_files.join(' ') : '' - def link_qc_tumor = qc_tumor_files ? """ - mkdir -p sample_dir/qc/tumor/mosdepth sample_dir/qc/tumor/cramino_aln sample_dir/qc/tumor/samtools - for f in ${qc_tumor_file_list}; do - fname=\$(basename "\$f") - case "\$fname" in - *.mosdepth.*.txt) ln -s "\$PWD/\$f" "sample_dir/qc/tumor/mosdepth/\$fname" ;; - *_cramino.txt) ln -s "\$PWD/\$f" "sample_dir/qc/tumor/cramino_aln/\$fname" ;; - *.flagstat|*.stats) ln -s "\$PWD/\$f" "sample_dir/qc/tumor/samtools/\$fname" ;; - esac - done - """ : '' - - def qc_normal_file_list = qc_normal_files ? qc_normal_files.join(' ') : '' - def link_qc_normal = qc_normal_files ? """ - mkdir -p sample_dir/qc/normal/mosdepth sample_dir/qc/normal/cramino_aln sample_dir/qc/normal/samtools - for f in ${qc_normal_file_list}; do - fname=\$(basename "\$f") - case "\$fname" in - *.mosdepth.*.txt) ln -s "\$PWD/\$f" "sample_dir/qc/normal/mosdepth/\$fname" ;; - *_cramino.txt) ln -s "\$PWD/\$f" "sample_dir/qc/normal/cramino_aln/\$fname" ;; - *.flagstat|*.stats) ln -s "\$PWD/\$f" "sample_dir/qc/normal/samtools/\$fname" ;; - esac - done + mkdir -p sample_dir/variants/phased + ln -s "\$PWD/${somatic_vcf}" sample_dir/variants/phased/somatic_smallvariants.vcf.gz """ : '' """ - # Quarto/Deno write a cache dir under \$HOME; point it at the task work dir - # (always writable) rather than relying on the container's \$HOME being bound. + # Quarto/Deno write a cache dir under \$HOME and a session dir under \$TMPDIR; + # point both at the task work dir, which is always writable and always bound into + # the container. Relying on the container's own \$HOME and /tmp fails on clusters + # that mount them read-only ("Read-only file system (os error 30): tmpdir"). export HOME=\$PWD + export TMPDIR=\$PWD/tmp TMP=\$PWD/tmp TEMP=\$PWD/tmp + mkdir -p "\$TMPDIR" # The Wave/conda-built container doesn't auto-source conda's activation hooks # (e.g. quarto needs QUARTO_SHARE_PATH); source them if present. Some hooks @@ -101,20 +76,31 @@ process LRSOMATICREPORT { done mkdir -p sample_dir - ${link_vep} - ${link_severus} + ${link_flat} ${link_somatic} - ${link_ascat} - ${link_qc_tumor} - ${link_qc_normal} - - # Quarto renders in-place next to the .qmd it's given (render_report.R's own - # post-render step relies on this). report_src is a single fixed path shared - # by every sample's task, so dereferencing it into a private, task-local copy - # avoids concurrent per-sample renders colliding on the same physical directory. - cp -rL "${report_src}" report_src_local - Rscript report_src_local/bin/render_report.R \\ + # QC is split tumor/normal by path: locate_outputs() takes the first suffix match + # outside any /normal/ component as tumor, and the first one inside it as normal. + # Link file by file rather than symlinking the staging directory itself -- R's + # list.files(recursive = TRUE) does not descend into symlinked directories. + if [ -d qc_tumor ]; then + mkdir -p sample_dir/qc/tumor + for f in qc_tumor/*; do ln -s "\$PWD/\$f" "sample_dir/qc/tumor/\$(basename "\$f")"; done + fi + if [ -d qc_normal ]; then + mkdir -p sample_dir/qc/normal + for f in qc_normal/*; do ln -s "\$PWD/\$f" "sample_dir/qc/normal/\$(basename "\$f")"; done + fi + + # Wakhan is addressed by fixed path, not by suffix: sample_dir/wakhan must hold + # solutions_ranks.tsv, *heatmap_ploidy_purity.html and the per-solution + # solution_/ directories (R/parse_ascat.R:locate_wakhan_cn_plots). + if [ -d wakhan ]; then + mkdir -p sample_dir/wakhan + for f in wakhan/*; do ln -s "\$PWD/\$f" "sample_dir/wakhan/\$(basename "\$f")"; done + fi + + Rscript ${report_src}/bin/render_report.R \\ --sample-dir sample_dir \\ --sample-id ${prefix} \\ --sex ${sex} \\ diff --git a/modules/local/lrsomaticreport/meta.yml b/modules/local/lrsomaticreport/meta.yml index 017df4b2..668c8c40 100644 --- a/modules/local/lrsomaticreport/meta.yml +++ b/modules/local/lrsomaticreport/meta.yml @@ -13,8 +13,8 @@ tools: documentation: "https://github.com/ljwharbers/lrsomatic_report/blob/main/README.md" tool_dev_url: "https://github.com/ljwharbers/lrsomatic_report" doi: "" - licence: null - identifier: null + licence: ["MIT"] + identifier: "" input: - - meta: @@ -24,14 +24,18 @@ input: - vep_somatic: type: file description: VEP-annotated somatic small-variant VCF (SOMATIC_VEP output), or `[]` if VEP was skipped - pattern: "*.vcf.gz" + pattern: "*_SOMATIC_VEP.vcf.gz" + - sv_vep: + type: file + description: VEP-annotated structural-variant VCF (SV_VEP output); the report's primary SV annotation source, or `[]` if VEP was skipped + pattern: "*_SV_VEP.vcf.gz" - severus_vcf: type: file - description: Severus somatic structural-variant VCF, or `[]` if not available - pattern: "*.vcf.gz" + description: Severus somatic structural-variant VCF (raw breakpoints, used for the circos tracks), or `[]` if not available + pattern: "severus_somatic.vcf.gz" - somatic_vcf: type: file - description: Final somatic small-variant VCF (ClairS/ClairS-TO/DeepSomatic or consensus), or `[]` if not available + description: Phased somatic small-variant VCF (the VCF that VEP annotated); source of the VAF, depth and phase-set columns, or `[]` if not available pattern: "*.vcf.gz" - ascat_files: type: file @@ -42,9 +46,12 @@ input: - qc_normal_files: type: file description: Collected normal-sample QC files (matched mode only), or `[]` for tumor-only samples or if QC was skipped + - wakhan_files: + type: file + description: Collected Wakhan outputs (solutions_ranks.tsv, the ploidy/purity heatmap HTML and the per-solution `solution_/` directories), or `[]` if Wakhan was skipped - - report_src: type: directory - description: Staged lrsomatic_report repository (bin/, R/, templates/, assets/), shared across all samples + description: lrsomatic_report source tree (bin/, R/, templates/, assets/), shared across all samples; defaults to the vendored copy at `assets/lrsomatic_report` output: report: @@ -63,11 +70,11 @@ output: - "lrsomatic_report": type: string description: The name of the tool - - "1.0": + - "1.1.0": type: string description: | - Manually pinned version (the tool has no CLI version flag; the report - footer literal is "LRSomatic report v1.0", templates/per_sample.qmd) + Manually pinned version (the tool has no CLI version flag); matches the + vendored release recorded in assets/lrsomatic_report/VENDORED.md topics: versions: @@ -77,7 +84,7 @@ topics: - lrsomatic_report: type: string description: The name of the tool - - "1.0": + - "1.1.0": type: string description: Manually pinned version (tool has no CLI version flag) diff --git a/modules/local/lrsomaticreport/tests/main.nf.test b/modules/local/lrsomaticreport/tests/main.nf.test index fc9c7f7c..876bf43a 100644 --- a/modules/local/lrsomaticreport/tests/main.nf.test +++ b/modules/local/lrsomaticreport/tests/main.nf.test @@ -18,11 +18,13 @@ nextflow_process { input[0] = [ [ id:'test', paired_data: null, sex: 'male' ], [], // vep_somatic + [], // sv_vep [], // severus_vcf [], // somatic_vcf [], // ascat_files [], // qc_tumor_files - [] // qc_normal_files + [], // qc_normal_files + [] // wakhan_files ] input[1] = file("${projectDir}/assets/lrsomatic_report", checkIfExists: true) """ @@ -38,19 +40,25 @@ nextflow_process { } - test("no optional inputs - real render") { + // Renders for real (no -stub): catches a broken container, a missing or incomplete + // vendored tool tree, and any CLI drift between the module and render_report.R -- + // none of which a stub run can see. The VEP somatic VCF is supplied so the + // small-variant table is actually built rather than short-circuited as "not available". + test("vep somatic vcf - real render") { when { process { """ input[0] = [ [ id:'test', paired_data: null, sex: 'male' ], - [], // vep_somatic + file("${projectDir}/modules/local/lrsomaticreport/tests/test_SOMATIC_VEP.vcf.gz", checkIfExists: true), + [], // sv_vep [], // severus_vcf [], // somatic_vcf [], // ascat_files [], // qc_tumor_files - [] // qc_normal_files + [], // qc_normal_files + [] // wakhan_files ] input[1] = file("${projectDir}/assets/lrsomatic_report", checkIfExists: true) """ @@ -59,7 +67,14 @@ nextflow_process { then { assert process.success - assert process.out.report.get(0).get(1).endsWith("_report.html") + assertAll( + { assert process.out.report.get(0).get(1).endsWith("test_report.html") }, + // The rendered HTML is not snapshotted (Quarto embeds timestamps and + // per-render element ids); assert on content that must be there instead. + { assert path(process.out.report.get(0).get(1)).readLines().size() > 0 }, + { assert path(process.out.report.get(0).get(1)).text.contains("TP53") }, + { assert snapshot(process.out.versions_lrsomaticreport).match("versions") } + ) } } diff --git a/modules/local/lrsomaticreport/tests/main.nf.test.snap b/modules/local/lrsomaticreport/tests/main.nf.test.snap index bc2974ad..4ffa7db9 100644 --- a/modules/local/lrsomaticreport/tests/main.nf.test.snap +++ b/modules/local/lrsomaticreport/tests/main.nf.test.snap @@ -16,7 +16,7 @@ [ "LRSOMATICREPORT", "lrsomatic_report", - "1.0" + "1.1.0" ] ], "report": [ @@ -33,12 +33,28 @@ [ "LRSOMATICREPORT", "lrsomatic_report", - "1.0" + "1.1.0" ] ] } ], - "timestamp": "2026-07-15T10:08:15.629341585", + "timestamp": "2026-08-12T11:17:36.821182267", + "meta": { + "nf-test": "0.9.4", + "nextflow": "26.04.3" + } + }, + "versions": { + "content": [ + [ + [ + "LRSOMATICREPORT", + "lrsomatic_report", + "1.1.0" + ] + ] + ], + "timestamp": "2026-08-12T11:19:49.036321128", "meta": { "nf-test": "0.9.4", "nextflow": "26.04.3" diff --git a/modules/local/lrsomaticreport/tests/test_SOMATIC_VEP.vcf.gz b/modules/local/lrsomaticreport/tests/test_SOMATIC_VEP.vcf.gz new file mode 100644 index 0000000000000000000000000000000000000000..0f7d35181cea5cd523e37240fac41f6e913408f4 GIT binary patch literal 620 zcmV-y0+an8iwFP!0000019g&7Z<{a_g`eGDff{LArHEm`K%z0pO&~!^0u5fIK1C$B z1c?}CoNAH$<7YtC)^z2~+%WcRwUT=Jqb?J@tn3tUN)kvnSYO~N>lEF z@>XlIwx#)fr-?RU+$(13(|B_^A1L?rF}XI>PX#Yo@qWszW-a}7q3lStjIml|-Tzj5 z;q_SyRPB7Iv|W(sTG&%gHRVeyD*qA6Xr160k?HgLFWhip%` zr;Wa-uF%A7BY6u=I8h@l_sc9!cgS`v$AX5;6}ftfWqoPC8V}vfLw2aH`#d{mg&O/ path, so the directory has to survive staging. + tuple val(meta), path("solution_*", type: 'dir') , emit: solution_dirs, optional: true // WARN: Manually update version information as tool does not provide on CLI tuple val("${task.process}"), val('wakhan'), val("0.4.3"), topic: versions, emit: versions_wakhan diff --git a/nextflow_schema.json b/nextflow_schema.json index 75fa71fc..1df018ad 100644 --- a/nextflow_schema.json +++ b/nextflow_schema.json @@ -311,11 +311,11 @@ "properties": { "report_src": { "type": "string", - "description": "Path to the lrsomatic_report repository (bin/, R/, templates/, assets/)" + "description": "Override the report tool source tree (bin/, R/, templates/, assets/). Defaults to the copy vendored in this repository; point it at a local checkout of lrsomatic_report to render with an unreleased version of the tool." }, "report_gene_panel": { "type": "string", - "description": "Gene panel for the report: a builtin panel name (e.g. 'lymphoid') or path to a TSV with a 'gene' column" + "description": "Gene panel selected when the report opens: 'none' for no filtering, a builtin panel name (e.g. 'lymphoid'), or a path to a TSV with a 'gene' column. Every builtin panel is always embedded in the report and can be switched to in the browser; this only sets the initial selection. Default (unset) is unfiltered." } } }, diff --git a/tests/clair_only.nf.test.snap b/tests/clair_only.nf.test.snap index 50bca67b..b0691182 100644 --- a/tests/clair_only.nf.test.snap +++ b/tests/clair_only.nf.test.snap @@ -46,6 +46,9 @@ "LONGPHASE_PHASE_SOMATIC": { "longphase": "2.0.1" }, + "LRSOMATICREPORT": { + "lrsomatic_report": "1.1.0" + }, "METAEXTRACT": { "samtools": 1.21 }, @@ -271,6 +274,8 @@ "sample1/qc/whatshap_stats/sample1_whatshap_stats.gtf", "sample1/qc/whatshap_stats/sample1_whatshap_stats.log", "sample1/qc/whatshap_stats/sample1_whatshap_stats.tsv", + "sample1/report", + "sample1/report/sample1_report.html", "sample1/variants", "sample1/variants/clair3", "sample1/variants/clair3/merge_output.vcf.gz", @@ -386,6 +391,8 @@ "sample2/qc/whatshap_stats/sample2_whatshap_stats.gtf", "sample2/qc/whatshap_stats/sample2_whatshap_stats.log", "sample2/qc/whatshap_stats/sample2_whatshap_stats.tsv", + "sample2/report", + "sample2/report/sample2_report.html", "sample2/variants", "sample2/variants/clair3", "sample2/variants/clair3/merge_output.vcf.gz", @@ -468,6 +475,8 @@ "sample3/qc/whatshap_stats/sample3_whatshap_stats.gtf", "sample3/qc/whatshap_stats/sample3_whatshap_stats.log", "sample3/qc/whatshap_stats/sample3_whatshap_stats.tsv", + "sample3/report", + "sample3/report/sample3_report.html", "sample3/variants", "sample3/variants/clairsto", "sample3/variants/clairsto/germline.vcf.gz", @@ -561,6 +570,8 @@ "sample4/qc/whatshap_stats/sample4_whatshap_stats.gtf", "sample4/qc/whatshap_stats/sample4_whatshap_stats.log", "sample4/qc/whatshap_stats/sample4_whatshap_stats.tsv", + "sample4/report", + "sample4/report/sample4_report.html", "sample4/variants", "sample4/variants/clairsto", "sample4/variants/clairsto/germline.vcf.gz", @@ -644,6 +655,8 @@ "sample5/qc/whatshap_stats/sample5_whatshap_stats.gtf", "sample5/qc/whatshap_stats/sample5_whatshap_stats.log", "sample5/qc/whatshap_stats/sample5_whatshap_stats.tsv", + "sample5/report", + "sample5/report/sample5_report.html", "sample5/variants", "sample5/variants/clairsto", "sample5/variants/clairsto/germline.vcf.gz", diff --git a/tests/consensus.nf.test.snap b/tests/consensus.nf.test.snap index d4a6c508..be6cfe72 100644 --- a/tests/consensus.nf.test.snap +++ b/tests/consensus.nf.test.snap @@ -79,6 +79,9 @@ "LONGPHASE_PHASE_SOMATIC": { "longphase": "2.0.1" }, + "LRSOMATICREPORT": { + "lrsomatic_report": "1.1.0" + }, "METAEXTRACT": { "samtools": 1.21 }, @@ -301,6 +304,8 @@ "sample1/qc/whatshap_stats/sample1_whatshap_stats.gtf", "sample1/qc/whatshap_stats/sample1_whatshap_stats.log", "sample1/qc/whatshap_stats/sample1_whatshap_stats.tsv", + "sample1/report", + "sample1/report/sample1_report.html", "sample1/variants", "sample1/variants/clair3", "sample1/variants/clair3/merge_output.vcf.gz", @@ -422,6 +427,8 @@ "sample2/qc/whatshap_stats/sample2_whatshap_stats.gtf", "sample2/qc/whatshap_stats/sample2_whatshap_stats.log", "sample2/qc/whatshap_stats/sample2_whatshap_stats.tsv", + "sample2/report", + "sample2/report/sample2_report.html", "sample2/variants", "sample2/variants/clair3", "sample2/variants/clair3/merge_output.vcf.gz", @@ -510,6 +517,8 @@ "sample3/qc/whatshap_stats/sample3_whatshap_stats.gtf", "sample3/qc/whatshap_stats/sample3_whatshap_stats.log", "sample3/qc/whatshap_stats/sample3_whatshap_stats.tsv", + "sample3/report", + "sample3/report/sample3_report.html", "sample3/variants", "sample3/variants/clairsto", "sample3/variants/clairsto/germline.vcf.gz", diff --git a/tests/deep_only.nf.test.snap b/tests/deep_only.nf.test.snap index e1087305..1f09e6b7 100644 --- a/tests/deep_only.nf.test.snap +++ b/tests/deep_only.nf.test.snap @@ -55,6 +55,9 @@ "LONGPHASE_PHASE_SOMATIC": { "longphase": "2.0.1" }, + "LRSOMATICREPORT": { + "lrsomatic_report": "1.1.0" + }, "METAEXTRACT": { "samtools": 1.21 }, @@ -271,6 +274,8 @@ "sample1/qc/whatshap_stats/sample1_whatshap_stats.gtf", "sample1/qc/whatshap_stats/sample1_whatshap_stats.log", "sample1/qc/whatshap_stats/sample1_whatshap_stats.tsv", + "sample1/report", + "sample1/report/sample1_report.html", "sample1/variants", "sample1/variants/deepsomatic", "sample1/variants/deepsomatic/sample1_somatic.vcf.gz", @@ -384,6 +389,8 @@ "sample2/qc/whatshap_stats/sample2_whatshap_stats.gtf", "sample2/qc/whatshap_stats/sample2_whatshap_stats.log", "sample2/qc/whatshap_stats/sample2_whatshap_stats.tsv", + "sample2/report", + "sample2/report/sample2_report.html", "sample2/variants", "sample2/variants/deepsomatic", "sample2/variants/deepsomatic/sample2_somatic.vcf.gz", @@ -464,6 +471,8 @@ "sample3/qc/whatshap_stats/sample3_whatshap_stats.gtf", "sample3/qc/whatshap_stats/sample3_whatshap_stats.log", "sample3/qc/whatshap_stats/sample3_whatshap_stats.tsv", + "sample3/report", + "sample3/report/sample3_report.html", "sample3/variants", "sample3/variants/deepsomatic", "sample3/variants/deepsomatic/sample3_somatic.vcf.gz", diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap index e2c044de..75df5711 100644 --- a/tests/default.nf.test.snap +++ b/tests/default.nf.test.snap @@ -47,7 +47,7 @@ "longphase": "2.0.1" }, "LRSOMATICREPORT": { - "lrsomatic_report": 1.0 + "lrsomatic_report": "1.1.0" }, "METAEXTRACT": { "samtools": 1.21 diff --git a/tests/union.nf.test.snap b/tests/union.nf.test.snap index df520c8d..0a0be4fc 100644 --- a/tests/union.nf.test.snap +++ b/tests/union.nf.test.snap @@ -76,6 +76,9 @@ "LONGPHASE_PHASE_SOMATIC": { "longphase": "2.0.1" }, + "LRSOMATICREPORT": { + "lrsomatic_report": "1.1.0" + }, "METAEXTRACT": { "samtools": 1.21 }, @@ -301,6 +304,8 @@ "sample1/qc/whatshap_stats/sample1_whatshap_stats.gtf", "sample1/qc/whatshap_stats/sample1_whatshap_stats.log", "sample1/qc/whatshap_stats/sample1_whatshap_stats.tsv", + "sample1/report", + "sample1/report/sample1_report.html", "sample1/variants", "sample1/variants/clair3", "sample1/variants/clair3/merge_output.vcf.gz", @@ -422,6 +427,8 @@ "sample2/qc/whatshap_stats/sample2_whatshap_stats.gtf", "sample2/qc/whatshap_stats/sample2_whatshap_stats.log", "sample2/qc/whatshap_stats/sample2_whatshap_stats.tsv", + "sample2/report", + "sample2/report/sample2_report.html", "sample2/variants", "sample2/variants/clair3", "sample2/variants/clair3/merge_output.vcf.gz", @@ -510,6 +517,8 @@ "sample3/qc/whatshap_stats/sample3_whatshap_stats.gtf", "sample3/qc/whatshap_stats/sample3_whatshap_stats.log", "sample3/qc/whatshap_stats/sample3_whatshap_stats.tsv", + "sample3/report", + "sample3/report/sample3_report.html", "sample3/variants", "sample3/variants/clairsto", "sample3/variants/clairsto/germline.vcf.gz", diff --git a/workflows/lrsomatic.nf b/workflows/lrsomatic.nf index 62d7b2a1..a4db8a64 100644 --- a/workflows/lrsomatic.nf +++ b/workflows/lrsomatic.nf @@ -813,6 +813,8 @@ workflow LRSOMATIC { .set { sv_vep } // sv_vep: [meta, severus_all_vcf, []] -- all SVs ready for VEP annotation + ch_sv_vep_vcf = channel.empty() + if(!params.skip_vep) { // // MODULE: SV_VEP (ENSEMBLVEP_VEP alias; label: process_medium) @@ -830,6 +832,8 @@ workflow LRSOMATIC { vep_custom, vep_custom_tbi ) + + ch_sv_vep_vcf = SV_VEP.out.vcf } @@ -967,6 +971,8 @@ workflow LRSOMATIC { // Output: WAKHAN assembly reports (written to outdir) // + ch_wakhan_files = channel.empty() + if (!params.skip_wakhan) { // Attach SEVERUS SV VCF to the severus_input channel (dropping the phased TBI) @@ -984,29 +990,37 @@ workflow LRSOMATIC { ch_fasta, file(params.centromere_bed) ) + + // The subset of WAKHAN's outputs the report renders: the ranked purity/ploidy + // solutions table, the ploidy/purity heatmap and each solution's directory + // (which holds that solution's genome copy-number/breakpoints plot). + ch_wakhan_files = WAKHAN.out.solutions_ranks + .mix(WAKHAN.out.heatmap_html, WAKHAN.out.solution_dirs) + .groupTuple() + .map { meta, files -> [meta, files.flatten()] } // solution_dirs contributes a list + // ch_wakhan_files: [meta, [file_or_dir, ...]] } // // MODULE: LRSOMATICREPORT (label: process_medium) // Final step: render a per-sample HTML report from the key analytical outputs - // (VEP-annotated somatic SNVs, Severus somatic SVs, ASCAT copy number, QC). + // (VEP-annotated somatic SNVs, Severus somatic SVs with their VEP annotation, + // ASCAT and Wakhan copy number, QC). // Every input is optional -- the report tool shows a "not available" notice for // any section whose file is missing, so joins below use `remainder: true` and a // plain String (tumor sample id) as the join key throughout, to avoid relying on // exact Groovy-map equality across differently-stripped meta values. // - // Known simplification: `ch_somatic_vcf` is the FINAL somatic small-variant VCF - // (single caller, or consensus if multiple somatic callers were combined). It is - // staged under variants/clairs/ (matched) or variants/clairsto/ (tumor-only) purely - // to drive the report tool's run-mode detection and its per-caller VAF column; if a - // consensus of multiple callers was used, that VAF column will not reflect a single - // real caller. The VEP-based variant table (the primary source) is unaffected. + // The VAF/depth/phasing columns come from the *phased* somatic VCF -- the same file + // SOMATIC_VEP annotated -- so the two halves of the small-variant table are guaranteed + // to describe the same variant set. Run mode (matched vs tumour-only) is derived by the + // report tool from whether normal-side QC is present, not declared here. // if (!params.skip_report) { // Canonical per-report-row identity: keyed on the tumor sample's own id (also - // used by severus_input/ascat_ch/ch_somatic_vcf), carrying the definitive meta + // used by severus_input/ascat_ch/wakhan_input), carrying the definitive meta // to attach to the final module call. severus_input .map { meta, _tumor_bam, _tumor_bai, _normal_bam, _normal_bai, _phased_vcf, _phased_tbi -> @@ -1019,11 +1033,15 @@ workflow LRSOMATIC { .map { meta, vcf -> [meta.id, vcf] } .set { report_vep_ch } + ch_sv_vep_vcf + .map { meta, vcf -> [meta.id, vcf] } + .set { report_sv_vep_ch } + SEVERUS.out.somatic_vcf .map { meta, vcf -> [meta.id, vcf] } .set { report_severus_ch } - ch_somatic_vcf + PHASING_HAPLOTYPING.out.phased_somatic_vcf .map { meta, vcf, _tbi -> [meta.id, vcf] } .set { report_somatic_ch } @@ -1031,6 +1049,10 @@ workflow LRSOMATIC { .map { meta, files -> [meta.id, files] } .set { report_ascat_ch } + ch_wakhan_files + .map { meta, files -> [meta.id, files] } + .set { report_wakhan_ch } + // Tumor-side QC: keyed by the sample's own id, which for tumor rows is already the report id ch_mosdepth_summary .mix(ch_mosdepth_global, ch_cramino_post_txt, ch_bam_stats, ch_bam_flagstat) @@ -1051,25 +1073,29 @@ workflow LRSOMATIC { report_id_meta .join(report_vep_ch, remainder: true) + .join(report_sv_vep_ch, remainder: true) .join(report_severus_ch, remainder: true) .join(report_somatic_ch, remainder: true) .join(report_ascat_ch, remainder: true) .join(report_qc_tumor_ch, remainder: true) .join(report_qc_normal_ch, remainder: true) - .filter { _id, meta, _vep, _severus, _somatic, _ascat, _qc_t, _qc_n -> meta != null } - .map { _id, meta, vep, severus, somatic, ascat, qc_t, qc_n -> + .join(report_wakhan_ch, remainder: true) + .filter { _id, meta, _vep, _sv_vep, _severus, _somatic, _ascat, _qc_t, _qc_n, _wakhan -> meta != null } + .map { _id, meta, vep, sv_vep_vcf, severus, somatic, ascat, qc_t, qc_n, wakhan -> return [ meta, - vep ?: [], - severus ?: [], - somatic ?: [], - ascat ?: [], - qc_t ?: [], - qc_n ?: [] + vep ?: [], + sv_vep_vcf ?: [], + severus ?: [], + somatic ?: [], + ascat ?: [], + qc_t ?: [], + qc_n ?: [], + wakhan ?: [] ] } .set { report_input_ch } - // report_input_ch: [meta, vep_somatic, severus_vcf, somatic_vcf, ascat_files, qc_tumor_files, qc_normal_files] + // report_input_ch: [meta, vep_somatic, sv_vep, severus_vcf, somatic_vcf, ascat_files, qc_tumor_files, qc_normal_files, wakhan_files] LRSOMATICREPORT ( report_input_ch, From 820bbf74b0a1b8fc270cb8035344b603b88701cb Mon Sep 17 00:00:00 2001 From: Luuk Harbers Date: Wed, 12 Aug 2026 11:34:10 +0200 Subject: [PATCH 06/17] fix: exempt .gitattributes from the nf-core template-unchanged check The vendored tool tree needs a `linguist-vendored` entry so GitHub does not count 565 KB of upstream R and SCSS as pipeline source, but .gitattributes is template-managed and any edit fails files_unchanged. Opt it out the way the repo already opts out CODE_OF_CONDUCT.md and the workflow files. Co-Authored-By: Claude Opus 5 --- .nf-core.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.nf-core.yml b/.nf-core.yml index 8c2b79f6..6340e359 100644 --- a/.nf-core.yml +++ b/.nf-core.yml @@ -9,6 +9,10 @@ lint: - .github/workflows/awsfulltest.yml - .github/CONTRIBUTING.md files_unchanged: + # Carries `assets/lrsomatic_report/** linguist-vendored` so the vendored tool + # source (see assets/lrsomatic_report/VENDORED.md) is excluded from GitHub's + # language statistics -- 338 KB of it is a single base64-font SCSS file. + - .gitattributes - CODE_OF_CONDUCT.md - assets/nf-core-lrsomatic_logo_light.png - docs/images/nf-core-lrsomatic_logo_light.png From d729fcf43775e7be32b8928c45051641a4ac0bed Mon Sep 17 00:00:00 2001 From: ljwharbers Date: Fri, 14 Aug 2026 14:35:13 +0200 Subject: [PATCH 07/17] fix: stage the report gene panel and stop clobbering CONDA_PREFIX Addresses the two review comments still live on PR #176. --report_gene_panel is documented as accepting a path to a TSV, but the raw param was interpolated straight into the command line: the file was never staged, so it was not bound into the docker/singularity container and render_report.R aborted with "--gene-panel not found". Add an optional `gene_panel` path input, wired from the workflow only when the param resolves to an existing file (a builtin panel name still travels via ext.args alone), and have conf/modules.config pass the quoted *base* name -- unchanged for a builtin, and the staged name for a TSV. Quoting also fixes panel paths containing spaces. The activation-hook loop hard-coded CONDA_PREFIX=/opt/conda, which is right for the Wave image but wrong under -profile conda, where it already points at the task's own env. Only fall back to /opt/conda when unset, and glob the hooks from $CONDA_PREFIX. Also pass checkIfExists to the report_src lookup so a bad --report_src fails fast, and cover the panel path with a real (non-stub) nf-test that renders with a user-supplied TSV -- it only passes if the file is genuinely staged into the container. Co-Authored-By: Claude Opus 5 --- .gitignore | 2 +- conf/modules.config | 7 +++- modules/local/lrsomaticreport/main.nf | 12 +++++- modules/local/lrsomaticreport/meta.yml | 4 ++ .../lrsomaticreport/tests/gene_panel.config | 11 +++++ .../local/lrsomaticreport/tests/main.nf.test | 42 +++++++++++++++++++ .../lrsomaticreport/tests/test_panel.tsv | 3 ++ workflows/lrsomatic.nf | 10 ++++- 8 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 modules/local/lrsomaticreport/tests/gene_panel.config create mode 100644 modules/local/lrsomaticreport/tests/test_panel.tsv diff --git a/.gitignore b/.gitignore index fb6eca4c..00ba0481 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,4 @@ null/ .nf-test/ .nf-test.log CLAUDE.local.md -.claude/ \ No newline at end of file +.claude/ diff --git a/conf/modules.config b/conf/modules.config index d4b90fad..286a8e3d 100644 --- a/conf/modules.config +++ b/conf/modules.config @@ -560,7 +560,12 @@ process { withName : '.*:LRSOMATICREPORT' { ext.prefix = { "${meta.id}" } - ext.args = { params.report_gene_panel ? "--gene-panel ${params.report_gene_panel}" : '' } + // --report_gene_panel is either a builtin panel name (or the `none` sentinel) or a + // path to a TSV. Pass the base name in both cases: a builtin name is unchanged by + // `.name`, and a TSV is staged into the task dir by the module's `gene_panel` input + // under exactly that name, so the tool resolves it relative to its working dir. + // Quoted so panel files living under a path with spaces stay a single argument. + ext.args = { params.report_gene_panel ? "--gene-panel '${file(params.report_gene_panel).name}'" : '' } publishDir = [ path: { "${params.outdir}/${meta.id}/report" }, mode: params.publish_dir_mode, diff --git a/modules/local/lrsomaticreport/main.nf b/modules/local/lrsomaticreport/main.nf index 7946353e..1794e50f 100644 --- a/modules/local/lrsomaticreport/main.nf +++ b/modules/local/lrsomaticreport/main.nf @@ -25,6 +25,12 @@ process LRSOMATICREPORT { // identically named -- staging both lists flat would collide. tuple val(meta), path(vep_somatic), path(sv_vep), path(severus_vcf), path(somatic_vcf), path(ascat_files), path(qc_tumor_files, stageAs: 'qc_tumor/*'), path(qc_normal_files, stageAs: 'qc_normal/*'), path(wakhan_files, stageAs: 'wakhan/*') path(report_src) // lrsomatic_report source tree (bin/, R/, templates/, assets/) + // Optional user-supplied gene panel TSV, staged so it is bound into the container; + // `[]` when --report_gene_panel names a builtin panel (or is unset), which the tool + // resolves from report_src/assets/gene_lists instead. Either way the `--gene-panel` + // argument itself is built in conf/modules.config, which passes the *base* name -- + // the staged name of this file when it is one. + path(gene_panel) output: tuple val(meta), path("*_report.html"), emit: report @@ -70,8 +76,10 @@ process LRSOMATICREPORT { # The Wave/conda-built container doesn't auto-source conda's activation hooks # (e.g. quarto needs QUARTO_SHARE_PATH); source them if present. Some hooks # (e.g. gcc_linux-64) reference \$CONDA_PREFIX under `set -u`, so export it first. - export CONDA_PREFIX=/opt/conda - for f in /opt/conda/etc/conda/activate.d/*.sh; do + # Under `-profile conda` CONDA_PREFIX already points at the task's own env, so + # only fall back to the container's /opt/conda when it is unset. + export CONDA_PREFIX="\${CONDA_PREFIX:-/opt/conda}" + for f in "\$CONDA_PREFIX"/etc/conda/activate.d/*.sh; do [ -f "\$f" ] && source "\$f" done diff --git a/modules/local/lrsomaticreport/meta.yml b/modules/local/lrsomaticreport/meta.yml index 668c8c40..b54f9d93 100644 --- a/modules/local/lrsomaticreport/meta.yml +++ b/modules/local/lrsomaticreport/meta.yml @@ -52,6 +52,10 @@ input: - - report_src: type: directory description: lrsomatic_report source tree (bin/, R/, templates/, assets/), shared across all samples; defaults to the vendored copy at `assets/lrsomatic_report` + - - gene_panel: + type: file + description: Optional user-supplied gene panel TSV (a `gene` column) to select on load, staged so it is bound into the container; `[]` when `--report_gene_panel` names a builtin panel or is unset + pattern: "*.tsv" output: report: diff --git a/modules/local/lrsomaticreport/tests/gene_panel.config b/modules/local/lrsomaticreport/tests/gene_panel.config new file mode 100644 index 00000000..7a320cc8 --- /dev/null +++ b/modules/local/lrsomaticreport/tests/gene_panel.config @@ -0,0 +1,11 @@ +/* + * conf/modules.config is not loaded for module-level nf-test runs, so reproduce the + * `--gene-panel` argument it builds. It passes `file(params.report_gene_panel).name`, + * i.e. the base name -- which for a user-supplied TSV is the name the `gene_panel` + * input stages it under in the task work dir. + */ +process { + withName: 'LRSOMATICREPORT' { + ext.args = "--gene-panel 'test_panel.tsv'" + } +} diff --git a/modules/local/lrsomaticreport/tests/main.nf.test b/modules/local/lrsomaticreport/tests/main.nf.test index 876bf43a..0753b931 100644 --- a/modules/local/lrsomaticreport/tests/main.nf.test +++ b/modules/local/lrsomaticreport/tests/main.nf.test @@ -27,6 +27,7 @@ nextflow_process { [] // wakhan_files ] input[1] = file("${projectDir}/assets/lrsomatic_report", checkIfExists: true) + input[2] = [] // gene_panel """ } } @@ -61,6 +62,7 @@ nextflow_process { [] // wakhan_files ] input[1] = file("${projectDir}/assets/lrsomatic_report", checkIfExists: true) + input[2] = [] // gene_panel """ } } @@ -79,4 +81,44 @@ nextflow_process { } + // A user-supplied panel TSV lives outside the task work dir, so it only reaches + // render_report.R if it is staged (and therefore bound into the container) via the + // `gene_panel` input -- otherwise the tool aborts with "--gene-panel not found". + // conf/modules.config passes the *base* name, which is the staged name here. + test("custom gene panel tsv - real render") { + + config "./gene_panel.config" + + when { + process { + """ + input[0] = [ + [ id:'test', paired_data: null, sex: 'male' ], + file("${projectDir}/modules/local/lrsomaticreport/tests/test_SOMATIC_VEP.vcf.gz", checkIfExists: true), + [], // sv_vep + [], // severus_vcf + [], // somatic_vcf + [], // ascat_files + [], // qc_tumor_files + [], // qc_normal_files + [] // wakhan_files + ] + input[1] = file("${projectDir}/assets/lrsomatic_report", checkIfExists: true) + input[2] = file("${projectDir}/modules/local/lrsomaticreport/tests/test_panel.tsv", checkIfExists: true) + """ + } + } + + then { + assert process.success + assertAll( + // The custom panel is registered alongside the builtins under its file + // base name, so its presence in the HTML proves the TSV was read. + { assert path(process.out.report.get(0).get(1)).text.contains("test_panel") }, + { assert path(process.out.report.get(0).get(1)).text.contains("TP53") } + ) + } + + } + } diff --git a/modules/local/lrsomaticreport/tests/test_panel.tsv b/modules/local/lrsomaticreport/tests/test_panel.tsv new file mode 100644 index 00000000..13ce7bbc --- /dev/null +++ b/modules/local/lrsomaticreport/tests/test_panel.tsv @@ -0,0 +1,3 @@ +gene panel notes +TP53 testpanel Tumour suppressor +KRAS testpanel Proto-oncogene diff --git a/workflows/lrsomatic.nf b/workflows/lrsomatic.nf index a4db8a64..b5df648e 100644 --- a/workflows/lrsomatic.nf +++ b/workflows/lrsomatic.nf @@ -1097,9 +1097,17 @@ workflow LRSOMATIC { .set { report_input_ch } // report_input_ch: [meta, vep_somatic, sv_vep, severus_vcf, somatic_vcf, ascat_files, qc_tumor_files, qc_normal_files, wakhan_files] + // --report_gene_panel accepts a builtin panel name, the `none` sentinel, or a path + // to a TSV. Only a real file needs staging (so it is bound into the container); + // a builtin name reaches the tool through ext.args alone -- see conf/modules.config. + def report_gene_panel_file = params.report_gene_panel && file(params.report_gene_panel).exists() + ? file(params.report_gene_panel, checkIfExists: true) + : [] + LRSOMATICREPORT ( report_input_ch, - file(params.report_src) + file(params.report_src, checkIfExists: true), + report_gene_panel_file ) } From 60847fcf74c07ea0ba75e1507238f7d6ba30d86b Mon Sep 17 00:00:00 2001 From: ljwharbers Date: Fri, 28 Aug 2026 14:53:41 +0200 Subject: [PATCH 08/17] chore: re-sync the vendored lrsomatic_report to v1.2.1 Brings in the breakend circos plot cross-linked to the SV table, per-reference gene panels (lymphoid.{hg38,t2t}, plus a new sarcoma panel) with coordinate matching and a hard error on a reference mismatch, the VAF-provenance footnote on the variant table, and phasing moved into QC as a collapsible block. Nothing on the pipeline side had to change to accommodate it: locate_outputs() is untouched upstream and every new file the tool reads lives inside the vendored tree, so the module's input tuple and staging layout stand. The dependency set is also unchanged -- a library()/require() grep over the upstream R/, bin/ and templates/ resolves to packages already pinned in environment.yml -- so both Wave images stay as they are. Version strings that are carried by hand were bumped together: the module's version topic, meta.yml, the module snapshot, the five pipeline-level snapshots and the CHANGELOG. VENDORED.md now records the tag itself as the vendored commit, spells out why the rm -rf before the copy is load-bearing (it is what retires the replaced lymphoid.tsv), and lists every file the next re-sync has to touch. Also names the per-sample report in README.md and tower.yml, which still described MultiQC as the only report. Module nf-test passes all three tests under -profile singularity, including both real renders. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 2 +- README.md | 3 +- assets/lrsomatic_report/R/circos.R | 7 +- assets/lrsomatic_report/R/circos_bnd.R | 172 +++++ assets/lrsomatic_report/R/parse_qc.R | 13 +- assets/lrsomatic_report/R/parse_severus.R | 593 +++++++++++++----- .../lrsomatic_report/R/parse_smallvariants.R | 203 +++++- assets/lrsomatic_report/R/sections.R | 14 + assets/lrsomatic_report/R/sections/sv.R | 34 +- assets/lrsomatic_report/R/utils.R | 272 +++++++- assets/lrsomatic_report/README.md | 76 ++- assets/lrsomatic_report/VENDORED.md | 49 +- .../assets/gene_lists/README.md | 77 ++- .../assets/gene_lists/lymphoid.hg38.tsv | 76 +++ .../assets/gene_lists/lymphoid.t2t.tsv | 76 +++ .../assets/gene_lists/lymphoid.tsv | 74 --- .../assets/gene_lists/sarcoma.hg38.tsv | 144 +++++ .../assets/gene_lists/sarcoma.t2t.tsv | 144 +++++ .../lrsomatic_report/assets/js/bnd_circos.js | 307 +++++++++ .../assets/styles/report.scss | 161 +++++ assets/lrsomatic_report/bin/render_report.R | 65 +- .../lrsomatic_report/templates/per_sample.qmd | 253 ++++++-- .../templates/sections/_gene_filter.qmd | 16 +- .../templates/sections/_header.qmd | 13 +- .../templates/sections/_qc.qmd | 32 +- .../templates/sections/_smallvariants.qmd | 80 ++- .../templates/sections/_sv.qmd | 399 +++++++++++- .../templates/sections/_whatshap.qmd | 17 +- docs/output.md | 6 +- docs/usage.md | 13 +- modules/local/lrsomaticreport/main.nf | 2 +- modules/local/lrsomaticreport/meta.yml | 4 +- .../lrsomaticreport/tests/main.nf.test.snap | 10 +- tests/clair_only.nf.test.snap | 2 +- tests/consensus.nf.test.snap | 2 +- tests/deep_only.nf.test.snap | 2 +- tests/default.nf.test.snap | 2 +- tests/union.nf.test.snap | 2 +- tower.yml | 2 + 39 files changed, 2949 insertions(+), 470 deletions(-) create mode 100644 assets/lrsomatic_report/R/circos_bnd.R create mode 100644 assets/lrsomatic_report/assets/gene_lists/lymphoid.hg38.tsv create mode 100644 assets/lrsomatic_report/assets/gene_lists/lymphoid.t2t.tsv delete mode 100644 assets/lrsomatic_report/assets/gene_lists/lymphoid.tsv create mode 100644 assets/lrsomatic_report/assets/gene_lists/sarcoma.hg38.tsv create mode 100644 assets/lrsomatic_report/assets/gene_lists/sarcoma.t2t.tsv create mode 100644 assets/lrsomatic_report/assets/js/bnd_circos.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 33fe4ab5..4e0d0d76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### `Added` - [#176](https://github.com/IntGenomicsLab/lrsomatic/pull/176) - Added `LRSOMATICREPORT` as the final pipeline step: a self-contained per-sample HTML report covering small variants, structural variants, copy number and QC. Skip it with `--skip_report`; choose the gene panel selected on load with `--report_gene_panel` (@ljwharbers). -- [#176](https://github.com/IntGenomicsLab/lrsomatic/pull/176) - Vendored the [lrsomatic_report](https://github.com/ljwharbers/lrsomatic_report) v1.1.0 tool source at `assets/lrsomatic_report`, so `nextflow run IntGenomicsLab/lrsomatic` ships it without a submodule checkout (@ljwharbers). +- [#176](https://github.com/IntGenomicsLab/lrsomatic/pull/176) - Vendored the [lrsomatic_report](https://github.com/ljwharbers/lrsomatic_report) v1.2.1 tool source at `assets/lrsomatic_report`, so `nextflow run IntGenomicsLab/lrsomatic` ships it without a submodule checkout (@ljwharbers). - [#176](https://github.com/IntGenomicsLab/lrsomatic/pull/176) - Added a `solution_dirs` output to the WAKHAN module so its per-solution copy-number plots can be staged downstream (@ljwharbers). ### `Fixed` diff --git a/README.md b/README.md index 6125f8a2..27dd8d47 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ IntGenomicsLab/lr_somatic was originally written by Luuk Harbers, Robert Forsyth ## Pipeline output -This pipeline produces a series of different output files. The main output is an aligned and phased tumour bam file. This bam file can be used by any typical downstream tool that uses bam files as input. Furthermore, we have sample-specific QC outputs from `cramino` (fastq), `cramino` (bam), `mosdepth`, `samtools` (stats/flagstat/idxstats), and optionally `fibertools`. Finally, we have a `multiqc` report from that combines the output from `mosdepth` and `samtools` into one html report. +This pipeline produces a series of different output files. The main output is an aligned and phased tumour bam file. This bam file can be used by any typical downstream tool that uses bam files as input. Furthermore, we have sample-specific QC outputs from `cramino` (fastq), `cramino` (bam), `mosdepth`, `samtools` (stats/flagstat/idxstats), and optionally `fibertools`. Finally, we have a `multiqc` report from that combines the output from `mosdepth` and `samtools` into one html report, and a self-contained per-sample HTML report (`/report/_report.html`) covering small variants, structural variants, copy number and QC in one place — disable it with `--skip_report`. Besides QC and the aligned and phased bam file, we have output from (structural) variant and copy number callers, of which some are optional. The output from these variant callers can be found in their respective folders. For small and structural variant callers (`clairS`, `clairS-TO`, and `severus`) these will contain, among others, `vcf` files with called variants. For `ascat` these contain files with final copy number information and plots of the copy number profiles. @@ -128,6 +128,7 @@ Example output directory structure: │ │ ├── germline │ │ ├── somatic │ │ ├── SVs +│ ├── report │ ├── Sample 2 │ ├── ascat diff --git a/assets/lrsomatic_report/R/circos.R b/assets/lrsomatic_report/R/circos.R index f1d210ca..d37144c6 100644 --- a/assets/lrsomatic_report/R/circos.R +++ b/assets/lrsomatic_report/R/circos.R @@ -48,8 +48,8 @@ BND_COLOUR = "#8a5fa3" # Draw a circos plot and write it to output_path (SVG or PNG depending on extension) # # @param snv_data data.table: chrom, pos, ref, alt (single-base SNVs only) -# @param sv_nontrans data.table from parse_severus_vcf()$nontrans -# @param sv_trans data.table from parse_severus_vcf()$translocations +# @param sv_nontrans data.table from severus_circos_tracks()$nontrans +# @param sv_trans data.table from severus_circos_tracks()$translocations # @param cnv_data data.table from parse_ascat_segments() # @param cytobands data.frame: chrom, start, end, name, stain # @param chrom_lengths named integer vector (chrom → bp length) @@ -246,6 +246,9 @@ draw_circos = function(snv_data = NULL, ) # ---- Translocation links (BND) in the centre ---------------------------- + # One arc per rearrangement: severus_circos_tracks() has already collapsed Severus's + # two mate records into one row, so nothing here draws the same arc twice (which used + # to show as doubled opacity at alpha.f = 0.5). if (nrow(sv_tr) > 0) { for (i in seq_len(nrow(sv_tr))) { tryCatch( diff --git a/assets/lrsomatic_report/R/circos_bnd.R b/assets/lrsomatic_report/R/circos_bnd.R new file mode 100644 index 00000000..87ec351e --- /dev/null +++ b/assets/lrsomatic_report/R/circos_bnd.R @@ -0,0 +1,172 @@ +suppressPackageStartupMessages({ + library(data.table) +}) + +# Breakend circos: a second, BND-only plot drawn over just the chromosomes a breakend +# touches, cross-linked to the SV table so selecting a row highlights its arc. +# +# R selects the data; the browser draws it. That split exists because the plot has to +# re-lay-out when the gene panel changes: which chromosomes get a sector, and therefore +# every sector's angular width, is a function of the *filtered* link set. A server-drawn +# SVG bakes those angles in, and its arcs are flattened point lists carrying no genomic +# coordinates, so no client script can recompute them — the previous version could only +# dim arcs in place, leaving every chromosome on the plot whatever the filter said. +# +# This also retires the sentinel-colour tagging that the inlined-SVG design needed +# (svglite emits no ids, so each object had to be drawn in a unique colour and the markup +# rewritten afterwards), along with its "a mistagged arc disables highlighting" fallback: +# nodes the client creates itself carry their own ids by construction. +# +# See assets/js/bnd_circos.js for the drawing half. + +# Above this the payload stops being worth its bytes: real samples on this cluster carry +# 50-115 arcs after mate collapse, but an unfiltered run can carry ~13k. +BND_CIRCOS_MAX_LINKS = 2000L + +# The BND classes with two loci to draw an arc between; svclass is set by +# parse_severus_somatic_records() / build_sv_table(). A "single breakend" has no partner +# locus and so cannot be drawn at all. +BND_CIRCOS_CLASSES = c("translocation", "intra-chr breakend") + +# ---- Data selection ------------------------------------------------------ + +# The rearrangements the circos can draw: BNDs with both loci known, on chromosomes the +# report is plotting. Callers pass the result to both bnd_panel_genes() and +# bnd_circos_data(), so the gene track cannot annotate an arc that was never drawn. +bnd_links = function(sv_table, chromosomes) { + cols = c("id", "svclass", "chrom_a", "pos_a", "chrom_b", "pos_b") + empty = data.table(id = character(), svclass = character(), + chrom_a = character(), pos_a = integer(), + chrom_b = character(), pos_b = integer()) + if (is.null(sv_table) || nrow(sv_table) == 0 || !all(cols %in% names(sv_table))) + return(empty) + + b = as.data.table(sv_table)[, ..cols] + b = b[svclass %in% BND_CIRCOS_CLASSES & + !is.na(chrom_a) & !is.na(pos_a) & !is.na(chrom_b) & !is.na(pos_b) & + chrom_a %in% chromosomes & chrom_b %in% chromosomes] + if (nrow(b) == 0) return(empty) + + # Deterministic order, so the payload is byte-identical across renders of the same data. + setorder(b, chrom_a, pos_a, chrom_b, pos_b, id) + b[] +} + +# The panel genes to label: for every coordinate-carrying panel, those whose interval +# falls within `window` of either breakend of a drawn arc. +# +# This is the same test, with the same window, that sv_panel_hits() applies to fill the +# table's panel_hit column — deliberately, because the report only shows a gene here once +# it appears in that column. Reimplementing the predicate would let the two drift. +# +# A symbol-only panel (and the custom paste-in panel) carries no coordinates, so it can +# produce panel_hit labels but no gene bodies. +bnd_panel_genes = function(links, all_panels, window = SV_PANEL_WINDOW_BND) { + empty = data.table(chrom = character(), start = integer(), end = integer(), + gene = character(), panels = character()) + if (is.null(links) || nrow(links) == 0) return(empty) + if (is.null(all_panels) || length(all_panels) == 0) return(empty) + + q = rbindlist(list( + data.table(chrom = links$chrom_a, pos = as.numeric(links$pos_a)), + data.table(chrom = links$chrom_b, pos = as.numeric(links$pos_b)) + )) + q = unique(q[!is.na(chrom) & !is.na(pos)]) + if (nrow(q) == 0) return(empty) + q[, `:=`(start = pmax(pos - window, 0), end = pos + window)] + + out = list() + for (nm in names(all_panels)) { + iv = panel_intervals(all_panels[[nm]]) + if (is.null(iv) || nrow(iv) == 0) next + iv = copy(iv)[, `:=`(start = as.numeric(start), end = as.numeric(end))] + setkey(iv, chrom, start, end) + ov = data.table::foverlaps(q[, .(chrom, start, end)], iv, + by.x = c("chrom", "start", "end"), + type = "any", nomatch = NULL) + if (nrow(ov) == 0) next + out[[nm]] = unique(ov[, .(chrom, start, end, gene, panel = nm)]) + } + if (length(out) == 0) return(empty) + + g = rbindlist(out) + g = g[, .(panels = paste(sort(unique(panel)), collapse = ",")), + by = .(chrom, start, end, gene)] + setorder(g, chrom, start, gene) + g[, `:=`(start = as.integer(start), end = as.integer(end))] + g[, .(chrom, start, end, gene, panels)] +} + + +# ---- Payload for the client-side plot ------------------------------------- + +# Everything the browser needs to draw the breakend circos at any filter state. +# +# Returns list(data, n_links, n_genes, chroms, reason). `data` is NULL when there is +# nothing to draw or too much of it, with `reason` saying which; the template shows that +# through section_notice() instead of an empty ring. +# +# `chroms` here is the *unfiltered* superset — every chromosome some drawable breakend +# touches. The client narrows it per filter; R only needs to bound the payload. +bnd_circos_data = function(links, genes = NULL, cytobands, chrom_lengths, chromosomes) { + + fail = function(reason) list(data = NULL, n_links = 0L, n_genes = 0L, + chroms = character(0), reason = reason) + + if (is.null(links) || nrow(links) == 0) + return(fail("No breakends with two mapped loci — nothing to draw arcs between.")) + if (nrow(links) > BND_CIRCOS_MAX_LINKS) + return(fail(sprintf(paste("%s breakend arcs is more than this plot can show", + "(limit %s). Select a gene panel above for a filtered", + "report, or read the table below."), + nrow(links), BND_CIRCOS_MAX_LINKS))) + + b = copy(as.data.table(links)) + chroms = chromosomes[chromosomes %in% unique(c(b$chrom_a, b$chrom_b))] + chroms = chroms[chroms %in% unique(cytobands$chrom)] + b = b[chrom_a %in% chroms & chrom_b %in% chroms] + if (nrow(b) == 0 || length(chroms) == 0) + return(fail("No breakends on the chromosomes this report plots.")) + chroms = chromosomes[chromosomes %in% unique(c(b$chrom_a, b$chrom_b))] + + lens = chrom_lengths[chroms] + # A locus past the end of its sector would be drawn outside the ring; clamp, as the + # circlize version did, rather than dropping the row and losing its table cross-link. + b[, `:=`(pos_a = pmin(pmax(as.numeric(pos_a), 1), lens[chrom_a]), + pos_b = pmin(pmax(as.numeric(pos_b), 1), lens[chrom_b]))] + + cy = as.data.table(cytobands)[chrom %in% chroms] + setorder(cy, chrom, start) + + g = if (is.null(genes)) data.table() else as.data.table(genes)[chrom %in% chroms] + if (nrow(g) > 0) setorder(g, chrom, start, gene) + + list( + data = list( + chromosomes = js_vec(chroms), + lengths = js_vec(as.numeric(lens)), + cytobands = js_rows(cy, c("chrom", "start", "end", "stain")), + links = js_rows(b, c("id", "svclass", "chrom_a", "pos_a", "chrom_b", "pos_b")), + genes = if (nrow(g) > 0) js_rows(g, c("chrom", "start", "end", "gene", "panels")) + else "[]" + ), + n_links = nrow(b), + n_genes = nrow(g), + chroms = chroms, + reason = NULL + ) +} + +# The ") +} diff --git a/assets/lrsomatic_report/R/parse_qc.R b/assets/lrsomatic_report/R/parse_qc.R index b95f90a3..1ab014f7 100644 --- a/assets/lrsomatic_report/R/parse_qc.R +++ b/assets/lrsomatic_report/R/parse_qc.R @@ -4,7 +4,11 @@ suppressPackageStartupMessages({ # Parse mosdepth summary (*.mosdepth.summary.txt) # Returns list: mean_depth, total_row (the "total" row from mosdepth) -parse_mosdepth_summary = function(summary_file) { +# +# `keep_chroms` is the reference's own contig list. Without it the per-chromosome table +# falls back to a bare "^chr" match, which admits the ~2500 chrUn_*_decoy contigs and — +# worse — comes back *empty* on any reference whose contigs carry no "chr" prefix. +parse_mosdepth_summary = function(summary_file, keep_chroms = NULL) { if (is.null(summary_file) || !file.exists(summary_file)) { return(list(mean_depth = NA_real_, table = data.table())) } @@ -14,7 +18,12 @@ parse_mosdepth_summary = function(summary_file) { mean_depth = if (nrow(total_row) > 0) total_row$mean[1] else NA_real_ # Keep per-chromosome rows (exclude region-level and total) - chr_rows = dt[grepl("^chr", chrom) & !grepl("_region", chrom)] + chr_rows = dt[chrom != "total" & !grepl("_region", chrom)] + if (!is.null(keep_chroms) && length(keep_chroms) > 0) { + chr_rows = chr_rows[ensure_chr_prefix(chrom) %in% ensure_chr_prefix(keep_chroms)] + } else { + chr_rows = chr_rows[grepl("^chr", chrom)] + } total_length = if (nrow(total_row) > 0) total_row$length[1] else NA_real_ total_bases = if (nrow(total_row) > 0) total_row$bases[1] else NA_real_ list(mean_depth = round(mean_depth, 2), total_length = total_length, total_bases = total_bases, table = chr_rows) diff --git a/assets/lrsomatic_report/R/parse_severus.R b/assets/lrsomatic_report/R/parse_severus.R index 170b3316..1e847640 100644 --- a/assets/lrsomatic_report/R/parse_severus.R +++ b/assets/lrsomatic_report/R/parse_severus.R @@ -2,13 +2,31 @@ suppressPackageStartupMessages({ library(data.table) }) -# Parse Severus somatic VCF for circos plot data. -# Returns list: $translocations (BND records) and $nontrans (DEL/DUP/INV/INS) -parse_severus_vcf = function(vcf_file) { - if (is.null(vcf_file) || !file.exists(vcf_file)) { - return(list(translocations = data.table(), nontrans = data.table())) - } - +# Severus writes both sides of a rearrangement as separate records (`_1` / `_2`, +# linked by INFO/MATE_ID) and tags single breakends "sBND", which fails a bare +# `== "BND"` test. Both are handled here, once, so that everything downstream +# — the SV table, the panel filter, the circos links and the SV count — sees one +# row per rearrangement carrying *both* breakend loci. +BND_SVTYPES = c("BND", "sBND") + +# The BND-derived `svclass` values (see the fcase in parse_severus_somatic_records()). +# What these three share against DEL/DUP/INV/INS is what sv_display_columns() turns on: +# a junction is *two loci*, so it has no span and no size, while a contiguous type is +# *one span* whose two records are its own start and end. BND_CIRCOS_CLASSES +# (R/circos_bnd.R) is this set minus "single breakend", which has no partner locus and +# therefore no arc. +SV_JUNCTION_CLASSES = c("translocation", "intra-chr breakend", "single breakend") + +# Panel-matching windows. Defined here as the single source of truth: they are +# used by sv_panel_hits() below and emitted to the report's client-side filter +# from templates/per_sample.qmd, so the R "Panel SVs" card and the JS row filter +# cannot drift apart. +SV_PANEL_WINDOW_BND = 1e6 # distance from either breakend of a BND +SV_PANEL_WINDOW_OTHER = 1e5 # distance from the span of a DEL/DUP/INV/INS + +# Read a VCF's data records, skipping the header. Returns an empty data.table +# rather than erroring for a header-only VCF. +.severus_read_vcf = function(vcf_file, col_names) { con = gzfile(vcf_file, "rb") skip_n = 0L repeat { @@ -22,145 +40,438 @@ parse_severus_vcf = function(vcf_file) { # fread() errors (rather than returning 0 rows) when skip lands exactly on # the last line of the file, i.e. a VCF with no variant records at all. dt = tryCatch( - fread(vcf_file, skip = skip_n + 1L, sep = "\t", header = FALSE, - select = 1:8, - col.names = c("CHROM", "POS", "ID", "REF", "ALT", "QUAL", "FILTER", "INFO")), + fread(vcf_file, skip = skip_n + 1L, sep = "\t", header = FALSE), error = function(e) data.table() ) - if (nrow(dt) == 0) { - return(list(translocations = data.table(), nontrans = data.table())) - } - - dt[, CHROM := ensure_chr_prefix(CHROM)] - - # Extract INFO sub-fields - .info_val = function(info_vec, key) { - pattern = paste0("(?:^|;)", key, "=([^;]+)") - m = regmatches(info_vec, regexpr(pattern, info_vec, perl = TRUE)) - ifelse(nchar(m) > 0, sub(paste0(".*="), "", m), NA_character_) - } - - dt[, SVTYPE := .info_val(INFO, "SVTYPE")] - dt[grepl("END=", INFO, fixed = TRUE), END := as.integer(.info_val(INFO[grepl("END=", INFO, fixed = TRUE)], "END"))] - dt[grepl("SVLEN=", INFO, fixed = TRUE), SVLEN := as.integer(.info_val(INFO[grepl("SVLEN=", INFO, fixed = TRUE)], "SVLEN"))] - - # BND partner chromosome/position from ALT field - # ALT format examples: "N[chr7:24547089[" or "]chr7:24547089]N" - dt[SVTYPE == "BND", CHROM2 := { - m = regmatches(ALT, regexpr("chr[^:]+", ALT, perl = TRUE)) - ifelse(nchar(m) > 0, m, NA_character_) - }] - dt[SVTYPE == "BND", POS2 := as.integer(regmatches(ALT, regexpr("(?<=:)\\d+", ALT, perl = TRUE)))] - - # Insertions have no END — use POS - dt[SVTYPE == "INS" | is.na(END), END := POS] - - # Colours and y-positions for non-BND SV track - SV_COL = c(INS = "#f97e02", DEL = "#020272", INV = "#e7cc02", DUP = "#e41a1c") - SV_YPOS = c(INS = 1.0, DEL = 0.66, INV = 0.33, DUP = 0.05) - dt[SVTYPE %in% names(SV_COL), circos_col := SV_COL[SVTYPE]] - dt[SVTYPE %in% names(SV_YPOS), circos_pos := SV_YPOS[SVTYPE]] + if (nrow(dt) == 0) return(data.table()) - translocations = dt[SVTYPE == "BND" & !is.na(CHROM2) & !is.na(POS2), - .(chrom = CHROM, pos = POS, chrom2 = CHROM2, pos2 = POS2)] + # Name the columns positionally rather than selecting a fixed count: a VCF with no + # FORMAT/SAMPLE columns would otherwise fail the read and come back as zero SVs, + # which reads identically to "this sample has none". + n = min(ncol(dt), length(col_names)) + setnames(dt, seq_len(n), col_names[seq_len(n)]) + for (nm in setdiff(col_names, names(dt))) dt[, (nm) := NA_character_] + dt[, ..col_names] +} - nontrans = dt[SVTYPE != "BND", - .(chrom = CHROM, pos = POS, end = END, svtype = SVTYPE, - svlen = SVLEN, circos_pos, circos_col)] +# One INFO sub-field per record, NA where the key is absent. Length-preserving: +# bare regmatches() silently drops non-matching elements, which is a length-mismatch +# error waiting for the first optional key (MATE_ID, END, SVLEN — i.e. most of them). +.info_val = function(info_vec, key) { + m = regexpr(paste0("(?:^|;)", key, "=([^;]+)"), info_vec, perl = TRUE) + out = rep(NA_character_, length(info_vec)) + hit = m > 0 + if (any(hit)) out[hit] = sub(".*=", "", regmatches(info_vec, m)) + out +} - list(translocations = translocations, nontrans = nontrans) +# Partner locus of a BND, from the ALT bracket notation — all four forms +# ("N[chr7:1234[", "]chr7:1234]N", "N]chr7:1234]", "[chr7:1234[N") and with or +# without a "chr" prefix on the contig, since a bare-contig reference would +# otherwise drop every breakend. A single breakend (sBND) has no bracket pair +# and yields NA. +.bnd_partner = function(alt) { + pat = "^.*?[\\[\\]]([^\\[\\]:]+):([0-9]+)[\\[\\]].*$" + has = grepl(pat, alt, perl = TRUE) + list( + chrom = ensure_chr_prefix(ifelse(has, sub(pat, "\\1", alt, perl = TRUE), NA_character_)), + pos = ifelse(has, suppressWarnings(as.integer(sub(pat, "\\2", alt, perl = TRUE))), + NA_integer_) + ) } -# Parse the somatic Severus VCF into one row per SV (id, svtype, coords, length, VAF). -# Used as the input to build_sv_table_from_vep() — a lighter-weight companion to -# parse_severus_vcf() above, which instead returns circos-ready translocation/non-BND tracks. +# Parse the somatic Severus VCF into one row per rearrangement: both breakend loci, +# type, length and VAF. Mate records are collapsed (`_1` is side A, `_2` side B), so +# this is also what the SV count and the circos links are derived from. +# +# Returns: id, id_b, svtype, svclass, chrom_a, pos_a, chrom_b, pos_b, sv_len, vaf +# For non-BND types chrom_b/pos_b are the SV's own end; for a single breakend they +# are NA. parse_severus_somatic_records = function(vcf_file) { if (is.null(vcf_file) || !file.exists(vcf_file)) return(data.table()) - con = gzfile(vcf_file, "rb") - skip_n = 0L - repeat { - line = readLines(con, n = 1, warn = FALSE) - if (length(line) == 0) break - if (startsWith(line, "#CHROM")) break - skip_n = skip_n + 1L - } - close(con) - - dt = tryCatch( - fread(vcf_file, skip = skip_n + 1L, sep = "\t", header = FALSE, select = 1:10, - col.names = c("CHROM", "POS", "ID", "REF", "ALT", "QUAL", "FILTER", "INFO", - "FORMAT", "SAMPLE1")), - error = function(e) data.table() - ) + dt = .severus_read_vcf(vcf_file, c("CHROM", "POS", "ID", "REF", "ALT", "QUAL", + "FILTER", "INFO", "FORMAT", "SAMPLE1")) if (nrow(dt) == 0) return(data.table()) dt[, CHROM := ensure_chr_prefix(CHROM)] - - .info_val = function(info_vec, key) { - pattern = paste0("(?:^|;)", key, "=([^;]+)") - m = regmatches(info_vec, regexpr(pattern, info_vec, perl = TRUE)) - ifelse(nchar(m) > 0, sub(paste0(".*="), "", m), NA_character_) + dt[, SVTYPE := .info_val(INFO, "SVTYPE")] + dt[, is_bnd := SVTYPE %in% BND_SVTYPES] + + # END and SVLEN are optional: a BND/INS-only VCF carries neither, so create the + # columns unconditionally before anything reads them. + dt[, `:=`(END = NA_integer_, SVLEN = NA_integer_)] + dt[grepl("END=", INFO, fixed = TRUE), END := as.integer(.info_val(INFO, "END"))] + dt[grepl("SVLEN=", INFO, fixed = TRUE), SVLEN := as.integer(.info_val(INFO, "SVLEN"))] + dt[is.na(END), END := POS] + + # Partner locus. For a BND it comes from ALT, which names it even when the mate + # record itself was filtered out of the VCF; for everything else it is the SV's + # own end on the same contig. + partner = .bnd_partner(dt$ALT) + dt[, `:=`(chrom_b = fifelse(is_bnd, partner$chrom, CHROM), + pos_b = fifelse(is_bnd, partner$pos, as.integer(END)))] + + dt[, mate_id := .info_val(INFO, "MATE_ID")] + + # One row per rearrangement. Group the two mate records on the unordered + # {ID, MATE_ID} pair, falling back to the shared `_1`/`_2` stem for older VCFs + # that carry no MATE_ID. Groups that are not exactly a pair (an unpaired `_1`, + # an orphaned `_2`, an sBND) stay as singletons rather than being dropped. + dt[, pair_key := fifelse(!is.na(mate_id), + paste(pmin(ID, mate_id), pmax(ID, mate_id), sep = "|"), + sub("_[12]$", "", ID))] + dt[, side_rank := fifelse(grepl("_1$", ID), 1L, 2L)] + dt[, n_in_pair := .N, by = pair_key] + if (any(dt$n_in_pair > 2L)) { + message("Severus VCF: ", sum(dt$n_in_pair > 2L), + " records share a mate group with more than two members; kept unpaired.") + dt[n_in_pair > 2L, pair_key := ID] + dt[, n_in_pair := .N, by = pair_key] } + setorder(dt, pair_key, side_rank, ID) + dt = dt[dt[, .I[1L], by = pair_key]$V1] - dt[, SVTYPE := .info_val(INFO, "SVTYPE")] - dt[grepl("END=", INFO, fixed = TRUE), END := as.integer(.info_val(INFO[grepl("END=", INFO, fixed = TRUE)], "END"))] - dt[grepl("SVLEN=", INFO, fixed = TRUE), SVLEN := as.integer(.info_val(INFO[grepl("SVLEN=", INFO, fixed = TRUE)], "SVLEN"))] + # The mate's record ID, which is what the per-side VEP annotation is keyed on. + dt[, id_b := fifelse(n_in_pair == 2L & !is.na(mate_id), mate_id, NA_character_)] - # Insertions and BNDs have no END — use POS - dt[SVTYPE %in% c("INS", "BND") | is.na(END), END := POS] + dt[, svclass := fcase( + is_bnd & is.na(chrom_b), "single breakend", + is_bnd & chrom_b != CHROM, "translocation", + is_bnd, "intra-chr breakend", + default = SVTYPE + )] # VAF from FORMAT/SAMPLE1 (format string is uniform for Severus output, but split # format-group by format-group defensively, as in parse_caller_vcf()) fmt_groups = unique(dt$FORMAT) - vaf_list = vector("numeric", nrow(dt)) + vaf_list = rep(NA_real_, nrow(dt)) for (fmt in fmt_groups) { idx_rows = which(dt$FORMAT == fmt) fields = strsplit(fmt, ":", fixed = TRUE)[[1]] vaf_idx = match("VAF", fields) + if (is.na(vaf_idx)) next split_s = strsplit(dt$SAMPLE1[idx_rows], ":", fixed = TRUE) - vaf_list[idx_rows] = if (!is.na(vaf_idx)) { - vapply(split_s, function(x) - if (length(x) >= vaf_idx) suppressWarnings(as.numeric(x[vaf_idx])) else NA_real_, - numeric(1)) - } else NA_real_ + vaf_list[idx_rows] = vapply(split_s, function(x) + if (length(x) >= vaf_idx) suppressWarnings(as.numeric(x[vaf_idx])) else NA_real_, + numeric(1)) } dt[, VAF := vaf_list] - dt[, .(id = ID, svtype = SVTYPE, chrom = CHROM, start = POS, end = END, + dt[, .(id = ID, id_b, svtype = SVTYPE, svclass, + chrom_a = CHROM, pos_a = POS, chrom_b, pos_b, sv_len = SVLEN, vaf = VAF)] } -# Build the SV display table by joining VEP CSQ gene annotations (from the SV VEP VCF, -# `parse_vep_vcf()` from R/parse_smallvariants.R) onto the somatic Severus SVs by locus. -# This is the primary path when a VEP SV VCF is available; build_sv_table() below (fed by -# the gene-annotated TSV) is the fallback for pipelines that don't produce a VEP SV VCF. -build_sv_table_from_vep = function(somatic_vcf, vep_sv_vcf) { - somatic = parse_severus_somatic_records(somatic_vcf) - if (nrow(somatic) == 0) return(data.table()) +# Circos tracks from the collapsed records: BND links (one per rearrangement, so each +# arc is drawn once) and a non-BND track carrying colour/y-position. +severus_circos_tracks = function(records) { + empty = list(translocations = data.table(), nontrans = data.table()) + if (is.null(records) || nrow(records) == 0) return(empty) + + is_bnd = records$svtype %in% BND_SVTYPES + + trans = records[is_bnd & !is.na(chrom_b) & !is.na(pos_b), + .(chrom = chrom_a, pos = pos_a, chrom2 = chrom_b, pos2 = pos_b)] + # Records are already mate-collapsed; dedup on the unordered endpoint pair as well, + # so a VCF without MATE_ID cannot draw the same arc twice. + if (nrow(trans) > 0) { + a = paste(trans$chrom, trans$pos, sep = ":") + b = paste(trans$chrom2, trans$pos2, sep = ":") + trans = unique(trans[, .link_key := paste(pmin(a, b), pmax(a, b))], + by = ".link_key")[, .link_key := NULL] + } + + SV_COL = c(INS = "#f97e02", DEL = "#020272", INV = "#e7cc02", DUP = "#e41a1c") + SV_YPOS = c(INS = 1.0, DEL = 0.66, INV = 0.33, DUP = 0.05) + nontrans = records[!is_bnd, + .(chrom = chrom_a, pos = pos_a, end = pos_b, svtype, svlen = sv_len, + circos_pos = unname(SV_YPOS[svtype]), circos_col = unname(SV_COL[svtype]))] + + list(translocations = trans, nontrans = nontrans) +} + +# Build the SV display table: mate-collapsed Severus records plus, as display context, +# the VEP gene symbol at each breakend (from the SV VEP VCF, `parse_vep_vcf()` in +# R/parse_smallvariants.R). +# +# The join is keyed on the VCF record ID, which is what links a VEP record back to the +# Severus record it was annotated from; a locus key is only used as a fallback for +# annotation files whose IDs don't overlap the Severus ones at all. Keying on the locus +# alone leaks annotations between distinct records that happen to start at the same base. +# +# These symbols are context, not the panel filter's key — see sv_panel_hits(). Whether +# VEP annotates a breakend at all is an invocation-dependent property of the sample +# (1.6%-90% of breakends across the samples measured), which is exactly why panel +# matching is done on coordinates instead. +build_sv_table_from_vep = function(records, vep_sv_vcf) { + if (is.character(records)) records = parse_severus_somatic_records(records) + if (is.null(records) || nrow(records) == 0) return(data.table()) + + sv = copy(records) + out_cols = c("id", "svclass", "svtype", "chrom_a", "pos_a", "gene_a", + "chrom_b", "pos_b", "gene_b", "sv_len", "vaf", + "consequence", "impact") vep = parse_vep_vcf(vep_sv_vcf) if (is.null(vep) || nrow(vep) == 0) { - somatic[, `:=`(gene_hits = NA_character_, consequence = NA_character_, impact = NA_character_)] - return(somatic[, .(id, gene_hits, svtype, chrom, start, end, sv_len, vaf, consequence, impact)]) + sv[, `:=`(gene_a = NA_character_, gene_b = NA_character_, + consequence = NA_character_, impact = NA_character_)] + return(sv[, ..out_cols]) } - # Keep the highest-impact annotation per locus, and collapse all distinct gene symbols - # hit at that locus into one comma-joined column. + # Highest-impact annotation first, so annot[1] below is the one that survives. impact_rank = c(HIGH = 1L, MODERATE = 2L, LOW = 3L, MODIFIER = 4L) vep[, impact_rank := impact_rank[impact]] vep[is.na(impact_rank), impact_rank := 5L] setorder(vep, impact_rank) - agg = vep[, .( - gene_hits = paste(unique(symbol[!is.na(symbol) & nzchar(symbol)]), collapse = ","), - consequence = consequence[1], - impact = impact[1] - ), by = .(chrom, pos)] - agg[!nzchar(gene_hits), gene_hits := NA_character_] + collapse_genes = function(x) { + g = unique(x[!is.na(x) & nzchar(x)]) + if (length(g) == 0) NA_character_ else paste(g, collapse = ",") + } + + by_id = if ("id" %in% names(vep) && any(!is.na(vep$id))) { + vep[!is.na(id) & nzchar(id), .(gene = collapse_genes(symbol), + consequence = consequence[1], impact = impact[1]), + by = .(id)] + } else NULL + + by_locus = vep[, .(gene = collapse_genes(symbol), + consequence = consequence[1], impact = impact[1]), + by = .(chrom, pos)] + + # Which key to use is decided once for the whole table, not per side: deciding it per + # side would silently locus-match side B (where BND mate IDs are the only IDs) while + # ID-matching side A. + use_id = !is.null(by_id) && any(c(sv$id, sv$id_b) %in% by_id$id) + + # Severus records that the annotation side filtered out (parse_vep_vcf() keeps only + # FILTER PASS/.) simply get no symbols — that is not a join bug. + side_annot = function(ids, chroms, positions) { + src = if (use_id) by_id else by_locus + m = if (use_id) match(ids, by_id$id) + else match(paste(chroms, positions), paste(by_locus$chrom, by_locus$pos)) + res = data.table(gene = NA_character_, consequence = NA_character_, + impact = NA_character_)[rep(1L, length(chroms))] + hit = !is.na(m) + if (any(hit)) res[hit, `:=`(gene = src$gene[m[hit]], + consequence = src$consequence[m[hit]], + impact = src$impact[m[hit]])] + res + } + + a = side_annot(sv$id, sv$chrom_a, sv$pos_a) + b = side_annot(sv$id_b, sv$chrom_b, sv$pos_b) + + sv[, `:=`(gene_a = a$gene, gene_b = b$gene)] + # Consequence/impact describe the rearrangement, taken from the higher-impact side. + rank_of = function(x) { r = unname(impact_rank[x]); r[is.na(r)] = 5L; r } + use_b = rank_of(b$impact) < rank_of(a$impact) + sv[, `:=`(consequence = fifelse(use_b, b$consequence, a$consequence), + impact = fifelse(use_b, b$impact, a$impact))] + + sv[, ..out_cols] +} + +# Human-readable location, size and gene columns for the SV table. +# +# The stored schema is bedpe-shaped (chrom_a/pos_a + chrom_b/pos_b) because that is the +# only shape a translocation fits, and everything downstream reads it — sv_panel_hits(), +# the client-side filter, bnd_links(). But it is the wrong *reading* for most rows: a +# DEL's two records are its own start and end, not two partners, and its gene_a/gene_b +# are just the genes at either edge of one span. So the raw columns stay (hidden in the +# rendered table, and still exported) and these are shown instead. +# +# The rule is `svclass`, not "same chromosome": parse_severus_somatic_records() sets +# chrom_b = chrom_a for DEL/DUP/INV/INS, so chrom_b == chrom_a is equally true of an +# intra-chromosomal breakend — and there the two loci are a junction, not a span the +# variant swallows. Keying off the contig would hand an intra-chr breakend a start, an +# end and a size it does not have. +# +# Returns a data.table of five columns, one row per row of `sv_table`, same order: +# locus the display string (see the fcase below) +# locus_sort sort key for `locus`, which as a display string sorts lexically +# size fmt_bp() of size_bp; "" for a junction +# size_bp the number behind `size`, and its sort key +# genes merged for a span, side-labelled for a junction +# `chrom_levels` orders the sort key: pass the report's plotted chromosome vector from +# chromosomes_for_sex(), which is already in natural order. Contigs outside it sort last. +sv_display_columns = function(sv_table, chrom_levels = NULL) { + empty = data.table(locus = character(), locus_sort = character(), + size = character(), size_bp = numeric(), genes = character()) + if (is.null(sv_table) || nrow(sv_table) == 0) return(empty) + + d = as.data.table(sv_table) + need = c("svclass", "chrom_a", "pos_a", "chrom_b", "pos_b", "sv_len", + "gene_a", "gene_b") + absent = setdiff(need, names(d)) + if (length(absent) > 0) + stop("sv_display_columns(): sv_table is missing ", paste(absent, collapse = ", ")) + + is_junction = d$svclass %in% SV_JUNCTION_CLASSES + pa = suppressWarnings(as.numeric(d$pos_a)) + pb = suppressWarnings(as.numeric(d$pos_b)) + + # formatC(), not format(): format() is vectorised to a *common* width and would pad + # every position out to the longest one in the table. + bp = function(x) formatC(x, format = "d", big.mark = ",") + at = function(chrom, pos) paste0(chrom, ":", bp(pos)) + + locus = fcase( + is.na(pa) | is.na(d$chrom_a), NA_character_, + d$svclass == "single breakend", paste0(at(d$chrom_a, pa), " (unpaired)"), + is_junction, + # An arrow, not a dash: these two loci are joined, they do not bound a span. Both + # sides are named even for an intra-chromosomal junction, so a per-column search + # for a chromosome matches it on either side. + fifelse(is.na(pb) | is.na(d$chrom_b), + paste0(at(d$chrom_a, pa), " (unpaired)"), + paste0(at(d$chrom_a, pa), + fifelse(d$svclass == "translocation", " → ", " ↔ "), + at(d$chrom_b, pb))), + # A contiguous type: one span. An INS has END == POS, so it is a single point. + default = fifelse(is.na(pb) | pb <= pa, + at(d$chrom_a, pa), + paste0(at(d$chrom_a, pa), "–", bp(pb))) + ) - merged = merge(somatic, agg, by.x = c("chrom", "start"), by.y = c("chrom", "pos"), all.x = TRUE) - merged[, .(id, gene_hits, svtype, chrom, start, end, sv_len, vaf, consequence, impact)] + # SVLEN where Severus wrote one — an INS's length is *not* its span — else the span. + len = suppressWarnings(as.numeric(d$sv_len)) + size_bp = fifelse(is_junction, NA_real_, fifelse(!is.na(len), abs(len), pb - pa)) + # as.character() is load-bearing: fmt_bp() is built on ifelse(), which returns a vector + # typed after its *test*, so an all-NA size_bp — every row a junction, i.e. a BND-only + # SV table — comes back logical and the fifelse() below would reject the type mismatch. + size = as.character(fmt_bp(size_bp)) + + split_genes = function(x) { + if (is.na(x) || !nzchar(x)) return(character()) + g = trimws(strsplit(x, ",", fixed = TRUE)[[1]]) + unique(g[nzchar(g)]) + } + # A middot rather than the whitespace a mock-up would use: DT escapes cell content, so + #   is unavailable, and HTML collapses a run of spaces to one. + genes = vapply(seq_len(nrow(d)), function(i) { + a = split_genes(d$gene_a[i]); b = split_genes(d$gene_b[i]) + if (!is_junction[i]) return(paste(unique(c(a, b)), collapse = ", ")) + sides = c(if (length(a)) paste0("A: ", paste(a, collapse = ",")), + if (length(b)) paste0("B: ", paste(b, collapse = ","))) + paste(sides, collapse = " · ") + }, character(1)) + + rank = if (length(chrom_levels)) match(d$chrom_a, chrom_levels) + else rep(NA_integer_, nrow(d)) + rank[is.na(rank)] = 999L + locus_sort = fifelse(is.na(pa), "", sprintf("%03d:%011.0f", rank, pa)) + + data.table(locus = fifelse(is.na(locus), "", locus), + locus_sort = locus_sort, + size = fifelse(is.na(size), "", size), + size_bp = size_bp, + genes = genes) +} + +# Which panel genes each SV hits, and how it hit them. +# +# Coordinate-carrying panels (see load_gene_panel()) are matched on position: a panel +# interval within `bnd_window` of either breakend of a BND, or within `other_window` of +# the span of any other type. That needs no annotation on the SV row, which is the point +# — VEP breakend coverage is sample-dependent, so symbol matching hides breakends on +# samples whose VEP run didn't annotate them. +# +# A symbol-only panel falls back to a direct hit on either side's VEP symbol: today's +# behaviour, but reading both breakends instead of one. +# +# Returns one character label per row of `sv_table` ("" = no hit). Each hit reads +# "GENE (side, how)": `side` is the breakend it matched on (A/B, or "span" for a +# contiguous type), and `how` is "direct" when the locus overlaps the gene interval, or +# the distance to it otherwise. Without that second token a breakend most of a megabase +# away reads exactly like one inside the gene — the BND window is wide, and proximity is +# not disruption. The "Panel SVs" summary card counts the non-empty entries; the +# client-side filter applies the same test with the same windows and builds the same +# labels. +sv_panel_hits = function(sv_table, panel, + bnd_window = SV_PANEL_WINDOW_BND, + other_window = SV_PANEL_WINDOW_OTHER) { + n = if (is.null(sv_table)) 0L else nrow(sv_table) + if (n == 0) return(character(0)) + if (is.null(panel)) return(rep("", n)) + + label = function(genes, side, how) paste0(genes, " (", side, ", ", how, ")") + + if (!isTRUE(panel$has_coords)) { + symbols = toupper(panel$genes) + hit_side = function(col) { + if (!col %in% names(sv_table)) return(rep("", n)) + vapply(sv_table[[col]], function(cell) { + if (is.na(cell) || !nzchar(cell)) return("") + g = trimws(unlist(strsplit(toupper(as.character(cell)), "[;,]+"))) + g = unique(g[g %in% symbols]) + if (length(g) == 0) "" else paste(g, collapse = ",") + }, character(1), USE.NAMES = FALSE) + } + ha = hit_side("gene_a"); hb = hit_side("gene_b") + # A VEP symbol sits on the breakend itself, so a symbol hit is always direct — and + # there are no coordinates on this path to measure anything else from. + return(vapply(seq_len(n), function(i) { + parts = c(if (nzchar(ha[i])) label(ha[i], "A", "direct"), + if (nzchar(hb[i])) label(hb[i], "B", "direct")) + paste(parts, collapse = ", ") + }, character(1))) + } + + iv = panel_intervals(panel) + if (is.null(iv) || nrow(iv) == 0) return(rep("", n)) + + is_bnd = sv_table$svtype %in% BND_SVTYPES + # `start`/`end` are padded by the window and are what foverlaps() matches on. `qlo`/`qhi` + # carry the *unpadded* locus through the join: the gap to a gene has to be measured from + # where the breakend actually is, not from the edge of the window around it. + q = rbindlist(list( + # Each breakend of a BND gets its own window; the two sides can be on + # different contigs, so they cannot be one interval. + data.table(row = which(is_bnd), side = "A", + chrom = sv_table$chrom_a[is_bnd], + start = sv_table$pos_a[is_bnd] - bnd_window, + end = sv_table$pos_a[is_bnd] + bnd_window, + qlo = sv_table$pos_a[is_bnd], + qhi = sv_table$pos_a[is_bnd]), + data.table(row = which(is_bnd), side = "B", + chrom = sv_table$chrom_b[is_bnd], + start = sv_table$pos_b[is_bnd] - bnd_window, + end = sv_table$pos_b[is_bnd] + bnd_window, + qlo = sv_table$pos_b[is_bnd], + qhi = sv_table$pos_b[is_bnd]), + # Other types are contiguous: one window around the whole span. + data.table(row = which(!is_bnd), side = "span", + chrom = sv_table$chrom_a[!is_bnd], + start = pmin(sv_table$pos_a[!is_bnd], sv_table$pos_b[!is_bnd]) - other_window, + end = pmax(sv_table$pos_a[!is_bnd], sv_table$pos_b[!is_bnd]) + other_window, + qlo = pmin(sv_table$pos_a[!is_bnd], sv_table$pos_b[!is_bnd]), + qhi = pmax(sv_table$pos_a[!is_bnd], sv_table$pos_b[!is_bnd])) + )) + q = q[!is.na(chrom) & !is.na(start) & !is.na(end)] + if (nrow(q) == 0) return(rep("", n)) + q[, start := pmax(as.numeric(start), 0)] + q[, end := as.numeric(end)] + q[, `:=`(qlo = as.numeric(qlo), qhi = as.numeric(qhi))] + iv = copy(iv) + iv[, `:=`(start = as.numeric(start), end = as.numeric(end))] + setkey(iv, chrom, start, end) + + ov = data.table::foverlaps(q, iv, by.x = c("chrom", "start", "end"), + type = "any", nomatch = NULL) + if (nrow(ov) == 0) return(rep("", n)) + + # `start`/`end` are the gene's own interval here — foverlaps() renamed the padded query + # window to i.start/i.end. A zero gap means the locus lands inside the gene. + ov[, gap := pmax(0, pmax(start - qhi, qlo - end))] + ov[, how := fifelse(gap == 0, "direct", fmt_bp(gap))] + + per_row = ov[, .(hit = paste(unique(label(gene, side, how)), collapse = ", ")), by = row] + out = rep("", n) + out[per_row$row] = per_row$hit + out } # Parse the gene-annotated Severus TSV (filtered_SV2/SV_filtered_with_gene_annotations.tsv) @@ -180,50 +491,38 @@ parse_severus_gene_tsv = function(tsv_file) { dt } -# Build the SV display table. -# gene_panel: character vector of HGNC symbols to keep, or NULL to return all SVs (one row each). -build_sv_table = function(sv_tsv, gene_panel = NULL) { +# Build the SV display table from the gene-annotated Severus TSV — the fallback for +# pipelines that produce it instead of a VEP SV VCF. One row per SV, mapped onto the +# same column contract as the VEP path, so the panel filter, the counts and the section +# template have one schema to know about (and so this path gets coordinate matching too). +build_sv_table = function(sv_tsv) { if (is.null(sv_tsv) || nrow(sv_tsv) == 0) return(data.table()) - sv_tsv = copy(sv_tsv) - - # When no panel is supplied, return one row per SV without explosion - if (is.null(gene_panel)) { - display_cols = intersect( - c("ID", "SVTYPE", "DETAILED_TYPE", - "START_CHROM", "START_POS", "END_CHROM", "END_POS", - "SV_LEN", "VAF", "NHL_GENE_HITS", "COSMIC_GENE_HITS", - "NHL_NEAREST_GENE_HITS_1MBWINDOW"), - names(sv_tsv) - ) - return(sv_tsv[, ..display_cols]) - } - - # Panel-filtered path: explode multi-gene gene_hits, filter, return one row per gene×SV - sv_tsv[, .ridx := .I] - - sv_long = sv_tsv[, { - raw = as.character(gene_hits[1]) - genes = unique(trimws(unlist(strsplit(raw, "[;,]+")))) - genes = genes[nchar(genes) > 0 & genes != "-" & toupper(genes) != "NA"] - if (length(genes) == 0) genes = NA_character_ - list(gene = genes) - }, by = .ridx] - - sv_long = merge(sv_long, sv_tsv, by = ".ridx") - sv_long[, .ridx := NULL] - sv_tsv[, .ridx := NULL] - - # Filter by gene panel (always, even if panel is empty) - sv_long = sv_long[!is.na(gene) & gene %in% gene_panel] - if (nrow(sv_long) == 0) return(data.table()) - - display_cols = intersect( - c("gene", "ID", "SVTYPE", "DETAILED_TYPE", - "START_CHROM", "START_POS", "END_CHROM", "END_POS", - "SV_LEN", "VAF", "NHL_GENE_HITS", "COSMIC_GENE_HITS", - "NHL_NEAREST_GENE_HITS_1MBWINDOW"), - names(sv_long) + col = function(nm, default = NA) if (nm %in% names(sv_tsv)) sv_tsv[[nm]] else default + chrom_a = ensure_chr_prefix(as.character(col("START_CHROM", NA_character_))) + chrom_b = ensure_chr_prefix(as.character(col("END_CHROM", NA_character_))) + svtype = as.character(col("SVTYPE", NA_character_)) + + data.table( + id = as.character(col("ID", NA_character_)), + svclass = fcase( + is.na(chrom_a) | is.na(chrom_b), svtype, + svtype %in% BND_SVTYPES & chrom_a != chrom_b, "translocation", + svtype %in% BND_SVTYPES, "intra-chr breakend", + default = svtype + ), + svtype = svtype, + chrom_a = chrom_a, + pos_a = suppressWarnings(as.integer(col("START_POS", NA_integer_))), + # The TSV's gene hits are span-level, with no per-breakend split — hence side A only. + gene_a = as.character(if ("NHL_GENE_HITS" %in% names(sv_tsv)) sv_tsv$NHL_GENE_HITS + else col("COSMIC_GENE_HITS", NA_character_)), + chrom_b = chrom_b, + pos_b = suppressWarnings(as.integer(col("END_POS", NA_integer_))), + gene_b = NA_character_, + sv_len = suppressWarnings(as.integer(col("SV_LEN", NA_integer_))), + vaf = suppressWarnings(as.numeric(col("VAF", NA_real_))), + consequence = as.character(col("DETAILED_TYPE", NA_character_)), + impact = NA_character_ ) - sv_long[, ..display_cols] } diff --git a/assets/lrsomatic_report/R/parse_smallvariants.R b/assets/lrsomatic_report/R/parse_smallvariants.R index f0c837b5..470f6e09 100644 --- a/assets/lrsomatic_report/R/parse_smallvariants.R +++ b/assets/lrsomatic_report/R/parse_smallvariants.R @@ -104,6 +104,17 @@ parse_vep_text = function(vep_file) { # with parse_vep_vcf(). dt[, caller := NA_character_] + # Record identity, for contract parity with parse_vep_vcf()'s `id`. The text format + # has no VCF ID column, so this is VEP's own Uploaded_variation name — never a + # caller-assigned ID, which is why the SV table's ID-keyed join only ever sees the + # VCF path. + dt[, id := variant_id] + + # chrom/pos/ref/alt here come from `variant_id`, i.e. VEP's own notation — indels + # already shifted one base right and possibly dash-form. See coord_space in + # build_variant_table(). + dt[, coord_space := "vep"] + dt } @@ -172,12 +183,16 @@ parse_vep_vcf = function(vep_file) { # A variant reported by two somatic callers appears as two records; collapse to one # row per variant so the CSQ expansion below doesn't duplicate every annotation. - dt = dt[, .(CSQ = CSQ[1], caller = paste(sort(unique(caller)), collapse = ",")), + # ID rides along: it is what links an SV annotation record back to the caller record + # it came from (see build_sv_table_from_vep()), and is more reliable than the locus. + dt = dt[, .(CSQ = CSQ[1], id = ID[1], + caller = paste(sort(unique(caller)), collapse = ",")), by = .(CHROM, POS, REF, ALT)] + dt[id == ".", id := NA_character_] # One row per gene/transcript annotation (comma-separated CSQ entries) dt_long = dt[, .(csq_entry = unlist(strsplit(CSQ, ",", fixed = TRUE))), - by = .(CHROM, POS, REF, ALT, caller)] + by = .(CHROM, POS, REF, ALT, id, caller)] if (nrow(dt_long) == 0) return(NULL) # Split each entry on "|", materialising only the fields actually used below @@ -215,46 +230,123 @@ parse_vep_vcf = function(vep_file) { dt_long[, ref := REF] dt_long[, alt := ALT] + # Unlike parse_vep_text(), these are the VCF's own POS/REF/ALT — the *original* input + # coordinate, not VEP's shifted one. Keying them as VEP-space is what stopped every + # indel on this path from joining. See build_variant_table(). + dt_long[, coord_space := "vcf"] + # dbSNP / COSMIC IDs, derived from Existing_variation dt_long = derive_dbsnp_cosmic(dt_long) - dt_long[, .(chrom, pos, ref, alt, symbol, gene_id, consequence, impact, hgvsp, - existing, dbsnp, cosmic, sift, polyphen, caller)] + dt_long[, .(chrom, pos, ref, alt, id, symbol, gene_id, consequence, impact, hgvsp, + existing, dbsnp, cosmic, sift, polyphen, caller, coord_space)] +} + +# Which sample column of a VCF to read, given its sample names. +# One sample is unambiguous; otherwise prefer the one named for this report, then the +# first that is not obviously the normal. Falling through to the first column is a guess, +# so say so — silently reporting the normal's VAF is the failure this exists to prevent. +pick_sample_column = function(samples, sample_id = NULL, vcf_file = "") { + if (length(samples) == 1L) return(1L) + + if (!is.null(sample_id) && nzchar(sample_id)) { + i = match(sample_id, samples) + if (!is.na(i)) return(i) + } + not_normal = which(!grepl("^(normal|blood|germline|ref)", samples, ignore.case = TRUE)) + if (length(not_normal) > 0) { + if (length(not_normal) > 1L || is.null(sample_id)) + warning("VCF '", vcf_file, "' has samples [", paste(samples, collapse = ", "), + "]; reading '", samples[not_normal[1]], "'.") + return(not_normal[1]) + } + warning("VCF '", vcf_file, "' has samples [", paste(samples, collapse = ", "), + "] and none looks like a tumour; reading '", samples[1], "'.") + 1L +} + +# The allele fraction, from whichever FORMAT tag this VCF happens to use. +# +# LRSomatic *renames* this tag according to which caller was prioritised — see +# STANDARDIZE_AF in subworkflows/local/small_variant_consensus.nf: AF -> VAF for +# deepvariant/deepsomatic, VAF -> AF for clair — and a `consensus` merge skips the +# renaming entirely, so both names reach this parser. Hard-coding "AF" is what produced +# an entirely empty VAF column on a DeepSomatic run. AD is the last resort: every caller +# in the pipeline emits it, and it is the only recoverable source when neither tag exists. +allele_fraction = function(field, fields) { + for (tag in c("AF", "VAF")) { + if (tag %in% fields) { + # Multi-allelic records carry one value per ALT; the first is the one that pairs + # with the record's first ALT allele, which is all this table joins on. + v = suppressWarnings(as.numeric(sub(",.*$", "", field(tag)))) + if (any(!is.na(v))) return(v) + } + } + if ("AD" %in% fields) { + ad = strsplit(field("AD"), ",", fixed = TRUE) + return(vapply(ad, function(x) { + n = suppressWarnings(as.numeric(x)) + if (length(n) < 2L || anyNA(n[1:2]) || sum(n[1:2]) == 0) return(NA_real_) + n[2] / sum(n[1:2]) + }, numeric(1))) + } + rep(NA_real_, length(field("GT"))) } # Parse a raw caller VCF for variant coordinates, VAF, depth and phasing. # `vcf_file` may be several paths, in which case they are read and stacked into one # table — ClairS splits its matched-mode output into snvs.vcf.gz + indel.vcf.gz. # Returns data.table with: chrom, pos, ref, alt, vaf, dp, gt, ps, caller -parse_caller_vcf = function(vcf_file, caller_name = "unknown") { +# +# `sample_id` picks the sample column by name. Reading it positionally reports the +# *normal's* VAF/DP on a matched two-sample VCF — wrong numbers, no error, and nothing in +# the report that would reveal it. +parse_caller_vcf = function(vcf_file, caller_name = "unknown", sample_id = NULL) { if (is.null(vcf_file)) return(NULL) if (length(vcf_file) > 1) { - parts = lapply(vcf_file, parse_caller_vcf, caller_name = caller_name) + parts = lapply(vcf_file, parse_caller_vcf, caller_name = caller_name, + sample_id = sample_id) parts = parts[!vapply(parts, is.null, logical(1))] return(if (length(parts) > 0) rbindlist(parts) else NULL) } if (!file.exists(vcf_file)) return(NULL) - # Count header lines + # Count header lines, keeping the #CHROM line itself — it carries the sample names. con = gzfile(vcf_file, "rb") skip_n = 0L + chrom_line = NULL repeat { line = readLines(con, n = 1, warn = FALSE) if (length(line) == 0) break - if (startsWith(line, "#CHROM")) break + if (startsWith(line, "#CHROM")) { chrom_line = line; break } skip_n = skip_n + 1L } close(con) + if (is.null(chrom_line)) return(NULL) - # Read up to 10 columns (standard VCF single-sample layout) - col_names = c("CHROM", "POS", "ID", "REF", "ALT", "QUAL", "FILTER", "INFO", "FORMAT", "SAMPLE1") - dt = fread(vcf_file, skip = skip_n + 1L, sep = "\t", header = FALSE, - select = 1:10, col.names = col_names) - if (nrow(dt) == 0) return(NULL) + header = strsplit(chrom_line, "\t", fixed = TRUE)[[1]] + samples = if (length(header) > 9L) header[10:length(header)] else character(0) + # A sites-only VCF has no genotypes to read. Return NULL rather than letting fread() + # error on `select = 1:10` — every other parser here degrades gracefully. + if (length(samples) == 0) return(NULL) + + sample_col = pick_sample_column(samples, sample_id, vcf_file) + + # 1:9 are the fixed VCF columns; the chosen sample sits at 9 + its index. + col_names = c("CHROM", "POS", "ID", "REF", "ALT", "QUAL", "FILTER", "INFO", "FORMAT", + "SAMPLE1") + dt = tryCatch( + fread(vcf_file, skip = skip_n + 1L, sep = "\t", header = FALSE, + select = c(1:9, 9L + sample_col), col.names = col_names), + error = function(e) { + warning("Could not read VCF '", vcf_file, "': ", conditionMessage(e)) + NULL + }) + if (is.null(dt) || nrow(dt) == 0) return(NULL) dt[, CHROM := ensure_chr_prefix(CHROM)] - # Extract AF, DP, GT and PS from the FORMAT + SAMPLE1 columns. + # Extract the allele fraction, DP, GT and PS from the FORMAT + SAMPLE1 columns. # Work format-group by format-group to avoid splitting every single row redundantly. fmt_groups = unique(dt$FORMAT) vaf_list = rep(NA_real_, nrow(dt)) @@ -275,7 +367,7 @@ parse_caller_vcf = function(vcf_file, caller_name = "unknown") { character(1)) } - vaf_list[idx_rows] = suppressWarnings(as.numeric(field("AF"))) + vaf_list[idx_rows] = allele_fraction(field, fields) dp_list[idx_rows] = suppressWarnings(as.integer(field("DP"))) gt_list[idx_rows] = field("GT") ps_list[idx_rows] = field("PS") @@ -291,6 +383,78 @@ parse_caller_vcf = function(vcf_file, caller_name = "unknown") { caller = caller_name) } +# Header-only provenance for the VCF(s) that supplied VAF/DP/GT/PS. +# +# Which file this is matters to a reader: when the pipeline ran several somatic callers +# through consensus, the FORMAT fields in the surviving VCF come from whichever caller won +# each merge, so they need not correspond to the `callers` column beside them. The report +# names the file rather than silently implying one caller — see the footnote in +# templates/sections/_smallvariants.qmd. +# +# `vcf_files` may be several paths (parse_caller_vcf() stacks them; ClairS splits matched +# mode into snvs.vcf.gz + indel.vcf.gz). Returns NULL when there is nothing to describe, +# matching the graceful-missing contract of the parsers above. Only the header is read — +# no record parsing — so this stays cheap on a multi-million-variant VCF. +vaf_provenance = function(vcf_files, sample_dir = NULL, sample_id = NULL) { + if (is.null(vcf_files) || length(vcf_files) == 0) return(NULL) + vcf_files = vcf_files[!is.na(vcf_files) & nzchar(vcf_files)] + if (length(vcf_files) == 0) return(NULL) + + # Paths read better relative to the sample directory the caller passed in. + rel = function(p) { + if (is.null(sample_dir) || !nzchar(sample_dir)) return(p) + root = sub("/+$", "", normalizePath(sample_dir, mustWork = FALSE)) + full = normalizePath(p, mustWork = FALSE) + if (startsWith(full, paste0(root, "/"))) substring(full, nchar(root) + 2L) else p + } + + # "##source=Clair3" and friends. Plenty of VCFs carry no source line at all, which is why + # the footnote treats this as optional colour rather than the identifying fact. + read_sources = function(p) { + if (!file.exists(p)) return(character(0)) + con = gzfile(p, "rb") + on.exit(close(con), add = TRUE) + out = character(0) + repeat { + line = readLines(con, n = 1, warn = FALSE) + if (length(line) == 0) break + if (!startsWith(line, "##")) break # #CHROM or a malformed header ends the scan + if (startsWith(line, "##source=")) out = c(out, sub("^##source=", "", line)) + } + out + } + + # Which sample column parse_caller_vcf() will have read. Worth naming in the footnote: + # on a matched two-sample VCF the wrong pick reports the normal's VAF for every somatic + # variant, and nothing else in the report would show it. + read_sample = function(p) { + if (!file.exists(p)) return(NA_character_) + con = gzfile(p, "rb") + on.exit(close(con), add = TRUE) + repeat { + line = readLines(con, n = 1, warn = FALSE) + if (length(line) == 0) return(NA_character_) + if (startsWith(line, "#CHROM")) { + header = strsplit(line, "\t", fixed = TRUE)[[1]] + if (length(header) <= 9L) return(NA_character_) + samples = header[10:length(header)] + # Only worth reporting when there was a choice to get wrong. + if (length(samples) == 1L) return(NA_character_) + return(samples[suppressWarnings(pick_sample_column(samples, sample_id, p))]) + } + if (!startsWith(line, "##")) return(NA_character_) + } + } + chosen = unique(unlist(lapply(vcf_files, read_sample))) + chosen = chosen[!is.na(chosen)] + + list( + paths = unname(vapply(vcf_files, rel, character(1))), + sources = unique(unlist(lapply(vcf_files, read_sources))), + sample = chosen + ) +} + # Canonical variant key, used to join VEP annotation rows to the VCF they came from. # # VEP always reports an indel one base to the right of the VCF anchor, and writes the @@ -371,7 +535,14 @@ build_variant_table = function(vep_data, vaf_data, gene_panel = NULL) { # Join VEP rows to the VCF they were produced from, via the canonical key that # reconciles the two sides' indel representations (see variant_key()). - vep_data[, join_key := variant_key(chrom, pos, ref, alt, space = "vep")] + # + # The space is the parser's to declare, not ours to assume: parse_vep_text() returns + # VEP-space coordinates (read from `variant_id`), parse_vep_vcf() returns the VCF's own + # POS/REF/ALT. Hard-coding "vep" here shifted only one side of the key on the CSQ path, + # so no indel could ever match and its VAF/DP/GT/PS came back silently NA. + vep_space = if ("coord_space" %in% names(vep_data)) unique(vep_data$coord_space) else "vep" + stopifnot(length(vep_space) == 1L, vep_space %in% c("vep", "vcf")) + vep_data[, join_key := variant_key(chrom, pos, ref, alt, space = vep_space)] if (!is.null(vaf_data) && nrow(vaf_data) > 0) { vdt = vaf_data[, .(join_key = variant_key(chrom, pos, ref, alt, space = "vcf"), diff --git a/assets/lrsomatic_report/R/sections.R b/assets/lrsomatic_report/R/sections.R index d778dc0a..f72307dd 100644 --- a/assets/lrsomatic_report/R/sections.R +++ b/assets/lrsomatic_report/R/sections.R @@ -12,3 +12,17 @@ register_section = function(descriptor) { section_notice = function(msg) { tags$div(class = "alert alert-info", msg) } + +# Provenance and caveats that belong under a table, folded away by default. +# +# This text is the only thing that surfaces a silently broken join — "0 of 49,003 +# variants carry a VAF" is how the AF/VAF tag bug was caught — so it is collapsed rather +# than deleted. `summary` names what is inside, so a reader can tell whether it is worth +# opening without opening it. +table_details = function(..., summary = "Details") { + tags$details( + class = "table-details", + tags$summary(summary), + tags$div(class = "table-footnote", ...) + ) +} diff --git a/assets/lrsomatic_report/R/sections/sv.R b/assets/lrsomatic_report/R/sections/sv.R index 54bc6380..3bea1331 100644 --- a/assets/lrsomatic_report/R/sections/sv.R +++ b/assets/lrsomatic_report/R/sections/sv.R @@ -28,29 +28,36 @@ register_section(list( parse = function(inputs, section_data) { tabs = list() circ = list(nontrans = data.table(), translocations = data.table()) - any_annotation = FALSE + annotation_path = NULL for (nm in names(inputs$callers)) { caller_inputs = inputs$callers[[nm]] - v = parse_severus_vcf(caller_inputs$vcf) - # VEP SV VCF is the primary annotation source; the gene-annotated TSV (not produced - # by most pipelines) is a fallback for samples that have it instead. - if (!is.null(caller_inputs$vep_vcf)) { - t = build_sv_table_from_vep(caller_inputs$vcf, caller_inputs$vep_vcf) + # One parse of the caller VCF feeds both the table and the circos tracks, so the + # SV count, the panel filter and the drawn links cannot disagree about what a + # rearrangement is. + records = parse_severus_somatic_records(caller_inputs$vcf) + + if (nrow(records) > 0) { + # The VEP SV VCF only supplies the per-breakend display symbols; a missing one + # leaves those columns empty rather than losing the SVs, because panel matching + # is done on coordinates (see sv_panel_hits()). + t = build_sv_table_from_vep(records, caller_inputs$vep_vcf) + if (!is.null(caller_inputs$vep_vcf)) annotation_path = caller_inputs$vep_vcf } else { - g = parse_severus_gene_tsv(caller_inputs$gene_tsv) - t = build_sv_table(g, gene_panel = NULL) + # No caller VCF: the gene-annotated TSV is the fallback for pipelines that + # publish it instead. + t = build_sv_table(parse_severus_gene_tsv(caller_inputs$gene_tsv)) + if (nrow(t) > 0) annotation_path = caller_inputs$gene_tsv } + if (!is.null(t) && nrow(t) > 0) { - any_annotation = TRUE t[, caller := nm] tabs[[nm]] = t } - # Circos tracks are drawn from raw breakpoints, not the gene table; + # Circos tracks are drawn from the same mate-collapsed records as the table; # with a single caller today, last-write-wins is a no-op. - circ$nontrans = v$nontrans - circ$translocations = v$translocations + circ = severus_circos_tracks(records) } tbl = if (length(tabs) > 0) rbindlist(tabs, fill = TRUE) else data.table() @@ -58,7 +65,8 @@ register_section(list( list( table = tbl, circos = circ, - annotation_found = any_annotation + annotation_path = annotation_path, + annotation_found = !is.null(annotation_path) ) } )) diff --git a/assets/lrsomatic_report/R/utils.R b/assets/lrsomatic_report/R/utils.R index 127e040f..404c7e54 100644 --- a/assets/lrsomatic_report/R/utils.R +++ b/assets/lrsomatic_report/R/utils.R @@ -2,8 +2,14 @@ suppressPackageStartupMessages({ library(data.table) }) +# Type-stable: fread() reads a bare-contig CHROM column ("1", "2", ...) as integer, and +# ifelse() on an all-NA test returns logical NA — both of which break every downstream +# string comparison, so coerce on the way in and on the way out. ensure_chr_prefix = function(x) { - ifelse(startsWith(x, "chr"), x, paste0("chr", x)) + x = as.character(x) + out = as.character(ifelse(startsWith(x, "chr"), x, paste0("chr", x))) + out[is.na(x)] = NA_character_ + out } strip_chr_prefix = function(x) { @@ -28,23 +34,166 @@ extract_extra_key = function(extra_vec, key) { }, character(1), USE.NAMES = FALSE) } -# Load a gene panel TSV or plain text file; returns character vector of gene symbols -load_gene_panel = function(path) { +# ---- Gene panels --------------------------------------------------------- +# +# A panel is a plain list — not a data.table — because it round-trips through +# Quarto's execute_params as YAML (see bin/render_report.R): +# +# list(name, path, reference, has_coords, genes, +# chrom, start, end, interval_gene) # the last four only when has_coords +# +# `genes` is deduplicated, for symbol matching; the interval vectors are parallel and +# not deduplicated, because one symbol can legitimately carry several loci. +# +# `has_coords` selects the SV matching mode: a panel carrying chrom/start/end is +# matched against SV breakend coordinates with a window (sv_panel_hits()), while a +# symbol-only panel can only be matched against the per-side VEP symbols. Small +# variants always match on `genes` — VEP annotates SNVs reliably, and per-gene +# symbols are the right semantics there — so one file serves both tables. + +# Canonical reference names. Panel coordinates are only valid for one reference, so +# a coordinate-carrying panel has to declare which, and the declaration is compared +# against the reference the report was rendered for. +normalise_reference_name = function(x) { + if (is.null(x) || length(x) != 1 || is.na(x) || !nzchar(trimws(x))) return(NA_character_) + x = tolower(trimws(x)) + if (x %in% c("t2t", "chm13", "chm13v2", "chm13v2.0", "t2t-chm13")) return("t2t") + if (x %in% c("hg38", "grch38", "hg38-noalt")) return("hg38") + x +} + +# Reference suffix in a builtin panel filename ("lymphoid.hg38.tsv" -> "hg38"). +# Returns list(name, reference); reference is NA for an unsuffixed file. +.split_panel_filename = function(path) { + stem = tools::file_path_sans_ext(basename(path)) + parts = strsplit(stem, ".", fixed = TRUE)[[1]] + if (length(parts) >= 2) { + ref = normalise_reference_name(parts[length(parts)]) + if (!is.na(ref) && ref %in% c("t2t", "hg38")) + return(list(name = paste(parts[-length(parts)], collapse = "."), reference = ref)) + } + list(name = stem, reference = NA_character_) +} + +# Read the leading "#"-comment block of a panel TSV, which may declare the reference +# the coordinates belong to ("# reference: hg38"). +.panel_header = function(path) { + lines = readLines(path, warn = FALSE) + n_comment = 0L + declared = NA_character_ + for (ln in lines) { + if (!startsWith(ln, "#")) break + n_comment = n_comment + 1L + m = regmatches(ln, regexpr("^#\\s*reference\\s*:\\s*\\S+", ln)) + if (length(m) > 0) declared = sub("^#\\s*reference\\s*:\\s*", "", m) + } + list(n_comment = n_comment, reference = declared) +} + +# Load a gene panel TSV. Required: a `gene` column (or a single unnamed column of +# symbols). Optional but all-or-nothing: `chrom` (or `chr`), `start`, `end` — a file +# carrying some but not all three is malformed and errors rather than silently +# downgrading to symbol matching. +# +# `reference` is the reference the report is being rendered for. A coordinate-carrying +# panel that declares a different one errors; one that declares none loads with +# reference "" ("unverified" in the section footnote) rather than being guessed at. +load_gene_panel = function(path, reference = NULL) { if (!file.exists(path)) stop("Gene panel file not found: ", path) + + hdr = .panel_header(path) dt = tryCatch( - fread(path, header = TRUE, sep = "\t", fill = TRUE), - error = function(e) fread(path, header = FALSE, sep = "\t", fill = TRUE) + fread(path, header = TRUE, sep = "\t", fill = TRUE, skip = hdr$n_comment), + error = function(e) fread(path, header = FALSE, sep = "\t", fill = TRUE, + skip = hdr$n_comment) + ) + if (nrow(dt) == 0 && ncol(dt) == 0) stop("Gene panel file is empty: ", path) + + # copy(): setnames() rewrites the names vector in place, which would otherwise + # clobber this reference to it. + orig_names = copy(names(dt)) + setnames(dt, tolower(names(dt))) + setnames(dt, old = c("chr", "chromosome"), new = c("chrom", "chrom"), skip_absent = TRUE) + + if ("gene" %in% names(dt)) { + genes = as.character(dt[["gene"]]) + } else if (ncol(dt) == 1) { + # A headerless one-column list: fread consumed the first symbol as the column + # name, so put it back — with its original casing — rather than dropping it. + genes = c(orig_names[1], as.character(dt[[1]])) + } else { + genes = as.character(dt[[1]]) + } + genes = trimws(genes) + keep = nzchar(genes) & !is.na(genes) & genes != "-" + + coord_cols = c("chrom", "start", "end") + present = intersect(coord_cols, names(dt)) + if (length(present) > 0 && length(present) < 3) { + stop("Gene panel ", path, " carries coordinate column(s) ", + paste(present, collapse = ", "), " but not all of ", + paste(coord_cols, collapse = ", "), + ". Supply all three, or none for symbol-only matching.") + } + has_coords = length(present) == 3 + + # A `reference` column is an alternative to the "# reference:" comment line. + declared = hdr$reference + if (is.na(declared) && "reference" %in% names(dt)) { + vals = unique(trimws(as.character(dt[["reference"]]))) + vals = vals[nzchar(vals) & !is.na(vals)] + if (length(vals) > 1) + stop("Gene panel ", path, " declares more than one reference: ", + paste(vals, collapse = ", ")) + if (length(vals) == 1) declared = vals + } + declared_norm = normalise_reference_name(declared) + want = normalise_reference_name(reference) + + # Only coordinate panels are reference-specific; a symbol-only panel is agnostic. + if (has_coords && !is.na(declared_norm) && !is.na(want) && declared_norm != want) { + stop("Gene panel ", path, " declares reference '", declared_norm, + "' but the report is being rendered against '", want, + "'. Panel coordinates are only valid for the reference they were built on.") + } + + out = list( + name = .split_panel_filename(path)$name, + path = path, + reference = if (is.na(declared_norm)) "" else declared_norm, + has_coords = has_coords, + genes = unique(genes[keep]) ) - gene_col = if ("gene" %in% tolower(names(dt))) names(dt)[tolower(names(dt)) == "gene"][1] else names(dt)[1] - unique(dt[[gene_col]]) + + if (has_coords) { + chrom = ensure_chr_prefix(trimws(as.character(dt[["chrom"]]))) + start = suppressWarnings(as.integer(dt[["start"]])) + end = suppressWarnings(as.integer(dt[["end"]])) + bad = keep & (is.na(chrom) | !nzchar(chrom) | is.na(start) | is.na(end)) + if (any(bad)) + stop("Gene panel ", path, " has missing or non-numeric coordinates for: ", + paste(utils::head(genes[bad], 5), collapse = ", "), + if (sum(bad) > 5) paste0(" (and ", sum(bad) - 5L, " more)") else "") + out$chrom = chrom[keep] + out$start = start[keep] + out$end = end[keep] + # `genes` is deduplicated for symbol matching; the interval vectors are not, + # because one symbol can legitimately carry several loci. + out$interval_gene = genes[keep] + } + + out } -# Filter a data frame to rows where the gene column matches the panel. -# panel_genes = NULL means "no panel" and returns dt untouched; an empty -# character vector is a genuinely empty panel and filters everything out. -filter_by_gene_panel = function(dt, panel_genes, gene_col = "gene") { - if (is.null(panel_genes)) return(dt) - dt[dt[[gene_col]] %in% panel_genes, ] +# Panel intervals as a keyed data.table, or NULL for a symbol-only panel. +panel_intervals = function(panel) { + if (is.null(panel) || !isTRUE(panel$has_coords)) return(NULL) + dt = data.table(gene = as.character(panel$interval_gene), + chrom = as.character(panel$chrom), + start = as.integer(panel$start), + end = as.integer(panel$end)) + setkey(dt, chrom, start, end) + dt } # Is a --gene-panel argument the "no filtering" sentinel? @@ -53,27 +202,106 @@ is_no_gene_panel = function(panel_arg) { identical(tolower(trimws(panel_arg)), "none") } +# Path of a builtin panel, preferring the variant built for `reference` +# ("lymphoid.hg38.tsv") over an unsuffixed one ("lymphoid.tsv"). NULL if neither exists. +builtin_panel_path = function(assets_dir, name, reference = NULL) { + ref = normalise_reference_name(reference) + dir = file.path(assets_dir, "gene_lists") + candidates = c(if (!is.na(ref)) file.path(dir, paste0(name, ".", ref, ".tsv")), + file.path(dir, paste0(name, ".tsv"))) + hit = candidates[file.exists(candidates)] + if (length(hit) > 0) hit[1] else NULL +} + # Resolve a --gene-panel arg: the "none" sentinel (no filtering, returns NULL), # a builtin name ("lymphoid"), or a path to a TSV. A value that is neither is an # error rather than a silent fall-through to unfiltered output. -resolve_gene_panel = function(panel_arg, assets_dir) { +resolve_gene_panel = function(panel_arg, assets_dir, reference = NULL) { if (is_no_gene_panel(panel_arg)) return(NULL) - builtin_path = file.path(assets_dir, "gene_lists", paste0(panel_arg, ".tsv")) - if (file.exists(builtin_path)) return(load_gene_panel(builtin_path)) - if (file.exists(panel_arg)) return(load_gene_panel(panel_arg)) + builtin = builtin_panel_path(assets_dir, panel_arg, reference) + if (!is.null(builtin)) return(load_gene_panel(builtin, reference)) + if (file.exists(panel_arg)) return(load_gene_panel(panel_arg, reference)) stop("Gene panel not found (tried builtin '", panel_arg, "' and as file path)") } -# Load all gene panels from assets/gene_lists/*.tsv -# Returns a named list (name = panel name, value = character vector of gene symbols) -load_all_gene_panels = function(assets_dir) { +# Load all gene panels from assets/gene_lists/*.tsv, resolving reference-specific +# files ("lymphoid.hg38.tsv" / "lymphoid.t2t.tsv") to one selectable "lymphoid" entry. +# A panel that only ships for other references is skipped — offering it would mean +# matching coordinates from the wrong genome. +# Returns a named list of panel objects (see load_gene_panel()). +load_all_gene_panels = function(assets_dir, reference = NULL) { tsv_files = Sys.glob(file.path(assets_dir, "gene_lists", "*.tsv")) if (length(tsv_files) == 0) return(list()) - panels = lapply(tsv_files, load_gene_panel) - names(panels) = tools::file_path_sans_ext(basename(tsv_files)) + + meta = lapply(tsv_files, .split_panel_filename) + ref = normalise_reference_name(reference) + + panels = list() + for (nm in unique(vapply(meta, `[[`, character(1), "name"))) { + idx = which(vapply(meta, `[[`, character(1), "name") == nm) + refs = vapply(meta[idx], function(m) m$reference, character(1)) + pick = if (!is.na(ref) && any(refs == ref, na.rm = TRUE)) idx[which(refs == ref)[1]] + else if (any(is.na(refs))) idx[which(is.na(refs))[1]] + else NA_integer_ + if (is.na(pick)) { + message("Gene panel '", nm, "' ships only for reference(s) ", + paste(unique(refs), collapse = ", "), " — not offered for ", + if (is.na(ref)) "an unknown reference" else ref) + next + } + p = tryCatch(load_gene_panel(tsv_files[pick], reference), + error = function(e) { + message("Skipping gene panel '", nm, "': ", conditionMessage(e)); NULL }) + if (!is.null(p)) panels[[nm]] = p + } panels } +# ---- Small JS serialisation helpers -------------------------------------- +# The report ships panel data and column positions to its own client-side filter. +# Keeping these here means the R table and the JS that indexes it are generated +# from the same object, instead of the JS re-deriving positions from the rendered +# header (which breaks under DT's filter row and Scroller's cloned thead). + +js_quote = function(x) paste0('"', gsub('"', '\\\\"', as.character(x)), '"') + +# {"col":0,"other":1} — column name to zero-based index, for a rownames=FALSE DT. +js_col_index_map = function(nms) { + if (length(nms) == 0) return("{}") + paste0("{", paste0(js_quote(nms), ":", seq_along(nms) - 1L, collapse = ","), "}") +} + +# One JS number literal per element. Element-wise rather than vectorised because +# format() on a vector pads every element to a common format — c(1, 2.5) becomes +# "1.0","2.5" — and scientific notation on a base-pair coordinate would read back as a +# different number than the one it names. +js_num = function(x) { + vapply(x, function(v) { + if (is.na(v)) "null" else format(v, scientific = FALSE, trim = TRUE) + }, character(1), USE.NAMES = FALSE) +} + +.js_cell = function(v) if (is.numeric(v)) js_num(v) else ifelse(is.na(v), "null", js_quote(v)) + +# A JS array literal from an atomic vector. Numbers are emitted bare, everything else +# quoted; NA becomes null so the client can test for it. Written by hand rather than with +# jsonlite because that would be a new dependency in both recipe/meta.yaml and the +# pipeline's environment.yml — see the "R package requirements" note in CLAUDE.md. +js_vec = function(x) { + if (length(x) == 0) return("[]") + paste0("[", paste(.js_cell(x), collapse = ","), "]") +} + +# A JS array of arrays, one inner array per row of `dt`, columns in `cols` order. +# Row-major and positional: far smaller than an array of objects, which matters when the +# payload is a few thousand cytobands inlined into a self-contained HTML file. +js_rows = function(dt, cols) { + if (is.null(dt) || nrow(dt) == 0) return("[]") + cells = lapply(cols, function(cl) .js_cell(dt[[cl]])) + rows = do.call(paste, c(cells, sep = ",")) + paste0("[[", paste(rows, collapse = "],["), "]]") +} + # Format a number for human-readable display fmt_bp = function(x) { x = as.numeric(x) diff --git a/assets/lrsomatic_report/README.md b/assets/lrsomatic_report/README.md index 6d5efe0d..a6d981a6 100644 --- a/assets/lrsomatic_report/README.md +++ b/assets/lrsomatic_report/README.md @@ -4,10 +4,10 @@ Standalone reporting tool for the [LRSomatic](https://github.com/nf-core/lrsomat - **Summary header**: purity, ploidy, coverage, N50, variant counts - **Circos plot**: somatic SNVs (6-class SBS colours), non-BND SVs, ASCAT copy number, translocation links +- **Breakend circos**: a second circos over just the chromosomes a breakend touches, with panel gene bodies and names; selecting a row in the SV table highlights that rearrangement's arc, and the gene-panel filter dims the arcs it hides - **Interactive variant table**: VEP-annotated somatic small variants, optionally filtered to a gene panel, with VAF, depth and phasing -- **Interactive SV table**: Severus structural variants annotated with gene overlaps, sharing the same gene-panel filter -- **Phasing**: per-chromosome WhatsHap statistics (germline) -- **QC details**: mosdepth coverage, samtools flagstat, cramino read stats +- **Interactive SV table**: Severus structural variants, one row per rearrangement with both breakend loci, sharing the same gene-panel filter (matched on breakend position) +- **QC details**: mosdepth coverage, samtools flagstat, cramino read stats, and per-chromosome WhatsHap phasing statistics (germline) ## Quick start @@ -49,17 +49,49 @@ panel selected when the report opens — see [Gene panels](#gene-panels). > - `--gene-panel` now defaults to `none` instead of `lymphoid`: reports open unfiltered > unless a panel is asked for. Pass `--gene-panel lymphoid` to restore the old default. +> **Changed in v1.2.1:** +> - The SV table is **one row per rearrangement**, not one per breakend record: Severus's +> `_1`/`_2` mate records are collapsed, and each row carries both loci +> (`chrom_a`/`pos_a`, `chrom_b`/`pos_b`) plus a `svclass` separating interchromosomal +> translocations from intra-chromosomal breakends. The SV count and the circos links +> halve accordingly — they were double-counting. The single `gene_hits` column is +> replaced by per-breakend `gene_a`/`gene_b`, and a `panel_hit` column names which panel +> gene matched, on which side, and how: each entry reads `GENE (side, how)` — for +> example `RB1 (span, direct)` when the span overlaps the gene, or `RB1 (B, 188.5 kb)` +> when breakend B only fell inside the search window. The windows are wide (1 Mb around a +> BND), so that second token is what separates a disrupted gene from a nearby one. +> - SVs are matched against a gene panel by **coordinate**, not gene symbol, whenever the +> panel carries `chrom`/`start`/`end` — see [Gene panels](#gene-panels). The bundled +> `lymphoid.tsv` is replaced by `lymphoid.hg38.tsv` and `lymphoid.t2t.tsv`; a custom +> symbol-only TSV still works and still matches on symbols. + ## Gene panels Reports are **unfiltered by default**. `--gene-panel` only chooses which panel is selected when the report opens; the rendered HTML always contains every variant and every builtin panel, so a reader can switch panels (or paste a custom gene list) in the browser without re-rendering. -Built-in panels live in `assets/gene_lists/`. Each is a TSV with a `gene` column (HGNC symbols). +Built-in panels live in `assets/gene_lists/`. Each is a TSV with a `gene` column (HGNC symbols) +and, optionally, `chrom`/`start`/`end` — which changes how structural variants are matched: + +| Panel columns | Small variants | Structural variants | +|---|---|---| +| `gene` only | symbol match | symbol match on the annotated breakend genes — no positional window | +| `gene, chrom, start, end` | symbol match | within **1 Mb of either breakend** of a BND, or **100 kb of the span** of any other type | + +Coordinate matching is the reliable mode: whether VEP annotates a breakend with a gene symbol +at all depends on the sample's VEP invocation (1.6%–90% of breakend rows across the samples +measured), so a symbol-only panel can hide exactly the translocations it exists to find. The +note under the SV table says which mode is in force. + +Because coordinates are only valid for one genome, a coordinate panel must declare its +reference (a leading `# reference: hg38` line, or a `reference` column) and a mismatch with the +rendered reference is a hard error. Builtins ship one file per reference and are offered as a +single entry, resolved against the detected one. | Panel | Description | |---|---| -| `lymphoid` | ~70 recurrently mutated genes in B-cell lymphomas (DLBCL, FL, CLL, MCL, BL, MALT) | +| `lymphoid` | 72 recurrently mutated genes in B-cell lymphomas (DLBCL, FL, CLL, MCL, BL, MALT), as `lymphoid.hg38.tsv` and `lymphoid.t2t.tsv` | ```bash --gene-panel lymphoid # open with the builtin lymphoid panel applied @@ -67,7 +99,8 @@ Built-in panels live in `assets/gene_lists/`. Each is a TSV with a `gene` column ``` A `--gene-panel` value that is neither `none`, a builtin name, nor an existing file is an error — -a typo will not silently produce an unfiltered report. +a typo will not silently produce an unfiltered report. See +[`assets/gene_lists/README.md`](assets/gene_lists/README.md) for the full file format. ## Expected input layout @@ -100,6 +133,14 @@ variant set; VAF, depth, genotype and phase set are joined from the VCF that VEP yields VAF and depth but no phase set. If none is found the table still renders, without those columns. +A footnote under the variant table names the file those columns actually came from and how +many variants they cover. **One VCF supplies them for the whole table**, so if the run +combined several somatic callers by consensus, the VAF, depth, genotype and phase set come +from whichever caller won each merge and need not match the `callers` column beside them. +Variants reported by more than one caller are highlighted in that column, and the footnote +says so. (Only the joined columns are ambiguous — the variant *set* is taken per record from +the VEP file.) + VEP writes indels at a different position and sometimes in a different allele notation than the VCF it was given, so the join is made on a normalised key — see `variant_key()` in `R/parse_smallvariants.R`. @@ -133,15 +174,22 @@ Auto-detection reads `##contig` lines from the VEP somatic VCF. ## R package requirements +`recipe/meta.yaml` is the source of truth for runtime dependencies — it is what the +Bioconda package and the pipeline's container are built from. The list below mirrors it; +if the two ever disagree, the recipe is right. + Install in your R environment if missing: ```r -install.packages(c("data.table", "dplyr", "tidyr", "DT", "htmltools", - "optparse", "quarto", "yaml", "ggplot2", "svglite")) -BiocManager::install(c("circlize", "ComplexHeatmap", "GenomicRanges")) -# paletteer, prismatic are optional (not required by this version) +install.packages(c("data.table", "dplyr", "DT", "htmltools", "optparse", + "quarto", "yaml", "ggplot2", "svglite", "knitr", + "R.utils", "base64enc")) +BiocManager::install("circlize") ``` +Plus the `quarto` CLI itself. `R.utils` is not called directly — `data.table::fread()` +requires it to read the gzipped VCFs. + Tested with R 4.4.1 and Quarto 1.5.57. ## Repository structure @@ -154,19 +202,19 @@ lrsomatic_report/ │ ├── references.R Cytoband + chrom-length loading, reference auto-detection │ ├── locate_outputs.R Discover per-tool output files in a sample directory │ ├── parse_smallvariants.R VEP text + raw caller VCF parsers; build variant table -│ ├── parse_severus.R Severus VCF + gene TSV parsers; build SV table +│ ├── parse_severus.R Severus VCF parsing (mate-collapsed), SV table, panel matching │ ├── parse_ascat.R ASCAT segments + purity/ploidy parsers │ ├── parse_qc.R Mosdepth, cramino, flagstat parsers -│ └── circos.R draw_circos() — generates the circos SVG +│ ├── circos.R draw_circos() — the genome-wide circos SVG +│ └── circos_bnd.R draw_bnd_circos() — the breakend circos, inline and row-linked ├── templates/per_sample.qmd Quarto template (HTML report) ├── assets/ │ ├── references/{t2t,hg38}/ Cytobands + chrom lengths (bundled, no network needed) -│ └── gene_lists/ lymphoid.tsv + README +│ └── gene_lists/ lymphoid.{hg38,t2t}.tsv + README └── tests/ Unit tests (testthat) ``` ## Roadmap - **v2**: Cohort report (oncoprint, recurrence tables across multiple samples) -- **v2**: Nextflow module wrapping this CLI as a final pipeline step - **v2**: Wakhan haplotype-resolved copy-number integration diff --git a/assets/lrsomatic_report/VENDORED.md b/assets/lrsomatic_report/VENDORED.md index e81ee1d7..53846779 100644 --- a/assets/lrsomatic_report/VENDORED.md +++ b/assets/lrsomatic_report/VENDORED.md @@ -6,15 +6,10 @@ Do not edit it here — fix upstream, tag a release, and re-sync. | | | |---|---| | Upstream | | -| Release | `v1.1.0` (`9d660a77d5f23f92e1f7ff34f85da7956f445009`) | -| Vendored commit | `d17a636aeb3f79462b7f58db9102f4030941195b` (`main`) | +| Release | `v1.2.1` (`1c28f9cde24e903a5a607897e275108e3dff6677`) | +| Vendored commit | `1c28f9cde24e903a5a607897e275108e3dff6677` (the tag itself) | | License | MIT (see `LICENSE`) | -The vendored commit is two chore commits ahead of the `v1.1.0` tag. Neither changes -behaviour: `b4cc7620` scrubs real sample identifiers out of the README and the -`--sample-id` help string, `d17a636a` drops a development helper script. Vendoring the -tag itself would publish those identifiers in this repository. - ## Why vendored rather than a submodule `nextflow run IntGenomicsLab/lrsomatic` clones the pipeline repository but does **not** @@ -34,24 +29,44 @@ Only what `bin/render_report.R` needs at run time: bin/ R/ templates/ assets/ LICENSE README.md ``` -Upstream `docs/`, `tests/` and `recipe/` are deliberately excluded — the same set marked -`export-ignore` in the upstream `.gitattributes`. +Upstream `docs/`, `tests/`, `recipe/` and `CLAUDE.md` are deliberately excluded. (Upstream +marks `docs/` and `tests/` `export-ignore` in its `.gitattributes`; `recipe/` and +`CLAUDE.md` are not marked, so they must be left behind by hand when copying.) ## Re-syncing on the next upstream release ```bash -TAG=v1.2.0 -git clone --depth 1 --branch "$TAG" https://github.com/ljwharbers/lrsomatic_report.git /tmp/lrr +TAG=v1.3.0 +git clone --depth 1 --branch "$TAG" https://github.com/ljwharbers/lrsomatic_report.git "$TMPDIR/lrr" rm -rf assets/lrsomatic_report/{bin,R,templates,assets,LICENSE,README.md} -cp -a /tmp/lrr/{bin,R,templates,assets,LICENSE,README.md} assets/lrsomatic_report/ -# then update the table above, and: -# - modules/local/lrsomaticreport/environment.yml if upstream recipe/meta.yaml gained a dependency -# - modules/local/lrsomaticreport/main.nf container digest + the hard-coded version topic -# - modules/local/lrsomaticreport/meta.yml the same version string +cp -a "$TMPDIR"/lrr/{bin,R,templates,assets,LICENSE,README.md} assets/lrsomatic_report/ ``` -Rebuild the container after any `environment.yml` change so the image and the file agree: +The `rm -rf` before the copy is not optional: it is what removes files *deleted* upstream. +Copying over the top would have left the pre-v1.2.1 `assets/gene_lists/lymphoid.tsv` behind, +where `load_all_gene_panels()` would glob it as a second, reference-less `lymphoid` panel +alongside the `lymphoid.hg38.tsv` / `lymphoid.t2t.tsv` pair that replaced it. + +Then update the table above, and: + +- `modules/local/lrsomaticreport/environment.yml` — if upstream `recipe/meta.yaml` gained a + dependency. The two files are kept in exact agreement; check with a `library()`/`require()` + grep over the upstream `R/`, `bin/` and `templates/` rather than trusting the README. +- `modules/local/lrsomaticreport/main.nf` — the hard-coded version topic (the tool has no + `--version` flag), and the container digests **only if `environment.yml` changed**. +- `modules/local/lrsomaticreport/meta.yml` — the same version string, in two places. +- `modules/local/lrsomaticreport/tests/main.nf.test.snap` — regenerate. +- `tests/{default,clair_only,deep_only,consensus,union}.nf.test.snap` — the + `"lrsomatic_report": ""` line in each. +- `CHANGELOG.md` — the vendored version named in the `#176` entry. + +Nothing checks these for consistency; miss one and the snapshots go stale silently. + +Rebuild the containers after any `environment.yml` change so the images and the file agree. +**Two** builds are needed and both must be updated in `main.nf`: `--singularity` produces a +Singularity-native SIF (the `oras://` reference), the default build a genuine OCI image. ```bash wave --conda-file modules/local/lrsomaticreport/environment.yml --freeze --await +wave --conda-file modules/local/lrsomaticreport/environment.yml --freeze --await --singularity ``` diff --git a/assets/lrsomatic_report/assets/gene_lists/README.md b/assets/lrsomatic_report/assets/gene_lists/README.md index 8a0714f7..437982b5 100644 --- a/assets/lrsomatic_report/assets/gene_lists/README.md +++ b/assets/lrsomatic_report/assets/gene_lists/README.md @@ -1,8 +1,45 @@ # Gene Panel Lists -Each file is a TSV with a required `gene` column (HGNC symbol) and optional metadata columns (`panel`, `notes`). +Each file is a TSV with a required `gene` column (HGNC symbol). Coordinate columns +`chrom` (or `chr`), `start` and `end` are optional but **all-or-nothing** — a file with +some but not all three is rejected rather than quietly falling back to symbol matching. -To supply a custom panel at render time: +| Panel columns | Small-variant filter | SV filter | +|---|---|---| +| `gene` only | symbol match | direct-hit symbol match on either breakend's VEP gene — no windows | +| `gene, chrom, start, end` | symbol match | coordinate match: within 1 Mb of a breakend (BND) or 100 kb of the SV span (other types) | + +Coordinate matching is what makes breakend filtering reliable: whether a BND carries a +VEP gene symbol at all depends on the sample's VEP invocation (1.6%–90% of breakends +across the samples measured), so a symbol-only panel can hide the very translocations it +exists to find. Matching on coordinates needs no annotation on the row. + +Optional metadata columns (`panel`, `notes`) are ignored by the loader and kept for the +reader. + +## Reference declaration + +Panel coordinates are only valid for the reference they were built on — matching an hg38 +panel against a T2T sample produces wrong hits with no error anywhere. A +coordinate-carrying panel must therefore declare its reference, either as a leading +comment line: + +``` +# reference: hg38 +gene chrom start end +MYC chr8 127735434 127742951 +``` + +or as a `reference` column. A panel whose declared reference differs from the one the +report is rendered against is a **hard error**. A panel that declares none loads, but the +SV section footnote says "reference unverified". Symbol-only panels are +reference-agnostic and need no declaration. + +Builtin panels ship one file per reference (`lymphoid.hg38.tsv`, `lymphoid.t2t.tsv`) and +are presented as a single selectable `lymphoid` entry, resolved against the detected +reference. + +## Supplying a custom panel ```bash Rscript bin/render_report.R \ @@ -11,10 +48,42 @@ Rscript bin/render_report.R \ --gene-panel /path/to/my_genes.tsv ``` -The minimal format of a custom panel file is one gene symbol per line (no header needed if there is only one column, but a TSV with a `gene` header is preferred). +A one-column file of symbols (with or without a `gene` header) is accepted, and gives +symbol-only matching. The report's "Custom…" textarea takes bare symbols, so it is +symbol-only too. ## Bundled panels | File | Contents | |---|---| -| `lymphoid.tsv` | ~70 recurrently mutated genes in B-cell lymphomas (DLBCL, FL, MCL, CLL, BL, MALT) | +| `lymphoid.hg38.tsv` | 72 recurrently mutated genes in B-cell lymphomas (DLBCL, FL, MCL, CLL, BL, MALT), GENCODE v46 gene spans | +| `lymphoid.t2t.tsv` | the same 72 genes, spans from the CHM13v2.0 RefSeq Liftoff v5.1 annotation | +| `sarcoma.hg38.tsv` | 140 soft-tissue and bone sarcoma genes — tumour suppressors, amplification targets and recurrent fusion partners — GENCODE v46 gene spans | +| `sarcoma.t2t.tsv` | the same 140 genes, spans from the CHM13v2.0 RefSeq Liftoff v5.1 annotation | + +Regenerating them is mechanical — gene spans keyed on `gene_name`, taken from +`gene` features (GENCODE) or the min/max of `transcript` features (Liftoff, which has no +`gene` feature), restricted to `chr1`–`chr22`, `chrX`, `chrY`: + +- hg38: `references/GRCh38.alt-masked-V2/annotation/gencode.v46.basic.annotation.gtf.gz` +- t2t: `references/chm13_v2.0_maskedY.rCRS/annotation/chm13v2.0_RefSeq_Liftoff_v5.1.gtf` + +Neither annotation is keyed on current HGNC symbols throughout, so aliases in the source +gene lists were mapped by hand. For `lymphoid`: `CD20`→`MS4A1` (already present, so the +rows merged), `GEF1`→`ARHGEF1`, `HIST1H1E`→`H1-4`. For `sarcoma`, from an input list of +151 lines: `VEGFR2`→`KDR`, `VEGFR3`→`FLT4`, `MKL2`→`MRTFB`, `MGEA5`→`OGA`, plus +`HER2`→`ERBB2`, `SYT`→`SS18`, `H3F3A`→`H3-3A` and `H3F3B`→`H3-3B`, whose targets were +already listed — those merged, as did seven verbatim duplicates, leaving 140 genes. + +Two `sarcoma` symbols need the T2T annotation handled specially, and both are recorded in +that file's comment header: + +- `POU2AF3` — the Liftoff annotation predates the rename and carries it as `COLCA2`. +- `DUX4L10` — the Liftoff annotation has no D4Z4 paralogs at all (only `DUX4` itself), so + this span comes from `GCF_009914755.1_T2T-CHM13v2.0_genomic.gtf.gz`, whose chromosomes + are NCBI accessions (`NC_060925.1` = `chr1` … `NC_060948.1` = `chrY`). Its 10q26 + position agrees with the hg38 locus, so this is a genuine match rather than a guess. + +A missing coordinate is a **hard error** in `load_gene_panel()`, not a per-row downgrade to +symbol matching, so a gene that resolves in one annotation and not the other has to be +either mapped or dropped from that reference's file — it cannot be left blank. diff --git a/assets/lrsomatic_report/assets/gene_lists/lymphoid.hg38.tsv b/assets/lrsomatic_report/assets/gene_lists/lymphoid.hg38.tsv new file mode 100644 index 00000000..3317ff37 --- /dev/null +++ b/assets/lrsomatic_report/assets/gene_lists/lymphoid.hg38.tsv @@ -0,0 +1,76 @@ +# reference: hg38 +# gene spans from gencode.v46.basic.annotation.gtf.gz +# coordinates are 1-based inclusive gene spans; only valid for the reference declared above +gene chrom start end panel notes +TNFRSF14 chr1 2555639 2565382 lymphoid Immune checkpoint; FL +PIK3CD chr1 9629889 9729114 lymphoid PI3K catalytic subunit delta +SPEN chr1 15836095 15940456 lymphoid Transcriptional repressor +ID3 chr1 23557926 23559501 lymphoid BL; inhibits E-proteins/TCF3 +ARID1A chr1 26693236 26782104 lymphoid SWI/SNF chromatin remodeling +BCL10 chr1 85265776 85276632 lymphoid CBM complex; NF-kB +NRAS chr1 114704469 114716771 lymphoid RAS signaling +CD58 chr1 116514534 116571039 lymphoid Immune evasion +PTEN chr10 87862638 87971930 lymphoid PI3K pathway tumour suppressor +MS4A1 chr11 60455846 60470752 lymphoid CD20; B-cell surface marker; rituximab target +CCND1 chr11 69641156 69654474 lymphoid Cyclin D1; t(11;14) in MCL +FAT3 chr11 92224818 92896473 lymphoid Tumour suppressor +BIRC3 chr11 102317484 102339403 lymphoid IAP; NF-kB; CLL +ATM chr11 108222804 108369102 lymphoid DNA damage response; CLL/MCL +KRAS chr12 25205246 25250936 lymphoid RAS signaling +KMT2D chr12 49018975 49060794 lymphoid Histone methyltransferase (MLL4) +BTG1 chr12 92140278 92145846 lymphoid Anti-proliferative; DLBCL +DTX1 chr12 113056730 113098028 lymphoid Notch pathway effector +FOXO1 chr13 40555667 40666641 lymphoid Transcription factor; BCL6 target +RB1 chr13 48303744 48599436 lymphoid Tumour suppressor; cell cycle +B2M chr15 44711358 44718851 lymphoid HLA class I; immune evasion +MAP2K1 chr15 66386837 66491656 lymphoid ERK signaling +CREBBP chr16 3725054 3880713 lymphoid Acetyltransferase; loss-of-function in FL/DLBCL +CIITA chr16 10866222 10943021 lymphoid MHC class II transactivator +PRKCB chr16 23835983 24220611 lymphoid Protein kinase C beta +CD19 chr16 28931965 28939342 lymphoid BCR coreceptor; therapy target +TP53 chr17 7661779 7687546 lymphoid Tumour suppressor +CD79B chr17 63928738 63932336 lymphoid BCR co-receptor signaling +GNA13 chr17 65009289 65056740 lymphoid G-protein; germinal center exit +MALT1 chr18 58671465 58754477 lymphoid Paracaspase; NF-kB; MALT lymphoma +BCL2 chr18 63123346 63320128 lymphoid Anti-apoptotic; t(14;18) in FL/DLBCL +RPS15 chr19 1438358 1440495 lymphoid Ribosomal; CLL +TCF3 chr19 1609291 1652615 lymphoid BL; E-box transcription factor +SMARCA4 chr19 10960932 11079426 lymphoid Chromatin remodeling +MEF2B chr19 19145567 19192131 lymphoid Transcription factor; FL/DLBCL +ARHGEF1 chr19 41883173 41930150 lymphoid Guanine nucleotide exchange +SOX11 chr2 5692384 5701385 lymphoid MCL marker +BCL11A chr2 60450520 60554467 lymphoid Transcription factor; lymphoma +DUSP2 chr2 96143169 96145440 lymphoid MAP kinase phosphatase +CXCR4 chr2 136114349 136119177 lymphoid Chemokine receptor; CLL/WM +SF3B1 chr2 197388515 197435079 lymphoid Splicing factor; CLL +SAMHD1 chr20 36890229 36951893 lymphoid dNTP hydrolase; CLL +EP300 chr22 41092510 41180077 lymphoid Acetyltransferase +MYD88 chr3 38138552 38143024 lymphoid TLR signaling adaptor; L265P hotspot +SETD2 chr3 47016428 47164113 lymphoid H3K36 methyltransferase +RHOA chr3 49359139 49412998 lymphoid Rho GTPase; AITL G17V hotspot +TBL1XR1 chr3 177019340 177228000 lymphoid Transcription corepressor +PIK3CA chr3 179148114 179240093 lymphoid PI3K catalytic subunit alpha +KLHL6 chr3 183487551 183555706 lymphoid BCR signaling ubiquitin adaptor +BCL6 chr3 187721377 187745725 lymphoid Transcription factor; t(3;14) in DLBCL +FBXW7 chr4 152320544 152536092 lymphoid Ubiquitin E3 ligase +FAT1 chr4 186587794 186726722 lymphoid Tumour suppressor; Hippo pathway +IRF4 chr6 391739 411443 lymphoid Transcription factor; MYC target +H1-4 chr6 26156329 26157115 lymphoid Linker histone H1; DLBCL +HLA-A chr6 29941260 29949572 lymphoid Immune evasion +HLA-C chr6 31268749 31272130 lymphoid Immune evasion +HLA-B chr6 31353872 31367067 lymphoid Immune evasion +PIM1 chr6 37170152 37175428 lymphoid Kinase; BCR/TLR signaling +CCND3 chr6 41934934 42050357 lymphoid Cyclin D3; DLBCL hotspot +SGK1 chr6 134169248 134318112 lymphoid Kinase; germinal center +TNFAIP3 chr6 137867214 137883314 lymphoid A20; NF-kB negative regulator +CARD11 chr7 2906142 3044228 lymphoid NF-kB signaling scaffold +PCLO chr7 82754012 83162930 lymphoid Pepe-scaffold; recurrently mutated +BRAF chr7 140719327 140924929 lymphoid MAPK kinase; HCL V600E +EZH2 chr7 148807257 148884321 lymphoid Histone methyltransferase; Y641/A677/A687 hotspots +LYN chr8 55879835 56014169 lymphoid Src family kinase; BCR signaling +MYC chr8 127735434 127742951 lymphoid Proto-oncogene, BCL translocations +CDKN2A chr9 21967752 21995301 lymphoid Cell cycle regulator (p16/p14ARF) +SYK chr9 90801787 90898549 lymphoid BCR/FcR signaling kinase +NOTCH1 chr9 136494433 136546048 lymphoid Notch pathway; CLL +DDX3X chrX 41333348 41364472 lymphoid RNA helicase; Burkitt/DLBCL +BTK chrX 101349338 101390796 lymphoid BCR kinase; ibrutinib target diff --git a/assets/lrsomatic_report/assets/gene_lists/lymphoid.t2t.tsv b/assets/lrsomatic_report/assets/gene_lists/lymphoid.t2t.tsv new file mode 100644 index 00000000..ca05a853 --- /dev/null +++ b/assets/lrsomatic_report/assets/gene_lists/lymphoid.t2t.tsv @@ -0,0 +1,76 @@ +# reference: t2t +# gene spans from chm13v2.0_RefSeq_Liftoff_v5.1.gtf +# coordinates are 1-based inclusive gene spans; only valid for the reference declared above +gene chrom start end panel notes +TNFRSF14 chr1 1995878 2005473 lymphoid Immune checkpoint; FL +PIK3CD chr1 9170040 9271897 lymphoid PI3K catalytic subunit delta +SPEN chr1 15288973 15381736 lymphoid Transcriptional repressor +ID3 chr1 23392495 23394070 lymphoid BL; inhibits E-proteins/TCF3 +ARID1A chr1 26533960 26620059 lymphoid SWI/SNF chromatin remodeling +BCL10 chr1 85106896 85117748 lymphoid CBM complex; NF-kB +NRAS chr1 114715929 114728216 lymphoid RAS signaling +CD58 chr1 116524958 116581474 lymphoid Immune evasion +PTEN chr10 88747528 88855830 lymphoid PI3K pathway tumour suppressor +MS4A1 chr11 60406975 60421883 lymphoid CD20; B-cell surface marker; rituximab target +CCND1 chr11 69658031 69671351 lymphoid Cyclin D1; t(11;14) in MCL +FAT3 chr11 92147480 92825282 lymphoid Tumour suppressor +BIRC3 chr11 102319603 102341520 lymphoid IAP; NF-kB; CLL +ATM chr11 108230609 108376596 lymphoid DNA damage response; CLL/MCL +KRAS chr12 25076496 25122152 lymphoid RAS signaling +KMT2D chr12 48981150 49022967 lymphoid Histone methyltransferase (MLL4) +BTG1 chr12 92117840 92123409 lymphoid Anti-proliferative; DLBCL +DTX1 chr12 113033383 113074667 lymphoid Notch pathway effector +FOXO1 chr13 39774673 39885620 lymphoid Transcription factor; BCL6 target +RB1 chr13 47524085 47702182 lymphoid Tumour suppressor; cell cycle +B2M chr15 42519493 42526121 lymphoid HLA class I; immune evasion +MAP2K1 chr15 64208363 64313019 lymphoid ERK signaling +CREBBP chr16 3752324 3907918 lymphoid Acetyltransferase; loss-of-function in FL/DLBCL +CIITA chr16 10902174 10978998 lymphoid MHC class II transactivator +PRKCB chr16 24111361 24497130 lymphoid Protein kinase C beta +CD19 chr16 29213087 29220458 lymphoid BCR coreceptor; therapy target +TP53 chr17 7572544 7591594 lymphoid Tumour suppressor +CD79B chr17 64799505 64803094 lymphoid BCR co-receptor signaling +GNA13 chr17 65879260 65926708 lymphoid G-protein; germinal center exit +MALT1 chr18 58872604 58955588 lymphoid Paracaspase; NF-kB; MALT lymphoma +BCL2 chr18 63326497 63525151 lymphoid Anti-apoptotic; t(14;18) in FL/DLBCL +RPS15 chr19 1408393 1410492 lymphoid Ribosomal; CLL +TCF3 chr19 1580117 1623781 lymphoid BL; E-box transcription factor +SMARCA4 chr19 11088037 11189277 lymphoid Chromatin remodeling +MEF2B chr19 19282103 19306796 lymphoid Transcription factor; FL/DLBCL +ARHGEF1 chr19 44702671 44749424 lymphoid Guanine nucleotide exchange +SOX11 chr2 5713811 5722812 lymphoid MCL marker +BCL11A chr2 60456325 60559492 lymphoid Transcription factor; lymphoma +DUSP2 chr2 96649773 96652044 lymphoid MAP kinase phosphatase +CXCR4 chr2 136558831 136562630 lymphoid Chemokine receptor; CLL/WM +SF3B1 chr2 197873439 197918732 lymphoid Splicing factor; CLL +SAMHD1 chr20 38614121 38676051 lymphoid dNTP hydrolase; CLL +EP300 chr22 41567504 41655012 lymphoid Acetyltransferase +MYD88 chr3 38144330 38148691 lymphoid TLR signaling adaptor; L265P hotspot +SETD2 chr3 47032799 47181207 lymphoid H3K36 methyltransferase +RHOA chr3 49388518 49441355 lymphoid Rho GTPase; AITL G17V hotspot +TBL1XR1 chr3 179822332 180004917 lymphoid Transcription corepressor +PIK3CA chr3 181951954 182043930 lymphoid PI3K catalytic subunit alpha +KLHL6 chr3 186295709 186363954 lymphoid BCR signaling ubiquitin adaptor +BCL6 chr3 190538887 190562977 lymphoid Transcription factor; t(3;14) in DLBCL +FBXW7 chr4 155643699 155859255 lymphoid Ubiquitin E3 ligase +FAT1 chr4 189934525 190073402 lymphoid Tumour suppressor; Hippo pathway +IRF4 chr6 250136 269771 lymphoid Transcription factor; MYC target +H1-4 chr6 26024465 26025251 lymphoid Linker histone H1; DLBCL +HLA-A chr6 29806459 29809798 lymphoid Immune evasion +HLA-C chr6 31134915 31138246 lymphoid Immune evasion +HLA-B chr6 31209767 31213072 lymphoid Immune evasion +PIM1 chr6 36993698 36998970 lymphoid Kinase; BCR/TLR signaling +CCND3 chr6 41763493 41877153 lymphoid Cyclin D3; DLBCL hotspot +SGK1 chr6 135357191 135506235 lymphoid Kinase; germinal center +TNFAIP3 chr6 139054770 139071725 lymphoid A20; NF-kB negative regulator +CARD11 chr7 3019747 3157416 lymphoid NF-kB signaling scaffold +PCLO chr7 84005231 84414103 lymphoid Pepe-scaffold; recurrently mutated +BRAF chr7 142027505 142239131 lymphoid MAPK kinase; HCL V600E +EZH2 chr7 149989157 150066070 lymphoid Histone methyltransferase; Y641/A677/A687 hotspots +LYN chr8 56256887 56391325 lymphoid Src family kinase; BCR signaling +MYC chr8 128862888 128870405 lymphoid Proto-oncogene, BCL translocations +CDKN2A chr9 21982052 22009697 lymphoid Cell cycle regulator (p16/p14ARF) +SYK chr9 102966952 103064392 lymphoid BCR/FcR signaling kinase +NOTCH1 chr9 148723532 148777907 lymphoid Notch pathway; CLL +DDX3X chrX 40735393 40766556 lymphoid RNA helicase; Burkitt/DLBCL +BTK chrX 99793571 99834908 lymphoid BCR kinase; ibrutinib target diff --git a/assets/lrsomatic_report/assets/gene_lists/lymphoid.tsv b/assets/lrsomatic_report/assets/gene_lists/lymphoid.tsv deleted file mode 100644 index c0098a53..00000000 --- a/assets/lrsomatic_report/assets/gene_lists/lymphoid.tsv +++ /dev/null @@ -1,74 +0,0 @@ -gene panel notes -MYC lymphoid Proto-oncogene, BCL translocations -BCL2 lymphoid Anti-apoptotic; t(14;18) in FL/DLBCL -BCL6 lymphoid Transcription factor; t(3;14) in DLBCL -TP53 lymphoid Tumour suppressor -CDKN2A lymphoid Cell cycle regulator (p16/p14ARF) -MYD88 lymphoid TLR signaling adaptor; L265P hotspot -CD79B lymphoid BCR co-receptor signaling -EZH2 lymphoid Histone methyltransferase; Y641/A677/A687 hotspots -KMT2D lymphoid Histone methyltransferase (MLL4) -CREBBP lymphoid Acetyltransferase; loss-of-function in FL/DLBCL -EP300 lymphoid Acetyltransferase -CARD11 lymphoid NF-kB signaling scaffold -TNFAIP3 lymphoid A20; NF-kB negative regulator -B2M lymphoid HLA class I; immune evasion -CD58 lymphoid Immune evasion -HLA-A lymphoid Immune evasion -HLA-B lymphoid Immune evasion -HLA-C lymphoid Immune evasion -FOXO1 lymphoid Transcription factor; BCL6 target -GNA13 lymphoid G-protein; germinal center exit -RB1 lymphoid Tumour suppressor; cell cycle -CCND1 lymphoid Cyclin D1; t(11;14) in MCL -CCND3 lymphoid Cyclin D3; DLBCL hotspot -SOX11 lymphoid MCL marker -ATM lymphoid DNA damage response; CLL/MCL -SF3B1 lymphoid Splicing factor; CLL -NOTCH1 lymphoid Notch pathway; CLL -BIRC3 lymphoid IAP; NF-kB; CLL -BTK lymphoid BCR kinase; ibrutinib target -DTX1 lymphoid Notch pathway effector -SGK1 lymphoid Kinase; germinal center -PIM1 lymphoid Kinase; BCR/TLR signaling -PCLO lymphoid Pepe-scaffold; recurrently mutated -FAT1 lymphoid Tumour suppressor; Hippo pathway -FAT3 lymphoid Tumour suppressor -SPEN lymphoid Transcriptional repressor -MEF2B lymphoid Transcription factor; FL/DLBCL -KLHL6 lymphoid BCR signaling ubiquitin adaptor -SMARCA4 lymphoid Chromatin remodeling -IRF4 lymphoid Transcription factor; MYC target -ARID1A lymphoid SWI/SNF chromatin remodeling -HIST1H1E lymphoid Linker histone H1; DLBCL -DUSP2 lymphoid MAP kinase phosphatase -BTG1 lymphoid Anti-proliferative; DLBCL -CIITA lymphoid MHC class II transactivator -CXCR4 lymphoid Chemokine receptor; CLL/WM -RHOA lymphoid Rho GTPase; AITL G17V hotspot -SYK lymphoid BCR/FcR signaling kinase -PRKCB lymphoid Protein kinase C beta -KRAS lymphoid RAS signaling -NRAS lymphoid RAS signaling -BRAF lymphoid MAPK kinase; HCL V600E -MAP2K1 lymphoid ERK signaling -PIK3CA lymphoid PI3K catalytic subunit alpha -PIK3CD lymphoid PI3K catalytic subunit delta -PTEN lymphoid PI3K pathway tumour suppressor -ID3 lymphoid BL; inhibits E-proteins/TCF3 -TCF3 lymphoid BL; E-box transcription factor -BCL11A lymphoid Transcription factor; lymphoma -SAMHD1 lymphoid dNTP hydrolase; CLL -RPS15 lymphoid Ribosomal; CLL -FBXW7 lymphoid Ubiquitin E3 ligase -TBL1XR1 lymphoid Transcription corepressor -DDX3X lymphoid RNA helicase; Burkitt/DLBCL -SETD2 lymphoid H3K36 methyltransferase -GEF1 lymphoid Guanine nucleotide exchange -LYN lymphoid Src family kinase; BCR signaling -CD19 lymphoid BCR coreceptor; therapy target -CD20 lymphoid Rituximab target (MS4A1) -MS4A1 lymphoid CD20; B-cell surface marker -TNFRSF14 lymphoid Immune checkpoint; FL -BCL10 lymphoid CBM complex; NF-kB -MALT1 lymphoid Paracaspase; NF-kB; MALT lymphoma diff --git a/assets/lrsomatic_report/assets/gene_lists/sarcoma.hg38.tsv b/assets/lrsomatic_report/assets/gene_lists/sarcoma.hg38.tsv new file mode 100644 index 00000000..0529c1b9 --- /dev/null +++ b/assets/lrsomatic_report/assets/gene_lists/sarcoma.hg38.tsv @@ -0,0 +1,144 @@ +# reference: hg38 +# gene spans from gencode.v46.basic.annotation.gtf.gz +# coordinates are 1-based inclusive gene spans; only valid for the reference declared above +gene chrom start end panel notes +CAMTA1 chr1 6785454 7769706 sarcoma WWTR1-CAMTA1; epithelioid haemangioendothelioma +SDHB chr1 17018664 17054151 sarcoma SDH-deficient GIST and paraganglioma +PAX7 chr1 18630846 18748866 sarcoma PAX7-FOXO1; alveolar rhabdomyosarcoma +MEAF6 chr1 37489993 37514766 sarcoma MEAF6-PHF1; ossifying fibromyxoid tumour +JUN chr1 58776845 58784048 sarcoma 1p32 amplification in DDLPS +TGFBR3 chr1 91680343 91906335 sarcoma TGFBR3-OGA lipofibromatosis-like neural tumour +CSF1 chr1 109910242 109930992 sarcoma COL6A3-CSF1; tenosynovial giant cell tumour +NRAS chr1 114704469 114716771 sarcoma RAS signalling +TPM3 chr1 154155308 154194648 sarcoma TPM3-NTRK1 / ALK fusion partner +NTRK1 chr1 156815636 156881850 sarcoma Kinase fusions; infantile fibrosarcoma-like +SDHC chr1 161314381 161363206 sarcoma SDH-deficient GIST; Carney triad +MDM4 chr1 204516379 204558120 sarcoma p53 negative regulator; 1q32 amplification +H3-3A chr1 226061851 226072019 sarcoma H3F3A G34W; giant cell tumour of bone +EPC1 chr10 32267751 32378798 sarcoma EPC1-PHF1; endometrial stromal sarcoma +RET chr10 43077064 43130351 sarcoma Kinase fusions +PTEN chr10 87862638 87971930 sarcoma PI3K pathway tumour suppressor +OGA chr10 101784443 101818465 sarcoma MGEA5; TGFBR3-OGA lipofibromatosis-like tumour +FGFR2 chr10 121478332 121598458 sarcoma RTK fusion and amplification +DUX4L10 chr10 133743332 133744598 sarcoma 10q26 D4Z4 paralog; CIC-DUX4L partner +WT1 chr11 32387775 32435564 sarcoma EWSR1-WT1; desmoplastic small round cell tumour +CREB3L1 chr11 46277662 46321409 sarcoma FUS-CREB3L1; low-grade fibromyxoid sarcoma +SDHAF2 chr11 61430042 61446839 sarcoma SDH complex assembly; SDH-deficient GIST +CCND1 chr11 69641156 69654474 sarcoma Cyclin D1 amplification +EED chr11 86201212 86278813 sarcoma PRC2; MPNST loss +MAML2 chr11 95976598 96343195 sarcoma MAML2 fusions +YAP1 chr11 102110447 102233424 sarcoma YAP1-TFE3 / YAP1-MRTFB haemangioendothelioma +POU2AF3 chr11 111298546 111308735 sarcoma COLCA2; EWSR1-POU2AF3 sarcoma +SDHD chr11 112086824 112120016 sarcoma SDH-deficient GIST and paraganglioma +KMT2A chr11 118436456 118526832 sarcoma MLL; rare sarcoma fusion partner +FLI1 chr11 128686535 128813267 sarcoma EWSR1-FLI1; Ewing sarcoma +CCND2 chr12 4269771 4305353 sarcoma Cyclin D2 amplification +ETV6 chr12 11649674 11895377 sarcoma ETV6-NTRK3; infantile fibrosarcoma +KRAS chr12 25205246 25250936 sarcoma RAS signalling +KMT2D chr12 49018975 49060794 sarcoma Histone methyltransferase; tumour suppressor +ATF1 chr12 50763710 50821162 sarcoma EWSR1-ATF1; clear cell sarcoma, AFH +TFCP2 chr12 51093656 51173135 sarcoma FUS/EWSR1-TFCP2 epithelioid rhabdomyosarcoma +NAB2 chr12 57089043 57095476 sarcoma NAB2-STAT6; solitary fibrous tumour +STAT6 chr12 57095408 57132139 sarcoma NAB2-STAT6; solitary fibrous tumour +GLI1 chr12 57459785 57472268 sarcoma GLI1-altered soft tissue tumour; 12q13 amplicon +DDIT3 chr12 57516588 57521737 sarcoma FUS/EWSR1-DDIT3; myxoid liposarcoma +CDK4 chr12 57747727 57756013 sarcoma 12q13-14; co-amplified with MDM2 in DDLPS +HMGA2 chr12 65824460 65966291 sarcoma 12q14-15; lipoma and DDLPS fusions +MDM2 chr12 68808177 68845544 sarcoma 12q15 amplification; WDLPS/DDLPS hallmark +BRCA2 chr13 32315086 32400268 sarcoma Homologous recombination repair +FOXO1 chr13 40555667 40666641 sarcoma PAX3/PAX7-FOXO1; alveolar rhabdomyosarcoma +RB1 chr13 48303744 48599436 sarcoma Cell cycle; leiomyosarcoma/osteosarcoma +GPC5 chr13 91398621 92873682 sarcoma 13q31 amplification; rhabdomyosarcoma +FOS chr14 75278826 75282230 sarcoma FOS rearrangement; osteoblastoma, epithelioid haemangioma +NUTM1 chr15 34343315 34357737 sarcoma NUTM1 fusions +TCF12 chr15 56918623 57299281 sarcoma TCF12 fusions; ossifying fibromyxoid tumour +NTRK3 chr15 87859751 88256791 sarcoma ETV6-NTRK3; infantile fibrosarcoma +MRTFB chr16 14071319 14266773 sarcoma MKL2; MRTFB-YAP1 haemangioendothelioma +PRKCB chr16 23835983 24220611 sarcoma PRKC fusion partner +FUS chr16 31180138 31196963 sarcoma FET family; FUS-DDIT3 myxoid liposarcoma +YWHAE chr17 1344275 1400222 sarcoma YWHAE-NUTM2; endometrial stromal sarcoma +USP6 chr17 5116032 5175034 sarcoma Aneurysmal bone cyst, nodular fasciitis +TP53 chr17 7661779 7687546 sarcoma Li-Fraumeni; osteosarcoma, leiomyosarcoma +TOP3A chr17 18271428 18315007 sarcoma 17p11 amplicon +NF1 chr17 31094927 31382116 sarcoma MPNST; RAS-MAPK tumour suppressor +SUZ12 chr17 31937007 32001038 sarcoma PRC2; JAZF1-SUZ12 ESS, MPNST loss +TAF15 chr17 35809482 35864615 sarcoma FET family; Ewing-like sarcoma +ERBB2 chr17 39687914 39730426 sarcoma HER2; RTK amplification +BRCA1 chr17 43044295 43170245 sarcoma Homologous recombination repair +ETV4 chr17 43527844 43579620 sarcoma ETV4 rearrangement +COL1A1 chr17 50184101 50201632 sarcoma COL1A1-PDGFB; dermatofibrosarcoma protuberans +PRKCA chr17 66302613 66810743 sarcoma PRKC fusions; chordoid-type tumours +H3-3B chr17 75776434 75785893 sarcoma H3F3B K36M; chondroblastoma +SS18 chr18 26016253 26091217 sarcoma SS18-SSX; synovial sarcoma +SMARCA4 chr19 10960932 11079426 sarcoma SWI/SNF; SMARCA4-deficient sarcoma +TPM4 chr19 16067021 16103002 sarcoma TPM4-ALK fusion partner +CIC chr19 42268537 42295797 sarcoma CIC-DUX4 round cell sarcoma +FOSB chr19 45467995 45475179 sarcoma FOSB rearrangement; pseudomyogenic haemangioendothelioma +MYCN chr2 15940550 15947007 sarcoma Amplification; rhabdomyosarcoma +NCOA1 chr2 24491254 24770702 sarcoma PAX3-NCOA1; rhabdomyosarcoma +ALK chr2 29192774 29921586 sarcoma Kinase fusions; inflammatory myofibroblastic tumour +EML4 chr2 42169353 42332548 sarcoma Kinase fusion partner (NTRK3/ALK) +GLI2 chr2 120735623 120992653 sarcoma Hedgehog pathway effector +CREB1 chr2 207529737 207605988 sarcoma EWSR1-CREB1; angiomatoid fibrous histiocytoma +FN1 chr2 215360440 215436073 sarcoma FN1 fusions; calcifying aponeurotic fibroma +FEV chr2 218981087 218985184 sarcoma EWSR1-FEV; Ewing sarcoma +PAX3 chr2 222199887 222298998 sarcoma PAX3-FOXO1; alveolar rhabdomyosarcoma +COL6A3 chr2 237324003 237414328 sarcoma COL6A3-CSF1; tenosynovial giant cell tumour +ERG chr21 38380027 38661780 sarcoma FUS-ERG / EWSR1-ERG; Ewing sarcoma +SMARCB1 chr22 23786931 23838009 sarcoma INI1; epithelioid sarcoma, rhabdoid tumour +EWSR1 chr22 29268009 29300525 sarcoma FET family; Ewing sarcoma and many other fusions +NF2 chr22 29603553 29698598 sarcoma Schwannoma and mesothelioma tumour suppressor +PATZ1 chr22 31325804 31346346 sarcoma EWSR1-PATZ1 round cell sarcoma +PDGFB chr22 39223359 39244982 sarcoma COL1A1-PDGFB; dermatofibrosarcoma protuberans +RAF1 chr3 12582101 12664201 sarcoma Kinase fusion; MAPK activation +CTNNB1 chr3 41194741 41260096 sarcoma Beta-catenin; desmoid fibromatosis +PRKCD chr3 53156009 53192717 sarcoma PRKC fusion partner +VGLL3 chr3 86876388 86991149 sarcoma 3p12 amplification +TFG chr3 100709295 100748964 sarcoma Kinase fusion partner (NTRK1/ROS1/ALK) +WWTR1 chr3 149517235 149736714 sarcoma WWTR1-CAMTA1; epithelioid haemangioendothelioma +LPP chr3 188153284 188890671 sarcoma HMGA2-LPP; lipoma, chondroid hamartoma +FGFR3 chr4 1793293 1808872 sarcoma RTK fusion and amplification +PDGFRA chr4 54229280 54298245 sarcoma GIST; imatinib target +KIT chr4 54657267 54740783 sarcoma GIST; imatinib target +KDR chr4 55078481 55125595 sarcoma VEGFR2; angiosarcoma +DUX4 chr4 190173774 190185942 sarcoma CIC-DUX4; round cell sarcoma +SDHA chr5 218303 257082 sarcoma SDH-deficient GIST and paraganglioma +TERT chr5 1253147 1295068 sarcoma Promoter and structural activation +TRIO chr5 14143342 14532128 sarcoma TRIO-TERT; undifferentiated sarcoma +APC chr5 112707518 112846239 sarcoma Wnt pathway; desmoid fibromatosis +PDGFRB chr5 150113839 150155872 sarcoma Kinase fusions in myofibroblastic tumours +CDX1 chr5 150166778 150184558 sarcoma Rare fusion partner +NPM1 chr5 171387116 171411810 sarcoma Fusion partner +FGFR4 chr5 177086905 177098144 sarcoma Rhabdomyosarcoma; RTK +FLT4 chr5 180601506 180649624 sarcoma VEGFR3; angiosarcoma +PHF1 chr6 33410399 33416453 sarcoma PHF1 fusions; ossifying fibromyxoid tumour, ESS +FOXO3 chr6 108559835 108684774 sarcoma FOXO family transcription factor +VGLL2 chr6 117265558 117273565 sarcoma VGLL2 fusions; infantile spindle cell RMS +ROS1 chr6 117287353 117425942 sarcoma Kinase fusions +ETV1 chr7 13891229 13991425 sarcoma ETV1 rearrangement +JAZF1 chr7 27830573 28180795 sarcoma JAZF1-SUZ12; endometrial stromal sarcoma +EGFR chr7 55019017 55211628 sarcoma Receptor tyrosine kinase amplification +CDK6 chr7 92604921 92836573 sarcoma Cell cycle kinase amplification +MET chr7 116672196 116798377 sarcoma Receptor tyrosine kinase amplification +BRAF chr7 140719327 140924929 sarcoma MAPK activation +CNTNAP2 chr7 146116002 148420998 sarcoma 7q35 deletion +FGFR1 chr8 38400215 38468834 sarcoma RTK fusion and amplification +PLAG1 chr8 56160909 56211324 sarcoma PLAG1 fusions; lipoblastoma, myoepithelioma +MYBL1 chr8 66562175 66614247 sarcoma Transcription factor; rare rearrangement +NCOA2 chr8 70109782 70403808 sarcoma NCOA2 fusions; congenital spindle cell RMS +HEY1 chr8 79762371 79767857 sarcoma HEY1-NCOA2; mesenchymal chondrosarcoma +MYC chr8 127735434 127742951 sarcoma 8q24 amplification; radiation-associated angiosarcoma +PTPRD chr9 8314246 10613002 sarcoma Deletion; tumour suppressor +CDKN2A chr9 21967752 21995301 sarcoma CDK4/6 inhibitor; deleted in MPNST/DDLPS +CDKN2B chr9 22002903 22009305 sarcoma Co-deleted with CDKN2A +VCP chr9 35053928 35072668 sarcoma Fusion partner +NTRK2 chr9 84668375 85095751 sarcoma Kinase fusions +NR4A3 chr9 99821855 99866891 sarcoma EWSR1-NR4A3; extraskeletal myxoid chondrosarcoma +BCOR chrX 40049815 40177329 sarcoma BCOR-CCNB3 / BCOR-ITD round cell sarcoma +SSX1 chrX 48255392 48267444 sarcoma SS18-SSX1; synovial sarcoma +SSX4 chrX 48383516 48393347 sarcoma SS18-SSX4; synovial sarcoma +TFE3 chrX 49028726 49043410 sarcoma ASPSCR1-TFE3 alveolar soft part sarcoma; PEComa +CCNB3 chrX 50202713 50351914 sarcoma BCOR-CCNB3 round cell sarcoma +SSX2 chrX 52696896 52707178 sarcoma SS18-SSX2; synovial sarcoma +FOXO4 chrX 71095851 71103532 sarcoma FOXO family transcription factor +OGT chrX 71533087 71575892 sarcoma OGT-PHF1; endometrial stromal sarcoma diff --git a/assets/lrsomatic_report/assets/gene_lists/sarcoma.t2t.tsv b/assets/lrsomatic_report/assets/gene_lists/sarcoma.t2t.tsv new file mode 100644 index 00000000..77fe19a2 --- /dev/null +++ b/assets/lrsomatic_report/assets/gene_lists/sarcoma.t2t.tsv @@ -0,0 +1,144 @@ +# reference: t2t +# gene spans from chm13v2.0_RefSeq_Liftoff_v5.1.gtf, except DUX4L10 (absent there) from GCF_009914755.1_T2T-CHM13v2.0_genomic.gtf.gz +# coordinates are 1-based inclusive gene spans; only valid for the reference declared above +gene chrom start end panel notes +CAMTA1 chr1 6313294 7300752 sarcoma WWTR1-CAMTA1; epithelioid haemangioendothelioma +SDHB chr1 16829151 16864469 sarcoma SDH-deficient GIST and paraganglioma +PAX7 chr1 18450908 18568930 sarcoma PAX7-FOXO1; alveolar rhabdomyosarcoma +MEAF6 chr1 37354747 37379496 sarcoma MEAF6-PHF1; ossifying fibromyxoid tumour +JUN chr1 58659236 58662492 sarcoma 1p32 amplification in DDLPS +TGFBR3 chr1 91524931 91750857 sarcoma TGFBR3-OGA lipofibromatosis-like neural tumour +CSF1 chr1 109920643 109941109 sarcoma COL6A3-CSF1; tenosynovial giant cell tumour +NRAS chr1 114715929 114728216 sarcoma RAS signalling +TPM3 chr1 153292536 153329333 sarcoma TPM3-NTRK1 / ALK fusion partner +NTRK1 chr1 155952603 156018667 sarcoma Kinase fusions; infantile fibrosarcoma-like +SDHC chr1 160451814 160512783 sarcoma SDH-deficient GIST; Carney triad +MDM4 chr1 203780802 203822528 sarcoma p53 negative regulator; 1q32 amplification +H3-3A chr1 225249743 225259926 sarcoma H3F3A G34W; giant cell tumour of bone +EPC1 chr10 32296867 32407873 sarcoma EPC1-PHF1; endometrial stromal sarcoma +RET chr10 43954542 44007848 sarcoma Kinase fusions +PTEN chr10 88747528 88855830 sarcoma PI3K pathway tumour suppressor +OGA chr10 102667945 102701933 sarcoma MGEA5; TGFBR3-OGA lipofibromatosis-like tumour +FGFR2 chr10 122374405 122494614 sarcoma RTK fusion and amplification +DUX4L10 chr10 134694717 134695995 sarcoma 10q26 D4Z4 paralog; CIC-DUX4L partner +WT1 chr11 32523264 32571024 sarcoma EWSR1-WT1; desmoplastic small round cell tumour +CREB3L1 chr11 46433747 46477484 sarcoma FUS-CREB3L1; low-grade fibromyxoid sarcoma +SDHAF2 chr11 61419022 61435631 sarcoma SDH complex assembly; SDH-deficient GIST +CCND1 chr11 69658031 69671351 sarcoma Cyclin D1 amplification +EED chr11 86186227 86230015 sarcoma PRC2; MPNST loss +MAML2 chr11 95983995 96349198 sarcoma MAML2 fusions +YAP1 chr11 102112538 102235509 sarcoma YAP1-TFE3 / YAP1-MRTFB haemangioendothelioma +POU2AF3 chr11 111308735 111318916 sarcoma COLCA2; EWSR1-POU2AF3 sarcoma +SDHD chr11 112097127 112106049 sarcoma SDH-deficient GIST and paraganglioma +KMT2A chr11 118455794 118546121 sarcoma MLL; rare sarcoma fusion partner +FLI1 chr11 128718579 128845984 sarcoma EWSR1-FLI1; Ewing sarcoma +CCND2 chr12 4280521 4312135 sarcoma Cyclin D2 amplification +ETV6 chr12 11518944 11764496 sarcoma ETV6-NTRK3; infantile fibrosarcoma +KRAS chr12 25076496 25122152 sarcoma RAS signalling +KMT2D chr12 48981150 49022967 sarcoma Histone methyltransferase; tumour suppressor +ATF1 chr12 50726620 50784323 sarcoma EWSR1-ATF1; clear cell sarcoma, AFH +TFCP2 chr12 51056573 51136022 sarcoma FUS/EWSR1-TFCP2 epithelioid rhabdomyosarcoma +NAB2 chr12 57056986 57063348 sarcoma NAB2-STAT6; solitary fibrous tumour +STAT6 chr12 57063280 57079195 sarcoma NAB2-STAT6; solitary fibrous tumour +GLI1 chr12 57428074 57440557 sarcoma GLI1-altered soft tissue tumour; 12q13 amplicon +DDIT3 chr12 57484816 57489926 sarcoma FUS/EWSR1-DDIT3; myxoid liposarcoma +CDK4 chr12 57716081 57720660 sarcoma 12q13-14; co-amplified with MDM2 in DDLPS +HMGA2 chr12 65803963 65945820 sarcoma 12q14-15; lipoma and DDLPS fusions +MDM2 chr12 68787755 68830265 sarcoma 12q15 amplification; WDLPS/DDLPS hallmark +BRCA2 chr13 31532753 31617510 sarcoma Homologous recombination repair +FOXO1 chr13 39774673 39885620 sarcoma PAX3/PAX7-FOXO1; alveolar rhabdomyosarcoma +RB1 chr13 47524085 47702182 sarcoma Cell cycle; leiomyosarcoma/osteosarcoma +GPC5 chr13 90601154 92070908 sarcoma 13q31 amplification; rhabdomyosarcoma +FOS chr14 69488228 69491630 sarcoma FOS rearrangement; osteoblastoma, epithelioid haemangioma +NUTM1 chr15 32141620 32158194 sarcoma NUTM1 fusions +TCF12 chr15 54721353 55094637 sarcoma TCF12 fusions; ossifying fibromyxoid tumour +NTRK3 chr15 85614325 86011346 sarcoma ETV6-NTRK3; infantile fibrosarcoma +MRTFB chr16 14032048 14304042 sarcoma MKL2; MRTFB-YAP1 haemangioendothelioma +PRKCB chr16 24111361 24497130 sarcoma PRKC fusion partner +FUS chr16 31567541 31582292 sarcoma FET family; FUS-DDIT3 myxoid liposarcoma +YWHAE chr17 1232983 1288946 sarcoma YWHAE-NUTM2; endometrial stromal sarcoma +USP6 chr17 5009412 5068339 sarcoma Aneurysmal bone cyst, nodular fasciitis +TP53 chr17 7572544 7591594 sarcoma Li-Fraumeni; osteosarcoma, leiomyosarcoma +TOP3A chr17 18218234 18261800 sarcoma 17p11 amplicon +NF1 chr17 32040661 32323039 sarcoma MPNST; RAS-MAPK tumour suppressor +SUZ12 chr17 32882881 32946915 sarcoma PRC2; JAZF1-SUZ12 ESS, MPNST loss +TAF15 chr17 36757395 36795152 sarcoma FET family; Ewing-like sarcoma +ERBB2 chr17 40551660 40592218 sarcoma HER2; RTK amplification +BRCA1 chr17 43902857 43983996 sarcoma Homologous recombination repair +ETV4 chr17 44380321 44398821 sarcoma ETV4 rearrangement +COL1A1 chr17 51051162 51068680 sarcoma COL1A1-PDGFB; dermatofibrosarcoma protuberans +PRKCA chr17 67172339 67686559 sarcoma PRKC fusions; chordoid-type tumours +H3-3B chr17 76669660 76673005 sarcoma H3F3B K36M; chondroblastoma +SS18 chr18 26210888 26285880 sarcoma SS18-SSX; synovial sarcoma +SMARCA4 chr19 11088037 11189277 sarcoma SWI/SNF; SMARCA4-deficient sarcoma +TPM4 chr19 16201542 16237074 sarcoma TPM4-ALK fusion partner +CIC chr19 45087903 45115169 sarcoma CIC-DUX4 round cell sarcoma +FOSB chr19 48295244 48302427 sarcoma FOSB rearrangement; pseudomyogenic haemangioendothelioma +MYCN chr2 15972182 15978635 sarcoma Amplification; rhabdomyosarcoma +NCOA1 chr2 24525913 24805480 sarcoma PAX3-NCOA1; rhabdomyosarcoma +ALK chr2 29236229 29965553 sarcoma Kinase fusions; inflammatory myofibroblastic tumour +EML4 chr2 42174851 42337951 sarcoma Kinase fusion partner (NTRK3/ALK) +GLI2 chr2 121171586 121428571 sarcoma Hedgehog pathway effector +CREB1 chr2 208003958 208080006 sarcoma EWSR1-CREB1; angiomatoid fibrous histiocytoma +FN1 chr2 215845895 215921094 sarcoma FN1 fusions; calcifying aponeurotic fibroma +FEV chr2 219469462 219473559 sarcoma EWSR1-FEV; Ewing sarcoma +PAX3 chr2 222684993 222784124 sarcoma PAX3-FOXO1; alveolar rhabdomyosarcoma +COL6A3 chr2 237815093 237905193 sarcoma COL6A3-CSF1; tenosynovial giant cell tumour +ERG chr21 36750675 37045540 sarcoma FUS-ERG / EWSR1-ERG; Ewing sarcoma +SMARCB1 chr22 24234168 24285193 sarcoma INI1; epithelioid sarcoma, rhabdoid tumour +EWSR1 chr22 29731759 29763936 sarcoma FET family; Ewing sarcoma and many other fusions +NF2 chr22 30066918 30161963 sarcoma Schwannoma and mesothelioma tumour suppressor +PATZ1 chr22 31789769 31810306 sarcoma EWSR1-PATZ1 round cell sarcoma +PDGFB chr22 39694033 39715652 sarcoma COL1A1-PDGFB; dermatofibrosarcoma protuberans +RAF1 chr3 12582405 12665632 sarcoma Kinase fusion; MAPK activation +CTNNB1 chr3 41214904 41255840 sarcoma Beta-catenin; desmoid fibromatosis +PRKCD chr3 53194119 53225596 sarcoma PRKC fusion partner +VGLL3 chr3 87012305 87065510 sarcoma 3p12 amplification +TFG chr3 103415045 103454716 sarcoma Kinase fusion partner (NTRK1/ROS1/ALK) +WWTR1 chr3 152268462 152476040 sarcoma WWTR1-CAMTA1; epithelioid haemangioendothelioma +LPP chr3 190970544 191707297 sarcoma HMGA2-LPP; lipoma, chondroid hamartoma +FGFR3 chr4 1791772 1807344 sarcoma RTK fusion and amplification +PDGFRA chr4 57718046 57786996 sarcoma GIST; imatinib target +KIT chr4 58146698 58229411 sarcoma GIST; imatinib target +KDR chr4 58566962 58614067 sarcoma VEGFR2; angiosarcoma +DUX4 chr4 193541579 193553139 sarcoma CIC-DUX4; round cell sarcoma +SDHA chr5 209363 258771 sarcoma SDH-deficient GIST and paraganglioma +TERT chr5 1160074 1202878 sarcoma Promoter and structural activation +TRIO chr5 14080499 14449245 sarcoma TRIO-TERT; undifferentiated sarcoma +APC chr5 113218062 113356772 sarcoma Wnt pathway; desmoid fibromatosis +PDGFRB chr5 150650431 150692448 sarcoma Kinase fusions in myofibroblastic tumours +CDX1 chr5 150703382 150721163 sarcoma Rare fusion partner +NPM1 chr5 171927442 171951228 sarcoma Fusion partner +FGFR4 chr5 177630123 177641352 sarcoma Rhabdomyosarcoma; RTK +FLT4 chr5 181157398 181206988 sarcoma VEGFR3; angiosarcoma +PHF1 chr6 33231774 33237801 sarcoma PHF1 fusions; ossifying fibromyxoid tumour, ESS +FOXO3 chr6 109737167 109862124 sarcoma FOXO family transcription factor +VGLL2 chr6 118449447 118457522 sarcoma VGLL2 fusions; infantile spindle cell RMS +ROS1 chr6 118471267 118609847 sarcoma Kinase fusions +ETV1 chr7 14023217 14123457 sarcoma ETV1 rearrangement +JAZF1 chr7 27968286 28318176 sarcoma JAZF1-SUZ12; endometrial stromal sarcoma +EGFR chr7 55178937 55372056 sarcoma Receptor tyrosine kinase amplification +CDK6 chr7 93846868 94078562 sarcoma Cell cycle kinase amplification +MET chr7 117987305 118113574 sarcoma Receptor tyrosine kinase amplification +BRAF chr7 142027505 142239131 sarcoma MAPK activation +CNTNAP2 chr7 147296849 149602894 sarcoma 7q35 deletion +FGFR1 chr8 38688107 38745588 sarcoma RTK fusion and amplification +PLAG1 chr8 56537883 56588262 sarcoma PLAG1 fusions; lipoblastoma, myoepithelioma +MYBL1 chr8 66987925 67038972 sarcoma Transcription factor; rare rearrangement +NCOA2 chr8 70539621 70886370 sarcoma NCOA2 fusions; congenital spindle cell RMS +HEY1 chr8 80195346 80199104 sarcoma HEY1-NCOA2; mesenchymal chondrosarcoma +MYC chr8 128862888 128870405 sarcoma 8q24 amplification; radiation-associated angiosarcoma +PTPRD chr9 8320802 10622316 sarcoma Deletion; tumour suppressor +CDKN2A chr9 21982052 22009697 sarcoma CDK4/6 inhibitor; deleted in MPNST/DDLPS +CDKN2B chr9 22017276 22023690 sarcoma Co-deleted with CDKN2A +VCP chr9 35075243 35091804 sarcoma Fusion partner +NTRK2 chr9 96818888 97177857 sarcoma Kinase fusions +NR4A3 chr9 111993520 112038527 sarcoma EWSR1-NR4A3; extraskeletal myxoid chondrosarcoma +BCOR chrX 39452491 39578567 sarcoma BCOR-CCNB3 / BCOR-ITD round cell sarcoma +SSX1 chrX 47664400 47676453 sarcoma SS18-SSX1; synovial sarcoma +SSX4 chrX 47811050 47820861 sarcoma SS18-SSX4; synovial sarcoma +TFE3 chrX 48440314 48454961 sarcoma ASPSCR1-TFE3 alveolar soft part sarcoma; PEComa +CCNB3 chrX 49520371 49623348 sarcoma BCOR-CCNB3 round cell sarcoma +SSX2 chrX 52036147 52046488 sarcoma SS18-SSX2; synovial sarcoma +FOXO4 chrX 69529947 69537628 sarcoma FOXO family transcription factor +OGT chrX 69966355 70009143 sarcoma OGT-PHF1; endometrial stromal sarcoma diff --git a/assets/lrsomatic_report/assets/js/bnd_circos.js b/assets/lrsomatic_report/assets/js/bnd_circos.js new file mode 100644 index 00000000..5d13d237 --- /dev/null +++ b/assets/lrsomatic_report/assets/js/bnd_circos.js @@ -0,0 +1,307 @@ +/* Breakend circos, drawn in the browser from window.BND_DATA (see R/circos_bnd.R). + * + * The point of drawing here rather than in R is re-layout: which chromosomes get a + * sector, and therefore every sector's angular width, depends on which links survive the + * current filter. A server-drawn SVG bakes those angles in and can only dim what it + * already drew. + * + * Coordinates are computed in a fixed 1000x1000 user space and the SVG is scaled by its + * viewBox, so nothing here needs to know the rendered pixel size. + */ +(function () { + "use strict"; + + var SVG_NS = "http://www.w3.org/2000/svg"; + + // Geometry, in the 1000x1000 user space. Ordered outward from the centre. + // The ring is kept well inside the box because gene labels extend outward from + // R_LABEL: a label at 3 o'clock starts at 500+R_LABEL and needs room for its text, and + // the root clips whatever runs past the viewBox. + var CX = 500, CY = 500; + var R_LINK = 314; // arcs terminate here, just inside the ideogram + var R_IDEO = 322; // ideogram ring, inner edge + var R_IDEO2 = 344; // ideogram ring, outer edge + var R_TICK = 352; // gene connector, inner end + var R_LABEL = 378; // gene labels sit on this radius + var R_CHROM = 300; // chromosome names, inside the ring + + var GAP_DEG = 2; // between adjacent sectors + var GAP_LAST_DEG = 6; // after the last, so the ring has a visible seam + var START_DEG = 90; // 12 o'clock, matching the genome-wide circos + var MIN_LABEL_SEP_DEG = 3.4; // greedy de-overlap target for gene labels + + // Giemsa stains, as circlize draws them. + var STAIN = { + gneg: "#f5f2ec", gpos25: "#d5cfc4", gpos50: "#b3aa9a", gpos75: "#8d8271", + gpos100: "#6b6153", gpos: "#6b6153", acen: "#b8593f", gvar: "#9d94c4", + stalk: "#8fa2b8" + }; + + function el(name, attrs) { + var n = document.createElementNS(SVG_NS, name); + for (var k in attrs) if (attrs[k] !== null && attrs[k] !== undefined) { + n.setAttribute(k, attrs[k]); + } + return n; + } + + // --- Layout ------------------------------------------------------------ + + /* Angular extent of each visible chromosome, proportional to its length over whatever + * is left of the circle once the inter-sector gaps are taken out. This is the whole + * reason the plot is drawn client-side: drop a chromosome and every other sector moves. + */ + function layout(chroms, lenOf) { + var total = 0, i; + for (i = 0; i < chroms.length; i++) total += lenOf(chroms[i]); + var gaps = GAP_DEG * Math.max(chroms.length - 1, 0) + GAP_LAST_DEG; + var usable = 360 - gaps; + if (total <= 0 || usable <= 0) return {}; + + var out = {}, at = START_DEG; + for (i = 0; i < chroms.length; i++) { + var span = usable * (lenOf(chroms[i]) / total); + // Angles run clockwise from 12 o'clock, which is how circlize lays these out. + out[chroms[i]] = { start: at, span: span, len: lenOf(chroms[i]) }; + at -= span + GAP_DEG; + } + return out; + } + + function angleOf(sectors, chrom, pos) { + var s = sectors[chrom]; + if (!s) return null; + var f = s.len > 0 ? Math.min(Math.max(pos / s.len, 0), 1) : 0; + return s.start - f * s.span; + } + + function pt(deg, r) { + var rad = deg * Math.PI / 180; + return [CX + r * Math.cos(rad), CY - r * Math.sin(rad)]; + } + + // Annular sector between two angles, as a filled path. + function ringPath(a0, a1, r0, r1) { + var p0 = pt(a0, r1), p1 = pt(a1, r1), p2 = pt(a1, r0), p3 = pt(a0, r0); + var large = Math.abs(a1 - a0) > 180 ? 1 : 0; + // sweep 1 = clockwise on the outer edge, because angles decrease as we go round. + return "M" + p0[0] + "," + p0[1] + + "A" + r1 + "," + r1 + " 0 " + large + " 1 " + p1[0] + "," + p1[1] + + "L" + p2[0] + "," + p2[1] + + "A" + r0 + "," + r0 + " 0 " + large + " 0 " + p3[0] + "," + p3[1] + "Z"; + } + + /* Push labels apart along the circumference until none is closer than + * MIN_LABEL_SEP_DEG to its neighbour. One greedy forward pass then one backward pass: + * the forward pass can crowd the last label, and the backward pass relieves it. + * Labels keep their radius, so a connector line is what shows the displacement. + */ + function deoverlap(items) { + if (items.length < 2) return items; + items.sort(function (a, b) { return b.angle - a.angle; }); + var i; + for (i = 1; i < items.length; i++) { + var minA = items[i - 1].angle - MIN_LABEL_SEP_DEG; + if (items[i].angle > minA) items[i].angle = minA; + } + for (i = items.length - 2; i >= 0; i--) { + var maxA = items[i + 1].angle + MIN_LABEL_SEP_DEG; + if (items[i].angle < maxA) items[i].angle = maxA; + } + return items; + } + + // --- Drawing ----------------------------------------------------------- + + function draw(host, D, visibleIds, visibleGenes, selected) { + while (host.firstChild) host.removeChild(host.firstChild); + + var lenOf = {}, i; + for (i = 0; i < D.chromosomes.length; i++) lenOf[D.chromosomes[i]] = D.lengths[i]; + + // Only the links this filter leaves visible; null means "no filter yet". + var links = D.links.filter(function (l) { + return visibleIds === null || visibleIds.has(l.id); + }); + + var touched = {}; + links.forEach(function (l) { touched[l.chromA] = 1; touched[l.chromB] = 1; }); + var chroms = D.chromosomes.filter(function (c) { return touched[c]; }); + + if (!chroms.length) { + var note = document.createElement("p"); + note.className = "bnd-circos-empty"; + note.textContent = "No breakends match the current filter."; + host.appendChild(note); + return { arcs: 0, genes: 0, chroms: 0 }; + } + + var sectors = layout(chroms, function (c) { return lenOf[c] || 0; }); + + var svg = el("svg", { + viewBox: "0 0 1000 1000", + preserveAspectRatio: "xMidYMid meet", + role: "img", + "aria-label": "Breakend circos over " + chroms.length + " chromosomes" + }); + + var gBands = el("g", { class: "bnd-bands" }); + var gBodies = el("g", { class: "bnd-bodies" }); + var gLines = el("g", { class: "bnd-lines" }); + var gLabels = el("g", { class: "bnd-labels" }); + var gChrom = el("g", { class: "bnd-chroms" }); + var gLinks = el("g", { class: "bnd-links" }); + // Arcs last so they sit above the ring; labels above those again. + [gLinks, gBands, gBodies, gLines, gChrom, gLabels].forEach(function (g) { + svg.appendChild(g); + }); + + // Ideogram bands. + D.cytobands.forEach(function (b) { + if (!sectors[b.chrom]) return; + var a0 = angleOf(sectors, b.chrom, b.start); + var a1 = angleOf(sectors, b.chrom, b.end); + if (a0 === null || a1 === null || a0 === a1) return; + gBands.appendChild(el("path", { + d: ringPath(a0, a1, R_IDEO, R_IDEO2), + class: "bnd-band", + fill: STAIN[b.stain] || STAIN.gneg, + "data-chrom": b.chrom + })); + }); + + // Sector outline, so a chromosome with sparse banding still reads as one block. + chroms.forEach(function (c) { + var s = sectors[c]; + gBands.appendChild(el("path", { + d: ringPath(s.start, s.start - s.span, R_IDEO, R_IDEO2), + class: "bnd-sector", + "data-chrom": c + })); + }); + + // Chromosome names, inside the ring, upright. + chroms.forEach(function (c) { + var s = sectors[c]; + var p = pt(s.start - s.span / 2, R_CHROM); + var t = el("text", { + x: p[0], y: p[1], class: "bnd-chrom-label", "data-chrom": c, + "text-anchor": "middle", "dominant-baseline": "middle" + }); + t.textContent = c.replace(/^chr/, ""); + gChrom.appendChild(t); + }); + + // Gene track: only the genes the visible rows actually name. + var genes = D.genes.filter(function (g) { + return sectors[g.chrom] && visibleGenes.has(g.gene); + }); + + // A gene span is invisible at this scale — MYC is 7 kb against a 145 Mb sector — so + // bodies get a floor of about half a degree. They are markers, not spans to scale. + var placed = deoverlap(genes.map(function (g) { + var mid = (g.start + g.end) / 2; + return { + gene: g.gene, panels: g.panels, chrom: g.chrom, + at: angleOf(sectors, g.chrom, mid), + angle: angleOf(sectors, g.chrom, mid), + a0: angleOf(sectors, g.chrom, g.start), + a1: angleOf(sectors, g.chrom, g.end) + }; + }).filter(function (g) { return g.at !== null; })); + + placed.forEach(function (g) { + var half = Math.max(Math.abs(g.a0 - g.a1) / 2, 0.25); + gBodies.appendChild(el("path", { + d: ringPath(g.at + half, g.at - half, R_IDEO, R_IDEO2), + class: "bnd-gene-body", + "data-gene": g.gene, "data-panels": g.panels + })); + + // Connector from the locus out to wherever de-overlapping moved the label. + var p0 = pt(g.at, R_TICK), p1 = pt(g.angle, R_LABEL - 6); + gLines.appendChild(el("polyline", { + points: p0[0] + "," + p0[1] + " " + p1[0] + "," + p1[1], + class: "bnd-gene-line", "data-gene": g.gene + })); + + // Labels read outward on the right half and inward on the left, so none is upside + // down. Flipping is why each needs its own rotate() rather than a shared transform. + var flip = Math.cos(g.angle * Math.PI / 180) < 0; + var lp = pt(g.angle, R_LABEL); + var rot = flip ? (180 - g.angle) : -g.angle; + var t = el("text", { + x: lp[0], y: lp[1], + class: "bnd-gene-label", + "data-gene": g.gene, "data-panels": g.panels, + "text-anchor": flip ? "end" : "start", + "dominant-baseline": "middle", + transform: "rotate(" + rot + " " + lp[0] + " " + lp[1] + ")" + }); + t.textContent = g.gene; + gLabels.appendChild(t); + }); + + // Arcs. A quadratic Bezier with its control point at the centre gives circlize's + // familiar bundling: near-antipodal links run almost straight, close ones bow tight. + links.forEach(function (l) { + var aA = angleOf(sectors, l.chromA, l.posA); + var aB = angleOf(sectors, l.chromB, l.posB); + if (aA === null || aB === null) return; + var pA = pt(aA, R_LINK), pB = pt(aB, R_LINK); + var picked = selected.has(l.id); + var arc = el("path", { + d: "M" + pA[0] + "," + pA[1] + "Q" + CX + "," + CY + " " + pB[0] + "," + pB[1], + class: "bnd-link" + (picked ? " is-selected" : ""), + "data-svid": l.id, + "data-svclass": l.svclass, + "data-chrom-a": l.chromA, + "data-chrom-b": l.chromB + }); + // Selected arcs go last within the group so they draw over their neighbours. + if (picked) gLinks.appendChild(arc); else gLinks.insertBefore(arc, gLinks.firstChild); + }); + + host.appendChild(svg); + return { arcs: links.length, genes: placed.length, chroms: chroms.length }; + } + + // --- Public surface ---------------------------------------------------- + + function normalise(raw) { + return { + chromosomes: raw.chromosomes || [], + lengths: raw.lengths || [], + cytobands: (raw.cytobands || []).map(function (r) { + return { chrom: r[0], start: r[1], end: r[2], stain: r[3] }; + }), + links: (raw.links || []).map(function (r) { + return { id: r[0], svclass: r[1], chromA: r[2], posA: r[3], + chromB: r[4], posB: r[5] }; + }), + genes: (raw.genes || []).map(function (r) { + return { chrom: r[0], start: r[1], end: r[2], gene: r[3], panels: r[4] }; + }) + }; + } + + var DATA = null; + + window.bndCircos = { + /* Draw into `host` for the given filter state. + * visibleIds: Set of SV ids the table currently shows, or null for "everything". + * visibleGenes: Set of gene symbols those rows name in their panel_hit column. + * selected: Set of SV ids the user has clicked. + * Returns {arcs, genes, chroms} for the caption. + */ + render: function (host, visibleIds, visibleGenes, selected) { + if (!host) return { arcs: 0, genes: 0, chroms: 0 }; + if (!DATA) { + if (!window.BND_DATA) return { arcs: 0, genes: 0, chroms: 0 }; + DATA = normalise(window.BND_DATA); + } + return draw(host, DATA, visibleIds, visibleGenes || new Set(), + selected || new Set()); + } + }; +})(); diff --git a/assets/lrsomatic_report/assets/styles/report.scss b/assets/lrsomatic_report/assets/styles/report.scss index 444572b7..ac3fd96c 100644 --- a/assets/lrsomatic_report/assets/styles/report.scss +++ b/assets/lrsomatic_report/assets/styles/report.scss @@ -114,6 +114,10 @@ $border-radius-lg: 12px; --circos-cnv-total: #1b1e22; // BND/translocation --circos-bnd: #8a5fa3; + // Breakend circos only (R/circos_bnd.R): the arc a table row selects, and the + // panel-gene bodies drawn on the ideogram. + --circos-bnd-selected: #d7263d; + --circos-bnd-gene: #0d5c75; // Metric-card accent colours --metric-purity: #{$report-danger}; @@ -151,6 +155,8 @@ $border-radius-lg: 12px; // Circos legend swatches that would be invisible on dark bg — lighten to match --color-text --circos-snv-cg: #e8e6df; --circos-cnv-total: #e8e6df; + // The breakend circos keeps a light plate, so its own colours are unchanged; only + // the caption and the selected-row tint sit on the dark surface. } body { background: var(--color-bg) !important; color: var(--color-text) !important; } @@ -862,6 +868,161 @@ div.dt-buttons { .circos-eyebrow { color: #666; } } +// ============================================================ +// Breakend circos — drawn client-side, cross-linked to the SV table +// ============================================================ +// The plot is built by assets/js/bnd_circos.js and redrawn on every filter change, so +// that its sector layout follows the *filtered* link set. The drawing code sets geometry +// and nothing else; everything visual is here. +.bnd-circos { + background: #fcfbf7; + border: 1px solid #d8d3c8; + border-radius: var(--card-radius); + padding: 18px 18px 10px; + box-shadow: var(--card-shadow), inset 0 0 0 1px rgba(255,255,255,0.6); + max-width: 640px; + margin: 0 auto; + // The plate stays light in dark mode, as .circos-container does, which is also what + // lets the labels resolve currentColor to dark ink in both themes. + color: #1b1e22; + + // aspect-ratio rather than relying on `height: auto` resolving through the viewBox: + // an inline with no width/height attributes has fallen back to the default + // 150px height in some engines, which would squash the plot. + svg { width: 100%; height: auto; aspect-ratio: 1 / 1; display: block; } + + .bnd-link { + fill: none; + stroke: var(--circos-bnd); + stroke-width: 1; + opacity: 0.5; + transition: opacity 0.12s ease, stroke 0.12s ease, stroke-width 0.12s ease; + } + // Filtered-out arcs are not drawn at all, so there is no dim state to style. + .bnd-link.is-selected { + stroke: var(--circos-bnd-selected); + stroke-width: 2.4; + opacity: 1; + } + + // Ideogram: the bands carry their Giemsa fill inline (it is data, not decoration); + // the sector outline underneath keeps a sparsely-banded chromosome reading as a block. + .bnd-sector { fill: none; stroke: #b0a99c; stroke-width: 0.6; } + .bnd-band { stroke: none; } + + .bnd-gene-body { fill: var(--circos-bnd-gene); stroke: none; } + .bnd-gene-line { fill: none; stroke: #b8b2a6; stroke-width: 0.8; } + + .bnd-gene-label { + font-family: var(--font-sans); + font-size: 15px; // user-space units: the viewBox is 1000 wide + font-weight: 600; + fill: var(--circos-bnd-gene); + } + .bnd-chrom-label { + font-family: var(--font-sans); + font-size: 17px; + font-weight: 600; + fill: currentColor; + } + + .bnd-circos-empty { + text-align: center; + font-size: 0.8rem; + color: #6b6153; + padding: 3rem 0; + margin: 0; + } +} + +@media (prefers-color-scheme: dark) { + .bnd-circos { + border-color: #4a4540; + box-shadow: 0 0 0 1px rgba(0,0,0,0.5), 0 8px 32px -8px rgba(0,0,0,0.7); + } +} + +.bnd-circos-caption { + font-family: var(--font-mono); + font-size: 0.7rem; + letter-spacing: 0.04em; + color: var(--color-text-muted); + text-align: center; + margin: 0.55rem 0 0; +} + +// Rows picked in the SV table — the ones whose arcs are highlighted +#sv-table table.dataTable tbody tr { cursor: pointer; } +#sv-table table.dataTable tbody tr.bnd-selected td { + background: var(--color-primary-muted) !important; + box-shadow: inset 3px 0 0 var(--circos-bnd-selected); +} + +// ============================================================ +// Table footnote — provenance / caveats printed under a data table +// ============================================================ +.table-footnote { + font-size: 0.78rem; + line-height: 1.5; + color: var(--color-text-muted); + margin: 0.6rem 0 0; + max-width: 78ch; + + code { + font-size: 0.95em; + background: color-mix(in srgb, var(--color-text-muted) 10%, transparent); + padding: 0.05em 0.35em; + border-radius: 3px; + } + + // The consensus caveat is the one line a reader must not skim past. + .table-footnote__warn { color: var(--color-text); font-weight: 500; } +} + +// Footnotes are folded away by default — they are provenance, consulted on demand, not +// something to read past on every table. The disclosure stays quiet until hovered. +.table-details { + margin: 0.6rem 0 0; + + > summary { + cursor: pointer; + width: fit-content; + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--color-text-muted); + opacity: 0.75; + list-style: none; // suppress the default triangle; ::before draws it + user-select: none; + + &::-webkit-details-marker { display: none; } + + &::before { + content: "▸"; + display: inline-block; + margin-right: 0.4em; + font-size: 0.9em; + transition: transform 0.12s ease; + } + + &:hover { opacity: 1; } + } + + &[open] > summary::before { transform: rotate(90deg); } + + // The body is a .table-footnote; its own top margin would double the gap. + > .table-footnote { margin-top: 0.4rem; } +} + +@media print { + .table-footnote { color: #666; } + // Print is a static medium — a collapsed disclosure would silently drop the + // provenance from the printed page. + .table-details > .table-footnote { display: block !important; } + .table-details > summary { display: none; } +} + // ============================================================ // Callouts // ============================================================ diff --git a/assets/lrsomatic_report/bin/render_report.R b/assets/lrsomatic_report/bin/render_report.R index c1293969..15d83271 100755 --- a/assets/lrsomatic_report/bin/render_report.R +++ b/assets/lrsomatic_report/bin/render_report.R @@ -55,34 +55,6 @@ output = if (!is.null(opt[["output"]])) opt[["output"]] else title = if (!is.null(opt[["title"]])) opt[["title"]] else paste0("LRSomatic Report – ", sample_id) -# ---- Load all available gene panels ---------------------------------------- -# The rendered report always ships every builtin panel so the reader can switch -# panels client-side; --gene-panel only decides which one is selected on load. -# "__all__" is the sentinel the report's JS uses for "no filter" — it must stay -# in sync with templates/sections/_gene_filter.qmd and the search hook in -# templates/per_sample.qmd. -all_panels = load_all_gene_panels(file.path(repo_dir, "assets")) -default_panel = if (is_no_gene_panel(gene_panel)) { - gene_panel = "none" - "__all__" -} else if (file.exists(file.path(repo_dir, "assets", "gene_lists", - paste0(gene_panel, ".tsv")))) { - gene_panel -} else if (file.exists(gene_panel)) { - # A user-supplied TSV: register it alongside the builtins so it can be - # selected on load (and switched away from and back to) in the report. - nm = tools::file_path_sans_ext(basename(gene_panel)) - if (nm %in% names(all_panels)) nm = paste0(nm, "-custom") - all_panels[[nm]] = load_gene_panel(gene_panel) - # Absolute, because the template resolves it again from Quarto's own working - # directory (the copied template dir), not from where this script was invoked. - gene_panel = normalizePath(gene_panel) - nm -} else { - abort(paste0("--gene-panel not found: tried builtin '", gene_panel, - "' and as a file path. Use 'none' for no filtering.")) -} - # ---- Locate per-tool outputs --------------------------------------------- message("Locating outputs in: ", sample_dir) outputs = locate_outputs(sample_dir, sample_id) @@ -93,6 +65,8 @@ message("Somatic VAF VCF: ", ifelse(is.null(outputs$somatic_vaf_vcf), "NOT FOUND message("ASCAT segments: ", ifelse(is.null(outputs$ascat_segments), "NOT FOUND", outputs$ascat_segments)) # ---- Auto-detect reference ----------------------------------------------- +# Resolved before the gene panels below, which are reference-specific: a panel's +# coordinates are only valid for the genome they were built on. reference = opt[["reference"]] if (reference == "auto") { # Reuse already-resolved paths rather than a fixed vep/somatic/* glob @@ -107,6 +81,41 @@ if (reference == "auto") { } reference = tolower(reference) +# ---- Load all available gene panels ---------------------------------------- +# The rendered report always ships every builtin panel so the reader can switch +# panels client-side; --gene-panel only decides which one is selected on load. +# "__all__" is the sentinel the report's JS uses for "no filter" — it must stay +# in sync with templates/sections/_gene_filter.qmd and the search hook in +# templates/per_sample.qmd. +# Builtins that ship per reference ("lymphoid.hg38.tsv") resolve to one entry for +# the reference detected above; a coordinate panel declaring a different one is a +# hard error rather than a filter matching the wrong genome. +all_panels = load_all_gene_panels(file.path(repo_dir, "assets"), reference) +default_panel = if (is_no_gene_panel(gene_panel)) { + gene_panel = "none" + "__all__" +} else if (!is.null(builtin_panel_path(file.path(repo_dir, "assets"), gene_panel, reference))) { + # Load it here too, so a builtin that fails to resolve against this reference aborts + # now rather than part-way through the Quarto render. + invisible(tryCatch(resolve_gene_panel(gene_panel, file.path(repo_dir, "assets"), reference), + error = function(e) abort(conditionMessage(e)))) + gene_panel +} else if (file.exists(gene_panel)) { + # A user-supplied TSV: register it alongside the builtins so it can be + # selected on load (and switched away from and back to) in the report. + nm = tools::file_path_sans_ext(basename(gene_panel)) + if (nm %in% names(all_panels)) nm = paste0(nm, "-custom") + all_panels[[nm]] = tryCatch(load_gene_panel(gene_panel, reference), + error = function(e) abort(conditionMessage(e))) + # Absolute, because the template resolves it again from Quarto's own working + # directory (the copied template dir), not from where this script was invoked. + gene_panel = normalizePath(gene_panel) + nm +} else { + abort(paste0("--gene-panel not found: tried builtin '", gene_panel, + "' and as a file path. Use 'none' for no filtering.")) +} + # ---- Render the Quarto template ----------------------------------------- # Copy templates/ and assets/ into a writable working directory: repo_dir's # own templates/ may be read-only (e.g. inside a container), and Quarto diff --git a/assets/lrsomatic_report/templates/per_sample.qmd b/assets/lrsomatic_report/templates/per_sample.qmd index 33f058fd..4f528345 100644 --- a/assets/lrsomatic_report/templates/per_sample.qmd +++ b/assets/lrsomatic_report/templates/per_sample.qmd @@ -42,6 +42,7 @@ source(file.path(repo_dir, "R/parse_severus.R")) source(file.path(repo_dir, "R/parse_ascat.R")) source(file.path(repo_dir, "R/parse_qc.R")) source(file.path(repo_dir, "R/circos.R")) +source(file.path(repo_dir, "R/circos_bnd.R")) source(file.path(repo_dir, "R/sections.R")) for (f in list.files(file.path(repo_dir, "R/sections"), pattern = "\\.R$", full.names = TRUE)) { @@ -68,11 +69,14 @@ chromosomes = chromosomes[chromosomes %in% unique(cytobands$chrom)] # Load the gene panel selected on load (NULL when --gene-panel none, i.e. no # filtering). Only the "Panel variants"/"Panel SVs" summary cards use it — the # tables themselves are built unfiltered and filtered client-side. -panel_arg = params$gene_panel -panel_genes = tryCatch( - resolve_gene_panel(panel_arg, file.path(repo_dir, "assets")), - error = function(e) { message("Gene panel error: ", e$message); NULL } -) +# +# Deliberately not wrapped in tryCatch: a panel that can't be resolved, or whose +# declared reference is not the one being rendered against, has to fail the render +# rather than produce a report whose filter silently matches nothing (or matches the +# wrong genome's coordinates). +panel_arg = params$gene_panel +panel = resolve_gene_panel(panel_arg, file.path(repo_dir, "assets"), params$reference) +panel_genes = if (is.null(panel)) NULL else panel$genes # Parse ASCAT ascat_segments = parse_ascat_segments(outputs$ascat_segments) @@ -83,12 +87,12 @@ wakhan_solutions = parse_wakhan_solutions(outputs$wakhan_solutions) wakhan_cn_plots = locate_wakhan_cn_plots(outputs$wakhan_dir, wakhan_solutions) # Parse QC -qc_mosdepth = parse_mosdepth_summary(outputs$mosdepth_summary) +qc_mosdepth = parse_mosdepth_summary(outputs$mosdepth_summary, names(chrom_lens)) qc_mosdist = parse_mosdepth_dist(outputs$mosdepth_dist) qc_cramino = parse_cramino(outputs$cramino) qc_flagstat = parse_flagstat(outputs$flagstat) samtools_stats = parse_samtools_stats(outputs$samtools_stats) -qc_normal_mosdepth = parse_mosdepth_summary(outputs$normal_mosdepth_summary) +qc_normal_mosdepth = parse_mosdepth_summary(outputs$normal_mosdepth_summary, names(chrom_lens)) qc_normal_cramino = parse_cramino(outputs$normal_cramino) qc_normal_flagstat = parse_flagstat(outputs$normal_flagstat) qc_normal_samtools_stats = parse_samtools_stats(outputs$normal_samtools_stats) @@ -96,11 +100,25 @@ qc_normal_samtools_stats = parse_samtools_stats(outputs$normal_samtools_stats) # Parse VEP + raw callers vep_data = parse_vep(outputs$vep_somatic) -vaf_data = parse_caller_vcf(outputs$somatic_vaf_vcf, "somatic") +vaf_data = parse_caller_vcf(outputs$somatic_vaf_vcf, "somatic", sample_id = params$sample_id) + +# Where the VAF/DP/GT/PS columns came from, for the footnote under the variant table. One +# file supplies them for every variant, which is ambiguous after a consensus merge — see +# templates/sections/_smallvariants.qmd. +vaf_prov = vaf_provenance(outputs$somatic_vaf_vcf, sample_dir, sample_id = params$sample_id) variant_table = build_variant_table(vep_data, vaf_data, gene_panel = NULL) tmb_info = compute_tmb(variant_table) +# Join coverage: a silently failing variant_key() shows up here as a near-zero count +# rather than as an error (see the variant_key() note in CLAUDE.md). +n_vaf = if (!is.null(variant_table) && "vaf" %in% names(variant_table)) + sum(!is.na(variant_table$vaf)) else NA_integer_ +# Variants reported by more than one caller: `callers` collapses to "clairs,deepsomatic". +# Always 0 on the VEP text path, which carries no per-variant caller at all. +n_multi = if (!is.null(variant_table) && "callers" %in% names(variant_table)) + sum(grepl(",", variant_table$callers, fixed = TRUE)) else 0L + # SNV data for circos (from VEP file — contains all somatic variants) snv_circos = NULL if (!is.null(vep_data) && nrow(vep_data) > 0) { @@ -128,37 +146,156 @@ tryCatch({ # Summary counts sv_table = SECTION_DATA$sv$table n_snv = if (!is.null(vep_data)) nrow(unique(vep_data[, .(chrom, pos, ref, alt)])) else NA_integer_ -n_sv = if (!is.null(SECTION_DATA$sv)) - nrow(SECTION_DATA$sv$circos$nontrans) + nrow(SECTION_DATA$sv$circos$translocations) else NA_integer_ +# One rearrangement, one row: Severus writes both sides of a breakend as separate +# records, and parse_severus_somatic_records() has already collapsed them. +n_sv = if (!is.null(sv_table) && nrow(sv_table) > 0) nrow(sv_table) else + if (!is.null(SECTION_DATA$sv)) + nrow(SECTION_DATA$sv$circos$nontrans) + + nrow(SECTION_DATA$sv$circos$translocations) else NA_integer_ +# Interchromosomal breakends, reported apart from the intra-chromosomal ones (which +# are mostly foldbacks, and which the old count filed under "translocations"). +n_trans = if (!is.null(sv_table) && "svclass" %in% names(sv_table)) + sum(sv_table$svclass == "translocation", na.rm = TRUE) else NA_integer_ +n_intra_bnd = if (!is.null(sv_table) && "svclass" %in% names(sv_table)) + sum(sv_table$svclass == "intra-chr breakend", na.rm = TRUE) else NA_integer_ # No panel selected on load → the panel cards have nothing to count, so they read # "N/A" rather than a "0" that looks like "no panel genes hit". -have_panel = length(panel_genes) > 0 +have_panel = !is.null(panel) && length(panel_genes) > 0 n_panel_vars = if (!have_panel) NA_integer_ else if (!is.null(variant_table)) sum(variant_table$symbol %in% panel_genes, na.rm = TRUE) else 0L -# Gene-hits column differs by annotation source: "gene_hits" (VEP path) vs -# "NHL_GENE_HITS" (gene-annotated TSV fallback path) -sv_gene_col = intersect(c("gene_hits", "NHL_GENE_HITS"), names(sv_table)) -n_panel_svs = if (!have_panel) NA_integer_ else - if (!is.null(sv_table) && nrow(sv_table) > 0 && length(sv_gene_col) > 0) - sum(vapply(sv_table[[sv_gene_col[1]]], function(h) - any(trimws(unlist(strsplit(as.character(h), "[;,]+"))) %in% panel_genes), - logical(1)), na.rm = TRUE) else 0L +# Same test, same windows as the client-side filter — sv_panel_hits() is the one +# implementation both call, so the card and the filter cannot drift. +sv_panel_hit = if (!is.null(sv_table) && nrow(sv_table) > 0) + sv_panel_hits(sv_table, panel) else character(0) +n_panel_svs = if (!have_panel) NA_integer_ else sum(nzchar(sv_panel_hit)) ``` ```{r panel-js-data, results='asis'} all_p = if (!is.null(params$all_panels) && length(params$all_panels) > 0) params$all_panels else list() -js_panels = paste0( - "const GENE_PANELS = {", - paste(vapply(names(all_p), function(nm) { - genes_json = paste0('"', all_p[[nm]], '"', collapse = ", ") - paste0('"', nm, '": new Set([', genes_json, '])') - }, character(1)), collapse = ",\n"), - "};\n", - 'const DEFAULT_PANEL = "', params$default_panel, '";\n' -) -cat("\n", sep = "") + +# Each panel ships both forms: the symbol set (small variants always match on symbols, +# and a symbol-only panel matches SVs that way too) and, for a coordinate-carrying +# panel, its intervals. The two windows come from R (SV_PANEL_WINDOW_* in +# R/parse_severus.R) rather than being written out again here, so the "Panel SVs" card +# and this filter cannot drift apart. +panel_defs = vapply(names(all_p), function(nm) { + p = all_p[[nm]] + syms = paste0("new Set([", + paste(js_quote(toupper(as.character(p$genes))), collapse = ","), "])") + ivs = if (isTRUE(p$has_coords)) + paste0("[", paste(sprintf("[%s,%s,%s,%s]", + js_quote(p$chrom), as.integer(p$start), as.integer(p$end), + js_quote(toupper(as.character(p$interval_gene)))), + collapse = ","), "]") + else "[]" + sprintf("%s: {symbols: %s, hasCoords: %s, intervals: %s}", + js_quote(nm), syms, if (isTRUE(p$has_coords)) "true" else "false", ivs) +}, character(1)) + +cat("\n") ``` {{< include sections/_header.qmd >}} @@ -185,53 +322,35 @@ cat("\n", sep = "") --- -{{< include sections/_whatshap.qmd >}} - ---- - {{< include sections/_qc.qmd >}} --- -*Report generated `r format(Sys.time(), "%Y-%m-%d %H:%M")` · LRSomatic report v1.1.0* +*Report generated `r format(Sys.time(), "%Y-%m-%d %H:%M")` · LRSomatic report v1.2.1* ```{=html} ')) diff --git a/assets/lrsomatic_report/templates/sections/_header.qmd b/assets/lrsomatic_report/templates/sections/_header.qmd index f32e5dc5..897c02e7 100644 --- a/assets/lrsomatic_report/templates/sections/_header.qmd +++ b/assets/lrsomatic_report/templates/sections/_header.qmd @@ -29,9 +29,13 @@ card = function(label, value, css_class = "", subtitle = NULL) { ) } -# Keep acronym uppercase but let the trailing plural "s" stay lowercase +# Keep the acronym uppercase but let the trailing plural "s" stay lowercase. +# Built as one HTML string rather than nested tags: htmltools puts each child on its own +# line, and that newline renders as a space — the label came out as "SNV s". lc_plural = function(text) { - tags$span(substr(text, 1, nchar(text) - 1L), tags$span(class = "lc", "s")) + stopifnot(endsWith(text, "s")) + HTML(paste0(htmlEscape(substr(text, 1, nchar(text) - 1L)), + 's')) } div(class = "metric-grid", @@ -40,7 +44,10 @@ div(class = "metric-grid", card("Mean coverage", paste0(fmt_val(qc_mosdepth$mean_depth), "×"), "metric-coverage"), card("Read N50", if (!is.na(qc_cramino$n50)) fmt_bp(qc_cramino$n50) else "N/A", "metric-n50"), card(lc_plural("Somatic SNVs"), fmt_val(n_snv, 0), "metric-snvs"), - card(lc_plural("Somatic SVs"), fmt_val(n_sv, 0), "metric-svs"), + # One row per rearrangement, not per breakend record — see the note under the SV table. + card(lc_plural("Somatic SVs"), fmt_val(n_sv, 0), "metric-svs", + subtitle = if (!is.na(n_trans) && !is.na(n_intra_bnd)) + paste0(n_trans, " interchrom · ", n_intra_bnd, " intrachrom BND") else NULL), card("Panel variants",fmt_val(n_panel_vars, 0), "metric-panel-vars"), card(lc_plural("Panel SVs"), fmt_val(n_panel_svs, 0), "metric-panel-svs"), card("Coding TMB", diff --git a/assets/lrsomatic_report/templates/sections/_qc.qmd b/assets/lrsomatic_report/templates/sections/_qc.qmd index ed5f8698..63e78b7f 100644 --- a/assets/lrsomatic_report/templates/sections/_qc.qmd +++ b/assets/lrsomatic_report/templates/sections/_qc.qmd @@ -3,20 +3,24 @@ ::: {.callout-note collapse="true"} ### Coverage summary -```{r coverage-table, results='asis'} +```{r coverage-table} +# The widget must be the chunk's *value*, not print()ed: under quarto_render() +# interactive() is FALSE and getOption("viewer") is NULL, so print.htmlwidget writes a +# standalone file to a temp dir and returns invisibly — knit_print.htmlwidget, which is +# what emits the widget div and registers its dependencies, never runs. That is why this +# table rendered as an empty callout while every other DT in the report worked. +cov_dt = function(x) DT::datatable(x, rownames = FALSE, + options = list(pageLength = 30, dom = "ft", + scrollY = "300px")) show_normal_cov = outputs$has_normal && !is.null(qc_normal_mosdepth) && nrow(qc_normal_mosdepth$table) > 0 -if (show_normal_cov) cat("**Tumour**\n\n") -if (!is.null(qc_mosdepth$table) && nrow(qc_mosdepth$table) > 0) { - print(DT::datatable(qc_mosdepth$table, rownames = FALSE, - options = list(pageLength = 30, dom = "ft", scrollY = "300px"))) -} else { - cat("Mosdepth summary not available.\n") -} -if (show_normal_cov) { - cat("\n\n**Normal**\n\n") - print(DT::datatable(qc_normal_mosdepth$table, rownames = FALSE, - options = list(pageLength = 30, dom = "ft", scrollY = "300px"))) -} +have_cov = !is.null(qc_mosdepth$table) && nrow(qc_mosdepth$table) > 0 + +htmltools::tagList( + if (show_normal_cov) tags$p(tags$strong("Tumour")), + if (have_cov) cov_dt(qc_mosdepth$table) else section_notice("Mosdepth summary not available."), + if (show_normal_cov) tags$p(tags$strong("Normal")), + if (show_normal_cov) cov_dt(qc_normal_mosdepth$table) +) ``` ::: @@ -70,3 +74,5 @@ if (show_normal_cr) { } ``` ::: + +{{< include sections/_whatshap.qmd >}} diff --git a/assets/lrsomatic_report/templates/sections/_smallvariants.qmd b/assets/lrsomatic_report/templates/sections/_smallvariants.qmd index d0089ebc..2c089630 100644 --- a/assets/lrsomatic_report/templates/sections/_smallvariants.qmd +++ b/assets/lrsomatic_report/templates/sections/_smallvariants.qmd @@ -1,6 +1,6 @@ ## Small variants -```{r small-variant-table} +```{r small-variant-prepare, results='asis'} if (!is.null(variant_table) && nrow(variant_table) > 0) { # Format the VAF as a percentage dt_display = copy(variant_table) @@ -8,8 +8,23 @@ if (!is.null(variant_table) && nrow(variant_table) > 0) { dt_display[, vaf := round(vaf * 100, 1)] setnames(dt_display, "vaf", "VAF%") } + # The panel filter reads the symbol column by name, not by a hard-coded position. + cat("\n", + sep = "") +} +``` + +```{r small-variant-table} +if (!is.null(variant_table) && nrow(variant_table) > 0) { - DT::datatable( + # Cells to flag: a variant reported by several callers has its VAF/DP/GT/PS taken from + # whichever one won the consensus merge, so those columns need not describe the caller + # named beside them. styleEqual() needs the literal values, not a predicate. + mc_vals = if ("callers" %in% names(dt_display)) + unique(grep(",", dt_display$callers, fixed = TRUE, value = TRUE)) else + character(0) + + snv_dt = DT::datatable( dt_display, rownames = FALSE, filter = "top", @@ -35,5 +50,66 @@ if (!is.null(variant_table) && nrow(variant_table) > 0) { c("#f7e3df", "#f6edd6", "#e4efe3", "#f3f1ea") ) ) + + if (length(mc_vals) > 0) { + snv_dt = DT::formatStyle( + snv_dt, + columns = "callers", + target = "cell", + # Deliberately outside the impact palette above — the same tint in two columns of + # one table would read as one scale. + backgroundColor = DT::styleEqual(mc_vals, rep("#e6ecf5", length(mc_vals))) + ) + } + + # Provenance footnote. One VCF supplies VAF/DP/GT/PS for every variant in this table, and + # after a consensus merge its FORMAT fields come from whichever caller won each variant — + # so say which file it was, how much of the table it actually covered, and flag the rows + # where the attribution is ambiguous. + # + # n_multi is always 0 on the VEP text path, which carries no per-variant caller: the + # caveat therefore appears only on the merged multi-caller (CSQ) path, which is exactly + # where the ambiguity exists. + footnote = table_details( + summary = "VAF provenance", + tags$p( + if (!is.null(vaf_prov)) { + tagList( + "VAF, depth, genotype and phase set are joined from ", + lapply(seq_along(vaf_prov$paths), function(i) { + tagList(if (i > 1) ", ", tags$code(vaf_prov$paths[i])) + }), + # Punctuation rides along with the preceding element: adjacent tagList members + # are emitted on separate lines, which HTML collapses to a space. + if (length(vaf_prov$sources) > 0) + paste0(" (", paste(vaf_prov$sources, collapse = ", "), ");") else ";", + "the variant set itself comes from the VEP annotation.", + # Named only when the VCF held more than one sample — that is the only case + # where picking the wrong column would silently report the normal's VAF. + if (length(vaf_prov$sample) > 0) + sprintf("Read from sample %s.", paste(vaf_prov$sample, collapse = ", ")) else NULL, + if (!is.na(n_vaf)) + sprintf("%s of %s variants carry a VAF.", + format(n_vaf, big.mark = ","), + format(nrow(variant_table), big.mark = ",")) else NULL + ) + } else { + "No VAF source VCF was found, so the VAF, depth, genotype and phase set columns are empty." + }, + if (length(mc_vals) > 0) tagList( + tags$br(), + tags$span( + class = "table-footnote__warn", + sprintf(paste("%s variants were reported by more than one caller (highlighted).", + "Their VAF, depth, genotype and phase set come from whichever caller", + "won the consensus merge, so those columns need not correspond to the", + "callers column beside them."), + format(n_multi, big.mark = ",")) + ) + ) + ) + ) + + tagList(snv_dt, footnote) } ``` diff --git a/assets/lrsomatic_report/templates/sections/_sv.qmd b/assets/lrsomatic_report/templates/sections/_sv.qmd index 129f10a4..3590385e 100644 --- a/assets/lrsomatic_report/templates/sections/_sv.qmd +++ b/assets/lrsomatic_report/templates/sections/_sv.qmd @@ -1,47 +1,408 @@ -## Structural variants in gene panel +## Structural variants ```{r sv-info} +# The table is built from the caller VCF, not from the annotation, so it survives a +# missing VEP SV VCF — an empty table here means no SV records were found at all. if (is.null(sv_table) || nrow(sv_table) == 0) { - section_notice( - if (!isTRUE(SECTION_DATA$sv$annotation_found)) "SV annotation file (VEP or gene TSV) not found." - else "No somatic structural variants detected." + section_notice("No somatic structural variants detected.") +} +``` + +```{r sv-bnd-circos} +# Breakend circos: the same rearrangements as the table below, drawn as arcs over just +# the chromosomes they touch. R only selects and serialises the data — the plot itself is +# built by assets/js/bnd_circos.js, so that it can re-lay-out to the chromosomes that +# survive the current filter instead of dimming a fixed set. See R/circos_bnd.R. +bnd_res = NULL +if (!is.null(sv_table) && nrow(sv_table) > 0) { + bnd_res = tryCatch({ + # One filtered set feeds both calls, so the gene track cannot label an arc that was + # never drawn. + bnd_drawn = bnd_links(sv_table, chromosomes) + bnd_circos_data(bnd_drawn, + bnd_panel_genes(bnd_drawn, params$all_panels), + cytobands, chrom_lens, chromosomes) + }, error = function(e) { + message("Breakend circos failed: ", conditionMessage(e)) + NULL + }) +} + +if (!is.null(bnd_res) && !is.null(bnd_res$data)) { + legend_item = function(swatch, var, label) { + tags$div(class = "circos-legend__item", + tags$span(class = paste0("circos-swatch--", swatch), + style = paste0("--_swatch: var(", var, ")")), + label) + } + tagList( + # The chromosome count is filled in by the plot, since it changes with the filter. + tags$p(class = "circos-eyebrow", + tags$span(id = "bnd-eyebrow", "Breakends"), + " · select a row below to highlight its arc"), + tags$div(class = "bnd-circos", id = "bnd-circos"), + tags$div(class = "circos-legend", + tags$div(class = "circos-legend__group", + tags$div(class = "circos-legend__title", "Breakend circos"), + legend_item("arc", "--circos-bnd", "Rearrangement"), + legend_item("arc", "--circos-bnd-selected", "Selected in the table"), + legend_item("bar", "--circos-bnd-gene", "Panel gene") + ) + ), + tags$p(class = "bnd-circos-caption", tags$span(id = "bnd-shown-count")) ) +} else if (!is.null(bnd_res)) { + section_notice(bnd_res$reason) +} +``` + +```{r sv-bnd-circos-data, results='asis'} +# The payload, then the drawing code. bnd_circos.js is inlined from its own file rather +# than pasted into an R string so it stays lintable and editable as JavaScript; the +# report is self-contained, so it cannot be linked. +if (!is.null(bnd_res) && !is.null(bnd_res$data)) { + cat(bnd_circos_script(bnd_res), "\n") + cat("\n") +} +``` + +```{r sv-prepare, results='asis'} +# Column positions are published from R rather than scraped from the rendered header: +# see the panel-filter script in per_sample.qmd. +if (!is.null(sv_table) && nrow(sv_table) > 0) { + sv_display = copy(sv_table) + # Populated client-side by the panel filter, so switching panels updates it on the + # existing redraw. The load-time panel's hits are already in sv_panel_hit (same + # function, same windows) and drive the "Panel SVs" card. + sv_display[, panel_hit := ""] + # Readable location/size/gene columns. The bedpe-shaped raw columns stay in the frame + # — hidden below, but still read by every by-name lookup in the JS layer and still + # exported — so this is presentation only: see sv_display_columns() in + # R/parse_severus.R for why svclass, not the contig, decides what a row means. + sv_display = cbind(sv_display, sv_display_columns(sv_display, chromosomes)) + setcolorder(sv_display, c("id", "panel_hit", "svclass", "svtype", + "locus", "size", "genes", + "vaf", "consequence", "impact", "caller")) + cat("\n", + sep = "") } ``` ```{r sv-table} if (!is.null(sv_table) && nrow(sv_table) > 0) { - # Column casing differs by annotation source: "svtype" (VEP path) vs "SVTYPE" (TSV path) - svtype_col = intersect(c("svtype", "SVTYPE"), names(sv_table)) + idx0 = function(nm) which(names(sv_display) == nm) - 1L + + # A breakend annotated as truncating a whole span can carry a hundred-odd symbols. + # Truncate the *display* only — sorting, searching and the CSV/copy buttons below ask + # for other renderings, all of which return the full list. + gene_render = JS( + "function(data, type) {", + " if (type !== 'display' || !data) return data;", + " const g = String(data).split(',');", + " if (g.length <= 6) return data;", + " return '' +", + " g.slice(0, 6).join(',') + ' +' + (g.length - 6) + ' more';", + "}") + + # Same predicate as the row filter, so the column always agrees with what is shown. + panel_hit_render = JS( + "function(data, type, row) {", + " return window.svPanelHitLabel ? window.svPanelHitLabel(row, window.SV_COLS) : '';", + "}") + + # `locus` and `size` are formatted strings, so they sort lexically — "chr2:…" after + # "chr13:…", "613 kb" after "1.2 Mb". Sort them on the hidden numeric column built + # alongside each. Only the sort types are redirected: display, filter and export all + # fall through to the readable string, which is what lets a per-column search for + # "chr8" match a translocation on *either* side — something the chrom_a box could not. + orthogonal_render = function(key) JS( + "function(data, type, row) {", + " if (type !== 'sort' && type !== 'type') return data;", + " const cols = window.SV_COLS || {};", + sprintf(" const k = cols['%s'];", key), + " return k === undefined ? data : row[k];", + "}") + + # Kept in the frame, out of the view: every by-name lookup in the JS layer reads these + # (svPanelHits() in per_sample.qmd, rowId() below), and the export selector below keeps + # them in the CSV. DataTables hands render and search callbacks the whole row array, + # invisible columns included, so hiding them costs nothing downstream. + hidden = c("chrom_a", "pos_a", "chrom_b", "pos_b", "gene_a", "gene_b", + "sv_len", "size_bp", "locus_sort") + + exp_opts = list(orthogonal = "export", + columns = seq_len(ncol(sv_display)) - 1L) dtbl = DT::datatable( - sv_table, + sv_display, rownames = FALSE, filter = "top", elementId = "sv-table", + # A static DT widget carries no selection config, and the binding then defaults + # data.selection.mode to "none" and does no row selection at all + # (DT/htmlwidgets/datatables.js:230). Saying so explicitly keeps that true if this + # table ever gains a server/Shiny mode, so the breakend circos's own selection below + # stays the only one and cannot end up fighting DT's `.selected` styling. + selection = "none", extensions = c("Buttons", "Scroller"), options = list( dom = "Bfrtip", - buttons = c("copy", "csv"), + # "export" rather than "sort": the sort rendering of `locus` is now a zero-padded + # key, and of `size` a bare number. Neither renderer recognises "export", so both + # fall through to the full readable string — untruncated, since only "display" + # truncates. The explicit column list keeps the hidden raw coordinate columns in + # the download, which Buttons' ":visible" default would drop. + buttons = list(list(extend = "copy", exportOptions = exp_opts), + list(extend = "csv", exportOptions = exp_opts)), scrollX = TRUE, scrollY = "350px", scroller = TRUE, deferRender = TRUE, pageLength = 25, + columnDefs = list( + list(targets = idx0("panel_hit"), render = panel_hit_render), + list(targets = idx0("genes"), render = gene_render), + list(targets = idx0("locus"), render = orthogonal_render("locus_sort")), + list(targets = idx0("size"), render = orthogonal_render("size_bp")), + list(targets = vapply(hidden, idx0, integer(1), USE.NAMES = FALSE), + visible = FALSE) + ), initComplete = JS("function() { window.svTableElem = this.api().table().node(); }") ) - ) - if (length(svtype_col) > 0) { - dtbl = dtbl |> - DT::formatStyle( - columns = svtype_col[1], - target = "cell", - backgroundColor = DT::styleEqual( - c("DEL", "DUP", "INV", "INS", "BND"), - c("#dbeafe", "#dcfce7", "#fef9c3", "#fee2e2", "#f3e8ff") - ) + ) |> + DT::formatStyle( + columns = "svtype", + target = "cell", + backgroundColor = DT::styleEqual( + c("DEL", "DUP", "INV", "INS", "BND", "sBND"), + c("#dbeafe", "#dcfce7", "#fef9c3", "#fee2e2", "#f3e8ff", "#f3e8ff") ) - } + ) dtbl } ``` + +```{r sv-bnd-circos-js, results='asis'} +# Cross-link the table to the circos. This owns the row selection outright — see the +# `selection` note above. Row identity comes from the DataTables API rather than from a +# DOM id, because Scroller plus deferRender recycle the nodes, which is also why the +# selected class is re-applied on every draw. +# +# Every filter change redraws the plot from window.BND_DATA rather than restyling what is +# already there, so the sector layout follows the filtered link set. The old viewBox +# repair is gone with the inlined markup it existed for: Quarto's HTML pipeline lowercases +# attribute names on markup it processes, but nodes built by createElementNS() never pass +# through it. +if (!is.null(bnd_res) && !is.null(bnd_res$data)) { + cat("\n") +} +``` + +```{r sv-footnote} +# Provenance footnote, following the vaf_provenance() pattern in _smallvariants.qmd: +# say where the breakend symbols came from, and — because it is the difference between +# a filter that works on every sample and one that works on some — which panel matching +# mode is active, against which reference, with which windows. +if (!is.null(sv_table) && nrow(sv_table) > 0) { + rel = function(p) { + if (is.null(p)) return(NULL) + root = sub("/+$", "", normalizePath(sample_dir, mustWork = FALSE)) + full = normalizePath(p, mustWork = FALSE) + if (startsWith(full, paste0(root, "/"))) substring(full, nchar(root) + 2L) else p + } + + # Built as one HTML string rather than a tagList: htmltools puts each child on its + # own line, which HTML collapses to a space — leaving " ," after every . + code = function(x) paste0("", htmltools::htmlEscape(x), "") + + panel_note = if (is.null(panel)) { + "No gene panel is applied on load; select one above to filter both tables." + } else if (isTRUE(panel$has_coords)) { + sprintf(paste("Panel %s matches on breakend coordinates (%s intervals,", + "within %s of either breakend of a BND or %s of the span of any other", + "type)%s Each %s entry names the gene, the side it matched on (%s, %s,", + "or %s for a contiguous type), and then either %s — the breakend or span", + "overlaps the gene itself — or the distance to it. Those windows are wide,", + "so a hit is not on its own a disrupted gene: read the second token."), + code(basename(panel$path)), length(panel$start), + fmt_bp(SV_PANEL_WINDOW_BND), fmt_bp(SV_PANEL_WINDOW_OTHER), + if (nzchar(panel$reference)) paste0(", built on ", panel$reference, ".") + else paste(", whose reference is unverified — the panel declares none, so", + "nothing checks it against this report's."), + code("panel_hit"), code("A"), code("B"), code("span"), code("direct")) + } else { + sprintf(paste("Panel %s carries no coordinates, so SVs are matched on gene", + "symbol only: a breakend VEP did not annotate cannot match,", + "whatever it is near. Every %s entry therefore reads %s — a symbol sits", + "on the breakend itself, and there is no distance to report. Add %s, %s", + "and %s columns to match on position instead."), + code(basename(panel$path)), code("panel_hit"), code("direct"), + code("chrom"), code("start"), code("end")) + } + + annot_note = if (!is.null(SECTION_DATA$sv$annotation_path)) + sprintf(paste("Breakend genes are from %s, joined on record ID; records that", + "annotation dropped (it keeps only FILTER PASS) have none."), + code(rel(SECTION_DATA$sv$annotation_path))) + else + "No SV annotation file was found, so the gene column is empty." + + # The point of the locus/size convention, said where the columns are: a contiguous type + # is one span, a breakend is two joined loci, and conflating them is exactly what the + # old chrom_a/pos_a/chrom_b/pos_b columns invited. + locus_note = sprintf(paste("%s reads as a span (%s) for a contiguous type and as two", + "joined loci (%s or %s) for a breakend, which is also why %s", + "is blank for a breakend: its two loci bound no interval, so", + "there is no length to report. The underlying %s columns are", + "hidden rather than dropped — they are still what the panel", + "filter matches on, and the copy and CSV buttons still export", + "them. %s merges both sides for a contiguous type and labels", + "them %s and %s for a breakend, where which partner carries", + "which gene is the point."), + code("locus"), code("chr13:48,303,151–48,915,700"), + code("→"), code("↔"), code("size"), + paste(code("chrom_a"), code("pos_a"), code("chrom_b"), + code("pos_b"), sep = ", "), + code("genes"), code("A:"), code("B:")) + + # What the circos above is showing, said once, next to the numbers it shares with the + # table. Only mentioned when there is a plot to explain. + circos_note = if (!is.null(bnd_res) && !is.null(bnd_res$data)) + sprintf(paste("The circos above draws one arc per breakend with both loci mapped, and", + "is redrawn whenever the filter changes — it shows only the chromosomes", + "the visible rows touch, out of the %d these %d arc%s span in total.", + "Gene bodies are the panel genes within %s of a breakend — the same test", + "that fills the %s column, so a gene is drawn exactly when a visible row", + "names it, and a panel carrying no coordinates draws none. Bodies are", + "markers at a minimum visible width, not spans to scale."), + length(bnd_res$chroms), bnd_res$n_links, + if (bnd_res$n_links == 1) "" else "s", + fmt_bp(SV_PANEL_WINDOW_BND), code("panel_hit")) + else NULL + + table_details( + summary = "How these rows and this plot are built", + HTML(paste( + sprintf("%s rearrangement%s, one row each — Severus writes both sides of a breakend as two records, and mate pairs are collapsed here%s.", + nrow(sv_table), if (nrow(sv_table) == 1) "" else "s", + if (!is.na(n_trans) && !is.na(n_intra_bnd)) + sprintf(" (%s interchromosomal, %s intra-chromosomal breakend%s)", + n_trans, n_intra_bnd, if (n_intra_bnd == 1) "" else "s") + else ""), + locus_note, annot_note, panel_note, circos_note)) + ) +} +``` diff --git a/assets/lrsomatic_report/templates/sections/_whatshap.qmd b/assets/lrsomatic_report/templates/sections/_whatshap.qmd index db40b7bd..f45b936a 100644 --- a/assets/lrsomatic_report/templates/sections/_whatshap.qmd +++ b/assets/lrsomatic_report/templates/sections/_whatshap.qmd @@ -1,4 +1,5 @@ -## Phasing +::: {.callout-note collapse="true"} +### Phasing ```{r whatshap-info} whatshap = SECTION_DATA[["whatshap"]] @@ -21,12 +22,6 @@ if (!is.null(whatshap) && nrow(whatshap$per_chrom) > 0) { } htmltools::tagList( - htmltools::p( - htmltools::tags$em( - sprintf("Germline phasing statistics, from %s.", - if (!is.na(whatshap$vcf)) whatshap$vcf else "the phased germline VCF") - ) - ), DT::datatable( wt, rownames = FALSE, @@ -42,7 +37,15 @@ if (!is.null(whatshap) && nrow(whatshap$per_chrom) > 0) { pageLength = 25, columnDefs = list(list(className = "dt-left", targets = "_all")) ) + ), + # These statistics are germline, not somatic — worth stating, but not worth a line + # of prose above every table. See table_details() in R/sections.R. + table_details( + summary = "Source", + sprintf("Germline phasing statistics, from %s.", + if (!is.na(whatshap$vcf)) whatshap$vcf else "the phased germline VCF") ) ) } ``` +::: diff --git a/docs/output.md b/docs/output.md index 519ffb0a..4f4b6f30 100644 --- a/docs/output.md +++ b/docs/output.md @@ -538,10 +538,10 @@ This is the final step of the pipeline, run after SNV/SV calling, ASCAT, WAKHAN Sections: -- **Small variants** — the VEP-annotated somatic SNVs/indels, with VAF, depth and phase set taken from the phased somatic VCF that VEP annotated. Unfiltered by default; see `--report_gene_panel` in [usage](usage.md#report-options) for panel filtering. -- **Structural variants** — SEVERUS breakpoints, annotated from the VEP SV VCF (`{sample}_SV_VEP.vcf.gz`). Skipping VEP leaves the SV table unannotated but still drawn on the circos plot. +- **Small variants** — the VEP-annotated somatic SNVs/indels, with VAF, depth and phase set taken from the phased somatic VCF that VEP annotated. A footnote under the table names the file those VAF columns came from and how many rows they joined to; after a consensus run it also flags that the VAF of a multi-caller variant comes from whichever caller won the merge, so it need not match the `callers` column beside it. Unfiltered by default; see `--report_gene_panel` in [usage](usage.md#report-options) for panel filtering. +- **Structural variants** — SEVERUS breakpoints, annotated from the VEP SV VCF (`{sample}_SV_VEP.vcf.gz`), one row per rearrangement. Breakends additionally get their own circos plot, cross-linked to the SV table and redrawn as the table is filtered. Skipping VEP leaves the SV table unannotated but still drawn on the circos plot. - **Copy number** — ASCAT purity/ploidy plus its diagnostic plots, and, when WAKHAN ran, its ranked purity/ploidy solutions with the interactive per-solution genome copy-number/breakpoint plots and the ploidy/purity heatmap. -- **QC** — mosdepth, cramino and samtools statistics; for a matched tumour/normal pair both sides are shown side by side. +- **QC** — mosdepth, cramino and samtools statistics; for a matched tumour/normal pair both sides are shown side by side. Phasing statistics (WhatsHap) are a collapsible block within this section. The report is one self-contained file — plots and tables are embedded, so it can be copied or emailed on its own. diff --git a/docs/usage.md b/docs/usage.md index c6c50a68..3f8446b0 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -214,7 +214,7 @@ If you want to run with a CHM13 reference without using `--genome CHM13` (for ex | Parameter | Description | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `--report_src` | Override the report tool source tree (bin/, R/, templates/, assets/). Not needed for normal runs: a copy of [lrsomatic_report](https://github.com/ljwharbers/lrsomatic_report) ships inside the pipeline. Point it at a local checkout to render with an unreleased version of the tool. Default = `${projectDir}/assets/lrsomatic_report` | -| `--report_gene_panel` | Gene panel selected when the report opens. One of `none` (no filtering), a builtin panel name (e.g. `lymphoid`), or a path to a TSV file with a `gene` column. Default = `null`, i.e. unfiltered | +| `--report_gene_panel` | Gene panel selected when the report opens. One of `none` (no filtering), a builtin panel name (`lymphoid` or `sarcoma`), or a path to a TSV file with a `gene` column. Default = `null`, i.e. unfiltered | Gene panel filtering is a view, not a filter on the data: every builtin panel is embedded in the rendered report and the reader can switch between them (or back to the unfiltered table) @@ -227,6 +227,17 @@ TP53 mypanel Tumour suppressor KRAS mypanel Oncogene ``` +A panel may also carry `chrom`, `start` and `end` columns — all three or none. With +coordinates, structural variants are matched on position (within 1 Mb of a breakend, or +100 kb of the SV span) rather than on the VEP gene symbol, which is what makes breakend +filtering reliable: whether a breakend carries a gene symbol at all depends on the VEP +invocation. A coordinate-carrying panel must declare the reference its coordinates are +valid for, either as a leading `# reference: hg38` comment or as a `reference` column; a +panel declaring a reference other than the one the sample was called against is a hard +error rather than a silently wrong filter. Symbol-only panels need no declaration. The +builtin panels ship one file per reference and are selected by their bare name +(`lymphoid`, `sarcoma`), resolved against the detected reference. + ```bash nextflow run IntGenomicsLab/lrsomatic \ -profile \ diff --git a/modules/local/lrsomaticreport/main.nf b/modules/local/lrsomaticreport/main.nf index 1794e50f..b6759001 100644 --- a/modules/local/lrsomaticreport/main.nf +++ b/modules/local/lrsomaticreport/main.nf @@ -36,7 +36,7 @@ process LRSOMATICREPORT { tuple val(meta), path("*_report.html"), emit: report // No CLI version flag is provided by the tool; keep in sync with the vendored // release recorded in assets/lrsomatic_report/VENDORED.md - tuple val("${task.process}"), val('lrsomatic_report'), val("1.1.0"), topic: versions, emit: versions_lrsomaticreport + tuple val("${task.process}"), val('lrsomatic_report'), val("1.2.1"), topic: versions, emit: versions_lrsomaticreport when: task.ext.when == null || task.ext.when diff --git a/modules/local/lrsomaticreport/meta.yml b/modules/local/lrsomaticreport/meta.yml index b54f9d93..ba0cc88e 100644 --- a/modules/local/lrsomaticreport/meta.yml +++ b/modules/local/lrsomaticreport/meta.yml @@ -74,7 +74,7 @@ output: - "lrsomatic_report": type: string description: The name of the tool - - "1.1.0": + - "1.2.1": type: string description: | Manually pinned version (the tool has no CLI version flag); matches the @@ -88,7 +88,7 @@ topics: - lrsomatic_report: type: string description: The name of the tool - - "1.1.0": + - "1.2.1": type: string description: Manually pinned version (tool has no CLI version flag) diff --git a/modules/local/lrsomaticreport/tests/main.nf.test.snap b/modules/local/lrsomaticreport/tests/main.nf.test.snap index 4ffa7db9..2f1e0d55 100644 --- a/modules/local/lrsomaticreport/tests/main.nf.test.snap +++ b/modules/local/lrsomaticreport/tests/main.nf.test.snap @@ -16,7 +16,7 @@ [ "LRSOMATICREPORT", "lrsomatic_report", - "1.1.0" + "1.2.1" ] ], "report": [ @@ -33,12 +33,12 @@ [ "LRSOMATICREPORT", "lrsomatic_report", - "1.1.0" + "1.2.1" ] ] } ], - "timestamp": "2026-08-12T11:17:36.821182267", + "timestamp": "2026-08-28T14:51:14.481830202", "meta": { "nf-test": "0.9.4", "nextflow": "26.04.3" @@ -50,11 +50,11 @@ [ "LRSOMATICREPORT", "lrsomatic_report", - "1.1.0" + "1.2.1" ] ] ], - "timestamp": "2026-08-12T11:19:49.036321128", + "timestamp": "2026-08-28T14:51:33.53813418", "meta": { "nf-test": "0.9.4", "nextflow": "26.04.3" diff --git a/tests/clair_only.nf.test.snap b/tests/clair_only.nf.test.snap index b0691182..72006ff4 100644 --- a/tests/clair_only.nf.test.snap +++ b/tests/clair_only.nf.test.snap @@ -47,7 +47,7 @@ "longphase": "2.0.1" }, "LRSOMATICREPORT": { - "lrsomatic_report": "1.1.0" + "lrsomatic_report": "1.2.1" }, "METAEXTRACT": { "samtools": 1.21 diff --git a/tests/consensus.nf.test.snap b/tests/consensus.nf.test.snap index be6cfe72..fa69aaa8 100644 --- a/tests/consensus.nf.test.snap +++ b/tests/consensus.nf.test.snap @@ -80,7 +80,7 @@ "longphase": "2.0.1" }, "LRSOMATICREPORT": { - "lrsomatic_report": "1.1.0" + "lrsomatic_report": "1.2.1" }, "METAEXTRACT": { "samtools": 1.21 diff --git a/tests/deep_only.nf.test.snap b/tests/deep_only.nf.test.snap index 1f09e6b7..371cb0d2 100644 --- a/tests/deep_only.nf.test.snap +++ b/tests/deep_only.nf.test.snap @@ -56,7 +56,7 @@ "longphase": "2.0.1" }, "LRSOMATICREPORT": { - "lrsomatic_report": "1.1.0" + "lrsomatic_report": "1.2.1" }, "METAEXTRACT": { "samtools": 1.21 diff --git a/tests/default.nf.test.snap b/tests/default.nf.test.snap index 75df5711..75f3a908 100644 --- a/tests/default.nf.test.snap +++ b/tests/default.nf.test.snap @@ -47,7 +47,7 @@ "longphase": "2.0.1" }, "LRSOMATICREPORT": { - "lrsomatic_report": "1.1.0" + "lrsomatic_report": "1.2.1" }, "METAEXTRACT": { "samtools": 1.21 diff --git a/tests/union.nf.test.snap b/tests/union.nf.test.snap index 0a0be4fc..9f37967f 100644 --- a/tests/union.nf.test.snap +++ b/tests/union.nf.test.snap @@ -77,7 +77,7 @@ "longphase": "2.0.1" }, "LRSOMATICREPORT": { - "lrsomatic_report": "1.1.0" + "lrsomatic_report": "1.2.1" }, "METAEXTRACT": { "samtools": 1.21 diff --git a/tower.yml b/tower.yml index 787aedfe..316af346 100644 --- a/tower.yml +++ b/tower.yml @@ -1,5 +1,7 @@ reports: multiqc_report.html: display: "MultiQC HTML report" + "**/report/*_report.html": + display: "Per-sample LRSomatic HTML report" samplesheet.csv: display: "Auto-created samplesheet with collated metadata and FASTQ paths" From 1942b509eb80b08231bf328848e0c6ab65dbfa21 Mon Sep 17 00:00:00 2001 From: Laurens Lambrechts Date: Tue, 1 Sep 2026 11:19:19 +0200 Subject: [PATCH 09/17] fix(lrsomaticreport): drop task.ext reference from container directive `task` is not in scope inside a `container {}` directive closure -- the process hasn't been scheduled yet when the directive is resolved, so referencing task.ext.singularity_pull_docker_container throws "No such variable: task" and kills the run on this Nextflow build (25.09.0-beta). Nothing in our config ever sets that key for this process, so the guard was a no-op for us regardless; dropping it is a behaviour-neutral fix here, but worth checking against whatever Nextflow version the guard was originally written for. Found smoke-testing LRSOMATICREPORT on pilot15 (PacBio HiFi, tumour-only, CHM13) -- everything upstream of the report module ran cleanly, only this directive failed. --- modules/local/lrsomaticreport/main.nf | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/local/lrsomaticreport/main.nf b/modules/local/lrsomaticreport/main.nf index b6759001..08ed1eb3 100644 --- a/modules/local/lrsomaticreport/main.nf +++ b/modules/local/lrsomaticreport/main.nf @@ -11,7 +11,13 @@ process LRSOMATICREPORT { // build produces a genuine OCI image (the plain tag). Rebuild both whenever // environment.yml changes: // wave --conda-file modules/local/lrsomaticreport/environment.yml --freeze --await [--singularity] - container "${workflow.containerEngine == 'singularity' && !task.ext.singularity_pull_docker_container + // NOTE (local patch, not upstream): dropped the `&& !task.ext.singularity_pull_docker_container` + // guard -- `task` is not in scope inside a `container` directive closure on this cluster's + // Nextflow build (25.09.0-beta), so the original line threw "No such variable: task" and + // killed the whole run at the very last step. Nothing in this project's config ever sets + // ext.singularity_pull_docker_container for this process, so the guard was always a no-op + // for us anyway -- safe to drop rather than work around. + container "${workflow.containerEngine == 'singularity' ? 'oras://community.wave.seqera.io/library/r-base_quarto_r-base64enc_r-data.table_pruned:dc62d809aa6fd497' : 'community.wave.seqera.io/library/r-base_quarto_r-base64enc_r-data.table_pruned:c1049dbaf31bf178'}" From fe4de42e42d1e7a72d1a16ecfaf5863f0aa3e858 Mon Sep 17 00:00:00 2001 From: Laurens Lambrechts Date: Tue, 1 Sep 2026 11:20:04 +0200 Subject: [PATCH 10/17] fix(lrsomatic): flatten ch_ascat_files before it reaches LRSOMATICREPORT ASCAT.out.png already emits a list of PNGs per sample, not one emission per file. Mixing it with the two single-file ASCAT channels and grouping with .groupTuple() collects that list as one element of the outer list rather than flattening it, so LRSOMATICREPORT's path(ascat_files) input receives [file, file, [file, file, ...]] and Nextflow rejects the nested ArrayList: Not a valid path value type: java.util.ArrayList (...) ch_wakhan_files a few lines below hits the identical shape problem -- its own comment notes "solution_dirs contributes a list" -- and already carries this same .flatten() fix. ch_ascat_files looks like the one channel it wasn't also applied to. Only surfaces when ASCAT actually runs; --skip_wakhan sidesteps the equivalent bug on the Wakhan side, which is presumably why this one went unnoticed. Found on the resume immediately after the container directive fix (attempt 2), once the run reached far enough to hit LRSOMATICREPORT with real ASCAT output. --- workflows/lrsomatic.nf | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/workflows/lrsomatic.nf b/workflows/lrsomatic.nf index b5df648e..b01f20d3 100644 --- a/workflows/lrsomatic.nf +++ b/workflows/lrsomatic.nf @@ -956,9 +956,16 @@ workflow LRSOMATIC { // Collect all ASCAT copy-number files (segments_raw, purityploidy, diagnostic PNGs) per sample // for the final report module -- it globs by suffix, so exact grouping doesn't matter. + // NOTE (local patch, not upstream): added .flatten() -- ASCAT.out.png already emits a + // list of PNGs per sample, so groupTuple() alone leaves a nested list ([file, file, + // [file, file, ...]]), which the LRSOMATICREPORT module's `path(ascat_files)` input + // rejects ("Not a valid path value type: java.util.ArrayList"). Same fix already + // applied to ch_wakhan_files just below (its own comment: "solution_dirs contributes + // a list") -- this channel was the one spot that fix wasn't also applied. ch_ascat_files = ASCAT.out.segments_raw .mix(ASCAT.out.purityploidy, ASCAT.out.png) .groupTuple() + .map { meta, files -> [meta, files.flatten()] } // ch_ascat_files: [meta, [file, file, ...]] } From 5dfdb697d0d036fa3e2c80f277a314bd21da150e Mon Sep 17 00:00:00 2001 From: ljwharbers Date: Tue, 1 Sep 2026 13:19:17 +0200 Subject: [PATCH 11/17] Remove explanatory comments from container and ascat fixes Co-Authored-By: Claude Opus 5 --- modules/local/lrsomaticreport/main.nf | 6 ------ workflows/lrsomatic.nf | 6 ------ 2 files changed, 12 deletions(-) diff --git a/modules/local/lrsomaticreport/main.nf b/modules/local/lrsomaticreport/main.nf index 08ed1eb3..d0094065 100644 --- a/modules/local/lrsomaticreport/main.nf +++ b/modules/local/lrsomaticreport/main.nf @@ -11,12 +11,6 @@ process LRSOMATICREPORT { // build produces a genuine OCI image (the plain tag). Rebuild both whenever // environment.yml changes: // wave --conda-file modules/local/lrsomaticreport/environment.yml --freeze --await [--singularity] - // NOTE (local patch, not upstream): dropped the `&& !task.ext.singularity_pull_docker_container` - // guard -- `task` is not in scope inside a `container` directive closure on this cluster's - // Nextflow build (25.09.0-beta), so the original line threw "No such variable: task" and - // killed the whole run at the very last step. Nothing in this project's config ever sets - // ext.singularity_pull_docker_container for this process, so the guard was always a no-op - // for us anyway -- safe to drop rather than work around. container "${workflow.containerEngine == 'singularity' ? 'oras://community.wave.seqera.io/library/r-base_quarto_r-base64enc_r-data.table_pruned:dc62d809aa6fd497' : 'community.wave.seqera.io/library/r-base_quarto_r-base64enc_r-data.table_pruned:c1049dbaf31bf178'}" diff --git a/workflows/lrsomatic.nf b/workflows/lrsomatic.nf index b01f20d3..aa28407c 100644 --- a/workflows/lrsomatic.nf +++ b/workflows/lrsomatic.nf @@ -956,12 +956,6 @@ workflow LRSOMATIC { // Collect all ASCAT copy-number files (segments_raw, purityploidy, diagnostic PNGs) per sample // for the final report module -- it globs by suffix, so exact grouping doesn't matter. - // NOTE (local patch, not upstream): added .flatten() -- ASCAT.out.png already emits a - // list of PNGs per sample, so groupTuple() alone leaves a nested list ([file, file, - // [file, file, ...]]), which the LRSOMATICREPORT module's `path(ascat_files)` input - // rejects ("Not a valid path value type: java.util.ArrayList"). Same fix already - // applied to ch_wakhan_files just below (its own comment: "solution_dirs contributes - // a list") -- this channel was the one spot that fix wasn't also applied. ch_ascat_files = ASCAT.out.segments_raw .mix(ASCAT.out.purityploidy, ASCAT.out.png) .groupTuple() From 6eb07dfb3d30a3aee7a0a2ef1b2b820134b92e8d Mon Sep 17 00:00:00 2001 From: ljwharbers Date: Tue, 1 Sep 2026 22:23:35 +0200 Subject: [PATCH 12/17] feat: apply several report gene panels at once The report tool's --gene-panel became repeatable in lrsomatic_report v1.3.0 (upstream PR #16), so any number of panels can be active in one report and a variant or SV is kept if it hits any of them. Nothing pipeline-side could deliver that while the vendored copy was v1.2.1, where is_no_gene_panel() reads a vector of panels as "no panel", so this both re-syncs the tool and widens --report_gene_panel to a comma-separated list of builtin names and/or TSV paths. A single value behaves exactly as before. Only real files are staged, now into gene_panels/ rather than the task root: there are N of them and their names come from the user, while the root also holds sample_dir/, versions.yml and the output HTML. Two panel files sharing a base name would collide there whatever directories they came from, so that combination is rejected up front. conf/modules.config builds the repeated --gene-panel flags, and does so with string operations only. `file()` is not in scope inside an ext.args closure -- the delegate is the config script binding -- and the failure is a task-time MissingMethodException, not a parse error. The pre-existing `file(params.report_gene_panel).name` had the same defect and was simply never reached: every pipeline-level test left report_gene_panel null, and the module tests hard-code ext.args. tests/default.nf.test now passes a builtin and a panel file, which is what catches it. So "is this entry a panel file?" is a textual test (a path separator, or a .tsv suffix) shared by the staging decision and the flag builder, rather than a filesystem probe on one side and a guess on the other. validateReportGenePanels() then rejects at launch anything where the textual test and the filesystem would disagree -- along with a missing panel file, a name that is not a builtin, `none` mixed with a real panel, and duplicate base names. Previously a typo'd path staged nothing, raised nothing, and killed the run inside the report task after alignment, calling and annotation had all run. The vendored re-sync needed no dependency change: a library()/require() grep over v1.3.0's R/, bin/ and templates/ resolves to packages already pinned in environment.yml, and upstream recipe/meta.yaml only moved its version string. Both Wave container digests therefore stand. Version strings carried by hand were bumped together per VENDORED.md: the module's version topic, meta.yml, the module snapshot and the five pipeline snapshots. Verified under -profile test,singularity on Mindwell: all four module tests pass, including a new three-panel real render that asserts DEFAULT_PANELS names every panel -- had only the last --gene-panel survived, it would name one. The rendered report shows four panel checkboxes with the three requested ones ticked. default, deep_only, consensus and union pass at pipeline level. clair_only fails on md5s this branch cannot affect (sample{1,2} normal samtools stats, severus breakpoints_double.csv and read_qual.txt, and the merged sample4_tumor.bam, whose digest differs between two runs of identical code); its snapshot is otherwise untouched here apart from the version string. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 4 +- assets/lrsomatic_report/R/parse_severus.R | 42 +- assets/lrsomatic_report/R/utils.R | 160 ++++++ assets/lrsomatic_report/README.md | 62 ++- assets/lrsomatic_report/VENDORED.md | 13 +- .../assets/gene_lists/README.md | 11 + .../assets/js/facet_filter.js | 464 ++++++++++++++++++ .../assets/styles/report.scss | 247 +++++++++- assets/lrsomatic_report/bin/render_report.R | 87 ++-- .../lrsomatic_report/templates/per_sample.qmd | 156 ++++-- .../templates/sections/_gene_filter.qmd | 51 +- .../templates/sections/_smallvariants.qmd | 20 + .../templates/sections/_sv.qmd | 110 ++++- conf/modules.config | 33 +- docs/output.md | 5 + docs/usage.md | 38 +- modules/local/lrsomaticreport/main.nf | 14 +- modules/local/lrsomaticreport/meta.yml | 8 +- .../lrsomaticreport/tests/gene_panel.config | 8 +- .../local/lrsomaticreport/tests/main.nf.test | 65 ++- .../lrsomaticreport/tests/main.nf.test.snap | 6 +- .../tests/multi_gene_panel.config | 11 + .../lrsomaticreport/tests/test_panel_b.tsv | 3 + nextflow_schema.json | 2 +- ro-crate-metadata.json | 2 +- .../utils_nfcore_lrsomatic_pipeline/main.nf | 95 ++++ tests/clair_only.nf.test.snap | 2 +- tests/consensus.nf.test.snap | 2 +- tests/deep_only.nf.test.snap | 2 +- tests/default.nf.test | 16 + tests/default.nf.test.snap | 2 +- tests/union.nf.test.snap | 2 +- workflows/lrsomatic.nf | 18 +- 33 files changed, 1582 insertions(+), 179 deletions(-) create mode 100644 assets/lrsomatic_report/assets/js/facet_filter.js create mode 100644 modules/local/lrsomaticreport/tests/multi_gene_panel.config create mode 100644 modules/local/lrsomaticreport/tests/test_panel_b.tsv diff --git a/CHANGELOG.md b/CHANGELOG.md index 272ee334..6fdfcdbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### `Added` - [#176](https://github.com/IntGenomicsLab/lrsomatic/pull/176) - Added `LRSOMATICREPORT` as the final pipeline step: a self-contained per-sample HTML report covering small variants, structural variants, copy number and QC. Skip it with `--skip_report`; choose the gene panel selected on load with `--report_gene_panel` (@ljwharbers). -- [#176](https://github.com/IntGenomicsLab/lrsomatic/pull/176) - Vendored the [lrsomatic_report](https://github.com/ljwharbers/lrsomatic_report) v1.2.1 tool source at `assets/lrsomatic_report`, so `nextflow run IntGenomicsLab/lrsomatic` ships it without a submodule checkout (@ljwharbers). +- [#176](https://github.com/IntGenomicsLab/lrsomatic/pull/176) - Vendored the [lrsomatic_report](https://github.com/ljwharbers/lrsomatic_report) v1.3.0 tool source at `assets/lrsomatic_report`, so `nextflow run IntGenomicsLab/lrsomatic` ships it without a submodule checkout (@ljwharbers). - [#176](https://github.com/IntGenomicsLab/lrsomatic/pull/176) - Added a `solution_dirs` output to the WAKHAN module so its per-solution copy-number plots can be staged downstream (@ljwharbers). ### `Changed` +- [#186](https://github.com/IntGenomicsLab/lrsomatic/pull/186) - Re-synced the vendored [lrsomatic_report](https://github.com/ljwharbers/lrsomatic_report) to v1.3.0, which adds tickbox dropdown filters on the categorical columns of both variant tables and turns the report's gene panel selector into checkboxes (@ljwharbers). +- [#186](https://github.com/IntGenomicsLab/lrsomatic/pull/186) - `--report_gene_panel` now takes a comma-separated list, so several panels can be applied at once: a variant or SV is kept if it hits any of them. Panel values are also validated at launch instead of failing inside the report task (@ljwharbers). - [#184](https://github.com/IntGenomicsLab/lrsomatic/pull/184) - Replaced the CHM13 Severus panel of normals with the merged 1000 Genomes + ASAP panel (@AmberVerhasselt). ### `Fixed` diff --git a/assets/lrsomatic_report/R/parse_severus.R b/assets/lrsomatic_report/R/parse_severus.R index 1e847640..63456922 100644 --- a/assets/lrsomatic_report/R/parse_severus.R +++ b/assets/lrsomatic_report/R/parse_severus.R @@ -390,14 +390,52 @@ sv_display_columns = function(sv_table, chrom_levels = NULL) { # not disruption. The "Panel SVs" summary card counts the non-empty entries; the # client-side filter applies the same test with the same windows and builds the same # labels. -sv_panel_hits = function(sv_table, panel, +# +# Several panels can be active at once, so `panels` is either a single panel object or a +# named list of them; a hit against any of them counts (union). When more than one is +# active each label gains a " [name]" suffix saying which panel matched — with one panel +# the labels are exactly what they always were, since there is nothing to disambiguate. +# svPanelHits() in the panel-js-data chunk of templates/per_sample.qmd mirrors this rule +# and must change with it. +sv_panel_hits = function(sv_table, panels, bnd_window = SV_PANEL_WINDOW_BND, other_window = SV_PANEL_WINDOW_OTHER) { n = if (is.null(sv_table)) 0L else nrow(sv_table) if (n == 0) return(character(0)) + + ps = .as_panel_list(panels) + if (length(ps) == 0) return(rep("", n)) + if (length(ps) == 1) return(.sv_panel_hits_one(sv_table, ps[[1]], "", bnd_window, other_window)) + + per_panel = lapply(names(ps), function(nm) + .sv_panel_hits_one(sv_table, ps[[nm]], paste0(" [", nm, "]"), bnd_window, other_window)) + vapply(seq_len(n), function(i) { + parts = unique(unlist(lapply(per_panel, `[[`, i), use.names = FALSE)) + paste(parts[nzchar(parts)], collapse = ", ") + }, character(1)) +} + +# A panel object is a plain list, and so is a list of panels — tell them apart by the +# fields load_gene_panel() always sets rather than by class. +.as_panel_list = function(panels) { + if (is.null(panels) || length(panels) == 0) return(list()) + if (!is.null(panels$genes) || !is.null(panels$has_coords)) { + nm = if (!is.null(panels$name)) as.character(panels$name)[1] else "panel" + return(setNames(list(panels), nm)) + } + ps = panels[!vapply(panels, is.null, logical(1))] + if (is.null(names(ps))) names(ps) = paste0("panel", seq_along(ps)) + ps +} + +# One panel's per-row labels. `tag` is appended to every label ("" for a lone panel). +.sv_panel_hits_one = function(sv_table, panel, tag = "", + bnd_window = SV_PANEL_WINDOW_BND, + other_window = SV_PANEL_WINDOW_OTHER) { + n = nrow(sv_table) if (is.null(panel)) return(rep("", n)) - label = function(genes, side, how) paste0(genes, " (", side, ", ", how, ")") + label = function(genes, side, how) paste0(genes, " (", side, ", ", how, ")", tag) if (!isTRUE(panel$has_coords)) { symbols = toupper(panel$genes) diff --git a/assets/lrsomatic_report/R/utils.R b/assets/lrsomatic_report/R/utils.R index 404c7e54..df6cfb7b 100644 --- a/assets/lrsomatic_report/R/utils.R +++ b/assets/lrsomatic_report/R/utils.R @@ -257,6 +257,74 @@ load_all_gene_panels = function(assets_dir, reference = NULL) { panels } +# Give a user-supplied panel a key that doesn't collide with an already-registered +# one. The first collision keeps the plain "-custom" suffix the single-panel code +# used; further collisions number from 2, so several TSVs sharing a basename all +# stay selectable instead of overwriting each other. +unique_panel_name = function(nm, taken) { + if (!(nm %in% taken)) return(nm) + cand = paste0(nm, "-custom") + i = 1L + while (cand %in% taken) { + i = i + 1L + cand = paste0(nm, "-custom", i) + } + cand +} + +# Resolve the panel keys selected on load into real panel objects, keyed by the same +# names params$all_panels uses (which is also what the checkboxes carry). +# +# Deliberately re-read from disk rather than reusing params$all_panels directly: a +# panel object that has round-tripped through Quarto's YAML execute_params comes back +# with list-typed vectors, and panel_intervals() and the `%in%` symbol tests want real +# atomic ones. A custom TSV's key is not a builtin name, so its location is recovered +# from the registered object's own $path. +# +# Not wrapped in tryCatch, for the same reason resolve_gene_panel() isn't: a panel that +# can't be resolved has to fail the render rather than silently match nothing. +resolve_selected_panels = function(keys, all_panels, assets_dir, reference = NULL) { + keys = setdiff(as.character(unlist(keys)), "__all__") + keys = keys[!is.na(keys) & nzchar(keys)] + if (length(keys) == 0) return(list()) + out = list() + for (k in unique(keys)) { + p = if (!is.null(all_panels)) all_panels[[k]] else NULL + path = if (!is.null(p) && !is.null(p$path)) as.character(p$path)[1] else NA_character_ + out[[k]] = if (!is.na(path) && file.exists(path)) load_gene_panel(path, reference) + else resolve_gene_panel(k, assets_dir, reference) + } + out[!vapply(out, is.null, logical(1))] +} + +# Pull every occurrence of a repeatable flag out of an argv vector. +# +# optparse has no action="append": given `--gene-panel a --gene-panel b` it silently +# keeps only "b". So the flag is stripped from argv here and parse_args() is handed the +# remainder; its make_option() entry stays in option_list purely so --help documents it. +# Both `--flag value` and `--flag=value` are accepted. +extract_repeated_option = function(args, flag) { + args = as.character(args) + vals = character(0) + rest = character(0) + i = 1L + while (i <= length(args)) { + a = args[i] + if (identical(a, flag)) { + if (i == length(args)) stop(flag, " requires a value") + vals = c(vals, args[i + 1L]) + i = i + 2L + } else if (startsWith(a, paste0(flag, "="))) { + vals = c(vals, substring(a, nchar(flag) + 2L)) + i = i + 1L + } else { + rest = c(rest, a) + i = i + 1L + } + } + list(values = vals, rest = rest) +} + # ---- Small JS serialisation helpers -------------------------------------- # The report ships panel data and column positions to its own client-side filter. # Keeping these here means the R table and the JS that indexes it are generated @@ -302,6 +370,98 @@ js_rows = function(dt, cols) { paste0("[[", paste(rows, collapse = "],["), "]]") } +# VEP's impact severity order. The tickbox dropdowns present impact in this order rather +# than by count, because severity is the only order a reader expects — and it is the order +# the styleEqual() palettes in _smallvariants.qmd and _sv.qmd already use. +IMPACT_LEVELS = c("HIGH", "MODERATE", "LOW", "MODIFIER") + +# A column with fewer than this many distinct values is not worth a dropdown, and one with +# more would inline a large payload into a self-contained report: either way it keeps its +# plain text filter. 0 and 1 are real cases, not defects — `callers` is "" for every row on +# the VEP text path, and the SV table has a single caller today. +FACET_MIN_VALUES = 2L +FACET_MAX_VALUES = 200L + +# Distinct values, with row counts, for the checkbox-dropdown ("tickbox") column filters — +# see assets/js/facet_filter.js. Enumerated here rather than by a client-side scan because +# the small-variant table runs 27k–167k rows. +# +# cols : facet column names as they appear in the *display* frame (post-setnames), which +# is what the client resolves through window.SNV_COLS / window.SV_COLS. +# seps : named vector of per-column separators. A column named here is split into tokens, +# one not named is matched whole. The separator has to mirror how the cell was +# built — `consequence` is VEP's "&"-joined terms rewritten to commas and `callers` +# is paste(sort(unique(caller)), collapse = ",") — so that ticking one term matches +# a two-term cell. It travels in the payload rather than being hard-coded in the JS. +# levels : named list of fixed value orders (impact). Values not listed fall in after them, +# by descending row count then alphabetically, so the output is deterministic. +# +# Returns a JS object literal keyed by column name: +# {"impact":{"sep":null,"values":[["HIGH",1203],["MODERATE",8140],[null,17]]}, +# "consequence":{"sep":",","values":[["intron_variant",90210], ...]}} +# `sep: null` means match the whole cell. A `null` value is the "no value" bucket (NA, or +# empty after trimming) and always sorts last; its "(none)" label is applied client-side, so +# a literal cell value of "(none)" cannot collide with it. Counts are *rows* per distinct +# token — a split column's counts therefore sum to more than nrow() — and they are over the +# whole table, never recomputed per filter. +# +# A column that is absent, or outside [FACET_MIN_VALUES, FACET_MAX_VALUES] distinct values, +# is omitted with a message() and keeps its text box. The message is the point: a renamed +# facet column is otherwise invisible, because the text box left behind looks intentional. +js_facet_defs = function(dt, cols, seps = character(0), levels = list()) { + if (is.null(dt) || nrow(dt) == 0 || length(cols) == 0) return("{}") + entries = character(0) + + for (nm in cols) { + if (!nm %in% names(dt)) { + message("Facet column '", nm, "' is not in the table - no value filter for it.") + next + } + sp = if (nm %in% names(seps)) seps[[nm]] else NA_character_ + v = as.character(dt[[nm]]) + + if (!is.na(sp) && nzchar(sp)) { + lst = strsplit(v, sp, fixed = TRUE) + lens = lengths(lst) + # A cell that splits into nothing at all still contributes its row to the NA bucket. + lst[lens == 0L] = NA_character_ + d = data.table(row = rep.int(seq_along(v), pmax(lens, 1L)), + tok = trimws(unlist(lst, use.names = FALSE))) + d = unique(d, by = c("row", "tok")) # "a,a" counts its row once + } else { + d = data.table(row = seq_along(v), tok = trimws(v)) + } + d[is.na(tok) | !nzchar(tok), tok := NA_character_] + + cnt = d[, .N, by = tok] + n_val = nrow(cnt[!is.na(tok)]) + if (n_val < FACET_MIN_VALUES || n_val > FACET_MAX_VALUES) { + message("Facet column '", nm, "': ", n_val, + " distinct value(s) - keeping the plain text filter.") + next + } + + # Fixed levels first (only those actually present), then by descending count, then + # alphabetically so ties are stable. The NA bucket is an escape hatch rather than a + # value competing for attention, so it sorts last whatever its count. + lv = intersect(as.character(levels[[nm]]), cnt$tok[!is.na(cnt$tok)]) + rank = ifelse(is.na(cnt$tok), length(lv) + 2L, + ifelse(cnt$tok %in% lv, match(cnt$tok, lv), length(lv) + 1L)) + cnt = cnt[order(rank, -N, tok)] + + vals = sprintf("[%s,%s]", + ifelse(is.na(cnt$tok), "null", js_quote(cnt$tok)), + js_num(cnt$N)) + entries = c(entries, + sprintf('%s:{"sep":%s,"values":[%s]}', + js_quote(nm), + if (is.na(sp) || !nzchar(sp)) "null" else js_quote(sp), + paste(vals, collapse = ","))) + } + + paste0("{", paste(entries, collapse = ","), "}") +} + # Format a number for human-readable display fmt_bp = function(x) { x = as.numeric(x) diff --git a/assets/lrsomatic_report/README.md b/assets/lrsomatic_report/README.md index a6d981a6..b2c7a22b 100644 --- a/assets/lrsomatic_report/README.md +++ b/assets/lrsomatic_report/README.md @@ -27,7 +27,8 @@ Matched and tumour-only runs take the same command: the run mode and every input are discovered from the sample directory. Tables render unfiltered. Add `--gene-panel lymphoid` (or a path to your own TSV) to have a -panel selected when the report opens — see [Gene panels](#gene-panels). +panel applied when the report opens, and pass the option more than once to apply several at +the same time — see [Gene panels](#gene-panels). ## All options @@ -37,7 +38,8 @@ panel selected when the report opens — see [Gene panels](#gene-panels). --reference t2t | hg38 | auto (default: auto) --sex male | female | XY | XX (required) --gene-panel none | builtin panel name (e.g. lymphoid) | path to a custom TSV - (default: none — tables render unfiltered) + (default: none — tables render unfiltered). Repeatable: pass it + several times to apply several panels at once (union). --output Output HTML path (default: _report.html in current dir) --title Report title ``` @@ -65,11 +67,44 @@ panel selected when the report opens — see [Gene panels](#gene-panels). > `lymphoid.tsv` is replaced by `lymphoid.hg38.tsv` and `lymphoid.t2t.tsv`; a custom > symbol-only TSV still works and still matches on symbols. +> **Changed in v1.3.0:** +> - The categorical columns of both variant tables now filter by **tickbox dropdown** +> instead of a free-text box: `consequence`, `impact` and `callers` on the small-variant +> table, `svclass`, `svtype`, `impact`, `consequence` and `caller` on the SV table. Each +> dropdown lists the values actually present in that sample with a row count, so it also +> answers "what is even in this column?". Every other column keeps its text box, and the +> table's own search box still does substring across all columns. +> - Ticking several values in one column is **OR**; ticking values in two columns is +> **AND**. A ticked term matches a cell holding several — ticking `missense_variant` also +> shows a row whose consequence is `missense_variant,splice_region_variant`. Counts are +> over all rows and do not change as you filter, and `(none)` selects the rows with no +> value in that column. +> - A column with fewer than two distinct values keeps its plain text box rather than +> offering an empty dropdown. That is expected for `callers` when VEP produced its default +> text output (which carries no per-variant caller) and for the SV `caller` column, which +> has one value today. +> - Ticks survive changing the gene panel, are reflected in the "N shown" count, and are +> honoured by the copy/CSV buttons. **Clear value filters** in the panel bar resets them — +> it appears only while something is ticked, since a filter set on a header that has been +> scrolled past is otherwise easy to lose track of. +> - Fixed: the "N small variants · N SVs shown" line went stale when a per-column filter was +> used, having only followed the gene-panel selector. +> - `--gene-panel` is now **repeatable**, and the report's panel selector is a row of +> checkboxes rather than a dropdown: tick any number and a row is kept if it hits **any** +> of them. No box ticked is the unfiltered state, so the "All genes" entry is gone. With +> two or more ticked, each `panel_hit` entry ends with the panel it matched in square +> brackets — with one, the labels read exactly as before. `--gene-panel none` combined +> with a real panel is now an error rather than a case where one quietly wins. +> - The SV table's footnote about how the panel matches now follows the ticked boxes; it +> previously described the load-time panel and went stale the moment a reader switched. + ## Gene panels -Reports are **unfiltered by default**. `--gene-panel` only chooses which panel is selected when +Reports are **unfiltered by default**. `--gene-panel` only chooses which panels are ticked when the report opens; the rendered HTML always contains every variant and every builtin panel, so a -reader can switch panels (or paste a custom gene list) in the browser without re-rendering. +reader can tick and untick panels (or paste a custom gene list) in the browser without +re-rendering. In the report the panels are checkboxes: tick any number and a variant or SV is +kept if it hits **any** of them, and with none ticked the tables are unfiltered. Built-in panels live in `assets/gene_lists/`. Each is a TSV with a `gene` column (HGNC symbols) and, optionally, `chrom`/`start`/`end` — which changes how structural variants are matched: @@ -96,10 +131,18 @@ single entry, resolved against the detected one. ```bash --gene-panel lymphoid # open with the builtin lymphoid panel applied --gene-panel /path/to/my_genes.tsv # must have a 'gene' column or be a single-column file + +# Repeatable — open with both applied, a row kept if it hits either: +--gene-panel lymphoid --gene-panel /path/to/my_genes.tsv ``` +With more than one panel ticked, each `panel_hit` entry ends with the panel it matched in +square brackets (`MYC (A, direct) [lymphoid]`); with one, there is no suffix. Two TSVs sharing +a basename both stay selectable — the second is registered as `-custom`. + A `--gene-panel` value that is neither `none`, a builtin name, nor an existing file is an error — -a typo will not silently produce an unfiltered report. See +a typo will not silently produce an unfiltered report. `none` combined with a real panel is an +error too, rather than a case where one of the two quietly wins. See [`assets/gene_lists/README.md`](assets/gene_lists/README.md) for the full file format. ## Expected input layout @@ -206,12 +249,15 @@ lrsomatic_report/ │ ├── parse_ascat.R ASCAT segments + purity/ploidy parsers │ ├── parse_qc.R Mosdepth, cramino, flagstat parsers │ ├── circos.R draw_circos() — the genome-wide circos SVG -│ └── circos_bnd.R draw_bnd_circos() — the breakend circos, inline and row-linked +│ └── circos_bnd.R Breakend circos: selects and serialises it for the browser ├── templates/per_sample.qmd Quarto template (HTML report) ├── assets/ │ ├── references/{t2t,hg38}/ Cytobands + chrom lengths (bundled, no network needed) -│ └── gene_lists/ lymphoid.{hg38,t2t}.tsv + README -└── tests/ Unit tests (testthat) +│ ├── gene_lists/ lymphoid.{hg38,t2t}.tsv + README +│ ├── styles/ report.scss (the report theme) +│ └── js/ bnd_circos.js (breakend circos), facet_filter.js (tickbox +│ column filters) — inlined at render time +└── tests/ testthat unit tests + tests/js (node, no dependencies) ``` ## Roadmap diff --git a/assets/lrsomatic_report/VENDORED.md b/assets/lrsomatic_report/VENDORED.md index 53846779..67ce9cdc 100644 --- a/assets/lrsomatic_report/VENDORED.md +++ b/assets/lrsomatic_report/VENDORED.md @@ -6,8 +6,8 @@ Do not edit it here — fix upstream, tag a release, and re-sync. | | | |---|---| | Upstream | | -| Release | `v1.2.1` (`1c28f9cde24e903a5a607897e275108e3dff6677`) | -| Vendored commit | `1c28f9cde24e903a5a607897e275108e3dff6677` (the tag itself) | +| Release | `v1.3.0` (`236ab35d5d7c018df1c6b477ecc867c60efd2ff9`) | +| Vendored commit | `236ab35d5d7c018df1c6b477ecc867c60efd2ff9` (the tag itself) | | License | MIT (see `LICENSE`) | ## Why vendored rather than a submodule @@ -29,14 +29,15 @@ Only what `bin/render_report.R` needs at run time: bin/ R/ templates/ assets/ LICENSE README.md ``` -Upstream `docs/`, `tests/`, `recipe/` and `CLAUDE.md` are deliberately excluded. (Upstream -marks `docs/` and `tests/` `export-ignore` in its `.gitattributes`; `recipe/` and -`CLAUDE.md` are not marked, so they must be left behind by hand when copying.) +Upstream `docs/`, `tests/` (both `tests/testthat/` and `tests/js/`), `recipe/` and +`CLAUDE.md` are deliberately excluded. (Upstream marks `docs/` and `tests/` +`export-ignore` in its `.gitattributes`; `recipe/` and `CLAUDE.md` are not marked, so +they must be left behind by hand when copying.) ## Re-syncing on the next upstream release ```bash -TAG=v1.3.0 +TAG=v1.4.0 git clone --depth 1 --branch "$TAG" https://github.com/ljwharbers/lrsomatic_report.git "$TMPDIR/lrr" rm -rf assets/lrsomatic_report/{bin,R,templates,assets,LICENSE,README.md} cp -a "$TMPDIR"/lrr/{bin,R,templates,assets,LICENSE,README.md} assets/lrsomatic_report/ diff --git a/assets/lrsomatic_report/assets/gene_lists/README.md b/assets/lrsomatic_report/assets/gene_lists/README.md index 437982b5..bd4419b5 100644 --- a/assets/lrsomatic_report/assets/gene_lists/README.md +++ b/assets/lrsomatic_report/assets/gene_lists/README.md @@ -52,6 +52,17 @@ A one-column file of symbols (with or without a `gene` header) is accepted, and symbol-only matching. The report's "Custom…" textarea takes bare symbols, so it is symbol-only too. +`--gene-panel` is repeatable, so several panels can be applied at once — a builtin and your +own list together, say. A variant or SV is kept if it hits any of them: + +```bash + --gene-panel lymphoid --gene-panel /path/to/my_genes.tsv +``` + +Each panel is registered under its filename stem; two files sharing a basename both stay +selectable, the second as `-custom`. Every registered panel is a checkbox in the +report, so the reader can retick them without re-rendering. + ## Bundled panels | File | Contents | diff --git a/assets/lrsomatic_report/assets/js/facet_filter.js b/assets/lrsomatic_report/assets/js/facet_filter.js new file mode 100644 index 00000000..1009d92d --- /dev/null +++ b/assets/lrsomatic_report/assets/js/facet_filter.js @@ -0,0 +1,464 @@ +// Checkbox-dropdown ("tickbox") column filters for the small-variant and SV tables. +// +// DT gives every column a free-text search box (filter = "top"), which only helps a reader +// who already knows what the column contains — nobody remembers the ~30 VEP consequence +// terms. For the categorical columns this replaces that box with a dropdown listing the +// values actually present in this sample, with per-value row counts, so the filter doubles +// as a summary of the column. +// +// R publishes the values (window.SNV_FACETS / window.SV_FACETS, from js_facet_defs() in +// R/utils.R) and the column-name -> index maps (window.SNV_COLS / window.SV_COLS) that +// every by-name lookup here goes through. Four things about the DOM this works in, each +// learned from DT/DataTables internals rather than assumed: +// +// 1. DT's own `div.form-group` in the filter cell is KEPT and the inside it only +// hidden. DT binds the handler that un-clips `overflow:hidden` on +// .dataTables_scrollHead to that div, so triggering its "show"/"hide" events is what +// lets an open menu escape the scroll head under scrollX. The widget is appended +// inside that div so DT's own $td.children('div').first()/.last() lookups still hold. +// 2. No `id` attributes anywhere in this markup, and no handlers delegated from +// `document` onto widget classes. DataTables rebuilds a zero-height sizing clone of +// the whole header in the scroll body on every draw, refilling each cell by innerHTML +// after stripping ids — an id here would reappear duplicated once per draw. Hence +// aria-expanded rather than aria-controls, handlers bound to the nodes we create, and +// every query rooted at api.table().header(), never at `document`. +// 3. The R-side column index is NOT the DOM position: DataTables detaches the filter +// cells of visible:false columns. columns(':visible').indexes() translates it, and the +// header label above the chosen cell is compared to the column name — a reorder that +// broke the translation would otherwise attach a working dropdown to the wrong column. +// 4. Ticked state lives here, in a closure, not in the DOM and not in DT's per-column +// search slots. That is what makes it survive a gene-panel redraw, Scroller's node +// recycling and the per-draw header clone. +(function () { + "use strict"; + + // consequence is ~30 terms; impact is 4. Below this a find box is just clutter. + var SEARCH_THRESHOLD = 12; + // One draw over 167k rows runs two ext.search predicates per row, so coalesce rapid ticks. + var DRAW_DEBOUNCE_MS = 120; + var LABEL_MAX = 16; + + // Which tables get facets, keyed on the globals they publish from initComplete — the same + // identity test the gene-panel predicate in per_sample.qmd uses, which excludes the QC, + // ASCAT and phasing DT tables without any extra condition. + var TABLES = [ + { key: "snv", + elem: function () { return window.snvTableElem; }, + cols: function () { return window.SNV_COLS; }, + facets: function () { return window.SNV_FACETS; }, + requested: function () { return window.SNV_FACET_COLS; } }, + { key: "sv", + elem: function () { return window.svTableElem; }, + cols: function () { return window.SV_COLS; }, + facets: function () { return window.SV_FACETS; }, + requested: function () { return window.SV_FACET_COLS; } } + ]; + + // key -> { colName -> Set of ticked tokens }. `null` in a Set is the "(none)" bucket. + // A column absent from a table's object is unconstrained. + var STATE = { snv: {}, sv: {} }; + var ATTACHED = {}; + var WIDGETS = []; // every built widget, for the "clear all" control + var PENDING = {}; // per-table draw debounce timers + var OPEN = null; // the one open menu + + function tableFor(node) { + for (var i = 0; i < TABLES.length; i++) if (TABLES[i].elem() === node) return TABLES[i]; + return null; + } + + function esc(s) { + return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + } + + // ---- matching ------------------------------------------------------------ + // Mirrors how the cell was built, which is why the separator comes from R rather than + // being guessed here: `consequence` is VEP's "&"-joined terms rewritten to commas + // (R/parse_smallvariants.R) and `callers` is paste(sort(unique(caller)), collapse = ","). + // NA and "" both collapse to the single null bucket the dropdown labels "(none)". + function tokens(v, sep) { + if (v === null || v === undefined) return [null]; + var s = String(v).trim(); + if (s === "") return [null]; + if (!sep) return [s]; + var out = s.split(sep).map(function (x) { return x.trim(); }) + .filter(function (x) { return x.length > 0; }); + return out.length ? out : [null]; + } + + // One predicate for both tables, keyed on the table node exactly as the gene-panel + // predicate is. OR within a column, AND across columns. Both predicates are ext.search + // entries, so DataTables ANDs them: a tick composes with the panel filter and with every + // remaining per-column text box for free. + // + // Chosen over column().search() with a regex because the token rule is the load-bearing + // part: as a regex it would be re-encoded in a second language with an escaping + // obligation (svclass values carry spaces, callers carries "clairs-to"), and an + // unescaped metacharacter yields a filter that matches MORE rows and says nothing. + $.fn.dataTable.ext.search.push(function (settings, searchData, index, rowData) { + var t = tableFor(settings.nTable); + if (!t) return true; // another DT table in the report + var sel = STATE[t.key], names = Object.keys(sel); + if (names.length === 0) return true; + var cols = t.cols() || {}, defs = t.facets() || {}; + // rowData is the original (typed) row, searchData the rendered strings. Always the + // former: the SV table renders `locus`/`size` orthogonally, and mixing the two sources + // per column is the trap variant_key() documents on the R side. + var row = rowData || searchData; + for (var i = 0; i < names.length; i++) { + var chosen = sel[names[i]]; + if (!chosen || chosen.size === 0) continue; + var idx = cols[names[i]]; + // Defensive only — widgets are built solely for columns that resolved. A vanished + // column must not empty the table; the loud failure is the attach-time warning. + if (idx === undefined) continue; + var def = defs[names[i]]; + var toks = tokens(row[idx], def ? def.sep : null); + var hit = false; + for (var k = 0; k < toks.length; k++) { + if (chosen.has(toks[k])) { hit = true; break; } + } + if (!hit) return false; // AND across columns + } + return true; + }); + + function scheduleDraw(w) { + clearTimeout(PENDING[w.key]); + PENDING[w.key] = setTimeout(function () { + // resetPaging default: a filter change should return to the top, which is also what + // Scroller does with the scroll position. + w.api.draw(); + }, DRAW_DEBOUNCE_MS); + } + + // ---- the open menu ------------------------------------------------------- + function closeOpen() { + if (!OPEN) return; + var w = OPEN; + w.$menu.prop("hidden", true).removeClass("facet__menu--right"); + w.$btn.attr("aria-expanded", "false"); + w.$wrap.trigger("hide"); // DT re-clips the scroll head + w.$wrapper.removeClass("facet-open"); // and the report's own wrapper overflow + OPEN = null; + } + + function openMenu(w) { + if (OPEN === w) { closeOpen(); return; } + closeOpen(); + w.$wrap.trigger("show"); + w.$wrapper.addClass("facet-open"); + w.$menu.prop("hidden", false); + w.$btn.attr("aria-expanded", "true"); + OPEN = w; + // Flip to the right edge when the menu would run off the viewport. Cheap, and the + // faceted columns sit far enough right in the SV table for it to matter. + var box = w.$menu[0].getBoundingClientRect(); + if (box.right > window.innerWidth - 8) w.$menu.addClass("facet__menu--right"); + if (w.$find.length) w.$find.trigger("focus"); + } + + // ---- widget ------------------------------------------------------------- + function markup(name, def) { + var withFind = def.values.length > SEARCH_THRESHOLD; + var opts = def.values.map(function (v) { + var tok = v[0], n = v[1]; + var isNone = (tok === null); + var label = isNone ? "(none)" : tok; + return ''; + }).join(""); + + return '
' + + '' + + '' + + '
'; + } + + function tokenOf($cb) { + return $cb.data("none") ? null : String($cb.attr("data-tok")); + } + + // A column with nothing ticked is deliberately absent from STATE rather than holding an + // empty Set, so the predicate's `names.length === 0` fast path stays genuinely free for + // the untouched table — it runs per row, 167k times per draw. + var NO_SELECTION = new Set(); + + function readSel(w) { + return STATE[w.key][w.name] || NO_SELECTION; + } + + function ensureSel(w) { + var sel = STATE[w.key][w.name]; + if (!sel) { sel = new Set(); STATE[w.key][w.name] = sel; } + return sel; + } + + function pruneSel(w) { + var sel = STATE[w.key][w.name]; + if (sel && sel.size === 0) delete STATE[w.key][w.name]; + } + + function trunc(s) { + return s.length > LABEL_MAX ? s.slice(0, LABEL_MAX - 1) + "…" : s; + } + + function refreshLabel(w) { + var sel = readSel(w), n = sel.size, text, title = ""; + if (n === 0) { + text = "All"; + } else if (n === 1) { + var only = sel.values().next().value; + var lab = (only === null) ? "(none)" : only; + text = trunc(lab); + title = lab; + } else { + text = n + " selected"; + title = Array.from(sel).map(function (v) { return v === null ? "(none)" : v; }) + .join(", "); + } + w.$label.text(text); + w.$btn.attr("title", title || ("Filter " + w.name + " by value")); + w.$container.toggleClass("facet--active", n > 0); + } + + function anyActive() { + return TABLES.some(function (t) { + return Object.keys(STATE[t.key]).some(function (c) { + return STATE[t.key][c] && STATE[t.key][c].size > 0; + }); + }); + } + + // The report's own control bar owns the reset, because with scrollY = "400px" a ticked + // filter can be scrolled out of sight and "the table is empty and I don't know why" is + // the predictable question. Absent in an older template, hence the guard. + function syncClearButton() { + var el = document.getElementById("facet-clear"); + if (el) el.hidden = !anyActive(); + } + + function clearAll() { + WIDGETS.forEach(function (w) { + delete STATE[w.key][w.name]; + w.$menu.find('input[type="checkbox"]').prop("checked", false); + refreshLabel(w); + }); + syncClearButton(); + TABLES.forEach(function (t) { + var node = t.elem(); + if (node && $.fn.dataTable.isDataTable(node)) $(node).DataTable().draw(); + }); + } + + function build(t, api, $td, name, def) { + var $wrap = $td.children("div").first(); // DT's div.form-group.has-feedback + var $input = $wrap.children("input"); + + // Hidden, not removed: DT holds a reference to this input, and the scroll-head + // un-clipping handler is bound to $wrap itself. We never write to the input, so this + // column's DataTables search stays "" and cannot collide with the facet filter. + $input.hide().attr("tabindex", "-1").attr("aria-hidden", "true"); + $wrap.children("span.glyphicon").hide(); + + var $container = $(markup(name, def)); + $wrap.append($container); // inside $wrap, see note 1 at the top + + // Both of these clip: report.scss sets overflow:hidden on .dataTables_wrapper AND on + // the htmlwidget's own .datatables div (for the card's border radius), and DT's + // show/hide handler only un-clips the scroll head inside them. + var $cont = $(api.table().container()); + + var w = { + key: t.key, name: name, api: api, + $wrap: $wrap, $wrapper: $cont.add($cont.closest(".datatables")), + $container: $container, + $btn: $container.find(".facet__btn"), + $label: $container.find(".facet__label"), + $menu: $container.find(".facet__menu"), + $find: $container.find(".facet__find"), + $list: $container.find(".facet__list") + }; + WIDGETS.push(w); + + w.$btn.on("click", function (e) { + e.stopPropagation(); + openMenu(w); + }); + + // Delegated within our own node — never from `document`, which the per-draw header + // clone would also match. + w.$menu.on("change", 'input[type="checkbox"]', function () { + var sel = ensureSel(w), tok = tokenOf($(this)); + if (this.checked) sel.add(tok); else sel.delete(tok); + pruneSel(w); + refreshLabel(w); + syncClearButton(); + scheduleDraw(w); + }); + + w.$menu.find(".facet__all, .facet__none").on("click", function () { + var on = $(this).hasClass("facet__all"); + var sel = ensureSel(w); + // Only what is currently shown, which is what the button titles say — once the find + // box has narrowed the list, acting on the hidden values too would be a surprise. + w.$list.children(".facet__opt").not(".facet__opt--hidden") + .find('input[type="checkbox"]').each(function () { + this.checked = on; + var tok = tokenOf($(this)); + if (on) sel.add(tok); else sel.delete(tok); + }); + pruneSel(w); + refreshLabel(w); + syncClearButton(); + scheduleDraw(w); + }); + + if (w.$find.length) { + w.$find.on("input", function () { + var q = this.value.trim().toLowerCase(); + w.$list.children(".facet__opt").each(function () { + var txt = $(this).find(".facet__v").text().toLowerCase(); + $(this).toggleClass("facet__opt--hidden", q.length > 0 && txt.indexOf(q) < 0); + }); + }); + } + + w.$container.on("keydown", function (e) { + if (e.key === "Escape" || e.keyCode === 27) { + closeOpen(); + w.$btn.trigger("focus"); + } + }); + + // A click anywhere inside the menu must not reach the outside-click handler below. + w.$menu.on("click", function (e) { e.stopPropagation(); }); + + refreshLabel(w); + return w; + } + + // ---- attach ------------------------------------------------------------- + function attach(t) { + if (ATTACHED[t.key]) return; + var node = t.elem(); + if (!node || !$.fn.dataTable.isDataTable(node)) return; + var defs = t.facets(), cols = t.cols(); + if (!defs || !cols) return; + + var api = $(node).DataTable(); + var $head = $(api.table().header()); // the live thead, wherever DT moved it + var $rows = $head.children("tr"); + if ($rows.length < 2) return; // no filter row: nothing to replace + ATTACHED[t.key] = true; + + var $label = $rows.first().children("th,td"); + var $filter = $rows.last().children("td"); + var visIdx = api.columns(":visible").indexes().toArray(); + + if ($filter.length !== visIdx.length) { + console.warn("facet_filter: " + t.key + " filter row has " + $filter.length + + " cells for " + visIdx.length + + " visible columns — leaving the text filters alone."); + return; + } + + // R drops a column it cannot offer a dropdown for — fewer than two distinct values, or + // gone from the frame entirely — and knitr swallows its message(), so this is where that + // decision becomes visible. console.info rather than warn: for `callers` on the VEP text + // path and the SV `caller` column it is the expected outcome, and warnings here are + // reserved for something actually being wrong. + var requested = t.requested() || []; + var missing = requested.filter(function (n) { return !(n in defs); }); + if (missing.length) { + console.info("facet_filter: no value list for " + t.key + " column(s) " + + missing.join(", ") + " (fewer than " + + "2 distinct values, or not in the table) — they keep their text filter."); + } + + Object.keys(defs).forEach(function (name) { + var idx = cols[name]; + if (idx === undefined) { + console.warn("facet_filter: '" + name + "' is not in " + t.key.toUpperCase() + + "_COLS — was the column renamed?"); + return; + } + var pos = visIdx.indexOf(idx); + if (pos < 0) { + console.warn("facet_filter: '" + name + "' is a hidden column."); + return; + } + // Decisive cross-check on the translation: the label above the cell we picked must be + // the column we think it is. Attaching an svtype dropdown to the locus column would + // otherwise look like a filter that works and filters the wrong thing. + var got = $.trim($label.eq(pos).text()); + if (got !== name) { + console.warn("facet_filter: header mismatch for '" + name + "' at visible column " + + pos + " (found '" + got + "') — skipping."); + return; + } + var $td = $filter.eq(pos); + if ($td.attr("data-type") !== "character") { + // A factor/logical column gets DT's own selectize control; two widgets in one cell + // would fight over the same input. + console.warn("facet_filter: '" + name + "' has data-type '" + + $td.attr("data-type") + "' — leaving DT's own filter in place."); + return; + } + build(t, api, $td, name, defs[name]); + }); + + syncClearButton(); + } + + // DT wires its own filter-row handlers after $().DataTable() returns, i.e. after init.dt + // has fired, and one of those is the handler that un-clips the scroll head for an open + // menu. Defer a tick so it exists before we touch the cell. The load sweep is the + // idempotent fallback for a table that initialised before this script was parsed. + $(document).on("init.dt", function (e, settings) { + var t = tableFor(settings.nTable); + if (t) setTimeout(function () { attach(t); }, 0); + }); + + $(window).on("load", function () { + TABLES.forEach(function (t) { if (t.elem()) attach(t); }); + var clear = document.getElementById("facet-clear"); + if (clear) clear.addEventListener("click", clearAll); + syncClearButton(); + }); + + // The one outside-click handler. Bound to document on purpose — it tests containment + // against the open menu, not against a widget class, so the header's sizing clone is + // irrelevant to it. + $(document).on("mousedown", function (e) { + if (OPEN && !OPEN.$container[0].contains(e.target)) closeOpen(); + }); + + // Which values are ticked, for inspection from the browser console and for + // tests/js/test-facet-predicate.js — the ticked state is otherwise unreachable without a + // DOM. Same reason window.svPanelState is exposed in per_sample.qmd: one shared object + // beats re-deriving the state from the rendered header. + window.facetFilterState = STATE; +})(); diff --git a/assets/lrsomatic_report/assets/styles/report.scss b/assets/lrsomatic_report/assets/styles/report.scss index ac3fd96c..e7326a4e 100644 --- a/assets/lrsomatic_report/assets/styles/report.scss +++ b/assets/lrsomatic_report/assets/styles/report.scss @@ -517,21 +517,72 @@ blockquote { white-space: nowrap; } -.panel-controls__select { - padding: 6px 12px; - border-radius: 6px; +.panel-controls__toggles { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +// One chip per panel. `.is-on` is toggled from JS rather than styled off :checked, so +// the chip and the checkbox cannot disagree after a programmatic Clear. +.panel-toggle { + display: inline-flex; + align-items: center; + gap: 6px; + margin: 0; + padding: 5px 11px; + border-radius: 999px; border: 1px solid var(--color-border); + background: var(--color-bg); + color: var(--color-text); font-family: var(--font-sans); font-size: 0.85rem; font-weight: 500; - background: var(--color-bg); - color: var(--color-text); cursor: pointer; - transition: border-color 0.15s, box-shadow 0.15s; + user-select: none; + transition: border-color 0.15s, background 0.15s, box-shadow 0.15s; - &:focus { - outline: none; + &:hover { border-color: var(--color-primary); } + + &.is-on { + border-color: var(--color-primary); + background: color-mix(in srgb, var(--color-primary) 14%, transparent); + } + + input { + margin: 0; + cursor: pointer; + accent-color: var(--color-primary); + } + + &:focus-within { + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 22%, transparent); + } +} + +// Shared by both buttons in the control bar: #panel-clear, which unticks every gene +// panel, and #facet-clear, which clears the column tickbox filters and stays hidden +// until one is set (see assets/js/facet_filter.js). They are two neutral secondary +// controls sitting side by side, so they take one style — restyling one restyles both. +.panel-controls__clear { + padding: 5px 11px; + border-radius: 6px; + border: 1px solid var(--color-border); + background: transparent; + color: var(--color-text-muted); + font-family: var(--font-sans); + font-size: 0.78rem; + cursor: pointer; + transition: border-color 0.15s, color 0.15s, box-shadow 0.15s; + + &:hover { border-color: var(--color-primary); + color: var(--color-text); + } + + &:focus-visible { + outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 22%, transparent); } } @@ -695,6 +746,184 @@ div.dt-buttons { } } +// ============================================================ +// Column facet filters (tickbox dropdowns) +// ============================================================ +// The widget replaces DT's text input inside DT's own div.form-group — see +// assets/js/facet_filter.js for why that div is kept rather than replaced. Everything here +// is built from the palette custom properties, which the dark-mode block above already +// redefines, so there are no separate dark rules. +.facet { + position: relative; + display: block; + text-transform: none; + letter-spacing: 0; +} + +// Typography matches the `thead input` rule above, so a faceted column reads at the same +// visual weight as the text boxes beside it. +.facet__btn { + display: flex; + align-items: center; + gap: 6px; + width: 100%; + padding: 3px 7px; + border: 1px solid var(--color-border); + border-radius: 5px; + background: var(--color-bg); + color: var(--color-text); + font-family: var(--font-mono); + font-size: 0.72rem; + font-weight: 400; + text-align: left; + cursor: pointer; + + &:focus-visible { + outline: none; + border-color: var(--color-primary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-primary) 22%, transparent); + } +} + +.facet__label { + flex: 1 1 auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.facet__caret { + flex: 0 0 auto; + color: var(--color-text-muted); + font-size: 0.7rem; +} + +// A set filter has to be visible: with 400px of scrolled table the header is often the only +// place it shows. +.facet--active .facet__btn { + border-color: var(--color-primary); + color: var(--color-primary); + font-weight: 500; +} + +.facet__menu { + position: absolute; + top: calc(100% + 4px); + left: 0; + z-index: 30; + min-width: 210px; + max-width: 320px; + padding: 6px; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--card-radius); + box-shadow: var(--card-shadow); + + // Flipped by the script when the menu would run off the right of the viewport. + &--right { left: auto; right: 0; } + + // The base rule below sets display, so [hidden] needs saying explicitly. + &[hidden] { display: none; } +} + +.facet__tools { + display: flex; + align-items: center; + gap: 4px; + padding: 2px 2px 6px; + border-bottom: 1px solid var(--color-border); + + .facet__find { + flex: 1 1 auto; + min-width: 0; + padding: 3px 7px; + border: 1px solid var(--color-border); + border-radius: 5px; + background: var(--color-bg); + color: var(--color-text); + font-family: var(--font-mono); + font-size: 0.7rem; + } + + .facet__all, + .facet__none { + flex: 0 0 auto; + padding: 3px 7px; + border: 1px solid var(--color-border); + border-radius: 5px; + background: var(--color-bg); + color: var(--color-text-muted); + font-family: var(--font-sans); + font-size: 0.68rem; + cursor: pointer; + + &:hover { color: var(--color-primary); border-color: var(--color-primary); } + } +} + +.facet__list { + max-height: 240px; // consequence is ~30 VEP terms + overflow-y: auto; + padding-top: 4px; +} + +.facet__opt { + display: flex; + align-items: center; + gap: 7px; + margin: 0; + padding: 3px 5px; + border-radius: 4px; + font-weight: 400; + cursor: pointer; + + &:hover { background: var(--color-surface-alt); } + + // The `thead input` rule pads every input in the header; a checkbox is not a text box. + input[type="checkbox"] { + flex: 0 0 auto; + margin: 0; + padding: 0 !important; + width: auto !important; + cursor: pointer; + } + + &--hidden { display: none; } + &--none .facet__v { color: var(--color-text-muted); font-style: italic; } +} + +.facet__v { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: var(--font-mono); + font-size: 0.72rem; + color: var(--color-text); +} + +.facet__n { + flex: 0 0 auto; + font-family: var(--font-mono); + font-size: 0.68rem; + color: var(--color-text-muted); +} + +.facet__foot { + margin: 6px 0 2px; + padding: 0 4px; + font-family: var(--font-sans); + font-size: 0.64rem; + color: var(--color-text-muted); +} + +// Both of these carry overflow:hidden above (for the card's border radius), and DT's own +// show/hide handler only un-clips the scroll head inside them — so an open menu near the +// bottom or right edge of a short table would be cut off. Relaxed only while one is open. +.datatables.facet-open, +div.dataTables_wrapper.facet-open { overflow: visible; } + // ============================================================ // Tabset (ASCAT panel-tabset) // ============================================================ @@ -1073,7 +1302,7 @@ main > p:last-child em, @media print { #TOC, .sidebar-navigation, .quarto-sidebar, .dt-buttons, div.dt-buttons, .panel-controls, - #custom-gene-panel, .dataTables_filter, .dataTables_length, + #custom-gene-panel, .facet, .dataTables_filter, .dataTables_length, .dataTables_paginate, .dataTables_info { display: none !important; } body { diff --git a/assets/lrsomatic_report/bin/render_report.R b/assets/lrsomatic_report/bin/render_report.R index 15d83271..b89d39e4 100755 --- a/assets/lrsomatic_report/bin/render_report.R +++ b/assets/lrsomatic_report/bin/render_report.R @@ -29,14 +29,23 @@ option_list = list( make_option("--sex", type = "character", default = NULL, help = "Biological sex: male | female | XY | XX (required)"), make_option("--gene-panel", type = "character", default = "none", - help = "Gene panel applied on load: none | builtin name (lymphoid) | path to TSV (default: none, i.e. unfiltered)"), + help = paste("Gene panel applied on load: none | builtin name (lymphoid) | path to TSV", + "(default: none, i.e. unfiltered). Repeatable — pass it several times to", + "open with several panels applied at once; a variant or SV is kept if it", + "hits any of them.")), make_option("--output", type = "character", default = NULL, help = "Output HTML path (default: _report.html in current dir)"), make_option("--title", type = "character", default = NULL, help = "Report title (default: 'LRSomatic Report – ')") ) -opt = parse_args(OptionParser(option_list = option_list)) +# --gene-panel is repeatable, which optparse cannot express (it has no action="append" +# and would keep only the last value). Strip every occurrence from argv first and hand +# parse_args() the remainder; --help and --version are untouched by the pre-scan. +argv = commandArgs(trailingOnly = TRUE) +gene_panel_args = extract_repeated_option(argv, "--gene-panel") +opt = parse_args(OptionParser(option_list = option_list), args = gene_panel_args$rest) +gene_panels = if (length(gene_panel_args$values) == 0) "none" else gene_panel_args$values # ---- Validate required arguments ---------------------------------------- abort = function(...) { cat("ERROR:", ..., "\n"); quit(status = 1) } @@ -49,7 +58,6 @@ sample_id = if (!is.null(opt[["sample-id"]])) opt[["sample-id"]] else basename sex = tolower(trimws(opt[["sex"]])) sex = switch(sex, xy = "male", xx = "female", sex) # normalise XY/XX -gene_panel = opt[["gene-panel"]] output = if (!is.null(opt[["output"]])) opt[["output"]] else file.path(getwd(), paste0(sample_id, "_report.html")) title = if (!is.null(opt[["title"]])) opt[["title"]] else @@ -83,38 +91,52 @@ reference = tolower(reference) # ---- Load all available gene panels ---------------------------------------- # The rendered report always ships every builtin panel so the reader can switch -# panels client-side; --gene-panel only decides which one is selected on load. -# "__all__" is the sentinel the report's JS uses for "no filter" — it must stay -# in sync with templates/sections/_gene_filter.qmd and the search hook in -# templates/per_sample.qmd. +# panels client-side; --gene-panel only decides which ones are checked on load. +# "__all__" is the sentinel for "nothing selected"; it survives here and in the +# params default of templates/per_sample.qmd, but no longer reaches the browser — +# in the report, no panel checked *is* the unfiltered state. # Builtins that ship per reference ("lymphoid.hg38.tsv") resolve to one entry for # the reference detected above; a coordinate panel declaring a different one is a # hard error rather than a filter matching the wrong genome. -all_panels = load_all_gene_panels(file.path(repo_dir, "assets"), reference) -default_panel = if (is_no_gene_panel(gene_panel)) { - gene_panel = "none" - "__all__" -} else if (!is.null(builtin_panel_path(file.path(repo_dir, "assets"), gene_panel, reference))) { - # Load it here too, so a builtin that fails to resolve against this reference aborts - # now rather than part-way through the Quarto render. - invisible(tryCatch(resolve_gene_panel(gene_panel, file.path(repo_dir, "assets"), reference), - error = function(e) abort(conditionMessage(e)))) - gene_panel -} else if (file.exists(gene_panel)) { - # A user-supplied TSV: register it alongside the builtins so it can be - # selected on load (and switched away from and back to) in the report. - nm = tools::file_path_sans_ext(basename(gene_panel)) - if (nm %in% names(all_panels)) nm = paste0(nm, "-custom") - all_panels[[nm]] = tryCatch(load_gene_panel(gene_panel, reference), - error = function(e) abort(conditionMessage(e))) - # Absolute, because the template resolves it again from Quarto's own working - # directory (the copied template dir), not from where this script was invoked. - gene_panel = normalizePath(gene_panel) - nm -} else { - abort(paste0("--gene-panel not found: tried builtin '", gene_panel, - "' and as a file path. Use 'none' for no filtering.")) +assets_dir = file.path(repo_dir, "assets") +all_panels = load_all_gene_panels(assets_dir, reference) + +# "none" means unfiltered, so combining it with a real panel is contradictory rather +# than a case where one of the two quietly wins. +if (any(vapply(gene_panels, is_no_gene_panel, logical(1))) && length(gene_panels) > 1) { + abort("--gene-panel none cannot be combined with other panels; drop the 'none'.") +} +# Deduplicate by resolved path so the same TSV passed twice registers once rather +# than a second time under a "-custom" key. +gene_panels = unique(vapply(gene_panels, function(g) + if (file.exists(g)) normalizePath(g) else g, character(1))) + +default_panels = character(0) +for (gp in gene_panels) { + if (is_no_gene_panel(gp)) next + if (!is.null(builtin_panel_path(assets_dir, gp, reference))) { + # Load it here too, so a builtin that fails to resolve against this reference aborts + # now rather than part-way through the Quarto render. + invisible(tryCatch(resolve_gene_panel(gp, assets_dir, reference), + error = function(e) abort(conditionMessage(e)))) + default_panels = c(default_panels, gp) + } else if (file.exists(gp)) { + # A user-supplied TSV: register it alongside the builtins so it can be + # selected on load (and switched away from and back to) in the report. + nm = unique_panel_name(tools::file_path_sans_ext(basename(gp)), names(all_panels)) + all_panels[[nm]] = tryCatch(load_gene_panel(gp, reference), + error = function(e) abort(conditionMessage(e))) + default_panels = c(default_panels, nm) + } else { + abort(paste0("--gene-panel not found: tried builtin '", gp, + "' and as a file path. Use 'none' for no filtering.")) + } } +default_panels = unique(default_panels) +# Kept as a sentinel rather than character(0): an empty vector round-trips through +# Quarto's YAML execute_params as NULL, not as an empty character vector. +if (length(default_panels) == 0) default_panels = "__all__" +message("Gene panels selected on load: ", paste(default_panels, collapse = ", ")) # ---- Render the Quarto template ----------------------------------------- # Copy templates/ and assets/ into a writable working directory: repo_dir's @@ -138,8 +160,7 @@ quarto::quarto_render( sample_dir = sample_dir, reference = reference, sex = sex, - gene_panel = gene_panel, - default_panel = default_panel, + default_panels = default_panels, all_panels = all_panels, title = title, repo_dir = repo_dir, diff --git a/assets/lrsomatic_report/templates/per_sample.qmd b/assets/lrsomatic_report/templates/per_sample.qmd index 4f528345..15c684f3 100644 --- a/assets/lrsomatic_report/templates/per_sample.qmd +++ b/assets/lrsomatic_report/templates/per_sample.qmd @@ -15,8 +15,7 @@ params: sample_dir: "" reference: "t2t" sex: "female" - gene_panel: "none" # "none" | builtin panel name | path to a TSV - default_panel: "__all__" # panel selected on load; "__all__" = no filter + default_panels: "__all__" # panel keys checked on load; "__all__" = none checked all_panels: NULL # named list from load_all_gene_panels() title: "LRSomatic Report" repo_dir: "." @@ -66,17 +65,18 @@ chrom_lens = load_chrom_lengths(params$reference, file.path(repo_dir, "assets" chromosomes = chromosomes_for_sex(params$sex) chromosomes = chromosomes[chromosomes %in% unique(cytobands$chrom)] -# Load the gene panel selected on load (NULL when --gene-panel none, i.e. no -# filtering). Only the "Panel variants"/"Panel SVs" summary cards use it — the -# tables themselves are built unfiltered and filtered client-side. +# Load the gene panels checked on load (an empty list when --gene-panel none, i.e. no +# filtering). Several can be active at once and they union. Only the "Panel +# variants"/"Panel SVs" summary cards use them — the tables themselves are built +# unfiltered and filtered client-side. # # Deliberately not wrapped in tryCatch: a panel that can't be resolved, or whose # declared reference is not the one being rendered against, has to fail the render # rather than produce a report whose filter silently matches nothing (or matches the # wrong genome's coordinates). -panel_arg = params$gene_panel -panel = resolve_gene_panel(panel_arg, file.path(repo_dir, "assets"), params$reference) -panel_genes = if (is.null(panel)) NULL else panel$genes +panels = resolve_selected_panels(params$default_panels, params$all_panels, + file.path(repo_dir, "assets"), params$reference) +panel_genes = unique(unlist(lapply(panels, `[[`, "genes"), use.names = FALSE)) # Parse ASCAT ascat_segments = parse_ascat_segments(outputs$ascat_segments) @@ -158,16 +158,17 @@ n_trans = if (!is.null(sv_table) && "svclass" %in% names(sv_table)) sum(sv_table$svclass == "translocation", na.rm = TRUE) else NA_integer_ n_intra_bnd = if (!is.null(sv_table) && "svclass" %in% names(sv_table)) sum(sv_table$svclass == "intra-chr breakend", na.rm = TRUE) else NA_integer_ -# No panel selected on load → the panel cards have nothing to count, so they read -# "N/A" rather than a "0" that looks like "no panel genes hit". -have_panel = !is.null(panel) && length(panel_genes) > 0 +# No panel checked on load → the panel cards have nothing to count, so they read +# "N/A" rather than a "0" that looks like "no panel genes hit". With several panels +# checked the counts are the union, matching what the browser shows. +have_panel = length(panels) > 0 && length(panel_genes) > 0 n_panel_vars = if (!have_panel) NA_integer_ else if (!is.null(variant_table)) sum(variant_table$symbol %in% panel_genes, na.rm = TRUE) else 0L # Same test, same windows as the client-side filter — sv_panel_hits() is the one # implementation both call, so the card and the filter cannot drift. sv_panel_hit = if (!is.null(sv_table) && nrow(sv_table) > 0) - sv_panel_hits(sv_table, panel) else character(0) + sv_panel_hits(sv_table, panels) else character(0) n_panel_svs = if (!have_panel) NA_integer_ else sum(nzchar(sv_panel_hit)) ``` @@ -196,26 +197,45 @@ panel_defs = vapply(names(all_p), function(nm) { cat("\n") --- -*Report generated `r format(Sys.time(), "%Y-%m-%d %H:%M")` · LRSomatic report v1.2.1* +*Report generated `r format(Sys.time(), "%Y-%m-%d %H:%M")` · LRSomatic report v1.3.0* ```{=html} \n") $.fn.dataTable.ext.search.push(function (settings, searchData, index, rowData) { const node = settings.nTable; if (node !== window.snvTableElem && node !== window.svTableElem) return true; - if (!state.panel || state.panel === "__all__") return true; + // No panel ticked is the unfiltered state. + if (!state.panels || !state.panels.length) return true; // rowData carries the original (typed) values; searchData is the rendered // strings, and breakend positions have to be compared as numbers. @@ -373,6 +414,21 @@ cat("\n") if (el) el.textContent = parts.length ? parts.join(" · ") + " shown" : ""; } + // Which panels are ticked, in the order they appear in the chip row. + function readCheckboxes() { + return $(".panel-checkbox:checked").map(function () { return this.value; }).get(); + } + + // Chip appearance and the custom textarea follow the checkboxes, not the other way + // round, so the load-time `checked` attributes and a Clear both land in one place. + function syncControls() { + $(".panel-checkbox").each(function () { + $(this).closest(".panel-toggle").toggleClass("is-on", this.checked); + }); + $("#custom-gene-panel").toggle(state.panels.indexOf("__custom__") >= 0); + if (window.updatePanelNotes) window.updatePanelNotes(); + } + function redraw() { if (window.snvTableElem) $(window.snvTableElem).DataTable().draw(); if (window.svTableElem) $(window.svTableElem).DataTable().draw(); @@ -384,13 +440,33 @@ cat("\n") } $(document).ready(function () { - $("#panel-select").on("change", function () { - state.panel = this.value; - const isCustom = state.panel === "__custom__"; - $("#custom-gene-panel").toggle(isCustom); + // The count follows any draw of either table, not just a panel change. The per-column + // search boxes and the tickbox dropdowns redraw directly — exactly as the breakend + // circos's own draw.dt handler in _sv.qmd already assumes — so before this, typing in a + // column filter left "N small variants shown" stale. It also means facet_filter.js + // needs no coupling to this block: it calls draw(), and the count and the circos follow. + $(document).on("draw.dt", function (e) { + if (e.target === window.snvTableElem || e.target === window.svTableElem) updateCount(); + }); + + $(".panel-checkbox").on("change", function () { + state.panels = readCheckboxes(); + syncControls(); redraw(); }); + $("#panel-clear").on("click", function () { + $(".panel-checkbox").prop("checked", false); + state.panels = []; + syncControls(); + redraw(); + }); + + // The boxes carry their load-time state as `checked` attributes; adopt it rather + // than assuming DEFAULT_PANELS and the rendered chips agree. + state.panels = readCheckboxes(); + syncControls(); + let debounceTimer; $("#custom-gene-input").on("input", function () { clearTimeout(debounceTimer); @@ -413,3 +489,15 @@ cat("\n") })(); ``` + +```{r facet-filter-js, results='asis'} +# Checkbox-dropdown ("tickbox") column filters for the two big tables. Inlined once, at the +# end, from its own .js file rather than pasted into an R string — same reason as +# bnd_circos.js: it stays lintable JavaScript, and the report is self-contained so it +# cannot be linked. Each table's values (window.SNV_FACETS / window.SV_FACETS) and column +# map are emitted by its own prepare chunk above; the widget reads them when a table +# announces itself, so this only has to be parsed before the tables initialise. +cat("\n") +``` diff --git a/assets/lrsomatic_report/templates/sections/_gene_filter.qmd b/assets/lrsomatic_report/templates/sections/_gene_filter.qmd index 0af132cd..bc8a9cef 100644 --- a/assets/lrsomatic_report/templates/sections/_gene_filter.qmd +++ b/assets/lrsomatic_report/templates/sections/_gene_filter.qmd @@ -3,10 +3,14 @@ ```{r gene-filter-note} table_details( summary = "What the panel filter applies to", - "Applies to both the small-variant and structural-variant tables below. Tables are", - "unfiltered unless a panel is selected here. Small variants match on gene symbol; SVs", - "match on breakend position when the panel carries coordinates — see the note under the", - "SV table for which mode is in force.", + "Applies to both the small-variant and structural-variant tables below. Tick any number", + "of panels: a variant or SV is kept if it hits", tags$strong("any"), "of them, and with none", + "ticked the tables are unfiltered. Small variants match on gene symbol; SVs match on", + "breakend position when the panel carries coordinates — see the note under the SV table", + "for which mode each ticked panel is in.", + tags$br(), + "With more than one panel ticked, each panel_hit entry names the panel it matched in", + "square brackets.", tags$br(), "A custom list is bare symbols, which carry no coordinates, so its SVs are matched on", "the annotated breakend genes only — no positional window." @@ -14,28 +18,41 @@ table_details( ``` ```{r gene-filter-ui, results='asis'} -# "__all__" (no filter) is listed first and is the default selection unless the -# render was given a --gene-panel. Every builtin panel stays selectable either way. +# One checkbox per registered panel. There is no "all genes" entry: no box ticked *is* +# the unfiltered state, which is one fewer sentinel to keep in sync than the old +# "__all__" \n') +chips = "" for (nm in names(all_p)) { label = paste0(toupper(substr(nm, 1, 1)), substr(nm, 2, nchar(nm))) - panel_opts = paste0(panel_opts, - '\n') + chips = paste0(chips, chip(nm, label)) } -panel_opts = paste0(panel_opts, - '\n') +chips = paste0(chips, chip("__custom__", "Custom…")) cat(paste0('
- - + Gene panels: +
+', chips, '
+ + +