首页 > 解决方案 > d3.js 强制有向树矩阵

问题描述

在以前的应用程序中,我将独立树调整为“矩阵”格式,该格式使用行和列在一个视觉对象中显示许多小的层次关系。请参阅:D3.js v5 - Swarm Tree - 如何迭代地围绕树节点居中集群

对于下一个项目,我尝试了同样的事情,但使用了“force-directed-trees”。

在我遵循与上述相同的程序之后遇到了一个障碍,并且在此过程中没有引发任何错误。

片段:

var margins = {top:100, bottom:300, left:100, right:100};

var height = 600;
var width = 900;

var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;

var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);

var graphGroup = svg.append('g')
.attr('transform', "translate("+margins.left+","+margins.top+")");

  var data =
[ {name:"Company1", tree:  {
   "name": "Product offerings",
   "children": [

    {"name": "Equity line", "size": 2200,
  "children": [
    {"name": "Equity fund 1", "size": 800,  "type" : "equity"},
    {"name": "Equity fund 2", "size": 600,  "type" : "equity"},
    {"name": "Equity fund 3", "size": 300,  "type" : "equity"},
    {"name": "Equity fund 4", "size": 250,  "type" : "equity"},
    {"name": "Equity fund 5", "size": 250,  "type" : "equity"},
    {"name": "Equity fund 6", "size": 525,  "type" : "equity"},
   ]
  },

    {"name": "Bond fund line", "size": 1400,
   "children": [
     {"name": "Bond fund 1", "size": 800,  "type" : "bond"},
     {"name": "Bond fund 2", "size": 600,  "type" : "bond"},
     {"name": "Bond fund 3", "size": 300,  "type" : "bond"},
     {"name": "Bond fund 4", "size": 250,  "type" : "bond"},
   ]
 },

 {"name": "Balanced line", "size": 1400,
"children": [
  {"name": "Bond fund 1", "size": 800,  "type" : "balanced"},
  {"name": "Bond fund 2", "size": 600,  "type" : "balanced"},
  {"name": "Bond fund 3", "size": 300,  "type" : "balanced"},
  {"name": "Bond fund 4", "size": 250,  "type" : "balanced"},
  {"name": "Bond fund 1", "size": 800,  "type" : "balanced"},
  {"name": "Bond fund 2", "size": 600,  "type" : "balanced"},
  {"name": "Bond fund 3", "size": 300,  "type" : "balanced"},
  {"name": "Bond fund 4", "size": 250,  "type" : "balanced"},
  {"name": "Bond fund 1", "size": 800,  "type" : "balanced"},
  {"name": "Bond fund 2", "size": 600,  "type" : "balanced"},
  {"name": "Bond fund 3", "size": 300,  "type" : "balanced"},
  {"name": "Bond fund 4", "size": 250,  "type" : "balanced"},
]
  },

   ]
 }},
 {name:"Company2", tree:  {
        "name": "Product offerings",
        "children": [

         {"name": "Equity line", "size": 2200,
      "children": [
         {"name": "Equity fund 1", "size": 800,  "type" : "equity"},
         {"name": "Equity fund 2", "size": 600,  "type" : "equity"},
         {"name": "Equity fund 3", "size": 300,  "type" : "equity"},
         {"name": "Equity fund 4", "size": 250,  "type" : "equity"},
        {"name": "Equity fund 5", "size": 250,  "type" : "equity"},
        {"name": "Equity fund 6", "size": 525,  "type" : "equity"},
        ]
      },

         {"name": "Bond fund line", "size": 1400,
       "children": [
          {"name": "Bond fund 1", "size": 800,  "type" : "bond"},
          {"name": "Bond fund 2", "size": 600,  "type" : "bond"},
          {"name": "Bond fund 3", "size": 300,  "type" : "bond"},
          {"name": "Bond fund 4", "size": 250,  "type" : "bond"},
        ]
      },

      {"name": "Balanced line", "size": 1400,
     "children": [
       {"name": "Bond fund 1", "size": 800,  "type" : "balanced"},
       {"name": "Bond fund 2", "size": 600,  "type" : "balanced"},
       {"name": "Bond fund 3", "size": 300,  "type" : "balanced"},
       {"name": "Bond fund 4", "size": 250,  "type" : "balanced"},
       {"name": "Bond fund 1", "size": 800,  "type" : "balanced"},
       {"name": "Bond fund 2", "size": 600,  "type" : "balanced"},
       {"name": "Bond fund 3", "size": 300,  "type" : "balanced"},
       {"name": "Bond fund 4", "size": 250,  "type" : "balanced"},
       {"name": "Bond fund 1", "size": 800,  "type" : "balanced"},
       {"name": "Bond fund 2", "size": 600,  "type" : "balanced"},
       {"name": "Bond fund 3", "size": 300,  "type" : "balanced"},
       {"name": "Bond fund 4", "size": 250,  "type" : "balanced"},
     ]
   },

        ]
      }},

]


  var columns = 3;
  var spacing = 200;
  var vSpacing = 180;

  var regionG = graphGroup.selectAll('.region')
