「数据可视化 D3系列」入门第十一章:力导向图深度解析与实现


一、力导向图核心概念

力导向图是一种通过物理模拟来展示复杂关系网络的图表类型,特别适合表现社交网络、知识图谱、系统拓扑等关系型数据。其核心原理是通过模拟粒子间的物理作用力(电荷斥力、弹簧引力等)自动计算节点的最优布局。

核心API详解

1. 力模拟系统

const simulation = d3.forceSimulation(nodes)
    .force("charge", d3.forceManyBody().strength(-100)) // 节点间作用力
    .force("link", d3.forceLink(links).id(d => d.id))   // 连接线作用力
    .force("center", d3.forceCenter(width/2, height/2)) // 向心力
    .force("collision", d3.forceCollide().radius(20));  // 碰撞检测

2. 关键作用力类型

力类型作用描述常用配置方法
forceManyBody节点间电荷力(正为引力,负为斥力).strength()
forceLink连接线弹簧力.distance().id().strength()
forceCenter向中心点的引力.x().y()
forceCollide防止节点重叠的碰撞力.radius().strength()
forceX/Y沿X/Y轴方向的定位力.strength().x()/.y()

3. 动态控制方法

simulation
    .alpha(0.3)        // 设置当前alpha值(0-1)
    .alphaTarget(0.1)  // 设置目标alpha值
    .alphaDecay(0.02)  // 设置衰减率(默认0.0228)
    .velocityDecay(0.4)// 设置速度衰减(0-1)
    .restart()         // 重启模拟
    .stop()            // 停止模拟
    .tick()            // 手动推进模拟一步

二、增强版力导向图实现

