cesium 热力图

cesium 热力图

什么是热力图

热力图,是一种通过对色块着色来显示数据的统计图表

热力图在各领域用途

  • 一场世界杯足球竞赛----评委们通常利用热图了解到冠军队伍中门将、后卫、中场和前锋的跑位,让我们一目了然地看到多名球员在比赛中跑位的差异
  • 生物学热图----通常用在分子生物学范畴,可以显示从DNA微阵列获得的大量可比较样本(不同状态下的细胞、不同患者的样本)中的很多基因的表达水平
  • 天气、地震预测----气象局还可利用热图判断地震震源位置,可清楚看出哪些地方是地震高发区(频率最高)

heatmap.js

www.patrick-wied.at/static/heat...

使用heatmap.js库可以快速生成热力图

cesium中结合heatmap集成热力图

heatmap的使用很简单,但是他是在一个二维平面区域内绘制热力图,x,y是二维平面内的坐标,比如有个200*200的区域,x:10,y:15就是在此位置添加一个热力。但是cesium中是GIS,使用是的WGS84坐标系,而且使用经纬度进行标记位置,所以这里需要一个转换,转换好后用heatmap生成热力图,然后取出热力图图片,在cesium中绘制一个多边形,贴图采用热力图图片,就将热力图结合到cesium模型、地形上了。

坐标转换流程(重点)

  1. 先确定要绘制热力图的区域

    js 复制代码
    // 要绘制热力图区域数据
    const polygon = [
      { longitude: 120.68281005, latitude: 30.51066356 },
      { longitude: 120.68231251, latitude: 30.51353815 },
      { longitude: 120.69327431, latitude: 30.51833967 },
      { longitude: 120.69607266, latitude: 30.51186293 }
    ];
    const bpoint = [];
    for (const coord of polygon) {
      bpoint.push(new Cartographic(coord.longitude, coord.latitude, 0));
    }
    // 创建尽可能小的Rectangle,该矩形包含所提供数组中的所有位置
    const bound = Rectangle.fromCartographicArray(bpoint);
  2. 将WGS84边界框转换为墨卡托边界框(即可理解成将经纬度坐标转换成平面xy坐标)

    js 复制代码
    /**
    * 将WGS84边界框转换为墨卡托边界框(即可理解成将经纬度坐标转换成平面xy坐标)
    * @param {*} bb WGS84边界框,如{north, east, south, west}
    * @returns
    */
    CesiumHeatmap.wgs84ToMercatorBB = function (bb) {
      const WMP = new Cesium.WebMercatorProjection();
      const sw = WMP.project(Cesium.Cartographic.fromDegrees(bb.west, bb.south));
      const ne = WMP.project(Cesium.Cartographic.fromDegrees(bb.east, bb.north));
      return {
        north: ne.y,
        east: ne.x,
        south: sw.y,
        west: sw.x
      };
    };
  3. 根据墨卡托边界框计算宽高

    js 复制代码
    /**
     * 根据墨卡托边界框计算宽高
    * @param {*} mbb 墨卡托边界框
    */
    _setWidthAndHeight(mbb) {
      const width = mbb.east > 0 && mbb.west < 0 ? mbb.east + Math.abs(mbb.west) : Math.abs(mbb.east - mbb.west);
      const height = mbb.north > 0 && mbb.south < 0 ? mbb.north + Math.abs(mbb.south) : Math.abs(mbb.north - mbb.south);
      this.width = Math.round(width);
      this.height = Math.round(height);
    }
  4. 根据宽高生成DOM渲染区域

    js 复制代码
    /**
     * 获取动态生成的DOM渲染区域
    * @param {*} width 宽度
    * @param {*} height 高度
    * @returns DOM
    */
    CesiumHeatmap._getContainer = function (width, height) {
      const div = document.createElement('div');
      div.setAttribute('id', 'heatmap-cesium-div');
      div.setAttribute('style', 'width: ' + width + 'px; height: ' + height + 'px; margin: 0px; display: none;');
      document.body.appendChild(div);
      return div;
    };
  5. 创建热力图h337

    js 复制代码
    this._heatmap = h337.create(this._options);
  6. 生成热力图heatmap库所需的数据

