---
title: "Filter annotations by category at runtime | Geosplats sdk"
description: "Filter annotations by category using visibility toggle or color dimming."
---

> For AI agents: a documentation index is available at [/llms.txt](https://docs.maptiler.com/llms.txt). Markdown versions of all pages are available by appending .md to any URL.

# Filter annotations by category at runtime

This example demonstrates how to filter 3D Gaussian splat annotations by category at runtime using the `setVisible()` and `setColor()` methods.

### NPM module setup

```bash
npm install --save @maptiler/geosplats
```

#### main.js

```js
import { SplatModel, Map, config } from "@maptiler/geosplats";
import "@maptiler/geosplats/dist/maptiler-geosplats.css";

config.apiKey = "YOUR_MAPTILER_API_KEY_HERE";

const map = new Map({
  apiKey: config.apiKey,
  container: "map",
  center: [9.59401, 59.22317],
  zoom: 17.5,
  pitch: 50,
  bearing: -15,
});

let splatModel;
map.on("load", () => {
  splatModel = new SplatModel({
    model: "YOUR_MAPTILER_SPLAT_MODEL_ID_HERE",
    revealParams: { enabled: true },
  });
  map.addSplatModel(splatModel);
});

const CATEGORY_COLORS = {
  structure: "#3174ff",
  installations: "#e69439",
  safety: "#2bb673",
};

const DIMMED_COLOR = [150, 150, 150, 0.35];

const poiData = [
  {
    id: "framing",
    pos: { x: -0.35, z: -0.45 },
    icon: "🏗️",
    label: "Steel Frame",
    category: "structure",
    popup: "<h3>Steel Framework</h3><p>Primary structural steel columns and rafters assembly.</p>",
  },
  {
    id: "foundation",
    pos: { x: 0.25, z: 0.35 },
    icon: "🧱",
    label: "Foundation Slab",
    category: "structure",
    popup:
      "<h3>Reinforced Concrete Slab</h3><p>Industrial ground floor slab curing inspection.</p>",
  },
  {
    id: "hvac",
    pos: { x: 0.05, z: -0.15 },
    icon: "💨",
    label: "HVAC Ductwork",
    category: "installations",
    popup: "<h3>HVAC Installation</h3><p>Main ventilation air ducts and extraction lines.</p>",
  },
  {
    id: "electrical",
    pos: { x: -0.55, z: 0.2 },
    icon: "⚡",
    label: "Electrical Panel",
    category: "installations",
    popup:
      "<h3>Main Switchgear Panel</h3><p>High-voltage cable conduit trays and distribution boards.</p>",
  },
  {
    id: "fire_safety",
    pos: { x: 0.45, z: -0.5 },
    icon: "🧯",
    label: "Fire Point",
    category: "safety",
    popup:
      "<h3>Emergency Fire Point</h3><p>Site fire extinguisher stand and alarm trigger point.</p>",
  },
  {
    id: "ppe_point",
    pos: { x: -0.1, z: 0.6 },
    icon: "🦺",
    label: "PPE Station",
    category: "safety",
    popup:
      "<h3>PPE Mandatory Zone</h3><p>Hard hat and high-vis vest required beyond this checkpoint.</p>",
  },
];

const allAnnotations = [];
let currentCategory = "all";
let isDimMode = false;

function applyFilter() {
  for (const ann of allAnnotations) {
    const data = ann.getData();
    const matches = currentCategory === "all" || data.category === currentCategory;

    if (isDimMode) {
      ann.setVisible(true);
      ann.setColor(matches ? data.defaultColor : DIMMED_COLOR);
    } else {
      ann.setVisible(matches);
      ann.setColor(data.defaultColor);
    }
  }
}

map.on("splatmodel:load", () => {
  for (const item of poiData) {
    const color = CATEGORY_COLORS[item.category] || "#3174ff";
    const ann = splatModel.addAnnotation({
      pos: item.pos,
      icon: item.icon,
      label: item.label,
      color,
      popup: item.popup,
      data: { category: item.category, defaultColor: color },
    });
    if (ann) {
      allAnnotations.push(ann);
    }
  }
});

document.querySelectorAll(".filter-buttons button").forEach((btn) => {
  btn.addEventListener("click", () => {
    document
      .querySelectorAll(".filter-buttons button")
      .forEach((b) => b.classList.remove("active"));
    btn.classList.add("active");
    currentCategory = btn.getAttribute("data-cat");
    applyFilter();
  });
});

document.getElementById("dimModeCheckbox").addEventListener("change", (e) => {
  isDimMode = e.target.checked;
  applyFilter();
});
```

#### index.html

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="robots" content="noindex" />
    <title>Filter annotations by category at runtime</title>
    <link rel="stylesheet" href="./style.css" />
  </head>
  <body>
    <div class="filter-panel">
      <div class="filter-buttons">
        <button class="active" data-cat="all">All</button>
        <button data-cat="structure">Structure</button>
        <button data-cat="installations">Installations</button>
        <button data-cat="safety">Safety</button>
      </div>
      <div class="filter-options">
        <input type="checkbox" id="dimModeCheckbox" />
        <label for="dimModeCheckbox">Dim inactive pins instead of hiding</label>
      </div>
    </div>
    <div id="map"></div>
    <script type="module" src="./main.js"></script>
  </body>
</html>
```

#### style.css

```css
body {
  font-family: Inter, sans-serif;
  margin: 0;
  overflow: hidden;
}

#map {
  width: 100%;
  height: 100vh;
}

