bharat-choropleth 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/index.d.ts +170 -0
- package/dist/index.js +833 -0
- package/dist/index.js.map +1 -0
- package/dist/style.css +120 -0
- package/package.json +71 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/IndiaChoropleth.tsx","../src/geometry.ts","../src/legend.ts","../src/tooltip-position.ts","../src/small-regions.ts","../src/useControllableState.ts"],"sourcesContent":["import { geoMercator, geoPath, type GeoProjection } from \"d3-geo\";\nimport { useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState, type MouseEvent as ReactMouseEvent } from \"react\";\nimport { asFeatureCollection, totalOf } from \"./geometry\";\nimport { legendBucketLabel, legendBuckets, swatchIndexOf } from \"./legend\";\nimport { placeTooltip } from \"./tooltip-position\";\nimport {\n boundsOfRing,\n distanceToParts,\n keepsTrueGeometry,\n labelPointFor,\n enlargeSmallParts,\n largestRingExtent,\n placeOutsideLabel,\n ringsToPath,\n scatteredHitArea,\n type Box,\n type Point,\n} from \"./small-regions\";\nimport type {\n ColorContext,\n ColorScale,\n IndiaChoroplethProps,\n InsightContext,\n MapFeature,\n MapFeatureCollection,\n MapLayer,\n MapRegion,\n ReferenceOverlay,\n TooltipContext,\n} from \"./types\";\nimport { useControllableState } from \"./useControllableState\";\n\nconst VIEWBOX = { width: 960, height: 640, padding: 28 };\nconst DEFAULT_COLORS = [\"#d9f1ed\", \"#b9e3dd\", \"#8fd1c8\", \"#5bb9ae\", \"#2f9c90\", \"#147b71\", \"#075b55\"] as const;\nconst DEFAULT_FORMAT = new Intl.NumberFormat(\"en-IN\").format;\n// Space between the region centroid and the tooltip edge. Mirrors the .75rem in style.css.\nconst TOOLTIP_GAP_PX = 12;\n// Small-region handling, in view-box units. Mirrors the DOM and Dart renderers.\nconst SMALL_REGION_EXTENT = 22;\nconst SMALL_REGION_CLICK_RADIUS = 14;\nconst MIN_REGION_MARKER_SIZE = 7;\n\ntype PreparedRegion = MapRegion & {\n path: string;\n /**\n * Hull covering a scattered region's parts and the space between them, drawn\n * invisibly under every outline so hovering the water inside Lakshadweep\n * reaches Lakshadweep. Null for regions that are one part or big enough to\n * point at directly.\n */\n hitPath: string | null;\n centroid: [number, number];\n /** Bounding box of each separate part, for measuring how close a click landed. */\n partBounds: Box[];\n /** Longest side of the largest part — the measure of \"too small to use\". */\n extent: number;\n};\n\n/**\n * Project a feature's rings into view-box coordinates. Needed alongside the SVG\n * path string because the small-region helpers work on coordinates, and a path\n * string cannot be measured per part.\n */\nfunction projectedRings(feature: MapFeature, projection: GeoProjection): Point[][] {\n const geometry = feature.geometry;\n if (!geometry) return [];\n const polygons =\n geometry.type === \"Polygon\" ? [geometry.coordinates]\n : geometry.type === \"MultiPolygon\" ? geometry.coordinates\n : [];\n const rings: Point[][] = [];\n for (const polygon of polygons) {\n for (const ring of polygon) {\n const projected: Point[] = [];\n for (const position of ring) {\n const point = projection(position as [number, number]);\n if (point && Number.isFinite(point[0]) && Number.isFinite(point[1])) projected.push([point[0], point[1]]);\n }\n if (projected.length > 0) rings.push(projected);\n }\n }\n return rings;\n}\ntype PreparedReferenceOverlay = { id: string; label: string; description: string; path: string };\n\nfunction colorFor(value: number | null, region: MapRegion, min: number, max: number, scale: ColorScale): string {\n if (typeof scale === \"function\") {\n const context: ColorContext = { min, max, feature: region.feature, id: region.id };\n return scale(value, context);\n }\n // The same index the legend filters by, deliberately: \"highlight the regions\n // painted in this colour\" has to be true by construction, not by two formulas\n // that happen to agree until one of them is tweaked.\n const index = swatchIndexOf(value, min, max, scale.length);\n return index === null ? \"var(--india-map-empty)\" : scale[index] ?? \"var(--india-map-empty)\";\n}\n\nfunction makeProjection(collection: MapFeatureCollection): GeoProjection {\n return geoMercator().fitExtent(\n [[VIEWBOX.padding, VIEWBOX.padding], [VIEWBOX.width - VIEWBOX.padding, VIEWBOX.height - VIEWBOX.padding]],\n collection,\n );\n}\n\nfunction prepareLayer(\n layer: MapLayer,\n projection = makeProjection(asFeatureCollection(layer.geometry)),\n minPartExtent = 0,\n): PreparedRegion[] {\n const collection = asFeatureCollection(layer.geometry);\n const path = geoPath(projection);\n return collection.features.map((feature) => {\n const centroid = path.centroid(feature) as [number, number];\n const bounds = path.bounds(feature);\n const fallbackCentroid: [number, number] = [\n (bounds[0][0] + bounds[1][0]) / 2,\n (bounds[0][1] + bounds[1][1]) / 2,\n ];\n const region: MapRegion = {\n id: layer.getId(feature),\n label: layer.getLabel(feature),\n value: layer.getValue(feature),\n meta: layer.getMeta?.(feature),\n feature,\n };\n // Puducherry and anything else on the keep-true list is drawn as it really\n // is, however small, because there is no room around it to grow into.\n const exaggerate = minPartExtent > 0 && !keepsTrueGeometry(region.id);\n const rings = exaggerate\n ? enlargeSmallParts(projectedRings(feature, projection), minPartExtent)\n : projectedRings(feature, projection);\n const fallback: [number, number] = centroid.every(Number.isFinite) ? centroid : fallbackCentroid;\n const hull = scatteredHitArea(rings, SMALL_REGION_EXTENT);\n // With exaggeration on, the drawn outline has to come from the moved rings\n // rather than d3's path generator, so what is drawn, measured, labelled and\n // clicked are all the same geometry.\n return {\n ...region,\n path: exaggerate ? ringsToPath(rings) : (path(feature) ?? \"\"),\n hitPath: hull ? ringsToPath([hull]) : null,\n // The largest part's centroid, not the whole feature's: averaging across\n // parts puts an island group's label out at sea between its islands.\n centroid: rings.length > 0 ? (labelPointFor(rings, fallback) as [number, number]) : fallback,\n partBounds: rings.map(boundsOfRing),\n extent: largestRingExtent(rings),\n };\n });\n}\n\nfunction prepareReferenceOverlay(overlay: ReferenceOverlay, projection: GeoProjection): PreparedReferenceOverlay[] {\n const path = geoPath(projection);\n return asFeatureCollection(overlay.geometry).features.map((feature) => ({\n id: overlay.getId(feature),\n label: overlay.getLabel(feature),\n description: overlay.getDescription(feature),\n path: path(feature) ?? \"\",\n }));\n}\n\nfunction ordinal(n: number): string {\n const lastTwo = n % 100;\n if (lastTwo >= 11 && lastTwo <= 13) return `${n}th`;\n return `${n}${[\"th\", \"st\", \"nd\", \"rd\"][n % 10] ?? \"th\"}`;\n}\n\nfunction defaultTooltip(context: TooltipContext, formatValue: (value: number) => string) {\n return (\n <>\n <strong>{context.label}</strong>\n {/* Same wording as the region's own aria-label, so the two never disagree. */}\n <b>{context.value === null ? \"No data\" : formatValue(context.value)}</b>\n {context.share !== null ? (\n <>\n {/* A bar makes the share readable at a glance; it repeats the number\n beside it, so it's decorative and hidden from assistive tech. */}\n <span className=\"india-choropleth__tooltip-bar\" aria-hidden=\"true\">\n <span style={{ width: `${Math.max(context.share, 1.5)}%` }} />\n </span>\n <small>\n {[\n `${context.share.toFixed(1)}% of total`,\n ...(context.rank !== null ? [`${ordinal(context.rank)} of ${context.rankedCount}`] : []),\n ].join(\" · \")}\n </small>\n </>\n ) : null}\n </>\n );\n}\n\n/**\n * A data-agnostic, accessible SVG India map renderer. Import `@india-choropleth/react/style.css`\n * once in the host app; data and boundaries intentionally remain separate.\n */\nexport function IndiaChoropleth({\n states,\n referenceOverlay,\n loadDistricts,\n loadDistrictReferenceOverlay,\n drillDownId,\n defaultDrillDownId = null,\n onDrillDownChange,\n selectedId,\n defaultSelectedId = null,\n onSelectedChange,\n onInspect,\n onInsight,\n onRegionClick,\n onBackgroundClick,\n colorScale = DEFAULT_COLORS,\n formatValue = DEFAULT_FORMAT,\n renderTooltip,\n renderInsights,\n showLegend = true,\n showBreadcrumb = true,\n legendLabels = [\"Lower\", \"Higher\"],\n referenceOverlayLegendLabel = \"Reference context · data unavailable\",\n referenceOverlayMergeIds = [],\n referenceOverlayFill = \"hatch\",\n showRegionValues = false,\n minPartExtent = 0,\n minDistrictPartExtent,\n className,\n ariaLabel = \"Interactive choropleth map\",\n interactive = true,\n}: IndiaChoroplethProps) {\n const tooltipId = useId();\n const canvasRef = useRef<HTMLDivElement | null>(null);\n const tooltipAnchorRef = useRef<HTMLDivElement | null>(null);\n const hatchId = `${useId()}-reference-hatch`;\n const [activeDrillDownId, setActiveDrillDownId] = useControllableState(drillDownId, defaultDrillDownId);\n const [activeSelectedId, setActiveSelectedId] = useControllableState(selectedId, defaultSelectedId);\n const [loadedDistricts, setLoadedDistricts] = useState<{ stateId: string; layer: MapLayer } | null>(null);\n const [loadedDistrictReferenceOverlay, setLoadedDistrictReferenceOverlay] = useState<{ stateId: string; overlay: ReferenceOverlay | null } | null>(null);\n const [loadingState, setLoadingState] = useState<string | null>(null);\n const [loadError, setLoadError] = useState<Error | null>(null);\n const pathRefs = useRef<Record<string, SVGPathElement | null>>({});\n const restoreFocusId = useRef<string | null>(null);\n\n const stateCollection = useMemo(() => asFeatureCollection(states.geometry), [states]);\n const referenceCollection = useMemo(\n () => referenceOverlay ? asFeatureCollection(referenceOverlay.geometry) : null,\n [referenceOverlay],\n );\n const nationalProjection = useMemo(\n () => makeProjection({ type: \"FeatureCollection\", features: [...stateCollection.features, ...(referenceCollection?.features ?? [])] }),\n [referenceCollection, stateCollection],\n );\n const stateRegions = useMemo(() => prepareLayer(states, nationalProjection, minPartExtent), [minPartExtent, nationalProjection, states]);\n const referenceRegions = useMemo(\n () => referenceOverlay ? prepareReferenceOverlay(referenceOverlay, nationalProjection) : [],\n [nationalProjection, referenceOverlay],\n );\n const drilledState = useMemo(\n () => stateRegions.find((region) => region.id === activeDrillDownId) ?? null,\n [activeDrillDownId, stateRegions],\n );\n const isDrillRequested = Boolean(drilledState && activeDrillDownId);\n const level = isDrillRequested ? \"district\" : \"state\";\n const districtLayer = loadedDistricts?.stateId === activeDrillDownId ? loadedDistricts.layer : null;\n const districtReferenceOverlay = loadedDistrictReferenceOverlay?.stateId === activeDrillDownId\n ? loadedDistrictReferenceOverlay.overlay\n : null;\n const districtCollection = useMemo(\n () => districtLayer ? asFeatureCollection(districtLayer.geometry) : null,\n [districtLayer],\n );\n const districtReferenceCollection = useMemo(\n () => districtReferenceOverlay ? asFeatureCollection(districtReferenceOverlay.geometry) : null,\n [districtReferenceOverlay],\n );\n const districtProjection = useMemo(\n () => districtCollection ? makeProjection({ type: \"FeatureCollection\", features: [...districtCollection.features, ...(districtReferenceCollection?.features ?? [])] }) : null,\n [districtCollection, districtReferenceCollection],\n );\n const regions = useMemo(\n () => level === \"district\" && districtLayer && districtProjection\n ? prepareLayer(districtLayer, districtProjection, minDistrictPartExtent ?? minPartExtent)\n : level === \"state\" ? stateRegions : [],\n [districtLayer, districtProjection, level, minDistrictPartExtent, minPartExtent, stateRegions],\n );\n const districtReferenceRegions = useMemo(\n () => level === \"district\" && districtReferenceOverlay && districtProjection ? prepareReferenceOverlay(districtReferenceOverlay, districtProjection) : [],\n [districtProjection, districtReferenceOverlay, level],\n );\n const selected = regions.find((region) => region.id === activeSelectedId) ?? null;\n const [inspectedId, setInspectedId] = useState<string | null>(null);\n const inspectedIdRef = useRef<string | null>(null);\n // Hover/focus only — no fallback to `selected`, so the floating tooltip clears\n // when the pointer/focus leaves instead of sticking on the selected region.\n const inspected = regions.find((region) => region.id === inspectedId) ?? null;\n\n const values = useMemo(() => regions.map((region) => region.value).filter((value): value is number => value !== null), [regions]);\n const total = useMemo(() => totalOf(values), [values]);\n const min = values.length ? Math.min(...values) : 0;\n const max = values.length ? Math.max(...values) : 0;\n\n // The legend doubles as a filter. A function colour scale has no swatches of\n // its own, so the default ramp stands in and the bands still read low to high.\n const legendColors = typeof colorScale === \"function\" ? DEFAULT_COLORS : colorScale;\n const buckets = useMemo(\n () => legendBuckets(legendColors, regions.map((region) => region.value), min, max),\n [legendColors, max, min, regions],\n );\n const [activeBucket, setActiveBucket] = useState<number | null>(null);\n // Clamped here rather than only in the effect below: effects run after paint,\n // so a stale index would dim against the previous level's bands for a frame.\n const filterBucket = activeBucket !== null && activeBucket < buckets.length ? activeBucket : null;\n // Keyed on the ramp's contents, not its identity — a host passing an inline\n // array literal would otherwise clear the filter on every re-render.\n const colorScaleKey = typeof colorScale === \"function\" ? \"function\" : colorScale.join(\",\");\n // Bands come from this level's own min and max, and change with the ramp, so an\n // index picked under one of them means something else under another.\n useEffect(() => { setActiveBucket(null); }, [activeDrillDownId, colorScaleKey, level]);\n\n const highlighted = useMemo(\n () => filterBucket === null\n ? null\n : new Set(regions\n .filter((region) => swatchIndexOf(region.value, min, max, legendColors.length) === filterBucket)\n .map((region) => region.id)),\n [filterBucket, legendColors.length, max, min, regions],\n );\n // Dimmed regions stay hoverable: the filter is about where the eye goes, and a\n // region you can see is a region whose number should still be reachable.\n const isDimmed = (id: string) => highlighted !== null && !highlighted.has(id);\n\n useEffect(() => {\n let cancelled = false;\n if (!activeDrillDownId || !loadDistricts) {\n setLoadedDistricts(null);\n setLoadingState(null);\n setLoadError(null);\n return;\n }\n const sourceState = stateRegions.find((region) => region.id === activeDrillDownId);\n if (!sourceState) return;\n setLoadingState(activeDrillDownId);\n setLoadError(null);\n setLoadedDistricts(null);\n loadDistricts(activeDrillDownId, sourceState)\n .then((loaded) => { if (!cancelled) setLoadedDistricts({ stateId: activeDrillDownId, layer: loaded }); })\n .catch((error: unknown) => { if (!cancelled) setLoadError(error instanceof Error ? error : new Error(\"Unable to load districts.\")); })\n .finally(() => { if (!cancelled) setLoadingState(null); });\n return () => { cancelled = true; };\n }, [activeDrillDownId, loadDistricts, stateRegions]);\n\n useEffect(() => {\n let cancelled = false;\n if (!activeDrillDownId || !loadDistrictReferenceOverlay) {\n setLoadedDistrictReferenceOverlay(null);\n return;\n }\n const sourceState = stateRegions.find((region) => region.id === activeDrillDownId);\n if (!sourceState) return;\n setLoadedDistrictReferenceOverlay(null);\n loadDistrictReferenceOverlay(activeDrillDownId, sourceState)\n .then((overlay) => { if (!cancelled) setLoadedDistrictReferenceOverlay({ stateId: activeDrillDownId, overlay }); })\n // Optional reference context must not prevent a usable district data view.\n .catch(() => { if (!cancelled) setLoadedDistrictReferenceOverlay({ stateId: activeDrillDownId, overlay: null }); });\n return () => { cancelled = true; };\n }, [activeDrillDownId, loadDistrictReferenceOverlay, stateRegions]);\n\n useEffect(() => {\n const regionId = restoreFocusId.current;\n if (!regionId || level !== \"state\") return;\n restoreFocusId.current = null;\n pathRefs.current[regionId]?.focus();\n }, [level, regions]);\n\n useEffect(() => {\n if (!inspectedId) return;\n if (!regions.some((region) => region.id === inspectedId)) {\n inspectedIdRef.current = null;\n setInspectedId(null);\n }\n }, [inspectedId, regions]);\n\n // Mirrors `inspectedId` synchronously so a single gesture that exits both a region\n // and the canvas doesn't report the clear twice. Deliberately limited to redundant\n // clears: re-inspecting the same region must still notify, because focus restoration\n // after breadcrumb-back re-inspects the region `goBack` already primed.\n const inspect = (region: PreparedRegion | null) => {\n const nextId = region?.id ?? null;\n if (nextId === null && inspectedIdRef.current === null) return;\n inspectedIdRef.current = nextId;\n setInspectedId(nextId);\n onInspect?.(region ?? null, level);\n };\n\n const activate = (region: PreparedRegion) => {\n onRegionClick?.(region, level);\n setActiveSelectedId(region.id);\n onSelectedChange?.(region, level);\n if (level === \"state\" && loadDistricts) {\n setActiveSelectedId(null);\n setActiveDrillDownId(region.id);\n onDrillDownChange?.(region.id, region);\n }\n };\n\n const goBack = () => {\n const priorState = drilledState ?? undefined;\n setActiveDrillDownId(null);\n setActiveSelectedId(priorState?.id ?? null);\n inspectedIdRef.current = priorState?.id ?? null;\n setInspectedId(priorState?.id ?? null);\n restoreFocusId.current = priorState?.id ?? null;\n onDrillDownChange?.(null, priorState);\n };\n\n // Shared by the tooltip and the insight panel so the two can never disagree\n // about share or rank — they only differ in which region they describe.\n const toTooltipContext = useCallback((region: PreparedRegion): TooltipContext => {\n const valued = regions.filter((candidate) => candidate.value !== null);\n // Ties share the better rank (\"2nd of 36\" twice, then 4th), which is what a\n // reader expects from a leaderboard and avoids an arbitrary tiebreak.\n const rank = region.value === null\n ? null\n : valued.filter((candidate) => (candidate.value ?? 0) > (region.value ?? 0)).length + 1;\n return {\n ...region,\n level,\n total,\n share: region.value === null || total === 0 ? null : (region.value / total) * 100,\n rank,\n rankedCount: valued.length,\n };\n }, [level, regions, total]);\n\n const tooltipContext = useMemo<TooltipContext | null>(\n () => inspected ? toTooltipContext(inspected) : null,\n [inspected, toTooltipContext],\n );\n // Unlike the tooltip, the host-owned insight panel is meant to stay informative\n // when nothing is hovered, so it still falls back to the selected region.\n const insightSource = inspected ?? selected;\n const insightContext = useMemo<InsightContext | null>(() => insightSource\n ? { ...toTooltipContext(insightSource), selected: insightSource.id === selected?.id }\n : null, [insightSource, selected?.id, toTooltipContext]);\n /**\n * Where each region's value label goes, and whether it needs a leader line.\n * Small regions are moved into clear space beside themselves; everything else\n * keeps its number at its centroid.\n */\n const valuePlacements = useMemo(() => {\n const coversAnother = (region: PreparedRegion, at: Point, halfWidth: number, halfHeight: number) => {\n const probes: Point[] = [\n at,\n [at[0] - halfWidth, at[1] - halfHeight],\n [at[0] + halfWidth, at[1] + halfHeight],\n [at[0] + halfWidth, at[1] - halfHeight],\n [at[0] - halfWidth, at[1] + halfHeight],\n ];\n return regions.some((other) => other.id !== region.id && other.partBounds.some(([minX, minY, maxX, maxY]) =>\n probes.some((probe) => probe[0] >= minX && probe[0] <= maxX && probe[1] >= minY && probe[1] <= maxY)));\n };\n\n return regions.map((region) => {\n const text = region.value === null ? \"—\" : formatValue(region.value);\n // Glyph metrics without measuring the DOM: the stylesheet sets 11px bold,\n // and digits in that face are close enough to half-em wide for placement.\n const halfWidth = Math.max(text.length * 3.1, 3);\n const halfHeight = 5.5;\n const isSmall = region.extent > 0 && region.extent < SMALL_REGION_EXTENT;\n const fitsInside = region.partBounds.some(([minX, minY, maxX, maxY]) =>\n maxX - minX >= halfWidth * 2 && maxY - minY >= halfHeight * 2);\n\n if (!isSmall && fitsInside) return { region, text, at: region.centroid as Point, leader: null };\n\n const outside = isSmall\n ? placeOutsideLabel({\n anchor: region.centroid,\n clearance: Math.max(region.extent, MIN_REGION_MARKER_SIZE) / 2 + 4 + halfWidth,\n halfSize: [halfWidth, halfHeight],\n viewBox: [VIEWBOX.width, VIEWBOX.height],\n centre: [VIEWBOX.width / 2, VIEWBOX.height / 2],\n // Moving a label out only helps if there is open space to move it into.\n // Goa and Puducherry have sea beside them; Delhi is ringed by other\n // states, so its number stays put rather than landing on a neighbour.\n isBlocked: (candidate) => coversAnother(region, candidate, halfWidth, halfHeight),\n })\n : null;\n\n if (!outside) {\n return fitsInside ? { region, text, at: region.centroid as Point, leader: null } : null;\n }\n\n const gap = Math.max(region.extent, MIN_REGION_MARKER_SIZE) / 2 + 1;\n const dx = outside[0] - region.centroid[0];\n const dy = outside[1] - region.centroid[1];\n const length = Math.hypot(dx, dy);\n if (length <= gap + halfWidth) return { region, text, at: outside, leader: null };\n return {\n region,\n text,\n at: outside,\n leader: [\n [region.centroid[0] + (dx / length) * gap, region.centroid[1] + (dy / length) * gap],\n [outside[0] - (dx / length) * (halfWidth + 1.5), outside[1] - (dy / length) * (halfWidth + 1.5)],\n ] as [Point, Point],\n };\n }).filter((placement): placement is NonNullable<typeof placement> => placement !== null);\n }, [formatValue, regions]);\n\n const smallMarkers = useMemo(\n () => regions.filter((region) => region.extent > 0 && region.extent < MIN_REGION_MARKER_SIZE),\n [regions],\n );\n\n /**\n * A click that reaches the svg itself missed every region path. Either it\n * landed near a small one — Goa, Puducherry, the island groups, all awkward to\n * hit — or it is a click on open sea, which clears the selection.\n */\n const handleBackgroundClick = (event: ReactMouseEvent<SVGSVGElement>) => {\n if (event.target !== event.currentTarget) return; // a region handled it\n // An unlaid-out svg (or a test environment that reports zero-size rects) has\n // no usable coordinates, so the proximity step is skipped — but the click is\n // still a click on the background and must clear the selection.\n const rect = event.currentTarget.getBoundingClientRect();\n const point: Point | null = rect.width > 0 && rect.height > 0\n ? (() => {\n // The svg scales its view box to fit while preserving aspect ratio, so\n // the scale is the smaller ratio and the remainder is centring.\n const scale = Math.min(rect.width / VIEWBOX.width, rect.height / VIEWBOX.height);\n return [\n (event.clientX - rect.left - (rect.width - VIEWBOX.width * scale) / 2) / scale,\n (event.clientY - rect.top - (rect.height - VIEWBOX.height * scale) / 2) / scale,\n ] as Point;\n })()\n : null;\n\n let nearest: PreparedRegion | null = null;\n if (point) {\n let nearestDistance = Infinity;\n for (const region of regions) {\n if (region.extent <= 0 || region.extent >= SMALL_REGION_EXTENT) continue;\n const distance = distanceToParts(point, region.partBounds);\n if (distance <= SMALL_REGION_CLICK_RADIUS && distance < nearestDistance) {\n nearestDistance = distance;\n nearest = region;\n }\n }\n }\n if (nearest) { activate(nearest); return; }\n\n onBackgroundClick?.();\n if (activeSelectedId !== null) {\n setActiveSelectedId(null);\n onSelectedChange?.(null, level);\n }\n };\n\n const canDrill = Boolean(loadDistricts && level === \"state\");\n const visibleReferenceRegions = level === \"state\" ? referenceRegions : districtReferenceRegions;\n const mergedReferenceIds = useMemo(() => new Set(referenceOverlayMergeIds), [referenceOverlayMergeIds]);\n\n useEffect(() => { onInsight?.(insightContext); }, [insightContext, onInsight]);\n\n // Nudge the anchored tooltip back inside the map. This runs in a layout effect,\n // not an effect, so the un-nudged position never paints — that one frame would be\n // a visible jump on exactly the edge regions this exists to fix. Keyed on the\n // content, not just the region, because the box is sized by what's in it.\n useLayoutEffect(() => {\n const anchor = tooltipAnchorRef.current;\n const canvas = canvasRef.current;\n if (!anchor || !canvas) return;\n\n // Measure the default placement (centered, above), so the correction is\n // computed against a known starting point rather than the last region's.\n anchor.style.removeProperty(\"--india-map-tooltip-dx\");\n anchor.style.removeProperty(\"--india-map-tooltip-dy\");\n\n const tooltipRect = anchor.getBoundingClientRect();\n const boundsRect = canvas.getBoundingClientRect();\n if (tooltipRect.width === 0 || boundsRect.width === 0) return; // not laid out (hidden, or jsdom)\n\n const { dx, side } = placeTooltip(tooltipRect, boundsRect, TOOLTIP_GAP_PX);\n if (dx !== 0) anchor.style.setProperty(\"--india-map-tooltip-dx\", `${Math.round(dx)}px`);\n if (side === \"below\") anchor.style.setProperty(\"--india-map-tooltip-dy\", `${TOOLTIP_GAP_PX}px`);\n }, [tooltipContext?.id, tooltipContext?.value, tooltipContext?.label, renderTooltip]);\n\n return (\n <section className={[\"india-choropleth\", !interactive && \"india-choropleth--static\", className].filter(Boolean).join(\" \")} aria-busy={loadingState ? \"true\" : undefined}>\n {showBreadcrumb ? (\n <div className=\"india-choropleth__toolbar\">\n <nav className=\"india-choropleth__breadcrumb\" aria-label=\"Map hierarchy\">\n {drilledState ? <button className=\"india-choropleth__back\" type=\"button\" onClick={goBack}>All states</button> : <span>All states</span>}\n {drilledState ? <><span aria-hidden=\"true\">/</span><span aria-current=\"page\">{drilledState.label}</span></> : null}\n </nav>\n </div>\n ) : null}\n <div ref={canvasRef} className=\"india-choropleth__canvas\" onMouseLeave={interactive ? () => inspect(null) : undefined}>\n {isDrillRequested && (!districtLayer || loadError) ? (\n <div className=\"india-choropleth__status\" role={loadError ? \"alert\" : \"status\"}>\n {loadError ? loadError.message : loadingState ? \"Loading districts…\" : \"District data is unavailable for this state.\"}\n </div>\n ) : regions.length === 0 ? (\n <div className=\"india-choropleth__status\" role=\"status\">No district data is available for this state.</div>\n ) : (\n <svg\n className={`india-choropleth__svg${filterBucket !== null ? \" india-choropleth__svg--filtered\" : \"\"}`}\n viewBox={`0 0 ${VIEWBOX.width} ${VIEWBOX.height}`}\n role=\"group\"\n aria-label={ariaLabel}\n onKeyDown={interactive ? (event) => { if (event.key === \"Escape\") { event.preventDefault(); inspect(null); } } : undefined}\n onClick={interactive ? handleBackgroundClick : undefined}\n >\n {visibleReferenceRegions.length > 0 ? (\n <>\n <defs>\n <pattern id={hatchId} width=\"8\" height=\"8\" patternUnits=\"userSpaceOnUse\" patternTransform=\"rotate(45)\">\n <rect width=\"8\" height=\"8\" fill=\"var(--india-map-reference-bg)\" />\n <line x1=\"0\" y1=\"0\" x2=\"0\" y2=\"8\" stroke=\"var(--india-map-reference-hatch)\" strokeWidth=\"2\" />\n </pattern>\n </defs>\n <g className=\"india-choropleth__reference-fill\" role=\"group\" aria-label=\"Non-statistical reference context.\">\n {visibleReferenceRegions.map((region) => (\n <path\n key={region.id}\n d={region.path}\n fill={referenceOverlayFill === \"solid\" ? \"var(--india-map-reference-bg)\" : `url(#${hatchId})`}\n aria-label={`${region.label}.${region.description ? ` ${region.description}` : \"\"}`}\n role=\"img\"\n />\n ))}\n </g>\n </>\n ) : null}\n {/* Hit areas for scattered regions, first so that every real outline is\n painted on top of them: the hull spanning Lakshadweep's islands is\n open sea, but Puducherry's spans the Tamil Nadu coast, and a hull\n must never take a pointer from a region that is actually there.\n Not focusable — keyboard focus has no coordinates, so the one tab\n stop stays on the region itself. */}\n {interactive ? (\n <g className=\"india-choropleth__hit-areas\" aria-hidden=\"true\">\n {regions.filter((region) => region.hitPath).map((region) => (\n <path\n key={region.id}\n d={region.hitPath!}\n fill=\"none\"\n pointerEvents=\"all\"\n tabIndex={-1}\n onMouseEnter={() => inspect(region)}\n onMouseLeave={() => inspect(null)}\n onClick={() => activate(region)}\n />\n ))}\n </g>\n ) : null}\n {regions.map((region) => {\n const isInspected = region.id === inspected?.id;\n const isSelected = region.id === selected?.id;\n const action = canDrill ? \"Activate to view districts.\" : \"Activate to select.\";\n const textValue = region.value === null ? \"No data\" : formatValue(region.value);\n return (\n <path\n key={region.id}\n className={`india-choropleth__region${isInspected ? \" india-choropleth__region--inspected\" : \"\"}${isSelected ? \" india-choropleth__region--selected\" : \"\"}${mergedReferenceIds.has(region.id) ? \" india-choropleth__region--reference-merged\" : \"\"}${isDimmed(region.id) ? \" india-choropleth__dimmed\" : \"\"}`}\n d={region.path}\n fill={colorFor(region.value, region, min, max, colorScale)}\n tabIndex={interactive ? 0 : -1}\n role={interactive ? \"button\" : undefined}\n aria-label={`${region.label}, ${textValue}. ${action}`}\n aria-pressed={interactive ? isSelected : undefined}\n aria-describedby={isInspected ? tooltipId : undefined}\n ref={(element) => { pathRefs.current[region.id] = element; }}\n onMouseEnter={interactive ? () => inspect(region) : undefined}\n // The canvas is much wider than the drawn map, so leaving a region\n // usually lands on blank canvas rather than leaving the canvas at all.\n // Without this the last-hovered tooltip stays pinned indefinitely.\n // Moving straight to a sibling region dispatches this leave and that\n // region's enter from the same native event, so the tooltip switches\n // in one batch instead of blanking.\n onMouseLeave={interactive ? () => inspect(null) : undefined}\n onFocus={interactive ? () => inspect(region) : undefined}\n // Tabbing to a sibling region re-inspects it synchronously right after\n // this fires, so clearing here only matters when focus leaves the map.\n onBlur={interactive ? () => inspect(null) : undefined}\n onClick={interactive ? () => activate(region) : undefined}\n onKeyDown={interactive ? (event) => {\n if (event.key === \"Enter\" || event.key === \" \") { event.preventDefault(); activate(region); }\n } : undefined}\n />\n );\n })}\n {/* A region whose largest part is smaller than the marker is invisible at\n this scale — Puducherry's enclaves are a couple of units — so it gets\n a dot in its own colour instead of nothing at all. The dot takes the\n pointer as well: it is painted over whatever is beneath it, so it is\n what the reader sees and aims at, and the outline it stands in for is\n too small to hover. Focus stays on the region path, its one tab stop. */}\n {smallMarkers.length > 0 ? (\n <g className=\"india-choropleth__small-markers\" aria-hidden=\"true\">\n {smallMarkers.map((region) => (\n <circle\n key={region.id}\n className={isDimmed(region.id) ? \"india-choropleth__dimmed\" : undefined}\n cx={region.centroid[0]}\n cy={region.centroid[1]}\n r={MIN_REGION_MARKER_SIZE / 2}\n fill={colorFor(region.value, region, min, max, colorScale)}\n onMouseEnter={interactive ? () => inspect(region) : undefined}\n onMouseLeave={interactive ? () => inspect(null) : undefined}\n onClick={interactive ? () => activate(region) : undefined}\n />\n ))}\n </g>\n ) : null}\n {showRegionValues ? (\n <>\n <g className=\"india-choropleth__value-leaders\" aria-hidden=\"true\">\n {valuePlacements.filter((p) => p.leader).map((p) => (\n <line\n key={p.region.id}\n className={isDimmed(p.region.id) ? \"india-choropleth__dimmed\" : undefined}\n x1={p.leader![0][0]} y1={p.leader![0][1]} x2={p.leader![1][0]} y2={p.leader![1][1]}\n />\n ))}\n </g>\n <g className={`india-choropleth__region-values${level === \"district\" ? \" india-choropleth__region-values--district\" : \"\"}`} aria-hidden=\"true\">\n {valuePlacements.map((p) => (\n <text\n key={p.region.id}\n className={isDimmed(p.region.id) ? \"india-choropleth__dimmed\" : undefined}\n x={p.at[0]} y={p.at[1]} textAnchor=\"middle\" dominantBaseline=\"central\"\n >\n {p.text}\n </text>\n ))}\n </g>\n </>\n ) : null}\n {visibleReferenceRegions.length > 0 ? (\n <g className=\"india-choropleth__reference-outline\" aria-hidden=\"true\">\n {visibleReferenceRegions.map((region) => <path key={region.id} d={region.path} fill=\"none\" />)}\n </g>\n ) : null}\n {/* Selection is drawn as a ring on top of everything, rather than by recoloring\n the region: on a choropleth the fill *is* the data, so overwriting it made the\n selected region's color stop meaning anything. Hovering a region lifts it 2px,\n so the ring matches that lift when the selected region is also the inspected\n one — otherwise the fill slides out from under its own outline. */}\n {selected ? (\n <g\n className={`india-choropleth__selection${selected.id === inspected?.id ? \" india-choropleth__selection--lifted\" : \"\"}`}\n aria-hidden=\"true\"\n >\n <path className=\"india-choropleth__selection-halo\" d={selected.path} fill=\"none\" />\n <path className=\"india-choropleth__selection-ring\" d={selected.path} fill=\"none\" />\n </g>\n ) : null}\n </svg>\n )}\n {tooltipContext && regions.length > 0 ? (\n <div ref={tooltipAnchorRef} className=\"india-choropleth__tooltip-anchor\" style={{ left: `${(inspected!.centroid[0] / VIEWBOX.width) * 100}%`, top: `${(inspected!.centroid[1] / VIEWBOX.height) * 100}%` }}>\n {/* Deliberately not `role=\"status\"`. As a live region it re-announced the whole\n tooltip on every hover *and* every focus move, and the content is richer now.\n The region aria-label already carries label + value, and `aria-describedby`\n reads this box out on focus — once, on demand. */}\n <div id={tooltipId} className=\"india-choropleth__tooltip\">\n {renderTooltip ? renderTooltip(tooltipContext) : defaultTooltip(tooltipContext, formatValue)}\n </div>\n </div>\n ) : null}\n </div>\n {showLegend ? (\n <div\n className=\"india-choropleth__legend\"\n role=\"group\"\n aria-label={`Color scale: ${legendLabels[0]} to ${legendLabels[1]} values`}\n onKeyDown={interactive ? (event) => { if (event.key === \"Escape\") { event.preventDefault(); setActiveBucket(null); } } : undefined}\n >\n <span>{legendLabels[0]}</span><div className=\"india-choropleth__swatches\" aria-hidden={interactive ? undefined : true}>\n {/* Each swatch filters the map to its own band. An empty band is left\n as a swatch you can still read — silently skipping it would hide\n the fact that the ramp has a gap there — but it does nothing,\n because filtering to nothing just dims the whole map. */}\n {buckets.map((bucket) => interactive ? (\n <button\n key={bucket.index}\n type=\"button\"\n className={`india-choropleth__swatch${filterBucket === bucket.index ? \" india-choropleth__swatch--active\" : \"\"}${filterBucket !== null && filterBucket !== bucket.index ? \" india-choropleth__swatch--muted\" : \"\"}`}\n style={{ backgroundColor: bucket.color }}\n aria-pressed={filterBucket === bucket.index}\n aria-disabled={bucket.matches === 0 ? true : undefined}\n aria-label={legendBucketLabel(bucket, formatValue)}\n // Sighted readers get the same sentence the accessible name carries,\n // which is the only place an empty band explains itself now that it\n // is no longer faded.\n title={legendBucketLabel(bucket, formatValue)}\n onClick={() => {\n if (bucket.matches === 0) return;\n setActiveBucket(filterBucket === bucket.index ? null : bucket.index);\n }}\n />\n ) : (\n <i className=\"india-choropleth__swatch\" key={bucket.index} style={{ backgroundColor: bucket.color }} />\n ))}\n </div><span>{legendLabels[1]}</span>\n {visibleReferenceRegions.length > 0 ? <><i className={`india-choropleth__reference-key${referenceOverlayFill === \"solid\" ? \" india-choropleth__reference-key--solid\" : \"\"}`} aria-hidden=\"true\" /><span>{referenceOverlayLegendLabel}</span></> : null}\n </div>\n ) : null}\n {renderInsights ? <aside className=\"india-choropleth__insights\" aria-live=\"polite\">{renderInsights(insightContext)}</aside> : null}\n </section>\n );\n}\n","import { feature as topoFeature } from \"topojson-client\";\nimport type { GeometryObject } from \"topojson-specification\";\nimport type { GeometrySource, MapFeatureCollection } from \"./types\";\n\nexport function asFeatureCollection(source: GeometrySource): MapFeatureCollection {\n if (\"type\" in source && source.type === \"FeatureCollection\") {\n return source;\n }\n\n const topoSource = source as Exclude<GeometrySource, MapFeatureCollection>;\n\n const object = (typeof topoSource.object === \"string\"\n ? topoSource.topology.objects[topoSource.object]!\n : topoSource.object) as unknown as GeometryObject;\n if (!object) {\n throw new Error(\"The named TopoJSON object does not exist in this topology.\");\n }\n\n const unpacked = topoFeature(topoSource.topology, object);\n return unpacked.type === \"FeatureCollection\"\n ? (unpacked as MapFeatureCollection)\n : { type: \"FeatureCollection\", features: [unpacked] };\n}\n\nexport function totalOf(values: readonly (number | null)[]) {\n return values.reduce<number>((total, value) => total + (value ?? 0), 0);\n}\n","/**\n * The legend, as a filter.\n *\n * Every value on the map is painted from one of the ramp's colours. That makes\n * the legend a ready-made set of value bands, and picking a band is the\n * question a reader of a choropleth actually has: *which regions are the dark\n * ones?* These helpers answer it — which swatch a value belongs to, what each\n * swatch stands for, and how many regions land there.\n *\n * [swatchIndexOf] is the single definition of that mapping: the renderers pick\n * a region's fill with it too, so \"highlight the regions painted in this\n * colour\" is true by construction rather than by two formulas agreeing.\n *\n * Pure functions of values, so the DOM renderer and the React component share\n * one definition of the behaviour.\n */\n\n/** One swatch: the colour, and what the map actually has in it. */\nexport interface LegendBucket {\n index: number;\n color: string;\n /**\n * Lowest and highest value that lands here, or null when nothing does.\n *\n * The band the ramp *nominally* covers is a half-step either side of the\n * swatch's own stop, which lands on numbers like \"24.333 to 31\" that appear\n * nowhere in the data. What a reader wants to know is what picking this\n * swatch will give them, so the range is measured from the regions in it.\n */\n from: number | null;\n to: number | null;\n /** How many regions land here. Zero means the swatch would filter to nothing. */\n matches: number;\n}\n\n/**\n * Which swatch a value is painted from, or null when there is nothing to paint\n * — no value, or no ramp to paint it with.\n *\n * A ramp of one colour, or data with no spread at all, collapses to the top\n * swatch: there is a single band and every value is in it.\n */\nexport function swatchIndexOf(value: number | null, min: number, max: number, count: number): number | null {\n if (value === null || count <= 0) return null;\n if (count === 1 || max === min) return count - 1;\n const index = Math.round(((value - min) / (max - min)) * (count - 1));\n return Math.min(Math.max(index, 0), count - 1);\n}\n\n/**\n * The ramp described swatch by swatch.\n *\n * With a function colour scale the renderers show the default ramp, because a\n * function has no swatches to show. The bands are still the honest reading of\n * \"lower to higher\"; the swatch colour just isn't any region's actual fill.\n */\nexport function legendBuckets(\n colors: readonly string[],\n values: readonly (number | null)[],\n min: number,\n max: number,\n): LegendBucket[] {\n const count = colors.length;\n return colors.map((color, index) => {\n const members = values.filter((value): value is number => swatchIndexOf(value, min, max, count) === index);\n return {\n index,\n color,\n from: members.length ? Math.min(...members) : null,\n to: members.length ? Math.max(...members) : null,\n matches: members.length,\n };\n });\n}\n\n/**\n * What a swatch does, in words — the accessible name for its control, since the\n * colour itself carries the meaning and a screen reader cannot see it.\n */\nexport function legendBucketLabel(bucket: LegendBucket, formatValue: (value: number) => string): string {\n if (bucket.matches === 0 || bucket.from === null || bucket.to === null) return \"No regions in this band\";\n const range = bucket.from === bucket.to\n ? formatValue(bucket.from)\n : `${formatValue(bucket.from)} to ${formatValue(bucket.to)}`;\n return `Highlight ${bucket.matches} ${bucket.matches === 1 ? \"region\" : \"regions\"}, ${range}`;\n}\n\n","/**\n * Edge handling for the centroid-anchored tooltip.\n *\n * The tooltip is anchored to the region's centroid rather than the cursor on\n * purpose: hover and keyboard focus run through the same inspect path, and a\n * focused region has no cursor position to follow. That anchor still has to be\n * nudged so the box never leaves the map — a state at the top of the map (Jammu\n * & Kashmir on a narrow screen) would otherwise render *above* the map, over\n * whatever the host page put there.\n *\n * Kept as a pure function of two rects so both the DOM renderer and the React\n * component can share the same math.\n */\n\nexport interface Rect {\n left: number;\n right: number;\n top: number;\n bottom: number;\n width: number;\n height: number;\n}\n\nexport interface TooltipPlacement {\n /** Horizontal correction in px, applied on top of the centering translate. */\n dx: number;\n /** Which side of the centroid the tooltip sits on. */\n side: \"above\" | \"below\";\n}\n\n/**\n * @param tooltip The tooltip's rect as currently laid out (centered above the centroid).\n * @param bounds The area the tooltip must stay inside — the map canvas.\n * @param gap Space between the centroid and the tooltip edge, in px.\n * @param padding Minimum breathing room between the tooltip and the bounds edge, in px.\n */\nexport function placeTooltip(tooltip: Rect, bounds: Rect, gap: number, padding = 4): TooltipPlacement {\n let dx = 0;\n // A tooltip wider than the space available can't satisfy both edges; favor the\n // left one so the region name (which leads the content) stays readable.\n if (tooltip.right > bounds.right - padding) dx = bounds.right - padding - tooltip.right;\n if (tooltip.left + dx < bounds.left + padding) dx = bounds.left + padding - tooltip.left;\n\n // Flip below only when there genuinely isn't room above. The flipped position\n // is `gap` below the centroid, which is `height + 2 * gap` further down.\n const side: TooltipPlacement[\"side\"] = tooltip.top < bounds.top + padding ? \"below\" : \"above\";\n\n return { dx, side };\n}\n","/**\n * Geometry for regions that are too small to use normally.\n *\n * Goa is a few view-box units across, Puducherry is scattered enclaves, and\n * Lakshadweep's islands are a fraction of a pixel at national scale. Left alone\n * they are invisible, unclickable, and their value labels either don't fit or\n * land on a neighbour. These helpers back the three fixes for that: a marker so\n * they can be seen, a click buffer so they can be hit, and label placement that\n * moves a number into open space beside the region.\n *\n * Pure functions of projected coordinates, so the DOM renderer, the React\n * component and the Dart port can all share one definition of the behaviour.\n */\n\nexport type Point = readonly [number, number];\n\n/** Axis-aligned box as `[minX, minY, maxX, maxY]`. */\nexport type Box = readonly [number, number, number, number];\n\n/**\n * Regions kept at their true size even when they are small enough to qualify\n * for exaggeration.\n *\n * Exaggeration assumes a region has room to grow into. Puducherry does not: it\n * is four coastal enclaves *inside* Tamil Nadu, so growing them to the\n * visibility threshold pushes each one several units into the state around it,\n * and the map ends up showing a Puducherry that is the wrong shape in the wrong\n * place. Lakshadweep's islands grow into open sea, where nothing is displaced,\n * which is the case the feature was built for.\n *\n * Matched on the id containing the name because host data brings its own id\n * scheme; the bundled layer uses `in-cs-34-puducherry`. A district id inside the\n * UT matches too, which is a no-op: drilled into, its districts fill the map and\n * are far past the threshold that would have grown them.\n */\nconst TRUE_GEOMETRY_REGIONS = [\"puducherry\"];\n\nexport function keepsTrueGeometry(id: string): boolean {\n const normalized = id.toLowerCase();\n return TRUE_GEOMETRY_REGIONS.some((name) => normalized.includes(name));\n}\n\nexport function boundsOfRing(ring: readonly Point[]): Box {\n let minX = Infinity;\n let minY = Infinity;\n let maxX = -Infinity;\n let maxY = -Infinity;\n for (const [x, y] of ring) {\n if (x < minX) minX = x;\n if (x > maxX) maxX = x;\n if (y < minY) minY = y;\n if (y > maxY) maxY = y;\n }\n return [minX, minY, maxX, maxY];\n}\n\n/**\n * Longest side of the largest ring's box. This is the measure of \"small\", not\n * the feature's overall bounds: an island group's bounds can be large while\n * every island in it is sub-pixel.\n */\nexport function largestRingExtent(rings: readonly (readonly Point[])[]): number {\n let largest = 0;\n for (const ring of rings) {\n if (ring.length === 0) continue;\n const [minX, minY, maxX, maxY] = boundsOfRing(ring);\n const extent = Math.max(maxX - minX, maxY - minY);\n if (extent > largest) largest = extent;\n }\n return largest;\n}\n\nfunction signedArea(ring: readonly Point[]): number {\n let total = 0;\n for (let i = 0; i < ring.length; i++) {\n const [ax, ay] = ring[i]!;\n const [bx, by] = ring[(i + 1) % ring.length]!;\n total += ax * by - bx * ay;\n }\n return total / 2;\n}\n\n/**\n * Area-weighted centroid of the largest ring.\n *\n * Deliberately not the whole feature's centroid: averaging across parts pulls\n * Andaman & Nicobar's label out to sea between its islands, and Gujarat's off\n * its own coast.\n */\nexport function labelPointFor(rings: readonly (readonly Point[])[], fallback: Point): Point {\n let largest: readonly Point[] | null = null;\n let largestArea = 0;\n for (const ring of rings) {\n if (ring.length < 3) continue;\n const area = Math.abs(signedArea(ring));\n if (area > largestArea) {\n largestArea = area;\n largest = ring;\n }\n }\n if (!largest || largestArea === 0) return fallback;\n\n const area = signedArea(largest);\n let cx = 0;\n let cy = 0;\n for (let i = 0; i < largest.length; i++) {\n const [ax, ay] = largest[i]!;\n const [bx, by] = largest[(i + 1) % largest.length]!;\n const cross = ax * by - bx * ay;\n cx += (ax + bx) * cross;\n cy += (ay + by) * cross;\n }\n const centroid: Point = [cx / (6 * area), cy / (6 * area)];\n return Number.isFinite(centroid[0]) && Number.isFinite(centroid[1]) ? centroid : fallback;\n}\n\n/** Distance from a point to the nearest edge of a box; zero when inside it. */\nexport function distanceToBox(point: Point, box: Box): number {\n const [x, y] = point;\n const [minX, minY, maxX, maxY] = box;\n const dx = Math.max(minX - x, 0) + Math.max(x - maxX, 0);\n const dy = Math.max(minY - y, 0) + Math.max(y - maxY, 0);\n return Math.hypot(dx, dy);\n}\n\n/** Distance to the nearest of a region's parts. */\nexport function distanceToParts(point: Point, parts: readonly Box[]): number {\n let nearest = Infinity;\n for (const part of parts) {\n const distance = distanceToBox(point, part);\n if (distance < nearest) nearest = distance;\n }\n return nearest;\n}\n\n/**\n * Angles to try when placing a small region's label, as turns from the\n * away-from-centre direction: straight out first, then progressively to either\n * side, and back inward only as a last resort.\n *\n * One fixed direction is not enough. Puducherry sits south-east of the map's\n * middle, so the radial direction runs inland into Tamil Nadu while its open\n * water is due east.\n */\nconst LABEL_SEARCH_TURNS = [\n 0,\n Math.PI / 6, -Math.PI / 6,\n Math.PI / 3, -Math.PI / 3,\n Math.PI / 2, -Math.PI / 2,\n (2 * Math.PI) / 3, -(2 * Math.PI) / 3,\n (5 * Math.PI) / 6, -(5 * Math.PI) / 6,\n Math.PI,\n] as const;\n\nexport interface OutsideLabelOptions {\n /** Where the region is, in view-box units. */\n anchor: Point;\n /** How far the label must clear the region itself. */\n clearance: number;\n /** Half the label's width and height, used to keep it inside the view box. */\n halfSize: Point;\n /** The view box the label must stay within, as `[width, height]`. */\n viewBox: Point;\n /** Direction is measured away from this point — normally the view box's middle. */\n centre: Point;\n /** Returns true when a label centred here would cover another region. */\n isBlocked: (candidate: Point) => boolean;\n}\n\n/**\n * Find a clear spot beside a region for its value label, or null when the\n * region is hemmed in on every side (Delhi, ringed by other states) and the\n * label is better left where it was.\n */\nexport function placeOutsideLabel(options: OutsideLabelOptions): Point | null {\n const { anchor, clearance, halfSize, viewBox, centre, isBlocked } = options;\n const dx = anchor[0] - centre[0];\n const dy = anchor[1] - centre[1];\n const base = dx === 0 && dy === 0 ? 0 : Math.atan2(dy, dx);\n\n for (const turn of LABEL_SEARCH_TURNS) {\n const angle = base + turn;\n const candidate: Point = [\n clamp(anchor[0] + Math.cos(angle) * clearance, halfSize[0], viewBox[0] - halfSize[0]),\n clamp(anchor[1] + Math.sin(angle) * clearance, halfSize[1], viewBox[1] - halfSize[1]),\n ];\n if (!isBlocked(candidate)) return candidate;\n }\n return null;\n}\n\nfunction clamp(value: number, lower: number, upper: number): number {\n return Math.min(Math.max(value, lower), upper);\n}\n\n/**\n * Grow a feature whose whole geometry is too small to see, about each part's\n * own centre.\n *\n * Two guards make this safe, and both were learned the hard way:\n *\n * 1. It applies only when the feature's *largest* part is under [minExtent] —\n * that is, the whole region is tiny. West Bengal is a large state whose\n * Sundarbans delta is fourteen small islets; enlarging those individually\n * blew them up into an overlapping mess across the river mouth. A region is\n * either small enough to need help or it is not.\n * 2. Once a feature qualifies, each part grows about its own centre until it is\n * visible. This preserves every part's location and aspect ratio instead of\n * turning a dispersed archipelago into one large, misplaced blob. The cap\n * keeps the tiniest specks from reading as real landmass.\n *\n * Growth is capped by [maxScale] so a speck never reads as a real landmass.\n */\nexport function enlargeSmallParts(\n rings: readonly (readonly Point[])[],\n minExtent: number,\n maxScale = 8,\n): Point[][] {\n const unchanged = () => rings.map((ring) => [...ring]);\n if (minExtent <= 0) return unchanged();\n // Only a feature that is small *as a whole* qualifies.\n const largest = largestRingExtent(rings);\n if (largest <= 0 || largest >= minExtent) return unchanged();\n\n return rings.map((ring) => {\n const [minX, minY, maxX, maxY] = boundsOfRing(ring);\n const extent = Math.max(maxX - minX, maxY - minY);\n const scale = extent <= 0 ? 1 : Math.min(minExtent / extent, maxScale);\n const cx = (minX + maxX) / 2;\n const cy = (minY + maxY) / 2;\n return ring.map(([x, y]) => [cx + (x - cx) * scale, cy + (y - cy) * scale] as Point);\n });\n}\n\n/**\n * Convex hull of a set of points, as one closed ring (Andrew's monotone chain).\n * Returns the input when there is nothing to wrap — fewer than three distinct\n * points, or all of them collinear.\n */\nexport function convexHull(points: readonly Point[]): Point[] {\n const sorted = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]);\n const unique: Point[] = [];\n for (const point of sorted) {\n const prior = unique.at(-1);\n if (!prior || prior[0] !== point[0] || prior[1] !== point[1]) unique.push(point);\n }\n if (unique.length < 3) return unique.map((point) => [...point] as Point);\n\n const turn = (o: Point, a: Point, b: Point) =>\n (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);\n const half = (ordered: readonly Point[]) => {\n const chain: Point[] = [];\n for (const point of ordered) {\n while (chain.length >= 2 && turn(chain[chain.length - 2]!, chain[chain.length - 1]!, point) <= 0) chain.pop();\n chain.push(point);\n }\n return chain;\n };\n\n const lower = half(unique);\n const upper = half([...unique].reverse());\n // Each chain repeats the other's first point, so drop both endpoints once.\n const hull = [...lower.slice(0, -1), ...upper.slice(0, -1)];\n return hull.length >= 3 ? hull : unique.map((point) => [...point] as Point);\n}\n\n/**\n * One continuous hit area for a region scattered across separate parts: the\n * convex hull of everything it is drawn as.\n *\n * Lakshadweep is twenty specks in open sea. Even exaggerated they are a poor\n * pointer target, and the water they enclose is how the group reads on the map,\n * so treating that water as part of the region is what a reader expects. The\n * hull is safe to be generous with only because every renderer tests it *after*\n * every real outline has missed, so it can never take a hover from a neighbour\n * it happens to span.\n *\n * Null when the region needs no help: a single part, or already big enough to\n * point at directly ([maxExtent], the same \"too small to use\" threshold the\n * click buffer and value labels work from).\n */\nexport function scatteredHitArea(\n rings: readonly (readonly Point[])[],\n maxExtent: number,\n): Point[] | null {\n if (rings.length < 2 || maxExtent <= 0) return null;\n const extent = largestRingExtent(rings);\n if (extent <= 0 || extent >= maxExtent) return null;\n const hull = convexHull(rings.flat());\n return hull.length >= 3 ? hull : null;\n}\n\n/** An SVG path string for a set of rings, used when exaggeration has moved them. */\nexport function ringsToPath(rings: readonly (readonly Point[])[]): string {\n let d = \"\";\n for (const ring of rings) {\n if (ring.length === 0) continue;\n d += `M${ring[0]![0]},${ring[0]![1]}`;\n for (let i = 1; i < ring.length; i++) d += `L${ring[i]![0]},${ring[i]![1]}`;\n d += \"Z\";\n }\n return d;\n}\n","import { useCallback, useState } from \"react\";\n\nexport function useControllableState<T>(controlled: T | undefined, initial: T) {\n const [uncontrolled, setUncontrolled] = useState<T>(initial);\n const value = controlled === undefined ? uncontrolled : controlled;\n const setValue = useCallback((next: T) => {\n if (controlled === undefined) setUncontrolled(next);\n }, [controlled]);\n return [value, setValue] as const;\n}\n"],"mappings":"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","names":["useCallback","useState","area","useState","useCallback"]}
|
package/dist/style.css
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
.india-choropleth {
|
|
2
|
+
--india-map-stroke: #fff;
|
|
3
|
+
--india-map-stroke-active: #0f1b38;
|
|
4
|
+
/* Border widths, in screen pixels (the strokes are non-scaling). Override any
|
|
5
|
+
of these to retheme without touching the renderer. */
|
|
6
|
+
--india-map-border-width: 2.5;
|
|
7
|
+
--india-map-border-width-active: 2;
|
|
8
|
+
--india-map-selection-width: 3;
|
|
9
|
+
--india-map-small-marker-stroke: #53617b;
|
|
10
|
+
--india-map-focus: #0f766e;
|
|
11
|
+
--india-map-active: #ff725f;
|
|
12
|
+
--india-map-empty: #e7edf0;
|
|
13
|
+
--india-map-tooltip-bg: #fff;
|
|
14
|
+
--india-map-tooltip-border: #b8c2ce;
|
|
15
|
+
--india-map-text: #081435;
|
|
16
|
+
--india-map-muted: #53617b;
|
|
17
|
+
--india-map-line: #dce3e7;
|
|
18
|
+
--india-map-reference-bg: #eef1f3;
|
|
19
|
+
--india-map-reference-hatch: #9ba8af;
|
|
20
|
+
--india-map-reference-stroke: #6f7d85;
|
|
21
|
+
position: relative;
|
|
22
|
+
color: var(--india-map-text);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.india-choropleth__toolbar { min-height: 40px; display: flex; align-items: center; justify-content: flex-end; }
|
|
26
|
+
.india-choropleth__breadcrumb { display: flex; align-items: center; gap: .5rem; font: 500 .875rem/1.3 system-ui, sans-serif; }
|
|
27
|
+
.india-choropleth__back { border: 0; padding: .375rem; background: transparent; color: var(--india-map-focus); font: inherit; cursor: pointer; }
|
|
28
|
+
.india-choropleth__back:hover { text-decoration: underline; }
|
|
29
|
+
.india-choropleth__back:focus-visible { outline: 3px solid color-mix(in srgb, var(--india-map-focus) 35%, transparent); outline-offset: 2px; }
|
|
30
|
+
.india-choropleth__canvas { position: relative; width: 100%; }
|
|
31
|
+
.india-choropleth__status { min-height: 16rem; padding: 2rem; border: 1px dashed var(--india-map-line); display: grid; place-items: center; color: var(--india-map-muted); font: 500 .9375rem/1.5 system-ui, sans-serif; text-align: center; }
|
|
32
|
+
.india-choropleth__svg { display: block; width: 100%; height: auto; overflow: visible; }
|
|
33
|
+
.india-choropleth__region { stroke: var(--india-map-stroke); stroke-width: var(--india-map-border-width); stroke-linejoin: round; vector-effect: non-scaling-stroke; cursor: pointer; -webkit-tap-highlight-color: transparent; transition: filter 160ms ease, opacity 160ms ease, stroke 160ms ease, stroke-width 160ms ease, transform 160ms ease; transform-box: fill-box; transform-origin: center; }
|
|
34
|
+
/* An SVG outline draws a rectangle around the path's bounding box, not its actual
|
|
35
|
+
shape. Suppress it here (both our own prior rule and the browser default) and
|
|
36
|
+
rely on the shape-following stroke/shadow/lift below as the focus indicator.
|
|
37
|
+
`:focus` and `:focus-visible` are separate rules (not a comma list) so a browser
|
|
38
|
+
that doesn't parse `:focus-visible` still applies the plain `:focus` one instead
|
|
39
|
+
of silently dropping the whole rule and falling back to its own default ring. */
|
|
40
|
+
.india-choropleth__region:focus { outline: none; }
|
|
41
|
+
.india-choropleth__region:focus-visible { outline: none; }
|
|
42
|
+
.india-choropleth__region--reference-merged { stroke: transparent; }
|
|
43
|
+
.india-choropleth--static .india-choropleth__region { cursor: default; }
|
|
44
|
+
.india-choropleth__region:is(:hover, :focus-visible), .india-choropleth__region--inspected { stroke: var(--india-map-stroke-active); stroke-width: var(--india-map-border-width-active); filter: drop-shadow(0 5px 4px rgb(8 20 53 / .14)); transform: translateY(-2px); }
|
|
45
|
+
/* Selection is a ring, never a fill: on a choropleth the fill carries the value,
|
|
46
|
+
so recoloring the picked region made its color stop meaning anything. The halo
|
|
47
|
+
sits under the ring so the accent stays legible on pale and saturated fills alike. */
|
|
48
|
+
.india-choropleth__selection { pointer-events: none; transition: transform 160ms ease; }
|
|
49
|
+
.india-choropleth__selection--lifted { transform: translateY(-2px); }
|
|
50
|
+
.india-choropleth__selection-halo { stroke: var(--india-map-stroke); stroke-width: calc(var(--india-map-selection-width) * 2); stroke-linejoin: round; vector-effect: non-scaling-stroke; }
|
|
51
|
+
.india-choropleth__selection-ring { stroke: var(--india-map-active); stroke-width: var(--india-map-selection-width); stroke-linejoin: round; vector-effect: non-scaling-stroke; }
|
|
52
|
+
.india-choropleth__reference-fill path { pointer-events: none; stroke: var(--india-map-reference-stroke); stroke-width: 1; vector-effect: non-scaling-stroke; }
|
|
53
|
+
.india-choropleth__reference-outline path { pointer-events: none; stroke: var(--india-map-reference-stroke); stroke-width: 1.6; stroke-dasharray: 4 3; vector-effect: non-scaling-stroke; }
|
|
54
|
+
/* Dulled by a legend filter — the regions outside the picked band, plus their
|
|
55
|
+
markers, values and leader lines. Dulled rather than hidden, so the map keeps
|
|
56
|
+
its shape and a dimmed region can still be hovered for its number. The
|
|
57
|
+
reference overlay is left alone: it is non-statistical context, in no band.
|
|
58
|
+
|
|
59
|
+
Greyed as well as faded, because fading alone does not separate the pale end
|
|
60
|
+
of a ramp: a dark fill at low opacity lands on roughly the same light grey as
|
|
61
|
+
a pale fill at full opacity, so picking the lightest band left the map reading
|
|
62
|
+
as though nothing had been picked. Draining the colour instead makes "in the
|
|
63
|
+
band" mean "still coloured", whichever band it is. */
|
|
64
|
+
.india-choropleth__dimmed { opacity: .25; filter: grayscale(1); }
|
|
65
|
+
/* The region's own hover rule outranks this one on `filter`, so pointing at a
|
|
66
|
+
dulled region brings its colour back along with the usual lift. */
|
|
67
|
+
.india-choropleth__region.india-choropleth__dimmed:is(:hover, :focus-visible) { opacity: .6; }
|
|
68
|
+
/* And the survivors are lifted off the page, so the picked band reads as a group
|
|
69
|
+
even where its own colour is nearly white. A shadow rather than a heavier
|
|
70
|
+
outline: strokes here are non-scaling, so a 2px dark edge around one of
|
|
71
|
+
Lakshadweep's one-unit islands swallows the island whole. */
|
|
72
|
+
.india-choropleth__svg--filtered .india-choropleth__region:not(.india-choropleth__dimmed) { filter: drop-shadow(0 2px 3px rgb(8 20 53 / .28)); }
|
|
73
|
+
/* The water an island group encloses, hoverable as part of the group. Invisible
|
|
74
|
+
by design and painted under every region, so it only ever picks up a pointer
|
|
75
|
+
that would otherwise have hit nothing. */
|
|
76
|
+
.india-choropleth__hit-areas path { cursor: pointer; }
|
|
77
|
+
/* A dot standing in for a region too small to see at this scale (Puducherry's
|
|
78
|
+
enclaves). Filled with the region's own ramp colour by the renderer, so it
|
|
79
|
+
still reads as data rather than decoration — and hoverable, because the dot
|
|
80
|
+
is what the reader can see and aim at. */
|
|
81
|
+
.india-choropleth__small-markers circle { cursor: pointer; stroke: var(--india-map-small-marker-stroke); stroke-width: var(--india-map-border-width); vector-effect: non-scaling-stroke; }
|
|
82
|
+
.india-choropleth--static .india-choropleth__small-markers circle { pointer-events: none; cursor: default; }
|
|
83
|
+
/* Leader from a small region to its value label, which sits outside the shape. */
|
|
84
|
+
.india-choropleth__value-leaders line { pointer-events: none; stroke: var(--india-map-small-marker-stroke); stroke-width: 1; vector-effect: non-scaling-stroke; }
|
|
85
|
+
.india-choropleth__region-values { pointer-events: none; fill: var(--india-map-text); font: 700 11px/1 system-ui, sans-serif; paint-order: stroke; stroke: rgb(255 255 255 / .82); stroke-width: 3px; stroke-linejoin: round; }
|
|
86
|
+
.india-choropleth__region-values--district { font-size: 9px; }
|
|
87
|
+
/* --india-map-tooltip-dx/dy are set by the renderer to keep the box inside the map:
|
|
88
|
+
dx nudges it away from a left/right edge, dy flips it below a region near the top. */
|
|
89
|
+
.india-choropleth__tooltip-anchor { position: absolute; z-index: 1; pointer-events: none; transform: translate(calc(-50% + var(--india-map-tooltip-dx, 0px)), var(--india-map-tooltip-dy, calc(-100% - .75rem))); }
|
|
90
|
+
.india-choropleth__tooltip { min-width: 8rem; padding: .75rem .875rem; border: 1px solid var(--india-map-tooltip-border); border-radius: .625rem; display: grid; gap: .18rem; background: var(--india-map-tooltip-bg); box-shadow: 0 .5rem 1.25rem rgb(8 20 53 / .11); font: 500 .875rem/1.3 system-ui, sans-serif; }
|
|
91
|
+
.india-choropleth__tooltip strong { font-size: .875rem; }
|
|
92
|
+
.india-choropleth__tooltip b { font-size: 1.2rem; }
|
|
93
|
+
.india-choropleth__tooltip small { color: var(--india-map-muted); font-size: .75rem; }
|
|
94
|
+
.india-choropleth__tooltip-bar { display: block; height: .3rem; margin: .1rem 0 .15rem; border-radius: .3rem; background: var(--india-map-empty); overflow: hidden; }
|
|
95
|
+
.india-choropleth__tooltip-bar > span { display: block; height: 100%; border-radius: inherit; background: var(--india-map-active); }
|
|
96
|
+
.india-choropleth__legend { margin-top: .75rem; display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: .75rem; color: var(--india-map-muted); font: .8125rem/1.3 system-ui, sans-serif; }
|
|
97
|
+
.india-choropleth__swatches { display: flex; gap: .25rem; }
|
|
98
|
+
.india-choropleth__swatch { width: clamp(1.75rem, 7vw, 4.25rem); height: .75rem; border-radius: .1rem; }
|
|
99
|
+
/* On an interactive map each swatch filters to its own band. The bar stays the
|
|
100
|
+
same 12px it has always been; a transparent border above and below it lifts
|
|
101
|
+
the pointer and touch target to 24px, and the negative margin gives that
|
|
102
|
+
height back so the legend row is unchanged. Deliberately top and bottom only:
|
|
103
|
+
widening sideways too would push the row out and leave neighbouring targets
|
|
104
|
+
overlapping in the 4px gap between them, where a click is then decided by
|
|
105
|
+
paint order rather than by which swatch you aimed at. */
|
|
106
|
+
button.india-choropleth__swatch { box-sizing: content-box; margin: -.375rem 0; padding: 0; border: 0 solid transparent; border-top-width: .375rem; border-bottom-width: .375rem; background-clip: padding-box; cursor: pointer; appearance: none; transition: opacity 160ms ease, box-shadow 160ms ease; }
|
|
107
|
+
button.india-choropleth__swatch:focus-visible { outline: 3px solid color-mix(in srgb, var(--india-map-focus) 35%, transparent); outline-offset: 1px; }
|
|
108
|
+
/* A band no region falls in stays at full strength: the ramp is continuous, and
|
|
109
|
+
fading one stop out of the middle of it reads as a broken legend rather than
|
|
110
|
+
as an empty band. It is inert instead — default cursor, and a title saying so
|
|
111
|
+
— because filtering to nothing would just dim the whole map. */
|
|
112
|
+
button.india-choropleth__swatch[aria-disabled="true"] { cursor: default; }
|
|
113
|
+
/* The picked band, and the bands it is picked over. */
|
|
114
|
+
.india-choropleth__swatch--active { box-shadow: 0 0 0 2px var(--india-map-stroke), 0 0 0 4px var(--india-map-active); }
|
|
115
|
+
.india-choropleth__swatch--muted { opacity: .3; }
|
|
116
|
+
.india-choropleth__reference-key { width: 1rem; height: .75rem; border: 1px solid var(--india-map-reference-stroke); background: repeating-linear-gradient(45deg, var(--india-map-reference-bg) 0 3px, var(--india-map-reference-hatch) 3px 4px); }
|
|
117
|
+
.india-choropleth__reference-key--solid { background: var(--india-map-reference-bg); }
|
|
118
|
+
.india-choropleth__insights { margin-top: 1rem; }
|
|
119
|
+
@media (prefers-reduced-motion: reduce) { .india-choropleth__region, button.india-choropleth__swatch { transition: none; } }
|
|
120
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bharat-choropleth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Accessible, dependency-light SVG choropleths for India drill-down dashboards.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"choropleth",
|
|
7
|
+
"india",
|
|
8
|
+
"map",
|
|
9
|
+
"react",
|
|
10
|
+
"svg",
|
|
11
|
+
"topojson"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/shashankbudem/bharat-choropleth.git",
|
|
20
|
+
"directory": "packages/react"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/shashankbudem/bharat-choropleth#readme",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/shashankbudem/bharat-choropleth/issues"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"sideEffects": ["./dist/style.css"],
|
|
28
|
+
"files": ["dist"],
|
|
29
|
+
"main": "./dist/index.js",
|
|
30
|
+
"module": "./dist/index.js",
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"exports": {
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
|
+
"import": "./dist/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./style.css": "./dist/style.css"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"react": "^18.2.0 || ^19.0.0",
|
|
41
|
+
"react-dom": "^18.2.0 || ^19.0.0"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"d3-geo": "^3.1.1",
|
|
45
|
+
"topojson-client": "^3.1.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
49
|
+
"@testing-library/react": "^16.1.0",
|
|
50
|
+
"@types/d3-geo": "^3.1.0",
|
|
51
|
+
"@types/geojson": "^7946.0.16",
|
|
52
|
+
"@types/react": "^19.0.0",
|
|
53
|
+
"@types/react-dom": "^19.0.0",
|
|
54
|
+
"@types/topojson-client": "^3.1.5",
|
|
55
|
+
"@types/topojson-specification": "^1.0.2",
|
|
56
|
+
"jsdom": "^25.0.1",
|
|
57
|
+
"react": "^19.0.0",
|
|
58
|
+
"react-dom": "^19.0.0",
|
|
59
|
+
"tsup": "^8.3.5",
|
|
60
|
+
"typescript": "^5.7.2",
|
|
61
|
+
"vitest": "^2.1.8"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "tsup src/index.ts --format esm --dts --sourcemap --external react --external react-dom && cp src/style.css dist/style.css",
|
|
65
|
+
"prepack": "pnpm run build && pnpm run typecheck",
|
|
66
|
+
"prepublishOnly": "pnpm test",
|
|
67
|
+
"test": "vitest run",
|
|
68
|
+
"test:watch": "vitest",
|
|
69
|
+
"typecheck": "tsc --noEmit"
|
|
70
|
+
}
|
|
71
|
+
}
|