Skip to main content
Eine Immobilienkarte mit Clustering in JavaScript erstellen
Tutorials

Eine Immobilienkarte mit Clustering in JavaScript erstellen

Erstellen Sie eine Immobilienlistenkarte mit Preismarkierungen, Clustering, Adresssuche und Preisbereichsfilter in reinem JavaScript oder React mit der MapAtlas Maps API.

Brent van der Heiden9 min read
#real estate map#property map#map clustering#javascript#proptech#maps api

Immobiliensuche ist ein räumliches Problem. Käufer und Mieter denken in Stadtvierteln, Pendelzeiten und der Nähe zu Schulen, nicht in Postleitzahlen und Straßenlisten. Eine Oberfläche, die die Karte in den Mittelpunkt stellt, passt dazu, wie Menschen suchen. Nutzer können ihre Suche so zuerst räumlich eingrenzen und danach nach Preis, Zimmerzahl oder anderen Merkmalen filtern.

Dieses Tutorial baut eine produktionsreife Karte für Immobilieninserate: geclusterte Marker bei niedrigem Zoom, Preisbeschriftungen an einzelnen Markern, ein Popup mit Inseratsdetails per Klick, eine Postleitzahlsuche, die die Karte neu zentriert, und einen Preisschieberegler, der die sichtbaren Inserate in Echtzeit filtert. Die React-Komponente selbst umfasst etwa 65 Zeilen, der vollständige Aufbau mit Layern und Klick-Handlern kommt auf rund 150. Dieselbe Logik funktioniert auch in reinem JavaScript, falls Sie auf ein Framework verzichten möchten.

Außerdem finden Sie einen Abschnitt zu den DSGVO-Pflichten für PropTech-Plattformen in der EU. Die Standortdaten, die bei Immobiliensuchen entstehen, sind personenbezogene Daten und müssen entsprechend behandelt werden.

Wenn Sie noch keine Erfahrung mit Karten-APIs haben, lesen Sie zuerst das Tutorial Interaktive Karten zu Ihrer Website hinzufügen. Es behandelt die Grundlagen, auf denen dieser Artikel aufbaut.

Datenstruktur für Immobilien

Jedes Inserat braucht Koordinaten, einen Preis und genug Metadaten für das Popup. Legen Sie die Daten als GeoJSON-FeatureCollection an. So lassen sie sich ohne Umwandlung direkt als Kartenquelle verwenden.

const properties = {
  type: "FeatureCollection",
  features: [
    {
      type: "Feature",
      geometry: { type: "Point", coordinates: [4.8952, 52.3702] },
      properties: {
        id: "prop-001",
        price: 485000,
        bedrooms: 3,
        sqm: 112,
        address: "Keizersgracht 142, Amsterdam",
        type: "apartment",
        status: "for-sale"
      }
    },
    {
      type: "Feature",
      geometry: { type: "Point", coordinates: [4.9123, 52.3601] },
      properties: {
        id: "prop-002",
        price: 1250,
        bedrooms: 2,
        sqm: 78,
        address: "Sarphatistraat 58, Amsterdam",
        type: "apartment",
        status: "for-rent"
      }
    },
    {
      type: "Feature",
      geometry: { type: "Point", coordinates: [4.8801, 52.3780] },
      properties: {
        id: "prop-003",
        price: 720000,
        bedrooms: 4,
        sqm: 195,
        address: "Herengracht 380, Amsterdam",
        type: "house",
        status: "for-sale"
      }
    }
  ]
};

Auf einer echten Plattform kommt dieses Array aus Ihrer Inserats-API, etwa über einen fetch-Aufruf beim Laden der Karte oder bei einer Änderung der Suchparameter.

Die Karte einrichten

Initialisieren Sie die Karte mit einer Ansicht, die Ihren Immobilienmarkt zentral abdeckt. Die MapAtlas Maps API ist mit Mapbox GL JS kompatibel. Der Initialisierungsaufruf ist daher identisch mit dem für Mapbox, nur mit einer Kachel-URL von MapAtlas.

import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';

const map = new mapmetricsgl.Map({
  container: 'map',
  style: 'https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/style.json&token=YOUR_API_KEY',
  center: [4.9041, 52.3676],
  zoom: 13
});

Ein heller, kontrastarmer Stil eignet sich besonders gut für Immobilienkarten: Auf dem hellen Untergrund heben sich Preisbeschriftungen und farbige Marker ab, ohne dass die Karte unruhig wirkt.

Clustering mit Preisbeschriftungen

Das zentrale UI-Muster einer Immobilienkarte sind Preisbeschriftungen an einzelnen Markern und Zählblasen an Clustern. Die Quellenoption cluster: true gruppiert nahe beieinanderliegende Punkte automatisch. Für Cluster und einzelne Marker legen Sie jeweils eigene Layer an.

