Community Profiles
Search any community in Massachusetts to view their profile:
+
+
+
+
+
+
+
+
+
{
- toProfile(muni.toLowerCase().replace(/\s+/g, '-'));
- }}
+ onSelect={handleMuniSelect}
placeholder={'Search for a community ...'}
className={"small"}
/>
@@ -24,7 +145,11 @@ const CommunitySelectorView = ({ muniLines, muniFill, municipalityPoly, toProfil
);
diff --git a/src/components/RPAregionProfilesView.jsx b/src/components/RPAregionProfilesView.jsx
new file mode 100644
index 0000000..d8a52de
--- /dev/null
+++ b/src/components/RPAregionProfilesView.jsx
@@ -0,0 +1,506 @@
+import React, { useEffect, useState } from "react";
+import { Link, useParams } from "react-router-dom";
+import { useDispatch, useSelector } from "react-redux";
+import styled from 'styled-components';
+import Tab from "./Tab";
+import Dropdown from "./field/Dropdown";
+import tabs from "../constants/tabs";
+import charts from "../constants/charts";
+import { selectRPAregionData,fetchRPAregionChartData } from "../reducers/rparegionSlice";
+import StackedBarChart from "../containers/visualizations/StackedBarChart";
+import StackedAreaChart from "../containers/visualizations/StackedAreaChart";
+import ChartDetails from "./visualizations/ChartDetails";
+import PieChart from "../containers/visualizations/PieChart";
+import LineChart from "../containers/visualizations/LineChart";
+import DownloadAllChartsButton from './field/DownloadAllChartsButton';
+import DataTableModal from './field/DataTableModal';
+
+// Styled Components
+const MunicipalitiesList = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-top: 1rem;
+ margin-bottom: 1rem;
+ height: 100px;
+ overflow-y: auto;
+ padding: 10px;
+ background-color: #ffffff;
+ border: 1px solid #e0e0e0;
+ border-radius: 6px;
+
+ &::-webkit-scrollbar {
+ width: 8px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: #f1f1f1;
+ border-radius: 4px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: #888;
+ border-radius: 4px;
+ }
+
+ &::-webkit-scrollbar-thumb:hover {
+ background: #555;
+ }
+`;
+
+const MunicipalitiesRow = styled.div`
+ display: flex;
+ gap: 8px;
+ flex: 0 0 auto;
+ width: 100%;
+ min-height: 35px;
+`;
+
+const MunicipalityLinkWrapper = styled.div`
+ flex: 0 0 calc((100% - 72px) / 10); /* (100% - (9 * 8px gaps)) / 10 items */
+ min-width: 90px;
+`;
+
+const StyledLink = styled(Link)`
+ color: #0066cc;
+ text-decoration: none;
+ padding: 6px 24px 6px 8px;
+ border-radius: 4px;
+ background-color: #f5f5f5;
+ font-size: 13px;
+ white-space: nowrap;
+ border: 1px solid #e0e0e0;
+ transition: all 0.2s ease;
+ text-align: center;
+ width: 100%;
+ height: 35px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ position: relative;
+
+ &::after {
+ content: "↗";
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ transform: translateY(-50%);
+ font-size: 12px;
+ opacity: 0.7;
+ }
+
+ &:hover {
+ background-color: #e5e5e5;
+ text-decoration: underline;
+ border-color: #ccc;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+
+ &::after {
+ opacity: 1;
+ }
+ }
+`;
+
+const chunkArray = (array, size) => {
+ const chunked = [];
+ for (let i = 0; i < array.length; i += size) {
+ chunked.push(array.slice(i, i + size));
+ }
+ return chunked;
+};
+
+const RPAregionProfilesView = () => {
+ const dispatch = useDispatch();
+ const { rpaId, tab } = useParams();
+ const [activeTab, setActiveTab] = useState(tab || 'demographics');
+ const [modalConfig, setModalConfig] = useState({
+ show: false,
+ data: null,
+ title: ''
+ });
+
+ const rparegionData = useSelector(selectRPAregionData);
+ const municipalities = rparegionData[rpaId]?.municipalities || [];
+ const rpa_name = rparegionData[rpaId]?.rpa_name || '';
+
+
+
+ // Effect for fetching chart data
+ useEffect(() => {
+ if (charts[activeTab]) {
+ Object.values(charts[activeTab]).forEach((chart) =>
+ dispatch(fetchRPAregionChartData({ rpa_id: rpaId, chartInfo: chart }))
+ );
+ }
+ }, [activeTab, rpaId, dispatch]);
+
+ const handleShowModal = (data, title) => {
+ setModalConfig({
+ show: true,
+ data: data,
+ title: `${title} (Aggregated)`
+ });
+ }
+
+ const handleCloseModal = () => {
+ setModalConfig({
+ show: false,
+ data: null,
+ title: ''
+ });
+ };
+
+ return (
+
+
+
+ {"< Back"}
+
+
+
+
+
+
+ This RPA region contains {municipalities.length} municipalities. The charts below show aggregated data for all municipalities in this region.
+
+
+ {chunkArray(municipalities, 10).map((row, rowIndex) => (
+
+ {row.map(muni => (
+
+
+ {muni.muni_name}
+
+
+ ))}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {tabs.map((tabItem) => (
+ -
+ setActiveTab(tabItem.value)}
+ >
+ {tabItem.label}
+
+
+ ))}
+
+
+ setActiveTab(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default React.memo(RPAregionProfilesView);
\ No newline at end of file
diff --git a/src/components/SubregionProfilesView.jsx b/src/components/SubregionProfilesView.jsx
new file mode 100644
index 0000000..60a2c82
--- /dev/null
+++ b/src/components/SubregionProfilesView.jsx
@@ -0,0 +1,518 @@
+import React, { useEffect, useState } from "react";
+import { Link, useParams } from "react-router-dom";
+import { useDispatch, useSelector } from "react-redux";
+import styled from 'styled-components';
+import Tab from "./Tab";
+import Dropdown from "./field/Dropdown";
+import tabs from "../constants/tabs";
+import charts from "../constants/charts";
+import { fetchSubregionChartData, fetchSubregionData, selectSubregionData } from "../reducers/subregionSlice";
+import StackedBarChart from "../containers/visualizations/StackedBarChart";
+import StackedAreaChart from "../containers/visualizations/StackedAreaChart";
+import ChartDetails from "./visualizations/ChartDetails";
+import PieChart from "../containers/visualizations/PieChart";
+import LineChart from "../containers/visualizations/LineChart";
+import DownloadAllChartsButton from './field/DownloadAllChartsButton';
+import DataTableModal from './field/DataTableModal';
+
+// Styled Components
+const MunicipalitiesList = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ margin-top: 1rem;
+ margin-bottom: 1rem;
+ height: 100px;
+ overflow-y: auto;
+ padding: 10px;
+ background-color: #ffffff;
+ border: 1px solid #e0e0e0;
+ border-radius: 6px;
+
+ &::-webkit-scrollbar {
+ width: 8px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: #f1f1f1;
+ border-radius: 4px;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: #888;
+ border-radius: 4px;
+ }
+
+ &::-webkit-scrollbar-thumb:hover {
+ background: #555;
+ }
+`;
+
+const MunicipalitiesRow = styled.div`
+ display: flex;
+ gap: 8px;
+ flex: 0 0 auto;
+ width: 100%;
+ min-height: 35px;
+`;
+
+const MunicipalityLinkWrapper = styled.div`
+ flex: 0 0 calc((100% - 72px) / 10); /* (100% - (9 * 8px gaps)) / 10 items */
+ min-width: 90px;
+`;
+
+const StyledLink = styled(Link)`
+ color: #0066cc;
+ text-decoration: none;
+ padding: 6px 24px 6px 8px;
+ border-radius: 4px;
+ background-color: #f5f5f5;
+ font-size: 13px;
+ white-space: nowrap;
+ border: 1px solid #e0e0e0;
+ transition: all 0.2s ease;
+ text-align: center;
+ width: 100%;
+ height: 35px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ position: relative;
+
+ &::after {
+ content: "↗";
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ transform: translateY(-50%);
+ font-size: 12px;
+ opacity: 0.7;
+ }
+
+ &:hover {
+ background-color: #e5e5e5;
+ text-decoration: underline;
+ border-color: #ccc;
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+
+ &::after {
+ opacity: 1;
+ }
+ }
+`;
+
+const SUBREGIONS = {
+ 355: 'Inner Core Committee [ICC]',
+ 356: 'Minuteman Advisory Group on Interlocal Coordination [MAGIC]',
+ 357: 'MetroWest Regional Collaborative [MWRC]',
+ 358: 'North Shore Task Force [NSTF]',
+ 359: 'North Suburban Planning Council [NSPC]',
+ 360: 'South Shore Coalition [SSC]',
+ 361: 'South West Advisory Planning Committee [SWAP]',
+ 362: 'Three Rivers Interlocal Council [TRIC]'
+};
+
+const chunkArray = (array, size) => {
+ const chunked = [];
+ for (let i = 0; i < array.length; i += size) {
+ chunked.push(array.slice(i, i + size));
+ }
+ return chunked;
+};
+
+const SubregionProfilesView = () => {
+ const dispatch = useDispatch();
+ const { subregionId, tab } = useParams();
+ const [activeTab, setActiveTab] = useState(tab || 'demographics');
+ const [modalConfig, setModalConfig] = useState({
+ show: false,
+ data: null,
+ title: ''
+ });
+
+ const subregionData = useSelector(selectSubregionData);
+ const municipalities = subregionData[subregionId]?.municipalities || [];
+
+ useEffect(() => {
+ setActiveTab(tab);
+ }, [tab]);
+
+ // Effect for fetching chart data
+ useEffect(() => {
+ if (charts[activeTab]) {
+ Object.values(charts[activeTab]).forEach((chart) =>
+ dispatch(fetchSubregionChartData({ subregionId: subregionId, chartInfo: chart }))
+ );
+ }
+ }, [activeTab, subregionId, dispatch]);
+
+ const handleShowModal = (data, title) => {
+ setModalConfig({
+ show: true,
+ data: data,
+ title: `${title} (Aggregated)`
+ });
+ }
+
+ const handleCloseModal = () => {
+ setModalConfig({
+ show: false,
+ data: null,
+ title: ''
+ });
+ };
+
+ return (
+
+
+
+ {"< Back"}
+
+
+
+ {SUBREGIONS[subregionId]}
+
+
+
+
+ This subregion contains {municipalities.length} municipalities. The charts below show aggregated data for all municipalities in this subregion.
+
+
+ {chunkArray(municipalities, 10).map((row, rowIndex) => (
+
+ {row.map(muni => (
+
+
+ {muni.muni_name}
+
+
+ ))}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {tabs.map((tabItem) => (
+ -
+ setActiveTab(tabItem.value)}
+ >
+ {tabItem.label}
+
+
+ ))}
+
+
+ setActiveTab(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default React.memo(SubregionProfilesView);
\ No newline at end of file
diff --git a/src/components/field/DownloadAllChartsButton.jsx b/src/components/field/DownloadAllChartsButton.jsx
index afa0fcf..9336f86 100644
--- a/src/components/field/DownloadAllChartsButton.jsx
+++ b/src/components/field/DownloadAllChartsButton.jsx
@@ -6,6 +6,8 @@ import * as XLSX from "xlsx";
import charts from "../../constants/charts";
import PropTypes from "prop-types";
import { fetchChartData } from "../../reducers/chartSlice";
+import { fetchSubregionChartData } from "../../reducers/subregionSlice";
+import { fetchRPAregionChartData } from "../../reducers/rparegionSlice";
import { store } from "../../store";
const spin = keyframes`
@@ -50,20 +52,25 @@ const LoadingText = styled.span`
font-size: 14px;
`;
-const makeSelectAllChartsData = (allTables, muni) => {
+const makeSelectAllChartsData = (allTables, muni, datatype) => {
return createSelector(
- [
- (state) => {
- return state.chart.cache;
- },
- ],
+ [(state) => {
+ switch (datatype) {
+ case 'subregion':
+ return state.subregion.cache;
+ case 'rpa':
+ return state.rparegion.cache;
+ default:
+ return state.chart.cache;
+ }
+ }],
(cache) => {
const result = allTables.reduce(
(acc, table) => ({
...acc,
[table]: cache[table]?.[muni] || [],
}),
- {} // initial value as an empty object
+ {}
);
return result;
}
@@ -83,11 +90,11 @@ const allTables = (() => {
return Array.from(tables);
})();
-export default function DownloadAllChartsButton({ muni }) {
+export default function DownloadAllChartsButton({ muni, datatype }) {
const dispatch = useDispatch();
const [isLoading, setIsLoading] = useState(false);
const [loadingStatus, setLoadingStatus] = useState("");
- const selectAllChartsData = makeSelectAllChartsData(allTables, muni);
+ const selectAllChartsData = makeSelectAllChartsData(allTables, muni, datatype);
const allData = useSelector(selectAllChartsData);
const fetchMissingData = async () => {
@@ -103,10 +110,26 @@ export default function DownloadAllChartsButton({ muni }) {
if (needsFetch) {
totalToFetch++;
+ let fetchPromise;
+ switch (datatype) {
+ case 'subregion':
+ fetchPromise = dispatch(
+ fetchSubregionChartData({ subregionId: muni, chartInfo })
+ );
+ break;
+ case 'rpa':
+ fetchPromise = dispatch(
+ fetchRPAregionChartData({ rpa_id: muni, chartInfo })
+ );
+ break;
+ default:
+ fetchPromise = dispatch(
+ fetchChartData({ chartInfo, municipality: muni })
+ );
+ }
+
fetchPromises.push(
- dispatch(
- fetchChartData({ chartInfo: chartInfo, municipality: muni })
- ).then(() => {
+ fetchPromise.then(() => {
fetched++;
setLoadingStatus(`Fetching data (${fetched}/${totalToFetch})`);
})
@@ -135,7 +158,18 @@ export default function DownloadAllChartsButton({ muni }) {
Object.values(charts).forEach((category) => {
Object.values(category).forEach((chartInfo) => {
Object.keys(chartInfo.tables).forEach((tableName) => {
- excelData[tableName] = state.chart.cache[tableName]?.[muni] || [];
+ let data;
+ switch (datatype) {
+ case 'subregion':
+ data = state.subregion.cache[tableName]?.[muni] || [];
+ break;
+ case 'rpa':
+ data = state.rparegion.cache[tableName]?.[muni] || [];
+ break;
+ default:
+ data = state.chart.cache[tableName]?.[muni] || [];
+ }
+ excelData[tableName] = data;
});
});
});
@@ -165,9 +199,14 @@ export default function DownloadAllChartsButton({ muni }) {
Object.entries(data).forEach(([tableName, tableData]) => {
if (tableData && tableData.length > 0) {
- // Create worksheet with municipality header
- const muniHeader = [['Municipality:', muni], []];
- const ws = XLSX.utils.aoa_to_sheet(muniHeader);
+ // Create worksheet with header based on data type
+ const header = [
+ [datatype === 'subregion' ? 'Subregion:' :
+ datatype === 'rpa' ? 'RPA Region:' :
+ 'Municipality:', muni],
+ []
+ ];
+ const ws = XLSX.utils.aoa_to_sheet(header);
// Add the data starting at row 2
XLSX.utils.sheet_add_json(ws, tableData, { origin: 'A2', skipHeader: false });
@@ -180,13 +219,14 @@ export default function DownloadAllChartsButton({ muni }) {
});
setLoadingStatus("Downloading...");
- XLSX.writeFile(wb, `${muni}_all_charts_data.xlsx`);
+ const filename = `${muni}_${datatype}_charts_data.xlsx`;
+ XLSX.writeFile(wb, filename);
};
return (
{isLoading && }
@@ -199,4 +239,5 @@ export default function DownloadAllChartsButton({ muni }) {
DownloadAllChartsButton.propTypes = {
muni: PropTypes.string.isRequired,
+ datatype: PropTypes.oneOf(['municipality', 'subregion', 'rpa']).isRequired,
};
diff --git a/src/components/field/DownloadChartButton.jsx b/src/components/field/DownloadChartButton.jsx
index 76b2c39..4dc1ff8 100644
--- a/src/components/field/DownloadChartButton.jsx
+++ b/src/components/field/DownloadChartButton.jsx
@@ -19,6 +19,16 @@ const StyledButton = styled.button`
background: #5DB37A;
}
`;
+const SUBREGIONS = {
+ 355: 'Inner Core Committee [ICC]',
+ 356: 'Minuteman Advisory Group on Interlocal Coordination [MAGIC]',
+ 357: 'MetroWest Regional Collaborative [MWRC]',
+ 358: 'North Shore Task Force [NSTF]',
+ 359: 'North Suburban Planning Council [NSPC]',
+ 360: 'South Shore Coalition [SSC]',
+ 361: 'South West Advisory Planning Committee [SWAP]',
+ 362: 'Three Rivers Interlocal Council [TRIC]'
+};
const makeSelectChartData = (tables, muni) => createSelector(
[(state) => state.chart.cache],
@@ -28,7 +38,7 @@ const makeSelectChartData = (tables, muni) => createSelector(
}), {})
);
-export default function DownloadChartButton({ chart, muni }) {
+export default function DownloadChartButton({ chart, muni, isSubregion, isRPAregion }) {
const selectChartData = React.useMemo(
() => makeSelectChartData(Object.keys(chart.tables), muni),
[chart.tables, muni]
@@ -36,11 +46,42 @@ export default function DownloadChartButton({ chart, muni }) {
const chartData = useSelector(selectChartData);
+ // Add selector for subregion cache
+ const selectSubregionCache = createSelector(
+ [(state) => state.subregion.cache],
+ (cache) => {
+ if (isSubregion) {
+ const tableName = Object.keys(chart.tables)[0];
+ return cache[tableName]?.[muni] || [];
+ }
+ return [];
+ }
+ );
+
+ const selectRPAregionCache = createSelector(
+ [(state) => state.rparegion.cache],
+ (cache) => {
+ if (isRPAregion) {
+ const tableName = Object.keys(chart.tables)[0];
+ return cache[tableName]?.[muni] || [];
+ }
+ return [];
+ }
+ );
+
+ const subregionCache = useSelector(selectSubregionCache);
+ const rpaCache = useSelector(selectRPAregionCache);
const downloadCsv = () => {
try {
const tableName = Object.keys(chartData)[0];
- const data = chartData[tableName];
-
+ let data;
+ if (isRPAregion) {
+ data = rpaCache;
+ } else {
+ data = isSubregion ? subregionCache : chartData[tableName];
+ }
+
+
if (!data || data.length === 0) {
console.error('No data available for the selected municipality.');
return;
@@ -48,8 +89,16 @@ export default function DownloadChartButton({ chart, muni }) {
// Convert data to CSV
const headers = Object.keys(data[0]);
+ let firstRow;
+ if (isSubregion) {
+ firstRow = ['Subregion:', SUBREGIONS[muni]];
+ } else if (isRPAregion) {
+ firstRow = ['RPAregion:', "MAPC"];
+ } else {
+ firstRow = ['Municipality:', muni];
+ }
const csv = [
- ['Municipality:', muni].join(','),
+ firstRow,
headers.join(','),
...data.map(row =>
headers.map(header => {
@@ -65,7 +114,7 @@ export default function DownloadChartButton({ chart, muni }) {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
- link.setAttribute('download', `${chart.title}_${muni}.csv`);
+ link.setAttribute('download', `${chart.title}_${isSubregion ? SUBREGIONS[muni] : muni}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
diff --git a/src/components/visualizations/ChartDetails.jsx b/src/components/visualizations/ChartDetails.jsx
index 2dee7ea..4b802f2 100644
--- a/src/components/visualizations/ChartDetails.jsx
+++ b/src/components/visualizations/ChartDetails.jsx
@@ -45,7 +45,7 @@ const makeSelectChartData = (tables, muni) => createSelector(
}), {})
);
-const ChartDetails = ({ chart, children, muni, onViewData }) => {
+const ChartDetails = ({ chart, children, muni, onViewData, isSubregion, isRPAregion }) => {
const [timeframe, setTimeframe] = useState(typeof chart.timeframe === 'string' ? chart.timeframe : 'Unknown');
const selectChartData = React.useMemo(
@@ -57,6 +57,32 @@ const ChartDetails = ({ chart, children, muni, onViewData }) => {
const tableName = Object.keys(chartData)[0];
const data = chartData[tableName];
+ // Add selector for subregion cache
+ const selectSubregionCache = createSelector(
+ [(state) => state.subregion.cache],
+ (cache) => {
+ if (isSubregion) {
+ const tableName = Object.keys(chart.tables)[0];
+ return cache[tableName]?.[muni] || [];
+ }
+ return [];
+ }
+ );
+
+ const selectRPAregionCache = createSelector(
+ [(state) => state.rparegion.cache],
+ (cache) => {
+ if (isRPAregion) {
+ const tableName = Object.keys(chart.tables)[0];
+ return cache[tableName]?.[muni] || [];
+ }
+ return [];
+ }
+ );
+
+ const subregionCache = useSelector(selectSubregionCache);
+ const rpaCache = useSelector(selectRPAregionCache);
+
useEffect(() => {
if (typeof chart.timeframe === 'function') {
chart.timeframe().then(setTimeframe);
@@ -64,7 +90,14 @@ const ChartDetails = ({ chart, children, muni, onViewData }) => {
}, [chart.timeframe]);
const handleViewData = () => {
- onViewData(data, chart.title);
+ if (isSubregion) {
+ // Use the cached aggregated data from subregion state
+ onViewData(subregionCache, chart.title);
+ } else if (isRPAregion) {
+ onViewData(rpaCache, chart.title);
+ } else {
+ onViewData(data, chart.title);
+ }
};
return (
@@ -72,15 +105,21 @@ const ChartDetails = ({ chart, children, muni, onViewData }) => {
{chart.title || 'Chart Title'}
+ {isSubregion && ' (Aggregated)'}
View Data
-
+
{children}
@@ -130,6 +169,12 @@ ChartDetails.propTypes = {
}).isRequired,
muni: PropTypes.string.isRequired,
onViewData: PropTypes.func.isRequired,
+ isSubregion: PropTypes.bool,
+};
+
+ChartDetails.defaultProps = {
+ isSubregion: false,
+ isRPAregion: false
};
export default ChartDetails;
diff --git a/src/components/visualizations/LineChart.jsx b/src/components/visualizations/LineChart.jsx
index 9272872..540d748 100644
--- a/src/components/visualizations/LineChart.jsx
+++ b/src/components/visualizations/LineChart.jsx
@@ -319,28 +319,29 @@ class LineChart extends React.Component {
LineChart.propTypes = {
xAxis: PropTypes.shape({
label: PropTypes.string.isRequired,
+ format: PropTypes.func,
min: PropTypes.number,
max: PropTypes.number,
ticks: PropTypes.number,
- format: PropTypes.func,
}).isRequired,
yAxis: PropTypes.shape({
label: PropTypes.string.isRequired,
+ format: PropTypes.func,
min: PropTypes.number,
max: PropTypes.number,
ticks: PropTypes.number,
- format: PropTypes.func,
}).isRequired,
data: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
+ values: PropTypes.arrayOf(PropTypes.array).isRequired,
color: PropTypes.string,
- values: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)).isRequired,
})
).isRequired,
- hasData: PropTypes.bool.isRequired,
+ hasData: PropTypes.bool,
width: PropTypes.number,
height: PropTypes.number,
+ isSubregion: PropTypes.bool,
};
export default LineChart;
diff --git a/src/components/visualizations/PieChart.jsx b/src/components/visualizations/PieChart.jsx
index c182cb9..c567b66 100644
--- a/src/components/visualizations/PieChart.jsx
+++ b/src/components/visualizations/PieChart.jsx
@@ -204,13 +204,17 @@ class PieChart extends React.Component {
}
PieChart.propTypes = {
- colors: PropTypes.arrayOf(PropTypes.string),
data: PropTypes.arrayOf(
PropTypes.shape({
- value: PropTypes.number.isRequired,
label: PropTypes.string.isRequired,
+ value: PropTypes.number.isRequired,
})
).isRequired,
+ colors: PropTypes.arrayOf(PropTypes.string),
+ hasData: PropTypes.bool,
+ width: PropTypes.number,
+ height: PropTypes.number,
+ isSubregion: PropTypes.bool,
};
export default PieChart;
diff --git a/src/components/visualizations/StackedAreaChart.jsx b/src/components/visualizations/StackedAreaChart.jsx
index 4ba8ddf..8b7fec8 100644
--- a/src/components/visualizations/StackedAreaChart.jsx
+++ b/src/components/visualizations/StackedAreaChart.jsx
@@ -271,25 +271,27 @@ class StackedAreaChart extends React.Component {
}
StackedAreaChart.propTypes = {
- data: PropTypes.arrayOf(
- PropTypes.shape({
- x: PropTypes.number.isRequired,
- y: PropTypes.number.isRequired,
- z: PropTypes.string.isRequired,
- })
- ).isRequired,
xAxis: PropTypes.shape({
label: PropTypes.string.isRequired,
format: PropTypes.func,
+ ticks: PropTypes.number,
}).isRequired,
yAxis: PropTypes.shape({
label: PropTypes.string.isRequired,
format: PropTypes.func,
}).isRequired,
+ data: PropTypes.arrayOf(
+ PropTypes.shape({
+ x: PropTypes.number.isRequired,
+ y: PropTypes.number.isRequired,
+ z: PropTypes.string.isRequired,
+ })
+ ).isRequired,
colors: PropTypes.arrayOf(PropTypes.string),
hasData: PropTypes.bool,
width: PropTypes.number,
height: PropTypes.number,
+ isSubregion: PropTypes.bool,
};
StackedAreaChart.defaultProps = {
diff --git a/src/components/visualizations/StackedBarChart.jsx b/src/components/visualizations/StackedBarChart.jsx
index 3f01074..f10f034 100644
--- a/src/components/visualizations/StackedBarChart.jsx
+++ b/src/components/visualizations/StackedBarChart.jsx
@@ -176,7 +176,16 @@ const StackedBarChart = (props) => {
.range([0, width])
: d3
.scaleBand()
- .domain(data.map(d => d.x))
+ .domain(data.map(d => d.x).sort((a, b) => {
+ // If both values can be parsed as numbers (e.g. years), sort numerically
+ const numA = parseInt(a);
+ const numB = parseInt(b);
+ if (!isNaN(numA) && !isNaN(numB)) {
+ return numA - numB;
+ }
+ // Otherwise use the provided sort function or default string comparison
+ return props.xAxis.sort ? props.xAxis.sort(a, b) : (a > b ? 1 : -1);
+ }))
.range([0, width])
.paddingInner(0.2);
@@ -263,12 +272,12 @@ const StackedBarChart = (props) => {
// Add axes with proper formatting
const xAxis = props.horizontal
- ? d3.axisBottom(xScale).tickFormat(props.xAxis.format || (d => d < 1 ? d3.format('.0%')(d) : d))
+ ? d3.axisBottom(xScale).tickFormat(props.xAxis.format || (d => d <= 1 ? d3.format('.0%')(d) : d))
: d3.axisBottom(xScale).tickFormat(props.xAxis.format);
const yAxis = props.horizontal
? d3.axisLeft(yScale)
- : d3.axisLeft(yScale).tickFormat(props.yAxis.format || (d => d < 1 ? d3.format('.0%')(d) : d));
+ : d3.axisLeft(yScale).tickFormat(props.yAxis.format || (d => d <= 1 ? d3.format('.0%')(d) : d));
// Add axes
const xAxisG = g
@@ -394,6 +403,8 @@ StackedBarChart.propTypes = {
wrapLeftLabel: PropTypes.bool,
width: PropTypes.number,
height: PropTypes.number,
+ isSubregion: PropTypes.bool,
+ isRPAregion: PropTypes.bool,
};
export default StackedBarChart;
\ No newline at end of file
diff --git a/src/constants/charts.js b/src/constants/charts.js
index 79a0e3a..f236e35 100644
--- a/src/constants/charts.js
+++ b/src/constants/charts.js
@@ -124,6 +124,46 @@ export default {
[]
);
},
+ subregionDataQuery: (subregionId) => {
+ const queryString = `
+ select
+ acs_year,
+ nhwhi,
+ nhaa,
+ nhna,
+ nhas,
+ nhpi,
+ nhoth,
+ nhmlt,
+ lat
+ from
+ tabular.b03002_race_ethnicity_acs_m
+ where muni_id = '${subregionId}'
+ AND acs_year = ( SELECT MAX(acs_year)
+ FROM tabular.b03002_race_ethnicity_acs_m)
+ `;
+ return queryString;
+ },
+ rparegionDataQuery: (rpaId) => {
+ const queryString = `
+ select
+ acs_year,
+ nhwhi,
+ nhaa,
+ nhna,
+ nhas,
+ nhpi,
+ nhoth,
+ nhmlt,
+ lat
+ from
+ tabular.b03002_race_ethnicity_acs_m
+ where muni_id = '${rpaId}'
+ AND acs_year = ( SELECT MAX(acs_year)
+ FROM tabular.b03002_race_ethnicity_acs_m)
+ `;
+ return queryString;
+ },
},
pop_by_age: {
type: "stacked-bar",
@@ -189,6 +229,62 @@ export default {
z: chart.labels[k],
}));
},
+ subregionDataQuery: (subregionId) => {
+ const queryString = `
+ SELECT
+ years,
+ totpop,
+ pop_u18,
+ pop18_24,
+ pop25_34,
+ pop35_39,
+ pop40_44,
+ pop45_49,
+ pop50_54,
+ pop55_59,
+ pop60_61,
+ pop62_64,
+ pop65_66,
+ pop67_69,
+ pop70_74,
+ pop75_79,
+ pop80_84,
+ pop85o
+ FROM
+ tabular.census2010_p12_pop_by_age_m
+ WHERE
+ muni_id = '${subregionId}'
+ `;
+ return queryString;
+ },
+ rparegionDataQuery: (rpaId) => {
+ const queryString = `
+ SELECT
+ years,
+ totpop,
+ pop_u18,
+ pop18_24,
+ pop25_34,
+ pop35_39,
+ pop40_44,
+ pop45_49,
+ pop50_54,
+ pop55_59,
+ pop60_61,
+ pop62_64,
+ pop65_66,
+ pop67_69,
+ pop70_74,
+ pop75_79,
+ pop80_84,
+ pop85o
+ FROM
+ tabular.census2010_p12_pop_by_age_m
+ WHERE
+ muni_id = '${rpaId}'
+ `;
+ return queryString;
+ },
},
},
economy: {
@@ -247,6 +343,30 @@ export default {
[]
);
},
+ subregionDataQuery: (subregionId) => {
+ const queryString = `
+ SELECT
+ acs_year,
+ SUM(emp) AS emp,
+ SUM(unemp) AS unemp
+ FROM tabular.b23025_employment_acs_m
+ WHERE muni_id = '${subregionId}'
+ AND acs_year IN (
+ SELECT DISTINCT acs_year
+ FROM tabular.b23025_employment_acs_m
+ WHERE muni_id IN (
+ SELECT muni_id
+ FROM tabular._datakeys_muni_all
+ WHERE subrg_id = ${subregionId}
+ )
+ ORDER BY acs_year DESC
+ LIMIT 2
+ )
+ GROUP BY acs_year
+ ORDER BY acs_year DESC;
+ `;
+ return queryString;
+ },
},
emp_by_sector: {
type: "stacked-area",
@@ -333,6 +453,34 @@ export default {
}, []);
return data;
},
+ subregionDataQuery: (subregionId) => {
+ const queryString = `
+ SELECT
+ cal_year,
+ naicstitle,
+ naicscode,
+ avgemp
+ FROM tabular.econ_es202_naics_2d_m
+ WHERE muni_id = '${subregionId}'
+ AND naicstitle IS NOT NULL
+ ORDER BY cal_year, naicstitle;
+ `;
+ return queryString;
+ },
+ rparegionDataQuery: (rpaId) => {
+ const queryString = `
+ SELECT
+ cal_year,
+ naicstitle,
+ naicscode,
+ avgemp
+ FROM tabular.econ_es202_naics_2d_m
+ WHERE muni_id = '${rpaId}'
+ AND naicstitle IS NOT NULL
+ ORDER BY cal_year, naicstitle;
+ `;
+ return queryString;
+ },
},
},
education: {
@@ -434,6 +582,16 @@ export default {
);
return data;
},
+ subregionDataQuery: (subregionId) => {
+ const queryString = `
+ `;
+ return queryString;
+ },
+ rparegionDataQuery: (rpaId) => {
+ const queryString = `
+ `;
+ return queryString;
+ },
},
edu_attainment_by_race: {
type: "stacked-bar",
@@ -552,6 +710,48 @@ export default {
[]
);
},
+ subregionDataQuery: (subregionId) => {
+ const queryString = `
+ SELECT
+ acs_year,
+ nhwlh, nhwhs, nhwsc, nhwbd,
+ aalh, aahs, aasc, aabd,
+ nalh, nahs, nasc, nabd,
+ aslh, ashs, assc, asbd,
+ pilh, pihs, pisc, pibd,
+ othlh, othhs, othsc, othbd,
+ mltlh, mlths, mltsc, mltbd,
+ latlh, laths, latsc, latbd
+ FROM tabular.c15002_educational_attainment_by_race_acs_m
+ WHERE muni_id = '${subregionId}'
+ AND acs_year = (
+ SELECT MAX(acs_year)
+ FROM tabular.c15002_educational_attainment_by_race_acs_m
+ )
+ `;
+ return queryString;
+ },
+ rparegionDataQuery: (rpaId) => {
+ const queryString = `
+ SELECT
+ acs_year,
+ nhwlh, nhwhs, nhwsc, nhwbd,
+ aalh, aahs, aasc, aabd,
+ nalh, nahs, nasc, nabd,
+ aslh, ashs, assc, asbd,
+ pilh, pihs, pisc, pibd,
+ othlh, othhs, othsc, othbd,
+ mltlh, mlths, mltsc, mltbd,
+ latlh, laths, latsc, latbd
+ FROM tabular.c15002_educational_attainment_by_race_acs_m
+ WHERE muni_id = '${rpaId}'
+ AND acs_year = (
+ SELECT MAX(acs_year)
+ FROM tabular.c15002_educational_attainment_by_race_acs_m
+ )
+ `;
+ return queryString;
+ },
},
},
governance: {
@@ -617,6 +817,30 @@ export default {
label: chart.labels[key],
}));
},
+ subregionDataQuery: (subregionId) => {
+ const queryString = `
+ SELECT
+ fy,
+ res_taxes,
+ os_taxes,
+ comm_taxes,
+ ind_taxes,
+ p_prop_tax,
+ tot_rev
+ FROM tabular.econ_municipal_taxes_revenue_m
+ WHERE muni_id = '${subregionId}'
+ AND fy = (
+ SELECT MAX(fy)
+ FROM tabular.econ_municipal_taxes_revenue_m
+ WHERE muni_id IN (
+ SELECT muni_id
+ FROM tabular._datakeys_muni_all
+ WHERE subrg_id = ${subregionId}
+ )
+ )
+ `;
+ return queryString;
+ },
},
},
environment: {
@@ -677,6 +901,10 @@ export default {
},
];
},
+ subregionDataQuery: (subregionId) => {
+ const queryString = ``;
+ return queryString;
+ },
},
energy_usage_gas: {
type: "stacked-area",
@@ -731,8 +959,16 @@ SELECT CONCAT(MIN(cal_year), '-', MAX(cal_year)) AS latest_year FROM years;`;
]),
[]
);
+ console.log("energy_usage_gas", data);
return data;
},
+ subregionDataQuery: (subregionId) => {
+ const queryString1 = `
+ `;
+ const queryString2 = `
+ `;
+ return [queryString1, queryString2];
+ },
},
energy_usage_electricity: {
type: "stacked-area",
@@ -789,6 +1025,13 @@ SELECT CONCAT(MIN(cal_year), '-', MAX(cal_year)) AS latest_year FROM years;`;
[]
);
},
+ subregionDataQuery: (subregionId) => {
+ const queryString1 = `
+ `;
+ const queryString2 = `
+ `;
+ return [queryString1, queryString2];
+ },
},
},
housing: {
@@ -876,11 +1119,38 @@ SELECT CONCAT(MIN(cal_year), '-', MAX(cal_year)) AS latest_year FROM years;`;
},
];
},
+ subregionDataQuery: (subregionId) => {
+ const queryString = `
+ SELECT
+ acs_year,
+ occv2,
+ cb,
+ o_notcb,
+ r_notcb,
+ ocb3050,
+ rcb3050,
+ cb_3050,
+ o_cb50,
+ r_cb50,
+ cb_50
+ FROM tabular.b25091_b25070_costburden_acs_m
+ WHERE muni_id = '${subregionId}'
+ AND acs_year = (
+ SELECT MAX(acs_year)
+ FROM tabular.b25091_b25070_costburden_acs_m
+ )
+ `;
+ return queryString;
+ },
},
units_permitted: {
type: "stacked-area",
title: "Housing Units Permitted",
- xAxis: { label: "Year" },
+ xAxis: {
+ label: "Year",
+ format: format.string.default,
+ sort: (a, b) => parseInt(a) - parseInt(b)
+ },
yAxis: { label: "Units Permitted" },
tables: {
"tabular.hous_building_permits_m": {
@@ -950,6 +1220,10 @@ SELECT CONCAT(MIN(cal_year), '-', MAX(cal_year)) AS latest_year FROM years;`;
[]
);
},
+ subregionDataQuery: (subregionId) => {
+ const queryString = ``;
+ return queryString;
+ },
},
},
"public-health": {
@@ -1047,6 +1321,11 @@ SELECT CONCAT(MIN(cal_year), '-', MAX(cal_year)) AS latest_year FROM years;`;
[]
);
},
+ subregionDataQuery: (subregionId) => {
+ const queryString = `
+ `;
+ return queryString;
+ },
},
hospitalizations: {
type: "stacked-bar",
@@ -1150,6 +1429,11 @@ SELECT CONCAT(MIN(cal_year), '-', MAX(cal_year)) AS latest_year FROM years;`;
);
return [];
},
+ subregionDataQuery: (subregionId) => {
+ const queryString = `
+ `;
+ return queryString;
+ },
},
},
transportation: {
@@ -1207,6 +1491,10 @@ SELECT CONCAT(MIN(cal_year), '-', MAX(cal_year)) AS latest_year FROM years;`;
[]
);
},
+ subregionDataQuery: (subregionId) => {
+ const queryString = ``;
+ return queryString;
+ },
},
commute_to_work: {
type: "pie",
@@ -1259,6 +1547,27 @@ SELECT CONCAT(MIN(cal_year), '-', MAX(cal_year)) AS latest_year FROM years;`;
label: chart.labels[key],
}));
},
+ subregionDataQuery: (subregionId) => {
+ const queryString = `
+ SELECT
+ acs_year,
+ ctvsngl,
+ carpool,
+ pub,
+ taxi,
+ mcycle,
+ bicycle,
+ walk,
+ other
+ FROM tabular.b08301_means_transportation_to_work_by_residence_acs_m
+ WHERE muni_id = '${subregionId}'
+ AND acs_year = (
+ SELECT MAX(acs_year)
+ FROM tabular.b08301_means_transportation_to_work_by_residence_acs_m
+ )
+ `;
+ return queryString;
+ },
},
},
};
diff --git a/src/containers/visualizations/LineChart.js b/src/containers/visualizations/LineChart.js
index 7e241a1..0ed81be 100644
--- a/src/containers/visualizations/LineChart.js
+++ b/src/containers/visualizations/LineChart.js
@@ -1,6 +1,7 @@
import { connect } from 'react-redux';
import LineChart from '../../components/visualizations/LineChart';
+
function valuesHaveData(transformedData) {
if (!Array.isArray(transformedData) || transformedData.length === 0) return false;
@@ -11,12 +12,71 @@ function valuesHaveData(transformedData) {
);
}
+
const mapStateToProps = (state, props) => {
- const { muni, chart } = props;
+ const { muni, chart, isSubregion, isRPAregion } = props;
const tables = Object.keys(chart.tables);
- if (tables.every((table) => state.chart.cache[table] && state.chart.cache[table][muni])) {
- // Create a new object for muniTables with spread operator
+ // Handle RPA region data
+ if (isRPAregion) {
+ if (tables.every((table) => state.rparegion.cache[table] && state.rparegion.cache[table][muni])) {
+ const rparegionTables = tables.reduce((acc, table) => ({
+ ...acc,
+ [table]: state.rparegion.cache[table][muni]
+ }), {});
+
+ try {
+ const transformedData = chart.transformer(rparegionTables, chart);
+ return {
+ ...props,
+ xAxis: chart.xAxis,
+ yAxis: chart.yAxis,
+ data: transformedData,
+ hasData: valuesHaveData(transformedData),
+ };
+ } catch (error) {
+ console.error('Error transforming RPA region data:', error);
+ return {
+ ...props,
+ xAxis: { label: '' },
+ yAxis: { label: '' },
+ data: [],
+ hasData: false,
+ };
+ }
+ }
+ }
+ // Handle subregion data
+ else if (isSubregion) {
+ if (tables.every((table) => state.subregion.cache[table] && state.subregion.cache[table][muni])) {
+ const subregionTables = tables.reduce((acc, table) => ({
+ ...acc,
+ [table]: state.subregion.cache[table][muni]
+ }), {});
+
+ try {
+ const transformedData = chart.transformer(subregionTables, chart);
+ return {
+ ...props,
+ xAxis: chart.xAxis,
+ yAxis: chart.yAxis,
+ data: transformedData,
+ hasData: valuesHaveData(transformedData),
+ };
+ } catch (error) {
+ console.error('Error transforming subregion data:', error);
+ return {
+ ...props,
+ xAxis: { label: '' },
+ yAxis: { label: '' },
+ data: [],
+ hasData: false,
+ };
+ }
+ }
+ }
+ // Handle regular municipality data
+ else if (tables.every((table) => state.chart.cache[table] && state.chart.cache[table][muni])) {
const muniTables = tables.reduce((acc, table) => ({
...acc,
[table]: state.chart.cache[table][muni]
@@ -24,7 +84,6 @@ const mapStateToProps = (state, props) => {
try {
const transformedData = chart.transformer(muniTables, chart);
-
return {
...props,
xAxis: chart.xAxis,
@@ -36,10 +95,10 @@ const mapStateToProps = (state, props) => {
console.error('Error transforming data:', error);
return {
...props,
- xAxis: { label: '' },
- yAxis: { label: '' },
- data: [],
- hasData: false,
+ xAxis: { label: '' },
+ yAxis: { label: '' },
+ data: [],
+ hasData: false,
};
}
}
@@ -56,4 +115,4 @@ const mapStateToProps = (state, props) => {
const mapDispatchToProps = (dispatch, props) => ({});
export default connect(mapStateToProps, mapDispatchToProps)(LineChart);
-export { valuesHaveData };
+export {valuesHaveData};
diff --git a/src/containers/visualizations/PieChart.js b/src/containers/visualizations/PieChart.js
index fccfb4f..4a3863e 100644
--- a/src/containers/visualizations/PieChart.js
+++ b/src/containers/visualizations/PieChart.js
@@ -1,10 +1,11 @@
import { connect } from 'react-redux';
import PieChart from '../../components/visualizations/PieChart';
+
function valuesHaveData(transformedData) {
const checkData = transformedData.reduce((acc, row) => {
let datumHasValue = false;
- if (row.value !== null && row.value !== 0) {
+ if (row.y !== null && row.y !== 0) {
datumHasValue = true;
}
acc.push(datumHasValue);
@@ -18,24 +19,87 @@ function valuesHaveData(transformedData) {
}
const mapStateToProps = (state, props) => {
- const { muni, chart } = props;
+ const { muni, chart, isSubregion, isRPAregion } = props;
const tables = Object.keys(chart.tables);
- if (tables.every((table) => state.chart.cache[table] && state.chart.cache[table][muni])) {
- // Create muniTables while preserving original functionality
- const muniTables = tables.reduce((acc, table) =>
- Object.assign({}, acc, {
- [table]: state.chart.cache[table][muni]
+ // Handle RPA region data
+ if (isRPAregion) {
+ if (tables.every((table) => state.rparegion.cache[table] && state.rparegion.cache[table][muni])) {
+ const rparegionTables = tables.reduce((acc, table) => ({
+ ...acc,
+ [table]: state.rparegion.cache[table][muni]
+ }), {});
+
+ try {
+ const transformedData = chart.transformer(rparegionTables, chart);
+ return {
+ ...props,
+ xAxis: chart.xAxis,
+ data: transformedData,
+ hasData: valuesHaveData(transformedData),
+ };
+ } catch (error) {
+ console.error('Error transforming RPA region data:', error);
+ return {
+ ...props,
+ xAxis: { format: (d) => d },
+ data: [],
+ hasData: false,
+ };
+ }
+ }
+ }
+ // Handle subregion data
+ else if (isSubregion) {
+ if (tables.every((table) => state.subregion.cache[table] && state.subregion.cache[table][muni])) {
+ const subregionTables = tables.reduce((acc, table) => ({
+ ...acc,
+ [table]: state.subregion.cache[table][muni]
}), {});
- const transformedData = chart.transformer(muniTables, chart);
-
- return {
- ...props,
- xAxis: chart.xAxis,
- data: transformedData,
- hasData: valuesHaveData(transformedData),
- };
+ try {
+ const transformedData = chart.transformer(subregionTables, chart);
+ return {
+ ...props,
+ xAxis: chart.xAxis,
+ data: transformedData,
+ hasData: valuesHaveData(transformedData),
+ };
+ } catch (error) {
+ console.error('Error transforming subregion data:', error);
+ return {
+ ...props,
+ xAxis: { format: (d) => d },
+ data: [],
+ hasData: false,
+ };
+ }
+ }
+ }
+ // Handle regular municipality data
+ else if (tables.every((table) => state.chart.cache[table] && state.chart.cache[table][muni])) {
+ const muniTables = tables.reduce((acc, table) => ({
+ ...acc,
+ [table]: state.chart.cache[table][muni]
+ }), {});
+
+ try {
+ const transformedData = chart.transformer(muniTables, chart);
+ return {
+ ...props,
+ xAxis: chart.xAxis,
+ data: transformedData,
+ hasData: valuesHaveData(transformedData),
+ };
+ } catch (error) {
+ console.error('Error transforming data:', error);
+ return {
+ ...props,
+ xAxis: { format: (d) => d },
+ data: [],
+ hasData: false,
+ };
+ }
}
return {
diff --git a/src/containers/visualizations/StackedAreaChart.js b/src/containers/visualizations/StackedAreaChart.js
index 66c83db..6495d6f 100644
--- a/src/containers/visualizations/StackedAreaChart.js
+++ b/src/containers/visualizations/StackedAreaChart.js
@@ -17,27 +17,94 @@ function valuesHaveData(transformedData) {
return false;
}
+
const mapStateToProps = (state, props) => {
- const { muni, chart } = props;
+ const { muni, chart, isSubregion, isRPAregion } = props;
const tables = Object.keys(chart.tables);
- if (tables.every((table) => state.chart.cache[table] && state.chart.cache[table][muni])) {
- const muniTables = tables.reduce((acc, table) => Object.assign(acc, { [table]: state.chart.cache[table][muni] }), {});
- return {
- ...props,
- xAxis: chart.xAxis,
- yAxis: chart.yAxis,
- data: chart.transformer(muniTables, chart),
- hasData: valuesHaveData(chart.transformer(muniTables, chart)),
- };
+
+ // Handle RPA region data
+ if (isRPAregion) {
+ if (tables.every((table) => state.rparegion.cache[table] && state.rparegion.cache[table][muni])) {
+ const rparegionTables = tables.reduce((acc, table) => ({
+ ...acc,
+ [table]: state.rparegion.cache[table][muni]
+ }), {});
+
+ try {
+ const transformedData = chart.transformer(rparegionTables, chart);
+ return {
+ ...props,
+ xAxis: chart.xAxis,
+ data: transformedData,
+ hasData: valuesHaveData(transformedData),
+ };
+ } catch (error) {
+ console.error('Error transforming RPA region data:', error);
+ return {
+ ...props,
+ xAxis: { format: (d) => d },
+ data: [],
+ hasData: false,
+ };
+ }
+ }
+ }
+ // Handle subregion data
+ else if (isSubregion) {
+ if (tables.every((table) => state.subregion.cache[table] && state.subregion.cache[table][muni])) {
+ const subregionTables = tables.reduce((acc, table) => ({
+ ...acc,
+ [table]: state.subregion.cache[table][muni]
+ }), {});
+
+ try {
+ const transformedData = chart.transformer(subregionTables, chart);
+ return {
+ ...props,
+ xAxis: chart.xAxis,
+ data: transformedData,
+ hasData: valuesHaveData(transformedData),
+ };
+ } catch (error) {
+ console.error('Error transforming subregion data:', error);
+ return {
+ ...props,
+ xAxis: { format: (d) => d },
+ data: [],
+ hasData: false,
+ };
+ }
+ }
}
+ // Handle regular municipality data
+ else if (tables.every((table) => state.chart.cache[table] && state.chart.cache[table][muni])) {
+ const muniTables = tables.reduce((acc, table) => ({
+ ...acc,
+ [table]: state.chart.cache[table][muni]
+ }), {});
+
+ try {
+ const transformedData = chart.transformer(muniTables, chart);
+ return {
+ ...props,
+ xAxis: chart.xAxis,
+ data: transformedData,
+ hasData: valuesHaveData(transformedData),
+ };
+ } catch (error) {
+ console.error('Error transforming data:', error);
+ return {
+ ...props,
+ xAxis: { format: (d) => d },
+ data: [],
+ hasData: false,
+ };
+ }
+ }
+
return {
...props,
- xAxis: {
- label: '',
- },
- yAxis: {
- label: '',
- },
+ xAxis: { format: (d) => d },
data: [],
hasData: false,
};
@@ -46,4 +113,3 @@ const mapStateToProps = (state, props) => {
const mapDispatchToProps = (dispatch, props) => ({});
export default connect(mapStateToProps, mapDispatchToProps)(StackedAreaChart);
-export { valuesHaveData };
diff --git a/src/containers/visualizations/StackedBarChart.js b/src/containers/visualizations/StackedBarChart.js
index 536c9d2..151c9d5 100644
--- a/src/containers/visualizations/StackedBarChart.js
+++ b/src/containers/visualizations/StackedBarChart.js
@@ -18,9 +18,33 @@ function valuesHaveData(transformedData) {
}
const mapStateToProps = (state, props) => {
- const { muni, chart } = props;
+ const { muni, chart, isSubregion, isRPAregion } = props;
const tables = Object.keys(chart.tables);
- if (tables.every((table) => state.chart.cache[table] && state.chart.cache[table][muni])) {
+
+ // Handle subregion data
+ if (isSubregion) {
+ if (tables.every((table) => state.subregion.cache[table] && state.subregion.cache[table][muni])) {
+ const subregionTables = tables.reduce((acc, table) => Object.assign(acc, { [table]: state.subregion.cache[table][muni] }), {});
+ return {
+ ...props,
+ xAxis: chart.xAxis,
+ yAxis: chart.yAxis,
+ data: chart.transformer(subregionTables, chart),
+ hasData: valuesHaveData(chart.transformer(subregionTables, chart)),
+ };
+ }
+ } else if (isRPAregion) {
+ if (tables.every((table) => state.rparegion.cache[table] && state.rparegion.cache[table][muni])) {
+ const rpaTables = tables.reduce((acc, table) => Object.assign(acc, { [table]: state.rparegion.cache[table][muni] }), {});
+ return {
+ ...props,
+ xAxis: chart.xAxis,
+ yAxis: chart.yAxis,
+ data: chart.transformer(rpaTables, chart),
+ hasData: valuesHaveData(chart.transformer(rpaTables, chart)),
+ };
+ }
+ } else if (tables.every((table) => state.chart.cache[table] && state.chart.cache[table][muni])) {
const muniTables = tables.reduce((acc, table) => Object.assign(acc, { [table]: state.chart.cache[table][muni] }), {});
return {
...props,
@@ -30,6 +54,7 @@ const mapStateToProps = (state, props) => {
hasData: valuesHaveData(chart.transformer(muniTables, chart)),
};
}
+
return {
...props,
xAxis: {
@@ -46,4 +71,4 @@ const mapStateToProps = (state, props) => {
const mapDispatchToProps = (dispatch, props) => ({});
export default connect(mapStateToProps, mapDispatchToProps)(StackedBarChart);
-export { valuesHaveData };
+export { valuesHaveData };
\ No newline at end of file
diff --git a/src/main.jsx b/src/main.jsx
index a4d8c83..fffa3bb 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -12,6 +12,8 @@ import CalenderEntry from "./components/gallery/CalendarEntry";
import store from "./store";
import "../src/assets/styles/app.scss";
import CommunityProfilesPage from "./pages/CommunityProfilesPage";
+import SubregionProfilesPage from "./pages/SubregionProfilesPage";
+import RPAregionProfilesPage from "./pages/RPAregionProfilesPage";
import tabs from "./constants/tabs";
import municipalities from "./assets/data/ma-munis.json";
@@ -21,6 +23,8 @@ const muniOptions = municipalities.features.map(
);
const tabOptions = tabs.map(tab => tab.value);
+// todo: get this from the api ?
+const VALID_SUBREGIONS = ['355', '356', '357', '358', '359', '360', '361', '362'];
const ProfileRoute = ({ muniOptions, tabOptions }) => {
const { muni, tab } = useParams();
@@ -36,6 +40,39 @@ const ProfileRoute = ({ muniOptions, tabOptions }) => {
return ;
};
+const SubregionProfileRoute = ({ tabOptions }) => {
+ const { subregionId, tab } = useParams();
+
+ if (!VALID_SUBREGIONS.includes(subregionId)) {
+ return ;
+ }
+
+ if (!tab || !tabOptions.includes(tab)) {
+ return ;
+ }
+
+ if (!subregionId || !VALID_SUBREGIONS.includes(subregionId)) {
+ return Subregion not found
;
+ }
+
+ return ;
+};
+
+const RPAProfileRoute = ({ tabOptions }) => {
+ const { rpaId, tab } = useParams();
+
+ if (!tab || !tabOptions.includes(tab)) {
+ return ;
+ }
+
+ if(!rpaId) {
+ return RPA not found
;
+ }
+
+ return ;
+};
+
+
const router = createBrowserRouter([
{
path: "/",
@@ -73,7 +110,16 @@ const router = createBrowserRouter([
{
path: "/profile/:muni/:tab?",
element:
- },{
+ },
+ {
+ path: "/profile/subregion/:subregionId/:tab?",
+ element:
+ },
+ {
+ path: "/profile/rpa/:rpaId/:tab?",
+ element:
+ },
+ {
path:"gallery",
children:[
{
diff --git a/src/pages/CommunitySelectorPage.jsx b/src/pages/CommunitySelectorPage.jsx
index 927b68c..0a2f6a0 100644
--- a/src/pages/CommunitySelectorPage.jsx
+++ b/src/pages/CommunitySelectorPage.jsx
@@ -1,77 +1,91 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
+import { createSelector } from '@reduxjs/toolkit';
import CommunitySelectorView from '../components/CommunitySelectorView';
import { fillPoly, emptyPoly } from '../reducers/municipalitySlice';
-// Container component that handles data and logic
-const CommunitySelectorPage = () => {
- const dispatch = useDispatch();
- const navigate = useNavigate();
-
- const { municipality, search } = useSelector(state => ({
- municipality: state.municipality,
- search: state.search.municipality
- }));
+// Memoized selectors
+const selectMunicipalityState = state => state.municipality;
+const selectSearchState = state => state.search.municipality;
- // Process map data
- const munisPoly = { ...municipality.geojson };
- const { results, hovering } = search;
+const selectProcessedMapData = createSelector(
+ [selectMunicipalityState, selectSearchState],
+ (municipality, search) => {
+ const munisPoly = { ...municipality.geojson };
+ const { results, hovering } = search;
- const lineFeatures = results.length
- ? {
- ...munisPoly,
- features: munisPoly.features.filter((feature) =>
- !results.length ||
- results.indexOf(feature.properties.town.toLowerCase()) > -1
- ),
- }
- : munisPoly;
+ const lineFeatures = results.length
+ ? {
+ ...munisPoly,
+ features: munisPoly.features.filter((feature) =>
+ !results.length ||
+ results.indexOf(feature.properties.town.toLowerCase()) > -1
+ ),
+ }
+ : munisPoly;
- const muniLines = {
- type: 'line',
- geojson: lineFeatures,
- };
+ const muniLines = {
+ type: 'line',
+ geojson: lineFeatures,
+ };
- let muniFill = {
- type: 'fill',
- geojson: { ...munisPoly, features: [] },
- };
+ let muniFill = {
+ type: 'fill',
+ geojson: { ...munisPoly, features: [] },
+ };
- if (hovering) {
- const upperHovering = hovering.toUpperCase();
- let filledMuniIndex = null;
+ if (hovering) {
+ const upperHovering = hovering.toUpperCase();
+ let filledMuniIndex = null;
- munisPoly.features.some((feature, i) => {
- if (feature.properties.town === upperHovering) {
- filledMuniIndex = i;
- return true;
- }
- return false;
- });
+ munisPoly.features.some((feature, i) => {
+ if (feature.properties.town === upperHovering) {
+ filledMuniIndex = i;
+ return true;
+ }
+ return false;
+ });
- if (filledMuniIndex !== null) {
- muniFill.geojson = {
- ...munisPoly,
- features: [munisPoly.features[filledMuniIndex]],
- };
+ if (filledMuniIndex !== null) {
+ muniFill.geojson = {
+ ...munisPoly,
+ features: [munisPoly.features[filledMuniIndex]],
+ };
+ }
}
+
+ return {
+ muniLines,
+ muniFill,
+ municipalityPoly: munisPoly
+ };
}
+);
+
+// Container component that handles data and logic
+const CommunitySelectorPage = () => {
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+
+ // Use memoized selector
+ const { muniLines, muniFill, municipalityPoly } = useSelector(selectProcessedMapData);
- const handleMunicipalitySelect = (municipality) => {
+ // Memoize handler
+ const handleMunicipalitySelect = useMemo(() => (municipality) => {
const formattedMuni = municipality.toLowerCase().replace(/\s+/g, '-');
dispatch(fillPoly(formattedMuni));
navigate(`/profile/${formattedMuni}`);
- };
+ }, [dispatch, navigate]);
return (
);
};
-export default CommunitySelectorPage;
\ No newline at end of file
+export default React.memo(CommunitySelectorPage);
\ No newline at end of file
diff --git a/src/pages/RPAregionProfilesPage.jsx b/src/pages/RPAregionProfilesPage.jsx
new file mode 100644
index 0000000..779e1ad
--- /dev/null
+++ b/src/pages/RPAregionProfilesPage.jsx
@@ -0,0 +1,33 @@
+import React, { useEffect } from 'react';
+import { useParams } from 'react-router-dom';
+import { useSelector, useDispatch } from 'react-redux';
+import RPAregionProfilesView from '../components/RPAregionProfilesView';
+import { fetchRPAregionData, selectRPAregionData, selectRPAregionLoading, selectRPAregionError } from '../reducers/rparegionSlice';
+
+const RPAregionProfilesPage = () => {
+ const dispatch = useDispatch();
+
+ // Get all RPA region data from Redux store
+ const rparegionData = useSelector(selectRPAregionData);
+ const loading = useSelector(selectRPAregionLoading);
+ const error = useSelector(selectRPAregionError);
+
+ // Effect for fetching RPA region data
+ useEffect(() => {
+ if (!Object.keys(rparegionData).length) {
+ dispatch(fetchRPAregionData());
+ }
+ }, [dispatch, rparegionData]);
+
+ if (loading) {
+ return Loading RPA region data...
;
+ }
+
+ if (error) {
+ return Error loading RPA region data: {error}
;
+ }
+
+ return ;
+};
+
+export default RPAregionProfilesPage;
\ No newline at end of file
diff --git a/src/pages/SubregionProfilesPage.jsx b/src/pages/SubregionProfilesPage.jsx
new file mode 100644
index 0000000..632dd23
--- /dev/null
+++ b/src/pages/SubregionProfilesPage.jsx
@@ -0,0 +1,34 @@
+import React, { useEffect } from 'react';
+import { useParams } from 'react-router-dom';
+import { useSelector, useDispatch } from 'react-redux';
+import SubregionProfilesView from '../components/SubregionProfilesView';
+import { fetchSubregionData } from '../reducers/subregionSlice';
+
+const SubregionProfilesPage = () => {
+ const dispatch = useDispatch();
+ const { subregionId } = useParams();
+
+ // Get all subregion data from Redux store
+ const subregionData = useSelector(state => state.subregion.data);
+ const loading = useSelector(state => state.subregion.loading);
+ const error = useSelector(state => state.subregion.error);
+
+ // Effect for fetching subregion data
+ useEffect(() => {
+ if (!Object.keys(subregionData).length) {
+ dispatch(fetchSubregionData());
+ }
+ }, [dispatch, subregionData]);
+
+ if (loading) {
+ return Loading subregion data...
;
+ }
+
+ if (error) {
+ return Error loading subregion data: {error}
;
+ }
+
+ return ;
+};
+
+export default SubregionProfilesPage;
\ No newline at end of file
diff --git a/src/reducers/chartSlice.js b/src/reducers/chartSlice.js
index ee8ed1f..0c2e730 100644
--- a/src/reducers/chartSlice.js
+++ b/src/reducers/chartSlice.js
@@ -94,6 +94,7 @@ const chartSlice = createSlice({
state.cache[table] = {};
}
state.cache[table][muni] = data;
+ console.log("updateChart= ", table, muni, data);
},
},
extraReducers: (builder) => {
diff --git a/src/reducers/rparegionSlice.js b/src/reducers/rparegionSlice.js
new file mode 100644
index 0000000..e1c5dfc
--- /dev/null
+++ b/src/reducers/rparegionSlice.js
@@ -0,0 +1,187 @@
+import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
+import locations from '../constants/locations';
+
+export const fetchRPAregionChartData = createAsyncThunk(
+ "rparegion/fetchChartData",
+ async ({ rpa_id , chartInfo }, { dispatch, getState }) => {
+ console.log('fetching rpa region chart data', rpa_id, chartInfo);
+ const { rparegion } = getState();
+ const tableNames = Object.keys(chartInfo.tables);
+
+ const dispatchUpdate = (data, tableName) => {
+ dispatch(
+ updateRPAregionChart({
+ table: tableName,
+ muni: rpa_id,
+ data,
+ })
+ );
+ };
+
+ // Get all queries first
+ const queries = chartInfo.rparegionDataQuery(rpa_id);
+
+ // Handle multiple tables - match each table with its corresponding query
+ if (tableNames.length > 1) {
+ // Make sure we have the same number of queries as tables
+ if (queries.length !== tableNames.length) {
+ console.error("Mismatch between number of tables and queries");
+ return;
+ }
+
+ // Process each table with its corresponding query
+ for (let i = 0; i < tableNames.length; i++) {
+ const tableName = tableNames[i];
+ const query = queries[i];
+ let { years } = chartInfo.tables[tableName];
+
+ if (rparegion.cache[tableName]?.[rpa_id]) {
+ continue;
+ }
+
+ if (typeof years === "function") {
+ try {
+ const yearsResult = await years();
+ years = yearsResult;
+ } catch (error) {
+ console.error("Error executing years function:", error);
+ continue;
+ }
+ }
+
+ try {
+ const api = `${locations.BROWSER_API}?token=${locations.DS_TOKEN}&query=${query}`;
+ const response = await fetch(api);
+ const payload = (await response.json()) || {};
+ dispatchUpdate(payload.rows, tableName);
+ } catch (error) {
+ console.error(`Error fetching data for table ${tableName}:`, error);
+ }
+ }
+ return;
+ }
+
+ // Handle single table
+ const tableName = tableNames[0];
+ let { years } = chartInfo.tables[tableName];
+
+ if (typeof years === "function") {
+ try {
+ const yearsResult = await years();
+ years = yearsResult;
+ } catch (error) {
+ console.error("Error executing years function:", error);
+ return;
+ }
+ }
+
+ try {
+ // For single table, use the first (and should be only) query
+ const query = Array.isArray(queries) ? queries[0] : queries;
+ const api = `${locations.BROWSER_API}?token=${locations.DS_TOKEN}&query=${query}`;
+ const response = await fetch(api);
+ const payload = (await response.json()) || {};
+ dispatchUpdate(payload.rows, tableName);
+ return { table: tableName, muni: rpa_id, data: payload.rows };
+ } catch (error) {
+ console.error(`Error fetching data for table ${tableName}:`, error);
+ throw error;
+ }
+ }
+);
+
+export const fetchRPAregionData = createAsyncThunk(
+ "rparegion/fetchData",
+ async () => {
+ const api = `${locations.BROWSER_API}?token=${locations.DS_TOKEN}&query=`;
+ const query = `
+ SELECT
+ muni_id,
+ muni_name,
+ region as rpa_name,
+ region_id as rpa_id
+ FROM tabular._datakeys_muni_all
+ WHERE rpa_name IS NOT NULL
+ ORDER BY region, muni_name
+ `;
+
+ const response = await fetch(`${api}${encodeURIComponent(query)}`);
+ const payload = await response.json();
+
+ // Transform the data into the desired structure
+ const rparegionMap = {};
+
+ payload.rows?.forEach((row) => {
+ const { muni_id, muni_name, rpa_name, rpa_id} = row;
+
+ if (!rparegionMap[rpa_id]) {
+ rparegionMap[rpa_id] = {
+ rpa_name,
+ municipalities: [],
+ totalMunis: 0,
+ };
+ }
+
+ // Add municipality data
+ rparegionMap[rpa_id].municipalities.push({
+ muni_name,
+ muni_id,
+ });
+
+ rparegionMap[rpa_id].totalMunis =
+ rparegionMap[rpa_id].municipalities.length;
+ });
+
+ return rparegionMap;
+ }
+);
+
+const rparegionSlice = createSlice({
+ name: "rparegion",
+ initialState: {
+ data: {},
+ loading: false,
+ error: null,
+ cache: {}
+ },
+ reducers: {
+ updateRPAregionChart: (state, action) => {
+ const { table: tableName, muni: rparegionId, data } = action.payload;
+
+ if (!tableName || !rparegionId || !data) {
+ return;
+ }
+
+ if (!state.cache[tableName]) {
+ state.cache[tableName] = {};
+ }
+
+ state.cache[tableName][rparegionId] = data;
+ },
+ },
+ extraReducers: (builder) => {
+ builder
+ .addCase(fetchRPAregionData.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(fetchRPAregionData.fulfilled, (state, action) => {
+ state.loading = false;
+ state.data = action.payload;
+ })
+ .addCase(fetchRPAregionData.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.error.message;
+ })
+ .addCase(fetchRPAregionChartData.fulfilled, (state, action) => {
+ // No need to handle this case since we're using updateRPAregionChart reducer
+ });
+ },
+});
+
+export const { updateRPAregionChart } = rparegionSlice.actions;
+export const selectRPAregionData = (state) => state.rparegion.data;
+export const selectRPAregionLoading = (state) => state.rparegion.loading;
+export const selectRPAregionError = (state) => state.rparegion.error;
+
+export default rparegionSlice.reducer;
\ No newline at end of file
diff --git a/src/reducers/subregionSlice.js b/src/reducers/subregionSlice.js
new file mode 100644
index 0000000..58ac77b
--- /dev/null
+++ b/src/reducers/subregionSlice.js
@@ -0,0 +1,179 @@
+import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
+import locations from "../constants/locations";
+
+//fetch subregion chart data
+export const fetchSubregionChartData = createAsyncThunk(
+ "subregion/fetchChartData",
+ async ({ subregionId, chartInfo }, { dispatch, getState }) => {
+ const { subregion } = getState();
+ const tableNames = Object.keys(chartInfo.tables);
+
+ const dispatchUpdate = (data, tableName) => {
+ dispatch(
+ updateSubregionChart({
+ table: tableName,
+ muni: subregionId,
+ data,
+ })
+ );
+ };
+
+ // Get all queries first
+ const queries = chartInfo.subregionDataQuery(subregionId);
+
+ // Handle multiple tables - match each table with its corresponding query
+ if (tableNames.length > 1) {
+ // Make sure we have the same number of queries as tables
+ if (queries.length !== tableNames.length) {
+ console.error("Mismatch between number of tables and queries");
+ return;
+ }
+
+ // Process each table with its corresponding query
+ for (let i = 0; i < tableNames.length; i++) {
+ const tableName = tableNames[i];
+ const query = queries[i];
+ let { years } = chartInfo.tables[tableName];
+
+ if (subregion.cache[tableName]?.[subregionId]) {
+ continue;
+ }
+
+ if (typeof years === "function") {
+ try {
+ const yearsResult = await years();
+ years = yearsResult;
+ } catch (error) {
+ console.error("Error executing years function:", error);
+ continue;
+ }
+ }
+
+ try {
+ const api = `${locations.BROWSER_API}?token=${locations.DS_TOKEN}&query=${query}`;
+ const response = await fetch(api);
+ const payload = (await response.json()) || {};
+ dispatchUpdate(payload.rows, tableName);
+ } catch (error) {
+ console.error(`Error fetching data for table ${tableName}:`, error);
+ }
+ }
+ return;
+ }
+
+ // Handle single table
+ const tableName = tableNames[0];
+ let { years } = chartInfo.tables[tableName];
+
+ if (typeof years === "function") {
+ try {
+ const yearsResult = await years();
+ years = yearsResult;
+ } catch (error) {
+ console.error("Error executing years function:", error);
+ return;
+ }
+ }
+
+ try {
+ // For single table, use the first (and should be only) query
+ const query = Array.isArray(queries) ? queries[0] : queries;
+ const api = `${locations.BROWSER_API}?token=${locations.DS_TOKEN}&query=${query}`;
+ const response = await fetch(api);
+ const payload = (await response.json()) || {};
+ dispatchUpdate(payload.rows, tableName);
+ } catch (error) {
+ console.error(`Error fetching data for table ${tableName}:`, error);
+ }
+ }
+);
+
+export const fetchSubregionData = createAsyncThunk(
+ "subregion/fetchData",
+ async () => {
+ const api = `${locations.BROWSER_API}?token=${locations.DS_TOKEN}&query=`;
+ const query = `
+ SELECT
+ muni_id,
+ muni_name,
+ subrg_id,
+ subrg_acr
+ FROM tabular._datakeys_muni_all
+ WHERE subrg_id IS NOT NULL
+ ORDER BY subrg_id, muni_name
+ `;
+
+ const response = await fetch(`${api}${encodeURIComponent(query)}`);
+ const payload = await response.json();
+
+ // Transform the data into the desired structure
+ const subregionMap = {};
+
+ payload.rows?.forEach((row) => {
+ const { muni_id, muni_name, subrg_id } = row;
+
+ if (!subregionMap[subrg_id]) {
+ subregionMap[subrg_id] = {
+ municipalities: [],
+ totalMunis: 0,
+ };
+ }
+
+ // Add municipality data
+ subregionMap[subrg_id].municipalities.push({
+ muni_name,
+ muni_id,
+ });
+
+ subregionMap[subrg_id].totalMunis =
+ subregionMap[subrg_id].municipalities.length;
+ });
+
+ return subregionMap;
+ }
+);
+
+const subregionSlice = createSlice({
+ name: "subregion",
+ initialState: {
+ data: {},
+ loading: false,
+ error: null,
+ cache: {}
+ },
+ reducers: {
+ updateSubregionChart: (state, action) => {
+ const { table: tableName, muni: subregionId, data } = action.payload; // Fix destructuring
+
+ if (!tableName || !subregionId || !data) {
+ return;
+ }
+
+ if (!state.cache[tableName]) {
+ state.cache[tableName] = {};
+ }
+
+ state.cache[tableName][subregionId] = data;
+ },
+ },
+ extraReducers: (builder) => {
+ builder
+ .addCase(fetchSubregionData.pending, (state) => {
+ state.loading = true;
+ state.error = null;
+ })
+ .addCase(fetchSubregionData.fulfilled, (state, action) => {
+ state.loading = false;
+ state.data = action.payload;
+ })
+ .addCase(fetchSubregionData.rejected, (state, action) => {
+ state.loading = false;
+ state.error = action.error.message;
+ });
+ },
+});
+
+export const selectSubregionData = (state) => state.subregion.data;
+export const selectSubregionLoading = (state) => state.subregion.loading;
+export const { updateSubregionChart } = subregionSlice.actions;
+export default subregionSlice.reducer;
diff --git a/src/store.js b/src/store.js
index f567f52..7743460 100644
--- a/src/store.js
+++ b/src/store.js
@@ -3,6 +3,8 @@ import datasetReducer from './reducers/datasetSlice';
import searchReducer from './reducers/searchSlice';
import municipalityReducer from './reducers/municipalitySlice';
import chartReducer from './reducers/chartSlice';
+import subregionReducer from './reducers/subregionSlice';
+import rparegionReducer from './reducers/rparegionSlice';
export const store = configureStore({
reducer: {
@@ -10,6 +12,8 @@ export const store = configureStore({
search: searchReducer,
municipality: municipalityReducer,
chart: chartReducer,
+ subregion: subregionReducer,
+ rparegion: rparegionReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({