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

如何实现内圈分组与橙色外圈的动态生成?

D3.js 动态团队可视化问题解决

1. 内圈成员动态分组实现

核心逻辑是基于成员数据的分组标识,分配角度区间并均匀排布组内成员:

  • 用d3.group()按分组字段聚合成员数据
  • 按各组成员数量占比,分配内圈的角度范围(总角度为2π)
  • 组内成员在对应角度区间内均匀分布位置
  • 动态更新时,重新计算角度并通过过渡动画更新成员位置

2. 橙色外圈服务动态展示

通过D3的enter/update/exit数据绑定模式,实现服务弧的动态增删改:

  • 定义外圈弧生成器,设置内半径为内圈外半径,外半径更大
  • enter阶段:创建新的橙色弧元素,绑定初始数据
  • update阶段:更新现有弧的角度范围与样式
  • exit阶段:移除失效的弧元素并添加过渡动画

完整可运行代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>动态团队可视化</title>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <style>
        .team-container {
            width: 800px;
            height: 800px;
            margin: 0 auto;
            position: relative;
        }
        .inner-member {
            cursor: pointer;
            transition: transform 0.3s;
        }
        .inner-member:hover {
            transform: scale(1.1);
        }
        .outer-service {
            fill: #ff7f24; /* 橙色 */
            opacity: 0.8;
            transition: opacity 0.3s;
        }
        .outer-service:hover {
            opacity: 1;
        }
        .control-btn {
            margin: 20px;
            padding: 10px 20px;
            font-size: 16px;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <div class="team-container">
        <svg id="team-svg" width="800" height="800"></svg>
    </div>
    <button class="control-btn" onclick="updateData()">更新数据</button>

    <script>
        const width = 800;
        const height = 800;
        const centerX = width / 2;
        const centerY = height / 2;

        // 内圈配置
        const innerRadius = 150;
        const innerMemberRadius = 30;
        // 外圈配置
        const outerInnerRadius = 220;
        const outerOuterRadius = 250;

        const svg = d3.select("#team-svg")
            .attr("width", width)
            .attr("height", height);

        // 初始数据
        let memberData = [
            { id: 1, name: "成员1", group: "设计组" },
            { id: 2, name: "成员2", group: "设计组" },
            { id: 3, name: "成员3", group: "开发组" },
            { id: 4, name: "成员4", group: "开发组" },
            { id: 5, name: "成员5", group: "测试组" }
        ];

        let serviceData = [
            { id: "s1", name: "UI设计", angleRange: [0, Math.PI/3] },
            { id: "s2", name: "后端开发", angleRange: [Math.PI/3, 2*Math.PI/3] }
        ];

        // 初始化可视化
        initVisualization();

        function initVisualization() {
            renderInnerMembers();
            renderOuterServices();
        }

        // 渲染内圈成员(带分组)
        function renderInnerMembers() {
            // 按分组聚合数据
            const groupedMembers = d3.group(memberData, d => d.group);
            const groups = Array.from(groupedMembers.entries());
            const totalMembers = memberData.length;
            const totalAngle = 2 * Math.PI;

            // 计算每个组的角度范围
            let currentAngle = -Math.PI/2; // 从顶部开始排布
            const groupAngleInfo = groups.map(([group, members]) => {
                const groupAngle = (members.length / totalMembers) * totalAngle;
                const angleRange = [currentAngle, currentAngle + groupAngle];
                currentAngle += groupAngle;
                return { group, members, angleRange };
            });

            // 处理成员元素的enter/update/exit
            const members = svg.selectAll(".inner-member")
                .data(memberData, d => d.id);

            // 移除旧元素
            members.exit().remove();

            // 添加新元素
            const newMembers = members.enter()
                .append("g")
                .attr("class", "inner-member");

            newMembers.append("circle")
                .attr("r", innerMemberRadius)
                .attr("fill", "#1f77b4");

            newMembers.append("text")
                .attr("text-anchor", "middle")
                .attr("dy", 5)
                .attr("fill", "#fff")
                .text(d => d.name);

            // 合并更新所有元素
            const allMembers = newMembers.merge(members);

            // 计算每个成员的位置
            allMembers.each(function(d) {
                const groupInfo = groupAngleInfo.find(g => g.group === d.group);
                const memberIndex = groupInfo.members.findIndex(m => m.id === d.id);
                const groupStartAngle = groupInfo.angleRange[0];
                const groupEndAngle = groupInfo.angleRange[1];
                // 组内成员均匀分布,留出首尾空隙
                const memberAngle = groupStartAngle + ((groupEndAngle - groupStartAngle) / (groupInfo.members.length + 1)) * (memberIndex + 1);
                
                const x = centerX + innerRadius * Math.cos(memberAngle);
                const y = centerY + innerRadius * Math.sin(memberAngle);

                d3.select(this)
                    .transition()
                    .duration(800)
                    .attr("transform", `translate(${x}, ${y})`);
            });
        }

        // 渲染外圈服务
        function renderOuterServices() {
            // 定义弧生成器
            const arcGenerator = d3.arc()
                .innerRadius(outerInnerRadius)
                .outerRadius(outerOuterRadius)
                .startAngle(d => d.angleRange[0])
                .endAngle(d => d.angleRange[1]);

            // 处理服务元素的enter/update/exit
            const services = svg.selectAll(".outer-service")
                .data(serviceData, d => d.id);

            // 移除旧元素,添加淡出动画
            services.exit()
                .transition()
                .duration(800)
                .attr("opacity", 0)
                .remove();

            // 添加新元素,添加淡入动画
            const newServices = services.enter()
                .append("path")
                .attr("class", "outer-service")
                .attr("opacity", 0);

            // 合并更新所有元素
            const allServices = newServices.merge(services);

            allServices.transition()
                .duration(800)
                .attr("opacity", 0.8)
                .attr("d", arcGenerator)
                .attr("transform", `translate(${centerX}, ${centerY})`);
        }

        // 动态更新数据示例
        function updateData() {
            // 随机修改成员分组和服务数据,模拟动态场景
            memberData = [
                { id: 1, name: "成员1", group: "开发组" },
                { id: 2, name: "成员2", group: "设计组" },
                { id: 3, name: "成员3", group: "开发组" },
                { id: 6, name: "成员6", group: "测试组" },
                { id: 7, name: "成员7", group: "测试组" },
                { id: 8, name: "成员8", group: "产品组" }
            ];

            serviceData = [
                { id: "s1", name: "UI设计", angleRange: [0, Math.PI/4] },
                { id: "s3", name: "测试服务", angleRange: [Math.PI/4, Math.PI/2] },
                { id: "s4", name: "产品咨询", angleRange: [Math.PI/2, Math.PI] }
            ];

            // 重新渲染
            renderInnerMembers();
            renderOuterServices();
        }
    </script>
</body>
</html>

关键细节说明

  • 内圈分组:聚合数据后按成员占比分发角度,组内成员均匀排布,更新时自动重新计算并过渡
  • 外圈服务:通过数据绑定模式动态管理弧元素,配合过渡动画保证视觉流畅性
  • 点击「更新数据」按钮可触发动态变化,验证功能有效性

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 14:10:26