Files
mini-programs/src/components/Radar/index.tsx

187 lines
5.8 KiB
TypeScript
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.
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 offset = 10;
const textX = center.x + (radius + offset) * Math.cos(angle);
const textY = center.y + (radius + offset) * Math.sin(angle);
ctx.font = "12px sans-serif";
ctx.fillStyle = "#333";
ctx.textBaseline = "middle";
if (
Math.abs(angle) < 0.01 ||
Math.abs(Math.abs(angle) - Math.PI) < 0.01
) {
ctx.textAlign = "center";
} else if (angle > -Math.PI / 2 && angle < Math.PI / 2) {
ctx.textAlign = "left";
} else {
ctx.textAlign = "right";
}
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;