@@ -29,7 +36,12 @@ export function SiteHeader() {
{item.label}
diff --git a/components/state-explorer-controls.tsx b/components/state-explorer-controls.tsx
new file mode 100644
index 0000000..d88c2ee
--- /dev/null
+++ b/components/state-explorer-controls.tsx
@@ -0,0 +1,82 @@
+"use client";
+
+import Link from "next/link";
+import { startTransition, useState } from "react";
+import { useRouter } from "next/navigation";
+
+import { Badge } from "@/components/ui/badge";
+import { buttonVariants } from "@/components/ui/button";
+import { Card, CardDescription, CardTitle } from "@/components/ui/card";
+
+type StateExplorerControlsProps = {
+ jurisdictions: Array<{
+ slug: string;
+ name: string;
+ abbr: string;
+ }>;
+ selectedSlug: string;
+};
+
+export function StateExplorerControls({
+ jurisdictions,
+ selectedSlug
+}: StateExplorerControlsProps) {
+ const router = useRouter();
+ const [value, setValue] = useState(selectedSlug);
+ const selectedJurisdiction =
+ jurisdictions.find((jurisdiction) => jurisdiction.slug === value) ?? jurisdictions[0];
+
+ function handleChange(nextSlug: string) {
+ setValue(nextSlug);
+
+ startTransition(() => {
+ router.replace(`/states?state=${nextSlug}`, { scroll: false });
+ });
+ }
+
+ return (
+
+
+ States
+ Pick a state and see where it stacks up.
+
+ Use the explorer to jump to any state, scan its strongest and weakest rankings, and then dig into the latest metric cards and trends below.
+
+
+
+
+ State
+ handleChange(event.target.value)}
+ >
+ {jurisdictions.map((jurisdiction) => (
+
+ {jurisdiction.name}
+
+ ))}
+
+
+
+
+
Current pick
+
+ {selectedJurisdiction?.name} {selectedJurisdiction ? `(${selectedJurisdiction.abbr})` : null}
+
+
+
+ Open shareable page
+
+
+
+
+ );
+}
diff --git a/components/state-profile-view.tsx b/components/state-profile-view.tsx
index 7ca9e97..ffc48bb 100644
--- a/components/state-profile-view.tsx
+++ b/components/state-profile-view.tsx
@@ -5,18 +5,74 @@ import { TrendChart } from "@/components/trend-chart";
type StateProfileViewProps = {
profile: StateProfile;
+ showHeading?: boolean;
};
-export function StateProfileView({ profile }: StateProfileViewProps) {
+export function StateProfileView({
+ profile,
+ showHeading = true
+}: StateProfileViewProps) {
+ const rankedMetrics = profile.metrics.filter(
+ (metric) => metric.latest != null
+ );
+ const bestMetric = rankedMetrics.reduce<(typeof rankedMetrics)[number] | null>(
+ (best, metric) =>
+ best == null || metric.latest!.rank < best.latest!.rank ? metric : best,
+ null
+ );
+ const weakestMetric = rankedMetrics.reduce<(typeof rankedMetrics)[number] | null>(
+ (weakest, metric) =>
+ weakest == null || metric.latest!.rank > weakest.latest!.rank ? metric : weakest,
+ null
+ );
+ const topTenCount = rankedMetrics.filter((metric) => metric.latest!.rank <= 10).length;
+ const bottomTenCount = rankedMetrics.filter((metric) => metric.latest!.rank >= 42).length;
+
return (
-
- State profile
- {profile.jurisdiction.name}
-
- Latest rank and recent trend for each MVP metric.
-
-
+ {showHeading ? (
+
+ State profile
+ {profile.jurisdiction.name}
+
+ Latest rank and recent trend for each MVP metric.
+
+
+ ) : null}
+
+
+
Best standing
+
+ {bestMetric ? bestMetric.definition.label : "Waiting for data"}
+
+
+ {bestMetric ? `Rank #${bestMetric.latest!.rank}` : "No ranking available yet"}
+
+
+
+
Weakest standing
+
+ {weakestMetric ? weakestMetric.definition.label : "Waiting for data"}
+
+
+ {weakestMetric ? `Rank #${weakestMetric.latest!.rank}` : "No ranking available yet"}
+
+
+
+
Top 10 finishes
+
{topTenCount}
+
+ Metrics where {profile.jurisdiction.name} is in the national top ten.
+
+
+
+
Bottom 10 finishes
+
{bottomTenCount}
+
+ Metrics where {profile.jurisdiction.name} is in the bottom ten.
+
+
+
{profile.metrics.map((metric) => (
diff --git a/lib/data/queries.ts b/lib/data/queries.ts
index 698b8ec..4daf6eb 100644
--- a/lib/data/queries.ts
+++ b/lib/data/queries.ts
@@ -4,7 +4,13 @@ import { notFound } from "next/navigation";
import { prisma } from "@/lib/db";
import { METRIC_BY_ID, METRIC_CATALOG } from "@/lib/metrics/catalog";
import { rankObservations } from "@/lib/metrics/ranking";
-import type { MetricDefinition, MetricObservation, RefreshStatus, StateProfile } from "@/lib/types";
+import type {
+ MetricDefinition,
+ MetricObservation,
+ RefreshStatus,
+ StateProfile,
+ StateStackupSummary
+} from "@/lib/types";
function toNumber(value: unknown) {
if (value == null) {
@@ -287,6 +293,69 @@ export async function getStateProfile(slug: string): Promise {
};
}
+export async function getStateStackupSummaries(): Promise {
+ const definitions = await prisma.metricDefinition.findMany({
+ orderBy: [
+ { category: "asc" },
+ { label: "asc" }
+ ]
+ });
+
+ const rankings = await Promise.all(
+ definitions.map(async (definition) => {
+ const ranking = await getLatestRanking(definition.id);
+
+ return {
+ metricId: definition.id,
+ metricLabel: definition.label,
+ rows: ranking.rows
+ };
+ })
+ );
+
+ const entriesByJurisdiction = new Map<
+ string,
+ {
+ jurisdiction: StateStackupSummary["jurisdiction"];
+ items: Array<{ metricId: string; metricLabel: string; rank: number }>;
+ }
+ >();
+
+ for (const ranking of rankings) {
+ for (const row of ranking.rows) {
+ const current = entriesByJurisdiction.get(row.jurisdiction.slug) ?? {
+ jurisdiction: row.jurisdiction,
+ items: []
+ };
+
+ current.items.push({
+ metricId: ranking.metricId,
+ metricLabel: ranking.metricLabel,
+ rank: row.rank
+ });
+
+ entriesByJurisdiction.set(row.jurisdiction.slug, current);
+ }
+ }
+
+ return [...entriesByJurisdiction.values()]
+ .map((entry) => {
+ const best = [...entry.items]
+ .sort((left, right) => left.rank - right.rank || left.metricLabel.localeCompare(right.metricLabel))
+ .slice(0, 3);
+ const worst = [...entry.items]
+ .sort((left, right) => right.rank - left.rank || left.metricLabel.localeCompare(right.metricLabel))
+ .slice(0, 3);
+
+ return {
+ jurisdiction: entry.jurisdiction,
+ best,
+ worst
+ };
+ })
+ .sort((left, right) => left.jurisdiction.name.localeCompare(right.jurisdiction.name));
+}
+
function metricCadenceToPrisma(cadence: "MONTHLY" | "QUARTERLY" | "ANNUAL") {
switch (cadence) {
case "MONTHLY":
diff --git a/lib/types.ts b/lib/types.ts
index f856b70..3092411 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -62,6 +62,18 @@ export type StateProfile = {
metrics: StateProfileMetric[];
};
+export type StateStackupItem = {
+ metricId: string;
+ metricLabel: string;
+ rank: number;
+};
+
+export type StateStackupSummary = {
+ jurisdiction: MetricObservation["jurisdiction"];
+ best: StateStackupItem[];
+ worst: StateStackupItem[];
+};
+
export type RefreshStatus = {
metricId: string;
status: "RUNNING" | "SUCCESS" | "FAILED" | "SKIPPED";
diff --git a/tests/component/choropleth-map.test.tsx b/tests/component/choropleth-map.test.tsx
new file mode 100644
index 0000000..5eabd29
--- /dev/null
+++ b/tests/component/choropleth-map.test.tsx
@@ -0,0 +1,117 @@
+import React from "react";
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { ChoroplethMap } from "@/components/choropleth-map";
+import type { RankingRow, StateStackupSummary } from "@/lib/types";
+
+const push = vi.fn();
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({
+ push
+ })
+}));
+
+vi.mock("react-simple-maps", () => ({
+ ComposableMap: ({ children }: { children: React.ReactNode }) => {children} ,
+ Geographies: ({
+ children
+ }: {
+ children: (args: {
+ geographies: Array<{ id: string | number; rsmKey: string }>;
+ }) => React.ReactNode;
+ }) =>
+ children({
+ geographies: [{ id: "06", rsmKey: "california" }]
+ }),
+ Geography: ({
+ geography,
+ onMouseEnter,
+ onMouseMove,
+ onMouseLeave,
+ onClick,
+ onFocus,
+ onBlur,
+ onKeyDown,
+ ...props
+ }: React.SVGProps & {
+ geography: { id: string | number; rsmKey: string };
+ }) => (
+
+ )
+}));
+
+const rows: RankingRow[] = [
+ {
+ rank: 5,
+ tied: false,
+ jurisdiction: {
+ slug: "california",
+ name: "California",
+ abbr: "CA",
+ fips: "06"
+ },
+ value: 1.1,
+ benchmarkValue: null,
+ deltaFromBenchmark: null,
+ percentile: 90,
+ periodLabel: "Dec 2025",
+ releaseDate: null,
+ sourceUrl: "https://example.gov"
+ }
+];
+
+const stateSummaries: StateStackupSummary[] = [
+ {
+ jurisdiction: {
+ slug: "california",
+ name: "California",
+ abbr: "CA",
+ fips: "06"
+ },
+ best: [
+ { metricId: "a", metricLabel: "Payroll job growth", rank: 2 },
+ { metricId: "b", metricLabel: "Population growth", rank: 4 },
+ { metricId: "c", metricLabel: "Median household income", rank: 8 }
+ ],
+ worst: [
+ { metricId: "x", metricLabel: "Unemployment rate", rank: 40 },
+ { metricId: "y", metricLabel: "Gasoline cost", rank: 45 },
+ { metricId: "z", metricLabel: "Taxes per capita", rank: 49 }
+ ]
+ }
+];
+
+describe("ChoroplethMap", () => {
+ it("shows a state stackup tooltip on hover and routes into the states explorer on click", () => {
+ render( );
+
+ const geography = screen.getByTestId("geography-06");
+
+ fireEvent.mouseEnter(geography, {
+ clientX: 120,
+ clientY: 140
+ });
+
+ expect(screen.getByText("California")).toBeInTheDocument();
+ expect(screen.getByText("Best 3")).toBeInTheDocument();
+ expect(screen.getByText("Worst 3")).toBeInTheDocument();
+ expect(screen.getByText("Payroll job growth")).toBeInTheDocument();
+ expect(screen.getByText("Rank #49")).toBeInTheDocument();
+
+ fireEvent.click(geography);
+
+ expect(push).toHaveBeenCalledWith("/states?state=california");
+ });
+});
diff --git a/tests/component/state-explorer-controls.test.tsx b/tests/component/state-explorer-controls.test.tsx
new file mode 100644
index 0000000..ae76585
--- /dev/null
+++ b/tests/component/state-explorer-controls.test.tsx
@@ -0,0 +1,34 @@
+import React from "react";
+import { fireEvent, render, screen } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
+
+import { StateExplorerControls } from "@/components/state-explorer-controls";
+
+const replace = vi.fn();
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({
+ replace
+ })
+}));
+
+describe("StateExplorerControls", () => {
+ it("updates the selected state and pushes the explorer route", () => {
+ render(
+
+ );
+
+ fireEvent.change(screen.getByLabelText("State"), {
+ target: { value: "colorado" }
+ });
+
+ expect(screen.getByDisplayValue("Colorado")).toBeInTheDocument();
+ expect(replace).toHaveBeenCalledWith("/states?state=colorado", { scroll: false });
+ });
+});
diff --git a/tests/component/state-profile-view.test.tsx b/tests/component/state-profile-view.test.tsx
new file mode 100644
index 0000000..2ba4a23
--- /dev/null
+++ b/tests/component/state-profile-view.test.tsx
@@ -0,0 +1,102 @@
+import React from "react";
+import { render, screen } from "@testing-library/react";
+import { describe, expect, it } from "vitest";
+
+import { StateProfileView } from "@/components/state-profile-view";
+import type { StateProfile } from "@/lib/types";
+
+const profile: StateProfile = {
+ jurisdiction: {
+ slug: "colorado",
+ name: "Colorado",
+ abbr: "CO",
+ fips: "08"
+ },
+ metrics: [
+ {
+ definition: {
+ id: "payroll_growth",
+ label: "Payroll job growth",
+ category: "Economy",
+ sourceName: "BLS",
+ sourceUrl: "https://bls.gov",
+ cadence: "MONTHLY",
+ betterDirection: "HIGHER",
+ unit: "percent",
+ description: "desc",
+ caveats: null,
+ methodology: "method"
+ },
+ latest: {
+ rank: 4,
+ tied: false,
+ jurisdiction: {
+ slug: "colorado",
+ name: "Colorado",
+ abbr: "CO",
+ fips: "08"
+ },
+ value: 1.4,
+ benchmarkValue: null,
+ deltaFromBenchmark: null,
+ percentile: 94,
+ periodLabel: "Dec 2025",
+ releaseDate: null,
+ sourceUrl: "https://example.gov"
+ },
+ trend: []
+ },
+ {
+ definition: {
+ id: "poverty_rate",
+ label: "Poverty rate",
+ category: "People & Affordability",
+ sourceName: "Census",
+ sourceUrl: "https://census.gov",
+ cadence: "ANNUAL",
+ betterDirection: "LOWER",
+ unit: "percent",
+ description: "desc",
+ caveats: null,
+ methodology: "method"
+ },
+ latest: {
+ rank: 45,
+ tied: false,
+ jurisdiction: {
+ slug: "colorado",
+ name: "Colorado",
+ abbr: "CO",
+ fips: "08"
+ },
+ value: 10.5,
+ benchmarkValue: null,
+ deltaFromBenchmark: null,
+ percentile: 12,
+ periodLabel: "2025",
+ releaseDate: null,
+ sourceUrl: "https://example.gov"
+ },
+ trend: []
+ }
+ ]
+};
+
+describe("StateProfileView", () => {
+ it("surfaces a quick ranking summary for the selected state", () => {
+ render( );
+
+ expect(screen.getByText("Best standing")).toBeInTheDocument();
+ expect(screen.getAllByText("Payroll job growth").length).toBeGreaterThan(0);
+ expect(screen.getByText("Rank #4")).toBeInTheDocument();
+ expect(screen.getByText("Weakest standing")).toBeInTheDocument();
+ expect(screen.getAllByText("Poverty rate").length).toBeGreaterThan(0);
+ expect(screen.getByText("Rank #45")).toBeInTheDocument();
+ expect(
+ screen.getByText("Metrics where Colorado is in the national top ten.")
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("Metrics where Colorado is in the bottom ten.")
+ ).toBeInTheDocument();
+ });
+});