Skip to content

Commit

Permalink
Merge pull request #131 from y330/ChromosomeView-Yonah
Browse files Browse the repository at this point in the history
This pull request adds the Chromosome View.
The chromosome view lays out all the chromosomes for a species and shows you where given genes are on the chromosomes.
  • Loading branch information
y330 authored Aug 8, 2024
2 parents 4c43465 + c0bd541 commit 93f1060
Show file tree
Hide file tree
Showing 17 changed files with 1,407 additions and 44 deletions.
6 changes: 5 additions & 1 deletion Eplant/UI/LeftNav/Collections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ import {
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import GeneticElement from '@eplant/GeneticElement'
import { useCollections, useGeneticElements } from '@eplant/state'
import {
useActiveGeneId,
useCollections,
useGeneticElements,
} from '@eplant/state'
import { Add, Check, ExpandMore } from '@mui/icons-material'
import {
Box,
Expand Down
11 changes: 6 additions & 5 deletions Eplant/UI/LeftNav/GeneSearch/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,23 @@ export function SearchGroup({
}: {
addGeneticElements: (gene: GeneticElement[]) => void
}) {
const [speciesList, setSpeciesList] = useSpecies()
const [species, setSpecies] = useState<Species>()

// Commedned out until we get multi-species support
// const [searchingByExpression, setSearchingByExpression] = useState<boolean>(false)
// const [searchingByPhenotype, setSearchingByPhenotype] = useState<boolean>(false)
// const [speciesList, setSpeciesList] = useSpecies()

// useEffect(() => {
// if (!species && speciesList.length) setSpecies(speciesList[0])
// }, [species])
useEffect(() => {
if (!species && speciesList.length) setSpecies(speciesList[0])
}, [species])

return (
<Stack direction='column' spacing={2}>
{/* Species selector */}

{/* Commenting this out until we get multi-species support
{/* Commenting this out until we get multi-species support
<TextField
select
size='small'
Expand Down Expand Up @@ -81,7 +82,7 @@ export function SearchGroup({
)
}}
></SearchBar>
{/* TODO: Implement alternate search options
{/* TODO: Implement alternate search options
<MenuButton disabled variant="contained">
Search by expression
</MenuButton>
Expand Down
2 changes: 2 additions & 0 deletions Eplant/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createContext, useContext } from 'react'

import CellEFP from './views/CellEFP'
import ChromosomeViewer from './views/ChromosomeViewer'
import DebugView from './views/DebugView'
import ExperimentEFP from './views/ExperimentEFP'
import FallbackView from './views/FallbackView'
Expand Down Expand Up @@ -31,6 +32,7 @@ const userViews = [
PlantEFP,
CellEFP,
ExperimentEFP,
ChromosomeViewer,
]

// List of views that are used to lookup a view by id
Expand Down
6 changes: 6 additions & 0 deletions Eplant/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
padding: 0;
margin: 0;
}

body {
overscroll-behavior: none;
}
Expand All @@ -14,3 +15,8 @@ body {
max-width: 100%;
max-height: 100%;
}

.ChromosomeZoomBtn {
border: none;
background-color: transparent;
}
308 changes: 308 additions & 0 deletions Eplant/views/ChromosomeViewer/Viewer/Chromosome.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
// -------
// IMPORTS
// -------
import React, { FC, useEffect, useLayoutEffect, useState } from 'react'

import { useCollections, useGeneticElements } from '@eplant/state'
import { Unstable_Popup as Popup } from '@mui/base/Unstable_Popup'
import ArrowLeft from '@mui/icons-material/ArrowLeft'
import Box from '@mui/material/Box'
import ClickAwayListener from '@mui/material/ClickAwayListener'
import useTheme from '@mui/material/styles/useTheme'
import Typography from '@mui/material/Typography'

import {
CentromereItem,
ChromosomeItem,
GeneAnnotationItem,
GeneRange,
} from '../types'

import GeneAnnotation from './GeneAnnotation'
import GeneList from './GeneList'
import {
getChromosomeSvg,
getChromosomeXCoordinate,
getGeneAnnotation,
pixelToBp,
} from './utilities'

//----------
// TYPES
//----------
interface ChromosomeProps {
scale: number
chromosome: ChromosomeItem
}

//----------
// COMPONENT
//----------
const Chromosome: FC<ChromosomeProps> = ({ scale, chromosome }) => {
// State
const [isHovered, setIsHovered] = useState<boolean>(false)
const [anchorOrigin, setAnchorOrigin] = useState<number[]>([])
const [anchorEl, setAnchorEl] = useState<null | any>(null)
const [geneRange, setGeneRange] = useState<GeneRange>({
start: 0,
end: 0,
})
// Gene Annotation drawing
const [geneAnnotationArray, setGeneAnnotationArray] = useState<
GeneAnnotationItem[]
>([])

// Global State
const [geneticElements] = useGeneticElements()
const [collections] = useCollections()
const theme = useTheme()

// SVG drawing
const centromeres: CentromereItem[] = chromosome.centromeres
const hasCentromeres: boolean = centromeres.length > 0
const lastCentromereEnd: number = hasCentromeres
? centromeres[centromeres.length - 1].end
: 0

const x: number = 10
const y: number = 0
const width: number = 10
const perBpHeight: number = 0.000015
let start: number = 0

// Gene List popover variables
const openPopup = Boolean(anchorEl)

// Fire before paint, converts geneticElements into geneAnnotationArray
useLayoutEffect(() => {
// TODO: move this to top level to prevent uneccessary api calls
const poplar = false
const species = poplar ? 'Populus_trichocarpa' : 'Arabidopsis_thaliana'
let newGeneAnnotationArray: GeneAnnotationItem[] = []
geneticElements.map((gene) => {
// for each item in geneticElements, fetch it's gene information to add to it's geneAnnotation
fetch(
`https://bar.utoronto.ca/eplant${
poplar ? '_poplar' : ''
}/cgi-bin/querygene.cgi?species=${species}&term=${gene.id}`
)
.then((response) => response.json())
.then((geneItem) => {
if (geneItem.chromosome === chromosome.id) {
newGeneAnnotationArray = geneAnnotationArray
const geneAnnotation: GeneAnnotationItem =
getGeneAnnotation(geneItem)

// Make sure new geneAnnotation is not already in geneAnnotationArray
const isDuplicate = newGeneAnnotationArray.some((gene) => {
if (gene.id === geneAnnotation.id) {
return true
}
return false
})
if (!isDuplicate) {
newGeneAnnotationArray.push(geneAnnotation)
setGeneAnnotationArray(newGeneAnnotationArray)
}
}
})
.catch((err) => {
console.log(err)
})
})
}, [])

// Execute on first render, after paint
useEffect(() => {
const svg: HTMLElement & SVGSVGElement = getChromosomeSvg(chromosome.id)
// Get the bounds of the SVG content
const bbox: SVGRect = svg.getBBox()
// Update the width and height using the size of the contents
svg.setAttribute('width', `${bbox.x + bbox.width + bbox.x}`)
svg.setAttribute('height', `${bbox.y + bbox.height + bbox.y}`)
}, [])

//--------------
// Event Handling
//--------------
// Handle click on chromosome
const handleClick = (event: React.MouseEvent<SVGRectElement>) => {
// define virtual element to attach geneList popup to
const virtualEl = {
getBoundingClientRect() {
return {
left: getChromosomeXCoordinate(chromosome.id) + 100, // distanceX from the right of the chromosome (TODO: determine what side of the screen click is on and accordingly place popup on left or right of chromosome)
top: event.clientY - 60, // distanceY from click(has weird functionality- need to fix)
width: 0,
height: 0,
}
},
}
setAnchorEl(anchorEl ? null : virtualEl)
setAnchorOrigin([event.clientX, event.clientY])
setGeneRange(pixelToBp(geneRange, chromosome, event.clientY))
}
const handleMouseOverChromosome = () => {
setIsHovered(true)
}
const handleMouseLeaveChromosome = () => {
setIsHovered(false)
}
// Handle popup close
const handleClose = () => {
setAnchorOrigin([])
setAnchorEl(null)
}
return (
<>
{/* GENE LIST POPUP */}
<Popup
open={openPopup}
anchor={anchorEl}
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
}}
>
<ArrowLeft
fontSize='medium'
htmlColor={theme.palette.primary.main}
sx={{
position: 'relative',
}}
/>
<div>
<Typography variant='caption' sx={{ fontSize: 9 }}>
{geneRange.start.toLocaleString()}
</Typography>

<ClickAwayListener onClickAway={handleClose}>
<Box
sx={{
background: theme.palette.background.paper,
border: `1.5px solid ${theme.palette.primary.dark}`,
p: 0,
width: '180px',
maxHeight: '100px',
minHeight: 30,
overflowY: 'scroll',
overflowX: 'clip',
}}
>
<GeneList
id={chromosome.id}
start={geneRange.start}
end={geneRange.end}
anchorOrigin={anchorOrigin}
/>
</Box>
</ClickAwayListener>
<Typography variant='caption' sx={{ fontSize: 9 }}>
{geneRange.end.toLocaleString()}
</Typography>
</div>
</Popup>
{/* =============== */}
{/* CHROMOSOME SVG */}
<svg
id={chromosome.id + '_svg'}
width='0'
height={chromosome.size * perBpHeight}
viewBox='0 0 width height'
preserveAspectRatio='xMidYMid meet'
overflow='visible'
>
<g>
{/* CENTROMERIC LAYER */}
{hasCentromeres ? (
<rect
x={x}
y={y}
width={width * 0.6}
height={chromosome.size * perBpHeight}
ry={width / 2}
fill='gray'
/>
) : (
<rect
x={x}
y={y}
width={width}
height={chromosome.size * perBpHeight}
ry={chromosome.size * perBpHeight > 10 ? width / 2 : '50%'}
fill='gray'
/>
)}
{/* NON-CENTROMERIC LAYERS */}
{/* note: all except last can be drawn in a loop */}
{hasCentromeres &&
centromeres.map((centromere, index) => {
start = index === 0 ? 0 : centromeres[index - 1].end
const end = centromere.start
return (
<rect
x={x - 2}
y={y + start * perBpHeight}
width={width}
height={(end - start) * perBpHeight}
ry={
(end - start) * perBpHeight < 4
? 2
: (end - start) * perBpHeight < 10
? 3
: width / 2
}
fill='gray'
key={index}
/>
)
})}
{/* LAST NON-CENTROMERIC LAYER > must be drawn seperately */}
{hasCentromeres && (
<rect
x={x - 2}
y={y + lastCentromereEnd * perBpHeight}
width={width}
height={(chromosome.size - lastCentromereEnd) * perBpHeight}
rx={width / 2}
ry={width / 2}
fill='gray'
/>
)}
{/* INPUT LAYER (transparent, must be drawn on top of other layers) */}
<rect
id={chromosome.id + '_input'}
x={x - 2}
y={y}
width={width}
height={chromosome.size * perBpHeight}
rx={width / 2}
ry={width / 2}
fill='transparent'
onMouseEnter={handleMouseOverChromosome}
onMouseLeave={handleMouseLeaveChromosome}
cursor={isHovered ? 'pointer' : 'default'}
onClick={handleClick}
/>
</g>
{/* GENES ANNOTATION TAGS */}
<g id={`${chromosome.id}_geneAnnotationTags`}>
{geneAnnotationArray.map((gene, i) => {
// Flatten collections into array of gene IDs - important for not drawing removed gene annotations
const flatGeneCollection = collections.flatMap(
(collection) => collection.genes
)
// Make sure that the gene actually is in a collection
// fixes bug where the gene annotations for removed genes dont disappear
if (flatGeneCollection.includes(gene.id)) {
return <GeneAnnotation key={i} gene={gene} scale={scale} />
}
})}
</g>
</svg>
</>
)
}

export default Chromosome
Loading

0 comments on commit 93f1060

Please sign in to comment.