.filter-panel {
  position: absolute;
  top: 16px;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(4px);
  padding: 10px 18px;
  border-radius: 12px;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
  z-index: 10;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}

.filter-buttons {
  display: flex;
  gap: 6px;
}

.filter-buttons button {
  font-size: 13px;
  padding: 4px 12px;
  border-radius: 20px;
  border: 1px solid #3174ff;
  background: transparent;
  color: #3174ff;
  cursor: pointer;
}

.filter-buttons button.active {
  background: #3174ff;
  color: #fff;
}

.filter-options {
  font-size: 12px;
  color: #555;
  display: flex;
  align-items: center;
  gap: 6px;
}
```

### Basic JavaScript setup

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Filter annotations by category at runtime</title>
    <link
      rel="stylesheet"
      href="https://cdn.maptiler.com/maptiler-geosplats/v1.2.1/maptiler-geosplats.css"
    />
    <style>
      body {
        font-family: Inter, sans-serif;
        margin: 0;
        overflow: hidden;
      }

      #map {
        width: 100%;
        height: 100vh;
      }

      .filter-panel {
        position: absolute;
        top: 16px;
        left: 50%;
        transform: translateX(-50%);
        background: rgba(255, 255, 255, 0.95);
        backdrop-filter: blur(4px);
        padding: 10px 18px;
        border-radius: 12px;
        box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
        z-index: 10;
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 8px;
      }

      .filter-buttons {
        display: flex;
        gap: 6px;
      }

      .filter-buttons button {
        font-size: 13px;
        padding: 4px 12px;
        border-radius: 20px;
        border: 1px solid #3174ff;
        background: transparent;
        color: #3174ff;
        cursor: pointer;
      }

      .filter-buttons button.active {
        background: #3174ff;
        color: #fff;
      }

      .filter-options {
        font-size: 12px;
        color: #555;
        display: flex;
        align-items: center;
        gap: 6px;
      }
    </style>
  </head>

  <body>
    <div class="filter-panel">
      <div class="filter-buttons">
        <button class="active" data-cat="all">All</button>
        <button data-cat="structure">Structure</button>
        <button data-cat="installations">Installations</button>
        <button data-cat="safety">Safety</button>
      </div>
      <div class="filter-options">
        <input type="checkbox" id="dimModeCheckbox" />
        <label for="dimModeCheckbox">Dim inactive pins instead of hiding</label>
      </div>
    </div>
    <div id="map"></div>
    <script type="module">
      import {
        SplatModel,
        Map,
        config,
      } from "https://cdn.maptiler.com/maptiler-geosplats/v1.2.1/maptiler-geosplats.mjs";

      config.apiKey = "YOUR_MAPTILER_API_KEY_HERE";

      const map = new Map({
        apiKey: config.apiKey,
        container: "map",
        center: [9.59401, 59.22317],
        zoom: 17.5,
        pitch: 50,
        bearing: -15,
      });

      let splatModel;
      map.on("load", () => {
        splatModel = new SplatModel({
          model: "YOUR_MAPTILER_SPLAT_MODEL_ID_HERE",
          revealParams: { enabled: true },
        });
        map.addSplatModel(splatModel);
      });

      const CATEGORY_COLORS = {
        structure: "#3174ff",
        installations: "#e69439",
        safety: "#2bb673",
      };

      const DIMMED_COLOR = [150, 150, 150, 0.35];

      const poiData = [
        {
          id: "framing",
          pos: { x: -0.35, z: -0.45 },
          icon: "🏗️",
          label: "Steel Frame",
          category: "structure",
          popup:
            "<h3>Steel Framework</h3><p>Primary structural steel columns and rafters assembly.</p>",
        },
        {
          id: "foundation",
          pos: { x: 0.25, z: 0.35 },
          icon: "🧱",
          label: "Foundation Slab",
          category: "structure",
          popup:
            "<h3>Reinforced Concrete Slab</h3><p>Industrial ground floor slab curing inspection.</p>",
        },
        {
          id: "hvac",
          pos: { x: 0.05, z: -0.15 },
          icon: "💨",
          label: "HVAC Ductwork",
          category: "installations",
          popup:
            "<h3>HVAC Installation</h3><p>Main ventilation air ducts and extraction lines.</p>",
        },
        {
          id: "electrical",
          pos: { x: -0.55, z: 0.2 },
          icon: "⚡",
          label: "Electrical Panel",
          category: "installations",
          popup:
            "<h3>Main Switchgear Panel</h3><p>High-voltage cable conduit trays and distribution boards.</p>",
        },
        {
          id: "fire_safety",
          pos: { x: 0.45, z: -0.5 },
          icon: "🧯",
          label: "Fire Point",
          category: "safety",
          popup:
            "<h3>Emergency Fire Point</h3><p>Site fire extinguisher stand and alarm trigger point.</p>",
        },
        {
          id: "ppe_point",
          pos: { x: -0.1, z: 0.6 },
          icon: "🦺",
          label: "PPE Station",
          category: "safety",
          popup:
            "<h3>PPE Mandatory Zone</h3><p>Hard hat and high-vis vest required beyond this checkpoint.</p>",
        },
      ];

      const allAnnotations = [];
      let currentCategory = "all";
      let isDimMode = false;

      function applyFilter() {
        for (const ann of allAnnotations) {
          const data = ann.getData();
          const matches = currentCategory === "all" || data.category === currentCategory;

          if (isDimMode) {
            ann.setVisible(true);
            ann.setColor(matches ? data.defaultColor : DIMMED_COLOR);
          } else {
            ann.setVisible(matches);
            ann.setColor(data.defaultColor);
          }
        }
      }

      map.on("splatmodel:load", () => {
        for (const item of poiData) {
          const color = CATEGORY_COLORS[item.category] || "#3174ff";
          const ann = splatModel.addAnnotation({
            pos: item.pos,
            icon: item.icon,
            label: item.label,
            color,
            popup: item.popup,
            data: { category: item.category, defaultColor: color },
          });
          if (ann) {
            allAnnotations.push(ann);
          }
        }
      });

      document.querySelectorAll(".filter-buttons button").forEach((btn) => {
        btn.addEventListener("click", () => {
          document
            .querySelectorAll(".filter-buttons button")
            .forEach((b) => b.classList.remove("active"));
          btn.classList.add("active");
          currentCategory = btn.getAttribute("data-cat");
          applyFilter();
        });
      });

      document.getElementById("dimModeCheckbox").addEventListener("change", (e) => {
        isDimMode = e.target.checked;
        applyFilter();
      });
    </script>
  </body>
</html>
```

