diff --git a/CHANGELOG.md b/CHANGELOG.md index 72e4718..4a48c48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [1.5.0] - 2024-12-10 + +### Changed +- Changed all 'grey' word of the site into 'gray' to accommodate US language practice [#341](https://github.com/policy-design-lab/pdl-frontend/issues/341) +- Changed comma-handled multi-practice selecting query to delimiter-handled query for EQIP and CSP total page selector [#344](https://github.com/policy-design-lab/pdl-frontend/issues/344) + +### Fixed +- Fixed the bug where the map tips lagged in rendering HTML code and processing styles on the EQIP and CSO total page [#345](https://github.com/policy-design-lab/pdl-frontend/issues/345) + ## [1.4.0] - 2024-11-26 ### Added @@ -310,6 +319,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Map data json [#12](https://github.com/policy-design-lab/pdl-frontend/issues/12) - Final landing page changes for initial milestone [#15](https://github.com/policy-design-lab/pdl-frontend/issues/15) +[1.5.0]: https://github.com/policy-design-lab/pdl-frontend/compare/1.4.0...1.5.0 [1.4.0]: https://github.com/policy-design-lab/pdl-frontend/compare/1.3.0...1.4.0 [1.3.0]: https://github.com/policy-design-lab/pdl-frontend/compare/1.2.0...1.3.0 [1.2.0]: https://github.com/policy-design-lab/pdl-frontend/compare/1.1.0...1.2.0 diff --git a/package-lock.json b/package-lock.json index 1d86614..569962a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "policy-design-lab", - "version": "1.4.0", + "version": "1.5.0", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index 61b4be5..5434c85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "policy-design-lab", - "version": "1.4.0", + "version": "1.5.0", "description": "the front end of policy design lab", "repository": "https://github.com/policy-design-lab/pdl-frontend", "main": "src/app.tsx", diff --git a/src/components/acep/ACEPTotalMap.tsx b/src/components/acep/ACEPTotalMap.tsx index 09f61b7..16e5a42 100644 --- a/src/components/acep/ACEPTotalMap.tsx +++ b/src/components/acep/ACEPTotalMap.tsx @@ -235,7 +235,7 @@ const titleElement = (attribute, year): JSX.Element => { {attribute} Benefits from {year} {" "} - In any state that appears in grey, there is no available data + In any state that appears in gray, there is no available data ); diff --git a/src/components/crp/CRPTotalMap.tsx b/src/components/crp/CRPTotalMap.tsx index 318cd25..80e9142 100644 --- a/src/components/crp/CRPTotalMap.tsx +++ b/src/components/crp/CRPTotalMap.tsx @@ -242,7 +242,7 @@ const titleElement = (attribute, year): JSX.Element => { {attribute} Benefits from {year} {" "} - In any state that appears in grey, there is no available data + In any state that appears in gray, there is no available data ); diff --git a/src/components/crp/CategoryMap.tsx b/src/components/crp/CategoryMap.tsx index aac0021..4b59963 100644 --- a/src/components/crp/CategoryMap.tsx +++ b/src/components/crp/CategoryMap.tsx @@ -316,7 +316,7 @@ const titleElement = (attribute, year): JSX.Element => { {attribute} Benefits from {year} {" "} - In any state that appears in grey, there is no available data + In any state that appears in gray, there is no available data ); diff --git a/src/components/csp/CategoryMap.tsx b/src/components/csp/CategoryMap.tsx index 0f26251..a5dad95 100644 --- a/src/components/csp/CategoryMap.tsx +++ b/src/components/csp/CategoryMap.tsx @@ -290,7 +290,7 @@ const titleElement = (attribute, year): JSX.Element => { {attribute} Benefits from {year} {" "} - In any state that appears in grey, there is no available data + In any state that appears in gray, there is no available data ); diff --git a/src/components/eqip/CategoryMap.tsx b/src/components/eqip/CategoryMap.tsx index 082823a..7743329 100644 --- a/src/components/eqip/CategoryMap.tsx +++ b/src/components/eqip/CategoryMap.tsx @@ -236,7 +236,7 @@ const titleElement = (attribute, year): JSX.Element => { {attribute} Benefits from {year} {" "} - In any state that appears in grey, there is no available data + In any state that appears in gray, there is no available data ); diff --git a/src/components/ira/IRADollarMap.tsx b/src/components/ira/IRADollarMap.tsx index bde90a3..d3390b1 100644 --- a/src/components/ira/IRADollarMap.tsx +++ b/src/components/ira/IRADollarMap.tsx @@ -493,7 +493,7 @@ const IRADollarMap = ({ thresholds.push(nonZeroData[adjustedIndex]); } const colorScale = d3.scaleThreshold().domain(thresholds).range(mapColor); - // For IRA, only if all practices are zero, the state will be colored as grey + // For IRA, only if all practices are zero, the state will be colored as gray let zeroPoints = []; statePerformance[year].forEach((state) => { if (practices[0] === "Total") { diff --git a/src/components/ira/IRAPredictedMap.tsx b/src/components/ira/IRAPredictedMap.tsx index 8ddb30b..a043e8a 100644 --- a/src/components/ira/IRAPredictedMap.tsx +++ b/src/components/ira/IRAPredictedMap.tsx @@ -330,7 +330,7 @@ const IRAPredictedMap = ({ thresholds.push(nonZeroData[adjustedIndex]); } const colorScale = d3.scaleThreshold().domain(thresholds).range(mapColor); - // For IRA, only if all practices are zero, the state will be colored as grey + // For IRA, only if all practices are zero, the state will be colored as gray let zeroPoints = []; predictedPerformance[year].forEach((state) => { if (practices[0] === "Total") { diff --git a/src/components/rcpp/RCPPTotalMap.tsx b/src/components/rcpp/RCPPTotalMap.tsx index 03a43cf..47a5e9f 100644 --- a/src/components/rcpp/RCPPTotalMap.tsx +++ b/src/components/rcpp/RCPPTotalMap.tsx @@ -243,7 +243,7 @@ const titleElement = (attribute, year): JSX.Element => { {attribute} Benefits from {year} {" "} - In any state that appears in grey, there is no available data + In any state that appears in gray, there is no available data ); diff --git a/src/components/shared/DrawLegend.tsx b/src/components/shared/DrawLegend.tsx index 7421014..b0efe2f 100644 --- a/src/components/shared/DrawLegend.tsx +++ b/src/components/shared/DrawLegend.tsx @@ -164,7 +164,7 @@ export default function DrawLegend({ .attr("class", "legendTextSide") .attr("x", -1000) .attr("y", -1000) - .text("In any state that appears in grey, there is no available data"); + .text("In any state that appears in gray, there is no available data"); const middleBox = middleText.node().getBBox(); middleText.remove(); baseSVG @@ -172,7 +172,7 @@ export default function DrawLegend({ .attr("class", "legendTextSide") .attr("x", (svgWidth + margin * 2) / 2 - middleBox.width / 2) .attr("y", 80) - .text("In any state that appears in grey, there is no available data"); + .text("In any state that appears in gray, there is no available data"); } } else { baseSVG.attr("height", 40); diff --git a/src/components/shared/titleii/TitleIIPracticeMap.tsx b/src/components/shared/titleii/TitleIIPracticeMap.tsx index 7ce3fd8..e07c530 100644 --- a/src/components/shared/titleii/TitleIIPracticeMap.tsx +++ b/src/components/shared/titleii/TitleIIPracticeMap.tsx @@ -1,4 +1,5 @@ import React, { useState, useMemo } from "react"; +import { CircularProgress } from "@mui/material"; import { geoCentroid } from "d3-geo"; import { ComposableMap, Geographies, Geography, Marker, Annotation } from "react-simple-maps"; import ReactTooltip from "react-tooltip"; @@ -48,11 +49,22 @@ const MapChart = ({ year, stateCodes, colorScale, - selectedPractices + selectedPractices, + classes }) => { - const classes = useStyles(); + const handleMouseEnter = React.useCallback( + (geo, record) => { + const tooltipContent = computeTooltipContent(geo, record, selectedPractices, classes, getNationalTotal); + setReactTooltipContent(tooltipContent); + }, + [selectedPractices, classes, getNationalTotal] + ); + const handleMouseLeave = React.useCallback(() => { + setReactTooltipContent(""); + }, []); + return ( -
+
{({ geographies }) => ( @@ -73,18 +85,8 @@ const MapChart = ({ { - const tooltipContent = computeTooltipContent( - geo, - record, - selectedPractices, - classes, - getNationalTotal - ); - ReactTooltip.rebuild(); - setReactTooltipContent(tooltipContent); - }} - onMouseLeave={() => setReactTooltipContent("")} + onMouseEnter={() => handleMouseEnter(geo, record)} + onMouseLeave={handleMouseLeave} fill={colorScale(practiceTotal || 0) || "#D2D2D2"} stroke="#FFF" style={{ @@ -130,20 +132,6 @@ const MapChart = ({
); }; - -function encodeQueryParams(params) { - return Object.entries(params) - .map(([key, value]) => { - const encodedValue = encodeURIComponent(value); - return `${key}=${encodedValue}`; - }) - .join("&"); -} - -function buildURL(baseUrl, params) { - const encodedParams = encodeQueryParams(params); - return `${baseUrl}?${encodedParams}`; -} const TitleIIPracticeMap = ({ programName, initialStatePerformance, @@ -158,6 +146,38 @@ const TitleIIPracticeMap = ({ const classes = useStyles(); const [selectedPractices, setSelectedPractices] = useState(["All Practices"]); const [isLoading, setIsLoading] = useState(false); + const [tooltipVisible, setTooltipVisible] = useState(false); + // help tooltip style to load without lag + React.useEffect(() => { + if (content) { + ReactTooltip.hide(); + ReactTooltip.rebuild(); + setTooltipVisible(true); + } else { + setTooltipVisible(false); + } + }, [content]); + React.useEffect(() => { + const style = document.createElement("style"); + style.innerHTML = ` + .__react_component_tooltip { + padding: 0 !important; + margin: 0 !important; + opacity: 1 !important; + } + .__react_component_tooltip.show { + opacity: 1 !important; + } + `; + document.head.appendChild(style); + return () => { + ReactTooltip.hide(); + document.head.removeChild(style); + }; + }, []); + const setTooltipContent = React.useCallback((newContent) => { + setContent(newContent); + }, []); const practiceCategories = useMemo(() => { return getPracticeCategories(practiceNames); @@ -183,7 +203,7 @@ const TitleIIPracticeMap = ({ setStatePerformance(initialStatePerformance); return; } - const encodedCodes = selectedCodes.map((code) => encodeURIComponent(code)).join(","); + const encodedCodes = selectedCodes.map((code) => encodeURIComponent(code)).join("|"); const url = `${ config.apiUrl }/titles/title-ii/programs/${programName.toLowerCase()}/state-distribution?practice_code=${encodedCodes}`; @@ -214,11 +234,6 @@ const TitleIIPracticeMap = ({ } fetchStatePerformanceData(newSelected); }; - - React.useEffect(() => { - ReactTooltip.rebuild(); - }, [statePerformance, selectedPractices]); - const practiceData = useMemo(() => { return getPracticeData(statePerformance, year, selectedPractices); }, [statePerformance, year, selectedPractices]); @@ -247,6 +262,38 @@ const TitleIIPracticeMap = ({ } fetchStatePerformanceData(newSelected); }; + const tooltipComponent = useMemo( + () => ( + (tooltipVisible ? content : null)} + overridePosition={({ left, top }, _e, _t, node) => { + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + if (!node) return { left, top }; + const tooltipRect = node.getBoundingClientRect(); + + let updatedLeft = left; + let updatedTop = top; + + if (left + tooltipRect.width > viewportWidth) { + updatedLeft = viewportWidth - tooltipRect.width - 10; + } + if (top + tooltipRect.height > viewportHeight) { + updatedTop = viewportHeight - tooltipRect.height - 10; + } + + return { left: updatedLeft, top: updatedTop }; + }} + /> + ), + [classes, content, tooltipVisible] + ); + const shouldShowLoading = isLoading && !selectedPractices.includes("All Practices"); const hasValidData = statePerformance && statePerformance[year] && statePerformance[year].length > 0; if (!hasValidData && !shouldShowLoading) { @@ -349,32 +396,27 @@ const TitleIIPracticeMap = ({ {shouldShowLoading ? ( - Loading data... + ) : ( hasValidData && ( - + <> + + {tooltipComponent} + ) )} -
- - {content} - -
); }; @@ -384,11 +426,11 @@ const titleElement = (programName, practices: string[], year: string): JSX.Eleme return ( - {practiceLabel === "All Practices" ? `Total ${programName}` : practiceLabel} Benefits - from {year} + {practiceLabel === "All Practices" ? `Total ${programName}` : "Selected Practices"}{" "} + Benefits from {year} - Grey states indicate no available data + Gray states indicate no available data ); diff --git a/src/components/shared/titleii/TitleIIPracticeTable.tsx b/src/components/shared/titleii/TitleIIPracticeTable.tsx index ecd8f31..a0c6d73 100644 --- a/src/components/shared/titleii/TitleIIPracticeTable.tsx +++ b/src/components/shared/titleii/TitleIIPracticeTable.tsx @@ -180,83 +180,97 @@ function Table({ programName, columns, data }) { - {headerGroups.map((headerGroup) => ( - - - {headerGroup.headers - .filter((_, index) => visibleColumnIndices.includes(index)) - .map((column) => ( - + + {headerGroup.headers + .filter((_, index) => visibleColumnIndices.includes(index)) + .map((column) => ( + - ))} - - ))} + })()} + + + ))} + + ); + })} - {page.map((row, i) => { + {page.map((row) => { prepareRow(row); + const rowProps = row.getRowProps(); return ( - +
- {headerGroup.headers[0].render("Header")} - - {(() => { - const column = headerGroup.headers[0]; - if (!column.isSorted) - return ( - - - - ); - if (column.isSortedDesc) - return {"\u{25BC}"}; - return {"\u{25B2}"}; - })()} - - + {headerGroups.map((headerGroup) => { + const headerGroupProps = headerGroup.getHeaderGroupProps(); + return ( +
+ {headerGroup.headers[0].render("Header")} + {(() => { - const headerText = column.render("Header"); - if (typeof headerText === "string" && headerText.includes(":")) { - const [beforeColon, afterColon] = headerText.split(":"); + const column = headerGroup.headers[0]; + if (!column.isSorted) + return ( + + + + ); + if (column.isSortedDesc) return ( - <> -
- {beforeColon} -
- {afterColon.trim()} - + {"\u{25BC}"} ); - } - return headerText; + return {"\u{25B2}"}; })()} - + +
{(() => { - if (!column.isSorted) + const headerText = column.render("Header"); + if ( + typeof headerText === "string" && + headerText.includes(":") + ) { + const [beforeColon, afterColon] = headerText.split(":"); return ( - - - + <> +
+ {beforeColon} +
+ {afterColon.trim()} + ); - if (column.isSortedDesc) + } + return headerText; + })()} + + {(() => { + if (!column.isSorted) + return ( + + + + ); + if (column.isSortedDesc) + return ( + + {"\u{25BC}"} + + ); return ( - {"\u{25BC}"} + {"\u{25B2}"} ); - return ( - {"\u{25B2}"} - ); - })()} - -
{row.cells .filter((_, index) => visibleColumnIndices.includes(index)) - .map((cell, j) => ( + .map((cell, cellIndex) => ( @@ -329,7 +343,7 @@ function Table({ programName, columns, data }) { }} > {[10, 25, 40, 51].map((size) => ( - ))}