| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- #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: <input_mrd>,
- ), node(
- (1.8, 0), [Variants Diag: #num(data.vcf_stats.n_constit_init)], corner-radius: 2pt, extrude: (0, 3), name: <input_diag>,
- ), node(
- (1, 1), align(center)[Variant in MRD ?], shape: diamond, name: <is_in_mrd>,
- ), edge(<input_mrd>, "s", <is_in_mrd>, "-|>"), edge(<input_diag>, "s", <is_in_mrd>, "-|>"), edge(<is_in_mrd>, <is_low_mrd>, "-|>", [Yes], label-pos: 0.8), node(
- (0.25, 2), [MRD variant depth \ < 4 ?], shape: diamond, name: <is_low_mrd>,
- ), edge(<is_low_mrd>, <low_mrd>, "-|>"), node(
- (0, 3), [Low MRD depth: #num(data.vcf_stats.n_low_mrd_depth)], shape: parallelogram, name: <low_mrd>,
- ), edge(<is_in_mrd>, <next>, "-|>", [No], label-pos: 0.8), node(
- (1.85, 2), [To BAM filter: #num(data.bam_stats.n_lasting)], shape: chevron, extrude: (-3, 0), name: <next>, stroke: cr_colors.green,
- ), edge(<is_low_mrd>, <homo>, "-|>"), node((1.5, 3), [VAF = 100% ?], shape: diamond, name: <homo>), edge(<homo>, <constit>, "-|>", [Yes], label-pos: 0.5, bend: -80deg), edge(<homo>, <chi>, "-|>", [No], label-pos: 0.6), node(
- (1.5, 4), [$#sym.chi^2$ VAF MRD vs Diag ?], shape: diamond, name: <chi>,
- ), edge(<chi>, <constit>, "-|>", label-pos: 0.8), node(
- (1, 5), [Constit: #num(data.vcf_stats.n_constit)], shape: parallelogram, name: <constit>,
- ), edge(<chi>, <loh>, "-|>", [p < 0.01], label-pos: 0.8), node(
- (2, 5), [LOH: #num(data.vcf_stats.n_loh)], shape: parallelogram, name: <loh>,
- ),
- )
- }
- #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: <input_mrd>,
- ), edge(<input_mrd>, <depth>, "-|>"), node((0.75, 1), [MRD alignement depth ?], shape: diamond, name: <depth>), edge(<depth>, <low_depth>, "-|>", [< 4]), node(
- (0, 2), [Low MRD depth: #num(data.bam_stats.n_low_mrd_depth)], shape: parallelogram, name: <low_depth>,
- ), edge(<depth>, <seen>, "-|>"), node(
- (0.75, 3), [Alt. base seen in MRD pileup ?], shape: diamond, name: <seen>,
- ), edge(<seen>, <constit>, "-|>", [Yes]), node(
- (0, 4), [Constit: #num(data.bam_stats.n_constit)], shape: parallelogram, name: <constit>,
- ), edge(<seen>, <is_div>, "-|>", [No]), node(
- (1.1, 4), [Sequence #sym.plus.minus 20nt \ diversity ?], shape: diamond, name: <is_div>,
- ), edge(<is_div>, <low_div>, "-|>", [entropy < 1.8]), node(
- (0.25, 5), [Low diversity, artefact: #num(data.bam_stats.n_low_diversity)], shape: parallelogram, name: <low_div>,
- ), edge(<is_div>, <somatic>, "-|>"), node(
- (1.75, 5), [Somatic: #num(data.bam_stats.n_somatic)], shape: hexagon, extrude: (-3, 0), name: <somatic>, 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
|