17、D3力布局的高级应用与实践

D3力布局的高级应用与实践

1. 生成动量

在之前的实践中,我们已经接触过力布局节点对象及其 {x, y} 属性,这些属性决定了节点在布局中的位置。现在,我们将探讨物理运动模拟的另一个有趣方面:动量。D3力布局内置了对动量模拟的支持,这依赖于节点对象上的 {px, py} 属性。

操作步骤
1. 打开以下文件的本地副本: https://github.com/NickQiZhu/d3-cookbook/blob/master/src/chapter11/momentum-and-friction.html
2. 修改代码,首先禁用重力和电荷,然后为新添加的节点赋予一些初始速度。代码如下:

<script type="text/javascript">
    var force = d3.layout.force()
            .gravity(0)
            .charge(0)
            .friction(0.95);
    var svg = d3.select("body").append("svg:svg");
    force.on("tick", function () {
        // omitted, same as previous recipe
       ...
    });
    var previousPoint;
    svg.on("mousemove", function () {
        var point = d3.mouse(this),
            node = {
                x: point[0],
                y: point[1],
                px: previousPoint ? previousPoint[0] : point[0],
                py: previousPoint ? previousPoint[1] : point[1]
            };
        previousPoint = point;
        // omitted, same as previous recipe
       ...
    });
</script> 

这个实践生成了一个粒子系统,其初始方向速度与用户的鼠标移动成正比。

工作原理
整体结构与之前的实践非常相似,当用户移动鼠标时会生成粒子。一旦力模拟开始,粒子的位置完全由力布局在其 tick 事件监听器函数中控制。不过,在这个实践中,我们关闭了重力和电荷,以便更清晰地关注动量。保留了一些摩擦力,使速度衰减,让模拟看起来更真实。力布局配置如下:

var force = d3.layout.force()
            .gravity(0)
            .charge(0)
            .friction(0.95);

主要区别在于,我们不仅跟踪当前鼠标位置,还跟踪上一个鼠标位置。每当用户移动鼠标时,会生成一个包含当前位置 {x, y} 和上一个位置 {px, py} 的节点对象。由于用户鼠标位置是按固定间隔采样的,用户移动鼠标越快,这两个位置之间的距离就越远。力布局会自动将这两个位置的属性和方向信息转换为我们创建的每个粒子的初始动量。

力布局节点对象的其他属性
| 属性 | 描述 |
| ---- | ---- |
| index | 节点在节点数组中的从零开始的索引 |
| x | 当前节点位置的x坐标 |
| y | 当前节点位置的y坐标 |
| px | 上一个节点位置的x坐标 |
| py | 上一个节点位置的y坐标 |
| fixed | 一个布尔值,指示节点位置是否锁定 |
| weight | 节点权重,即关联链接的数量 |

2. 设置链接约束

到目前为止,我们已经介绍了力布局的一些重要方面,如重力、电荷、摩擦力和动量。现在,我们将讨论另一个关键功能:链接。D3力布局实现了可扩展的简单图约束,我们将演示如何结合其他力来利用链接约束。

操作步骤
1. 打开以下文件的本地副本: https://github.com/NickQiZhu/d3-cookbook/blob/master/src/chapter11/link-constraint.html
2. 实现代码,当用户点击鼠标时,生成一个由节点之间的链接约束的力导向粒子环。代码如下:

