Geographic heatmap
Show spatial concentration through geographic heat points. Chart ID: geographicHeatmap.
This is an optional geographic capability from @revolist/charts/geographic. It uses an application-supplied provider and pack.render(...). It is separate from the native createChart(...) catalog and the Pivot chart picker.
Live example
Sample demand · Portugal, Spain and France
Loading example map…
Simplified mainland outlines with illustrative values, using an equirectangular projection. Islands are omitted. This example supplies its own geometry; use a map provider and licensed assets for production.
| Country | Sample demand |
|---|---|
| Portugal | 42 |
| Spain | 85 |
| France | 120 |
Reading the chart
Warmer and stronger spots indicate higher sample demand at the supplied points. This example contains only three points; it does not estimate a continuous country-wide distribution or aggregate raw observations.
Click a mark or focus it with the keyboard and press Enter to inspect its sample value. The table supplies a text alternative.
Data requirements
Supply a revisioned chart dataset, geographic and value bindings, and a provider implementing load(request, { signal }). The response must match the request revision and chart type and include a projection identifier.
Return points with longitude/latitude positions, projected viewport positions, values, and positive radii in viewport pixels.
The application owns geometry, projection, assets and attribution. This local example uses original, approximate mainland outlines and synthetic demand values; it needs no external map downloads.
Create this chart
Follow the installation guide. Save the provider below as geographic-examples.ts and use the Vue component alongside it with type="geographicHeatmap". The component demonstrates rendering, theme and resize updates, node activation and cleanup using the public geographic API.
Complete provider and sample data (TypeScript)
import { createChartDataset } from '@revolist/charts/core';import { createChartGeographicCapabilityPack, type ChartGeographicProvider,} from '@revolist/charts/geographic';
export const geographicItems = [ { type: 'choropleth', slug: 'choropleth', label: 'Choropleth', description: 'Compare values through filled geographic regions.' }, { type: 'proportionalSymbolMap', slug: 'proportional-symbol-map', label: 'Proportional symbol map', description: 'Compare values through sized symbols at geographic locations.' }, { type: 'geographicHeatmap', slug: 'geographic-heatmap', label: 'Geographic heatmap', description: 'Show spatial concentration through geographic heat points.' },] as const;
// Original, deliberately simplified mainland outlines for teaching; not boundary data.const territories = [ { country: 'Portugal', value: 42, center: [-8, 39.5] as const, outline: [[-9.5, 42], [-8.8, 41.9], [-8.2, 41.8], [-6.6, 41.9], [-7, 40], [-7.5, 38.5], [-7.4, 37.2], [-9, 37], [-9.5, 38.7]] }, { country: 'Spain', value: 85, center: [-3.7, 40.4] as const, outline: [[-9.5, 43], [-8, 43.7], [-2, 43.4], [0, 42.7], [3.2, 42.4], [3, 41.8], [0.7, 40.7], [-0.2, 38], [-2, 36.7], [-5.6, 36], [-7.4, 37.2], [-7.5, 38.5], [-7, 40], [-6.6, 41.9], [-8.2, 41.8], [-8.8, 41.9], [-9.5, 42]] }, { country: 'France', value: 120, center: [2.3, 46.5] as const, outline: [[-4.8, 48.5], [-1.8, 48.7], [-1.8, 49.7], [0, 49.5], [1.8, 50.9], [2.6, 51.1], [4.8, 50], [8.2, 49], [7.5, 47.6], [6, 46.5], [7, 44.2], [7.5, 43.8], [5, 43.1], [3.2, 43.3], [3.2, 42.4], [0, 42.7], [-2, 43.4], [-1.2, 46.3], [-4.8, 48]] },];export const geographicDataset = createChartDataset(territories, { category: { field: 'country' }, series: [{ field: 'value', label: 'Sample demand' }],});
export const geographicProvider: ChartGeographicProvider = { id: 'portal-mainland-example', supportedTypes: geographicItems.map(item => item.type), async load(request, { signal }) { signal.throwIfAborted(); const { width, height } = request.viewport; // Equirectangular projection with a common scale to preserve aspect ratio. const scale = Math.min((width - 48) / 18, (height - 64) / 17); const project = ([longitude, latitude]: readonly number[]): readonly [number, number] => [width / 2 + (longitude + 1) * scale, (height - 28) / 2 - (latitude - 44) * scale]; const paths = territories.map(territory => territory.outline.map((position, index) => `${index ? 'L' : 'M'}${project(position).join(' ')}`).join(' ') + ' Z'); const common = { revision: request.dataset.revision, projectionId: 'example-equirectangular', attribution: 'Original simplified mainland outlines · illustrative values', }; const regions = territories.map((territory, index) => ({ id: territory.country, label: territory.country, categoryId: request.dataset.categories[index].id, seriesId: request.dataset.series[0].id, value: request.dataset.values[index][0], path: paths[index], })); if (request.chartType === 'choropleth') return { ...common, chartType: request.chartType, regions }; const points = regions.map((region, index) => ({ ...region, position: territories[index].center, projectedPosition: project(territories[index].center), })); const layers = [{ kind: 'background' as const, paths }]; if (request.chartType === 'proportionalSymbolMap') return { ...common, layers, chartType: request.chartType, symbols: points.map(point => ({ ...point, size: point.value })), }; return { ...common, layers, chartType: request.chartType, points: points.flatMap(point => point.value === null ? [] : [{ ...point, value: point.value, radius: 32 }]) }; },};export const createGeographicExamplePack = () => createChartGeographicCapabilityPack(geographicProvider);Complete Vue component and rendering lifecycle
<script setup lang="ts">import { onBeforeUnmount, onMounted, ref } from 'vue';import type { ChartGeographicType, ChartGeographicRenderInput, ChartGeographicRendererInstance } from '@revolist/charts/geographic';import { createGeographicExamplePack, geographicDataset, geographicItems } from './geographic-examples';
const props = defineProps<{ type: ChartGeographicType }>();const host = ref<HTMLElement>();const ready = ref(false);const error = ref('');const status = ref('Activate a region or point to inspect its value.');let instance: ChartGeographicRendererInstance | undefined;let resize: ResizeObserver | undefined;let appearance: MutationObserver | undefined;let disposed = false;const formatValue = (value: number | null) => value === null ? 'Missing' : String(value);function input(): ChartGeographicRenderInput { return { container: host.value!, request: { chartId: `portal-${props.type}`, chartType: props.type, dataset: geographicDataset, bindings: { geographic: ['pivot:category'], value: [geographicDataset.series[0].id], ...(props.type === 'proportionalSymbolMap' ? { size: [geographicDataset.series[0].id] } : {}) }, theme: document.documentElement.dataset.theme === 'dark' ? 'dark' : 'light', viewport: { width: Math.max(200, host.value!.clientWidth), height: 360 }, }, localeText: { chartAriaLabel: type => `${geographicItems.find(item => item.type === type)!.label}: sample demand in Portugal, Spain and France`, loading: 'Loading example map', loadError: cause => cause.message, regionAriaLabel: (label, value) => `${label}: ${value}`, symbolAriaLabel: (label, value, size) => `${label}: ${value}; size ${size}`, heatPointAriaLabel: value => `Sample demand: ${value}`, formatValue, }, onNodeClick: ({ datum }) => { status.value = `${datum.id}: ${formatValue(datum.value)} sample demand`; }, onError: cause => { error.value = cause.message; }, };}async function update() { if (disposed || !instance) return; try { await instance.update(input()); } catch (cause) { if (!disposed) error.value = cause instanceof Error ? cause.message : String(cause); }}onMounted(async () => { try { instance = createGeographicExamplePack().render(input()); await instance.ready; if (disposed) return; ready.value = true; resize = new ResizeObserver(() => { void update(); }); resize.observe(host.value!); appearance = new MutationObserver(() => { void update(); }); appearance.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); } catch (cause) { if (!disposed) error.value = cause instanceof Error ? cause.message : String(cause); }});onBeforeUnmount(() => { disposed = true; resize?.disconnect(); appearance?.disconnect(); instance?.destroy(); });</script>
<template> <section class="geographic-demo not-content" :data-geographic-chart="type" :data-ready="ready" aria-label="Geographic chart example"> <p>Sample demand · Portugal, Spain and France</p> <div ref="host" class="geographic-demo__plot"></div> <p role="status">{{ ready ? status : 'Loading example map…' }}</p> <p v-if="error" role="alert">{{ error }}</p> <p>Simplified mainland outlines with illustrative values, using an equirectangular projection. Islands are omitted. This example supplies its own geometry; use a map provider and licensed assets for production.</p> <table><thead><tr><th>Country</th><th>Sample demand</th></tr></thead><tbody> <tr v-for="(category, index) in geographicDataset.categories" :key="category.id"><td>{{ category.label }}</td><td>{{ geographicDataset.values[index][0] }}</td></tr> </tbody></table> </section></template>
<style scoped>.geographic-demo { margin: 1rem 0; padding: 1rem; border: 1px solid var(--sl-color-gray-5); border-radius: 12px; background: var(--sl-color-bg); min-width: 0; }.geographic-demo__plot { width: 100%; height: 360px; min-width: 0; }.geographic-demo p { font-size: .85rem; color: var(--sl-color-gray-2); }.geographic-demo table { width: 100%; border-collapse: collapse; font-size: .85rem; }.geographic-demo th, .geographic-demo td { padding: .4rem .65rem; text-align: left; border-bottom: 1px solid var(--sl-color-gray-5); }.geographic-demo__plot :deep(svg text) { fill: currentColor; }.geographic-demo [role=alert] { color: var(--sl-color-red-high); }@media (max-width: 500px) { .geographic-demo { padding: .5rem; } }</style>For other frameworks, create the pack with createChartGeographicCapabilityPack(provider) after the host mounts, call pack.render(input), await instance.ready, and use instance.update(input) for new requests. Disconnect observers and call instance.destroy() on unmount; superseded and destroyed loads are cancelled by the renderer.
Related geographic charts
- Choropleth — Compare values through filled geographic regions.
- Proportional symbol map — Compare values through sized symbols at geographic locations.