From edfc9b9e60c39d16154078d9e69cd3cc4047d722 Mon Sep 17 00:00:00 2001 From: "Wendy(Pengyin) Shan" Date: Mon, 18 Nov 2024 12:43:29 -0600 Subject: [PATCH 01/16] update eqip selector to fit the new api --- src/components/eqip/EQIPPracticeMap.tsx | 514 ++++++++++++++++++ src/components/eqip/EQIPPracticeTable.tsx | 438 +++++++++++++++ .../shared/titleii/GetPracticeTotal.tsx | 31 ++ src/pages/EQIPPage.tsx | 83 +-- 4 files changed, 1033 insertions(+), 33 deletions(-) create mode 100644 src/components/eqip/EQIPPracticeMap.tsx create mode 100644 src/components/eqip/EQIPPracticeTable.tsx create mode 100644 src/components/shared/titleii/GetPracticeTotal.tsx diff --git a/src/components/eqip/EQIPPracticeMap.tsx b/src/components/eqip/EQIPPracticeMap.tsx new file mode 100644 index 0000000..5eb430e --- /dev/null +++ b/src/components/eqip/EQIPPracticeMap.tsx @@ -0,0 +1,514 @@ +import React, { useState, useMemo } from "react"; +import { geoCentroid } from "d3-geo"; +import { ComposableMap, Geographies, Geography, Marker, Annotation } from "react-simple-maps"; +import ReactTooltip from "react-tooltip"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import FormControl from "@mui/material/FormControl"; +import FormLabel from "@mui/material/FormLabel"; +import Select from "@mui/material/Select"; +import MenuItem from "@mui/material/MenuItem"; +import Chip from "@mui/material/Chip"; +import * as d3 from "d3"; +import { config } from "../../app.config"; +import { useStyles, tooltipBkgColor } from "../shared/MapTooltip"; +import "../../styles/map.css"; +import legendConfig from "../../utils/legendConfig.json"; +import DrawLegend from "../shared/DrawLegend"; +import getPracticeTotal from "../shared/titleii/GetPracticeTotal"; +import { ShortFormat } from "../shared/ConvertionFormats"; + +const geoUrl = "https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json"; + +const offsets = { + VT: [50, -8], + NH: [34, 2], + MA: [30, -1], + RI: [28, 2], + CT: [35, 10], + NJ: [34, 1], + DE: [33, 0], + MD: [47, 10], + DC: [49, 21] +}; + +const MapChart = ({ + getNationalTotal, + setReactTooltipContent, + maxValue, + allStates, + statePerformance, + year, + stateCodes, + colorScale, + selectedPractices +}) => { + const classes = useStyles(); + const computeTooltipContent = (geo, record) => { + if (!record) return ""; + let tooltipContent = ` +
+
+ ${geo.properties.name} +
+ + `; + let practiceTotal = 0; + if (selectedPractices.includes("All Practices")) { + practiceTotal = record.totalPaymentInDollars || 0; + tooltipContent += ` + + + + `; + } else { + selectedPractices.forEach((practice, index) => { + const practiceAmount = getPracticeTotal(record, practice); + practiceTotal += practiceAmount; + const displayName = practice.replace(/\s*\(\d+\)$/, ""); + tooltipContent += ` + + + + `; + }); + } + const nationalTotal = getNationalTotal(selectedPractices); + const totalPercentage = nationalTotal > 0 ? (practiceTotal / nationalTotal) * 100 : 0; + tooltipContent += ` + + + + + + + + + +
All Practices: + $${ShortFormat(practiceTotal, undefined, 2)} +
+ ${displayName}: + + $${ShortFormat(practiceAmount, undefined, 2)} +
PCT. Nationwide: + ${totalPercentage > 0 ? `${totalPercentage.toFixed(2)}%` : "0%"} +
+
`; + return tooltipContent; + }; + + return ( +
+ + + {({ geographies }) => ( + <> + {geographies.map((geo) => { + const record = statePerformance[year]?.find( + (v) => stateCodes[v.state] === geo.properties.name + ); + let practiceTotal = 0; + if (selectedPractices.includes("All Practices")) { + practiceTotal = record?.totalPaymentInDollars || 0; + } else { + selectedPractices.forEach((practice) => { + practiceTotal += getPracticeTotal(record, practice); + }); + } + return ( + { + const tooltipContent = computeTooltipContent(geo, record); + ReactTooltip.rebuild(); + setReactTooltipContent(tooltipContent); + }} + onMouseLeave={() => setReactTooltipContent("")} + fill={colorScale(practiceTotal || 0) || "#D2D2D2"} + stroke="#FFF" + style={{ + default: { stroke: "#FFFFFF", strokeWidth: 0.75, outline: "none" }, + hover: { stroke: "#232323", strokeWidth: 2, outline: "none" }, + pressed: { fill: "#345feb", outline: "none" } + }} + /> + ); + })} + {geographies.map((geo) => { + const centroid = geoCentroid(geo); + const cur = allStates.find((s) => s.val === geo.id); + return ( + + {cur && + centroid[0] > -160 && + centroid[0] < -67 && + (Object.keys(offsets).indexOf(cur.id) === -1 ? ( + + + {cur.id} + + + ) : ( + + + {cur.id} + + + ))} + + ); + })} + + )} + + +
+ ); +}; + +interface Practice { + practiceName: string; + practiceCode: string; + totalPaymentInDollars: number; +} + +interface PracticeCategory { + practiceCategoryName: string; + totalPaymentInDollars: number; + practices: Practice[]; +} + +interface StatePerformance { + state: string; + totalPaymentInDollars: number; + statutes: Array<{ + statuteName: string; + totalPaymentInDollars: number; + practiceCategories: PracticeCategory[]; + }>; +} + +interface EQIPPracticeMapProps { + initialStatePerformance: Record; + allStates: any; + year: string; + stateCodes: any; + practiceNames: Array<{ + practiceName: string; + practiceCode: string; + }>; + onPracticeChange?: (practices: string[]) => void; +} +const EQIPPracticeMap = ({ + initialStatePerformance, + allStates, + year, + stateCodes, + practiceNames, + onPracticeChange +}: EQIPPracticeMapProps) => { + const [content, setContent] = useState(""); + const [statePerformance, setStatePerformance] = useState(initialStatePerformance); + const classes = useStyles(); + const [selectedPractices, setSelectedPractices] = useState(["All Practices"]); + const [isLoading, setIsLoading] = useState(false); + + const practiceCategories = useMemo(() => { + if (!practiceNames || practiceNames.length === 0) { + return ["All Practices"]; + } + return ["All Practices", ...practiceNames]; + }, [practiceNames]); + const fetchStatePerformanceData = async (selectedPracticeNames: string[]) => { + if (selectedPracticeNames.includes("All Practices")) { + setStatePerformance(initialStatePerformance); + return; + } + setIsLoading(true); + try { + const selectedCodes = selectedPracticeNames + .map((practiceName) => { + const match = practiceName.match(/\((\d+)\)$/); + return match ? match[1] : null; + }) + .filter((code) => code !== null); + if (selectedCodes.length === 0) { + setStatePerformance(initialStatePerformance); + return; + } + const url = `${ + config.apiUrl + }/titles/title-ii/programs/eqip/state-distribution?practice_code=${selectedCodes.join(",")}`; + const response = await fetch(url); + const data = await response.json(); + setStatePerformance(data); + } catch (error) { + console.error("Error fetching state performance data:", error); + setStatePerformance(initialStatePerformance); + } finally { + setIsLoading(false); + } + }; + const handlePracticeChange = (event: any) => { + const value = event.target.value; + let newSelected = typeof value === "string" ? value.split(",") : value; + if (newSelected.includes("All Practices")) { + newSelected = + newSelected.length === 1 ? ["All Practices"] : newSelected.filter((p) => p !== "All Practices"); + } + if (newSelected.length === 0) { + newSelected = ["All Practices"]; + } + setSelectedPractices(newSelected); + if (onPracticeChange) { + onPracticeChange(newSelected); + } + fetchStatePerformanceData(newSelected); + }; + + React.useEffect(() => { + ReactTooltip.rebuild(); + }, [statePerformance, selectedPractices]); + + const practiceData = useMemo(() => { + if (!statePerformance[year]) return []; + return statePerformance[year] + .map((state) => { + let total = 0; + if (selectedPractices.includes("All Practices")) { + total = state.totalPaymentInDollars || 0; + } else { + state.statutes?.forEach((statute) => { + statute.practiceCategories?.forEach((category) => { + selectedPractices.forEach((practice) => { + const codeMatch = practice.match(/\((\d+)\)$/); + const practiceCode = codeMatch ? codeMatch[1] : null; + + if (!practiceCode) return; + + if (!category.practices || category.practices.length === 0) { + if (practice.includes(category.practiceCategoryName)) { + total += category.totalPaymentInDollars || 0; + } + } else { + category.practices.forEach((p) => { + const match = p.practiceName?.match(/\((\d+)\)$/)?.[1]; + if (match === practiceCode) { + total += p.totalPaymentInDollars || 0; + } + }); + } + }); + }); + }); + } + return total; + }) + .filter((value) => value > 0); + }, [statePerformance, year, selectedPractices]); + const maxValue = useMemo(() => Math.max(...practiceData, 0), [practiceData]); + const customScale = useMemo(() => { + if (practiceData.length === 0) return legendConfig["Total EQIP"]; + + const sortedData = [...practiceData].sort((a, b) => a - b); + const quintileSize = Math.ceil(sortedData.length / 5); + + return [ + sortedData[quintileSize], + sortedData[quintileSize * 2], + sortedData[quintileSize * 3], + sortedData[quintileSize * 4] + ]; + }, [practiceData]); + const mapColor = ["#F0F9E8", "#BAE4BC", "#7BCCC4", "#43A2CA", "#0868AC"]; + const colorScale = d3.scaleThreshold(customScale, mapColor); + const getNationalTotal = React.useCallback( + (practices: string[]) => { + let total = 0; + if (!statePerformance[year]) return total; + + statePerformance[year].forEach((state) => { + if (practices.includes("All Practices")) { + total += state.totalPaymentInDollars || 0; + } else { + practices.forEach((practice) => { + const codeMatch = practice.match(/\((\d+)\)$/); + const practiceCode = codeMatch ? codeMatch[1] : null; + + if (!practiceCode) return; + + state.statutes?.forEach((statute) => { + statute.practiceCategories?.forEach((category) => { + if (!category.practices || category.practices.length === 0) { + if (practice.includes(category.practiceCategoryName)) { + total += category.totalPaymentInDollars || 0; + } + } else { + category.practices.forEach((p) => { + const match = p.practiceName?.match(/\((\d+)\)$/)?.[1]; + if (match === practiceCode) { + total += p.totalPaymentInDollars || 0; + } + }); + } + }); + }); + }); + } + }); + return total; + }, + [statePerformance, year] + ); + const handleChipDelete = (value: string) => { + const newSelected = selectedPractices.filter((p) => p !== value); + const finalSelected = newSelected.length === 0 ? ["All Practices"] : newSelected; + setSelectedPractices(finalSelected); + if (onPracticeChange) { + onPracticeChange(finalSelected); + } + fetchStatePerformanceData(finalSelected); + }; + const shouldShowLoading = isLoading && !selectedPractices.includes("All Practices"); + const hasValidData = statePerformance && statePerformance[year] && statePerformance[year].length > 0; + if (!hasValidData && !shouldShowLoading) { + return ( + + No data available + + ); + } + return ( + + + {maxValue !== 0 ? ( + + ) : ( +
+ {titleElement(selectedPractices, year)} + + + Please select at least one practice category. + + +
+ )} +
+ + + + Select Practice + + + + + + {shouldShowLoading ? ( + + Loading data... + + ) : ( + hasValidData && ( + + ) + )} +
+ + {content} + +
+
+ ); +}; + +const titleElement = (practices: string[], year: string): JSX.Element => { + const practiceLabel = practices.length > 0 ? practices.join(", ") : "No practices selected"; + return ( + + + {practiceLabel === "All Practices" ? "Total EQIP" : practiceLabel} Benefits from{" "} + {year} + + + Grey states indicate no available data + + + ); +}; + +export default EQIPPracticeMap; diff --git a/src/components/eqip/EQIPPracticeTable.tsx b/src/components/eqip/EQIPPracticeTable.tsx new file mode 100644 index 0000000..ad09e54 --- /dev/null +++ b/src/components/eqip/EQIPPracticeTable.tsx @@ -0,0 +1,438 @@ +import React from "react"; +import styled from "styled-components"; +import { CSVLink } from "react-csv"; +import { useTable, useSortBy, usePagination } from "react-table"; +import SwapVertIcon from "@mui/icons-material/SwapVert"; +import { Grid, TableContainer, Typography, Box } from "@mui/material"; +import { compareWithDollarSign, compareWithPercentSign } from "../shared/TableCompareFunctions"; +import getPracticeTotal from "../shared/titleii/GetPracticeTotal"; +import "../../styles/table.css"; +import getCSVData from "../shared/getCSVData"; + +const Styles = styled.div` + padding: 0; + margin: 0; + + table { + border-spacing: 0; + border: 1px solid #e4ebe7; + border-left: none; + border-right: none; + min-width: 100%; + overflow-x: auto; + + tr { + :last-child { + td { + border-bottom: 0; + } + } + } + + th { + background-color: rgba(241, 241, 241, 1); + padding: 1em 3em; + cursor: pointer; + text-align: left; + } + + th:not(:first-of-type) { + text-align: right; + } + + td[class$="cell0"] { + padding-right: 10em; + } + + td[class$="cell1"], + td[class$="cell2"], + td[class$="cell3"], + td[class$="cell4"], + td[class$="cell5"], + td[class$="cell6"] { + text-align: right; + } + + td { + padding: 1em 3em; + border-bottom: 1px solid #e4ebe7; + border-right: none; + + :last-child { + border-right: 0; + } + } + } + + .pagination { + margin-top: 1.5em; + } + + @media screen and (max-width: 1024px) { + th, + td { + padding: 8px; + } + td[class$="cell0"] { + padding-right: 1em; + } + .pagination { + margin-top: 8px; + } + } + + .downloadbtn { + background-color: rgba(47, 113, 100, 1); + padding: 8px 16px; + border-radius: 4px; + color: #fff; + text-decoration: none; + display: block; + cursor: pointer; + margin-bottom: 1em; + text-align: center; + } +`; + +function Table({ columns, data, initialState }) { + const state = React.useMemo(() => initialState, []); + const { + getTableProps, + getTableBodyProps, + headerGroups, + rows, + prepareRow, + page, + canPreviousPage, + canNextPage, + pageOptions, + pageCount, + gotoPage, + nextPage, + previousPage, + setPageSize, + state: { pageIndex, pageSize } + } = useTable( + { + columns, + data, + initialState: { pageSize: 10 } + }, + useSortBy, + usePagination + ); + + return ( +
+ {data && data.length > 0 ? ( +
+ + Export This Table to CSV + + + + + {headerGroups.map((headerGroup) => ( + + {headerGroup.headers.map((column) => ( + + ))} + + ))} + + + {page.map((row, i) => { + prepareRow(row); + return ( + + {row.cells.map((cell, j) => ( + + ))} + + ); + })} + +
+ {(() => { + const headerText = column.render("Header"); + if (typeof headerText === "string" && headerText.includes(":")) { + const [beforeColon, afterColon] = headerText.split(":"); + return ( + <> +
+ {beforeColon} +
+ {afterColon.trim()} + + ); + } + return headerText; + })()} + + {(() => { + if (!column.isSorted) + return ( + + + + ); + if (column.isSortedDesc) + return ( + {"\u{25BC}"} + ); + return {"\u{25B2}"}; + })()} + +
+ {cell.render("Cell")} +
+ + + + {" "} + {" "} + {" "} + {" "} + + Page{" "} + + {pageIndex + 1} of {pageOptions.length} + {" "} + + + | Go to page:{" "} + { + let p = e.target.value ? Number(e.target.value) - 1 : 0; + if (p > pageOptions.length) p = pageOptions.length - 1; + if (p < 0) p = 0; + gotoPage(p); + }} + style={{ width: "3em" }} + /> + {" "} + + + + {pageSize * (pageIndex + 1) <= rows.length ? ( + + Showing the first {pageSize * (pageIndex + 1)} results of {rows.length} rows + + ) : ( + + Showing the first {rows.length} results of {rows.length} rows + + )} + + +
+ ) : ( +

Loading data...

+ )} +
+ ); +} + +function EQIPPracticeTable({ + statePerformance, + year, + stateCodes, + selectedPractices +}: { + statePerformance: any; + year: string; + stateCodes: any; + selectedPractices: string[]; +}): JSX.Element { + const getNationalTotalForPractice = React.useCallback( + (practice: string) => { + let total = 0; + if (!statePerformance[year]) return total; + + statePerformance[year].forEach((state) => { + total += getPracticeTotal(state, practice); + }); + return total; + }, + [statePerformance, year] + ); + const getNationalTotal = React.useCallback( + (practices: string[]) => { + if (practices.includes("All Practices")) { + let total = 0; + statePerformance[year]?.forEach((state) => { + total += state.totalPaymentInDollars || 0; + }); + return total; + } + let total = 0; + practices.forEach((practice) => { + total += getNationalTotalForPractice(practice); + }); + return total; + }, + [statePerformance, year, getNationalTotalForPractice] + ); + + const resultData = React.useMemo(() => { + if (!statePerformance[year]) return []; + + return statePerformance[year].map((stateData) => { + const stateCode = stateCodes.find((obj) => obj.code === stateData.state); + const stateName = stateCode ? stateCode.name : stateData.state; + const row: any = { + state: stateName + }; + let totalBenefits = 0; + if (selectedPractices.includes("All Practices")) { + totalBenefits = stateData.totalPaymentInDollars || 0; + const nationalTotal = getNationalTotal(["All Practices"]); + const totalPercentage = (totalBenefits / nationalTotal) * 100; + row["Total Benefits"] = `$${totalBenefits.toLocaleString(undefined, { minimumFractionDigits: 2 })}`; + row["Percentage Nationwide"] = `${totalPercentage.toFixed(2)}%`; + } else { + selectedPractices.forEach((practice) => { + const practiceBenefits = getPracticeTotal(stateData, practice); + totalBenefits += practiceBenefits; + const practiceNationalTotal = getNationalTotalForPractice(practice); + const practicePercentage = + practiceNationalTotal > 0 ? (practiceBenefits / practiceNationalTotal) * 100 : 0; + + const displayName = practice.replace(/\s*\(\d+\)$/, ""); + row[`${displayName}: Benefits`] = `$${practiceBenefits.toLocaleString(undefined, { + minimumFractionDigits: 2 + })}`; + row[`${displayName}: Percentage Nationwide`] = `${practicePercentage.toFixed(2)}%`; + }); + const totalNational = getNationalTotal(selectedPractices); + const totalPercentage = totalNational > 0 ? (totalBenefits / totalNational) * 100 : 0; + row["Total Benefits"] = `$${totalBenefits.toLocaleString(undefined, { minimumFractionDigits: 2 })}`; + row["Percentage Nationwide"] = `${totalPercentage.toFixed(2)}%`; + } + return row; + }); + }, [statePerformance, year, stateCodes, selectedPractices, getNationalTotal, getNationalTotalForPractice]); + + const columns = React.useMemo(() => { + const cols = [ + { + Header: "STATE", + accessor: "state" + } + ]; + if (selectedPractices.includes("All Practices")) { + cols.push( + { + Header: "Total Benefits", + accessor: "Total Benefits", + sortType: compareWithDollarSign + }, + { + Header: "Pct. Nationwide", + accessor: "Percentage Nationwide", + sortType: compareWithPercentSign + } + ); + } else { + cols.push( + { + Header: "Total Benefits", + accessor: "Total Benefits", + sortType: compareWithDollarSign + }, + { + Header: "Pct. Nationwide", + accessor: "Percentage Nationwide", + sortType: compareWithPercentSign + } + ); + selectedPractices.forEach((practice) => { + const displayName = practice.replace(/\s*\(\d+\)$/, ""); + cols.push( + { + Header: `${displayName}: Benefits`, + accessor: `${displayName}: Benefits`, + sortType: compareWithDollarSign + }, + { + Header: `${displayName}: Pct. Nationwide`, + accessor: `${displayName}: Percentage Nationwide`, + sortType: compareWithPercentSign + } + ); + }); + } + + return cols; + }, [selectedPractices]); + + return ( + + + + + + + EQIP Practice Benefits by State + + + + + + + + + + ); +} +export default EQIPPracticeTable; diff --git a/src/components/shared/titleii/GetPracticeTotal.tsx b/src/components/shared/titleii/GetPracticeTotal.tsx new file mode 100644 index 0000000..2d34cba --- /dev/null +++ b/src/components/shared/titleii/GetPracticeTotal.tsx @@ -0,0 +1,31 @@ +const getPracticeTotal = (record, practiceName) => { + if (!record || !record.statutes) return 0; + if (practiceName === "All Practices") { + return record.totalPaymentInDollars || 0; + } + const codeMatch = practiceName.match(/\((\d+)\)$/); + const practiceCode = codeMatch ? codeMatch[1] : null; + const practiceName_noCode = practiceName.replace(/\s*\(\d+\)$/, ""); + let total = 0; + record.statutes?.forEach((statute) => { + if (!statute.practiceCategories) return; + statute.practiceCategories.forEach((category) => { + if (practiceName_noCode === category.practiceCategoryName) { + total += category.totalPaymentInDollars || 0; + return; + } + + if (practiceCode && Array.isArray(category.practices) && category.practices.length > 0) { + category.practices.forEach((practice) => { + const currentPracticeCode = practice.practiceName?.match(/\((\d+)\)$/)?.[1]; + if (currentPracticeCode === practiceCode) { + total += practice.totalPaymentInDollars || 0; + } + }); + } + }); + }); + return total; +}; + +export default getPracticeTotal; diff --git a/src/pages/EQIPPage.tsx b/src/pages/EQIPPage.tsx index f527419..9a02613 100644 --- a/src/pages/EQIPPage.tsx +++ b/src/pages/EQIPPage.tsx @@ -4,13 +4,18 @@ import { createTheme, ThemeProvider, Typography } from "@mui/material"; import NavBar from "../components/NavBar"; import Drawer from "../components/ProgramDrawer"; import SemiDonutChart from "../components/SemiDonutChart"; -import DataTable from "../components/eqip/EQIPTotalTable"; -import EqipTotalMap from "../components/eqip/EQIPTotalMap"; +import DataTable from "../components/eqip/EQIPPracticeTable"; import CategoryTable from "../components/eqip/CategoryTable"; import CategoryMap from "../components/eqip/CategoryMap"; import { config } from "../app.config"; import { convertAllState, getJsonDataFromUrl } from "../utils/apiutil"; import NavSearchBar from "../components/shared/NavSearchBar"; +import EQIPPracticeMap from "../components/eqip/EQIPPracticeMap"; + +interface PracticeName { + practiceName: string; + practiceCode: string; +} export default function EQIPPage(): JSX.Element { const [checked, setChecked] = React.useState(0); @@ -34,40 +39,52 @@ export default function EQIPPage(): JSX.Element { const [statePerformance, setStatePerformance] = React.useState({}); const [allStates, setAllStates] = React.useState({}); const [stateCodesData, setStateCodesData] = React.useState({}); - const [stateCodesArray, setStateCodesArray] = React.useState([]); + const [stateCodesArray, setStateCodesArray] = React.useState<{ code: string; name: string }[]>([]); const [totalChartData, setTotalChartData] = React.useState([{}]); const [sixAChartData, setSixAChartData] = React.useState([{}]); const [sixBChartData, setSixBChartData] = React.useState([{}]); const [aTotal, setATotal] = React.useState(0); const [bTotal, setBTotal] = React.useState(0); const [zeroCategories, setZeroCategories] = React.useState([]); + const [isDataReady, setIsDataReady] = React.useState(false); + // connect to selector endpoint + const [selectedPractices, setSelectedPractices] = React.useState(["All Practices"]); + const [eqipPracticeNames, setEqipPracticeNames] = React.useState([]); + const handlePracticeChange = (practices: string[]) => { + setSelectedPractices(practices); + }; const eqip_year = "2018-2022"; - React.useEffect(() => { - const state_perf_url = `${config.apiUrl}/titles/title-ii/programs/eqip/state-distribution`; - getJsonDataFromUrl(state_perf_url).then((response) => { - const converted_perf_json = response; - setStatePerformance(converted_perf_json); - }); - - const allstates_url = `${config.apiUrl}/states`; - getJsonDataFromUrl(allstates_url).then((response) => { - const converted_json = response; - setAllStates(converted_json); - }); - const statecode_url = `${config.apiUrl}/statecodes`; - getJsonDataFromUrl(statecode_url).then((response) => { - setStateCodesArray(response); - const converted_json = convertAllState(response); - setStateCodesData(converted_json); - }); - - const chartdata_url = `${config.apiUrl}/titles/title-ii/programs/eqip/summary`; - getJsonDataFromUrl(chartdata_url).then((response) => { - const converted_chart_json = response; - processData(converted_chart_json); - }); + React.useEffect(() => { + const fetchData = async () => { + try { + const [ + statePerformanceResponse, + allStatesResponse, + stateCodesResponse, + chartDataResponse, + eqipPracticeNameResponse + ] = await Promise.all([ + getJsonDataFromUrl(`${config.apiUrl}/titles/title-ii/programs/eqip/state-distribution`), + getJsonDataFromUrl(`${config.apiUrl}/states`), + getJsonDataFromUrl(`${config.apiUrl}/statecodes`), + getJsonDataFromUrl(`${config.apiUrl}/titles/title-ii/programs/eqip/summary`), + getJsonDataFromUrl(`${config.apiUrl}/titles/title-ii/programs/eqip/practice-names`) + ]); + setStatePerformance(statePerformanceResponse); + setAllStates(allStatesResponse); + setStateCodesArray(stateCodesResponse); + const converted_json = convertAllState(stateCodesResponse); + setStateCodesData(converted_json); + processData(chartDataResponse); + setEqipPracticeNames(eqipPracticeNameResponse); + setIsDataReady(true); + } catch (error) { + console.error("Error fetching data:", error); + } + }; + fetchData(); }, []); const processData = (chartData) => { @@ -158,10 +175,7 @@ export default function EQIPPage(): JSX.Element { return ( - {allStates.length > 0 && - statePerformance[eqip_year] !== undefined && - zeroCategories.length > 0 && - stateCodesArray.length > 0 ? ( + {isDataReady ? ( @@ -176,11 +190,13 @@ export default function EQIPPage(): JSX.Element { component="div" sx={{ width: "85%", m: "auto", display: checked !== 0 ? "none" : "block" }} > - From 379701e0536b906e99f876999038db1108d92151 Mon Sep 17 00:00:00 2001 From: "Wendy(Pengyin) Shan" Date: Mon, 18 Nov 2024 15:41:36 -0600 Subject: [PATCH 02/16] update menu style and enable All Practice to select back --- src/components/eqip/EQIPPracticeMap.tsx | 46 ++++++++++++++++++------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/src/components/eqip/EQIPPracticeMap.tsx b/src/components/eqip/EQIPPracticeMap.tsx index 5eb430e..bf3666b 100644 --- a/src/components/eqip/EQIPPracticeMap.tsx +++ b/src/components/eqip/EQIPPracticeMap.tsx @@ -257,16 +257,17 @@ const EQIPPracticeMap = ({ setIsLoading(false); } }; - const handlePracticeChange = (event: any) => { + const handlePracticeChange = (event) => { const value = event.target.value; let newSelected = typeof value === "string" ? value.split(",") : value; - if (newSelected.includes("All Practices")) { - newSelected = - newSelected.length === 1 ? ["All Practices"] : newSelected.filter((p) => p !== "All Practices"); - } - if (newSelected.length === 0) { + if (newSelected.includes("All Practices") && !selectedPractices.includes("All Practices")) { + newSelected = ["All Practices"]; + } else if (newSelected.length > 1 && newSelected.includes("All Practices")) { + newSelected = newSelected.filter((p) => p !== "All Practices"); + } else if (newSelected.length === 0) { newSelected = ["All Practices"]; } + setSelectedPractices(newSelected); if (onPracticeChange) { onPracticeChange(newSelected); @@ -334,7 +335,6 @@ const EQIPPracticeMap = ({ (practices: string[]) => { let total = 0; if (!statePerformance[year]) return total; - statePerformance[year].forEach((state) => { if (practices.includes("All Practices")) { total += state.totalPaymentInDollars || 0; @@ -368,14 +368,18 @@ const EQIPPracticeMap = ({ }, [statePerformance, year] ); - const handleChipDelete = (value: string) => { - const newSelected = selectedPractices.filter((p) => p !== value); - const finalSelected = newSelected.length === 0 ? ["All Practices"] : newSelected; - setSelectedPractices(finalSelected); + const handleChipDelete = (practiceToDelete) => { + let newSelected; + if (selectedPractices.length === 1) { + newSelected = ["All Practices"]; + } else { + newSelected = selectedPractices.filter((p) => p !== practiceToDelete); + } + setSelectedPractices(newSelected); if (onPracticeChange) { - onPracticeChange(finalSelected); + onPracticeChange(newSelected); } - fetchStatePerformanceData(finalSelected); + fetchStatePerformanceData(newSelected); }; const shouldShowLoading = isLoading && !selectedPractices.includes("All Practices"); const hasValidData = statePerformance && statePerformance[year] && statePerformance[year].length > 0; @@ -451,6 +455,22 @@ const EQIPPracticeMap = ({ )} sx={{ minWidth: 300 }} + MenuProps={{ + PaperProps: { + sx: { + maxHeight: 500, + overflowY: "auto", + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + border: "1px solid lightgray", + boxShadow: "0 4px 6px rgba(0,0,0,0.1)", + bgcolor: "background.paper" + // extra style for menu since the length of the list is long + } + } + }} > {practiceCategories.map((practice) => ( Date: Mon, 18 Nov 2024 15:59:56 -0600 Subject: [PATCH 03/16] seperate total columns and all practices columns in eqip table --- src/components/eqip/EQIPPracticeTable.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/eqip/EQIPPracticeTable.tsx b/src/components/eqip/EQIPPracticeTable.tsx index ad09e54..9c32024 100644 --- a/src/components/eqip/EQIPPracticeTable.tsx +++ b/src/components/eqip/EQIPPracticeTable.tsx @@ -356,12 +356,12 @@ function EQIPPracticeTable({ if (selectedPractices.includes("All Practices")) { cols.push( { - Header: "Total Benefits", + Header: "Total EQIP Benefits", accessor: "Total Benefits", sortType: compareWithDollarSign }, { - Header: "Pct. Nationwide", + Header: "EQIP Pct. Nationwide", accessor: "Percentage Nationwide", sortType: compareWithPercentSign } @@ -369,12 +369,12 @@ function EQIPPracticeTable({ } else { cols.push( { - Header: "Total Benefits", + Header: "Total Benefits for Selected Practices", accessor: "Total Benefits", sortType: compareWithDollarSign }, { - Header: "Pct. Nationwide", + Header: "Pct. Nationwide for Selected Practices", accessor: "Percentage Nationwide", sortType: compareWithPercentSign } From 70164af7b67c3b18cffdcd609fbc0035c5e44c23 Mon Sep 17 00:00:00 2001 From: "Wendy(Pengyin) Shan" Date: Thu, 21 Nov 2024 09:54:30 -0600 Subject: [PATCH 04/16] add CSP selector, optimize code to use shared component and eliminate some page warnings --- src/components/NavBar.tsx | 1 + src/components/SemiDonutChart.tsx | 4 +- src/components/csp/CSPTotalMap.tsx | 235 ------------------ src/components/csp/CSPTotalTable.tsx | 210 ---------------- src/components/eqip/EQIPTotalMap.tsx | 217 ---------------- src/components/eqip/EQIPTotalTable.tsx | 210 ---------------- .../shared/titleii/GetPracticeTotal.tsx | 31 --- src/components/shared/titleii/Interface.tsx | 39 +++ .../shared/titleii/PracticeMethods.tsx | 179 +++++++++++++ .../titleii/TitleIIPracticeMap.tsx} | 228 +++-------------- .../titleii/TitleIIPracticeTable.tsx} | 40 ++- src/pages/CSPPage.tsx | 74 +++--- src/pages/EQIPPage.tsx | 14 +- 13 files changed, 325 insertions(+), 1157 deletions(-) delete mode 100644 src/components/csp/CSPTotalMap.tsx delete mode 100644 src/components/csp/CSPTotalTable.tsx delete mode 100644 src/components/eqip/EQIPTotalMap.tsx delete mode 100644 src/components/eqip/EQIPTotalTable.tsx delete mode 100644 src/components/shared/titleii/GetPracticeTotal.tsx create mode 100644 src/components/shared/titleii/Interface.tsx create mode 100644 src/components/shared/titleii/PracticeMethods.tsx rename src/components/{eqip/EQIPPracticeMap.tsx => shared/titleii/TitleIIPracticeMap.tsx} (63%) rename src/components/{eqip/EQIPPracticeTable.tsx => shared/titleii/TitleIIPracticeTable.tsx} (94%) diff --git a/src/components/NavBar.tsx b/src/components/NavBar.tsx index 7d294d2..3bee376 100644 --- a/src/components/NavBar.tsx +++ b/src/components/NavBar.tsx @@ -75,6 +75,7 @@ export default function NavBar({ diff --git a/src/components/SemiDonutChart.tsx b/src/components/SemiDonutChart.tsx index 7f58d40..9e1471e 100644 --- a/src/components/SemiDonutChart.tsx +++ b/src/components/SemiDonutChart.tsx @@ -74,7 +74,7 @@ export default function SemiDonutChart({ data, label1, label2 }: any): JSX.Eleme />
- - - - - - - - - - -
Benefits: - ${ShortFormat(totalPaymentInDollars, undefined, 2)} -
- PCT. Nationwide: - - {totalPaymentInPercentageNationwide - ? `${totalPaymentInPercentageNationwide} %` - : "0%"} -
- - ); - const fillColour = () => { - if (totalPaymentInDollars) { - if (totalPaymentInDollars !== 0) return colorScale(totalPaymentInDollars); - return "#D2D2D2"; - } - return "#D2D2D2"; - }; - return ( - { - setReactTooltipContent(hoverContent); - }} - onMouseLeave={() => { - setReactTooltipContent(""); - }} - fill={fillColour()} - stroke="#FFF" - style={{ - default: { stroke: "#FFFFFF", strokeWidth: 0.75, outline: "none" }, - hover: { - stroke: "#232323", - strokeWidth: 2, - outline: "none" - }, - pressed: { - fill: "#345feb", - outline: "none" - } - }} - /> - ); - })} - {geographies.map((geo) => { - const centroid = geoCentroid(geo); - const cur = allStates.find((s) => s.val === geo.id); - return ( - - {cur && - centroid[0] > -160 && - centroid[0] < -67 && - (Object.keys(offsets).indexOf(cur.id) === -1 ? ( - - - {cur.id} - - - ) : ( - - - {cur.id} - - - ))} - - ); - })} - - )} - - - ) : ( - -

Loading Map Data...

-
- )} - - ); -}; - -MapChart.propTypes = { - setReactTooltipContent: PropTypes.func -}; - -const CSPTotalMap = ({ - statePerformance, - allStates, - year, - stateCodes -}: { - statePerformance: any; - allStates: any; - year: string; - stateCodes: any; -}): JSX.Element => { - const quantizeArray: number[] = []; - const category = "Total CSP"; - statePerformance[year].forEach((value) => quantizeArray.push(value.totalPaymentInDollars)); - const maxValue = Math.max(...quantizeArray); - const mapColor = ["#F0F9E8", "#BAE4BC", "#7BCCC4", "#43A2CA", "#0868AC"]; - const customScale = legendConfig[category]; - const colorScale = d3.scaleThreshold(customScale, mapColor); - const [content, setContent] = useState(""); - - const classes = useStyles(); - return ( -
-
- - {maxValue !== 0 ? ( - - ) : ( -
- {titleElement(category, year)} - - - {category} data in {year} is unavailable for all states. - - -
- )} -
- - - -
- - {content} - -
-
-
- ); -}; -const titleElement = (attribute, year): JSX.Element => { - return ( - - - {attribute} Benefits from {year} - {" "} - - In any state that appears in grey, there is no available data - - - ); -}; -export default CSPTotalMap; diff --git a/src/components/csp/CSPTotalTable.tsx b/src/components/csp/CSPTotalTable.tsx deleted file mode 100644 index ae1571b..0000000 --- a/src/components/csp/CSPTotalTable.tsx +++ /dev/null @@ -1,210 +0,0 @@ -import React from "react"; -import styled from "styled-components"; -import { useTable, useSortBy } from "react-table"; -import SwapVertIcon from "@mui/icons-material/SwapVert"; -import Box from "@mui/material/Box"; -import "../../styles/table.css"; -import { compareWithDollarSign, compareWithPercentSign } from "../shared/TableCompareFunctions"; - -const Styles = styled.div` - padding: 1rem; - - table { - border-spacing: 0; - border: 1px solid #e4ebe7; - border-left: none; - border-right: none; - - tr { - :last-child { - td { - border-bottom: 0; - } - } - } - - th { - background-color: #f1f1f1; - padding: 1rem; - } - - td { - margin: 0; - padding: 1rem; - padding-left: 5rem; - padding-right: 5rem; - border-bottom: 1px solid #e4ebe7; - border-right: none; - - :last-child { - border-right: 0; - } - } - } -`; - -// eslint-disable-next-line -function Table({ columns, data }: { columns: any; data: any }) { - const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable( - { - columns, - data - }, - useSortBy - ); - - const firstPageRows = rows.slice(0, 50); - - return ( - <> - - - {headerGroups.map((headerGroup) => ( - - {headerGroup.headers.map((column) => ( - // Add the sorting props to control sorting. - - ))} - - ))} - - - { - // eslint-disable-next-line - firstPageRows.map((row, i) => { - prepareRow(row); - return ( - - {row.cells.map((cell) => { - return ( - - ); - })} - - ); - }) - } - -
- - {column.render("Header")} -
- {(() => { - if (!column.isSorted) - return ( - - - - ); - if (column.isSortedDesc) return {"\u{25BC}"}; - return {"\u{25B2}"}; - })()} -
-
-
- {cell.render("Cell")} -
-
-
- Showing the first {rows.length} results of {rows.length} rows -
- - ); -} - -function App({ - statePerformance, - year, - stateCodes -}: { - statePerformance: any; - year: string; - stateCodes: any; -}): JSX.Element { - const cspTableData: any[] = []; - - // eslint-disable-next-line no-restricted-syntax - statePerformance[year].forEach((value) => { - const newRecord = () => { - let stateName = ""; - stateCodes.forEach((sValue) => { - if (sValue.code.toUpperCase() === value.state.toUpperCase()) { - stateName = sValue.name; - } - }); - return { - state: stateName, - cspBenefit: `$${value.totalPaymentInDollars - .toLocaleString(undefined, { minimumFractionDigits: 2 }) - .toString()}`, - percentage: `${value.totalPaymentInPercentageNationwide.toString()}%` - }; - }; - cspTableData.push(newRecord()); - }); - - const columns = React.useMemo( - () => [ - { - Header: STATES, - accessor: "state", - paddingLeft: "5rem", - paddingRight: "32rem" - }, - { - Header: ( - - CSP BENEFITS - - ), - accessor: "cspBenefit", - sortType: compareWithDollarSign, - Cell: function styleCells(row) { - return
{row.value}
; - } - }, - { - Header: PCT. NATIONWIDE, - accessor: "percentage", - sortType: compareWithPercentSign, - Cell: function styleCells(row) { - return
{row.value}
; - } - } - ], - [] - ); - - return ( - - - Object.keys(obj).length > 0)} /> - - - ); -} - -export default App; diff --git a/src/components/eqip/EQIPTotalMap.tsx b/src/components/eqip/EQIPTotalMap.tsx deleted file mode 100644 index a4de1ff..0000000 --- a/src/components/eqip/EQIPTotalMap.tsx +++ /dev/null @@ -1,217 +0,0 @@ -import React, { useState } from "react"; -import { geoCentroid } from "d3-geo"; -import { ComposableMap, Geographies, Geography, Marker, Annotation } from "react-simple-maps"; -import ReactTooltip from "react-tooltip"; -import Box from "@mui/material/Box"; -import Typography from "@mui/material/Typography"; -import * as d3 from "d3"; -import PropTypes from "prop-types"; -import { useStyles, tooltipBkgColor, topTipStyle } from "../shared/MapTooltip"; -import "../../styles/map.css"; -import legendConfig from "../../utils/legendConfig.json"; -import DrawLegend from "../shared/DrawLegend"; -import { ShortFormat } from "../shared/ConvertionFormats"; - -const geoUrl = "https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json"; - -const offsets = { - VT: [50, -8], - NH: [34, 2], - MA: [30, -1], - RI: [28, 2], - CT: [35, 10], - NJ: [34, 1], - DE: [33, 0], - MD: [47, 10], - DC: [49, 21] -}; - -const MapChart = ({ setReactTooltipContent, maxValue, allStates, statePerformance, year, stateCodes, colorScale }) => { - const classes = useStyles(); - return ( -
- - - {({ geographies }) => ( - <> - {geographies.map((geo) => { - const record = statePerformance[year].filter( - (v) => stateCodes[v.state] === geo.properties.name - )[0]; - if (record === undefined || record.length === 0) { - return null; - } - const totalPaymentInDollars = record.totalPaymentInDollars; - const totalPaymentInPercentageNationwide = record.totalPaymentInPercentageNationwide; - const hoverContent = ( -
-
- {geo.properties.name} -
-
- - - - - - - - - - -
Benefits: - ${ShortFormat(totalPaymentInDollars, undefined, 2)} -
- PCT. Nationwide: - - {totalPaymentInPercentageNationwide - ? `${totalPaymentInPercentageNationwide} %` - : "0%"} -
- - ); - return ( - { - setReactTooltipContent(hoverContent); - }} - onMouseLeave={() => { - setReactTooltipContent(""); - }} - fill={colorScale(totalPaymentInDollars || { value: 0 }) || "#D2D2D2"} - stroke="#FFF" - style={{ - default: { stroke: "#FFFFFF", strokeWidth: 0.75, outline: "none" }, - hover: { - stroke: "#232323", - strokeWidth: 2, - outline: "none" - }, - pressed: { - fill: "#345feb", - outline: "none" - } - }} - /> - ); - })} - {geographies.map((geo) => { - const centroid = geoCentroid(geo); - const cur = allStates.find((s) => s.val === geo.id); - return ( - - {cur && - centroid[0] > -160 && - centroid[0] < -67 && - (Object.keys(offsets).indexOf(cur.id) === -1 ? ( - - - {cur.id} - - - ) : ( - - - {cur.id} - - - ))} - - ); - })} - - )} - - - - ); -}; - -MapChart.propTypes = { - setReactTooltipContent: PropTypes.func, - maxValue: PropTypes.number -}; - -const EQIPTotalMap = ({ - statePerformance, - allStates, - year, - stateCodes -}: { - statePerformance: any; - allStates: any; - year: string; - stateCodes: any; -}): JSX.Element => { - const quantizeArray: number[] = []; - const category = "Total EQIP"; - statePerformance[year].forEach((value) => quantizeArray.push(value.totalPaymentInDollars)); - const maxValue = Math.max(...quantizeArray); - const mapColor = ["#F0F9E8", "#BAE4BC", "#7BCCC4", "#43A2CA", "#0868AC"]; - const customScale = legendConfig[category]; - const colorScale = d3.scaleThreshold(customScale, mapColor); - const [content, setContent] = useState(""); - - const classes = useStyles(); - return ( -
- - {maxValue !== 0 ? ( - - ) : ( -
- {titleElement(category, year)} - - - {category} data in {year} is unavailable for all states. - - -
- )} -
- -
- - {content} - -
-
- ); -}; -const titleElement = (attribute, year): JSX.Element => { - return ( - - - {attribute} Benefits from {year} - {" "} - - In any state that appears in grey, there is no available data - - - ); -}; -export default EQIPTotalMap; diff --git a/src/components/eqip/EQIPTotalTable.tsx b/src/components/eqip/EQIPTotalTable.tsx deleted file mode 100644 index b9863c7..0000000 --- a/src/components/eqip/EQIPTotalTable.tsx +++ /dev/null @@ -1,210 +0,0 @@ -import React from "react"; -import styled from "styled-components"; -import { useTable, useSortBy } from "react-table"; -import SwapVertIcon from "@mui/icons-material/SwapVert"; -import Box from "@mui/material/Box"; -import "../../styles/table.css"; -import { compareWithDollarSign, compareWithPercentSign } from "../shared/TableCompareFunctions"; - -const Styles = styled.div` - padding: 1rem; - - table { - border-spacing: 0; - border: 1px solid #e4ebe7; - border-left: none; - border-right: none; - - tr { - :last-child { - td { - border-bottom: 0; - } - } - } - - th { - background-color: #f1f1f1; - padding: 1rem; - } - - td { - margin: 0; - padding: 1rem; - padding-left: 5rem; - padding-right: 5rem; - border-bottom: 1px solid #e4ebe7; - border-right: none; - - :last-child { - border-right: 0; - } - } - } -`; - -// eslint-disable-next-line -function Table({ columns, data }: { columns: any; data: any }) { - const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = useTable( - { - columns, - data - }, - useSortBy - ); - - const firstPageRows = rows.slice(0, 50); - - return ( - <> - - - {headerGroups.map((headerGroup) => ( - - {headerGroup.headers.map((column) => ( - // Add the sorting props to control sorting. - - ))} - - ))} - - - { - // eslint-disable-next-line - firstPageRows.map((row, i) => { - prepareRow(row); - return ( - - {row.cells.map((cell) => { - return ( - - ); - })} - - ); - }) - } - -
- - {column.render("Header")} -
- {(() => { - if (!column.isSorted) - return ( - - - - ); - if (column.isSortedDesc) return {"\u{25BC}"}; - return {"\u{25B2}"}; - })()} -
-
-
- {cell.render("Cell")} -
-
-
- Showing the first {rows.length} results of {rows.length} rows -
- - ); -} - -function App({ - statePerformance, - year, - stateCodes -}: { - statePerformance: any; - year: string; - stateCodes: any; -}): JSX.Element { - const eqipTableData: any[] = []; - - // eslint-disable-next-line no-restricted-syntax - statePerformance[year].forEach((value) => { - const newRecord = () => { - let stateName = ""; - stateCodes.forEach((sValue) => { - if (sValue.code.toUpperCase() === value.state.toUpperCase()) { - stateName = sValue.name; - } - }); - return { - state: stateName, - eqipBenefit: `$${value.totalPaymentInDollars - .toLocaleString(undefined, { minimumFractionDigits: 2 }) - .toString()}`, - percentage: `${value.totalPaymentInPercentageNationwide.toString()}%` - }; - }; - eqipTableData.push(newRecord()); - }); - - const columns = React.useMemo( - () => [ - { - Header: STATES, - accessor: "state", - paddingLeft: "5rem", - paddingRight: "32rem" - }, - { - Header: ( - - EQIP BENEFITS - - ), - accessor: "eqipBenefit", - sortType: compareWithDollarSign, - Cell: function styleCells(row) { - return
{row.value}
; - } - }, - { - Header: PCT. NATIONWIDE, - accessor: "percentage", - sortType: compareWithPercentSign, - Cell: function styleCells(row) { - return
{row.value}
; - } - } - ], - [] - ); - - return ( - - - - - - ); -} - -export default App; diff --git a/src/components/shared/titleii/GetPracticeTotal.tsx b/src/components/shared/titleii/GetPracticeTotal.tsx deleted file mode 100644 index 2d34cba..0000000 --- a/src/components/shared/titleii/GetPracticeTotal.tsx +++ /dev/null @@ -1,31 +0,0 @@ -const getPracticeTotal = (record, practiceName) => { - if (!record || !record.statutes) return 0; - if (practiceName === "All Practices") { - return record.totalPaymentInDollars || 0; - } - const codeMatch = practiceName.match(/\((\d+)\)$/); - const practiceCode = codeMatch ? codeMatch[1] : null; - const practiceName_noCode = practiceName.replace(/\s*\(\d+\)$/, ""); - let total = 0; - record.statutes?.forEach((statute) => { - if (!statute.practiceCategories) return; - statute.practiceCategories.forEach((category) => { - if (practiceName_noCode === category.practiceCategoryName) { - total += category.totalPaymentInDollars || 0; - return; - } - - if (practiceCode && Array.isArray(category.practices) && category.practices.length > 0) { - category.practices.forEach((practice) => { - const currentPracticeCode = practice.practiceName?.match(/\((\d+)\)$/)?.[1]; - if (currentPracticeCode === practiceCode) { - total += practice.totalPaymentInDollars || 0; - } - }); - } - }); - }); - return total; -}; - -export default getPracticeTotal; diff --git a/src/components/shared/titleii/Interface.tsx b/src/components/shared/titleii/Interface.tsx new file mode 100644 index 0000000..8d9e924 --- /dev/null +++ b/src/components/shared/titleii/Interface.tsx @@ -0,0 +1,39 @@ +export interface PracticeName { + practiceName: string; + practiceCode: string; +} + +export interface Practice { + practiceName: string; + practiceCode: string; + totalPaymentInDollars: number; +} + +export interface PracticeCategory { + practiceCategoryName: string; + totalPaymentInDollars: number; + practices: Practice[]; +} + +export interface StatePerformance { + state: string; + totalPaymentInDollars: number; + statutes: Array<{ + statuteName: string; + totalPaymentInDollars: number; + practiceCategories: PracticeCategory[]; + }>; +} + +export interface PracticeMapProps { + programName: string; + initialStatePerformance: Record; + allStates: any; + year: string; + stateCodes: any; + practiceNames: Array<{ + practiceName: string; + practiceCode: string; + }>; + onPracticeChange?: (practices: string[]) => void; +} diff --git a/src/components/shared/titleii/PracticeMethods.tsx b/src/components/shared/titleii/PracticeMethods.tsx new file mode 100644 index 0000000..778b296 --- /dev/null +++ b/src/components/shared/titleii/PracticeMethods.tsx @@ -0,0 +1,179 @@ +import legendConfig from "../../../utils/legendConfig.json"; +import { ShortFormat } from "../ConvertionFormats"; + +export const getPracticeData = (statePerformance, year, selectedPractices) => { + if (!statePerformance[year]) return []; + return statePerformance[year] + .map((state) => { + let total = 0; + if (selectedPractices.includes("All Practices")) { + total = state.totalPaymentInDollars || 0; + } else { + state.statutes?.forEach((statute) => { + statute.practiceCategories?.forEach((category) => { + selectedPractices.forEach((practice) => { + const codeMatch = practice.match(/\((\d+)\)$/); + const practiceCode = codeMatch ? codeMatch[1] : null; + + if (!practiceCode) return; + + if (!category.practices || category.practices.length === 0) { + if (practice.includes(category.practiceCategoryName)) { + total += category.totalPaymentInDollars || 0; + } + } else { + category.practices.forEach((p) => { + const match = p.practiceName?.match(/\((\d+)\)$/)?.[1]; + if (match === practiceCode) { + total += p.totalPaymentInDollars || 0; + } + }); + } + }); + }); + }); + } + return total; + }) + .filter((value) => value > 0); +}; + +export const getPracticeCategories = (practiceNames) => { + if (!practiceNames || practiceNames.length === 0) { + return ["All Practices"]; + } + return ["All Practices", ...practiceNames]; +}; + +export const getPracticeTotal = (record, practiceName) => { + if (!record || !record.statutes) return 0; + if (practiceName === "All Practices") { + return record.totalPaymentInDollars || 0; + } + const codeMatch = practiceName.match(/\((\d+)\)$/); + const practiceCode = codeMatch ? codeMatch[1] : null; + const practiceName_noCode = practiceName.replace(/\s*\(\d+\)$/, ""); + let total = 0; + record.statutes?.forEach((statute) => { + if (!statute.practiceCategories) return; + statute.practiceCategories.forEach((category) => { + if (practiceName_noCode === category.practiceCategoryName) { + total += category.totalPaymentInDollars || 0; + return; + } + + if (practiceCode && Array.isArray(category.practices) && category.practices.length > 0) { + category.practices.forEach((practice) => { + const currentPracticeCode = practice.practiceName?.match(/\((\d+)\)$/)?.[1]; + if (currentPracticeCode === practiceCode) { + total += practice.totalPaymentInDollars || 0; + } + }); + } + }); + }); + return total; +}; + +export const calculateNationalTotalMap = (statePerformance, practices, year) => { + let total = 0; + if (!statePerformance[year]) return total; + statePerformance[year].forEach((state) => { + if (practices.includes("All Practices")) { + total += state.totalPaymentInDollars || 0; + } else { + practices.forEach((practice) => { + const codeMatch = practice.match(/\((\d+)\)$/); + const practiceCode = codeMatch ? codeMatch[1] : null; + if (!practiceCode) return; + state.statutes?.forEach((statute) => { + statute.practiceCategories?.forEach((category) => { + if (!category.practices || category.practices.length === 0) { + if (practice.includes(category.practiceCategoryName)) { + total += category.totalPaymentInDollars || 0; + } + } else { + category.practices.forEach((p) => { + const match = p.practiceName?.match(/\((\d+)\)$/)?.[1]; + if (match === practiceCode) { + total += p.totalPaymentInDollars || 0; + } + }); + } + }); + }); + }); + } + }); + return total; +}; + +export const getCustomScale = (practiceData, configName) => { + if (practiceData.length === 0) return legendConfig[configName]; + + const sortedData = [...practiceData].sort((a, b) => a - b); + const quintileSize = Math.ceil(sortedData.length / 5); + + return [ + sortedData[quintileSize], + sortedData[quintileSize * 2], + sortedData[quintileSize * 3], + sortedData[quintileSize * 4] + ]; +}; + +export const computeTooltipContent = (geo, record, selectedPractices, classes, getNationalTotal) => { + if (!record) return ""; + let tooltipContent = ` +
+
+ ${geo.properties.name} +
+
+ `; + let practiceTotal = 0; + if (selectedPractices.includes("All Practices")) { + practiceTotal = record.totalPaymentInDollars || 0; + tooltipContent += ` + + + + `; + } else { + selectedPractices.forEach((practice, index) => { + const practiceAmount = getPracticeTotal(record, practice); + practiceTotal += practiceAmount; + const displayName = practice.replace(/\s*\(\d+\)$/, ""); + tooltipContent += ` + + + + `; + }); + } + const nationalTotal = getNationalTotal(selectedPractices); + const totalPercentage = nationalTotal > 0 ? (practiceTotal / nationalTotal) * 100 : 0; + tooltipContent += ` + + + + + + + + + +
All Practices: + $${ShortFormat(practiceTotal, undefined, 2)} +
+ ${displayName}: + + $${ShortFormat(practiceAmount, undefined, 2)} +
PCT. Nationwide: + ${totalPercentage > 0 ? `${totalPercentage.toFixed(2)}%` : "0%"} +
+ `; + return tooltipContent; +}; diff --git a/src/components/eqip/EQIPPracticeMap.tsx b/src/components/shared/titleii/TitleIIPracticeMap.tsx similarity index 63% rename from src/components/eqip/EQIPPracticeMap.tsx rename to src/components/shared/titleii/TitleIIPracticeMap.tsx index bf3666b..ab6dc1b 100644 --- a/src/components/eqip/EQIPPracticeMap.tsx +++ b/src/components/shared/titleii/TitleIIPracticeMap.tsx @@ -10,13 +10,19 @@ import Select from "@mui/material/Select"; import MenuItem from "@mui/material/MenuItem"; import Chip from "@mui/material/Chip"; import * as d3 from "d3"; -import { config } from "../../app.config"; -import { useStyles, tooltipBkgColor } from "../shared/MapTooltip"; -import "../../styles/map.css"; -import legendConfig from "../../utils/legendConfig.json"; -import DrawLegend from "../shared/DrawLegend"; -import getPracticeTotal from "../shared/titleii/GetPracticeTotal"; -import { ShortFormat } from "../shared/ConvertionFormats"; +import { config } from "../../../app.config"; +import { useStyles, tooltipBkgColor } from "../MapTooltip"; +import "../../../styles/map.css"; +import DrawLegend from "../DrawLegend"; +import { + getPracticeTotal, + calculateNationalTotalMap, + getPracticeCategories, + getPracticeData, + getCustomScale, + computeTooltipContent +} from "./PracticeMethods"; +import { PracticeMapProps } from "./Interface"; const geoUrl = "https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json"; @@ -44,62 +50,6 @@ const MapChart = ({ selectedPractices }) => { const classes = useStyles(); - const computeTooltipContent = (geo, record) => { - if (!record) return ""; - let tooltipContent = ` -
-
- ${geo.properties.name} -
- - `; - let practiceTotal = 0; - if (selectedPractices.includes("All Practices")) { - practiceTotal = record.totalPaymentInDollars || 0; - tooltipContent += ` - - - - `; - } else { - selectedPractices.forEach((practice, index) => { - const practiceAmount = getPracticeTotal(record, practice); - practiceTotal += practiceAmount; - const displayName = practice.replace(/\s*\(\d+\)$/, ""); - tooltipContent += ` - - - - `; - }); - } - const nationalTotal = getNationalTotal(selectedPractices); - const totalPercentage = nationalTotal > 0 ? (practiceTotal / nationalTotal) * 100 : 0; - tooltipContent += ` - - - - - - - - - -
All Practices: - $${ShortFormat(practiceTotal, undefined, 2)} -
- ${displayName}: - - $${ShortFormat(practiceAmount, undefined, 2)} -
PCT. Nationwide: - ${totalPercentage > 0 ? `${totalPercentage.toFixed(2)}%` : "0%"} -
-
`; - return tooltipContent; - }; - return (
@@ -123,7 +73,13 @@ const MapChart = ({ key={geo.rsmKey} geography={geo} onMouseEnter={() => { - const tooltipContent = computeTooltipContent(geo, record); + const tooltipContent = computeTooltipContent( + geo, + record, + selectedPractices, + classes, + getNationalTotal + ); ReactTooltip.rebuild(); setReactTooltipContent(tooltipContent); }} @@ -174,47 +130,15 @@ const MapChart = ({ ); }; -interface Practice { - practiceName: string; - practiceCode: string; - totalPaymentInDollars: number; -} - -interface PracticeCategory { - practiceCategoryName: string; - totalPaymentInDollars: number; - practices: Practice[]; -} - -interface StatePerformance { - state: string; - totalPaymentInDollars: number; - statutes: Array<{ - statuteName: string; - totalPaymentInDollars: number; - practiceCategories: PracticeCategory[]; - }>; -} - -interface EQIPPracticeMapProps { - initialStatePerformance: Record; - allStates: any; - year: string; - stateCodes: any; - practiceNames: Array<{ - practiceName: string; - practiceCode: string; - }>; - onPracticeChange?: (practices: string[]) => void; -} -const EQIPPracticeMap = ({ +const TitleIIPracticeMap = ({ + programName, initialStatePerformance, allStates, year, stateCodes, practiceNames, onPracticeChange -}: EQIPPracticeMapProps) => { +}: PracticeMapProps) => { const [content, setContent] = useState(""); const [statePerformance, setStatePerformance] = useState(initialStatePerformance); const classes = useStyles(); @@ -222,10 +146,7 @@ const EQIPPracticeMap = ({ const [isLoading, setIsLoading] = useState(false); const practiceCategories = useMemo(() => { - if (!practiceNames || practiceNames.length === 0) { - return ["All Practices"]; - } - return ["All Practices", ...practiceNames]; + return getPracticeCategories(practiceNames); }, [practiceNames]); const fetchStatePerformanceData = async (selectedPracticeNames: string[]) => { if (selectedPracticeNames.includes("All Practices")) { @@ -246,7 +167,9 @@ const EQIPPracticeMap = ({ } const url = `${ config.apiUrl - }/titles/title-ii/programs/eqip/state-distribution?practice_code=${selectedCodes.join(",")}`; + }/titles/title-ii/programs/${programName.toLowerCase()}/state-distribution?practice_code=${selectedCodes.join( + "," + )}`; const response = await fetch(url); const data = await response.json(); setStatePerformance(data); @@ -280,91 +203,17 @@ const EQIPPracticeMap = ({ }, [statePerformance, selectedPractices]); const practiceData = useMemo(() => { - if (!statePerformance[year]) return []; - return statePerformance[year] - .map((state) => { - let total = 0; - if (selectedPractices.includes("All Practices")) { - total = state.totalPaymentInDollars || 0; - } else { - state.statutes?.forEach((statute) => { - statute.practiceCategories?.forEach((category) => { - selectedPractices.forEach((practice) => { - const codeMatch = practice.match(/\((\d+)\)$/); - const practiceCode = codeMatch ? codeMatch[1] : null; - - if (!practiceCode) return; - - if (!category.practices || category.practices.length === 0) { - if (practice.includes(category.practiceCategoryName)) { - total += category.totalPaymentInDollars || 0; - } - } else { - category.practices.forEach((p) => { - const match = p.practiceName?.match(/\((\d+)\)$/)?.[1]; - if (match === practiceCode) { - total += p.totalPaymentInDollars || 0; - } - }); - } - }); - }); - }); - } - return total; - }) - .filter((value) => value > 0); + return getPracticeData(statePerformance, year, selectedPractices); }, [statePerformance, year, selectedPractices]); const maxValue = useMemo(() => Math.max(...practiceData, 0), [practiceData]); const customScale = useMemo(() => { - if (practiceData.length === 0) return legendConfig["Total EQIP"]; - - const sortedData = [...practiceData].sort((a, b) => a - b); - const quintileSize = Math.ceil(sortedData.length / 5); - - return [ - sortedData[quintileSize], - sortedData[quintileSize * 2], - sortedData[quintileSize * 3], - sortedData[quintileSize * 4] - ]; + return getCustomScale(practiceData, `Total ${programName}`); }, [practiceData]); const mapColor = ["#F0F9E8", "#BAE4BC", "#7BCCC4", "#43A2CA", "#0868AC"]; const colorScale = d3.scaleThreshold(customScale, mapColor); const getNationalTotal = React.useCallback( (practices: string[]) => { - let total = 0; - if (!statePerformance[year]) return total; - statePerformance[year].forEach((state) => { - if (practices.includes("All Practices")) { - total += state.totalPaymentInDollars || 0; - } else { - practices.forEach((practice) => { - const codeMatch = practice.match(/\((\d+)\)$/); - const practiceCode = codeMatch ? codeMatch[1] : null; - - if (!practiceCode) return; - - state.statutes?.forEach((statute) => { - statute.practiceCategories?.forEach((category) => { - if (!category.practices || category.practices.length === 0) { - if (practice.includes(category.practiceCategoryName)) { - total += category.totalPaymentInDollars || 0; - } - } else { - category.practices.forEach((p) => { - const match = p.practiceName?.match(/\((\d+)\)$/)?.[1]; - if (match === practiceCode) { - total += p.totalPaymentInDollars || 0; - } - }); - } - }); - }); - }); - } - }); - return total; + return calculateNationalTotalMap(statePerformance, practices, year); }, [statePerformance, year] ); @@ -393,10 +242,10 @@ const EQIPPracticeMap = ({ return ( - {maxValue !== 0 ? ( + {selectedPractices.length > 0 ? ( ) : (
- {titleElement(selectedPractices, year)} + {titleElement(programName, selectedPractices, year)} Please select at least one practice category. @@ -461,9 +310,6 @@ const EQIPPracticeMap = ({ maxHeight: 500, overflowY: "auto", position: "absolute", - top: "50%", - left: "50%", - transform: "translate(-50%, -50%)", border: "1px solid lightgray", boxShadow: "0 4px 6px rgba(0,0,0,0.1)", bgcolor: "background.paper" @@ -516,13 +362,13 @@ const EQIPPracticeMap = ({ ); }; -const titleElement = (practices: string[], year: string): JSX.Element => { +const titleElement = (programName, practices: string[], year: string): JSX.Element => { const practiceLabel = practices.length > 0 ? practices.join(", ") : "No practices selected"; return ( - {practiceLabel === "All Practices" ? "Total EQIP" : practiceLabel} Benefits from{" "} - {year} + {practiceLabel === "All Practices" ? `Total ${programName}` : practiceLabel} Benefits + from {year} Grey states indicate no available data @@ -531,4 +377,4 @@ const titleElement = (practices: string[], year: string): JSX.Element => { ); }; -export default EQIPPracticeMap; +export default TitleIIPracticeMap; diff --git a/src/components/eqip/EQIPPracticeTable.tsx b/src/components/shared/titleii/TitleIIPracticeTable.tsx similarity index 94% rename from src/components/eqip/EQIPPracticeTable.tsx rename to src/components/shared/titleii/TitleIIPracticeTable.tsx index 9c32024..fa91131 100644 --- a/src/components/eqip/EQIPPracticeTable.tsx +++ b/src/components/shared/titleii/TitleIIPracticeTable.tsx @@ -4,10 +4,10 @@ import { CSVLink } from "react-csv"; import { useTable, useSortBy, usePagination } from "react-table"; import SwapVertIcon from "@mui/icons-material/SwapVert"; import { Grid, TableContainer, Typography, Box } from "@mui/material"; -import { compareWithDollarSign, compareWithPercentSign } from "../shared/TableCompareFunctions"; -import getPracticeTotal from "../shared/titleii/GetPracticeTotal"; -import "../../styles/table.css"; -import getCSVData from "../shared/getCSVData"; +import { compareWithDollarSign, compareWithPercentSign } from "../TableCompareFunctions"; +import "../../../styles/table.css"; +import getCSVData from "../getCSVData"; +import { getPracticeTotal } from "./PracticeMethods"; const Styles = styled.div` padding: 0; @@ -94,8 +94,7 @@ const Styles = styled.div` } `; -function Table({ columns, data, initialState }) { - const state = React.useMemo(() => initialState, []); +function Table({ programName, columns, data }) { const { getTableProps, getTableBodyProps, @@ -121,16 +120,12 @@ function Table({ columns, data, initialState }) { useSortBy, usePagination ); - + const fileName = `${programName}-practice-data.csv`; return (
{data && data.length > 0 ? (
- + Export This Table to CSV @@ -266,12 +261,14 @@ function Table({ columns, data, initialState }) { ); } -function EQIPPracticeTable({ +function TitleIIPracticeTable({ + programName, statePerformance, year, stateCodes, selectedPractices }: { + programName: string; statePerformance: any; year: string; stateCodes: any; @@ -356,12 +353,12 @@ function EQIPPracticeTable({ if (selectedPractices.includes("All Practices")) { cols.push( { - Header: "Total EQIP Benefits", + Header: `Total ${programName} Benefits`, accessor: "Total Benefits", sortType: compareWithDollarSign }, { - Header: "EQIP Pct. Nationwide", + Header: `${programName} Pct. Nationwide`, accessor: "Percentage Nationwide", sortType: compareWithPercentSign } @@ -416,23 +413,16 @@ function EQIPPracticeTable({ paddingTop: 0.6 }} > - EQIP Practice Benefits by State + ${programName} Practice Benefits by State - +
); } -export default EQIPPracticeTable; +export default TitleIIPracticeTable; diff --git a/src/pages/CSPPage.tsx b/src/pages/CSPPage.tsx index afdb1fa..4bbe62f 100644 --- a/src/pages/CSPPage.tsx +++ b/src/pages/CSPPage.tsx @@ -4,13 +4,14 @@ import { createTheme, ThemeProvider, Typography } from "@mui/material"; import NavBar from "../components/NavBar"; import Drawer from "../components/ProgramDrawer"; import SemiDonutChart from "../components/SemiDonutChart"; -import DataTable from "../components/csp/CSPTotalTable"; -import CSPTotalMap from "../components/csp/CSPTotalMap"; +import DataTable from "../components/shared/titleii/TitleIIPracticeTable"; import CategoryTable from "../components/csp/CategoryTable"; import CategoryMap from "../components/csp/CategoryMap"; import { config } from "../app.config"; import { convertAllState, getJsonDataFromUrl } from "../utils/apiutil"; import NavSearchBar from "../components/shared/NavSearchBar"; +import { PracticeName } from "../components/shared/titleii/Interface"; +import TitleIIPracticeMap from "../components/shared/titleii/TitleIIPracticeMap"; export default function CSPPage(): JSX.Element { const [checked, setChecked] = React.useState(0); @@ -26,6 +27,14 @@ export default function CSPPage(): JSX.Element { const [secondTotal, setSecondTotal] = React.useState(0); const [thirdTotal, setThirdTotal] = React.useState(0); const [zeroCategories, setZeroCategories] = React.useState([]); + const [isDataReady, setIsDataReady] = React.useState(false); + + // connect to selector endpoint + const [selectedPractices, setSelectedPractices] = React.useState(["All Practices"]); + const [cspPracticeNames, setCspPracticeNames] = React.useState([]); + const handlePracticeChange = (practices: string[]) => { + setSelectedPractices(practices); + }; const defaultTheme = createTheme(); let landManagementTotal = 0; @@ -53,30 +62,34 @@ export default function CSPPage(): JSX.Element { const csp_year = "2018-2022"; React.useEffect(() => { - const state_perf_url = `${config.apiUrl}/titles/title-ii/programs/csp/state-distribution`; - getJsonDataFromUrl(state_perf_url).then((response) => { - const converted_perf_json = response; - setStatePerformance(converted_perf_json); - }); - - const allstates_url = `${config.apiUrl}/states`; - getJsonDataFromUrl(allstates_url).then((response) => { - const converted_json = response; - setAllStates(converted_json); - }); - - const statecode_url = `${config.apiUrl}/statecodes`; - getJsonDataFromUrl(statecode_url).then((response) => { - setStateCodesArray(response); - const converted_json = convertAllState(response); - setStateCodesData(converted_json); - }); - - const chartdata_url = `${config.apiUrl}/titles/title-ii/programs/csp/summary`; - getJsonDataFromUrl(chartdata_url).then((response) => { - const converted_chart_json = response; - processData(converted_chart_json); - }); + const fetchData = async () => { + try { + const [ + statePerformanceResponse, + allStatesResponse, + stateCodesResponse, + chartDataResponse, + cspPracticeNameResponse + ] = await Promise.all([ + getJsonDataFromUrl(`${config.apiUrl}/titles/title-ii/programs/csp/state-distribution`), + getJsonDataFromUrl(`${config.apiUrl}/states`), + getJsonDataFromUrl(`${config.apiUrl}/statecodes`), + getJsonDataFromUrl(`${config.apiUrl}/titles/title-ii/programs/csp/summary`), + getJsonDataFromUrl(`${config.apiUrl}/titles/title-ii/programs/csp/practice-names`) + ]); + setStatePerformance(statePerformanceResponse); + setAllStates(allStatesResponse); + setStateCodesArray(stateCodesResponse); + const converted_json = convertAllState(stateCodesResponse); + setStateCodesData(converted_json); + processData(chartDataResponse); + setCspPracticeNames(cspPracticeNameResponse); + setIsDataReady(true); + } catch (error) { + console.error("Error fetching data:", error); + } + }; + fetchData(); }, []); const processData = (chartData) => { @@ -228,11 +241,14 @@ export default function CSPPage(): JSX.Element { display: checked !== 0 ? "none" : "block" }} > - diff --git a/src/pages/EQIPPage.tsx b/src/pages/EQIPPage.tsx index 9a02613..2ac7524 100644 --- a/src/pages/EQIPPage.tsx +++ b/src/pages/EQIPPage.tsx @@ -4,18 +4,14 @@ import { createTheme, ThemeProvider, Typography } from "@mui/material"; import NavBar from "../components/NavBar"; import Drawer from "../components/ProgramDrawer"; import SemiDonutChart from "../components/SemiDonutChart"; -import DataTable from "../components/eqip/EQIPPracticeTable"; +import DataTable from "../components/shared/titleii/TitleIIPracticeTable"; import CategoryTable from "../components/eqip/CategoryTable"; import CategoryMap from "../components/eqip/CategoryMap"; import { config } from "../app.config"; import { convertAllState, getJsonDataFromUrl } from "../utils/apiutil"; import NavSearchBar from "../components/shared/NavSearchBar"; -import EQIPPracticeMap from "../components/eqip/EQIPPracticeMap"; - -interface PracticeName { - practiceName: string; - practiceCode: string; -} +import { PracticeName } from "../components/shared/titleii/Interface"; +import TitleIIPracticeMap from "../components/shared/titleii/TitleIIPracticeMap"; export default function EQIPPage(): JSX.Element { const [checked, setChecked] = React.useState(0); @@ -190,7 +186,8 @@ export default function EQIPPage(): JSX.Element { component="div" sx={{ width: "85%", m: "auto", display: checked !== 0 ? "none" : "block" }} > - Date: Thu, 21 Nov 2024 15:30:04 -0600 Subject: [PATCH 05/16] add column pagination to all selector-powered table --- src/components/ira/IRADollarTable.tsx | 188 ++++++++++++----- src/components/ira/IRAPercentageTable.tsx | 185 ++++++++++++----- .../ira/IRAPredictedDollarTable.tsx | 193 ++++++++++++----- .../ira/IRAPredictedPercentageTable.tsx | 182 +++++++++++----- .../shared/titleii/TitleIIPracticeTable.tsx | 194 +++++++++++++----- 5 files changed, 685 insertions(+), 257 deletions(-) diff --git a/src/components/ira/IRADollarTable.tsx b/src/components/ira/IRADollarTable.tsx index f187cc2..a624839 100644 --- a/src/components/ira/IRADollarTable.tsx +++ b/src/components/ira/IRADollarTable.tsx @@ -4,13 +4,7 @@ import { CSVLink } from "react-csv"; import { useTable, useSortBy, usePagination } from "react-table"; import SwapVertIcon from "@mui/icons-material/SwapVert"; import { Grid, TableContainer, Typography, Box, Button } from "@mui/material"; -import { - compareWithNumber, - compareWithAlphabetic, - compareWithDollarSign, - compareWithPercentSign, - sortByDollars -} from "../shared/TableCompareFunctions"; +import { compareWithNumber, compareWithAlphabetic, compareWithDollarSign } from "../shared/TableCompareFunctions"; import "../../styles/table.css"; import getCSVData from "../shared/getCSVData"; @@ -265,6 +259,14 @@ function IRADollarTable({ // eslint-disable-next-line function Table({ columns, data, initialState }: { columns: any; data: any; initialState: any }) { const state = React.useMemo(() => initialState, []); + const [columnPage, setColumnPage] = React.useState(0); + const columnsPerPage = 6; + const visibleColumnIndices = React.useMemo(() => { + const startIndex = columnPage * columnsPerPage + 1; + const endIndex = Math.min(startIndex + columnsPerPage, columns.length); + return Array.from({ length: endIndex - startIndex }, (_, i) => i + startIndex); + }, [columnPage, columnsPerPage, columns.length]); + const totalColumnPages = Math.ceil((columns.length - 1) / columnsPerPage); const { getTableProps, getTableBodyProps, @@ -297,73 +299,151 @@ function Table({ columns, data, initialState }: { columns: any; data: any; initi Export This Table to CSV + + + + + Column Page + + {columnPage + 1} of {totalColumnPages} + + + + +
{headerGroups.map((headerGroup) => ( - {headerGroup.headers.map((column) => ( - + {headerGroup.headers + .filter((_, index) => visibleColumnIndices.includes(index)) + .map((column) => ( + - ))} + })()} + + + ))} ))} - {page.map((row, i) => { + {page.map((row) => { prepareRow(row); return ( - - {row.cells.map((cell, j) => ( - - ))} + + + {row.cells + .filter((_, index) => visibleColumnIndices.includes(index)) + .map((cell, j) => ( + + ))} ); })} diff --git a/src/components/ira/IRAPercentageTable.tsx b/src/components/ira/IRAPercentageTable.tsx index 9e433ff..3f2e949 100644 --- a/src/components/ira/IRAPercentageTable.tsx +++ b/src/components/ira/IRAPercentageTable.tsx @@ -3,7 +3,7 @@ import styled from "styled-components"; import { CSVLink } from "react-csv"; import { useTable, useSortBy, usePagination } from "react-table"; import SwapVertIcon from "@mui/icons-material/SwapVert"; -import { Grid, TableContainer, Typography, Box, Button } from "@mui/material"; +import { Grid, TableContainer, Typography, Box } from "@mui/material"; import { compareWithAlphabetic, compareWithPercentSign } from "../shared/TableCompareFunctions"; import "../../styles/table.css"; import getCSVData from "../shared/getCSVData"; @@ -338,6 +338,14 @@ function IRAPercentageTable({ // eslint-disable-next-line function Table({ columns, data, initialState }: { columns: any; data: any; initialState: any }) { const state = React.useMemo(() => initialState, []); + const [columnPage, setColumnPage] = React.useState(0); + const columnsPerPage = 6; + const visibleColumnIndices = React.useMemo(() => { + const startIndex = columnPage * columnsPerPage + 1; + const endIndex = Math.min(startIndex + columnsPerPage, columns.length); + return Array.from({ length: endIndex - startIndex }, (_, i) => i + startIndex); + }, [columnPage, columnsPerPage, columns.length]); + const totalColumnPages = Math.ceil((columns.length - 1) / columnsPerPage); const { getTableProps, getTableBodyProps, @@ -371,73 +379,152 @@ function Table({ columns, data, initialState }: { columns: any; data: any; initi Export This Table to CSV - + + + + + Column Page + + {columnPage + 1} of {totalColumnPages} + + + + +
+ + {headerGroup.headers[0].render("Header")} + {(() => { - const headerText = column.render("Header"); - if (headerText.includes(":")) { - const [beforeColon, afterColon] = headerText.split(":"); + if (!headerGroup.headers[0].isSorted) return ( - <> -
- {beforeColon} -
- {afterColon.trim()} - + + + ); - } - return headerText; + if (headerGroup.headers[0].isSortedDesc) + return {"\u{25BC}"}; + return {"\u{25B2}"}; })()} - + +
{(() => { - if (!column.isSorted) + const headerText = column.render("Header"); + if (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}"}; - })()} - -
- {cell.render("Cell")} -
+ {row.cells[0].render("Cell")} + + {cell.render("Cell")} +
{headerGroups.map((headerGroup) => ( - {headerGroup.headers.map((column) => ( - + + {headerGroup.headers + .filter((_, index) => visibleColumnIndices.includes(index)) + .map((column) => ( + - ))} + })()} + + + ))} ))} - {page.map((row, i) => { + {page.map((row) => { prepareRow(row); return ( - - {row.cells.map((cell, j) => ( - - ))} + + + + {row.cells + .filter((_, index) => visibleColumnIndices.includes(index)) + .map((cell, j) => ( + + ))} ); })} diff --git a/src/components/ira/IRAPredictedDollarTable.tsx b/src/components/ira/IRAPredictedDollarTable.tsx index 029bac4..91caa1f 100644 --- a/src/components/ira/IRAPredictedDollarTable.tsx +++ b/src/components/ira/IRAPredictedDollarTable.tsx @@ -1,16 +1,10 @@ -import React from "react"; +import React, { useState } from "react"; import styled from "styled-components"; import { CSVLink } from "react-csv"; import { useTable, useSortBy, usePagination } from "react-table"; import SwapVertIcon from "@mui/icons-material/SwapVert"; import { Grid, TableContainer, Typography, Box, Button } from "@mui/material"; -import { - compareWithNumber, - compareWithAlphabetic, - compareWithDollarSign, - compareWithPercentSign, - sortByDollars -} from "../shared/TableCompareFunctions"; +import { compareWithAlphabetic, compareWithDollarSign } from "../shared/TableCompareFunctions"; import "../../styles/table.css"; import getCSVData from "../shared/getCSVData"; @@ -252,6 +246,14 @@ function IRAPredictedDollarTable({ // eslint-disable-next-line function Table({ columns, data, initialState }: { columns: any; data: any; initialState: any }) { const state = React.useMemo(() => initialState, []); + const [columnPage, setColumnPage] = useState(0); + const columnsPerPage = 6; + const visibleColumnIndices = React.useMemo(() => { + const startIndex = columnPage * columnsPerPage + 1; + const endIndex = Math.min(startIndex + columnsPerPage, columns.length); + return Array.from({ length: endIndex - startIndex }, (_, i) => i + startIndex); + }, [columnPage, columnsPerPage, columns.length]); + const totalColumnPages = Math.ceil((columns.length - 1) / columnsPerPage); const { getTableProps, getTableBodyProps, @@ -284,73 +286,152 @@ function Table({ columns, data, initialState }: { columns: any; data: any; initi Export This Table to CSV - + + + + + Column Page + + {columnPage + 1} of {totalColumnPages} + + + + +
+ + {headerGroup.headers[0].render("Header")} + {(() => { - const headerText = column.render("Header"); - if (headerText.includes(":")) { - const [beforeColon, afterColon] = headerText.split(":"); + if (!headerGroup.headers[0].isSorted) return ( - <> -
- {beforeColon} -
- {afterColon.trim()} - + + + ); - } - return headerText; + if (headerGroup.headers[0].isSortedDesc) + return {"\u{25BC}"}; + return {"\u{25B2}"}; })()} - + +
{(() => { - if (!column.isSorted) + const headerText = column.render("Header"); + if (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}"}; - })()} - -
- {cell.render("Cell")} -
+ {row.cells[0].render("Cell")} + + {cell.render("Cell")} +
{headerGroups.map((headerGroup) => ( - {headerGroup.headers.map((column) => ( - + + {headerGroup.headers + .filter((_, index) => visibleColumnIndices.includes(index)) + .map((column) => ( + - ))} + })()} + + + ))} ))} - {page.map((row, i) => { + {page.map((row) => { prepareRow(row); return ( - - {row.cells.map((cell, j) => ( - - ))} + + + + {row.cells + .filter((_, index) => visibleColumnIndices.includes(index)) + .map((cell, j) => ( + + ))} ); })} diff --git a/src/components/ira/IRAPredictedPercentageTable.tsx b/src/components/ira/IRAPredictedPercentageTable.tsx index 7f7e61a..8e870a1 100644 --- a/src/components/ira/IRAPredictedPercentageTable.tsx +++ b/src/components/ira/IRAPredictedPercentageTable.tsx @@ -315,6 +315,14 @@ function IRAPredictedPercentageTable({ // eslint-disable-next-line function Table({ columns, data, initialState }: { columns: any; data: any; initialState: any }) { const state = React.useMemo(() => initialState, []); + const [columnPage, setColumnPage] = React.useState(0); + const columnsPerPage = 6; + const visibleColumnIndices = React.useMemo(() => { + const startIndex = columnPage * columnsPerPage + 1; + const endIndex = Math.min(startIndex + columnsPerPage, columns.length); + return Array.from({ length: endIndex - startIndex }, (_, i) => i + startIndex); + }, [columnPage, columnsPerPage, columns.length]); + const totalColumnPages = Math.ceil((columns.length - 1) / columnsPerPage); const { getTableProps, getTableBodyProps, @@ -348,73 +356,153 @@ function Table({ columns, data, initialState }: { columns: any; data: any; initi Export This Table to CSV + + + + + Column Page + + {columnPage + 1} of {totalColumnPages} + + + + +
+ + {headerGroup.headers[0].render("Header")} + {(() => { - const headerText = column.render("Header"); - if (headerText.includes(":")) { - const [beforeColon, afterColon] = headerText.split(":"); + if (!headerGroup.headers[0].isSorted) return ( - <> -
- {beforeColon} -
- {afterColon.trim()} - + + + ); - } - return headerText; + if (headerGroup.headers[0].isSortedDesc) + return {"\u{25BC}"}; + return {"\u{25B2}"}; })()} - + +
{(() => { - if (!column.isSorted) + const headerText = column.render("Header"); + if (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}"}; - })()} - -
- {cell.render("Cell")} -
+ {row.cells[0].render("Cell")} + + {cell.render("Cell")} +
{headerGroups.map((headerGroup) => ( - {headerGroup.headers.map((column) => ( - + + {headerGroup.headers + .filter((_, index) => visibleColumnIndices.includes(index)) + .map((column) => ( + - ))} + })()} + + + ))} ))} - {page.map((row, i) => { + {page.map((row) => { prepareRow(row); return ( - - {row.cells.map((cell, j) => ( - - ))} + + + + {row.cells + .filter((_, index) => visibleColumnIndices.includes(index)) + .map((cell, j) => ( + + ))} ); })} diff --git a/src/components/shared/titleii/TitleIIPracticeTable.tsx b/src/components/shared/titleii/TitleIIPracticeTable.tsx index fa91131..409ff6b 100644 --- a/src/components/shared/titleii/TitleIIPracticeTable.tsx +++ b/src/components/shared/titleii/TitleIIPracticeTable.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useState } from "react"; import styled from "styled-components"; import { CSVLink } from "react-csv"; import { useTable, useSortBy, usePagination } from "react-table"; @@ -95,6 +95,15 @@ const Styles = styled.div` `; function Table({ programName, columns, data }) { + const [columnPage, setColumnPage] = useState(0); + const columnsPerPage = 6; + const visibleColumnIndices = React.useMemo(() => { + const startIndex = columnPage * columnsPerPage + 1; + const endIndex = Math.min(startIndex + columnsPerPage, columns.length); + return Array.from({ length: endIndex - startIndex }, (_, i) => i + startIndex); + }, [columnPage, columnsPerPage, columns.length]); + + const totalColumnPages = Math.ceil((columns.length - 1) / columnsPerPage); const { getTableProps, getTableBodyProps, @@ -128,45 +137,116 @@ function Table({ programName, columns, data }) { Export This Table to CSV - + + + + + Column Page + + {columnPage + 1} of {totalColumnPages} + + + + +
+ + {headerGroup.headers[0].render("Header")} + {(() => { - const headerText = column.render("Header"); - if (headerText.includes(":")) { - const [beforeColon, afterColon] = headerText.split(":"); + if (!headerGroup.headers[0].isSorted) return ( - <> -
- {beforeColon} -
- {afterColon.trim()} - + + + ); - } - return headerText; + if (headerGroup.headers[0].isSortedDesc) + return {"\u{25BC}"}; + return {"\u{25B2}"}; })()} - + +
{(() => { - if (!column.isSorted) + const headerText = column.render("Header"); + if (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}"}; - })()} - -
- {cell.render("Cell")} -
+ {row.cells[0].render("Cell")} + + {cell.render("Cell")} +
{headerGroups.map((headerGroup) => ( - - {headerGroup.headers.map((column) => ( - + + {headerGroup.headers + .filter((_, index) => visibleColumnIndices.includes(index)) + .map((column) => ( + - ))} + })()} + + + ))} ))} @@ -174,23 +254,35 @@ function Table({ programName, columns, data }) { {page.map((row, i) => { prepareRow(row); return ( - - {row.cells.map((cell, j) => ( - - ))} + + + {row.cells + .filter((_, index) => visibleColumnIndices.includes(index)) + .map((cell, j) => ( + + ))} ); })}
+
+ {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 ( - <> -
- {beforeColon} -
- {afterColon.trim()} - + + + ); - } - return headerText; + if (column.isSortedDesc) + return {"\u{25BC}"}; + 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}"}; - })()} - -
- {cell.render("Cell")} -
+ {row.cells[0].render("Cell")} + + {cell.render("Cell")} +
- {" "} + {" "} + {" "} + {" "} - - Page{" "} - + + + Page + {pageIndex + 1} of {pageOptions.length} - {" "} + - | Go to page:{" "} + | Go to page: - {" "} +