D3绘制旭日图如何限制仅渲染指定深度层级解决卡顿问题
D3旭日图如何限制渲染节点深度解决大数据量卡顿问题
问题描述
在使用D3处理嵌套对象数组时,是否支持仅渲染指定深度的节点?
调用D3的.selectAll方法绘制可缩放旭日图时,全量渲染完整嵌套对象的所有节点会因为数据体量过大导致页面交互严重卡顿,希望能限制图表仅渲染指定X层深度的节点。
当前核心实现代码如下:
svg.selectAll('path') .data(partition(root) .descendants()) .enter() .append('path') .style('fill', (d) => { let hue; const current = d; if (current.depth === 0) { return '#33cccc'; } if (current.depth <= 1) { hue = hueDXScale(d.x0); current.fill = d3.hsl(hue, 0.5, 0.6); return current.fill; } if(current.depth <= 2){ console.log("depth > 2: ", d) current.fill = current.parent.fill.brighter(0.5); const hsl = d3.hsl(current.fill); hue = hueDXScale(current.x0); const colorshift = hsl.h + (hue / 4); return d3.hsl(colorshift, hsl.s, hsl.l); } }) .attr('stroke', '#fff') .attr('stroke-width', '1') .on('click', d => click(d, node, svg, x, y, radius, arc)) .on('mouseover', function (d) { if (props.tooltip) { d3.select(this).style('cursor', 'pointer'); tooltip.html(() => { const name = utils.formatNameTooltip(d); return name; }); return tooltip.transition().duration(50).style('opacity', 1); } return null; }) .on('mousemove', () => { if (props.tooltip) { tooltip .style('top', `${d3.event.pageY - 50}px`) .style('left', `${props.tooltipPosition === 'right' ? d3.event.pageX - 100 : d3.event.pageX - 50}px`); } return null; }) .on('mouseout', function () { if (props.tooltip) { d3.select(this).style('cursor', 'default'); tooltip.transition().duration(50).style('opacity', 0); } return null; });
当前渲染效果:
目前可以通过current.depth属性识别深度大于2的节点,疑问点为:是直接为深度超过2的节点设置display:none样式隐藏即可,还是存在更优的实现方案,从渲染逻辑层面就不生成深度超过2的节点?
解决方案
绝对不要用*display:none*隐藏多余节点,这种方式依然会生成全量DOM节点插入页面,完全无法解决卡顿问题,只是视觉上隐藏了多余节点而已。
最优方案是在数据处理阶段就过滤/截断超出深度的节点,从根源上减少布局计算量和需要渲染的DOM数量,性能提升最明显。
方案1:绑定数据时直接过滤(改动最小)
你现有代码的问题是直接把partition(root).descendants()返回的全量节点作为渲染数据源,只需要在这一步加一层深度过滤即可,其余业务逻辑完全不需要修改:
// 自定义需要渲染的最大深度,例如设为2就只渲染0-2层共3级节点 const MAX_RENDER_DEPTH = 2; svg.selectAll('path') // 过滤掉深度超过阈值的节点,从源头减少需要渲染的元素 .data(partition(root).descendants().filter(d => d.depth <= MAX_RENDER_DEPTH)) .enter() .append('path') // 后续的fill样式、属性设置、事件绑定逻辑保持原有写法即可 .style('fill', (d) => { let hue; const current = d; if (current.depth === 0) { return '#33cccc'; } if (current.depth <= 1) { hue = hueDXScale(d.x0); current.fill = d3.hsl(hue, 0.5, 0.6); return current.fill; } if(current.depth <= 2){ current.fill = current.parent.fill.brighter(0.5); const hsl = d3.hsl(current.fill); hue = hueDXScale(current.x0); const colorshift = hsl.h + (hue / 4); return d3.hsl(colorshift, hsl.s, hsl.l); } }) .attr('stroke', '#fff') .attr('stroke-width', '1') // 剩余事件监听逻辑无需改动
方案2:提前截断原始数据(性能最优,适合超大数据量)
如果你的数据嵌套层级极深,d3.partition计算全量节点本身就会产生明显的性能开销,可以在调用布局计算之前先递归截断原始层级数据,只保留需要的深度,进一步减少不必要的计算:
/** * 递归截断层级数据,仅保留指定深度内的节点 * @param {object} node 层级节点 * @param {number} maxDepth 最大保留深度 * @param {number} currentDepth 当前遍历深度 * @returns 截断后的节点 */ function truncateHierarchy(node, maxDepth, currentDepth = 0) { if (currentDepth >= maxDepth) { // 超出最大深度直接删除子节点,避免后续布局计算多余内容 delete node.children; return node; } if (node.children?.length) { node.children = node.children.map(child => truncateHierarchy(child, maxDepth, currentDepth + 1)); } return node; } // 使用方式:先截断原始数据,再生成层次结构、计算分区布局 const truncatedData = truncateHierarchy(d3.hierarchy(originalSourceData), MAX_RENDER_DEPTH); const renderNodes = partition(truncatedData).descendants(); // 后续直接用renderNodes作为数据源绑定即可
如果需要实现点击节点下钻查看深层数据的交互,只需要在点击事件触发时,以当前点击节点为根重新截断数据、重绘图表即可,不需要初始加载时就渲染全量节点。
内容的提问来源于stack exchange,提问作者matt1331
相关产品推荐
相关产品推荐

