mirror of
https://github.com/DOI-DO/j40-cejst-2.git
synced 2025-02-23 01:54:18 -08:00
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:
parent
a787bd71ab
commit
36f43b2d44
25 changed files with 1430 additions and 27185 deletions
376
client/compiled-lang/en.json
Normal file
376
client/compiled-lang/en.json
Normal file
|
@ -0,0 +1,376 @@
|
||||||
|
{
|
||||||
|
"areaDetail.geographicInfo.censusBlockGroup": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Census block group:"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areaDetail.geographicInfo.county": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "County:"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areaDetail.geographicInfo.population": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Population:"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areaDetail.geographicInfo.state": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "State:"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areaDetail.indicator.education": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Education"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areaDetail.indicator.houseBurden": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Housing Burden"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areaDetail.indicator.linguisticIsolation": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Linguistic Isolation"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areaDetail.indicator.poverty": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Poverty"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areaDetail.indicator.unemployment": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Unemployment"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areaDetail.indicators.indicatorColumnHeader": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "INDICATORS"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areaDetail.indicators.percentileColumnHeader": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "PERCENTILE (0-100)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areaDetail.priorityInfo.categorization": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Categorization"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areaDetail.priorityInfo.cumulativeIndexScore": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Cumulative Index Score"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areaDetail.priorityInfo.percentile": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "percentile"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areasOfInterest.climate": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Climate change"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areasOfInterest.energy": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Clean energy and energy efficiency"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areasOfInterest.housing": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Affordable and sustainable housing"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areasOfInterest.pollution": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Remediation of legacy pollution"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areasOfInterest.training": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Training and workforce development"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areasOfInterest.transit": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Clean transit"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"areasOfInterest.water": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Clean water infrastructure"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"footer.arialabel": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Footer navigation"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"footer.findcontactlink": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Find a contact at USA.gov"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"footer.foialink": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Freedom of Information Act (FOIA)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"footer.logo.title": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Council on Environmental Quality"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"footer.moreinfoheader": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "More Information"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"footer.privacylink": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Privacy Policy"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"footer.questionsheader": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Have a question about government services?"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"footer.whitehouselogoalt": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Whitehouse logo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"header.about": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "About"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"header.contact": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Contact"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"header.explore": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Explore the tool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"header.methodology": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Methodology"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"header.timeline": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Timeline"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"header.title": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Justice40"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index.aboutContent.header": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "About Justice40"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index.aboutContent.p1": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "In an effort to address historical environmental injustices, President Biden created the Justice40 Initiative on January 27, 2021. The Justice40 Initiative directs 40% of the benefits from federal investments in seven key areas to overburdened and underserved communities."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index.aboutContent.p2": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Federal agencies will prioritize benefits using a new climate and economic justice screening tool. This screening tool will be a map that visualizes data to compare the cumulative impacts of environmental, climate, and economic factors. It is being developed by the Council on Environmental Quality (CEQ) with guidance from environmental justice leaders and communities affected by environmental injustices. The first version of the screening tool will be released in July 2021. However, the screening tool and data being used will be continuously updated to better reflect the lived experiences of community members."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index.aboutContent.p3": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Read more about the Justice40 Initiative in President Biden’s "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "presidentLink"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index.presidentalLinkLabel": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Executive Order on Tackling the Climate Crisis at Home and Abroad."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index.presidentalLinkUri": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "https://www.whitehouse.gov/briefing-room/presidential-actions/2021/01/27/executive-order-on-tackling-the-climate-crisis-at-home-and-abroad/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index.section2.header": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Areas of Focus"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index.section3.header": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "A Transparent, Community-First Approach"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index.section3.inclusive": [
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "inlineHeader"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": " Many areas which lack investments also lack environmental data and would be overlooked using available environmental data. CEQ is actively reaching out to groups that have historically been excluded from decision-making, such as groups in rural and tribal areas, to understand their needs and ask for their input."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index.section3.inclusiveLabel": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Inclusive:"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index.section3.intro": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Successful initiatives are guided by direct input from the communities they are serving. CEQ commits to transparency, inclusivity, and iteration in building this screening tool."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index.section3.iterative": [
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "inlineHeader"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": " The initial community prioritization list provided by the screening tool is the beginning of a collaborative process in score refinement, rather than a final answer. CEQ has received recommendations on data sets from community interviews, the White House Environmental Justice Advisory Council, and through public comment, but establishing a score that is truly representative will be a long-term, ongoing process. As communities submit feedback and recommendations, CEQ will continue to improve the tools being built and the processes for stakeholder and public engagement."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index.section3.iterativeLabel": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Iterative:"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index.section3.transparent": [
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "inlineHeader"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": " The code and data behind the screening tool are open source, meaning it is available for the public to review and contribute to. This tool is being developed publicly so that communities, academic experts, and anyone who’s interested can be involved in the tool-building process."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index.section3.transparentLabel": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Transparent:"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"map.territoryFocus.alaska.long": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Alaska"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"map.territoryFocus.alaska.short": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "AK"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"map.territoryFocus.focusOn": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Focus on "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": 1,
|
||||||
|
"value": "territory"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"map.territoryFocus.hawaii.long": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Hawaii"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"map.territoryFocus.hawaii.short": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "HI"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"map.territoryFocus.lower48.long": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Lower 48"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"map.territoryFocus.lower48.short": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "48"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"map.territoryFocus.puerto_rico.long": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Puerto Rico"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"map.territoryFocus.puerto_rico.short": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "PR"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
3
client/cypress/e2e/constants.js
Normal file
3
client/cypress/e2e/constants.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const ENDPOINTS = {
|
||||||
|
EXPLORE_THE_TOOL: '/en/cejst',
|
||||||
|
};
|
67
client/cypress/e2e/mapInfoPanel.spec.js
Normal file
67
client/cypress/e2e/mapInfoPanel.spec.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
// / <reference types="Cypress" />
|
||||||
|
|
||||||
|
/*
|
||||||
|
A risk with this test is that if the feature/area that is currently being selected become non-prioritized, then this
|
||||||
|
test will fail. However it would be a major win for that area!
|
||||||
|
*/
|
||||||
|
import {ENDPOINTS} from './constants';
|
||||||
|
|
||||||
|
const devices = [
|
||||||
|
[1024, 720],
|
||||||
|
['iphone-6', 'portrait'],
|
||||||
|
['samsung-s10', 'portrait'],
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('tests that the map side panel renders MapIntroduction component', () => {
|
||||||
|
devices.forEach((device) => {
|
||||||
|
it(`should render MapIntroduction component on ${device[0]} x ${device[1]}`, () => {
|
||||||
|
cy.visit(ENDPOINTS.EXPLORE_THE_TOOL);
|
||||||
|
cy.viewport(device[0], device[1]);
|
||||||
|
cy.get('aside').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Todo: Ticket #423:
|
||||||
|
*
|
||||||
|
* After fixing the PR deployed URL in regards to the mobile view (parent height
|
||||||
|
* not setting to 44vh) by creating a state variable to force a re-render, this
|
||||||
|
* cypress test regressed and is now failing. Need to investigate why.
|
||||||
|
*
|
||||||
|
* See this ticket for more info:
|
||||||
|
* https://app.zenhub.com/workspaces/justice40-60993f6e05473d0010ec44e3/issues/usds/justice40-tool/423
|
||||||
|
*
|
||||||
|
* Tried
|
||||||
|
* 1. reloading the page
|
||||||
|
* 2. forcing the 44vh value via a selector (data-cy and class)
|
||||||
|
*/
|
||||||
|
// describe('tests that the map side panel renders AreaDetail component', () => {
|
||||||
|
// devices.forEach((device) => {
|
||||||
|
// it(`should render AreaDetail component on ${device[0]} x ${device[1]}`, () => {
|
||||||
|
// // Only set the viewport for mobile devices:
|
||||||
|
// cy.visit(ENDPOINTS.EXPLORE_THE_TOOL);
|
||||||
|
// if (!Number.isInteger(device[0])) cy.viewport(device[0], device[1]);
|
||||||
|
// cy.reload(true);
|
||||||
|
// cy.get('div[class*="mapContainer"]').invoke('attr', 'height', '44vh');
|
||||||
|
// cy.getMap().then((map) => {
|
||||||
|
// // Loop over the click event simulating zooming by a certain amount
|
||||||
|
// // The map will end at the end zoom level
|
||||||
|
// const endZoomLevel = device[0].isInteger ? 11 : 10;
|
||||||
|
// for (let zoom = 3; zoom <= endZoomLevel; zoom++) {
|
||||||
|
// cy.get('.mapboxgl-ctrl-zoom-in > .mapboxgl-ctrl-icon').click();
|
||||||
|
// cy.waitForMapIdle(map);
|
||||||
|
// }
|
||||||
|
// cy.get('.overlays').click('bottomRight');
|
||||||
|
// cy.get('aside').should('be.visible');
|
||||||
|
// cy.get('[data-cy="score"]').should('be.visible');
|
||||||
|
// cy.get('[data-cy="indicatorBox"]').should('be.visible');
|
||||||
|
// cy.get('[data-cy="indicatorBox"]')
|
||||||
|
// .each((indicator) => cy.wrap(indicator)
|
||||||
|
// // currently the padding-top on desktop = 1.5rem => 24px
|
||||||
|
// // currently the padding-top on mobile = .8 rem => 8px
|
||||||
|
// .should('have.css', 'padding-top', `${Number.isInteger(device[0]) ? '24px' : '8px'}`));
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
27128
client/package-lock.json
generated
27128
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -79,6 +79,7 @@
|
||||||
"maplibre-gl": ">=1.14.0",
|
"maplibre-gl": ">=1.14.0",
|
||||||
"query-string": "^7.0.0",
|
"query-string": "^7.0.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
|
"react-device-detect": "^1.17.0",
|
||||||
"react-dom": "^17.0.1",
|
"react-dom": "^17.0.1",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-intl": "^5.20.4",
|
"react-intl": "^5.20.4",
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
$sidebar-background: rgba(35, 55, 75, 0.9);
|
@import "./areaDetailUtils.scss";
|
||||||
$sidebar-color: #ffffff;
|
|
||||||
|
|
||||||
.mapContainer {
|
.mapAndInfoPanelContainer {
|
||||||
height: 676px;
|
display: flex;
|
||||||
|
height: 81vh; //desktop
|
||||||
|
|
||||||
|
@media screen and (max-width: $mobileBreakpoint) {
|
||||||
|
flex-direction: column;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.j40Popup {
|
.j40Popup {
|
||||||
max-height: 50%;
|
width: 375px;
|
||||||
overflow-y: scroll;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigationControl {
|
.navigationControl {
|
||||||
|
@ -25,3 +29,9 @@ $sidebar-color: #ffffff;
|
||||||
right: 1.25em;
|
right: 1.25em;
|
||||||
top: 5em;
|
top: 5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mapInfoPanel {
|
||||||
|
border: 2px solid $sidePanelBorderColor;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-width: 22rem;
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
declare namespace J40MapModuleScssNamespace {
|
declare namespace J40MapModuleScssNamespace {
|
||||||
export interface IJ40MapModuleScss {
|
export interface IJ40MapModuleScss {
|
||||||
mapContainer: string;
|
mapAndInfoPanelContainer: string;
|
||||||
j40Popup: string;
|
j40Popup: string;
|
||||||
territoryFocusButton: string;
|
territoryFocusButton: string;
|
||||||
territoryFocusContainer: string;
|
territoryFocusContainer: string;
|
||||||
navigationControl: string;
|
navigationControl: string;
|
||||||
fullscreenControl: string;
|
fullscreenControl: string;
|
||||||
geolocateControl: string;
|
geolocateControl: string;
|
||||||
|
detailView: string;
|
||||||
|
mapInfoPanel: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
|
// External Libs:
|
||||||
import React, {MouseEvent, useRef, useState} from 'react';
|
import React, {MouseEvent, useRef, useState} from 'react';
|
||||||
import {Map, MapboxGeoJSONFeature, LngLatBoundsLike} from 'maplibre-gl';
|
import {Map, MapboxGeoJSONFeature, LngLatBoundsLike} from 'maplibre-gl';
|
||||||
import ReactMapGL, {
|
import ReactMapGL, {
|
||||||
|
@ -11,13 +12,20 @@ import ReactMapGL, {
|
||||||
FlyToInterpolator,
|
FlyToInterpolator,
|
||||||
FullscreenControl,
|
FullscreenControl,
|
||||||
MapRef} from 'react-map-gl';
|
MapRef} from 'react-map-gl';
|
||||||
import {makeMapStyle} from '../data/mapStyle';
|
|
||||||
import AreaDetail from './areaDetail';
|
|
||||||
import bbox from '@turf/bbox';
|
import bbox from '@turf/bbox';
|
||||||
import * as d3 from 'd3-ease';
|
import * as d3 from 'd3-ease';
|
||||||
import {useFlags} from '../contexts/FlagContext';
|
import {isMobile} from 'react-device-detect';
|
||||||
import TerritoryFocusControl from './territoryFocusControl';
|
|
||||||
|
|
||||||
|
// 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 'maplibre-gl/dist/maplibre-gl.css';
|
||||||
import * as constants from '../data/constants';
|
import * as constants from '../data/constants';
|
||||||
import * as styles from './J40Map.module.scss';
|
import * as styles from './J40Map.module.scss';
|
||||||
|
@ -31,13 +39,14 @@ declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
interface IDetailViewInterface {
|
export interface IDetailViewInterface {
|
||||||
latitude: number
|
latitude: number
|
||||||
longitude: number
|
longitude: number
|
||||||
zoom: number
|
zoom: number
|
||||||
properties: constants.J40Properties,
|
properties: constants.J40Properties,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const J40Map = () => {
|
const J40Map = () => {
|
||||||
const [viewport, setViewport] = useState<ViewportProps>({
|
const [viewport, setViewport] = useState<ViewportProps>({
|
||||||
latitude: constants.DEFAULT_CENTER[0],
|
latitude: constants.DEFAULT_CENTER[0],
|
||||||
|
@ -49,6 +58,8 @@ const J40Map = () => {
|
||||||
const [detailViewData, setDetailViewData] = useState<IDetailViewInterface>();
|
const [detailViewData, setDetailViewData] = useState<IDetailViewInterface>();
|
||||||
const [transitionInProgress, setTransitionInProgress] = useState<boolean>(false);
|
const [transitionInProgress, setTransitionInProgress] = useState<boolean>(false);
|
||||||
const [geolocationInProgress, setGeolocationInProgress] = useState<boolean>(false);
|
const [geolocationInProgress, setGeolocationInProgress] = useState<boolean>(false);
|
||||||
|
const [isMobileMapState, setIsMobileMapState] = useState<boolean>(false);
|
||||||
|
|
||||||
const mapRef = useRef<MapRef>(null);
|
const mapRef = useRef<MapRef>(null);
|
||||||
const flags = useFlags();
|
const flags = useFlags();
|
||||||
|
|
||||||
|
@ -66,6 +77,7 @@ const J40Map = () => {
|
||||||
padding: 40,
|
padding: 40,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// If we've selected a new feature, set 'selected' to false
|
// If we've selected a new feature, set 'selected' to false
|
||||||
if (selectedFeature && feature.id !== selectedFeature.id) {
|
if (selectedFeature && feature.id !== selectedFeature.id) {
|
||||||
setMapSelected(selectedFeature, false);
|
setMapSelected(selectedFeature, false);
|
||||||
|
@ -90,6 +102,8 @@ const J40Map = () => {
|
||||||
if (typeof window !== 'undefined' && window.Cypress && mapRef.current) {
|
if (typeof window !== 'undefined' && window.Cypress && mapRef.current) {
|
||||||
window.underlyingMap = mapRef.current.getMap();
|
window.underlyingMap = mapRef.current.getMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isMobile) setIsMobileMapState(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -110,6 +124,7 @@ const J40Map = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const setMapSelected = (feature:MapboxGeoJSONFeature, isSelected:boolean) : void => {
|
const setMapSelected = (feature:MapboxGeoJSONFeature, isSelected:boolean) : void => {
|
||||||
// The below can be confirmed during debug with:
|
// The below can be confirmed during debug with:
|
||||||
// mapRef.current.getFeatureState({"id":feature.id, "source":feature.source, "sourceLayer":feature.sourceLayer})
|
// mapRef.current.getFeatureState({"id":feature.id, "source":feature.source, "sourceLayer":feature.sourceLayer})
|
||||||
|
@ -125,6 +140,7 @@ const J40Map = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const onClickTerritoryFocusButton = (event: MouseEvent<HTMLButtonElement>) => {
|
const onClickTerritoryFocusButton = (event: MouseEvent<HTMLButtonElement>) => {
|
||||||
const buttonID = event.target && (event.target as HTMLElement).id;
|
const buttonID = event.target && (event.target as HTMLElement).id;
|
||||||
|
|
||||||
|
@ -164,16 +180,15 @@ const J40Map = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className={styles.mapAndInfoPanelContainer}>
|
||||||
<ReactMapGL
|
<ReactMapGL
|
||||||
{...viewport}
|
{...viewport}
|
||||||
className={styles.mapContainer}
|
|
||||||
mapStyle={makeMapStyle(flags)}
|
mapStyle={makeMapStyle(flags)}
|
||||||
minZoom={constants.GLOBAL_MIN_ZOOM}
|
minZoom={constants.GLOBAL_MIN_ZOOM}
|
||||||
maxZoom={constants.GLOBAL_MAX_ZOOM}
|
maxZoom={constants.GLOBAL_MAX_ZOOM}
|
||||||
mapOptions={{hash: true}}
|
mapOptions={{hash: true}}
|
||||||
width="100%"
|
width="100%"
|
||||||
height="52vw"
|
height={isMobileMapState ? '44vh' : '100%'}
|
||||||
dragRotate={false}
|
dragRotate={false}
|
||||||
touchRotate={false}
|
touchRotate={false}
|
||||||
interactiveLayerIds={[constants.HIGH_SCORE_LAYER_NAME]}
|
interactiveLayerIds={[constants.HIGH_SCORE_LAYER_NAME]}
|
||||||
|
@ -183,8 +198,9 @@ const J40Map = () => {
|
||||||
onTransitionStart={onTransitionStart}
|
onTransitionStart={onTransitionStart}
|
||||||
onTransitionEnd={onTransitionEnd}
|
onTransitionEnd={onTransitionEnd}
|
||||||
ref={mapRef}
|
ref={mapRef}
|
||||||
|
data-cy={'reactMapGL'}
|
||||||
>
|
>
|
||||||
{(detailViewData && !transitionInProgress) && (
|
{('fs' in flags && detailViewData && !transitionInProgress) && (
|
||||||
<Popup
|
<Popup
|
||||||
className={styles.j40Popup}
|
className={styles.j40Popup}
|
||||||
tipSize={5}
|
tipSize={5}
|
||||||
|
@ -198,7 +214,6 @@ const J40Map = () => {
|
||||||
<AreaDetail properties={detailViewData.properties} />
|
<AreaDetail properties={detailViewData.properties} />
|
||||||
</Popup>
|
</Popup>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<NavigationControl
|
<NavigationControl
|
||||||
showCompass={false}
|
showCompass={false}
|
||||||
className={styles.navigationControl}
|
className={styles.navigationControl}
|
||||||
|
@ -214,7 +229,12 @@ const J40Map = () => {
|
||||||
<TerritoryFocusControl onClickTerritoryFocusButton={onClickTerritoryFocusButton}/>
|
<TerritoryFocusControl onClickTerritoryFocusButton={onClickTerritoryFocusButton}/>
|
||||||
{'fs' in flags ? <FullscreenControl className={styles.fullscreenControl}/> :'' }
|
{'fs' in flags ? <FullscreenControl className={styles.fullscreenControl}/> :'' }
|
||||||
</ReactMapGL>
|
</ReactMapGL>
|
||||||
</>
|
<MapInfoPanel
|
||||||
|
className={styles.mapInfoPanel}
|
||||||
|
featureProperties={detailViewData?.properties}
|
||||||
|
selectedFeatureId={selectedFeature?.id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
159
client/src/components/__snapshots__/areaDetail.test.tsx.snap
Normal file
159
client/src/components/__snapshots__/areaDetail.test.tsx.snap
Normal 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 didn’t 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>
|
||||||
|
`;
|
|
@ -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>
|
||||||
|
`;
|
|
@ -1,23 +1,156 @@
|
||||||
.areaDetailTable {
|
@import "./areaDetailUtils.scss";
|
||||||
max-width: 31.6vw;
|
|
||||||
|
$sidePanelLabelFontColor: #171716;
|
||||||
|
$featureSelectBorderColor: #00bde3;
|
||||||
|
|
||||||
|
@mixin sidePanelLabelStyle {
|
||||||
|
font-size: small;
|
||||||
|
color: $sidePanelLabelFontColor;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.titleContainer {
|
@mixin categorizationCircleStyle {
|
||||||
display: flex;
|
height: 0.6rem;
|
||||||
flex-direction: column;
|
width: 0.6rem;
|
||||||
padding: 22px 22px;
|
border-radius: 100%;
|
||||||
}
|
align-self: center;
|
||||||
|
margin-top: 0.8rem;
|
||||||
.titleIndicatorName {
|
margin-right: 0.5rem;
|
||||||
font-weight: bold;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.areaDetailContainer {
|
.areaDetailContainer {
|
||||||
max-height: 50vh;
|
display: flex;
|
||||||
overflow: scroll;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.areaDetailTableContainer {
|
// top row styles
|
||||||
overflow: auto;
|
.topRow {
|
||||||
padding: 22px;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,27 @@
|
||||||
declare namespace MapModuleScssNamespace {
|
declare namespace MapModuleScssNamespace {
|
||||||
export interface IMapModuleScss {
|
export interface IMapModuleScss {
|
||||||
areaDetailContainer: string;
|
areaDetailContainer: string;
|
||||||
areaDetailTable:string;
|
topRow:string;
|
||||||
areaDetailTableContainer:string;
|
cumulativeIndexScore:string;
|
||||||
titleContainer:string;
|
scoreSuperscript: string;
|
||||||
titleIndicatorName: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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
47
client/src/components/areaDetail.test.tsx
Normal file
47
client/src/components/areaDetail.test.tsx
Normal 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]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,92 +1,210 @@
|
||||||
|
// External Libs:
|
||||||
import * as React from 'react';
|
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 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 {
|
interface IAreaDetailProps {
|
||||||
properties: constants.J40Properties,
|
properties: constants.J40Properties,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const AreaDetail = ({properties}:IAreaDetailProps) => {
|
const AreaDetail = ({properties}:IAreaDetailProps) => {
|
||||||
const readablePercent = (percent: number) => {
|
const intl = useIntl();
|
||||||
return `${(percent * 100).toFixed(2)}`;
|
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 didn’t 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 didn’t 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) => {
|
const indicators = [povertyInfo, eduInfo, linIsoInfo, umemployInfo, houseBurden];
|
||||||
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 [categorization, categoryCircleStyle] = getCategorization(score);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<aside className={styles.areaDetailContainer} data-cy={'aside'}>
|
||||||
{properties ?
|
<header className={styles.topRow }>
|
||||||
<div className={styles.areaDetailContainer}>
|
<div className={styles.cumulativeIndexScore}>
|
||||||
{getTitleContent()}
|
<div className={styles.topRowTitle}>{intl.formatMessage(messages.cumulativeIndexScore)}</div>
|
||||||
<div className={styles.areaDetailTableContainer}>
|
<div className={styles.score} data-cy={'score'}>{`${readablePercent(score)}`}
|
||||||
<table className={'usa-table usa-table--borderless ' + styles.areaDetailTable}>
|
<sup className={styles.scoreSuperscript}><span>th</span></sup>
|
||||||
<thead>
|
</div>
|
||||||
<tr>
|
<div className={styles.topRowSubTitle}>{intl.formatMessage(messages.percentile)}</div>
|
||||||
<th scope="col">INDICATOR</th>
|
|
||||||
<th scope="col">VALUE</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{getBodyContent()}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
10
client/src/components/areaDetailUtils.scss
Normal file
10
client/src/components/areaDetailUtils.scss
Normal 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;
|
51
client/src/components/mapInfoPanel.test.tsx
Normal file
51
client/src/components/mapInfoPanel.test.tsx
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
22
client/src/components/mapInfoPanel.tsx
Normal file
22
client/src/components/mapInfoPanel.tsx
Normal 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;
|
34
client/src/components/mapIntroduction.module.scss
Normal file
34
client/src/components/mapIntroduction.module.scss
Normal 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;
|
||||||
|
}
|
18
client/src/components/mapIntroduction.module.scss.d.ts
vendored
Normal file
18
client/src/components/mapIntroduction.module.scss.d.ts
vendored
Normal 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;
|
16
client/src/components/mapIntroduction.test.tsx
Normal file
16
client/src/components/mapIntroduction.test.tsx
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
45
client/src/components/mapIntroduction.tsx
Normal file
45
client/src/components/mapIntroduction.tsx
Normal 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;
|
|
@ -1,13 +1,14 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import J40Map from './J40Map';
|
import J40Map from './J40Map';
|
||||||
|
import MapLegend from '../components/mapLegend';
|
||||||
|
|
||||||
const MapWrapper = () => {
|
const MapWrapper = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<section>
|
||||||
{
|
<h2>Explore the Tool</h2>
|
||||||
<J40Map />
|
<J40Map />
|
||||||
}
|
<MapLegend />
|
||||||
</div>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import {LngLatBoundsLike} from 'maplibre-gl';
|
import {LngLatBoundsLike} from 'maplibre-gl';
|
||||||
|
import {isMobile as isMobileReactDeviceDetect} from 'react-device-detect';
|
||||||
|
|
||||||
// URLS
|
// URLS
|
||||||
export const FEATURE_TILE_BASE_URL = 'https://d2zjid6n5ja2pt.cloudfront.net';
|
export const FEATURE_TILE_BASE_URL = 'https://d2zjid6n5ja2pt.cloudfront.net';
|
||||||
|
@ -22,13 +23,20 @@ export const HIGH_SCORE_LAYER_NAME = 'score-high-layer';
|
||||||
export const LOW_SCORE_SOURCE_NAME = 'score-low';
|
export const LOW_SCORE_SOURCE_NAME = 'score-low';
|
||||||
export const LOW_SCORE_LAYER_NAME = 'score-low-layer';
|
export const LOW_SCORE_LAYER_NAME = 'score-low-layer';
|
||||||
export const SELECTED_PROPERTY = 'selected';
|
export const SELECTED_PROPERTY = 'selected';
|
||||||
|
export const POVERTY_PROPERTY_PERCENTILE = 'Poverty (Less than 200% of federal poverty line) (percentile)';
|
||||||
|
export const HOUSING_BURDEN_PROPERTY_PERCENTILE = 'Housing burden (percent) (percentile)';
|
||||||
|
export const LINGUISTIC_ISOLATION_PROPERTY_PERCENTILE = 'Linguistic isolation (percent) (percentile)';
|
||||||
|
export const UNEMPLOYMENT_PROPERTY_PERCENTILE = 'Unemployed civilians (percent) (percentile)';
|
||||||
|
export const TOTAL_POPULATION = 'Total population';
|
||||||
|
|
||||||
|
export const EDUCATION_PROPERTY_PERCENTILE = 'Percent individuals age 25 or over ' +
|
||||||
|
'with less than high school degree (percentile)';
|
||||||
|
|
||||||
// The name of the layer within the tiles that contains the score
|
// The name of the layer within the tiles that contains the score
|
||||||
export const SCORE_SOURCE_LAYER = 'blocks';
|
export const SCORE_SOURCE_LAYER = 'blocks';
|
||||||
|
|
||||||
export type J40Properties = { [key: string]: any };
|
export type J40Properties = { [key: string]: any };
|
||||||
|
|
||||||
|
|
||||||
// Zoom
|
// Zoom
|
||||||
export const GLOBAL_MIN_ZOOM = 3;
|
export const GLOBAL_MIN_ZOOM = 3;
|
||||||
export const GLOBAL_MAX_ZOOM = 22;
|
export const GLOBAL_MAX_ZOOM = 22;
|
||||||
|
@ -100,4 +108,4 @@ export const SCORE_BOUNDARY_LOW = 0.0;
|
||||||
export const SCORE_BOUNDARY_THRESHOLD = 0.6;
|
export const SCORE_BOUNDARY_THRESHOLD = 0.6;
|
||||||
export const SCORE_BOUNDARY_PRIORITIZED = 0.75;
|
export const SCORE_BOUNDARY_PRIORITIZED = 0.75;
|
||||||
|
|
||||||
export const isMobile = typeof window !== 'undefined' && (window.innerWidth < 400);
|
export const isMobile = isMobileReactDeviceDetect;
|
||||||
|
|
|
@ -1,4 +1,60 @@
|
||||||
{
|
{
|
||||||
|
"areaDetail.geographicInfo.censusBlockGroup": {
|
||||||
|
"defaultMessage": "Census block group:",
|
||||||
|
"description": "the census block group id number of the feature selected"
|
||||||
|
},
|
||||||
|
"areaDetail.geographicInfo.county": {
|
||||||
|
"defaultMessage": "County:",
|
||||||
|
"description": "the county of the feature selected"
|
||||||
|
},
|
||||||
|
"areaDetail.geographicInfo.population": {
|
||||||
|
"defaultMessage": "Population:",
|
||||||
|
"description": "the population of the feature selected"
|
||||||
|
},
|
||||||
|
"areaDetail.geographicInfo.state": {
|
||||||
|
"defaultMessage": "State:",
|
||||||
|
"description": "the state of the feature selected"
|
||||||
|
},
|
||||||
|
"areaDetail.indicator.education": {
|
||||||
|
"defaultMessage": "Education",
|
||||||
|
"description": "Percent of people age 25 or older that didn’t get a high school diploma"
|
||||||
|
},
|
||||||
|
"areaDetail.indicator.houseBurden": {
|
||||||
|
"defaultMessage": "Housing Burden",
|
||||||
|
"description": "Households that are low income and spend more than 30% of their income to housing costs"
|
||||||
|
},
|
||||||
|
"areaDetail.indicator.linguisticIsolation": {
|
||||||
|
"defaultMessage": "Linguistic Isolation",
|
||||||
|
"description": "Households in which all members speak a non-English language and speak English less than \"very well\""
|
||||||
|
},
|
||||||
|
"areaDetail.indicator.poverty": {
|
||||||
|
"defaultMessage": "Poverty",
|
||||||
|
"description": "Household income is less than or equal to twice the federal \"poverty level\""
|
||||||
|
},
|
||||||
|
"areaDetail.indicator.unemployment": {
|
||||||
|
"defaultMessage": "Unemployment",
|
||||||
|
"description": "Number of unemployed people as a percentage of the labor force"
|
||||||
|
},
|
||||||
|
"areaDetail.indicators.indicatorColumnHeader": {
|
||||||
|
"defaultMessage": "INDICATORS",
|
||||||
|
"description": "the population of the feature selected"
|
||||||
|
},
|
||||||
|
"areaDetail.indicators.percentileColumnHeader": {
|
||||||
|
"defaultMessage": "PERCENTILE (0-100)",
|
||||||
|
"description": "the population of the feature selected"
|
||||||
|
},
|
||||||
|
"areaDetail.priorityInfo.categorization": {
|
||||||
|
"defaultMessage": "Categorization",
|
||||||
|
"description": "the categorization of prioritized, threshold or non-prioritized"
|
||||||
|
},
|
||||||
|
"areaDetail.priorityInfo.cumulativeIndexScore": {
|
||||||
|
"defaultMessage": "Cumulative Index Score",
|
||||||
|
"description": "the cumulative score of the feature selected"
|
||||||
|
},
|
||||||
|
"areaDetail.priorityInfo.percentile": {
|
||||||
|
"defaultMessage": "percentile",
|
||||||
|
"description": "the percentil of the feature selected"
|
||||||
|
},
|
||||||
"areasOfInterest.climate": {
|
"areasOfInterest.climate": {
|
||||||
"defaultMessage": "Climate change",
|
"defaultMessage": "Climate change",
|
||||||
"description": "item in areasOfInterest list"
|
"description": "item in areasOfInterest list"
|
||||||
|
|
|
@ -2,7 +2,6 @@ import React from 'react';
|
||||||
import Layout from '../components/layout';
|
import Layout from '../components/layout';
|
||||||
import MapWrapper from '../components/mapWrapper';
|
import MapWrapper from '../components/mapWrapper';
|
||||||
import HowYouCanHelp from '../components/HowYouCanHelp';
|
import HowYouCanHelp from '../components/HowYouCanHelp';
|
||||||
import MapLegend from '../components/mapLegend';
|
|
||||||
import DownloadPacket from '../components/downloadPacket';
|
import DownloadPacket from '../components/downloadPacket';
|
||||||
import * as styles from './cejst.module.scss';
|
import * as styles from './cejst.module.scss';
|
||||||
|
|
||||||
|
@ -52,10 +51,7 @@ const CEJSTPage = ({location}: IMapPageProps) => {
|
||||||
<DownloadPacket />
|
<DownloadPacket />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<h2>Explore the Tool</h2>
|
|
||||||
<MapWrapper />
|
<MapWrapper />
|
||||||
<MapLegend />
|
|
||||||
<HowYouCanHelp />
|
<HowYouCanHelp />
|
||||||
</main>
|
</main>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
Loading…
Add table
Reference in a new issue