Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
36a3b68
Test
Yukthiw Dec 4, 2025
7252286
Fix yaml
Yukthiw Dec 4, 2025
50b762a
ignore build on staging-debug
Yukthiw Dec 4, 2025
250b6ad
Revert guthub actions change
Yukthiw Dec 16, 2025
d440513
Changed entrypoint path back to being base relative
Yukthiw Dec 16, 2025
661f723
Moving to hashrouter
Yukthiw Dec 25, 2025
df7cafe
Trying to fix view alignment issues
Yukthiw Dec 26, 2025
0b88eee
Revert "Trying to fix view alignment issues"
Yukthiw Dec 26, 2025
1a16eb3
Revert "Moving to hashrouter"
Yukthiw Dec 26, 2025
be755dd
Changing entrypoint again
Yukthiw Dec 26, 2025
737afaa
Setting homepage variable
Yukthiw Dec 26, 2025
99ac02c
Added workaround for github pages
Yukthiw Dec 26, 2025
59908da
refactored mask modal
Yukthiw Jan 5, 2026
8fefe7a
Worldefp conversion to routing
Yukthiw Jan 5, 2026
eb8b179
Fixing local url navigation
Yukthiw Jan 5, 2026
6fa358c
Added state management and reconstruction from url params
Yukthiw Jan 5, 2026
371812c
Fixed genedist chart
Yukthiw Jan 8, 2026
b38bce5
Added map selector
Yukthiw Jan 9, 2026
b5d573d
formatting
Yukthiw Jan 9, 2026
755db80
Icon change
Yukthiw Jan 9, 2026
da85a9a
Precipitation Overlay
Yukthiw Apr 12, 2026
65a84ca
Climate legends
Yukthiw Apr 16, 2026
9df19da
Fixed rendering of info
Yukthiw Apr 16, 2026
8446c0b
Max zoom capped at 8
Yukthiw Apr 16, 2026
cdb6ea6
Added tile fetching from url
Yukthiw Jun 21, 2026
05bfd0e
deleting temp files
Yukthiw Jun 21, 2026
a811801
reverting some stuff
Yukthiw Jun 21, 2026
9a16090
removing claude.md
Yukthiw Jun 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
branches-ignore:
- main
- staging
- staging-debug
pull_request:
jobs:
build:
Expand Down
4 changes: 2 additions & 2 deletions Eplant/config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import InteractionsViewer from './views/InteractionsViewer'
import NavigatorView from './views/NavigatorView'
import PlantEFP from './views/PlantEFP'
import PublicationViewer from './views/PublicationViewer'
// import WorldEFP from './views/WorldEFP'
import WorldEFP from './views/WorldEFP'
import { type ViewMetadata } from './View'

export type EplantConfig = {
Expand All @@ -34,7 +34,7 @@ const userViewMetadata = [
PlantEFP,
CellEFP,
ExperimentEFP,
// WorldEFP,
WorldEFP,
ChromosomeViewerObject,
NavigatorView,
InteractionsViewer,
Expand Down
5 changes: 5 additions & 0 deletions Eplant/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { InteractionsViewObject } from './views/InteractionsViewer/InteractionsV
import { NavigatorViewObject } from './views/NavigatorView/NavigatorView'
import { PlantEFPView } from './views/PlantEFP/PlantEFP'
import { PublicationsViewer } from './views/PublicationViewer/PublicationsView'
import { WorldEFPView } from './views/WorldEFP/WorldEFP'
import { Config, defaultConfig } from './config'
import Eplant from './Eplant'

Expand Down Expand Up @@ -65,6 +66,10 @@ const router = createBrowserRouter(
path: 'interactions-view/:geneid?',
element: <InteractionsViewObject></InteractionsViewObject>,
},
{
path: 'world-efp/:geneid?',
element: <WorldEFPView></WorldEFPView>,
},
],
errorElement: <ErrorBoundary></ErrorBoundary>,
},
Expand Down
69 changes: 69 additions & 0 deletions Eplant/views/WorldEFP/ClimateOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useEffect } from 'react'

import { useMap } from '@vis.gl/react-google-maps'

import { getOverlayMaxZoom, getTileUrl } from './overlayTiles'
import { OverlayType } from './types'

