Weighted heatmaps
Visualize density with an independent, unclustered weighted source.
On this page
Complete example: clusters and heatmapComplete example: clusters and heatmap#
<script setup lang="ts">
import { ref } from 'vue'
import { NeshanMapLibre, type Coordinates, type MapResourceFailure } from 'vue-neshan-maps'
import 'vue-neshan-maps/style.css'
const webKey = import.meta.env.VITE_NESHAN_WEB_KEY
const center = ref<Coordinates>([35.72, 51.36])
const zoom = ref(12)
const clustered = ref(true), heat = ref(false)
const map = ref<InstanceType<typeof NeshanMapLibre>>()
const failure = ref<MapResourceFailure>()
const data = ref<GeoJSON.FeatureCollection>({
type: 'FeatureCollection',
features: Array.from({ length: 1000 }, (_, id) => ({
type: 'Feature', id, properties: { weight: 1 + id % 5 },
geometry: { type: 'Point', coordinates: [51.36 + Math.sin(id * 2.4) * 0.05, 35.72 + Math.cos(id * 1.7) * 0.04] },
})),
})
const clusterOptions = { radius: 55, maxZoom: 14, minPoints: 2, expandOnClick: true }
const heatOptions = { weightProperty: 'weight', radius: 30, intensity: 0.9, opacity: 0.65 }
function replaceData() {
data.value = { ...data.value, features: data.value.features.slice(0, 500) }
}
</script>
<template>
<label><input v-model="clustered" type="checkbox">Clusters</label>
<label><input v-model="heat" type="checkbox">Heatmap</label>
<button @click="replaceData">Keep 500 sample points</button>
<p v-if="failure" role="alert">{{ failure.kind }}: {{ failure.message }}</p>
<NeshanMapLibre ref="map" v-model="center" v-model:zoom="zoom" :api-key="webKey"
:geojson="data" :cluster="clustered ? clusterOptions : false"
:heatmap="heat ? heatOptions : false" traffic poi :center-marker="false"
:styles="{ height: '550px' }" @resource-error="failure = $event" />
</template>The generated coordinates are sample data, not locations returned by Neshan. Vue public coordinates are [latitude, longitude]; GeoJSON coordinates are [longitude, latitude].
Clustering affects points in geojson, not DOM markers in markers. To cluster many places, represent them as GeoJSON points. geojsonOptions.cluster still works; an explicitly supplied cluster prop takes precedence. A radius is in screen pixels. maxZoom is the last zoom at which clustering occurs. minPoints defaults to 2. Colors can be configured through color, pointColor, textColor.
Clicking a cluster emits cluster-click with { clusterId, pointCount, coordinates } and expands it by default. Set expandOnClick: false for your own interaction. Methods return promises:
const leaves = await map.value!.getClusterLeaves(clusterId, 20, 0)
const expansionZoom = await map.value!.expandCluster(clusterId, [35.72, 51.36])Cluster IDs belong to the current worker data and may change after replacing data or rebuilding a style. Wait for the source to finish loading before querying a newly created cluster. The optional center is latitude first. Leaves use GeoJSON coordinates and properties.
Heatmap weights read weightProperty; missing or nonnumeric values become 0, negative values are clamped to 0. Without that option all points have weight 1. Defaults: radius 24px, intensity 1, opacity 0.8, maxZoom 22. A heatmap uses a separate unclustered source so it can be displayed together with clusters. It adds a second worker copy of the data; for very large datasets enable only the visualization you need. geojsonLayers replaces the ordinary point/line/fill styles; generated heatmap and cluster layers are added separately.