Add tribal info to side panel and feature selection

- create a state variable to keep track of weather or not the layer was toggled
- allow mapInfoPanel to reset on layer switch
- allow AreaDetail to show census and tribal info
- allow LayerSelector to set layer toggled
- Add selectedFeature to both MapTribal and MapTract components
- create various tribal constants for styling
- i18n constants
This commit is contained in:
Vim USDS 2022-08-06 03:14:38 -07:00
parent d4aed789cc
commit 1d3af3023b
11 changed files with 228 additions and 112 deletions

View file

@ -22,6 +22,7 @@ import mailIcon from '/node_modules/uswds/dist/img/usa-icons/mail_outline.svg';
interface IAreaDetailProps {
properties: constants.J40Properties,
hash: string[],
isCensusLayerSelected: boolean,
}
/**
@ -60,7 +61,8 @@ export interface ICategory {
isExceed1MoreBurden: boolean | null,
isExceedBothSocioBurdens: boolean | null,
}
const AreaDetail = ({properties, hash}: IAreaDetailProps) => {
const AreaDetail = ({properties, hash, isCensusLayerSelected}: IAreaDetailProps) => {
const intl = useIntl();
// console.log the properties of the census that is selected:
@ -72,6 +74,7 @@ const AreaDetail = ({properties, hash}: IAreaDetailProps) => {
const countyName = properties[constants.COUNTY_NAME] ? properties[constants.COUNTY_NAME] : "N/A";
const stateName = properties[constants.STATE_NAME] ? properties[constants.STATE_NAME] : "N/A";
const sidePanelState = properties[constants.SIDE_PANEL_STATE];
const landAreaName = properties[constants.LAND_AREA_NAME];
const isCommunityFocus = score >= constants.SCORE_BOUNDARY_THRESHOLD;
@ -572,96 +575,110 @@ const AreaDetail = ({properties, hash}: IAreaDetailProps) => {
{EXPLORE_COPY.SIDE_PANEL_VERION.TITLE}
</div>
{/* Census Info */}
<ul className={styles.censusRow}>
<li>
<span className={styles.censusLabel}>
{intl.formatMessage(EXPLORE_COPY.SIDE_PANEL_CBG_INFO.CENSUS_BLOCK_GROUP)}
</span>
<span className={styles.censusText}>{` ${blockGroup}`}</span>
</li>
<li>
<span className={styles.censusLabel}>
{intl.formatMessage(EXPLORE_COPY.SIDE_PANEL_CBG_INFO.COUNTY)}
</span>
<span className={styles.censusText}>{` ${countyName}`}</span>
</li>
<li>
<span className={styles.censusLabel}>
{properties[constants.SIDE_PANEL_STATE] !== constants.SIDE_PANEL_STATE_VALUES.NATION ?
intl.formatMessage(EXPLORE_COPY.SIDE_PANEL_CBG_INFO.TERRITORY) :
intl.formatMessage(EXPLORE_COPY.SIDE_PANEL_CBG_INFO.STATE)
}
</span>
<span className={styles.censusText}>{` ${stateName}`}</span>
</li>
<li>
<span className={styles.censusLabel}>
{intl.formatMessage(EXPLORE_COPY.SIDE_PANEL_CBG_INFO.POPULATION)}
</span>
<span className={styles.censusText}>{` ${population.toLocaleString()}`}</span>
</li>
</ul>
{
isCensusLayerSelected ? (
<>
{/* Census Info */}
<ul className={styles.censusRow}>
<li>
<span className={styles.censusLabel}>
{intl.formatMessage(EXPLORE_COPY.SIDE_PANEL_CBG_INFO.CENSUS_BLOCK_GROUP)}
</span>
<span className={styles.censusText}>{` ${blockGroup}`}</span>
</li>
<li>
<span className={styles.censusLabel}>
{intl.formatMessage(EXPLORE_COPY.SIDE_PANEL_CBG_INFO.COUNTY)}
</span>
<span className={styles.censusText}>{` ${countyName}`}</span>
</li>
<li>
<span className={styles.censusLabel}>
{properties[constants.SIDE_PANEL_STATE] !== constants.SIDE_PANEL_STATE_VALUES.NATION ?
intl.formatMessage(EXPLORE_COPY.SIDE_PANEL_CBG_INFO.TERRITORY) :
intl.formatMessage(EXPLORE_COPY.SIDE_PANEL_CBG_INFO.STATE)
}
</span>
<span className={styles.censusText}>{` ${stateName}`}</span>
</li>
<li>
<span className={styles.censusLabel}>
{intl.formatMessage(EXPLORE_COPY.SIDE_PANEL_CBG_INFO.POPULATION)}
</span>
<span className={styles.censusText}>{` ${population.toLocaleString()}`}</span>
</li>
</ul>
{/* Disadvantaged? */}
<div className={styles.categorization}>
{/* Disadvantaged? */}
<div className={styles.categorization}>
{/* Questions asking if disadvantaged? */}
<div className={styles.isInFocus}>
{EXPLORE_COPY.COMMUNITY.IS_FOCUS}
</div>
{/* YES with Dot or NO with no Dot */}
<div className={styles.communityOfFocus}>
{isCommunityFocus ?
<>
<h3>{EXPLORE_COPY.COMMUNITY.OF_FOCUS}</h3>
<DisadvantageDot isDisadvantaged={isCommunityFocus} />
</> :
<h3>{EXPLORE_COPY.COMMUNITY.NOT_OF_FOCUS}</h3>
}
</div>
{/* Number of categories exceeded */}
<div className={styles.showCategoriesExceed}>
{EXPLORE_COPY.numberOfCategoriesExceeded(properties[constants.COUNT_OF_CATEGORIES_DISADV])}
</div>
{/* Number of thresholds exceeded */}
{/* <div className={styles.showThresholdExceed}>
{EXPLORE_COPY.numberOfThresholdsExceeded(properties[constants.TOTAL_NUMBER_OF_DISADVANTAGE_INDICATORS])}
</div> */}
{/* Send Feedback button */}
<a
className={styles.sendFeedbackLink}
// The mailto string must be on a single line otherwise the email does not display subject and body
href={`
mailto:${COMMON_COPY.FEEDBACK_EMAIL}?subject=${feedbackEmailSubject}&body=${feedbackEmailBody}
`}
target={"_blank"}
rel="noreferrer"
>
<Button
type="button"
className={styles.sendFeedbackBtn}
>
<div className={styles.buttonContainer}>
<div className={styles.buttonText}>
{EXPLORE_COPY.COMMUNITY.SEND_FEEDBACK.TITLE}
</div>
<img
className={styles.buttonImage}
src={mailIcon}
alt={intl.formatMessage(EXPLORE_COPY.COMMUNITY.SEND_FEEDBACK.IMG_ICON.ALT_TAG)}
/>
{/* Questions asking if disadvantaged? */}
<div className={styles.isInFocus}>
{EXPLORE_COPY.COMMUNITY.IS_FOCUS}
</div>
</Button>
</a>
</div>
{/* YES with Dot or NO with no Dot */}
<div className={styles.communityOfFocus}>
{isCommunityFocus ?
<>
<h3>{EXPLORE_COPY.COMMUNITY.OF_FOCUS}</h3>
<DisadvantageDot isDisadvantaged={isCommunityFocus} />
</> :
<h3>{EXPLORE_COPY.COMMUNITY.NOT_OF_FOCUS}</h3>
}
</div>
{/* Number of categories exceeded */}
<div className={styles.showCategoriesExceed}>
{EXPLORE_COPY.numberOfCategoriesExceeded(properties[constants.COUNT_OF_CATEGORIES_DISADV])}
</div>
{/* Number of thresholds exceeded */}
{/* <div className={styles.showThresholdExceed}>
{EXPLORE_COPY.numberOfThresholdsExceeded(properties[constants.TOTAL_NUMBER_OF_DISADVANTAGE_INDICATORS])}
</div> */}
{/* Send Feedback button */}
<a
className={styles.sendFeedbackLink}
// The mailto string must be on a single line otherwise the email does not display subject and body
href={`
mailto:${COMMON_COPY.FEEDBACK_EMAIL}?subject=${feedbackEmailSubject}&body=${feedbackEmailBody}
`}
target={"_blank"}
rel="noreferrer"
>
<Button
type="button"
className={styles.sendFeedbackBtn}
>
<div className={styles.buttonContainer}>
<div className={styles.buttonText}>
{EXPLORE_COPY.COMMUNITY.SEND_FEEDBACK.TITLE}
</div>
<img
className={styles.buttonImage}
src={mailIcon}
alt={intl.formatMessage(EXPLORE_COPY.COMMUNITY.SEND_FEEDBACK.IMG_ICON.ALT_TAG)}
/>
</div>
</Button>
</a>
</div>
</>
) : (
<ul className={styles.censusRow}>
<li>
<span className={styles.censusLabel}>
{intl.formatMessage(EXPLORE_COPY.SIDE_PANEL_TRIBAL_INFO.LAND_AREA_NAME)}
</span>
<span className={styles.censusText}>{` ${landAreaName}`}</span>
</li>
</ul>
)}
{/* All category accordions in this component */}
<Accordion multiselectable={true} items={categoryItems} />
{isCensusLayerSelected && <Accordion multiselectable={true} items={categoryItems} />}
</aside>
);

