Skip to content
Vue Neshan MapsDeveloper documentation

Weighted heatmaps

Visualize density with an independent, unclustered weighted source.

3 min readVue 3 / Nuxt 4Live example included
On this pageComplete example: clusters and heatmap
Interactive exampleMapLibre

Run on a real Neshan map

Bring a domain-allowed web key. Sample geometry; real basemap.

Allow sajadjanat.github.io. Your key stays in memory and is excluded from copied code.

Complete example: clusters and heatmap#

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

ts
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.

Independent community library. Map services and attribution belong to Neshan and OpenStreetMap. · Sajad Janat · MIT