Skip to content

Commit

Permalink
Refactor regionIntersections
Browse files Browse the repository at this point in the history
Here we refactor the regionIntersections helper (now called regionsInExons) to make it more reusable, and also easier to read.
  • Loading branch information
phildarnowsky-broad committed May 1, 2024
1 parent a714d73 commit 50722d0
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 119 deletions.
66 changes: 66 additions & 0 deletions browser/src/ConstraintTrack.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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 },
{ start: 6, stop: 8, i: 2 },
],
},
{
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 },
{ start: 6, stop: 8, i: 2 },
],
},
{
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 },
{ start: 3, stop: 4, i: 1 },
{ start: 6, stop: 8, i: 2 },
],
},
{
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 },
{ start: 3, stop: 4, misc_field_1: 2, misc_field_2: 4 },
{ start: 6, stop: 8, misc_field_1: 6, misc_field_2: 8 },
],
},
]

testCases.forEach(({ regions, exons, expected }) => {
expect(regionsInExons(regions as GenericRegion[], exons as Exon[])).toEqual(expected)
})
})
33 changes: 32 additions & 1 deletion browser/src/ConstraintTrack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import styled from 'styled-components'
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;
Expand Down Expand Up @@ -36,7 +37,7 @@ const TopPanel = styled.div`
margin-bottom: 5px;
`

interface GenericRegion {
export interface GenericRegion {
start: number
stop: number
}
Expand All @@ -52,6 +53,36 @@ type Props<R extends GenericRegion> = {
valueFn: (region: R) => string
}

export const regionsInExons = <R extends GenericRegion>(regions: R[], exons: Exon[]): 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: R = { ...region, start: maxStart, stop: minStop }
intersections.push(next)
}

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