js 复制代码
/**
 * 将WGS84坐标点转换成热力图上的点
 * @param {*} p WGS84经纬度坐标点数据
 * @returns
 */
wgs84PointToHeatmapPoint(p) {
  return this.mercatorPointToHeatmapPoint(CesiumHeatmap.wgs84ToMercator(p));
}

/**
 * 将WGS84位置转换为墨卡托位置
 * @param {*} p WGS84位置 如{lon: longitude, lat: latitude}
 * @returns
 */
CesiumHeatmap.wgs84ToMercator = function (p) {
  const WMP = new Cesium.WebMercatorProjection();
  const mp = WMP.project(Cesium.Cartographic.fromDegrees(p.lon, p.lat));
  return {
    x: mp.x,
    y: mp.y
  };
};

/**
 * 将墨卡托坐标点转换成热力图上的点
 * @param {*} p 墨卡托坐标点数据
 * @returns
 */
mercatorPointToHeatmapPoint(p) {
  var pn = {};

  pn.x = Math.round(p.x - this._mbounds.west);
  pn.y = Math.round(p.y - this._mbounds.south);
  pn.y = this.height - pn.y;

  return pn;
}

其基本思路就是:将热力值的WGS坐标转换成墨卡托坐标,然后与之前的墨卡托边界框计算得到x,y,然后就可以使用heatmap库进行生成热力图

cesiumHeatMap封装

将上述代码进行封装

js 复制代码
import * as Cesium from 'cesium';
import h337 from './heatmap.js';

var CesiumHeatmap = {
  defaults: {
    maxOpacity: 0.8, // 如果在热图选项对象中没有给出,则使用的最大不透明度
    minOpacity: 0.1, // 如果在热图选项对象中没有给出,则使用的最小不透明度
    blur: 0.85, // 如果在热图选项对象中没有给出,则使用的模糊
    gradient: {
      '.3': 'blue',
      '.65': 'yellow',
      '.8': 'orange',
      '.95': 'red'
    } // 如果热图选项对象中没有给出,则使用的梯度
  }
};

/**
 * 创建CesiumHeatmap实例
 * @param {*} bound
 * @param {*} options
 * @returns
 */
CesiumHeatmap.create = function (bound, options) {
  var instance = new HeatmapInstance(bound, options);
  return instance;
};

/**
 * 获取动态生成的DOM渲染区域
 * @param {*} width 宽度
 * @param {*} height 高度
 * @returns DOM
 */
CesiumHeatmap._getContainer = function (width, height) {
  const div = document.createElement('div');
  div.setAttribute('id', 'heatmap-cesium-div');
  div.setAttribute('style', 'width: ' + width + 'px; height: ' + height + 'px; margin: 0px; display: none;');
  document.body.appendChild(div);
  return div;
};

/**
 * 删除动态生成的DOM渲染区域
 */
CesiumHeatmap._removeContainer = function () {
  let div = document.getElementById('heatmap-cesium-div');
  document.body.removeChild(div);
};

/**
 * 将WGS84位置转换为墨卡托位置
 * @param {*} p WGS84位置 如{lon: longitude, lat: latitude}
 * @returns
 */
CesiumHeatmap.wgs84ToMercator = function (p) {
  const WMP = new Cesium.WebMercatorProjection();
  const mp = WMP.project(Cesium.Cartographic.fromDegrees(p.lon, p.lat));
  return {
    x: mp.x,
    y: mp.y
  };
};

/**
 * 将WGS84边界框转换为墨卡托边界框(即可理解成将经纬度坐标转换成平面xy坐标)
 * @param {*} bb WGS84边界框,如{north, east, south, west}
 * @returns
 */
CesiumHeatmap.wgs84ToMercatorBB = function (bb) {
  const WMP = new Cesium.WebMercatorProjection();
  const sw = WMP.project(Cesium.Cartographic.fromDegrees(bb.west, bb.south));
  const ne = WMP.project(Cesium.Cartographic.fromDegrees(bb.east, bb.north));
  return {
    north: ne.y,
    east: ne.x,
    south: sw.y,
    west: sw.x
  };
};

