177 lines
5.6 KiB
TypeScript
177 lines
5.6 KiB
TypeScript
import Taro from "@tarojs/taro";
|
||
import { View, Canvas } from "@tarojs/components";
|
||
import { useEffect, forwardRef, useImperativeHandle } from "react";
|
||
|
||
const RadarChart: React.FC = forwardRef((props, ref) => {
|
||
const { data } = props;
|
||
|
||
const maxValue = 100;
|
||
const levels = 5;
|
||
const radius = 100;
|
||
const center = { x: 160, y: 160 };
|
||
|
||
useEffect(() => {
|
||
if (data.length > 0) {
|
||
const { texts, vals } = data.reduce(
|
||
(res, item) => {
|
||
const [text, val] = item;
|
||
return {
|
||
texts: [...res.texts, text],
|
||
vals: [...res.vals, val],
|
||
};
|
||
},
|
||
{ texts: [], vals: [] }
|
||
);
|
||
renderCanvas(texts, vals);
|
||
}
|
||
}, [data]);
|
||
|
||
function renderCanvas(labels, values) {
|
||
const query = Taro.createSelectorQuery();
|
||
query
|
||
.select("#radarCanvas")
|
||
.fields({ node: true, size: true })
|
||
.exec((res) => {
|
||
const canvas = res[0].node as HTMLCanvasElement;
|
||
const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
|
||
const dpr = Taro.getWindowInfo().pixelRatio;
|
||
canvas.width = res[0].width * dpr;
|
||
canvas.height = res[0].height * dpr;
|
||
ctx.scale(dpr, dpr);
|
||
|
||
// 启用抗锯齿,消除锯齿
|
||
ctx.imageSmoothingEnabled = true;
|
||
ctx.imageSmoothingQuality = "high";
|
||
// 设置线条端点样式为圆形,减少锯齿
|
||
ctx.lineCap = "round";
|
||
ctx.lineJoin = "round";
|
||
|
||
// === 绘制圆形网格 ===
|
||
for (let i = levels; i >= 1; i--) {
|
||
const r = (radius / levels) * i;
|
||
ctx.beginPath();
|
||
ctx.arc(center.x, center.y, r, 0, Math.PI * 2);
|
||
if (i % 2 === 1) {
|
||
ctx.fillStyle = "rgba(255, 255, 255, 0.4)";
|
||
ctx.fill();
|
||
} else {
|
||
ctx.fillStyle = "rgba(149, 249, 225, 0.4)";
|
||
ctx.fill();
|
||
}
|
||
// 根据层级设置不同的线条颜色,中间圆圈使用更浅的颜色
|
||
if (i === 1) {
|
||
// 最内圈使用最浅的颜色
|
||
ctx.strokeStyle = "#E5E5E5";
|
||
} else if (i <= 3) {
|
||
// 中间圆圈使用较浅的颜色
|
||
ctx.strokeStyle = "#E0E0E0";
|
||
} else {
|
||
// 外圈使用稍深但仍然较浅的颜色
|
||
ctx.strokeStyle = "#D5D5D5";
|
||
}
|
||
// 设置线条宽度为1px,确保清晰
|
||
ctx.lineWidth = 0.5;
|
||
ctx.stroke();
|
||
}
|
||
|
||
// === 坐标轴 & 标签 ===
|
||
labels.forEach((label, i) => {
|
||
const angle = ((Math.PI * 2) / labels.length) * i - Math.PI / 2;
|
||
const x = center.x + radius * Math.cos(angle);
|
||
const y = center.y + radius * Math.sin(angle);
|
||
|
||
// 坐标轴
|
||
ctx.beginPath();
|
||
ctx.moveTo(center.x, center.y);
|
||
ctx.lineTo(x, y);
|
||
// 坐标轴线条也使用较浅的颜色,与圆圈保持一致
|
||
ctx.strokeStyle = "#E0E0E0";
|
||
ctx.lineWidth = 1;
|
||
ctx.stroke();
|
||
|
||
// 标签:沿轴线外侧延伸,文字中心对齐轴线端点
|
||
const labelOffset = 28;
|
||
const textX = center.x + (radius + labelOffset) * Math.cos(angle);
|
||
const textY = center.y + (radius + labelOffset) * Math.sin(angle);
|
||
|
||
ctx.font = "12px sans-serif";
|
||
ctx.fillStyle = "#333";
|
||
ctx.textBaseline = "middle";
|
||
ctx.textAlign = "center";
|
||
|
||
ctx.fillText(label, textX, textY);
|
||
});
|
||
|
||
// === 数据区域 ===
|
||
// ctx.beginPath();
|
||
// values.forEach((val, i) => {
|
||
// const angle = ((Math.PI * 2) / labels.length) * i - Math.PI / 2;
|
||
// const r = (val / maxValue) * radius;
|
||
// const x = center.x + r * Math.cos(angle);
|
||
// const y = center.y + r * Math.sin(angle);
|
||
// if (i === 0) ctx.moveTo(x, y);
|
||
// else ctx.lineTo(x, y);
|
||
// });
|
||
// ctx.closePath();
|
||
// ctx.fillStyle = "rgba(0,200,180,0.3)";
|
||
// ctx.fill();
|
||
// ctx.strokeStyle = "#00c8b4";
|
||
// ctx.lineWidth = 3;
|
||
// ctx.stroke();
|
||
const baseRadius = (radius / levels) * 1; // 第二个环为起点
|
||
const usableRadius = radius - baseRadius;
|
||
|
||
ctx.beginPath();
|
||
values.forEach((val, i) => {
|
||
const angle = ((Math.PI * 2) / labels.length) * i - Math.PI / 2;
|
||
|
||
// 关键修改:值从第二个环开始
|
||
const r = baseRadius + (val / maxValue) * usableRadius;
|
||
|
||
const x = center.x + r * Math.cos(angle);
|
||
const y = center.y + r * Math.sin(angle);
|
||
|
||
if (i === 0) ctx.moveTo(x, y);
|
||
else ctx.lineTo(x, y);
|
||
});
|
||
ctx.closePath();
|
||
ctx.fillStyle = "rgba(0,200,180,0.3)";
|
||
ctx.fill();
|
||
ctx.strokeStyle = "#00c8b4";
|
||
ctx.lineWidth = 3;
|
||
ctx.stroke();
|
||
});
|
||
}
|
||
|
||
useImperativeHandle(ref, () => ({
|
||
generateImage: () =>
|
||
new Promise((resolve, reject) => {
|
||
const query = Taro.createSelectorQuery();
|
||
query
|
||
.select("#radarCanvas")
|
||
.fields({ node: true, size: true })
|
||
.exec((res) => {
|
||
const canvas = res[0].node;
|
||
// ⚠️ 关键:传 canvas,而不是 canvasId
|
||
Taro.canvasToTempFilePath({
|
||
canvas,
|
||
success: (res) => resolve(res.tempFilePath),
|
||
fail: (err) => reject(err),
|
||
});
|
||
});
|
||
}),
|
||
}));
|
||
|
||
return (
|
||
<View>
|
||
<Canvas
|
||
type="2d"
|
||
id="radarCanvas"
|
||
style={{ width: "320px", height: "320px", background: "transparent" }}
|
||
/>
|
||
</View>
|
||
);
|
||
});
|
||
|
||
export default RadarChart;
|