Skip to content

Commit 5d880cc

Browse files
Generalize RegionalMissenseConstraintTrack
Here we extract a generic `ConstraintTrack` component from `RegionalMissenseConstraintTrack` so that we can build a mitochondrial constraint track on the new generic component.
1 parent 05b1b54 commit 5d880cc

File tree

7 files changed

+344
-281
lines changed

7 files changed

+344
-281
lines changed

browser/src/ConstraintTrack.spec.tsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { regionsInExons, GenericRegion } from './ConstraintTrack'
2+
import { exonFactory } from './__factories__/Gene'
3+
import { Exon } from './TranscriptPage/TranscriptPage'
4+
5+
test('regionsInExons', () => {
6+
const testCases = [
7+
{
8+
regions: [
9+
{ start: 2, stop: 4, i: 1 },
10+
{ start: 6, stop: 8, i: 2 },
11+
],
12+
exons: [exonFactory.build({ start: 2, stop: 6 }), exonFactory.build({ start: 6, stop: 9 })],
13+
expected: [
14+
{ start: 2, stop: 4, i: 1, unclamped_start: 2, unclamped_stop: 4 },
15+
{ start: 6, stop: 8, i: 2, unclamped_start: 6, unclamped_stop: 8 },
16+
],
17+
},
18+
{
19+
regions: [
20+
{ start: 2, stop: 4, i: 1 },
21+
{ start: 6, stop: 8, i: 2 },
22+
],
23+
exons: [exonFactory.build({ start: 1, stop: 8 })],
24+
expected: [
25+
{ start: 2, stop: 4, i: 1, unclamped_start: 2, unclamped_stop: 4 },
26+
{ start: 6, stop: 8, i: 2, unclamped_start: 6, unclamped_stop: 8 },
27+
],
28+
},
29+
{
30+
regions: [
31+
{ start: 2, stop: 4, i: 1 },
32+
{ start: 6, stop: 8, i: 2 },
33+
],
34+
exons: [
35+
exonFactory.build({ start: 1, stop: 3 }),
36+
exonFactory.build({ start: 3, stop: 5 }),
37+
exonFactory.build({ start: 5, stop: 9 }),
38+
],
39+
expected: [
40+
{ start: 2, stop: 3, i: 1, unclamped_start: 2, unclamped_stop: 4 },
41+
{ start: 3, stop: 4, i: 1, unclamped_start: 2, unclamped_stop: 4 },
42+
{ start: 6, stop: 8, i: 2, unclamped_start: 6, unclamped_stop: 8 },
43+
],
44+
},
45+
{
46+
regions: [
47+
{ start: 2, stop: 4, misc_field_1: 2, misc_field_2: 4 },
48+
{ start: 6, stop: 8, misc_field_1: 6, misc_field_2: 8 },
49+
],
50+
exons: [
51+
exonFactory.build({ start: 1, stop: 3 }),
52+
exonFactory.build({ start: 3, stop: 5 }),
53+
exonFactory.build({ start: 5, stop: 9 }),
54+
],
55+
expected: [
56+
{
57+
start: 2,
58+
stop: 3,
59+
misc_field_1: 2,
60+
misc_field_2: 4,
61+
unclamped_start: 2,
62+
unclamped_stop: 4,
63+
},
64+
{
65+
start: 3,
66+
stop: 4,
67+
misc_field_1: 2,
68+
misc_field_2: 4,
69+
unclamped_start: 2,
70+
unclamped_stop: 4,
71+
},
72+
{
73+
start: 6,
74+
stop: 8,
75+
misc_field_1: 6,
76+
misc_field_2: 8,
77+
unclamped_start: 6,
78+
unclamped_stop: 8,
79+
},
80+
],
81+
},
82+
]
83+
84+
testCases.forEach(({ regions, exons, expected }) => {
85+
expect(regionsInExons(regions as GenericRegion[], exons as Exon[])).toEqual(expected)
86+
})
87+
})

