diff --git a/CHANGELOG.md b/CHANGELOG.md index cd6a0444..ea2e914a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ 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). +## [0.10.0] - 2023-10-17 + +### Added + +- ACEP page and corresponding components for its attributes [#200](https://github.com/policy-design-lab/pdl-frontend/issues/200) + +- RCPP page and corresponding components for its attributes [#196](https://github.com/policy-design-lab/pdl-frontend/issues/196) + +### Changed +- Reverse some unnecessary changes in EQIP, CSP and CRP table [#199](https://github.com/policy-design-lab/pdl-frontend/issues/199) + + +## Fixed +- Adjust program drawer to remove over-length scroll bar in Chrome/Firefox [#198](https://github.com/policy-design-lab/pdl-frontend/issues/198) + + ## [0.9.0] - 2023-09-18 ### Added diff --git a/package-lock.json b/package-lock.json index 37a3fa67..0477b923 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "policy-design-lab", - "version": "0.9.0", + "version": "0.10.0", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index 86774664..44c9dc71 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "policy-design-lab", - "version": "0.9.0", + "version": "0.10.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/ProgramDrawer.tsx b/src/components/ProgramDrawer.tsx index 6afcd731..a30e1e82 100644 --- a/src/components/ProgramDrawer.tsx +++ b/src/components/ProgramDrawer.tsx @@ -23,6 +23,8 @@ ProgramDrawer.propTypes = { }; let currentChecked = 0; +const menuHeight = window.innerHeight < 900 ? "38%" : "40%"; + function EQIPCheckboxList({ setEQIPChecked, setShowPopUp, zeroCategory }) { const [checked, setChecked] = React.useState(currentChecked); @@ -436,10 +438,78 @@ function CRPCheckboxList({ setCRPChecked, setShowPopUp, zeroCategory }) { ); } +function RCPPCheckboxList({ setRCPPChecked, setShowPopUp, zeroCategory }) { + const [checked, setChecked] = React.useState(currentChecked); + + const handleToggle = (value: number) => () => { + setChecked(value); + setRCPPChecked(value); + currentChecked = value; + setShowPopUp(false); + }; + + const RCPPList = ["Total RCPP"]; + + return ( + + {RCPPList.map((category, value) => { + const labelId = `checkbox-list-label-${value}`; + if (zeroCategory && zeroCategory.includes(category)) { + return ( + + + + + + + ); + } + return ( + + + + + + + + + ); + })} + + ); +} + interface ProgramDrawerProps { setEQIPChecked?: (value: number) => void; setCSPChecked?: (value: number) => void; setCRPChecked?: (value: number) => void; + setRCPPChecked?: (value: number) => void; zeroCategories?: string[]; } @@ -447,6 +517,7 @@ export default function ProgramDrawer({ setEQIPChecked, setCSPChecked, setCRPChecked, + setRCPPChecked, zeroCategories }: ProgramDrawerProps): JSX.Element { const location = useLocation(); @@ -507,8 +578,42 @@ export default function ProgramDrawer({ prevCrpOpen.current = crpOpen; }, [crpOpen]); + const [rcppOpen, setRcppOpen] = React.useState(false); + const rcppRef = React.useRef(null); + const handleRcppClick = () => { + if (location.pathname !== "/rcpp") { + navigate("/rcpp"); + window.location.reload(false); + } + }; + const prevRcppOpen = React.useRef(rcppOpen); + React.useEffect(() => { + if (prevRcppOpen.current && !rcppOpen) { + rcppRef.current.focus(); + } + + prevRcppOpen.current = rcppOpen; + }, [rcppOpen]); + + // ACEP Menu + const [acepOpen, setAcepOpen] = React.useState(false); + const acepRef = React.useRef(null); + const handleAcepClick = () => { + if (location.pathname !== "/acep") { + navigate("/acep"); + window.location.reload(false); + } else { + setAcepOpen((prevAcepOpen) => !prevAcepOpen); + } + }; + const prevAcepOpen = React.useRef(acepOpen); + React.useEffect(() => { + if (prevAcepOpen.current && !acepOpen) { + acepRef.current.focus(); + } - const crpMenuHeight = window.innerHeight < 900 ? "38%" : "40%"; + prevAcepOpen.current = acepOpen; + }, [acepOpen]); return ( - + Total Conservation Programs Benefits @@ -570,7 +675,7 @@ export default function ProgramDrawer({ anchorEl={eqipRef.current} role={undefined} placement="right-start" - sx={{ height: "50%", overflowY: "scroll", maxWidth: "20%" }} + sx={{ height: "50%", overflowY: "auto", maxWidth: "20%" }} > - - ACEP: Agriculture Conservation Easement Program - - - RCPP: Regional Conservation Partnership Program - + + + + {location.pathname === "/acep" ? ( + + ACEP: Agriculture Conservation Easement Program + + ) : ( + ACEP: Agriculture Conservation Easement Program + )} + + + + + + + {location.pathname === "/rcpp" ? ( + + RCPP: Regional Conservation Partnership Program + + ) : ( + RCPP: Regional Conservation Partnership Program + )} + {/* + */} + + + Other Conservation diff --git a/src/components/SemiDonutChart.tsx b/src/components/SemiDonutChart.tsx index d41e4d3c..dcc94073 100644 --- a/src/components/SemiDonutChart.tsx +++ b/src/components/SemiDonutChart.tsx @@ -14,7 +14,7 @@ export default function SemiDonutChart({ data, label1, label2 }: any): JSX.Eleme return ( cx ? "start" : "end"} dominantBaseline="central"> - {`${(percent * 100).toFixed(1)}%`} + {`${(percent * 100).toFixed(2)}%`} ); }; diff --git a/src/components/acep/ACEPCategoryMap.tsx b/src/components/acep/ACEPCategoryMap.tsx new file mode 100644 index 00000000..0a1e5f62 --- /dev/null +++ b/src/components/acep/ACEPCategoryMap.tsx @@ -0,0 +1,289 @@ +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 Divider from "@mui/material/Divider"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import PropTypes from "prop-types"; +import * as d3 from "d3"; +import "../../styles/map.css"; +import legendConfig from "../../utils/legendConfig.json"; +import DrawLegend from "../shared/DrawLegend"; +import { getValueFromAttrDollar, getValueFromAttrPercentage } from "../../utils/apiutil"; + +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 = (props) => { + const { year, setTooltipContent, category, allStates, stateCodes, statePerformance, colorScale, attribute } = props; + + return ( +
+ + + {({ geographies }) => ( + <> + {geographies.map((geo) => { + let keyDollar; + let keyPercentage = ""; + const record = statePerformance[year].filter((v) => v.state === geo.properties.name)[0]; + if (record === undefined || record.length === 0) { + return null; + } + if (attribute === "contracts") { + keyDollar = "totalContracts"; + keyPercentage = "contractsInPercentageNationwide"; + } else if (attribute === "acres") { + keyDollar = "totalAcres"; + keyPercentage = "acresInPercentageNationwide"; + } else { + keyDollar = getValueFromAttrDollar(record.programs[0], attribute); + keyPercentage = getValueFromAttrPercentage(record.programs[0], attribute); + } + const categoryPayment = record.programs[0][keyDollar]; + const nationwidePercentage = record.programs[0][keyPercentage]; + const hoverContent = ( + + + {geo.properties.name} + + + {Number(categoryPayment) < 1000000 + ? `$${Number(Number(categoryPayment) / 1000.0).toLocaleString( + undefined, + { + maximumFractionDigits: 2 + } + )}K` + : `$${Number( + Number(categoryPayment) / 1000000.0 + ).toLocaleString(undefined, { + maximumFractionDigits: 2 + })}M`} + + + + {nationwidePercentage ? `${nationwidePercentage} %` : "0%"} + + + + + ); + const fillColour = () => { + if (categoryPayment) { + if (categoryPayment !== 0) return colorScale(categoryPayment); + return "#D2D2D2"; + } + return "#D2D2D2"; + }; + return ( + { + setTooltipContent(hoverContent); + }} + onMouseLeave={() => { + setTooltipContent(""); + }} + fill={fillColour()} + stroke="#FFFFFF" + 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 = { + year: PropTypes.string, + setTooltipContent: PropTypes.func, + category: PropTypes.string +}; + +const CategoryMap = ({ + year, + category, + attribute, + statePerformance, + allStates, + stateCodes +}: { + year: string; + category: string; + attribute: string; + statePerformance: any; + allStates: any; + stateCodes: any; +}): JSX.Element => { + const [content, setContent] = useState(""); + const title = `ACEP ${category} from ${year}`; + const quantizeArray: number[] = []; + const zeroPoints = []; + + statePerformance[year].forEach((value) => { + const programRecord = value.programs; + const ACur = programRecord[0]; + let key = ""; + if (attribute === "contracts") key = "totalContracts"; + else if (attribute === "acres") key = "totalAcres"; + else { + key = getValueFromAttrDollar(ACur, attribute); + key = key !== "" ? key : attribute; + } + quantizeArray.push(ACur[key]); + ACur[key] === 0 && zeroPoints.push(value.state); + return null; + }); + const years = "2018-2022"; + const maxValue = Math.max(...quantizeArray); + const mapColor = ["#F0F9E8", "#BAE4BC", "#7BCCC4", "#43A2CA", "#0868AC"]; + const customScale = legendConfig[category]; + const colorScale = d3.scaleThreshold(customScale, mapColor); + return ( +
+ {maxValue !== 0 ? ( + + {maxValue !== 0 ? ( + + ) : ( +
+ {titleElement(category, years)} + + + {category} data in {years} is unavailable for all states. + + +
+ )} +
+ ) : ( + + + + {title} + + + + + {title} data is unavailable for all states. + + + + )} + +
+ + {content} + +
+
+ ); +}; +const titleElement = (attribute, year): JSX.Element => { + return ( + + + {attribute} from {year} + {" "} + + In any state that appears in grey, there is no available data + + + ); +}; +export default CategoryMap; diff --git a/src/components/acep/ACEPTable.tsx b/src/components/acep/ACEPTable.tsx new file mode 100644 index 00000000..d54b53c8 --- /dev/null +++ b/src/components/acep/ACEPTable.tsx @@ -0,0 +1,396 @@ +import React from "react"; +import styled from "styled-components"; +import { useTable, useSortBy, usePagination } from "react-table"; +import { Grid, TableContainer, Typography, Box } from "@mui/material"; +import { + compareWithNumber, + compareWithAlphabetic, + compareWithDollarSign, + compareWithPercentSign +} from "../shared/TableCompareFunctions"; +import "../../styles/table.css"; + +function AcepProgramTable({ + tableTitle, + program, + attributes, + stateCodes, + AcepData, + year, + colors, + skipColumns +}): JSX.Element { + const resultData = []; + const hashmap = {}; + // eslint-disable-next-line no-restricted-syntax + AcepData[year].forEach((stateData) => { + const state = stateData.state; + let programData = null; + programData = stateData.programs.filter((p) => { + return p.programName.toString() === program; + }); + hashmap[state] = {}; + attributes.forEach((attribute) => { + const attributeData = programData[0][attribute]; + hashmap[state][attribute] = attributeData; + }); + }); + Object.keys(hashmap).forEach((s) => { + const newRecord = { + state: Object.values(stateCodes).filter((stateCode) => { + return stateCode === s; + })[0] + }; + Object.entries(hashmap[s]).forEach(([attr, value]) => { + if (attr.includes("Percentage")) { + newRecord[attr] = `${value.toString()}%`; + } else if (attr === "totalAcres" || attr === "totalContracts") { + newRecord[attr] = `${ + value.toLocaleString(undefined, { minimumFractionDigits: 2 }).toString().split(".")[0] + }`; + } else { + newRecord[attr] = `$${ + value.toLocaleString(undefined, { minimumFractionDigits: 2 }).toString().split(".")[0] + }`; + } + }); + resultData.push(newRecord); + }); + const columnPrep = []; + columnPrep.push({ Header: "STATE", accessor: "state", sortType: compareWithAlphabetic }); + attributes.forEach((attribute) => { + let sortMethod = compareWithDollarSign; + if (attribute.includes("Percentage")) sortMethod = compareWithPercentSign; + if (attribute.includes("totalContracts") || attribute.includes("totalAcres")) sortMethod = compareWithNumber; + let attrName = attribute + .replace(/([A-Z])/g, " $1") + .trim() + .split(" ") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" ") + .toUpperCase(); + if (attribute === "assistancePaymentInDollars") attrName = "Total Payment in Dollars".toUpperCase(); + if (attribute === "assistancePaymentInPercentageNationwide") + attrName = "Total Payment In Percentage Nationwide".toUpperCase(); + const json = { + Header: attrName, + accessor: attribute, + sortType: sortMethod + }; + columnPrep.push(json); + }); + const columns = React.useMemo(() => columnPrep, []); + const paymentsIndex = columns.findIndex((c) => c.accessor === "assistancePaymentInDollars"); + const acresIndex = columns.findIndex((c) => c.accessor === "totalAcres"); + const contractsIndex = columns.findIndex((c) => c.accessor === "totalContracts"); + const paymentsPercentageIndex = columns.findIndex((c) => c.accessor === "assistancePaymentInPercentageNationwide"); + const contractsPercentageIndex = columns.findIndex((c) => c.accessor === "contractsInPercentageNationwide"); + const acresPercentageIndex = columns.findIndex((c) => c.accessor === "acresInPercentageNationwide"); + const Styles = styled.div` + padding: 0; + margin: 0; + + table { + border-spacing: 0; + border: 1px solid #e4ebe7; + border-left: none; + border-right: none; + width: 100%; + + 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$="cell${paymentsIndex}"] { + background-color: ${colors[0]}; + } + + td[class$="cell${paymentsPercentageIndex}"] { + background-color: ${colors[0]}; + } + td[class$="cell${contractsIndex}"] { + background-color: ${colors[1]}; + } + td[class$="cell${contractsPercentageIndex}"] { + background-color: ${colors[1]}; + } + td[class$="cell${acresIndex}"] { + background-color: ${colors[2]}; + } + td[class$="cell${acresPercentageIndex}"] { + background-color: ${colors[2]}; + } + + 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; + } + } + + table .tableArrow{ + margin-left: 8px; + } + } + .pagination { + margin-top: 1.5em; + } + + @media screen and (max-width: 1440px) { + table { + font-size: 0.9em; + + th:not(:first-of-type) { + text-align: left; + } + } + table th, + table td { + padding: 1em; + text-align: left; + } + .pagination { + margin-top: 8px; + } + } + + .acepBox > .stateTitle { + margin-top: 0.5em; + font-size: 1.2em; + text-align: left; + margin-bottom: 1em; + padding-top: 0.5em; + } + + . + `; + return ( + + + + + + + Comparing {tableTitle} ({year}) + + + + + + !skipColumns.includes(column.accessor))} + data={resultData} + initialState={{ + pageSize: 5, + pageIndex: 0 + }} + /> + + + + ); +} + +// eslint-disable-next-line +function Table({ columns, data, initialState }: { columns: any; data: any; initialState: any }) { + 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, + state + }, + useSortBy, + usePagination + ); + return ( +
+
+ + {headerGroups.map((headerGroup) => ( + + {headerGroup.headers.map((column) => ( + // Add the sorting props to control sorting. + + ))} + + ))} + + + { + // eslint-disable-next-line + page.map((row, i) => { + prepareRow(row); + return ( + + {row.cells.map((cell, j) => { + return ( + + ); + })} + + ); + }) + } + +
+ {column.render("Header")} + + {(() => { + if (!column.isSorted) + return ( + + {"\u{2B83}"} + + ); + 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 {parseInt(pageSize, 10) * (pageIndex + 1)} results of {rows.length} rows + + ) : ( + + Showing the first {rows.length} results of {rows.length}rows + + )} + + + + ); +} + +export default AcepProgramTable; diff --git a/src/components/acep/ACEPTotalMap.tsx b/src/components/acep/ACEPTotalMap.tsx new file mode 100644 index 00000000..1cc1c1ae --- /dev/null +++ b/src/components/acep/ACEPTotalMap.tsx @@ -0,0 +1,258 @@ +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 Divider from "@mui/material/Divider"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import * as d3 from "d3"; +import PropTypes from "prop-types"; +import "../../styles/map.css"; +import legendConfig from "../../utils/legendConfig.json"; +import DrawLegend from "../shared/DrawLegend"; + +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 = (props) => { + const { setTooltipContent, allStates, statePerformance, year, stateCodes, colorScale } = props; + + return ( +
+ {allStates.length > 0 && statePerformance[year] !== undefined ? ( + + + {({ geographies }) => ( + <> + {geographies.map((geo) => { + const record = statePerformance[year].filter( + (v) => v.state === geo.properties.name + )[0]; + if (record === undefined || record.length === 0) { + return null; + } + const totalPaymentInDollars = record.programs[0].assistancePaymentInDollars; + const assistancePaymentInPercentageNationwide = + record.programs[0].assistancePaymentInPercentageNationwide; + const hoverContent = ( + + + {geo.properties.name} + + + {Number(totalPaymentInDollars) < 1000000 + ? `$${Number( + Number(totalPaymentInDollars) / 1000.0 + ).toLocaleString(undefined, { + maximumFractionDigits: 2 + })}K` + : `$${Number( + Number(totalPaymentInDollars) / 1000000.0 + ).toLocaleString(undefined, { + maximumFractionDigits: 2 + })}M`} + + + + {assistancePaymentInPercentageNationwide + ? `${assistancePaymentInPercentageNationwide} %` + : "0%"} + + + + + ); + const fillColour = () => { + if (totalPaymentInDollars) { + if (totalPaymentInDollars !== 0) return colorScale(totalPaymentInDollars); + return "#D2D2D2"; + } + return "#D2D2D2"; + }; + return ( + { + setTooltipContent(hoverContent); + }} + onMouseLeave={() => { + setTooltipContent(""); + }} + 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 = { + setTooltipContent: PropTypes.func +}; + +const ACEPTotalMap = ({ + program, + attribute, + year, + statePerformance, + stateCodes, + allStates +}: { + program: string; + attribute: any; + year: string; + statePerformance: any; + stateCodes: any; + allStates: any; +}): JSX.Element => { + const [content, setContent] = useState(""); + const quantizeArray: number[] = []; + const zeroPoints = []; + statePerformance[year].forEach((value) => { + const programRecord = value.programs; + const ACur = programRecord.find((s) => s.programName === program); + const key = "assistancePaymentInDollars"; + quantizeArray.push(ACur[key]); + ACur[key] === 0 && zeroPoints.push(value.state); + return null; + }); + const category = "Total ACEP"; + const years = "2018-2022"; + const maxValue = Math.max(...quantizeArray); + const mapColor = ["#F0F9E8", "#BAE4BC", "#7BCCC4", "#43A2CA", "#0868AC"]; + const customScale = legendConfig[category]; + const colorScale = d3.scaleThreshold(customScale, mapColor); + return ( +
+
+ + {maxValue !== 0 ? ( + + ) : ( +
+ {titleElement(category, years)} + + + {category} data in {years} 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 ACEPTotalMap; diff --git a/src/components/acep/AcepTreeMap.tsx b/src/components/acep/AcepTreeMap.tsx new file mode 100644 index 00000000..bb47c264 --- /dev/null +++ b/src/components/acep/AcepTreeMap.tsx @@ -0,0 +1,390 @@ +import * as React from "react"; +import * as d3 from "d3"; +import styled from "styled-components"; +import { Checkbox, FormControlLabel, FormGroup, Grid, IconButton, SvgIcon, Typography } from "@mui/material"; +import SortIcon from "@mui/icons-material/Sort"; +import TreeMapSquares from "./TreeMapSquares"; +import { DownloadIcon } from "../shared/DownloadIcon"; + +const Styles = styled.div` + ".muibuttonbase-root, muicheckbox-root:hover": { + background: "none"; + } +`; +const sortDataByAttribute = (data, attr) => { + data.sort((a, b) => { + if (a[attr] < b[attr]) { + return 1; + } + if (a[attr] > b[attr]) { + return -1; + } + return 0; + }); + return data; +}; +const scaling = (value, scaling_factor, base, max, min, maxCut, minCut) => { + let temp = Math.min(Math.max(value, minCut), maxCut); + temp = Math.log(temp * (base ** scaling_factor - 1) + 1) / (Math.log(base) * scaling_factor); + return value === 0 ? 0 : temp * (max - min) + min; +}; +const transform = (data) => { + let minacres = Infinity; + let maxacres = -Infinity; + let minpayments = Infinity; + let maxpayments = -Infinity; + let mincontracts = Infinity; + let maxcontracts = -Infinity; + data.forEach((stateData) => { + minacres = Math.min(minacres, stateData.acres); + maxacres = Math.max(maxacres, stateData.acres); + minpayments = Math.min(minpayments, stateData.payments); + maxpayments = Math.max(maxpayments, stateData.payments); + mincontracts = Math.min(mincontracts, stateData.contracts); + maxcontracts = Math.max(maxcontracts, stateData.contracts); + }); + const res = data.map((stateData) => { + const transformedJson = { acres: 0, payments: 0, contracts: 0, state: "" }; + transformedJson.acres = stateData.acres === 0 ? 0 : (stateData.acres - minacres) / (maxacres - minacres); + transformedJson.payments = + stateData.payments === 0 ? 0 : (stateData.payments - minpayments) / (maxpayments - minpayments); + transformedJson.contracts = + stateData.contracts === 0 ? 0 : (stateData.contracts - mincontracts) / (maxcontracts - mincontracts); + transformedJson.state = stateData.state; + return transformedJson; + }); + const res2 = res.map((stateData) => { + const transformedJson = { acres: 0, payments: 0, contracts: 0, state: "" }; + transformedJson.acres = scaling(stateData.acres, 1, 10, 0.9, 0.2, 1, 0); + transformedJson.payments = scaling(stateData.payments, 1, 10, 1, 0.1, 1, 0); + transformedJson.contracts = scaling(stateData.contracts, 1, 10, 0.7, 0.05, 1, 0); + transformedJson.state = stateData.state; + return transformedJson; + }); + return res2; +}; +export default function AcepTreeMap({ program, TreeMapData, year, stateCodes, svgW, svgH }): JSX.Element { + const paymentsColor = "#1F78B4"; + const acresColor = "#66BB6A"; + const contractsColor = "#C81194"; + const stCodes = stateCodes; + const rn = React.useRef(null); + const acepDiv = React.useRef(null); + const [sortPaymentButtonColor, setPaymentSortButtonColor] = React.useState(paymentsColor); + const [sortBaseAcresButtonColor, setSortBaseAcresButtonColor] = React.useState("#CCC"); + const [sortRecipientsButtonColor, setSortRecipientsButtonColor] = React.useState("#CCC"); + const [AcepTreeMapIllustration, setAcepTreeMapIllustration] = React.useState(window.innerWidth * 0.06); + const [chartData, setChartData] = React.useState(sortDataByAttribute(transform(TreeMapData[1]), "payments")); + const [availableAttributes, setAvailableAttributes] = React.useState(["payments", "acres", "contracts"]); + const [svgWidth, setSvgWidth] = React.useState(svgW); + const [svgHeight, setSvgHeight] = React.useState(svgH); + const [checkedState, setCheckedState] = React.useState({ + paymentsChecked: true, + acresChecked: true, + contractsChecked: true + }); + const { paymentsChecked, acresChecked, contractsChecked } = checkedState; + let widthPercentage = 0.7; + const heightPercentage = 0.8; + if (window.innerWidth <= 1440) { + widthPercentage = 0.6; + } + const handleResize: () => void = () => { + setSvgWidth(window.innerWidth * widthPercentage); + setSvgHeight(window.innerHeight * heightPercentage); + setAcepTreeMapIllustration(window.innerWidth * 0.05); + }; + React.useEffect(() => { + window.addEventListener("resize", handleResize); + drawIllustration(); + return () => window.removeEventListener("resize", handleResize); + }); + const handleSortClick = (e, attr) => { + setPaymentSortButtonColor("#CCC"); + setSortBaseAcresButtonColor("#CCC"); + setSortRecipientsButtonColor("#CCC"); + Array.from(document.querySelectorAll(".sortIcon")).forEach((el) => { + if (el.parentElement === e.currentTarget) { + if (el.classList.contains("sortPayments")) { + setPaymentSortButtonColor(paymentsColor); + setChartData(sortDataByAttribute(transform(TreeMapData[1]), "payments")); + } + if (el.classList.contains("sortBaseAcres")) { + setSortBaseAcresButtonColor(acresColor); + setChartData(sortDataByAttribute(transform(TreeMapData[1]), "acres")); + } + if (el.classList.contains("sortRecipients")) { + setSortRecipientsButtonColor(contractsColor); + setChartData(sortDataByAttribute(transform(TreeMapData[1]), "contracts")); + } + } + }); + }; + const handleSquareChange = (event: React.ChangeEvent) => { + const checkedList: string[] = []; + const temp = { + ...checkedState, + [event.target.name]: event.target.checked + }; + if (temp.acresChecked === true) checkedList.push("acres"); + if (temp.paymentsChecked === true) checkedList.push("payments"); + if (temp.contractsChecked === true) checkedList.push("contracts"); + setCheckedState(temp); + setAvailableAttributes(checkedList); + }; + const downloadSVG = (status) => { + if (acepDiv.current !== undefined && status) { + const svgElement = acepDiv.current.querySelector("#AcepTreeMap"); + const svgData = new XMLSerializer().serializeToString(svgElement); + const blob = new Blob([svgData], { type: "image/svg+xml" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = "acep-treemap.svg"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + URL.revokeObjectURL(url); + } + }; + function drawIllustration() { + d3.select(rn.current).selectAll("*").remove(); + if (chartData[0].payments !== 0) { + const test = d3 + .select(rn.current) + .append("rect") + .attr("x", 0) + .attr("y", 0) + .attr("width", AcepTreeMapIllustration) + .attr("height", AcepTreeMapIllustration) + .attr("fill", paymentsColor); + } + if (chartData[0].acres !== 0) { + d3.select(rn.current) + .append("rect") + .attr("x", 0) + .attr("y", AcepTreeMapIllustration * 0.3) + .attr("width", AcepTreeMapIllustration * 0.7) + .attr("height", AcepTreeMapIllustration * 0.7) + .attr("fill", acresColor); + } + if (chartData[0].contracts !== 0) { + d3.select(rn.current) + .append("rect") + .attr("x", 0) + .attr("y", AcepTreeMapIllustration * 0.6) + .attr("width", AcepTreeMapIllustration * 0.4) + .attr("height", AcepTreeMapIllustration * 0.4) + .attr("fill", contractsColor); + } + } + + /* eslint-disable */ + return ( + + + + + + {program.includes("(") + ? `Comparing Total ${program + .match(/\((.*?)\)/g) + .map((match) => match.slice(1, -1))} Benefits, Acres and No. of Contracts (${year})` + : `Comparing Total ${program} Benefits, Acres and No. of Contracts (${year})`} + { + event.stopPropagation(); + downloadSVG(true); + }} + /> + + + + {" "} + + Hover over the squares to view detailed data. +
+ The size differences of the squares represent the differences in relative amount{" "} + within the same category. For example, a larger purple square indicate a higher + number of no. of contracts compared to another smaller purple square, but it does not + necessarily indicate a greater number of no. of contracts compared to a smaller green square + representing acres. +
+
+
+ + + + + + {chartData[0].payments !== 0 ? ( + handleSortClick(event, "payments")} + sx={{ + borderRadius: "2px" + }} + > + + + ) : null} + {chartData[0].acres !== 0 ? ( + handleSortClick(event, "acres")} + sx={{ + borderRadius: "2px" + }} + > + + + ) : null} + {chartData[0].contracts !== 0 ? ( + handleSortClick(event, "contracts")} + sx={{ + borderRadius: "2px" + }} + > + + + ) : null} + + + + + {chartData[0].payments !== 0 ? ( + + } + label="Total Benefits ($)" + sx={{ color: paymentsColor }} + /> + ) : null} + {chartData[0].acres !== 0 ? ( + + } + label="Acres" + sx={{ color: acresColor }} + /> + ) : null} + {chartData[0].contracts !== 0 ? ( + + } + label="No. of Contracts" + sx={{ color: contractsColor }} + /> + ) : null} + + + + + + + + + Click the buttons + above to sort squares by total benefits, acres or no. of contracts. + + + +
+ + + + + +
+ ); + /* eslint-enable */ +} diff --git a/src/components/acep/TreeMapSquares.tsx b/src/components/acep/TreeMapSquares.tsx new file mode 100644 index 00000000..a775c77f --- /dev/null +++ b/src/components/acep/TreeMapSquares.tsx @@ -0,0 +1,251 @@ +import React, { useState } from "react"; +import styled from "@emotion/styled"; +import * as d3 from "d3"; +import { ShortFormat } from "../shared/ConvertionFormats"; + +export default function TreeMapSquares({ + svgWidth, + svgHeight, + stateCodes, + originalData, + chartData, + color, + availableAttributes, + program +}): JSX.Element { + const Styles = styled.div` + #AcepTreeMap { + font-family: "Roboto", sans-serif; + } + `; + + const rn = React.useRef(null); + React.useEffect(() => { + if (chartData) { + d3.select(rn.current).selectAll(".base").remove(); + const base = d3.select(rn.current).append("g").attr("class", "base"); + const margin = 30; + const lineMargin = 80; + const baseSize = 10; + const stepSize = 20; + let rowTrack = 0; + let largestSquare = 200; + if (window.innerWidth >= 1920) { + largestSquare = 250; + } + let yTrack = largestSquare + lineMargin; + const d = chartData.filter( + (stateData) => stateData.acres !== 0 || stateData.payments !== 0 || stateData.contracts !== 0 + ); + let i = 0; + let lineNumber = 0; + let maxInLine = 0; + while (i < d.length) { + const stateData = d[i]; + if ( + rowTrack + margin <= + svgWidth - largestSquare * Math.max(stateData.acres, stateData.payments, stateData.contracts) + ) { + const squareGroup = base.append("g").attr("class", "squareGroup"); + const re_sorted = [stateData.acres, stateData.payments, stateData.contracts].sort((a, b) => b - a); + maxInLine = re_sorted[0]; + const acresOriginalData = ShortFormat(originalData.find((s) => s.state === stateData.state).acres); + const paymentsOriginalData = ShortFormat( + originalData.find((s) => s.state === stateData.state).payments + ); + const contractsOriginalData = ShortFormat( + originalData.find((s) => s.state === stateData.state).contracts + ); + const collectedOriginalData = { + acres: acresOriginalData, + payments: paymentsOriginalData, + contracts: contractsOriginalData + }; + for (let index = 0; index < re_sorted.length; index += 1) { + const value = re_sorted[index]; + for (let j = 0; j < Object.values(stateData).length; j += 1) { + const v = Object.values(stateData)[j]; + if (Number(v) === value) { + const [key] = Object.entries(stateData).find(([_, val]) => val === value) || []; + if (key && availableAttributes.includes(key)) { + squareGroup + .append("rect") + .attr("class", "TreeMapSquare") + .attr("width", value * largestSquare) + .attr("height", value * largestSquare) + .attr("x", rowTrack) + .attr("y", yTrack - value * largestSquare) + .attr("fill", color[key]); + const firstFewLines = window.innerWidth >= 1920 ? 3 : 2; + if (yTrack <= (largestSquare + lineMargin) * firstFewLines) { + const inSquareText = squareGroup + .append("text") + .attr("id", `inSquareText${value}`) + .text( + key === "payments" + ? `$${collectedOriginalData[key]}` + : collectedOriginalData[key] + ) + .style("font-size", "0.7em") + .style("fill", "white"); + const textLength = inSquareText.node().getComputedTextLength(); + if (rowTrack + value * largestSquare - textLength - 10 > 0) { + inSquareText + .attr("x", rowTrack + value * largestSquare - textLength - 10) + .attr("y", yTrack - value * largestSquare + 18); + } else { + inSquareText.attr("x", 0).attr("y", yTrack - value * largestSquare + 18); + } + } + } + } + } + } + squareGroup + .on("mouseover", function (e) { + base.selectAll(".TreeMapSquareTip").remove(); + // eslint-disable-next-line no-restricted-globals + const mousePos = d3.pointer(event, squareGroup.node()); + const tipGroup = base.append("g").attr("class", "TreeMapSquareTip"); + const xPosition = mousePos[0] + 160 > svgWidth ? mousePos[0] - 160 : mousePos[0]; + const tipHeight = 100; + tipGroup + .append("rect") + .attr("x", xPosition) + .attr("y", mousePos[1]) + .attr("width", 155) + .attr("height", tipHeight) + .attr("rx", 5) + .attr("ry", 5) + .attr("fill", "#2F7164") + .style("opacity", 0.8) + .style("z-index", 100000); + tipGroup + .append("text") + .text( + `${ + Object.values(stateCodes).filter( + (stateCode) => stateCode === stateData.state + )[0] + }` + ) + .attr("x", xPosition + baseSize) + .attr("y", mousePos[1] + stepSize * 1) + .style("font-size", "0.9em") + .style("font-weight", "700") + .style("fill", "white"); + tipGroup + .append("text") + .text(`Total Benefits: $${paymentsOriginalData}`) + .attr("x", xPosition + baseSize) + .attr("y", mousePos[1] + stepSize * 2) + .style("font-size", "0.8em") + .style("fill", "white"); + + tipGroup + .append("text") + .text(`Acres: ${acresOriginalData}`) + .attr("x", xPosition + baseSize) + .attr("y", mousePos[1] + stepSize * 3) + .style("font-size", "0.8em") + .style("fill", "white"); + tipGroup + .append("text") + .text(`No. of Contracts: ${contractsOriginalData}`) + .attr("x", xPosition + 10) + .attr("y", mousePos[1] + stepSize * 4) + .style("font-size", "0.8em") + .style("fill", "white"); + }) + .on("mouseleave", function (e) { + base.selectAll(".TreeMapSquareTip").remove(); + }); + const stateName = squareGroup + .append("text") + .text(Object.values(stateCodes).filter((stateCode) => stateCode === stateData.state)[0]) + .style("font-size", "0.75em"); + const textLength = stateName.node().getComputedTextLength(); + if (rowTrack + (maxInLine * largestSquare) / 2 - textLength / 2 < 0) { + stateName.attr("x", 0).attr("y", yTrack + 16); + } else { + stateName + .attr("x", rowTrack + (maxInLine * largestSquare) / 2 - textLength / 2) + .attr("y", yTrack + 16); + } + rowTrack = rowTrack + maxInLine * largestSquare + margin * 2; + i += 1; + } else { + let max = 0; + for (let temp = 1; temp <= 5; temp += 1) { + if (chartData[temp + 1]) { + const nextOne = chartData[temp + 1]; + max = + max > [nextOne.acres, nextOne.payments, nextOne.contracts].sort((a, b) => b - a)[0] + ? max + : [nextOne.acres, nextOne.payments, nextOne.contracts].sort((a, b) => b - a)[0]; + } + } + if (max <= 0.8) { + yTrack = yTrack + largestSquare * max + lineMargin * 0.5; + } else { + yTrack = yTrack + largestSquare + lineMargin; + } + lineNumber += 1; + rowTrack = 0; + } + } + // check if see if any of state has all zeros + const zeroData = chartData.filter( + (stateData) => stateData.acres === 0 && stateData.payments === 0 && stateData.contracts === 0 + ); + let j = 0; + lineNumber = 0; + yTrack = rowTrack !== margin * 0.8 ? yTrack : yTrack - 20; + while (j < zeroData.length) { + const stateData = zeroData[j]; + if (stateData) { + if ( + rowTrack + margin <= + svgWidth - largestSquare * Math.max(stateData.acres, stateData.payments, stateData.contracts) + ) { + const zeros = base.append("g").attr("class", "ZeroGroup"); + zeros + .append("rect") + .attr("width", 20) + .attr("height", 20) + .attr("x", rowTrack) + .attr("y", yTrack - 20) + .attr("fill", "#DDD"); + zeros + .append("text") + .text("0") + .attr("x", rowTrack + 6.5) + .attr("y", yTrack - 5) + .style("font-size", "0.8em") + .style("fill", "white"); + const underSquare = zeros.append("text").text(stateData.state).style("font-size", "0.8em"); + const textLength = underSquare.node().getComputedTextLength(); + if (rowTrack + (20 - textLength) / 2 < 0) { + underSquare.attr("x", 0).attr("y", yTrack + 16); + } else { + underSquare.attr("x", rowTrack + (20 - textLength) / 2).attr("y", yTrack + 16); + } + rowTrack = rowTrack + 20 + margin * 2; + j += 1; + } else { + yTrack = yTrack + largestSquare * 0.8 - lineNumber * 20; + lineNumber += 5; + rowTrack = 0; + } + } + } + d3.select(rn.current).attr("height", yTrack + largestSquare); + } + }); + + return ( + + + + ); +} diff --git a/src/components/cropinsurance/CropInsuranceMap.tsx b/src/components/cropinsurance/CropInsuranceMap.tsx index 636ba943..e418f666 100644 --- a/src/components/cropinsurance/CropInsuranceMap.tsx +++ b/src/components/cropinsurance/CropInsuranceMap.tsx @@ -55,8 +55,8 @@ const MapChart = ({ return null; } const key = - getValueFromAttr(state.programs[0], attribute) !== "" - ? getValueFromAttr(state.programs[0], attribute) + getValueFromAttrDollar(state.programs[0], attribute) !== "" + ? getValueFromAttrDollar(state.programs[0], attribute) : "totalNetFarmerBenefit"; programPayment = state.programs[0][key]; const hoverContent = ( @@ -196,7 +196,7 @@ const CropInsuranceMap = ({ statePerformance[year].forEach((value) => { const programRecord = value.programs; const ACur = programRecord.find((s) => s.programName === program); - let key = getValueFromAttr(ACur, attribute); + let key = getValueFromAttrDollar(ACur, attribute); key = key !== "" ? key : "totalNetFarmerBenefit"; quantizeArray.push(ACur[key]); ACur[key] === 0 && zeroPoints.push(value.state); @@ -309,7 +309,7 @@ const titleElement = ({ attribute, year }): JSX.Element => { ); }; -const getValueFromAttr = (stateRecord, attribute): string => { +const getValueFromAttrDollar = (stateRecord, attribute): string => { let ans = ""; Object.keys(stateRecord).forEach((key) => { const match = key.match(/^(.*?)(?=\s*InDollars)/); diff --git a/src/components/crp/CRPTotalMap.tsx b/src/components/crp/CRPTotalMap.tsx index 0eb0a188..7fe1cf24 100644 --- a/src/components/crp/CRPTotalMap.tsx +++ b/src/components/crp/CRPTotalMap.tsx @@ -10,7 +10,7 @@ import PropTypes from "prop-types"; import "../../styles/map.css"; import legendConfig from "../../utils/legendConfig.json"; import DrawLegend from "../shared/DrawLegend"; -import { getValueFromAttr } from "../../utils/apiutil"; +import { getValueFromAttrDollar } from "../../utils/apiutil"; const geoUrl = "https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json"; @@ -186,7 +186,7 @@ const CRPTotalMap = ({ statePerformance[year].forEach((value) => { const programRecord = value.programs; const ACur = programRecord.find((s) => s.programName === program); - let key = getValueFromAttr(ACur, attribute); + let key = getValueFromAttrDollar(ACur, attribute); key = key !== "" ? key : attribute; quantizeArray.push(ACur[key]); ACur[key] === 0 && zeroPoints.push(value.state); diff --git a/src/components/crp/CRPTotalTable.tsx b/src/components/crp/CRPTotalTable.tsx index 0853e7ef..cc4089e6 100644 --- a/src/components/crp/CRPTotalTable.tsx +++ b/src/components/crp/CRPTotalTable.tsx @@ -6,7 +6,7 @@ import "../../styles/table.css"; const Styles = styled.div` padding: 1rem; - margin-left: ${window.innerWidth <= 1440 ? "480px" : "auto"}; + ${window.innerWidth <= 1440 ? "margin-left: 480px" : ""}; table { border-spacing: 0; diff --git a/src/components/crp/CategoryMap.tsx b/src/components/crp/CategoryMap.tsx index 07e8a20f..e3ab9cba 100644 --- a/src/components/crp/CategoryMap.tsx +++ b/src/components/crp/CategoryMap.tsx @@ -11,7 +11,7 @@ import * as d3 from "d3"; import "../../styles/map.css"; import legendConfig from "../../utils/legendConfig.json"; import DrawLegend from "../shared/DrawLegend"; -import { getValueFromAttr } from "../../utils/apiutil"; +import { getValueFromAttrDollar } from "../../utils/apiutil"; const geoUrl = "https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json"; @@ -236,7 +236,7 @@ const CategoryMap = ({ } }); } - let key = getValueFromAttr(ACur, attribute); + let key = getValueFromAttrDollar(ACur, attribute); key = key !== "" ? key : attribute; quantizeArray.push(ACur[key]); ACur[key] === 0 && zeroPoints.push(value.state); diff --git a/src/components/crp/CategoryTable.tsx b/src/components/crp/CategoryTable.tsx index 0b61037b..2f15c7c4 100644 --- a/src/components/crp/CategoryTable.tsx +++ b/src/components/crp/CategoryTable.tsx @@ -6,7 +6,7 @@ import "../../styles/table.css"; const Styles = styled.div` padding: 1rem; - margin-left: ${window.innerWidth <= 1440 ? "480px" : "auto"}; + ${window.innerWidth <= 1440 ? "margin-left: 480px" : ""}; table { border-spacing: 0; diff --git a/src/components/csp/CSPTotalTable.tsx b/src/components/csp/CSPTotalTable.tsx index 2f97ca6a..067a0fb8 100644 --- a/src/components/csp/CSPTotalTable.tsx +++ b/src/components/csp/CSPTotalTable.tsx @@ -6,7 +6,6 @@ import "../../styles/table.css"; const Styles = styled.div` padding: 1rem; - margin-left: ${window.innerWidth <= 1440 ? "480px" : "auto"}; table { border-spacing: 0; diff --git a/src/components/csp/CategoryTable.tsx b/src/components/csp/CategoryTable.tsx index 6c60acdb..be2b7e3e 100644 --- a/src/components/csp/CategoryTable.tsx +++ b/src/components/csp/CategoryTable.tsx @@ -6,7 +6,6 @@ import "../../styles/table.css"; const Styles = styled.div` padding: 1rem; - margin-left: ${window.innerWidth <= 1440 ? "480px" : "auto"}; table { border-spacing: 0; diff --git a/src/components/eqip/CategoryTable.tsx b/src/components/eqip/CategoryTable.tsx index 8874ec4a..4e6ad158 100644 --- a/src/components/eqip/CategoryTable.tsx +++ b/src/components/eqip/CategoryTable.tsx @@ -6,7 +6,6 @@ import "../../styles/table.css"; const Styles = styled.div` padding: 1rem; - margin-left: ${window.innerWidth <= 1440 ? "480px" : "auto"}; table { border-spacing: 0; diff --git a/src/components/eqip/EQIPTotalTable.tsx b/src/components/eqip/EQIPTotalTable.tsx index e00c7cc8..b58a2ceb 100644 --- a/src/components/eqip/EQIPTotalTable.tsx +++ b/src/components/eqip/EQIPTotalTable.tsx @@ -6,7 +6,6 @@ import "../../styles/table.css"; const Styles = styled.div` padding: 1rem; - margin-left: ${window.innerWidth <= 1440 ? "480px" : "auto"}; table { border-spacing: 0; diff --git a/src/components/rcpp/RCPPTotalMap.tsx b/src/components/rcpp/RCPPTotalMap.tsx new file mode 100644 index 00000000..f264d47e --- /dev/null +++ b/src/components/rcpp/RCPPTotalMap.tsx @@ -0,0 +1,262 @@ +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 Divider from "@mui/material/Divider"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import * as d3 from "d3"; +import PropTypes from "prop-types"; +import "../../styles/map.css"; +import legendConfig from "../../utils/legendConfig.json"; +import DrawLegend from "../shared/DrawLegend"; +import { getValueFromAttrDollar } from "../../utils/apiutil"; + +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 = (props) => { + const { setTooltipContent, allStates, statePerformance, year, stateCodes, colorScale } = props; + return ( +
+ {allStates.length > 0 && statePerformance[year] !== undefined ? ( + + + {({ geographies }) => ( + <> + {geographies.map((geo) => { + const record = statePerformance[year].filter( + (v) => v.state === geo.properties.name + )[0]; + if (record === undefined || record.length === 0) { + return null; + } + // the totalPaymentInDolloars in RCPP is financial assistancePaymentInDollars + const totalPaymentInDollars = record.programs[0].assistancePaymentInDollars; + // since the total is financial assistance, the percentage should also be financial one + const totalPaymentInPercentageNationwide = + record.programs[0].assistancePaymentInPercentageNationwide; + const hoverContent = ( + + + {geo.properties.name} + + + {Number(totalPaymentInDollars) < 1000000 + ? `$${Number( + Number(totalPaymentInDollars) / 1000.0 + ).toLocaleString(undefined, { + maximumFractionDigits: 2 + })}K` + : `$${Number( + Number(totalPaymentInDollars) / 1000000.0 + ).toLocaleString(undefined, { + maximumFractionDigits: 2 + })}M`} + + + + {totalPaymentInPercentageNationwide + ? `${totalPaymentInPercentageNationwide} %` + : "0%"} + + + + + ); + const fillColour = () => { + if (totalPaymentInDollars) { + if (totalPaymentInDollars !== 0) return colorScale(totalPaymentInDollars); + return "#D2D2D2"; + } + return "#D2D2D2"; + }; + return ( + { + setTooltipContent(hoverContent); + }} + onMouseLeave={() => { + setTooltipContent(""); + }} + 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 = { + setTooltipContent: PropTypes.func +}; + +const RCPPTotalMap = ({ + program, + attribute, + year, + statePerformance, + stateCodes, + allStates +}: { + program: string; + attribute: any; + year: string; + statePerformance: any; + stateCodes: any; + allStates: any; +}): JSX.Element => { + const [content, setContent] = useState(""); + const quantizeArray: number[] = []; + const zeroPoints = []; + statePerformance[year].forEach((value) => { + const programRecord = value.programs; + const ACur = programRecord.find((s) => s.programName === program); + let key = getValueFromAttrDollar(ACur, attribute); + key = key !== "" ? key : attribute; + quantizeArray.push(ACur[key]); + ACur[key] === 0 && zeroPoints.push(value.state); + return null; + }); + const category = "Total RCPP"; + const years = "2018-2022"; + const maxValue = Math.max(...quantizeArray); + const mapColor = ["#F0F9E8", "#BAE4BC", "#7BCCC4", "#43A2CA", "#0868AC"]; + const customScale = legendConfig[category]; + const colorScale = d3.scaleThreshold(customScale, mapColor); + return ( +
+
+ + {maxValue !== 0 ? ( + + ) : ( +
+ {titleElement(category, years)} + + + {category} data in {years} 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 RCPPTotalMap; diff --git a/src/components/rcpp/RCPPTotalTable.tsx b/src/components/rcpp/RCPPTotalTable.tsx new file mode 100644 index 00000000..845589d0 --- /dev/null +++ b/src/components/rcpp/RCPPTotalTable.tsx @@ -0,0 +1,249 @@ +import React from "react"; +import styled from "styled-components"; +import { useTable, useSortBy } from "react-table"; +import Box from "@mui/material/Box"; +import "../../styles/table.css"; +import { compareWithDollarSign, compareWithNumber } 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 {"\u{2B83}"}; + 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: any; + stateCodes: any; +}): JSX.Element { + const rcppTableData: any[] = []; + + // eslint-disable-next-line no-restricted-syntax + statePerformance[year].forEach((value) => { + const totalRcpp = value.programs.find((s) => s.programName === "RCPP"); + const stateName = value.state; + const newRecord = () => { + return { + state: stateName, + rcppBenefit: `$${ + totalRcpp.paymentInDollars + .toLocaleString(undefined, { minimumFractionDigits: 2 }) + .toString() + .split(".")[0] + }`, + percentage: `${totalRcpp.assistancePaymentInPercentageNationwide.toString()}%`, + noContract: `${totalRcpp.totalContracts + .toLocaleString(undefined, { minimumFractionDigits: 0 }) + .toString()}`, + totAcre: `${totalRcpp.totalAcres.toLocaleString(undefined, { minimumFractionDigits: 0 }).toString()}`, + finPayment: `$${ + totalRcpp.assistancePaymentInDollars + .toLocaleString(undefined, { minimumFractionDigits: 2 }) + .toString() + .split(".")[0] + }`, + reimbursePayment: `$${ + totalRcpp.reimbursePaymentInDollars + .toLocaleString(undefined, { minimumFractionDigits: 2 }) + .toString() + .split(".")[0] + }`, + techPayment: `$${ + totalRcpp.techPaymentInDollars + .toLocaleString(undefined, { minimumFractionDigits: 2 }) + .toString() + .split(".")[0] + }` + }; + }; + rcppTableData.push(newRecord()); + }); + + // PENDING: The 'pl' below are hard coded values that are inherited from old code design. Need to update this in the future + const columns = React.useMemo( + () => [ + { + Header: STATES, + accessor: "state", + paddingLeft: "5rem", + paddingRight: "5rem" + }, + { + Header: ( + + TOTAL RCPP BENEFITS + + ), + accessor: "finPayment", + sortType: compareWithDollarSign, + Cell: function styleCells(row) { + return
{row.value}
; + } + }, + { + Header: ( + + NO. OF CONTRACTS + + ), + accessor: "noContract", + sortType: compareWithNumber, + Cell: function styleCells(row) { + return
{row.value}
; + } + }, + { + Header: ( + +
ACRES
+
+ ), + accessor: "totAcre", + sortType: compareWithNumber, + Cell: function styleCells(row) { + return
{row.value}
; + } + }, + { + Header: ( + +
PCT. NATIONWIDE
+
+ ), + accessor: "percentage", + sortType: compareWithNumber, + Cell: function styleCells(row) { + return
{row.value}
; + } + } + ], + [] + ); + + return ( + + + + + + ); +} + +export default App; diff --git a/src/components/shared/NavSearchBar.tsx b/src/components/shared/NavSearchBar.tsx index 6a27915c..3d98ea78 100644 --- a/src/components/shared/NavSearchBar.tsx +++ b/src/components/shared/NavSearchBar.tsx @@ -1,7 +1,6 @@ import * as React from "react"; -import { Grid, IconButton, Typography, Box } from "@mui/material"; +import { Grid, Typography, Box } from "@mui/material"; import SearchIcon from "@mui/icons-material/Search"; -import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; export default function NavSearchBar({ bkColor = "rgba(255, 255, 255, 1)", @@ -17,7 +16,7 @@ export default function NavSearchBar({ borderLeft: 0, borderRight: 0, borderColor: brColor, - margin: 1, + mb: 1, backgroundColor: bkColor }} > diff --git a/src/components/title1/Title1TreeMap.tsx b/src/components/title1/Title1TreeMap.tsx index 1eb4a903..2d8a3cdb 100644 --- a/src/components/title1/Title1TreeMap.tsx +++ b/src/components/title1/Title1TreeMap.tsx @@ -85,9 +85,9 @@ export default function Title1TreeMap({ program, TreeMapData, year, stateCodes, recipientsChecked: true }); const { paymentsChecked, baseAcresChecked, recipientsChecked } = checkedState; - let widthPercentage = 0.5; + let widthPercentage = 0.7; const heightPercentage = 0.8; - if (window.innerWidth >= 1920) { + if (window.innerWidth <= 1440) { widthPercentage = 0.6; } const handleResize: () => void = () => { @@ -97,6 +97,7 @@ export default function Title1TreeMap({ program, TreeMapData, year, stateCodes, }; React.useEffect(() => { window.addEventListener("resize", handleResize); + drawIllustration(); return () => window.removeEventListener("resize", handleResize); }); const handleSortClick = (e, attr) => { @@ -147,32 +148,34 @@ export default function Title1TreeMap({ program, TreeMapData, year, stateCodes, URL.revokeObjectURL(url); } }; - if (chartData[0].payments !== 0) { - d3.select(rn.current) - .append("rect") - .attr("x", 0) - .attr("y", 0) - .attr("width", Title1TreeMapIllustration) - .attr("height", Title1TreeMapIllustration) - .attr("fill", paymentsColor); - } - if (chartData[0].baseAcres !== 0) { - d3.select(rn.current) - .append("rect") - .attr("x", 0) - .attr("y", Title1TreeMapIllustration * 0.3) - .attr("width", Title1TreeMapIllustration * 0.7) - .attr("height", Title1TreeMapIllustration * 0.7) - .attr("fill", baseAcresColor); - } - if (chartData[0].recipients !== 0) { - d3.select(rn.current) - .append("rect") - .attr("x", 0) - .attr("y", Title1TreeMapIllustration * 0.6) - .attr("width", Title1TreeMapIllustration * 0.4) - .attr("height", Title1TreeMapIllustration * 0.4) - .attr("fill", recipientsColor); + function drawIllustration() { + if (chartData[0].payments !== 0) { + d3.select(rn.current) + .append("rect") + .attr("x", 0) + .attr("y", 0) + .attr("width", Title1TreeMapIllustration) + .attr("height", Title1TreeMapIllustration) + .attr("fill", paymentsColor); + } + if (chartData[0].baseAcres !== 0) { + d3.select(rn.current) + .append("rect") + .attr("x", 0) + .attr("y", Title1TreeMapIllustration * 0.3) + .attr("width", Title1TreeMapIllustration * 0.7) + .attr("height", Title1TreeMapIllustration * 0.7) + .attr("fill", baseAcresColor); + } + if (chartData[0].recipients !== 0) { + d3.select(rn.current) + .append("rect") + .attr("x", 0) + .attr("y", Title1TreeMapIllustration * 0.6) + .attr("width", Title1TreeMapIllustration * 0.4) + .attr("height", Title1TreeMapIllustration * 0.4) + .attr("fill", recipientsColor); + } } /* eslint-disable */ return ( @@ -197,7 +200,8 @@ export default function Title1TreeMap({ program, TreeMapData, year, stateCodes, }} > - The payments,base acres and payment recipients are calculated as the total of the data from 2014-2021. 2022 payments for Title I have not yet been paid. + The payments,base acres and payment recipients are calculated as the total of the data + from 2014-2021. 2022 payments for Title I have not yet been paid. The size differences of the squares represent the differences in relative amount{" "} within the same category. For example, a larger purple square indicate a higher - number of avg. recipients compared to another smaller purple square, but it does not necessarily - indicate a greater number of avg. recipients compared to a smaller yellow square representing - payments. + number of avg. recipients compared to another smaller purple square, but it does not + necessarily indicate a greater number of avg. recipients compared to a smaller yellow square + representing payments. diff --git a/src/main.tsx b/src/main.tsx index 4c198db2..6625aeb0 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,9 +4,11 @@ import LandingPage from "./pages/LandingPage"; import EQIPPage from "./pages/EQIPPage"; import CSPPage from "./pages/CSPPage"; import CRPPage from "./pages/CRPPage"; +import RCPPPage from "./pages/RCPPPage"; import SNAPPage from "./pages/SNAPPage"; import TitleIPage from "./pages/TitleIPage"; import CropInsurancePage from "./pages/CropInsurancePage"; +import ACEPPage from "./pages/ACEPPage"; const ScrollToTop = (props: any) => { const location = useLocation(); @@ -25,6 +27,8 @@ export default function Main(): JSX.Element { } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/src/pages/ACEPPage.tsx b/src/pages/ACEPPage.tsx new file mode 100644 index 00000000..739b0a25 --- /dev/null +++ b/src/pages/ACEPPage.tsx @@ -0,0 +1,252 @@ +import Box from "@mui/material/Box"; +import * as React from "react"; +import { createTheme, Grid, ThemeProvider, ToggleButton, ToggleButtonGroup, Typography } from "@mui/material"; +import TableChartIcon from "@mui/icons-material/TableChart"; +import InsertChartIcon from "@mui/icons-material/InsertChart"; +import NavBar from "../components/NavBar"; +import Drawer from "../components/ProgramDrawer"; +import ACEPTotalMap from "../components/acep/ACEPTotalMap"; +import { config } from "../app.config"; +import { convertAllState, getJsonDataFromUrl } from "../utils/apiutil"; +import NavSearchBar from "../components/shared/NavSearchBar"; +import ACEPTable from "../components/acep/ACEPTable"; +import AcepTreeMap from "../components/acep/AcepTreeMap"; +import "../styles/subpage.css"; + +export default function ACEPPage(): JSX.Element { + const year = "2018-2022"; + const [checked, setChecked] = React.useState(0); + + const [stateDistributionData, setStateDistributionData] = React.useState({}); + const [stateCodesData, setStateCodesData] = React.useState({}); + const [stateCodesArray, setStateCodesArray] = React.useState({}); + const [allStatesData, setAllStatesData] = React.useState([]); + const [zeroCategories, setZeroCategories] = React.useState([]); + const [totalAcep, setTotalAcep] = React.useState(0); + const [tab, setTab] = React.useState(0); + + const defaultTheme = createTheme(); + const zeroCategory = []; + let totalACEPPaymentInDollars = 0; + let totalContracts = 0; + let totalAcres = 0; + const assistancePaymentInDollars = 0; + const reimbursePaymentInDollars = 0; + const techPaymentInDollars = 0; + + React.useEffect(() => { + const allstates_url = `${config.apiUrl}/states`; + getJsonDataFromUrl(allstates_url).then((response) => { + setAllStatesData(response); + }); + + const statecode_url = `${config.apiUrl}/statecodes`; + getJsonDataFromUrl(statecode_url).then((response) => { + setStateCodesArray(response); + const converted_json = convertAllState(response); + setStateCodesData(converted_json); + }); + + const statedistribution_url = `${config.apiUrl}/programs/conservation/acep/state-distribution`; + getJsonDataFromUrl(statedistribution_url).then((response) => { + setStateDistributionData(response); + }); + + const chartData_url = `${config.apiUrl}/programs/conservation/acep/subprograms`; + getJsonDataFromUrl(chartData_url).then((response) => { + processData(response); + }); + }, []); + + const switchChartTable = (event, newTab) => { + if (newTab !== null) { + setTab(newTab); + } + }; + const processData = (chartData) => { + if (chartData.programs === undefined) return; + const cur1 = chartData.programs.find((s) => s.programName === "ACEP"); + + totalACEPPaymentInDollars = cur1.assistancePaymentInDollars; + setTotalAcep(totalACEPPaymentInDollars); + if (totalACEPPaymentInDollars === 0) zeroCategory.push("ACEP"); + totalContracts = cur1.totalContracts; + if (totalContracts === 0) zeroCategory.push("Total Contracts"); + totalAcres = cur1.totalAcre; + if (totalAcres === 0) zeroCategory.push("Total Acres"); + // assistancePaymentInDollars = cur1.assistancePaymentInDollars; + // if (assistancePaymentInDollars === 0) zeroCategory.push("Assistance Payment"); + // reimbursePaymentInDollars = cur1.reimbursePaymentInDollars; + // if (reimbursePaymentInDollars === 0) zeroCategory.push("Reimburse Payment"); + // techPaymentInDollars = cur1.techPaymentInDollars; + // if (techPaymentInDollars === 0) zeroCategory.push("Tech Payment"); + setZeroCategories(zeroCategory); + }; + function prepData(program, subprogram, data, dataYear) { + const organizedData: Record[] = []; + const originalData: Record[] = []; + data[dataYear].forEach((stateData) => { + const state = stateData.state; + const programData = stateData.programs.filter((p) => { + return p.programName.toString() === program; + }); + organizedData.push({ + state, + acres: programData[0].totalAcres, + payments: programData[0].assistancePaymentInDollars, + contracts: programData[0].totalContracts + }); + originalData.push({ + state, + acres: programData[0].totalAcres, + payments: programData[0].assistancePaymentInDollars, + contracts: programData[0].totalContracts + }); + }); + return [organizedData, originalData]; + } + return ( + + {Object.keys(stateCodesData).length > 0 && + Object.keys(allStatesData).length > 0 && + Object.keys(stateDistributionData).length > 0 ? ( + + + + + + + + + + + + + + ACEP: Agriculture Conservation Easement Program + + + + + Total Benefits ({year}): + + $ + { + totalAcep + .toLocaleString(undefined, { minimumFractionDigits: 2 }) + .toString() + .split(".")[0] + } + + + + + + + A conservation easement is a permanent (or long term) property right in agricultural + land for conservation of natural resources. Agricultural Land Easements (ALE) limit + non-agricultural uses of eligible land, including grazing land, to protect farmland from + development or other pressures and works through land trusts or other entities such as + state and local governments. Wetland Reserve Easements (WRE) protect, restore, and + enhance wetlands that have been previously degraded due to agricultural uses. In the + Agricultural Act of 2014, Congress combined existing conservation easement programs into + a single program. + + + + + + Performance by States + + + + + + + + + + + + + 1440 ? window.innerWidth * 0.7 : window.innerWidth * 0.6 + } + svgH={3000} + /> + + + + + + + + + ) : ( +

Loading data...

+ )} +
+ ); +} diff --git a/src/pages/RCPPPage.tsx b/src/pages/RCPPPage.tsx new file mode 100644 index 00000000..1180536a --- /dev/null +++ b/src/pages/RCPPPage.tsx @@ -0,0 +1,174 @@ +import Box from "@mui/material/Box"; +import * as React from "react"; +import { createTheme, ThemeProvider, Typography } from "@mui/material"; +import NavBar from "../components/NavBar"; +import Drawer from "../components/ProgramDrawer"; +import DataTable from "../components/rcpp/RCPPTotalTable"; +import RCPPTotalMap from "../components/rcpp/RCPPTotalMap"; +// import CategoryTable from "../components/rcpp/CategoryTable"; +// import CategoryMap from "../components/rcpp/CategoryMap"; +import { config } from "../app.config"; +import { convertAllState, getJsonDataFromUrl } from "../utils/apiutil"; +import NavSearchBar from "../components/shared/NavSearchBar"; + +export default function RCPPPage(): JSX.Element { + const year = "2018-2022"; + const attribute = "assistancePaymentInDollars"; + const [checked, setChecked] = React.useState(0); + + const [stateDistributionData, setStateDistributionData] = React.useState({}); + const [stateCodesData, setStateCodesData] = React.useState({}); + const [stateCodesArray, setStateCodesArray] = React.useState({}); + const [allStatesData, setAllStatesData] = React.useState([]); + const [totalChartData, setTotalChartData] = React.useState([{}]); + const [zeroCategories, setZeroCategories] = React.useState([]); + const [totalRcpp, setTotalRcpp] = React.useState(0); + const [totalBenefit, setTotalBenefit] = React.useState(""); + + const defaultTheme = createTheme(); + const zeroCategory = []; + let totalRCPPPaymentInDollars = 0; + let totalContracts = 0; + let totalAcres = 0; + let assistancePayments = 0; + let reimbursePayments = 0; + let techPayments = 0; + + React.useEffect(() => { + const allstates_url = `${config.apiUrl}/states`; + getJsonDataFromUrl(allstates_url).then((response) => { + setAllStatesData(response); + }); + + const statecode_url = `${config.apiUrl}/statecodes`; + getJsonDataFromUrl(statecode_url).then((response) => { + setStateCodesArray(response); + const converted_json = convertAllState(response); + setStateCodesData(converted_json); + }); + + const statedistribution_url = `${config.apiUrl}/programs/conservation/rcpp/state-distribution`; + getJsonDataFromUrl(statedistribution_url).then((response) => { + setStateDistributionData(response); + }); + + const chartData_url = `${config.apiUrl}/programs/conservation/rcpp/subprograms`; + getJsonDataFromUrl(chartData_url).then((response) => { + processData(response); + }); + }, []); + + const processData = (chartData) => { + if (chartData.programs === undefined) return; + + const cur1 = chartData.programs.find((s) => s.programName === "RCPP"); + + totalRCPPPaymentInDollars = cur1.paymentInDollars; + setTotalRcpp(totalRCPPPaymentInDollars); + if (totalRCPPPaymentInDollars === 0) zeroCategory.push("RCPP"); + totalContracts = cur1.totalContracts; + totalAcres = cur1.totalAcres; + assistancePayments = cur1.assistancePaymentInDollars; + reimbursePayments = cur1.reimbursePaymentInDollars; + techPayments = cur1.techPaymentInDollars; + + setZeroCategories(zeroCategory); + + setTotalChartData([ + { name: "Total Financial Assistance Payments", value: assistancePayments, color: "#2F7164" }, + { name: "Total Reimbursable Payments", value: reimbursePayments, color: "#869397" }, + { name: "Total Techinical Assistance Payments", value: techPayments, color: "#9CBAB4" } + ]); + + setTotalBenefit( + // total benefit should be financial assistance payment + assistancePayments.toLocaleString(undefined, { minimumFractionDigits: 2 }).toString().split(".")[0] + ); + }; + + return ( + + {Object.keys(stateCodesData).length > 0 && + Object.keys(allStatesData).length > 0 && + Object.keys(stateDistributionData).length > 0 ? ( + + + + + + + + + + + + + + RCPP: Regional Conservation Partnership Program + + + + + Total Benefits ({year}): + ${totalBenefit} + + + + + + RCPP assists with the conservation, protection, restoration, and sustainable uses of + soil, water, wildlife, and related natural resources on a regional or watershed scale + and combines federal funding with private partner funding and other contributions. RCPP + works across multiple farms (or nonindustrial private forest operations) through + multiple conservation practices to achieve greater conservation outcomes, encourage + flexibility in program operation, and leverage private resources. Congress created the + RCPP in the Agricultural Act of 2014 and revised the program in the Agricultural + Improvement Act of 2018. + + + + + + Overall Performance of States + + + + + + + + ) : ( +

Loading data...

+ )} +
+ ); +} diff --git a/src/pages/TitleIPage.tsx b/src/pages/TitleIPage.tsx index 70578c20..c6c0e800 100644 --- a/src/pages/TitleIPage.tsx +++ b/src/pages/TitleIPage.tsx @@ -33,7 +33,7 @@ export default function TitleIPage(): JSX.Element { const title1Div = React.useRef(null); const [checked, setChecked] = React.useState("0"); const mapColor = ["#F9F9D3", "#F9D48B", "#F59020", "#D95F0E", "#993404"]; - let initTreeMapWidthRatio = 0.6; + const initTreeMapWidthRatio = 0.6; React.useEffect(() => { const allstates_url = `${config.apiUrl}/states`; @@ -49,9 +49,6 @@ export default function TitleIPage(): JSX.Element { getJsonDataFromUrl(statedistribution_url).then((response) => { setStateDistributionData(response); }); - if (window.innerWidth >= 1920) { - initTreeMapWidthRatio = 0.7; - } }, []); const switchChartTable = (event, newTab) => { @@ -256,7 +253,11 @@ export default function TitleIPage(): JSX.Element { )} stateCodes={stateCodesData} year="2014-2021" - svgW={window.innerWidth * initTreeMapWidthRatio} + svgW={ + window.innerWidth > 1440 + ? window.innerWidth * 0.75 + : window.innerWidth * 0.65 + } svgH={3000} /> diff --git a/src/styles/subpage.css b/src/styles/subpage.css index e05bbcf0..00cc6f4c 100644 --- a/src/styles/subpage.css +++ b/src/styles/subpage.css @@ -31,8 +31,10 @@ display: flex; } - .stateTitleContainer .stateTitle { - margin-top: 0.5em; + .stateTitleContainer > .stateTitle { + font-weight: 700; + margin-top: 1em; + font-size: 1.2em; } @media only screen and (max-width: 1440px) { @@ -65,10 +67,12 @@ display: flex; } - .stateTitleContainer .stateTitle { + + .stateTitleContainer > .stateTitle { margin-top: 0.5em; } - + + .sideBar-short .MuiPaper-root { max-height: 99vh; } diff --git a/src/utils/apiutil.tsx b/src/utils/apiutil.tsx index 8d417e1d..10b31f23 100644 --- a/src/utils/apiutil.tsx +++ b/src/utils/apiutil.tsx @@ -18,12 +18,26 @@ export function convertAllState(inlist) { return JSON.parse(conv_str); } -export const getValueFromAttr = (stateRecord, attribute): string => { +export const getValueFromAttrDollar = (stateRecord, attribute): string => { + let ans = ""; + if (attribute) { + Object.keys(stateRecord).forEach((key) => { + const match = key.toLowerCase().match(/(.*?)(?=\s*indollars)/); + const extractedKey = match ? match[1] : ""; + if (extractedKey.includes(attribute)) { + ans = key; + } + }); + } + return ans; +}; + +export const getValueFromAttrPercentage = (stateRecord, attribute): string => { let ans = ""; Object.keys(stateRecord).forEach((key) => { - const match = key.match(/^(.*?)(?=\s*InDollars)/); - const extractedKey = match ? match[1] : key; - if (extractedKey === attribute) { + const match = key.toLowerCase().match(/(.*?)(?=\s*inpercentagenationwide)/); + const extractedKey = match ? match[1] : ""; + if (extractedKey.includes(attribute)) { ans = key; } }); diff --git a/src/utils/legendConfig.json b/src/utils/legendConfig.json index edd6c2f5..713876ad 100644 --- a/src/utils/legendConfig.json +++ b/src/utils/legendConfig.json @@ -60,6 +60,8 @@ "CREP Only":[100000, 10000000, 50000000, 80000000], "Continuous Non-CREP":[10000, 100000, 100000000, 200000000], "Farmable Wetland":[10000, 100000, 500000, 10000000], - "Grassland-CRP":[10000, 100000, 500000, 10000000] -} + "Grassland-CRP":[10000, 100000, 500000, 10000000], + "Total ACEP": [5000000, 10000000, 50000000, 100000000], + "Total RCPP": [1000000, 5000000, 10000000, 20000000] +}