map.on('load', () => {
  map.addSource('properties', {
    type: 'geojson',
    data: properties,
    cluster: true,
    clusterMaxZoom: 14,
    clusterRadius: 60
  });

  // Cluster-Kreise, Größe wächst mit der Anzahl der Inserate
  map.addLayer({
    id: 'clusters',
    type: 'circle',
    source: 'properties',
    filter: ['has', 'point_count'],
    paint: {
      'circle-color': '#2563EB',
      'circle-radius': [
        'step', ['get', 'point_count'],
        22, 5, 30, 20, 38
      ],
      'circle-stroke-width': 3,
      'circle-stroke-color': '#ffffff'
    }
  });

  // Beschriftungen mit der Anzahl pro Cluster
  map.addLayer({
    id: 'cluster-label',
    type: 'symbol',
    source: 'properties',
    filter: ['has', 'point_count'],
    layout: {
      'text-field': '{point_count_abbreviated}',
      'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
      'text-size': 14
    },
    paint: { 'text-color': '#ffffff' }
  });

  // Einzelne Immobilienmarker, weißer Kreis mit Preistext
  map.addLayer({
    id: 'property-price',
    type: 'symbol',
    source: 'properties',
    filter: ['!', ['has', 'point_count']],
    layout: {
      'text-field': [
        'concat',
        '€',
        ['to-string', ['round', ['/', ['get', 'price'], 1000]]],
        'k'
      ],
      'text-font': ['DIN Offc Pro Bold', 'Arial Unicode MS Bold'],
      'text-size': 12
    },
    paint: {
      'text-color': '#1e293b',
      'text-halo-color': '#ffffff',
      'text-halo-width': 2
    }
  });
});

Klick-Interaktionen

Ein Klick auf einen Cluster zoomt die Karte so weit hinein, dass die einzelnen Inserate sichtbar werden. Ein Klick auf eine einzelne Immobilie öffnet ein Popup mit Details.

// In den angeklickten Cluster zoomen
map.on('click', 'clusters', (e) => {
  const [feature] = map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
  map.getSource('properties').getClusterExpansionZoom(
    feature.properties.cluster_id,
    (err, zoom) => {
      if (!err) map.easeTo({ center: feature.geometry.coordinates, zoom });
    }
  );
});

// Detail-Popup für ein einzelnes Inserat
map.on('click', 'property-price', (e) => {
  const { address, price, bedrooms, sqm, status, id } = e.features[0].properties;
  const coords = e.features[0].geometry.coordinates.slice();
  const formattedPrice = status === 'for-rent'
    ? `€${price.toLocaleString()}/mo`
    : `€${price.toLocaleString()}`;

  new mapmetricsgl.Popup({ offset: 10 })
    .setLngLat(coords)
    .setHTML(`
      <div style="min-width:200px">
        <strong style="font-size:15px">${formattedPrice}</strong>
        <p style="margin:4px 0">${address}</p>
        <p style="margin:4px 0;color:#64748b">${bedrooms} bed · ${sqm} m²</p>
        <a href="/listings/${id}" style="color:#2563EB;font-weight:600">View listing →</a>
      </div>
    `)
    .addTo(map);
});

