Skip to content

Commit

Permalink
feat: WorldEFP v1 (#138)
Browse files Browse the repository at this point in the history
* WorldEFP View

---------

Co-authored-by: MikeY <[email protected]>
  • Loading branch information
Yukthiw and mwkyuen authored Jan 23, 2025
1 parent b45ce9f commit 1b7a33e
Show file tree
Hide file tree
Showing 15 changed files with 599 additions and 11 deletions.
2 changes: 2 additions & 0 deletions Eplant/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import GeneInfoView from './views/GeneInfoView'
import GetStartedView from './views/GetStartedView'
import PlantEFP from './views/PlantEFP'
import PublicationViewer from './views/PublicationViewer'
import WorldEFP from './views/WorldEFP'
import { type View } from './View'

export type EplantConfig = {
Expand All @@ -32,6 +33,7 @@ const userViews = [
PlantEFP,
CellEFP,
ExperimentEFP,
WorldEFP,
ChromosomeViewer,
]

Expand Down
36 changes: 36 additions & 0 deletions Eplant/views/WorldEFP/InfoContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { styled, Theme } from '@mui/material'

interface InfoContentProps {
id: string
mean: number
std: number
sampleSize: number
}

const InfoContent = ({ id, mean, std, sampleSize }: InfoContentProps) => {
return (
<StyledInfoContent>
<p>
<strong>{id}</strong>
</p>
<p>Mean: {mean.toFixed(2)}</p>
<p>Standard error: {std.toFixed(2)}</p>
<p>Sample size: {sampleSize}</p>
</StyledInfoContent>
)
}

const StyledInfoContent = styled('div')(() => ({
wordWrap: 'break-word',
maxWidth: '300px',
'& p': {
margin: '5px 0',
color: 'black',
},
'& strong': {
display: 'block',
marginBottom: '10px',
},
}))

export default InfoContent
95 changes: 95 additions & 0 deletions Eplant/views/WorldEFP/MapContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { useCallback } from 'react'

import { ViewDispatch } from '@eplant/View'
import { useTheme } from '@mui/material'
import {
APIProvider,
Map,
MapCameraChangedEvent,
MapEvent,
useMap,
} from '@vis.gl/react-google-maps'

import { getColor } from '../eFP/svg'
import GeneDistributionChart from '../eFP/Viewer/GeneDistributionChart'
import Legend from '../eFP/Viewer/legend'

import MapMarker from './MapMarker'
import { WorldEFPAction, WorldEFPData, WorldEFPState } from './types'

interface MapContainerProps {
activeData: WorldEFPData
state: WorldEFPState
dispatch: ViewDispatch<WorldEFPAction>
}
const MapContainer = ({ activeData, state, dispatch }: MapContainerProps) => {
const theme = useTheme()
const map = useMap('WorldEFP')

const hangleDragEnd = (event: MapEvent) => {
const mapPos = map?.getCenter()
if (!mapPos) return

const coords = { lat: mapPos.lat(), lng: mapPos.lng() }
dispatch({
type: 'set-map-position',
position: coords,
})
}

const handleZoom = (event: MapCameraChangedEvent) => {
dispatch({
type: 'set-map-zoom',
zoom: event.detail.zoom,
})
}

return (
<Map
defaultCenter={state.position}
defaultZoom={2}
mapId={import.meta.env.VITE_MAP_ID}
streetViewControl={false}
mapTypeId={'roadmap'}
mapTypeControl={false}
onDragend={hangleDragEnd}
onZoomChanged={handleZoom}
id='WorldEFP'
>
{activeData.positions.map((pos, index) => {
const color = getColor(
activeData.efpData.groups[index].mean,
activeData.efpData.groups[index],
1,
theme,
state.colorMode,
activeData.efpData.groups[index].std,
state.maskThreshold,
state.maskingEnabled
)
return (
<MapMarker
theme={theme}
key={index}
color={color}
data={activeData.efpData.groups[index]}
position={pos}
></MapMarker>
)
})}
<Legend
sx={(theme) => ({
position: 'absolute',
left: theme.spacing(2),
bottom: theme.spacing(4),
zIndex: 10,
})}
colorMode={'absolute'}
data={activeData.efpData}
></Legend>
<GeneDistributionChart data={activeData.efpData} />
</Map>
)
}

export default MapContainer
98 changes: 98 additions & 0 deletions Eplant/views/WorldEFP/MapMarker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { useEffect, useState } from 'react'

import Modal from '@eplant/UI/Modal'
import { Theme, useTheme } from '@mui/material'
import {
AdvancedMarker,
InfoWindow,
useAdvancedMarkerRef,
} from '@vis.gl/react-google-maps'

import { EFPGroup } from '../eFP/types'

import WorldEFPIcon from './icon'
import InfoContent from './InfoContent'
import { ModalContent } from './ModalContent'
import { Coordinates } from './types'

interface MapMarkerProps {
theme: Theme
color: string
position: Coordinates
data: EFPGroup
}

const MapMarker = ({ theme, data, color, position }: MapMarkerProps) => {
const [showPopup, setShowPopup] = useState(false)
const [showModal, setShowModal] = useState(false)
const [markerRef, marker] = useAdvancedMarkerRef()
const handleMouseOver = () => {
setShowPopup(true)
}

const handleMouseOut = () => {
setShowPopup(false)
}

const handleClick = () => {
setShowPopup(false)
setShowModal(true)
}

useEffect(() => {
if (marker) {
marker.addListener('gmp-click', () => {}) // Need this to have mouseover logic work for some reason
marker.content?.addEventListener('mouseover', handleMouseOver)
marker.content?.addEventListener('mouseout', handleMouseOut)

return () => {
marker.content?.removeEventListener('mouseover', handleMouseOver)
marker.content?.removeEventListener('mouseout', handleMouseOut)
}
}
}, [data, marker])

return (
<>
<AdvancedMarker
ref={markerRef}
position={position}
clickable={true}
onClick={handleClick}
>
<WorldEFPIcon sx={{ fill: color }} />
</AdvancedMarker>
{showPopup && (
<InfoWindow
anchor={marker}
maxWidth={300}
disableAutoPan={true}
headerDisabled={true}
>
<InfoContent
id={data.name}
mean={data.mean}
std={data.std}
sampleSize={data.samples}
></InfoContent>
</InfoWindow>
)}
<Modal
open={showModal}
onClose={() => {
setShowModal(false)
}}
>
<ModalContent
theme={theme}
id={data.name}
mean={data.mean}
std={data.std}
sampleSize={data.samples}
></ModalContent>
</Modal>
</>
)
}

export default MapMarker
45 changes: 45 additions & 0 deletions Eplant/views/WorldEFP/ModalContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Box, Theme } from '@mui/material'
import { styled } from '@mui/material'

interface ModalContentProps {
theme: Theme
id: string
mean: number
std: number
sampleSize: number
}
export const ModalContent = ({
theme,
id,
mean,
std,
sampleSize,
}: ModalContentProps) => {
return (
<StyledBox theme={theme}>
<p>
<strong>{id}</strong>
</p>
<p>Mean: {mean.toFixed(2)}</p>
<p>Standard error: {std.toFixed(2)}</p>
<p>Sample size: {sampleSize}</p>
</StyledBox>
)
}

const StyledBox = styled(Box)(({ theme }: { theme: any }) => ({
top: '50%',
left: '50%',
width: 400,
borderRadius: '24px',
padding: theme.spacing(4),
backgroundColor: theme.palette.background,
'& p': {
margin: '5px 0',
color: theme.palette.secondary.contrastText,
},
'& strong': {
display: 'block',
marginBottom: '10px',
},
}))
18 changes: 18 additions & 0 deletions Eplant/views/WorldEFP/icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { styled } from '@mui/material'
export default styled((props) => {
return (
<svg
width='24'
height='24'
viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'
{...props}
>
<path d='M20.2573 0.500008C18.0687 0.496985 15.941 1.37872 14.4035 2.99125C14.4034 2.99125 14.4034 2.99144 14.4034 2.99144C12.5067 4.98218 11.7899 7.80009 12.4965 10.4301C11.7218 11.733 11.1167 13.1451 10.767 14.7081C10.6077 14.3831 10.4356 14.0661 10.2527 13.756C10.8507 11.4709 10.2219 9.02883 8.57598 7.30109V7.30128C8.57598 7.30109 8.57579 7.30109 8.57579 7.30109C7.23324 5.89301 5.37531 5.12312 3.46446 5.12593C2.92944 5.12668 2.3904 5.18789 1.85726 5.31278L1.87294 5.31108V5.31089C1.74125 5.33866 1.61939 5.40101 1.51983 5.49151C1.42026 5.58201 1.34658 5.69744 1.30634 5.82592C0.571027 8.19455 1.18164 10.7752 2.89956 12.5778C2.89956 12.5778 2.89975 12.5778 2.89975 12.578C4.48277 14.2383 6.78243 15.0109 9.03982 14.6765C9.81668 16.0351 10.3368 17.5229 10.4706 19.2325V22.7645L10.4708 22.7643C10.4708 22.9654 10.5505 23.1581 10.6928 23.3003C10.8348 23.4424 11.0276 23.5223 11.2286 23.5223C11.4298 23.5223 11.6225 23.4424 11.7646 23.3003C11.9068 23.1581 11.9867 22.9654 11.9867 22.7643V16.7747C12.1458 14.7333 12.7744 12.9617 13.71 11.347C16.3223 11.7616 18.9925 10.8781 20.8251 8.9567L20.8249 8.95689V8.9567C22.7923 6.89263 23.4911 3.93872 22.6492 1.22732V1.22751C22.609 1.09904 22.5353 0.983598 22.4357 0.893101C22.3362 0.802603 22.2143 0.740254 22.0826 0.712294L22.0983 0.714183C21.4877 0.571164 20.8701 0.501073 20.2574 0.500125L20.2573 0.500008ZM20.2548 2.01599C20.6083 2.01637 20.9637 2.04547 21.3179 2.10366C21.8042 4.16942 21.2185 6.34681 19.7279 7.91097C18.4043 9.29903 16.5353 10.0211 14.6388 9.92213C15.7714 8.35589 17.1743 6.91869 18.6728 5.48981L18.6726 5.49C18.8181 5.35133 18.9026 5.16032 18.9073 4.95947C18.9122 4.75846 18.8368 4.56385 18.6982 4.41836C18.5436 4.25606 18.3252 4.17067 18.1015 4.18484C17.9237 4.19618 17.7556 4.26986 17.6265 4.39266C16.2473 5.70764 14.8984 7.07848 13.7478 8.58731C13.6991 6.92584 14.3112 5.28632 15.5009 4.03751C16.7465 2.73105 18.475 2.01388 20.2549 2.01615L20.2548 2.01599ZM3.46746 6.64177C4.96966 6.63969 6.428 7.24504 7.47885 8.34706C8.40931 9.32381 8.92074 10.5829 8.9574 11.8774C8.00575 10.669 6.91316 9.56318 5.80074 8.50256V8.50275C5.6717 8.37975 5.50355 8.30607 5.32577 8.29473C5.10208 8.28056 4.88367 8.36615 4.72912 8.52825C4.59045 8.67373 4.51507 8.86832 4.51998 9.06937C4.5247 9.27039 4.60916 9.46121 4.75463 9.5999C5.99005 10.7779 7.14609 11.9587 8.09286 13.2349C6.5612 13.2602 5.06771 12.6545 3.99741 11.5319C2.75673 10.2302 2.2604 8.42625 2.64128 6.70548C2.91674 6.66373 3.19294 6.64238 3.46766 6.642L3.46746 6.64177Z' />
</svg>
)
})(({ theme }) => ({
fill: theme.palette.text.primary,
}))

// This icon is used in the dropdown view selection menu
Loading

0 comments on commit 1b7a33e

Please sign in to comment.