class HeatmapInstance {
  constructor(bound, heatmapOptions) {
    if (!bound) return null;
    if (!heatmapOptions) heatmapOptions = {};

    this._options = heatmapOptions;

    this._options.gradient = this._options.gradient ? this._options.gradient : CesiumHeatmap.defaults.gradient;
    this._options.maxOpacity = this._options.maxOpacity ? this._options.maxOpacity : CesiumHeatmap.defaults.maxOpacity;
    this._options.minOpacity = this._options.minOpacity ? this._options.minOpacity : CesiumHeatmap.defaults.minOpacity;
    this._options.blur = this._options.blur ? this._options.blur : CesiumHeatmap.defaults.blur;

    this._mbounds = CesiumHeatmap.wgs84ToMercatorBB(bound);
    this._setWidthAndHeight(this._mbounds);

    this._options.radius = Math.round(
      this._options.radius ? this._options.radius : this.width > this.height ? this.width : this.height
    );
    this._options.container = CesiumHeatmap._getContainer(this.width, this.height);

    this._heatmap = h337.create(this._options);
  }

  /**
   * 根据墨卡托边界框计算宽高
   * @param {*} mbb 墨卡托边界框
   */
  _setWidthAndHeight(mbb) {
    const width = mbb.east > 0 && mbb.west < 0 ? mbb.east + Math.abs(mbb.west) : Math.abs(mbb.east - mbb.west);
    const height = mbb.north > 0 && mbb.south < 0 ? mbb.north + Math.abs(mbb.south) : Math.abs(mbb.north - mbb.south);
    this.width = Math.round(width);
    this.height = Math.round(height);
  }

  /**
   * 设置WGS84位置数组
   * @param {*} min 数据值允许的最小值
   * @param {*} max 数据值允许的最大值
   * @param {*} data WGS84坐标下的数据点数组,值如{lon, lat, value}
   * @returns 返回热力图生成后的图片
   */
  setWGS84Data(min, max, data) {
    if (!(data && data.length > 0 && min !== null && min !== false && max !== null && max !== false)) return false;

    var convdata = [];
    for (var i = 0; i < data.length; i++) {
      var gp = data[i];

      var hp = this.wgs84PointToHeatmapPoint(gp);
      if (gp.value || gp.value === 0) {
        hp.value = gp.value;
      }

      convdata.push(hp);
    }

    this._heatmap.setData({
      min: min,
      max: max,
      data: convdata
    });

    CesiumHeatmap._removeContainer();

    return this._heatmap.getDataURL();
  }

  /**
   * 将WGS84坐标点转换成热力图上的点
   * @param {*} p WGS84经纬度坐标点数据
   * @returns
   */
  wgs84PointToHeatmapPoint(p) {
    return this.mercatorPointToHeatmapPoint(CesiumHeatmap.wgs84ToMercator(p));
  }

  /**
   * 将墨卡托坐标点转换成热力图上的点
   * @param {*} p 墨卡托坐标点数据
   * @returns
   */
  mercatorPointToHeatmapPoint(p) {
    var pn = {};

    pn.x = Math.round(p.x - this._mbounds.west);
    pn.y = Math.round(p.y - this._mbounds.south);
    pn.y = this.height - pn.y;

    return pn;
  }
}

export { CesiumHeatmap };

写一个例子调用

js 复制代码
// 热力图数据
const heatPoints = [
  { lon: 120.69530288, lat: 30.51613188, value: 10 },
  { lon: 120.68330288, lat: 30.51213188, value: 50 },
  { lon: 120.68350288, lat: 30.51413188, value: 20 },
  { lon: 120.68830288, lat: 30.51213188, value: 10 }
];

// 要绘制热力图区域数据
const box = [
  { longitude: 120.68281005, latitude: 30.51066356 },
  { longitude: 120.68231251, latitude: 30.51353815 },
  { longitude: 120.69327431, latitude: 30.51833967 },
  { longitude: 120.69607266, latitude: 30.51186293 }
];
const points = [];
const bpoint = [];
for (const coord of box) {
  points.push(coord.longitude, coord.latitude);
  bpoint.push(new Cartographic(coord.longitude, coord.latitude, 0));
}
// 创建尽可能小的 Rectangle,该矩形包含所提供数组中的所有位置
const bound = Rectangle.fromCartographicArray(bpoint);

