Skip to content

Commit

Permalink
Generalize RegionalMissenseConstraintTrack
Browse files Browse the repository at this point in the history
Here we extract a generic `ConstraintTrack` component from `RegionalMissenseConstraintTrack` so that we can build a mitochondrial constraint track on the new generic component.
  • Loading branch information
phildarnowsky-broad committed May 3, 2024
1 parent 05b1b54 commit 5d880cc
Show file tree
Hide file tree
Showing 7 changed files with 344 additions and 281 deletions.
87 changes: 87 additions & 0 deletions browser/src/ConstraintTrack.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { regionsInExons, GenericRegion } from './ConstraintTrack'
import { exonFactory } from './__factories__/Gene'
import { Exon } from './TranscriptPage/TranscriptPage'

test('regionsInExons', () => {
const testCases = [
{
regions: [
{ start: 2, stop: 4, i: 1 },
{ start: 6, stop: 8, i: 2 },
],
exons: [exonFactory.build({ start: 2, stop: 6 }), exonFactory.build({ start: 6, stop: 9 })],
expected: [
{ start: 2, stop: 4, i: 1, unclamped_start: 2, unclamped_stop: 4 },
{ start: 6, stop: 8, i: 2, unclamped_start: 6, unclamped_stop: 8 },
],
},
{
regions: [
{ start: 2, stop: 4, i: 1 },
{ start: 6, stop: 8, i: 2 },
],
exons: [exonFactory.build({ start: 1, stop: 8 })],
expected: [
{ start: 2, stop: 4, i: 1, unclamped_start: 2, unclamped_stop: 4 },
{ start: 6, stop: 8, i: 2, unclamped_start: 6, unclamped_stop: 8 },
],
},
{
regions: [
{ start: 2, stop: 4, i: 1 },
{ start: 6, stop: 8, i: 2 },
],
exons: [
exonFactory.build({ start: 1, stop: 3 }),
exonFactory.build({ start: 3, stop: 5 }),
exonFactory.build({ start: 5, stop: 9 }),
],
expected: [
{ start: 2, stop: 3, i: 1, unclamped_start: 2, unclamped_stop: 4 },
{ start: 3, stop: 4, i: 1, unclamped_start: 2, unclamped_stop: 4 },
{ start: 6, stop: 8, i: 2, unclamped_start: 6, unclamped_stop: 8 },
],
},
{
regions: [
{ start: 2, stop: 4, misc_field_1: 2, misc_field_2: 4 },
{ start: 6, stop: 8, misc_field_1: 6, misc_field_2: 8 },
],
exons: [
exonFactory.build({ start: 1, stop: 3 }),
exonFactory.build({ start: 3, stop: 5 }),
exonFactory.build({ start: 5, stop: 9 }),
],
expected: [
{
start: 2,
stop: 3,
misc_field_1: 2,
misc_field_2: 4,
unclamped_start: 2,
unclamped_stop: 4,
},
{
start: 3,
stop: 4,
misc_field_1: 2,
misc_field_2: 4,
unclamped_start: 2,
unclamped_stop: 4,
},
{
start: 6,
stop: 8,
misc_field_1: 6,
misc_field_2: 8,
unclamped_start: 6,
unclamped_stop: 8,
},
],
},
]

testCases.forEach(({ regions, exons, expected }) => {
expect(regionsInExons(regions as GenericRegion[], exons as Exon[])).toEqual(expected)
})
})
210 changes: 210 additions & 0 deletions browser/src/ConstraintTrack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import React, { ReactNode } from 'react'
import styled from 'styled-components'
// @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module '@gno... Remove this comment to see the full error message
import { Track } from '@gnomad/region-viewer'
import InfoButton from './help/InfoButton'
import { TooltipAnchor } from '@gnomad/ui'
import { Exon } from './TranscriptPage/TranscriptPage'