browser/src/ConstraintTrack.tsx

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import React, { ReactNode } from 'react'
2+
import styled from 'styled-components'
3+
// @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module '@gno... Remove this comment to see the full error message
4+
import { Track } from '@gnomad/region-viewer'
5+
import InfoButton from './help/InfoButton'
6+
import { TooltipAnchor } from '@gnomad/ui'
7+
import { Exon } from './TranscriptPage/TranscriptPage'
8+
9+
export const PlotWrapper = styled.div`
10+
display: flex;
11+
flex-direction: column;
12+
justify-content: center;
13+
height: 100%;
14+
`
15+
16+
type TrackProps = {
17+
scalePosition: (input: number) => number
18+
width: number
19+
}
20+
21+
const Wrapper = styled.div`
22+
display: flex;
23+
flex-direction: column;
24+
margin-bottom: 1em;
25+
`
26+
27+
export const SidePanel = styled.div`
28+
display: flex;
29+
align-items: center;
30+
height: 100%;
31+
`
32+
33+
const TopPanel = styled.div`
34+
display: flex;
35+
justify-content: flex-end;
36+
width: 100%;
37+
margin-bottom: 5px;
38+
`
39+
40+
export interface GenericRegion {
41+
start: number
42+
stop: number
43+
}
44+
45+
// When clamping constrained regions to exons, we remember the original
46+
// boundaries of the region for display in the region tooltip
47+
export type RegionWithUnclamped<R extends GenericRegion> = R & {
48+
unclamped_start: number
49+
unclamped_stop: number
50+
}
51+
52+
type Props<R extends GenericRegion> = {
53+
trackTitle: string
54+
allRegions: R[] | null
55+
constrainedRegions: RegionWithUnclamped<R>[]
56+
infobuttonTopic: string
57+
legend: ReactNode
58+
tooltipComponent: any // TK any
59+
colorFn: (region: R) => string
60+
valueFn: (region: R) => string
61+
}
62+
63+
export const regionsInExons = <R extends GenericRegion>(
64+
regions: R[],
65+
exons: Exon[]
66+
): RegionWithUnclamped<R>[] => {
67+
const sortedRegions = regions.sort((a, b) => a.start - b.start)
68+
const sortedExons = exons.sort((a, b) => a.start - b.start)
69+
70+
const intersections = []
71+
72+
let regionIndex = 0
73+
let exonIndex = 0
74+
75+
while (regionIndex < regions.length && exonIndex < exons.length) {
76+
const region = sortedRegions[regionIndex]
77+
const exon = sortedExons[exonIndex]
78+
const maxStart = Math.max(region.start, exon.start)
79+
const minStop = Math.min(region.stop, exon.stop)
80+
81+
if (maxStart < minStop) {
82+
const next: RegionWithUnclamped<R> = {
83+
...region,
84+
start: maxStart,
85+
stop: minStop,
86+
unclamped_start: region.start,
87+
unclamped_stop: region.stop,
88+
}
89+
intersections.push(next)
90+
}
91+
92+
if (region.stop === minStop) {
93+
regionIndex += 1
94+
}
95+
if (exon.stop === minStop) {
96+
exonIndex += 1
97+
}
98+
}
99+
return intersections
100+
}
101+
102+
const ConstraintTrack = <R extends GenericRegion>({
103+
trackTitle,
104+
allRegions,
105+
constrainedRegions,
106+
infobuttonTopic,
107+
legend,
108+
tooltipComponent,
109+
colorFn,
110+
valueFn,
111+
}: Props<R>) => (
112+
<Wrapper>
113+
<Track
114+
renderLeftPanel={() => (
115+
<SidePanel>
116+
<span>{trackTitle}</span>
117+
<InfoButton topic={infobuttonTopic} />
118+
</SidePanel>
119+
)}
120+
>
121+
{({ scalePosition, width }: TrackProps) => (
122+
<>
123+
<TopPanel>{legend}</TopPanel>
124+
<PlotWrapper>
125+
<svg height={55} width={width}>
126+
{constrainedRegions.map((region: RegionWithUnclamped<R>) => {
127+
const startX = scalePosition(region.start)
128+
const stopX = scalePosition(region.stop)
129+
const regionWidth = stopX - startX
130+
131+
return (
132+
<TooltipAnchor
133+
key={`${region.start}-${region.stop}`}
134+
// @ts-expect-error need to redefine TooltipAnchor to allow arbitrary props for the children type-safely
135+
region={region}
136+
isTranscript={allRegions && allRegions.length === 1}
137+
tooltipComponent={tooltipComponent}
138+
>
139+
<g>
140+
<rect
141+
x={startX}
142+
y={0}
143+
width={regionWidth}
144+
height={15}
145+
fill={colorFn(region)}
146+
stroke="black"
147+
/>
148+
</g>
149+
</TooltipAnchor>
150+
)
151+
})}
152+
<g transform="translate(0,20)">
153+
{allRegions &&
154+
allRegions.map((region: R, index: number) => {
155+
const startX = scalePosition(region.start)
156+
const stopX = scalePosition(region.stop)
157+
const regionWidth = stopX - startX
158+
const midX = (startX + stopX) / 2
159+
const offset = index * 0
160+
161+
return (
162+
<g key={`${region.start}-${region.stop}`}>
163+
<line
164+
x1={startX}
165+
y1={2 + offset}
166+
x2={startX}
167+
y2={11 + offset}
168+
stroke="#424242"
169+
/>
170+
<line
171+
x1={startX}
172+
y1={7 + offset}
173+
x2={stopX}
174+
y2={7 + offset}
175+
stroke="#424242"
176+
/>
177+
<line
178+
x1={stopX}
179+
y1={2 + offset}
180+
x2={stopX}
181+
y2={11 + offset}
182+
stroke="#424242"
183+
/>
184+
{regionWidth > 40 && (
185+
<>
186+
<rect
187+
x={midX - 15}
188+
y={3 + offset}
189+
width={30}
190+
height={5}
191+
fill="#fafafa"
192+
/>
193+
<text x={midX} y={8 + offset} dy="0.33em" textAnchor="middle">
194+
{valueFn(region)}
195+
</text>
196+
</>
197+
)}
198+
</g>
199+
)
200+
})}
201+
</g>
202+
</svg>
203+
</PlotWrapper>
204+
</>
205+
)}
206+
</Track>
207+
</Wrapper>
208+
)
209+
210+
export default ConstraintTrack

