// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. /** * Plotly Choropleth template. * * Native `choropleth` trace: Plotly resolves `locations` through its own * built-in atlas (US states / world countries) keyed by a short code * (`locationmode`), so — unlike the Vega-Lite template, which joins the * user's rows into a fetched TopoJSON via a `lookup` transform — no external * geometry needs to be fetched or joined here. * * Data contract (unchanged from Vega-Lite): `id` carries the region name/ * code, `color` the measure, optional `detail` a display label. Values are * resolved through the SAME gazetteer as the Vega-Lite choropleth * (`chart-types/geo.ts`) — just to a USPS/ISO-3 code instead of a numeric * TopoJSON feature id — so a dataset that renders as a US or World map on * one backend renders the same way on the other. */ import { ChartTemplateDef, ChartPropertyDef } from '../../core/types'; import { resolveUsStateCode, resolveCountryCode, GeoCodeResolver, inferChoroplethScope, semanticScope, pickMapScope, } from '../../chart-types/geo'; import { toTypeString } from '../../core/field-semantics'; // Plotly's own built-in named `'Blues'` colorscale runs dark→light as the // value increases (z=0 → "rgb(5,10,172)" dark blue, z=1 → "rgb(220,220,220)" // light gray) — the OPPOSITE of the light→dark sequential convention used // everywhere else in this codebase (and by every other backend's default // choropleth/heatmap ramp). For a choropleth that's actively misleading (the // highest-value region reads as the *palest*), so this template uses an // explicit ColorBrewer-style light→dark stop array instead of the `'Blues'` // string. Plotly's stock `'RdBu'` (used for the diverging case below) is NOT // affected — its low→mid→high stops already run blue→gray→red, matching the // diverging convention — so it's used as-is. const SEQUENTIAL_BLUES: Array<[number, string]> = [ [0, '#eff3ff'], [0.25, '#bdd7e7'], [0.5, '#6baed6'], [0.75, '#3182bd'], [1, '#08519c'], ]; const regionProperty: ChartPropertyDef = { key: 'region', label: 'Region', type: 'discrete', options: [ { value: 'auto', label: 'Auto-detect' }, { value: 'us', label: 'United States' }, { value: 'world', label: 'World' }, ], defaultValue: 'auto', }; /** Configure `layout.geo` for the chosen scope (no projection controls — mirrors the VL Choropleth). */ function buildGeoLayout(scope: 'us' | 'world'): any { if (scope === 'us') { return { scope: 'usa', projection: { type: 'albers usa' }, showlakes: true, lakecolor: '#ffffff', }; } return { scope: 'world', projection: { type: 'natural earth' }, showframe: false, showcoastlines: false, showcountries: false, // borders drawn by the choropleth fill itself }; } export const plChoroplethDef: ChartTemplateDef = { chart: 'Choropleth', template: { mark: 'geoshape', encoding: {} }, channels: ['id', 'color', 'detail'], markCognitiveChannel: 'color', instantiate: (spec, ctx) => { const { channelSemantics, chartProperties, colorDecisions } = ctx; const idField = channelSemantics.id?.field; const colorField = channelSemantics.color?.field; const labelField = channelSemantics.detail?.field ?? idField; if (!idField) return; const rows = ctx.fullTable ?? ctx.table ?? []; const semType = toTypeString(ctx.semanticTypes?.[idField]); const semScope = semanticScope(semType); const scope = pickMapScope(chartProperties, semScope, () => inferChoroplethScope(rows, idField)); const codeResolver: GeoCodeResolver = scope === 'us' ? resolveUsStateCode : resolveCountryCode; const locationmode = scope === 'us' ? 'USA-states' : 'ISO-3'; const locations: string[] = []; const z: number[] = []; const labels: string[] = []; for (const r of rows) { const code = codeResolver(r[idField]); if (!code) continue; const raw = colorField ? Number(r[colorField]) : undefined; if (colorField && !Number.isFinite(raw)) continue; locations.push(code); z.push(colorField ? (raw as number) : 1); labels.push(labelField ? String(r[labelField] ?? r[idField]) : String(r[idField])); } if (locations.length === 0) return; const decision = colorDecisions?.color ?? colorDecisions?.group; const diverging = decision?.schemeType === 'diverging'; const geo = buildGeoLayout(scope); const valueLabel = colorField ?? 'Value'; Object.assign(spec, { data: [{ type: 'choropleth', locations, locationmode, z, text: labels, colorscale: diverging ? 'RdBu' : SEQUENTIAL_BLUES, colorbar: { title: { text: valueLabel } }, marker: { line: { color: '#ffffff', width: 0.5 } }, hovertemplate: `%{text}
${valueLabel}: %{z}`, }], layout: { geo, showlegend: false, margin: { t: 24, b: 24, l: 8, r: 8 }, }, _width: scope === 'us' ? 520 : 620, _height: scope === 'us' ? 320 : 360, }); delete spec.mark; delete spec.encoding; }, properties: [regionProperty], };