- {id}
+
+ {id.split(/
/i).map((line, i, arr) => (
+
+ {line}
+ {i < arr.length - 1 &&
}
+
+ ))}
+
Mean: {mean.toFixed(2)}
Standard error: {std.toFixed(2)}
diff --git a/Eplant/views/WorldEFP/MapContainer.tsx b/Eplant/views/WorldEFP/MapContainer.tsx
index 2b405eca..4a3aa7dc 100644
--- a/Eplant/views/WorldEFP/MapContainer.tsx
+++ b/Eplant/views/WorldEFP/MapContainer.tsx
@@ -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,
@@ -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
+ 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 (
)
}
diff --git a/Eplant/views/WorldEFP/MapTypeSelector.tsx b/Eplant/views/WorldEFP/MapTypeSelector.tsx
new file mode 100644
index 00000000..3fac5a1d
--- /dev/null
+++ b/Eplant/views/WorldEFP/MapTypeSelector.tsx
@@ -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 (
+ ({
+ 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',
+ })}
+ >
+
+
+ ({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: theme.spacing(0.5),
+ padding: theme.spacing(0.5, 1, 1),
+ })}
+ >
+ {MAP_TYPES.map((type) => (
+
+ ))}
+
+
+
+ )
+}
+
+export default MapTypeSelector
diff --git a/Eplant/views/WorldEFP/OverlayLegend.tsx b/Eplant/views/WorldEFP/OverlayLegend.tsx
new file mode 100644
index 00000000..242f9bd6
--- /dev/null
+++ b/Eplant/views/WorldEFP/OverlayLegend.tsx
@@ -0,0 +1,107 @@
+import { Box, Typography } from '@mui/material'
+import { alpha } from '@mui/material/styles'
+
+import { OverlayType } from './types'
+
+interface OverlayLegendConfig {
+ title: [string, string]
+ imageSrc: string
+ max: string
+ min: string
+}
+
+const LEGEND_CONFIG: Record<
+ Exclude,
+ OverlayLegendConfig
+> = {
+ [OverlayType.Precipitation]: {
+ title: ['Annual', 'Precipitation'],
+ imageSrc: '/img/climateLegend.png',
+ max: '10577 mm',
+ min: '13 mm',
+ },
+ [OverlayType.HistoricalMinTemp]: {
+ title: ['Minimum', 'Temperature'],
+ imageSrc: '/img/climateLegendFlip.png',
+ max: '31.9 °C',
+ min: '-27.8 °C',
+ },
+ [OverlayType.HistoricalMaxTemp]: {
+ title: ['Maximum', 'Temperature'],
+ imageSrc: '/img/climateLegendFlip.png',
+ max: '31.9 °C',
+ min: '-27.8 °C',
+ },
+}
+
+interface OverlayLegendProps {
+ overlay: OverlayType
+}
+
+const OverlayLegend = ({ overlay }: OverlayLegendProps) => {
+ if (overlay === OverlayType.None) return null
+
+ const { title, imageSrc, max, min } = LEGEND_CONFIG[overlay]
+
+ return (
+ ({
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'flex-start',
+ gap: theme.spacing(0.5),
+ 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)',
+ })}
+ >
+ {/* Title */}
+
+ {title.map((line) => (
+ ({
+ display: 'block',
+ color: theme.palette.text.primary,
+ fontWeight: 600,
+ lineHeight: 1.2,
+ })}
+ >
+ {line}
+
+ ))}
+
+
+ {/* Scale image with max/min labels */}
+
+
+
+
+ {max}
+
+
+ {min}
+
+
+
+
+ )
+}
+
+export default OverlayLegend
diff --git a/Eplant/views/WorldEFP/OverlaySelector.tsx b/Eplant/views/WorldEFP/OverlaySelector.tsx
new file mode 100644
index 00000000..d8a11f62
--- /dev/null
+++ b/Eplant/views/WorldEFP/OverlaySelector.tsx
@@ -0,0 +1,121 @@
+import { useState } from 'react'
+
+import LayersIcon from '@mui/icons-material/Layers'
+import { Box, Button, Collapse } from '@mui/material'
+import { alpha } from '@mui/material/styles'
+
+import { OverlayType, WorldEFPState } from './types'
+
+const OVERLAY_LABELS: Record = {
+ [OverlayType.None]: 'None',
+ [OverlayType.Precipitation]: 'Annual Precip.',
+ [OverlayType.HistoricalMinTemp]: 'Min. Temp.',
+ [OverlayType.HistoricalMaxTemp]: 'Max. Temp.',
+}
+
+const OVERLAY_TYPES = Object.values(OverlayType)
+
+type OverlaySelectorProps = {
+ overlay: WorldEFPState['overlay']
+ onSelect: (overlay: WorldEFPState['overlay']) => void
+}
+
+const OverlaySelector = ({ overlay, onSelect }: OverlaySelectorProps) => {
+ const [isOpen, setIsOpen] = useState(false)
+
+ const handleSelect = (next: OverlayType) => {
+ onSelect(next)
+ setIsOpen(false)
+ }
+
+ return (
+ ({
+ width: isOpen ? theme.spacing(20) : 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',
+ })}
+ >
+
+
+ ({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: theme.spacing(0.5),
+ padding: theme.spacing(0.5, 1, 1),
+ minWidth: theme.spacing(20),
+ })}
+ >
+ {OVERLAY_TYPES.map((type) => (
+
+ ))}
+
+
+
+ )
+}
+
+export default OverlaySelector
diff --git a/Eplant/views/WorldEFP/WorldEFP.tsx b/Eplant/views/WorldEFP/WorldEFP.tsx
new file mode 100644
index 00000000..2d9664ad
--- /dev/null
+++ b/Eplant/views/WorldEFP/WorldEFP.tsx
@@ -0,0 +1,164 @@
+import { useEffect, useState } from 'react'
+import { useOutletContext } from 'react-router-dom'
+
+import GeneticElement from '@eplant/GeneticElement'
+import { useURLState } from '@eplant/state/URLStateProvider'
+import LoadingPage from '@eplant/UI/Layout/ViewContainer/LoadingPage'
+import { ViewContext } from '@eplant/UI/Layout/ViewContainer/types'
+import { ViewDataError } from '@eplant/View'
+import { useQuery } from '@tanstack/react-query'
+import { APIProvider } from '@vis.gl/react-google-maps'
+
+import { EFPData, EFPGroup } from '../eFP/types'
+import GeneDistributionChart from '../eFP/Viewer/GeneDistributionChart'
+import MaskModal from '../eFP/Viewer/MaskModal'
+
+import MapContainer from './MapContainer'
+import {
+ Coordinates,
+ WorldEFPData,
+ WorldEFPMicroArrayResponse,
+ WorldEFPState,
+ WorldEFPStateSchema,
+} from './types'
+import WorldEFP from '.'
+
+export const WorldEFPView = () => {
+ const { geneticElement } = useOutletContext()
+ const { state, setState, initializeState } = useURLState()
+ const [loadAmount, setLoadAmount] = useState(0)
+
+ const { data, isLoading, isError, error } = useQuery<
+ WorldEFPData,
+ ViewDataError
+ >({
+ queryKey: [`world-efp-${geneticElement?.id}`],
+ queryFn: async () => {
+ return worldEFPLoader(geneticElement, setLoadAmount)
+ },
+ })
+ useEffect(() => {
+ // On mount, initialize state
+ initializeState(WorldEFPStateSchema)
+ }, [])
+
+ if (!geneticElement) {
+ return (
+
+ )
+ } else if (isError) {
+ return (
+
+ )
+ } else if (isLoading && loadAmount < 100) {
+ return (
+
+ )
+ } else if (!data || !state) return <>>
+
+ return (
+ <>
+
+
+
+ {
+ setState({ ...state, maskModalVisible: !state.maskModalVisible })
+ }}
+ onSubmit={(threshold) => {
+ setState({
+ ...state,
+ maskThreshold: threshold,
+ maskingEnabled: !state.maskingEnabled,
+ maskModalVisible: !state.maskModalVisible,
+ })
+ }}
+ />
+ >
+ )
+}
+
+export const worldEFPLoader = async (
+ geneticElement: GeneticElement | null,
+ loadEvent: (loaded: number) => void
+) => {
+ if (!geneticElement) throw ViewDataError.UNSUPPORTED_GENE
+
+ const microArrayDataURL = `https://bar.utoronto.ca/api_dev/microarray_gene_expression/world_efp/arabidopsis/${geneticElement.id}`
+ const microArrayData: WorldEFPMicroArrayResponse = await fetch(
+ microArrayDataURL
+ )
+ .then((response) => {
+ if (!response.ok) {
+ throw new Error(`HTTP error! Status: ${response.status}`)
+ }
+ return response.json()
+ })
+ .catch((error) => {
+ console.error('Error fetching map marker data:', error)
+ throw error
+ })
+
+ const positions: Coordinates[] = []
+ const groupData: EFPGroup[] = []
+ Object.entries(microArrayData.data).forEach(([key, marker]) => {
+ // Coordinates
+ positions.push({
+ lat: parseFloat(marker.position.lat),
+ lng: parseFloat(marker.position.lng),
+ })
+ const samples = Object.values(marker.values)
+ // Sample data
+ const mean = samples.reduce((sum, value) => sum + value, 0) / samples.length
+ const efpGroupData = {
+ name: marker.id,
+ tissues: [],
+ mean: mean,
+ min: Math.min(...samples),
+ max: Math.max(...samples),
+ std: Math.sqrt(
+ samples.reduce((sum, value) => Math.pow(value - mean, 2)) /
+ (samples.length - 1)
+ ),
+ samples: samples.length,
+ }
+
+ groupData.push(efpGroupData)
+ })
+
+ const totalSamples = groupData.reduce((sum, group) => sum + group.samples, 0)
+ const totalMean =
+ groupData.reduce((sum, group) => sum + group.mean * group.samples, 0) /
+ totalSamples
+
+ const efpData = {
+ groups: groupData,
+ mean: totalMean,
+ min: Math.min(...groupData.map((group) => group.min)),
+ max: Math.max(...groupData.map((group) => group.max)),
+ std: 0, // This isn't needed, just set to 0 for convenience
+ samples: totalSamples,
+ } as EFPData
+
+ return {
+ positions: positions,
+ efpData: efpData,
+ }
+}
diff --git a/Eplant/views/WorldEFP/index.tsx b/Eplant/views/WorldEFP/index.tsx
index 08aa7000..0d0a117d 100644
--- a/Eplant/views/WorldEFP/index.tsx
+++ b/Eplant/views/WorldEFP/index.tsx
@@ -1,196 +1,46 @@
-// import GeneticElement from '@eplant/GeneticElement'
-// import { ViewDataError } from '@eplant/View/viewData'
-// import { APIProvider } from '@vis.gl/react-google-maps'
+import { StateAction, ViewMetadata } from '@eplant/View'
+import BuildRoundedIcon from '@mui/icons-material/BuildRounded'
+import ColorLensIcon from '@mui/icons-material/ColorLens'
+import PublicIcon from '@mui/icons-material/Public'
-// import { EFPData, EFPGroup } from '../eFP/types'
-// import MaskModal from '../eFP/Viewer/MaskModal'
-
-// import WorldEFPIcon from './icon'
-// import MapContainer from './MapContainer'
-// import {
-// Coordinates,
-// WorldEFPAction,
-// WorldEFPData,
-// WorldEFPMicroArrayResponse,
-// WorldEFPState,
-// } from './types'
-// import { ViewMetadata } from '@eplant/View'
-// const WorldEFP: ViewMetadata = {
-// name: 'World-EFP',
-// id: 'World-EFP',
-// getInitialState() {
-// return {
-// position: { lat: 25, lng: 0 },
-// zoom: 2,
-// mapTypeId: 'roadmap',
-// maskingEnabled: false,
-// maskModalVisible: false,
-// maskThreshold: 100,
-// colorMode: 'absolute',
-// }
-// },
-// async getInitialData(
-// gene: GeneticElement | null,
-// loadEvent: (progress: number) => void
-// ) {
-// if (!gene) throw ViewDataError.UNSUPPORTED_GENE
-// const microArrayDataURL = `https://bar.utoronto.ca/api_dev/microarray_gene_expression/world_efp/arabidopsis/${gene.id}`
-// const microArrayData: WorldEFPMicroArrayResponse = await fetch(
-// microArrayDataURL
-// )
-// .then((response) => {
-// if (!response.ok) {
-// throw new Error(`HTTP error! Status: ${response.status}`)
-// }
-// return response.json()
-// })
-// .catch((error) => {
-// console.error('Error fetching map marker data:', error)
-// throw error
-// })
-
-// const positions: Coordinates[] = []
-// const groupData: EFPGroup[] = []
-// Object.entries(microArrayData.data).forEach(([key, marker]) => {
-// // Coordinates
-// positions.push({
-// lat: parseFloat(marker.position.lat),
-// lng: parseFloat(marker.position.lng),
-// })
-// const samples = Object.values(marker.values)
-// // Sample data
-// const mean =
-// samples.reduce((sum, value) => sum + value, 0) / samples.length
-// const efpGroupData = {
-// name: marker.id,
-// tissues: [],
-// mean: mean,
-// min: Math.min(...samples),
-// max: Math.max(...samples),
-// std: Math.sqrt(
-// samples.reduce((sum, value) => Math.pow(value - mean, 2)) /
-// (samples.length - 1)
-// ),
-// samples: samples.length,
-// }
-
-// groupData.push(efpGroupData)
-// })
-
-// const totalSamples = groupData.reduce(
-// (sum, group) => sum + group.samples,
-// 0
-// )
-// const totalMean =
-// groupData.reduce((sum, group) => sum + group.mean * group.samples, 0) /
-// totalSamples
-
-// const efpData = {
-// groups: groupData,
-// mean: totalMean,
-// min: Math.min(...groupData.map((group) => group.min)),
-// max: Math.max(...groupData.map((group) => group.max)),
-// std: 0, // This isn't needed, just set to 0 for convenience
-// samples: totalSamples,
-// } as EFPData
-
-// return {
-// positions: positions,
-// efpData: efpData,
-// }
-// },
-// component({
-// geneticElement,
-// dispatch,
-// activeData,
-// state,
-// }: ViewProps) {
-// if (!geneticElement) return <>>
-// return (
-// <>
-//
-//
-//
-// dispatch({ type: 'toggle-mask-modal' })}
-// onSubmit={(threshold) =>
-// dispatch({
-// type: 'set-mask-threshold',
-// threshold: threshold,
-// })
-// }
-// />
-// >
-// )
-// },
-// icon: () => ,
-// description: '',
-// // TODO: If dark theme is active, use ThumbnailDark
-// citation({ gene }) {
-// return
-// },
-// reducer: (state: WorldEFPState, action: WorldEFPAction) => {
-// switch (action.type) {
-// case 'toggle-color-mode':
-// return {
-// ...state,
-// colorMode:
-// state.colorMode == 'absolute'
-// ? ('relative' as const)
-// : ('absolute' as const),
-// }
-// case 'toggle-mask-modal':
-// if (state.maskingEnabled) {
-// return {
-// ...state,
-// maskingEnabled: !state.maskingEnabled,
-// }
-// } else {
-// return {
-// ...state,
-// maskModalVisible: !state.maskModalVisible,
-// }
-// }
-// case 'set-mask-threshold':
-// return {
-// ...state,
-// maskThreshold: action.threshold,
-// maskingEnabled: !state.maskingEnabled,
-// maskModalVisible: !state.maskModalVisible,
-// }
-// case 'set-map-position':
-// return {
-// ...state,
-// position: action.position,
-// }
-// case 'set-map-zoom':
-// return {
-// ...state,
-// zoom: action.zoom,
-// }
-// default:
-// return state
-// }
-// },
-// actions: [
-// {
-// action: { type: 'reset-transform' },
-// render: () => <>Reset pan/zoom>,
-// },
-// {
-// action: { type: 'toggle-color-mode' },
-// render: (props) => <>Toggle data mode: {props.state.colorMode}>,
-// },
-// {
-// action: { type: 'toggle-mask-modal' },
-// render: () => <>Mask data>,
-// },
-// ],
-// }
-// export default WorldEFP
+import { WorldEFPData, WorldEFPState } from './types'
+const WorldEFP: ViewMetadata = {
+ name: 'World-EFP',
+ id: 'world-efp',
+ icon: () => ,
+ description: '',
+ // TODO: If dark theme is active, use ThumbnailDark
+ citation({ gene }) {
+ return
+ },
+ actions: [
+ {
+ name: 'Toggle Color Mode',
+ description: 'Toggle between absolute and relative color modes',
+ icon: ,
+ mutation: (prevState) => ({
+ ...prevState,
+ colorMode: prevState.colorMode == 'absolute' ? 'relative' : 'absolute',
+ }),
+ },
+ {
+ name: 'Toggle Masking',
+ description: 'Toggle colour masking',
+ icon: ,
+ mutation: (prevState) => {
+ if (prevState.maskingEnabled) {
+ return {
+ ...prevState,
+ maskingEnabled: !prevState.maskingEnabled,
+ }
+ } else {
+ return {
+ ...prevState,
+ maskModalVisible: !prevState.maskModalVisible,
+ }
+ }
+ },
+ },
+ ] as StateAction[],
+}
+export default WorldEFP
diff --git a/Eplant/views/WorldEFP/overlayTiles.ts b/Eplant/views/WorldEFP/overlayTiles.ts
new file mode 100644
index 00000000..36ff0e8a
--- /dev/null
+++ b/Eplant/views/WorldEFP/overlayTiles.ts
@@ -0,0 +1,53 @@
+import { OverlayType } from './types'
+
+export type TileMap = Record // "zoom_x_y" -> URL
+
+export type OverlayTileData = {
+ tileMap: TileMap
+ maxZoom: number
+}
+
+const BASE_PATH = '/temp_world_efp'
+
+/**
+ * Builds a tileMap by constructing public URLs for all tiles in a grid up to maxZoom.
+ * At each zoom level z the grid is 2^z x 2^z.
+ */
+function buildTileMap(
+ dir: string,
+ prefix: string,
+ maxZoom: number
+): OverlayTileData {
+ const tileMap: TileMap = {}
+ for (let zoom = 0; zoom <= maxZoom; zoom++) {
+ const count = 1 << zoom
+ for (let x = 0; x < count; x++) {
+ for (let y = 0; y < count; y++) {
+ tileMap[`${zoom}_${x}_${y}`] =
+ `${BASE_PATH}/${dir}/${prefix}&zoom=${zoom}&x=${x}&y=${y}.png`
+ }
+ }
+ }
+ return { tileMap, maxZoom }
+}
+
+/**
+ * Fetches overlay tile data for the given overlay type.
+ *
+ * Currently a mock backed by files in public/temp_world_efp.
+ * Replace each case body with a fetch() call to the real tile API endpoint
+ * when available — the return type stays the same.
+ */
+export async function fetchOverlayTiles(
+ overlay: Exclude
+): Promise {
+ switch (overlay) {
+ case OverlayType.Precipitation:
+ return buildTileMap('AnnualPrecip', 'Annual_Precipitation', 2)
+ case OverlayType.HistoricalMinTemp:
+ // No tiles yet — empty map causes getTileUrl to fall back to placeholder
+ return { tileMap: {}, maxZoom: 2 }
+ case OverlayType.HistoricalMaxTemp:
+ return { tileMap: {}, maxZoom: 2 }
+ }
+}
diff --git a/Eplant/views/WorldEFP/types.tsx b/Eplant/views/WorldEFP/types.tsx
index 9c7ba1e3..89434f2d 100644
--- a/Eplant/views/WorldEFP/types.tsx
+++ b/Eplant/views/WorldEFP/types.tsx
@@ -1,19 +1,44 @@
-import { ColorMode, EFPData } from '../eFP/types'
+import { z } from 'zod'
+
+import { EFPData } from '../eFP/types'
export type Coordinates = { lat: number; lng: number }
-type MapTypeId = 'roadmap' | 'satellite' | 'hybrid' | 'terrain'
+export enum MapTypeId {
+ Roadmap = 'roadmap',
+ Satellite = 'satellite',
+ Hybrid = 'hybrid',
+ Terrain = 'terrain',
+}
+
+export enum ColorMode {
+ Absolute = 'absolute',
+ Relative = 'relative',
+}
-export type WorldEFPState = {
- position: Coordinates
- zoom: number
- mapTypeId: MapTypeId
- maskModalVisible: boolean
- maskingEnabled: boolean
- maskThreshold: number
- colorMode: ColorMode
+export enum OverlayType {
+ None = 'None',
+ Precipitation = 'Precipitation',
+ HistoricalMinTemp = 'HistoricalMinTemp',
+ HistoricalMaxTemp = 'HistoricalMaxTemp',
}
+export const WorldEFPStateSchema = z.object({
+ position: z.object({
+ lat: z.number().default(25),
+ lng: z.number().default(0),
+ }),
+ zoom: z.number().min(0).max(8).default(2),
+ mapTypeId: z.nativeEnum(MapTypeId).default(MapTypeId.Roadmap),
+ maskModalVisible: z.boolean().default(false),
+ maskingEnabled: z.boolean().default(false),
+ maskThreshold: z.number().min(0).max(100).default(100),
+ colorMode: z.nativeEnum(ColorMode).default(ColorMode.Absolute),
+ overlay: z.nativeEnum(OverlayType).default(OverlayType.None),
+})
+
+export type WorldEFPState = z.infer
+
export type WorldEFPData = {
positions: Coordinates[]
efpData: EFPData
@@ -23,7 +48,6 @@ export interface WorldEFPMicroArrayResponse {
wasSuccessful: boolean
data: { [key: string]: WorldEFPMicroArrayData }
}
-
export interface WorldEFPMicroArrayData {
source: string
id: string
@@ -34,10 +58,3 @@ export interface WorldEFPMicroArrayData {
values: { [key: string]: number }
code: string
}
-export type WorldEFPAction =
- | { type: 'toggle-color-mode' }
- | { type: 'toggle-masking' }
- | { type: 'toggle-mask-modal' }
- | { type: 'set-mask-threshold'; threshold: number }
- | { type: 'set-map-position'; position: Coordinates }
- | { type: 'set-map-zoom'; zoom: number }
diff --git a/Eplant/views/eFP/Viewer/EFPViewer.tsx b/Eplant/views/eFP/Viewer/EFPViewer.tsx
index 42b815d5..c9bf6e4a 100644
--- a/Eplant/views/eFP/Viewer/EFPViewer.tsx
+++ b/Eplant/views/eFP/Viewer/EFPViewer.tsx
@@ -208,7 +208,7 @@ export const EFPViewer = ({
/>
setViewState({ ...state, maskModalVisible: false })
diff --git a/Eplant/views/eFP/Viewer/GeneDistributionChart.tsx b/Eplant/views/eFP/Viewer/GeneDistributionChart.tsx
index b6afbc1c..4513794d 100644
--- a/Eplant/views/eFP/Viewer/GeneDistributionChart.tsx
+++ b/Eplant/views/eFP/Viewer/GeneDistributionChart.tsx
@@ -1,13 +1,19 @@
import * as React from 'react'
-import { SVGProps, useEffect, useMemo, useState } from 'react'
+import { useEffect, useState } from 'react'
-import { useDarkMode } from '@eplant/state'
-import { Mail } from '@mui/icons-material'
import { useTheme } from '@mui/material'
import { EFPData } from '../types'
-const GeneDistributionChart = ({ data }: { data: EFPData }) => {
+type GeneDistributionChartProps = {
+ data: EFPData
+ containerStyle?: React.CSSProperties
+}
+
+const GeneDistributionChart = ({
+ data,
+ containerStyle,
+}: GeneDistributionChartProps) => {
const theme = useTheme()
const [geneRanking, setGeneRanking] = useState<{
[key: string]: string
@@ -41,10 +47,11 @@ const GeneDistributionChart = ({ data }: { data: EFPData }) => {
style={{
display: 'flex',
flexDirection: 'column',
- position: 'relative',
+ position: 'absolute',
zIndex: 10,
width: '100%',
height: '10%',
+ ...containerStyle,
}}
>
{geneRanking ? (
diff --git a/Eplant/views/eFP/Viewer/MaskModal.tsx b/Eplant/views/eFP/Viewer/MaskModal.tsx
index 56efaacf..bd90b2ac 100644
--- a/Eplant/views/eFP/Viewer/MaskModal.tsx
+++ b/Eplant/views/eFP/Viewer/MaskModal.tsx
@@ -10,25 +10,28 @@ import {
useTheme,
} from '@mui/material'
-import { EFPViewerState } from './types'
-
// Modal component with a slider
interface MaskModalProps {
isVisible: boolean
- state: EFPViewerState
+ threshold: number
onClose: () => void
onSubmit: (threshhold: number) => void
}
-const MaskModal = ({ isVisible, state, onClose, onSubmit }: MaskModalProps) => {
- const [sliderValue, setSliderValue] = useState(state.maskThreshold)
+const MaskModal = ({
+ isVisible,
+ threshold,
+ onClose,
+ onSubmit,
+}: MaskModalProps) => {
+ const [sliderValue, setSliderValue] = useState(threshold)
const theme = useTheme()
const handleSliderChange = (event: Event, newValue: number | number[]) => {
setSliderValue(newValue as number)
}
const handleClose = () => {
- setSliderValue(state.maskThreshold)
+ setSliderValue(threshold)
onClose()
}
diff --git a/index.html b/index.html
index a00036c6..dc22a37e 100644
--- a/index.html
+++ b/index.html
@@ -4,7 +4,30 @@
-
+
-
+
Eplant
diff --git a/package.json b/package.json
index 571621d0..9ee5b673 100644
--- a/package.json
+++ b/package.json
@@ -4,6 +4,7 @@
"description": "Eplant",
"main": "index.ts",
"type": "module",
+ "homepage": "https://bioanalyticresource.github.io/ePlant/",
"scripts": {
"test": "jest --passWithNoTests",
"dev": "vite",
diff --git a/public/404.html b/public/404.html
new file mode 100644
index 00000000..2a3ccbdd
--- /dev/null
+++ b/public/404.html
@@ -0,0 +1,35 @@
+
+
+
+
+
+ Redirecting…
+
+
+
+
diff --git a/public/img/climateLegend.png b/public/img/climateLegend.png
new file mode 100644
index 00000000..69f06122
Binary files /dev/null and b/public/img/climateLegend.png differ
diff --git a/public/img/climateLegendFlip.png b/public/img/climateLegendFlip.png
new file mode 100644
index 00000000..3842c0da
Binary files /dev/null and b/public/img/climateLegendFlip.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=0&x=0&y=0.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=0&x=0&y=0.png
new file mode 100644
index 00000000..ed5b940e
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=0&x=0&y=0.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=1&x=0&y=0.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=1&x=0&y=0.png
new file mode 100644
index 00000000..05922f54
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=1&x=0&y=0.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=1&x=0&y=1.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=1&x=0&y=1.png
new file mode 100644
index 00000000..c0390a39
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=1&x=0&y=1.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=1&x=1&y=0.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=1&x=1&y=0.png
new file mode 100644
index 00000000..e2829bd0
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=1&x=1&y=0.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=1&x=1&y=1.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=1&x=1&y=1.png
new file mode 100644
index 00000000..2e3f1c26
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=1&x=1&y=1.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=0&y=0.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=0&y=0.png
new file mode 100644
index 00000000..c4990492
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=0&y=0.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=0&y=1.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=0&y=1.png
new file mode 100644
index 00000000..06f4bfc8
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=0&y=1.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=0&y=2.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=0&y=2.png
new file mode 100644
index 00000000..ba589a24
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=0&y=2.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=0&y=3.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=0&y=3.png
new file mode 100644
index 00000000..409583ed
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=0&y=3.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=1&y=0.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=1&y=0.png
new file mode 100644
index 00000000..8c629e25
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=1&y=0.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=1&y=1.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=1&y=1.png
new file mode 100644
index 00000000..44e3a5b9
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=1&y=1.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=1&y=2.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=1&y=2.png
new file mode 100644
index 00000000..e35543db
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=1&y=2.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=1&y=3.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=1&y=3.png
new file mode 100644
index 00000000..409583ed
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=1&y=3.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=2&y=0.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=2&y=0.png
new file mode 100644
index 00000000..db7bc420
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=2&y=0.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=2&y=1.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=2&y=1.png
new file mode 100644
index 00000000..3835c650
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=2&y=1.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=2&y=2.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=2&y=2.png
new file mode 100644
index 00000000..477b1095
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=2&y=2.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=2&y=3.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=2&y=3.png
new file mode 100644
index 00000000..409583ed
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=2&y=3.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=3&y=0.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=3&y=0.png
new file mode 100644
index 00000000..caebf89d
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=3&y=0.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=3&y=1.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=3&y=1.png
new file mode 100644
index 00000000..8b5b0205
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=3&y=1.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=3&y=2.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=3&y=2.png
new file mode 100644
index 00000000..d5846eb2
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=3&y=2.png differ
diff --git a/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=3&y=3.png b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=3&y=3.png
new file mode 100644
index 00000000..409583ed
Binary files /dev/null and b/public/temp_world_efp/AnnualPrecip/Annual_Precipitation&zoom=2&x=3&y=3.png differ
diff --git a/public/temp_world_efp/tile-placeholder.png b/public/temp_world_efp/tile-placeholder.png
new file mode 100644
index 00000000..fa0a6410
Binary files /dev/null and b/public/temp_world_efp/tile-placeholder.png differ