diff --git a/.claude/hooks/format.py b/.claude/hooks/format.py
index 28497771..853e06d7 100644
--- a/.claude/hooks/format.py
+++ b/.claude/hooks/format.py
@@ -1,4 +1,3 @@
-#!/usr/bin/env python3
"""PostToolUse hook: format files Claude edits.
.py -> ruff format (config in backend/pyproject.toml)
diff --git a/backend/api/models/__init__.py b/backend/api/models/__init__.py
index 09f13e01..a5da42c7 100644
--- a/backend/api/models/__init__.py
+++ b/backend/api/models/__init__.py
@@ -1,6 +1,3 @@
-# ruff: noqa: F401
-
-
from .request_models import (
DPSeriesRequest,
FilterRequest,
diff --git a/backend/api/models/request_models.py b/backend/api/models/request_models.py
index add76358..5ffa20d2 100644
--- a/backend/api/models/request_models.py
+++ b/backend/api/models/request_models.py
@@ -3,7 +3,9 @@
from pydantic import BaseModel, model_validator
-MAX_YEAR = datetime.now().year - 2
+from query.clock import EASTERN_STD_TIME
+
+MAX_YEAR = datetime.now(EASTERN_STD_TIME).year - 2
class RangeFilter(BaseModel):
diff --git a/backend/api/routes/get_routes/get_wholedata.py b/backend/api/routes/get_routes/get_wholedata.py
index 226fe9db..d6621a74 100644
--- a/backend/api/routes/get_routes/get_wholedata.py
+++ b/backend/api/routes/get_routes/get_wholedata.py
@@ -32,7 +32,7 @@ async def read_flood_data():
# VT Municipalities Endpoint
@router.get("/data/vermont/municipalities")
-async def read_municipalities_data():
+def read_municipalities_data():
with open(
DATA_DIR / "vermont" / "municipalities.json", "r", encoding="utf-8"
) as file:
diff --git a/backend/data_collection/acs5.py b/backend/data_collection/acs5.py
index 5b04995d..7eaeb2c4 100644
--- a/backend/data_collection/acs5.py
+++ b/backend/data_collection/acs5.py
@@ -20,6 +20,7 @@
from data_collection.base import ALL_GEOS
from data_collection.census import tidy_census
+from query.clock import EASTERN_STD_TIME
# Define API key through the .env file
API_KEY = os.environ.get("CENSUS_API_KEY")
@@ -36,7 +37,7 @@
STORAGE_LOCATION = "Data/Census/ACS_5"
ID_VARS = ["year", "geo_type", "table", "NAME", "state", "county"]
-MAX_YEAR = datetime.now().year - 1
+MAX_YEAR = datetime.now(EASTERN_STD_TIME).year - 1
YEARS = range(2009, MAX_YEAR)
@@ -73,7 +74,7 @@ def fetch_table(year, table, for_clause, in_clause):
df["year"] = year
df["table"] = table
return df
- except Exception as e:
+ except Exception as e: # noqa: BLE001 -- skip and keep the run going
print(f" SKIP {year} / {table} / {for_clause}: {e}")
return None
@@ -133,7 +134,7 @@ def run_acs5_scrape(years: range = YEARS, geos: list = GEOS, append: bool = Fals
tidy_year["table"] = table
tidy_frames.append(tidy_year)
- except Exception as e:
+ except Exception as e: # noqa: BLE001 -- skip and keep the run going
print(f" SKIP tidy {year} / {table}: {e}")
if tidy_frames:
@@ -150,11 +151,11 @@ def run_acs5_scrape(years: range = YEARS, geos: list = GEOS, append: bool = Fals
def merge_tidy_tables():
"""Merge the per-table tidy parquets into one combined file."""
tidy_frames = []
- for table, label in TABLES.items():
+ for label in TABLES.values():
path = f"{STORAGE_LOCATION}/vt_acs5_{label}_data_tidy.parquet"
try:
tidy_frames.append(pd.read_parquet(path))
- except Exception as e:
+ except Exception as e: # noqa: BLE001 -- skip and keep the run going
print(f" SKIP {path}: {e}")
if tidy_frames:
diff --git a/backend/data_collection/base.py b/backend/data_collection/base.py
index 7874e006..9eb1d830 100644
--- a/backend/data_collection/base.py
+++ b/backend/data_collection/base.py
@@ -30,6 +30,7 @@
import requests
from data_collection.census import split_name_col
+from query.clock import EASTERN_STD_TIME
# Define API key through the .env file
API_KEY = os.environ.get("CENSUS_API_KEY")
@@ -52,7 +53,7 @@
GEOS = [(k, *v) for k, v in ALL_GEOS.items()]
-MAX_YEAR = datetime.now().year - 2
+MAX_YEAR = datetime.now(EASTERN_STD_TIME).year - 2
# ---------------------------------------------------------------------------
# Data structures
@@ -101,7 +102,7 @@ def fetch(
if c[0] == "B":
df[c] = pd.to_numeric(df[c], errors="coerce")
return df
- except Exception as e:
+ except Exception as e: # noqa: BLE001 -- skip and keep the run going
print(f" SKIP {year} / {for_clause}: {e}")
return None
@@ -114,9 +115,13 @@ def fetch(
def pct(val: float, total: float) -> float | None:
try:
if total and total > 0:
- return round(val / total * 100, 1)
- except Exception:
- pass
+ return round((val / total) * 100, 1)
+ except OverflowError:
+ print(f"OverflowError computing pct({val}, {total})")
+ except FloatingPointError:
+ print(f"FloatingPointError computing pct({val}, {total})")
+ except ValueError:
+ print(f"ValueError computing pct({val}, {total})")
return None
diff --git a/backend/data_collection/cdc.py b/backend/data_collection/cdc.py
index c6a70751..6afdc784 100644
--- a/backend/data_collection/cdc.py
+++ b/backend/data_collection/cdc.py
@@ -7,10 +7,11 @@
Fetches Vermont CDC data from the CDC SODA API (In "Open Data Format")
"""
-import pandas as pd
+from io import BytesIO
+
import geopandas as gpd
+import pandas as pd
import requests
-from io import BytesIO
# ---------------------------------------------------------------------------
# API endpoints
diff --git a/backend/data_collection/demographics.py b/backend/data_collection/demographics.py
index c8fef05d..23370335 100644
--- a/backend/data_collection/demographics.py
+++ b/backend/data_collection/demographics.py
@@ -13,6 +13,7 @@
import pandas as pd
from data_collection.base import ALL_GEOS, VarGroup, run_acs_b_scrape
+from query.clock import EASTERN_STD_TIME
# ---------------------------------------------------------------------------
# Age band definitions: (label, male_suffix_range, female_suffix_range)
@@ -29,7 +30,7 @@
("75 Plus", range(23, 26), range(47, 50)),
]
-MAX_YEAR = datetime.now().year - 1
+MAX_YEAR = datetime.now(EASTERN_STD_TIME).year - 1
YEARS = range(2009, MAX_YEAR)
diff --git a/backend/data_collection/economic.py b/backend/data_collection/economic.py
index 0e8cd6cf..f0dc625f 100644
--- a/backend/data_collection/economic.py
+++ b/backend/data_collection/economic.py
@@ -25,6 +25,7 @@
import pandas as pd
from data_collection.base import ALL_GEOS, VarGroup, run_acs_b_scrape
+from query.clock import EASTERN_STD_TIME
SL = "Labor Force"
SI = "Income"
@@ -86,7 +87,7 @@
"B19301": ["B19301_001E"],
}
-MAX_YEAR = datetime.now().year - 1
+MAX_YEAR = datetime.now(EASTERN_STD_TIME).year - 1
YEARS = range(2009, MAX_YEAR)
diff --git a/backend/data_collection/education.py b/backend/data_collection/education.py
index 4ceb6b18..be9483c4 100644
--- a/backend/data_collection/education.py
+++ b/backend/data_collection/education.py
@@ -17,6 +17,7 @@
import pandas as pd
from data_collection.base import ALL_GEOS, VarGroup, run_acs_b_scrape
+from query.clock import EASTERN_STD_TIME
TOTAL = "B15003_001E"
S = "Educational Attainment"
@@ -41,7 +42,7 @@
"B15003": [TOTAL] + [f"B15003_{str(i).zfill(3)}E" for i in range(2, 26)],
}
-MAX_YEAR = datetime.now().year - 1
+MAX_YEAR = datetime.now(EASTERN_STD_TIME).year - 1
YEARS = range(2009, MAX_YEAR)
diff --git a/backend/data_collection/housing.py b/backend/data_collection/housing.py
index d5784cd6..1e99abdc 100644
--- a/backend/data_collection/housing.py
+++ b/backend/data_collection/housing.py
@@ -20,6 +20,7 @@
import pandas as pd
from data_collection.base import ALL_GEOS, VarGroup, run_acs_b_scrape
+from query.clock import EASTERN_STD_TIME
S = "Housing"
@@ -45,7 +46,7 @@
"B25077": ["B25077_001E"],
}
-MAX_YEAR = datetime.now().year - 1
+MAX_YEAR = datetime.now(EASTERN_STD_TIME).year - 1
YEARS = range(2009, MAX_YEAR)
diff --git a/backend/data_collection/qcew.py b/backend/data_collection/qcew.py
index b7308c93..9ed8c4e2 100644
--- a/backend/data_collection/qcew.py
+++ b/backend/data_collection/qcew.py
@@ -25,6 +25,8 @@
import pandas as pd
import requests
+from query.clock import EASTERN_STD_TIME
+
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
@@ -89,7 +91,7 @@
BASE_URL = "https://data.bls.gov/cew/data/api/{year}/{q}/area/{fips}.csv"
QUARTERS = [1, 2, 3, 4]
-MAX_YEAR = datetime.now().year - 1
+MAX_YEAR = datetime.now(EASTERN_STD_TIME).year - 1
YEARS = range(2009, MAX_YEAR)
@@ -107,7 +109,7 @@ def fetch_quarter(year: int, quarter: int, area_fips: str) -> pd.DataFrame | Non
df = pd.read_csv(StringIO(r.text), dtype=str)
df.columns = df.columns.str.strip()
return df
- except Exception as e:
+ except Exception as e: # noqa: BLE001 -- skip and keep the run going
print(f" SKIP {year}Q{quarter} / {area_fips}: {e}")
return None
diff --git a/backend/query/__init__.py b/backend/query/__init__.py
index afa9a553..c9583417 100644
--- a/backend/query/__init__.py
+++ b/backend/query/__init__.py
@@ -36,7 +36,7 @@
}
_NAME_TO_MODULE = {n: m for m, names in _EXPORTS.items() for n in names}
-__all__ = sorted(_NAME_TO_MODULE)
+__all__ = sorted(_NAME_TO_MODULE) # noqa: PLE0605
def __getattr__(name: str):
diff --git a/backend/query/clock.py b/backend/query/clock.py
new file mode 100644
index 00000000..0fccfd08
--- /dev/null
+++ b/backend/query/clock.py
@@ -0,0 +1,5 @@
+from datetime import timedelta, timezone
+
+# Fixed UTC-5 (EST, no DST): MAX_YEAR only needs the year, and the year
+# boundary always falls in winter.
+EASTERN_STD_TIME = timezone(timedelta(hours=-5))
diff --git a/backend/query/comparison.py b/backend/query/comparison.py
index d1a5c322..b7746f4c 100644
--- a/backend/query/comparison.py
+++ b/backend/query/comparison.py
@@ -21,6 +21,7 @@
from api.models import FilterSource
from query.cdc import build_cmap, to_rgba
+from query.clock import EASTERN_STD_TIME
from query.production_db import get_db
from query.sql_render import sql_filter_block
@@ -35,7 +36,7 @@
ACS_SENTINEL = -666666666
# Vintages this recent aren't final yet (ACS5/CDC Places both lag ~2 years),
# so the newest year at or before this is the one used per table.
-MAX_YEAR = datetime.now().year - 2
+MAX_YEAR = datetime.now(EASTERN_STD_TIME).year - 2
class NoDataError(ValueError):
diff --git a/backend/query/core_functions.py b/backend/query/core_functions.py
index ef8857aa..8d58f35a 100644
--- a/backend/query/core_functions.py
+++ b/backend/query/core_functions.py
@@ -117,8 +117,8 @@ def filter_tree(
tree_labels: list[str],
table: str,
db=DB,
- rangemap: dict = {},
- badgemap: dict = {},
+ rangemap: dict | None = None,
+ badgemap: dict | None = None,
) -> FilterResponse:
"""
Info for cascading filter UI.
@@ -126,6 +126,10 @@ def filter_tree(
When we consolidate all SQL to one source/place, we'll update this to be just one.
TODO: update to not pass in tree_labels but just use from schema
"""
+ if rangemap is None:
+ rangemap = {}
+ if badgemap is None:
+ badgemap = {}
cols = [colmap.get(label) for label in tree_labels]
select = ", ".join(f'"{col}"' for col in cols)
order = ", ".join(str(i + 1) for i in range(len(cols)))
diff --git a/backend/run_data_cleaning.py b/backend/run_data_cleaning.py
index 8548d422..57f05791 100644
--- a/backend/run_data_cleaning.py
+++ b/backend/run_data_cleaning.py
@@ -49,7 +49,7 @@ def run_master_clean(script_name: str | None = None):
cleaner.main(con)
print(f"Completed {name}")
- except Exception as e:
+ except Exception as e: # noqa: BLE001 -- isolate any cleaner failure so the rest still run
failed.append(name)
print(f"FAILED {name}: {e}")
diff --git a/backend/run_data_collection.py b/backend/run_data_collection.py
index f6710f96..d85c686c 100644
--- a/backend/run_data_collection.py
+++ b/backend/run_data_collection.py
@@ -1,5 +1,5 @@
import argparse
-from datetime import datetime, timedelta, timezone
+from datetime import datetime
from data_collection import (
acs5,
@@ -20,6 +20,7 @@
zoning,
)
from lake_build import get_connection, insert_year, replace_table
+from query.clock import EASTERN_STD_TIME
YEARLY_SCRAPERS = [
acs5,
@@ -43,8 +44,7 @@
zoning,
]
-eastern_std_time = timezone(timedelta(hours=5))
-MAX_YEAR = datetime.now(eastern_std_time).year - 1
+MAX_YEAR = datetime.now(EASTERN_STD_TIME).year - 1
def run_scraper(
diff --git a/backend/tests/test_base.py b/backend/tests/test_base.py
index 7e49a564..665147dc 100644
--- a/backend/tests/test_base.py
+++ b/backend/tests/test_base.py
@@ -5,7 +5,6 @@
import pandas as pd
import pytest
-
from data_collection_OLD.base import VarGroup, compute_tidy_generic, pct
# ---------------------------------------------------------------------------
diff --git a/backend/tests/test_lake.py b/backend/tests/test_lake.py
index 5fadbd5a..9c619110 100644
--- a/backend/tests/test_lake.py
+++ b/backend/tests/test_lake.py
@@ -75,7 +75,7 @@ def inspect_schema(schema: str) -> None:
print(f"Years: {years[0]} → {years[1]} ({years[2]} distinct)")
- except Exception as e:
+ except Exception as e: # noqa: BLE001 -- skip and keep the run going
print(f"Could not inspect years: {e}")
diff --git a/frontend/src/app/about/contact/page.tsx b/frontend/src/app/about/contact/page.tsx
index 5e09092e..dcafc759 100644
--- a/frontend/src/app/about/contact/page.tsx
+++ b/frontend/src/app/about/contact/page.tsx
@@ -1,12 +1,13 @@
'use client';
-import React, { useState } from 'react';
+import { useState } from 'react';
import {
Alert,
Box,
Button,
Card,
+ Checkbox,
Container,
Group,
Select,
@@ -44,6 +45,7 @@ interface FeedbackFormValues {
email: string;
feedbackType: string;
message: string;
+ joinEmailList: boolean;
}
function FeedbackForm() {
@@ -56,6 +58,7 @@ function FeedbackForm() {
email: '',
feedbackType: '',
message: '',
+ joinEmailList: false,
},
validate: {
name: isNotEmpty('Please enter your name'),
@@ -81,6 +84,7 @@ function FeedbackForm() {
email: values.email,
feedback_type: values.feedbackType,
message: values.message,
+ email_list_opt_in: values.joinEmailList ? 'Yes' : 'No',
}),
});
@@ -184,6 +188,25 @@ function FeedbackForm() {
}}
/>
+
+
+
+
{status === 'success' && (
}
diff --git a/frontend/src/app/about/page.tsx b/frontend/src/app/about/page.tsx
index e575d7db..23a30ab4 100644
--- a/frontend/src/app/about/page.tsx
+++ b/frontend/src/app/about/page.tsx
@@ -2,15 +2,12 @@
import {
Accordion,
- Badge,
Box,
Button,
Card,
Container,
- Divider,
Grid,
Group,
- Paper,
SimpleGrid,
Stack,
Text,
@@ -42,7 +39,6 @@ import Link from 'next/link';
import * as motion from 'motion/react-client';
import { COLORS, FONTS } from '@/app/theme';
-import { color } from 'd3';
// -----------------------------------------------------------------------------
// Hero
@@ -693,7 +689,7 @@ function WhoItsFor() {
/>
- {audiences.map((audience, index, icon) => (
+ {audiences.map((audience, index) => (
Data Sources
{' '}
- to learn more about what's available.
+ to learn more about what's available.
>
),
},
diff --git a/frontend/src/app/data-comparison/dp-explorer/page.tsx b/frontend/src/app/data-comparison/dp-explorer/page.tsx
index e8d2f6ef..a6c1d2e1 100644
--- a/frontend/src/app/data-comparison/dp-explorer/page.tsx
+++ b/frontend/src/app/data-comparison/dp-explorer/page.tsx
@@ -30,7 +30,10 @@ import {
MinusIcon,
WarningIcon,
} from '@phosphor-icons/react';
-import { useProfile } from '@/components/profile/profileStore';
+import {
+ useProfile,
+ YEAR_MAX_OVERALL,
+} from '@/components/profile/profileStore';
import { BASE_API_URL } from '@/config';
import { ChartStack } from '@/components/Charts';
import { createChartItem } from '@/utils/itemFactory';
@@ -540,7 +543,9 @@ export default function DPExplorerPage() {
// Available years derived from fetched data
const [availableYears, setAvailableYears] = useState([]);
- const YEAR_MAX_OVERALL = new Date().getFullYear() - 2;
+ // stringified so the effect re-runs on name changes, not object identity
+ const nameA = makeName(sideA);
+ const nameB = makeName(sideB);
useEffect(() => {
if (!isComplete) {
@@ -567,7 +572,7 @@ export default function DPExplorerPage() {
})
.then((r) => r.data);
- Promise.all([post(makeName(sideA)), post(makeName(sideB))])
+ Promise.all([post(nameA), post(nameB)])
.then(([aRes, bRes]) => {
const aData = aRes.data ?? [];
const bData = bRes.data ?? [];
@@ -598,9 +603,8 @@ export default function DPExplorerPage() {
subcategory,
variable,
measure,
- // stringify name to avoid object ref changes
- makeName(sideA),
- makeName(sideB),
+ nameA,
+ nameB,
]);
// ---------------------------------------------------------------------------
diff --git a/frontend/src/app/data-comparison/reports-by-topic/ReportsByTopic.tsx b/frontend/src/app/data-comparison/reports-by-topic/ReportsByTopic.tsx
index 11f84c03..db1c038a 100644
--- a/frontend/src/app/data-comparison/reports-by-topic/ReportsByTopic.tsx
+++ b/frontend/src/app/data-comparison/reports-by-topic/ReportsByTopic.tsx
@@ -6,7 +6,6 @@ import axios from 'axios';
import {
Alert,
Box,
- Button,
Container,
Group,
Grid,
@@ -30,8 +29,7 @@ import {
// import { ChartStack } from '@/components/Charts';
// import { createChartItem } from '@/utils/itemFactory';
import { DataRow } from '@/types/cachedCharts';
-import { IconDownload, IconInfoCircle } from '@tabler/icons-react';
-import { exportReport } from '@/utils/exportReport';
+import { IconInfoCircle } from '@tabler/icons-react';
import { DashboardSection, TOPIC_SLUGS, topicPath } from './topics';
// ---------------------------------------------------------------------------
@@ -313,7 +311,13 @@ function HeroSection({
year,
setYear,
availableYears,
-}: any) {
+}: {
+ section: string;
+ setSection: (value: string) => void;
+ year: number;
+ setYear: (value: number) => void;
+ availableYears: number[];
+}) {
return (
(null);
- const [isExporting, setIsExporting] = useState(false);
// ---------------------------------------------------------------------------
// Fetch both locations whenever section or location names change — plus,
@@ -788,22 +791,6 @@ export default function ReportsByTopic({
]),
);
- const handleExportPdf = async () => {
- setIsExporting(true);
- try {
- await exportReport('demographics-dashboard-report', {
- title: section,
- primaryName: myLocation.name,
- comparisonName: comparison.name,
- year: year,
- });
- } catch (err) {
- console.error('PDF export failed:', err);
- } finally {
- setIsExporting(false);
- }
- };
-
// ---------------------------------------------------------------------------
// Render
// ---------------------------------------------------------------------------
diff --git a/frontend/src/app/data-comparison/variable-comparison/page.tsx b/frontend/src/app/data-comparison/variable-comparison/page.tsx
index 275aac52..b395cab7 100644
--- a/frontend/src/app/data-comparison/variable-comparison/page.tsx
+++ b/frontend/src/app/data-comparison/variable-comparison/page.tsx
@@ -1,5 +1,5 @@
'use client';
-import { Select, keys, Paper, Text, SegmentedControl } from '@mantine/core';
+import { Paper, Text, SegmentedControl } from '@mantine/core';
import { useState } from 'react';
import type { FeatureCollection } from 'geojson';
import { BASE_API_URL } from '@/config';
diff --git a/frontend/src/app/data-viewer/FieldLabel.tsx b/frontend/src/app/data-viewer/FieldLabel.tsx
index 7ae0da51..1ef744c7 100644
--- a/frontend/src/app/data-viewer/FieldLabel.tsx
+++ b/frontend/src/app/data-viewer/FieldLabel.tsx
@@ -1,4 +1,4 @@
-import { COLORS, FONTS } from '@/app/theme';
+import { FONTS } from '@/app/theme';
import { Text } from '@mantine/core';
export function FieldLabel({
diff --git a/frontend/src/app/data-viewer/page.tsx b/frontend/src/app/data-viewer/page.tsx
index a3792221..6cbc3342 100644
--- a/frontend/src/app/data-viewer/page.tsx
+++ b/frontend/src/app/data-viewer/page.tsx
@@ -22,7 +22,7 @@ import {
} from '@phosphor-icons/react';
import { createChartItem, createTableItem } from '@/utils/itemFactory';
import { ChartStack } from '@/components/Charts';
-import { useProfile } from '@/components/profile/profileStore';
+import { useProfile, type Location } from '@/components/profile/profileStore';
import {
useApplyFilters,
buildFilters,
@@ -66,7 +66,7 @@ function StatCards() {
setData(data as Row[]);
},
});
- }, [myLocation, yearMin, yearMax]);
+ }, [applyFilters, myLocation, yearMin, yearMax]);
const metrics = data.reduce>((acc, d) => {
acc[d.Variable] = d.Value;
@@ -127,8 +127,8 @@ function HeroSection({
activeTab,
setActiveTab,
}: {
- myLocation: any;
- comparison: any;
+ myLocation: Location;
+ comparison: Location;
interests: string[];
yearMin: number;
yearMax: number;
@@ -333,6 +333,11 @@ function ProfileField({
);
}
+const tableDefs = chartDefs.filter((c) => c.subtype.startsWith('renderTable'));
+const nonTableDefs = chartDefs.filter(
+ (c) => !c.subtype.startsWith('renderTable'),
+);
+
export default function DataViewerPage() {
const {
myLocation,
@@ -342,9 +347,7 @@ export default function DataViewerPage() {
yearMax,
openProfileModal,
} = useProfile();
- const [chartData, setChartData] = useState<
- Record
- >({});
+ const [chartData, setChartData] = useState>({});
const [compareChartData, setCompareChartData] = useState<
Record
>({});
@@ -352,16 +355,10 @@ export default function DataViewerPage() {
Record
>({});
- const [focusMode, setFocusMode] = useState<'all' | 'focus'>('all');
+ const [focusMode] = useState<'all' | 'focus'>('all');
const [activeTab, setActiveTab] = useState(null);
const applyFilters = useApplyFilters();
- const tableDefs = chartDefs.filter((c) =>
- c.subtype.startsWith('renderTable'),
- );
- const nonTableDefs = chartDefs.filter(
- (c) => !c.subtype.startsWith('renderTable'),
- );
const categoryIcons: Record = {
Housing: HouseLineIcon,
@@ -425,7 +422,7 @@ export default function DataViewerPage() {
})),
});
});
- }, [myLocation, comparison, yearMin, yearMax]);
+ }, [applyFilters, myLocation, comparison, yearMin, yearMax]);
useEffect(() => {
const seen = new Set();
@@ -487,7 +484,7 @@ export default function DataViewerPage() {
});
}
});
- }, [myLocation, comparison, yearMin, yearMax]);
+ }, [applyFilters, myLocation, comparison, yearMin, yearMax]);
// QCEW employment data is county-level only; swap to a note card for town selections
const isSubcountyLocation = myLocation.type === 'town';
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index adfe633b..299776a4 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -4,7 +4,7 @@ import { Zilla_Slab } from 'next/font/google';
import '@mantine/core/styles.css';
import './globals.css';
-import { MantineProvider, Container, Box } from '@mantine/core';
+import { MantineProvider, Box } from '@mantine/core';
import HeaderMenu from '../components/HeaderMenu';
import Maintenance from '../components/Maintenance';
diff --git a/frontend/src/app/mapping/[slug]/page_content.tsx b/frontend/src/app/mapping/[slug]/page_content.tsx
index c276c211..61247540 100644
--- a/frontend/src/app/mapping/[slug]/page_content.tsx
+++ b/frontend/src/app/mapping/[slug]/page_content.tsx
@@ -68,6 +68,10 @@ const MAP_CONFIG: Record = {
export default function MappingContent() {
const params = useParams();
const slug = params?.slug as string | undefined;
+ return ;
+}
+
+function MappingBody({ slug }: { slug?: string }) {
const config = slug ? MAP_CONFIG[slug] : undefined;
const townBorderDef = config?.townBorder ?? false;
@@ -83,16 +87,6 @@ export default function MappingContent() {
const [legendData, setLegendData] = useState([]);
- /*
- * Reset map state when navigating between map pages.
- */
- useEffect(() => {
- setData(null);
- setLegendData([]);
- setShowCountyLines(townBorderDef);
- setLargeBorder(largeBorderDef);
- }, [slug, townBorderDef, largeBorderDef]);
-
/*
* Load initial map data.
*
@@ -143,10 +137,7 @@ export default function MappingContent() {
useEffect(() => {
const legendURL = config?.legendURL;
- if (!legendURL) {
- setLegendData([]);
- return;
- }
+ if (!legendURL) return;
let cancelled = false;
diff --git a/frontend/src/app/mapping/geoUtils.ts b/frontend/src/app/mapping/geoUtils.ts
index d820c02b..3953ff9f 100644
--- a/frontend/src/app/mapping/geoUtils.ts
+++ b/frontend/src/app/mapping/geoUtils.ts
@@ -10,7 +10,7 @@ export function getFeatureBBox(
let maxLng = -Infinity;
let maxLat = -Infinity;
- const processCoords = (coords: any) => {
+ const processCoords = (coords: unknown) => {
if (!Array.isArray(coords) || coords.length === 0) return;
// Check if we reached a coordinate pair [lng, lat]
diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx
index 98583464..d70d8623 100644
--- a/frontend/src/app/page.tsx
+++ b/frontend/src/app/page.tsx
@@ -25,7 +25,6 @@ import {
MapTrifoldIcon,
ChartBarIcon,
DownloadSimpleIcon,
- HandHeartIcon,
PencilSimpleIcon,
GithubLogoIcon,
UsersThreeIcon,
@@ -35,7 +34,7 @@ import {
NewspaperIcon,
} from '@phosphor-icons/react';
-import { useProfile } from '@/components/profile/profileStore';
+import { useProfile, type Location } from '@/components/profile/profileStore';
import { COLORS, FONTS } from '@/app/theme';
@@ -245,8 +244,8 @@ function HeroSection({
yearMax,
openProfileModal,
}: {
- myLocation: any;
- comparison: any;
+ myLocation: Location;
+ comparison: Location;
interests: string[];
yearMin: number;
yearMax: number;
@@ -334,8 +333,8 @@ function HeroSection({
style={{ color: 'rgba(246,245,239,0.78)' }}
>
Zoning, flood risk, housing, demographics, and more
- community-level data for all 251 towns and cities. It's free to
- explore and open to all.
+ community-level data for all 251 towns and cities. It's
+ free to explore and open to all.
@@ -743,7 +742,7 @@ function ValueAdds() {
}}
mb={8}
>
- What's different here
+ What's different here
Compared to a typical government data portal.
@@ -827,7 +826,7 @@ function CommunitySection() {
mx="auto"
style={{ color: 'rgba(246,245,239,0.78)' }}
>
- The platform's code and data pipelines are open source. Towns,
+ The platform's code and data pipelines are open source. Towns,
researchers, and volunteers help shape what gets added next — and
anyone can inspect exactly how a number was calculated.
diff --git a/frontend/src/app/resources/page.tsx b/frontend/src/app/resources/page.tsx
index f12f9992..17f3ced5 100644
--- a/frontend/src/app/resources/page.tsx
+++ b/frontend/src/app/resources/page.tsx
@@ -17,7 +17,6 @@ import Link from 'next/link';
import {
ArrowRightIcon,
BellIcon,
- CalculatorIcon,
DatabaseIcon,
FileTextIcon,
GithubLogoIcon,
diff --git a/frontend/src/app/working-report/page.tsx b/frontend/src/app/working-report/page.tsx
index f33ce31d..f3e1c777 100644
--- a/frontend/src/app/working-report/page.tsx
+++ b/frontend/src/app/working-report/page.tsx
@@ -476,6 +476,11 @@ function SortableChartItem({
);
}
+const tableDefs = chartDefs.filter((c) => c.subtype.startsWith('renderTable'));
+const nonTableDefs = chartDefs.filter(
+ (c) => !c.subtype.startsWith('renderTable'),
+);
+
export default function WorkingReport() {
const chartsRef = useRef(null);
const [isPdfMode, setIsPdfMode] = useState(false);
@@ -588,12 +593,6 @@ export default function WorkingReport() {
>({});
const applyFilters = useApplyFilters();
- const tableDefs = chartDefs.filter((c) =>
- c.subtype.startsWith('renderTable'),
- );
- const nonTableDefs = chartDefs.filter(
- (c) => !c.subtype.startsWith('renderTable'),
- );
useEffect(() => {
nonTableDefs.forEach((chart: ChartDef) => {
@@ -641,7 +640,7 @@ export default function WorkingReport() {
})),
});
});
- }, [myLocation, comparison, yearMin, yearMax]);
+ }, [applyFilters, myLocation, comparison, yearMin, yearMax]);
useEffect(() => {
const seen = new Set();
@@ -700,7 +699,7 @@ export default function WorkingReport() {
});
}
});
- }, [myLocation, comparison, yearMin, yearMax]); // eslint-disable-line react-hooks/exhaustive-deps
+ }, [applyFilters, myLocation, comparison, yearMin, yearMax]);
// ---------- build chart items ----------
const isSubcountyLocation = myLocation.type === 'town';
diff --git a/frontend/src/components/Charts/Bar.tsx b/frontend/src/components/Charts/Bar.tsx
index 2a9055ce..53d02c3a 100644
--- a/frontend/src/components/Charts/Bar.tsx
+++ b/frontend/src/components/Charts/Bar.tsx
@@ -24,13 +24,15 @@ import {
Tooltip as TooltipJS,
Legend as LegendJS,
TooltipItem,
+ ChartEvent,
+ LegendItem,
+ LegendElement,
} from 'chart.js';
ChartJS.register(CategoryScale, LinearScale, BarElement, TooltipJS, LegendJS);
import { ChartItem, DataRow } from '@/types/cachedCharts';
import { usePdfMode } from '@/contexts/PdfModeContext';
import {
- Title,
Box,
Group,
Text,
@@ -213,14 +215,14 @@ const CompareDiffPerXBarChartSVG = ({
const includeCategories = chart.chartParams?.includeCategories;
const filteredData = includeCategories
- ? chart.data.filter((entry: any) =>
- includeCategories.includes(entry[chart.xField]),
+ ? chart.data.filter((entry) =>
+ includeCategories.includes(entry[chart.xField] as string),
)
: chart.data;
const filteredCompareData =
includeCategories && chart.compareData
- ? chart.compareData.filter((entry: any) =>
- includeCategories.includes(entry[chart.xField]),
+ ? chart.compareData.filter((entry) =>
+ includeCategories.includes(entry[chart.xField] as string),
)
: (chart.compareData ?? []);
@@ -283,8 +285,8 @@ const CompareDiffPerXBarChart = ({
// Filter primary dataset based on categories if specified
const filteredData = useMemo(() => {
return includeCategories
- ? rows.filter((entry: any) =>
- includeCategories.includes(entry[chart.xField]),
+ ? rows.filter((entry) =>
+ includeCategories.includes(entry[chart.xField] as string),
)
: rows;
}, [rows, includeCategories, chart.xField]);
@@ -292,15 +294,15 @@ const CompareDiffPerXBarChart = ({
// Filter comparison dataset based on categories if specified
const filteredCompareData = useMemo(() => {
return includeCategories
- ? compareRows.filter((entry: any) =>
- includeCategories.includes(entry[chart.xField]),
+ ? compareRows.filter((entry) =>
+ includeCategories.includes(entry[chart.xField] as string),
)
: compareRows;
}, [compareRows, includeCategories, chart.xField]);
// Derive exact plottable rows and report back to ChartCard for TableView
const plotData = useMemo(() => {
- return filteredData.map((entry: any, i: number) => {
+ return filteredData.map((entry, i) => {
const cmpEntry = filteredCompareData[i];
return {
[chart.xField]: entry[chart.xField],
@@ -325,26 +327,26 @@ const CompareDiffPerXBarChart = ({
const data = useMemo(() => {
let colors: string[];
- if (colorField && (rows[0] as any)?.[colorField]) {
- colors = filteredData.map((entry: any) => entry[colorField]);
+ if (colorField && rows[0]?.[colorField]) {
+ colors = filteredData.map((entry) => entry[colorField] as string);
} else {
const colorScale = d3.scaleOrdinal(
- (d3 as any)[colorScheme],
+ d3Schemes[colorScheme],
);
colors = filteredData.map((_, index) => colorScale(index.toString()));
}
return {
- labels: filteredData.map((entry: any) => entry[xField]),
+ labels: filteredData.map((entry) => entry[xField]),
datasets: [
{
label: primaryLabel,
- data: filteredData.map((entry: any) => entry[yField]),
+ data: filteredData.map((entry) => entry[yField] as number),
backgroundColor: colors,
},
{
label: compareLabel,
- data: filteredCompareData.map((entry: any) => entry[yField]),
+ data: filteredCompareData.map((entry) => entry[yField] as number),
backgroundColor: filteredCompareData.map(() => '#D3D3D3'),
},
],
@@ -371,8 +373,8 @@ const CompareDiffPerXBarChart = ({
legend: { display: true },
tooltip: {
callbacks: {
- label: (context: any) => {
- const value = context.parsed.y;
+ label: (context: TooltipItem<'bar'>) => {
+ const value = context.parsed.y ?? 0;
return percentFormat ? `${value}%` : value.toLocaleString();
},
},
@@ -381,7 +383,7 @@ const CompareDiffPerXBarChart = ({
scales: {
y: {
ticks: {
- callback: (value: any) =>
+ callback: (value: string | number) =>
percentFormat ? `${value}%` : value.toLocaleString(),
},
},
@@ -586,13 +588,13 @@ const ZoningAllowanceStackedBarChart = ({
legend: {
display: true,
labels: {
- generateLabels: (chart: any) =>
+ generateLabels: (chart: ChartJS) =>
stackKeys.map((key) => {
const mainIndex = chart.data.datasets.findIndex(
- (ds: any) => ds.label === `${key}`,
+ (ds) => ds.label === `${key}`,
);
const compareIndex = chart.data.datasets.findIndex(
- (ds: any) => ds.label === `${key}`,
+ (ds) => ds.label === `${key}`,
);
const mainMeta = chart.getDatasetMeta(mainIndex);
@@ -613,11 +615,15 @@ const ZoningAllowanceStackedBarChart = ({
};
}),
},
- onClick: (_e: any, legendItem: any, legend: any) => {
+ onClick: (
+ _e: ChartEvent,
+ legendItem: LegendItem,
+ legend: LegendElement<'bar'>,
+ ) => {
const chart = legend.chart;
const key = legendItem.text;
- chart.data.datasets.forEach((ds: any, idx: number) => {
+ chart.data.datasets.forEach((ds, idx) => {
if (ds.label?.startsWith(key)) {
const meta = chart.getDatasetMeta(idx);
meta.hidden = !(meta.hidden ?? false);
@@ -629,8 +635,8 @@ const ZoningAllowanceStackedBarChart = ({
},
tooltip: {
callbacks: {
- label: (ctx: any) =>
- `${ctx.dataset.label}: ${ctx.raw?.toLocaleString?.() ?? ctx.raw}`,
+ label: (ctx: TooltipItem<'bar'>) =>
+ `${ctx.dataset.label}: ${(ctx.raw as number | undefined)?.toLocaleString() ?? ctx.raw}`,
},
},
},
diff --git a/frontend/src/components/Charts/EmploymentAreaChart.tsx b/frontend/src/components/Charts/EmploymentAreaChart.tsx
index ecc2b108..b0179141 100644
--- a/frontend/src/components/Charts/EmploymentAreaChart.tsx
+++ b/frontend/src/components/Charts/EmploymentAreaChart.tsx
@@ -28,8 +28,8 @@ const SECTOR_COLORS: Record = {
'Other Services': '#c0c0c0',
};
-const yAxisFmt = (v: any) => `${(v / 1000).toFixed(0)}k`;
-const tooltipFmt = (v: any) => `${v.toLocaleString(0)}`;
+const yAxisFmt = (v: number) => `${(v / 1000).toFixed(0)}k`;
+const tooltipFmt = (v: unknown) => (v as number).toLocaleString();
type EmpView = 'stacked' | 'trend' | 'table';
@@ -57,7 +57,7 @@ export const EmploymentAreaChart = ({
chart,
view = 'report',
}: {
- chart: ChartItem;
+ chart: ChartItem;
view?: 'gallery' | 'report';
}) => {
const isPdfMode = usePdfMode();
@@ -67,7 +67,20 @@ export const EmploymentAreaChart = ({
// Gallery tiles always show the stacked view — no controls, no state to manage.
const activeView: EmpView = isPdfMode || isGallery ? 'stacked' : localView;
- const data = chart.data as any[];
+ const [hidden, setHidden] = useState>(new Set());
+ const toggleSeries = (key: string) => {
+ setHidden((prev) => {
+ const next = new Set(prev);
+ if (next.has(key)) {
+ next.delete(key);
+ } else {
+ next.add(key);
+ }
+ return next;
+ });
+ };
+
+ const data = chart.data;
if (!data?.length) return null;
const sectors = Object.keys(data[0]).filter((k) => k !== 'quarter_label');
@@ -78,7 +91,7 @@ export const EmploymentAreaChart = ({
const galleryTicks = computeGalleryTicks(q1Ticks, 4);
- const dataWithTotal = data.map((row) => ({
+ const dataWithTotal: DataRow[] = data.map((row) => ({
...row,
Total: sectors.reduce((sum, s) => {
const v = row[s];
@@ -102,19 +115,6 @@ export const EmploymentAreaChart = ({
const tableRows = [...dataWithTotal].reverse();
- const [hidden, setHidden] = useState>(new Set());
- const toggleSeries = (key: string) => {
- setHidden((prev) => {
- const next = new Set(prev);
- if (next.has(key)) {
- next.delete(key);
- } else {
- next.add(key);
- }
- return next;
- });
- };
-
const height = isGallery ? GALLERY_H : INNER_H;
return (
@@ -186,8 +186,8 @@ export const EmploymentAreaChart = ({
paddingBottom: '8px',
}}
>
- {payload?.map((entry: any) => {
- const key = entry.value;
+ {payload?.map((entry) => {
+ const key = entry.value as string;
const isHidden = hidden.has(key);
return (
{
chart: ChartItem;
@@ -107,7 +106,7 @@ export const TableView = ({
}
// Render the comparison toggle header control
- const ComparisonToggleHeader = () => (
+ const comparisonToggleHeader = (