diff --git a/CHANGELOG.md b/CHANGELOG.md index 66e61631..cd6a0444 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,19 @@ 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.9.0] - 2023-09-18 + +### Added +- CRP page and corresponding components for its attributes [#170](https://github.com/policy-design-lab/pdl-frontend/issues/170) + +### Changed + +- Replace the color legends on the EQIP, CSP, and CRP pages with the customized scheme [189](https://github.com/policy-design-lab/pdl-frontend/issues/189) +- Adjusted the menu height of CRP page on small screen [#190](https://github.com/policy-design-lab/pdl-frontend/issues/190) + +### Fixed +- The tables for Title II shows right most column in any screen size [#192](https://github.com/policy-design-lab/pdl-frontend/issues/192) + ## [0.8.0] - 2023-09-06 ### Added @@ -139,6 +152,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Map data json [#12](https://github.com/policy-design-lab/pdl-frontend/issues/12) - Final landing page changes for initial milestone [#15](https://github.com/policy-design-lab/pdl-frontend/issues/15) +[0.9.0]: https://github.com/policy-design-lab/pdl-frontend/compare/0.8.0...0.9.0 [0.8.0]: https://github.com/policy-design-lab/pdl-frontend/compare/0.7.0...0.8.0 [0.7.0]: https://github.com/policy-design-lab/pdl-frontend/compare/0.6.0...0.7.0 [0.6.0]: https://github.com/policy-design-lab/pdl-frontend/compare/0.5.1...0.6.0 diff --git a/package-lock.json b/package-lock.json index 3172fa74..37a3fa67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "policy-design-lab", - "version": "0.8.0", + "version": "0.9.0", "lockfileVersion": 2, "requires": true, "packages": { diff --git a/package.json b/package.json index 0be04d68..86774664 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "policy-design-lab", - "version": "0.8.0", + "version": "0.9.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/HorizontalStackedBar.tsx b/src/components/HorizontalStackedBar.tsx deleted file mode 100644 index c7a393b1..00000000 --- a/src/components/HorizontalStackedBar.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import React from "react"; -import HSBar from "react-horizontal-stacked-bar-chart"; -import { Box, Typography } from "@mui/material"; - -export default function HorizontalStackedBar({ - title, - color1, - color2, - color3, - color4, - color5, - label1, - label2, - label3, - label4, - label5, - label6 -}: { - title: string; - color1: string; - color2: string; - color3: string; - color4: string; - color5: string; - label1: string; - label2: string; - label3: string; - label4: string; - label5: string; - label6: string; -}): JSX.Element { - return ( - - - - {title} - - - - - - - - ); -} diff --git a/src/components/ProgramDrawer.tsx b/src/components/ProgramDrawer.tsx index aa3fcc23..6afcd731 100644 --- a/src/components/ProgramDrawer.tsx +++ b/src/components/ProgramDrawer.tsx @@ -56,7 +56,7 @@ function EQIPCheckboxList({ setEQIPChecked, setShowPopUp, zeroCategory }) { if (zeroCategory && zeroCategory.includes(category)) { return ( - + - + ); @@ -336,15 +340,113 @@ function CSPCheckboxList({ setCSPChecked, setShowPopUp, zeroCategory }) { ); } +function CRPCheckboxList({ setCRPChecked, setShowPopUp, zeroCategory }) { + const [checked, setChecked] = React.useState(currentChecked); + + const handleToggle = (value: number) => () => { + setChecked(value); + setCRPChecked(value); + currentChecked = value; + setShowPopUp(false); + }; + + const CRPList = [ + "Total CRP", + "Total General Sign-up", + "Total Continuous Sign-Up", + "CREP Only", + "Continuous Non-CREP", + "Farmable Wetland", + "Grassland" + ]; + + return ( + + {CRPList.map((category, value) => { + const labelId = `checkbox-list-label-${value}`; + if (zeroCategory && zeroCategory.includes(category)) { + return ( + + + + + + + ); + } + if (category !== "CREP Only" && category !== "Continuous Non-CREP" && category !== "Farmable Wetland") { + return ( + + + + + + + ); + } + return ( + + + + + + + + + ); + })} + + ); +} + interface ProgramDrawerProps { setEQIPChecked?: (value: number) => void; setCSPChecked?: (value: number) => void; + setCRPChecked?: (value: number) => void; zeroCategories?: string[]; } export default function ProgramDrawer({ setEQIPChecked, setCSPChecked, + setCRPChecked, zeroCategories }: ProgramDrawerProps): JSX.Element { const location = useLocation(); @@ -387,6 +489,26 @@ export default function ProgramDrawer({ prevCspOpen.current = cspOpen; }, [cspOpen]); + const [crpOpen, setCrpOpen] = React.useState(false); + const crpRef = React.useRef(null); + const handleCrpClick = () => { + if (location.pathname !== "/crp") { + navigate("/crp"); + window.location.reload(false); + } else { + setCrpOpen((prevCrpOpen) => !prevCrpOpen); + } + }; + const prevCrpOpen = React.useRef(crpOpen); + React.useEffect(() => { + if (prevCrpOpen.current && !crpOpen) { + crpRef.current.focus(); + } + + prevCrpOpen.current = crpOpen; + }, [crpOpen]); + + const crpMenuHeight = window.innerHeight < 900 ? "38%" : "40%"; return ( - - CRP: Conservation Reserve Program - + + + + {location.pathname === "/crp" ? ( + + CRP: Conservation Reserve Program + + ) : ( + CRP: Conservation Reserve Program + )} + + + + STATUTE + + + + + + + + + + + + ACEP: Agriculture Conservation Easement Program diff --git a/src/components/crp/CRPTotalMap.tsx b/src/components/crp/CRPTotalMap.tsx new file mode 100644 index 00000000..0eb0a188 --- /dev/null +++ b/src/components/crp/CRPTotalMap.tsx @@ -0,0 +1,260 @@ +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 { getValueFromAttr } 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) => stateCodes[v.state] === geo.properties.name + )[0]; + if (record === undefined || record.length === 0) { + return null; + } + const totalPaymentInDollars = record.programs[0].paymentInDollars; + const totalPaymentInPercentageNationwide = + record.programs[0].paymentInPercentageNationwide; + 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 CRPTotalMap = ({ + 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 = getValueFromAttr(ACur, attribute); + key = key !== "" ? key : attribute; + quantizeArray.push(ACur[key]); + ACur[key] === 0 && zeroPoints.push(value.state); + return null; + }); + const category = "Total CRP"; + 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 CRPTotalMap; diff --git a/src/components/crp/CRPTotalTable.tsx b/src/components/crp/CRPTotalTable.tsx new file mode 100644 index 00000000..0853e7ef --- /dev/null +++ b/src/components/crp/CRPTotalTable.tsx @@ -0,0 +1,272 @@ +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"; + +const Styles = styled.div` + padding: 1rem; + margin-left: ${window.innerWidth <= 1440 ? "480px" : "auto"}; + + 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 { + function compareWithDollarSign(rowA, rowB, id, desc) { + const a = Number.parseFloat(rowA.values[id].substring(1).replaceAll(",", "")); + const b = Number.parseFloat(rowB.values[id].substring(1).replaceAll(",", "")); + if (a > b) return 1; + if (a < b) return -1; + return 0; + } + + function compareWithPercentSign(rowA, rowB, id, desc) { + const a = Number.parseFloat(rowA.values[id].replaceAll("%", "")); + const b = Number.parseFloat(rowB.values[id].replaceAll("%", "")); + if (a > b) return 1; + if (a < b) return -1; + return 0; + } + + function compareNumber(rowA, rowB, id, desc) { + const a = Number.parseInt(rowA.values[id].replaceAll(",", ""), 10); + const b = Number.parseInt(rowB.values[id].replaceAll(",", ""), 10); + if (a > b) return 1; + if (a < b) return -1; + return 0; + } + + const crpTableData: any[] = []; + + // eslint-disable-next-line no-restricted-syntax + statePerformance[year].forEach((value) => { + const totalCrp = value.programs.find((s) => s.programName === "Total CRP"); + let stateName; + stateCodes.forEach((sValue) => { + if (sValue.code.toUpperCase() === value.state.toUpperCase()) { + stateName = sValue.name; + } + }); + const newRecord = () => { + return { + state: stateName, + crpBenefit: `$${totalCrp.paymentInDollars + .toLocaleString(undefined, { minimumFractionDigits: 2 }) + .toString()}`, + percentage: `${totalCrp.paymentInPercentageNationwide.toString()}%`, + noContract: `${totalCrp.totalContracts + .toLocaleString(undefined, { minimumFractionDigits: 0 }) + .toString()}`, + noFarm: `${totalCrp.totalFarms.toLocaleString(undefined, { minimumFractionDigits: 0 }).toString()}`, + totAcre: `${totalCrp.totalAcre.toLocaleString(undefined, { minimumFractionDigits: 0 }).toString()}` + }; + }; + crpTableData.push(newRecord()); + }); + + const columns = React.useMemo( + () => [ + { + Header: STATES, + accessor: "state", + paddingLeft: "5rem", + paddingRight: "5rem" + }, + { + Header: ( + + TOTAL CRP BENEFITS + + ), + accessor: "crpBenefit", + sortType: compareWithDollarSign, + Cell: function styleCells(row) { + return
{row.value}
; + } + }, + { + Header: ( + + PCT. NATIONWIDE
(as of Sep. 2022) +
+ ), + accessor: "percentage", + sortType: compareWithPercentSign, + Cell: function styleCells(row) { + return
{row.value}
; + } + }, + { + Header: ( + + NO. OF CONTRACTS
(as of Sep. 2022) +
+ ), + accessor: "noContract", + sortType: compareNumber, + Cell: function styleCells(row) { + return
{row.value}
; + } + }, + { + Header: ( + + NO. OF FARMS
(as of Sep. 2022) +
+ ), + accessor: "noFarm", + sortType: compareNumber, + Cell: function styleCells(row) { + return
{row.value}
; + } + }, + { + Header: ( + + ACRES
(as of Sep. 2022) +
+ ), + accessor: "totAcre", + sortType: compareNumber, + Cell: function styleCells(row) { + return
{row.value}
; + } + } + ], + [] + ); + + return ( + + + + + + ); +} + +export default App; diff --git a/src/components/crp/CategoryMap.tsx b/src/components/crp/CategoryMap.tsx new file mode 100644 index 00000000..07e8a20f --- /dev/null +++ b/src/components/crp/CategoryMap.tsx @@ -0,0 +1,327 @@ +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 { scaleQuantize } from "d3-scale"; +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 { getValueFromAttr } 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 } = props; + let categoryRecord; + 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; + } + let ACur = {}; + let BCur = {}; + let CCur = {}; + let DCur = {}; + let ECur = {}; + let FCur = {}; + record.programs.forEach((value) => { + if (value.programName === "Total General Sign-Up") { + ACur = value; + } else if (value.programName === "Total Continuous Sign-Up") { + BCur = value; + value.subPrograms.forEach((subValue) => { + if (subValue.programName === "CREP Only") { + CCur = subValue; + } else if (subValue.programName === "Continuous Non-CREP") { + DCur = subValue; + } else if (subValue.programName === "Farmable Wetland") { + ECur = subValue; + } + }); + } else if (value.programName === "Grassland") { + FCur = value; + } + }); + if (category === "Total General Sign-Up") { + categoryRecord = ACur; + } else if (category === "Total Continuous Sign-Up") { + categoryRecord = BCur; + } else if (category === "CREP Only") { + categoryRecord = CCur; + } else if (category === "Continuous Non-CREP") { + categoryRecord = DCur; + } else if (category === "Farmable Wetland") { + categoryRecord = ECur; + } else { + categoryRecord = FCur; + } + const categoryPayment = categoryRecord.paymentInDollars; + const nationwidePercentage = categoryRecord.paymentInPercentageNationwide; + 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(""); + let title = `CRP ${category} from ${year}`; + const quantizeArray: number[] = []; + const zeroPoints = []; + + statePerformance[year].forEach((value) => { + const programRecord = value.programs; + let ACur = {}; + if ( + category === "Total General Sign-Up" || + category === "Total Continuous Sign-Up" || + category === "Grassland" + ) { + ACur = programRecord.find((s) => s.programName === category); + } else if (category === "CREP Only" || category === "Continuous Non-CREP" || category === "Farmable Wetland") { + const contSingUp = programRecord.find((s) => s.programName === "Total Continuous Sign-Up"); + const subPrograms = contSingUp.subPrograms; + title = `CRP Total Continuous, ${category} from ${year}`; + subPrograms.forEach((subValue) => { + if (subValue.programName === category) { + ACur = subValue; + } + }); + } + let key = getValueFromAttr(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 === "Grassland" ? "Grassland-CRP" : 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} Benefits from {year} + {" "} + + In any state that appears in grey, there is no available data + + + ); +}; +export default CategoryMap; diff --git a/src/components/crp/CategoryTable.tsx b/src/components/crp/CategoryTable.tsx new file mode 100644 index 00000000..0b61037b --- /dev/null +++ b/src/components/crp/CategoryTable.tsx @@ -0,0 +1,316 @@ +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"; + +const Styles = styled.div` + padding: 1rem; + margin-left: ${window.innerWidth <= 1440 ? "480px" : "auto"}; + + 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-top: 1rem; + padding-bottom: 1rem; + } + + td { + margin: 0; + padding: 0rem; + padding-left: 5rem; + padding-right: 5rem; + border-bottom: 1px solid #e4ebe7; + border-right: none; + + :last-child { + border-right: 2rem; + } + } + } +`; + +// eslint-disable-next-line +function Table({ columns, data }: { columns: any; data: any; statePerformance: 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({ + category, + statePerformance, + year, + stateCodes +}: { + category: string; + statePerformance: any; + year: any; + stateCodes: any; +}): JSX.Element { + const crpTableData: any[] = []; + statePerformance[year].forEach((value) => { + const totalCrp = value.programs.find((s) => s.programName === "Total CRP"); + let categoryCrp; + if ( + category === "Total General Sign-Up" || + category === "Total Continuous Sign-Up" || + category === "Grassland" + ) { + categoryCrp = value.programs.find((s) => s.programName === category); + } else if (category === "CREP Only" || category === "Continuous Non-CREP" || category === "Farmable Wetland") { + const contSingUp = value.programs.find((s) => s.programName === "Total Continuous Sign-Up"); + const subPrograms = contSingUp.subPrograms; + subPrograms.forEach((subValue) => { + if (subValue.programName === category) { + categoryCrp = subValue; + } + }); + } + + let stateName; + // let percentageValue = 0; + // if (Number.parseInt(totalCrp.paymentInDollars, 10) > 0) { + // percentageValue = + // (Number.parseInt(categoryCrp.paymentInDollars, 10) / Number.parseInt(totalCrp.paymentInDollars, 10)) * + // 100; + // } + + stateCodes.forEach((sValue) => { + if (sValue.code.toUpperCase() === value.state.toUpperCase()) { + stateName = sValue.name; + } + }); + const newRecord = () => { + return { + state: stateName, + categoryBenefit: `$${categoryCrp.paymentInDollars + .toLocaleString(undefined, { minimumFractionDigits: 2 }) + .toString()}`, + categoryPercentage: `${categoryCrp.paymentInPercentageWithinState + .toLocaleString(undefined, { minimumFractionDigits: 2 }) + .toString()}%`, + crpBenefit: `$${totalCrp.paymentInDollars + .toLocaleString(undefined, { minimumFractionDigits: 2 }) + .toString()}`, + percentage: `${categoryCrp.paymentInPercentageNationwide.toString()}%` + }; + }; + crpTableData.push(newRecord()); + }); + + function compareWithDollarSign(rowA, rowB, id, desc) { + const a = Number.parseFloat(rowA.values[id].substring(1).replaceAll(",", "")); + const b = Number.parseFloat(rowB.values[id].substring(1).replaceAll(",", "")); + if (a > b) return 1; + if (a < b) return -1; + return 0; + } + + function compareWithPercentSign(rowA, rowB, id, desc) { + const a = Number.parseFloat(rowA.values[id].replaceAll("%", "")); + const b = Number.parseFloat(rowB.values[id].replaceAll("%", "")); + if (a > b) return 1; + if (a < b) return -1; + return 0; + } + + const columns = React.useMemo( + () => [ + { + Header: ( + + STATES + + ), + accessor: "state", + Cell: function styleCells(props: { + value: boolean | React.ReactChild | React.ReactFragment | React.ReactPortal | null | undefined; + }) { + return ( +
+ + {props.value} + +
+ ); + } + }, + { + Header: ( + + {`${category} Benefit`.toUpperCase()} + + ), + accessor: "categoryBenefit", + sortType: compareWithDollarSign, + Cell: function styleCells(props: { + value: boolean | React.ReactChild | React.ReactFragment | React.ReactPortal | null | undefined; + }) { + return ( +
+ + {props.value} + +
+ ); + } + }, + { + Header: ( + + {`${category} Percentage Within State`.toUpperCase()} + + ), + accessor: "categoryPercentage", + sortType: compareWithPercentSign, + Cell: function styleCells(props: { + value: boolean | React.ReactChild | React.ReactFragment | React.ReactPortal | null | undefined; + }) { + return ( +
+ + {props.value} + +
+ ); + } + }, + { + Header: ( + + CRP BENEFITS + + ), + accessor: "crpBenefit", + 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/csp/CSPTotalMap.tsx b/src/components/csp/CSPTotalMap.tsx index b2520fa4..13ab076e 100644 --- a/src/components/csp/CSPTotalMap.tsx +++ b/src/components/csp/CSPTotalMap.tsx @@ -2,14 +2,14 @@ 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 { scaleQuantize } from "d3-scale"; 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 HorizontalStackedBar from "../HorizontalStackedBar"; +import legendConfig from "../../utils/legendConfig.json"; +import DrawLegend from "../shared/DrawLegend"; const geoUrl = "https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json"; @@ -26,10 +26,7 @@ const offsets = { }; const MapChart = (props) => { - const { setTooltipContent, maxValue, allStates, statePerformance } = props; - const colorScale = scaleQuantize() - .domain([0, maxValue]) - .range(["#F0F9E8", "#BAE4BC", "#7BCCC4", "#43A2CA", "#0868AC"]); + const { setTooltipContent, allStates, statePerformance, colorScale } = props; return (
@@ -162,12 +159,12 @@ const MapChart = (props) => { }; MapChart.propTypes = { - setTooltipContent: PropTypes.func, - maxValue: PropTypes.number + setTooltipContent: PropTypes.func }; const CSPTotalMap = ({ statePerformance, allStates }: { statePerformance: any; allStates: any }): JSX.Element => { const quantizeArray: number[] = []; + const category = "Total CSP"; Object.values(statePerformance).map((value) => { if (Array.isArray(value)) { quantizeArray.push(value[0].totalPaymentInDollars); @@ -175,11 +172,9 @@ const CSPTotalMap = ({ statePerformance, allStates }: { statePerformance: any; a return null; }); const maxValue = Math.max(...quantizeArray); - const label1 = (maxValue / 5) * 0; - const label2 = (maxValue / 5) * 1; - const label3 = (maxValue / 5) * 2; - const label4 = (maxValue / 5) * 3; - const label5 = (maxValue / 5) * 4; + const mapColor = ["#F0F9E8", "#BAE4BC", "#7BCCC4", "#43A2CA", "#0868AC"]; + const customScale = legendConfig[category]; + const colorScale = d3.scaleThreshold(customScale, mapColor); const [content, setContent] = useState(""); // issue158: since eqip and csp are using old data structure (i.e. year is not the first level of data structure), going into array to find the year let years = "2018-2022"; @@ -193,32 +188,28 @@ const CSPTotalMap = ({ statePerformance, allStates }: { statePerformance: any; a
- + {maxValue !== 0 ? ( + + ) : ( +
+ {titleElement(category, years)} + + + {category} data in {years} is unavailable for all states. + + +
+ )}
@@ -237,5 +229,16 @@ const CSPTotalMap = ({ statePerformance, allStates }: { statePerformance: any; a
); }; - +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 index 067a0fb8..2f97ca6a 100644 --- a/src/components/csp/CSPTotalTable.tsx +++ b/src/components/csp/CSPTotalTable.tsx @@ -6,6 +6,7 @@ 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/CategoryMap.tsx b/src/components/csp/CategoryMap.tsx index f6675d8b..d25df0d2 100644 --- a/src/components/csp/CategoryMap.tsx +++ b/src/components/csp/CategoryMap.tsx @@ -7,8 +7,10 @@ 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 HorizontalStackedBar from "../HorizontalStackedBar"; +import legendConfig from "../../utils/legendConfig.json"; +import DrawLegend from "../shared/DrawLegend"; const geoUrl = "https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json"; @@ -25,10 +27,7 @@ const offsets = { }; const MapChart = (props) => { - const { setTooltipContent, category, maxValue, allStates, statePerformance } = props; - const colorScale = scaleQuantize() - .domain([0, maxValue]) - .range(["#F0F9E8", "#BAE4BC", "#7BCCC4", "#43A2CA", "#0868AC"]); + const { setTooltipContent, category, allStates, statePerformance, colorScale } = props; let categoryRecord; return (
@@ -176,8 +175,7 @@ const MapChart = (props) => { MapChart.propTypes = { setTooltipContent: PropTypes.func, - category: PropTypes.string, - maxValue: PropTypes.number + category: PropTypes.string }; const CategoryMap = ({ @@ -225,71 +223,42 @@ const CategoryMap = ({ return null; }); const maxValue = Math.max(...quantizeArray); - const label1 = (maxValue / 5) * 0; - const label2 = (maxValue / 5) * 1; - const label3 = (maxValue / 5) * 2; - const label4 = (maxValue / 5) * 3; - const label5 = (maxValue / 5) * 4; + const mapColor = ["#F0F9E8", "#BAE4BC", "#7BCCC4", "#43A2CA", "#0868AC"]; + let legendCategory = category; + if (category === "Structural") legendCategory = "Structural-CSP"; + if (category === "Vegetative") legendCategory = "Vegetative-CSP"; + if (category === "Land management") legendCategory = "Land management-CSP"; + if (category === "Forest management") legendCategory = "Forest management-CSP"; + if (category === "Soil testing") legendCategory = "Soil testing-CSP"; + if (category === "Other improvement") legendCategory = "Other improvement-CSP"; + const customScale = legendConfig[legendCategory]; + const colorScale = d3.scaleThreshold(customScale, mapColor); return (
{maxValue !== 0 ? ( - = 1000000 - ? `$${Number(label2 / 1000000).toLocaleString(undefined, { - maximumFractionDigits: 0 - })}M` - : `$${Number(label2 / 1000.0).toLocaleString(undefined, { - maximumFractionDigits: 1 - })}K` - } - label3={ - label3 >= 1000000 - ? `$${Number(label3 / 1000000).toLocaleString(undefined, { - maximumFractionDigits: 0 - })}M` - : `$${Number(label3 / 1000.0).toLocaleString(undefined, { - maximumFractionDigits: 1 - })}K` - } - label4={ - label4 >= 1000000 - ? `$${Number(label4 / 1000000).toLocaleString(undefined, { - maximumFractionDigits: 0 - })}M` - : `$${Number(label4 / 1000.0).toLocaleString(undefined, { - maximumFractionDigits: 1 - })}K` - } - label5={ - label5 >= 1000000 - ? `$${Number(label5 / 1000000).toLocaleString(undefined, { - maximumFractionDigits: 0 - })}M` - : `$${Number(label5 / 1000.0).toLocaleString(undefined, { - maximumFractionDigits: 1 - })}K` - } - label6={ - maxValue >= 1000000 - ? `$${Number(maxValue / 1000000).toLocaleString(undefined, { - maximumFractionDigits: 0 - })}M` - : `$${Number(maxValue / 1000.0).toLocaleString(undefined, { - maximumFractionDigits: 1 - })}K` - } - /> + {maxValue !== 0 ? ( + + ) : ( +
+ {titleElement(category, years)} + + + {category} data in {years} is unavailable for all states. + + +
+ )}
) : (
@@ -327,5 +296,16 @@ const CategoryMap = ({
); }; - +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 CategoryMap; diff --git a/src/components/csp/CategoryTable.tsx b/src/components/csp/CategoryTable.tsx index be2b7e3e..6c60acdb 100644 --- a/src/components/csp/CategoryTable.tsx +++ b/src/components/csp/CategoryTable.tsx @@ -6,6 +6,7 @@ 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/CategoryMap.tsx b/src/components/eqip/CategoryMap.tsx index 389074b9..93c6a976 100644 --- a/src/components/eqip/CategoryMap.tsx +++ b/src/components/eqip/CategoryMap.tsx @@ -2,13 +2,14 @@ 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 { scaleQuantize } from "d3-scale"; 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 HorizontalStackedBar from "../HorizontalStackedBar"; +import legendConfig from "../../utils/legendConfig.json"; +import DrawLegend from "../shared/DrawLegend"; const geoUrl = "https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json"; @@ -25,10 +26,7 @@ const offsets = { }; const MapChart = (props) => { - const { setTooltipContent, category, maxValue, statePerformance, allStates } = props; - const colorScale = scaleQuantize() - .domain([0, maxValue]) - .range(["#F0F9E8", "#BAE4BC", "#7BCCC4", "#43A2CA", "#0868AC"]); + const { setTooltipContent, category, statePerformance, allStates, colorScale } = props; return (
@@ -163,8 +161,7 @@ const MapChart = (props) => { MapChart.propTypes = { setTooltipContent: PropTypes.func, - category: PropTypes.string, - maxValue: PropTypes.number + category: PropTypes.string }; const CategoryMap = ({ @@ -185,7 +182,6 @@ const CategoryMap = ({ ) { years = Array(Array(Array(Object.values(statePerformance)[0])[0])[0])[0][0].years; } - const title = `${category} Benefits from ${years}`; const quantizeArray: number[] = []; Object.values(statePerformance).map((value) => { const statuteRecord = value[0].statutes; @@ -199,77 +195,41 @@ const CategoryMap = ({ return null; }); const maxValue = Math.max(...quantizeArray); - const label1 = (maxValue / 5) * 0; - const label2 = (maxValue / 5) * 1; - const label3 = (maxValue / 5) * 2; - const label4 = (maxValue / 5) * 3; - const label5 = (maxValue / 5) * 4; + const mapColor = ["#F0F9E8", "#BAE4BC", "#7BCCC4", "#43A2CA", "#0868AC"]; + const customScale = legendConfig[category]; + const colorScale = d3.scaleThreshold(customScale, mapColor); return (
- = 1000000 - ? `$${Number(label2 / 1000000).toLocaleString(undefined, { - maximumFractionDigits: 0 - })}M` - : `$${Number(label2 / 1000.0).toLocaleString(undefined, { - maximumFractionDigits: 1 - })}K` - } - label3={ - label3 >= 1000000 - ? `$${Number(label3 / 1000000).toLocaleString(undefined, { - maximumFractionDigits: 0 - })}M` - : `$${Number(label3 / 1000.0).toLocaleString(undefined, { - maximumFractionDigits: 1 - })}K` - } - label4={ - label4 >= 1000000 - ? `$${Number(label4 / 1000000).toLocaleString(undefined, { - maximumFractionDigits: 0 - })}M` - : `$${Number(label4 / 1000.0).toLocaleString(undefined, { - maximumFractionDigits: 1 - })}K` - } - label5={ - label5 >= 1000000 - ? `$${Number(label5 / 1000000).toLocaleString(undefined, { - maximumFractionDigits: 0 - })}M` - : `$${Number(label5 / 1000.0).toLocaleString(undefined, { - maximumFractionDigits: 1 - })}K` - } - label6={ - maxValue >= 1000000 - ? `$${Number(maxValue / 1000000).toLocaleString(undefined, { - maximumFractionDigits: 0 - })}M` - : `$${Number(maxValue / 1000.0).toLocaleString(undefined, { - maximumFractionDigits: 1 - })}K` - } - /> + {maxValue !== 0 ? ( + + ) : ( +
+ {titleElement(category, years)} + + + {category} data in {years} is unavailable for all states. + + +
+ )}
@@ -279,5 +239,16 @@ const CategoryMap = ({
); }; - +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 CategoryMap; diff --git a/src/components/eqip/CategoryTable.tsx b/src/components/eqip/CategoryTable.tsx index 4e6ad158..8874ec4a 100644 --- a/src/components/eqip/CategoryTable.tsx +++ b/src/components/eqip/CategoryTable.tsx @@ -6,6 +6,7 @@ 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/EQIPTotalMap.tsx b/src/components/eqip/EQIPTotalMap.tsx index 71a9ed49..f766998b 100644 --- a/src/components/eqip/EQIPTotalMap.tsx +++ b/src/components/eqip/EQIPTotalMap.tsx @@ -6,10 +6,11 @@ import { scaleQuantize } from "d3-scale"; 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 HorizontalStackedBar from "../HorizontalStackedBar"; +import legendConfig from "../../utils/legendConfig.json"; +import DrawLegend from "../shared/DrawLegend"; const geoUrl = "https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json"; @@ -25,11 +26,7 @@ const offsets = { DC: [49, 21] }; -const MapChart = ({ setTooltipContent, maxValue, allStates, statePerformance }) => { - const colorScale = scaleQuantize() - .domain([0, maxValue]) - .range(["#F0F9E8", "#BAE4BC", "#7BCCC4", "#43A2CA", "#0868AC"]); - +const MapChart = ({ setTooltipContent, maxValue, allStates, statePerformance, colorScale }) => { return (
@@ -153,13 +150,12 @@ MapChart.propTypes = { const EQIPTotalMap = ({ statePerformance, allStates }: { statePerformance: any; allStates: any }): JSX.Element => { const quantizeArray: number[] = []; + const category = "Total EQIP"; Object.values(statePerformance).map((value) => quantizeArray.push(value[0].totalPaymentInDollars)); const maxValue = Math.max(...quantizeArray); - const label1 = (maxValue / 5) * 0; - const label2 = (maxValue / 5) * 1; - const label3 = (maxValue / 5) * 2; - const label4 = (maxValue / 5) * 3; - const label5 = (maxValue / 5) * 4; + const mapColor = ["#F0F9E8", "#BAE4BC", "#7BCCC4", "#43A2CA", "#0868AC"]; + const customScale = legendConfig[category]; + const colorScale = d3.scaleThreshold(customScale, mapColor); const [content, setContent] = useState(""); // issue158: since eqip and csp are using old data structure (i.e. year is not the first level of data structure), going into array to find the year let years = "2018-2022"; @@ -172,38 +168,35 @@ const EQIPTotalMap = ({ statePerformance, allStates }: { statePerformance: any; return (
- + {maxValue !== 0 ? ( + + ) : ( +
+ {titleElement(category, years)} + + + {category} data in {years} is unavailable for all states. + + +
+ )}
@@ -213,5 +206,16 @@ const EQIPTotalMap = ({ statePerformance, allStates }: { statePerformance: any;
); }; - +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 index b58a2ceb..e00c7cc8 100644 --- a/src/components/eqip/EQIPTotalTable.tsx +++ b/src/components/eqip/EQIPTotalTable.tsx @@ -6,6 +6,7 @@ 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/shared/DrawLegend.tsx b/src/components/shared/DrawLegend.tsx index 742e0713..7a4486ef 100644 --- a/src/components/shared/DrawLegend.tsx +++ b/src/components/shared/DrawLegend.tsx @@ -3,6 +3,7 @@ import * as d3 from "d3"; import { Box, Typography } from "@mui/material"; import { ShortFormat } from "./ConvertionFormats"; import "../../styles/drawLegend.css"; + /** * Keys in legendConfig.json must match the 'searchKey' variable in DrawLegend.tsx file. * If there's any changes in legendConfig.json, please re-check and update the 'searchKey' variable here. @@ -47,7 +48,6 @@ export default function DrawLegend({ const customScale = colorScale.domain(); cut_points.push(Math.min(...programData)); cut_points = cut_points.concat(customScale); - const legendRectX: number[] = []; if (Math.min(...programData) !== Infinity && Math.max(...programData) !== Infinity) { baseSVG.selectAll("text").remove(); @@ -68,108 +68,121 @@ export default function DrawLegend({ data_distribution.push( programData.filter((d) => d >= cut_points[cut_points.length - 1]).length / programData.length ); - // Leave following part as the backup of solution 2. - // data_distribution.push( - // programData - // .filter((d) => d >= cut_points[cut_points.length - 1]) - // .reduce((accumulator, currentValue) => accumulator + currentValue, 0) / - // programData.reduce((accumulator, currentValue) => accumulator + currentValue, 0) - // ); const svgWidth = baseSVG.attr("width") - margin * 2; - baseSVG - .selectAll(null) - .data(data_distribution) - .enter() - .append("rect") - .attr("class", "legendRect") - .attr("id", (d) => `legendRect${d}`) - .attr("x", (d, i) => { - if (i === 0) { - return margin; - } - const sum = data_distribution.slice(0, i).reduce((acc, curr) => acc + curr, 0); - return margin + svgWidth * sum; - }) - .attr("y", () => { - return 20; - }) - .attr("width", (d) => { - return d * svgWidth; - }) - .attr("height", 10) - .style("fill", (d, i) => prepColor[i]); - cut_points = cut_points.concat(Math.max(...programData)); - baseSVG - .selectAll(".legendRect") - .nodes() - .forEach((d) => { - legendRectX.push(Number(d3.select(d).attr("x"))); - }); - const last = - legendRectX[legendRectX.length - 1] + data_distribution[data_distribution.length - 1] * svgWidth; - legendRectX.push(last); - if (window.innerWidth > 1679) { + // No need to show color length if there are less than five colors (i.e. not enough data points or label is not correctly identified) + if (!data_distribution.includes(0)) { baseSVG .selectAll(null) - .data(legendRectX) + .data(data_distribution) .enter() - .append("text") - .attr("class", "legendText") - .attr("id", (d) => `legendText${d}`) - .attr("y", 50) + .append("rect") + .attr("class", "legendRect") + .attr("id", (d) => `legendRect${d}`) .attr("x", (d, i) => { - return i === 0 ? d : d - margin / 4; - }) - .text((d, i) => { - if (isRatio) { - return `${Math.round(cut_points[i] * 100)}%`; - } - if (i === 0 && !notDollar) { - const res = ShortFormat(Math.round(cut_points[i]), i); - return res.indexOf("-") < 0 ? `$${res}` : `-$${res.substring(1)}`; + if (i === 0) { + return margin; } - return ShortFormat(Math.round(cut_points[i]), i); - }); - } else { - baseSVG - .selectAll(null) - .data(legendRectX) - .enter() - .append("text") - .attr("class", "legendText") - .attr("id", (d) => `legendText${d}`) - .attr("y", (d, i) => { - return i % 2 === 0 ? 50 : 10; + const sum = data_distribution.slice(0, i).reduce((acc, curr) => acc + curr, 0); + return margin + svgWidth * sum; }) - .attr("x", (d, i) => { - return i === 0 ? d : d - margin / 4; + .attr("y", () => { + return 20; }) - .text((d, i) => { - if (isRatio) { - return `${Math.round(cut_points[i] * 100)}%`; - } - if (i === 0 && !notDollar) { - const res = ShortFormat(Math.round(cut_points[i]), i); - return res.indexOf("-") < 0 ? `$${res}` : `-$${res.substring(1)}`; - } - return ShortFormat(Math.round(cut_points[i]), i); + .attr("width", (d) => { + return d * svgWidth; + }) + .attr("height", 10) + .style("fill", (d, i) => prepColor[i]); + cut_points = cut_points.concat(Math.max(...programData)); + baseSVG + .selectAll(".legendRect") + .nodes() + .forEach((d) => { + legendRectX.push(Number(d3.select(d).attr("x"))); }); - } - if (emptyState.length !== 0) { + const last = + legendRectX[legendRectX.length - 1] + + data_distribution[data_distribution.length - 1] * svgWidth; + legendRectX.push(last); + if (window.innerWidth > 1679) { + baseSVG + .selectAll(null) + .data(legendRectX) + .enter() + .append("text") + .attr("class", "legendText") + .attr("id", (d) => `legendText${d}`) + .attr("y", 50) + .attr("x", (d, i) => { + return i === 0 ? d : d - margin / 4; + }) + .text((d, i) => { + if (isRatio) { + return `${Math.round(cut_points[i] * 100)}%`; + } + if (i === 0 && !notDollar) { + const res = ShortFormat(Math.round(cut_points[i]), i); + return res.indexOf("-") < 0 ? `$${res}` : `-$${res.substring(1)}`; + } + return ShortFormat(Math.round(cut_points[i]), i); + }); + } else { + baseSVG + .selectAll(null) + .data(legendRectX) + .enter() + .append("text") + .attr("class", "legendText") + .attr("id", (d) => `legendText${d}`) + .attr("y", (d, i) => { + return i % 2 === 0 ? 50 : 10; + }) + .attr("x", (d, i) => { + return i === 0 ? d : d - margin / 4; + }) + .text((d, i) => { + if (isRatio) { + return `${Math.round(cut_points[i] * 100)}%`; + } + if (i === 0 && !notDollar) { + const res = ShortFormat(Math.round(cut_points[i]), i); + return res.indexOf("-") < 0 ? `$${res}` : `-$${res.substring(1)}`; + } + return ShortFormat(Math.round(cut_points[i]), i); + }); + } + if (emptyState.length !== 0) { + const middleText = baseSVG + .append("text") + .attr("class", "legendTextSide") + .attr("x", -1000) + .attr("y", -1000) + .text(`${emptyState.join(", ")}'s data is not available`); + const middleBox = middleText.node().getBBox(); + middleText.remove(); + baseSVG + .append("text") + .attr("class", "legendTextSide") + .attr("x", (svgWidth + margin * 2) / 2 - middleBox.width / 2) + .attr("y", 80) + .text(`${emptyState.join(", ")}'s data is not available`); + } + } else { + baseSVG.attr("height", 40); const middleText = baseSVG .append("text") .attr("class", "legendTextSide") .attr("x", -1000) .attr("y", -1000) - .text(`${emptyState.join(", ")}'s data is not available`); + .text("There isn't sufficient data to display the legend"); const middleBox = middleText.node().getBBox(); middleText.remove(); baseSVG .append("text") .attr("class", "legendTextSide") .attr("x", (svgWidth + margin * 2) / 2 - middleBox.width / 2) - .attr("y", 80) - .text(`${emptyState.join(", ")}'s data is not available`); + .attr("y", 16) + .text("There isn't sufficient data to display the legend"); } } } diff --git a/src/components/shared/NavSearchBar.tsx b/src/components/shared/NavSearchBar.tsx index a9b768ab..6a27915c 100644 --- a/src/components/shared/NavSearchBar.tsx +++ b/src/components/shared/NavSearchBar.tsx @@ -17,7 +17,7 @@ export default function NavSearchBar({ borderLeft: 0, borderRight: 0, borderColor: brColor, - padding: 1, + margin: 1, backgroundColor: bkColor }} > diff --git a/src/main.tsx b/src/main.tsx index cf749636..4c198db2 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,6 +3,7 @@ import { Routes, Route, useLocation } from "react-router-dom"; import LandingPage from "./pages/LandingPage"; import EQIPPage from "./pages/EQIPPage"; import CSPPage from "./pages/CSPPage"; +import CRPPage from "./pages/CRPPage"; import SNAPPage from "./pages/SNAPPage"; import TitleIPage from "./pages/TitleIPage"; import CropInsurancePage from "./pages/CropInsurancePage"; @@ -23,6 +24,7 @@ export default function Main(): JSX.Element { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/pages/CRPPage.tsx b/src/pages/CRPPage.tsx new file mode 100644 index 00000000..57d1085c --- /dev/null +++ b/src/pages/CRPPage.tsx @@ -0,0 +1,380 @@ +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 SemiDonutChart from "../components/SemiDonutChart"; +import DataTable from "../components/crp/CRPTotalTable"; +import CRPTotalMap from "../components/crp/CRPTotalMap"; +import CategoryTable from "../components/crp/CategoryTable"; +import CategoryMap from "../components/crp/CategoryMap"; +import { config } from "../app.config"; +import { convertAllState, getJsonDataFromUrl } from "../utils/apiutil"; +import NavSearchBar from "../components/shared/NavSearchBar"; + +export default function CRPPage(): JSX.Element { + const year = "2018-2022"; + const attribute = "paymentInDollars"; + 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 [subChartData, setSubChartData] = React.useState([{}]); + const [zeroCategories, setZeroCategories] = React.useState([]); + const [totalCrp, setTotalCrp] = React.useState(0); + const [totalSub, setTotalSub] = React.useState(0); + + const defaultTheme = createTheme(); + const zeroCategory = []; + let totalCRPPaymentInDollars = 0; + let generalSignUpPaymentInDollars = 0; + let continuousSingUpPaymentInDollars = 0; + let crepPaymentInDollars = 0; + let nocCrepPaymentInDollars = 0; + let wetlandPaymentInDollars = 0; + let grasslandPyamentInDollars = 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/crp/state-distribution`; + getJsonDataFromUrl(statedistribution_url).then((response) => { + setStateDistributionData(response); + }); + + const chartData_url = `${config.apiUrl}/programs/conservation/crp/subprograms`; + getJsonDataFromUrl(chartData_url).then((response) => { + processData(response); + }); + }, []); + + const processData = (chartData) => { + if (chartData.programs === undefined) return; + + const cur1 = chartData.programs.find((s) => s.programName === "Total CRP"); + const cur2 = chartData.programs.find((s) => s.programName === "Total General Sign-Up"); + const cur3 = chartData.programs.find((s) => s.programName === "Total Continuous"); + let cur4; + let cur5; + let cur6; + const cur7 = chartData.programs.find((s) => s.programName === "Grassland"); + const subCurs = cur3.subPrograms; + + if (subCurs !== undefined) { + subCurs.forEach((value) => { + if (value.programName === "CREP Only") { + cur4 = value; + } else if (value.programName === "Continuous Non-CREP") { + cur5 = value; + } else if (value.programName === "Farmable Wetland") { + cur6 = value; + } + }); + + totalCRPPaymentInDollars = cur1.paymentInDollars; + setTotalCrp(totalCRPPaymentInDollars); + if (totalCRPPaymentInDollars === 0) zeroCategory.push("Total CRP"); + generalSignUpPaymentInDollars = cur2.paymentInDollars; + if (generalSignUpPaymentInDollars === 0) zeroCategory.push("Total General Sign-Up"); + continuousSingUpPaymentInDollars = cur3.paymentInDollars; + if (continuousSingUpPaymentInDollars === 0) zeroCategory.push("Total Continuous"); + setTotalSub(continuousSingUpPaymentInDollars); + crepPaymentInDollars = cur4.paymentInDollars; + if (crepPaymentInDollars === 0) zeroCategory.push("CREP Only"); + nocCrepPaymentInDollars = cur5.paymentInDollars; + if (nocCrepPaymentInDollars === 0) zeroCategory.push("Continuous Non-CREP"); + wetlandPaymentInDollars = cur6.paymentInDollars; + if (wetlandPaymentInDollars === 0) zeroCategory.push("Farmable Wetland"); + grasslandPyamentInDollars = cur7.paymentInDollars; + if (grasslandPyamentInDollars === 0) zeroCategory.push("Grassland"); + + setZeroCategories(zeroCategory); + + setTotalChartData([ + { name: "Total General Sign-Up", value: generalSignUpPaymentInDollars, color: "#2F7164" }, + { name: "Total Continuous", value: continuousSingUpPaymentInDollars, color: "#869397" }, + { name: "Grassland", value: grasslandPyamentInDollars, color: "#9CBAB4" } + ]); + + setSubChartData([ + { name: "CREP Only", value: crepPaymentInDollars, color: "#2F7164" }, + { name: "Continuous Non-CREP", value: nocCrepPaymentInDollars, color: "#869397" }, + { name: "Farmable Wetland", value: wetlandPaymentInDollars, color: "#9CBAB4" } + ]); + } + }; + + return ( + + {Object.keys(stateCodesData).length > 0 && + Object.keys(allStatesData).length > 0 && + Object.keys(stateDistributionData).length > 0 ? ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CRP: Category of Practice Performance + + + + + + + CRP: Category of Practice Performance + + + + 2 && checked < 6 ? "block" : "none" + }} + > + + + CRP Total Continuous: Sub-Category of Practice Performance + + + + + + CRP provides annual rental payments to landowners in return for conservation practices + that remove the acres from production and implements conservation cover (e.g., grasses + and/or trees). CRP contracts are for 10 or 15 years and enrolled land can be an entire + field or portions of a field (e.g., grass waterways or buffers). CRP spending will be + visualized using the following categories. + + + +
+ + + + + + + 2 && checked < 6 ? "block" : "none" }}> + + +
+ + + + Overall Performance of States + + + + + + + + + + + + + + + + + + + + + + + +
+
+ ) : ( +

Loading data...

+ )} +
+ ); +} diff --git a/src/pages/EQIPPage.tsx b/src/pages/EQIPPage.tsx index 1c67bc3f..dc35e581 100644 --- a/src/pages/EQIPPage.tsx +++ b/src/pages/EQIPPage.tsx @@ -28,6 +28,7 @@ export default function EQIPPage(): JSX.Element { let comprehensiveNutrientMgtTotal = 0; let resourceConservingCropRotationTotal = 0; let soilHealthTotal = 0; + const zeroCategory = []; // connect to api endpoint const [statePerformance, setStatePerformance] = React.useState({}); @@ -37,6 +38,7 @@ export default function EQIPPage(): JSX.Element { const [sixBChartData, setSixBChartData] = React.useState([{}]); const [aTotal, setATotal] = React.useState(0); const [bTotal, setBTotal] = React.useState(0); + const [zeroCategories, setZeroCategories] = React.useState([]); React.useEffect(() => { const state_perf_url = `${config.apiUrl}/programs/conservation/eqip/state-distribution`; @@ -90,18 +92,30 @@ export default function EQIPPage(): JSX.Element { const soilHealthCur = BCur.find((s) => s.practiceCategoryName === "Soil health"); structuralTotal += Number(structuralCur.totalPaymentInDollars); + if (structuralTotal === 0) zeroCategory.push("Structural"); landManagementTotal += Number(landManagementCur.totalPaymentInDollars); + if (landManagementTotal === 0) zeroCategory.push("Land management"); vegetativeTotal += Number(vegetativeCur.totalPaymentInDollars); + if (vegetativeTotal === 0) zeroCategory.push("Vegetative"); forestManagementTotal += Number(forestManagementCur.totalPaymentInDollars); + if (forestManagementTotal === 0) zeroCategory.push("Forest management"); soilRemediationTotal += Number(soilRemediationCur.totalPaymentInDollars); + if (soilRemediationTotal === 0) zeroCategory.push("Soil remediation"); other6ATotal += Number(other6ACur.totalPaymentInDollars); + if (other6ATotal === 0) zeroCategory.push("Other improvement"); soilTestingTotal += Number(soilTestingCur.totalPaymentInDollars); + if (soilTestingTotal === 0) zeroCategory.push("Soil testing"); otherPlanningTotal += Number(otherPlanningCur.totalPaymentInDollars); + if (otherPlanningTotal === 0) zeroCategory.push("Other planning"); conservationPlanningAssessmentTotal += Number(conservationPlanningAssessmentCur.totalPaymentInDollars); + if (conservationPlanningAssessmentTotal === 0) zeroCategory.push("Conservation planning assessment"); comprehensiveNutrientMgtTotal += Number(comprehensiveNutrientMgtCur.totalPaymentInDollars); + if (comprehensiveNutrientMgtTotal === 0) zeroCategory.push("Comprehensive Nutrient Mgt."); resourceConservingCropRotationTotal += Number(resourceConservingCropRotationCur.totalPaymentInDollars); + if (resourceConservingCropRotationTotal === 0) zeroCategory.push("Resource-conserving crop rotation"); soilHealthTotal += Number(soilHealthCur.totalPaymentInDollars); + if (soilHealthTotal === 0) zeroCategory.push("Soil health"); setSixAChartData([ { name: "Structural", value: structuralTotal, color: "#2F7164" }, @@ -127,6 +141,8 @@ export default function EQIPPage(): JSX.Element { { name: "6 (A)", value: sixATotal, color: "#2F7164" }, { name: "6 (B)", value: sixBTotal, color: "#9CBAB4" } ]); + + setZeroCategories(zeroCategory); }; return ( @@ -140,7 +156,7 @@ export default function EQIPPage(): JSX.Element { subtext="Environmental Quality Incentives Program (EQIP)" /> - + { + let ans = ""; + Object.keys(stateRecord).forEach((key) => { + const match = key.match(/^(.*?)(?=\s*InDollars)/); + const extractedKey = match ? match[1] : key; + if (extractedKey === attribute) { + ans = key; + } + }); + return ans; +}; diff --git a/src/utils/legendConfig.json b/src/utils/legendConfig.json index 72937b00..edd6c2f5 100644 --- a/src/utils/legendConfig.json +++ b/src/utils/legendConfig.json @@ -2,14 +2,16 @@ "18-22 All Programs Total": [5000000000, 10000000000, 30000000000, 40000000000], "Crop Insurance Total":[0, 500000000, 1000000000, 5000000000], "totalNetFarmerBenefit": [0, 500000000, 1000000000, 5000000000], - "Title I Total":[100000000, 500000000, 1000000000, 2000000000], + "Title I Total":[50000000, 100000000, 500000000, 1000000000], "Total Commodities Programs": [100000000, 500000000, 1000000000, 2000000000], "Title II Total":[100000000, 500000000, 1000000000, 2000000000], "SNAP Total": [1000000000, 5000000000, 10000000000, 20000000000], + "Agriculture Risk Coverage (ARC)": [10000000, 50000000, 100000000, 500000000], "Agriculture Risk Coverage County Option (ARC-CO)": [50000000, 100000000, 250000000, 500000000], "Agriculture Risk Coverage Individual Coverage (ARC-IC)": [5000000, 10000000, 25000000, 50000000], "Price Loss Coverage (PLC)":[50000000, 100000000, 500000000, 1000000000], + "totalIndemnities":[10000000, 100000000, 1000000000, 5000000000], "totalPremium":[10000000, 100000000, 1000000000, 5000000000], "totalPremiumSubsidy": [5000000, 100000000, 500000000, 1000000000], @@ -17,6 +19,47 @@ "totalPoliciesEarningPremium": [1000, 10000, 100000, 500000], "averageLiabilities":[50000000, 1000000000, 5000000000, 10000000000], "lossRatio":[0.5, 0.75, 1, 1.5], - "averageInsuredAreaInAcres":[10000, 5000000, 10000000, 20000000] + "averageInsuredAreaInAcres":[10000, 5000000, 10000000, 20000000], + + "Total EQIP": [10000000, 50000000, 100000000, 200000000], + "Land management": [1000000, 10000000, 30000000, 50000000], + "Forest management": [500000, 1000000, 3000000,10000000], + "Structural":[10000000, 50000000, 80000000, 100000000], + "Soil remediation":[100000, 500000, 1000000, 5000000], + "Vegetative":[1000000, 5000000, 10000000, 20000000], + "Other improvement":[100000, 1000000, 5000000, 10000000], + "Soil testing":[100, 500, 1000, 3000], + "Other planning":[10000, 100000, 500000, 1000000], + "Conservation planning assessment":[5000, 100000, 500000, 1000000], + "Resource-conserving crop rotation":[500, 1000, 5000, 10000], + "Soil health":[10000, 100000, 500000, 1000000], + "Comprehensive Nutrient Mgt.":[1000, 10000, 100000, 1000000], + + "Total CSP": [10000000, 50000000, 100000000, 200000000], + "2018 Practices": [1000000, 5000000, 10000000, 80000000], + "2014 Eligible Land": [5000000, 10000000, 80000000, 150000000], + "Forest management-CSP": [100000, 500000, 1000000, 10000000], + "Land management-CSP": [10000, 1000000, 5000000, 10000000], + "Existing activity payments": [100000, 1000000, 3000000, 5000000], + "Vegetative-CSP": [500000, 1000000, 5000000, 10000000], + "Other improvement-CSP": [5000, 1000000, 5000000, 10000000], + "Soil remediation": [100000, 500000, 1000000, 3000000], + "Structural-CSP": [50000, 100000, 500000, 1000000], + "Bundles": [50000, 100000, 300000, 800000], + "Soil testing-CSP": [100000, 500000, 1000000, 10000000], + "Cropland": [1000000, 10000000, 100000000, 200000000], + "Non-industrial private forestland": [500000, 1000000, 3000000, 8000000], + "Pastureland": [1000000, 5000000, 8000000, 10000000], + "Grassland": [5000, 100000, 400000, 8000000], + "Other: supplemental, adjustment & other": [500000, 1000000, 5000000, 10000000], + "Rangeland": [100000, 5000000, 10000000, 30000000], + + "Total CRP": [100000, 10000000, 100000000, 500000000], + "Total General Sign-Up":[10000, 100000, 10000000, 100000000], + "Total Continuous Sign-Up":[10000, 100000, 100000000, 1000000000], + "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] }