browser/src/GenePage/__snapshots__/GenePage.spec.tsx.snap

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12128,7 +12128,7 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1" has no unexpected changes 1`
1212812128
}
1212912129
>
1213012130
<div
12131-
className="RegionalMissenseConstraintTrack__SidePanel-sc-1lzrnv-4 clrqtu"
12131+
className="ConstraintTrack__SidePanel-sc-113yvfg-2 dJMFds"
1213212132
>
1213312133
<span>
1213412134
Regional missense constraint
@@ -12172,7 +12172,7 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1" has no unexpected changes 1`
1217212172
}
1217312173
>
1217412174
<div
12175-
className="RegionalMissenseConstraintTrack__PlotWrapper-sc-1lzrnv-1 liGyhW"
12175+
className="ConstraintTrack__PlotWrapper-sc-113yvfg-0 fHKmMU"
1217612176
>
1217712177
<svg
1217812178
height={35}
@@ -13764,7 +13764,7 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1_controls" has no unexpected c
1376413764
}
1376513765
>
1376613766
<div
13767-
className="RegionalMissenseConstraintTrack__SidePanel-sc-1lzrnv-4 clrqtu"
13767+
className="ConstraintTrack__SidePanel-sc-113yvfg-2 dJMFds"
1376813768
>
1376913769
<span>
1377013770
Regional missense constraint
@@ -13808,7 +13808,7 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1_controls" has no unexpected c
1380813808
}
1380913809
>
1381013810
<div
13811-
className="RegionalMissenseConstraintTrack__PlotWrapper-sc-1lzrnv-1 liGyhW"
13811+
className="ConstraintTrack__PlotWrapper-sc-113yvfg-0 fHKmMU"
1381213812
>
1381313813
<svg
1381413814
height={35}
@@ -15400,7 +15400,7 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1_non_cancer" has no unexpected
1540015400
}
1540115401
>
1540215402
<div
15403-
className="RegionalMissenseConstraintTrack__SidePanel-sc-1lzrnv-4 clrqtu"
15403+
className="ConstraintTrack__SidePanel-sc-113yvfg-2 dJMFds"
1540415404
>
1540515405
<span>
1540615406
Regional missense constraint
@@ -15444,7 +15444,7 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1_non_cancer" has no unexpected
1544415444
}
1544515445
>
1544615446
<div
15447-
className="RegionalMissenseConstraintTrack__PlotWrapper-sc-1lzrnv-1 liGyhW"
15447+
className="ConstraintTrack__PlotWrapper-sc-113yvfg-0 fHKmMU"
1544815448
>
1544915449
<svg
1545015450
height={35}
@@ -17036,7 +17036,7 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1_non_neuro" has no unexpected
1703617036
}
1703717037
>
1703817038
<div
17039-
className="RegionalMissenseConstraintTrack__SidePanel-sc-1lzrnv-4 clrqtu"
17039+
className="ConstraintTrack__SidePanel-sc-113yvfg-2 dJMFds"
1704017040
>
1704117041
<span>
1704217042
Regional missense constraint
@@ -17080,7 +17080,7 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1_non_neuro" has no unexpected
1708017080
}
1708117081
>
1708217082
<div
17083-
className="RegionalMissenseConstraintTrack__PlotWrapper-sc-1lzrnv-1 liGyhW"
17083+
className="ConstraintTrack__PlotWrapper-sc-113yvfg-0 fHKmMU"
1708417084
>
1708517085
<svg
1708617086
height={35}
@@ -18672,7 +18672,7 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1_non_topmed" has no unexpected
1867218672
}
1867318673
>
1867418674
<div
18675-
className="RegionalMissenseConstraintTrack__SidePanel-sc-1lzrnv-4 clrqtu"
18675+
className="ConstraintTrack__SidePanel-sc-113yvfg-2 dJMFds"
1867618676
>
1867718677
<span>
1867818678
Regional missense constraint
@@ -18716,7 +18716,7 @@ exports[`GenePage with non-SV dataset "gnomad_r2_1_non_topmed" has no unexpected
1871618716
}
1871718717
>
1871818718
<div
18719-
className="RegionalMissenseConstraintTrack__PlotWrapper-sc-1lzrnv-1 liGyhW"
18719+
className="ConstraintTrack__PlotWrapper-sc-113yvfg-0 fHKmMU"
1872018720
>
1872118721
<svg
1872218722
height={35}

0 commit comments

Comments
 (0)