mirror of
https://github.com/DOI-DO/j40-cejst-2.git
synced 2025-02-23 01:54:18 -08:00
refactoring popup into shared component
This commit is contained in:
parent
ce56b1ce1f
commit
4dbe05275f
6 changed files with 130 additions and 77 deletions
|
@ -1,11 +1,10 @@
|
||||||
import React, {useRef, useEffect, useState} from 'react';
|
import React, {useRef, useEffect, useState} from 'react';
|
||||||
import * as styles from './mapPopup.module.scss';
|
import * as styles from './mapPopup.module.scss';
|
||||||
import Overlay from 'ol/Overlay';
|
import Overlay from 'ol/Overlay';
|
||||||
import {Table} from '@trussworks/react-uswds';
|
|
||||||
import {Coordinate} from 'ol/Coordinate';
|
import {Coordinate} from 'ol/Coordinate';
|
||||||
import Map from 'ol/Map';
|
import Map from 'ol/Map';
|
||||||
import {FeatureLike} from 'ol/Feature';
|
import {FeatureLike} from 'ol/Feature';
|
||||||
import * as constants from '../data/constants';
|
import PopupContent from './popupContent';
|
||||||
|
|
||||||
interface IMapPopupProps {
|
interface IMapPopupProps {
|
||||||
map: Map,
|
map: Map,
|
||||||
|
@ -19,10 +18,6 @@ const MapPopup = ({map, selectedFeature, position}: IMapPopupProps) => {
|
||||||
const popupContentElement = useRef<HTMLDivElement>(null);
|
const popupContentElement = useRef<HTMLDivElement>(null);
|
||||||
const [currentOverlay, setCurrentOverlay] = useState<Overlay>();
|
const [currentOverlay, setCurrentOverlay] = useState<Overlay>();
|
||||||
|
|
||||||
const readablePercent = (percent: number) => {
|
|
||||||
return `${(percent * 100).toFixed(2)}%`;
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
popupCloserElement.current!.onclick = function() {
|
popupCloserElement.current!.onclick = function() {
|
||||||
overlay.setPosition(undefined);
|
overlay.setPosition(undefined);
|
||||||
|
@ -47,81 +42,13 @@ const MapPopup = ({map, selectedFeature, position}: IMapPopupProps) => {
|
||||||
}
|
}
|
||||||
}, [position]);
|
}, [position]);
|
||||||
|
|
||||||
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 properties = selectedFeature.getProperties();
|
|
||||||
const blockGroup = properties['GEOID10'];
|
|
||||||
const score = properties[constants.SCORE_PROPERTY];
|
|
||||||
return (
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td><strong>Census Block Group:</strong></td>
|
|
||||||
<td>{blockGroup}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><strong>Just Progress Categorization:</strong></td>
|
|
||||||
<td>{getCategorization(score)}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td><strong>Cumulative Index Score:</strong></td>
|
|
||||||
<td>{readablePercent(score)}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getBodyContent = () => {
|
|
||||||
const properties = selectedFeature.getProperties();
|
|
||||||
const rows = [];
|
|
||||||
for (const [key, value] of Object.entries(properties)) {
|
|
||||||
// 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 popupContent = (selectedFeature) ? (
|
|
||||||
<div>
|
|
||||||
{getTitleContent()}
|
|
||||||
<Table bordered={false}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th scope="col">Indicator</th>
|
|
||||||
<th scope="col">Percentile(0-100)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{getBodyContent()}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div ref={popupContainerElement}
|
<div ref={popupContainerElement}
|
||||||
className={styles.popupContainer}>
|
className={styles.popupContainer}>
|
||||||
<a href="#" ref={popupCloserElement} className={styles.popupCloser}></a>
|
<a href="#" ref={popupCloserElement} className={styles.popupCloser}></a>
|
||||||
<div ref={popupContentElement} className={styles.popupContent}>
|
<div ref={popupContentElement} className={styles.popupContent}>
|
||||||
{popupContent}
|
<PopupContent properties={selectedFeature?.getProperties()} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -20,3 +20,9 @@ $sidebar-color: #ffffff;
|
||||||
margin: 12px;
|
margin: 12px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mapboxgl-popup {
|
||||||
|
background-color: red;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ declare namespace MapboxMapModuleScssNamespace {
|
||||||
export interface IMapboxMapModuleScss {
|
export interface IMapboxMapModuleScss {
|
||||||
sidebar: string;
|
sidebar: string;
|
||||||
mapContainer: string;
|
mapContainer: string;
|
||||||
|
mapboxglPopup: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
import React, {useRef, useEffect, useState} from 'react';
|
import React, {useRef, useEffect, useState} from 'react';
|
||||||
import {LngLatBoundsLike, Map, NavigationControl} from 'mapbox-gl';
|
import {LngLatBoundsLike,
|
||||||
|
Map,
|
||||||
|
NavigationControl,
|
||||||
|
PopupOptions,
|
||||||
|
Popup} from 'mapbox-gl';
|
||||||
import mapStyle from '../data/mapStyle';
|
import mapStyle from '../data/mapStyle';
|
||||||
import ZoomWarning from './zoomWarning';
|
import ZoomWarning from './zoomWarning';
|
||||||
|
import PopupContent from './popupContent';
|
||||||
import * as styles from './mapboxMap.module.scss';
|
import * as styles from './mapboxMap.module.scss';
|
||||||
import * as constants from '../data/constants';
|
import * as constants from '../data/constants';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
|
type ClickEvent = mapboxgl.MapMouseEvent & mapboxgl.EventData;
|
||||||
|
|
||||||
const MapboxMap = () => {
|
const MapboxMap = () => {
|
||||||
const mapContainer = React.useRef<HTMLDivElement>(null);
|
const mapContainer = React.useRef<HTMLDivElement>(null);
|
||||||
|
@ -26,11 +34,33 @@ const MapboxMap = () => {
|
||||||
maxZoom: constants.GLOBAL_MAX_ZOOM,
|
maxZoom: constants.GLOBAL_MAX_ZOOM,
|
||||||
maxBounds: constants.GLOBAL_MAX_BOUNDS as LngLatBoundsLike,
|
maxBounds: constants.GLOBAL_MAX_BOUNDS as LngLatBoundsLike,
|
||||||
});
|
});
|
||||||
|
initialMap.on('click', handleClick);
|
||||||
initialMap.addControl(new NavigationControl());
|
initialMap.addControl(new NavigationControl());
|
||||||
map.current = initialMap;
|
map.current = initialMap;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleClick = (e: ClickEvent) => {
|
||||||
|
const map = e.target;
|
||||||
|
const clickedCoord = e.point;
|
||||||
|
const features = map.queryRenderedFeatures(clickedCoord, {
|
||||||
|
layers: ['score-low'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (features.length && features[0].properties) {
|
||||||
|
const placeholder = document.createElement('div');
|
||||||
|
ReactDOM.render(<PopupContent properties={features[0].properties} />, placeholder);
|
||||||
|
const options : PopupOptions = {
|
||||||
|
offset: [0, 0],
|
||||||
|
className: styles.mapboxglPopup,
|
||||||
|
};
|
||||||
|
new Popup(options)
|
||||||
|
.setLngLat(e.lngLat)
|
||||||
|
.setDOMContent(placeholder)
|
||||||
|
.setMaxWidth('300px')
|
||||||
|
.addTo(map);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map.current) return; // wait for map to initialize
|
if (!map.current) return; // wait for map to initialize
|
||||||
map.current.on('move', () => {
|
map.current.on('move', () => {
|
||||||
|
|
87
client/src/components/popupContent.tsx
Normal file
87
client/src/components/popupContent.tsx
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import {Table} from '@trussworks/react-uswds';
|
||||||
|
import * as constants from '../data/constants';
|
||||||
|
|
||||||
|
interface IPopupContentProps {
|
||||||
|
properties: constants.J40Properties,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const PopupContent = ({properties}:IPopupContentProps) => {
|
||||||
|
const readablePercent = (percent: number) => {
|
||||||
|
return `${(percent * 100).toFixed(2)}%`;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = (properties: constants.J40Properties) => {
|
||||||
|
const blockGroup = properties[constants.GEOID_PROPERTY];
|
||||||
|
const score = properties[constants.SCORE_PROPERTY] as number;
|
||||||
|
return (
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Census Block Group:</strong></td>
|
||||||
|
<td>{blockGroup}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Just Progress Categorization:</strong></td>
|
||||||
|
<td>{getCategorization(score)}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Cumulative Index Score:</strong></td>
|
||||||
|
<td>{readablePercent(score)}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBodyContent = (properties: constants.J40Properties) => {
|
||||||
|
const rows = [];
|
||||||
|
for (const [key, value] of Object.entries(properties)) {
|
||||||
|
// Filter out all caps
|
||||||
|
if (!key.match(/^[A-Z0-9]+$/)) {
|
||||||
|
rows.push(<tr key={key} >
|
||||||
|
<td>{key}</td>
|
||||||
|
<td>{value}</td>
|
||||||
|
</tr>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{properties ?
|
||||||
|
<div>
|
||||||
|
{getTitleContent(properties)}
|
||||||
|
<Table bordered={false}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Indicator</th>
|
||||||
|
<th scope="col">Percentile(0-100)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{getBodyContent(properties)}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
</div> :
|
||||||
|
'' }
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PopupContent;
|
|
@ -1,4 +1,5 @@
|
||||||
export const SCORE_PROPERTY = 'Score D (percentile)';
|
export const SCORE_PROPERTY = 'Score D (percentile)';
|
||||||
|
export const GEOID_PROPERTY = 'GEOID10';
|
||||||
export const GLOBAL_MIN_ZOOM = 3;
|
export const GLOBAL_MIN_ZOOM = 3;
|
||||||
export const GLOBAL_MAX_ZOOM = 11;
|
export const GLOBAL_MAX_ZOOM = 11;
|
||||||
export const GLOBAL_MIN_ZOOM_LOW = 3;
|
export const GLOBAL_MIN_ZOOM_LOW = 3;
|
||||||
|
@ -7,3 +8,4 @@ export const GLOBAL_MIN_ZOOM_HIGH = 9;
|
||||||
export const GLOBAL_MAX_ZOOM_HIGH = 11;
|
export const GLOBAL_MAX_ZOOM_HIGH = 11;
|
||||||
export const GLOBAL_MAX_BOUNDS = [[-167.276413, 5.499550], [-52.233040, 83.162102]];
|
export const GLOBAL_MAX_BOUNDS = [[-167.276413, 5.499550], [-52.233040, 83.162102]];
|
||||||
export const DEFAULT_CENTER = [32.4687126, -86.502136];
|
export const DEFAULT_CENTER = [32.4687126, -86.502136];
|
||||||
|
export type J40Properties = { [key: string]: any };
|
||||||
|
|
Loading…
Add table
Reference in a new issue