Source: bemap-maplibre/bemap-maplibre-v5/bemap-map-maplibre-heatmap.js

/**
 * BeNomad BeMap JavaScript API - MapLibre v5 - Heatmap
 *
 * Usage:
 *   var heatmap = new bemap.HeatmapLayer({
 *     name: 'earthquakes',
 *     points: [
 *       { lon: 2.35, lat: 48.85, weight: 5 },
 *       { lon: 7.26, lat: 43.71, weight: 3 }
 *     ],
 *     radius: 25,
 *     intensity: 1.5,
 *     colors: ['rgba(0,0,255,0)', 'royalblue', 'cyan', 'lime', 'yellow', 'red']
 *   });
 *   map.addHeatmap(heatmap);
 */

bemap.MapLibreMap.prototype.addHeatmap = function(layer, options) {
  if (!layer || !bemap.inheritsof(layer, bemap.HeatmapLayer)) return this;

  var _this = this;
  this._addOwnToProperties(layer);

  var sourceId = layer._bemapId + '-heatmap-src';
  var layerId = layer._bemapId + '-heatmap';
  layer._maplibreSourceId = sourceId;
  layer._maplibreLayerId = layerId;

  // Build GeoJSON FeatureCollection from points
  var features = [];
  for (var i = 0; i < layer.points.length; i++) {
    var pt = layer.points[i];
    features.push({
      type: 'Feature',
      geometry: { type: 'Point', coordinates: [pt.lon, pt.lat] },
      properties: { weight: pt.weight || pt[layer.weightProperty] || 1 }
    });
  }

  var geojsonData = { type: 'FeatureCollection', features: features };

  // Build color ramp
  var colorRamp = ['interpolate', ['linear'], ['heatmap-density']];
  var colors = layer.colors;
  for (var c = 0; c < colors.length; c++) {
    colorRamp.push(c / (colors.length - 1));
    colorRamp.push(colors[c]);
  }

  var addToMap = function() {
    if (!_this.native.getSource(sourceId)) {
      _this.native.addSource(sourceId, { type: 'geojson', data: geojsonData });
      _this.native.addLayer({
        id: layerId,
        type: 'heatmap',
        source: sourceId,
        paint: {
          'heatmap-weight': ['interpolate', ['linear'], ['get', 'weight'], 0, 0, 10, 1],
          'heatmap-intensity': ['interpolate', ['linear'], ['zoom'], 0, layer.intensity, 20, layer.intensity * 3],
          'heatmap-color': colorRamp,
          'heatmap-radius': ['interpolate', ['linear'], ['zoom'], 0, 2, 20, layer.radius],
          'heatmap-opacity': layer.opacity
        }
      });
    }
  };

  if (this.native.isStyleLoaded()) { addToMap(); } else { this.native.on('load', addToMap); }

  layer.native = { sourceId: sourceId, layerId: layerId };
  if (layer.map === null) layer.map = this;

  var _self = this;
  if (typeof this._registerEffect === 'function') {
    this._registerEffect('heatmap:' + layer._bemapId, function () { _self.removeHeatmap(layer); });
  }

  return this;
};

bemap.MapLibreMap.prototype.removeHeatmap = function(layer) {
  if (layer && layer.native) {
    try {
      if (this.native.getLayer(layer._maplibreLayerId)) this.native.removeLayer(layer._maplibreLayerId);
      if (this.native.getSource(layer._maplibreSourceId)) this.native.removeSource(layer._maplibreSourceId);
    } catch(e) {}
    if (typeof this._unregisterEffect === 'function' && layer._bemapId) {
      this._unregisterEffect('heatmap:' + layer._bemapId);
    }
    if (layer._bemapId) delete this._featureRegistry[layer._bemapId];
    layer.map = null;
  }
  return this;
};

bemap.MapLibreMap.prototype.updateHeatmap = function(layer, data) {
  if (layer && layer._maplibreSourceId) {
    var features = [];
    for (var i = 0; i < data.length; i++) {
      var pt = data[i];
      features.push({
        type: 'Feature',
        geometry: { type: 'Point', coordinates: [pt.lon, pt.lat] },
        properties: { weight: pt.weight || 1 }
      });
    }
    var src = this.native.getSource(layer._maplibreSourceId);
    if (src) src.setData({ type: 'FeatureCollection', features: features });
    layer.points = data;
  }
  return this;
};