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

React Nivo CirclePacking点击页面气泡异常重排问题求助

问题描述
  • 使用React Nivo的CirclePacking组件展示三级数据层级:Year(depth 0)> Province(depth 1)> City(depth 2)
  • 图表渲染正常,但点击页面任意位置(图表内外)时气泡都会发生重排
  • 移除Year层级,改为二级层级(Province(depth 0)> City(depth 1))后,问题消失
问题根源

问题出在labelsFilter回调函数中:

  • 该函数内部调用了setZoomedNode和setNodeZero修改组件状态
  • labelsFilter会在图表每次渲染(包括页面点击触发的组件更新)时执行,状态更新会触发组件重渲染,进而导致气泡重排
  • 三级层级下状态更新的触发更频繁,而二级层级时逻辑触发条件不满足,因此问题消失
解决方案
  1. 将初始自动设置zoomedNode的逻辑从labelsFilter移到useEffect中,避免在过滤函数中修改状态
  2. 让labelsFilter仅负责标签显示逻辑判断,不执行任何状态更新操作
  3. 可选:使用React.memo包裹组件或优化状态依赖,减少不必要的重渲染
修改后的组件代码示例
const BubbleChart = (props) => {
    const clickTimer = useRef();

    const [nivoObject, setNivoObject] = useState({});
    const [bigData, setBigData] = useState({});
    const [hierarchyCopy, setHierarchyCopy] = useState();
    const [selectedSchoolId, setSelectedSchooId] = useState();

    const [zoomedNode, setZoomedNode] = useState();
    const [nodeZero, setNodeZero] = useState();
    const [selectedNode, setSelectedNode] = useState();
    const [selectedNodeArPath, setSelectedNodeArPath] = useState();

    const [finalDepth, setFinalDepth] = useState(2);

    // 初始化设置默认zoomedNode
    useEffect(() => {
        if (nivoObject.children && !zoomedNode) {
            const rootNode = { ...nivoObject, depth: 0 };
            setNodeZero(rootNode);
            setZoomedNode(rootNode);
        }
    }, [nivoObject, zoomedNode]);

    return (
        <ResponsiveCirclePacking
            data={nivoObject}
            id="id"
            value="nodeSize"
            margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
            padding={13}
            zoomedId={zoomedNode?.id}
            onClick={(node, event) => {
                setSelectedNode(node);
                clearTimeout(clickTimer.current);
                if (event.detail === 1) {
                    clickTimer.current = setTimeout(() => {
                        setZoomedNode(zoomedNode?.id === node.id ? null : node);
                    }, 300);
                }
            }}
            enableLabels={true}
            label={(dat) => `${bigData[dat.data.source][dat.data.objId]["title"]}`}
            labelComponent={(labelProps) => {
                return (
                    <HighChartCustomeLabel
                        {...labelProps}
                        zoomedNode={zoomedNode}
                        submitHandler={nodeClickHandler}
                        finalDepth={finalDepth}
                    />
                );
            }}
            labelsFilter={(label) => {
                // 仅做过滤判断,不修改状态
                if (zoomedNode?.depth === finalDepth) {
                    return label.node.depth === zoomedNode?.depth;
                } else {
                    return label.node.depth === zoomedNode?.depth + 1;
                }
            }}
            labelsSkipRadius={10}
            labelTextColor="#fff"
            tooltip={CustomTooltip}
            borderWidth={0}
            colorBy="depth"
            colors={["#4880FF33", "#00A353cc", "#52477199", "#493F6499"]}
            motionConfig="slow"
        />
    );
};
原始数据结构
{"id": "DATA0","source": "DATA","objId": "DATA0","children": [{"id": "AY2122","source": "ACADEMIC_YEAR","objId": "AY2122","children": [{"id": "AY2122-PROVINCE1","source": "PROVINCE","objId": "PROVINCE1","children": [{"id": "AY2122-PROVINCE1-CITY34","source": "CITY","objId": "CITY34","buildings": [],"nodeSize": 10},{"id": "AY2122-PROVINCE1-CITY35","source": "CITY","objId": "CITY35","buildings": [{"source": "BUILDING","objId": "19"},{"source": "BUILDING","objId": "48"}],"nodeSize": 30}]},{"id": "AY2122-PROVINCE2","source": "PROVINCE","objId": "PROVINCE2","children": [{"id": "AY2122-PROVINCE2-CITY4","source": "CITY","objId": "CITY4","buildings": [],"nodeSize": 10},{"id": "AY2122-PROVINCE2-CITY18","source": "CITY","objId": "CITY18","buildings": [{"source": "BUILDING","objId": "8"}],"nodeSize": 20}]},{"id": "AY2122-PROVINCE3","source": "PROVINCE","objId": "PROVINCE3","children": [{"id": "AY2122-PROVINCE3-CITY33","source": "CITY","objId": "CITY33","buildings": [{"source": "BUILDING","objId": "77"},{"source": "BUILDING","objId": "108"},{"source": "BUILDING","objId": "189"}],"nodeSize": 40}]}]},{"id": "AY2223","source": "ACADEMIC_YEAR","objId": "AY2223","children": [{"id": "AY2223-PROVINCE1","source": "PROVINCE","objId": "PROVINCE1","children": [{"id": "AY2223-PROVINCE1-CITY29","source": "CITY","objId": "CITY29","buildings": [{"source": "BUILDING","objId": "29"},{"source": "BUILDING","objId": "46"}],"nodeSize": 30},{"id": "AY2223-PROVINCE1-CITY34","source": "CITY","objId": "CITY34","buildings": [],"nodeSize": 10}]},{"id": "AY2223-PROVINCE2","source": "PROVINCE","objId": "PROVINCE2","children": [{"id": "AY2223-PROVINCE2-CITY4","source": "CITY","objId": "CITY4","buildings": [],"nodeSize": 10},{"id": "AY2223-PROVINCE2-CITY20","source": "CITY","objId": "CITY20","buildings": [{"source": "BUILDING","objId": "198"}],"nodeSize": 20}]},{"id": "AY2223-PROVINCE3","source": "PROVINCE","objId": "PROVINCE3","children": [{"id": "AY2223-PROVINCE3-CITY33","source": "CITY","objId": "CITY33","buildings": [{"source": "BUILDING","objId": "77"},{"source": "BUILDING","objId": "108"}],"nodeSize": 30}]}]}]}
原始组件代码
const BubbleChart =(props)=>{

    const clickTimer = useRef();

    const [nivoObject, setNivoObject] = useState({});
    const [bigData, setBigData] = useState({});
    const [hierarchyCopy, setHierarchyCopy] = useState();
    const [selectedSchoolId, setSelectedSchooId] = useState();

    const [zoomedNode, setZoomedNode] = useState();
    const [nodeZero, setNodeZero] = useState();
    const [selectedNode, setSelectedNode] = useState();
    const [selectedNodeArPath, setSelectedNodeArPath] = useState();

    const [finalDepth, setFinalDepth] = useState(2);
   

return (
  <ResponsiveCirclePacking

                            data={nivoObject}
                            id="id"
                            value="nodeSize"
                            margin={{ top: 20, right: 20, bottom: 20, left: 20 }}
                            padding={13}

                            zoomedId={zoomedNode?.id}
                            onClick={(node, event) => {

                                setSelectedNode(node);
                                clearTimeout(clickTimer.current);

                                if (event.detail === 1) {
                                    // On single click

                                    clickTimer.current = setTimeout(() => {
                                        setZoomedNode(zoomedNode?.id === node.id ? null : node);
                                    }, 300);
                                }
                            }}

                            enableLabels={true}
                            label={(dat) => true && `${bigData[dat.data.source][dat.data.objId]["title"]}`}
                            labelComponent={(labelProps) => {
                                return (
                                    <HighChartCustomeLabel
                                        {...labelProps}
                                        zoomedNode={zoomedNode}
                                        submitHandler={nodeClickHandler}
                                        finalDepth={finalDepth}
                                    />
                                );
                            }}
                            labelsFilter={(label) => {

                                if (label.node.depth === 0 && !zoomedNode) {
                                    setNodeZero(label.node);
                                    setZoomedNode(label.node);
                                }

                                if (zoomedNode?.depth === finalDepth) {
                                    return (label.node.depth === zoomedNode?.depth);
                                } else {
                                    return (label.node.depth === zoomedNode?.depth + 1);
                                }
                            }}
                            labelsSkipRadius={10}
                            labelTextColor="#fff"

                            tooltip={CustomTooltip}

                            borderWidth={0}
                            colorBy="depth"
                            colors={["#4880FF33", "#00A353cc", "#52477199", "#493F6499"]}
                            motionConfig="slow"
                        />
 );
};

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 12:35:24