<script type="text/javascript">
    var force = d3.layout.force()
            .gravity(0.1)
            .charge(-30)
            .friction(0.95)
            .linkDistance(20)
            .linkStrength(1);
    var duration = 60000; // in milliseconds
    var svg = d3.select("body").append("svg:svg");
    force.size([1100, 600])
        .on("tick", function () {
            // omitted, will be discussed in details later
            ...
        });
    function offset() {
        return Math.random() * 100;
    }
    function createNodes(point) {
        var numberOfNodes = Math.round(Math.random() * 10);
        var nodes = [];
        for (var i = 0; i < numberOfNodes; ++i) {
            nodes.push({
                x: point[0] + offset(), 
                y: point[1] + offset()
            });
        }
        return nodes;
    }
    function createLinks(nodes) {
        var links = [];
        for (var i = 0; i < nodes.length; ++i) { 
            if(i == nodes.length - 1) 
                links.push(
                    {source: nodes[i], target: nodes[0]}
                );
            else
                links.push(
                    {source: nodes[i], target: nodes[i + 1]}
                );
        }
        return links;
    }
    svg.on("click", function () {
        var point = d3.mouse(this),
                nodes = createNodes(point),
                links = createLinks(nodes);
        nodes.forEach(function (node) {
            svg.append("circle")
                    .data([node])
                .attr("class", "node")
                .attr("cx", function (d) {return d.x;})
                .attr("cy", function (d) {return d.y;})
                .attr("r", 1e-6)
                .call(force.drag)
                    .transition()
                .attr("r", 7)
                    .transition()
                    .delay(duration)
                .attr("r", 1e-6)
                .each("end", function () {force.nodes().shift();})
                .remove();
        });
        links.forEach(function (link) {
            svg.append("line") 
                    .data([link])
                .attr("class", "line")
                .attr("x1", function (d) {
                   return d.source.x;
                    })
                .attr("y1", function (d) {
                   return d.source.y;
                })
                .attr("x2", function (d) {
                   return d.target.x;
                    })
                .attr("y2", function (d) {
                   return d.target.y;
                })
                    .transition()
                    .delay(duration)
                .style("stroke-opacity", 1e-6)
                .each("end", function () {
                   force.links().shift();
                })
                .remove();
        });
        nodes.forEach(function (n) {force.nodes().push(n);});
        links.forEach(function (l) { 
          force.links().push(l);
       });
        force.start();
    });
</script>

工作原理
链接约束为力辅助可视化增加了另一个有用的维度。我们设置力布局的参数如下:

var force = d3.layout.force()
            .gravity(0.1)
            .charge(-30)
            .friction(0.95)
            .linkDistance(20)
            .linkStrength(1);

除了重力、电荷和摩擦力,这次我们有两个额外的参数:链接距离和链接强度。
- linkDistance :可以是一个常量或一个函数,默认为20像素。在布局开始时评估链接距离,并作为弱几何约束实现。对于布局的每个 tick ,会计算每对链接节点之间的距离,并与目标距离进行比较,然后将链接相互靠近或远离。
- linkStength :可以是一个常量或一个函数,默认为1。链接强度设置链接的强度(刚性),值范围在 [0, 1] 之间。链接强度也在布局开始时评估。

当用户点击鼠标时,会创建随机数量的节点,并将其置于力布局的控制之下。主要的新增部分是链接的创建和控制逻辑。在 createLinks 函数中,创建了 n - 1 个链接对象,将一组节点连接成一个环。每个链接对象必须指定两个属性 source target ,告诉力布局哪对节点由这个链接对象连接。创建后,我们使用 svg:line 元素来可视化链接。最后,需要将链接对象添加到力布局的 links 数组中,以便它们可以受到力布局的控制。在 tick 函数中,需要将力布局生成的定位数据转换为SVG实现。

流程图

graph TD;
    A[用户点击鼠标] --> B[创建节点];
    B --> C[创建链接];
    C --> D[添加节点到力布局];
    D --> E[添加链接到力布局];
    E --> F[开始力布局];
    F --> G[更新节点和链接位置];
3. 使用力辅助可视化

到目前为止,我们已经学会了使用力布局来可视化粒子和链接,类似于在经典应用中使用力导向图。但这并不是在可视化中利用力的唯一方式。我们将探索一种称为力辅助可视化的技术,通过利用力为可视化添加一些随机性和任意性。

操作步骤
1. 打开以下文件的本地副本: https://github.com/NickQiZhu/d3-cookbook/blob/master/src/chapter11/arbitrary-visualization.html
2. 实现代码,当用户点击鼠标时生成气泡。气泡由填充渐变颜色的 svg:path 元素组成。代码如下:

<svg>
    <defs>
        <radialGradient id="gradient" cx="50%" cy="50%" r="100%"  fx="50%" fy="50%">
            <stop offset="0%" style="stop-color:blue;stop-opacity:0"/>
            <stop offset="100%" style="stop-color:rgb(255,255,255);stop-opacity:1"/>
        </radialGradient>
    </defs>