.data(data)
.enter()
.append('g')
.attr('class', 'region')
.attr('id', (d, i) => 'region' + i)
.attr('transform', (d, k) => {
  var horSpace = (k % columns) * spacing;
  var vertSpace = ~~((k / columns)) * vSpacing;
  return "translate(" + horSpace + "," + vertSpace + ")";
});


var colorMap = {
  'equity':"#003366",
  'bond':"#f6d18b",
  'balanced':"#95b3d7"
};

  //const root = d3.hierarchy(data);
  //const links = root.links();
  //const nodes = root.descendants();

  const simulation = d3.forceSimulation(d3.hierarchy(function(d) {return d.tree}).descendants())
  .force("link", d3.forceLink(d3.hierarchy(function(d) {return d.tree}).links()).id(d => d.id).distance(0).strength(1))
  .force("charge", d3.forceManyBody().strength(-50))
  .force("x", d3.forceX())
  .force("y", d3.forceY());

  const link = regionG.append("g")
  .attr("stroke", "#999")
  .attr("stroke-opacity", 0.6)
.selectAll("line")
.data(d3.hierarchy(function(d) {return d.tree}).links())
.join("line");

  const node = regionG.append("g")
  .attr("fill", "#fff")
  .attr("stroke", "#000")
  .attr("stroke-width", 1.5)
.selectAll("circle")
.data(d3.hierarchy(function(d) {return d.tree}).descendants())
.join("circle")
  .attr("fill", d => d.children ? null : colorMap[d.data.type])
  .attr("stroke", d => d.children ? null : "#fff")
  .attr("r", 3.5);

  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);

node
    .attr("cx", d => d.x)
    .attr("cy", d => d.y);
  });
<script src="https://d3js.org/d3.v5.min.js"></script>

正如我们所看到的,我使用相同的方法,构造data为一个对象数组。一个对象条目是tree-- 它包含层次结构数据。(为了简单起见,我将列表限制为两个。

问题

鉴于我们迄今为止在早期相同的方法上取得的成功,有什么可能会导致力导向树的过程失败?

注意如果在没有初始力动画的情况下实现力树矩阵是可以接受的(实际上更可取)。

标签: javascriptd3.js

解决方案


当前代码中存在一些问题,一个是由于您创建圆圈的方式,每个力布局只有一个圆圈:

.data(d3.hierarchy(function(d) {return d.tree}).descendants())

代替:

.data(function(d) { return d3.hierarchy(d.tree).descendants() })

您也不会将实际节点传递给模拟。因为您有多组数据,所以最好有多个力模拟(尤其是所有都占据相同的坐标空间)。我认为在这里使用 selection.each() 为每个根创建力模拟会更容易。我们可以计算一次节点和链接(我们不想创建表示相同层次结构的新对象)并将它们传递给模拟和输入循环:

regionG.each(simulate);
  
function simulate(d) {
     let g = d3.select(this);
     let tree = d3.hierarchy(d.tree);
     let nodes = tree.descendants();
     let links = tree.links();
        
    const simulation = d3.forceSimulation(tree.descendants())
     .force("link", d3.forceLink(links).id(d => d.id).distance(0).strength(1))
     .force("charge", d3.forceManyBody().strength(-50))
     .force("x", d3.forceX())
     .force("y", d3.forceY());
    
    const link = g.append("g")
      .selectAll("line")
      .data(links)
      .join("line")
      ...
    
   const node = g.append("g")
     .selectAll("circle")
     .data(nodes)
     .join("circle")
     ...
    
  simulation.on("tick", ... })
    
}

var margins = {top:100, bottom:300, left:100, right:100};

var height = 600;
var width = 900;

var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;

var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);

var graphGroup = svg.append('g')
.attr('transform', "translate("+margins.left+","+margins.top+")");

  var data =