// Cursor beim Hovern ändern
map.on('mouseenter', 'property-price', () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', 'property-price', () => { map.getCanvas().style.cursor = ''; });
map.on('mouseenter', 'clusters',       () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mouseleave', 'clusters',       () => { map.getCanvas().style.cursor = ''; });

Preisfilter

Ein Schieberegler, der die sichtbaren Inserate filtert, gehört zu den wertvollsten UI-Ergänzungen einer Immobilienkarte. Mit dem Expression-System von Mapbox GL JS aktualisieren Sie den Filter clientseitig ohne Netzwerk-Roundtrip. Gefiltert wird direkt im Browser auf dem bereits geladenen GeoJSON.

function applyPriceFilter(min, max) {
  const filter = ['all',
    ['!', ['has', 'point_count']],
    ['>=', ['get', 'price'], min],
    ['<=', ['get', 'price'], max]
  ];
  map.setFilter('property-price', filter);
}

// Eingabefelder für den Preisbereich verbinden
const minInput = document.getElementById('price-min');
const maxInput = document.getElementById('price-max');

function onRangeChange() {
  applyPriceFilter(Number(minInput.value), Number(maxInput.value));
}

minInput.addEventListener('input', onRangeChange);
maxInput.addEventListener('input', onRangeChange);

Beachten Sie: Cluster werden aus den Quelldaten berechnet, nicht aus Layer-Filtern. Die Clusterzahlen enthalten daher weiterhin Inserate, die der Schieberegler ausgeblendet hat. Um beides synchron zu halten, filtern Sie die Inserate in JavaScript und übergeben das Ergebnis an map.getSource('properties').setData().

Adresssuche mit der Geocoding API

Lassen Sie Nutzer ein Stadtviertel oder eine Postleitzahl eingeben, um die Karte neu zu zentrieren. Die Geocoding API liefert GeoJSON-Features, die Koordinaten lassen sich also direkt an map.flyTo übergeben.

async function searchArea(query) {
  const url = new URL('https://gateway.mapmetrics-atlas.net/forward-geocode/');
  url.searchParams.set('text', query);
  url.searchParams.set('token', 'YOUR_API_KEY');
  url.searchParams.set('size', '1');

  const res  = await fetch(url);
  const data = await res.json();

  if (!data.features.length) return;

  const [lng, lat] = data.features[0].geometry.coordinates;
  map.flyTo({ center: [lng, lat], zoom: 13 });
}

document.getElementById('area-search').addEventListener('keydown', (e) => {
  if (e.key === 'Enter') searchArea(e.target.value.trim());
});

Für eine Suche nach Fahrzeit, etwa „Zeige mir Immobilien, die höchstens 20 Minuten von dieser Schule entfernt sind“, ergänzen Sie ein Isochronen-Overlay über die MapAtlas Travel Time API. Wie Sie dieses Polygon abrufen und anzeigen, zeigt der Artikel Isochronenkarten erklärt.

Die vollständige React-Komponente

So fassen Sie alles Obige in einer React-Komponente mit sauberem Lifecycle-Management zusammen:

import { useEffect, useRef, useState } from 'react';
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';

export function PropertyMap({ listings, apiKey }) {
  const containerRef = useRef(null);
  const mapRef       = useRef(null);
  const [priceRange, setPriceRange] = useState([0, 2000000]);

  useEffect(() => {
    const map = new mapmetricsgl.Map({
      container: containerRef.current,
      style: `https://gateway.mapmetrics-atlas.net/styles/?fileName=YOUR_STYLE_ID/style.json&token=${apiKey}`,
      center: [4.9041, 52.3676],
      zoom: 13
    });
    mapRef.current = map;

    map.on('load', () => {
      map.addSource('properties', {
        type: 'geojson',
        data: { type: 'FeatureCollection', features: listings },
        cluster: true,
        clusterMaxZoom: 14,
        clusterRadius: 60
      });
      // Layer clusters, cluster-label und property-price hinzufügen (siehe oben)
      // Klick-Handler hinzufügen (siehe oben)
    });

    return () => map.remove();
  }, [apiKey]);

  // Filter aktualisieren, wenn sich der Preisschieberegler ändert
  useEffect(() => {
    const map = mapRef.current;
    if (!map || !map.getLayer('property-price')) return;
    map.setFilter('property-price', [
      'all',
      ['!', ['has', 'point_count']],
      ['>=', ['get', 'price'], priceRange[0]],
      ['<=', ['get', 'price'], priceRange[1]]
    ]);
  }, [priceRange]);

  return (
    <div>
      <div style={{ padding: '12px 0', display: 'flex', gap: 12 }}>
        <label>
          Min €
          <input type="range" min={0} max={2000000} step={10000}
            value={priceRange[0]}
            onChange={e => setPriceRange([+e.target.value, priceRange[1]])} />
        </label>
        <label>
          Max €
          <input type="range" min={0} max={2000000} step={10000}
            value={priceRange[1]}
            onChange={e => setPriceRange([priceRange[0], +e.target.value])} />
        </label>
      </div>
      <div ref={containerRef} style={{ width: '100%', height: '600px' }} />
    </div>
  );
}

Das ist das Grundgerüst der Komponente, etwa 65 Zeilen einschließlich Preisschieberegler und Lifecycle-Cleanup. Ergänzen Sie den Code für Layer und Klick-Handler aus den Abschnitten oben, um sie zu vervollständigen.

DSGVO-Hinweise für PropTech in der EU

Gibt ein Immobiliensuchender auf Ihrer Karte seine Wohnadresse ein oder klickt auf „Meinen Standort verwenden“, teilt er personenbezogene Daten im Sinne von Art. 4 Nr. 1 DSGVO, der Standortdaten ausdrücklich als Kennung nennt. Diese Standortdaten verraten, wo die Person wohnt, und lassen unter Umständen Rückschlüsse auf weitere sensible Merkmale zu.

Praktische Schritte für die Einhaltung der DSGVO:

  • Leiten Sie Geocoding-Anfragen über Ihr Backend, statt die Geocoding API direkt aus dem Browser aufzurufen. Ihr Backend kann identifizierende Header entfernen, bevor es die Anfrage weiterleitet.
  • Protokollieren oder speichern Sie Suchkoordinaten nicht über die Sitzung hinaus, es sei denn, Sie haben eine dokumentierte Rechtsgrundlage nach Art. 6 DSGVO, etwa eine Einwilligung.
  • Bieten Sie eine Funktion wie „Benachrichtigen, wenn eine Immobilie in meiner Nähe meine Kriterien erfüllt“ an, behandeln Sie das gespeicherte Suchgebiet als personenbezogene Daten mit festgelegter Speicherdauer.

Der Leitfaden für EU-Entwickler zu DSGVO-konformen Karten-APIs behandelt das Thema vollständig, einschließlich Auftragsverarbeitungsverträgen und Anforderungen an den Datenstandort. MapAtlas verarbeitet Daten innerhalb der EU und stellt den Auftragsverarbeitungsvertrag bereit, den Art. 28 Abs. 3 DSGVO zwischen Verantwortlichem und Auftragsverarbeiter vorschreibt.

Weiteren Kontext dazu, wie PropTech-Plattformen MapAtlas im Produktivbetrieb einsetzen, bietet die Lösungsseite für die Immobilienbranche.

Nächste Schritte

Häufig gestellte Fragen

Kann ich MapAtlas für eine Immobilien-Website mit Inseraten nutzen?

Ja. MapAtlas deckt ab, was Immobilienportale brauchen: Karten zur Immobiliensuche, Investment-Dashboards und Karten für Mietangebote. Die Maps API ist mit Mapbox GL JS kompatibel, sodass sich bestehender Mapbox-basierter Code für Immobilienkarten mit minimalen Änderungen migrieren lässt.

Wie funktioniert Marker-Clustering bei Immobilienkarten?

Clustering fasst nahe beieinanderliegende Immobilienmarker zu einem einzelnen Kreis mit ihrer Anzahl zusammen, wenn die Karte herausgezoomt ist. Zoomt der Nutzer hinein, lösen sich die Cluster in einzelne Marker auf. MapAtlas unterstützt Clustering für GeoJSON-Quellen nativ: Setzen Sie cluster: true auf der Quelle, den Rest übernimmt das SDK.

Unterliegen Standortdaten von Immobiliensuchenden der DSGVO?

Ja. Sucht ein Nutzer auf einer Immobilienseite nach seiner Wohnadresse oder seinem aktuellen Standort, handelt es sich um personenbezogene Daten nach Art. 4 DSGVO. PropTech-Plattformen in der EU sollten Geocoding-Anfragen über einen Backend-Proxy leiten und Richtlinien zur Datenminimierung und Speicherdauer anwenden, statt clientseitige API-Aufrufe zu machen, die Nutzerstandorte auf Servern von Drittanbietern protokollieren.

Häufig gestellte Fragen

Kann ich MapAtlas für eine Immobilien-Website mit Inseraten nutzen?

Ja. MapAtlas deckt ab, was Immobilienportale brauchen: Karten zur Immobiliensuche, Investment-Dashboards und Karten für Mietangebote. Die Maps API ist mit Mapbox GL JS kompatibel, sodass sich bestehender Mapbox-basierter Code für Immobilienkarten mit minimalen Änderungen migrieren lässt.

Wie funktioniert Marker-Clustering bei Immobilienkarten?

Clustering fasst nahe beieinanderliegende Immobilienmarker zu einem einzelnen Kreis mit ihrer Anzahl zusammen, wenn die Karte herausgezoomt ist. Zoomt der Nutzer hinein, lösen sich die Cluster in einzelne Marker auf. MapAtlas unterstützt Clustering für GeoJSON-Quellen nativ: Setzen Sie cluster: true auf der Quelle, den Rest übernimmt das SDK.

Unterliegen Standortdaten von Immobiliensuchenden der DSGVO?

Ja. Sucht ein Nutzer auf einer Immobilienseite nach seiner Wohnadresse oder seinem aktuellen Standort, handelt es sich um personenbezogene Daten nach Art. 4 DSGVO. PropTech-Plattformen in der EU sollten Geocoding-Anfragen über einen Backend-Proxy leiten und Richtlinien zur Datenminimierung und Speicherdauer anwenden, statt clientseitige API-Aufrufe zu machen, die Nutzerstandorte auf Servern von Drittanbietern protokollieren.

Hat Ihnen das geholfen? Teilen Sie es.

Über den Autor

Brent van der Heiden

Verfasst von

Brent van der Heiden

Co-Founder & CEO at MapAtlas

Brent built MapAtlas out of a conviction that developers deserve location APIs with fair pricing and genuine end-user privacy. Before that he co-founded MapMetrics, a community mapping and navigation app. He writes about geospatial infrastructure, map data, AI search visibility, and how location data powers the products people rely on every day.

Alle Artikel ansehen →
Zurück zum Blog