const heatMap = CesiumHeatmap.create(bound, {
  // heatmap相应参数
  backgroundColor: 'rgba(0,0,0,0)',
  radius: 150,
  maxOpacity: 0.5,
  minOpacity: 0,
  blur: 0.85
});

const img = heatMap.setWGS84Data(0, 60, heatPoints);
this.app.viewerCesium.entities.add({
  polygon: {
    hierarchy: Cartesian3.fromDegreesArray(points), // 多边形
    material: new ImageMaterialProperty({
      image: img
    })
  }
});

heatmap.js 代码修改

如果你采用es6模块化的方式引入heatmap,运行时会报错

看看官方的issues给出的解决方案github.com/pa7/heatmap...

其实并没有解决,这个热力图的库上次维护还是7年前。所以我的解决方案就是,拷贝heatmap.js到本地,删除相关无用代码,以下是代码

js 复制代码
// Heatmap Config stores default values and will be merged with instance config
var HeatmapConfig = {
  defaultRadius: 40,
  defaultRenderer: 'canvas2d',
  defaultGradient: { 0.25: 'rgb(0,0,255)', 0.55: 'rgb(0,255,0)', 0.85: 'yellow', 1.0: 'rgb(255,0,0)' },
  defaultMaxOpacity: 1,
  defaultMinOpacity: 0,
  defaultBlur: 0.85,
  defaultXField: 'x',
  defaultYField: 'y',
  defaultValueField: 'value',
  plugins: {}
};
var Store = (function StoreClosure() {
  var Store = function Store(config) {
    this._coordinator = {};
    this._data = [];
    this._radi = [];
    this._min = 10;
    this._max = 1;
    this._xField = config['xField'] || config.defaultXField;
    this._yField = config['yField'] || config.defaultYField;
    this._valueField = config['valueField'] || config.defaultValueField;

    if (config['radius']) {
      this._cfgRadius = config['radius'];
    }
  };

  var defaultRadius = HeatmapConfig.defaultRadius;

  Store.prototype = {
    // when forceRender = false -> called from setData, omits renderall event
    _organiseData: function (dataPoint, forceRender) {
      var x = dataPoint[this._xField];
      var y = dataPoint[this._yField];
      var radi = this._radi;
      var store = this._data;
      var max = this._max;
      var min = this._min;
      var value = dataPoint[this._valueField] || 1;
      var radius = dataPoint.radius || this._cfgRadius || defaultRadius;

      if (!store[x]) {
        store[x] = [];
        radi[x] = [];
      }

      if (!store[x][y]) {
        store[x][y] = value;
        radi[x][y] = radius;
      } else {
        store[x][y] += value;
      }
      var storedVal = store[x][y];

      if (storedVal > max) {
        if (!forceRender) {
          this._max = storedVal;
        } else {
          this.setDataMax(storedVal);
        }
        return false;
      } else if (storedVal < min) {
        if (!forceRender) {
          this._min = storedVal;
        } else {
          this.setDataMin(storedVal);
        }
        return false;
      } else {
        return {
          x: x,
          y: y,
          value: value,
          radius: radius,
          min: min,
          max: max
        };
      }
    },
    _unOrganizeData: function () {
      var unorganizedData = [];
      var data = this._data;
      var radi = this._radi;

      for (var x in data) {
        for (var y in data[x]) {
          unorganizedData.push({
            x: x,
            y: y,
            radius: radi[x][y],
            value: data[x][y]
          });
        }
      }
      return {
        min: this._min,
        max: this._max,
        data: unorganizedData
      };
    },
    _onExtremaChange: function () {
      this._coordinator.emit('extremachange', {
        min: this._min,
        max: this._max
      });
    },
    addData: function () {
      if (arguments[0].length > 0) {
        var dataArr = arguments[0];
        var dataLen = dataArr.length;
        while (dataLen--) {
          this.addData.call(this, dataArr[dataLen]);
        }
      } else {
        // add to store
        var organisedEntry = this._organiseData(arguments[0], true);
        if (organisedEntry) {
          // if it's the first datapoint initialize the extremas with it
          if (this._data.length === 0) {
            this._min = this._max = organisedEntry.value;
          }
          this._coordinator.emit('renderpartial', {
            min: this._min,
            max: this._max,
            data: [organisedEntry]
          });
        }
      }
      return this;
    },
    setData: function (data) {
      var dataPoints = data.data;
      var pointsLen = dataPoints.length;

      // reset data arrays
      this._data = [];
      this._radi = [];

      for (var i = 0; i < pointsLen; i++) {
        this._organiseData(dataPoints[i], false);
      }
      this._max = data.max;
      this._min = data.min || 0;

      this._onExtremaChange();
      this._coordinator.emit('renderall', this._getInternalData());
      return this;
    },
    removeData: function () {
      // TODO: implement
    },
    setDataMax: function (max) {
      this._max = max;
      this._onExtremaChange();
      this._coordinator.emit('renderall', this._getInternalData());
      return this;
    },
    setDataMin: function (min) {
      this._min = min;
      this._onExtremaChange();
      this._coordinator.emit('renderall', this._getInternalData());
      return this;
    },
    setCoordinator: function (coordinator) {
      this._coordinator = coordinator;
    },
    _getInternalData: function () {
      return {
        max: this._max,
        min: this._min,
        data: this._data,
        radi: this._radi
      };
    },
    getData: function () {
      return this._unOrganizeData();
    } /*,

      TODO: rethink.

    getValueAt: function(point) {
      var value;
      var radius = 100;
      var x = point.x;
      var y = point.y;
      var data = this._data;

      if (data[x] && data[x][y]) {
        return data[x][y];
      } else {
        var values = [];
        // radial search for datapoints based on default radius
        for(var distance = 1; distance < radius; distance++) {
          var neighbors = distance * 2 +1;
          var startX = x - distance;
          var startY = y - distance;

          for(var i = 0; i < neighbors; i++) {
            for (var o = 0; o < neighbors; o++) {
              if ((i == 0 || i == neighbors-1) || (o == 0 || o == neighbors-1)) {
                if (data[startY+i] && data[startY+i][startX+o]) {
                  values.push(data[startY+i][startX+o]);
                }
              } else {
                continue;
              } 
            }
          }
        }
        if (values.length > 0) {
          return Math.max.apply(Math, values);
        }
      }
      return false;
    }*/
  };

  return Store;
})();