[ {name:"Company1", tree:  {
   "name": "Product offerings",
   "children": [

    {"name": "Equity line", "size": 2200,
  "children": [
    {"name": "Equity fund 1", "size": 800,  "type" : "equity"},
    {"name": "Equity fund 2", "size": 600,  "type" : "equity"},
    {"name": "Equity fund 3", "size": 300,  "type" : "equity"},
    {"name": "Equity fund 4", "size": 250,  "type" : "equity"},
    {"name": "Equity fund 5", "size": 250,  "type" : "equity"},
    {"name": "Equity fund 6", "size": 525,  "type" : "equity"},
   ]
  },

    {"name": "Bond fund line", "size": 1400,
   "children": [
     {"name": "Bond fund 1", "size": 800,  "type" : "bond"},
     {"name": "Bond fund 2", "size": 600,  "type" : "bond"},
     {"name": "Bond fund 3", "size": 300,  "type" : "bond"},
     {"name": "Bond fund 4", "size": 250,  "type" : "bond"},
   ]
 },

 {"name": "Balanced line", "size": 1400,
"children": [
  {"name": "Bond fund 1", "size": 800,  "type" : "balanced"},
  {"name": "Bond fund 2", "size": 600,  "type" : "balanced"},
  {"name": "Bond fund 3", "size": 300,  "type" : "balanced"},
  {"name": "Bond fund 4", "size": 250,  "type" : "balanced"},
  {"name": "Bond fund 1", "size": 800,  "type" : "balanced"},
  {"name": "Bond fund 2", "size": 600,  "type" : "balanced"},
  {"name": "Bond fund 3", "size": 300,  "type" : "balanced"},
  {"name": "Bond fund 4", "size": 250,  "type" : "balanced"},
  {"name": "Bond fund 1", "size": 800,  "type" : "balanced"},
  {"name": "Bond fund 2", "size": 600,  "type" : "balanced"},
  {"name": "Bond fund 3", "size": 300,  "type" : "balanced"},
  {"name": "Bond fund 4", "size": 250,  "type" : "balanced"},
]
  },

   ]
 }},
 {name:"Company2", tree:  {
        "name": "Product offerings",
        "children": [

         {"name": "Equity line", "size": 2200,
      "children": [
         {"name": "Equity fund 1", "size": 800,  "type" : "equity"},
         {"name": "Equity fund 2", "size": 600,  "type" : "equity"},
         {"name": "Equity fund 3", "size": 300,  "type" : "equity"},
         {"name": "Equity fund 4", "size": 250,  "type" : "equity"},
        {"name": "Equity fund 5", "size": 250,  "type" : "equity"},
        {"name": "Equity fund 6", "size": 525,  "type" : "equity"},
        ]
      },

         {"name": "Bond fund line", "size": 1400,
       "children": [
          {"name": "Bond fund 1", "size": 800,  "type" : "bond"},
          {"name": "Bond fund 2", "size": 600,  "type" : "bond"},
          {"name": "Bond fund 3", "size": 300,  "type" : "bond"},
          {"name": "Bond fund 4", "size": 250,  "type" : "bond"},
        ]
      },

      {"name": "Balanced line", "size": 1400,
     "children": [
       {"name": "Bond fund 1", "size": 800,  "type" : "balanced"},
       {"name": "Bond fund 2", "size": 600,  "type" : "balanced"},
       {"name": "Bond fund 3", "size": 300,  "type" : "balanced"},
       {"name": "Bond fund 4", "size": 250,  "type" : "balanced"},
       {"name": "Bond fund 1", "size": 800,  "type" : "balanced"},
       {"name": "Bond fund 2", "size": 600,  "type" : "balanced"},
       {"name": "Bond fund 3", "size": 300,  "type" : "balanced"},
       {"name": "Bond fund 4", "size": 250,  "type" : "balanced"},
       {"name": "Bond fund 1", "size": 800,  "type" : "balanced"},
       {"name": "Bond fund 2", "size": 600,  "type" : "balanced"},
       {"name": "Bond fund 3", "size": 300,  "type" : "balanced"},
       {"name": "Bond fund 4", "size": 250,  "type" : "balanced"},
     ]
   },

        ]
      }},

]


  var columns = 3;
  var spacing = 200;
  var vSpacing = 180;

  var regionG = graphGroup.selectAll('.region')
