Skip to content

Commit

Permalink
feat: Added StateMap component
Browse files Browse the repository at this point in the history
  • Loading branch information
joncursi committed Mar 7, 2024
1 parent d5f4686 commit 5b71f01
Show file tree
Hide file tree
Showing 9 changed files with 378 additions and 4 deletions.
File renamed without changes.
File renamed without changes.
8 changes: 4 additions & 4 deletions src/components/GeomarketMap/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { GeomarketMap, GeomarketMapProps } from '.';
export default {
args: {
data: defaultData,
geoJSONPath: '/maps/geomarket.json',
geoJSONPath: '/maps/geomarkets.json',
mapboxAccessToken: process.env.STORYBOOK_MAPBOX_ACCESS_TOKEN,
processDataFn: defaultProcessDataFn,
},
Expand All @@ -31,7 +31,7 @@ export default {
export const Default: StoryObj<GeomarketMapProps> = {
args: {
data: defaultData,
geoJSONPath: '/maps/geomarket.json',
geoJSONPath: '/maps/geomarkets.json',
mapboxAccessToken: process.env.STORYBOOK_MAPBOX_ACCESS_TOKEN,
processDataFn: defaultProcessDataFn,
},
Expand All @@ -40,7 +40,7 @@ export const Default: StoryObj<GeomarketMapProps> = {
export const SelectedGeomarket: StoryObj<GeomarketMapProps> = {
args: {
data: defaultData,
geoJSONPath: '/maps/geomarket.json',
geoJSONPath: '/maps/geomarkets.json',
processDataFn: defaultProcessDataFn,
selectedGeomarket: ['MS-01'],
},
Expand All @@ -49,7 +49,7 @@ export const SelectedGeomarket: StoryObj<GeomarketMapProps> = {
export const MultipleSelectedGeomarkets: StoryObj<GeomarketMapProps> = {
args: {
data: defaultData,
geoJSONPath: '/maps/geomarket.json',
geoJSONPath: '/maps/geomarkets.json',
processDataFn: defaultProcessDataFn,
selectedGeomarket: ['MS-01', 'MS-02'],
},
Expand Down
11 changes: 11 additions & 0 deletions src/components/StateMap/__snapshots__/index.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`StateMap ACT theme matches the snapshot 1`] = `<div />`;

exports[`StateMap ACT_ET theme matches the snapshot 1`] = `<div />`;

exports[`StateMap ENCOURA theme matches the snapshot 1`] = `<div />`;

exports[`StateMap ENCOURA_CLASSIC theme matches the snapshot 1`] = `<div />`;

exports[`StateMap ENCOURAGE theme matches the snapshot 1`] = `<div />`;
56 changes: 56 additions & 0 deletions src/components/StateMap/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Copyright (c) ACT, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @prettier
*/

import { Meta, StoryObj } from '@storybook/react';

import { Playground } from '~/helpers/playground';

import { defaultData, defaultProcessDataFn } from './mocks';

import { StateMap, StateMapProps } from '.';

export default {
args: {
data: defaultData,
geoJSONPath: '/maps/states.json',
mapboxAccessToken: process.env.STORYBOOK_MAPBOX_ACCESS_TOKEN,
processDataFn: defaultProcessDataFn,
},
argTypes: Playground({}, StateMap),
component: StateMap,
tags: ['autodocs'],
title: 'Molecules / Maps / StateMap',
} as Meta<StateMapProps>;

export const Default: StoryObj<StateMapProps> = {
args: {
data: defaultData,
geoJSONPath: '/maps/states.json',
mapboxAccessToken: process.env.STORYBOOK_MAPBOX_ACCESS_TOKEN,
processDataFn: defaultProcessDataFn,
},
};

export const SelectedState: StoryObj<StateMapProps> = {
args: {
data: defaultData,
geoJSONPath: '/maps/states.json',
processDataFn: defaultProcessDataFn,
selectedState: ['48'],
},
};

export const MultipleSelectedStates: StoryObj<StateMapProps> = {
args: {
data: defaultData,
geoJSONPath: '/maps/states.json',
processDataFn: defaultProcessDataFn,
selectedState: ['48', '40'],
},
};
26 changes: 26 additions & 0 deletions src/components/StateMap/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Copyright (c) ACT, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @prettier
*/

import { standard } from '~/helpers/test';

import { defaultData, defaultProcessDataFn } from './mocks';

import { StateMap } from '.';

describe('StateMap', () => {
const Component = (
<StateMap
data={defaultData}
geoJSONPath="/maps/states.json"
mapboxAccessToken=""
processDataFn={defaultProcessDataFn}
/>
);
standard(Component);
});
165 changes: 165 additions & 0 deletions src/components/StateMap/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/**
* Copyright (c) ACT, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @prettier
*/

import { useTheme } from '@mui/material/styles';
import bbox from '@turf/bbox';
import { isString } from 'lodash';
import numeral from 'numeral';
import React from 'react';

import Map, {
MapProps,
FeatureHoverProps,
InitialBoundsPositionProps,
} from '~/components/Map';
import MapPopup, { MapPopupProps } from '~/components/MapPopup';
import { IMapDataProps } from '~/types';

import type GeoJSON from 'geojson';

export interface StateMapProps {
data: Array<IMapDataProps>;
geoJSONPath: string;
mapboxAccessToken: string;
mapPopupProps?: Omit<MapPopupProps, 'popupProps'>;
mapProps?: Omit<MapProps, 'mapboxAccessToken'>;
processDataFn?: (
featureCollection: GeoJSON.FeatureCollection<GeoJSON.Geometry>,
data: Array<IMapDataProps>,
) => GeoJSON.FeatureCollection<GeoJSON.Geometry>;
onHoverInfo?: FeatureHoverProps;
selectedState?: Array<string>;
setOnHoverInfo?: (newHoverInfo: FeatureHoverProps | undefined) => void;
tooltipElement?: React.ReactElement;
}

/**
* StateMap component which under the hood uses mapbox and react-map-gl. For this to work it's necessary
* to add the link bellow in the head of your page: <link href='https://api.tiles.mapbox.com/mapbox-gl-js/v2.7.0/mapbox-gl.css' rel='stylesheet' />
* For more information: https://visgl.github.io/react-map-gl/docs/get-started/get-started#styling
*/
export const StateMap: React.FC<StateMapProps> = ({
data,
geoJSONPath,
onHoverInfo,
mapboxAccessToken,
mapProps,
mapPopupProps,
processDataFn,
selectedState,
setOnHoverInfo,
tooltipElement,
}): React.ReactElement<StateMapProps> => {
const { breakpoints, spacing } = useTheme();

const [statesJSON, setStatesJSON] =
React.useState<GeoJSON.FeatureCollection<GeoJSON.Geometry>>();
const [hoverInfo, setHoverInfo] = React.useState<FeatureHoverProps>();
const finalHoverInfo = onHoverInfo || hoverInfo;

React.useEffect(() => {
fetch(geoJSONPath)
.then(resp => resp.json())
.then(json =>
setStatesJSON(json as GeoJSON.FeatureCollection<GeoJSON.Geometry>),
)
.catch(err => console.error('Could not load data', err)); // eslint-disable-line
}, []);

const processedData = React.useMemo(() => {
return statesJSON && processDataFn && processDataFn(statesJSON, data);
}, [data, statesJSON, processDataFn]);

const initialBoundsPosition = React.useMemo(():
| InitialBoundsPositionProps
| undefined => {
if (selectedState && processedData) {
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
const features = processedData.features.filter(f =>
selectedState.includes(f.id as string),
);

if (features.length > 0) {
const [minLng, minLat, maxLng, maxLat] = bbox({
features,
type: 'FeatureCollection',
});

return {
id: features.length > 1 ? undefined : selectedState[0],
position: [
[minLng, minLat],
[maxLng, maxLat],
],
};
}
}

return undefined;
}, [processedData, selectedState]);

// eslint-disable-next-line react/jsx-no-useless-fragment
if (!processedData) return <></>;

const parentHeight = mapProps?.height || 450;
const parentWidth = mapProps?.width || '100%';

return (
<Map
data={processedData}
height={parentHeight}
initialBoundsPosition={initialBoundsPosition}
mapboxAccessToken={mapboxAccessToken}
setHoverInfo={setOnHoverInfo || setHoverInfo}
width={parentWidth}
{...mapProps}
>
{tooltipElement ||
(finalHoverInfo &&
(isString(parentWidth) ||
parentWidth > breakpoints.values.sm - parseInt(spacing(12), 10)) ? (
<MapPopup
popupProps={{
latitude: finalHoverInfo.lat,
longitude: finalHoverInfo.lng,
}}
rows={[
{
title: 'State',
value: finalHoverInfo.feature.properties?.name as string,
},
{
title: 'Code',
value: finalHoverInfo.feature.properties?.abbr as string,
},
{
title: 'Volume',
value: numeral(
finalHoverInfo.feature.properties?.value as number,
).format('0,0'),
},
]}
{...mapPopupProps}
/>
) : undefined)}
</Map>
);
};

StateMap.defaultProps = {
mapPopupProps: undefined,
mapProps: undefined,
onHoverInfo: undefined,
processDataFn: undefined,
selectedState: undefined,
setOnHoverInfo: undefined,
tooltipElement: undefined,
};

export default StateMap;
Loading

0 comments on commit 5b71f01

Please sign in to comment.