import { useId, useState } from 'react' import { SwatchIcon } from '@heroicons/react/24/outline' import { CENSUS_SCALES, bubbleFillFromT, bubbleRadiusPx, colorFromT, metricToDisplayT, type CensusScaleId, } from '../utils/censusMapTransforms' import type { CensusValueMode } from '../utils/censusMapValueMode' import type { CensusChoroLegendSemantics } from '../utils/censusDataDictionary' import { InfoHelpTrigger } from './InfoHelpTrigger' const CHORO_LEGEND_GRADIENT_STOPS = 17 function LabelWithInfo({ label, help, labelClassName = 'text-xs font-semibold uppercase tracking-wide text-slate-500', }: { label: string help: string labelClassName?: string }) { return ( {label} ) } export function ChoroplethLegend({ min, max, scale, format, valueMode = 'raw', extentPoolsAllVintages = false, metricHelp, semantics, letterGradeLegend, }: { min: number max: number scale: CensusScaleId format: (v: number) => string valueMode?: CensusValueMode extentPoolsAllVintages?: boolean metricHelp?: string semantics?: CensusChoroLegendSemantics | null letterGradeLegend?: import('react').ReactNode }) { const legendHelpPanelId = useId() const [legendHelpOpen, setLegendHelpOpen] = useState(false) const n = CHORO_LEGEND_GRADIENT_STOPS const stops = Array.from({ length: n }, (_, i) => { const u = n <= 1 ? 0 : i / (n - 1) const v = min + u * (max - min) const t = metricToDisplayT(v, min, max, scale) ?? 0 return { offset: `${u * 100}%`, color: colorFromT(t), value: v } }) const tickUs = [0, 0.25, 0.5, 0.75, 1] as const const gradId = `census-ramp-${scale}-${Math.round(min)}-${Math.round(max)}` const legendInfoBody = [ 'Legend maps the displayed value to color using the selected transform. When multi-year data is loaded, low/high can be pooled across years so colors stay comparable as you change year.', metricHelp, ] .filter(Boolean) .join('\n\n') const legendFootnote = `Stops are evenly spaced in mapped value range; shading follows the selected transform (${CENSUS_SCALES.find((x) => x.id === scale)?.label ?? scale}). ${ valueMode === 'raw' ? extentPoolsAllVintages ? 'Percentile band (~4th–96th pct.) is computed across all years in the slider when multi-year trend data is present, so colors stay comparable as you change year.' : 'Extremes use percentile clipping so outliers do not wash out the map.' : valueMode === 'yoy' ? 'Legend shows percent change vs the prior year in the slider order.' : 'Legend shows percent difference from the national benchmark (population-weighted state composite when available).' }${ extentPoolsAllVintages && valueMode !== 'raw' ? ' Endpoints pool all years from trend data so the scale stays fixed while you scrub the year slider.' : '' }` return (
{metricHelp ? ( ) : ( What the colors mean )}
{semantics ? ( <>
{semantics.lowEnd} ←→ {semantics.highEnd}

{semantics.gradientHint}

) : null} {stops.map((s, i) => ( ))} {tickUs.map((frac) => ( {format(min + frac * (max - min))} ))} {letterGradeLegend ? (
{letterGradeLegend}
) : null}
{legendHelpOpen ? (

{legendInfoBody}

{legendFootnote}

) : null}
) } export function BubbleLegend({ min, max, scale, format, metricHelp, letterGradeLegend, }: { min: number max: number scale: CensusScaleId format: (v: number) => string metricHelp?: string letterGradeLegend?: import('react').ReactNode }) { const legendHelpPanelId = useId() const [legendHelpOpen, setLegendHelpOpen] = useState(false) const refs = [0.15, 0.5, 0.88].map((u) => min + u * (max - min)) const items = refs.map((v) => ({ v, r: bubbleRadiusPx(v, min, max, scale, 4, 22), t: metricToDisplayT(v, min, max, scale), label: format(v), })) const bubbleInfoBody = [ 'Circle area encodes the mapped value; color follows the Deep Ocean ramp (steel blue → teal → deep emerald) as on the map.', metricHelp, ] .filter(Boolean) .join('\n\n') return (
{metricHelp ? ( ) : ( Bubble size scale )}
{items.map((it, i) => (
{it.label}
))}
{letterGradeLegend ? (
{letterGradeLegend}
) : null}
{legendHelpOpen ? (

{bubbleInfoBody}

) : null}
) }