deck.gl Arc Layers
A deck.gl ArcLayer integrated into a MapLibre map, with hover and popup support.
<script lang="ts">
import MapLibre from 'svelte-maplibre/MapLibre.svelte';
import DeckGlLayer from 'svelte-maplibre/DeckGlLayer.svelte';
import { ArcLayer } from '@deck.gl/layers';
import CodeSample from '$site/CodeSample.svelte';
import code from './+page.svelte?raw';
import { geoCentroid } from 'd3-geo';
import clamp from 'just-clamp';
import counties from '$site/counties.json';
import states from '$site/states.json';
import type { Feature, FeatureCollection, Polygon } from 'geojson';
import Popup from 'svelte-maplibre/Popup.svelte';
import FillLayer from 'svelte-maplibre/FillLayer.svelte';
import GeoJson from 'svelte-maplibre/GeoJSON.svelte';
import { hoverStateFilter } from 'svelte-maplibre';
type GeoProperties = {
GEOID: string;
NAME: string;
STATEFP: string;
};
type ArcMode = 'showAll' | 'showOne';
type ArcData = {
fromName: string;
toName: string;
fromState: string;
toState: string;
source: [number, number]; // [longitude, latitude]
target: [number, number]; // [longitude, latitude]
sourceColor: [number, number, number]; // RGB values
targetColor: [number, number, number]; // RGB values
};
function calculateArcs(fc: FeatureCollection<Polygon, GeoProperties>): ArcData[] {
let centers = new Map(fc.features.map((f) => [f.properties?.GEOID, geoCentroid(f)]));
let count = fc.features.length > 100 ? 5000 : 100;
let indexes = Array.from({ length: count }, (_, i) => [
Math.ceil(Math.random() * (fc.features.length - 1)),
Math.ceil(Math.random() * (fc.features.length - 1)),
]);
return indexes.map(([fromIndex, toIndex]) => {
let from = fc.features[fromIndex];
let to = fc.features[toIndex];
return {
fromName: from.properties.NAME,
toName: to.properties.NAME,
fromState: from.properties.STATEFP,
toState: to.properties.STATEFP,
source: centers.get(from.properties.GEOID)!,
target: centers.get(to.properties.GEOID)!,
sourceColor: [255, 128, 0],
targetColor: [0, 125, 255],
};
});
}
let zoom = $state(3);
let hovered: ArcData | undefined = $state();
let mode = $state('showOne') as ArcMode;
let arcs = $derived(
calculateArcs(
(mode === 'showAll' ? states : counties) as unknown as FeatureCollection<
Polygon,
GeoProperties
>
)
);
let activeState = $state('');
$effect.pre(() => {
activeState = arcs[0].fromState;
});
</script>
<p>A deck.gl ArcLayer integrated into a MapLibre map, with hover and popup support.</p>
<fieldset class="mb-2 self-start border border-gray-400 p-2">
<legend>View Mode</legend>
<div class="flex flex-wrap gap-2">
<label>
<input type="radio" bind:group={mode} value="showOne" />
Show county arcs for hovered state
</label>
<label>
<input type="radio" bind:group={mode} value="showAll" />
Show state arcs
</label>
</div>
</fieldset>
<MapLibre
style="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
pitch={30}
center={[-100, 40]}
maxZoom={5}
bind:zoom
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full"
standardControls
>
<GeoJson id="states-base" data={states as unknown as FeatureCollection} promoteId="GEOID">
<!-- We use the base map to provide the visuals, but this gives something to click. -->
<FillLayer
id="counties-click"
hoverCursor="pointer"
paint={{
'fill-color': '#000',
'fill-opacity': mode === 'showOne' ? hoverStateFilter(0, 0.1) : 0,
}}
manageHoverState
onmousemove={(e) => {
if (mode === 'showOne') {
let newGeoId = e.features[0]?.properties?.STATEFP;
if (newGeoId !== activeState) {
activeState = newGeoId;
hovered = undefined;
}
}
}}
/>
</GeoJson>
<DeckGlLayer
type={ArcLayer}
data={arcs.filter((a, i) => {
if (mode === 'showAll') return i < 50000;
return a.fromState === activeState || a.toState === activeState;
})}
bind:hovered
getSourcePosition={(d: ArcData) => d.source}
getTargetPosition={(d: ArcData) => d.target}
getSourceColor={(d: ArcData) => d.sourceColor}
getTargetColor={(d: ArcData) => d.targetColor}
autoHighlight={mode === 'showAll'}
highlightColor={[30, 255, 30]}
getWidth={mode === 'showAll' ? 5 : 1}
getHeight={clamp(3 / zoom, 0, 1)}
>
<Popup openOn="click"
>{#snippet children({ data }: { data: ArcData | undefined })}
{#if data}
From {data.fromName} to {data.toName}
{/if}
{/snippet}
</Popup>
</DeckGlLayer>
</MapLibre>
<h4>
{#if hovered && mode === 'showAll'}
From {hovered.fromName} to {hovered.toName}
{:else if mode === 'showOne'}
{states.features.find((f) => f.properties.STATEFP === activeState)?.properties.NAME}
{:else}
Hover over an arc to see its endpoints
{/if}
</h4>