D3.js点击图表弹出模态框时图表渲染为纯文本问题排查
问题根因
一共三个核心问题导致渲染异常:
- SVG标签名写错:创建模态框图表容器时,代码写的是
.append("svg_modal"),这是不存在的自定义HTML标签,不是标准SVG根元素。所有SVG子元素(坐标轴、路径、文本)必须嵌套在标准<svg>标签下才会按SVG命名空间渲染,否则浏览器会把<text>当普通文本节点直接展示内容,<path>这类图形标签完全不生效。 - DOM渲染时序不匹配:
Wavelet()函数在组件首次加载的useEffect里就执行,但模态框默认是关闭状态,#contour-modal这个DOM节点根本还没挂载到页面上,D3选不到目标容器,自然没法往里面插入图表内容。 - 等值线数据计算用错比例尺:模态框图表的密度等值线计算时,调用的是小图的
x/y比例尺,尺寸参数也用的小图的width/height,就算前两个问题修复,模态框里的图也会出现错位、变形。
修复方案
按以下步骤改代码即可:
- 修正模态框SVG的标签名,把
.append("svg_modal")改成.append("svg")。 - 拆分图表渲染逻辑,不要在首次加载时就渲染模态框图表:页面内嵌的小图还是在首次加载的useEffect里渲染,模态框的图表改成模态框打开后、DOM节点挂载完成再渲染。
- 模态框的密度等值线数据单独计算,使用模态框自己的比例尺和对应尺寸参数。
- 给渲染小图的useEffect加上空依赖数组,避免组件每次重渲染都重复执行D3绘图造成DOM冗余。
修正后的核心代码参考
首先修改Wavelet函数,支持传入容器ID和尺寸标识,不用重复写两套逻辑:
function Wavelet(containerId, isModal = false) { const baseWidth = isModal ? window.innerWidth/3 : window.innerWidth /5; const baseHeight = isModal ? 400 : 200; const margin = {top: 20, right: 30, bottom: 30, left: 20}; const width = baseWidth - margin.left - margin.right; const height = baseHeight - margin.top - margin.bottom; // 清空目标容器 const container = d3.select(`#${containerId}`); container.selectAll("*").remove(); // 必须append标准svg标签,不能使用自定义标签名 const svg = container.append("svg") .attr("width", baseWidth + margin.left + margin.right) .attr("height", baseHeight + margin.top + margin.bottom) .append("g") .attr("transform", `translate(${margin.left},${margin.top})`); const color = d3.scaleLinear() .domain([0, 0.01]) .range(["#c3d7e8", "#23527a"]); // 原有数据集保持不变 const data = [ { x: 3.4, y: 4.2 },{ x: -1, y: 4.2 },{ x: -1, y: 2.8 },{ x: 3.6, y: 4.3 }, { x: -0.1, y: 3.7 },{ x: 4.7, y: 2.5 },{ x: 0.8, y: 3.6 },{ x: 4.7, y: 3.7 }, { x: -0.4, y: 4.2 },{ x: 0.1, y: 2.2 },{ x: 0.5, y: 3 },{ x: 4.3, y: 4.5 }, { x: 3.4, y: 2.7 },{ x: 4.4, y: 3.6 },{ x: 3.3, y: 0.6 },{ x: 3, y: 3.4 }, { x: 4.7, y: 0 },{ x: -0.7, y: 2.7 },{ x: 2.6, y: 2 },{ x: 0, y: -1 }, { x: 3.4, y: 4.5 },{ x: 3.9, y: 4.6 },{ x: 0.7, y: 3.9 },{ x: 3, y: 0.2 } ]; // 适配当前容器尺寸的坐标轴比例尺 const xScale = d3.scaleLinear().domain([-2,6]).range([0, width]); const yScale = d3.scaleLinear().domain([-2,5]).range([height, 0]); // 绘制坐标轴 svg.append("g") .attr("transform", `translate(0,${height})`) .call(d3.axisBottom(xScale)); svg.append("g").call(d3.axisLeft(yScale)); // 密度数据必须使用当前容器的比例尺和尺寸计算 const densityData = d3.contourDensity() .x(d => xScale(d.x)) .y(d => yScale(d.y)) .size([width, height]) .bandwidth(20) (data); // 绘制等值线 svg.selectAll("path") .data(densityData) .enter() .append("path") .attr("d", d3.geoPath()) .attr("fill", d => color(d.value)) .attr("stroke", "#4285f4"); } export default Wavelet
然后修改React侧的渲染逻辑:
// 首次加载只渲染页面内嵌的小图,加空依赖数组只执行一次 useEffect(() => { Wavelet("contour", false); }, []); // 监听模态框状态,打开且DOM挂载完成后再渲染模态框内的大图 useEffect(() => { if (isOpen && modalID === 4) { // setTimeout等待模态框DOM插入完成后再执行D3渲染,避免选不到容器 setTimeout(() => { Wavelet("contour-modal", true); }, 0) } }, [isOpen, modalID]); function showModal(id) { setIsopen(prev => !prev); setModalID(id); } function Graph() { return <div> <div id="contour" onClick={() => showModal(4)}></div> {isOpen && (modalID == 4) && ( <ModalContent onClose={() => setIsopen(false)}> <div className="modal_c"> <h3>{modalID}</h3> <div id="contour-modal"></div> <p> </p> </div> </ModalContent> )} </div> }
模态框组件的原有逻辑不需要修改,保持原状即可。
内容的提问来源于stack exchange,提问作者shiho
相关产品推荐
相关产品推荐