View file

@ -5,6 +5,7 @@ import {LocalizedComponent} from '../../../test/testHelpers';
import * as constants from '../../../data/constants';
// Todo: Update tests to take into account tribal layer selected
describe('rendering of the AreaDetail', () => {
const properties = {
[constants.POVERTY_BELOW_100_PERCENTILE]: .12,
@ -27,7 +28,7 @@ describe('rendering of the AreaDetail', () => {
it('checks if indicators for NATION is present', () => {
const {asFragment} = render(
<LocalizedComponent>
<AreaDetail properties={properties} hash={hash}/>
<AreaDetail properties={properties} hash={hash} isCensusLayerSelected={true}/>
</LocalizedComponent>,
);
expect(asFragment()).toMatchSnapshot();
@ -41,7 +42,7 @@ describe('rendering of the AreaDetail', () => {
const {asFragment} = render(
<LocalizedComponent>
<AreaDetail properties={propertiesPR} hash={hash}/>
<AreaDetail properties={propertiesPR} hash={hash} isCensusLayerSelected={true}/>
</LocalizedComponent>,
);
expect(asFragment()).toMatchSnapshot();
@ -59,7 +60,7 @@ describe('rendering of the AreaDetail', () => {
const {asFragment} = render(
<LocalizedComponent>
<AreaDetail properties={propertiesIA} hash={hash}/>
<AreaDetail properties={propertiesIA} hash={hash} isCensusLayerSelected={true}/>
</LocalizedComponent>,
);
expect(asFragment()).toMatchSnapshot();

View file

@ -85,6 +85,10 @@ const J40Map = ({location}: IJ40Interface) => {
const [geolocationInProgress, setGeolocationInProgress] = useState<boolean>(false);
const [isMobileMapState, setIsMobileMapState] = useState<boolean>(false);
const [censusSelected, setCensusSelected] = useState(true);
// In order to detect that the layer has been toggled (between census and tribal),
// this state variable will hold that information
const [layerToggled, setLayerToggled] = useState<boolean>(false);
const {width: windowWidth} = useWindowSize();
const mapRef = useRef<MapRef>(null);
@ -156,6 +160,9 @@ const J40Map = ({location}: IJ40Interface) => {
}
} else {
// This else clause will fire when the ID is null or empty. This is the case where the map is clicked
setLayerToggled(false);
// @ts-ignore
const feature = event.features && event.features[0];
@ -308,7 +315,11 @@ const J40Map = ({location}: IJ40Interface) => {
{/* This will allow to select between the census tract layer and the tribal lands layer */}
<LayerSelector censusSelected={censusSelected} setCensusSelected={setCensusSelected}/>
<LayerSelector
censusSelected={censusSelected}
setCensusSelected={setCensusSelected}
setLayerToggled={setLayerToggled}
/>
{/**
* The ReactMapGL component's props are grouped by the API's documentation. The component also has
@ -356,9 +367,15 @@ const J40Map = ({location}: IJ40Interface) => {
{/* Load either the Tribal layer or census layer */}
{
censusSelected ?
<MapTractLayers selectedFeature={selectedFeature} selectedFeatureId={selectedFeatureId}/> :
<MapTribalLayer />
censusSelected ?
<MapTractLayers
selectedFeature={selectedFeature}
selectedFeatureId={selectedFeatureId}
/> :
<MapTribalLayer
selectedFeature={selectedFeature}
selectedFeatureId={selectedFeatureId}
/>
}
{/* This will add the navigation controls of the zoom in and zoom out buttons */}
@ -392,7 +409,11 @@ const J40Map = ({location}: IJ40Interface) => {
onClose={setDetailViewData}
captureScroll={true}
>
<AreaDetail properties={detailViewData.properties} hash={zoomLatLngHash}/>
<AreaDetail
properties={detailViewData.properties}
hash={zoomLatLngHash}
isCensusLayerSelected={censusSelected}
/>
</Popup>
)}
{'fs' in flags ? <FullscreenControl className={styles.fullscreenControl}/> :'' }
@ -406,6 +427,8 @@ const J40Map = ({location}: IJ40Interface) => {
featureProperties={detailViewData?.properties}
selectedFeatureId={selectedFeature?.id}
hash={zoomLatLngHash}
isCensusLayerSelected={censusSelected}
layerToggled={layerToggled}
/>
</Grid>
</>

View file

@ -7,7 +7,7 @@ describe('rendering of the LayerSelector', () => {
it('checks if component renders census tracts selected', () => {
const {asFragment} = render(
<LocalizedComponent>
<LayerSelector censusSelected={true} setCensusSelected={() => {}}/>
<LayerSelector censusSelected={true} setCensusSelected={() => {}} setLayerToggled={() =>{}}/>
</LocalizedComponent>,
);
expect(asFragment()).toMatchSnapshot();
@ -16,7 +16,7 @@ describe('rendering of the LayerSelector', () => {
it('checks if component renders tribal selected', () => {
const {asFragment} = render(
<LocalizedComponent>
<LayerSelector censusSelected={false} setCensusSelected={() => {}}/>
<LayerSelector censusSelected={false} setCensusSelected={() => {}} setLayerToggled={()=> {}}/>
</LocalizedComponent>,
);
expect(asFragment()).toMatchSnapshot();

View file

@ -10,9 +10,10 @@ import * as EXPLORE_COPY from '../../data/copy/explore';
interface ILayerSelector {
censusSelected: boolean,
setCensusSelected: Dispatch<boolean>,
setLayerToggled: Dispatch<boolean>,
}
const LayerSelector = ({censusSelected, setCensusSelected}:ILayerSelector) => {
const LayerSelector = ({censusSelected, setCensusSelected, setLayerToggled}:ILayerSelector) => {
const intl = useIntl();
/**
@ -38,6 +39,13 @@ const LayerSelector = ({censusSelected, setCensusSelected}:ILayerSelector) => {
}
}, [width]);
// Anytime the censusSelected state variable changes, set the LayerToggled state
// variable
useEffect( () => {
setLayerToggled(true);
}, [censusSelected]);
// Handles toggle of tracts and tribal layer selection
const buttonClickHandler = (event) => {
if (event.target.id === 'census' && !censusSelected) {

View file

@ -1,5 +1,6 @@
import React, {useMemo} from 'react';
import {Source, Layer} from 'react-map-gl';
import {AnyLayer} from 'mapbox-gl';
// Contexts:
import {useFlags} from '../../contexts/FlagContext';
@ -7,10 +8,9 @@ import {useFlags} from '../../contexts/FlagContext';
import * as constants from '../../data/constants';
import * as COMMON_COPY from '../../data/copy/common';
// Todo: Update with real types if this works:
interface IMapTractLayers {
selectedFeatureId: any,
selectedFeature: any
selectedFeatureId: AnyLayer,
selectedFeature: AnyLayer,
}
/**
@ -56,7 +56,10 @@ export const featureURLForTilesetName = (tilesetName: string): string => {
}
};
const MapTractLayers = ({selectedFeatureId, selectedFeature}: IMapTractLayers) => {
const MapTractLayers = ({
selectedFeatureId,
selectedFeature,
}: IMapTractLayers) => {
const filter = useMemo(() => ['in', constants.GEOID_PROPERTY, selectedFeatureId], [selectedFeature]);
return (

View file

@ -1,8 +1,14 @@
import React from 'react';
import React, {useMemo} from 'react';
import {Source, Layer} from 'react-map-gl';
import {AnyLayer} from 'mapbox-gl';
import * as constants from '../../data/constants';
interface IMapTribalLayers {
selectedFeatureId: AnyLayer,
selectedFeature: AnyLayer,
}
/**
* This function will determine the URL for the tribal tiles.
* @return {string}
@ -18,7 +24,12 @@ export const tribalURL = (): string => {
].join('/');
};
const MapTribalLayer = () => {
const MapTribalLayer = ({
selectedFeatureId,
selectedFeature,
}: IMapTribalLayers) => {
const tribalSelectionFilter = useMemo(() => ['in', constants.TRIBAL_ID, selectedFeatureId], [selectedFeature]);
return (
<Source
id={constants.TRIBAL_SOURCE_NAME}
@ -29,7 +40,7 @@ const MapTribalLayer = () => {
maxzoom={constants.TRIBAL_MAX_ZOOM}
>
{/* Low zoom layer - prioritized features only */}
{/* Tribal layer */}
<Layer
id={constants.TRIBAL_LAYER_ID}
source-layer={constants.TRIBAL_SOURCE_LAYER}
@ -48,7 +59,7 @@ const MapTribalLayer = () => {
source-layer={constants.SCORE_SOURCE_LAYER}
type='line'
paint={{
'line-color': constants.TRIBAL_BORDER_COLOR,
'line-color': constants.FEATURE_BORDER_COLOR,
'line-width': constants.FEATURE_BORDER_WIDTH,
'line-opacity': constants.FEATURE_BORDER_OPACITY,
}}
@ -58,8 +69,9 @@ const MapTribalLayer = () => {
{/* Tribal layer - border styling around the selected feature */}
<Layer
id={constants.SELECTED_FEATURE_BORDER_LAYER_ID}
source-layer={constants.TRIBAL_SOURCE_NAME}
id={constants.SELECTED_TRIBAL_FEATURE_BORDER_LAYER_ID}
source-layer={constants.TRIBAL_SOURCE_LAYER}
filter={tribalSelectionFilter}
type='line'
paint={{
'line-color': constants.SELECTED_FEATURE_BORDER_COLOR,
@ -67,6 +79,23 @@ const MapTribalLayer = () => {
}}
minzoom={constants.TRIBAL_MIN_ZOOM}
/>
{/* Alaska layer */}
{/* // Todo: Figure out why this isn't working */}
<Layer
id={constants.SELECTED_FEATURE_BORDER_LAYER_ID}
source-layer={constants.TRIBAL_SOURCE_NAME}
// Using other filter expressions, such as equality decisions here
// may cause the open-source to error out on circle not defined
filter={['geometry-type', 'Point']}
type='circle'
paint={{
'circle-radius': 100,
'circle-color': '#007cbf',
}}
minzoom={constants.TRIBAL_MIN_ZOOM}
/>
</Source>
);
};

View file

@ -7,13 +7,33 @@ interface IMapInfoPanelProps {
featureProperties: { [key:string]: string | number } | undefined,
selectedFeatureId: string | number | undefined
hash: string[],
isCensusLayerSelected: boolean,
layerToggled: boolean, // indicates if census layer or tribal layer has been toggled
}
const MapInfoPanel = ({className, featureProperties, selectedFeatureId, hash}:IMapInfoPanelProps) => {
const MapInfoPanel = ({
className,
featureProperties,
selectedFeatureId,
hash,
isCensusLayerSelected,
layerToggled,
}:IMapInfoPanelProps) => {
return (
<div className={className} >
{(featureProperties && selectedFeatureId ) ?
<AreaDetail properties={featureProperties} hash={hash}/> :
{/* The tertiary conditional statement below will control the side panel state. Currently
there are two states, namely showing the AreaDetail or SidePanelInfo. When a feature
is selected, on - for example - the census tract layer, and if the Tribal Layer is the selected
the Side Panel should revert back to the SidePanelInfo.
A new boolean called layerToggle captures that a layer has been selected and to render
the SidePanelInfo component */}
{(featureProperties && selectedFeatureId && !layerToggled) ?
<AreaDetail
properties={featureProperties}
hash={hash}
isCensusLayerSelected={isCensusLayerSelected}
/> :
<SidePanelInfo />
}
</div>

View file

@ -27,6 +27,7 @@ export type J40Properties = { [key: string]: any };
// Tribal signals
export const TRIBAL_ID = 'tribalId';
export const LAND_AREA_NAME = 'landAreaName';
// Set the threshold percentile used by most indicators in the side panel
export const DEFAULT_THRESHOLD_PERCENTILE = 90;
@ -202,6 +203,7 @@ export const LOW_ZOOM_LAYER_ID = 'low-zoom-layer-id';
export const FEATURE_BORDER_LAYER_ID = 'feature-border-layer-id';
export const SELECTED_FEATURE_BORDER_LAYER_ID = 'selected-feature-border-layer-id';
export const TRIBAL_LAYER_ID = 'tribal-layer-id';
export const SELECTED_TRIBAL_FEATURE_BORDER_LAYER_ID = 'selected-feature-tribal-border-layer-id';
// Used in layer filters:
export const SCORE_PROPERTY_LOW = 'M_SCORE';
@ -234,9 +236,9 @@ export const FEATURE_BORDER_COLOR = '#4EA5CF';
export const SELECTED_FEATURE_BORDER_COLOR = '#1A4480';
export const PRIORITIZED_FEATURE_FILL_COLOR = '#768FB3';
export const TRIBAL_BORDER_COLOR = '#0000FF';
export const SELECTED_TRIBAL_BORDER_COLOR = '#FF0000';
export const TRIBAL_FILL_COLOR = '#00FF00';
export const TRIBAL_BORDER_COLOR = '##4EA5CF';
export const SELECTED_TRIBAL_BORDER_COLOR = '#1A4480';
export const TRIBAL_FILL_COLOR = '#768FB3';
// Widths

View file

@ -300,6 +300,15 @@ export const SIDE_PANEL_CBG_INFO = defineMessages({
},
});
export const SIDE_PANEL_TRIBAL_INFO = defineMessages({
LAND_AREA_NAME: {
id: 'explore.map.page.side.panel.tribalInfo.landAreaName',
defaultMessage: 'Land Area Name:',
description: `Navigate to the explore the map page. Click on Tribal Lands, when the map is in view,
click on the map. The side panel will show the land area name of the feature selected`,
},
});
export const COMMUNITY = {
OF_FOCUS: <FormattedMessage
id={'explore.map.page.side.panel.community.of.focus'}

View file

@ -823,6 +823,10 @@
"defaultMessage": "AND",
"description": "Navigate to the explore the map page. When the map is in view, click on the map. Click on a category to expand. This is the AND spacer around thresholds."
},
"explore.map.page.side.panel.tribalInfo.landAreaName": {
"defaultMessage": "Land Area Name:",
"description": "Navigate to the explore the map page. Click on Tribal Lands, when the map is in view, \n click on the map. The side panel will show the land area name of the feature selected"
},
"explore.map.page.side.panel.version.title": {
"defaultMessage": "Methodology version {version}",
"description": "Navigate to the explore the map page. When the map is in view, click on the map. The side panel will show the methodology version number"