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 = (