Skip to content

Commit

Permalink
Merge pull request #3827 from broadinstitute/gtex-refactor
Browse files Browse the repository at this point in the history
Gtex refactor
  • Loading branch information
hanars authored Jan 22, 2024
2 parents 0357491 + 589e85a commit 5601daa
Show file tree
Hide file tree
Showing 8 changed files with 617 additions and 593 deletions.
526 changes: 135 additions & 391 deletions ui/package-lock.json

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,19 +71,19 @@
},
"dependencies": {
"@tommoor/remove-markdown": "^0.3.2",
"d3-array": "^1.2.4",
"d3-axis": "^1.0.12",
"d3-array": "^3.2.4",
"d3-axis": "^3.0.0",
"d3-drag": "^1.2.5",
"d3-hierarchy": "^1.1.9",
"d3-scale": "^2.2.2",
"d3-random": "^3.0.1",
"d3-scale": "^3.3.0",
"d3-selection": "^1.4.2",
"d3-shape": "^1.3.7",
"d3-shape": "^3.2.0",
"d3-zoom": "^1.8.3",
"draft-js": "^0.11.7",
"final-form": "^4.20.6",
"final-form-arrays": "^3.0.2",
"final-form-calculate": "^1.3.2",
"gtex-d3": "github:broadinstitute/gtex-viz#8d65862",
"igv": "^2.10.5",
"jquery": "^3.6.0",
"js-cookie": "^2.2.1",
Expand Down Expand Up @@ -149,7 +149,7 @@
"moduleNameMapper": {
"igv": "<rootDir>/config/jest/fileTransform.js",
"jsoneditor-react": "<rootDir>/config/jest/fileTransform.js",
"gtex-d3": "<rootDir>/config/jest/fileTransform.js",
"d3": "<rootDir>/config/jest/fileTransform.js",
"pedigreejs": "<rootDir>/config/jest/fileTransform.js",
"^react-native$": "react-native-web"
},
Expand Down
47 changes: 11 additions & 36 deletions ui/pages/Project/components/RnaSeqOutliers.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import PropTypes from 'prop-types'
import { Header } from 'semantic-ui-react'

import { extent } from 'd3-array'
import { axisBottom, axisLeft } from 'd3-axis'
import { scaleLinear, scaleLog, scalePow } from 'd3-scale'
import { select } from 'd3-selection'

import { GeneSearchLink } from 'shared/components/buttons/SearchResultsLink'
import { initializeD3 } from 'shared/components/graph/d3Utils'

const GRAPH_HEIGHT = 400
const GRAPH_WIDTH = 600
Expand All @@ -27,45 +27,24 @@ class RnaSeqOutliersGraph extends React.PureComponent {
componentDidUpdate(prevProp) {
const { data } = this.props
if (data !== prevProp.data) {
select(this.svg).selectAll('*').remove()
select(this.container).selectAll('*').remove()
this.initPlot()
}
}

initPlot = () => {
const { data: dataArray, genesById } = this.props

const svg = select(this.svg).append('g')
.attr('transform', `translate(${GRAPH_MARGIN.left},${GRAPH_MARGIN.top})`)

const x = scaleLinear().domain(extent(dataArray.map(d => d.zScore))).range([0, GRAPH_WIDTH])
const y = scaleLog().domain(extent(dataArray.map(d => d.pValue))).range([0, GRAPH_HEIGHT])
const r = scalePow().exponent(4).domain(extent(dataArray.map(d => Math.abs(d.deltaPsi)))).range([1, 10])

// x-axis
svg.append('g')
.attr('transform', `translate(0,${GRAPH_HEIGHT + 5})`)
.call(axisBottom(x).tickSizeOuter(0))

// y-axis
svg.append('g')
.attr('transform', 'translate(-10,0)')
.call(axisLeft(y).tickSizeOuter(0).ticks(5, val => -Math.log10(val)))

// x-axis label
svg.append('text')
.attr('text-anchor', 'end')
.attr('y', GRAPH_HEIGHT + GRAPH_MARGIN.bottom)
.attr('x', GRAPH_WIDTH / 2)
.text('Z-score')

// y-axis label
svg.append('text')
.attr('text-anchor', 'end')
.attr('transform', 'rotate(-90)')
.attr('y', 10 - GRAPH_MARGIN.left)
.attr('x', GRAPH_MARGIN.bottom - (GRAPH_HEIGHT / 2))
.text('-log(P-value)')
const svg = initializeD3(
select(this.container), { width: GRAPH_WIDTH, height: GRAPH_HEIGHT }, GRAPH_MARGIN, { x, y }, {
x: { text: 'Z-score', transform: xAxis => xAxis.tickSizeOuter(0) },
y: { text: '-log(P-value)', transform: yAxis => yAxis.tickSizeOuter(0).ticks(5, val => -Math.log10(val)) },
},
)

// scatterplot
const dataPoints = svg.append('g').selectAll('dot').data(dataArray).enter()
Expand All @@ -90,17 +69,13 @@ class RnaSeqOutliersGraph extends React.PureComponent {
.style('font-weight', 'bold')
}

setSvgElement = (element) => {
this.svg = element
setContainerElement = (element) => {
this.container = element
}

render() {
return (
<svg
ref={this.setSvgElement}
width={GRAPH_WIDTH + GRAPH_MARGIN.left + GRAPH_MARGIN.right}
height={GRAPH_HEIGHT + GRAPH_MARGIN.top + GRAPH_MARGIN.bottom}
/>
<div ref={this.setContainerElement} />
)
}

Expand Down
250 changes: 148 additions & 102 deletions ui/shared/components/graph/Gtex.jsx
Original file line number Diff line number Diff line change
@@ -1,95 +1,12 @@
import React from 'react'
import styled from 'styled-components'
import { launch } from 'gtex-d3/src/GeneExpressionViolinPlot'
import 'gtex-d3/css/violin.css'

import GtexLauncher, { GTEX_HOST } from './GtexLauncher'

const GtexContainer = styled.div`
#gene-expression-plot-toolbar {
margin-left: 100px;
margin-top: 20px;
.gene-expression-plot-option-label {
padding-right: 5px;
font-size: 15px;
font-variant: all-small-caps;
font-weight: 500;
}
.col-lg-1 {
display: none !important;
}
.col-lg-2 {
width: 20%;
float: left;
}
.col-lg-11 {
width: 100%;
}
.btn-group {
display: inline-block;
vertical-align: middle;
.btn {
padding: 5px 10px;
font-size: 12px;
line-height: 1.5;
border-radius: 3px;
cursor: pointer;
user-select: none;
border: 1px solid #ccc;
&:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
&.active, &:active, &:focus, &:hover {
background-color: #e6e6e6;
border-color: #adadad;
box-shadow: inset 0 3px 5px rgba(0,0,0,.125);
}
&+.btn {
margin-left: -1px;
}
}
}
.fa {
font-family: 'Icons';
font-style: normal;
&.fa-caret-up:before {
content: "\\f0d8";
}
&.fa-caret-down:before {
content: "\\f0d7";
}
}
}
#gene-expression-plot-svg {
.violin-x-axis, .violin-y-axis {
line, path {
stroke: Black;
}
}
.violin-x-axis text, .violin-y-axis text, text.violin-axis-label {
fill: Black;
font-size: 11.5px;
font-weight: 500;
}
}
`
import { deviation, extent, max, mean, median, min, quantile } from 'd3-array'
import { randomNormal } from 'd3-random'
import { scaleBand, scaleLinear } from 'd3-scale'
import { area } from 'd3-shape'

import { compareObjects } from 'shared/utils/sortUtils'
import { initializeD3, Tooltip } from './d3Utils'
import GtexLauncher, { queryGtex } from './GtexLauncher'

const MARGINS = {
top: 10,
Expand All @@ -98,23 +15,152 @@ const MARGINS = {
left: 50,
}
const DIMENSIONS = {
w: window.innerWidth * 0.8,
h: 400,
width: window.innerWidth * 0.8,
height: 400,
}

// Code adapted from https://github.com/broadinstitute/gtex-viz/blob/8d65862fbe7e5ab9b4d5be419568754e0d17bb07/src/GeneExpressionViolinPlot.js

const drawViolin = (svg, scale, tooltip) => (entry) => {
const violinG = svg.append('g')
.datum(entry)

const eDomain = extent(entry.values) // get the max and min in entry.values
const q1 = quantile(entry.values, 0.25)
const q3 = quantile(entry.values, 0.75)

// get the vertices
const rangeDeviation = (q3 - q1) / 1.34
const kernelBandwidth = 1.06 * Math.min(deviation(entry.values), rangeDeviation) * (entry.values.length ** -0.2)
// use up to 100 vertices along the Y axis (to create the violin path)
const vertices = scale.y.ticks(100).map(x => [x, mean(entry.values, (v) => {
const u = (x - v) / kernelBandwidth
return (1 / Math.sqrt(2 * Math.PI)) * Math.exp(-0.5 * u * u)
}) / kernelBandwidth]).filter(
// filter the vertices that aren't in the entry.values
d => d[0] >= eDomain[0] && d[0] <= eDomain[1],
)

if (vertices.length < 1 || vertices.some(Number.isNaN)) {
return
}

// define the z scale -- the violin width
const zMax = max(vertices, d => Math.abs(d[1]))
scale.z
.domain([-zMax, zMax])
.range([scale.x(entry.label), scale.x(entry.label) + scale.x.bandwidth()])

// visual rendering
const violin = area()
.x0(d => scale.z(d[1]))
.x1(d => scale.z(-d[1]))
.y(d => scale.y(d[0]))

const vPath = violinG.append('path')
.datum(vertices)
.attr('d', violin)
.style('fill', () => entry.color)
.style('opacity', 0.6)

// boxplot
const z = scale.z.domain()[1] / 3
const x = scale.z(-z)

// interquartile range
violinG.append('rect')
.attr('x', x)
.attr('y', scale.y(q3))
.attr('width', Math.abs(x - scale.z(z)))
.attr('height', Math.abs(scale.y(q3) - scale.y(q1)))
.style('fill', '#555f66')

// the median line
const medianY = scale.y(entry.median)
violinG.append('line')
.attr('x1', x)
.attr('x2', scale.z(z))
.attr('y1', medianY)
.attr('y2', medianY)
.style('stroke', '#fff')
.style('stroke-width', '2px')

const jitter = randomNormal(0, z / 2)
const iqr = Math.abs(q3 - q1)
const upper = max(entry.values.filter(d => d <= q3 + (iqr * 1.5)))
const lower = min(entry.values.filter(d => d >= q1 - (iqr * 1.5)))
const outliers = entry.values.filter(d => d < lower || d > upper)
violinG.append('g')
.selectAll('circle')
.data(outliers)
.enter()
.append('circle')
.attr('cx', () => scale.z(jitter()))
.attr('cy', d => scale.y(d))
.attr('fill', entry.color)
.attr('r', 1)

// mouse events
violinG.on('mouseover', () => {
vPath.style('opacity', 1)
tooltip.show(
`${entry.label}<br/>Sample size: ${entry.values.length}<br/>Median TPM: ${entry.median.toPrecision(4)}<br/>`,
x + 70,
medianY < 40 ? 10 : medianY - 40,
)
})
violinG.on('mouseout', () => {
vPath.style('opacity', 0.6)
})
}

const URLS = {
geneExp: `${GTEX_HOST}expression/geneExpression?gencodeId=`,
tissue: `${GTEX_HOST}dataset/tissueInfo?format=json`,
const renderViolinPlot = (violinPlotData, containerElement) => {
const xDomain = violinPlotData.map(({ label }) => label)
const yDomain = extent(violinPlotData.reduce((acc, { values }) => ([...acc, ...values]), []))

const scale = {
x: scaleBand()
.rangeRound([0, DIMENSIONS.width])
.domain(xDomain)
.paddingInner(0.2),
y: scaleLinear()
.rangeRound([DIMENSIONS.height, 0])
.domain(yDomain),
z: scaleLinear(), // the violin width, domain and range are determined later individually for each violin
}

const svg = initializeD3(containerElement, DIMENSIONS, MARGINS, scale, {
y: {
text: 'TPM',
transform: yAxis => yAxis.tickValues(scale.y.ticks(5)),
},
})

const tooltip = new Tooltip(containerElement)
violinPlotData.forEach(drawViolin(svg, scale, tooltip))
}

const GTEX_CONTAINER_ID = 'gene-expression-plot'
// seqr-specific code

const loadTissueData = onSuccess => queryGtex('dataset/tissueSiteDetail', {}, onSuccess)

const launchGtex = (geneId) => {
launch(GTEX_CONTAINER_ID, `${GTEX_CONTAINER_ID}-tooltip`, geneId, '', URLS, MARGINS, DIMENSIONS)
const renderGtex = (expressionData, tissueData, containerElement) => {
if ((expressionData?.data || []).length < 1) {
return
}
const tissueLookup = tissueData.data.reduce(
(acc, { tissueSiteDetailId, ...data }) => ({ ...acc, [tissueSiteDetailId]: data }), {},
)
const violinPlotData = expressionData.data.map(({ tissueSiteDetailId, data }) => ({
values: data.sort(),
median: median(data),
label: tissueLookup[tissueSiteDetailId]?.tissueSiteDetail,
color: `#${tissueLookup[tissueSiteDetailId]?.colorHex}`,
}))
violinPlotData.sort(compareObjects('label'))
renderViolinPlot(violinPlotData, containerElement)
}

export default props => (
<GtexContainer>
<GtexLauncher containerId={GTEX_CONTAINER_ID} launchGtex={launchGtex} {...props} />
</GtexContainer>
<GtexLauncher renderGtex={renderGtex} fetchAdditionalData={loadTissueData} {...props} />
)
Loading

0 comments on commit 5601daa

Please sign in to comment.