#let cr_colors = ( dark_grey: rgb("#333333"), beige: rgb("#fdf0d5"), light_grey: rgb("#eeeeee"), dark_red: rgb("#780000"), red: rgb("#c1121f"), blue: rgb("#669bbc"), dark_blue: rgb("#003049"), green: rgb("#29bf12"), ) #import "@preview/fletcher:0.5.1" as fletcher: diagram, node, edge #import "@local/svg-emoji:0.1.0": setup-emoji, noto, github #import "@preview/metro:0.3.0": * #import "@preview/cetz:0.2.2" #show: setup-emoji.with(font: noto) #set page(paper: "a4", fill: cr_colors.light_grey, footer: locate(loc => [ #set text(10pt) #if loc.page() != 1 { align(right, counter(page).display("1 / 1", both: true)) } ])) #show heading.where(level: 1): it => [ #set align(center) #set text(fill: cr_colors.dark_blue) #it.body #v(18pt) ] #show image: set text(font: "FreeSans") #set text(size: 16pt, font: "Futura", fill: cr_colors.dark_blue) #let contigs = ( "chr1", "chr2", "chr3", "chr4", "chr5", "chr6", "chr7", "chr8", "chr9", "chr10", "chr11", "chr12", "chr13", "chr14", "chr15", "chr16", "chr17", "chr18", "chr19", "chr20", "chr21", "chr22", "chrX", "chrY", ) #let parseCustomDate(dateString) = { let parts = dateString.split("T") let datePart = parts.at(0).replace("-", "/") let timePart = parts.at(1).split(":") let hour = timePart.at(0) let minute = timePart.at(1) return datePart + " " + hour + "h" + minute } #let formatString(input) = { let words = input.split("_") let capitalizedWords = words.map(word => { if word.len() > 0 { upper(word.first()) + word.slice(1) } else { word } }) capitalizedWords.join(" ") } #let si-fmt(val, precision: 1, sep: "\u{202F}", binary: false) = { let factor = if binary { 1024 } else { 1000 } let gt1_suffixes = ("k", "M", "G", "T", "P", "E", "Z", "Y") let lt1_suffixes = ("m", "μ", "n", "p", "f", "a", "z", "y") let scale = "" let unit = "" if type(val) == content { if val.has("text") { val = val.text } else if val.has("children") { val = val.children.map(content => content.text).join() } else { panic(val.children.map(content => content.text).join()) } } // if val contains a unit, split it off if type(val) == str { unit = val.find(regex("(\D+)$")) val = float(val.split(unit).at(0)) } if calc.abs(val) > 1 { for suffix in gt1_suffixes { if calc.abs(val) < factor { break } val /= factor scale += " " + suffix } } else if val != 0 and calc.abs(val) < 1 { for suffix in lt1_suffixes { if calc.abs(val) > 1 { break } val *= factor scale += " " + suffix } } let formatted = str(calc.round(val, digits: precision)) formatted + sep + scale.split().at(-1, default: "") + unit } #let reportCoverage(prefix) = { image(prefix + "_global.svg", width: 100%) for contig in contigs { heading(level: 4, contig) let path = prefix + "_" + contig image(path + "_chromosome.svg") let data = json(path + "_stats.json") grid( columns: (1fr, 2fr), gutter: 3pt, align( left + horizon, )[ #set text(size: 12pt) #table( stroke: none, columns: (auto, 1fr), gutter: 3pt, [Mean], [#calc.round(data.mean, digits: 2)], [Standard dev.], [#calc.round(data.std_dev, digits: 2)], ..data.breaks_values.map(r => ([#r.at(0)], [#calc.round(r.at(1) * 100, digits: 1)%])).flatten(), ) ], align(right, image(path + "_distrib.svg", width: 100%)), ) parbreak() } } #let reportBam(path) = { let data = json(path) table( gutter: 3pt, stroke: none, columns: (auto, 1fr), ..for (key, value) in data { if key != "cramino" and key != "composition" and key != "path" and key != "modified" { ([ #formatString(key) ], [ #value ]) } else if key == "modified" { ([ Modified Date (UTC) ], [ #parseCustomDate(value) ]) } else if key == "composition" { ([ Run(s) ], [ #for (i, v) in value.enumerate() { if i > 0 [ \ ] [#v.at(0).slice(0, 5): #calc.round(v.at(1), digits: 0)%] } ]) } else if key == "cramino" { for (k, v) in value { if k == "normalized_read_count_per_chromosome" {} else if k != "path" and k != "checksum" and k != "creation_time" and k != "file_name" { let k = formatString(k) let v = if type(v) == "integer" { si-fmt(v) } else { v } ([ #k ], [ #v ]) } else { () } }.flatten() } else { () } }.flatten(), ) } #let formatedReadCount(path) = { let data = json(path) let data = data.cramino.normalized_read_count_per_chromosome let res = () for contig in contigs { res.push(data.at(contig)) } res.push(data.at("chrM")) return res } #let printReadCount(diag_path, mrd_path) = { let index = 14; let c = contigs c.push("chrM") let diag = formatedReadCount(diag_path) let mrd = formatedReadCount(mrd_path) c.insert(0, "") diag.insert(0, "diag") mrd.insert(0, "mrd") let arrays1 = (c.slice(0, index), diag.slice(0, index), mrd.slice(0, index)) table( columns: arrays1.at(0).len(), ..arrays1.map(arr => arr.map(item => [#item])).flatten(), ) let arrays2 = (c.slice(index), diag.slice(index), mrd.slice(index)) arrays2.at(0).insert(0, "") arrays2.at(1).insert(0, "diag") arrays2.at(2).insert(0, "mrd") table( columns: arrays2.at(0).len(), ..arrays2.map(arr => arr.map(item => [#item])).flatten(), ) } #let variantsFlow(path) = { import fletcher.shapes: diamond, parallelogram, chevron let data = json(path) diagram( spacing: (1pt, 60pt), node-fill: gradient.radial(cr_colors.light_grey, cr_colors.blue, radius: 300%), node-stroke: cr_colors.dark_blue + 1pt, edge-stroke: 1pt, node-inset: 14pt, node( (0.2, 0), [Variants MRD: #num(data.vcf_stats.n_tumoral_init)], corner-radius: 2pt, extrude: (0, 3), name: , ), node( (1.8, 0), [Variants Diag: #num(data.vcf_stats.n_constit_init)], corner-radius: 2pt, extrude: (0, 3), name: , ), node( (1, 1), align(center)[Variant in MRD ?], shape: diamond, name: , ), edge(, "s", , "-|>"), edge(, "s", , "-|>"), edge(, , "-|>", [Yes], label-pos: 0.8), node( (0.25, 2), [MRD variant depth \ < 4 ?], shape: diamond, name: , ), edge(, , "-|>"), node( (0, 3), [Low MRD depth: #num(data.vcf_stats.n_low_mrd_depth)], shape: parallelogram, name: , ), edge(, , "-|>", [No], label-pos: 0.8), node( (1.85, 2), [To BAM filter: #num(data.bam_stats.n_lasting)], shape: chevron, extrude: (-3, 0), name: , stroke: cr_colors.green, ), edge(, , "-|>"), node((1.5, 3), [VAF = 100% ?], shape: diamond, name: ), edge(, , "-|>", [Yes], label-pos: 0.5, bend: -80deg), edge(, , "-|>", [No], label-pos: 0.6), node( (1.5, 4), [$#sym.chi^2$ VAF MRD vs Diag ?], shape: diamond, name: , ), edge(, , "-|>", label-pos: 0.8), node( (1, 5), [Constit: #num(data.vcf_stats.n_constit)], shape: parallelogram, name: , ), edge(, , "-|>", [p < 0.01], label-pos: 0.8), node( (2, 5), [LOH: #num(data.vcf_stats.n_loh)], shape: parallelogram, name: , ), ) } #let bamFilter(path) = { import fletcher.shapes: diamond, parallelogram, hexagon let data = json(path) diagram( spacing: (1pt, 60pt), node-fill: gradient.radial(cr_colors.light_grey, cr_colors.blue, radius: 300%), node-inset: 14pt, node-stroke: cr_colors.dark_blue + 1pt, edge-stroke: 1pt, node( (0.75, 0), [Variants not in MRD VCF: #num(data.bam_stats.n_lasting)], corner-radius: 2pt, extrude: (0, 3), name: , ), edge(, , "-|>"), node((0.75, 1), [MRD alignement depth ?], shape: diamond, name: ), edge(, , "-|>", [< 4]), node( (0, 2), [Low MRD depth: #num(data.bam_stats.n_low_mrd_depth)], shape: parallelogram, name: , ), edge(, , "-|>"), node( (0.75, 3), [Alt. base seen in MRD pileup ?], shape: diamond, name: , ), edge(, , "-|>", [Yes]), node( (0, 4), [Constit: #num(data.bam_stats.n_constit)], shape: parallelogram, name: , ), edge(, , "-|>", [No]), node( (1.1, 4), [Sequence #sym.plus.minus 20nt \ diversity ?], shape: diamond, name: , ), edge(, , "-|>", [entropy < 1.8]), node( (0.25, 5), [Low diversity, artefact: #num(data.bam_stats.n_low_diversity)], shape: parallelogram, name: , ), edge(, , "-|>"), node( (1.75, 5), [Somatic: #num(data.bam_stats.n_somatic)], shape: hexagon, extrude: (-3, 0), name: , stroke: cr_colors.green, ), ) } #let barCallers(path) = { import cetz.draw: * import cetz.chart let json_data = json(path).variants_stats let data = json_data.find(item => item.name == "callers_cat") let chart_data = data.counts.pairs().sorted(key: x => -x.at(1)) let max_value = chart_data.first().at(1) cetz.canvas( length: 80%, { set-style(axes: (bottom: (tick: (label: (angle: 45deg, anchor: "north-east"))))) chart.columnchart( chart_data, size: (1, 1), ) }) } #set heading(numbering: (..numbers) => { if numbers.pos().len() >= 2 and numbers.pos().len() <= 3 { numbering("1.1", ..numbers.pos().slice(1)) } }) #heading(level: 1, outlined: false)[Whole Genome Sequencing Report] #outline(title: "Table of Contents", depth: 3) #pagebreak() == Identity Camara == Alignement #grid( columns: (1fr, 1fr), gutter: 3pt, [ ==== Diagnostic sample #set text(size: 11pt) #reportBam( "/Turbine-pool/LongReads/report/BECERRA/report/data/BECERRA_diag_hs1_info.json", ) ], [ ==== MRD sample #set text(size: 11pt) #reportBam( "/Turbine-pool/LongReads/report/BECERRA/report/data/BECERRA_mrd_hs1_info.json", ) #set footnote(numbering: n => { " " }) #footnote[Values computed by #link("https://github.com/wdecoster/cramino")[cramino] v0.14.5 ] ], ) #pagebreak() === Normalized read count by chromosome #[ #set text(size: 10pt) #printReadCount( "/Turbine-pool/LongReads/report/BECERRA/report/data/BECERRA_diag_hs1_info.json", "/Turbine-pool/LongReads/report/BECERRA/report/data/BECERRA_mrd_hs1_info.json", ) ] === Coverage by chromosome ==== Proportion at given depth by chromosome #reportCoverage("/Turbine-pool/LongReads/report/BECERRA/report/data/scan/BECERRA") #set footnote(numbering: n => { " " }) #footnote[Values computed by Pandora development version] #pagebreak() == Variants === Variants calling ==== VCF filters #pad( left: -0.8cm, top: 0.8cm, variantsFlow( "/Turbine-pool/LongReads/report/BECERRA/report/data/CAMARA_variants_stats.json", ), ) ==== BAM filters #pad( top: 0.8cm, bamFilter( "/Turbine-pool/LongReads/report/BECERRA/report/data/CAMARA_variants_stats.json", ), ) #pagebreak() === Somatic variants #barCallers("/Turbine-pool/LongReads/report/BECERRA/report/data/CAMARA_variants_stats.json") === Selected Variants #pagebreak() == Conclusion hello ??? #lorem(150) ❌ #emoji.rocket