|
|
|
@ -32,6 +32,152 @@ ChartJS.register( |
|
|
|
|
|
|
|
const { Title } = Typography; |
|
|
|
|
|
|
|
// 使用 HTML Tooltip,避免 Chart.js Canvas 将多标靶内容裁剪掉。 |
|
|
|
const getOrCreateExternalTooltip = (chart) => { |
|
|
|
const canvas = chart.canvas; |
|
|
|
let tooltipEl = canvas._externalTooltipEl; |
|
|
|
|
|
|
|
if (!tooltipEl) { |
|
|
|
tooltipEl = document.createElement("div"); |
|
|
|
tooltipEl.className = "chartjs-external-tooltip"; |
|
|
|
Object.assign(tooltipEl.style, { |
|
|
|
position: "fixed", |
|
|
|
maxWidth: "260px", |
|
|
|
maxHeight: "min(280px, calc(100vh - 20px))", |
|
|
|
overflowY: "auto", |
|
|
|
padding: "10px 12px", |
|
|
|
background: "#ffffff", |
|
|
|
border: "1px solid #d9d9d9", |
|
|
|
borderRadius: "4px", |
|
|
|
boxShadow: "0 3px 12px rgba(0, 0, 0, 0.18)", |
|
|
|
color: "#333333", |
|
|
|
fontSize: "12px", |
|
|
|
lineHeight: "1.6", |
|
|
|
whiteSpace: "nowrap", |
|
|
|
pointerEvents: "none", |
|
|
|
opacity: "0", |
|
|
|
zIndex: "20", |
|
|
|
transform: "translate(-50%, -100%)", |
|
|
|
transition: "opacity 0.08s ease", |
|
|
|
}); |
|
|
|
tooltipEl.addEventListener("mouseenter", () => { |
|
|
|
// Tooltip 已经离开图表后,不允许仅靠再次移入旧位置重新显示。 |
|
|
|
if (tooltipEl.dataset.active !== "true") { |
|
|
|
return; |
|
|
|
} |
|
|
|
if (tooltipEl._hideTimer) { |
|
|
|
clearTimeout(tooltipEl._hideTimer); |
|
|
|
tooltipEl._hideTimer = null; |
|
|
|
} |
|
|
|
tooltipEl.dataset.hovered = "true"; |
|
|
|
tooltipEl.style.opacity = "1"; |
|
|
|
tooltipEl.style.pointerEvents = "auto"; |
|
|
|
}); |
|
|
|
tooltipEl.addEventListener("mouseleave", () => { |
|
|
|
tooltipEl.dataset.hovered = "false"; |
|
|
|
tooltipEl._hideTimer = setTimeout(() => { |
|
|
|
if (tooltipEl.dataset.hovered !== "true") { |
|
|
|
tooltipEl.style.opacity = "0"; |
|
|
|
tooltipEl.style.pointerEvents = "none"; |
|
|
|
tooltipEl.dataset.active = "false"; |
|
|
|
} |
|
|
|
}, 150); |
|
|
|
}); |
|
|
|
document.body.appendChild(tooltipEl); |
|
|
|
canvas._externalTooltipEl = tooltipEl; |
|
|
|
} |
|
|
|
|
|
|
|
return tooltipEl; |
|
|
|
}; |
|
|
|
|
|
|
|
const externalTooltipHandler = ({ chart, tooltip }) => { |
|
|
|
const tooltipEl = getOrCreateExternalTooltip(chart); |
|
|
|
|
|
|
|
if (tooltip.opacity === 0) { |
|
|
|
if (tooltipEl.dataset.hovered !== "true") { |
|
|
|
if (tooltipEl._hideTimer) { |
|
|
|
clearTimeout(tooltipEl._hideTimer); |
|
|
|
} |
|
|
|
tooltipEl._hideTimer = setTimeout(() => { |
|
|
|
if (tooltipEl.dataset.hovered !== "true") { |
|
|
|
tooltipEl.style.opacity = "0"; |
|
|
|
tooltipEl.style.pointerEvents = "none"; |
|
|
|
tooltipEl.dataset.active = "false"; |
|
|
|
} |
|
|
|
}, 150); |
|
|
|
} |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
if (tooltipEl._hideTimer) { |
|
|
|
clearTimeout(tooltipEl._hideTimer); |
|
|
|
tooltipEl._hideTimer = null; |
|
|
|
} |
|
|
|
|
|
|
|
tooltipEl.replaceChildren(); |
|
|
|
|
|
|
|
(tooltip.title || []).forEach((title) => { |
|
|
|
const titleEl = document.createElement("div"); |
|
|
|
titleEl.style.fontWeight = "600"; |
|
|
|
titleEl.style.marginBottom = "4px"; |
|
|
|
titleEl.textContent = title; |
|
|
|
tooltipEl.appendChild(titleEl); |
|
|
|
}); |
|
|
|
|
|
|
|
(tooltip.body || []).forEach((bodyItem) => { |
|
|
|
(bodyItem.lines || []).forEach((line, index) => { |
|
|
|
const lineEl = document.createElement("div"); |
|
|
|
lineEl.style.display = "flex"; |
|
|
|
lineEl.style.alignItems = "center"; |
|
|
|
|
|
|
|
const color = tooltip.labelColors?.[tooltip.body.indexOf(bodyItem)]; |
|
|
|
if (color) { |
|
|
|
const markerEl = document.createElement("span"); |
|
|
|
Object.assign(markerEl.style, { |
|
|
|
width: "8px", |
|
|
|
height: "8px", |
|
|
|
marginRight: "6px", |
|
|
|
borderRadius: "50%", |
|
|
|
// 与折线保持一致,优先使用不透明的 borderColor。 |
|
|
|
background: color.borderColor || color.backgroundColor, |
|
|
|
flex: "0 0 auto", |
|
|
|
}); |
|
|
|
lineEl.appendChild(markerEl); |
|
|
|
} |
|
|
|
|
|
|
|
const textEl = document.createElement("span"); |
|
|
|
textEl.textContent = line; |
|
|
|
lineEl.appendChild(textEl); |
|
|
|
tooltipEl.appendChild(lineEl); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
const canvasRect = chart.canvas.getBoundingClientRect(); |
|
|
|
const positionX = canvasRect.left + tooltip.caretX; |
|
|
|
const positionY = canvasRect.top + tooltip.caretY; |
|
|
|
const tooltipHeight = tooltipEl.offsetHeight; |
|
|
|
const tooltipWidth = tooltipEl.offsetWidth; |
|
|
|
const spaceAbove = positionY; |
|
|
|
const spaceBelow = window.innerHeight - positionY; |
|
|
|
const canPlaceAbove = |
|
|
|
spaceAbove >= tooltipHeight + 8 || spaceBelow < tooltipHeight + 8; |
|
|
|
const clampedX = Math.max( |
|
|
|
tooltipWidth / 2 + 8, |
|
|
|
Math.min(positionX, window.innerWidth - tooltipWidth / 2 - 8) |
|
|
|
); |
|
|
|
|
|
|
|
// 使用 fixed 定位,避免 Tooltip 超出图表后撑高整个页面。 |
|
|
|
// 上方空间不足时放到数据点下方,避免鼠标向上移动时离开可交互区域。 |
|
|
|
tooltipEl.style.transform = canPlaceAbove |
|
|
|
? "translate(-50%, calc(-100% - 8px))" |
|
|
|
: "translate(-50%, 8px)"; |
|
|
|
tooltipEl.style.opacity = "1"; |
|
|
|
tooltipEl.style.pointerEvents = "auto"; |
|
|
|
tooltipEl.dataset.active = "true"; |
|
|
|
tooltipEl.style.left = `${clampedX}px`; |
|
|
|
tooltipEl.style.top = `${positionY}px`; |
|
|
|
}; |
|
|
|
|
|
|
|
const RealtimeCharts = ({ tableData, lastUpdateTime, onDataExport,exportCount = 200, setExportCount }) => { |
|
|
|
const xChartRef = useRef(null); |
|
|
|
const yChartRef = useRef(null); |
|
|
|
@ -39,6 +185,18 @@ const RealtimeCharts = ({ tableData, lastUpdateTime, onDataExport,exportCount = |
|
|
|
// 添加标靶显示状态控制 |
|
|
|
const [visibleTargets, setVisibleTargets] = useState({}); |
|
|
|
|
|
|
|
// 外部 Tooltip 挂载在 body 上,组件卸载时主动清理,避免遗留 DOM。 |
|
|
|
useEffect(() => { |
|
|
|
return () => { |
|
|
|
[xChartRef.current, yChartRef.current].forEach((chart) => { |
|
|
|
const tooltipEl = chart?.canvas?._externalTooltipEl; |
|
|
|
if (tooltipEl?.parentNode) { |
|
|
|
tooltipEl.parentNode.removeChild(tooltipEl); |
|
|
|
} |
|
|
|
}); |
|
|
|
}; |
|
|
|
}, []); |
|
|
|
|
|
|
|
// 添加数据验证 |
|
|
|
if (!Array.isArray(tableData)) { |
|
|
|
return <div>数据格式错误</div>; |
|
|
|
@ -314,6 +472,8 @@ const RealtimeCharts = ({ tableData, lastUpdateTime, onDataExport,exportCount = |
|
|
|
display: false, // 隐藏默认图例,使用自定义控制器 |
|
|
|
}, |
|
|
|
tooltip: { |
|
|
|
enabled: false, |
|
|
|
external: externalTooltipHandler, |
|
|
|
filter: function (tooltipItem) { |
|
|
|
return tooltipItem.parsed.y !== null; |
|
|
|
}, |
|
|
|
@ -584,6 +744,8 @@ const RealtimeCharts = ({ tableData, lastUpdateTime, onDataExport,exportCount = |
|
|
|
border: "1px solid #e8e8e8", |
|
|
|
minHeight: "280px", |
|
|
|
padding: "16px", |
|
|
|
position: "relative", |
|
|
|
overflow: "visible", |
|
|
|
}} |
|
|
|
> |
|
|
|
<Line |
|
|
|
@ -602,6 +764,8 @@ const RealtimeCharts = ({ tableData, lastUpdateTime, onDataExport,exportCount = |
|
|
|
border: "1px solid #e8e8e8", |
|
|
|
minHeight: "280px", |
|
|
|
padding: "16px", |
|
|
|
position: "relative", |
|
|
|
overflow: "visible", |
|
|
|
}} |
|
|
|
> |
|
|
|
<Line |
|
|
|
|