Skip to content

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-layer

The 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
ItemWhat it is
map-layersThe shared layer lifecycle the data items use. Installed automatically.
route-lineDraw 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-layerDraw any GeoJSON as fills, lines and points in your --primary, with hover highlight and click events.
cluster-layerCluster many points: counts, click to zoom in, and summed properties.
heatmap-layerPoint density as a heatmap on your --primary, weighted by a property.
arc-layerCurved connections between pairs of places, with hover and click. Opposite directions bow apart.
flow-layerOrigin-destination flows in the flowmap.blue style: width by volume, arrowheads, location totals, time and top-N filters, optional clustering by zoom.
layer-legendA 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:

npx @unmap/cli add map-geojson-01

React projects. Installs components/unmap-map-geojson.tsx; render <MapGeojson apiKey={key} />.

Open the example page

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:

npx @unmap/cli add map-clusters-01

React projects. Installs components/unmap-map-clusters.tsx; render <MapClusters apiKey={key} />.

Open the example page

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:

npx @unmap/cli add map-heatmap-01

React projects. Installs components/unmap-map-heatmap.tsx; render <MapHeatmap apiKey={key} />.

Open the example page

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:

npx @unmap/cli add map-arcs-01

React projects. Installs components/unmap-map-arcs.tsx; render <MapArcs apiKey={key} />.

Open the example page

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:

npx @unmap/cli add map-flows-01

React projects. Installs components/unmap-map-flows.tsx; render <MapFlows apiKey={key} />.

Open the example page

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. Pass defaultValue to start with some off, or value and onValueChange (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. color sets one and color: false removes it. Filter values show a swatch only when you give them a color.
  • 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 filter gives 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-control is 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, not setLayoutProperty. It takes value, defaultValue and onValueChange (Vue: v-model and default-value), with rows keyed by overlay id such as energy.wells. lang="fr" writes the title and rows in French (React also exports the names as ENERGY_LABELS_FR), and colors maps an overlay id to a swatch colour, or to false to 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>