diff --git a/server/integration_tests/test_data/demo2_cities_feb2023/query_1/chart_config.json b/server/integration_tests/test_data/demo2_cities_feb2023/query_1/chart_config.json index 89aceeee1d..80013e66ec 100644 --- a/server/integration_tests/test_data/demo2_cities_feb2023/query_1/chart_config.json +++ b/server/integration_tests/test_data/demo2_cities_feb2023/query_1/chart_config.json @@ -56,7 +56,8 @@ }, "Percent_Student_AsAFractionOf_Count_Teacher": { "name": "Student-Teacher Ratio", - "statVar": "Percent_Student_AsAFractionOf_Count_Teacher" + "statVar": "Percent_Student_AsAFractionOf_Count_Teacher", + "unit": "%" } } } diff --git a/server/integration_tests/test_data/demo_feb2023/query_7/chart_config.json b/server/integration_tests/test_data/demo_feb2023/query_7/chart_config.json index 940e4ae91f..719a66ddb9 100644 --- a/server/integration_tests/test_data/demo_feb2023/query_7/chart_config.json +++ b/server/integration_tests/test_data/demo_feb2023/query_7/chart_config.json @@ -34,7 +34,8 @@ "statVarSpec": { "Percent_Person_WithHighBloodPressure": { "name": "High Bood Pressure", - "statVar": "Percent_Person_WithHighBloodPressure" + "statVar": "Percent_Person_WithHighBloodPressure", + "unit": "%" } } } diff --git a/server/integration_tests/test_data/demo_feb2023/query_8/chart_config.json b/server/integration_tests/test_data/demo_feb2023/query_8/chart_config.json index 2492f48fba..fa745dd6fd 100644 --- a/server/integration_tests/test_data/demo_feb2023/query_8/chart_config.json +++ b/server/integration_tests/test_data/demo_feb2023/query_8/chart_config.json @@ -34,7 +34,8 @@ "statVarSpec": { "Percent_Person_WithHighBloodPressure": { "name": "High Bood Pressure", - "statVar": "Percent_Person_WithHighBloodPressure" + "statVar": "Percent_Person_WithHighBloodPressure", + "unit": "%" } } } diff --git a/server/integration_tests/test_data/demo_feb2023/query_9/chart_config.json b/server/integration_tests/test_data/demo_feb2023/query_9/chart_config.json index b1196fc9df..f0cb160dcd 100644 --- a/server/integration_tests/test_data/demo_feb2023/query_9/chart_config.json +++ b/server/integration_tests/test_data/demo_feb2023/query_9/chart_config.json @@ -118,7 +118,8 @@ }, "Percent_Person_WithHighBloodPressure_scatter": { "name": "High Bood Pressure", - "statVar": "Percent_Person_WithHighBloodPressure" + "statVar": "Percent_Person_WithHighBloodPressure", + "unit": "%" }, "WagesTotal_Worker_NAICSProfessionalScientificTechnicalServices_scatter": { "name": "Total Wages Paid to Professional, Scientific, and Technical Services", diff --git a/server/lib/nl/page_config_builder.py b/server/lib/nl/page_config_builder.py index 63df1110b8..3acc6d4ab6 100644 --- a/server/lib/nl/page_config_builder.py +++ b/server/lib/nl/page_config_builder.py @@ -100,7 +100,6 @@ def build_page_config( all_svs.update(cspec.svs) all_svs = list(all_svs) sv2name = utils.get_sv_name(all_svs) - # TODO: use this when setting sv spec for map and ranking charts sv2unit = utils.get_sv_unit(all_svs) # Get footnotes of all SVs @@ -150,7 +149,8 @@ def build_page_config( for sv in cspec.svs: _, column = builder.new_chart(cspec.attr) stat_var_spec_map.update( - _map_chart_block(column, cspec.places[0], sv, sv2name, cspec.attr)) + _map_chart_block(column, cspec.places[0], sv, sv2name, sv2unit, + cspec.attr)) elif cspec.chart_type == ChartType.RANKING_CHART: if not _is_map_or_ranking_compatible(cspec): @@ -160,7 +160,7 @@ def build_page_config( if cspec.attr['source_topic'] == 'dc/topic/ProjectedClimateExtremes': stat_var_spec_map.update( _ranking_chart_block_climate_extremes(builder, pri_place, cspec.svs, - sv2name, sv2footnote, + sv2name, sv2unit, sv2footnote, cspec.attr)) else: @@ -183,7 +183,7 @@ def build_page_config( builder.block.title = _decorate_block_title(title=main_title, chart_origin=chart_origin) stat_var_spec_map.update( - _ranking_chart_block_nopc(column, pri_place, sv, sv2name, + _ranking_chart_block_nopc(column, pri_place, sv, sv2name, sv2unit, cspec.attr)) if cspec.attr['include_percapita'] and _should_add_percapita(sv): if not 'skip_map_for_ranking' in cspec.attr: @@ -198,7 +198,8 @@ def build_page_config( elif cspec.chart_type == ChartType.SCATTER_CHART: _, column = builder.new_chart(cspec.attr) stat_var_spec_map = _scatter_chart_block(column, cspec.places[0], - cspec.svs, sv2name, cspec.attr) + cspec.svs, sv2name, sv2unit, + cspec.attr) elif cspec.chart_type == ChartType.EVENT_CHART and event_config: block, column = builder.new_chart(cspec.attr) @@ -337,15 +338,15 @@ def _multiple_place_bar_block(column, places: List[Place], svs: List[str], return stat_var_spec_map -def _map_chart_block(column, place: Place, pri_sv: str, sv2name, attr): - svs_map = _map_chart_block_nopc(column, place, pri_sv, sv2name, attr) +def _map_chart_block(column, place: Place, pri_sv: str, sv2name, sv2unit, attr): + svs_map = _map_chart_block_nopc(column, place, pri_sv, sv2name, sv2unit, attr) if attr['include_percapita'] and _should_add_percapita(pri_sv): svs_map.update(_map_chart_block_pc(column, place, pri_sv, sv2name, attr)) return svs_map def _map_chart_block_nopc(column, place: Place, pri_sv: str, sv2name: Dict, - attr: Dict): + sv2unit: Dict, attr: Dict): # The main tile tile = column.tiles.add() tile.stat_var_key.append(pri_sv) @@ -356,7 +357,9 @@ def _map_chart_block_nopc(column, place: Place, pri_sv: str, sv2name: Dict, child_type=attr.get('place_type', '')) stat_var_spec_map = {} - stat_var_spec_map[pri_sv] = StatVarSpec(stat_var=pri_sv, name=sv2name[pri_sv]) + stat_var_spec_map[pri_sv] = StatVarSpec(stat_var=pri_sv, + name=sv2name[pri_sv], + unit=sv2unit[pri_sv]) return stat_var_spec_map @@ -418,7 +421,8 @@ def _does_extreme_mean_low(sv: str) -> bool: def _ranking_chart_block_climate_extremes(builder, pri_place: Place, pri_svs: List[str], sv2name: Dict, - sv2footnote: Dict, attr: Dict): + sv2unit: Dict, sv2footnote: Dict, + attr: Dict): footnotes = [] stat_var_spec_map = {} @@ -446,7 +450,8 @@ def _ranking_chart_block_climate_extremes(builder, pri_place: Place, if len(map_column.tiles): map_column = map_block.columns.add() stat_var_spec_map.update( - _map_chart_block_nopc(map_column, pri_place, sv, sv2name, attr)) + _map_chart_block_nopc(map_column, pri_place, sv, sv2name, sv2unit, + attr)) map_column.tiles[0].title = sv2name[ sv] # override decorated title (too long). @@ -458,7 +463,7 @@ def _ranking_chart_block_climate_extremes(builder, pri_place: Place, def _ranking_chart_block_nopc(column, pri_place: Place, pri_sv: str, - sv2name: Dict, attr: Dict): + sv2name: Dict, sv2unit: Dict, attr: Dict): # The main tile tile = column.tiles.add() tile.stat_var_key.append(pri_sv) @@ -470,12 +475,15 @@ def _ranking_chart_block_nopc(column, pri_place: Place, pri_sv: str, child_type=attr.get('place_type', '')) stat_var_spec_map = {} - stat_var_spec_map[pri_sv] = StatVarSpec(stat_var=pri_sv, name=sv2name[pri_sv]) + stat_var_spec_map[pri_sv] = StatVarSpec(stat_var=pri_sv, + name=sv2name[pri_sv], + unit=sv2unit[pri_sv]) if not 'skip_map_for_ranking' in attr: # Also add a map chart. stat_var_spec_map.update( - _map_chart_block_nopc(column, pri_place, pri_sv, sv2name, attr)) + _map_chart_block_nopc(column, pri_place, pri_sv, sv2name, sv2unit, + attr)) return stat_var_spec_map @@ -528,10 +536,11 @@ def _ranking_chart_block_pc(column, pri_place: Place, pri_sv: str, def _scatter_chart_block(column, pri_place: Place, sv_pair: List[str], sv2name, - attr: Dict): + sv2unit, attr: Dict): assert len(sv_pair) == 2 sv_names = [sv2name[sv_pair[0]], sv2name[sv_pair[1]]] + sv_units = [sv2unit[sv_pair[0]], sv2unit[sv_pair[1]]] sv_key_pair = [sv_pair[0] + '_scatter', sv_pair[1] + '_scatter'] change_to_pc = [False, False] @@ -554,7 +563,8 @@ def _scatter_chart_block(column, pri_place: Place, sv_pair: List[str], sv2name, scaling=100) else: stat_var_spec_map[sv_key_pair[i]] = StatVarSpec(stat_var=sv_pair[i], - name=sv_names[i]) + name=sv_names[i], + unit=sv_units[i]) # add a scatter config tile = column.tiles.add() diff --git a/server/tests/lib/nl/page_config_builder_test.py b/server/tests/lib/nl/page_config_builder_test.py index f63df061f7..0b4fd36aa7 100644 --- a/server/tests/lib/nl/page_config_builder_test.py +++ b/server/tests/lib/nl/page_config_builder_test.py @@ -420,6 +420,7 @@ value { stat_var: "Count_Farm" name: "Count_Farm-name" + unit: "Count_Farm-unit" } } stat_var_spec { @@ -437,6 +438,7 @@ value { stat_var: "Income_Farm" name: "Income_Farm-name" + unit: "Income_Farm-unit" } } stat_var_spec { @@ -493,6 +495,7 @@ value { stat_var: "Count_Farm" name: "Count_Farm-name" + unit: "Count_Farm-unit" } } stat_var_spec { @@ -500,6 +503,7 @@ value { stat_var: "Mean_Precipitation" name: "Mean_Precipitation-name" + unit: "Mean_Precipitation-unit" } } stat_var_spec { @@ -507,6 +511,7 @@ value { stat_var: "Income_Farm" name: "Income_Farm-name" + unit: "Income_Farm-unit" } } } @@ -566,6 +571,7 @@ value { stat_var: "Count_Agricultural_Workers" name: "Count_Agricultural_Workers-name" + unit: "Count_Agricultural_Workers-unit" } } stat_var_spec { diff --git a/static/js/components/tiles/bar_tile.tsx b/static/js/components/tiles/bar_tile.tsx index 34a849b929..88c7704c40 100644 --- a/static/js/components/tiles/bar_tile.tsx +++ b/static/js/components/tiles/bar_tile.tsx @@ -30,7 +30,11 @@ import { RankingPoint } from "../../types/ranking_unit_types"; import { stringifyFn } from "../../utils/axios"; import { dataGroupsToCsv } from "../../utils/chart_csv_utils"; import { getPlaceNames } from "../../utils/place_utils"; -import { getStatVarName, ReplacementStrings } from "../../utils/tile_utils"; +import { + getStatVarName, + getUnitString, + ReplacementStrings, +} from "../../utils/tile_utils"; import { ChartTileContainer } from "./chart_tile"; const NUM_PLACES = 6; @@ -55,6 +59,7 @@ interface BarTilePropType { interface BarChartData { dataGroup: DataGroup[]; sources: Set; + unit: string; } export function BarTile(props: BarTilePropType): JSX.Element { @@ -167,6 +172,7 @@ function processData( // Fetch the place names getPlaceNames(Array.from(popPoints).map((x) => x.placeDcid)).then( (placeNames) => { + let unit = ""; for (const point of popPoints) { const placeDcid = point.placeDcid; const dataPoints: DataPoint[] = []; @@ -183,6 +189,11 @@ function processData( }; if (raw.facets[stat.facet]) { sources.add(raw.facets[stat.facet].provenanceUrl); + const svUnit = getUnitString( + raw.facets[stat.facet].unit, + spec.denom + ); + unit = unit || svUnit; } if (spec.denom && spec.denom in raw.data) { const denomStat = raw.data[spec.denom][placeDcid]; @@ -198,9 +209,13 @@ function processData( new DataGroup(placeNames[placeDcid] || placeDcid, dataPoints) ); } + if (!_.isEmpty(props.statVarSpec)) { + unit = props.statVarSpec[0].unit || unit; + } setBarChartData({ dataGroup: dataGroups, sources: sources, + unit, }); } ); @@ -215,6 +230,6 @@ function draw(props: BarTilePropType, chartData: BarChartData): void { elem.offsetWidth, props.svgChartHeight, chartData.dataGroup, - props.statVarSpec[0].unit + chartData.unit ); } diff --git a/static/js/components/tiles/line_tile.tsx b/static/js/components/tiles/line_tile.tsx index f0b744a5b8..ed5a8305b8 100644 --- a/static/js/components/tiles/line_tile.tsx +++ b/static/js/components/tiles/line_tile.tsx @@ -29,7 +29,11 @@ import { NamedTypedPlace, StatVarSpec } from "../../shared/types"; import { computeRatio } from "../../tools/shared_util"; import { stringifyFn } from "../../utils/axios"; import { dataGroupsToCsv } from "../../utils/chart_csv_utils"; -import { getStatVarName, ReplacementStrings } from "../../utils/tile_utils"; +import { + getStatVarName, + getUnitString, + ReplacementStrings, +} from "../../utils/tile_utils"; import { ChartTileContainer } from "./chart_tile"; interface LineTilePropType { @@ -46,6 +50,7 @@ interface LineTilePropType { interface LineChartData { dataGroup: DataGroup[]; sources: Set; + unit: string; } export function LineTile(props: LineTilePropType): JSX.Element { @@ -67,7 +72,7 @@ export function LineTile(props: LineTilePropType): JSX.Element { useEffect(() => { if (lineChartData) { - draw(props, lineChartData.dataGroup, svgContainer); + draw(props, lineChartData, svgContainer); } }, [props, lineChartData]); @@ -132,7 +137,7 @@ function processData( function draw( props: LineTilePropType, - chartData: DataGroup[], + chartData: LineChartData, svgContainer: React.RefObject ): void { const elem = document.getElementById(props.id); @@ -142,10 +147,10 @@ function draw( props.id, elem.offsetWidth, props.svgChartHeight, - chartData, + chartData.dataGroup, false, false, - props.statVarSpec[0].unit + chartData.unit ); if (!isCompleteLine) { svgContainer.current.querySelectorAll(".dotted-warning")[0].className += @@ -164,6 +169,7 @@ function rawToChart( const dataGroups: DataGroup[] = []; const sources = new Set(); const allDates = new Set(); + let unit = ""; for (const spec of props.statVarSpec) { // Do not modify the React state. Create a clone. const series = raw.data[spec.statVar][props.place.dcid]; @@ -188,14 +194,20 @@ function rawToChart( dataPoints ) ); + const svUnit = getUnitString(raw.facets[series.facet].unit, spec.denom); + unit = unit || svUnit; sources.add(raw.facets[series.facet].provenanceUrl); } } for (let i = 0; i < dataGroups.length; i++) { dataGroups[i].value = expandDataPoints(dataGroups[i].value, allDates); } + if (!_.isEmpty(props.statVarSpec)) { + unit = props.statVarSpec[0].unit || unit; + } return { dataGroup: dataGroups, sources: sources, + unit, }; } diff --git a/static/js/components/tiles/map_tile.tsx b/static/js/components/tiles/map_tile.tsx index b98be8ce02..240304a92c 100644 --- a/static/js/components/tiles/map_tile.tsx +++ b/static/js/components/tiles/map_tile.tsx @@ -47,10 +47,11 @@ import { isChildPlaceOf, shouldShowMapBoundaries, } from "../../tools/shared_util"; +import { getUnit } from "../../tools/shared_util"; import { stringifyFn } from "../../utils/axios"; import { mapDataToCsv } from "../../utils/chart_csv_utils"; import { getDateRange } from "../../utils/string_utils"; -import { ReplacementStrings } from "../../utils/tile_utils"; +import { getUnitString, ReplacementStrings } from "../../utils/tile_utils"; import { ChartTileContainer } from "./chart_tile"; interface MapTilePropType { @@ -81,6 +82,7 @@ interface MapChartData { dateRange: string; isUsaPlace: boolean; showMapBoundaries: boolean; + unit: string; } export function MapTile(props: MapTilePropType): JSX.Element { @@ -106,7 +108,7 @@ export function MapTile(props: MapTilePropType): JSX.Element { if (rawData) { processData( rawData, - !_.isEmpty(props.statVarSpec.denom), + props.statVarSpec, props.place, props.statVarSpec.scaling, props.enclosedPlaceType, @@ -221,7 +223,7 @@ function fetchData( function processData( rawData: RawData, - isPerCapita: boolean, + statVarSpec: StatVarSpec, place: NamedTypedPlace, scaling: number, enclosedPlaceType: string, @@ -234,6 +236,7 @@ function processData( if (_.isEmpty(rawData.geoJson)) { return; } + const isPerCapita = !_.isEmpty(statVarSpec.denom); for (const geoFeature of rawData.geoJson.features) { const placeDcid = geoFeature.properties.geoDcid; const placeChartData = getPlaceChartData( @@ -263,6 +266,11 @@ function processData( if (_.isEmpty(dataValues)) { return; } + const statUnit = getUnit( + Object.values(rawData.placeStat), + rawData.metadataMap + ); + const unit = getUnitString(statUnit, statVarSpec.denom); setChartData({ dataValues, metadata, @@ -275,6 +283,7 @@ function processData( rawData.parentPlaces ), showMapBoundaries: shouldShowMapBoundaries(place, enclosedPlaceType), + unit: statVarSpec.unit || unit, }); } @@ -308,14 +317,14 @@ function draw( const chartDatavalue = chartData.dataValues[place.dcid]; value = formatNumber( Number(chartDatavalue.toPrecision(2)), - props.statVarSpec.unit + chartData.unit ); } else { value = formatNumber( Math.round( (chartData.dataValues[place.dcid] + Number.EPSILON) * 100 ) / 100, - props.statVarSpec.unit + chartData.unit ); } } @@ -325,7 +334,7 @@ function draw( legendContainer.current, height, colorScale, - props.statVarSpec.unit, + chartData.unit, 0 ); const chartWidth = svgContainer.current.offsetWidth - legendWidth; diff --git a/static/js/components/tiles/ranking_tile.tsx b/static/js/components/tiles/ranking_tile.tsx index dc7641eeb2..dbdd69b7b8 100644 --- a/static/js/components/tiles/ranking_tile.tsx +++ b/static/js/components/tiles/ranking_tile.tsx @@ -35,6 +35,7 @@ import { formatString, getSourcesJsx, getStatVarName, + getUnitString, } from "../../utils/tile_utils"; import { RankingUnit } from "../ranking_unit"; @@ -297,6 +298,7 @@ function pointApiToPerSvRankingData( } const arr = []; const sources = new Set(); + let svUnit = ""; for (const place in statData.data[spec.statVar]) { const statPoint = statData.data[spec.statVar][place]; const rankingPoint = { @@ -329,8 +331,10 @@ function pointApiToPerSvRankingData( arr.push(rankingPoint); if (statPoint.facet && statData.facets[statPoint.facet]) { const statPointSource = statData.facets[statPoint.facet].provenanceUrl; + const statPointUnit = statData.facets[statPoint.facet].unit; if (statPointSource) { sources.add(statPointSource); + svUnit = svUnit || statPointUnit; } } } @@ -338,9 +342,10 @@ function pointApiToPerSvRankingData( return a.value - b.value; }); const numDataPoints = arr.length; + svUnit = getUnitString(svUnit, spec.denom); rankingData[spec.statVar] = { points: arr, - unit: [spec.unit], + unit: [spec.unit || svUnit], scaling: [spec.scaling], numDataPoints, sources, diff --git a/static/js/components/tiles/scatter_tile.tsx b/static/js/components/tiles/scatter_tile.tsx index 6e7a9339d1..64a49192ea 100644 --- a/static/js/components/tiles/scatter_tile.tsx +++ b/static/js/components/tiles/scatter_tile.tsx @@ -32,12 +32,17 @@ import { ChartQuadrant } from "../../constants/scatter_chart_constants"; import { PointApiResponse, SeriesApiResponse } from "../../shared/stat_types"; import { NamedTypedPlace, StatVarSpec } from "../../shared/types"; import { getStatWithinPlace } from "../../tools/scatter/util"; +import { getUnit } from "../../tools/shared_util"; import { ScatterTileSpec } from "../../types/subject_page_proto_types"; import { stringifyFn } from "../../utils/axios"; import { scatterDataToCsv } from "../../utils/chart_csv_utils"; import { getStringOrNA } from "../../utils/number_utils"; import { getPlaceScatterData } from "../../utils/scatter_data_utils"; -import { getStatVarName, ReplacementStrings } from "../../utils/tile_utils"; +import { + getStatVarName, + getUnitString, + ReplacementStrings, +} from "../../utils/tile_utils"; import { ChartTileContainer } from "./chart_tile"; interface ScatterTilePropType { @@ -64,6 +69,8 @@ interface ScatterChartData { yStatVar: StatVarSpec; points: { [placeDcid: string]: Point }; sources: Set; + xUnit: string; + yUnit: string; } export function ScatterTile(props: ScatterTilePropType): JSX.Element { @@ -257,7 +264,30 @@ function processData( if (_.isEmpty(points)) { setErrorMsg("Sorry, we don't have data for those variables"); } - setChartdata({ xStatVar, yStatVar, points, sources }); + let xUnit = xStatVar.unit; + if (!xUnit) { + const xStatUnit = getUnit( + Object.values(xPlacePointStat), + rawData.placeStats.facets + ); + xUnit = getUnitString(xStatUnit, xStatVar.denom); + } + let yUnit = yStatVar.unit; + if (!yUnit) { + const yStatUnit = getUnit( + Object.values(yPlacePointStat), + rawData.placeStats.facets + ); + yUnit = getUnitString(yStatUnit, yStatVar.denom); + } + setChartdata({ + xStatVar, + yStatVar, + points, + sources, + xUnit, + yUnit, + }); } function getTooltipElement( diff --git a/static/js/i18n/i18n.tsx b/static/js/i18n/i18n.tsx index 49088943c1..be1c48b044 100644 --- a/static/js/i18n/i18n.tsx +++ b/static/js/i18n/i18n.tsx @@ -199,6 +199,7 @@ function formatNumber( formatOptions.style = "percent"; value = value / 100; // Values are scaled by formatter for percent display break; + case "MetricTon": case "t": shouldAddUnit = true; unitKey = "metric-ton"; @@ -232,6 +233,7 @@ function formatNumber( shouldAddUnit = true; unitKey = "micro-gram-per-cubic-meter"; break; + case "MetricTonCO2e": case "MTCO2e": shouldAddUnit = true; unitKey = "metric-tons-of-co2"; diff --git a/static/js/utils/string_utils.ts b/static/js/utils/string_utils.ts index a7a57527cd..c874d8caab 100644 --- a/static/js/utils/string_utils.ts +++ b/static/js/utils/string_utils.ts @@ -141,6 +141,12 @@ export function formatNumber( case "Percent": displayUnit = "%"; break; + case "MetricTonCO2e": + displayUnit = "MTCO2e"; + break; + case "MetricTon": + displayUnit = "t"; + break; } if (displayUnit) { returnText = `${returnText} ${displayUnit}`; diff --git a/static/js/utils/tile_utils.tsx b/static/js/utils/tile_utils.tsx index 7f1904be05..cdec9d1d82 100644 --- a/static/js/utils/tile_utils.tsx +++ b/static/js/utils/tile_utils.tsx @@ -103,3 +103,17 @@ export function getSourcesJsx(sources: Set): JSX.Element[] { }); return sourcesJsx; } + +/** + * Gets the unit given the unit for the stat var and the dcid of the denominator + * TODO(chejennifer): clean up all the getUnit functions in this repo + * @param statUnit the unit for the stat var + * @param denomDcid the dcid of the denominator + */ +export function getUnitString(statUnit: string, denomDcid?: string): string { + let unit = statUnit; + if (unit && denomDcid && denomDcid.includes("_Person")) { + unit += "per person"; + } + return unit; +}