.data(data)
.enter()
.append('g')
.attr('class', 'region')
.attr('id', (d, i) => 'region' + i)
.attr('transform', (d, k) => {
  var horSpace = (k % columns) * spacing;
  var vertSpace = ~~((k / columns)) * vSpacing;
  return "translate(" + horSpace + "," + vertSpace + ")";
});


var colorMap = {
  'equity':"#003366",
  'bond':"#f6d18b",
  'balanced':"#95b3d7"
};

  //const root = d3.hierarchy(data);
  //const links = root.links();
  //const nodes = root.descendants();
  
regionG.each(simulate);
  
function simulate(d) {
    let g = d3.select(this);
    let tree = d3.hierarchy(d.tree);
    let nodes = tree.descendants();
    let links = tree.links();
    
    const simulation = d3.forceSimulation(tree.descendants())
    .force("link", d3.forceLink(links).id(d => d.id).distance(0).strength(1))
    .force("charge", d3.forceManyBody().strength(-50))
    .force("x", d3.forceX())
    .force("y", d3.forceY());

    const link = g.append("g")
    .attr("stroke", "#999")
    .attr("stroke-opacity", 0.6)
  .selectAll("line")
  .data(links)
  .join("line");

    const node = g.append("g")
    .attr("fill", "#fff")
    .attr("stroke", "#000")
    .attr("stroke-width", 1.5)
  .selectAll("circle")
  .data(nodes)
  .join("circle")
    .attr("fill", d => d.children ? null : colorMap[d.data.type])
    .attr("stroke", d => d.children ? null : "#fff")
    .attr("r", 3.5);

 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);

  node
      .attr("cx", d => d.x)
      .attr("cy", d => d.y);
    });
    
 }
circle {
  fill: black;
  stroke-width: 1px;
  stroke:black;
}
<script src="https://d3js.org/d3.v5.min.js"></script>

为了避免动画,我们可以使用 simulation.tick(n) 来指定我们希望力手动向前移动 n 个刻度。在这种情况下,我们不需要刻度函数:我们可以在绘制时定位节点:

var margins = {top:100, bottom:300, left:100, right:100};

var height = 600;
var width = 900;

var totalWidth = width+margins.left+margins.right;
var totalHeight = height+margins.top+margins.bottom;

var svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);

var graphGroup = svg.append('g')
.attr('transform', "translate("+margins.left+","+margins.top+")");

  var data =
[ {name:"Company1", tree:  {
   "name": "Product offerings",
   "children": [

    {"name": "Equity line", "size": 2200,
  "children": [
    {"name": "Equity fund 1", "size": 800,  "type" : "equity"},
    {"name": "Equity fund 2", "size": 600,  "type" : "equity"},
    {"name": "Equity fund 3", "size": 300,  "type" : "equity"},
    {"name": "Equity fund 4", "size": 250,  "type" : "equity"},
    {"name": "Equity fund 5", "size": 250,  "type" : "equity"},
    {"name": "Equity fund 6", "size": 525,  "type" : "equity"},
   ]
  },

    {"name": "Bond fund line", "size": 1400,
   "children": [
     {"name": "Bond fund 1", "size": 800,  "type" : "bond"},
     {"name": "Bond fund 2", "size": 600,  "type" : "bond"},
     {"name": "Bond fund 3", "size": 300,  "type" : "bond"},
     {"name": "Bond fund 4", "size": 250,  "type" : "bond"},
   ]
 },

 {"name": "Balanced line", "size": 1400,
"children": [
  {"name": "Bond fund 1", "size": 800,  "type" : "balanced"},
  {"name": "Bond fund 2", "size": 600,  "type" : "balanced"},
  {"name": "Bond fund 3", "size": 300,  "type" : "balanced"},
  {"name": "Bond fund 4", "size": 250,  "type" : "balanced"},
  {"name": "Bond fund 1", "size": 800,  "type" : "balanced"},
  {"name": "Bond fund 2", "size": 600,  "type" : "balanced"},
  {"name": "Bond fund 3", "size": 300,  "type" : "balanced"},
  {"name": "Bond fund 4", "size": 250,  "type" : "balanced"},
  {"name": "Bond fund 1", "size": 800,  "type" : "balanced"},
  {"name": "Bond fund 2", "size": 600,  "type" : "balanced"},
  {"name": "Bond fund 3", "size": 300,  "type" : "balanced"},
  {"name": "Bond fund 4", "size": 250,  "type" : "balanced"},
]
  },

   ]
 }},
 {name:"Company2", tree:  {
        "name": "Product offerings",
        "children": [

         {"name": "Equity line", "size": 2200,
      "children": [
         {"name": "Equity fund 1", "size": 800,  "type" : "equity"},
         {"name": "Equity fund 2", "size": 600,  "type" : "equity"},
         {"name": "Equity fund 3", "size": 300,  "type" : "equity"},
         {"name": "Equity fund 4", "size": 250,  "type" : "equity"},
        {"name": "Equity fund 5", "size": 250,  "type" : "equity"},
        {"name": "Equity fund 6", "size": 525,  "type" : "equity"},
        ]
      },

         {"name": "Bond fund line", "size": 1400,
       "children": [
          {"name": "Bond fund 1", "size": 800,  "type" : "bond"},
          {"name": "Bond fund 2", "size": 600,  "type" : "bond"},
          {"name": "Bond fund 3", "size": 300,  "type" : "bond"},
          {"name": "Bond fund 4", "size": 250,  "type" : "bond"},
        ]
      },

      {"name": "Balanced line", "size": 1400,
     "children": [
       {"name": "Bond fund 1", "size": 800,  "type" : "balanced"},
       {"name": "Bond fund 2", "size": 600,  "type" : "balanced"},
       {"name": "Bond fund 3", "size": 300,  "type" : "balanced"},
       {"name": "Bond fund 4", "size": 250,  "type" : "balanced"},
       {"name": "Bond fund 1", "size": 800,  "type" : "balanced"},
       {"name": "Bond fund 2", "size": 600,  "type" : "balanced"},
       {"name": "Bond fund 3", "size": 300,  "type" : "balanced"},
       {"name": "Bond fund 4", "size": 250,  "type" : "balanced"},
       {"name": "Bond fund 1", "size": 800,  "type" : "balanced"},
       {"name": "Bond fund 2", "size": 600,  "type" : "balanced"},
       {"name": "Bond fund 3", "size": 300,  "type" : "balanced"},
       {"name": "Bond fund 4", "size": 250,  "type" : "balanced"},
     ]
   },

        ]
      }},

]


  var columns = 3;
  var spacing = 200;
  var vSpacing = 180;

  var regionG = graphGroup.selectAll('.region')