## Attaching categories to custom data

When creating annotations, you can pass arbitrary application metadata through the `data` property. The SDK stores this object untouched:

```js
splatModel.addAnnotation({
  pos: { x: -0.35, z: -0.45 },
  icon: "🏗️",
  label: "Steel Frame",
  data: { category: "structure", defaultColor: "#3174ff" },
});
```

Retrieve the stored data object at any point using `annotation.getData()`.

## Hiding unmatched annotations

To remove pins from view entirely when a filter is applied, call `annotation.setVisible(false)`:

```js
function filterByCategory(selectedCategory) {
  for (const annotation of allAnnotations) {
    const { category } = annotation.getData();
    const isMatch = selectedCategory === "all" || category === selectedCategory;
    annotation.setVisible(isMatch);
  }
}
```

Hiding an annotation automatically closes its popup if it is open, triggering `"annotation:inactive"` cleanly without leaving dangling UI elements.

## Dimming annotations with transparency

If you want to preserve spatial context rather than hiding pins completely, you can keep all annotations visible and dim inactive items using an RGBA color array:

```js
const DIMMED_COLOR = [150, 150, 150, 0.35];

function dimInactive(selectedCategory) {
  for (const annotation of allAnnotations) {
    const { category, defaultColor } = annotation.getData();
    const isMatch = selectedCategory === "all" || category === selectedCategory;
    annotation.setVisible(true);
    annotation.setColor(isMatch ? defaultColor : DIMMED_COLOR);
  }
}
```

## Learn more

Check out the [MapTiler GeoSplats SDK reference](https://docs.maptiler.com/geosplats-sdk/api) to explore the full set of methods for `SplatAnnotation`.

## Related examples

### [Add annotations](https://docs.maptiler.com/geosplats-sdk/examples/add-annotations/)

Examples

Add interactive labelled pins and popups to a splat model.

### [Pin styles and categories](https://docs.maptiler.com/geosplats-sdk/examples/annotation-styles/)

Examples

Style annotations with colors, icons, numbers, and custom popup placement.

### [Track visited annotations](https://docs.maptiler.com/geosplats-sdk/examples/annotation-visited-state/)

Examples

Mark annotations as visited and update pin icons at runtime.

### [Sync annotations with sidebar](https://docs.maptiler.com/geosplats-sdk/examples/sync-annotations-sidebar/)

Examples

Connect 3D model annotations bidirectionally with an external sidebar list.