var Canvas2dRenderer = (function Canvas2dRendererClosure() {
  var _getColorPalette = function (config) {
    var gradientConfig = config.gradient || config.defaultGradient;
    var paletteCanvas = document.createElement('canvas');
    var paletteCtx = paletteCanvas.getContext('2d');

    paletteCanvas.width = 256;
    paletteCanvas.height = 1;

    var gradient = paletteCtx.createLinearGradient(0, 0, 256, 1);
    for (var key in gradientConfig) {
      gradient.addColorStop(key, gradientConfig[key]);
    }

    paletteCtx.fillStyle = gradient;
    paletteCtx.fillRect(0, 0, 256, 1);

    return paletteCtx.getImageData(0, 0, 256, 1).data;
  };

  var _getPointTemplate = function (radius, blurFactor) {
    var tplCanvas = document.createElement('canvas');
    var tplCtx = tplCanvas.getContext('2d');
    var x = radius;
    var y = radius;
    tplCanvas.width = tplCanvas.height = radius * 2;

    if (blurFactor == 1) {
      tplCtx.beginPath();
      tplCtx.arc(x, y, radius, 0, 2 * Math.PI, false);
      tplCtx.fillStyle = 'rgba(0,0,0,1)';
      tplCtx.fill();
    } else {
      var gradient = tplCtx.createRadialGradient(x, y, radius * blurFactor, x, y, radius);
      gradient.addColorStop(0, 'rgba(0,0,0,1)');
      gradient.addColorStop(1, 'rgba(0,0,0,0)');
      tplCtx.fillStyle = gradient;
      tplCtx.fillRect(0, 0, 2 * radius, 2 * radius);
    }

    return tplCanvas;
  };

  var _prepareData = function (data) {
    var renderData = [];
    var min = data.min;
    var max = data.max;
    var radi = data.radi;
    var data = data.data;

    var xValues = Object.keys(data);
    var xValuesLen = xValues.length;

    while (xValuesLen--) {
      var xValue = xValues[xValuesLen];
      var yValues = Object.keys(data[xValue]);
      var yValuesLen = yValues.length;
      while (yValuesLen--) {
        var yValue = yValues[yValuesLen];
        var value = data[xValue][yValue];
        var radius = radi[xValue][yValue];
        renderData.push({
          x: xValue,
          y: yValue,
          value: value,
          radius: radius
        });
      }
    }

    return {
      min: min,
      max: max,
      data: renderData
    };
  };

  function Canvas2dRenderer(config) {
    var container = config.container;
    var shadowCanvas = (this.shadowCanvas = document.createElement('canvas'));
    var canvas = (this.canvas = config.canvas || document.createElement('canvas'));
    var renderBoundaries = (this._renderBoundaries = [10000, 10000, 0, 0]);

    var computed = getComputedStyle(config.container) || {};

    canvas.className = 'heatmap-canvas';

    this._width = canvas.width = shadowCanvas.width = config.width || +computed.width.replace(/px/, '');
    this._height = canvas.height = shadowCanvas.height = config.height || +computed.height.replace(/px/, '');

    this.shadowCtx = shadowCanvas.getContext('2d');
    this.ctx = canvas.getContext('2d');

    // @TODO:
    // conditional wrapper

    canvas.style.cssText = shadowCanvas.style.cssText = 'position:absolute;left:0;top:0;';

    container.style.position = 'relative';
    container.appendChild(canvas);

    this._palette = _getColorPalette(config);
    this._templates = {};

    this._setStyles(config);
  }

  Canvas2dRenderer.prototype = {
    renderPartial: function (data) {
      if (data.data.length > 0) {
        this._drawAlpha(data);
        this._colorize();
      }
    },
    renderAll: function (data) {
      // reset render boundaries
      this._clear();
      if (data.data.length > 0) {
        this._drawAlpha(_prepareData(data));
        this._colorize();
      }
    },
    _updateGradient: function (config) {
      this._palette = _getColorPalette(config);
    },
    updateConfig: function (config) {
      if (config['gradient']) {
        this._updateGradient(config);
      }
      this._setStyles(config);
    },
    setDimensions: function (width, height) {
      this._width = width;
      this._height = height;
      this.canvas.width = this.shadowCanvas.width = width;
      this.canvas.height = this.shadowCanvas.height = height;
    },
    _clear: function () {
      this.shadowCtx.clearRect(0, 0, this._width, this._height);
      this.ctx.clearRect(0, 0, this._width, this._height);
    },
    _setStyles: function (config) {
      this._blur = config.blur == 0 ? 0 : config.blur || config.defaultBlur;

      if (config.backgroundColor) {
        this.canvas.style.backgroundColor = config.backgroundColor;
      }

      this._width = this.canvas.width = this.shadowCanvas.width = config.width || this._width;
      this._height = this.canvas.height = this.shadowCanvas.height = config.height || this._height;

      this._opacity = (config.opacity || 0) * 255;
      this._maxOpacity = (config.maxOpacity || config.defaultMaxOpacity) * 255;
      this._minOpacity = (config.minOpacity || config.defaultMinOpacity) * 255;
      this._useGradientOpacity = !!config.useGradientOpacity;
    },
    _drawAlpha: function (data) {
      var min = (this._min = data.min);
      var max = (this._max = data.max);
      var data = data.data || [];
      var dataLen = data.length;
      // on a point basis?
      var blur = 1 - this._blur;

      while (dataLen--) {
        var point = data[dataLen];

        var x = point.x;
        var y = point.y;
        var radius = point.radius;
        // if value is bigger than max
        // use max as value
        var value = Math.min(point.value, max);
        var rectX = x - radius;
        var rectY = y - radius;
        var shadowCtx = this.shadowCtx;

        var tpl;
        if (!this._templates[radius]) {
          this._templates[radius] = tpl = _getPointTemplate(radius, blur);
        } else {
          tpl = this._templates[radius];
        }
        // value from minimum / value range
        // => [0, 1]
        var templateAlpha = (value - min) / (max - min);
        // this fixes #176: small values are not visible because globalAlpha < .01 cannot be read from imageData
        shadowCtx.globalAlpha = templateAlpha < 0.01 ? 0.01 : templateAlpha;

        shadowCtx.drawImage(tpl, rectX, rectY);

        // update renderBoundaries
        if (rectX < this._renderBoundaries[0]) {
          this._renderBoundaries[0] = rectX;
        }
        if (rectY < this._renderBoundaries[1]) {
          this._renderBoundaries[1] = rectY;
        }
        if (rectX + 2 * radius > this._renderBoundaries[2]) {
          this._renderBoundaries[2] = rectX + 2 * radius;
        }
        if (rectY + 2 * radius > this._renderBoundaries[3]) {
          this._renderBoundaries[3] = rectY + 2 * radius;
        }
      }
    },
    _colorize: function () {
      var x = this._renderBoundaries[0];
      var y = this._renderBoundaries[1];
      var width = this._renderBoundaries[2] - x;
      var height = this._renderBoundaries[3] - y;
      var maxWidth = this._width;
      var maxHeight = this._height;
      var opacity = this._opacity;
      var maxOpacity = this._maxOpacity;
      var minOpacity = this._minOpacity;
      var useGradientOpacity = this._useGradientOpacity;

      if (x < 0) {
        x = 0;
      }
      if (y < 0) {
        y = 0;
      }
      if (x + width > maxWidth) {
        width = maxWidth - x;
      }
      if (y + height > maxHeight) {
        height = maxHeight - y;
      }

      var img = this.shadowCtx.getImageData(x, y, width, height);
      var imgData = img.data;
      var len = imgData.length;
      var palette = this._palette;

      for (var i = 3; i < len; i += 4) {
        var alpha = imgData[i];
        var offset = alpha * 4;

        if (!offset) {
          continue;
        }

        var finalAlpha;
        if (opacity > 0) {
          finalAlpha = opacity;
        } else {
          if (alpha < maxOpacity) {
            if (alpha < minOpacity) {
              finalAlpha = minOpacity;
            } else {
              finalAlpha = alpha;
            }
          } else {
            finalAlpha = maxOpacity;
          }
        }

        imgData[i - 3] = palette[offset];
        imgData[i - 2] = palette[offset + 1];
        imgData[i - 1] = palette[offset + 2];
        imgData[i] = useGradientOpacity ? palette[offset + 3] : finalAlpha;
      }

      this.ctx.putImageData(img, x, y);

      this._renderBoundaries = [1000, 1000, 0, 0];
    },
    getValueAt: function (point) {
      var value;
      var shadowCtx = this.shadowCtx;
      var img = shadowCtx.getImageData(point.x, point.y, 1, 1);
      var data = img.data[3];
      var max = this._max;
      var min = this._min;

      value = (Math.abs(max - min) * (data / 255)) >> 0;

      return value;
    },
    getDataURL: function () {
      return this.canvas.toDataURL();
    }
  };

  return Canvas2dRenderer;
})();

