template.typ 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. #import sys: inputs
  2. #let cr_colors = (
  3. 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"),
  4. )
  5. #import "@preview/fletcher:0.5.1" as fletcher: diagram, node, edge
  6. // #import "@preview/metro:0.3.0": *
  7. // #import "@preview/cetz:0.2.2"
  8. #set page(paper: "a4", fill: cr_colors.light_grey, footer: locate(loc => [
  9. #set text(10pt)
  10. #if loc.page() != 1 {
  11. align(right, counter(page).display("1 / 1", both: true))
  12. }
  13. ]))
  14. #show heading.where(level: 1): it => [
  15. #set align(center)
  16. #set text(fill: cr_colors.dark_blue)
  17. #it.body
  18. #v(18pt)
  19. ]
  20. // #show image: set text(font: "FreeSans")
  21. #set text(size: 16pt, font: "Futura", fill: cr_colors.dark_blue)
  22. #let contigs = (
  23. "chr1", "chr2", "chr3", "chr4", "chr5", "chr6", "chr7", "chr8", "chr9", "chr10", "chr11", "chr12", "chr13", "chr14", "chr15", "chr16", "chr17", "chr18", "chr19", "chr20", "chr21", "chr22", "chrX", "chrY",
  24. )
  25. #let parseCustomDate(dateString) = {
  26. let parts = dateString.split("T")
  27. let datePart = parts.at(0).replace("-", "/")
  28. let timePart = parts.at(1).split(":")
  29. let hour = timePart.at(0)
  30. let minute = timePart.at(1)
  31. return datePart + " " + hour + "h" + minute
  32. }
  33. #let formatString(input) = {
  34. let words = input.split("_")
  35. let capitalizedWords = words.map(word => {
  36. if word.len() > 0 {
  37. upper(word.first()) + word.slice(1)
  38. } else {
  39. word
  40. }
  41. })
  42. capitalizedWords.join(" ")
  43. }
  44. #let si-fmt(val, precision: 1, sep: "\u{202F}", binary: false) = {
  45. let factor = if binary { 1024 } else { 1000 }
  46. let gt1_suffixes = ("k", "M", "G", "T", "P", "E", "Z", "Y")
  47. let lt1_suffixes = ("m", "μ", "n", "p", "f", "a", "z", "y")
  48. let scale = ""
  49. let unit = ""
  50. if type(val) == content {
  51. if val.has("text") {
  52. val = val.text
  53. } else if val.has("children") {
  54. val = val.children.map(content => content.text).join()
  55. } else {
  56. panic(val.children.map(content => content.text).join())
  57. }
  58. }
  59. // if val contains a unit, split it off
  60. if type(val) == str {
  61. unit = val.find(regex("(\D+)$"))
  62. val = float(val.split(unit).at(0))
  63. }
  64. if calc.abs(val) > 1 {
  65. for suffix in gt1_suffixes {
  66. if calc.abs(val) < factor {
  67. break
  68. }
  69. val /= factor
  70. scale += " " + suffix
  71. }
  72. } else if val != 0 and calc.abs(val) < 1 {
  73. for suffix in lt1_suffixes {
  74. if calc.abs(val) > 1 {
  75. break
  76. }
  77. val *= factor
  78. scale += " " + suffix
  79. }
  80. }
  81. let formatted = str(calc.round(val, digits: precision))
  82. formatted + sep + scale.split().at(-1, default: "") + unit
  83. }
  84. #let reportCoverage(prefix) = {
  85. image(prefix + "_global.svg", width: 100%)
  86. for contig in contigs {
  87. heading(level: 4, contig)
  88. let path = prefix + "_" + contig
  89. image(path + "_chromosome.svg")
  90. let data = json(path + "_stats.json")
  91. grid(
  92. columns: (1fr, 2fr), gutter: 3pt, align(
  93. left + horizon,
  94. )[
  95. #set text(size: 12pt)
  96. #table(
  97. 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(),
  98. )
  99. ], align(right, image(path + "_distrib.svg", width: 100%)),
  100. )
  101. parbreak()
  102. }
  103. }
  104. #let reportBam(path) = {
  105. let data = json(path)
  106. table(
  107. gutter: 3pt, stroke: none, columns: (auto, 1fr), ..for (key, value) in data {
  108. if key != "cramino" and key != "composition" and key != "path" and key != "modified" {
  109. ([ #formatString(key) ], [ #value ])
  110. } else if key == "modified" {
  111. ([ Modified Date (UTC) ], [ #parseCustomDate(value) ])
  112. } else if key == "composition" {
  113. ([ Run(s) ], [
  114. #for (i, v) in value.enumerate() {
  115. if i > 0 [ \ ]
  116. [#v.at(0).slice(0, 5): #calc.round(v.at(1), digits: 0)%]
  117. }
  118. ])
  119. } else if key == "cramino" {
  120. for (k, v) in value {
  121. if k == "normalized_read_count_per_chromosome" {} else if k != "path" and k != "checksum" and k != "creation_time" and k != "file_name" {
  122. let k = formatString(k)
  123. let v = if type(v) == "integer" { si-fmt(v) } else { v }
  124. ([ #k ], [ #v ])
  125. } else {
  126. ()
  127. }
  128. }.flatten()
  129. } else {
  130. ()
  131. }
  132. }.flatten(),
  133. )
  134. }
  135. #let formatedReadCount(path) = {
  136. let data = json(path)
  137. let data = data.cramino.normalized_read_count_per_chromosome
  138. let res = ()
  139. for contig in contigs {
  140. res.push(data.at(contig))
  141. }
  142. res.push(data.at("chrM"))
  143. return res
  144. }
  145. #let printReadCount(diag_path, mrd_path) = {
  146. let index = 14;
  147. let c = contigs
  148. c.push("chrM")
  149. let diag = formatedReadCount(diag_path)
  150. let mrd = formatedReadCount(mrd_path)
  151. c.insert(0, "")
  152. diag.insert(0, "diag")
  153. mrd.insert(0, "mrd")
  154. let arrays1 = (c.slice(0, index), diag.slice(0, index), mrd.slice(0, index))
  155. table(
  156. columns: arrays1.at(0).len(), ..arrays1.map(arr => arr.map(item => [#item])).flatten(),
  157. )
  158. let arrays2 = (c.slice(index), diag.slice(index), mrd.slice(index))
  159. arrays2.at(0).insert(0, "")
  160. arrays2.at(1).insert(0, "diag")
  161. arrays2.at(2).insert(0, "mrd")
  162. table(
  163. columns: arrays2.at(0).len(), ..arrays2.map(arr => arr.map(item => [#item])).flatten(),
  164. )
  165. }
  166. #let variantsFlow(path) = {
  167. import fletcher.shapes: diamond, parallelogram, chevron
  168. let data = json(path)
  169. set text(8pt)
  170. diagram(
  171. spacing: (8pt, 25pt), 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: 8pt, 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 filters: #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. set text(8pt)
  196. diagram(
  197. spacing: (8pt, 25pt), node-fill: gradient.radial(cr_colors.light_grey, cr_colors.blue, radius: 300%), node-inset: 8pt, node-stroke: cr_colors.dark_blue + 1pt, edge-stroke: 1pt, node(
  198. (0.75, 0), [Variants not in MRD VCF: #num(data.bam_stats.n_lasting)], corner-radius: 2pt, extrude: (0, 3), name: <input_mrd>,
  199. ), edge(<input_mrd>, <depth>, "-|>"), node((0.75, 1), [MRD alignement depth ?], shape: diamond, name: <depth>), edge(<depth>, <low_depth>, "-|>", [< 4]), node(
  200. (0, 2), [Low MRD depth: #num(data.bam_stats.n_low_mrd_depth)], shape: parallelogram, name: <low_depth>,
  201. ), edge(<depth>, <seen>, "-|>"), node(
  202. (0.75, 3), [Alt. base seen in MRD pileup ?], shape: diamond, name: <seen>,
  203. ), edge(<seen>, <constit>, "-|>", [Yes]), node(
  204. (0, 4), [Constit: #num(data.bam_stats.n_constit)], shape: parallelogram, name: <constit>,
  205. ), edge(<seen>, <is_div>, "-|>", [No]), node(
  206. (1.1, 4), [Sequence #sym.plus.minus 20nt \ diversity ?], shape: diamond, name: <is_div>,
  207. ), edge(<is_div>, <low_div>, "-|>", [entropy < 1.8]), node(
  208. (0.25, 5), [Low diversity, artefact: #num(data.bam_stats.n_low_diversity)], shape: parallelogram, name: <low_div>,
  209. ), edge(<is_div>, <somatic>, "-|>"), node(
  210. (1.75, 5), [Somatic: #num(data.bam_stats.n_somatic)], shape: hexagon, extrude: (-3, 0), name: <somatic>, stroke: cr_colors.green,
  211. ),
  212. )
  213. }
  214. #let barCallers(path) = {
  215. import cetz.draw: *
  216. import cetz.chart
  217. let json_data = json(path).variants_stats
  218. let data = json_data.find(item => item.name == "callers_cat")
  219. let chart_data = data.counts.pairs().sorted(key: x => -x.at(1))
  220. set text(11pt)
  221. cetz.canvas(
  222. length: 80%,
  223. {
  224. set-style(axes: (bottom: (tick: (label: (angle: 45deg, anchor: "north-east")))))
  225. chart.columnchart(
  226. chart_data,
  227. size: (1, 0.5),
  228. )
  229. })
  230. }
  231. #set heading(numbering: (..numbers) => {
  232. if numbers.pos().len() >= 2 and numbers.pos().len() <= 3 {
  233. numbering("1.1", ..numbers.pos().slice(1))
  234. }
  235. })
  236. #heading(level: 1, outlined: false)[Whole Genome Sequencing Report]
  237. #outline(title: "Table of Contents", depth: 3)
  238. #pagebreak()
  239. == Identity
  240. Camara