Proportional symbol map
Compare values through sized symbols at geographic locations. Chart ID: proportionalSymbolMap.
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
Larger symbols indicate higher sample demand at the supplied locations. The same mainland outlines provide context. Symbol locations are illustrative representative points, not observations across each entire country.
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 symbols with longitude/latitude positions, projected viewport positions, values, and non-negative sizes. Bind the size channel explicitly.
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="proportionalSymbolMap". 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.
- Geographic heatmap — Show spatial concentration through geographic heat points.
Explore the live catalog · Data & Configuration
Provider overlays and appearance
Section titled “Provider overlays and appearance”The response can include layers (at most 128): background SVG paths, regions,
symbols, heatmap points or lines routes. Backgrounds paint before the base
series; other overlays paint afterward in declared order. Routes supply their
projected path, stable ID/label, genuine categoryId/seriesId and source
value, plus an optional non-negative size. These are provider-projected
paths, not a built-in geographic routing or projection engine.
The request viewport has positive finite width/height and optional positive
pixelRatio. The provider owns projected positions and paths on resize. Heat
point radius is optional (18 viewport units when omitted); an authored radius
must be positive. projectionId, response type and dataset revision are
validated; source-bound overlay values must match the canonical dataset.
Pass rendererOptions on pack.render and each complete instance.update
input for theme, fonts, class names, element styles/formatters, diagnostics and
onRender. See geographic customization.
Locale callbacks format values, accessibility and load states. Click or
Enter/Space activation returns { chartType, datum, originalEvent } through
onNodeClick; application code decides what to do with the provider datum.
The capability instance exposes ready, update, focus and destroy;
it does not expose native chart model, CSV/image export or navigation methods.