优化渲染速度

This commit is contained in:
kron
2025-09-30 11:29:09 +08:00
parent c0aa6e8058
commit e636d02657

View File

@@ -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坐标