/**
* Normalizes tile coordinates per Google Maps conventions.
* - Does NOT repeat across the y-axis (returns null for out-of-bounds y)
* - Wraps across the x-axis (world repeats horizontally)
*
* Ported from the original WorldView implementation.
*/
function normalizeTileCoord(
coord: google.maps.Point,
zoom: number
): { x: number; y: number } | null {
const tileRange = 1 << zoom
const y = coord.y
let x = coord.x

if (y < 0 || y >= tileRange) return null
if (x < 0 || x >= tileRange) {
x = ((x % tileRange) + tileRange) % tileRange
}
return { x, y }
}

interface ClimateOverlayProps {
overlay: OverlayType
}

const ClimateOverlay = ({ overlay }: ClimateOverlayProps) => {
const map = useMap('WorldEFP')

useEffect(() => {
if (!map || !window.google?.maps || overlay === OverlayType.None) return

const typedOverlay = overlay as Exclude<OverlayType, OverlayType.None>
const maxZoom = getOverlayMaxZoom(typedOverlay)

const layer = new window.google.maps.ImageMapType({
getTileUrl: (coord: google.maps.Point, zoom: number): string | null => {
if (zoom > maxZoom) return null

const normalized = normalizeTileCoord(coord, zoom)
if (!normalized) return null

const { x, y } = normalized
return getTileUrl(typedOverlay, zoom, x, y)
},
tileSize: new window.google.maps.Size(256, 256),
opacity: 0.7,
name: overlay,
})

map.overlayMapTypes.push(layer)

return () => {
const index = map.overlayMapTypes.getArray().indexOf(layer)
if (index !== -1) map.overlayMapTypes.removeAt(index)
}
}, [map, overlay])

return null
}

export default ClimateOverlay
9 changes: 8 additions & 1 deletion Eplant/views/WorldEFP/InfoContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ const InfoContent = ({ id, mean, std, sampleSize }: InfoContentProps) => {
return (
<StyledInfoContent>
<p>
<strong>{id}</strong>
<strong>
{id.split(/<br\s*\/?>/i).map((line, i, arr) => (
<span key={i}>
{line}
{i < arr.length - 1 && <br />}
</span>
))}
</strong>
</p>
<p>Mean: {mean.toFixed(2)}</p>
<p>Standard error: {std.toFixed(2)}</p>
Expand Down
95 changes: 74 additions & 21 deletions Eplant/views/WorldEFP/MapContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useCallback } from 'react'
import { useEffect } from 'react'

import { ViewDispatch } from '@eplant/View'
import { useTheme } from '@mui/material'
import { Box, useTheme } from '@mui/material'
import { alpha } from '@mui/material/styles'
import {
APIProvider,
Map,
MapCameraChangedEvent,
MapEvent,
Expand All @@ -14,48 +13,53 @@ import { getColor } from '../eFP/svg'
import GeneDistributionChart from '../eFP/Viewer/GeneDistributionChart'
import Legend from '../eFP/Viewer/legend'

import ClimateOverlay from './ClimateOverlay'
import MapMarker from './MapMarker'
import { WorldEFPAction, WorldEFPData, WorldEFPState } from './types'
import MapTypeSelector from './MapTypeSelector'
import OverlayLegend from './OverlayLegend'
import OverlaySelector from './OverlaySelector'
import { ColorMode, WorldEFPData, WorldEFPState } from './types'

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

// set map state on load from cache, url or default
useEffect(() => {
map?.moveCamera({ zoom: state.zoom, center: state.position })
}, [map])

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,
})
setState({ ...state, position: coords })
}

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