export const PlotWrapper = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
`

type TrackProps = {
scalePosition: (input: number) => number
width: number
}

const Wrapper = styled.div`
display: flex;
flex-direction: column;
margin-bottom: 1em;
`

export const SidePanel = styled.div`
display: flex;
align-items: center;
height: 100%;
`

const TopPanel = styled.div`
display: flex;
justify-content: flex-end;
width: 100%;
margin-bottom: 5px;
`

export interface GenericRegion {
start: number
stop: number
}

// When clamping constrained regions to exons, we remember the original
// boundaries of the region for display in the region tooltip
export type RegionWithUnclamped<R extends GenericRegion> = R & {
unclamped_start: number
unclamped_stop: number
}

type Props<R extends GenericRegion> = {
trackTitle: string
allRegions: R[] | null
constrainedRegions: RegionWithUnclamped<R>[]
infobuttonTopic: string
legend: ReactNode
tooltipComponent: any // TK any
colorFn: (region: R) => string
valueFn: (region: R) => string
}

export const regionsInExons = <R extends GenericRegion>(
regions: R[],
exons: Exon[]
): RegionWithUnclamped<R>[] => {
const sortedRegions = regions.sort((a, b) => a.start - b.start)
const sortedExons = exons.sort((a, b) => a.start - b.start)

const intersections = []

let regionIndex = 0
let exonIndex = 0

while (regionIndex < regions.length && exonIndex < exons.length) {
const region = sortedRegions[regionIndex]
const exon = sortedExons[exonIndex]
const maxStart = Math.max(region.start, exon.start)
const minStop = Math.min(region.stop, exon.stop)

if (maxStart < minStop) {
const next: RegionWithUnclamped<R> = {
...region,
start: maxStart,
stop: minStop,
unclamped_start: region.start,
unclamped_stop: region.stop,
}
intersections.push(next)
}

if (region.stop === minStop) {
regionIndex += 1
}
if (exon.stop === minStop) {
exonIndex += 1
}
}
return intersections
}

const ConstraintTrack = <R extends GenericRegion>({
trackTitle,
allRegions,
constrainedRegions,
infobuttonTopic,
legend,
tooltipComponent,
colorFn,
valueFn,
}: Props<R>) => (
<Wrapper>
<Track
renderLeftPanel={() => (
<SidePanel>
<span>{trackTitle}</span>
<InfoButton topic={infobuttonTopic} />
</SidePanel>
)}
>
{({ scalePosition, width }: TrackProps) => (
<>
<TopPanel>{legend}</TopPanel>
<PlotWrapper>
<svg height={55} width={width}>
{constrainedRegions.map((region: RegionWithUnclamped<R>) => {
const startX = scalePosition(region.start)
const stopX = scalePosition(region.stop)
const regionWidth = stopX - startX

return (
<TooltipAnchor
key={`${region.start}-${region.stop}`}
// @ts-expect-error need to redefine TooltipAnchor to allow arbitrary props for the children type-safely
region={region}
isTranscript={allRegions && allRegions.length === 1}
tooltipComponent={tooltipComponent}
>
<g>
<rect
x={startX}
y={0}
width={regionWidth}
height={15}
fill={colorFn(region)}
stroke="black"
/>
</g>
</TooltipAnchor>
)
})}
<g transform="translate(0,20)">
{allRegions &&
allRegions.map((region: R, index: number) => {
const startX = scalePosition(region.start)
const stopX = scalePosition(region.stop)
const regionWidth = stopX - startX
const midX = (startX + stopX) / 2
const offset = index * 0

return (
<g key={`${region.start}-${region.stop}`}>
<line
x1={startX}
y1={2 + offset}
x2={startX}
y2={11 + offset}
stroke="#424242"
/>
<line
x1={startX}
y1={7 + offset}
x2={stopX}
y2={7 + offset}
stroke="#424242"
/>
<line
x1={stopX}
y1={2 + offset}
x2={stopX}
y2={11 + offset}
stroke="#424242"
/>
{regionWidth > 40 && (
<>
<rect
x={midX - 15}
y={3 + offset}
width={30}
height={5}
fill="#fafafa"
/>
<text x={midX} y={8 + offset} dy="0.33em" textAnchor="middle">
{valueFn(region)}
</text>
</>
)}
</g>
)
})}
</g>
</svg>
</PlotWrapper>
</>
)}
</Track>
</Wrapper>
)

export default ConstraintTrack
20 changes: 10 additions & 10 deletions browser/src/GenePage/__snapshots__/GenePage.spec.tsx.snap
Original file line number Diff line number Diff line change
Expand Up @@ -12128,7 +12128,7 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1" has no unexpected changes 1`
}
>
<div
className="RegionalMissenseConstraintTrack__SidePanel-sc-1lzrnv-4 clrqtu"
className="ConstraintTrack__SidePanel-sc-113yvfg-2 dJMFds"
>
<span>
Regional missense constraint
Expand Down Expand Up @@ -12172,7 +12172,7 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1" has no unexpected changes 1`
}
>
<div
className="RegionalMissenseConstraintTrack__PlotWrapper-sc-1lzrnv-1 liGyhW"
className="ConstraintTrack__PlotWrapper-sc-113yvfg-0 fHKmMU"
>
<svg
height={35}
Expand Down Expand Up @@ -13764,7 +13764,7 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1_controls" has no unexpected c
}
>
<div
className="RegionalMissenseConstraintTrack__SidePanel-sc-1lzrnv-4 clrqtu"
className="ConstraintTrack__SidePanel-sc-113yvfg-2 dJMFds"
>
<span>
Regional missense constraint
Expand Down Expand Up @@ -13808,7 +13808,7 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1_controls" has no unexpected c
}
>
<div
className="RegionalMissenseConstraintTrack__PlotWrapper-sc-1lzrnv-1 liGyhW"
className="ConstraintTrack__PlotWrapper-sc-113yvfg-0 fHKmMU"
>
<svg
height={35}
Expand Down Expand Up @@ -15400,7 +15400,7 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1_non_cancer" has no unexpected
}
>
<div
className="RegionalMissenseConstraintTrack__SidePanel-sc-1lzrnv-4 clrqtu"
className="ConstraintTrack__SidePanel-sc-113yvfg-2 dJMFds"
>
<span>
Regional missense constraint
Expand Down Expand Up @@ -15444,7 +15444,7 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1_non_cancer" has no unexpected
}
>
<div
className="RegionalMissenseConstraintTrack__PlotWrapper-sc-1lzrnv-1 liGyhW"
className="ConstraintTrack__PlotWrapper-sc-113yvfg-0 fHKmMU"
>
<svg
height={35}
Expand Down Expand Up @@ -17036,7 +17036,7 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1_non_neuro" has no unexpected
}
>
<div
className="RegionalMissenseConstraintTrack__SidePanel-sc-1lzrnv-4 clrqtu"
className="ConstraintTrack__SidePanel-sc-113yvfg-2 dJMFds"
>
<span>
Regional missense constraint
Expand Down Expand Up @@ -17080,7 +17080,7 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1_non_neuro" has no unexpected
}
>
<div
className="RegionalMissenseConstraintTrack__PlotWrapper-sc-1lzrnv-1 liGyhW"
className="ConstraintTrack__PlotWrapper-sc-113yvfg-0 fHKmMU"
>
<svg
height={35}
Expand Down Expand Up @@ -18672,7 +18672,7 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1_non_topmed" has no unexpected
}
>
<div
className="RegionalMissenseConstraintTrack__SidePanel-sc-1lzrnv-4 clrqtu"
className="ConstraintTrack__SidePanel-sc-113yvfg-2 dJMFds"
>
<span>
Regional missense constraint
Expand Down Expand Up @@ -18716,7 +18716,7 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1_non_topmed" has no unexpected
}
>
<div
className="RegionalMissenseConstraintTrack__PlotWrapper-sc-1lzrnv-1 liGyhW"
className="ConstraintTrack__PlotWrapper-sc-113yvfg-0 fHKmMU"
>
<svg
height={35}
Expand Down
Loading

0 comments on commit 5d880cc

Please sign in to comment.