You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

解决Lightweight Charts滚动加载新数据后返回起始位置问题

解决图表加载新数据后滚动位置重置的问题

问题说明

使用subscribeVisibleLogicalRangeChange()监听图表可视范围,能成功请求新数据,但设置新数据后图表会重新渲染并回到起始位置。需求是用户滚动时请求并追加新数据,同时保持当前滚动位置不变。

核心原因

  1. 数据覆盖而非追加:原fetchData直接用新数据替换旧数据,触发图表全量重绘
  2. 图表实例重复创建:依赖symbolData的useEffect在数据更新时会重新创建整个图表,导致滚动状态丢失
  3. 无滚动位置保存逻辑:加载新数据前未记录当前滚动位置,无法在数据更新后恢复

解决方案

  • 分离图表初始化与数据更新:图表实例仅在组件挂载时创建一次,避免重复初始化
  • 追加数据而非覆盖:更新状态时保留原有数据,只添加新获取的内容
  • 记录并恢复滚动位置:加载新数据前获取当前滚动位置,数据更新后恢复该位置
  • 添加加载防抖:用ref标记加载状态,防止重复触发数据请求

修改后的完整代码

1. 优化数据请求函数

// 用于标记是否正在加载,防止重复请求
const isLoading = useRef(false);
// 保存图表和系列实例的ref
const chartRef = useRef<IChartApi | null>(null);
const candleSeriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null);

const fetchData = async (record: number) => {
  if (isLoading.current) return;
  isLoading.current = true;

  try {
    const response = await getSymbolTimeSeries(
      "AAPL",
      selectedInterval,
      record,
    );
    const { values } = response;
    const newSeriesData = values.map((value: SymbolDataType) => {
      return createData(
        value.datetime,
        Number(value.open),
        Number(value.close),
        Number(value.high),
        Number(value.low),
      );
    }).sort((a: ChartDataType, b: ChartDataType) => a.time - b.time);

    // 追加新数据,而非覆盖原有数据
    setSymbolData(prev => [...prev, ...newSeriesData]);
  } finally {
    isLoading.current = false;
  }
};

2. 分离图表初始化与数据更新逻辑

// 仅在组件挂载和选中时间间隔变化时初始化图表
useEffect(() => {
  const width = tradeRef.current?.clientWidth;
  const height = tradeRef.current?.clientHeight;
  if (!tradeRef.current) return;

  const chart = createChart(tradeRef.current, {
    width,
    height,
    layout: {
      background: { color: "#000000" },
      textColor: "rgba(255, 255, 255, 0.9)",
    },
    grid: {
      vertLines: { color: "rgba(197, 203, 206, 0.5)" },
      horzLines: { color: "rgba(197, 203, 206, 0.5)" },
    },
    crosshair: { mode: CrosshairMode.Normal },
    rightPriceScale: { borderColor: "rgba(197, 203, 206, 0.8)" },
    timeScale: { borderColor: "rgba(197, 203, 206, 0.8)" },
  });

  const candleSeries = chart.addCandlestickSeries({
    upColor: "rgb(255, 24, 98)",
    downColor: "#000",
    borderDownColor: "rgb(60, 179, 113)",
    borderUpColor: "rgb(255, 24, 98)",
    wickDownColor: "rgb(255, 24, 98)",
    wickUpColor: "rgb(60, 179, 113)",
  });

  chartRef.current = chart;
  candleSeriesRef.current = candleSeries;

  // 初始加载数据
  fetchData(300).catch(console.error);

  const handleResize = () => {
    chart.applyOptions({
      width: tradeRef.current?.clientWidth,
      height: tradeRef.current?.clientHeight,
    });
  };

  async function onVisibleLogicalRangeChanged(
    newVisibleLogicalRange: { from: number } | null,
  ) {
    if (
      newVisibleLogicalRange !== null &&
      newVisibleLogicalRange.from < -106 &&
      !isLoading.current
    ) {
      // 记录当前滚动位置
      const currentPosition = chart.timeScale().scrollPosition();
      // 请求更多数据
      await fetchData(symbolData.length + 10);
      // 恢复滚动位置(false表示不带动画)
      chart.timeScale().scrollToPosition(currentPosition, false);
    }
  }

  chart.timeScale().subscribeVisibleLogicalRangeChange(onVisibleLogicalRangeChanged);
  window.addEventListener("resize", handleResize);

  return () => {
    window.removeEventListener("resize", handleResize);
    chart.timeScale().unsubscribeVisibleLogicalRangeChange(onVisibleLogicalRangeChanged);
    chart.remove();
  };
}, [selectedInterval]);

// 仅在数据更新时更新图表系列数据
useEffect(() => {
  if (candleSeriesRef.current && symbolData.length > 0) {
    candleSeriesRef.current.setData(symbolData);
  }
}, [symbolData]);

3. 组件结构(保持不变)

<Box
  ref={tradeRef}
  sx={{
    width: "calc(100vw - 550px)",
    height: "90%",
    mt: 1.5,
  }}
/>

内容的提问来源于stack exchange,提问作者Omar mero

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 02:00:55