Files
2026-06-16 11:24:50 +08:00

673 lines
25 KiB
TypeScript
Raw Permalink 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 React from 'react';
import { useEffect, useRef, useState, useCallback, useImperativeHandle, forwardRef } from 'react';
// 直接声明全局 lcjs 变量(已确认依赖已引入,无需再关联 arctionJS)
declare const lcjs: any;
// 定义组件 Props 类型
export interface WaterFallChartProps {
texts?: {
title?: string;
unit?: string;
};
btns?: string[];
className?: string;
style?: React.CSSProperties;
defaultPeriod?: number;
defaultResolution?: number;
fftPointNumber?: number;
xAxisRange: { start: number; end: number };
powerMin?: number;
powerMax?: number;
colorGain?: number;
}
// 定义组件暴露的实例方法类型(核心:明确对外暴露的方法)
export interface WaterFallChartInstance {
addData: (data: any[]) => void;
togglePause: () => void;
reset: () => void;
}
// 核心:移除 RenderWorker 类(不再需要定时轮询渲染)
// 核心:使用 forwardRef 包裹组件,支持暴露实例方法
const WaterFallChart = forwardRef<WaterFallChartInstance, WaterFallChartProps>((props, ref) => {
// 解构 props
const {
texts = {},
btns = [],
className = '',
style = {},
defaultPeriod = 100,
fftPointNumber = 1024, // 默认 FFT 点数
xAxisRange = { start: 0, end: 30000000 },
powerMin=-160,
powerMax=20,
colorGain= 20,
} = props;
// 容器 Ref(用于挂载 LightningChart
const containerRef = useRef<HTMLDivElement>(null);
// 图表实例 Ref(保存全局上下文,防止重复创建)
const chartRef = useRef<any>(null);
// 暂停状态(React 状态管理)
const [paused, setPaused] = useState(false);
const powerExtremesRef = useRef({
min: -95, // 初始默认最小值(匹配原有 setColorStyle 默认值,无数据时兜底)
max: 0, // 初始默认最大值(匹配原有 setColorStyle 默认值,无数据时兜底)
hasValidData: false, // 标记是否已有有效数据(区分默认值和实际数据值)
});
// 常量定义
const RANGE_TIME_MS = 10 * 1000;
const DefaultAxisYThickness = 45;
const MAX_CACHE_LENGTH = 500; // 缓存最大长度限制
const BUFFER_LENGTH = 450;
// 颜色数组
const defaultColorsArray = (() => {
const colors = "#000000,#000008,#000010,#000018,#000020,#000029,#000031,#000039,#000041,#00004a,#000052,#00005a,#000062,#00006a,#000073,#00007b,#000083,#00008b,#000094,#00009c,#0000a4,#0000ac,#0000b4,#0000bd,#0000c5,#0000cd,#0000d5,#0000de,#0000e6,#0000ee,#0000f6,#0000ff,#0000ff,#0006ff,#000dff,#0013ff,#001aff,#0020ff,#0027ff,#002dff,#0034ff,#003aff,#0041ff,#0047ff,#004eff,#0055ff,#005bff,#0062ff,#0068ff,#006fff,#0075ff,#007cff,#0082ff,#0089ff,#008fff,#0096ff,#009cff,#00a3ff,#00aaff,#00b0ff,#00b7ff,#00bdff,#00c4ff,#00caff,#00d1ff,#00d7ff,#00deff,#00e4ff,#00ebff,#00f1ff,#00f8ff,#00ffff,#00ffff,#00fff3,#00ffe8,#00ffdd,#00ffd2,#00ffc7,#00ffbc,#00ffb1,#00ffa6,#00ff9b,#00ff90,#00ff85,#00ff79,#00ff6e,#00ff63,#00ff58,#00ff4d,#00ff42,#00ff37,#00ff2c,#00ff21,#00ff16,#00ff0b,#00ff00,#00ff00,#0dff00,#1aff00,#28ff00,#35ff00,#43ff00,#50ff00,#5dff00,#6bff00,#78ff00,#86ff00,#93ff00,#a1ff00,#aeff00,#bbff00,#c9ff00,#d6ff00,#e4ff00,#f1ff00,#ffff00,#ffff00,#fffb00,#fff700,#fff300,#ffef00,#ffeb00,#ffe800,#ffe400,#ffe000,#ffdc00,#ffd800,#ffd500,#ffd100,#ffcd00,#ffc900,#ffc500,#ffc200,#ffbe00,#ffba00,#ffb600,#ffb200,#ffaf00,#ffab00,#ffa700,#ffa300,#ff9f00,#ff9c00,#ff9800,#ff9400,#ff9000,#ff8c00,#ff8900,#ff8500,#ff8100,#ff7d00,#ff7900,#ff7500,#ff7200,#ff6e00,#ff6a00,#ff6600,#ff6200,#ff5f00,#ff5b00,#ff5700,#ff5300,#ff4f00,#ff4c00,#ff4800,#ff4400,#ff4000,#ff3c00,#ff3900,#ff3500,#ff3100,#ff2d00,#ff2900,#ff2600,#ff2200,#ff1e00,#ff1a00,#ff1600,#ff1300,#ff0f00,#ff0b00,#ff0700,#ff0300,#ff0000,#ff0000,#ff0001,#ff0003,#ff0005,#ff0007,#ff0009,#ff000a,#ff000c,#ff000e,#ff0010,#ff0012,#ff0014,#ff0015,#ff0017,#ff0019,#ff001b,#ff001d,#ff001f,#ff0020,#ff0022,#ff0024,#ff0026,#ff0028,#ff002a,#ff002b,#ff002d,#ff002f,#ff0031,#ff0033,#ff0035,#ff0036,#ff0038,#ff003a,#ff003c,#ff003e,#ff0040,#ff0041,#ff0043,#ff0045,#ff0047,#ff0049,#ff004a,#ff004c,#ff004e,#ff0050,#ff0052,#ff0054,#ff0055,#ff0057,#ff0059,#ff005b,#ff005d,#ff005f,#ff0060,#ff0062,#ff0064,#ff0066,#ff0068,#ff006a,#ff006b,#ff006d,#ff006f,#ff0071,#ff0073,#ff0075,#ff0076,#ff0078,#ff007a,#ff007c,#ff007e,#ff0080,#ff0081"
.split(",")
.filter(color => color && /^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$/.test(color))
.filter((value, index, self) => self.indexOf(value) === index);
if (colors.length < 256) {
const fillCount = 256 - colors.length;
const lastColor = colors[colors.length - 1] || "#ff0000";
for (let i = 0; i < fillCount; i++) {
colors.push(lastColor);
}
}
return colors;
})();
const DEFAULT_COLORS = useRef<string[]>(defaultColorsArray).current;
// 图表上下文(移除 updater 定时相关,保留必要缓存)
const chartContext = useRef({
period: defaultPeriod,
last: [] as any[],
cache: [] as any[][],
resolution: fftPointNumber || 1920,
series: null as any, // 热力图系列实例
updater: { last: 0, timeout: 250 } as { last: number; timeout: number } | null, // 添加 updater 属性
});
// 辅助函数:提取单个数据批次中的有效功率极值
const extractBatchPowerExtremes = useCallback((dataBatch: any[]) => {
const validPowerPoints = dataBatch.flat().filter((item: any) => {
return typeof item === 'number' && !isNaN(item) && isFinite(item);
});
if (validPowerPoints.length === 0) {
return null;
}
const batchMin = Math.min(...validPowerPoints);
const batchMax = Math.max(...validPowerPoints);
return { min: batchMin, max: batchMax };
}, []);
// 数据平铺适配函数
const spreadData = useCallback((source: any[]) => {
const targetLength = fftPointNumber;
const { min: powerMin, max: powerMax } = powerExtremesRef.current;
if (source.length === 0) {
return [Array(targetLength).fill(powerMin)];
}
const validSource = source[0].map(item => Math.max(powerMin, Math.min(powerMax, item ?? powerMin)));
const targetRow: number[] = [];
const freqInterval = 30000000;
for (let i = 0; i < targetLength; i++) {
const freq = 0 + (i / targetLength) * freqInterval;
const freqRatio = (freq - 0) / freqInterval;
const sourceIndex = Math.floor(freqRatio * validSource.length);
const safeSourceIndex = Math.min(Math.max(sourceIndex, 0), validSource.length - 1);
targetRow.push(validSource[safeSourceIndex]);
}
return [targetRow];
}, [fftPointNumber]);
// 颜色样式配置函数
const setColorStyle1 = useCallback(() => {
const ctx = chartContext.current;
const globalExtremes = powerExtremesRef.current;
if (!ctx.series || !lcjs) {
console.error("热力图系列未初始化,无法配置颜色样式");
return;
}
const { ColorCSS, LUT, PalettedFill } = lcjs;
const powerMin = globalExtremes.hasValidData ? globalExtremes.min : -95;
const powerMax = globalExtremes.hasValidData ? globalExtremes.max : 0;
const valueRange = powerMax - powerMin;
if (valueRange <= 0) {
console.warn("功率极值范围无效(max <= min),使用默认颜色范围");
return;
}
const valueColors: { value: number; color: string }[] = [];
for (let i = 0; i < valueRange; i++) {
const colorIndex = Math.floor((i / valueRange) * 256);
const safeColorIndex = Math.min(colorIndex, DEFAULT_COLORS.length - 1);
const currentValue = i + powerMin;
const currentColor = DEFAULT_COLORS[safeColorIndex];
valueColors.push({
value: currentValue,
color: currentColor
});
}
const steps = valueColors.map(({ value, color }) => ({
value: value,
label: value,
color: ColorCSS(color),
}));
const lut = new LUT({
steps: steps,
units: "dB",
interpolate: false,
});
const paletteFill = new PalettedFill({ lut });
ctx.series.setFillStyle(paletteFill);
}, []);
const setColorStyle2 = useCallback(() => {
const ctx = chartContext.current;
const globalExtremes = powerExtremesRef.current;
if (!ctx.series || !lcjs) {
console.error("热力图系列未初始化,无法配置颜色样式");
return;
}
const { ColorCSS, LUT, PalettedFill } = lcjs;
// 步骤1:使用实际数据极值(贴合真实信号范围,避免色阶稀释)
// const powerMin = globalExtremes.hasValidData ? globalExtremes.min : -95;
// const powerMax = globalExtremes.hasValidData ? globalExtremes.max : 0;
const powerMin = -160;
const powerMax = 20;
const valueRange = powerMax - powerMin;
if (valueRange <= 0) {
console.warn("功率极值范围无效(max <= min),使用默认颜色范围");
return;
}
// 步骤2:新增信号阈值配置(核心:区分有效/无效信号)
// 阈值规则:取实际功率最小值+10dB(可根据你的信号强度调整该偏移量)
// 低于此阈值判定为「无信号」,统一设为纯黑/深黑
const signalThreshold = Math.min(powerMin, powerMax); // 防止阈值超过最大值,避免逻辑错误
const validSignalRange = powerMax - signalThreshold; // 有效信号的实际范围
// 步骤3:重构颜色映射数组(先填充无信号区域,再填充有效信号高对比度颜色)
const valueColors: { value: number; color: string }[] = [];
// 3.1 无信号区域:功率从min到threshold,统一映射为纯黑(低调不抢眼)
valueColors.push(
{ value: powerMin, color: "#000000" }, // 起始点
{ value: signalThreshold, color: "#000000" } // 阈值点(以下全黑)
);
// 3.2 有效信号区域:分配高对比度颜色,避免相近色
if (validSignalRange > 0) {
// 可选:替换为精简的高对比度颜色数组(比原有256色更易区分信号强弱)
const highContrastColors = [
"#0000ff", // 暗蓝(弱信号)
"#001A33", // 蓝
"#0088ff", // 浅蓝
"#33FFFF", // 绿
"#88ff00", // 浅绿
"#ffff00", // 亮黄
"#ff8800", // 橙
"#ff0000" // 红(极强信号)
];
for (let i = 0; i <= validSignalRange; i++) {
const currentValue = signalThreshold + i;
// 映射逻辑:将有效信号范围均匀分配到高对比度颜色数组(避免颜色过于密集)
const colorIndex = Math.floor((i / validSignalRange) * (highContrastColors.length - 1));
const currentColor = highContrastColors[colorIndex];
valueColors.push({
value: currentValue,
color: currentColor
});
}
} else {
// 无有效信号范围时,补充一个默认颜色(防止数组为空)
valueColors.push({ value: powerMax, color: "#0000ff" });
}
// 步骤4:生成LUT所需的steps(保持原有逻辑,映射颜色和值)
const steps = valueColors.map(({ value, color }) => ({
value: value,
label: value.toFixed(1), // 优化标签显示,保留1位小数更整洁
color: ColorCSS(color),
}));
// 步骤5:创建颜色查找表(关闭插值,确保信号边界清晰,不模糊)
const lut = new LUT({
steps: steps,
units: "dB",
interpolate: false, // 关键:关闭插值,避免有效/无效信号之间出现过渡杂色
});
const paletteFill = new PalettedFill({ lut });
ctx.series.setFillStyle(paletteFill);
}, []); // 补充依赖项,避免闭包导致的颜色数组更新异常
const setColorStyle = useCallback(() => {
const ctx = chartContext.current;
const globalExtremes = powerExtremesRef.current;
if (!ctx.series || !lcjs) {
console.error("热力图系列未初始化,无法配置颜色样式");
return;
}
const { ColorCSS, LUT, PalettedFill } = lcjs;
// 步骤1:保留固定功率范围,优化颜色在该区间的分布(黑色背景下更醒目)
const valueRange = powerMax - powerMin;
if (valueRange <= 0) {
console.warn("功率极值范围无效(max <= min),使用默认颜色范围");
return;
}
// 步骤2:优化信号阈值逻辑(真正区分无信号/有效信号,提升对比核心)
// 阈值规则:取 powerMin + 20dB(可调整,建议15-25dB),低于此值判定为「无信号(纯黑)」
// 解决原有阈值等于powerMin,无信号和弱信号混淆的问题
const signalOffset = colorGain; // 关键优化:增大阈值偏移,突出有效信号
const signalThreshold = Math.min(powerMin + signalOffset, powerMax - 5); // 防止阈值溢出有效区间
const validSignalRange = powerMax - signalThreshold; // 有效信号的实际可展示范围
// 步骤4:重构颜色映射数组(强化有效信号区间的颜色密度)
const valueColors: { value: number; color: string }[] = [];
// 4.1 无信号区域:powerMin → signalThreshold,纯黑(硬边界,无过渡)
// 解决原有弱信号被黑色淹没的问题,明确区分「无信号」和「有信号」
valueColors.push(
{ value: powerMin, color: "#000000" },
{ value: signalThreshold, color: "#000000" }
);
// 4.2 有效信号区域:分配高对比度颜色,且「分段加权」(中间信号区间颜色更密集)
if (validSignalRange > 0) {
// 细分有效信号区间,提升中间信号的对比差异
const colorStep = validSignalRange / (DEFAULT_COLORS.length * 2); // 颜色步长减半,提升密度
for (let i = 0; i <= validSignalRange; i += colorStep) {
const currentValue = signalThreshold + i;
if (currentValue > powerMax) break;
// 映射逻辑:优先分配高对比度颜色,避免均匀过渡导致的对比不足
const colorIndex = Math.floor(
(i / validSignalRange) * (DEFAULT_COLORS.length - 1)
);
const currentColor = DEFAULT_COLORS[Math.min(colorIndex, DEFAULT_COLORS.length - 1)];
valueColors.push({
value: currentValue,
color: currentColor,
});
}
} else {
// 无有效信号范围时,补充鲜艳颜色,避免视觉平淡
valueColors.push({ value: powerMax, color: "#0099FF" });
}
// 步骤5:优化LUT步骤配置(提升标签可读性,强化颜色边界)
const steps = valueColors.map(({ value, color }) => ({
value: value,
label: value.toFixed(0), // 优化:整数显示,更整洁,减少视觉干扰
color: ColorCSS(color),
}));
// 步骤6:微调颜色查找表(可选开启「轻度插值」,提升颜色过渡的层次感,不模糊边界)
// 关键:仅在有效信号区间内插值,无信号区域保持纯黑硬边界
const lut = new LUT({
steps: steps,
units: "dB",
interpolate: true, // 优化:开启轻度插值,让有效信号颜色过渡更自然,对比更鲜明
// 新增:设置颜色范围兜底,避免信号溢出时显示异常
min: powerMin,
max: powerMax,
});
const paletteFill = new PalettedFill({ lut });
ctx.series.setFillStyle(paletteFill);
}, [colorGain,powerMax,powerMin]);
// 重置图表系列函数
const resetChartSeries = useCallback(() => {
const ctx = chartContext.current;
const chart = chartRef.current;
if (!chart || !lcjs) {
console.warn("图表或坐标轴未初始化,无法重置系列");
return;
}
if (ctx.series) {
try {
chart.getSeries().forEach(series => series.dispose());
} catch (error) {
console.error("销毁旧系列失败", error);
}
}
const { emptyLine } = lcjs;
if (!emptyLine) {
console.error("lcjs 缺少 emptyLine API,无法配置系列样式");
return;
};
const targetResolution = fftPointNumber;
const freqInterval = 30000000;
const xStep = freqInterval > 0 ? (freqInterval / targetResolution) : 1;
try {
ctx.series = chart
.addHeatmapScrollingGridSeries({
scrollDimension: "rows",
resolution: targetResolution,
start: { x: 0, y: 0 },
step: { x: xStep, y: ctx.period },
})
.setWireframeStyle(emptyLine)
.setMouseInteractions(false)
.setDataCleaning({ minDataPointCount: 0 });
} catch (error) {
console.error("创建热力图系列失败", error);
return;
}
setColorStyle();
}, [setColorStyle, fftPointNumber]);
const addData = useCallback((data: any[]) => {
// 1. 暂停状态下不执行任何操作
if (paused) return;
// 2. 校验输入数据有效性
if (!Array.isArray(data) || data.length === 0) {
console.warn("传入的数据无效,跳过更新");
return;
}
const ctx = chartContext.current;
const now = window.performance.now();
// 3. 提取功率极值并更新全局极值
const batchExtremes = extractBatchPowerExtremes(data);
if (batchExtremes) {
const globalExtremes = powerExtremesRef.current;
if (!globalExtremes.hasValidData) {
globalExtremes.min = batchExtremes.min;
globalExtremes.max = batchExtremes.max;
globalExtremes.hasValidData = true;
// 首次获取有效数据,更新颜色样式
setColorStyle();
} else {
globalExtremes.min = Math.min(globalExtremes.min, batchExtremes.min);
globalExtremes.max = Math.max(globalExtremes.max, batchExtremes.max);
}
}
// 4. 数据缓存处理(限制缓存长度,防止内存溢出)
ctx.cache.push(data);
if (ctx.cache.length > MAX_CACHE_LENGTH) {
ctx.cache = ctx.cache.slice(-BUFFER_LENGTH);
}
// 5. 【核心】直接处理数据并渲染(单次add对应单次渲染)
let values = [data]; // 取出当前传入的单批数据
ctx.last = data; // 记录最后一批数据,用于兜底
// 6. 数据平铺适配(匹配FFT点数分辨率)
if (values.length < 1) {
values = spreadData(values);
}
// 7. 渲染到热力图系列(完成单次更新)
if (ctx.series) {
// 检查时间间隔,避免过于频繁的更新
const lastUpdateTime = ctx.updater?.last || 0;
const updateTimeout = ctx.updater?.timeout || 250;
if (now - lastUpdateTime >= updateTimeout) {
ctx.series.addIntensityValues(values);
// 更新最后渲染时间
if (!ctx.updater) {
ctx.updater = { last: now, timeout: 250 };
} else {
ctx.updater.last = now;
}
}
}
}, [paused, extractBatchPowerExtremes, setColorStyle, spreadData]);
// 暂停/继续切换方法
const togglePause = useCallback(() => {
setPaused(prev => !prev);
}, []);
// 监听X轴范围变化,更新瀑布图X轴(联动功能保留)
useEffect(() => {
if (!chartRef.current || !xAxisRange) return;
const chart = chartRef.current;
const axisX = chart.getDefaultAxisX();
const currentXInterval = axisX.getInterval();
if (
currentXInterval.start === xAxisRange.start &&
currentXInterval.end === xAxisRange.end
) {
return;
}
axisX.setInterval(
xAxisRange.start,
xAxisRange.end,
false,
true
);
}, [xAxisRange]);
// 暴露实例方法给父组件
useImperativeHandle(ref, () => ({
addData,
togglePause,
reset: resetChartSeries,
}), [addData, togglePause, resetChartSeries]);
// 初始化图表(移除 RenderWorker 相关逻辑,仅初始化图表和系列)
useEffect(() => {
if (!lcjs) {
console.error("LightningChart 未加载,请确保已引入相关依赖");
return;
}
const requiredAPIs = [
"AxisScrollStrategies",
"AxisTickStrategies",
"TimeFormattingFunctions",
"emptyLine",
"emptyFill",
"lightningChart",
"ColorCSS",
"LUT",
"PalettedFill"
];
const missingAPIs = requiredAPIs.filter(api => !lcjs[api]);
if (missingAPIs.length > 0) {
console.error("lcjs 缺少必要 API,无法初始化瀑布图", { missingAPIs });
return;
}
const {
AxisScrollStrategies,
AxisTickStrategies,
TimeFormattingFunctions,
emptyLine,
emptyFill,
lightningChart,
SolidFill,
ColorRGBA,
AutoCursorModes,
} = lcjs;
if (!containerRef.current) {
console.error("图表容器未找到,无法初始化 LightningChart");
return;
}
try {
const chart = lightningChart({
overrideInteractionMouseButtons: {
axisXYZoomMouseButton: 2,
chartXYPanMouseButton: 2,
chartXYRectangleZoomFitMouseButton: 0,
axisXYPanMouseButton: 0,
},
}).ChartXY({ container: containerRef.current });
chart
.setSeriesBackgroundFillStyle(emptyFill)
.setPadding(5)
.setPadding({ top: 0 })
.disableAnimations()
.setPadding({ left: 0, top: 0, right: 0, bottom: 0 })
.setMouseInteractionPan(false)
.setMouseInteractionWheelZoom(false)
.setMouseInteractionRectangleZoom(false)
.setMouseInteractionRectangleFit(false)
.setZoomingRectangleFillStyle(new SolidFill({ color: ColorRGBA(0, 255, 10, 100) }))
.setTitle("")
.setAutoCursorMode(AutoCursorModes.disabled);
chartRef.current = chart;
const axisX = chart.getDefaultAxisX();
const axisY = chart.getDefaultAxisY();
axisX
.setInterval(0, 30000000, false, true)
.setMouseInteractions(false)
.setAnimationScroll(false);
axisY.setMouseInteractions(false)
.setAnimationScroll(false);
// 轴刻度样式配置
function SetTickStyle(tick: any) {
return tick.setLabelFont((font: any) => font.setWeight('normal').setSize(12).setStyle('normal').setFamily('Arial').setVariant(true))
}
axisX.setTickStrategy(AxisTickStrategies.Numeric, (strategy: any) => {
return strategy
.setMajorTickStyle(SetTickStyle)
.setMinorTickStyle(SetTickStyle)
.setFormattingFunction((value: number) => {
let v = value;
const { start, end } = axisX.getInterval();
const span = Math.abs(end - start);
let unit = "Hz";
let factor = 1;
if (span >= 1e9) { unit = "GHz"; factor = 1e9; }
else if (span >= 1e6) { unit = "MHz"; factor = 1e6; }
else if (span >= 1e3) { unit = "KHz"; factor = 1e3; }
let num = (v / factor).toFixed(2);
if (num.endsWith(".00")) num = num.slice(0, -3);
return `${num}${unit}`;
});
});
axisY
.setScrollStrategy(AxisScrollStrategies.progressive)
.setTickStrategy(AxisTickStrategies.Numeric, (strategy: any) =>
strategy
.setMajorFormattingFunction((val: number) =>
TimeFormattingFunctions.hhmmss(val)
)
.setMinorFormattingFunction((val: number) =>
TimeFormattingFunctions.hhmmss(val)
)
.setMajorTickStyle((style: any) => strategy.getMinorTickStyle())
)
.setInterval(0, -RANGE_TIME_MS)
.setThickness(DefaultAxisYThickness)
.setTickStrategy(AxisTickStrategies.Numeric, (strategy: any) => {
return strategy
.setMajorTickStyle(SetTickStyle)
.setMinorTickStyle(SetTickStyle)
});
// 初始化图表系列(无定时Worker,仅初始化系列)
const ctx = chartContext.current;
requestAnimationFrame(() => {
resetChartSeries();
});
// 销毁回调
return () => {
chart?.dispose();
chartRef.current = null;
chartContext.current.series = null;
};
} catch (error) {
console.error("瀑布图初始化失败:", error);
}
}, [resetChartSeries, fftPointNumber]);
// 渲染 UI 组件
const renderUIElements = () => {
return (
<div className="waterfall-chart-ui" style={{ position: "absolute", top: 0, left: 0, right: 0, zIndex: 10 }}>
{texts.title && (
<div style={{ position: "absolute", top: 10, left: 10, color: "#fff" }}>
{texts.title}
</div>
)}
{texts.unit && (
<div style={{ position: "absolute", top: 10, right: 10, color: "#fff" }}>
{texts.unit}
</div>
)}
{/*
{btns.includes("pause") && (
<button
onClick={togglePause}
style={{
position: "absolute",
top: 10,
right: 20,
padding: "4px 12px",
backgroundColor: paused ? "#409eff" : "#113665",
color: "#fff",
border: "none",
borderRadius: "4px",
cursor: "pointer",
}}
>
{paused ? "继续" : "暂停"}
</button>
)} */}
</div>
);
};
return (
<div
ref={containerRef}
className={`waterfall-chart-container ${className}`}
style={{
position: "relative",
width: "100%",
height: "400px",
backgroundColor: "#000",
...style,
}}
>
{renderUIElements()}
</div>
);
});
export default WaterFallChart;