优化渲染速度
This commit is contained in:
@@ -19,7 +19,7 @@ function kernelEpanechnikov(bandwidth) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 核密度估计器
|
||||
* 核密度估计器 - 优化版本
|
||||
* @param {Function} kernel 核函数
|
||||
* @param {Array} range 范围[xmin, xmax]
|
||||
* @param {Number} samples 采样点数
|
||||
@@ -29,23 +29,75 @@ function kernelDensityEstimator(kernel, range, samples) {
|
||||
return function (data) {
|
||||
const gridSize = (range[1] - range[0]) / samples;
|
||||
const densityData = [];
|
||||
|
||||
for (let x = range[0]; x <= range[1]; x += gridSize) {
|
||||
for (let y = range[0]; y <= range[1]; y += gridSize) {
|
||||
let sum = 0;
|
||||
for (const point of data) {
|
||||
sum += kernel([x - point[0], y - point[1]]);
|
||||
const bandwidth = 0.8; // 从核函数中提取带宽
|
||||
|
||||
// 预计算核函数值缓存(减少重复计算)
|
||||
const kernelCache = new Map();
|
||||
const maxDistance = Math.ceil(bandwidth * 2 / gridSize); // 最大影响范围
|
||||
|
||||
for (let dx = -maxDistance; dx <= maxDistance; dx++) {
|
||||
for (let dy = -maxDistance; dy <= maxDistance; dy++) {
|
||||
const distance = Math.sqrt(dx * dx + dy * dy) * gridSize;
|
||||
if (distance <= bandwidth * 2) {
|
||||
kernelCache.set(`${dx},${dy}`, kernel([dx * gridSize, dy * gridSize]));
|
||||
}
|
||||
densityData.push([x, y, sum / data.length]);
|
||||
}
|
||||
}
|
||||
|
||||
// 归一化
|
||||
const maxDensity = Math.max(...densityData.map((d) => d[2]));
|
||||
densityData.forEach((d) => {
|
||||
if (maxDensity > 0) d[2] /= maxDensity;
|
||||
// 使用稀疏网格计算(只计算有数据点影响的区域)
|
||||
const affectedGridPoints = new Set();
|
||||
|
||||
// 第一步:找出所有受影响的网格点
|
||||
data.forEach(point => {
|
||||
const centerX = Math.round((point[0] - range[0]) / gridSize);
|
||||
const centerY = Math.round((point[1] - range[0]) / gridSize);
|
||||
|
||||
// 只考虑带宽范围内的网格点
|
||||
for (let dx = -maxDistance; dx <= maxDistance; dx++) {
|
||||
for (let dy = -maxDistance; dy <= maxDistance; dy++) {
|
||||
const gridX = centerX + dx;
|
||||
const gridY = centerY + dy;
|
||||
if (gridX >= 0 && gridX < samples && gridY >= 0 && gridY < samples) {
|
||||
affectedGridPoints.add(`${gridX},${gridY}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 第二步:只计算受影响的网格点
|
||||
affectedGridPoints.forEach(gridKey => {
|
||||
const [gridX, gridY] = gridKey.split(',').map(Number);
|
||||
const x = range[0] + gridX * gridSize;
|
||||
const y = range[0] + gridY * gridSize;
|
||||
|
||||
let sum = 0;
|
||||
let validPoints = 0;
|
||||
|
||||
// 只考虑附近的点(空间分割优化)
|
||||
data.forEach(point => {
|
||||
const dx = (x - point[0]) / gridSize;
|
||||
const dy = (y - point[1]) / gridSize;
|
||||
const cacheKey = `${Math.round(dx)},${Math.round(dy)}`;
|
||||
|
||||
if (kernelCache.has(cacheKey)) {
|
||||
sum += kernelCache.get(cacheKey);
|
||||
validPoints++;
|
||||
}
|
||||
});
|
||||
|
||||
if (validPoints > 0) {
|
||||
densityData.push([x, y, sum / data.length]);
|
||||
}
|
||||
});
|
||||
|
||||
// 归一化
|
||||
if (densityData.length > 0) {
|
||||
const maxDensity = Math.max(...densityData.map((d) => d[2]));
|
||||
densityData.forEach((d) => {
|
||||
if (maxDensity > 0) d[2] /= maxDensity;
|
||||
});
|
||||
}
|
||||
|
||||
return densityData;
|
||||
};
|
||||
}
|
||||
@@ -102,8 +154,11 @@ function getHeatColor(density) {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加缓存机制
|
||||
const heatmapCache = new Map();
|
||||
|
||||
/**
|
||||
* 基于小程序Canvas API绘制核密度估计热力图
|
||||
* 基于小程序Canvas API绘制核密度估计热力图 - 带缓存优化
|
||||
* @param {String} canvasId 画布ID
|
||||
* @param {Number} width 画布宽度
|
||||
* @param {Number} height 画布高度
|
||||
@@ -134,10 +189,68 @@ export function drawKDEHeatmap(canvasId, width, height, points, options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成缓存key(基于参数和数据点的哈希)
|
||||
const cacheKey = `${bandwidth}-${gridSize}-${range.join(',')}-${points.length}-${JSON.stringify(points.slice(0, 10))}`;
|
||||
|
||||
// 检查缓存
|
||||
if (heatmapCache.has(cacheKey)) {
|
||||
console.log('使用缓存的热力图数据');
|
||||
const cachedDensityData = heatmapCache.get(cacheKey);
|
||||
|
||||
// 直接使用缓存数据绘制
|
||||
const cellWidth = width / gridSize;
|
||||
const cellHeight = height / gridSize;
|
||||
const xRange = range[1] - range[0];
|
||||
const yRange = range[1] - range[0];
|
||||
|
||||
cachedDensityData.forEach((point) => {
|
||||
const [x, y, density] = point;
|
||||
const normalizedX = (x - range[0]) / xRange;
|
||||
const normalizedY = (y - range[0]) / yRange;
|
||||
const canvasX = normalizedX * width;
|
||||
const canvasY = normalizedY * height;
|
||||
const color = getHeatColor(density);
|
||||
|
||||
ctx.setFillStyle(color);
|
||||
ctx.beginPath();
|
||||
ctx.arc(canvasX, canvasY, Math.min(cellWidth, cellHeight) * 0.6, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
});
|
||||
|
||||
// 绘制原始数据点
|
||||
if (showPoints) {
|
||||
ctx.setFillStyle(pointColor);
|
||||
points.forEach((point) => {
|
||||
const [x, y] = point;
|
||||
const normalizedX = (x - range[0]) / xRange;
|
||||
const normalizedY = (y - range[0]) / yRange;
|
||||
const canvasX = normalizedX * width;
|
||||
const canvasY = normalizedY * height;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(canvasX, canvasY, 2.5, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
});
|
||||
}
|
||||
|
||||
ctx.draw(false, () => {
|
||||
console.log("KDE热力图绘制完成(缓存)");
|
||||
resolve();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算核密度估计
|
||||
const kernel = kernelEpanechnikov(bandwidth);
|
||||
const kde = kernelDensityEstimator(kernel, range, gridSize);
|
||||
const densityData = kde(points);
|
||||
|
||||
// 缓存结果(限制缓存大小)
|
||||
if (heatmapCache.size > 10) {
|
||||
const firstKey = heatmapCache.keys().next().value;
|
||||
heatmapCache.delete(firstKey);
|
||||
}
|
||||
heatmapCache.set(cacheKey, densityData);
|
||||
|
||||
// 计算网格大小
|
||||
const cellWidth = width / gridSize;
|
||||
@@ -145,47 +258,50 @@ export function drawKDEHeatmap(canvasId, width, height, points, options = {}) {
|
||||
const xRange = range[1] - range[0];
|
||||
const yRange = range[1] - range[0];
|
||||
|
||||
// 绘制热力图网格
|
||||
// 绘制热力图网格 - 批量绘制优化
|
||||
const colorGroups = new Map();
|
||||
|
||||
// 按颜色分组,减少setFillStyle调用
|
||||
densityData.forEach((point) => {
|
||||
const [x, y, density] = point;
|
||||
|
||||
// 将逻辑坐标转换为画布坐标
|
||||
const normalizedX = (x - range[0]) / xRange;
|
||||
const normalizedY = (y - range[0]) / yRange;
|
||||
const canvasX = normalizedX * width;
|
||||
const canvasY = normalizedY * height;
|
||||
|
||||
// 获取颜色
|
||||
const color = getHeatColor(density);
|
||||
|
||||
// 绘制单元格(使用圆形绘制,边缘更平滑)
|
||||
|
||||
if (!colorGroups.has(color)) {
|
||||
colorGroups.set(color, []);
|
||||
}
|
||||
colorGroups.get(color).push(point);
|
||||
});
|
||||
|
||||
// 批量绘制相同颜色的点
|
||||
colorGroups.forEach((points, color) => {
|
||||
ctx.setFillStyle(color);
|
||||
ctx.beginPath();
|
||||
ctx.arc(
|
||||
canvasX,
|
||||
canvasY,
|
||||
Math.min(cellWidth, cellHeight) * 0.6,
|
||||
0,
|
||||
2 * Math.PI
|
||||
);
|
||||
ctx.fill();
|
||||
points.forEach((point) => {
|
||||
const [x, y, density] = point;
|
||||
const normalizedX = (x - range[0]) / xRange;
|
||||
const normalizedY = (y - range[0]) / yRange;
|
||||
const canvasX = normalizedX * width;
|
||||
const canvasY = normalizedY * height;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(canvasX, canvasY, Math.min(cellWidth, cellHeight) * 0.6, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
});
|
||||
});
|
||||
|
||||
// 绘制原始数据点
|
||||
// 绘制原始数据点 - 批量绘制优化
|
||||
if (showPoints) {
|
||||
ctx.setFillStyle(pointColor);
|
||||
ctx.beginPath(); // 开始批量路径
|
||||
points.forEach((point) => {
|
||||
const [x, y] = point;
|
||||
const normalizedX = (x - range[0]) / xRange;
|
||||
const normalizedY = (y - range[0]) / yRange;
|
||||
const canvasX = normalizedX * width;
|
||||
const canvasY = normalizedY * height;
|
||||
|
||||
// 绘制小圆点
|
||||
ctx.beginPath();
|
||||
|
||||
ctx.arc(canvasX, canvasY, 2.5, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
});
|
||||
ctx.fill(); // 一次性填充所有圆点
|
||||
}
|
||||
|
||||
// 执行绘制
|
||||
@@ -235,6 +351,15 @@ export function generateKDEHeatmapImage(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除热力图缓存
|
||||
* 在数据或参数需要强制更新时调用
|
||||
*/
|
||||
export function clearHeatmapCache() {
|
||||
heatmapCache.clear();
|
||||
console.log('热力图缓存已清除');
|
||||
}
|
||||
|
||||
export const generateHeatMapData = (width, height, amount = 100) => {
|
||||
const data = [];
|
||||
const centerX = 0.5; // 中心点X坐标
|
||||
|
||||
Reference in New Issue
Block a user