👇 具体代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>D3.js 力导向图 Demo</title>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
        }
        .node {
            stroke: #fff;
            stroke-width: 1.5px;
        }
        .link {
            stroke: #999;
            stroke-opacity: 0.6;
        }
        .link-text {
            font-size: 10px;
            fill: #333;
            pointer-events: none;
        }
        .node-text {
            font-size: 12px;
            font-weight: bold;
            pointer-events: none;
        }
        .tooltip {
            position: absolute;
            padding: 8px;
            background: rgba(0, 0, 0, 0.8);
            color: white;
            border-radius: 4px;
            pointer-events: none;
            font-size: 12px;
        }
        .controls {
            margin-bottom: 15px;
        }
        button {
            padding: 5px 10px;
            margin-right: 10px;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <div class="controls">
        <button id="reset">重置布局</button>
        <button id="addNode">添加随机节点</button>
        <span>斥力强度: <input type="range" id="charge" min="-500" max="-50" value="-200"></span>
    </div>
    <svg width="800" height="600"></svg>
    <div class="tooltip"></div>

    <script>
        // 示例数据 - 人物和地点关系网络
        const nodes = [
            { id: 1, name: "北京", type: "location", size: 15 },
            { id: 2, name: "上海", type: "location", size: 15 },
            { id: 3, name: "广州", type: "location", size: 15 },
            { id: 4, name: "张三", type: "person", size: 10 },
            { id: 5, name: "李四", type: "person", size: 10 },
            { id: 6, name: "王五", type: "person", size: 10 },
            { id: 7, name: "CEO", type: "role", size: 12 },
            { id: 8, name: "CTO", type: "role", size: 12 }
        ];

        const links = [
            { source: 1, target: 4, relation: "居住", value: 2 },
            { source: 2, target: 5, relation: "工作", value: 1.5 },
            { source: 3, target: 6, relation: "出生", value: 2.5 },
            { source: 4, target: 5, relation: "朋友", value: 1 },
            { source: 5, target: 6, relation: "同事", value: 1.2 },
            { source: 4, target: 7, relation: "职位", value: 1.8 },
            { source: 5, target: 8, relation: "职位", value: 1.8 },
            { source: 7, target: 8, relation: "合作", value: 1.5 }
        ];

        // 初始化SVG
        const svg = d3.select('svg');
        const width = +svg.attr('width');
        const height = +svg.attr('height');
        const tooltip = d3.select('.tooltip');

        // 创建画布
        const g = svg.append('g');

        // 颜色比例尺
        const colorScale = d3.scaleOrdinal()
            .domain(["location", "person", "role"])
            .range(["#66c2a5", "#fc8d62", "#8da0cb"]);

        // 创建力导向图模拟
        const simulation = d3.forceSimulation(nodes)
            .force("link", d3.forceLink(links).id(d => d.id).distance(100))
            .force("charge", d3.forceManyBody().strength(-200))
            .force("center", d3.forceCenter(width / 2, height / 2))
            .force("collision", d3.forceCollide().radius(d => d.size + 5));

        // 创建连接线
        const link = g.append('g')
            .selectAll('.link')
            .data(links)
            .enter().append('line')
            .attr('class', 'link')
            .attr('stroke-width', d => Math.sqrt(d.value));

        // 创建连接线文字
        const linkText = g.append('g')
            .selectAll('.link-text')
            .data(links)
            .enter().append('text')
            .attr('class', 'link-text')
            .text(d => d.relation);

        // 创建节点组
        const node = g.append('g')
            .selectAll('.node')
            .data(nodes)
            .enter().append('g')
            .attr('class', 'node')
            .call(d3.drag()
                .on('start', dragStarted)
                .on('drag', dragged)
                .on('end', dragEnded)
            )
            .on('mouseover', showTooltip)
            .on('mouseout', hideTooltip);

        // 添加节点圆形
        node.append('circle')
            .attr('r', d => d.size)
            .attr('fill', d => colorScale(d.type))
            .attr('stroke', '#fff')
            .attr('stroke-width', 2);

        // 添加节点文字
        node.append('text')
            .attr('class', 'node-text')
            .attr('dy', 4)
            .text(d => d.name);

        // 模拟tick事件处理
        simulation.on('tick', () => {
            link
                .attr('x1', d => d.source.x)
                .attr('y1', d => d.source.y)
                .attr('x2', d => d.target.x)
                .attr('y2', d => d.target.y);
            
            linkText
                .attr('x', d => (d.source.x + d.target.x) / 2)
                .attr('y', d => (d.source.y + d.target.y) / 2);
            
            node
                .attr('transform', d => `translate(${d.x},${d.y})`);
        });

        // 拖拽事件处理
        function dragStarted(event, d) {
            if (!event.active) simulation.alphaTarget(0.3).restart();
            d.fx = d.x;
            d.fy = d.y;
        }

        function dragged(event, d) {
            d.fx = event.x;
            d.fy = event.y;
        }

        function dragEnded(event, d) {
            if (!event.active) simulation.alphaTarget(0);
            d.fx = null;
            d.fy = null;
        }

        // 工具提示
        function showTooltip(event, d) {
            tooltip.transition()
                .duration(200)
                .style('opacity', 0.9);
            tooltip.html(`<strong>${d.name}</strong><br/>类型: ${d.type}`)
                .style('left', (event.pageX + 10) + 'px')
                .style('top', (event.pageY - 28) + 'px');
            
            // 高亮相关节点和连接线
            node.select('circle').attr('opacity', 0.2);
            d3.select(this).select('circle').attr('opacity', 1);
            
            link.attr('stroke-opacity', 0.1);
            link.filter(l => l.source.id === d.id || l.target.id === d.id)
                .attr('stroke-opacity', 0.8)
                .attr('stroke', '#ff0000');
        }

        function hideTooltip() {
            tooltip.transition()
                .duration(500)
                .style('opacity', 0);
            
            // 恢复所有元素样式
            node.select('circle').attr('opacity', 1);
            link.attr('stroke-opacity', 0.6)
                .attr('stroke', '#999');
        }

        // 交互控制
        d3.select('#reset').on('click', () => {
            simulation.alpha(1).restart();
            nodes.forEach(d => {
                d.fx = null;
                d.fy = null;
            });
        });

        d3.select('#addNode').on('click', () => {
            const newNodeId = nodes.length + 1;
            const types = ["person", "location", "role"];
            const newNode = {
                id: newNodeId,
                name: `节点${newNodeId}`,
                type: types[Math.floor(Math.random() * types.length)],
                size: Math.random() * 10 + 8
            };
            nodes.push(newNode);
            
            // 随机连接到现有节点
            if (nodes.length > 1) {
                const randomTarget = Math.floor(Math.random() * (nodes.length - 1)) + 1;
                const relations = ["朋友", "同事", "居住", "工作", "合作", "管理"];
                links.push({
                    source: newNode.id,
                    target: randomTarget,
                    relation: relations[Math.floor(Math.random() * relations.length)],
                    value: Math.random() * 2 + 0.5
                });
            }
            
            // 更新图形
            updateGraph();
        });

        d3.select('#charge').on('input', function() {
            simulation.force('charge').strength(+this.value);
            simulation.alpha(0.3).restart();
        });

        // 更新图形函数
        function updateGraph() {
            // 更新节点
            const nodeUpdate = node.data(nodes, d => d.id);
            
            // 移除不再需要的节点
            nodeUpdate.exit().remove();
            
            // 添加新节点
            const newNode = nodeUpdate.enter()
                .append('g')
                .attr('class', 'node')
                .call(d3.drag()
                    .on('start', dragStarted)
                    .on('drag', dragged)
                    .on('end', dragEnded)
                )
                .on('mouseover', showTooltip)
                .on('mouseout', hideTooltip);
            
            newNode.append('circle')
                .attr('r', d => d.size)
                .attr('fill', d => colorScale(d.type))
                .attr('stroke', '#fff')
                .attr('stroke-width', 2);
            
            newNode.append('text')
                .attr('class', 'node-text')
                .attr('dy', 4)
                .text(d => d.name);
            
            // 合并更新
            node = newNode.merge(nodeUpdate);
            
            // 更新连接线
            const linkUpdate = link.data(links, d => `${d.source.id}-${d.target.id}`);
            
            // 移除不再需要的连接线
            linkUpdate.exit().remove();
            
            // 添加新连接线
            const newLink = linkUpdate.enter()
                .append('line')
                .attr('class', 'link')
                .attr('stroke-width', d => Math.sqrt(d.value));
            
            // 合并更新
            link = newLink.merge(linkUpdate);
            
            // 更新连接线文字
            const linkTextUpdate = linkText.data(links, d => `${d.source.id}-${d.target.id}`);
            
            // 移除不再需要的连接线文字
            linkTextUpdate.exit().remove();
            
            // 添加新连接线文字
            const newLinkText = linkTextUpdate.enter()
                .append('text')
                .attr('class', 'link-text')
                .text(d => d.relation);
            
            // 合并更新
            linkText = newLinkText.merge(linkTextUpdate);
            
            // 更新力导向图
            simulation.nodes(nodes);
            simulation.force("link").links(links);
            simulation.alpha(1).restart();
        }
    </script>
</body>
</html>

👇 实现效果
在这里插入图片描述


小结

核心实现要点

1. 力模拟系统构建

  • 多力组合实现复杂布局(电荷力+弹簧力+向心力+碰撞力)
  • 参数调优实现不同视觉效果

2. 动态交互体系

  • 拖拽行为与物理模拟的协调
  • 动态alpha值控制模拟过程
  • 实时tick更新机制

3. 可视化增强

  • 基于类型的颜色编码
  • 交互式高亮关联元素
  • 动态工具提示显示

高级特性实现

1. 动态数据更新

  • 节点/连接的实时添加
  • 模拟系统的热更新

2. 交互控制面板

  • 力参数实时调节
  • 布局重置功能

3. 视觉优化

  • 智能碰撞检测
  • 连接线权重可视化
  • 焦点元素高亮

下章预告:树状图

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

八了个戒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值