---
title: "VControlLegend"
description: "Interactive legend control with category, gradient, and size visualization"
canonical_url: "https://v-maplibre.geoql.in/controls/legend"
---
# VControlLegend

> Interactive legend control with category, gradient, and size visualization

<callout icon="i-lucide-palette" target="_blank" to="https://mapcn-vue.geoql.in/examples/maplibre-legend">

**Live Demo** - Try the legend control with different legend types

</callout>

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

```vue
<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

```vue
<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

```vue
<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.

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

### @filter-change

Emitted when the filter state changes.

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

### @update:filter

Emitted for v-model:filter binding.

## v-model Binding

```vue
<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

```vue
<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

```vue
<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:

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

### deck.gl Layers

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

```vue
<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

```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,
};
```


## Sitemap

See the full [sitemap](https://v-maplibre.geoql.in/sitemap.md) for all pages.
