Javascript 如何对D3中的节点进行排序,以便清除连接路径?

Javascript 如何对D3中的节点进行排序,以便清除连接路径?,javascript,algorithm,sorting,d3.js,data-visualization,Javascript,Algorithm,Sorting,D3.js,Data Visualization,我正在绘制一张图表,以显示两组之间的关系。 为了说明这个问题,让我们假设这两个群体是城市和超级英雄,对于每个城市,我们展示哪个超级英雄访问过它,反之亦然 我现在的代码 将鼠标悬停在每个节点上以查看其名称并高亮显示其连接的节点 如您所见,我在顶部有一行,每个城市有节点,底部有一行,每个超级英雄有节点,两行之间的路径显示了关系: 正如你所看到的,在大多数情况下,路径都很长,并且相互交叉很多。这使得数字图像在视觉上非常混乱 我确信,如果我以更智能的方式对城市和/或超级英雄进行排序,图表将更加清晰。


我正在绘制一张图表,以显示两组之间的关系。
为了说明这个问题,让我们假设这两个群体是城市和超级英雄,对于每个城市,我们展示哪个超级英雄访问过它,反之亦然

我现在的代码

将鼠标悬停在每个节点上以查看其名称并高亮显示其连接的节点

如您所见,我在顶部有一行,每个城市有节点,底部有一行,每个超级英雄有节点,两行之间的路径显示了关系:

正如你所看到的,在大多数情况下,路径都很长,并且相互交叉很多。这使得数字图像在视觉上非常混乱

我确信,如果我以更智能的方式对城市和/或超级英雄进行排序,图表将更加清晰。
(这对当前的城市秩序和超级英雄的秩序都没有意义,为了说明这一点,我在加载图表之前将这两个数组洗牌)

我的问题:
如何对城市和/或英雄进行排序,以便两者之间的联系更加清晰(路径不会那么长,路径不会相互交叉,等等)。

有没有一个众所周知的算法?可能是D3中的辅助函数?或者可能我使用了错误的图表类型。

可以使用其他可视化类型,这样可以更清楚地显示这些信息。脑海中浮现出一个网格,但它在视觉上可能不如你目前所看到的那样令人兴奋:

(迈克·博斯托克):

这种类型的可视化可以很容易地被添加以显示角色和位置重叠。栅格的镜像不会与位置/角色轴一起出现。但是,如果节点和链接是可取的,我们当然可以使用它


取消可视化的角度设置

传统上,我会将这种类型的图表进一步混淆。这会使学校的测验过于复杂化,以期说服老师,让我相信怀疑比追踪路径更容易。很容易使这些网络在视觉上无法追踪,这表明,如果你的可视化变得更详细,这可能不是一种理想的风格。但是,为了弥补我以前制作不可读网络的过失,让我们来分析一下我们所拥有的

当然,有一种很好的算法可以计算出绘制图表所需的最小路径长度,将孤立的网络彼此分离,并在x轴上将源和目标尽可能靠近地对齐。尽管这可能会将所有高度连接的节点放在一起,导致可视化的中心看起来像一盘意大利面条

但是,由于我懒惰,我宁愿让图表做某种自组织。幸运的是,在d3中我们有一个部队布局

让我们从总体布局开始,一排英雄和一排城市。我们可以设置一些力和参数来发挥作用,而不是随机化每个节点的位置并绘制连接线:

  • 固定y坐标,以便保留行
  • 高度重视链接距离,以便将链接节点紧密地拉在一起
  • 从节点之间的轻微吸引开始,允许混合,然后慢慢开始迫使它们分开
  • 随着布局的发展,增加碰撞半径,使节点的间距相等
如果我们做得正确,我们会发现一个很好的可视化,它可以分离孤立的网络(对于这种可视化来说是必不可少的),并且在很大程度上保持链接相对较短

下面是一个使用这种方法的随机输出示例(切成两半):

这种关系比随机排序的节点更清晰(侥幸的结果除外),有一些链接似乎延伸得很远,但数量相对有限。孤立的网络在视觉上也是分离的,这从拓扑角度来看很重要

作为比较,这是对原始布局的随机加载(我确实出于某种原因更改了节点样式和行顺序,但我手中的啤酒杯说不纠正它):

这是一个特别不幸的随机抽取,黑寡妇连接到x轴上的第一个和最后一个位置

原始布局中的平均链接远比部队布局中的平均链接水平-更多的垂直链接通过减少交叉点数量,同时缩短链接长度,从而提高清晰度和减少混乱

我们可以尝试量化部队布局带来的改进。例如,随机排序(样本大小=10)中的平均链路长度(测量为源和目标之间沿x轴的距离)为521 px,而强制布局排序的平均链路长度为208 px(样本大小也为10)。以下是每个版面链接长度的直方图:

我的样本量很小,但模式应该很清晰

好的,我已经展示了结果并说明了一般要求,但是演示怎么样。在不讨论力图如何工作或如何编码的情况下,这里是一个快速演示,下面是我使用的关键参数的快速说明:

  • 设置模拟的“开始”属性:
  • 一旦模拟开始,这些将被修改,但它们是良好的起点:

    var simulation = d3.forceSimulation()
        // set optimal distance to be 1 pixel between source and target:
        .force("link", d3.forceLink().id(function(d) { return d.name; }).distance(1))
        // set the distance at which forces apply to be limited to some distance,
        // make nodes attracted to each other to start:
        .force("charge", d3.forceManyBody().distanceMax(cities.length+1/width).strength(10))
        // try to keep nodes centered:
        .force("center", d3.forceCenter(width / 2, height / 2))
        // slow decay time, increases time to simulation end
        .alphaDecay(0.01);
    
  • 在模拟过程中修改力:
  • 我们想要改变一些力,比如当模拟逐渐减弱时,吸引力变成排斥力

    function ticked() {
        var force = this;
        var alpha = force.alpha(); // current alpha
        var padding = 20; // minimum distance from sides of visualization
        var targetSeparation = (width-padding*2)/(cities.length*2) // ideal separation between nodes on x axis.
    
        // if we are late in the simulation, change collide radius to ideal separation
        // also change the charge between nodes to repulsion
        if (alpha < 0.5) {
            force.force( "collide",d3.forceCollide((0.5 - alpha)*2*targetSeparation) )
                 .force("charge", d3.forceManyBody().distanceMax(targetSeparation).strength((0.5 - alpha) * 50))
        }
    
    勾选的函数(){
    var force=这个;
    var alpha=force.alpha();//当前alpha
    var padding=20;//与可视化侧面的最小距离
    var targetSeparation=(宽度填充*2)/(cities.length*2)//x轴上节点之间的理想间距。
    //如果我们是我
    
    function ticked() {
        var force = this;
        var alpha = force.alpha(); // current alpha
        var padding = 20; // minimum distance from sides of visualization
        var targetSeparation = (width-padding*2)/(cities.length*2) // ideal separation between nodes on x axis.
    
        // if we are late in the simulation, change collide radius to ideal separation
        // also change the charge between nodes to repulsion
        if (alpha < 0.5) {
            force.force( "collide",d3.forceCollide((0.5 - alpha)*2*targetSeparation) )
                 .force("charge", d3.forceManyBody().distanceMax(targetSeparation).strength((0.5 - alpha) * 50))
        }