</svg>
<script type="text/javascript">
    var force = d3.layout.force()
            .gravity(0.1)
            .charge(-30)
            .friction(0.95)
            .linkDistance(20)
            .linkStrength(0.5);
    var duration = 10000;
    var svg = d3.select("svg");
    var line = d3.svg.line()
            .interpolate("basis-closed")
            .x(function(d){return d.x;})
            .y(function(d){return d.y;});
    force.size([svg.node().clientWidth, svg.node().clientHeight])
        .on("tick", function () {
            // omitted, will be discussed in details later
            ...
        });
    function offset() {
        return Math.random() * 100;
    }
    function createNodes(point) {
        // omitted, same as previous recipe
       ...
    }
    function createLinks(nodes) {
        // omitted, same as previous recipe
       ...
    }
    svg.on("click", function () {
        var point = d3.mouse(this),
                nodes = createNodes(point),
                links = createLinks(nodes);
        var circles = svg.append("path")
                .data([nodes])
            .attr("class", "bubble")
            .attr("fill", "url(#gradient)") 
            .attr("d", function(d){return line(d);}) 
                .transition().delay(duration)
            .attr("fill-opacity", 0)
            .attr("stroke-opacity", 0)
            .each("end", function(){d3.select(this).remove();});
        nodes.forEach(function (n) {force.nodes().push(n);});
        links.forEach(function (l) {force.links().push(l);});
        force.start();
    });
</script>

工作原理
这个实践基于之前的实践,整体方法与之前创建力控制的粒子环的实践非常相似。主要区别在于,我们使用 d3.svg.line 生成器来创建勾勒气泡轮廓的 svg:path 元素,而不是使用 svg:circle svg:line 。在 tick 函数中,我们简单地重新调用线生成器函数来更新每个路径的 d 属性,从而使用力布局计算来动画化气泡。

4. 操纵力

之前我们探索了D3力布局的许多有趣方面和应用,但在之前的实践中,我们只是将力布局的计算(重力、电荷、摩擦力和动量)直接应用于可视化。现在,我们将更进一步,实现自定义力操纵,创建我们自己类型的力。

操作步骤
1. 打开以下文件的本地副本: https://github.com/NickQiZhu/d3-cookbook/blob/master/src/chapter11/multi-foci.html
2. 实现代码,首先生成五组彩色粒子,然后将相应的颜色和分类力拉力分配给用户的触摸,从而只拉动与颜色匹配的粒子。代码如下:

<script type="text/javascript">
    var svg = d3.select("body").append("svg:svg"),
            colors = d3.scale.category10(),
            w = 900,
            h = 600;
    svg.attr("width", w).attr("height", h);
    var force = d3.layout.force()
            .gravity(0.1)
            .charge(-30)
            .size([w, h]);
    var nodes = force.nodes(),
            centers = [];
    for (var i = 0; i < 5; ++i) {
        for (var j = 0; j < 50; ++j) {
            nodes.push({x: w / 2 + offset(), 
              y: h / 2 + offset(), 
              color: colors(i), 
              type: i}); 
        }
    }
    function offset() {
        return Math.random() * 100;
    }
    svg.selectAll("circle")
                .data(nodes).enter()
            .append("circle")
            .attr("class", "node")
            .attr("cx", function (d) {return d.x;})
            .attr("cy", function (d) {return d.y;})
            .attr("fill","function(d){return d.color;}")
            .attr("r", 1e-6)
                .transition()
            .attr("r", 4.5);
    force.on("tick", function(e) {
          var k = e.alpha * .2;
          nodes.forEach(function(node) {
            var center = centers[node.type];
            if(center){
                node.x += (center[0] - node.x) * k; 
                node.y += (center[1] - node.y) * k; 
            }
          });
          svg.selectAll("circle")
              .attr("cx", function(d) { return d.x; })
              .attr("cy", function(d) { return d.y; });
    });
    d3.select("body")
        .on("touchstart", touch)
        .on("touchend", touch);
    function touch() {
        d3.event.preventDefault();
        centers = d3.touches(svg.node());
        var g = svg.selectAll("g.touch")
                .data(centers, function (d) {
                    return d.identifier;
                });
        g.enter()
            .append("g")
            .attr("class", "touch")
            .attr("transform", function (d) {
                return "translate(" + d[0] + "," + d[1] + ")";
            })
            .append("circle")
                .attr("class", "touch")
                .attr("fill", function(d){
                   return colors(d.identifier);
                })
                    .transition()
                .attr("r", 50);
        g.exit().remove();
        force.resume();
    }
    force.start();
