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;

效果

相关推荐
Wetoria9 分钟前
管理 git 分支时,用 merge 还是 rebase?
前端·git
前端开发与ui设计的老司机19 分钟前
UI前端与数字孪生融合新领域:智慧环保的污染源监测与治理
前端·ui
一只小风华~32 分钟前
Web前端开发: :has功能性伪类选择器
前端·html·html5·web
Mr_Mao5 小时前
Naive Ultra:中后台 Naive UI 增强组件库
前端
前端小趴菜056 小时前
React-React.memo-props比较机制
前端·javascript·react.js
摸鱼仙人~7 小时前
styled-components:现代React样式解决方案
前端·react.js·前端框架
sasaraku.8 小时前
serviceWorker缓存资源
前端
RadiumAg9 小时前
记一道有趣的面试题
前端·javascript
yangzhi_emo9 小时前
ES6笔记2
开发语言·前端·javascript
yanlele9 小时前
我用爬虫抓取了 25 年 5 月掘金热门面试文章
前端·javascript·面试