Skip to main content
Bangun Peta Properti Real Estate dengan Clustering di JavaScript
Tutorials

Bangun Peta Properti Real Estate dengan Clustering di JavaScript

Bangun peta daftar properti dengan penanda label harga, clustering, pencarian alamat, dan filter rentang harga dalam JavaScript murni atau React menggunakan MapAtlas Maps API.

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

Pencarian properti adalah masalah spasial. Pembeli dan penyewa berpikir dalam hal lingkungan, waktu perjalanan, dan kedekatan dengan sekolah, bukan kode pos dan daftar jalan. Antarmuka berbasis peta sesuai dengan cara orang mencari, karena memungkinkan pengguna mendalami pencarian mereka secara spasial sebelum menyaring berdasarkan harga, kamar tidur, atau atribut lainnya.

Tutorial ini membangun peta listing properti siap produksi: penanda bergerombol pada zoom rendah, label harga pada penanda individual, klik ke popup dengan detail listing, pencarian kode pos yang memusatkan ulang peta, dan slider rentang harga yang menyaring listing yang terlihat secara real-time. Komponen React-nya sendiri sekitar 65 baris, dan versi lengkap dengan layer serta click handler mencapai sekitar 150 baris. Logika yang sama berfungsi dalam JavaScript vanila jika Anda lebih suka tanpa framework.

Anda juga akan menemukan catatan tentang kewajiban GDPR khusus untuk platform PropTech UE, karena data lokasi yang dihasilkan oleh pencarian properti adalah data pribadi dan perlu ditangani sesuai dengan itu.

Jika Anda baru mengenal API peta, tutorial Cara Menambahkan Peta Interaktif ke Situs Web Anda mencakup dasar-dasar sebelum yang ini dimulai.

Property Data Structure

Each listing needs coordinates, a price, and enough metadata for the popup. Keep this as a GeoJSON FeatureCollection, it plugs directly into the map source without transformation.

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"
      }
    }
  ]
};

In a real platform this array comes from your listings API, a fetch call on map load or a query parameter change.

Setting Up the Map

Initialize the map with a central view over your property market. The MapAtlas Maps API is Mapbox GL JS-compatible, so the initialisation call is identical to what you would write for Mapbox, with a MapAtlas tile URL.

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
});

Gaya yang terang dengan kontras rendah sangat cocok untuk peta properti, karena dasar yang lebih terang membuat label harga dan penanda berwarna menonjol tanpa kesan berantakan.

Clustering with Price Labels

The key UI pattern for a property map is showing price labels on individual markers and count bubbles on cluster circles. The cluster: true source option groups nearby points automatically. You add separate layers for clusters and individual markers.

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

  // Cluster circles, size scales with listing count
  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'
    }
  });

  // Cluster count labels
  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' }
  });

  // Individual property markers, white circle with price text
  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
    }
  });
});

Click Interactions

Clicking a cluster zooms the map to reveal its individual listings. Clicking an individual property opens a detail popup.

// Zoom into clicked cluster
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 for individual listing
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);
});

// Change cursor on hover
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 = ''; });

Price Range Filter

Slider rentang yang menyaring listing yang terlihat adalah salah satu tambahan UI paling bernilai untuk peta properti. Sistem expression Mapbox GL JS memungkinkan Anda memperbarui filter sebuah layer di sisi klien tanpa round-trip jaringan, dan penyaringan berlangsung di browser pada GeoJSON yang sudah dimuat.

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

// Wire up range inputs
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);

Perhatikan bahwa cluster dihitung dari data source, bukan dari filter layer, sehingga jumlah pada cluster tetap menyertakan listing yang disembunyikan oleh slider. Agar keduanya tetap sinkron, saring listing di JavaScript lalu teruskan hasilnya ke map.getSource('properties').setData().

Address Search with the Geocoding API

Let users type a neighbourhood or postcode to recentre the map. The Geocoding API returns GeoJSON features, so coordinates drop straight into map.flyTo.

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());
});

For travel-time search, "show me properties within 20 minutes of this school", add an isochrone overlay using the MapAtlas Travel Time API. The Isochrone Maps Explained article shows how to fetch and display that polygon.

The Complete React Component

Wrapping everything above into a React component with proper lifecycle management:

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
      });
      // Add cluster, cluster-label, property-price layers (see above)
      // Add click handlers (see above)
    });

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

  // Update filter when price range slider changes
  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>
  );
}

Itulah kerangka komponennya, sekitar 65 baris termasuk slider rentang harga dan pembersihan lifecycle; tambahkan kode layer dan click handler dari bagian-bagian di atas untuk melengkapinya.

GDPR Notes for EU PropTech

Saat pencari properti mengetik alamat rumahnya atau mengklik "gunakan lokasi saya" di peta Anda, ia membagikan data pribadi menurut Pasal 4(1) GDPR, yang menyebut data lokasi sebagai pengenal. Data lokasi itu mengungkap tempat tinggalnya, dan dari situ atribut sensitif lain tentang dirinya bisa disimpulkan.

Practical steps for EU compliance:

  • Route geocoding requests through your backend rather than calling the Geocoding API directly from the browser. Your backend can strip identifying headers before forwarding.
  • Jangan mencatat atau menyimpan koordinat pencarian melewati sesi, kecuali Anda memiliki dasar hukum yang terdokumentasi berdasarkan Pasal 6 GDPR, misalnya persetujuan.
  • If you implement "notify me when a property near me matches my criteria", treat the stored search area as personal data with a defined retention period.

Panduan Developer UE untuk Map API yang Patuh GDPR membahas topik ini secara lengkap, termasuk perjanjian pemroses dan persyaratan residensi data. MapAtlas memproses data di dalam UE dan menyediakan Data Processing Agreement yang diwajibkan Pasal 28(3) GDPR antara pengendali dan pemroses.

The Real Estate industry solution page has additional context on how PropTech platforms use MapAtlas in production.

Next Steps

Frequently Asked Questions

Can I use MapAtlas for a property listing website?

Ya. MapAtlas mencakup apa yang dibutuhkan situs properti: peta pencarian properti, dashboard investasi, dan peta listing sewa. Maps API kompatibel dengan Mapbox GL JS, sehingga kode peta properti berbasis Mapbox yang sudah ada dapat dimigrasikan dengan perubahan minimal.

How does marker clustering work for real estate maps?

Clustering groups nearby property markers into a single circle showing the count when the map is zoomed out. As the user zooms in, clusters split into individual markers. MapAtlas supports GeoJSON source clustering natively, set cluster: true on the source and the SDK handles the rest.

Is location data from property searchers subject to GDPR?

Yes. When a user searches by their home address or current location on a property site, that constitutes personal data under GDPR Article 4. EU PropTech platforms should route geocoding requests through a backend proxy and apply appropriate data minimisation and retention policies rather than making client-side API calls that log user locations to third-party servers.

Merasa ini berguna? Bagikan.

Tentang penulis

Brent van der Heiden

Ditulis oleh

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.

Lihat semua artikel →
Kembali ke blog