</script>

工作原理
第一步是创建彩色粒子,并在重力和排斥力之间建立标准力平衡。所有节点对象都包含单独的颜色和类型ID属性,以便稍后轻松识别。当用户触摸时,会创建一个 svg:circle 元素来表示触摸点。一旦确定了触摸点,所有自定义力的实现都在 tick 函数中完成。在 tick 函数中,我们遇到了一个新的概念: alpha 参数。 alpha 是力布局使用的内部冷却参数,从0.1开始,随着布局 tick 逐渐趋近于0。简单来说, alpha 值越高,力越混乱,当 alpha 接近0时,布局变得更稳定。在这个实现中,我们利用 alpha 值使自定义力实现与其他内置力同步冷却,因为粒子的移动是通过 k 系数( alpha 的导数)计算的,将它们移向匹配的触摸点。

通过以上这些实践,我们可以看到D3力布局在可视化中的强大功能和灵活性,可以根据不同的需求实现各种复杂的效果。

D3力布局的高级应用与实践(续)

5. 总结与对比

为了更清晰地理解不同力布局应用的特点,我们对前面介绍的几种实践进行总结与对比,如下表所示:
| 应用场景 | 主要操作 | 关键参数 | 效果特点 |
| ---- | ---- | ---- | ---- |
| 生成动量 | 禁用重力和电荷,跟踪鼠标位置赋予节点初始速度 | gravity(0) charge(0) friction(0.95) | 粒子系统初始方向速度与鼠标移动成正比,模拟动量效果 |
| 设置链接约束 | 用户点击鼠标生成力导向粒子环,创建节点和链接并控制 | gravity(0.1) charge(-30) friction(0.95) linkDistance(20) linkStrength(1) | 节点通过链接形成环,链接有距离和强度约束 |
| 力辅助可视化 | 用户点击鼠标生成气泡,使用 d3.svg.line 生成器创建 svg:path 元素 | gravity(0.1) charge(-30) friction(0.95) linkDistance(20) linkStrength(0.5) | 为可视化添加随机性和任意性,气泡有渐变效果 |
| 操纵力 | 生成彩色粒子,根据触摸点拉动匹配颜色的粒子 | gravity(0.1) charge(-30) | 实现自定义力操纵,彩色粒子受触摸点分类力影响 |

6. 注意事项与技巧

在使用D3力布局进行可视化开发时,有一些注意事项和技巧可以帮助我们更好地实现预期效果:
- 参数调整 :不同的参数设置会对可视化效果产生显著影响。例如, gravity charge friction 等参数需要根据具体需求进行调整。在生成动量的实践中,将 gravity charge 设置为0可以更专注于动量效果;而在设置链接约束时, linkDistance linkStrength 的合理设置能使节点之间的链接呈现出理想的状态。
- 性能优化 :当节点和链接数量较多时,力布局的计算量会增大,可能导致性能下降。可以通过合理设置 alpha 参数来控制布局的收敛速度,避免不必要的计算。同时,在 tick 函数中尽量减少复杂的操作,提高性能。
- 事件处理 :在处理鼠标点击、触摸等事件时,要注意事件的兼容性和响应性。例如,在操纵力的实践中,使用 d3.event.preventDefault() 可以防止默认事件的干扰,确保触摸事件的正常处理。

7. 拓展应用思路

D3力布局的应用场景非常广泛,除了前面介绍的几种实践,我们还可以拓展出更多有趣的应用:
- 社交网络可视化 :可以将节点表示为用户,链接表示用户之间的关系,通过力布局展示社交网络的结构。可以根据用户的活跃度、影响力等因素设置节点的大小和颜色,根据关系的强弱设置链接的粗细和颜色。
- 分子结构可视化 :将原子表示为节点,化学键表示为链接,利用力布局模拟分子的结构和动态变化。可以通过设置不同的力参数来模拟分子间的相互作用,展示分子的稳定性和反应过程。
- 物流网络可视化 :把仓库、配送中心等表示为节点,运输路线表示为链接,通过力布局展示物流网络的分布和流量情况。可以根据货物的运输量、运输时间等因素设置节点和链接的属性,帮助优化物流网络的规划和管理。

