template.typ 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897
  1. #let cr_colors = (
  2. dark_grey: rgb("#333333"),
  3. beige: rgb("#fdf0d5"),
  4. light_grey: rgb("#eeeeee"),
  5. dark_red: rgb("#780000"),
  6. red: rgb("#c1121f"),
  7. blue: rgb("#669bbc"),
  8. dark_blue: rgb("#003049"),
  9. green: rgb("#29bf12"),
  10. )
  11. #import "@preview/fletcher:0.5.1" as fletcher: diagram, node, edge
  12. #import "@preview/metro:0.3.0": *
  13. #import "@preview/cetz:0.2.2"
  14. #import "@preview/badgery:0.1.1": *
  15. #import "@preview/cmarker:0.1.1"
  16. #set page(
  17. paper: "a4",
  18. footer: locate(loc => [
  19. #set text(10pt)
  20. #let today = datetime.today()
  21. #if loc.page() != 1 {
  22. align(right, counter(page).display("1 / 1", both: true))
  23. }
  24. #align(center, [Dr. Thomas Steimlé --- #today.display("[day] [month repr:long] [year]")])
  25. ]),
  26. )
  27. #show heading: set text(font: "Futura")
  28. #show heading.where(level: 1): it => [
  29. #set align(center)
  30. #set text(fill: cr_colors.dark_blue)
  31. #it.body
  32. #v(18pt)
  33. ]
  34. #show image: set text(font: "FreeSans")
  35. #set text(size: 16pt, fill: cr_colors.dark_blue)
  36. #let contigs = (
  37. "chr1",
  38. "chr2",
  39. "chr3",
  40. "chr4",
  41. "chr5",
  42. "chr6",
  43. "chr7",
  44. "chr8",
  45. "chr9",
  46. "chr10",
  47. "chr11",
  48. "chr12",
  49. "chr13",
  50. "chr14",
  51. "chr15",
  52. "chr16",
  53. "chr17",
  54. "chr18",
  55. "chr19",
  56. "chr20",
  57. "chr21",
  58. "chr22",
  59. "chrX",
  60. "chrY",
  61. )
  62. #let parseCustomDate(dateString) = {
  63. let parts = dateString.split("T")
  64. let datePart = parts.at(0).replace("-", "/")
  65. let timePart = parts.at(1).split(":")
  66. let hour = timePart.at(0)
  67. let minute = timePart.at(1)
  68. return datePart + " " + hour + "h" + minute
  69. }
  70. #let formatString(input) = {
  71. let words = input.split("_")
  72. let capitalizedWords = words.map(word => {
  73. if word.len() > 0 {
  74. upper(word.first()) + word.slice(1)
  75. } else {
  76. word
  77. }
  78. })
  79. capitalizedWords.join(" ")
  80. }
  81. #let si-fmt(val, precision: 1, sep: "\u{202F}", binary: false) = {
  82. let factor = if binary {
  83. 1024
  84. } else {
  85. 1000
  86. }
  87. let gt1_suffixes = ("k", "M", "G", "T", "P", "E", "Z", "Y")
  88. let lt1_suffixes = ("m", "μ", "n", "p", "f", "a", "z", "y")
  89. let scale = ""
  90. let unit = ""
  91. if type(val) == content {
  92. if val.has("text") {
  93. val = val.text
  94. } else if val.has("children") {
  95. val = val.children.map(content => content.text).join()
  96. } else {
  97. panic(val.children.map(content => content.text).join())
  98. }
  99. }
  100. // if val contains a unit, split it off
  101. if type(val) == str {
  102. unit = val.find(regex("(\D+)$"))
  103. val = float(val.split(unit).at(0))
  104. }
  105. if calc.abs(val) > 1 {
  106. for suffix in gt1_suffixes {
  107. if calc.abs(val) < factor {
  108. break
  109. }
  110. val /= factor
  111. scale += " " + suffix
  112. }
  113. } else if val != 0 and calc.abs(val) < 1 {
  114. for suffix in lt1_suffixes {
  115. if calc.abs(val) > 1 {
  116. break
  117. }
  118. val *= factor
  119. scale += " " + suffix
  120. }
  121. }
  122. let formatted = str(calc.round(val, digits: precision))
  123. formatted + sep + scale.split().at(-1, default: "") + unit
  124. }
  125. #let reportCoverage(prefix) = {
  126. image(prefix + "_global.svg", width: 100%)
  127. for contig in contigs {
  128. heading(level: 4, contig)
  129. let path = prefix + "_" + contig
  130. image(path + "_chromosome.svg")
  131. let data = json(path + "_stats.json")
  132. grid(
  133. columns: (1fr, 2fr),
  134. gutter: 3pt,
  135. align(left + horizon)[
  136. #set text(size: 12pt)
  137. #table(
  138. stroke: none, columns: (auto, 1fr), gutter: 3pt, [Mean], [#calc.round(
  139. data.mean,
  140. digits: 2,
  141. )], [Standard dev.], [#calc.round(
  142. data.std_dev,
  143. digits: 2,
  144. )], ..data.breaks_values.map(r => (
  145. [#r.at(0)],
  146. [#calc.round(r.at(1) * 100, digits: 1)%],
  147. )).flatten(),
  148. )
  149. ],
  150. align(right, image(path + "_distrib.svg", width: 100%)),
  151. )
  152. parbreak()
  153. }
  154. }
  155. #let reportBam(path) = {
  156. let data = json(path)
  157. table(
  158. gutter: 3pt, stroke: none, columns: (auto, 1fr), ..for (key, value) in (
  159. data
  160. ) {
  161. if key != "cramino" and key != "composition" and key != "path" and key != "modified" {
  162. ([ #formatString(key) ], [ #value ])
  163. } else if key == "modified" {
  164. ([ Modified Date (UTC) ], [ #parseCustomDate(value) ])
  165. } else if key == "composition" {
  166. (
  167. [ Run(s) ],
  168. [
  169. #for (i, v) in value.enumerate() {
  170. if i > 0 [ \ ]
  171. [#v.at(0).slice(0, 5): #calc.round(v.at(1), digits: 0)%]
  172. }
  173. ],
  174. )
  175. } else if key == "cramino" {
  176. for (k, v) in value {
  177. if k == "normalized_read_count_per_chromosome" { } else if k != "path" and k != "checksum" and k != "creation_time" and k != "file_name" {
  178. let k = formatString(k)
  179. let v = if type(v) == "integer" {
  180. si-fmt(v)
  181. } else {
  182. v
  183. }
  184. ([ #k ], [ #v ])
  185. } else {
  186. ()
  187. }
  188. }.flatten()
  189. } else {
  190. ()
  191. }
  192. }.flatten(),
  193. )
  194. }
  195. #let formatedReadCount(path) = {
  196. let data = json(path)
  197. let data = data.cramino.normalized_read_count_per_chromosome
  198. let res = ()
  199. for contig in contigs {
  200. res.push(data.at(contig))
  201. }
  202. res.push(data.at("chrM"))
  203. return res
  204. }
  205. #let printReadCount(diag_path, mrd_path) = {
  206. let index = 14
  207. let c = contigs
  208. c.push("chrM")
  209. let diag = formatedReadCount(diag_path)
  210. let mrd = formatedReadCount(mrd_path)
  211. c.insert(0, "")
  212. diag.insert(0, "diag")
  213. mrd.insert(0, "mrd")
  214. let arrays1 = (c.slice(0, index), diag.slice(0, index), mrd.slice(0, index))
  215. table(
  216. columns: arrays1.at(0).len(), ..arrays1
  217. .map(arr => arr.map(item => [#item]))
  218. .flatten(),
  219. )
  220. let arrays2 = (c.slice(index), diag.slice(index), mrd.slice(index))
  221. arrays2.at(0).insert(0, "")
  222. arrays2.at(1).insert(0, "diag")
  223. arrays2.at(2).insert(0, "mrd")
  224. table(
  225. columns: arrays2.at(0).len(), ..arrays2
  226. .map(arr => arr.map(item => [#item]))
  227. .flatten(),
  228. )
  229. }
  230. #let variantsFlow(path) = {
  231. import fletcher.shapes: diamond, parallelogram, chevron
  232. let data = json(path)
  233. set text(8pt)
  234. diagram(
  235. spacing: (8pt, 25pt),
  236. node-fill: gradient.radial(
  237. cr_colors.light_grey,
  238. cr_colors.blue,
  239. radius: 300%,
  240. ),
  241. node-stroke: cr_colors.dark_blue + 1pt,
  242. edge-stroke: 1pt,
  243. mark-scale: 70%,
  244. node-inset: 8pt,
  245. node(
  246. (0.2, 0),
  247. [Variants MRD: #num(data.vcf_stats.n_tumoral_init)],
  248. corner-radius: 2pt,
  249. extrude: (0, 3),
  250. name: <input_mrd>,
  251. ),
  252. node(
  253. (1.8, 0),
  254. [Variants Diag: #num(data.vcf_stats.n_constit_init)],
  255. corner-radius: 2pt,
  256. extrude: (0, 3),
  257. name: <input_diag>,
  258. ),
  259. node(
  260. (1, 1),
  261. align(center)[Variant in MRD ?],
  262. shape: diamond,
  263. name: <is_in_mrd>,
  264. ),
  265. edge(<input_mrd>, "s", <is_in_mrd>, "-|>"),
  266. edge(<input_diag>, "s", <is_in_mrd>, "-|>"),
  267. edge(<is_in_mrd>, <is_low_mrd>, "-|>", [Yes], label-pos: 0.8),
  268. node(
  269. (0.25, 2),
  270. [MRD variant depth \ < 4 ?],
  271. shape: diamond,
  272. name: <is_low_mrd>,
  273. ),
  274. edge(<is_low_mrd>, <low_mrd>, "-|>"),
  275. node(
  276. (0, 3),
  277. [Low MRD depth: #num(data.vcf_stats.n_low_mrd_depth)],
  278. shape: parallelogram,
  279. name: <low_mrd>,
  280. ),
  281. edge(<is_in_mrd>, <next>, "-|>", [No], label-pos: 0.8),
  282. node(
  283. (1.85, 2),
  284. [To BAM filters: #num(data.bam_stats.n_lasting)],
  285. shape: chevron,
  286. extrude: (-3, 0),
  287. name: <next>,
  288. stroke: cr_colors.green,
  289. ),
  290. edge(<is_low_mrd>, <homo>, "-|>"),
  291. node((1.5, 3), [VAF = 100% ?], shape: diamond, name: <homo>),
  292. edge(<homo>, <constit>, "-|>", [Yes], label-pos: 0.5, bend: -80deg),
  293. edge(<homo>, <chi>, "-|>", [No], label-pos: 0.6),
  294. node(
  295. (1.5, 4),
  296. [$#sym.chi^2$ VAF MRD vs Diag ?],
  297. shape: diamond,
  298. name: <chi>,
  299. ),
  300. edge(<chi>, <constit>, "-|>", label-pos: 0.8),
  301. node(
  302. (1, 5),
  303. [Constit: #num(data.vcf_stats.n_constit)],
  304. shape: parallelogram,
  305. name: <constit>,
  306. ),
  307. edge(<chi>, <loh>, "-|>", [p < 0.01], label-pos: 0.8),
  308. node(
  309. (2, 5),
  310. [LOH: #num(data.vcf_stats.n_loh)],
  311. shape: parallelogram,
  312. name: <loh>,
  313. ),
  314. )
  315. }
  316. #let bamFilter(path) = {
  317. import fletcher.shapes: diamond, parallelogram, hexagon
  318. let data = json(path)
  319. set text(8pt)
  320. diagram(
  321. spacing: (8pt, 25pt),
  322. node-fill: gradient.radial(
  323. cr_colors.light_grey,
  324. cr_colors.blue,
  325. radius: 300%,
  326. ),
  327. node-inset: 8pt,
  328. node-stroke: cr_colors.dark_blue + 1pt,
  329. mark-scale: 70%,
  330. edge-stroke: 1pt,
  331. node(
  332. (0.75, 0),
  333. [Variants not in MRD VCF: #num(data.bam_stats.n_lasting)],
  334. corner-radius: 2pt,
  335. extrude: (0, 3),
  336. name: <input_mrd>,
  337. ),
  338. edge(<input_mrd>, <depth>, "-|>"),
  339. node((0.75, 1), [MRD alignement depth ?], shape: diamond, name: <depth>),
  340. edge(<depth>, <low_depth>, "-|>", [< 4]),
  341. node(
  342. (0, 2),
  343. [Low MRD depth: #num(data.bam_stats.n_low_mrd_depth)],
  344. shape: parallelogram,
  345. name: <low_depth>,
  346. ),
  347. edge(<depth>, <seen>, "-|>"),
  348. node(
  349. (0.75, 3),
  350. [Alt. base seen in MRD pileup ?],
  351. shape: diamond,
  352. name: <seen>,
  353. ),
  354. edge(<seen>, <constit>, "-|>", [Yes]),
  355. node(
  356. (0, 4),
  357. [Constit: #num(data.bam_stats.n_constit)],
  358. shape: parallelogram,
  359. name: <constit>,
  360. ),
  361. edge(<seen>, <is_div>, "-|>", [No]),
  362. node(
  363. (1.1, 4),
  364. [Sequence #sym.plus.minus 20nt \ diversity ?],
  365. shape: diamond,
  366. name: <is_div>,
  367. ),
  368. edge(<is_div>, <low_div>, "-|>", [entropy < 1.8]),
  369. node(
  370. (0.25, 5),
  371. [Low diversity, artefact: #num(data.bam_stats.n_low_diversity)],
  372. shape: parallelogram,
  373. name: <low_div>,
  374. ),
  375. edge(<is_div>, <somatic>, "-|>"),
  376. node(
  377. (1.75, 5),
  378. [Somatic: #num(data.bam_stats.n_somatic)],
  379. shape: hexagon,
  380. extrude: (-3, 0),
  381. name: <somatic>,
  382. stroke: cr_colors.green,
  383. ),
  384. )
  385. }
  386. #let barCallers(path) = {
  387. import cetz.draw: *
  388. import cetz.chart
  389. let json_data = json(path).variants_stats
  390. let data = json_data.find(item => item.name == "callers_cat")
  391. let chart_data = data.counts.pairs().sorted(key: x => -x.at(1))
  392. set text(11pt)
  393. cetz.canvas(
  394. length: 80%,
  395. {
  396. set-style(axes: (
  397. bottom: (tick: (label: (angle: 45deg, anchor: "north-east"))),
  398. ))
  399. chart.columnchart(
  400. chart_data,
  401. size: (1, 0.5),
  402. )
  403. },
  404. )
  405. }
  406. #let truncate(text, max-length) = {
  407. if text.len() <= max-length {
  408. text
  409. } else {
  410. text.slice(0, max-length - 3) + "..."
  411. }
  412. }
  413. // // #let add_newlines(text, n) = {
  414. // // let result = ""
  415. // // let chars = text.clusters()
  416. // // for (i, char) in chars.enumerate() {
  417. // // result += char
  418. // // if calc.rem((i + 1), n == 0 and i < chars.len() - 1 {
  419. // // result += "\n"
  420. // // }
  421. // // }
  422. // // result
  423. // // }
  424. //
  425. // #let break_long_words(text, max_length: 20, hyphen: "") = {
  426. // let words = text.split(" ")
  427. // let result = ()
  428. //
  429. // for word in words {
  430. // if word.len() <= max_length {
  431. // result.push(word)
  432. // } else {
  433. // let segments = ()
  434. // let current_segment = ""
  435. // for char in word.clusters() {
  436. // if current_segment.len() + 1 > max_length {
  437. // segments.push(current_segment + hyphen)
  438. // current_segment = ""
  439. // }
  440. // current_segment += char
  441. // }
  442. // if current_segment != "" {
  443. // segments.push(current_segment)
  444. // }
  445. // result += segments
  446. // }
  447. // }
  448. //
  449. // result.join(" ")
  450. // }
  451. //
  452. #let format_sequence(text, max_length: 40, hyphen: [#linebreak()]) = {
  453. let words = text.split(" ")
  454. let result = ()
  455. // result.push("\n")
  456. for word in words {
  457. if word.len() <= max_length {
  458. result.push(word)
  459. } else {
  460. let segments = ()
  461. let current_segment = ""
  462. for char in word.clusters() {
  463. if current_segment.len() + 1 > max_length {
  464. segments.push(current_segment + hyphen)
  465. current_segment = ""
  466. }
  467. current_segment += char
  468. }
  469. if current_segment != "" {
  470. segments.push(current_segment)
  471. }
  472. result += segments
  473. }
  474. }
  475. result.push("")
  476. let sequence = result.join(" ")
  477. box(width: 100%, par(leading: 0.2em, sequence))
  478. }
  479. #let dna(sequence, line_length: 60) = {
  480. let formatted = sequence.clusters().map(c => {
  481. if c == "A" {
  482. text(fill: red)[A]
  483. } else if c == "T" {
  484. text(fill: green)[T]
  485. } else if c == "C" {
  486. text(fill: blue)[C]
  487. } else if c == "G" {
  488. text(fill: orange)[G]
  489. } else {
  490. c
  491. }
  492. })
  493. let lines = formatted.chunks(line_length).map(line => line.join())
  494. let n_lines = lines.len()
  495. let lines = lines.join("\n")
  496. if n_lines > 1 {
  497. parbreak()
  498. }
  499. align(
  500. left,
  501. box(
  502. fill: luma(240),
  503. inset: (x: 0.5em, y: 0.5em),
  504. radius: 4pt,
  505. align(
  506. left,
  507. text(
  508. font: "Fira Code",
  509. size: 10pt,
  510. lines,
  511. ),
  512. ),
  513. ),
  514. )
  515. if n_lines > 1 {
  516. parbreak()
  517. }
  518. }
  519. #let to-string(content) = {
  520. if content.has("text") {
  521. content.text
  522. } else if content.has("children") {
  523. content.children.map(to-string).join("")
  524. } else if content.has("body") {
  525. to-string(content.body)
  526. } else if content == [ ] {
  527. " "
  528. }
  529. }
  530. #let format-number(num) = {
  531. let s = str(num).split("").filter(item => item != "")
  532. let result = ""
  533. let len = s.len()
  534. for (i, char) in s.enumerate() {
  535. result += char
  536. if (i < len - 1 and calc.rem((len - i - 1), 3) == 0) {
  537. result += ","
  538. }
  539. }
  540. result
  541. }
  542. #let format_json(json_data) = {
  543. let format_value(value) = {
  544. if value == none {
  545. ""
  546. } else if type(value) == "string" {
  547. if value != "." {
  548. value.replace(";", ", ").replace("=", ": ")
  549. } else {
  550. ""
  551. }
  552. } else if type(value) == "array" {
  553. let items = value.map(v => format_value(v))
  554. "[" + items.join(", ") + "]"
  555. } else if type(value) == "dictionary" {
  556. "{" + format_json(value) + "}"
  557. } else {
  558. str(value)
  559. }
  560. }
  561. if type(json_data) == "dictionary" {
  562. let result = ()
  563. for (key, value) in json_data {
  564. let formatted_value = format_value(value)
  565. if formatted_value != "" {
  566. if key == "svinsseq" {
  567. formatted_value = dna(formatted_value)
  568. }
  569. result.push(upper(key) + ": " + formatted_value)
  570. }
  571. }
  572. result.join(", ")
  573. } else {
  574. format_value(json_data)
  575. }
  576. }
  577. #let card(d) = {
  578. set text(12pt)
  579. let position_fmt = format-number(d.position)
  580. let title_bg_color = rgb("#f9fafb00")
  581. let grid_content = ()
  582. let callers_data = json.decode(d.callers_data)
  583. // Title
  584. let alt = d.alternative
  585. // TODO: add that in pandora_lib_variants
  586. if d.callers == "Nanomonsv" and alt == "<INS>" {
  587. alt = d.reference + callers_data.at(0).info.Nanomonsv.svinsseq
  588. }
  589. let title = d.contig + ":" + position_fmt + " " + d.reference + sym
  590. .quote
  591. .angle
  592. .r
  593. .single + truncate(alt, 30)
  594. grid_content.push(
  595. grid.cell(
  596. fill: cr_colors.light_grey,
  597. align: center,
  598. block(width: 100%, title),
  599. ),
  600. )
  601. // Consequences
  602. if d.consequence != none {
  603. let consequences = d.consequence.replace(",", ", ").replace(
  604. "_",
  605. " ",
  606. ) + " " + emph(strong(d.gene))
  607. grid_content.push(
  608. grid.cell(fill: cr_colors.light_grey, align: center, consequences),
  609. )
  610. }
  611. // hgvs_c
  612. if d.hgvs_c != none {
  613. grid_content.push(
  614. grid.cell(fill: rgb("#fef08a"), align: center, truncate(d.hgvs_c, 50)),
  615. )
  616. }
  617. // hgvs_c
  618. if d.hgvs_p != none {
  619. grid_content.push(
  620. grid.cell(fill: rgb("#fecaca"), align: center, truncate(d.hgvs_p, 50)),
  621. )
  622. }
  623. // Content
  624. let content = ()
  625. content.push(
  626. badge-red("VAF: " + str(calc.round(d.m_vaf * 100, digits: 2)) + "%"),
  627. )
  628. // content.push(" ")
  629. if d.cosmic_n != none {
  630. content.push(badge-red("Cosmic: " + str(d.cosmic_n)))
  631. }
  632. if d.gnomad_af != none {
  633. content.push(badge-blue("GnomAD: " + str(d.gnomad_af)))
  634. }
  635. let callers_contents = ()
  636. for caller_data in callers_data {
  637. let caller = ""
  638. for (k, v) in caller_data.format {
  639. caller = k
  640. }
  641. callers_contents.push(underline(caller) + ":")
  642. if caller_data.qual != none {
  643. callers_contents.push([
  644. Qual: #caller_data.qual,
  645. ])
  646. }
  647. callers_contents.push([
  648. #(
  649. format_json(caller_data.format.at(caller)),
  650. format_json(caller_data.info.at(caller)),
  651. ).filter(v => v != "").join(", ")
  652. ])
  653. }
  654. content.push(
  655. grid(
  656. columns: 1,
  657. inset: 0.5em,
  658. ..callers_contents
  659. ),
  660. )
  661. grid_content.push(grid.cell(fill: white, content.join(" ")))
  662. block(
  663. breakable: false,
  664. width: 100%,
  665. grid(
  666. columns: 1,
  667. inset: 0.5em,
  668. stroke: cr_colors.dark_grey,
  669. ..grid_content
  670. ),
  671. )
  672. }
  673. #let variants(path, interpretation: "PATHO") = {
  674. let data = json(path)
  675. let patho = data.filter(d => d.interpretation == interpretation)
  676. for var in patho {
  677. card(var)
  678. }
  679. }
  680. #set heading(numbering: (..numbers) => {
  681. if numbers.pos().len() >= 2 and numbers.pos().len() <= 3 {
  682. numbering("1.1", ..numbers.pos().slice(1))
  683. }
  684. })
  685. #set list(marker: [---])
  686. #heading(level: 1, outlined: false)[Whole Genome Sequencing Report]
  687. #outline(title: "Table of Contents", depth: 3)
  688. #pagebreak()
  689. == Interprétation
  690. #v(0.5cm)
  691. #let scoped-content = {
  692. show heading: it => {
  693. set text(font: "FreeSans", size: 14pt)
  694. align(left, it)
  695. v(5pt)
  696. }
  697. cmarker.render(
  698. read(sys.inputs.base + "/diag/report/" + sys.inputs.id + "_conclusion.md"), h1-level: 4,
  699. )
  700. }
  701. #scoped-content
  702. #pagebreak()
  703. == Sample identity
  704. #sys.inputs.id
  705. == Alignement
  706. #grid(
  707. columns: (1fr, 1fr),
  708. gutter: 3pt,
  709. [
  710. ==== Diagnostic sample
  711. #set text(size: 11pt)
  712. #reportBam(sys.inputs.base + "/diag/" + sys.inputs.id + "_diag_hs1_info.json")
  713. ],
  714. [
  715. ==== MRD sample
  716. #set text(size: 11pt)
  717. #reportBam(sys.inputs.base + "/mrd/" + sys.inputs.id + "_mrd_hs1_info.json")
  718. #set footnote(numbering: n => {
  719. " "
  720. })
  721. #footnote[Values computed by #link("https://github.com/wdecoster/cramino")[cramino] v0.14.5
  722. ]
  723. ],
  724. )
  725. #pagebreak()
  726. === Normalized read count by chromosome
  727. #[
  728. #set text(size: 10pt)
  729. #printReadCount(
  730. sys.inputs.base + "/diag/" + sys.inputs.id + "_diag_hs1_info.json",
  731. sys.inputs.base + "/mrd/" + sys.inputs.id + "_mrd_hs1_info.json",
  732. )
  733. ]
  734. == Variants
  735. === Variants calling
  736. #pagebreak()
  737. ==== VCF filters
  738. #pad(
  739. top: 0.8cm,
  740. align(
  741. center,
  742. scale(
  743. x: 100%,
  744. y: 100%,
  745. reflow: true,
  746. variantsFlow(sys.inputs.base + "/diag/report/data/" + sys.inputs.id + "_variants_stats.json"),
  747. ),
  748. ),
  749. )
  750. ==== BAM filters
  751. #pad(
  752. top: 0.8cm,
  753. align(
  754. center,
  755. scale(
  756. x: 100%,
  757. y: 100%,
  758. reflow: true,
  759. bamFilter(sys.inputs.base + "/diag/report/data/" + sys.inputs.id + "_variants_stats.json"),
  760. ),
  761. ),
  762. )
  763. #pagebreak()
  764. === Somatic variants
  765. ==== Callers
  766. #v(0.5cm)
  767. #image(sys.inputs.base + "/diag/report/data/" + sys.inputs.id + "_barcharts_callers.svg")
  768. ==== Consequences (VEP)
  769. #v(0.5cm)
  770. #image(sys.inputs.base + "/diag/report/data/" + sys.inputs.id + "_barcharts_consequences.svg")
  771. ==== NCBI features
  772. #v(0.5cm)
  773. #image(sys.inputs.base + "/diag/report/data/" + sys.inputs.id + "_barcharts_ncbi.svg")
  774. #pagebreak()
  775. === Selected Variants
  776. ==== Classification
  777. - Pathogenic: experimentally proved that the variant participate in the oncogenic process.
  778. - Likely pathogenic: gene or variant that could be linked to the oncogenic process in bibliography.
  779. - Unknown significance: somatic variant without more information.
  780. ==== Pathogenics
  781. #variants(
  782. sys.inputs.base + "/diag/report/data/" + sys.inputs.id + "_annot_variants.json",
  783. interpretation: "PATHO",
  784. )
  785. ==== Likely Pathogenics
  786. #variants(
  787. sys.inputs.base + "/diag/report/data/" + sys.inputs.id + "_annot_variants.json",
  788. interpretation: "PROBPATHO",
  789. )
  790. ==== Variants of Unknown Significance
  791. #variants(
  792. sys.inputs.base + "/diag/report/data/" + sys.inputs.id + "_annot_variants.json",
  793. interpretation: "US",
  794. )
  795. == Coverage by chromosome
  796. === Proportion at given depth by chromosome
  797. #reportCoverage(sys.inputs.base + "/diag/report/data/scan/" + sys.inputs.id)
  798. #set footnote(numbering: n => {
  799. " "
  800. })
  801. #footnote[Values computed by Pandora development version]
  802. == Method
  803. === Sample preparation and sequencing
  804. + DNA sampling and collection in EDTA tubes.
  805. + Buffy coat: pooling of multiple EDTA tubes then centrifugation (1200 rpm 10 minutes).
  806. + DNA extraction according to Maxwell® Promega RSC Buffy Coat DNA Kit.
  807. + Nanodrop DNA quantification.
  808. + DNA shearing: 3µg of DNA mechanically sheared by Covaris g-TUBE (8000 rpm, 1 minute).
  809. + DNA size qualification aiming a median of 10 kb determined by TapeStation.
  810. + Libary was constructed following the Oxford Nanopore Technologies Ligation Sequencing Kit V14 (SQK-LSK114) protocol with 1.5 µg as input DNA.
  811. + Qubit quantification
  812. + After evaluation of flowcell (rev 10) for pore availability (> 6000 available pores). Two distinct barcoded libraries were pooled for each flowcell.
  813. + sequencing run was initiated and controlled using MinKNOW software (sequencing 80 hours, data output format: Raw pod5 files).
  814. === Bioinformatic analysis
  815. + Orchestration and global analysis realized by in-house software (source code is accessible at Github).
  816. + Basecalling and alignment: dorado v0.8.2 with parameters "sup,5mC_5hmC --trim all" with alignment on hs1 genome (T2T chm13v2.0).
  817. + Variant calling was realized with ClairS v0.4.0, DeepVariant v1.6.1, DeepSomatic v1.7.0, Nanomonsv v0.7.2.
  818. + Variants filtering and merging done with in-house software.
  819. + Annotation: ensembl-VEP 112 with gene features defined by RefSeq Liftoff v5.1. SNP from gnomAD_4-2022_10 and Cosmic v99.
  820. + Interpretation and report generation performed on a local web service also published in open source.