提交 dc929422 编写于 作者: L lang

Heat map on map

上级 9274a65c
/**
* @file defines echarts Heatmap Chart
* @author Ovilia (me@zhangwenli.com)
* Inspired by https://github.com/mourner/simpleheat
*
* @module
*/
define(function (require) {
var BRUSH_SIZE = 20;
var GRADIENT_LEVELS = 256;
/**
* Heatmap Chart
*
* @class
*/
function Heatmap() {
var canvas = document.createElement('canvas');
this.canvas = canvas;
this.blurSize = 30;
this.opacity = 1;
this._gradientPixels = {};
}
Heatmap.prototype = {
/**
* Renders Heatmap and returns the rendered canvas
* @param {Array} data array of data, each has x, y, value
* @param {number} width canvas width
* @param {number} height canvas height
*/
update: function(data, width, height, normalize, colorFunc, isInRange) {
var brush = this._getBrush();
var gradientInRange = this._getGradient(data, colorFunc, 'inRange');
var gradientOutOfRange = this._getGradient(data, colorFunc, 'outOfRange');
var r = BRUSH_SIZE + this.blurSize;
var canvas = this.canvas;
var ctx = canvas.getContext('2d');
var len = data.length;
canvas.width = width;
canvas.height = height;
for (var i = 0; i < len; ++i) {
var p = data[i];
var x = p[0];
var y = p[1];
var value = p[2];
// calculate alpha using value
var alpha = normalize(value);
// draw with the circle brush with alpha
ctx.globalAlpha = alpha;
ctx.drawImage(brush, x - r, y - r);
}
// colorize the canvas using alpha value and set with gradient
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var pixels = imageData.data;
var len = pixels.length / 4;
while(len--) {
var id = len * 4 + 3;
var alpha = pixels[id] / 256;
var offset = Math.floor(alpha * (GRADIENT_LEVELS - 1));
var gradient = isInRange(alpha) ? gradientInRange : gradientOutOfRange;
pixels[id - 3] = gradient[offset * 4];
pixels[id - 2] = gradient[offset * 4 + 1];
pixels[id - 1] = gradient[offset * 4 + 2];
pixels[id] *= this.opacity * gradient[offset * 4 + 3];
}
ctx.putImageData(imageData, 0, 0);
return canvas;
},
/**
* get canvas of a black circle brush used for canvas to draw later
* @private
* @returns {Object} circle brush canvas
*/
_getBrush: function() {
var brushCanvas = this._brushCanvas || (this._brushCanvas = document.createElement('canvas'));
// set brush size
var r = BRUSH_SIZE + this.blurSize;
var d = r * 2;
brushCanvas.width = d;
brushCanvas.height = d;
var ctx = brushCanvas.getContext('2d');
ctx.clearRect(0, 0, d, d);
// in order to render shadow without the distinct circle,
// draw the distinct circle in an invisible place,
// and use shadowOffset to draw shadow in the center of the canvas
ctx.shadowOffsetX = d;
ctx.shadowBlur = this.blurSize;
// draw the shadow in black, and use alpha and shadow blur to generate
// color in color map
ctx.shadowColor = '#000';
// draw circle in the left to the canvas
ctx.beginPath();
ctx.arc(-r, r, BRUSH_SIZE, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
return brushCanvas;
},
/**
* get gradient color map
* @private
*/
_getGradient: function (data, colorFunc, state) {
var gradientPixels = this._gradientPixels;
var pixelsSingleState = gradientPixels[state] || (gradientPixels[state] = new Uint8ClampedArray(256 * 4));
var color = [];
var off = 0;
for (var i = 0; i < 256; i++) {
colorFunc[state](i / 255, true, color);
pixelsSingleState[off++] = color[0];
pixelsSingleState[off++] = color[1];
pixelsSingleState[off++] = color[2];
pixelsSingleState[off++] = color[3];
}
return pixelsSingleState;
}
};
return Heatmap;
});
......@@ -28,7 +28,7 @@ define(function (require) {
// No blur
// Available when heatmap is on geo
blurSize: 0
blurSize: 20
}
});
});
\ No newline at end of file
define(function (require) {
var graphic = require('../../util/graphic');
var HeatmapLayer = require('./HeatmapLayer');
var zrUtil = require('zrender/core/util');
function getIsInPiecewiseRange(dataExtent, pieceList, selected) {
var dataSpan = dataExtent[1] - dataExtent[0];
pieceList = zrUtil.map(pieceList, function (piece) {
return {
interval: [
(piece.interval[0] - dataExtent[0]) / dataSpan,
(piece.interval[1] - dataExtent[0]) / dataSpan
]
};
});
var len = pieceList.length;
var lastIndex = 0;
return function (val) {
// Try to find in the location of the last found
for (var i = lastIndex; i < len; i++) {
var interval = pieceList[i].interval;
if (interval[0] <= val && val <= interval[1]) {
lastIndex = i;
break;
}
}
if (i === len) { // Not found, back interation
for (var i = lastIndex - 1; i >= 0; i--) {
var interval = pieceList[i].interval;
if (interval[0] <= val && val <= interval[1]) {
lastIndex = i;
break;
}
}
}
return i >= 0 && i < len && selected[i];
};
}
function getIsInContinuousRange(dataExtent, range) {
var dataSpan = dataExtent[1] - dataExtent[0];
range = [
(range[0] - dataExtent[0]) / dataSpan,
(range[1] - dataExtent[0]) / dataSpan
];
return function (val) {
return val >= range[0] && val <= range[1];
};
}
return require('../../echarts').extendChartView({
type: 'heatmap',
init: function () {
this._hmLayer = new HeatmapLayer();
},
render: function (seriesModel, ecModel, api) {
var dataRangeOfThisSeries;
ecModel.eachComponent('dataRange', function (dataRange) {
dataRange.eachTargetSeries(function (targetSeries) {
if (targetSeries === seriesModel) {
dataRangeOfThisSeries = dataRange;
}
});
});
if (!dataRangeOfThisSeries) {
throw new Error('Heatmap must use with dataRange');
}
this.group.removeAll();
var coordSys = seriesModel.coordinateSystem;
if (coordSys.type === 'cartesian2d') {
this._renderOnCartesian(coordSys, seriesModel, ecModel, api);
this._renderOnCartesian(coordSys, seriesModel, api);
}
else if (coordSys.type === 'geo') {
this._renderOnGeo(coordSys, seriesModel, ecModel, api);
this._renderOnGeo(
coordSys, seriesModel, dataRangeOfThisSeries, api
);
}
},
_renderOnCartesian: function (cartesian, seriesModel, ecModel, api) {
_renderOnCartesian: function (cartesian, seriesModel, api) {
var xAxis = cartesian.getAxis('x');
var yAxis = cartesian.getAxis('y');
var group = this.group;
group.removeAll();
if (!(xAxis.type === 'category' && yAxis.type === 'category')) {
throw new Error('Heatmap on cartesian must have two category axes');
......@@ -35,6 +101,10 @@ define(function (require) {
data.each(['x', 'y', 'z'], function (x, y, z, idx) {
var itemModel = data.getItemModel(idx);
var point = cartesian.dataToPoint([x, y]);
// Ignore empty data
if (isNaN(z)) {
return;
}
var rect = new graphic.Rect({
shape: {
x: point[0] - width / 2,
......@@ -74,8 +144,64 @@ define(function (require) {
});
},
_renderOnGeo: function (geo, seriesModel, ecModel, api) {
_renderOnGeo: function (geo, seriesModel, dataRangeModel, api) {
var inRangeVisuals = dataRangeModel.targetVisuals.inRange;
var outOfRangeVisuals = dataRangeModel.targetVisuals.outOfRange;
// if (!visualMapping) {
// throw new Error('Data range must have color visuals');
// }
var data = seriesModel.getData();
var hmLayer = this._hmLayer;
hmLayer.blurSize = seriesModel.get('blurSize');
var rect = geo.getViewRect().clone();
var roamTransform = geo.getRoamTransform();
roamTransform && rect.applyTransform(roamTransform);
// Clamp on viewport
var x = Math.max(rect.x, 0);
var y = Math.max(rect.y, 0);
var x2 = Math.min(rect.width + rect.x, api.getWidth());
var y2 = Math.min(rect.height + rect.y, api.getHeight());
var width = x2 - x;
var height = y2 - y;
var points = data.mapArray(['lng', 'lat', 'value'], function (lng, lat, value) {
var pt = geo.dataToPoint([lng, lat]);
pt[0] -= x;
pt[1] -= y;
pt.push(value);
return pt;
});
var dataExtent = dataRangeModel.getExtent();
var isInRange = dataRangeModel.type === 'dataRange.continuous'
? getIsInContinuousRange(dataExtent, dataRangeModel.option.range)
: getIsInPiecewiseRange(
dataExtent, dataRangeModel.getPieceList(), dataRangeModel.option.selected
);
hmLayer.update(
points, width, height,
inRangeVisuals.color.getNormalizer(),
{
inRange: inRangeVisuals.color.getColorMapper(),
outOfRange: outOfRangeVisuals.color.getColorMapper()
},
isInRange
);
var img = new graphic.Image({
style: {
width: width,
height: height,
x: x,
y: y,
image: hmLayer.canvas
},
silent: true
});
this.group.add(img);
}
});
});
\ No newline at end of file
......@@ -16,26 +16,11 @@ define(function (require) {
function processSingleDataRange(dataRangeModel, ecModel) {
var visualMappings = dataRangeModel.targetVisuals;
var visualTypesMap = {};
var colorFuncsMap = {};
zrUtil.each(['inRange', 'outOfRange'], function (state) {
var visualTypes = VisualMapping.prepareVisualTypes(visualMappings[state]);
var colorFunc = zrUtil.filter(zrUtil.map(visualTypes, function (visualType) {
return visualMappings[state][visualType].getColorMapper;
}), function (func) {
return !!func;
})[0];
visualTypesMap[state] = visualTypes;
colorFuncsMap[state] = colorFunc;
});
// Cache color func
function colorFunc(value, out) {
var valueState = dataRangeModel.getValueState(value);
var colorFunc = colorFuncsMap[valueState];
// PENDING
return colorFunc && colorFunc(value, out);
}
dataRangeModel.eachTargetSeries(function (seriesModel) {
var data = seriesModel.getData();
var dimension = dataRangeModel.getDataDimension(data);
......@@ -49,8 +34,6 @@ define(function (require) {
data.setItemVisual(dataIndex, key, value);
}
data.setVisual('colorFunc', colorFunc);
data.each([dimension], function (value, index) {
// For performance consideration, do not use curry.
dataIndex = index;
......
......@@ -118,6 +118,13 @@ define(function (require) {
this._updateTransform();
},
/**
* @return {Array.<number}
*/
getRoamTransform: function () {
return this._roamTransform.transform;
},
/**
* Update transform from roam and mapLocation
* @private
......
......@@ -87,7 +87,11 @@ define(function (require) {
isValueActive: null,
mapValueToVisual: null
mapValueToVisual: null,
getNormalizer: function () {
return zrUtil.bind(this._normalizeData, this);
}
};
var visualHandlers = VisualMapping.visualHandlers = {
......@@ -103,20 +107,21 @@ define(function (require) {
getColorMapper: function () {
var visual = isCategory(this)
? this.option.visual
: zrUtil.map(this.option.visual, zrUtil.parse);
return isCategory(this)
? function (value) {
return getVisualForCategory(this, visual, this._normalizeData(value));
: zrUtil.map(this.option.visual, zrColor.parse);
return zrUtil.bind(
isCategory(this)
? function (value, isNormalized) {
!isNormalized && (value = this._normalizeData(value));
return getVisualForCategory(this, visual, value);
}
: function (value, out) {
: function (value, isNormalized, out) {
// If output rgb array
// which will be much faster and useful in pixel manipulation
var returnRGBArray = !!out;
out = zrColor.fastMapToColor(
this._normalizeData(value), visual, out
);
!isNormalized && (value = this._normalizeData(value));
out = zrColor.fastMapToColor(value, visual, out);
return returnRGBArray ? out : zrUtil.stringify(out, 'rgba');
};
}, this);
},
// value:
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册