8. 未来发展趋势

随着数据可视化技术的不断发展,D3力布局也将不断演进和完善。未来可能会出现以下发展趋势:
- 与其他技术融合 :D3力布局可能会与人工智能、机器学习等技术相结合,实现更智能、自适应的可视化效果。例如,通过机器学习算法自动调整力布局的参数,以适应不同类型的数据和用户需求。
- 支持更多交互方式 :除了鼠标和触摸交互,未来可能会支持更多的交互方式,如语音控制、手势识别等,为用户提供更便捷、自然的交互体验。
- 跨平台应用 :随着移动设备和物联网的普及,D3力布局可能会更好地支持跨平台应用,能够在不同的设备和操作系统上实现一致的可视化效果。

通过对D3力布局的深入学习和实践,我们可以充分发挥其强大的功能,创造出更加丰富、生动、有价值的可视化作品。无论是在学术研究、商业分析还是艺术创作等领域,D3力布局都有着广阔的应用前景。希望本文介绍的内容能够为你在数据可视化的道路上提供一些启发和帮助。

【事件触发一致性】研究多智能体网络如何通过分布式事件驱动控制实现有限时间内的共识(Matlab代码实现)内容概要:本文围绕多智能体网络中的事件触发一致性问题,研究如何通过分布式事件驱动控制实现有限时间内的共识,并提供了相应的Matlab代码实现方案。文中探讨了事件触发机制在降低通信负担、提升系统效率方面的优势,重点分析了多智能体系统在有限时间收敛的一致性控制策略,涉及系统模型构建、触发条件设计、稳定性收敛性分析等核心技术环节。此外,文档还展示了该技术在航空航天、电系统、机器人协同、无人机编队等多个前沿领域的潜在应用,体现了其跨学科的研究价值和工程实用性。; 适合人群:具备一定控制理论基础和Matlab编程能的研究生、科研人员及从事自动化、智能系统、多智能体协同控制等相关领域的工程技术人员。; 使用场景及目标:①用于理解和实现多智能体系统在有限时间内达成一致的分布式控制方法;②为事件触发控制、分布式优化、协同控制等课题提供算法设计仿真验证的技术参考;③支撑科研项目开发、学术论文复现及工程原型系统搭建; 阅读建议:建议结合文中提供的Matlab代码进行实践操作,重点关注事件触发条件的设计逻辑系统收敛性证明之间的关系,同时可延伸至其他应用场景进行二次开发性能优化。
【四旋翼无人机】具备螺旋桨倾斜机构的全驱动四旋翼无人机:建模控制研究(Matlab代码、Simulink仿真实现)内容概要:本文围绕具备螺旋桨倾斜机构的全驱动四旋翼无人机展开,重点研究其动学建模控制系统设计。通过Matlab代码Simulink仿真实现,详细阐述了该类无人机的运动学学模型构建过程,分析了螺旋桨倾斜机构如何提升无人机的全向机动能姿态控制性能,并设计相应的控制策略以实现稳定飞行精确轨迹跟踪。文中涵盖了从系统建模、控制器设计到仿真验证的完整流程,突出了全驱动结构相较于传统四旋翼在欠驱动问题上的优势。; 适合人群:具备一定控制理论基础和Matlab/Simulink使用经验的自动化、航空航天及相关专业的研究生、科研人员或无人机开发工程师。; 使用场景及目标:①学习全驱动四旋翼无人机的动学建模方法;②掌握基于Matlab/Simulink的无人机控制系统设计仿真技术;③深入理解螺旋桨倾斜机构对飞行性能的影响及其控制实现;④为相关课题研究或工程开发提供可复现的技术参考代码支持。; 阅读建议:建议读者结合提供的Matlab代码Simulink模型,逐步跟进文档中的建模控制设计步骤,动手实践仿真过程,以加深对全驱动无人机控制原理的理解,并可根据实际需求对模型控制器进行修改优化。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值