const ConstraintTrack = <R extends GenericRegion>({
trackTitle,
allRegions,
Expand Down
71 changes: 1 addition & 70 deletions browser/src/RegionalMissenseConstraintTrack.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,83 +2,14 @@ import React from 'react'
import renderer from 'react-test-renderer'
import { describe, expect, test } from '@jest/globals'
import RegionalMissenseConstraintTrack, {
regionIntersections,
} from './RegionalMissenseConstraintTrack'
import { Gene } from './GenePage/GenePage'
import {
RegionalMissenseConstraint,
RegionalMissenseConstraintRegion,
} from './RegionalMissenseConstraintTrack'
import { Gene } from './GenePage/GenePage'
import geneFactory from './__factories__/Gene'
// @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 { RegionViewerContext } from '@gnomad/region-viewer'

test('regionIntersections', () => {
const testCases = [
{
regions1: [
{ start: 2, stop: 4, i: 1 },
{ start: 6, stop: 8, i: 2 },
],
regions2: [
{ start: 2, stop: 6, j: 1 },
{ start: 6, stop: 9, j: 2 },
],
expected: [
{ start: 2, stop: 4, i: 1, j: 1 },
{ start: 6, stop: 8, i: 2, j: 2 },
],
},
{
regions1: [
{ start: 2, stop: 4, i: 1 },
{ start: 6, stop: 8, i: 2 },
],
regions2: [{ start: 1, stop: 8, j: 1 }],
expected: [
{ start: 2, stop: 4, i: 1, j: 1 },
{ start: 6, stop: 8, i: 2, j: 1 },
],
},
{
regions1: [
{ start: 2, stop: 4, i: 1 },
{ start: 6, stop: 8, i: 2 },
],
regions2: [
{ start: 1, stop: 3, j: 1 },
{ start: 3, stop: 5, j: 2 },
{ start: 5, stop: 9, j: 3 },
],
expected: [
{ start: 2, stop: 3, i: 1, j: 1 },
{ start: 3, stop: 4, i: 1, j: 2 },
{ start: 6, stop: 8, i: 2, j: 3 },
],
},
{
regions1: [
{ start: 2, stop: 4, misc_field_1: 2, misc_field_2: 4 },
{ start: 6, stop: 8, misc_field_1: 6, misc_field_2: 8 },
],
regions2: [
{ start: 1, stop: 3, j: 1 },
{ start: 3, stop: 5, j: 2 },
{ start: 5, stop: 9, j: 3 },
],
expected: [
{ start: 2, stop: 3, misc_field_1: 2, misc_field_2: 4, j: 1 },
{ start: 3, stop: 4, misc_field_1: 2, misc_field_2: 4, j: 2 },
{ start: 6, stop: 8, misc_field_1: 6, misc_field_2: 8, j: 3 },
],
},
]

testCases.forEach(({ regions1, regions2, expected }) => {
expect(regionIntersections([regions1, regions2])).toEqual(expected)
})
})

describe('RegionalMissenseConstraint', () => {
const childProps = {
centerPanelWidth: 500,
Expand Down
52 changes: 5 additions & 47 deletions browser/src/RegionalMissenseConstraintTrack.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React 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'
Expand All @@ -8,7 +7,7 @@ import Link from './Link'

import InfoButton from './help/InfoButton'
import { Gene } from './GenePage/GenePage'
import ConstraintTrack, { SidePanel, PlotWrapper } from './ConstraintTrack'
import ConstraintTrack, { SidePanel, PlotWrapper, regionsInExons } from './ConstraintTrack'

export type RegionalMissenseConstraintRegion = {
chrom: string
Expand Down Expand Up @@ -42,47 +41,6 @@ const RegionAttributeList = styled.dl`
}
`

export const regionIntersections = (
regionArrays: { start: number; stop: number }[][]
): RegionalMissenseConstraintRegion[] => {
const sortedRegionsArrays = regionArrays.map((regions) =>
[...regions].sort((a, b) => a.start - b.start)
)

const intersections = []

const indices = sortedRegionsArrays.map(() => 0)

while (sortedRegionsArrays.every((regions, i) => indices[i] < regions.length)) {
const maxStart = Math.max(...sortedRegionsArrays.map((regions, i) => regions[indices[i]].start))
const minStop = Math.min(...sortedRegionsArrays.map((regions, i) => regions[indices[i]].stop))

if (maxStart < minStop) {
const next = Object.assign(
// @ts-ignore TS2556: A spread argument must either have a tuple type or be ...
...[
{},
...sortedRegionsArrays.map((regions: { [x: string]: any }, i) => regions[indices[i]]),
{
start: maxStart,
stop: minStop,
},
]
)

intersections.push(next)
}

sortedRegionsArrays.forEach((regions, i) => {
if (regions[indices[i]].stop === minStop) {
indices[i] += 1
}
})
}

return intersections
}

// https://colorbrewer2.org/#type=sequential&scheme=YlOrRd&n=5
const colorScale = {
not_significant: '#e2e2e2',
Expand Down Expand Up @@ -301,16 +259,16 @@ const RegionalMissenseConstraintTrack = ({ regionalMissenseConstraint, gene }: P
}
}

const constrainedExons = regionIntersections([
const constraintInCodingSections = regionsInExons(
regionalMissenseConstraint.regions,
gene.exons.filter((exon) => exon.feature_type === 'CDS'),
])
gene.exons.filter((exon) => exon.feature_type === 'CDS')
)

return (
<ConstraintTrack
trackTitle="Regional missense constraint"
allRegions={regionalMissenseConstraint.regions}
constrainedRegions={constrainedExons}
constrainedRegions={constraintInCodingSections}
infobuttonTopic="regional-constraint"
colorFn={regionColor}
legend={<Legend />}
Expand Down
7 changes: 6 additions & 1 deletion browser/src/__factories__/Gene.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { Factory } from 'fishery'
import { Gene, GeneMetadata } from '../GenePage/GenePage'
import { Transcript } from '../TranscriptPage/TranscriptPage'
import { Transcript, Exon } from '../TranscriptPage/TranscriptPage'
import transcriptFactory from './Transcript'
import {
HeterozygousVariantCooccurrenceCountsPerSeverityAndAfFactory,
HomozygousVariantCooccurrenceCountsPerSeverityAndAfFactory,
} from './VariantCooccurrenceCountsPerSeverityAndAf'

export const exonFactory = Factory.define<Exon>(({ params }) => {
const { feature_type = 'CDS', start = 100, stop = 200 } = params
return { feature_type, start, stop }
})

const geneFactory = Factory.define<Gene>(({ params, associations }) => {
const {
gene_id = 'dummy_gene-1',
Expand Down

0 comments on commit 50722d0

Please sign in to comment.