mirror of
https://github.com/DOI-DO/j40-cejst-2.git
synced 2025-02-23 01:54:18 -08:00
Allow for Census Tract search in UI
This commit is contained in:
parent
4130c46aee
commit
cf4e35acce
15 changed files with 362 additions and 162 deletions
261
client/package-lock.json
generated
261
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -85,6 +85,7 @@
|
||||||
"gatsby-plugin-env-variables": "^2.2.0",
|
"gatsby-plugin-env-variables": "^2.2.0",
|
||||||
"gatsby-plugin-robots-txt": "^1.7.0",
|
"gatsby-plugin-robots-txt": "^1.7.0",
|
||||||
"gatsby-plugin-sitemap": "^4.10.0",
|
"gatsby-plugin-sitemap": "^4.10.0",
|
||||||
|
"js-search": "^2.0.1",
|
||||||
"mapbox-gl": "^1.13.2",
|
"mapbox-gl": "^1.13.2",
|
||||||
"maplibre-gl": "^1.14.0",
|
"maplibre-gl": "^1.14.0",
|
||||||
"query-string": "^7.1.3",
|
"query-string": "^7.1.3",
|
||||||
|
|
|
@ -130,7 +130,7 @@ const J40Map = ({location}: IJ40Interface) => {
|
||||||
const onClick = (event: MapEvent | React.MouseEvent<HTMLButtonElement>) => {
|
const onClick = (event: MapEvent | React.MouseEvent<HTMLButtonElement>) => {
|
||||||
// Stop all propagation / bubbling / capturing
|
// Stop all propagation / bubbling / capturing
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
(event as React.MouseEvent<HTMLButtonElement>).stopPropagation?.();
|
||||||
|
|
||||||
// Check if the click is for territories. Given the territories component's design, it can be
|
// Check if the click is for territories. Given the territories component's design, it can be
|
||||||
// guaranteed that each territory control will have an id. We use this ID to determine
|
// guaranteed that each territory control will have an id. We use this ID to determine
|
||||||
|
@ -167,8 +167,9 @@ const J40Map = ({location}: IJ40Interface) => {
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else if (event.target && (event.target as HTMLElement).nodeName == 'DIV' ) {
|
||||||
// This else clause will fire when the ID is null or empty. This is the case where the map is clicked
|
// This else clause will fire when the user clicks on the map and will ignore other controls
|
||||||
|
// such as the search box and buttons.
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const feature = event.features && event.features[0];
|
const feature = event.features && event.features[0];
|
||||||
|
|
|
@ -4,6 +4,8 @@ import {LngLatBoundsLike} from 'maplibre-gl';
|
||||||
import {useIntl} from 'gatsby-plugin-intl';
|
import {useIntl} from 'gatsby-plugin-intl';
|
||||||
import {Search} from '@trussworks/react-uswds';
|
import {Search} from '@trussworks/react-uswds';
|
||||||
import {useWindowSize} from 'react-use';
|
import {useWindowSize} from 'react-use';
|
||||||
|
import * as JsSearch from 'js-search';
|
||||||
|
import * as constants from '../../data/constants';
|
||||||
|
|
||||||
import MapSearchMessage from '../MapSearchMessage';
|
import MapSearchMessage from '../MapSearchMessage';
|
||||||
|
|
||||||
|
@ -14,6 +16,16 @@ interface IMapSearch {
|
||||||
goToPlace(bounds: LngLatBoundsLike):void;
|
goToPlace(bounds: LngLatBoundsLike):void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ISearchResult {
|
||||||
|
addresstype: string;
|
||||||
|
lat: string;
|
||||||
|
lon: string;
|
||||||
|
boundingbox: string[];
|
||||||
|
type: string;
|
||||||
|
// eslint-disable-next-line camelcase
|
||||||
|
place_rank: number;
|
||||||
|
}
|
||||||
|
|
||||||
const MapSearch = ({goToPlace}:IMapSearch) => {
|
const MapSearch = ({goToPlace}:IMapSearch) => {
|
||||||
// State to hold if the search results are empty or not:
|
// State to hold if the search results are empty or not:
|
||||||
const [isSearchResultsNull, setIsSearchResultsNull] = useState(false);
|
const [isSearchResultsNull, setIsSearchResultsNull] = useState(false);
|
||||||
|
@ -30,24 +42,99 @@ const MapSearch = ({goToPlace}:IMapSearch) => {
|
||||||
*/
|
*/
|
||||||
const {width, height} = useWindowSize();
|
const {width, height} = useWindowSize();
|
||||||
const [placeholderText, setPlaceholderText]= useState(EXPLORE_COPY.MAP.SEARCH_PLACEHOLDER);
|
const [placeholderText, setPlaceholderText]= useState(EXPLORE_COPY.MAP.SEARCH_PLACEHOLDER);
|
||||||
|
const [tractSearch, setTractSearch] = useState<JsSearch | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the tract search data and loads in the state.
|
||||||
|
*/
|
||||||
|
const getTractSearchData = async () => {
|
||||||
|
const searchDataUrl = `${constants.TILE_BASE_URL}/${constants.MAP_TRACT_SEARCH_PATH}`;
|
||||||
|
fetch(searchDataUrl)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
return response.json();
|
||||||
|
} else {
|
||||||
|
throw new Error(`${response.statusText} error with status code of ${response.status}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
// We use JsSearch to make it easy to load and quick to search.
|
||||||
|
const search = new JsSearch.Search('GEOID10');
|
||||||
|
search.indexStrategy = new JsSearch.ExactWordIndexStrategy();
|
||||||
|
search.addIndex('GEOID10');
|
||||||
|
search.addDocuments(data);
|
||||||
|
setTractSearch(search);
|
||||||
|
})
|
||||||
|
.catch((error) =>
|
||||||
|
console.error('Unable to read search tract table:', error));
|
||||||
|
};
|
||||||
|
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
width > height ? setPlaceholderText(EXPLORE_COPY.MAP.SEARCH_PLACEHOLDER): setPlaceholderText(EXPLORE_COPY.MAP.SEARCH_PLACEHOLDER_MOBILE);
|
width > height ? setPlaceholderText(EXPLORE_COPY.MAP.SEARCH_PLACEHOLDER): setPlaceholderText(EXPLORE_COPY.MAP.SEARCH_PLACEHOLDER_MOBILE);
|
||||||
}, [width]);
|
}, [width]);
|
||||||
|
|
||||||
|
useEffect(()=>{
|
||||||
|
getTractSearchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searchs for a given Census tract ID.
|
||||||
|
* @param {string} tract the 11 digit tract ID as a string
|
||||||
|
* @return {Array} an array of one search result, or null if no result found
|
||||||
|
*/
|
||||||
|
const searchForTract = (tract: string): [ISearchResult] | [] => {
|
||||||
|
// We create a bounding box just to get the tract in the view box.
|
||||||
|
// The size is not important.
|
||||||
|
const BOUNDING_BOX_SIZE_DD = 0.2;
|
||||||
|
if (tractSearch) {
|
||||||
|
// Convert 10 digit tracts to 11.
|
||||||
|
const searchTerm = tract.length == 10 ? '0' + tract : tract;
|
||||||
|
const result = tractSearch.search(searchTerm);
|
||||||
|
if (result.length > 0) {
|
||||||
|
const lat = Number(result[0].INTPTLAT10);
|
||||||
|
const lon = Number(result[0].INTPTLON10);
|
||||||
|
return [{
|
||||||
|
addresstype: 'tract',
|
||||||
|
boundingbox: [
|
||||||
|
(lat - (BOUNDING_BOX_SIZE_DD / 2)).toString(),
|
||||||
|
(lat + (BOUNDING_BOX_SIZE_DD / 2)).toString(),
|
||||||
|
(lon - (BOUNDING_BOX_SIZE_DD / 2)).toString(),
|
||||||
|
(lon + (BOUNDING_BOX_SIZE_DD / 2)).toString(),
|
||||||
|
],
|
||||||
|
lat: result[0].INTPTLAT10,
|
||||||
|
lon: result[0].INTPTLON10,
|
||||||
|
type: 'tract',
|
||||||
|
place_rank: 1,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
onSearchHandler will
|
onSearchHandler will
|
||||||
1. extract the search term from the input field
|
1. extract the search term from the input field
|
||||||
2. fetch data from the API and return the results as JSON and results to US only
|
2. Determine if the search term is a Census Tract or not.
|
||||||
3. if data is valid, destructure the boundingBox values from the search results
|
3. If it is a Census Tract, it will search the tract table for a bounding box.
|
||||||
4. pan the map to that location
|
4. If it is NOT a Census Tract, it will fetch data from the API and return the
|
||||||
|
results as JSON and results to US only. If data is valid, destructure the
|
||||||
|
boundingBox values from the search results.
|
||||||
|
4. Pan the map to that location
|
||||||
*/
|
*/
|
||||||
const onSearchHandler = async (event: React.FormEvent<HTMLFormElement>) => {
|
const onSearchHandler = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
const searchTerm = (event.currentTarget.elements.namedItem('search') as HTMLInputElement).value;
|
const searchTerm = (event.currentTarget.elements.namedItem('search') as HTMLInputElement).value;
|
||||||
|
let searchResults = null;
|
||||||
|
|
||||||
const searchResults = await fetch(
|
// If the search term a Census tract
|
||||||
|
const isTract = /^\d{10,11}$/.test(searchTerm);
|
||||||
|
if (isTract) {
|
||||||
|
setIsSearchResultsNull(false);
|
||||||
|
searchResults = searchForTract(searchTerm);
|
||||||
|
} else {
|
||||||
|
searchResults = await fetch(
|
||||||
`https://nominatim.openstreetmap.org/search?q=${searchTerm}&format=json&countrycodes=us`,
|
`https://nominatim.openstreetmap.org/search?q=${searchTerm}&format=json&countrycodes=us`,
|
||||||
{
|
{
|
||||||
mode: 'cors',
|
mode: 'cors',
|
||||||
|
@ -61,13 +148,12 @@ const MapSearch = ({goToPlace}:IMapSearch) => {
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('There has been a problem with your fetch operation:', error);
|
console.error('There has been a problem with your fetch operation:', error);
|
||||||
});
|
});
|
||||||
|
console.log('Nominatum search results: ', searchResults);
|
||||||
|
}
|
||||||
|
|
||||||
// If results are valid, set isSearchResultsNull to false and pan map to location:
|
// If results are valid, set isSearchResultsNull to false and pan map to location:
|
||||||
if (searchResults && searchResults.length > 0) {
|
if (searchResults && searchResults.length > 0) {
|
||||||
setIsSearchResultsNull(false);
|
setIsSearchResultsNull(false);
|
||||||
console.log('Nominatum search results: ', searchResults);
|
|
||||||
|
|
||||||
const [latMin, latMax, longMin, longMax] = searchResults[0].boundingbox;
|
const [latMin, latMax, longMin, longMax] = searchResults[0].boundingbox;
|
||||||
goToPlace([[Number(longMin), Number(latMin)], [Number(longMax), Number(latMax)]]);
|
goToPlace([[Number(longMin), Number(latMin)], [Number(longMax), Number(latMax)]]);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -23,7 +23,7 @@ exports[`rendering of the MapSearch checks if component renders 1`] = `
|
||||||
data-testid="textInput"
|
data-testid="textInput"
|
||||||
id="search-field"
|
id="search-field"
|
||||||
name="search"
|
name="search"
|
||||||
placeholder="Search for an address, city, state or ZIP"
|
placeholder="Search for an address, city, state, ZIP or Census Tract"
|
||||||
type="search"
|
type="search"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -38,13 +38,8 @@ export const featureURLForTilesetName = (tilesetName: string): string => {
|
||||||
} else {
|
} else {
|
||||||
// The feature tile base URL and path can either point locally or the CDN.
|
// The feature tile base URL and path can either point locally or the CDN.
|
||||||
// This is selected based on the DATA_SOURCE env variable.
|
// This is selected based on the DATA_SOURCE env variable.
|
||||||
const featureTileBaseURL = process.env.DATA_SOURCE === 'local' ?
|
const featureTileBaseURL = constants.TILE_BASE_URL;
|
||||||
process.env.GATSBY_LOCAL_TILES_BASE_URL :
|
const featureTilePath = constants.TILE_PATH;
|
||||||
process.env.GATSBY_CDN_TILES_BASE_URL;
|
|
||||||
|
|
||||||
const featureTilePath = process.env.DATA_SOURCE === 'local' ?
|
|
||||||
process.env.GATSBY_DATA_PIPELINE_SCORE_PATH_LOCAL :
|
|
||||||
process.env.GATSBY_2_0_SCORE_PATH;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
featureTileBaseURL,
|
featureTileBaseURL,
|
||||||
|
|
|
@ -380,3 +380,13 @@ export const CENSUS_TRACT_SURVEY_LINKS = {
|
||||||
EN: "https://eop.gov1.qualtrics.com/jfe/form/SV_8J5wGa8Ya4dMP9c",
|
EN: "https://eop.gov1.qualtrics.com/jfe/form/SV_8J5wGa8Ya4dMP9c",
|
||||||
ES: "https://eop.gov1.qualtrics.com/jfe/form/SV_eJXos5X4yekq6cC",
|
ES: "https://eop.gov1.qualtrics.com/jfe/form/SV_eJXos5X4yekq6cC",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const TILE_BASE_URL = process.env.DATA_SOURCE === "local" ?
|
||||||
|
process.env.GATSBY_LOCAL_TILES_BASE_URL :
|
||||||
|
process.env.GATSBY_CDN_TILES_BASE_URL;
|
||||||
|
|
||||||
|
export const TILE_PATH = process.env.DATA_SOURCE === "local" ?
|
||||||
|
process.env.GATSBY_DATA_PIPELINE_SCORE_PATH_LOCAL :
|
||||||
|
process.env.GATSBY_1_0_SCORE_PATH;
|
||||||
|
|
||||||
|
export const MAP_TRACT_SEARCH_PATH = "data_pipeline/data/score/search/tracts.json";
|
||||||
|
|
|
@ -92,7 +92,7 @@ export const MAP = defineMessages({
|
||||||
},
|
},
|
||||||
SEARCH_PLACEHOLDER: {
|
SEARCH_PLACEHOLDER: {
|
||||||
id: 'explore.map.page.map.search.placeholder.text',
|
id: 'explore.map.page.map.search.placeholder.text',
|
||||||
defaultMessage: 'Search for an address, city, state or ZIP',
|
defaultMessage: 'Search for an address, city, state, ZIP or Census Tract',
|
||||||
description: 'On the explore the map page, on the map, the placeholder text for search',
|
description: 'On the explore the map page, on the map, the placeholder text for search',
|
||||||
},
|
},
|
||||||
SEARCH_PLACEHOLDER_MOBILE: {
|
SEARCH_PLACEHOLDER_MOBILE: {
|
||||||
|
|
|
@ -688,7 +688,7 @@
|
||||||
"description": "On the explore the map page, on the map, the placeholder text for search"
|
"description": "On the explore the map page, on the map, the placeholder text for search"
|
||||||
},
|
},
|
||||||
"explore.map.page.map.search.placeholder.text": {
|
"explore.map.page.map.search.placeholder.text": {
|
||||||
"defaultMessage": "Search for an address, city, state or ZIP",
|
"defaultMessage": "Search for an address, city, state, ZIP or Census Tract",
|
||||||
"description": "On the explore the map page, on the map, the placeholder text for search"
|
"description": "On the explore the map page, on the map, the placeholder text for search"
|
||||||
},
|
},
|
||||||
"explore.map.page.map.search.results.empty.text": {
|
"explore.map.page.map.search.results.empty.text": {
|
||||||
|
|
|
@ -172,7 +172,7 @@
|
||||||
"explore.map.page.map.layer.selector.tribal.long": "Tierras tribales",
|
"explore.map.page.map.layer.selector.tribal.long": "Tierras tribales",
|
||||||
"explore.map.page.map.layer.selector.tribal.short": "Tribal",
|
"explore.map.page.map.layer.selector.tribal.short": "Tribal",
|
||||||
"explore.map.page.map.search.placeholder.mobile.text": "Búsqueda de ubicaciones ",
|
"explore.map.page.map.search.placeholder.mobile.text": "Búsqueda de ubicaciones ",
|
||||||
"explore.map.page.map.search.placeholder.text": "Busque una dirección, ciudad, estado o código postal.",
|
"explore.map.page.map.search.placeholder.text": "Busque una dirección, ciudad, estado, código postal o distrito censal.",
|
||||||
"explore.map.page.map.search.results.empty.text": "No se encontró la ubicación o ubicación desconocida. Intente una búsqueda distinta.",
|
"explore.map.page.map.search.results.empty.text": "No se encontró la ubicación o ubicación desconocida. Intente una búsqueda distinta.",
|
||||||
"explore.map.page.map.territoryFocus.alaska.long": "Alaska",
|
"explore.map.page.map.territoryFocus.alaska.long": "Alaska",
|
||||||
"explore.map.page.map.territoryFocus.alaska.short": "AK",
|
"explore.map.page.map.territoryFocus.alaska.short": "AK",
|
||||||
|
|
|
@ -24,6 +24,7 @@ DATA_CENSUS_DIR = DATA_PATH / "census"
|
||||||
DATA_CENSUS_CSV_DIR = DATA_CENSUS_DIR / "csv"
|
DATA_CENSUS_CSV_DIR = DATA_CENSUS_DIR / "csv"
|
||||||
DATA_CENSUS_CSV_FILE_PATH = DATA_CENSUS_CSV_DIR / "us.csv"
|
DATA_CENSUS_CSV_FILE_PATH = DATA_CENSUS_CSV_DIR / "us.csv"
|
||||||
DATA_CENSUS_CSV_STATE_FILE_PATH = DATA_CENSUS_CSV_DIR / "fips_states_2010.csv"
|
DATA_CENSUS_CSV_STATE_FILE_PATH = DATA_CENSUS_CSV_DIR / "fips_states_2010.csv"
|
||||||
|
DATA_CENSUS_GEOJSON_FILE_PATH = DATA_CENSUS_DIR / "geojson" / "us.json"
|
||||||
|
|
||||||
# Score paths
|
# Score paths
|
||||||
DATA_SCORE_DIR = DATA_PATH / "score"
|
DATA_SCORE_DIR = DATA_PATH / "score"
|
||||||
|
@ -46,6 +47,9 @@ DATA_SCORE_JSON_INDEX_FILE_PATH = (
|
||||||
## Tile path
|
## Tile path
|
||||||
DATA_SCORE_TILES_DIR = DATA_SCORE_DIR / "tiles"
|
DATA_SCORE_TILES_DIR = DATA_SCORE_DIR / "tiles"
|
||||||
|
|
||||||
|
## Tiles search
|
||||||
|
DATA_TILES_SEARCH_DIR = DATA_SCORE_DIR / "search"
|
||||||
|
|
||||||
# Downloadable paths
|
# Downloadable paths
|
||||||
if not os.environ.get("J40_VERSION_LABEL_STRING"):
|
if not os.environ.get("J40_VERSION_LABEL_STRING"):
|
||||||
version_str = "beta"
|
version_str = "beta"
|
||||||
|
@ -82,6 +86,7 @@ SCORE_VERSIONING_README_FILE_NAME = f"readme-version-{version_str}.md"
|
||||||
SCORE_VERSIONING_README_FILE_PATH = (
|
SCORE_VERSIONING_README_FILE_PATH = (
|
||||||
FILES_PATH / SCORE_VERSIONING_README_FILE_NAME
|
FILES_PATH / SCORE_VERSIONING_README_FILE_NAME
|
||||||
)
|
)
|
||||||
|
SCORE_TRACT_SEARCH_FILE_PATH = DATA_TILES_SEARCH_DIR / "tracts.json"
|
||||||
|
|
||||||
# For the codebook
|
# For the codebook
|
||||||
CEJST_SCORE_COLUMN_NAME = "score_name"
|
CEJST_SCORE_COLUMN_NAME = "score_name"
|
||||||
|
|
|
@ -4,6 +4,7 @@ from pathlib import Path
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from numpy import float64
|
from numpy import float64
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import geopandas as gpd
|
||||||
|
|
||||||
from data_pipeline.content.schemas.download_schemas import CodebookConfig
|
from data_pipeline.content.schemas.download_schemas import CodebookConfig
|
||||||
from data_pipeline.content.schemas.download_schemas import CSVConfig
|
from data_pipeline.content.schemas.download_schemas import CSVConfig
|
||||||
|
@ -42,10 +43,12 @@ class PostScoreETL(ExtractTransformLoad):
|
||||||
self.input_counties_df: pd.DataFrame
|
self.input_counties_df: pd.DataFrame
|
||||||
self.input_states_df: pd.DataFrame
|
self.input_states_df: pd.DataFrame
|
||||||
self.input_score_df: pd.DataFrame
|
self.input_score_df: pd.DataFrame
|
||||||
|
self.input_census_geo_df: gpd.GeoDataFrame
|
||||||
|
|
||||||
self.output_score_county_state_merged_df: pd.DataFrame
|
self.output_score_county_state_merged_df: pd.DataFrame
|
||||||
self.output_score_tiles_df: pd.DataFrame
|
self.output_score_tiles_df: pd.DataFrame
|
||||||
self.output_downloadable_df: pd.DataFrame
|
self.output_downloadable_df: pd.DataFrame
|
||||||
|
self.output_tract_search_df: pd.DataFrame
|
||||||
|
|
||||||
# Define some constants for the YAML file
|
# Define some constants for the YAML file
|
||||||
# TODO: Implement this as a marshmallow schema.
|
# TODO: Implement this as a marshmallow schema.
|
||||||
|
@ -105,6 +108,18 @@ class PostScoreETL(ExtractTransformLoad):
|
||||||
|
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
def _extract_census_geojson(self, geo_path: Path) -> gpd.GeoDataFrame:
|
||||||
|
"""
|
||||||
|
Read in the Census Geo JSON data.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
gpd.GeoDataFrame: the census geo json data
|
||||||
|
"""
|
||||||
|
logger.debug("Reading Census GeoJSON")
|
||||||
|
with open(geo_path, "r", encoding="utf-8") as file:
|
||||||
|
data = gpd.read_file(file)
|
||||||
|
return data
|
||||||
|
|
||||||
def extract(self, use_cached_data_sources: bool = False) -> None:
|
def extract(self, use_cached_data_sources: bool = False) -> None:
|
||||||
|
|
||||||
super().extract(
|
super().extract(
|
||||||
|
@ -131,6 +146,9 @@ class PostScoreETL(ExtractTransformLoad):
|
||||||
self.input_score_df = self._extract_score(
|
self.input_score_df = self._extract_score(
|
||||||
constants.DATA_SCORE_CSV_FULL_FILE_PATH
|
constants.DATA_SCORE_CSV_FULL_FILE_PATH
|
||||||
)
|
)
|
||||||
|
self.input_census_geo_df = self._extract_census_geojson(
|
||||||
|
constants.DATA_CENSUS_GEOJSON_FILE_PATH
|
||||||
|
)
|
||||||
|
|
||||||
def _transform_counties(
|
def _transform_counties(
|
||||||
self, initial_counties_df: pd.DataFrame
|
self, initial_counties_df: pd.DataFrame
|
||||||
|
@ -392,7 +410,23 @@ class PostScoreETL(ExtractTransformLoad):
|
||||||
|
|
||||||
return final_df
|
return final_df
|
||||||
|
|
||||||
|
def _create_tract_search_data(
|
||||||
|
self, census_geojson: gpd.GeoDataFrame
|
||||||
|
) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Generate a dataframe with only the tract IDs and the center lat/lon of each tract.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pd.DataFrame: a dataframe with the tract search data
|
||||||
|
"""
|
||||||
|
logger.debug("Creating Census tract search data")
|
||||||
|
columns_to_extract = ["GEOID10", "INTPTLAT10", "INTPTLON10"]
|
||||||
|
return pd.DataFrame(census_geojson[columns_to_extract])
|
||||||
|
|
||||||
def transform(self) -> None:
|
def transform(self) -> None:
|
||||||
|
self.output_tract_search_df = self._create_tract_search_data(
|
||||||
|
self.input_census_geo_df
|
||||||
|
)
|
||||||
transformed_counties = self._transform_counties(self.input_counties_df)
|
transformed_counties = self._transform_counties(self.input_counties_df)
|
||||||
transformed_states = self._transform_states(self.input_states_df)
|
transformed_states = self._transform_states(self.input_states_df)
|
||||||
transformed_score = self._transform_score(self.input_score_df)
|
transformed_score = self._transform_score(self.input_score_df)
|
||||||
|
@ -409,6 +443,9 @@ class PostScoreETL(ExtractTransformLoad):
|
||||||
self.output_score_county_state_merged_df = (
|
self.output_score_county_state_merged_df = (
|
||||||
output_score_county_state_merged_df
|
output_score_county_state_merged_df
|
||||||
)
|
)
|
||||||
|
self.output_tract_search_df = self._create_tract_search_data(
|
||||||
|
self.input_census_geo_df
|
||||||
|
)
|
||||||
|
|
||||||
def _load_score_csv_full(
|
def _load_score_csv_full(
|
||||||
self, score_county_state_merged: pd.DataFrame, score_csv_path: Path
|
self, score_county_state_merged: pd.DataFrame, score_csv_path: Path
|
||||||
|
@ -592,6 +629,13 @@ class PostScoreETL(ExtractTransformLoad):
|
||||||
]
|
]
|
||||||
zip_files(version_data_documentation_zip_path, files_to_compress)
|
zip_files(version_data_documentation_zip_path, files_to_compress)
|
||||||
|
|
||||||
|
def _load_search_tract_data(self, output_path: Path):
|
||||||
|
"""Write the Census tract search data."""
|
||||||
|
logger.debug("Writing Census tract search data")
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
# We use the records orientation to easily import the JSON in JS.
|
||||||
|
self.output_tract_search_df.to_json(output_path, orient="records")
|
||||||
|
|
||||||
def load(self) -> None:
|
def load(self) -> None:
|
||||||
self._load_score_csv_full(
|
self._load_score_csv_full(
|
||||||
self.output_score_county_state_merged_df,
|
self.output_score_county_state_merged_df,
|
||||||
|
@ -600,4 +644,5 @@ class PostScoreETL(ExtractTransformLoad):
|
||||||
self._load_tile_csv(
|
self._load_tile_csv(
|
||||||
self.output_score_tiles_df, constants.DATA_SCORE_CSV_TILES_FILE_PATH
|
self.output_score_tiles_df, constants.DATA_SCORE_CSV_TILES_FILE_PATH
|
||||||
)
|
)
|
||||||
|
self._load_search_tract_data(constants.SCORE_TRACT_SEARCH_FILE_PATH)
|
||||||
self._load_downloadable_zip(constants.SCORE_DOWNLOADABLE_DIR)
|
self._load_downloadable_zip(constants.SCORE_DOWNLOADABLE_DIR)
|
||||||
|
|
|
@ -3,6 +3,7 @@ from importlib import reload
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
import geopandas as gpd
|
||||||
import pytest
|
import pytest
|
||||||
from data_pipeline import config
|
from data_pipeline import config
|
||||||
from data_pipeline.etl.score import etl_score_post
|
from data_pipeline.etl.score import etl_score_post
|
||||||
|
@ -144,3 +145,13 @@ def downloadable_data_expected():
|
||||||
return pd.read_pickle(
|
return pd.read_pickle(
|
||||||
pytest.SNAPSHOT_DIR / "downloadable_data_expected.pkl"
|
pytest.SNAPSHOT_DIR / "downloadable_data_expected.pkl"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def census_geojson_sample_data(sample_data_dir) -> gpd.GeoDataFrame:
|
||||||
|
with open(
|
||||||
|
sample_data_dir / "census_60.geojson", "r", encoding="utf-8"
|
||||||
|
) as file:
|
||||||
|
data = gpd.read_file(file)
|
||||||
|
return data
|
||||||
|
return None
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -5,9 +5,12 @@ from pathlib import Path
|
||||||
|
|
||||||
import pandas.api.types as ptypes
|
import pandas.api.types as ptypes
|
||||||
import pandas.testing as pdt
|
import pandas.testing as pdt
|
||||||
|
import pandas as pd
|
||||||
|
import geopandas as gpd
|
||||||
from data_pipeline.content.schemas.download_schemas import CSVConfig
|
from data_pipeline.content.schemas.download_schemas import CSVConfig
|
||||||
from data_pipeline.etl.score import constants
|
from data_pipeline.etl.score import constants
|
||||||
from data_pipeline.utils import load_yaml_dict_from_file
|
from data_pipeline.utils import load_yaml_dict_from_file
|
||||||
|
from data_pipeline.etl.score.etl_score_post import PostScoreETL
|
||||||
|
|
||||||
# See conftest.py for all fixtures used in these tests
|
# See conftest.py for all fixtures used in these tests
|
||||||
|
|
||||||
|
@ -150,3 +153,16 @@ def test_load_downloadable_zip(etl, monkeypatch, score_data_expected):
|
||||||
assert constants.SCORE_DOWNLOADABLE_EXCEL_FILE_PATH.is_file()
|
assert constants.SCORE_DOWNLOADABLE_EXCEL_FILE_PATH.is_file()
|
||||||
assert constants.SCORE_DOWNLOADABLE_CSV_ZIP_FILE_PATH.is_file()
|
assert constants.SCORE_DOWNLOADABLE_CSV_ZIP_FILE_PATH.is_file()
|
||||||
assert constants.SCORE_DOWNLOADABLE_XLS_ZIP_FILE_PATH.is_file()
|
assert constants.SCORE_DOWNLOADABLE_XLS_ZIP_FILE_PATH.is_file()
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_tract_search_data(census_geojson_sample_data: gpd.GeoDataFrame):
|
||||||
|
# Sanity check
|
||||||
|
assert len(census_geojson_sample_data) > 0
|
||||||
|
|
||||||
|
result = PostScoreETL()._create_tract_search_data(census_geojson_sample_data)
|
||||||
|
assert isinstance(result, pd.DataFrame)
|
||||||
|
assert not result.columns.empty
|
||||||
|
columns = ["GEOID10", "INTPTLAT10", "INTPTLON10"]
|
||||||
|
for col in columns:
|
||||||
|
assert col in result.columns
|
||||||
|
assert len(census_geojson_sample_data) == len(result)
|
||||||
|
|
Loading…
Add table
Reference in a new issue