Files
shoot-miniprograms/src/kde-heatmap.js
2025-09-29 11:05:42 +08:00

271 lines
7.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 基于小程序Canvas API的核密度估计热力图
* 实现类似test.html中的效果但适配uni-app小程序环境
*/
/**
* Epanechnikov核函数
* @param {Number} bandwidth 带宽参数
* @returns {Function} 核函数
*/
function kernelEpanechnikov(bandwidth) {
return function (v) {
const r = Math.sqrt(v[0] * v[0] + v[1] * v[1]);
return r <= bandwidth
? (3 / (Math.PI * bandwidth * bandwidth)) *
(1 - (r * r) / (bandwidth * bandwidth))
: 0;
};
}
/**
* 核密度估计器
* @param {Function} kernel 核函数
* @param {Array} range 范围[xmin, xmax]
* @param {Number} samples 采样点数
* @returns {Function} 密度估计函数
*/
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]]);
}
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;
});
return densityData;
};
}
/**
* 生成随机射箭数据点
* @param {Number} centerCount 中心点数量
* @param {Number} pointsPerCenter 每个中心点的箭数
* @returns {Array} 箭矢坐标数组
*/
export function generateArcheryPoints(centerCount = 2, pointsPerCenter = 100) {
const points = [];
const range = 8; // 坐标范围 -4 到 4
const spread = 3; // 分散度
for (let i = 0; i < centerCount; i++) {
const centerX = Math.random() * range - range / 2;
const centerY = Math.random() * range - range / 2;
for (let j = 0; j < pointsPerCenter; j++) {
points.push([
centerX + (Math.random() - 0.5) * spread,
centerY + (Math.random() - 0.5) * spread,
]);
}
}
return points;
}
/**
* 颜色映射函数 - 将密度值映射到颜色
* @param {Number} density 密度值 0-1
* @returns {String} RGBA颜色字符串
*/
function getHeatColor(density) {
// 绿色系热力图:从浅绿到深绿
if (density < 0.1) return "rgba(0, 255, 0, 0)";
const alpha = Math.min(density * 1.2, 1); // 增强透明度
const intensity = density;
if (intensity < 0.5) {
// 低密度:浅绿色
const green = Math.round(200 + 55 * intensity);
const blue = Math.round(50 + 100 * intensity);
return `rgba(${Math.round(50 * intensity)}, ${green}, ${blue}, ${alpha * 0.7})`;
} else {
// 高密度:深绿色
const red = Math.round(50 * (intensity - 0.5) * 2);
const green = Math.round(180 + 75 * (1 - intensity));
const blue = Math.round(30 * (1 - intensity));
return `rgba(${red}, ${green}, ${blue}, ${alpha * 0.7})`;
}
}
/**
* 基于小程序Canvas API绘制核密度估计热力图
* @param {String} canvasId 画布ID
* @param {Number} width 画布宽度
* @param {Number} height 画布高度
* @param {Array} points 箭矢坐标数组 [[x, y], ...]
* @param {Object} options 可选参数
* @returns {Promise} 绘制完成的Promise
*/
export function drawKDEHeatmap(canvasId, width, height, points, options = {}) {
return new Promise((resolve, reject) => {
try {
const {
bandwidth = 0.8,
gridSize = 100,
range = [-4, 4],
showPoints = true,
pointColor = "rgba(255, 255, 255, 0.9)",
} = options;
// 创建绘图上下文
const ctx = uni.createCanvasContext(canvasId);
// 清空画布
ctx.clearRect(0, 0, width, height);
// 如果没有数据,直接绘制
if (!points || points.length === 0) {
ctx.draw(false, () => resolve());
return;
}
// 计算核密度估计
const kernel = kernelEpanechnikov(bandwidth);
const kde = kernelDensityEstimator(kernel, range, gridSize);
const densityData = kde(points);
// 计算网格大小
const cellWidth = width / gridSize;
const cellHeight = height / gridSize;
const xRange = range[1] - range[0];
const yRange = range[1] - range[0];
// 绘制热力图网格
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);
// 绘制单元格(使用圆形绘制,边缘更平滑)
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();
});
} catch (error) {
console.error("KDE热力图绘制失败:", error);
reject(error);
}
});
}
/**
* 生成热力图图片类似原有的generateHeatmapImage函数
* 但使用核密度估计算法
*/
export function generateKDEHeatmapImage(
canvasId,
width,
height,
points,
options = {}
) {
return new Promise((resolve, reject) => {
drawKDEHeatmap(canvasId, width, height, points, options)
.then(() => {
// 生成图片
uni.canvasToTempFilePath({
canvasId: canvasId,
width: width,
height: height,
destWidth: width * 3, // 提高输出分辨率,让图像更细腻
destHeight: height * 3,
success: (res) => {
console.log("KDE热力图图片生成成功:", res.tempFilePath);
resolve(res.tempFilePath);
},
fail: (error) => {
console.error("KDE热力图图片生成失败:", error);
reject(error);
},
});
})
.catch(reject);
});
}
export const generateHeatMapData = (width, height, amount = 100) => {
const data = [];
const centerX = 0.5; // 中心点X坐标
const centerY = 0.5; // 中心点Y坐标
for (let i = 0; i < amount; i++) {
let x, y;
// 30%的数据集中在中心区域(高斯分布)
if (Math.random() < 0.3) {
// 使用正态分布生成中心区域的数据
const angle = Math.random() * 2 * Math.PI;
const radius = Math.sqrt(-2 * Math.log(Math.random())) * 0.15; // 标准差0.15
x = centerX + radius * Math.cos(angle);
y = centerY + radius * Math.sin(angle);
} else {
x = Math.random() * 0.8 + 0.1; // 0.1-0.9范围
y = Math.random() * 0.8 + 0.1;
}
// 确保坐标在0-1范围内
x = Math.max(0.05, Math.min(0.95, x));
y = Math.max(0.05, Math.min(0.95, y));
data.push({
x: parseFloat(x.toFixed(3)),
y: parseFloat(y.toFixed(3)),
ring: Math.floor(Math.random() * 5) + 6, // 6-10环
});
}
return data;
};