Skip to content

Commit

Permalink
feat: pixel driller frontend
Browse files Browse the repository at this point in the history
  • Loading branch information
eatyourgreens committed Jan 31, 2025
1 parent ee7d0fc commit 8771ab4
Show file tree
Hide file tree
Showing 9 changed files with 255 additions and 0 deletions.
7 changes: 7 additions & 0 deletions frontend/dev-proxy/proxy-table.dev.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,12 @@
"pathRewrite": {
"^/api": "/"
}
},
"/pixel": {
"target": "http://localhost",
"changeOrigin": true,
"pathRewrite": {
"^/pixel": "/pixel"
}
}
}
7 changes: 7 additions & 0 deletions frontend/dev-proxy/proxy-table.docker.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,12 @@
"pathRewrite": {
"^/api": "/api"
}
},
"/pixel": {
"target": "http://localhost",
"changeOrigin": true,
"pathRewrite": {
"^/pixel": "/pixel"
}
}
}
44 changes: 44 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"@mui/icons-material": "^6.1.0",
"@mui/lab": "^6.0.0-beta.9",
"@mui/material": "^6.1.0",
"@mui/x-data-grid": "^7.24.1",
"@mui/x-tree-view": "^7.5.0",
"@react-hook/debounce": "^4.0.0",
"@recoiljs/refine": "^0.1.1",
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/details/DetailsSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { AdaptationsSidebar } from './adaptations/AdaptationsSidebar';
import { FeatureSidebar } from './features/FeatureSidebar';
import { RegionDetails } from './regions/RegionDetails';
import { SolutionsSidebar } from './solutions/SolutionsSidebar';
import { PixelData } from './pixel-data/PixelData';