return (
<Map
defaultCenter={state.position}
defaultZoom={2}
maxZoom={8}
mapId={import.meta.env.VITE_MAP_ID}
streetViewControl={false}
mapTypeId={'roadmap'}
mapTypeId={state.mapTypeId}
mapTypeControl={false}
onDragend={hangleDragEnd}
onZoomChanged={handleZoom}
id='WorldEFP'
>
<ClimateOverlay overlay={state.overlay} />
{activeData.positions.map((pos, index) => {
const color = getColor(
activeData.efpData.groups[index].mean,
Expand All @@ -77,17 +81,66 @@ const MapContainer = ({ activeData, state, dispatch }: MapContainerProps) => {
></MapMarker>
)
})}
<Legend
<Box
sx={(theme) => ({
position: 'absolute',
left: theme.spacing(2),
top: theme.spacing(2),
zIndex: 10,
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(1),
})}
>
<MapTypeSelector
mapTypeId={state.mapTypeId}
onSelect={(mapTypeId) => setState({ ...state, mapTypeId })}
/>
<OverlaySelector
overlay={state.overlay}
onSelect={(overlay) => setState({ ...state, overlay })}
/>
</Box>
<Box
sx={(theme) => ({
position: 'absolute',
left: theme.spacing(2),
bottom: theme.spacing(4),
zIndex: 10,
display: 'flex',
flexDirection: 'row',
alignItems: 'flex-end',
gap: theme.spacing(1),
})}
colorMode={'absolute'}
data={activeData.efpData}
></Legend>
<GeneDistributionChart data={activeData.efpData} />
>
<Box
sx={(theme) => ({
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
gap: theme.spacing(1),
padding: theme.spacing(1),
borderRadius: theme.spacing(1),
backgroundColor: alpha(theme.palette.background.active, 0.4),
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.18)',
border: `1px solid ${alpha(theme.palette.background.edge, 0.7)}`,
backdropFilter: 'blur(8px)',
WebkitBackdropFilter: 'blur(8px)',
})}
>
<GeneDistributionChart
data={activeData.efpData}
containerStyle={{
position: 'static',
width: 'auto',
height: 'auto',
marginLeft: '-12px',
}}
/>
<Legend colorMode={ColorMode.Absolute} data={activeData.efpData} />
</Box>
<OverlayLegend overlay={state.overlay} />
</Box>
</Map>
)
}
Expand Down
113 changes: 113 additions & 0 deletions Eplant/views/WorldEFP/MapTypeSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { useState } from 'react'

import MapIcon from '@mui/icons-material/Map'
import { Box, Button, Collapse } from '@mui/material'
import { alpha } from '@mui/material/styles'

import { MapTypeId, WorldEFPState } from './types'

const MAP_TYPES = Object.values(MapTypeId)

type MapTypeSelectorProps = {
mapTypeId: WorldEFPState['mapTypeId']
onSelect: (mapTypeId: WorldEFPState['mapTypeId']) => void
}

const MapTypeSelector = ({ mapTypeId, onSelect }: MapTypeSelectorProps) => {
const [isOpen, setIsOpen] = useState(false)

const handleMapTypeSelect = (nextType: MapTypeId) => {
onSelect(nextType)
setIsOpen(false)
}

const formatMapTypeLabel = (type: MapTypeId) =>
type.charAt(0).toUpperCase() + type.slice(1)

return (
<Box
sx={(theme) => ({
width: isOpen ? theme.spacing(15) : theme.spacing(5),
borderRadius: theme.spacing(1),
backgroundColor: alpha(theme.palette.background.active, 0.7),
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.18)',
border: `1px solid ${alpha(theme.palette.background.active, 0.7)}`,
backdropFilter: 'blur(8px)',
WebkitBackdropFilter: 'blur(8px)',
overflow: 'hidden',
transition: 'width 220ms ease, box-shadow 220ms ease',
})}
>
<Button
variant='text'
onClick={() => setIsOpen((open) => !open)}
sx={(theme) => ({
width: '100%',
justifyContent: 'flex-start',
textTransform: 'none',
color: theme.palette.text.primary,
padding: theme.spacing(1),
'&:hover': {
backgroundColor: alpha(theme.palette.background.active, 0.85),
},
})}
>
<MapIcon fontSize='small' />
<Box
sx={(theme) => ({
marginLeft: theme.spacing(1),
opacity: isOpen ? 1 : 0,
maxWidth: isOpen ? theme.spacing(12) : 0,
overflow: 'hidden',
whiteSpace: 'nowrap',
transition: 'opacity 200ms ease, max-width 220ms ease',
})}
>
{formatMapTypeLabel(mapTypeId)}
</Box>
</Button>
<Collapse in={isOpen} timeout={200} unmountOnExit>
<Box
sx={(theme) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(0.5),
padding: theme.spacing(0.5, 1, 1),
})}
>
{MAP_TYPES.map((type) => (
<Button
key={type}
variant='text'
onClick={() => handleMapTypeSelect(type as MapTypeId)}
sx={(theme) => ({
justifyContent: 'flex-start',
textTransform: 'none',
color: theme.palette.text.primary,
padding: theme.spacing(0.5, 1),
'&:hover': {
backgroundColor: alpha(theme.palette.background.active, 0.45),
},
...(mapTypeId === type && {
fontWeight: 600,
backgroundColor: alpha(theme.palette.background.active, 0.6),
borderRadius: theme.spacing(0.75),
'&:hover': {
backgroundColor: alpha(
theme.palette.background.active,
0.7
),
},
}),
})}
>
{formatMapTypeLabel(type)}
</Button>
))}
</Box>
</Collapse>
</Box>
)
}

export default MapTypeSelector
Loading
Loading