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

React+D3+react-grid-layout组件缩放异常及图表重复等问题求助

React + D3 + react-grid-layout 组件问题汇总及修复方案

初始问题

基于示例开发了React与D3结合的折线图组件,搭配react-grid-layout使用时,组件无法正常缩放。

初始代码

JS代码

const {
  useState,
  useRef,
  useEffect
} = React;
const { Responsive: ResponsiveGridLayout } = ReactGridLayout;

const Chart = React.forwardRef((props, ref) => {
  const [data] = useState([35, 78, 38, 35, 78, 38, 35, 78, 38, 35, 78, 38]);
  const svgRef = useRef();
  const [h, setHeight] = useState(100);
  const [w, setWidth] = useState(500);
  
  useEffect(() => {
    //setting up svg
    const svg = d3.select(svgRef.current)
      .attr('width', '99%')
      .attr('height', '20%')
      .style('background', '#d3d3d4')
      .style('margin-left', '10px')
      .style('margin-top', '10px')
      .style('overflow', 'visible')

    //setting up scaling
    const xScale = d3.scaleLinear()
      .domain([0, data.length - 1])
      .range([0, w]);
    const yScale = d3.scaleLinear()
      .domain([0, h])
      .range([h, 0]);
    const generateScaledLine = d3.line()
      .x((d, i) => xScale(i))
      .y(yScale)
      .curve(d3.curveCardinal)
    //setting up axis
    const xAxis = d3.axisBottom(xScale)
      .ticks(data.length)
      .tickFormat(i => i + 1);
    const yAxis = d3.axisLeft(yScale)
      .ticks(5);
    svg.append('g')
      .call(xAxis)
      .attr('transform', `translate(0 , ${h})`);
    svg.append('g')
      .call(yAxis)
    //setting up the data for the svg 
    svg.selectAll('.line')
      .data([data])
      .join('path')
      .attr('d', d => generateScaledLine(d))
      .attr('fill', 'none')
      .attr('stroke', 'black');
  }, [data, w]);

  console.log(props.width);
  return (
    <div ref={ref}>
      <svg ref={svgRef}></svg>
    </div>
  );
});

function Content({ width }) {
  const [items, setItems] = useState([]);
  const [layouts, setLayouts] = useState({ lg: [] });
  const onLayoutChange = (_, allLayouts) => {
    setLayouts(allLayouts);
  };
  const onRemoveItem = (itemId) => {
    if (itemId === null) {
      console.log("null");
    }
    setItems(items.filter((i) => i !== itemId));
  };
  
  const onAddItem = () => {
    const itemId = Math.max(...items, 0) + 1;
    setItems([...items, itemId]);
  };

  return (
    <div>
      <ResponsiveGridLayout
        draggableHandle=".drag-handle"
        className="layout"
        layouts={layouts}
        breakpoints={{ lg: 500, md: 400, sm: 300, xs: 200 }}
        cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
        allowOverlap={false}
        width={width}
        onLayoutChange={(layout, layouts) => onLayoutChange(layout, layouts)}
      >
        {items.map((key) => (
          <div
            key={key}
            data-grid={{ w: 4, h: 1, x: 0, minH: 1, minW: 4, y: 5 }}
          >
            <Chart />
            <button onClick={() => onRemoveItem(key)}>X</button>
          </div>
        ))}
      </ResponsiveGridLayout>
      <button onClick={onAddItem}>Add item</button>
    </div>
  );
}

ReactDOM.render(
  <Content width={500} />,
  document.getElementById('root')
);

HTML代码

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-grid-layout@1.3.4/dist/react-grid-layout.min.js"></script>

<div id="root" style="width: 500px; height: 300px"></div>

更新后遗留问题

在帮助下推进开发后,仍存在以下问题:

  1. 无法调整图表高度
  2. 删除图表后元素重复显示
  3. 修改后图表无法显示日期时间及折线

针对性修复方案

1. 解决缩放与高度调整问题

  • 监听容器尺寸变化,动态同步图表宽高
  • 绑定SVG尺寸到父容器实际大小,避免硬编码

修改后的Chart组件核心代码:

const Chart = React.forwardRef((props, ref) => {
  const [data] = useState([35, 78, 38, 35, 78, 38, 35, 78, 38, 35, 78, 38]);
  const svgRef = useRef();
  const containerRef = useRef(ref);
  const [dimensions, setDimensions] = useState({ width: 500, height: 100 });

  // 监听容器尺寸变化
  useEffect(() => {
    const observer = new ResizeObserver((entries) => {
      const { width, height } = entries[0].contentRect;
      // 减去margin预留空间
      setDimensions({ width: Math.floor(width - 20), height: Math.floor(height - 20) });
    });
    if (containerRef.current) observer.observe(containerRef.current);
    return () => observer.disconnect();
  }, [containerRef]);

  useEffect(() => {
    if (!dimensions.width || !dimensions.height) return;

    const svg = d3.select(svgRef.current)
      .attr('width', dimensions.width)
      .attr('height', dimensions.height)
      .style('background', '#d3d3d4')
      .style('overflow', 'visible');

    // 清空旧元素,避免重复渲染
    svg.selectAll('*').remove();

    // 设置图表margin
    const margin = { top: 10, right: 10, bottom: 20, left: 30 };
    const innerWidth = dimensions.width - margin.left - margin.right;
    const innerHeight = dimensions.height - margin.top - margin.bottom;

    // 基于数据动态计算y轴范围
    const xScale = d3.scaleLinear()
      .domain([0, data.length - 1])
      .range([0, innerWidth]);
    const yScale = d3.scaleLinear()
      .domain([0, d3.max(data)])
      .range([innerHeight, 0]);

    const generateScaledLine = d3.line()
      .x((d, i) => xScale(i))
      .y(yScale)
      .curve(d3.curveCardinal);

    // 渲染坐标轴
    const xAxis = d3.axisBottom(xScale)
      .ticks(data.length)
      .tickFormat(i => i + 1);
    const yAxis = d3.axisLeft(yScale)
      .ticks(5);

    svg.append('g')
      .attr('transform', `translate(${margin.left}, ${innerHeight + margin.top})`)
      .call(xAxis);

    svg.append('g')
      .attr('transform', `translate(${margin.left}, ${margin.top})`)
      .call(yAxis);

    // 渲染折线
    svg.append('g')
      .attr('transform', `translate(${margin.left}, ${margin.top})`)
      .selectAll('.line')
      .data([data])
      .join('path')
      .attr('class', 'line')
      .attr('d', generateScaledLine)
      .attr('fill', 'none')
      .attr('stroke', 'black');

  }, [data, dimensions]);

  return (
    <div ref={containerRef}>
      <svg ref={svgRef}></svg>
    </div>
  );
});

2. 解决图表删除后重复显示问题

在Content组件中确保每个图表容器的data-grid属性唯一,且删除时正确更新items数组:

{items.map((key, index) => (
  <div
    key={key}
    data-grid={{ w: 4, h: 2, x: 0, minH: 1, minW: 4, y: index * 2 }}
    style={{ position: 'relative' }}
  >
    <Chart />
    <button onClick={() => onRemoveItem(key)} style={{ position: 'absolute', top: 5, right: 5 }}>X</button>
  </div>
))}

3. 修复日期时间与折线显示问题

如果使用时间序列数据,需要切换到时间比例尺,并确保数据格式正确:

// 示例时间数据
const [data] = useState([
  { time: new Date('2024-01-01'), value: 35 },
  { time: new Date('2024-01-02'), value: 78 },
  { time: new Date('2024-01-03'), value: 38 },
]);

// 替换xScale为时间比例尺
const xScale = d3.scaleTime()
  .domain(d3.extent(data, d => d.time))
  .range([0, innerWidth]);

// 折线x轴访问器改为时间字段
const generateScaledLine = d3.line()
  .x(d => xScale(d.time))
  .y(d => yScale(d.value))
  .curve(d3.curveCardinal);

// 格式化日期轴标签
const xAxis = d3.axisBottom(xScale)
  .ticks(3)
  .tickFormat(d3.timeFormat('%m/%d'));

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.18 01:10:33