export const showAdaptationsTableState = selector<boolean>({
key: 'showAdaptationsTable',
Expand All @@ -18,6 +19,9 @@ export const DetailsSidebar = () => {
const showAdaptationsTable = useRecoilValue(showAdaptationsTableState);
return (
<>
<Box mb={2}>
<PixelData />
</Box>
<Box mb={2}>
<SolutionsSidebar />
</Box>
Expand Down
50 changes: 50 additions & 0 deletions frontend/src/details/pixel-data/PixelData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useRecoilValue, useSetRecoilState } from 'recoil';

import { Box, IconButton } from '@mui/material';
import { SidePanel } from 'details/SidePanel';
import { ErrorBoundary } from 'lib/react/ErrorBoundary';
import { MobileTabContentWatcher } from 'lib/map/layouts/tab-has-content';
import {
pixelDrillerDataHeaders,
pixelDrillerDataState,
pixelSelectionState,
} from 'lib/state/pixel-driller';
import { PixelDataGrid } from './PixelDataGrid';
import { Close } from '@mui/icons-material';

/**
* Display detailed information about a selected pixel (lat/lon point.)
*/
export const PixelData = () => {
const { data: selectedData } = useRecoilValue(pixelDrillerDataState);
const headers = useRecoilValue(pixelDrillerDataHeaders);
const setPixelSelection = useSetRecoilState(pixelSelectionState);

function clearSelectedLocation() {
setPixelSelection(null);
}

if (!selectedData) {
return null;
}
if (!headers.length) {
return null;
}
const hazards = [...new Set(selectedData.hazard)] as string[];

return (
<SidePanel position="relative">
<MobileTabContentWatcher tabId="details" />
<ErrorBoundary message="There was a problem displaying these details.">
<Box position="absolute" top={0} right={0} p={2}>
<IconButton onClick={clearSelectedLocation} title={'Close'}>
<Close />
</IconButton>
</Box>
{hazards.map((hazard) => (
<PixelDataGrid key={hazard} hazard={hazard} />
))}
</ErrorBoundary>
</SidePanel>
);
};
25 changes: 25 additions & 0 deletions frontend/src/details/pixel-data/PixelDataGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { DataGrid } from '@mui/x-data-grid';

import { pixelDrillerDataHeaders, pixelDrillerDataRows } from 'lib/state/pixel-driller';
import { useRecoilValue } from 'recoil';

const columns = [
{ field: 'variable', headerName: 'Variable' },
{ field: 'band_data', headerName: 'Band Data' },
{ field: 'unit', headerName: 'Unit' },
{ field: 'rp', headerName: 'Return Period' },
];

export const PixelDataGrid = ({ hazard }) => {
const headers = useRecoilValue(pixelDrillerDataHeaders);
const rows = useRecoilValue(pixelDrillerDataRows(hazard));
if (!headers.length) {
return null;
}
return (
<>
<h3>{hazard}</h3>
<DataGrid columns={columns} rows={rows} />
</>
);
};
4 changes: 4 additions & 0 deletions frontend/src/lib/state/interactions/use-interactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from './interaction-state';
import { RecoilStateFamily } from 'lib/recoil/types';
import { PickingInfo } from 'deck.gl/typed';
import { pixelSelectionState } from '../pixel-driller';

function processRasterTarget(info: any): RasterTarget {
const { bitmap, sourceLayer } = info;
Expand Down Expand Up @@ -118,6 +119,7 @@ export function useInteractions(

const setInteractionGroupHover = useSetInteractionGroupState(hoverState);
const setInteractionGroupSelection = useSetInteractionGroupState(selectionState);
const setPixelSelection = useSetRecoilState(pixelSelectionState);

const [primaryGroup] = [...interactionGroups.keys()];
const primaryGroupPickingRadius = interactionGroups.get(primaryGroup).pickingRadius;
Expand Down Expand Up @@ -176,6 +178,8 @@ export function useInteractions(
setInteractionGroupSelection(groupName, selectionTarget);
}
}
const [lon, lat] = info.coordinate;
setPixelSelection({ lon, lat });
};

/**
Expand Down
113 changes: 113 additions & 0 deletions frontend/src/lib/state/pixel-driller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { atom, noWait, RecoilState, RecoilValueReadOnly, selector, selectorFamily } from 'recoil';

const parameters = {
cyclone: {
epoch: 2010,
rcp: 'baseline',
confidence: 95,
},
fluvial: {
rcp: 'baseline',
epoch: 2010,
},
surface: {
rcp: 'baseline',
epoch: 2010,
},
};

const returnPeriods = [10, 20, 50, 100, 200, 500];

type PixelDrillerQueryParams = {
lat: number;
lon: number;
};

/**
* Latitude and longitude of the selected map pixel.
*/
export const pixelSelectionState: RecoilState<PixelDrillerQueryParams> = atom({
key: 'pixelSelection',
default: { lat: 0, lon: 0 },
});

/**
* Query to fetch hazard data for the selected map pixel.
*/
const pixelDrillerQuery = selector({
key: 'pixelDrillerQuery',
get: async ({ get }) => {
const { lat, lon } = get(pixelSelectionState);
const response = await fetch(`/pixel/${lon.toFixed(3)}/${lat.toFixed(3)}`);
const data = await response.json();
return data;
},
});

/**
* Loadable state for the current pixel driller data.
*/
export const pixelDrillerDataState = selector({
key: 'pixelDrillerDataState',
get: ({ get }) => {
const loadable = get(noWait(pixelDrillerQuery));
const data = loadable.state === 'hasValue' ? loadable.contents : null;
const error = loadable.state === 'hasError' ? loadable.contents : null;
return { data, error };
},
});

/**
* Column headers for the pixel driller data tables.
*/
export const pixelDrillerDataHeaders: RecoilValueReadOnly<string[]> = selector({
key: 'pixelDrillerDataHeaders',
get: ({ get }) => {
const pixelData = get(pixelDrillerDataState).data;
if (!pixelData) {
return [];
}
const headers = Object.keys(pixelData);
return headers;
},
});

function getPixelDataRows(pixelData, headers: string[], hazard: string) {
const rows = pixelData[headers[0]]
.map((_, rowNumber) => {
const row = { id: rowNumber };
headers.forEach((header) => {
row[header] = pixelData[header][rowNumber];
});
return row;
})
.map((row) => ({ ...row, band_data: row.band_data?.toFixed(2) }))
.filter((row) => row.hazard === hazard)
.filter((row) => returnPeriods.includes(row.rp))
.filter((row) => {
if (parameters[hazard]) {
const { rcp, epoch, confidence } = parameters[hazard];
if (confidence) {
return rcp === rcp && row.epoch === epoch && row.confidence === confidence;
}
return rcp === rcp && row.epoch === epoch;
}
return true;
});
return rows;
}

type Row = Record<string, string | number>;
/**
* Rows of pixel driller data for a specific hazard, epoch, RCP, and confidence level.
*/
export const pixelDrillerDataRows: (hazard: string) => RecoilValueReadOnly<Row[]> = selectorFamily({
key: 'pixelDrillerDataRows',
get:
(hazard: string) =>
({ get }) => {
const pixelData = get(pixelDrillerDataState).data;
const headers = get(pixelDrillerDataHeaders);
return getPixelDataRows(pixelData, headers, hazard);
},
});

0 comments on commit 8771ab4

Please sign in to comment.