Skip to content

Commit

Permalink
refactor(frontend): interaction layer state (#45)
Browse files Browse the repository at this point in the history
Build interaction layer state dynamically from the interaction group config. `hoverLayerStates` is a map of layer IDs to layer hover states and layer hover targets.

```js
const layerStates = useRecoilValue(layerHoverStates);
const { isHovered, target } = layerStates.get('hazards');
```

Move interaction state from `src/lib/data-map/interactions` to `src/state/interactions`.
  • Loading branch information
eatyourgreens authored Jun 26, 2024
1 parent 27ec7ba commit bb342a3
Show file tree
Hide file tree
Showing 23 changed files with 61 additions and 92 deletions.
2 changes: 1 addition & 1 deletion frontend/src/config/assets/asset-view-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import { selectableMvtLayer } from 'lib/deck/layers/selectable-mvt-layer';
import { getAssetDataFormats } from './data-formats';
import { ASSETS_SOURCE } from './source';
import { VectorTarget } from 'lib/data-map/interactions/use-interactions';
import { VectorTarget } from 'state/interactions/use-interactions';

interface ViewLayerMetadata {
group: string;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/config/interaction-groups.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InteractionGroupConfig } from 'lib/data-map/interactions/use-interactions';
import { InteractionGroupConfig } from 'state/interactions/use-interactions';

export const INTERACTION_GROUPS = new Map<string, InteractionGroupConfig>([
[
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/config/regions/population-view-layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { featureProperty } from 'lib/deck/props/data-source';
import { border, fillColor } from 'lib/deck/props/style';
import { RegionLevel } from './metadata';
import { REGIONS_SOURCE } from './source';
import { VectorTarget } from 'lib/data-map/interactions/use-interactions';
import { VectorTarget } from 'state/interactions/use-interactions';

export function populationViewLayer(regionLevel: RegionLevel): ViewLayer {
const source = REGIONS_SOURCE;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/details/DeselectButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Close } from '@mui/icons-material';
import { IconButton } from '@mui/material';
import { selectionState } from 'lib/data-map/interactions/interaction-state';
import { selectionState } from 'state/interactions/interaction-state';
import { useResetRecoilState } from 'recoil';

export const DeselectButton = ({ interactionGroup, title }) => {
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/details/features/FeatureSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { FC } from 'react';

import { FeatureSidebarContent } from './FeatureSidebarContent';
import { useRecoilValue } from 'recoil';
import { selectionState } from 'lib/data-map/interactions/interaction-state';
import { selectionState } from 'state/interactions/interaction-state';
import { SidePanel } from 'details/SidePanel';
import { ErrorBoundary } from 'lib/react/ErrorBoundary';
import { Box } from '@mui/system';
import { DeselectButton } from 'details/DeselectButton';
import { MobileTabContentWatcher } from 'pages/map/layouts/mobile/tab-has-content';
import { InteractionTarget, VectorTarget } from 'lib/data-map/interactions/use-interactions';
import { InteractionTarget, VectorTarget } from 'state/interactions/use-interactions';

export const FeatureSidebar: FC = () => {
const featureSelection = useRecoilValue(selectionState('assets'));
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/details/regions/RegionDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useRecoilValue } from 'recoil';

import { selectionState } from 'lib/data-map/interactions/interaction-state';
import { selectionState } from 'state/interactions/interaction-state';
import { Box } from '@mui/material';
import { RegionDetailsContent } from './RegionDetailsContent';
import { SidePanel } from 'details/SidePanel';
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/details/regions/RegionDetailsContent.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Typography } from '@mui/material';
import { REGIONS_METADATA } from 'config/regions/metadata';
import { DataItem } from 'details/features/detail-components';
import { InteractionTarget } from 'lib/data-map/interactions/use-interactions';
import { InteractionTarget } from 'state/interactions/use-interactions';
import { numFormat } from 'lib/helpers';
import { FC } from 'react';

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/details/solutions/SolutionsSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Box } from '@mui/material';
import { DeselectButton } from 'details/DeselectButton';
import { SidePanel } from 'details/SidePanel';
import { selectionState } from 'lib/data-map/interactions/interaction-state';
import { selectionState } from 'state/interactions/interaction-state';
import { ErrorBoundary } from 'lib/react/ErrorBoundary';
import { FC } from 'react';
import { useRecoilValue } from 'recoil';
import { SolutionsSidebarContent } from './SolutionsSidebarContent';
import { MobileTabContentWatcher } from 'pages/map/layouts/mobile/tab-has-content';
import { InteractionTarget, VectorTarget } from 'lib/data-map/interactions/use-interactions';
import { InteractionTarget, VectorTarget } from 'state/interactions/use-interactions';

export const SolutionsSidebar: FC = () => {
const featureSelection = useRecoilValue(selectionState('solutions'));
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/data-map/DataMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { useTriggerMemo } from '../hooks/use-trigger-memo';
import { useDataLoadTrigger } from './use-data-load-trigger';

import { DeckGLOverlay } from '../map/DeckGLOverlay';
import { useInteractions } from './interactions/use-interactions';
import { useInteractions } from 'state/interactions/use-interactions';
import { ViewLayer, ViewLayerParams } from './view-layers';
import { LayersList } from 'deck.gl/typed';

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/data-map/DataMapTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { FC } from 'react';
import { useRecoilValue } from 'recoil';

import { hoverPositionState } from './interactions/interaction-state';
import { hoverPositionState } from 'state/interactions/interaction-state';

export const DataMapTooltip: FC<{ children: React.ReactNode }> = ({ children }) => {
const tooltipXY = useRecoilValue(hoverPositionState);
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/lib/data-map/view-layers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ScaleSequential } from 'd3-scale';
import { DataLoader } from 'lib/data-loader/data-loader';
import { Accessor } from 'lib/deck/props/getters';
import { InteractionTarget, VectorTarget, RasterTarget } from './interactions/use-interactions';
import { InteractionTarget, VectorTarget, RasterTarget } from 'state/interactions/use-interactions';

export interface FieldSpec {
fieldGroup: string;
Expand Down
88 changes: 19 additions & 69 deletions frontend/src/map/tooltip/TooltipContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,97 +5,47 @@ import { useRecoilValue } from 'recoil';
import { VectorHoverDescription } from './content/VectorHoverDescription';
import { RasterHoverDescription } from './content/RasterHoverDescription';
import { RegionHoverDescription } from './content/RegionHoverDescription';
import { hasHover, hoverState } from 'lib/data-map/interactions/interaction-state';
import {
InteractionTarget,
VectorTarget,
RasterTarget,
} from 'lib/data-map/interactions/use-interactions';
import { showPopulationState } from 'state/regions';
import { layerHoverStates } from 'state/interactions/interaction-state';
import { InteractionTarget, VectorTarget, RasterTarget } from 'state/interactions/use-interactions';
import { SolutionHoverDescription } from './content/SolutionHoverDescription';
import { DroughtHoverDescription } from './content/DroughtHoverDescription';
import { ErrorBoundary } from 'lib/react/ErrorBoundary';

type MapDataLayer = InteractionTarget<VectorTarget | RasterTarget>;
type TooltipLayer = {
component: FC<{ hoveredObject: MapDataLayer }>;
target: MapDataLayer | MapDataLayer[] | null;
};

const TooltipSection = ({ children }) => (
<Box p={1} borderBottom="1px solid #ccc">
{children}
</Box>
);

const useLayerTarget = (layerId: string) => useRecoilValue(hoverState(layerId));
/**
* Define tooltip properties for data layers.
* @returns a map of tooltip components and their respective data layers, mapped by layer ID.
*/
function useTooltipLayers(): Map<string, TooltipLayer> {
return new Map<string, TooltipLayer>([
[
'assets',
{
component: VectorHoverDescription,
target: useLayerTarget('assets'),
},
],
[
'hazards',
{
component: RasterHoverDescription,
target: useLayerTarget('hazards'),
},
],
[
'regions',
{
component: RegionHoverDescription,
target: useLayerTarget('regions'),
},
],
[
'solutions',
{
component: SolutionHoverDescription,
target: useLayerTarget('solutions'),
},
],
[
'drought',
{
component: DroughtHoverDescription,
target: useLayerTarget('drought'),
},
],
]);
}

export const TooltipContent: FC = () => {
const layers = useTooltipLayers();
const layerEntries = [...layers.entries()];
const regionDataShown = useRecoilValue(showPopulationState);
const tooltipLayers: Map<string, FC<{ hoveredObject: MapDataLayer }>> = new Map<
string,
FC<{ hoveredObject: MapDataLayer }>
>([
['assets', VectorHoverDescription],
['hazards', RasterHoverDescription],
['regions', RegionHoverDescription],
['solutions', SolutionHoverDescription],
['drought', DroughtHoverDescription],
]);
const layerEntries = [...tooltipLayers.entries()];

function hasHovered([type, { target }]) {
if (type === 'regions' && !regionDataShown) {
return false;
}
return hasHover(target);
}
export const TooltipContent: FC = () => {
const layerStates = useRecoilValue(layerHoverStates);

const doShow = layerEntries.some(hasHovered);
const doShow = [...layerStates.values()].some(({ isHovered }) => isHovered);

if (!doShow) return null;

return (
<Paper>
<Box minWidth={200}>
<ErrorBoundary message="There was a problem displaying the tooltip.">
{layerEntries.map((layer) => {
const [type, { component: Component, target }] = layer;
if (hasHovered(layer)) {
{layerEntries.map(([type, Component]) => {
const { isHovered, target } = layerStates.get(type);
if (isHovered) {
if (Array.isArray(target)) {
return (
<TooltipSection key={type}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Typography } from '@mui/material';
import { DataItem } from 'details/features/detail-components';
import { InteractionTarget, VectorTarget } from 'lib/data-map/interactions/use-interactions';
import { InteractionTarget, VectorTarget } from 'state/interactions/use-interactions';
import { FC, useMemo } from 'react';
import { useRecoilValue } from 'recoil';
import {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FC, useMemo } from 'react';

import { InteractionTarget, RasterTarget } from 'lib/data-map/interactions/use-interactions';
import { InteractionTarget, RasterTarget } from 'state/interactions/use-interactions';

import { RASTER_COLOR_MAPS } from 'config/color-maps';
import { HAZARDS_METADATA } from 'config/hazards/metadata';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { REGIONS_METADATA } from '../../../config/regions/metadata';
import { InteractionTarget, VectorTarget } from 'lib/data-map/interactions/use-interactions';
import { InteractionTarget, VectorTarget } from 'state/interactions/use-interactions';
import { useRecoilValue } from 'recoil';
import { showPopulationState } from 'state/regions';
import { DataItem } from 'details/features/detail-components';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Typography } from '@mui/material';
import { VECTOR_COLOR_MAPS } from 'config/color-maps';
import { MARINE_HABITATS_LOOKUP } from 'config/solutions/domains';
import { DataItem } from 'details/features/detail-components';
import { InteractionTarget, VectorTarget } from 'lib/data-map/interactions/use-interactions';
import { InteractionTarget, VectorTarget } from 'state/interactions/use-interactions';
import startCase from 'lodash/startCase';
import { FC } from 'react';
import { habitatColorMap } from 'state/layers/marine';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Typography } from '@mui/material';
import { NETWORKS_METADATA } from 'config/networks/metadata';
import { DataItem } from 'details/features/detail-components';
import { InteractionTarget, VectorTarget } from 'lib/data-map/interactions/use-interactions';
import { InteractionTarget, VectorTarget } from 'state/interactions/use-interactions';
import { FC } from 'react';
import { useRecoilValue } from 'recoil';
import { singleViewLayerParamsState } from 'state/layers/view-layers-params';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import forEach from 'lodash/forEach';
import { atom, atomFamily, selector } from 'recoil';

import { INTERACTION_GROUPS } from 'config/interaction-groups';
import { isReset } from 'lib/recoil/is-reset';
import { showPopulationState } from 'state/regions';

import { InteractionTarget, RasterTarget, VectorTarget } from './use-interactions';

type InteractionLayer = InteractionTarget<VectorTarget> | InteractionTarget<RasterTarget>;
type IT = InteractionLayer | InteractionLayer[];

const interactionGroupIds = [...INTERACTION_GROUPS.keys()];

export function hasHover(target: IT) {
if (Array.isArray(target)) {
return target.length > 0;
Expand All @@ -20,6 +24,21 @@ export const hoverState = atomFamily<IT, string>({
default: null,
});

type LayerHoverState = { isHovered: boolean; target: IT };
export const layerHoverStates = selector({
key: 'layerHoverStates',
get: ({ get }) => {
const regionDataShown = get(showPopulationState);
const mapEntries = interactionGroupIds.map((group) => {
const target = get(hoverState(group));
const isHovered =
group === 'regions' ? regionDataShown && hasHover(target) : hasHover(target);
return [group, { isHovered, target }] as [string, LayerHoverState];
});
return new Map<string, LayerHoverState>(mapEntries);
},
});

export const hoverPositionState = atom({
key: 'hoverPosition',
default: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import mapValues from 'lodash/mapValues';
import { useCallback, useEffect, useMemo } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';

import { ViewLayer } from '../view-layers';
import { ViewLayer } from 'lib/data-map/view-layers';

import {
hoverState,
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/state/layers/drought.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
DROUGHT_RISK_VARIABLES_WITH_RCP,
} from 'config/drought/metadata';
import { colorMap } from 'lib/color-map';
import { VectorTarget } from 'lib/data-map/interactions/use-interactions';
import { VectorTarget } from 'state/interactions/use-interactions';
import { ColorSpec, FieldSpec, ViewLayer } from 'lib/data-map/view-layers';
import { selectableMvtLayer } from 'lib/deck/layers/selectable-mvt-layer';
import { dataColorMap } from 'lib/deck/props/color-map';
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/state/layers/marine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { fillColor } from 'lib/deck/props/style';
import { Accessor } from 'lib/deck/props/getters';
import { marineFiltersState } from 'state/solutions/marine-filters';
import { selectableMvtLayer } from 'lib/deck/layers/selectable-mvt-layer';
import { VectorTarget } from 'lib/data-map/interactions/use-interactions';
import { VectorTarget } from 'state/interactions/use-interactions';

export function habitatColorMap(x: string) {
return MARINE_HABITAT_COLORS[x]?.css ?? MARINE_HABITAT_COLORS['other'].css;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/state/layers/terrestrial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { selectableMvtLayer } from 'lib/deck/layers/selectable-mvt-layer';
import { getTerrestrialDataFormats } from 'config/solutions/data-formats';
import { getSolutionsDataAccessor } from 'config/solutions/data-access';
import { landuseFilterState } from 'state/solutions/landuse-tree';
import { VectorTarget } from 'lib/data-map/interactions/use-interactions';
import { VectorTarget } from 'state/interactions/use-interactions';

export function landuseColorMap(x: string) {
return TERRESTRIAL_LANDUSE_COLORS[x].css;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/state/layers/view-layers-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { StyleParams, ViewLayer, ViewLayerParams } from 'lib/data-map/view-layer

import { viewLayersFlatState } from './view-layers-flat';
import _ from 'lodash';
import { selectionState } from 'lib/data-map/interactions/interaction-state';
import { selectionState } from 'state/interactions/interaction-state';
import { networkStyleParamsState } from './networks';

export const viewLayerState = atomFamily<ViewLayer, string>({
Expand Down

0 comments on commit bb342a3

Please sign in to comment.