var Renderer = (function RendererClosure() {
  var rendererFn = false;

  if (HeatmapConfig['defaultRenderer'] === 'canvas2d') {
    rendererFn = Canvas2dRenderer;
  }

  return rendererFn;
})();

var Util = {
  merge: function () {
    var merged = {};
    var argsLen = arguments.length;
    for (var i = 0; i < argsLen; i++) {
      var obj = arguments[i];
      for (var key in obj) {
        merged[key] = obj[key];
      }
    }
    return merged;
  }
};
// Heatmap Constructor
var Heatmap = (function HeatmapClosure() {
  var Coordinator = (function CoordinatorClosure() {
    function Coordinator() {
      this.cStore = {};
    }

    Coordinator.prototype = {
      on: function (evtName, callback, scope) {
        var cStore = this.cStore;

        if (!cStore[evtName]) {
          cStore[evtName] = [];
        }
        cStore[evtName].push(function (data) {
          return callback.call(scope, data);
        });
      },
      emit: function (evtName, data) {
        var cStore = this.cStore;
        if (cStore[evtName]) {
          var len = cStore[evtName].length;
          for (var i = 0; i < len; i++) {
            var callback = cStore[evtName][i];
            callback(data);
          }
        }
      }
    };

    return Coordinator;
  })();

  var _connect = function (scope) {
    var renderer = scope._renderer;
    var coordinator = scope._coordinator;
    var store = scope._store;

    coordinator.on('renderpartial', renderer.renderPartial, renderer);
    coordinator.on('renderall', renderer.renderAll, renderer);
    coordinator.on('extremachange', function (data) {
      scope._config.onExtremaChange &&
        scope._config.onExtremaChange({
          min: data.min,
          max: data.max,
          gradient: scope._config['gradient'] || scope._config['defaultGradient']
        });
    });
    store.setCoordinator(coordinator);
  };

  function Heatmap() {
    var config = (this._config = Util.merge(HeatmapConfig, arguments[0] || {}));
    this._coordinator = new Coordinator();
    if (config['plugin']) {
      var pluginToLoad = config['plugin'];
      if (!HeatmapConfig.plugins[pluginToLoad]) {
        throw new Error("Plugin '" + pluginToLoad + "' not found. Maybe it was not registered.");
      } else {
        var plugin = HeatmapConfig.plugins[pluginToLoad];
        // set plugin renderer and store
        this._renderer = new plugin.renderer(config);
        this._store = new plugin.store(config);
      }
    } else {
      this._renderer = new Renderer(config);
      this._store = new Store(config);
    }
    _connect(this);
  }

  // @TODO:
  // add API documentation
  Heatmap.prototype = {
    addData: function () {
      this._store.addData.apply(this._store, arguments);
      return this;
    },
    removeData: function () {
      this._store.removeData && this._store.removeData.apply(this._store, arguments);
      return this;
    },
    setData: function () {
      this._store.setData.apply(this._store, arguments);
      return this;
    },
    setDataMax: function () {
      this._store.setDataMax.apply(this._store, arguments);
      return this;
    },
    setDataMin: function () {
      this._store.setDataMin.apply(this._store, arguments);
      return this;
    },
    configure: function (config) {
      this._config = Util.merge(this._config, config);
      this._renderer.updateConfig(this._config);
      this._coordinator.emit('renderall', this._store._getInternalData());
      return this;
    },
    repaint: function () {
      this._coordinator.emit('renderall', this._store._getInternalData());
      return this;
    },
    getData: function () {
      return this._store.getData();
    },
    getDataURL: function () {
      return this._renderer.getDataURL();
    },
    getValueAt: function (point) {
      if (this._store.getValueAt) {
        return this._store.getValueAt(point);
      } else if (this._renderer.getValueAt) {
        return this._renderer.getValueAt(point);
      } else {
        return null;
      }
    }
  };

  return Heatmap;
})();

// core
var heatmapFactory = {
  create: function (config) {
    return new Heatmap(config);
  },
  register: function (pluginKey, plugin) {
    HeatmapConfig.plugins[pluginKey] = plugin;
  }
};

export default heatmapFactory;

效果

相关推荐
jay神4 分钟前
基于 YOLOv8 的森林火灾检测系统(Web 版)
前端·yolo
code2cat8 分钟前
HTML基础篇之第一张网页:从index.html到浏览器里的任务清单
前端·html
miss1 小时前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan2 小时前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真2 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment2 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan2 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么2 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模
上海懒图科技2 小时前
工厂厂区电子地图定位导航:厂区人车精准定位,安全合规双保障
前端·arcgis·vue
西安小哥3 小时前
端侧跑大模型,真正的门槛不在参数量:一次 596 秒到 10 秒的压缩路径复盘
前端·人工智能