report.typ 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. #let cr_colors = (
  2. 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"),
  3. )
  4. #import "@preview/fletcher:0.5.1" as fletcher: diagram, node, edge
  5. #import "@local/svg-emoji:0.1.0": setup-emoji, noto, github
  6. #import "@preview/metro:0.3.0": *
  7. #import "@preview/cetz:0.2.2"
  8. #show: setup-emoji.with(font: noto)
  9. #set page(paper: "a4", fill: cr_colors.light_grey, footer: locate(loc => [
  10. #set text(10pt)
  11. #if loc.page() != 1 {
  12. align(right, counter(page).display("1 / 1", both: true))
  13. }
  14. ]))
  15. #show heading.where(level: 1): it => [
  16. #set align(center)
  17. #set text(fill: cr_colors.dark_blue)
  18. #it.body
  19. #v(18pt)
  20. ]
  21. #show image: set text(font: "FreeSans")
  22. #set text(size: 16pt, font: "Futura", fill: cr_colors.dark_blue)
  23. #let contigs = (
  24. "chr1", "chr2", "chr3", "chr4", "chr5", "chr6", "chr7", "chr8", "chr9", "chr10", "chr11", "chr12", "chr13", "chr14", "chr15", "chr16", "chr17", "chr18", "chr19", "chr20", "chr21", "chr22", "chrX", "chrY",
  25. )
  26. #let parseCustomDate(dateString) = {
  27. let parts = dateString.split("T")
  28. let datePart = parts.at(0).replace("-", "/")
  29. let timePart = parts.at(1).split(":")
  30. let hour = timePart.at(0)
  31. let minute = timePart.at(1)
  32. return datePart + " " + hour + "h" + minute
  33. }
  34. #let formatString(input) = {
  35. let words = input.split("_")
  36. let capitalizedWords = words.map(word => {
  37. if word.len() > 0 {
  38. upper(word.first()) + word.slice(1)
  39. } else {
  40. word
  41. }
  42. })
  43. capitalizedWords.join(" ")
  44. }
  45. #let si-fmt(val, precision: 1, sep: "\u{202F}", binary: false) = {
  46. let factor = if binary { 1024 } else { 1000 }
  47. let gt1_suffixes = ("k", "M", "G", "T", "P", "E", "Z", "Y")
  48. let lt1_suffixes = ("m", "μ", "n", "p", "f", "a", "z", "y")
  49. let scale = ""
  50. let unit = ""
  51. if type(val) == content {
  52. if val.has("text") {
  53. val = val.text
  54. } else if val.has("children") {
  55. val = val.children.map(content => content.text).join()
  56. } else {
  57. panic(val.children.map(content => content.text).join())
  58. }
  59. }
  60. // if val contains a unit, split it off
  61. if type(val) == str {
  62. unit = val.find(regex("(\D+)$"))
  63. val = float(val.split(unit).at(0))
  64. }
  65. if calc.abs(val) > 1 {
  66. for suffix in gt1_suffixes {
  67. if calc.abs(val) < factor {
  68. break
  69. }
  70. val /= factor
  71. scale += " " + suffix
  72. }
  73. } else if val != 0 and calc.abs(val) < 1 {
  74. for suffix in lt1_suffixes {
  75. if calc.abs(val) > 1 {
  76. break
  77. }
  78. val *= factor
  79. scale += " " + suffix
  80. }
  81. }
  82. let formatted = str(calc.round(val, digits: precision))
  83. formatted + sep + scale.split().at(-1, default: "") + unit
  84. }
  85. #let reportCoverage(prefix) = {
  86. image(prefix + "_global.svg", width: 100%)
  87. for contig in contigs {
  88. heading(level: 4, contig)
  89. let path = prefix + "_" + contig
  90. image(path + "_chromosome.svg")
  91. let data = json(path + "_stats.json")
  92. grid(
  93. columns: (1fr, 2fr), gutter: 3pt, align(
  94. left + horizon,
  95. )[
  96. #set text(size: 12pt)
  97. #table(
  98. 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(),
  99. )
  100. ], align(right, image(path + "_distrib.svg", width: 100%)),
  101. )
  102. parbreak()
  103. }
  104. }
  105. #let reportBam(path) = {
  106. let data = json(path)
  107. table(
  108. gutter: 3pt, stroke: none, columns: (auto, 1fr), ..for (key, value) in data {
  109. if key != "cramino" and key != "composition" and key != "path" and key != "modified" {
  110. ([ #formatString(key) ], [ #value ])
  111. } else if key == "modified" {
  112. ([ Modified Date (UTC) ], [ #parseCustomDate(value) ])
  113. } else if key == "composition" {
  114. ([ Run(s) ], [
  115. #for (i, v) in value.enumerate() {
  116. if i > 0 [ \ ]
  117. [#v.at(0).slice(0, 5): #calc.round(v.at(1), digits: 0)%]
  118. }
  119. ])
  120. } else if key == "cramino" {
  121. for (k, v) in value {
  122. if k == "normalized_read_count_per_chromosome" {} else if k != "path" and k != "checksum" and k != "creation_time" and k != "file_name" {
  123. let k = formatString(k)
  124. let v = if type(v) == "integer" { si-fmt(v) } else { v }
  125. ([ #k ], [ #v ])
  126. } else {
  127. ()
  128. }
  129. }.flatten()
  130. } else {
  131. ()
  132. }
  133. }.flatten(),
  134. )
  135. }
  136. #let formatedReadCount(path) = {
  137. let data = json(path)
  138. let data = data.cramino.normalized_read_count_per_chromosome
  139. let res = ()
  140. for contig in contigs {
  141. res.push(data.at(contig))
  142. }
  143. res.push(data.at("chrM"))
  144. return res
  145. }
  146. #let printReadCount(diag_path, mrd_path) = {
  147. let index = 14;
  148. let c = contigs
  149. c.push("chrM")
  150. let diag = formatedReadCount(diag_path)
  151. let mrd = formatedReadCount(mrd_path)
  152. c.insert(0, "")
  153. diag.insert(0, "diag")
  154. mrd.insert(0, "mrd")
  155. let arrays1 = (c.slice(0, index), diag.slice(0, index), mrd.slice(0, index))
  156. table(
  157. columns: arrays1.at(0).len(), ..arrays1.map(arr => arr.map(item => [#item])).flatten(),
  158. )
  159. let arrays2 = (c.slice(index), diag.slice(index), mrd.slice(index))
  160. arrays2.at(0).insert(0, "")
  161. arrays2.at(1).insert(0, "diag")
  162. arrays2.at(2).insert(0, "mrd")
  163. table(
  164. columns: arrays2.at(0).len(), ..arrays2.map(arr => arr.map(item => [#item])).flatten(),
  165. )
  166. }
  167. #let variantsFlow(path) = {
  168. import fletcher.shapes: diamond, parallelogram, chevron
  169. let data = json(path)
  170. diagram(
  171. 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(
  172. (0.2, 0), [Variants MRD: #num(data.vcf_stats.n_tumoral_init)], corner-radius: 2pt, extrude: (0, 3), name: <input_mrd>,
  173. ), node(
  174. (1.8, 0), [Variants Diag: #num(data.vcf_stats.n_constit_init)], corner-radius: 2pt, extrude: (0, 3), name: <input_diag>,
  175. ), node(
  176. (1, 1), align(center)[Variant in MRD ?], shape: diamond, name: <is_in_mrd>,
  177. ), 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(
  178. (0.25, 2), [MRD variant depth \ < 4 ?], shape: diamond, name: <is_low_mrd>,
  179. ), edge(<is_low_mrd>, <low_mrd>, "-|>"), node(
  180. (0, 3), [Low MRD depth: #num(data.vcf_stats.n_low_mrd_depth)], shape: parallelogram, name: <low_mrd>,
  181. ), edge(<is_in_mrd>, <next>, "-|>", [No], label-pos: 0.8), node(
  182. (1.85, 2), [To BAM filter: #num(data.bam_stats.n_lasting)], shape: chevron, extrude: (-3, 0), name: <next>, stroke: cr_colors.green,
  183. ), 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(
  184. (1.5, 4), [$#sym.chi^2$ VAF MRD vs Diag ?], shape: diamond, name: <chi>,
  185. ), edge(<chi>, <constit>, "-|>", label-pos: 0.8), node(
  186. (1, 5), [Constit: #num(data.vcf_stats.n_constit)], shape: parallelogram, name: <constit>,
  187. ), edge(<chi>, <loh>, "-|>", [p < 0.01], label-pos: 0.8), node(
  188. (2, 5), [LOH: #num(data.vcf_stats.n_loh)], shape: parallelogram, name: <loh>,
  189. ),
  190. )
  191. }
  192. #let bamFilter(path) = {
  193. import fletcher.shapes: diamond, parallelogram, hexagon
  194. let data = json(path)
  195. diagram(
  196. 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(
  197. (0.75, 0), [Variants not in MRD VCF: #num(data.bam_stats.n_lasting)], corner-radius: 2pt, extrude: (0, 3), name: <input_mrd>,
  198. ), edge(<input_mrd>, <depth>, "-|>"), node((0.75, 1), [MRD alignement depth ?], shape: diamond, name: <depth>), edge(<depth>, <low_depth>, "-|>", [< 4]), node(
  199. (0, 2), [Low MRD depth: #num(data.bam_stats.n_low_mrd_depth)], shape: parallelogram, name: <low_depth>,
  200. ), edge(<depth>, <seen>, "-|>"), node(
  201. (0.75, 3), [Alt. base seen in MRD pileup ?], shape: diamond, name: <seen>,
  202. ), edge(<seen>, <constit>, "-|>", [Yes]), node(
  203. (0, 4), [Constit: #num(data.bam_stats.n_constit)], shape: parallelogram, name: <constit>,
  204. ), edge(<seen>, <is_div>, "-|>", [No]), node(
  205. (1.1, 4), [Sequence #sym.plus.minus 20nt \ diversity ?], shape: diamond, name: <is_div>,
  206. ), edge(<is_div>, <low_div>, "-|>", [entropy < 1.8]), node(
  207. (0.25, 5), [Low diversity, artefact: #num(data.bam_stats.n_low_diversity)], shape: parallelogram, name: <low_div>,
  208. ), edge(<is_div>, <somatic>, "-|>"), node(
  209. (1.75, 5), [Somatic: #num(data.bam_stats.n_somatic)], shape: hexagon, extrude: (-3, 0), name: <somatic>, stroke: cr_colors.green,
  210. ),
  211. )
  212. }
  213. #let barCallers(path) = {
  214. import cetz.draw: *
  215. import cetz.chart
  216. let json_data = json(path).variants_stats
  217. let data = json_data.find(item => item.name == "callers_cat")
  218. let chart_data = data.counts.pairs().sorted(key: x => -x.at(1))
  219. let max_value = chart_data.first().at(1)
  220. cetz.canvas(
  221. length: 80%,
  222. {
  223. set-style(axes: (bottom: (tick: (label: (angle: 45deg, anchor: "north-east")))))
  224. chart.columnchart(
  225. chart_data,
  226. size: (1, 1),
  227. )
  228. })
  229. }
  230. #set heading(numbering: (..numbers) => {
  231. if numbers.pos().len() >= 2 and numbers.pos().len() <= 3 {
  232. numbering("1.1", ..numbers.pos().slice(1))
  233. }
  234. })
  235. #heading(level: 1, outlined: false)[Whole Genome Sequencing Report]
  236. #outline(title: "Table of Contents", depth: 3)
  237. #pagebreak()
  238. == Identity
  239. Camara
  240. == Alignement
  241. #grid(
  242. columns: (1fr, 1fr), gutter: 3pt, [
  243. ==== Diagnostic sample
  244. #set text(size: 11pt)
  245. #reportBam(
  246. "/Turbine-pool/LongReads/report/BECERRA/report/data/BECERRA_diag_hs1_info.json",
  247. )
  248. ], [
  249. ==== MRD sample
  250. #set text(size: 11pt)
  251. #reportBam(
  252. "/Turbine-pool/LongReads/report/BECERRA/report/data/BECERRA_mrd_hs1_info.json",
  253. )
  254. #set footnote(numbering: n => { " " })
  255. #footnote[Values computed by #link("https://github.com/wdecoster/cramino")[cramino] v0.14.5
  256. ]
  257. ],
  258. )
  259. #pagebreak()
  260. === Normalized read count by chromosome
  261. #[
  262. #set text(size: 10pt)
  263. #printReadCount(
  264. "/Turbine-pool/LongReads/report/BECERRA/report/data/BECERRA_diag_hs1_info.json", "/Turbine-pool/LongReads/report/BECERRA/report/data/BECERRA_mrd_hs1_info.json",
  265. )
  266. ]
  267. === Coverage by chromosome
  268. ==== Proportion at given depth by chromosome
  269. #reportCoverage("/Turbine-pool/LongReads/report/BECERRA/report/data/scan/BECERRA")
  270. #set footnote(numbering: n => { " " })
  271. #footnote[Values computed by Pandora development version]
  272. #pagebreak()
  273. == Variants
  274. === Variants calling
  275. ==== VCF filters
  276. #pad(
  277. left: -0.8cm, top: 0.8cm, variantsFlow(
  278. "/Turbine-pool/LongReads/report/BECERRA/report/data/CAMARA_variants_stats.json",
  279. ),
  280. )
  281. ==== BAM filters
  282. #pad(
  283. top: 0.8cm, bamFilter(
  284. "/Turbine-pool/LongReads/report/BECERRA/report/data/CAMARA_variants_stats.json",
  285. ),
  286. )
  287. #pagebreak()
  288. === Somatic variants
  289. #barCallers("/Turbine-pool/LongReads/report/BECERRA/report/data/CAMARA_variants_stats.json")
  290. === Selected Variants
  291. #pagebreak()
  292. == Conclusion
  293. hello ???
  294. #lorem(150)
  295. #emoji.rocket