From 7e2cebf1cc9ded8dae8936d9988719c143cbaa94 Mon Sep 17 00:00:00 2001 From: Graham McNeill Date: Mon, 9 Dec 2024 12:24:24 +0000 Subject: [PATCH] [Platform]: Phewas and Manhattan plot fixes (#601) * use linear scales * spacing and therapeutic areas labels --- .../study/GWASCredibleSets/ManhattanPlot.tsx | 48 ++- .../src/variant/GWASCredibleSets/Body.tsx | 2 + .../variant/GWASCredibleSets/PheWasPlot.tsx | 289 +++++++++--------- .../src/components/Plot/components/XLabel.jsx | 30 +- .../src/components/Plot/components/XTitle.jsx | 26 +- .../src/components/Plot/components/YLabel.jsx | 30 +- 6 files changed, 207 insertions(+), 218 deletions(-) diff --git a/packages/sections/src/study/GWASCredibleSets/ManhattanPlot.tsx b/packages/sections/src/study/GWASCredibleSets/ManhattanPlot.tsx index 44546b99b..e0a77a814 100644 --- a/packages/sections/src/study/GWASCredibleSets/ManhattanPlot.tsx +++ b/packages/sections/src/study/GWASCredibleSets/ManhattanPlot.tsx @@ -19,7 +19,7 @@ import { HTMLTooltipTable, HTMLTooltipRow, } from "ui"; -import { scaleLinear, scaleLog, min } from "d3"; +import { scaleLinear, min } from "d3"; import { ScientificNotation } from "ui"; import { naLabel } from "../../constants"; @@ -42,15 +42,24 @@ export default function ManhattanPlot({ loading, data }) { d.variant != null; }); if (data.length === 0) return null; + // eslint-disable-next-line + data = structuredClone(data); + data.forEach(d => { + d._y = Math.log10(d.pValueMantissa) + d.pValueExponent; + }); - const pValueMin = min(data, pValue); - const pValueMax = 1; + const yMin = min(data, d => d._y); + const yMax = 0; const genomePositions = {}; data.forEach(({ variant }) => { genomePositions[variant.id] = cumulativePosition(variant); }); + const xScale = scaleLinear().domain([0, genomeLength]); + const yScale = scaleLinear().domain([yMin, yMax]).nice(); // ensure min scale value <= yMin + yScale.domain([yScale.domain()[0], yMax]); // ensure max scale value is yMax - in case nice changed it + return ( @@ -63,13 +72,9 @@ export default function ManhattanPlot({ loading, data }) { fontFamily={fontFamily} data={data} yReverse - scales={{ - x: scaleLinear().domain([0, genomeLength]), - y: scaleLog().domain([pValueMin, pValueMax]), - }} + scales={{ x: xScale, y: yScale }} xTick={chromosomeInfo} > - [0, ...tickData.map(chromo => chromo.end)]} tickLength={15} @@ -91,13 +96,13 @@ export default function ManhattanPlot({ loading, data }) { - -Math.log10(v)} /> + Math.abs(v)} /> genomePositions[d.variant.id]} xx={d => genomePositions[d.variant.id]} - y={pValue} - yy={pValueMax} + y={d => d._y} + yy={yMax} stroke={markColor} strokeWidth={1} strokeOpacity={0.7} @@ -105,7 +110,7 @@ export default function ManhattanPlot({ loading, data }) { /> genomePositions[d.variant.id]} - y={pValue} + y={d => d._y} fill={background} stroke={markColor} strokeWidth={1.2} @@ -119,8 +124,8 @@ export default function ManhattanPlot({ loading, data }) { x={0} xx={genomeLength} dxx={8} - y={pValueMin} - yy={pValueMax} + y={yMin} + yy={yMax} dy={-8} dyy={0} fill={background} @@ -130,8 +135,8 @@ export default function ManhattanPlot({ loading, data }) { dataFrom="hover" x={d => genomePositions[d.variant.id]} xx={d => genomePositions[d.variant.id]} - y={pValue} - yy={pValueMax} + y={d => d._y} + yy={yMax} stroke={markColor} strokeWidth={1.7} strokeOpacity={1} @@ -139,13 +144,13 @@ export default function ManhattanPlot({ loading, data }) { genomePositions[d.variant.id]} - y={pValue} + y={d => d._y} fill={markColor} area={circleArea} /> genomePositions[d.variant.id]} - y={pValue} + y={d => d._y} pxWidth={290} pxHeight={200} content={tooltipContent} @@ -209,13 +214,6 @@ function tooltipContent(data) { ); } -function pValue(row) { - return Math.max( - row.pValueMantissa * 10 ** row.pValueExponent, - Number.MIN_VALUE - ); -} - // from: https://www.ncbi.nlm.nih.gov/grc/human/data // (first tab: "Chromosome lengths") const chromosomeInfo = [ diff --git a/packages/sections/src/variant/GWASCredibleSets/Body.tsx b/packages/sections/src/variant/GWASCredibleSets/Body.tsx index 84babb7e7..459a1f4a3 100644 --- a/packages/sections/src/variant/GWASCredibleSets/Body.tsx +++ b/packages/sections/src/variant/GWASCredibleSets/Body.tsx @@ -281,6 +281,8 @@ function Body({ id, entity }: BodyProps) { loading={request.loading} data={request.data?.variant.gwasCredibleSets.rows} id={id} + referenceAllele={request.data?.variant.referenceAllele} + alternateAllele={request.data?.variant.alternateAllele} /> ); }} diff --git a/packages/sections/src/variant/GWASCredibleSets/PheWasPlot.tsx b/packages/sections/src/variant/GWASCredibleSets/PheWasPlot.tsx index d97fcea9b..478737bc9 100644 --- a/packages/sections/src/variant/GWASCredibleSets/PheWasPlot.tsx +++ b/packages/sections/src/variant/GWASCredibleSets/PheWasPlot.tsx @@ -1,4 +1,4 @@ -import { Box, Skeleton, useTheme } from "@mui/material"; +import { Box, Skeleton, Typography, useTheme } from "@mui/material"; import { faArrowRightToBracket } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { @@ -10,7 +10,6 @@ import { Plot, Vis, YAxis, - XTick, YTick, XLabel, YLabel, @@ -22,17 +21,16 @@ import { HTMLTooltipTable, HTMLTooltipRow, } from "ui"; -import { scaleLinear, scaleLog, min, scaleOrdinal, schemeCategory10, schemeDark2 } from "d3"; +import { scaleLinear, min, scaleOrdinal } from "d3"; import { ScientificNotation } from "ui"; import { naLabel, credsetConfidenceMap } from "../../constants"; import { Fragment } from "react/jsx-runtime"; -export default function PheWasPlot({ loading, data, id }) { +export default function PheWasPlot({ loading, data, id, referenceAllele, alternateAllele }) { - const plotHeight = 440; + const plotHeight = 450; const theme = useTheme(); const background = theme.palette.background.paper; - // const markColor = theme.palette.primary.main; const fontFamily = theme.typography.fontFamily; const pointArea = 64; @@ -50,9 +48,6 @@ export default function PheWasPlot({ loading, data, id }) { '#188E61', '#BEE952', ]; - // const palette = schemeCategory10; - // const palette = schemeDark2; - // const palette = schemeSet1; if (loading) return ; if (data == null) return null; @@ -63,11 +58,15 @@ export default function PheWasPlot({ loading, data, id }) { d.pValueExponent != null && d.variant != null; }); - if (data.length === 0) return null; + // eslint-disable-next-line + data = structuredClone(data); + data.forEach(d => { + d._y = Math.log10(d.pValueMantissa) + d.pValueExponent; + }); - const pValueMin = min(data, pValue); - const pValueMax = 1; + const yMin = min(data, d => d._y); + const yMax = 0; const rowLookup = new Map(); // derived values for each row const diseaseGroups = new Map(); @@ -84,26 +83,20 @@ export default function PheWasPlot({ loading, data, id }) { .sort((a, b) => a[1].name.localeCompare(b[1].name)) .map(a => a[0]); if (diseaseGroups.has('__uncategorised__')) { - sortedDiseaseIds = sortedDiseaseIds.filter(id => id === '__uncategorised__'); - sortedDiseaseIds.shift('__uncategorised__'); + sortedDiseaseIds = sortedDiseaseIds.filter(id => id !== '__uncategorised__'); + sortedDiseaseIds.push('__uncategorised__'); } const xIntervals = new Map(); - // const xMidpoints = []; let xCumu = 0; - // const xGap = Math.ceil(data.length / 100); // gap between groups - // const xPad = Math.ceil(data.length / 100); // padding at ede of groups const xGap = data.length / 300; // gap between groups - // const xGap = 0; const xPad = data.length / 500; // padding at ede of groups const sortedData = []; - // const sortedDiseaseNames = []; for (const id of sortedDiseaseIds) { - const { name, data: newRows } = diseaseGroups.get(id); - // sortedDiseaseNames.push(name); + const { data: newRows } = diseaseGroups.get(id); xCumu += xGap; xIntervals.set(id, { start: xCumu }); xCumu += xPad; - newRows.sort((row1, row2) => pValue(row1) - pValue(row2)); + newRows.sort((row1, row2) => row1._y - row2._y); for (const row of newRows) { rowLookup.get(row).x = xCumu + 0.5; xCumu += 1; @@ -113,15 +106,9 @@ export default function PheWasPlot({ loading, data, id }) { xIntervals.get(id).end = xCumu; } - function xAnchor(row) { - const x = rowLookup.get(row).x; - return x < xCumu / 2 ? 'left' : 'right'; - } - - function yAnchor(row) { - const y = pValue(row); - return Math.log10(y) > Math.log10(pValueMin) / 2 ? 'bottom' : 'top'; - } + const xScale = scaleLinear().domain([0, xCumu]); + const yScale = scaleLinear().domain([yMin, yMax]).nice(); // ensure min scale value <= yMin + yScale.domain([yScale.domain()[0], yMax]); // ensure max scale value is yMax - in case nice changed it const colorDomain = ['background']; const colorRange = [background]; @@ -133,7 +120,7 @@ export default function PheWasPlot({ loading, data, id }) { const pointAttrs = { x: d => rowLookup.get(d).x, - y: pValue, + y: d => d._y, fill: d => { return d.variant.id === id ? rowLookup.get(d).therapeuticAreaId : 'background'; }, @@ -146,101 +133,114 @@ export default function PheWasPlot({ loading, data, id }) { } return ( - + <> + + {/* legend */} + + Beta > 0   + Beta < 0   + Beta {naLabel}   + Filled symbol:{" "} + + + + {" "}is lead variant + + + {/* plot */} + - - {/* */} - {/* need to use different XLabel elements to use different colors */} + - {[...xIntervals].map(([id, { start, end }]) => ( - - {/* */} - diseaseGroups.get(id).name} - padding={3} - textAnchor="start" - dx={-2} - style={{ - transformOrigin: '0% 50%', - transformBox: 'fill-box', - transform: "rotate(45deg)", - }} - fill={colorScale(id)} - /> - - ))} - d[1].start} - xx={d => d[1].end} - y={pValueMax} - yy={pValueMax} - stroke={d => d[0]} - strokeWidth={1} - /> - {/* */} - - -log - 10 - (pValue) - - - - -Math.log10(v)} /> - + {[...xIntervals].map(([id, { start, end }]) => ( + + diseaseGroups.get(id).name} + padding={3} + textAnchor="start" + dx={-2} + style={{ + transformOrigin: '0% 50%', + transformBox: 'fill-box', + transform: "rotate(45deg)", + }} + fill={colorScale(id)} + /> + + ))} + d[1].start} + xx={d => d[1].end} + y={yMax} + yy={yMax} + stroke={d => d[0]} + strokeWidth={1} + /> + + -log + 10 + (pValue) + + + + Math.abs(v)} /> + - {/* on hover */} - - - rowLookup.get(d).x} - y={d => pValueMin} - // y={pValue} - pxWidth={360} - pxHeight={350} - content={tooltipContent} - xOffset={40} - yOffset={-20} - /> + {/* on hover */} + + + rowLookup.get(d).x} + y={d => yMin} + pxWidth={360} + pxHeight={350} + content={tooltipContent} + xOffset={40} + yOffset={-20} + /> - {/* axes at end so fade rectangle doesn't cover them */} - {/* */} - + {/* axes at end so fade rectangle doesn't cover them */} + {/* */} + - - + + + + ); } @@ -346,36 +346,29 @@ function tooltipContent(data) { ); } -function pValue(row) { - return Math.max( - row.pValueMantissa * 10 ** row.pValueExponent, - Number.MIN_VALUE - ); -} - const therapeuticPriorities = { - MONDO_0045024: { name: "cell proliferation disorder", rank: 1 }, + MONDO_0045024: { name: "cell proliferation", rank: 1 }, EFO_0005741: { name: "infectious disease", rank: 2 }, - OTAR_0000014: { name: "pregnancy or perinatal disease", rank: 3 }, + OTAR_0000014: { name: "pregnancy or perinatal", rank: 3 }, EFO_0005932: { name: "animal disease", rank: 4 }, - MONDO_0024458: { name: "disease of visual system", rank: 5 }, - EFO_0000319: { name: "cardiovascular disease", rank: 6 }, - EFO_0009605: { name: "pancreas disease", rank: 7 }, - EFO_0010282: { name: "gastrointestinal disease", rank: 8 }, - OTAR_0000017: { name: "reproductive system or breast disease", rank: 9 }, - EFO_0010285: { name: "integumentary system disease", rank: 10 }, - EFO_0001379: { name: "endocrine system disease", rank: 11 }, - OTAR_0000010: { name: "respiratory or thoracic disease", rank: 12 }, - EFO_0009690: { name: "urinary system disease", rank: 13 }, - OTAR_0000006: { name: "musculoskeletal or connective tissue disease", rank: 14 }, + MONDO_0024458: { name: "visual system", rank: 5 }, + EFO_0000319: { name: "cardiovascular", rank: 6 }, + EFO_0009605: { name: "pancreas", rank: 7 }, + EFO_0010282: { name: "gastrointestinal", rank: 8 }, + OTAR_0000017: { name: "reproductive system or breast", rank: 9 }, + EFO_0010285: { name: "integumentary system", rank: 10 }, + EFO_0001379: { name: "endocrine system", rank: 11 }, + OTAR_0000010: { name: "respiratory or thoracic", rank: 12 }, + EFO_0009690: { name: "urinary system", rank: 13 }, + OTAR_0000006: { name: "musculoskeletal or connective ...", rank: 14 }, MONDO_0021205: { name: "disease of ear", rank: 15 }, - EFO_0000540: { name: "immune system disease", rank: 16 }, - EFO_0005803: { name: "hematologic disease", rank: 17 }, - EFO_0000618: { name: "nervous system disease", rank: 18 }, - MONDO_0002025: { name: "psychiatric disorder", rank: 19 }, - OTAR_0000020: { name: "nutritional or metabolic disease", rank: 20 }, - OTAR_0000018: { name: "genetic, familial or congenital disease", rank: 21 }, - OTAR_0000009: { name: "injury, poisoning or other complication", rank: 22 }, + EFO_0000540: { name: "immune system", rank: 16 }, + EFO_0005803: { name: "hematologic", rank: 17 }, + EFO_0000618: { name: "nervous system", rank: 18 }, + MONDO_0002025: { name: "psychiatric", rank: 19 }, + OTAR_0000020: { name: "nutritional or metabolic", rank: 20 }, + OTAR_0000018: { name: "genetic, familial or congenital", rank: 21 }, + OTAR_0000009: { name: "injury, poisoning or complication", rank: 22 }, EFO_0000651: { name: "phenotype", rank: 23 }, EFO_0001444: { name: "measurement", rank: 24 }, GO_0008150: { name: "biological process", rank: 25 }, diff --git a/packages/ui/src/components/Plot/components/XLabel.jsx b/packages/ui/src/components/Plot/components/XLabel.jsx index 88acc0058..587a71082 100644 --- a/packages/ui/src/components/Plot/components/XLabel.jsx +++ b/packages/ui/src/components/Plot/components/XLabel.jsx @@ -4,14 +4,14 @@ import { fromFrameOrPlot } from "../util/fromFrameOrPlot"; import { finalData } from "../util/finalData"; export default function XLabel({ - values, - position = 'bottom', - padding, - dx = 0, - dy = 0, - format, - ...textAttrs - }) { + values, + position = 'bottom', + padding, + dx = 0, + dy = 0, + format, + ...textAttrs +}) { const plot = usePlot(); if (!plot) { @@ -31,15 +31,13 @@ export default function XLabel({ ? v => plot.panelWidth - ops.scales.x(v) : ops.scales.x; - const leftOrigin = `translate(${ - plot.padding.left + dx},${ - position === 'top' - ? plot.padding.top - padding + dy - : plot.height - plot.padding.bottom + padding + dy - })`; + const leftOrigin = `translate(${plot.padding.left + dx},${position === 'top' + ? plot.padding.top - padding + dy + : plot.height - plot.padding.bottom + padding + dy + })`; return ( - + {tickValues.map((v, i) => { return ( {format ? format(v, i, tickValues, ops.xTick) : v} diff --git a/packages/ui/src/components/Plot/components/XTitle.jsx b/packages/ui/src/components/Plot/components/XTitle.jsx index 018f1c783..22ec7f83b 100644 --- a/packages/ui/src/components/Plot/components/XTitle.jsx +++ b/packages/ui/src/components/Plot/components/XTitle.jsx @@ -1,15 +1,15 @@ import { usePlot } from "../contexts/PlotContext"; export default function XTitle({ - children, - position = 'bottom', - align = 'center', // 'left', 'center' or 'right' - padding, - dx = 0, - dy = 0, - ...textAttrs // be very careful if change the transform-related CSS props - // used in the element - }) { + children, + position = 'bottom', + align = 'center', // 'left', 'center' or 'right' + padding, + dx = 0, + dy = 0, + ...textAttrs // be very careful if change the transform-related CSS props + // used in the element +}) { const plot = usePlot(); if (!plot) { @@ -34,13 +34,13 @@ export default function XTitle({ } x += dx; - let y, alignmentBaseline; + let y, dominantBaseline; if (position === 'top') { y = plot.padding.top - padding; - alignmentBaseline = 'baseline'; + dominantBaseline = 'baseline'; } else { y = plot.height - plot.padding.bottom + padding; - alignmentBaseline = 'hanging'; + dominantBaseline = 'hanging'; } y += dy; @@ -54,7 +54,7 @@ export default function XTitle({ fontStyle={plot.fontStyle} fontWeight={plot.fontWeight} textAnchor={textAnchor} - alignmentBaseline={alignmentBaseline} + dominantBaseline={dominantBaseline} {...textAttrs} > {children} diff --git a/packages/ui/src/components/Plot/components/YLabel.jsx b/packages/ui/src/components/Plot/components/YLabel.jsx index 73f8e4e23..2e348213a 100644 --- a/packages/ui/src/components/Plot/components/YLabel.jsx +++ b/packages/ui/src/components/Plot/components/YLabel.jsx @@ -4,14 +4,14 @@ import { fromFrameOrPlot } from "../util/fromFrameOrPlot"; import { finalData } from "../util/finalData"; export default function YLabel({ - values, - position = 'left', - padding, - dx = 0, - dy = 0, - format, - ...textAttrs - }) { + values, + position = 'left', + padding, + dx = 0, + dy = 0, + format, + ...textAttrs +}) { const plot = usePlot(); if (!plot) { @@ -31,15 +31,13 @@ export default function YLabel({ ? ops.scales.y : v => plot.panelHeight - ops.scales.y(v); - const topOrigin = `translate(${ - position === 'right' - ? plot.width - plot.padding.right + padding + dx - : plot.padding.left - padding + dx},${ - plot.padding.top + dy - })`; + const topOrigin = `translate(${position === 'right' + ? plot.width - plot.padding.right + padding + dx + : plot.padding.left - padding + dx},${plot.padding.top + dy + })`; return ( - + {tickValues.map((v, i) => { return ( {format ? format(v, i, tickValues, ops.yTick) : v}