Loading the family.
diff --git a/README.md b/README.md index cc4b123b..df90cf70 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ This pipeline takes raw sequencing data (FASTQ/BAM/VCF) from any vendor and runs - One Nextflow DSL2 pipeline from FASTQ, BAM or VCF to the report, and every step also as a bash script you can run on its own. - Optional family analysis of two or more processed relatives (step 38, a pedigree file): a GLnexus joint call, a somalier check that the samples are the people the pedigree says, which allele a child got from each parent, de novo candidates with both parents, and compound heterozygotes in trans or in cis ([multi-sample](https://geiserx.github.io/OwnGenome/multi-sample/#family-analysis-step-38-opt-in)). - Optional couple carrier screen for two partners of the pedigree (step 39): the recessive genes where both carry a change (ClinVar hits, deletions, SMN1 copy loss, FMR1 and FXN repeats) with the chance for a child, the X-linked findings, and what the screen does not see ([step 39](https://geiserx.github.io/OwnGenome/39-couple/)). -- On the live site, a page per family: the pedigree drawn from the family file, each child's chromosome map, the relationship check, the shared segments and the couple screen ([family page](https://geiserx.github.io/OwnGenome/multi-sample/#on-the-live-site)). +- On the live site, a page per family: a pedigree where each sequenced person opens their own page, the couple screen, who is related how as a grid, each child's stretches painted by parent, the de novo and compound-het candidates, and the members' medicines genes and haplogroups side by side ([family page](https://geiserx.github.io/OwnGenome/multi-sample/#on-the-live-site)). - Ends in an HTML report and a MultiQC summary. Alternative callers (GATK, FreeBayes, Strelka2, Octopus, BWA-MEM2, TIDDIT, GRIDSS) are there for benchmarking. ## Quick start @@ -66,7 +66,7 @@ The full documentation is at **https://geiserx.github.io/OwnGenome/**, one page - [Vendor compatibility guide](https://geiserx.github.io/OwnGenome/vendor-guide/): what each provider delivers and how to get it - [Pipeline overview](https://geiserx.github.io/OwnGenome/pipeline-overview/): every step with its tool and image, which ones a default run includes, and the page for each step - [Nextflow](https://geiserx.github.io/OwnGenome/nextflow/): the workflow runner, parallel steps and resume -- [Watch a run live](https://geiserx.github.io/OwnGenome/live-site/): a step map in the browser while the run goes, behind a password. The [demo](https://geiserx.github.io/OwnGenome/demo/) replays an invented run +- [Watch a run live](https://geiserx.github.io/OwnGenome/live-site/): each genome as a report that leads with what to take to a doctor, a page per finding, a report per family, and a timeline of the steps while the run goes, behind a password. The [demo](https://geiserx.github.io/OwnGenome/demo/) replays an invented run - [Interpreting your results](https://geiserx.github.io/OwnGenome/interpreting-results/): what each report means and what to do with it - [Multi-sample comparison](https://geiserx.github.io/OwnGenome/multi-sample/): partners, siblings, parents - [Long-read guide](https://geiserx.github.io/OwnGenome/long-read-guide/): Nanopore and PacBio HiFi diff --git a/assets/live/core.js b/assets/live/core.js index 64ec443f..b6e4ff07 100644 --- a/assets/live/core.js +++ b/assets/live/core.js @@ -1,6 +1,6 @@ -// core.js: what the live site's pages share (home.html, index.html, family.html). Element builders, +// core.js: what the live site's pages share (home.html, index.html, finding.html, family.html). Element builders, // a fetch that sends the session and goes to the login on a 401, the pedigree symbol, dates, the -// caveat block, the facts list and the table. Text only through textContent and createElementNS: +// caveat block, the facts list, the table, and the Journal's level marks and chromosome map. Text only through textContent and createElementNS: // no innerHTML, no inline script, no eval. Everything hangs on window.PGPUI. (function () { 'use strict'; @@ -124,6 +124,92 @@ }); } + // --- the Journal's shared marks: the level glyph and word, and the chromosome map of findings --- + var LEVEL_WORDS = { act: 'Take to a doctor', medicine: 'Tell your prescriber', review: 'Worth a look', + carrier: 'For family planning', trust: 'Data quality', info: 'For information' }; + var LEVEL_SHORT = { act: 'doctor', medicine: 'prescriber', review: 'look', carrier: 'family', trust: 'quality', info: 'info' }; + var LEVEL_ORDER = ['act', 'medicine', 'review', 'carrier', 'trust', 'info']; + // A level is always its glyph and a word, never a colour alone. + function level(lv, words, extra) { + var s = el('span', { 'class': 'lv ' + lv + (extra ? ' ' + extra : ''), 'data-level': lv }); + add(s, el('span', { 'class': 'g', 'aria-hidden': 'true' }), words === undefined ? LEVEL_WORDS[lv] || lv : words); + return s; + } + // A finding's level: a data-quality finding is always a problem with the data, so its mark is red. + function findingLevel(lv, words) { return level(lv, words, lv === 'trust' ? 'problem' : undefined); } + // GRCh38: [name, length Mb, centromere Mb]. + var CHR = [['1', 248.96, 123.4], ['2', 242.19, 93.9], ['3', 198.3, 90.9], ['4', 190.21, 50], ['5', 181.54, 48.8], + ['6', 170.81, 59.8], ['7', 159.35, 60.1], ['8', 145.14, 45.2], ['9', 138.39, 43], ['10', 133.8, 39.8], + ['11', 135.09, 53.4], ['12', 133.28, 35.5], ['13', 114.36, 17.7], ['14', 107.04, 17.2], ['15', 101.99, 19], + ['16', 90.34, 36.8], ['17', 83.26, 25.1], ['18', 80.37, 18.5], ['19', 58.62, 26.2], ['20', 64.44, 28.1], + ['21', 46.71, 12], ['22', 50.82, 15], ['X', 156.04, 60.6], ['Y', 57.23, 10.4]]; + var LV_COLOUR = { act: '#d03b3b', medicine: '#ec835a', review: '#fab219', carrier: '#9085e9', trust: '#c3c2b7', info: '#c3c2b7' }; + function glyph(g, lv, x, y) { + var c = LV_COLOUR[lv] || LV_COLOUR.info; + if (lv === 'act') { g.appendChild(svg('rect', { x: x - 4, y: y - 4, width: 8, height: 8, fill: c, transform: 'rotate(45 ' + x + ' ' + y + ')' })); } + else if (lv === 'medicine') { g.appendChild(svg('rect', { x: x - 5.5, y: y - 3, width: 11, height: 6, rx: 3, fill: c })); } + else if (lv === 'review') { g.appendChild(svg('path', { d: 'M' + x + ' ' + (y - 5) + 'L' + (x + 5) + ' ' + (y + 4) + 'L' + (x - 5) + ' ' + (y + 4) + 'Z', fill: c })); } + else if (lv === 'carrier') { + g.appendChild(svg('circle', { cx: x - 2.6, cy: y, r: 3.4, fill: 'none', stroke: c, 'stroke-width': 1.6 })); + g.appendChild(svg('circle', { cx: x + 2.6, cy: y, r: 3.4, fill: 'none', stroke: c, 'stroke-width': 1.6 })); + } else { g.appendChild(svg('circle', { cx: x, cy: y, r: 4, fill: 'none', stroke: c, 'stroke-width': 1.5 })); } + } + // "chr16:2087914" or "chr16:2084000-2096000" -> {chr: '16', mb: 2.08}; null for anything else. + function locus(v) { + var m = /^(?:chr)?([0-9]{1,2}|X|Y):([0-9]+)/.exec(String(v || '')); + return m ? { chr: m[1], mb: Number(m[2]) / 1e6 } : null; + } + // The chromosome map: the 24 chromosomes in a row, each finding a glyph on its chromosome, with its + // name beside it and the same words in a title (and in the list under the figure). + function karyogram(marks, opts) { + opts = opts || {}; + var W = opts.width || 680, H = opts.height || 190, top = 22, bottom = 22; + var slot = (W - 56) / CHR.length, scale = (H - top - bottom) / 249; + var s = svg('svg', { viewBox: '0 0 ' + W + ' ' + H, role: 'img', 'aria-label': opts.aria || 'Where the findings sit, by chromosome' }); + CHR.forEach(function (c, i) { + var x = i * slot + slot / 2 - 3.5; + s.appendChild(svg('rect', { 'class': 'chr', x: x, y: top, width: 7, height: Math.max(1, c[2] * scale - 1.2), rx: 3.5 })); + s.appendChild(svg('rect', { 'class': 'chr', x: x, y: top + c[2] * scale + 1.2, width: 7, height: Math.max(1, (c[1] - c[2]) * scale - 1.2), rx: 3.5 })); + var t = svg('text', { 'class': 'lab', x: x + 3.5, y: H - 6, 'text-anchor': 'middle' }); + t.textContent = c[0]; + s.appendChild(t); + }); + (marks || []).forEach(function (m) { + var i = -1; + CHR.forEach(function (c, j) { if (c[0] === m.chr) { i = j; } }); + if (i < 0) { return; } + var x = i * slot + slot / 2 + 7, y = top + Math.min(m.mb, CHR[i][1]) * scale; + var g = svg('g', { 'class': 'mk', tabindex: '0' }); + var tt = svg('title', {}); + tt.textContent = (LEVEL_WORDS[m.lv] || '') + ': ' + (m.tip || m.label); + g.appendChild(tt); + g.appendChild(svg('line', { x1: x - 10.5, x2: x - 1, y1: y, y2: y, stroke: LV_COLOUR[m.lv] || LV_COLOUR.info, 'stroke-width': 2 })); + glyph(g, m.lv, x + 4, y); + if (m.label) { + var t = svg('text', { x: x + 11, y: y + 3.5, stroke: '#0e0f10', 'stroke-width': 4, 'paint-order': 'stroke', 'stroke-linejoin': 'round' }); + t.textContent = m.label; + g.appendChild(t); + } + if (m.href) { + g.setAttribute('role', 'link'); + g.addEventListener('click', function () { window.location.href = m.href; }); + g.addEventListener('keydown', function (e) { if (e.key === 'Enter') { window.location.href = m.href; } }); + } + s.appendChild(g); + }); + return s; + } + // A plain time: "14:22" in UTC, "4 Nov" for a day. + var MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + function hm(utc) { var d = new Date(utc); return isFinite(d) ? ('0' + d.getUTCHours()).slice(-2) + ':' + ('0' + d.getUTCMinutes()).slice(-2) : ''; } + function dm(utc) { var d = new Date(utc); return isFinite(d) ? d.getUTCDate() + ' ' + MONTHS[d.getUTCMonth()] : ''; } + function hours(a, b) { + var x = Date.parse(a), y = Date.parse(b); + return isFinite(x) && isFinite(y) && y > x ? (y - x) / 3600e3 : null; + } + window.PGPUI = { el: el, svg: svg, add: add, getJSON: getJSON, avatar: avatar, day: day, caveat: caveat, - facts: facts, table: table, tablist: tablist, firstSentence: firstSentence }; + facts: facts, table: table, tablist: tablist, firstSentence: firstSentence, level: level, findingLevel: findingLevel, glyph: glyph, + karyogram: karyogram, locus: locus, LEVEL_WORDS: LEVEL_WORDS, LEVEL_SHORT: LEVEL_SHORT, LEVEL_ORDER: LEVEL_ORDER, + hm: hm, dm: dm, hours: hours }; }()); diff --git a/assets/live/family-page.js b/assets/live/family-page.js index 8a95a211..3a0213ff 100644 --- a/assets/live/family-page.js +++ b/assets/live/family-page.js @@ -1,8 +1,10 @@ // family-page.js: a family's page (assets/live/family.html), from family.json -// (bin/owngenome_live_family.py): the pedigree with each sequenced person a link to their page, the +// (bin/owngenome_live_family.py), the Journal design: a report in chapters, with a lead that answers +// "what does it mean for our children?". The pedigree with each sequenced person a link to their page, the // members with their run progress, what steps 38 and 39 planned, each couple's carrier screen in full -// (both carry, then X-linked, then one partner), who is related how, and each child's chromosome -// map. It draws what the steps wrote and computes nothing about genetics. Text through textContent, +// (both carry, then X-linked, then one partner), who is related how as a grid (a list of pairs on a +// phone), each child's stretches painted by parent, the de novo and compound-het candidates, and the +// members side by side. It draws what the steps wrote and computes nothing about genetics. Text through textContent, // the drawing through createElementNS; images from this server only. (function () { 'use strict'; @@ -11,11 +13,60 @@ var el = U.el, add = U.add, root = document.documentElement; var RESULT = { shared: 'Both carry', x_linked: 'X-linked', one_partner: 'One partner' }; + // A chapter: its roman number in brass, its title in the serif. + var CHAPTER = { 'Pedigree': 'I', 'Who is related how': 'II', 'Couple carrier screen': 'III', 'Each child': 'IV', + 'Side by side': 'V', 'Plan and reports': 'App.' }; function section(title, cls) { - var s = el('section', { 'class': 'card ' + (cls || '') }); - add(s, el('h2', {}, title)); + var s = el('section', { 'class': 'card ' + (cls || ''), id: 'ch-' + (cls || 'x') }); + var h = el('h2', {}); + if (CHAPTER[title]) { add(h, el('span', { 'class': 'secnum' }, CHAPTER[title])); } + add(h, title); + add(s, h); return s; } + // Colour follows the person: the members in their order, one colour each, on every family figure. + var PCOL = ['#d95926', '#3987e5', '#199e70', '#c98500', '#9085e9']; + function colourOf(f, sample) { + var i = (f.state.members || []).map(function (m) { return m.sample; }).indexOf(sample); + return i >= 0 ? PCOL[i % PCOL.length] : null; + } + // The four equally likely children of two carriers of one recessive gene: 1 affected, 2 carriers, 1 neither. + // Drawn only when the screen's own chance is 1 in 4: another chance (a partner with two copies) has + // another split, and the row's words say it. + function fourChildren(risk) { + if (!/^\s*1 in 4\b/.test(risk || '')) { return null; } + var box = el('div', { 'class': 'kids4' }); + var g = el('div', { 'class': 'grid4', role: 'img', 'aria-label': 'Each pregnancy: 1 in 4 affected, 2 in 4 carriers, 1 in 4 neither' }); + add(g, el('span', { 'class': 'aff', title: 'affected' }), el('span', { title: 'carrier' }), el('span', { title: 'carrier' }), + el('span', { 'class': 'non', title: 'neither' })); + add(box, g, add(el('div', { 'class': 'kt' }), el('b', {}, '1 in 4'), ' per pregnancy affected', el('br', {}), + '2 in 4 carriers, not affected; 1 in 4 neither')); + return box; + } + // The family's lead: what the screen, the relationship check and the children say, in plain words. + function lead(f) { + var st = f.state, out = []; + var seq = (st.members || []).filter(function (m) { return m.sequenced; }).length; + out.push([seq + ' genome' + (seq === 1 ? '' : 's') + ' of this family are read together here. ', false]); + (st.couples || []).forEach(function (c) { + if (!c.screened) { out.push([c.a + ' and ' + c.b + ' are not screened as a couple yet. ', false]); return; } + if (c.shared) { out.push([c.a + ' and ' + c.b + ' both carry a change in ' + c.shared + ' gene' + (c.shared > 1 ? 's' : '') + ': chapter III gives the chance per pregnancy for each. ', true]); } + else { out.push([c.a + ' and ' + c.b + ' share no carrier gene in what the screen sees; ', false], ['it does not see everything. ', false]); } + if (c.x_linked) { out.push([c.x_linked + ' X-linked finding' + (c.x_linked > 1 ? 's' : '') + ' bear on sons. ', true]); } + }); + var flags = (f.relationships || []).filter(function (r) { return r.verdict === 'mismatch' || r.verdict === 'warn'; }).length; + var rels = f.relationships || []; + var checked = rels.filter(function (r) { return r.verdict && r.verdict !== 'not_checked'; }).length; + if (rels.length) { + out.push([flags ? 'A measured relationship differs from the family tree: read chapter II before anything else. ' + : checked === rels.length ? 'The measured relationships match the family tree. ' + : 'Some relationships could not be checked: chapter II says which. ', !!flags]); + } + if ((st.children || []).length) { + out.push([(st.children.length === 1 ? 'One child' : st.children.length + ' children') + ': chapter IV shows what each inherited from whom.', false]); + } + return out; + } function memberUrl(f, s) { return f.runs && f.runs[s] ? f.runs[s].url : null; } // The pedigree drawn from the layout the server gives: a sequenced person with a run here is a link. @@ -38,6 +89,9 @@ var shape = p.sex === 'male' ? U.svg('rect', { x: p.x - h, y: p.y - h, width: L.size, height: L.size, 'class': cls }) : p.sex === 'female' ? U.svg('circle', { cx: p.x, cy: p.y, r: h, 'class': cls }) : U.svg('polygon', { points: [p.x, p.y - h, p.x + h, p.y, p.x, p.y + h, p.x - h, p.y].join(' '), 'class': cls }); + var pc = p.sequenced ? colourOf(f, p.sample) : null; + if (pc) { shape.style.setProperty('stroke', pc); } + if (!p.sequenced) { shape.style.setProperty('stroke-dasharray', '4 3'); } g.appendChild(shape); var t = U.svg('text', { x: p.x, y: p.y + h + 17, 'text-anchor': 'middle', 'class': 'ped-name' }); t.textContent = p.sample; @@ -125,6 +179,13 @@ 'screen does not see. '; if (c.report) { add(p, el('a', { href: c.report }, 'What the screen does not see')); } add(s, p); + rows.filter(function (r) { return r.result === 'shared'; }).forEach(function (r) { + var box = el('div', { 'class': 'mini', 'data-shared': r.gene }); + add(box, el('h3', { 'class': 'h3' }, (r.condition || r.gene) + ' (' + r.gene + ')')); + add(box, el('p', { 'class': 'soft' }, 'Both carry a change in ' + r.gene + '. Carriers are not affected; a child who inherits both changes is.')); + add(box, fourChildren(r.offspring_risk) || el('p', {}, 'Chance per pregnancy, as the screen gives it: ' + (r.offspring_risk || 'not given') + '.')); + add(s, box); + }); add(s, U.table({ columns: ['Result', 'Gene', 'Condition', 'Chance for a child', c.a, c.b, 'Why'], rows: rows.map(function (r) { var aFirst = r.partner_a === c.a; @@ -135,44 +196,211 @@ return s; } + function pairKey(a, b) { return [a, b].sort().join(' '); } + // Step 38's verdicts: ok (shown as matching), warn, mismatch, not_checked (too few sites to tell). + function mismatch(v) { return v === 'mismatch' || v === 'warn'; } function related(f) { var s = section('Who is related how', 'related'); var rel = f.relationships || [], ibd = f.ibd_pairs || []; if (!rel.length && !ibd.length) { add(s, el('p', { 'class': 'dim' }, 'Not run yet (step 38).')); return s; } - var byPair = {}; - ibd.forEach(function (r) { byPair[[r.sample_a, r.sample_b].sort().join(' ')] = r; }); + var byRel = {}, byIbd = {}, names = []; + function seen(n) { if (names.indexOf(n) < 0) { names.push(n); } } + rel.forEach(function (r) { byRel[pairKey(r.sample_a, r.sample_b)] = r; seen(r.sample_a); seen(r.sample_b); }); + ibd.forEach(function (r) { byIbd[pairKey(r.sample_a, r.sample_b)] = r; seen(r.sample_a); seen(r.sample_b); }); + var order = (f.state.members || []).map(function (m) { return m.sample; }); + names.sort(function (a, b) { return order.indexOf(a) - order.indexOf(b); }); + function cellText(a, b) { + var r = byRel[pairKey(a, b)] || {}, i = byIbd[pairKey(a, b)] || {}; + var lines = []; + lines.push(i.degree || (r.relatedness ? 'relatedness ' + r.relatedness : 'not measured')); + if (i.shared_cm) { lines.push(i.shared_cm + ' cM shared'); } + var declared = r.declared || i.declared; + if (declared) { lines.push('declared ' + declared); } + if (r.verdict === 'not_checked') { lines.push('not checked: too few sites'); } + else if (!r.verdict) { lines.push('relationship check not run'); } + return { lines: lines, flag: mismatch(r.verdict), verdict: r.verdict, reason: r.reason }; + } + // The grid: one cell per pair, the degree first, the declared relation and a flag when they differ. + var grid = el('div', { 'class': 'rel-grid-wrap' }); + var t = el('table', { 'class': 'rel-grid' }); + var head = el('tr', {}); + add(head, el('th', { scope: 'col' }, '')); + names.forEach(function (n) { add(head, el('th', { scope: 'col' }, n)); }); + add(t, add(el('thead', {}), head)); + var body = el('tbody', {}); + names.forEach(function (a) { + var tr = el('tr', {}); + add(tr, el('th', { scope: 'row' }, a)); + names.forEach(function (b) { + if (a === b) { add(tr, el('td', { 'class': 'self', 'aria-label': 'the same person' }, '·')); return; } + if (!byRel[pairKey(a, b)] && !byIbd[pairKey(a, b)]) { add(tr, el('td', { 'class': 'none' }, 'not measured')); return; } + var c = cellText(a, b); + var td = el('td', { 'class': c.flag ? 'flag' : '', title: c.reason || '' }); + c.lines.forEach(function (l, i) { add(td, el('span', { 'class': i ? 'sub' : 'main' }, l)); }); + if (c.flag) { add(td, el('span', { 'class': 'chip chip-attn' }, '⚑ ' + c.verdict)); } + add(tr, td); + }); + add(body, tr); + }); + add(t, body); + add(grid, t); + add(s, grid); + // On a phone: a list of pairs. + var ul = el('ul', { 'class': 'rel-pairs' }); + names.forEach(function (a, i) { + names.slice(i + 1).forEach(function (b) { + if (!byRel[pairKey(a, b)] && !byIbd[pairKey(a, b)]) { return; } + var c = cellText(a, b); + var li = el('li', { 'class': c.flag ? 'flag' : '' }); + add(li, el('span', { 'class': 'pair' }, a + ' and ' + b + ': '), c.lines.join(', ')); + if (c.flag) { add(li, ' ', el('span', { 'class': 'chip chip-attn' }, '⚑ ' + c.verdict + (c.reason ? ': ' + c.reason : ''))); } + add(ul, li); + }); + }); + add(s, ul); + add(s, el('p', { 'class': 'dim legend-line' }, 'Each cell: the degree the shared stretches point to (or somalier’s ' + + 'relatedness), the centimorgans shared and the declared relation. ⚑ marks a pair the measurement does not match.')); var rows = rel.map(function (r) { - var i = byPair[[r.sample_a, r.sample_b].sort().join(' ')] || {}; + var i = byIbd[pairKey(r.sample_a, r.sample_b)] || {}; return [r.sample_a + ' and ' + r.sample_b, r.declared, r.relatedness, r.ibs0, r.ibs2, i.shared_cm || '.', i.degree || '.', r.verdict, r.reason]; }); ibd.forEach(function (i) { - if (!rel.some(function (r) { return [r.sample_a, r.sample_b].sort().join(' ') === [i.sample_a, i.sample_b].sort().join(' '); })) { + if (!byRel[pairKey(i.sample_a, i.sample_b)]) { rows.push([i.sample_a + ' and ' + i.sample_b, i.declared, i.relatedness, '.', '.', i.shared_cm, i.degree, '.', '.']); } }); - add(s, U.table({ columns: ['Pair', 'Declared', 'Relatedness', 'IBS0', 'IBS2', 'Shared cM', 'Degree', 'Verdict', 'Reason'], + var d = el('details', { 'class': 'w-fallback' }); + add(d, el('summary', {}, 'Every measured number')); + add(d, U.table({ columns: ['Pair', 'Declared', 'Relatedness', 'IBS0', 'IBS2', 'Shared cM', 'Degree', 'Verdict', 'Reason'], rows: rows }, 20)); + add(s, d); if (!ibd.length) { add(s, el('p', { 'class': 'dim' }, 'Shared segments need step 26b’s panel: only the somalier half is here.')); } return s; } + // A child's stretches, painted: per chromosome the father's copy above the mother's, each stretch + // coloured by the grandparent it came from (or by the parent when no grandparent was sequenced). + var SIDE = { paternal: 'father', maternal: 'mother' }; + function painting(f, child, rows) { + var lengths = f.chrom_lengths || []; + var longest = Math.max.apply(null, lengths.map(function (x) { return x[1]; }).concat([1])); + var colours = Object.create(null), next = 1; + function colourOf(r) { + var who = r.ancestor && r.ancestor !== '.' ? r.ancestor : (r.parent && r.parent !== '.' ? r.parent : r.side + ' side'); + if (!colours[who]) { colours[who] = { n: next > 5 ? 5 : next++, side: r.side, basis: r.basis }; } + return { who: who, n: colours[who].n }; + } + var fig = el('figure', { 'class': 'painting', 'data-painting': child }); + var grid = el('div', { 'class': 'paint-grid', role: 'img', 'aria-label': 'Stretches ' + child + ' got from each parent, ' + + 'per chromosome; the table below lists them' }); + lengths.forEach(function (cl) { + var chrom = cl[0]; + add(grid, el('span', { 'class': 'paint-chr' }, chrom.replace(/^chr/, ''))); + var both = el('span', { 'class': 'paint-tracks' }); + ['paternal', 'maternal'].forEach(function (side) { + var track = el('span', { 'class': 'paint-track ' + side }); + track.style.setProperty('--len', (100 * cl[1] / longest).toFixed(2) + '%'); + rows.filter(function (r) { return r.chrom === chrom && r.side === side; }).forEach(function (r) { + var a = parseInt(r.start, 10), b = parseInt(r.end, 10); + if (!isFinite(a) || !isFinite(b) || b < a) { return; } + var c = colourOf(r); + var seg = el('span', { 'class': 'paint-seg anc' + c.n + (r.basis === 'other copy' ? ' other' : ''), + title: SIDE[side] + '’s copy, ' + chrom + ':' + a + '-' + b + ', from ' + c.who + ' (' + r.sites + ' sites, ' + + r.discordant + ' discordant)' }); + seg.style.setProperty('--a', (100 * a / cl[1]).toFixed(3) + '%'); + seg.style.setProperty('--w', Math.max(0.4, 100 * (b - a) / cl[1]).toFixed(3) + '%'); + add(track, seg); + }); + add(both, track); + }); + add(grid, both); + }); + add(fig, grid); + var leg = el('figcaption', { 'class': 'paint-legend' }); + add(leg, el('span', { 'class': 'dim' }, 'Upper line: the father’s copy; lower: the mother’s. Colour: who the stretch came from. ')); + Object.keys(colours).forEach(function (who) { + var it = el('span', { 'class': 'paint-key' }); + add(it, el('span', { 'class': 'paint-swatch anc' + colours[who].n + (colours[who].basis === 'other copy' ? ' other' : '') }), + who + (colours[who].basis === 'other copy' ? ' (the copy not from the sequenced parent)' : '')); + add(leg, it); + }); + add(leg, el('span', { 'class': 'dim' }, ' Grey: no stretch called (no informative site, a gap of the reference).')); + add(fig, leg); + var d = el('details', { 'class': 'w-fallback' }); + add(d, el('summary', {}, 'Every stretch (' + rows.length + ')')); + add(d, U.table({ columns: ['Copy', 'Chromosome', 'Start', 'End', 'From', 'Sites', 'Discordant', 'Basis'], + rows: rows.map(function (r) { return [SIDE[r.side] || r.side, r.chrom, r.start, r.end, + r.ancestor !== '.' ? r.ancestor : r.parent, r.sites, r.discordant, r.basis]; }) }, 12)); + add(fig, d); + return fig; + } + function children(f) { - var s = section('Inherited stretches', 'children'); + var s = section('Each child', 'children'); if (!f.state.children.length) { add(s, el('p', { 'class': 'dim' }, 'The pedigree names no child.')); } - f.state.children.forEach(function (c) { + var plan = {}; + (f.state.plan || []).forEach(function (r) { plan[r.analysis] = r; }); + function absent(file, analysis) { + var r = plan[analysis]; + return file + ' is not here' + (r ? ': ' + r.analysis.replace(/_/g, ' ') + ' ' + r.decision + (r.reason ? ' (' + r.reason + ')' : '') : '') + '.'; + } + f.state.children.forEach(function (c, i) { var d = el('details', { 'data-child': c.child }); + if (i === 0) { d.open = true; } add(d, el('summary', {}, c.child + ': father ' + (c.father || 'not named') + ', mother ' + (c.mother || 'not named'))); - if (c.segments) { + add(d, el('h3', {}, 'Inherited stretches')); + var rows = (f.segments || {})[c.child]; + if (rows && rows.length) { + add(d, painting(f, c.child, rows)); + } else if (c.segments) { add(d, el('img', { src: c.segments, alt: 'Chromosome map of ' + c.child, 'class': 'segments' })); } else { add(d, el('p', { 'class': 'dim' }, 'No chromosome map yet: step 38 draws one for a child with a sequenced parent.')); } + var dn = (f.de_novo || []).filter(function (r) { return r.child === c.child; }); + add(d, el('h3', {}, 'De novo candidates')); + if (dn.length) { + add(d, el('p', { 'class': 'dim' }, 'In the child and in neither parent. Most candidates in a genome are genotype errors: ' + + 'check the reads before reading anything into one.')); + add(d, U.table({ columns: ['Variant', 'Child', 'Father', 'Mother', 'Child DP / GQ', 'Allele balance', 'Quality'], + rows: dn.map(function (r) { return [r.chrom + ':' + r.pos + ' ' + r.ref + '>' + r.alt, r.child_gt, r.father_gt, + r.mother_gt, r.child_dp + ' / ' + r.child_gq, r.child_ab, r.quality]; }) }, 8)); + } else { + add(d, el('p', { 'class': 'dim' }, (f.trio_files || {})['de_novo.tsv'] ? 'None listed in de_novo.tsv for this child.' + : absent('de_novo.tsv', 'trio_phasing'))); + } + var ch = (f.compound_het || []).filter(function (r) { return r.child === c.child; }); + add(d, el('h3', {}, 'Compound-het pairs')); + if (ch.length) { + add(d, el('p', { 'class': 'dim' }, 'Two candidate variants in one gene, in trans first: one from each parent.')); + add(d, U.table({ columns: ['Gene', 'Phase', 'Variant A', 'From', 'Variant B', 'From'], + rows: ch.map(function (r) { return [r.gene, r.phase, r.variant_a, r.a_from, r.variant_b, r.b_from]; }) }, 8)); + } else { + add(d, el('p', { 'class': 'dim' }, (f.trio_files || {})['compound_het.tsv'] ? 'None listed in compound_het.tsv for this child.' + : absent('compound_het.tsv', 'trio_phasing'))); + } add(s, d); }); return s; } + // The members side by side: what each one's report says of the genes that differ most between people. + function sideBySide(f) { + var sb = f.side_by_side; + var s = section('Side by side', 'side-by-side'); + if (!sb || !sb.rows.length) { add(s, el('p', { 'class': 'dim' }, 'No member has a summary yet, or no summary lists these results.')); return s; } + add(s, el('p', { 'class': 'dim' }, 'Each member’s own report, one column per person: CYP2D6, the CPIC genes ' + + '(phenotypes in CPIC’s words), HLA, the haplogroups and the blood groups.')); + var group = null, rows = []; + sb.rows.forEach(function (r) { + rows.push([r.group === group ? '' : r.group, r.label].concat(r.values)); + group = r.group; + }); + add(s, U.table({ columns: ['', 'Gene or system'].concat(sb.members), rows: rows }, 14)); + return s; + } + function more(f) { var s = section('Plan and reports', 'plan'); var plan = f.state.plan || []; @@ -198,14 +426,26 @@ root.setAttribute('data-pgp-status', 'ready'); return; } + var cover = el('div', { 'class': 'famcover' }); + var cl = el('div', {}); + add(cl, el('div', { 'class': 'kicker' }, 'Family report \u00b7 in five chapters')); + add(cl, el('h1', { 'class': 'h-display' }, 'Family ' + f.state.name)); + var lp = el('p', { 'class': 'lede dropcap', id: 'family-lead' }); + lead(f).forEach(function (x) { add(lp, x[1] ? el('b', {}, x[0]) : x[0]); }); + add(cl, lp); + var toc = el('ol', { 'class': 'famtoc' }); + [['ch-ped', 'I', 'Pedigree and members'], ['ch-related', 'II', 'Who is related how'], ['ch-couples', 'III', 'Couple carrier screen'], + ['ch-children', 'IV', 'Each child: what they inherited'], ['ch-side-by-side', 'V', 'Side by side']].forEach(function (x) { + add(toc, add(el('li', {}), add(el('a', { href: '#' + x[0] }), el('span', { 'class': 'secnum' }, x[1]), el('span', {}, x[2])))); + }); + add(cover, cl, add(el('aside', {}), el('div', { 'class': 'fig-label' }, 'The chapters'), toc)); + add(page, cover); var top = el('div', { 'class': 'family-top' }); var ped = section('Pedigree', 'ped'); add(ped, pedigree(f), analyses(f)); add(top, ped, members(f)); add(page, top); - var grid = el('div', { 'class': 'family-grid' }); - add(grid, couples(f), related(f)); - add(page, grid, children(f)); + add(page, related(f), couples(f), children(f), sideBySide(f)); if ((f.state.plan || []).length || f.state.reports.length) { add(page, more(f)); } add(main, page); root.setAttribute('data-pgp-status', 'ready'); diff --git a/assets/live/family.html b/assets/live/family.html index ebe0d057..b685eca0 100644 --- a/assets/live/family.html +++ b/assets/live/family.html @@ -10,13 +10,14 @@ {{plugins}}
-Loading the family.