adds map side panel (#406)

* initial map side panel

* componentize MapSidePanel

* remove selection from J40Map

* adds isFeatureSelected to toggle component

* filters data from server for client UI

* styling and refactor

* added TODO

* adds styling to intro and pairing feedback

* add mobile styling

* adds popup back to fs feature flag

* adds tests and aria roles

* makes mobile content same as desktop

* prettier update

* initial e2e mapSidePanel test

* adds cypress tests on desktop and mobile

* adds sass util and updates cypress tests

* cleans up tests

* reverts tsconfig file

* fixes map alignment

* renaming and using constants

* renaming sidePanel to infoPanel

* intl messaging

* adds snapshot testing and utility sass file

* PR feedback
- adds intl messages
- adds data-cy attr to cy tests
- snapshot testing for unit tests
- fixes bug where side panel extends past the map
- moves all wrapper content in MapWrapper

* logs isMobile to troubleshoot deployed PR

* adds react-device-detect for isMobile detection

* adds new instance of map for mobile

* adds instance

* adds isMobile to state

* tests the fix for mobile map view on PR

* PR review feedback
- localize MapIntroduction
- update snapshot tests
- QA feedback
- constants.isMobile points to react-device-detect
This commit is contained in:
Vim 2021-07-27 12:05:25 -07:00 committed by GitHub
commit 36f43b2d44
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1430 additions and 27185 deletions

View file

@ -1,13 +1,17 @@
$sidebar-background: rgba(35, 55, 75, 0.9);
$sidebar-color: #ffffff;
@import "./areaDetailUtils.scss";
.mapContainer {
height: 676px;
.mapAndInfoPanelContainer {
display: flex;
height: 81vh; //desktop
@media screen and (max-width: $mobileBreakpoint) {
flex-direction: column;
height: fit-content;
}
}
.j40Popup {
max-height: 50%;
overflow-y: scroll;
width: 375px;
}
.navigationControl {
@ -25,3 +29,9 @@ $sidebar-color: #ffffff;
right: 1.25em;
top: 5em;
}
.mapInfoPanel {
border: 2px solid $sidePanelBorderColor;
overflow-y: auto;
max-width: 22rem;
}

View file

@ -1,12 +1,14 @@
declare namespace J40MapModuleScssNamespace {
export interface IJ40MapModuleScss {
mapContainer: string;
mapAndInfoPanelContainer: string;
j40Popup: string;
territoryFocusButton: string;
territoryFocusContainer: string;
navigationControl: string;
fullscreenControl: string;
geolocateControl: string;
detailView: string;
mapInfoPanel: string;
}
}

View file

@ -1,4 +1,5 @@
/* eslint-disable no-unused-vars */
// External Libs:
import React, {MouseEvent, useRef, useState} from 'react';
import {Map, MapboxGeoJSONFeature, LngLatBoundsLike} from 'maplibre-gl';
import ReactMapGL, {
@ -11,13 +12,20 @@ import ReactMapGL, {
FlyToInterpolator,
FullscreenControl,
MapRef} from 'react-map-gl';
import {makeMapStyle} from '../data/mapStyle';
import AreaDetail from './areaDetail';
import bbox from '@turf/bbox';
import * as d3 from 'd3-ease';
import {useFlags} from '../contexts/FlagContext';
import TerritoryFocusControl from './territoryFocusControl';
import {isMobile} from 'react-device-detect';
// Contexts:
import {useFlags} from '../contexts/FlagContext';
// Components:
import TerritoryFocusControl from './territoryFocusControl';
import MapInfoPanel from './mapInfoPanel';
import AreaDetail from './areaDetail';
// Styles and constants
import {makeMapStyle} from '../data/mapStyle';
import 'maplibre-gl/dist/maplibre-gl.css';
import * as constants from '../data/constants';
import * as styles from './J40Map.module.scss';
@ -31,13 +39,14 @@ declare global {
}
interface IDetailViewInterface {
export interface IDetailViewInterface {
latitude: number
longitude: number
zoom: number
properties: constants.J40Properties,
};
const J40Map = () => {
const [viewport, setViewport] = useState<ViewportProps>({
latitude: constants.DEFAULT_CENTER[0],
@ -49,6 +58,8 @@ const J40Map = () => {
const [detailViewData, setDetailViewData] = useState<IDetailViewInterface>();
const [transitionInProgress, setTransitionInProgress] = useState<boolean>(false);
const [geolocationInProgress, setGeolocationInProgress] = useState<boolean>(false);
const [isMobileMapState, setIsMobileMapState] = useState<boolean>(false);
const mapRef = useRef<MapRef>(null);
const flags = useFlags();
@ -66,6 +77,7 @@ const J40Map = () => {
padding: 40,
},
);
// If we've selected a new feature, set 'selected' to false
if (selectedFeature && feature.id !== selectedFeature.id) {
setMapSelected(selectedFeature, false);
@ -90,6 +102,8 @@ const J40Map = () => {
if (typeof window !== 'undefined' && window.Cypress && mapRef.current) {
window.underlyingMap = mapRef.current.getMap();
}
if (isMobile) setIsMobileMapState(true);
};
@ -110,6 +124,7 @@ const J40Map = () => {
});
};
const setMapSelected = (feature:MapboxGeoJSONFeature, isSelected:boolean) : void => {
// The below can be confirmed during debug with:
// mapRef.current.getFeatureState({"id":feature.id, "source":feature.source, "sourceLayer":feature.sourceLayer})
@ -125,6 +140,7 @@ const J40Map = () => {
}
};
const onClickTerritoryFocusButton = (event: MouseEvent<HTMLButtonElement>) => {
const buttonID = event.target && (event.target as HTMLElement).id;
@ -164,16 +180,15 @@ const J40Map = () => {
};
return (
<>
<div className={styles.mapAndInfoPanelContainer}>
<ReactMapGL
{...viewport}
className={styles.mapContainer}
mapStyle={makeMapStyle(flags)}
minZoom={constants.GLOBAL_MIN_ZOOM}
maxZoom={constants.GLOBAL_MAX_ZOOM}
mapOptions={{hash: true}}
width="100%"
height="52vw"
height={isMobileMapState ? '44vh' : '100%'}
dragRotate={false}
touchRotate={false}
interactiveLayerIds={[constants.HIGH_SCORE_LAYER_NAME]}
@ -183,8 +198,9 @@ const J40Map = () => {
onTransitionStart={onTransitionStart}
onTransitionEnd={onTransitionEnd}
ref={mapRef}
data-cy={'reactMapGL'}
>
{(detailViewData && !transitionInProgress) && (
{('fs' in flags && detailViewData && !transitionInProgress) && (
<Popup
className={styles.j40Popup}
tipSize={5}
@ -198,7 +214,6 @@ const J40Map = () => {
<AreaDetail properties={detailViewData.properties} />
</Popup>
)}
<NavigationControl
showCompass={false}
className={styles.navigationControl}
@ -214,7 +229,12 @@ const J40Map = () => {
<TerritoryFocusControl onClickTerritoryFocusButton={onClickTerritoryFocusButton}/>
{'fs' in flags ? <FullscreenControl className={styles.fullscreenControl}/> :'' }
</ReactMapGL>
</>
<MapInfoPanel
className={styles.mapInfoPanel}
featureProperties={detailViewData?.properties}
selectedFeatureId={selectedFeature?.id}
/>
</div>
);
};

View file

@ -0,0 +1,159 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rendering of the AreaDetail checks if various text fields are visible 1`] = `
<DocumentFragment>
<aside
data-cy="aside"
>
<header>
<div>
<div>
Cumulative Index Score
</div>
<div
data-cy="score"
>
9500.0
<sup>
<span>
th
</span>
</sup>
</div>
<div>
percentile
</div>
</div>
<div>
<div>
Categorization
</div>
<div>
<div />
<div>
Prioritized
</div>
</div>
</div>
</header>
<ul>
<li>
<span>
Census block group:
</span>
<span>
98729374234
</span>
</li>
<li>
<span>
County:
</span>
<span>
Washington County*
</span>
</li>
<li>
<span>
State:
</span>
<span>
District of Columbia*
</span>
</li>
<li>
<span>
Population:
</span>
<span>
3,435,435
</span>
</li>
</ul>
<div>
<div>
INDICATORS
</div>
<div>
PERCENTILE (0-100)
</div>
</div>
<li
data-cy="indicatorBox"
>
<div>
<div>
Poverty
</div>
<div>
Household income is less than or equal to twice the federal "poverty level"
</div>
</div>
<div>
9900.0
</div>
</li>
<li
data-cy="indicatorBox"
>
<div>
<div>
Education
</div>
<div>
Percent of people age 25 or older that didnt get a high school diploma
</div>
</div>
<div>
9800.0
</div>
</li>
<li
data-cy="indicatorBox"
>
<div>
<div>
Linguistic isolation
</div>
<div>
Households in which all members speak a non-English language and speak English less than "very well"
</div>
</div>
<div>
9700.0
</div>
</li>
<li
data-cy="indicatorBox"
>
<div>
<div>
Unemployment rate
</div>
<div>
Number of unemployed people as a percentage of the labor force
</div>
</div>
<div>
9600.0
</div>
</li>
<li
data-cy="indicatorBox"
>
<div>
<div>
Housing Burden
</div>
<div>
Households that are low income and spend more than 30% of their income to housing costs
</div>
</div>
<div>
9500.0
</div>
</li>
</aside>
,
</DocumentFragment>
`;

View file

@ -0,0 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`simulate a click on map hould match the snapshot of the MapInfoPanel component 1`] = `<DocumentFragment />`;
exports[`simulate app starting up, no click on map should match the snapshot of the MapIntroduction component 1`] = `
<DocumentFragment>
<div
class="someClassName"
>
<aside>
<header>
Zoom and select a census block group to view data
</header>
<div>
<img
src="test-file-stub"
/>
<div>
<div>
Did you know?
</div>
<cite>
A census block group is generally between 600 and 3,000 people. It is the smallest geographical unit for which the U.S. Census Bureau publishes sample data.
</cite>
</div>
</div>
</aside>
</div>
,
</DocumentFragment>
`;

View file

@ -1,23 +1,156 @@
.areaDetailTable {
max-width: 31.6vw;
@import "./areaDetailUtils.scss";
$sidePanelLabelFontColor: #171716;
$featureSelectBorderColor: #00bde3;
@mixin sidePanelLabelStyle {
font-size: small;
color: $sidePanelLabelFontColor;
font-weight: 600;
}
.titleContainer {
display: flex;
flex-direction: column;
padding: 22px 22px;
}
.titleIndicatorName {
font-weight: bold;
@mixin categorizationCircleStyle {
height: 0.6rem;
width: 0.6rem;
border-radius: 100%;
align-self: center;
margin-top: 0.8rem;
margin-right: 0.5rem;
opacity: 0.6;
}
.areaDetailContainer {
max-height: 50vh;
overflow: scroll;
display: flex;
flex-direction: column;
}
.areaDetailTableContainer {
overflow: auto;
padding: 22px;
// top row styles
.topRow {
display: flex;
}
.cumulativeIndexScore,
.categorization {
display: flex;
flex-direction: column;
align-items: center;
height: 7.7rem;
border-bottom: $sidePanelBorder;
flex: 1 0 50%;
padding-top: 2rem;
}
.topRowTitle,
.censusLabel {
@include sidePanelLabelStyle;
}
.topRowSubTitle {
font-size: small;
color: $sidePanelLabelFontColor;
}
.score {
font-size: xx-large;
font-weight: bolder;
}
.scoreSuperscript {
font-size: large;
padding-bottom: 1rem;
}
.categorization {
border-left: $sidePanelBorder;
}
.priority {
display: flex;
}
.prioritized {
@include categorizationCircleStyle;
background: #1a4480;
border: 1px solid $featureSelectBorderColor;
}
.threshold {
@include categorizationCircleStyle;
background: #d7dde7;
border: 1px solid $featureSelectBorderColor;
}
.nonPrioritized {
@include categorizationCircleStyle;
border: 1px solid $featureSelectBorderColor;
}
.prioritization {
font-size: large;
font-weight: bold;
padding-top: 0.8rem;
}
.censusRow {
display: flex;
flex-direction: column;
border-bottom: $sidePanelBorder;
list-style: none;
margin: 0;
}
//census row styles
.censusRow {
padding: 1rem;
}
.censusText,
.indicatorDescription {
font-size: small;
}
//Divider styles
.divider {
@include sidePanelLabelStyle;
display: flex;
justify-content: space-between;
border-bottom: $sidePanelBorder;
padding: 0.3rem 0.5rem 0.3rem 1rem;
background-color: #edeef0;
}
//Indicator box styles
.indicatorBox {
display: flex;
padding: 1.5rem 1rem;
border-bottom: $sidePanelBorder;
@media screen and (max-width: $mobileBreakpoint) {
justify-content: space-between;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
}
.indicatorBox:last-child {
border-bottom: none;
}
.indicatorTitle {
font-size: large;
font-weight: bolder;
}
.indicatorValue {
flex: 1 0 37%;
align-self: center;
padding-left: 2.4rem;
font-size: large;
@media screen and (max-width: $mobileBreakpoint) {
flex: 1 0 40%;
align-self: inherit;
padding-left: 3rem;
padding-top: 1rem;
}
}

View file

@ -1,10 +1,27 @@
declare namespace MapModuleScssNamespace {
export interface IMapModuleScss {
areaDetailContainer: string;
areaDetailTable:string;
areaDetailTableContainer:string;
titleContainer:string;
titleIndicatorName:string;
topRow:string;
cumulativeIndexScore:string;
scoreSuperscript: string;
topRowTitle:string;
topRowSubTitle:string;
categorization:string;
prioritized:string;
threshold:string;
nonPrioritized:string;
priority:string;
prioritization:string;
censusRow:string;
censusText: string;
censusLabel:string;
divider:string;
indicatorBox:string;
indicatorInfo:string;
indicatorTitle:string;
indicatorDescription:string;
indicatorValue:string;
score:string;
}
}

View file

@ -0,0 +1,47 @@
import * as React from 'react';
import {render} from '@testing-library/react';
import AreaDetail, {getCategorization, readablePercent} from './areaDetail';
import {LocalizedComponent} from '../test/testHelpers';
import * as constants from '../data/constants';
describe('rendering of the AreaDetail', () => {
const properties = {
[constants.POVERTY_PROPERTY_PERCENTILE]: 99,
[constants.EDUCATION_PROPERTY_PERCENTILE]: 98,
[constants.LINGUISTIC_ISOLATION_PROPERTY_PERCENTILE]: 97,
[constants.UNEMPLOYMENT_PROPERTY_PERCENTILE]: 96,
[constants.HOUSING_BURDEN_PROPERTY_PERCENTILE]: 95,
[constants.SCORE_PROPERTY_HIGH]: 95,
[constants.GEOID_PROPERTY]: 98729374234,
[constants.TOTAL_POPULATION]: 3435435,
};
const {asFragment} = render(
<LocalizedComponent>
<AreaDetail properties={properties}/>
</LocalizedComponent>,
)
;
it('checks if various text fields are visible', () => {
expect(asFragment()).toMatchSnapshot();
});
});
describe('tests the readablePercent function', () => {
expect(readablePercent(.9877665443)).toEqual('98.8');
});
describe('tests the getCategorization function', () => {
it(`should equal Prioritized for value >= ${constants.SCORE_BOUNDARY_LOW}`, () => {
expect(getCategorization(.756)).toEqual(['Prioritized', undefined]);
});
it(`should equal Threshold for .60 <= value < ${constants.SCORE_BOUNDARY_THRESHOLD}`, () => {
expect(getCategorization(.65)).toEqual(['Threshold', undefined]);
});
it(`should equal Non-prioritized for value < ${constants.SCORE_BOUNDARY_PRIORITIZED}`, () => {
expect(getCategorization(.53)).toEqual(['Non-prioritized', undefined]);
});
});

View file

@ -1,92 +1,210 @@
// External Libs:
import * as React from 'react';
import * as constants from '../data/constants';
import {useIntl} from 'gatsby-plugin-intl';
import {defineMessages} from 'react-intl';
// Components:
// Styles and constants
import * as styles from './areaDetail.module.scss';
import * as constants from '../data/constants';
export const readablePercent = (percent: number) => {
return `${(percent * 100).toFixed(1)}`;
};
export const getCategorization = (percentile: number) => {
let categorization;
let categoryCircleStyle;
if (percentile >= constants.SCORE_BOUNDARY_PRIORITIZED ) {
categorization = 'Prioritized';
categoryCircleStyle = styles.prioritized;
} else if (constants.SCORE_BOUNDARY_THRESHOLD <= percentile && percentile < constants.SCORE_BOUNDARY_PRIORITIZED) {
categorization = 'Threshold';
categoryCircleStyle = styles.threshold;
} else {
categorization = 'Non-prioritized';
categoryCircleStyle = styles.nonPrioritized;
}
return [categorization, categoryCircleStyle];
};
interface IAreaDetailProps {
properties: constants.J40Properties,
}
const AreaDetail = ({properties}:IAreaDetailProps) => {
const readablePercent = (percent: number) => {
return `${(percent * 100).toFixed(2)}`;
const intl = useIntl();
const messages = defineMessages({
cumulativeIndexScore: {
id: 'areaDetail.priorityInfo.cumulativeIndexScore',
defaultMessage: 'Cumulative Index Score',
description: 'the cumulative score of the feature selected',
},
percentile: {
id: 'areaDetail.priorityInfo.percentile',
defaultMessage: 'percentile',
description: 'the percentil of the feature selected',
},
categorization: {
id: 'areaDetail.priorityInfo.categorization',
defaultMessage: 'Categorization',
description: 'the categorization of prioritized, threshold or non-prioritized',
},
censusBlockGroup: {
id: 'areaDetail.geographicInfo.censusBlockGroup',
defaultMessage: 'Census block group:',
description: 'the census block group id number of the feature selected',
},
county: {
id: 'areaDetail.geographicInfo.county',
defaultMessage: 'County:',
description: 'the county of the feature selected',
},
state: {
id: 'areaDetail.geographicInfo.state',
defaultMessage: 'State: ',
description: 'the state of the feature selected',
},
population: {
id: 'areaDetail.geographicInfo.population',
defaultMessage: 'Population:',
description: 'the population of the feature selected',
},
indicatorColumnHeader: {
id: 'areaDetail.indicators.indicatorColumnHeader',
defaultMessage: 'INDICATORS',
description: 'the population of the feature selected',
},
percentileColumnHeader: {
id: 'areaDetail.indicators.percentileColumnHeader',
defaultMessage: 'PERCENTILE (0-100)',
description: 'the population of the feature selected',
},
poverty: {
id: 'areaDetail.indicator.poverty',
defaultMessage: 'Poverty',
description: 'Household income is less than or equal to twice the federal "poverty level"',
},
education: {
id: 'areaDetail.indicator.education',
defaultMessage: 'Education',
description: 'Percent of people age 25 or older that didnt get a high school diploma',
},
linguisticIsolation: {
id: 'areaDetail.indicator.linguisticIsolation',
defaultMessage: 'Linguistic isolation',
description: 'Households in which all members speak a non-English language and ' +
'speak English less than "very well"',
},
unemployment: {
id: 'areaDetail.indicator.unemployment',
defaultMessage: 'Unemployment rate',
description: 'Number of unemployed people as a percentage of the labor force',
},
houseBurden: {
id: 'areaDetail.indicator.houseBurden',
defaultMessage: 'Housing Burden',
description: 'Households that are low income and spend more than 30% of their income to housing costs',
},
});
const score = properties[constants.SCORE_PROPERTY_HIGH] as number;
const blockGroup = properties[constants.GEOID_PROPERTY];
const population = properties[constants.TOTAL_POPULATION];
interface indicatorInfo {
label: string,
description: string,
value: number,
}
// Todo: Ticket #367 will be replacing descriptions with YAML file
const povertyInfo:indicatorInfo = {
label: intl.formatMessage(messages.poverty),
description: 'Household income is less than or equal to twice the federal "poverty level"',
value: properties[constants.POVERTY_PROPERTY_PERCENTILE],
};
const eduInfo:indicatorInfo = {
label: intl.formatMessage(messages.education),
description: 'Percent of people age 25 or older that didnt get a high school diploma',
value: properties[constants.EDUCATION_PROPERTY_PERCENTILE],
};
const linIsoInfo:indicatorInfo = {
label: intl.formatMessage(messages.linguisticIsolation),
description: 'Households in which all members speak a non-English language and speak English less than "very well"',
value: properties[constants.LINGUISTIC_ISOLATION_PROPERTY_PERCENTILE],
};
const umemployInfo:indicatorInfo = {
label: intl.formatMessage(messages.unemployment),
description: 'Number of unemployed people as a percentage of the labor force',
value: properties[constants.UNEMPLOYMENT_PROPERTY_PERCENTILE],
};
const houseBurden:indicatorInfo = {
label: intl.formatMessage(messages.houseBurden),
description: 'Households that are low income and spend more than 30% of their income to housing costs',
value: properties[constants.HOUSING_BURDEN_PROPERTY_PERCENTILE],
};
const getCategorization = (percentile: number) => {
let categorization;
if (percentile >= 0.75 ) {
categorization = 'Prioritized';
} else if (0.60 <= percentile && percentile < 0.75) {
categorization = 'Threshold';
} else {
categorization = 'Non-prioritized';
}
return categorization;
};
const getTitleContent = () => {
const blockGroup = properties[constants.GEOID_PROPERTY];
const score = properties[constants.SCORE_PROPERTY_HIGH] as number;
return (
<div className={styles.titleContainer}>
<div>
<span className={styles.titleIndicatorName}>Census Block Group: </span>
<span>{blockGroup}</span>
</div>
<div>
<span className={styles.titleIndicatorName}>Just Progress Categorization: </span>
<span>{getCategorization(score)}</span>
</div>
<div>
<span className={styles.titleIndicatorName}>Cumulative Index Score: </span>
<span>{readablePercent(score)}</span>
</div>
</div>
);
};
const getBodyContent = () => {
const rows = [];
const sortedKeys = Object.entries(properties).sort();
for (let [key, value] of sortedKeys) {
// We should only format floats
if (typeof value === 'number' && value % 1 !== 0) {
value = readablePercent(value);
}
// Filter out all caps
if (!key.match(/^[A-Z0-9]+$/)) {
rows.push(<tr key={key} >
<td>{key}</td>
<td>{value}</td>
</tr>);
}
}
return rows;
};
const indicators = [povertyInfo, eduInfo, linIsoInfo, umemployInfo, houseBurden];
const [categorization, categoryCircleStyle] = getCategorization(score);
return (
<>
{properties ?
<div className={styles.areaDetailContainer}>
{getTitleContent()}
<div className={styles.areaDetailTableContainer}>
<table className={'usa-table usa-table--borderless ' + styles.areaDetailTable}>
<thead>
<tr>
<th scope="col">INDICATOR</th>
<th scope="col">VALUE</th>
</tr>
</thead>
<tbody>
{getBodyContent()}
</tbody>
</table>
<aside className={styles.areaDetailContainer} data-cy={'aside'}>
<header className={styles.topRow }>
<div className={styles.cumulativeIndexScore}>
<div className={styles.topRowTitle}>{intl.formatMessage(messages.cumulativeIndexScore)}</div>
<div className={styles.score} data-cy={'score'}>{`${readablePercent(score)}`}
<sup className={styles.scoreSuperscript}><span>th</span></sup>
</div>
<div className={styles.topRowSubTitle}>{intl.formatMessage(messages.percentile)}</div>
</div>
</div> :
'' }
</>
<div className={styles.categorization}>
<div className={styles.topRowTitle}>{intl.formatMessage(messages.categorization)}</div>
<div className={styles.priority}>
<div className={categoryCircleStyle} />
<div className={styles.prioritization}>{categorization}</div>
</div>
</div>
</header>
<ul className={styles.censusRow}>
<li>
<span className={styles.censusLabel}>{intl.formatMessage(messages.censusBlockGroup)} </span>
<span className={styles.censusText}>{blockGroup}</span>
</li>
<li>
<span className={styles.censusLabel}>{intl.formatMessage(messages.county)} </span>
<span className={styles.censusText}>{'Washington County*'}</span>
</li>
<li>
<span className={styles.censusLabel}>{intl.formatMessage(messages.state)}</span>
<span className={styles.censusText}>{'District of Columbia*'}</span>
</li>
<li>
<span className={styles.censusLabel}>{intl.formatMessage(messages.population)} </span>
<span className={styles.censusText}>{population.toLocaleString()}</span>
</li>
</ul>
<div className={styles.divider}>
<div>{intl.formatMessage(messages.indicatorColumnHeader)}</div>
<div>{intl.formatMessage(messages.percentileColumnHeader)}</div>
</div>
{indicators.map((indicator, index) => (
<li key={index} className={styles.indicatorBox} data-cy={'indicatorBox'}>
<div className={styles.indicatorInfo}>
<div className={styles.indicatorTitle}>{indicator.label}</div>
<div className={styles.indicatorDescription}>
{indicator.description}
</div>
</div>
<div className={styles.indicatorValue}>{readablePercent(indicator.value)}</div>
</li>
))}
</aside>
);
};

View file

@ -0,0 +1,10 @@
/*
This file is meant to hold styles that are shared with other SASS modules.
You can import this file into other SASS file to re-use styles.
Todo Design System: replace colors with tokens. Once these styles become more general the name can be less specific
*/
$sidePanelBorderColor: #f2f2f2;
$sidePanelBorder: 2px solid $sidePanelBorderColor;
$mobileBreakpoint: 400px;

View file

@ -0,0 +1,51 @@
import * as React from 'react';
import {render} from '@testing-library/react';
import MapInfoPanel from './mapInfoPanel';
import {LocalizedComponent} from '../test/testHelpers';
describe('simulate app starting up, no click on map', () => {
const {asFragment} = render(
<LocalizedComponent>
<MapInfoPanel
className={'someClassName'}
featureProperties={undefined}
selectedFeatureId={undefined}
/>
</LocalizedComponent>,
);
it('should match the snapshot of the MapIntroduction component', () => {
expect(asFragment()).toMatchSnapshot();
});
});
describe('simulate a click on map', () => {
const featureProperties = {
'GEOID10': '350459430003',
'Total population': 960,
'GEOID10 (percentile)': 0.5784380914343289,
'Housing burden (percent) (percentile)': 0.10073235017829946,
'Total population (percentile)': 0.2985685303608629,
'Linguistic isolation (percent) (percentile)': 0.9623469180109516,
'Percent of households in linguistic isolation (percentile)': 0.9230800651740774,
'Poverty (Less than 200% of federal poverty line) (percentile)': 0.947202643271775,
'Percent individuals age 25 or over with less than high school degree (percentile)': 0.7804232684164424,
'Unemployed civilians (percent) (percentile)': 0.9873599918675144,
'Score D (percentile)': 0.9321799276549586,
};
const selectedFeatureId = 345;
const {asFragment} = render(
<LocalizedComponent>
<MapInfoPanel
className={'J40Map-module--mapInfoPanel--8Ap7p'}
featureProperties={featureProperties}
selectedFeatureId={selectedFeatureId}
/>
</LocalizedComponent>,
);
it('hould match the snapshot of the MapInfoPanel component', () => {
expect(asFragment()).toMatchSnapshot();
});
});

View file

@ -0,0 +1,22 @@
import React from 'react';
import MapIntroduction from './mapIntroduction';
import AreaDetail from './areaDetail';
interface IMapInfoPanelProps {
className: string,
featureProperties: { [key:string]: string | number } | undefined,
selectedFeatureId: string | number | undefined
}
const MapInfoPanel = ({className, featureProperties, selectedFeatureId}:IMapInfoPanelProps) => {
return (
<div className={className} >
{(featureProperties && selectedFeatureId ) ?
<AreaDetail properties={featureProperties} /> :
<MapIntroduction />
}
</div>
);
};
export default MapInfoPanel;

View file

@ -0,0 +1,34 @@
.mapIntroContainer {
padding: 20px 20px;
}
.mapIntroHeader {
font-size: xx-large;
line-height: 1.9rem;
padding-top: 0.8rem;
padding-left: 0.3rem;
}
.mapIntroText {
display: flex;
margin-top: 2.4rem;
}
.mapIntroLightbulb {
flex: 1 0 10%;
align-self: flex-start;
}
.didYouKnowBox {
padding-left: 0.4rem;
padding-top: 0.2rem;
font-size: large;
}
.didYouKnow {
font-weight: 600;
}
.didYouKnowText {
width: 95%;
padding-top: 0.3rem;
line-height: 1.5rem;
}

View file

@ -0,0 +1,18 @@
declare namespace MapIntroductionModuleScssNamespace {
export interface IMapIntroductionModuleScss {
mapIntroContainer: string;
mapIntroHeader: string;
mapIntroText: string;
mapIntroLightbulb: string;
didYouKnowBox: string
didYouKnow: string
didYouKnowText: string
}
}
declare const MapIntroductionModuleScssModule: MapIntroductionModuleScssNamespace.IMapIntroductionModuleScss & {
/** WARNING: Only available when `css-loader` is used without `style-loader` or `mini-css-extract-plugin` */
locals: MapIntroductionModuleScssNamespace.IMapIntroductionModuleScss;
};
export = MapIntroductionModuleScssModule;

View file

@ -0,0 +1,16 @@
import * as React from 'react';
import {render, screen} from '@testing-library/react';
import MapIntroduction from './mapIntroduction';
import {LocalizedComponent} from '../../src/test/testHelpers';
describe('rendering of the component', () => {
render(
<LocalizedComponent>
<MapIntroduction />
</LocalizedComponent>,
);
it('renders the title', () => {
expect(screen.getByRole('banner')).toHaveTextContent('Zoom and select a census block group to view data');
});
});

View file

@ -0,0 +1,45 @@
import React from 'react';
import {useIntl} from 'gatsby-plugin-intl';
import {defineMessages} from 'react-intl';
// @ts-ignore
import lightbulbIcon from '/node_modules/uswds/dist/img/usa-icons/lightbulb_outline.svg';
import * as styles from './mapIntroduction.module.scss';
const MapIntroduction = () => {
const intl = useIntl();
const messages = defineMessages({
mapIntroHeader: {
id: 'mapIntro.mapIntroHeader',
defaultMessage: 'Zoom and select a census block group to view data',
description: 'introductory text of ways to use the map',
},
didYouKnow: {
id: 'mapIntro.didYouKnow',
defaultMessage: ' Did you know?',
description: 'text prompting a cite paragraph',
},
censusBlockGroupDefinition: {
id: 'mapIntro.censusBlockGroupDefinition',
defaultMessage: 'A census block group is generally between 600 and 3,000 people. ' +
'It is the smallest geographical unit for which the U.S. Census ' +
'Bureau publishes sample data.',
description: 'cites the definition and helpful information about census groups',
},
});
return (
<aside className={styles.mapIntroContainer}>
<header className={styles.mapIntroHeader}>{intl.formatMessage(messages.mapIntroHeader)}</header>
<div className={styles.mapIntroText}>
<img className={styles.mapIntroLightbulb} src={lightbulbIcon} />
<div className={styles.didYouKnowBox}>
<div className={styles.didYouKnow}>{intl.formatMessage(messages.didYouKnow)}</div>
<cite className={styles.didYouKnowText}>{intl.formatMessage(messages.censusBlockGroupDefinition)}</cite>
</div>
</div>
</aside>
);
};
export default MapIntroduction;

View file

@ -1,13 +1,14 @@
import * as React from 'react';
import J40Map from './J40Map';
import MapLegend from '../components/mapLegend';
const MapWrapper = () => {
return (
<div>
{
<J40Map />
}
</div>
<section>
<h2>Explore the Tool</h2>
<J40Map />
<MapLegend />
</section>
);
};