.data(data)
.enter()
.append('g')
.attr('class', 'region')
.attr('id', (d, i) => 'region' + i)
.attr('transform', (d, k) => {
  var horSpace = (k % columns) * spacing;
  var vertSpace = ~~((k / columns)) * vSpacing;
  return "translate(" + horSpace + "," + vertSpace + ")";
});


var colorMap = {
  'equity':"#003366",
  'bond':"#f6d18b",
  'balanced':"#95b3d7"
};

  //const root = d3.hierarchy(data);
  //const links = root.links();
  //const nodes = root.descendants();
  
regionG.each(simulate);
  
function simulate(d) {
    let g = d3.select(this);
    let tree = d3.hierarchy(d.tree);
    let nodes = tree.descendants();
    let links = tree.links();
    
    const simulation = d3.forceSimulation(tree.descendants())
    .force("link", d3.forceLink(links).id(d => d.id).distance(0).strength(1))
    .force("charge", d3.forceManyBody().strength(-50))
    .force("x", d3.forceX())
    .force("y", d3.forceY())
    .tick(400)
    

    const link = g.append("g")
    .attr("stroke", "#999")
    .attr("stroke-opacity", 0.6)
  .selectAll("line")
  .data(links)
  .join("line")
      .attr("x1", d => d.source.x)
      .attr("y1", d => d.source.y)
      .attr("x2", d => d.target.x)
      .attr("y2", d => d.target.y);  

    const node = g.append("g")
    .attr("fill", "#fff")
    .attr("stroke", "#000")
    .attr("stroke-width", 1.5)
  .selectAll("circle")
  .data(nodes)
  .join("circle")
    .attr("fill", d => d.children ? null : colorMap[d.data.type])
    .attr("stroke", d => d.children ? null : "#fff")
    .attr("r", 3.5)
      .attr("cx", d => d.x)
      .attr("cy", d => d.y);    


 }
circle {
  fill: black;
  stroke-width: 1px;
  stroke:black;
}
<script src="https://d3js.org/d3.v5.min.js"></script>

(您的模拟需要 400 个滴答声来冷却,所以我已经手动将它提前了 400 个滴答声,您可以尝试使用较小的数字来获得更少计算的布局,但模拟可能没有足够的步骤来整理力量之间的良好妥协)。


推荐阅读