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. 解决缩放与高度调整问题
- 监听容器尺寸变化,动态同步图表宽高
- 绑定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
相关产品推荐
相关产品推荐

