Skip to content

Commit 1b7a33e

Browse files
Yukthiwmwkyuen
andauthored
feat: WorldEFP v1 (#138)
* WorldEFP View --------- Co-authored-by: MikeY <39020680+mwkyuen@users.noreply.github.com>
1 parent b45ce9f commit 1b7a33e

15 files changed

Lines changed: 599 additions & 11 deletions

Eplant/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import GeneInfoView from './views/GeneInfoView'
99
import GetStartedView from './views/GetStartedView'
1010
import PlantEFP from './views/PlantEFP'
1111
import PublicationViewer from './views/PublicationViewer'
12+
import WorldEFP from './views/WorldEFP'
1213
import { type View } from './View'
1314

1415
export type EplantConfig = {
@@ -32,6 +33,7 @@ const userViews = [
3233
PlantEFP,
3334
CellEFP,
3435
ExperimentEFP,
36+
WorldEFP,
3537
ChromosomeViewer,
3638
]
3739

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { styled, Theme } from '@mui/material'
2+
3+
interface InfoContentProps {
4+
id: string
5+
mean: number
6+
std: number
7+
sampleSize: number
8+
}
9+
10+
const InfoContent = ({ id, mean, std, sampleSize }: InfoContentProps) => {
11+
return (
12+
<StyledInfoContent>
13+
<p>
14+
<strong>{id}</strong>
15+
</p>
16+
<p>Mean: {mean.toFixed(2)}</p>
17+
<p>Standard error: {std.toFixed(2)}</p>
18+
<p>Sample size: {sampleSize}</p>
19+
</StyledInfoContent>
20+
)
21+
}
22+
23+
const StyledInfoContent = styled('div')(() => ({
24+
wordWrap: 'break-word',
25+
maxWidth: '300px',
26+
'& p': {
27+
margin: '5px 0',
28+
color: 'black',
29+
},
30+
'& strong': {
31+
display: 'block',
32+
marginBottom: '10px',
33+
},
34+
}))
35+
36+
export default InfoContent
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { useCallback } from 'react'
2+
3+
import { ViewDispatch } from '@eplant/View'
4+
import { useTheme } from '@mui/material'
5+
import {
6+
APIProvider,
7+
Map,
8+
MapCameraChangedEvent,
9+
MapEvent,
10+
useMap,
11+
} from '@vis.gl/react-google-maps'
12+
13+
import { getColor } from '../eFP/svg'
14+
import GeneDistributionChart from '../eFP/Viewer/GeneDistributionChart'
15+
import Legend from '../eFP/Viewer/legend'
16+
17+
import MapMarker from './MapMarker'
18+
import { WorldEFPAction, WorldEFPData, WorldEFPState } from './types'
19+
20+
interface MapContainerProps {
21+
activeData: WorldEFPData
22+
state: WorldEFPState
23+
dispatch: ViewDispatch<WorldEFPAction>
24+
}
25+
const MapContainer = ({ activeData, state, dispatch }: MapContainerProps) => {
26+
const theme = useTheme()
27+
const map = useMap('WorldEFP')
28+
29+
const hangleDragEnd = (event: MapEvent) => {
30+
const mapPos = map?.getCenter()
31+
if (!mapPos) return
32+
33+
const coords = { lat: mapPos.lat(), lng: mapPos.lng() }
34+
dispatch({
35+
type: 'set-map-position',
36+
position: coords,
37+
})
38+
}
39+
40+
const handleZoom = (event: MapCameraChangedEvent) => {
41+
dispatch({
42+
type: 'set-map-zoom',
43+
zoom: event.detail.zoom,
44+
})
45+
}
46+
47+
return (
48+
<Map
49+
defaultCenter={state.position}
50+
defaultZoom={2}
51+
mapId={import.meta.env.VITE_MAP_ID}
52+
streetViewControl={false}
53+
mapTypeId={'roadmap'}
54+
mapTypeControl={false}
55+
onDragend={hangleDragEnd}
56+
onZoomChanged={handleZoom}
57+
id='WorldEFP'
58+
>
59+
{activeData.positions.map((pos, index) => {
60+
const color = getColor(
61+
activeData.efpData.groups[index].mean,
62+
activeData.efpData.groups[index],
63+
1,
64+
theme,
65+
state.colorMode,
66+
activeData.efpData.groups[index].std,
67+
state.maskThreshold,
68+
state.maskingEnabled
69+
)
70+
return (
71+
<MapMarker
72+
theme={theme}
73+
key={index}
74+
color={color}
75+
data={activeData.efpData.groups[index]}
76+
position={pos}
77+
></MapMarker>
78+
)
79+
})}
80+
<Legend
81+
sx={(theme) => ({
82+
position: 'absolute',
83+
left: theme.spacing(2),
84+
bottom: theme.spacing(4),
85+
zIndex: 10,
86+
})}
87+
colorMode={'absolute'}
88+
data={activeData.efpData}
89+
></Legend>
90+
<GeneDistributionChart data={activeData.efpData} />
91+
</Map>
92+
)
93+
}
94+
95+
export default MapContainer
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { useEffect, useState } from 'react'
2+
3+
import Modal from '@eplant/UI/Modal'
4+
import { Theme, useTheme } from '@mui/material'
5+
import {
6+
AdvancedMarker,
7+
InfoWindow,
8+
useAdvancedMarkerRef,
9+
} from '@vis.gl/react-google-maps'
10+
11+
import { EFPGroup } from '../eFP/types'
12+
13+
import WorldEFPIcon from './icon'
14+
import InfoContent from './InfoContent'
15+
import { ModalContent } from './ModalContent'
16+
import { Coordinates } from './types'
17+
18+
interface MapMarkerProps {
19+
theme: Theme
20+
color: string
21+
position: Coordinates
22+
data: EFPGroup
23+
}
24+
25+
const MapMarker = ({ theme, data, color, position }: MapMarkerProps) => {
26+
const [showPopup, setShowPopup] = useState(false)
27+
const [showModal, setShowModal] = useState(false)
28+
const [markerRef, marker] = useAdvancedMarkerRef()
29+
const handleMouseOver = () => {
30+
setShowPopup(true)
31+
}
32+
33+
const handleMouseOut = () => {
34+
setShowPopup(false)
35+
}
36+
37+
const handleClick = () => {
38+
setShowPopup(false)
39+
setShowModal(true)
40+
}
41+
42+
useEffect(() => {
43+
if (marker) {
44+
marker.addListener('gmp-click', () => {}) // Need this to have mouseover logic work for some reason
45+
marker.content?.addEventListener('mouseover', handleMouseOver)
46+
marker.content?.addEventListener('mouseout', handleMouseOut)
47+
48+
return () => {
49+
marker.content?.removeEventListener('mouseover', handleMouseOver)
50+
marker.content?.removeEventListener('mouseout', handleMouseOut)
51+
}
52+
}
53+
}, [data, marker])
54+
55+
return (
56+
<>
57+
<AdvancedMarker
58+
ref={markerRef}
59+
position={position}
60+
clickable={true}
61+
onClick={handleClick}
62+
>
63+
<WorldEFPIcon sx={{ fill: color }} />
64+
</AdvancedMarker>
65+
{showPopup && (
66+
<InfoWindow
67+
anchor={marker}
68+
maxWidth={300}
69+
disableAutoPan={true}
70+
headerDisabled={true}
71+
>
72+
<InfoContent
73+
id={data.name}
74+
mean={data.mean}
75+
std={data.std}
76+
sampleSize={data.samples}
77+
></InfoContent>
78+
</InfoWindow>
79+
)}
80+
<Modal
81+
open={showModal}
82+
onClose={() => {
83+
setShowModal(false)
84+
}}
85+
>
86+
<ModalContent
87+
theme={theme}
88+
id={data.name}
89+
mean={data.mean}
90+
std={data.std}
91+
sampleSize={data.samples}
92+
></ModalContent>
93+
</Modal>
94+
</>
95+
)
96+
}
97+
98+
export default MapMarker
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Box, Theme } from '@mui/material'
2+
import { styled } from '@mui/material'
3+
4+
interface ModalContentProps {
5+
theme: Theme
6+
id: string
7+
mean: number
8+
std: number
9+
sampleSize: number
10+
}
11+
export const ModalContent = ({
12+
theme,
13+
id,
14+
mean,
15+
std,
16+
sampleSize,
17+
}: ModalContentProps) => {
18+
return (
19+
<StyledBox theme={theme}>
20+
<p>
21+
<strong>{id}</strong>
22+
</p>
23+
<p>Mean: {mean.toFixed(2)}</p>
24+
<p>Standard error: {std.toFixed(2)}</p>
25+
<p>Sample size: {sampleSize}</p>
26+
</StyledBox>
27+
)
28+
}
29+
30+
const StyledBox = styled(Box)(({ theme }: { theme: any }) => ({
31+
top: '50%',
32+
left: '50%',
33+
width: 400,
34+
borderRadius: '24px',
35+
padding: theme.spacing(4),
36+
backgroundColor: theme.palette.background,
37+
'& p': {
38+
margin: '5px 0',
39+
color: theme.palette.secondary.contrastText,
40+
},
41+
'& strong': {
42+
display: 'block',
43+
marginBottom: '10px',
44+
},
45+
}))

Eplant/views/WorldEFP/icon.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { styled } from '@mui/material'
2+
export default styled((props) => {
3+
return (
4+
<svg
5+
width='24'
6+
height='24'
7+
viewBox='0 0 24 24'
8+
xmlns='http://www.w3.org/2000/svg'
9+
{...props}
10+
>
11+
<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' />
12+
</svg>
13+
)
14+
})(({ theme }) => ({
15+
fill: theme.palette.text.primary,
16+
}))
17+
18+
// This icon is used in the dropdown view selection menu

0 commit comments

Comments
 (0)