diff --git a/static/css/shared/_tiles.scss b/static/css/shared/_tiles.scss index a2dfec2830..268520fcac 100644 --- a/static/css/shared/_tiles.scss +++ b/static/css/shared/_tiles.scss @@ -263,7 +263,6 @@ $border-radius: 0.5rem; .dot { stroke-width: 0.3px; - r: 1.2px !important; opacity: .7; } diff --git a/static/js/chart/draw_d3_map.ts b/static/js/chart/draw_d3_map.ts index c9b3f7047b..c5dcdbc17a 100644 --- a/static/js/chart/draw_d3_map.ts +++ b/static/js/chart/draw_d3_map.ts @@ -473,6 +473,7 @@ export function drawD3Map( * @param projection geo projection used by the map * @param getPointColor function to get the color for each map point * @param getTooltipHtml function to get the html content for the tooltip + * @param minDotRadius smallest radius to use for the map points */ export function addMapPoints( domContainerId: string, @@ -480,7 +481,8 @@ export function addMapPoints( mapPointValues: { [placeDcid: string]: number }, projection: d3.GeoProjection, getPointColor?: (point: MapPoint) => string, - getTooltipHtml?: (place: NamedPlace) => string + getTooltipHtml?: (place: NamedPlace) => string, + minDotRadius?: number ): d3.Selection { // get the smallest diagonal length of a region on the d3 map. let minRegionDiagonal = Number.MAX_VALUE; @@ -495,7 +497,7 @@ export function addMapPoints( Math.pow(pathClientRect.height, 2) + Math.pow(pathClientRect.width, 2) ); }); - const minDotSize = Math.max(minRegionDiagonal * 0.02, 1.1); + const minDotSize = minDotRadius || Math.max(minRegionDiagonal * 0.02, 1.1); const filteredMapPoints = mapPoints.filter((point) => { return !_.isNull(projection([point.longitude, point.latitude])); }); @@ -533,7 +535,7 @@ export function addMapPoints( ) .attr("r", (point: MapPoint) => { if (_.isEmpty(pointSizeScale) || !mapPointValues[point.placeDcid]) { - return minDotSize * 2; + return minDotSize; } return pointSizeScale(mapPointValues[point.placeDcid]); }); diff --git a/static/js/components/tiles/disaster_event_map_tile.tsx b/static/js/components/tiles/disaster_event_map_tile.tsx index f2fd0d9d0c..8a584ceaaf 100644 --- a/static/js/components/tiles/disaster_event_map_tile.tsx +++ b/static/js/components/tiles/disaster_event_map_tile.tsx @@ -43,6 +43,7 @@ import { isChildPlaceOf } from "../../tools/shared_util"; import { DisasterEventMapPlaceInfo, DisasterEventPoint, + MapPointsData, } from "../../types/disaster_event_map_types"; import { EventTypeSpec } from "../../types/subject_page_proto_types"; import { @@ -50,6 +51,7 @@ import { fetchDisasterEventPoints, fetchGeoJsonData, getDate, + getMapPointsData, getSeverityFilters, onPointClicked, } from "../../utils/disaster_event_map_utils"; @@ -57,7 +59,6 @@ import { getEnclosedPlacesPromise, getParentPlacesPromise, } from "../../utils/place_utils"; -import { getPlaceNames } from "../../utils/place_utils"; import { ReplacementStrings } from "../../utils/tile_utils"; import { ChartTileContainer } from "./chart_tile"; import { DisasterEventMapFilters } from "./disaster_event_map_filters"; @@ -70,6 +71,7 @@ const CSS_SELECTOR_PREFIX = "disaster-event-map"; const DATE_SUBSTRING_IDX = 10; // TODO: make this config driven const REDIRECT_URL_PREFIX = "/disasters/"; +const MAP_POINTS_MIN_RADIUS = 0.8; interface DisasterEventMapTilePropType { // Id for this tile @@ -86,8 +88,10 @@ interface DisasterEventMapTilePropType { interface MapChartData { geoJson: GeoJsonData; - disasterEventPoints: DisasterEventPoint[]; sources: Set; + // key is disaster type and value is the map points data for that disaster + // type + mapPointsData: Record; } export function DisasterEventMapTile( @@ -338,10 +342,14 @@ export function DisasterEventMapTile( Object.values(disasterEventData.provenanceInfo).forEach((provInfo) => { sources.add(provInfo.provenanceUrl); }); + const mapPointsData = getMapPointsData( + disasterEventData.eventPoints, + props.eventTypeSpec + ); setMapChartData({ geoJson, - disasterEventPoints: disasterEventData.eventPoints, sources, + mapPointsData, }); }) .catch(() => { @@ -380,8 +388,7 @@ export function DisasterEventMapTile( null /* colorScale: no color scale since no data shown on the base map */, (geoDcid: GeoJsonFeatureProperties) => redirectAction(geoDcid.geoDcid) /* redirectAction */, - () => - "" /* getTooltipHtml: no tooltips to be shown on hover over a map region */, + (place: NamedPlace) => place.name || place.dcid /* getTooltipHtml */, () => true /* canClickRegion: allow all regions to be clickable */, false /* shouldGenerateLegend: no legend needs to be generated since no data for base map */, true /* shouldShowBoundaryLines */, @@ -390,24 +397,27 @@ export function DisasterEventMapTile( "" /* zoomDcid: no dcid to zoom in on */, zoomParams ); - const pointValues = {}; - const pointsLayer = addMapPoints( - props.id, - mapChartData.disasterEventPoints, - pointValues, - projection, - (point: DisasterEventPoint) => { - return props.eventTypeSpec[point.disasterType].color; - } - ); - pointsLayer.on("click", (point: DisasterEventPoint) => - onPointClicked( - infoCardRef.current, - svgContainerRef.current, - point, - d3.event - ) - ); + for (const mapPointsData of Object.values(mapChartData.mapPointsData)) { + const pointsLayer = addMapPoints( + props.id, + mapPointsData.points, + mapPointsData.values, + projection, + (point: DisasterEventPoint) => { + return props.eventTypeSpec[point.disasterType].color; + }, + undefined, + MAP_POINTS_MIN_RADIUS + ); + pointsLayer.on("click", (point: DisasterEventPoint) => + onPointClicked( + infoCardRef.current, + svgContainerRef.current, + point, + d3.event + ) + ); + } } /** diff --git a/static/js/types/disaster_event_map_types.ts b/static/js/types/disaster_event_map_types.ts index a487869c56..3697b78b48 100644 --- a/static/js/types/disaster_event_map_types.ts +++ b/static/js/types/disaster_event_map_types.ts @@ -76,3 +76,9 @@ export interface DisasterEventPointData { eventPoints: DisasterEventPoint[]; provenanceInfo: Record; } + +// Data about a set of map points to show on a map. +export interface MapPointsData { + points: DisasterEventPoint[]; + values: { [placeDcid: string]: number }; +} diff --git a/static/js/utils/disaster_event_map_utils.tsx b/static/js/utils/disaster_event_map_utils.tsx index 6413156491..93cbdf979d 100644 --- a/static/js/utils/disaster_event_map_utils.tsx +++ b/static/js/utils/disaster_event_map_utils.tsx @@ -40,6 +40,7 @@ import { DisasterEventDataApiResponse, DisasterEventPoint, DisasterEventPointData, + MapPointsData, } from "../types/disaster_event_map_types"; import { EventTypeSpec, @@ -444,3 +445,47 @@ export function getSeverityFilters( } return severityFilters; } + +/** + * gets the severity value for a disaster event point + * @param eventPoint event point to get the severity value from + * @param eventTypeSpec event type spec used for the disaster event map + */ +function getSeverityValue( + eventPoint: DisasterEventPoint, + eventTypeSpec: Record +): number { + const severityFilter = + eventTypeSpec[eventPoint.disasterType].defaultSeverityFilter; + if (!severityFilter || !(severityFilter.prop in eventPoint.severity)) { + return null; + } + return eventPoint.severity[severityFilter.prop]; +} + +/** + * Gets the map points data for each disaster type for a list of disaster event + * points. + * @param eventPoints event points to use for the map points data + * @param eventTypeSpec the event type spec for the disaster event map + */ +export function getMapPointsData( + eventPoints: DisasterEventPoint[], + eventTypeSpec: Record +): Record { + const mapPointsData = {}; + eventPoints.forEach((point) => { + if (!(point.disasterType in mapPointsData)) { + mapPointsData[point.disasterType] = { + points: [], + values: {}, + }; + } + mapPointsData[point.disasterType].points.push(point); + const severityValue = getSeverityValue(point, eventTypeSpec); + if (severityValue != null) { + mapPointsData[point.disasterType].values[point.placeDcid] = severityValue; + } + }); + return mapPointsData; +} diff --git a/static/js/utils/tests/disaster_event_map_utils.test.ts b/static/js/utils/tests/disaster_event_map_utils.test.ts index 4c016d219e..a88d393abe 100644 --- a/static/js/utils/tests/disaster_event_map_utils.test.ts +++ b/static/js/utils/tests/disaster_event_map_utils.test.ts @@ -22,10 +22,12 @@ import { EARTH_NAMED_TYPED_PLACE } from "../../shared/constants"; import { fetchDateList, fetchDisasterEventPoints, + getMapPointsData, } from "../disaster_event_map_utils"; const EARTHQUAKE_DISASTER_TYPE_ID = "earthquake"; const STORM_DISASTER_TYPE_ID = "storm"; +const FIRE_DISASTER_TYPE_ID = "fire"; const DISASTER_EVENT_TYPES = { [EARTHQUAKE_DISASTER_TYPE_ID]: ["EarthquakeEvent"], [STORM_DISASTER_TYPE_ID]: [ @@ -72,7 +74,7 @@ const EARTHQUAKE_EVENT_1_PROCESSED = { placeName: "earthquake1Name", latitude: 1, longitude: 1, - disasterType: "earthquake", + disasterType: EARTHQUAKE_DISASTER_TYPE_ID, startDate: "2022-01-01", severity: { magnitude: 5 }, endDate: "", @@ -100,7 +102,7 @@ const EARTHQUAKE_EVENT_2_PROCESSED = { placeName: "earthquake2Name", latitude: 1, longitude: 1, - disasterType: "earthquake", + disasterType: EARTHQUAKE_DISASTER_TYPE_ID, startDate: "2022-01-02", severity: { magnitude: 5 }, endDate: "", @@ -125,7 +127,7 @@ const EARTHQUAKE_EVENT_3_PROCESSED = { placeName: "earthquake3", latitude: 1, longitude: 1, - disasterType: "earthquake", + disasterType: EARTHQUAKE_DISASTER_TYPE_ID, startDate: "2022-02-01", severity: { magnitude: 5 }, endDate: "", @@ -150,7 +152,7 @@ const EARTHQUAKE_EVENT_4_PROCESSED = { placeName: "earthquake4", latitude: 1, longitude: 1, - disasterType: "earthquake", + disasterType: EARTHQUAKE_DISASTER_TYPE_ID, startDate: "2023-01-01", severity: { magnitude: 5 }, endDate: "", @@ -175,7 +177,7 @@ const EARTHQUAKE_EVENT_5_PROCESSED = { placeName: "earthquake5", latitude: 1, longitude: 1, - disasterType: "earthquake", + disasterType: EARTHQUAKE_DISASTER_TYPE_ID, startDate: "2023-02-01", severity: { magnitude: 5 }, endDate: "", @@ -197,7 +199,7 @@ const TORNADO_EVENT_1_PROCESSED = { placeName: "tornado1", latitude: 1, longitude: 1, - disasterType: "storm", + disasterType: STORM_DISASTER_TYPE_ID, startDate: "2022-01-01", severity: {}, endDate: "", @@ -218,7 +220,7 @@ const TORNADO_EVENT_2_PROCESSED = { placeName: "tornado2", latitude: 1, longitude: 1, - disasterType: "storm", + disasterType: STORM_DISASTER_TYPE_ID, startDate: "2022-03-03", severity: {}, endDate: "", @@ -239,13 +241,39 @@ const CYCLONE_EVENT_1_PROCESSED = { placeName: "cyclone1", latitude: 1, longitude: 1, - disasterType: "storm", + disasterType: STORM_DISASTER_TYPE_ID, startDate: "2022-01-01", severity: {}, endDate: "", provenanceId: "cycloneProv", }; +const FIRE_EVENT_POINT_1 = { + placeDcid: "fire1", + placeName: "fire1", + latitude: 1, + longitude: 1, + disasterType: FIRE_DISASTER_TYPE_ID, + startDate: "2022-01-01", + severity: {}, + endDate: "", + provenanceId: "fireProv", +}; + +const FIRE_EVENT_POINT_2 = { + placeDcid: "fire2", + placeName: "fire2", + latitude: 1, + longitude: 1, + disasterType: FIRE_DISASTER_TYPE_ID, + startDate: "2022-01-01", + severity: { + area: 2, + }, + endDate: "", + provenanceId: "fireProv", +}; + const EARTHQUAKE_PROV_INFO = { domain: "earthquakeDom", importName: "earthquakeImport", @@ -808,3 +836,59 @@ test("fetch data for single event with date range as YYYY-MM-DD", () => { }); }); }); + +test("getMapPointsData", () => { + const eventPoints = [ + FIRE_EVENT_POINT_1, + FIRE_EVENT_POINT_2, + EARTHQUAKE_EVENT_1_PROCESSED, + EARTHQUAKE_EVENT_2_PROCESSED, + TORNADO_EVENT_1_PROCESSED, + ]; + const eventSpec = { + [STORM_DISASTER_TYPE_ID]: { + id: STORM_DISASTER_TYPE_ID, + name: "", + eventTypeDcids: [], + color: "", + defaultSeverityFilter: null, + }, + [FIRE_DISASTER_TYPE_ID]: { + id: FIRE_DISASTER_TYPE_ID, + name: "", + eventTypeDcids: [], + color: "", + defaultSeverityFilter: { + prop: "area", + unit: "SquareKilometer", + lowerLimit: 1, + upperLimit: 10, + }, + }, + [EARTHQUAKE_DISASTER_TYPE_ID]: { + id: EARTHQUAKE_DISASTER_TYPE_ID, + name: "", + eventTypeDcids: [], + color: "", + defaultSeverityFilter: null, + }, + }; + const expectedMapPointsData = { + [STORM_DISASTER_TYPE_ID]: { + points: [TORNADO_EVENT_1_PROCESSED], + values: {}, + }, + [FIRE_DISASTER_TYPE_ID]: { + points: [FIRE_EVENT_POINT_1, FIRE_EVENT_POINT_2], + values: { + fire2: 2, + }, + }, + [EARTHQUAKE_DISASTER_TYPE_ID]: { + points: [EARTHQUAKE_EVENT_1_PROCESSED, EARTHQUAKE_EVENT_2_PROCESSED], + values: {}, + }, + }; + const result = getMapPointsData(eventPoints, eventSpec); + expect(result).toEqual(expectedMapPointsData); +});