Skip to content

Commit

Permalink
feat: pixel driller frontend (#251)
Browse files Browse the repository at this point in the history
First pass at a frontend for the pixel-driller API. Clicking anywhere on the map opens a righthand sidebar panel with tables of hazard information for that point.
  • Loading branch information
eatyourgreens authored Feb 6, 2025
1 parent a88df49 commit 717aa38
Show file tree
Hide file tree
Showing 12 changed files with 4,128 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"
}
}
}
64 changes: 64 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.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { StoryObj, Meta } from '@storybook/react';
import { expect, within } from '@storybook/test';
import { http, HttpResponse } from 'msw';

import mockPixelData from 'mocks/details/pixel-data/mockPixelData.json';
import { PixelData } from './PixelData';

function FixedWidthDecorator(Story) {
return (
<div style={{ width: '60ch' }}>
<Story />
</div>
);
}

const meta = {
title: 'Details/PixelData',
component: PixelData,
decorators: [FixedWidthDecorator],
} as Meta;

export default meta;

type Story = StoryObj<typeof meta>;

export const Default: Story = {
parameters: {
msw: {
handlers: [
http.get('/pixel/0.000/0.000', () => {
return HttpResponse.json(mockPixelData);
}),
],
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(await canvas.findByText('Cyclones: speed (m s-1)')).toBeTruthy();
expect(await canvas.findByText('River flooding: depth (m)')).toBeTruthy();
expect(await canvas.findByText('Surface flooding: depth (m)')).toBeTruthy();
const grids = await canvas.findAllByRole('grid');
expect(grids).toHaveLength(6);
grids.forEach((grid, i) => {

Check warning on line 43 in frontend/src/details/pixel-data/PixelData.stories.tsx

View workflow job for this annotation

GitHub Actions / build

'i' is defined but never used
const rowGroup = within(grid).getByRole('rowgroup');
expect(rowGroup).toBeTruthy();
const rows = within(rowGroup).getAllByRole('row');
expect(rows.length).toBeGreaterThan(0);
});
},
};
52 changes: 52 additions & 0 deletions frontend/src/details/pixel-data/PixelData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
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)];

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) => (
<Box key={hazard} mt={2}>
<PixelDataGrid hazard={hazard} />
</Box>
))}
</ErrorBoundary>
</SidePanel>
);
};
52 changes: 52 additions & 0 deletions frontend/src/details/pixel-data/PixelDataGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { DataGrid } from '@mui/x-data-grid';

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

import './hazard-table.css';
import { Typography } from '@mui/material';

const headings = {
cyclone: 'Cyclones',
fluvial: 'River flooding',
surface: 'Surface flooding',
coastal_mangrove: 'Coastal (mangrove)',
coastal_nomangrove: 'Coastal (no mangrove)',
coastal_nomangrove_minus_mangrove: 'Coastal (no mangrove - mangrove)',
};

const displayReturnPeriods = new Set([10, 20, 50, 100, 200, 500]);

export const PixelDataGrid = ({ hazard }) => {
const headers = useRecoilValue(pixelDrillerDataHeaders);
const rows = useRecoilValue(pixelDrillerDataRows(hazard));
const dataReturnPeriods = useRecoilValue(pixelDrillerDataRPs(hazard));
const columns = [
{ field: 'epoch', headerName: 'Epoch' },
{ field: 'rcp', headerName: 'RCP' },
];
const returnPeriods = displayReturnPeriods.intersection(dataReturnPeriods);

returnPeriods.forEach((rp) => {
columns.push({ field: `rp-${rp}`, headerName: `RP ${rp}` });
});
if (!headers.length) {
return null;
}
const variable = rows[0].variable;
const unit = rows[0].unit;

return (
<>
<Typography variant="subtitle2" component="h3">
{headings[hazard]}: {variable} ({unit})
</Typography>

<DataGrid columns={columns} rows={rows} density="compact" />
</>
);
};
3 changes: 3 additions & 0 deletions frontend/src/details/pixel-data/hazard-table.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.MuiDataGrid-footerContainer {
display: none;
}
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
Loading

0 comments on commit 717aa38

Please sign in to comment.