Controls

VControlLegend

Interactive legend control with category, gradient, and size visualization
Live Demo - Try the legend control with different legend types

Overview

The VControlLegend component provides an interactive legend for map layers. It supports three legend types:

  • Category: Color-coded categories with click-to-filter
  • Gradient: Continuous color ramp with min/max labels
  • Size: Proportional symbol scale

Features:

  • Auto-generate legend from MapLibre paint expressions
  • Interactive filtering (click to show/hide categories)
  • Support for MapLibre and deck.gl layer filtering
  • Collapsible panel
  • v-model binding for filter state

Usage

Category Legend

<script setup lang="ts">
  import { VMap, VControlLegend, VLayerMaplibreGeojson } from '@geoql/v-maplibre';

  const mapOptions = {
    style: 'https://basemaps.cartocdn.com/gl/positron-gl-style/style.json',
    center: [-74.5, 40],
    zoom: 9,
  };

  const legendItems = [
    { value: 'residential', label: 'Residential', color: '#4CAF50' },
    { value: 'commercial', label: 'Commercial', color: '#2196F3' },
    { value: 'industrial', label: 'Industrial', color: '#FF9800' },
  ];
</script>

<template>
  <VMap :options="mapOptions" style="height: 500px">
    <VLayerMaplibreGeojson
      source-id="buildings"
      layer-id="buildings-layer"
      :source="buildingsSource"
      :layer="buildingsLayer"
    />
    <VControlLegend
      :layer-ids="['buildings-layer']"
      type="category"
      :items="legendItems"
      title="Land Use"
      position="top-right"
      :interactive="true"
    />
  </VMap>
</template>

Gradient Legend

<script setup lang="ts">
  import { VMap, VControlLegend } from '@geoql/v-maplibre';

  const gradientItems = [
    {
      min: 0,
      max: 100,
      colors: ['#ffffcc', '#a1dab4', '#41b6c4', '#225ea8'],
      minLabel: 'Low',
      maxLabel: 'High',
    },
  ];
</script>

<template>
  <VMap :options="mapOptions" style="height: 500px">
    <VControlLegend
      :layer-ids="['choropleth-layer']"
      type="gradient"
      :items="gradientItems"
      title="Population Density"
    />
  </VMap>
</template>

Size Legend

<script setup lang="ts">
  import { VMap, VControlLegend } from '@geoql/v-maplibre';

  const sizeItems = [
    { value: 10, label: 'Small (< 1000)', size: 8 },
    { value: 50, label: 'Medium (1000-5000)', size: 16 },
    { value: 100, label: 'Large (> 5000)', size: 24 },
  ];
</script>

<template>
  <VMap :options="mapOptions" style="height: 500px">
    <VControlLegend
      :layer-ids="['points-layer']"
      type="size"
      :items="sizeItems"
      title="City Population"
    />
  </VMap>
</template>

Props

layerIds

  • Type: string[]
  • Required: true

Array of layer IDs to filter when legend items are toggled.

type

  • Type: 'category' | 'gradient' | 'size'
  • Required: false
  • Default: 'category'

The legend type to render.

items

  • Type: LegendItem[]
  • Required: false

Explicit legend items. If not provided and autoGenerate is true, items are generated from paint properties.

position

  • Type: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'
  • Required: false
  • Default: 'top-right'

Position of the legend control on the map.

property

  • Type: string
  • Required: false

MapLibre paint property to read for auto-generation and filtering (e.g., 'fill-color').

autoGenerate

  • Type: boolean
  • Required: false
  • Default: false

Auto-generate legend items from MapLibre paint expressions. Supports match, step, and interpolate expressions.

title

  • Type: string
  • Required: false
  • Default: 'Legend'

Title displayed in the legend header.

collapsed

  • Type: boolean
  • Required: false
  • Default: false

Start with the legend collapsed.

interactive

  • Type: boolean
  • Required: false
  • Default: true

Enable click-to-filter functionality for category legends.

Events

@item-click

Emitted when a legend item is clicked.

interface ItemClickEvent {
  item: LegendItem;
  index: number;
  visible: boolean;
}

@filter-change

Emitted when the filter state changes.

interface FilterChangeEvent {
  filter: FilterState;
  layerIds: string[];
}

@update:filter

Emitted for v-model:filter binding.

v-model Binding

<script setup lang="ts">
  import { ref } from 'vue';
  import type { FilterState } from '@geoql/v-maplibre';

  const filterState = ref<FilterState>({
    visibleValues: ['residential', 'commercial', 'industrial'],
  });
</script>

<template>
  <VControlLegend
    :layer-ids="['buildings-layer']"
    type="category"
    :items="legendItems"
    v-model:filter="filterState"
  />

  <p>Visible categories: {{ filterState.visibleValues.join(', ') }}</p>
</template>

Auto-Generation

The legend can auto-generate items from MapLibre paint expressions:

From Match Expression

<template>
  <!-- Layer with fill-color: ['match', ['get', 'type'], 'A', '#ff0000', 'B', '#00ff00', '#cccccc'] -->
  <VControlLegend
    :layer-ids="['my-layer']"
    type="category"
    :auto-generate="true"
    property="fill-color"
  />
</template>

From Interpolate Expression

<template>
  <!-- Layer with fill-color: ['interpolate', ['linear'], ['get', 'value'], 0, '#0000ff', 100, '#ff0000'] -->
  <VControlLegend
    :layer-ids="['choropleth']"
    type="gradient"
    :auto-generate="true"
    property="fill-color"
  />
</template>

Layer Filtering

MapLibre Layers

When a category is toggled off, the control applies a filter to hide matching features:

map.setFilter(layerId, ['in', ['get', 'property'], ['literal', visibleValues]]);

deck.gl Layers

deck.gl layer filtering requires DataFilterExtension to be pre-configured:

<script setup lang="ts">
  import { DataFilterExtension } from '@deck.gl/extensions';

  const layer = new ScatterplotLayer({
    id: 'scatter',
    data: points,
    extensions: [new DataFilterExtension()],
    getFilterValue: (d) => categoryToIndex(d.category),
    filterRange: [0, Infinity],
  });
</script>

If a deck.gl layer doesn't have DataFilterExtension, a warning is logged.

TypeScript

import type {
  LegendType,
  LegendItem,
  CategoryLegendItem,
  GradientLegendItem,
  SizeLegendItem,
  FilterState,
  LegendControlOptions,
} from '@geoql/v-maplibre';

// Category item
const categoryItem: CategoryLegendItem = {
  value: 'residential',
  label: 'Residential',
  color: '#4CAF50',
  visible: true,
  count: 150, // Optional feature count
};

// Gradient item
const gradientItem: GradientLegendItem = {
  min: 0,
  max: 100,
  colors: ['#0000ff', '#ff0000'],
  minLabel: 'Low',
  maxLabel: 'High',
  stops: [0, 25, 50, 75, 100], // Optional color stops
};

// Size item
const sizeItem: SizeLegendItem = {
  value: 50,
  label: 'Medium',
  size: 16,
};

// Filter state
const filterState: FilterState = {
  visibleValues: ['residential', 'commercial'],
  minRange: 0, // For gradient filtering
  maxRange: 100,
};