diff --git a/client/cypress/e2e/map.spec.js b/client/cypress/e2e/map.spec.js index 9b7cd22a..ee0dface 100644 --- a/client/cypress/e2e/map.spec.js +++ b/client/cypress/e2e/map.spec.js @@ -39,7 +39,7 @@ describe('Tests for the Explore the Map page', () => { return map.getFeatureState( { 'id': id, - 'source': constants.SCORE_SOURCE_NAME, + 'source': constants.HIGH_SCORE_SOURCE_NAME, 'sourceLayer': constants.SCORE_SOURCE_LAYER, }, ); diff --git a/client/src/components/J40Map.tsx b/client/src/components/J40Map.tsx index 5f6b9f71..691cf97c 100644 --- a/client/src/components/J40Map.tsx +++ b/client/src/components/J40Map.tsx @@ -8,7 +8,6 @@ import maplibregl, {LngLatBoundsLike, LngLatLike, MapboxGeoJSONFeature} from 'maplibre-gl'; import mapStyle from '../data/mapStyle'; -import ZoomWarning from './zoomWarning'; import PopupContent from './popupContent'; import * as constants from '../data/constants'; import ReactDOM from 'react-dom'; @@ -76,7 +75,7 @@ const J40Map = () => { const map = e.target; const clickedCoord = e.point; const features = map.queryRenderedFeatures(clickedCoord, { - layers: ['score'], + layers: [constants.HIGH_SCORE_LAYER_NAME], }); const feature = features && features[0]; if (feature) { @@ -107,10 +106,10 @@ const J40Map = () => { mapRef.current.on('move', () => { setZoom(mapRef.current.getZoom()); }); - mapRef.current.on('mouseenter', 'score', () => { + mapRef.current.on('mouseenter', constants.HIGH_SCORE_LAYER_NAME, () => { mapRef.current.getCanvas().style.cursor = 'pointer'; }); - mapRef.current.on('mouseleave', 'score', () => { + mapRef.current.on('mouseleave', constants.HIGH_SCORE_LAYER_NAME, () => { mapRef.current.getCanvas().style.cursor = ''; }); }, [mapRef]); @@ -177,7 +176,6 @@ const J40Map = () => {
-
); }; diff --git a/client/src/data/constants.tsx b/client/src/data/constants.tsx index 7bb0ae1a..5da6fd4b 100644 --- a/client/src/data/constants.tsx +++ b/client/src/data/constants.tsx @@ -1,13 +1,22 @@ // URLS -export const FEATURE_TILE_BASE_URL = 'https://d2zjid6n5ja2pt.cloudfront.net/0629_demo'; +export const FEATURE_TILE_BASE_URL = 'https://d2zjid6n5ja2pt.cloudfront.net'; +const XYZ_SUFFIX = '{z}/{x}/{y}.pbf'; +export const FEATURE_TILE_HIGH_ZOOM_URL = `${FEATURE_TILE_BASE_URL}/0629_demo/${XYZ_SUFFIX}`; +export const FEATURE_TILE_LOW_ZOOM_URL = `${FEATURE_TILE_BASE_URL}/tiles_low/${XYZ_SUFFIX}`; + // Performance markers export const PERFORMANCE_MARKER_MAP_IDLE = 'MAP_IDLE'; // Properties -export const SCORE_PROPERTY = 'Score D (percentile)'; +export const SCORE_PROPERTY_HIGH = 'Score D (percentile)'; +export const SCORE_PROPERTY_LOW = 'D_SCORE'; export const GEOID_PROPERTY = 'GEOID10'; -export const SCORE_SOURCE_NAME = 'score'; +export const HIGH_SCORE_SOURCE_NAME = 'score-high'; +export const HIGH_SCORE_LAYER_NAME = 'score-high-layer'; +export const LOW_SCORE_SOURCE_NAME = 'score-low'; +export const LOW_SCORE_LAYER_NAME = 'score-low-layer'; + // The name of the layer within the tiles that contains the score export const SCORE_SOURCE_LAYER = 'blocks'; @@ -18,8 +27,10 @@ export type J40Properties = { [key: string]: any }; export const GLOBAL_MIN_ZOOM = 3; export const GLOBAL_MAX_ZOOM = 22; export const GLOBAL_MIN_ZOOM_LOW = 3; -export const GLOBAL_MAX_ZOOM_LOW = 9; -export const GLOBAL_MIN_ZOOM_HIGH = 9; +export const GLOBAL_MAX_ZOOM_LOW = 7; +export const GLOBAL_MIN_ZOOM_HIGHLIGHT = 9; +export const GLOBAL_MAX_ZOOM_HIGHLIGHT = 22; +export const GLOBAL_MIN_ZOOM_HIGH = 7; export const GLOBAL_MAX_ZOOM_HIGH = 11; // Bounds @@ -74,3 +85,13 @@ export const MIN_COLOR = '#FFFFFF'; export const MED_COLOR = '#D1DAE6'; export const MAX_COLOR = '#768FB3'; export const BORDER_HIGHLIGHT_COLOR = '#00BDE3'; + +// Widths +export const HIGHLIGHT_BORDER_WIDTH = 5.0; + +// Score boundaries +export const SCORE_BOUNDARY_LOW = 0.0; +export const SCORE_BOUNDARY_THRESHOLD = 0.6; +export const SCORE_BOUNDARY_PRIORITIZED = 0.75; + +export const isMobile = typeof window !== 'undefined' && (window.innerWidth < 400); diff --git a/client/src/data/mapStyle.tsx b/client/src/data/mapStyle.tsx index 5a81c06f..1b872c0d 100644 --- a/client/src/data/mapStyle.tsx +++ b/client/src/data/mapStyle.tsx @@ -43,47 +43,70 @@ function makePaint({ return paintDescriptor; } +const imageSuffix = constants.isMobile ? '' : '@2x'; + const mapStyle : Style = { 'version': 8, 'sources': { 'carto': { 'type': 'raster', - 'tiles': [ - 'https://a.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', - 'https://b.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', - 'https://c.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', - 'https://d.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', + 'tiles': + [ + `https://a.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}${imageSuffix}.png`, + `https://b.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}${imageSuffix}.png`, + `https://c.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}${imageSuffix}.png`, + `https://d.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}${imageSuffix}.png`, ], + 'minzoom': constants.GLOBAL_MIN_ZOOM, + 'maxzoom': constants.GLOBAL_MAX_ZOOM, }, 'geo': { 'type': 'raster', 'tiles': [ 'https://mt0.google.com/vt/lyrs=p&hl=en&x={x}&y={y}&z={z}', ], + 'minzoom': constants.GLOBAL_MIN_ZOOM, + 'maxzoom': constants.GLOBAL_MAX_ZOOM, }, - 'score': { + 'score-high': { + // "Score-high" represents the full set of data + // at the census block group level. It is only shown + // at high zoom levels to avoid performance issues at lower zooms 'type': 'vector', // Our current tippecanoe command does not set an id. // The below line promotes the GEOID10 property to the ID - 'promoteId': 'GEOID10', + 'promoteId': constants.GEOID_PROPERTY, 'tiles': [ - `${constants.FEATURE_TILE_BASE_URL}/{z}/{x}/{y}.pbf`, - // For local development, use: - // 'http://localhost:8080/data/tl_2010_bg_with_data/{z}/{x}/{y}.pbf', + constants.FEATURE_TILE_HIGH_ZOOM_URL, ], - // Seeting maxzoom here enables 'overzooming' // e.g. continued zooming beyond the max bounds. // More here: https://docs.mapbox.com/help/glossary/overzoom/ + 'minzoom': constants.GLOBAL_MIN_ZOOM_HIGH, 'maxzoom': constants.GLOBAL_MAX_ZOOM_HIGH, }, + 'score-low': { + // "Score-low" represents a tileset at the level of bucketed tracts. + // census block group information is `dissolve`d into tracts, then + // each tract is `dissolve`d into one of ten buckets. It is meant + // to give us a favorable tradeoff between performance and fidelity. + 'type': 'vector', + 'promoteId': constants.GEOID_PROPERTY, + 'tiles': [ + constants.FEATURE_TILE_LOW_ZOOM_URL, + // For local development, use: + // 'http://localhost:8080/data/tl_2010_bg_with_data/{z}/{x}/{y}.pbf', + ], + 'minzoom': constants.GLOBAL_MIN_ZOOM_LOW, + 'maxzoom': constants.GLOBAL_MAX_ZOOM_LOW, + }, 'labels': { 'type': 'raster', 'tiles': [ - 'https://cartodb-basemaps-a.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}@2x.png', - 'https://cartodb-basemaps-b.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}@2x.png', - 'https://cartodb-basemaps-c.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}@2x.png', - 'https://cartodb-basemaps-d.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}@2x.png', + `https://cartodb-basemaps-a.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}${imageSuffix}.png`, + `https://cartodb-basemaps-b.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}${imageSuffix}.png`, + `https://cartodb-basemaps-c.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}${imageSuffix}.png`, + `https://cartodb-basemaps-d.global.ssl.fastly.net/light_only_labels/{z}/{x}/{y}${imageSuffix}.png`, ], }, }, @@ -92,42 +115,60 @@ const mapStyle : Style = { 'id': 'carto', 'source': 'carto', 'type': 'raster', - 'minzoom': constants.GLOBAL_MIN_ZOOM - 1, + 'minzoom': constants.GLOBAL_MIN_ZOOM, + 'maxzoom': constants.GLOBAL_MAX_ZOOM, }, { 'id': 'geo', 'source': 'geo', 'type': 'raster', - 'minzoom': constants.GLOBAL_MIN_ZOOM - 1, 'layout': { - // Make the layer visible by default. + // Make the layer invisible by default. 'visibility': 'none', }, - }, - { - 'id': 'score', - 'source': constants.SCORE_SOURCE_NAME, - 'source-layer': constants.SCORE_SOURCE_LAYER, - 'type': 'fill', - 'filter': ['all', - ['>', constants.SCORE_PROPERTY, 0.6], - // ['in', 'STATEFP10', '01', '30', '34', '35', '36'], - ], - 'paint': makePaint({ - field: constants.SCORE_PROPERTY, - minRamp: 0, - medRamp: 0.6, - maxRamp: 0.75, - }), 'minzoom': constants.GLOBAL_MIN_ZOOM, 'maxzoom': constants.GLOBAL_MAX_ZOOM, }, { + 'id': constants.HIGH_SCORE_LAYER_NAME, + 'source': constants.HIGH_SCORE_SOURCE_NAME, + 'source-layer': constants.SCORE_SOURCE_LAYER, + 'type': 'fill', + 'filter': ['all', + ['>', constants.SCORE_PROPERTY_HIGH, constants.SCORE_BOUNDARY_THRESHOLD], + ], + 'paint': makePaint({ + field: constants.SCORE_PROPERTY_HIGH, + minRamp: constants.SCORE_BOUNDARY_LOW, + medRamp: constants.SCORE_BOUNDARY_THRESHOLD, + maxRamp: constants.SCORE_BOUNDARY_PRIORITIZED, + }), + 'minzoom': constants.GLOBAL_MIN_ZOOM_HIGH, + }, + { + 'id': constants.LOW_SCORE_LAYER_NAME, + 'source': constants.LOW_SCORE_SOURCE_NAME, + 'source-layer': constants.SCORE_SOURCE_LAYER, + 'type': 'fill', + 'filter': ['all', + ['>', constants.SCORE_PROPERTY_LOW, constants.SCORE_BOUNDARY_THRESHOLD], + ], + 'paint': makePaint({ + field: constants.SCORE_PROPERTY_LOW, + minRamp: constants.SCORE_BOUNDARY_LOW, + medRamp: constants.SCORE_BOUNDARY_THRESHOLD, + maxRamp: constants.SCORE_BOUNDARY_PRIORITIZED, + }), + 'minzoom': constants.GLOBAL_MIN_ZOOM_LOW, + 'maxzoom': constants.GLOBAL_MAX_ZOOM_LOW, + }, + { + // "Score-highlights" represents the border + // around given tiles that appears at higher zooms 'id': 'score-highlights', - 'source': 'score', + 'source': constants.HIGH_SCORE_SOURCE_NAME, 'source-layer': constants.SCORE_SOURCE_LAYER, 'type': 'line', - 'minzoom': constants.GLOBAL_MIN_ZOOM_HIGH, 'layout': { 'visibility': 'visible', 'line-join': 'round', @@ -138,13 +179,15 @@ const mapStyle : Style = { 'line-width': 0.8, 'line-opacity': 0.5, }, + 'minzoom': constants.GLOBAL_MIN_ZOOM_HIGHLIGHT, + 'maxzoom': constants.GLOBAL_MAX_ZOOM_HIGHLIGHT, }, { - // This layer queries the feature-state property "selected" and - // highlights the border of the selected region if true + // "score-border-highlight" is used to highlight + // the currently-selected feature 'id': 'score-border-highlight', 'type': 'line', - 'source': 'score', + 'source': constants.HIGH_SCORE_SOURCE_NAME, 'source-layer': constants.SCORE_SOURCE_LAYER, 'layout': {}, 'paint': { @@ -152,16 +195,20 @@ const mapStyle : Style = { 'line-width': [ 'case', ['boolean', ['feature-state', 'selected'], false], - 5.0, + constants.HIGHLIGHT_BORDER_WIDTH, 0, ], }, + 'minzoom': constants.GLOBAL_MIN_ZOOM_HIGH, + 'maxzoom': constants.GLOBAL_MAX_ZOOM_HIGH, }, { + // We put labels last to ensure prominence 'id': 'labels-only', 'type': 'raster', 'source': 'labels', 'minzoom': constants.GLOBAL_MIN_ZOOM, + 'maxzoom': constants.GLOBAL_MAX_ZOOM, }, ], };