diff --git a/static/js/shared/ga_events.test.tsx b/static/js/shared/ga_events.test.tsx index 86305453b0..125b6c035c 100644 --- a/static/js/shared/ga_events.test.tsx +++ b/static/js/shared/ga_events.test.tsx @@ -197,6 +197,7 @@ const MAP_POINTS: MapPoint[] = [ const MAP_PROPS = { breadcrumbDataValues: { PLACE_DCID: NUMBER }, dates: new Set([""]), + entities: [PLACE_DCID], geoJsonData: { features: [], properties: { diff --git a/static/js/tools/map/chart.tsx b/static/js/tools/map/chart.tsx index 569ac68597..ed8324a11c 100644 --- a/static/js/tools/map/chart.tsx +++ b/static/js/tools/map/chart.tsx @@ -78,6 +78,7 @@ interface ChartProps { facets: Record; statVarToFacets: StatVarFacetMap; statVarSpecs: StatVarSpec[]; + entities: string[]; } export const MAP_CONTAINER_ID = "choropleth-map"; @@ -200,6 +201,7 @@ export function Chart(props: ChartProps): ReactElement { (); + + if ( + svDcid === statVar.value.dcid && + chartStore.mapValuesDates.data?.numerFacets + ) { + chartStore.mapValuesDates.data.numerFacets.forEach((f) => { + if (facetInfo.metadataMap[f]) { + selectedFacetIds.add(f); + } + }); + } - const facetIdsToAdd = - selectedFacetId && facetInfo.metadataMap[selectedFacetId] - ? [selectedFacetId] - : Object.keys(facetInfo.metadataMap); + const facetIdsToAdd = Array.from(selectedFacetIds); for (const facetId of facetIdsToAdd) { statVarToFacets[svDcid].add(facetId); @@ -211,8 +218,36 @@ export function ChartLoader(): ReactElement { } } + // If per capita, explicitly include the denominator facets + // that were tracked during data processing. + const denom = statVar.value.denom; + const denomFacets = chartStore.mapValuesDates.data?.denomFacets; + + if (statVar.value.perCapita && denom && denomFacets?.size > 0) { + if (!statVarToFacets[denom]) { + statVarToFacets[denom] = new Set(); + } + + const denomMetadataMap = chartStore.denomStat.data?.facets || {}; + + for (const facetId of Array.from(denomFacets)) { + statVarToFacets[denom].add(facetId); + if (denomMetadataMap[facetId]) { + facets[facetId] = denomMetadataMap[facetId]; + } + } + } + return { facets, statVarToFacets }; - }, [facetList, statVar.value.dcid, statVar.value.metahash]); + }, [ + chartStore.denomStat.data?.facets, + chartStore.mapValuesDates.data?.denomFacets, + chartStore.mapValuesDates.data?.numerFacets, + facetList, + statVar.value.dcid, + statVar.value.denom, + statVar.value.perCapita, + ]); /** * Callback function for building observation specifications. @@ -348,6 +383,15 @@ export function ChartLoader(): ReactElement { ); const footer = document.getElementById("metadata").dataset.footer || ""; + + const entities = Array.from( + new Set([ + ...Object.keys(chartStore.mapValuesDates.data?.mapValues || {}), + ...Object.keys(chartStore.mapPointValues.data || {}), + ...Object.keys(chartStore.breadcrumbValues.data || {}), + ]) + ); + return (
{display.value.showTimeSlider && sampleDates && @@ -409,6 +454,7 @@ export function ChartLoader(): ReactElement { {footer &&
* {footer}
} ; unit?: string; + denomFacets?: Set; + numerFacets?: Set; }; context?: DataContext; }; diff --git a/static/js/tools/map/compute/map_value_dates.ts b/static/js/tools/map/compute/map_value_dates.ts index fb37f63e3b..5dab0f2921 100644 --- a/static/js/tools/map/compute/map_value_dates.ts +++ b/static/js/tools/map/compute/map_value_dates.ts @@ -64,6 +64,8 @@ export function useComputeMapValueAndDate( const mapValues = {}; const sources = new Set(); const mapDates = new Set(); + const denomFacets = new Set(); + const numerFacets = new Set(); const metadata = {}; const facets = Object.assign( {}, @@ -98,6 +100,15 @@ export function useComputeMapValueAndDate( } mapValues[placeDcid] = placeChartData.value; mapDates.add(placeChartData.date); + + if (placeChartData.denomFacet) { + denomFacets.add(placeChartData.denomFacet); + } + + if (wantedFacetData[placeDcid] && wantedFacetData[placeDcid].facet) { + numerFacets.add(wantedFacetData[placeDcid].facet); + } + if (!_.isEmpty(placeChartData.metadata)) { metadata[placeDcid] = placeChartData.metadata; } @@ -117,7 +128,7 @@ export function useComputeMapValueAndDate( statVar: _.cloneDeep(statVar.value), placeInfo: _.cloneDeep(placeInfo.value), }, - payload: { mapValues, mapDates, unit }, + payload: { mapValues, mapDates, unit, denomFacets, numerFacets }, }); }, [ dateCtx.value, diff --git a/static/js/tools/map/util.ts b/static/js/tools/map/util.ts index 797190aad1..74793d170b 100644 --- a/static/js/tools/map/util.ts +++ b/static/js/tools/map/util.ts @@ -548,6 +548,7 @@ interface PlaceChartData { date: string; value: number; unit?: string; + denomFacet?: string; } /** @@ -574,6 +575,7 @@ export function getPlaceChartData( const facetId = stat.facet; const statVarSource = metadataMap[facetId].provenanceUrl; let value = stat.value === undefined ? 0 : stat.value; + let denomFacet = null; const metadata: DataPointMetadata = { popDate: "", popSource: "", @@ -588,6 +590,7 @@ export function getPlaceChartData( return { metadata, sources, date: placeStatDate, value }; } const popFacetId = placePopData.facet; + denomFacet = popFacetId; const popSeries = placePopData.series; const popSource = metadataMap[popFacetId].provenanceUrl; metadata.popSource = popSource; @@ -606,7 +609,7 @@ export function getPlaceChartData( } sources.push(statVarSource); const unit = getUnit(metadataMap[facetId]); - return { metadata, sources, date: placeStatDate, value, unit }; + return { metadata, sources, date: placeStatDate, value, unit, denomFacet }; } /** diff --git a/static/js/tools/scatter/chart.tsx b/static/js/tools/scatter/chart.tsx index 5262a5e65b..c8a695e49b 100644 --- a/static/js/tools/scatter/chart.tsx +++ b/static/js/tools/scatter/chart.tsx @@ -25,6 +25,7 @@ import React, { ReactElement, RefObject, useEffect, + useMemo, useRef, useState, } from "react"; @@ -113,6 +114,10 @@ export function Chart(props: ChartPropsType): ReactElement { xDates.add(point.xDate); yDates.add(point.yDate); }); + const entities = useMemo( + () => Object.keys(props.points || {}), + [props.points] + ); const xTitle = getTitle(Array.from(xDates), props.xLabel); const yTitle = getTitle(Array.from(yDates), props.yLabel); // Tooltip needs to start off hidden @@ -239,6 +244,7 @@ export function Chart(props: ChartPropsType): ReactElement { ; xUnit: string; yUnit: string; + xDenomFacets: Set; + yDenomFacets: Set; + xNumerFacets: Set; + yNumerFacets: Set; }; export function ChartLoader(): ReactElement { @@ -143,43 +147,52 @@ export function ChartLoader(): ReactElement { const facets: Record = {}; const statVarToFacets: StatVarFacetMap = {}; - if (!cache || !cache.baseFacets || !cache.metadataMap) { + if (!cache || !cache.metadataMap || !chartData) { return { facets, statVarToFacets }; } - // We build the statVar to facet mapping and the metadata map - for (const statVarDcid in cache.baseFacets) { + // Helper to map plotted facets to their variable and populate metadata + const processFacets = ( + statVarDcid: string, + plottedFacets: Set + ): void => { + if (!statVarDcid || !plottedFacets || plottedFacets.size === 0) return; + if (!statVarToFacets[statVarDcid]) { statVarToFacets[statVarDcid] = new Set(); } - // Check if there is a specific facet selected for this variable - const selectedFacetIds = new Set(); - - if (xVal.statVarDcid === statVarDcid && xVal.metahash) { - selectedFacetIds.add(xVal.metahash); - } - if (yVal.statVarDcid === statVarDcid && yVal.metahash) { - selectedFacetIds.add(yVal.metahash); - } - - // If facets have been selected, we add only those to `facets`. - // If none are selected, we add all facets associated with the variable. - const facetIdsToConsider = - selectedFacetIds.size > 0 - ? Array.from(selectedFacetIds) - : Object.keys(cache.baseFacets[statVarDcid]); - - for (const facetId of facetIdsToConsider) { + for (const facetId of Array.from(plottedFacets)) { statVarToFacets[statVarDcid].add(facetId); if (cache.metadataMap[facetId]) { facets[facetId] = cache.metadataMap[facetId]; } } + }; + + //Add in numerators that appear in the chart + processFacets(xVal.statVarDcid, chartData.xNumerFacets); + processFacets(yVal.statVarDcid, chartData.yNumerFacets); + + //Add in denominators that appear in the chart + if (xVal.perCapita && xVal.denom) { + processFacets(xVal.denom, chartData.xDenomFacets); + } + if (yVal.perCapita && yVal.denom) { + processFacets(yVal.denom, chartData.yDenomFacets); } return { facets, statVarToFacets }; - }, [cache, xVal.statVarDcid, xVal.metahash, yVal.statVarDcid, yVal.metahash]); + }, [ + cache, + chartData, + xVal.statVarDcid, + xVal.perCapita, + xVal.denom, + yVal.statVarDcid, + yVal.perCapita, + yVal.denom, + ]); /** * Callback function for building observation specifications. @@ -337,6 +350,7 @@ export function ChartLoader(): ReactElement { /> = new Set(); let xUnit = ""; let yUnit = ""; + const xDenomFacets: Set = new Set(); + const yDenomFacets: Set = new Set(); + const xNumerFacets: Set = new Set(); + const yNumerFacets: Set = new Set(); + for (const namedPlace of place.enclosedPlaces) { const xDenom = x.perCapita ? x.denom : null; const yDenom = y.perCapita ? y.denom : null; @@ -600,11 +620,38 @@ function getChartData( sources.add(source); } }); + + // Compile the used numerator and denominator facets + if (placeChartData.xDenomFacet) { + xDenomFacets.add(placeChartData.xDenomFacet); + } + if (placeChartData.yDenomFacet) { + yDenomFacets.add(placeChartData.yDenomFacet); + } + + const xNumerFacet = xStatData?.[namedPlace.dcid]?.facet; + if (xNumerFacet) { + xNumerFacets.add(xNumerFacet); + } + const yNumerFacet = yStatData?.[namedPlace.dcid]?.facet; + if (yNumerFacet) { + yNumerFacets.add(yNumerFacet); + } + points[namedPlace.dcid] = placeChartData.point; xUnit = xUnit || placeChartData.xUnit; yUnit = yUnit || placeChartData.yUnit; } - return { points, sources, xUnit, yUnit }; + return { + points, + sources, + xUnit, + yUnit, + xDenomFacets, + yDenomFacets, + xNumerFacets, + yNumerFacets, + }; } function getFacetInfo( diff --git a/static/js/tools/shared/vis_tools/tool_chart_footer.tsx b/static/js/tools/shared/vis_tools/tool_chart_footer.tsx index 5da9f0097e..701ad43a4b 100644 --- a/static/js/tools/shared/vis_tools/tool_chart_footer.tsx +++ b/static/js/tools/shared/vis_tools/tool_chart_footer.tsx @@ -70,6 +70,8 @@ interface ToolChartFooterProps { facets?: Record; // A mapping of which stat var used which facets statVarToFacets?: StatVarFacetMap; + // Array of entity dcids + entities?: string[]; // A mapping of stat var dcids to their specific min and max date range from the chart statVarDateRanges?: Record; // the stat vars to link to in the metadata modal @@ -187,6 +189,7 @@ export function ToolChartFooter(props: ToolChartFooterProps): ReactElement { {!_.isEmpty(props.sources) && ( { } // Get the denom for ChartEmbed if (this.props.pc && this.props.denom) { - const denomFacetId = findFirstAvailableFacet( - places, - (place) => - this.state.rawData.statAllData[this.props.denom]?.[place]?.[0] - ?.facet - ); - if (denomFacetId) { - embedStatVarToFacets[this.props.denom] = new Set([denomFacetId]); + if ( + this.state.statData.denomFacets && + this.state.statData.denomFacets.size > 0 + ) { + embedStatVarToFacets[this.props.denom] = + this.state.statData.denomFacets; } } } @@ -249,6 +247,7 @@ class Chart extends Component {
{ {this.state.isDataLoaded && ( { statVars: ["Count_Person"], sources: new Set(["source1", "source2"]), measurementMethods: new Set(), + denomFacets: new Set(), }); }); @@ -589,6 +590,7 @@ test("get stats data where latest date with data for all stat vars is not the la statVars: ["Count_Person"], sources: new Set(["source1", "source2"]), measurementMethods: new Set(), + denomFacets: new Set(), }); }); @@ -700,6 +702,7 @@ test("get stats data where there is no date with data for all stat vars", () => provenanceUrl: "source2", }, }, + denomFacets: new Set(), }); }); @@ -803,6 +806,7 @@ test("get stats data with per capita with population size 0", () => { provenanceUrl: "source2", }, }, + denomFacets: new Set(["fac1"]), }); }); @@ -1013,6 +1017,7 @@ test("get stats data with Per capita with specified denominators", () => { provenanceUrl: "source2", }, }, + denomFacets: new Set(["facet1", "facet2"]), }); }); @@ -1144,6 +1149,7 @@ test("get stats data with per capita with specified denominators - missing place provenanceUrl: "source2", }, }, + denomFacets: new Set(["facet1"]), }); }); @@ -1271,6 +1277,7 @@ test("get stat data with specified source", () => { provenanceUrl: "source2", }, }, + denomFacets: new Set(), }); }); @@ -1305,6 +1312,7 @@ test("StatsData test", () => { dates: [], sources: new Set(), measurementMethods: new Set(), + denomFacets: new Set(), }; expect(getStatVarGroupWithTime(statData, "geoId/01")).toEqual([]); }); @@ -1353,6 +1361,7 @@ test("transform from models - multiple places", () => { statVars: ["Max_Temperature_RCP26"], sources: new Set(["nasa.gov"]), measurementMethods: new Set(["NASA_Mean_CCSM4"]), + denomFacets: new Set(), }; const modelStatAllResponse: SeriesAllApiResponse = { @@ -1477,6 +1486,7 @@ test("transform from models - multiple places", () => { statVars: ["Max_Temperature_RCP26"], sources: new Set(["model.nasa.gov"]), measurementMethods: new Set(["Mean across models"]), + denomFacets: new Set(), }, { // model StatData @@ -1561,6 +1571,7 @@ test("transform from models - multiple places", () => { ], sources: new Set(["model.nasa.gov"]), measurementMethods: new Set(["NASA_Mean_CCSM4", "NASA_Mean_HadGEM2-AO"]), + denomFacets: new Set(), }, ]; expect( @@ -1601,6 +1612,7 @@ test("transform from models - multiple obs periods", () => { statVars: ["Max_Temperature_RCP26"], sources: new Set(["model.nasa.gov"]), measurementMethods: new Set(["NASA_Mean_CCSM4"]), + denomFacets: new Set(), }; const modelStatAllResponse: SeriesAllApiResponse = { @@ -1722,6 +1734,7 @@ test("transform from models - multiple obs periods", () => { statVars: ["Max_Temperature_RCP26"], sources: new Set(["p1y.nasa.gov"]), measurementMethods: new Set(["Mean across models"]), + denomFacets: new Set(), }, { // model StatData @@ -1780,6 +1793,7 @@ test("transform from models - multiple obs periods", () => { ], sources: new Set(["p1y.nasa.gov"]), measurementMethods: new Set(["NASA_Mean_CCSM4", "NASA_Mean_HadGEM2-AO"]), + denomFacets: new Set(), }, ]; diff --git a/static/js/tools/timeline/data_fetcher.ts b/static/js/tools/timeline/data_fetcher.ts index 658e384246..6d05064461 100644 --- a/static/js/tools/timeline/data_fetcher.ts +++ b/static/js/tools/timeline/data_fetcher.ts @@ -47,6 +47,7 @@ export interface StatData { // Keyed by facet id. facets: Record; displayNames?: DisplayNameApiResponse; + denomFacets?: Set; } /** @@ -201,6 +202,12 @@ export function getStatData( denom = "", scaling = 1 ): StatData { + const facets = Object.assign( + {}, + rawData.statAllData.facets, + rawData.denomData.facets + ); + const result: StatData = { places, statVars, @@ -209,12 +216,10 @@ export function getStatData( sources: new Set(), measurementMethods: new Set(), displayNames: {}, - facets: rawData.statAllData.facets, + facets, + denomFacets: new Set(), }; - const facets = Object.assign( - rawData.statAllData.facets, - rawData.denomData.facets - ); + const allDates = new Set(); for (const sv of statVars) { @@ -256,9 +261,9 @@ export function getStatData( rawData.denomData.data[denom][place].series, scaling ); - result.sources.add( - facets[rawData.denomData.data[denom][place].facet].provenanceUrl - ); + const denomFacet = rawData.denomData.data[denom][place].facet; + result.denomFacets.add(denomFacet); + result.sources.add(facets[denomFacet].provenanceUrl); } svData[place] = selectedSeries; } @@ -309,6 +314,7 @@ export function statDataFromModels( sources: new Set(), measurementMethods: new Set(), facets: {}, + denomFacets: new Set(), }; if (!mainStatData.dates.length) { return [mainStatData, modelData]; diff --git a/static/js/utils/scatter_data_utils.ts b/static/js/utils/scatter_data_utils.ts index 5ba7537012..6a9401f1b9 100644 --- a/static/js/utils/scatter_data_utils.ts +++ b/static/js/utils/scatter_data_utils.ts @@ -34,6 +34,7 @@ interface PlaceAxisChartData { popValue?: number; popDate?: string; unit?: string; + denomFacet?: string; } /** @@ -72,10 +73,13 @@ function getPlaceAxisChartData( let value = obs.value || 0; let denomValue = null; let denomDate = null; + let denomFacet = null; + if (!_.isEmpty(denomSeries)) { const denomObs = getMatchingObservation(denomSeries.series, obs.date); denomValue = denomObs.value; denomDate = denomObs.date; + denomFacet = denomSeries.facet; value /= denomValue; } if (scaling) { @@ -96,12 +100,14 @@ function getPlaceAxisChartData( // Otherwise, use the default "Count_Person" variable. popValue = popValue || popObs.value; popDate = popDate || popObs.date; + denomFacet = denomFacet || popSeries.facet; } else { console.log(`No population data for ${placeDcid}`); } } const unit = getUnit(metadataMap[metaHash]); - return { value, statDate, sources, popValue, popDate, unit }; + + return { value, statDate, sources, popValue, popDate, unit, denomFacet }; } interface PlaceScatterData { @@ -109,6 +115,8 @@ interface PlaceScatterData { sources: string[]; xUnit?: string; yUnit?: string; + xDenomFacet?: string; + yDenomFacet?: string; } /** @@ -173,5 +181,12 @@ export function getPlaceScatterData( yPopDate: yChartData.popDate, }; const sources = xChartData.sources.concat(yChartData.sources); - return { point, sources, xUnit: xChartData.unit, yUnit: yChartData.unit }; + return { + point, + sources, + xUnit: xChartData.unit, + yUnit: yChartData.unit, + xDenomFacet: xChartData.denomFacet, + yDenomFacet: yChartData.denomFacet, + }; }