Data layers
Each layer item adds its own source and layers below the map's labels, colours them from your design tokens, and adds them again after a style swap. map-layers is the shared lifecycle underneath them and installs with them.
npx shadcn@latest registry add @unmap=https://registry.unmap.dev/r/{name}.json
npx shadcn@latest add @unmap/geojson-layernpx shadcn-vue@latest add https://registry.unmap.dev/r/vue/geojson-layer.jsonThe React line registers the registry once per project, so later items can be named @unmap/<item>; shadcn-vue has no registry add and takes the item URL directly. Files land in components/ui/ beside your own; nothing is overwritten without asking.
Browse every item on registry.unmap.dev| Item | What it is |
|---|---|
map-layers | The shared layer lifecycle the data items use. Installed automatically. |
route-line | Draw a route or any line: theme casing, alternatives, travelled progress, and a colour gradient by value per point. The routing panel draws with it. |
geojson-layer | Draw any GeoJSON as fills, lines and points in your --primary, with hover highlight and click events. |
cluster-layer | Cluster many points: counts, click to zoom in, and summed properties. |
heatmap-layer | Point density as a heatmap on your --primary, weighted by a property. |
arc-layer | Curved connections between pairs of places, with hover and click. Opposite directions bow apart. |
flow-layer | Origin-destination flows in the flowmap.blue style: width by volume, arrowheads, location totals, time and top-N filters, optional clustering by zoom. |
layer-legend | A checkbox per row that switches style layers on and off, with optional per-value filters and colour swatches, on your Field and Checkbox. Survives a style swap. |
Draw your own route
route-line draws any list of [lng, lat] points, so a route from @unmap/routing goes straight in:
import { Map } from "@/components/ui/map";
import { RouteLine } from "@/components/ui/route-line";
const route = await router.route(from, to, { mode: "auto" });
<Map apiKey={key} center={from} zoom={12}>
<RouteLine coordinates={route.geometry.coordinates} />
</Map>It sits below the map's labels, takes your --primary (or the theme's route colour), and comes
back after a mode or theme change. active={false} draws an alternative in
--muted-foreground, progress={0.4} shows how far along a trip is, and
gradient={{ values }} colours the line by one value per point on your --chart-1 to
--chart-5. An alternative stays under the active route whatever order they render in.
In Vue the component is UnmapRouteLine, with the same props:
<script setup lang="ts">
import { ref } from "vue";
import { Router, type RouteResult } from "@unmap/routing";
import { UnmapMap } from "@/components/ui/map";
import { UnmapRouteLine } from "@/components/ui/route-line";
const apiKey = import.meta.env.VITE_UNMAP_KEY!;
const route = ref<RouteResult | null>(null);
new Router({ key: apiKey })
.route([-114.0719, 51.0447], [-113.9871, 51.0899], { mode: "auto" })
.then((result) => (route.value = result));
</script>
<template>
<UnmapMap :api-key="apiKey" :center="[-114.07, 51.05]" :zoom="12">
<UnmapRouteLine v-if="route" :coordinates="route.geometry.coordinates" />
</UnmapMap>
</template>Draw your own data
Five layers draw data you bring: GeoJSON, clusters, a heatmap, arcs and flows. In Vue they are
UnmapGeojsonLayer, UnmapClusterLayer, UnmapHeatmapLayer, UnmapArcLayer and
UnmapFlowLayer, with the same props in kebab-case in the template.
GeoJSON
geojson-layer draws any GeoJSON (a FeatureCollection, a Feature, or a URL) as fills, lines
and points, filtered to each feature's geometry type. Set interactive (or pass onClick) and
the feature under the pointer is highlighted and reported:
import { Map } from "@/components/ui/map";
import { GeoJSONLayer } from "@/components/ui/geojson-layer";
<Map apiKey={key} center={[-114.07, 51.05]} zoom={11}>
<GeoJSONLayer data={parks} onClick={(event) => console.log(event.feature.properties)} />
</Map>Try it, then add it as a starting point:
React projects. Installs components/unmap-map-geojson.tsx; render <MapGeojson apiKey={key} />.
In Vue, interactive cannot be inferred from a listener the way onClick can in React, so pass it
explicitly:
<script setup lang="ts">
import { UnmapMap } from "@/components/ui/map";
import { UnmapGeojsonLayer } from "@/components/ui/geojson-layer";
import type { MapFeatureEvent } from "@/lib/map-layers";
const apiKey = import.meta.env.VITE_UNMAP_KEY!;
const parks = { type: "FeatureCollection", features: [] };
function onClick(event: MapFeatureEvent) {
console.log(event.feature.properties);
}
</script>
<template>
<UnmapMap :api-key="apiKey" :center="[-114.07, 51.05]" :zoom="11">
<UnmapGeojsonLayer :data="parks" interactive @click="onClick" />
</UnmapMap>
</template>Clusters
cluster-layer takes many Point features and clusters them with MapLibre's own clustering (only
Point: a MultiPoint is not clustered). A click on a cluster zooms in, and sum adds a numeric
property across each cluster:
import { Map } from "@/components/ui/map";
import { ClusterLayer } from "@/components/ui/cluster-layer";
<Map apiKey={key} center={[-114.07, 51.05]} zoom={11}>
<ClusterLayer data={clinics} sum={["beds"]} />
</Map>Click a cluster and the map zooms in until it splits:
React projects. Installs components/unmap-map-clusters.tsx; render <MapClusters apiKey={key} />.
The same in Vue:
<script setup lang="ts">
import { UnmapMap } from "@/components/ui/map";
import { UnmapClusterLayer } from "@/components/ui/cluster-layer";
const apiKey = import.meta.env.VITE_UNMAP_KEY!;
const clinics = { type: "FeatureCollection", features: [] };
</script>
<template>
<UnmapMap :api-key="apiKey" :center="[-114.07, 51.05]" :zoom="11">
<UnmapClusterLayer :data="clinics" :sum="['beds']" />
</UnmapMap>
</template>Heatmap
heatmap-layer draws point density instead of individual points, on your --primary unless you
pass colors. weight weighs each point by a numeric property. The GeoJSON, cluster and heatmap
layers sit below the map's labels, take your tokens, and come back after a mode or theme change,
the same as route-line:
import { Map } from "@/components/ui/map";
import { HeatmapLayer } from "@/components/ui/heatmap-layer";
<Map apiKey={key} center={[-114.07, 51.05]} zoom={11}>
<HeatmapLayer data={collisions} weight="injuries" />
</Map>Switch the weighting and watch the hot spots move:
React projects. Installs components/unmap-map-heatmap.tsx; render <MapHeatmap apiKey={key} />.
The same in Vue:
<script setup lang="ts">
import { UnmapMap } from "@/components/ui/map";
import { UnmapHeatmapLayer } from "@/components/ui/heatmap-layer";
const apiKey = import.meta.env.VITE_UNMAP_KEY!;
const collisions = { type: "FeatureCollection", features: [] };
</script>
<template>
<UnmapMap :api-key="apiKey" :center="[-114.07, 51.05]" :zoom="11">
<UnmapHeatmapLayer :data="collisions" weight="injuries" />
</UnmapMap>
</template>Arcs
arc-layer draws curved connections between pairs of places: flights, trade routes, referrals.
Opposite directions bow apart, so a route back never overlaps the route out. Each arc is
{ id, from, to } with [longitude, latitude] ends, and onClick or onHover reports the arc
under the pointer:
import { Map } from "@/components/ui/map";
import { ArcLayer } from "@/components/ui/arc-layer";
const flights = [
{ id: "yyc-yul", from: [-114.01, 51.13], to: [-73.74, 45.47] },
{ id: "yul-yyc", from: [-73.74, 45.47], to: [-114.01, 51.13] },
];
<Map apiKey={key} center={[-94, 50]} zoom={3}>
<ArcLayer data={flights} onClick={(event) => console.log(event.feature.properties)} />
</Map>Click an arc or pick a city to read the link and its distance:
React projects. Installs components/unmap-map-arcs.tsx; render <MapArcs apiKey={key} />.
In Vue, set interactive so @click and @hover fire:
<script setup lang="ts">
import { UnmapMap } from "@/components/ui/map";
import { UnmapArcLayer } from "@/components/ui/arc-layer";
import type { MapFeatureEvent } from "@/lib/map-layers";
const apiKey = import.meta.env.VITE_UNMAP_KEY!;
const flights: { id: string; from: [number, number]; to: [number, number] }[] = [
{ id: "yyc-yul", from: [-114.01, 51.13], to: [-73.74, 45.47] },
{ id: "yul-yyc", from: [-73.74, 45.47], to: [-114.01, 51.13] },
];
function onClick(event: MapFeatureEvent) {
console.log(event.feature.properties);
}
</script>
<template>
<UnmapMap :api-key="apiKey" :center="[-94, 50]" :zoom="3">
<UnmapArcLayer :data="flights" interactive @click="onClick" />
</UnmapMap>
</template>Flows
flow-layer draws origin-destination flows in the flowmap.blue style: width by volume, arrowheads
mid-line show direction, and a circle at each location sized by everything flowing through it.
locations is { id, name?, lat, lon }[] and flows is { origin, dest, count, time? }[], where
origin and dest are location ids; topN keeps only the largest pairs and cluster merges
nearby locations as you zoom out:
import { Map } from "@/components/ui/map";
import { FlowLayer } from "@/components/ui/flow-layer";
<Map apiKey={key} center={[-96, 62]} zoom={3}>
<FlowLayer locations={provinces} flows={migration} topN={50} />
</Map>Flows between the same pair are summed. A flow from a place to itself, or between two places at
the same coordinates, is never drawn. timeRange compares numbers, and numeric strings like
"2020", as numbers; otherwise it reads strings as dates and numbers as epoch milliseconds. A flow
whose time does not parse, such as a 2020/2021 label, is always kept, as is a flow with no time.
Widths and circle sizes scale over what is drawn, so the largest drawn flow is always the widest.
To compare years, pass the same countDomain and totalDomain (Vue count-domain and
total-domain) to every year, such as [0, largestFlow]: a width then means the same count in
each year, and a count outside the domain clamps.
Location totals and clusters count only the pairs that are drawn, after minCount and topN, so
small flows you filter out do not add up inside a cluster. A cluster reports places, how many
locations it holds, and has no name, so its label is yours to word. The layer rebuilds whenever
locations or flows is a new array: for large tables, pass stable arrays (state or useMemo in
React, a ref or computed in Vue).
Hover or tap a province for its totals in and out:
React projects. Installs components/unmap-map-flows.tsx; render <MapFlows apiKey={key} />.
In Vue, flow-layer takes interactive too, and reports whichever flow or location is under the
pointer through @hover:
<script setup lang="ts">
import { UnmapMap } from "@/components/ui/map";
import { UnmapFlowLayer } from "@/components/ui/flow-layer";
import type { MapFeatureEvent } from "@/lib/map-layers";
const apiKey = import.meta.env.VITE_UNMAP_KEY!;
const provinces: { id: string; lat: number; lon: number }[] = [];
const migration: { origin: string; dest: string; count: number }[] = [];
function onHover(event: MapFeatureEvent | null) {
console.log(event?.feature.properties);
}
</script>
<template>
<UnmapMap :api-key="apiKey" :center="[-96, 62]" :zoom="3">
<UnmapFlowLayer :locations="provinces" :flows="migration" :top-n="50" interactive @hover="onHover" />
</UnmapMap>
</template>Layer legend
layer-legend is a checkbox per row that switches style layers on and off, on your shadcn Field
and Checkbox. Each row names the layers it controls. An id ending in * matches every layer that
starts with the rest: stations-* catches every layer a <GeoJSONLayer id="stations"> draws, and
overlay_energy.wells_* the circle and label layers the gateway adds for the wells overlay. A row
with a filter gets a checkbox per value of a feature property, and unchecking a value hides the
features that carry it:
import { Map } from "@/components/ui/map";
import { GeoJSONLayer } from "@/components/ui/geojson-layer";
import { LayerLegend } from "@/components/ui/layer-legend";
<Map apiKey={key} center={[-114.07, 51.05]} zoom={11}>
<GeoJSONLayer id="stations" data={stations} />
<LayerLegend
title="Charging"
items={[
{
id: "stations",
label: "Stations",
layers: ["stations-*"],
filter: {
property: "status",
values: [
{ value: "open", label: "Open" },
{ value: "planned", label: "Planned" },
],
},
},
]}
/>
</Map>- The state is
{ hidden, excluded }: the row ids switched off, and per row the values switched off. PassdefaultValueto start with some off, orvalueandonValueChange(Vue:v-model) to control it. - The legend owns the visibility of the layers it names: a checked row makes them visible.
- A layer belongs to one row across every legend on the map. Within a legend the first row that names it decides. The first legend to apply to a layer (usually the first mounted) owns its visibility and its filter until it unmounts or stops naming the layer. Until then another legend's checkboxes for that layer change nothing; afterwards that legend takes over on its next pass, the next time the map's style changes, and puts its own values on the layer's own filter in place of the old legend's.
- Each row shows a swatch in its first layer's colour when that colour is a plain value.
colorsets one andcolor: falseremoves it. Filter values show a swatch only when you give them acolor. - A style swap (mode, style, theme or
lang) keeps what was switched off, because the legend applies its state again after every swap. A layer added later, by an overlay or another item, picks up the state when it appears, and an id the style does not have is skipped. - Removing a row's
filtergives its layers their own filter back, and an unchecked value the row no longer lists is dropped, so no feature stays hidden without a checkbox to bring it back. The memory of which filter the legend wrote is kept per map, so a legend inside a Popover or a Tab that unmounts and mounts again still knows what it changed. - Unmounting a legend leaves the map as it is. Pass
value(Vue:v-model) and keep the state in your own component to keep the checkboxes in step across remounts. energy-layer-controlis this legend with the energy overlays filled in. It renders a bare fieldset, so wrap it in your own Card or Popover when it floats over the map. The legend owns those layers' visibility, so drive it with the checkboxes, notsetLayoutProperty. It takesvalue,defaultValueandonValueChange(Vue:v-modelanddefault-value), with rows keyed by overlay id such asenergy.wells.lang="fr"writes the title and rows in French (React also exports the names asENERGY_LABELS_FR), andcolorsmaps an overlay id to a swatch colour, or tofalseto hide that row's swatch; a row left out keeps its layer's colour.
<script setup lang="ts">
import { ref } from "vue";
import { UnmapMap } from "@/components/ui/map";
import { UnmapGeojsonLayer } from "@/components/ui/geojson-layer";
import { UnmapLayerLegend } from "@/components/ui/layer-legend";
const apiKey = import.meta.env.VITE_UNMAP_KEY;
const stations = { type: "FeatureCollection", features: [] };
const items = [
{
id: "stations",
label: "Stations",
layers: ["stations-*"],
filter: {
property: "status",
values: [
{ value: "open", label: "Open" },
{ value: "planned", label: "Planned" },
],
},
},
];
const legend = ref({ hidden: [] as string[], excluded: { stations: ["planned"] } });
</script>
<template>
<UnmapMap :api-key="apiKey" :center="[-114.07, 51.05]" :zoom="11">
<UnmapGeojsonLayer id="stations" :data="stations" />
<UnmapLayerLegend v-model="legend" title="Charging" :items="items" />
</UnmapMap>
</template>