我试图模仿一个按时间段描绘多种关系的视觉效果,如下所示(时间段 = 代):
javascript - d3.js 多重关系视觉/linkHorizo​​ntal()/纠结树-LMLPHP
然而,到目前为止,我的努力还没有成功。我仍然在浏览器中得到空白输出。片段中的硬编码数据和代码:

var margins = {top:20, bottom:300, left:30, 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 levels = [
    [{id: 'Chaos'}],
    [
      {id: 'Gaea', parents: ['Chaos']},
      {id: 'Uranus'}
    ],
    [
      {id: 'Oceanus', parents: ['Gaea', 'Uranus']},
      {id: 'Thethys', parents: ['Gaea', 'Uranus']},
      {id: 'Pontus'},
      {id: 'Rhea', parents: ['Gaea', 'Uranus']},
      {id: 'Cronus', parents: ['Gaea', 'Uranus']},
      {id: 'Coeus', parents: ['Gaea', 'Uranus']},
      {id: 'Phoebe', parents: ['Gaea', 'Uranus']},
      {id: 'Crius', parents: ['Gaea', 'Uranus']},
      {id: 'Hyperion', parents: ['Gaea', 'Uranus']},
      {id: 'Iapetus', parents: ['Gaea', 'Uranus']},
      {id: 'Thea', parents: ['Gaea', 'Uranus']},
      {id: 'Themis', parents: ['Gaea', 'Uranus']},
      {id: 'Mnemosyne', parents: ['Gaea', 'Uranus']}
    ],
    [
      {id: 'Doris', parents: ['Oceanus', 'Thethys']},
      {id: 'Neures', parents: ['Pontus', 'Gaea']},
      {id: 'Dionne'},
      {id: 'Demeter', parents: ['Rhea', 'Cronus']},
      {id: 'Hades', parents: ['Rhea', 'Cronus']},
      {id: 'Hera', parents: ['Rhea', 'Cronus']},
      {id: 'Alcmene'},
      {id: 'Zeus', parents: ['Rhea', 'Cronus']},
      {id: 'Eris'},
      {id: 'Leto', parents: ['Coeus', 'Phoebe']},
      {id: 'Amphitrite'},
      {id: 'Medusa'},
      {id: 'Poseidon', parents: ['Rhea', 'Cronus']},
      {id: 'Hestia', parents: ['Rhea', 'Cronus']}
    ],
    [
      {id: 'Thetis', parents: ['Doris', 'Neures']},
      {id: 'Peleus'},
      {id: 'Anchises'},
      {id: 'Adonis'},
      {id: 'Aphrodite', parents: ['Zeus', 'Dionne']},
      {id: 'Persephone', parents: ['Zeus', 'Demeter']},
      {id: 'Ares', parents: ['Zeus', 'Hera']},
      {id: 'Hephaestus', parents: ['Zeus', 'Hera']},
      {id: 'Hebe', parents: ['Zeus', 'Hera']},
      {id: 'Hercules', parents: ['Zeus', 'Alcmene']},
      {id: 'Megara'},
      {id: 'Deianira'},
      {id: 'Eileithya', parents: ['Zeus', 'Hera']},
      {id: 'Ate', parents: ['Zeus', 'Eris']},
      {id: 'Leda'},
      {id: 'Athena', parents: ['Zeus']},
      {id: 'Apollo', parents: ['Zeus', 'Leto']},
      {id: 'Artemis', parents: ['Zeus', 'Leto']},
      {id: 'Triton', parents: ['Poseidon', 'Amphitrite']},
      {id: 'Pegasus', parents: ['Poseidon', 'Medusa']},
      {id: 'Orion', parents: ['Poseidon']},
      {id: 'Polyphemus', parents: ['Poseidon']}
    ],
    [
      {id: 'Deidamia'},
      {id: 'Achilles', parents: ['Peleus', 'Thetis']},
      {id: 'Creusa'},
      {id: 'Aeneas', parents: ['Anchises', 'Aphrodite']},
      {id: 'Lavinia'},
      {id: 'Eros', parents: ['Hephaestus', 'Aphrodite']},
      {id: 'Helen', parents: ['Leda', 'Zeus']},
      {id: 'Menelaus'},
      {id: 'Polydueces', parents: ['Leda', 'Zeus']}
    ],
    [
      {id: 'Andromache'},
      {id: 'Neoptolemus', parents: ['Deidamia', 'Achilles']},
      {id: 'Aeneas(2)', parents: ['Creusa', 'Aeneas']},
      {id: 'Pompilius', parents: ['Creusa', 'Aeneas']},
      {id: 'Iulus', parents: ['Lavinia', 'Aeneas']},
      {id: 'Hermione', parents: ['Helen', 'Menelaus']}
    ]
  ]

  // precompute level depth
  levels.forEach((l,i) => l.forEach(n => n.level = i))

  var nodes = levels.reduce( ((a,x) => a.concat(x)), [] )
  var nodes_index = {}
  nodes.forEach(d => nodes_index[d.id] = d)

  // objectification
  nodes.forEach(d => {
    d.parents = (d.parents === undefined ? [] : d.parents).map(p => nodes_index[p])
  })

  // precompute bundles
  levels.forEach((l, i) => {
    var index = {}
    l.forEach(n => {
      if(n.parents.length == 0) {
        return
      }

      var id = n.parents.map(d => d.id).sort().join('--')
      if (id in index) {
        index[id].parents = index[id].parents.concat(n.parents)
      }
      else {
        index[id] = {id: id, parents: n.parents.slice(), level: i}
      }
      n.bundle = index[id]
    })
    l.bundles = Object.keys(index).map(k => index[k])
    l.bundles.forEach((b, i) => b.i = i)
  })

  var links = []
  nodes.forEach(d => {
    d.parents.forEach(p => links.push({source: d, bundle: d.bundle, target: p}))
  })

  var bundles = levels.reduce( ((a,x) => a.concat(x.bundles)), [] )

  // reverse pointer from parent to bundles
  bundles.forEach(b => b.parents.forEach(p => {
    if(p.bundles_index === undefined) {
      p.bundles_index = {}
    }
    if(!(b.id in p.bundles_index)) {
      p.bundles_index[b.id] = []
    }
    p.bundles_index[b.id].push(b)
  }))

  nodes.forEach(n => {
    if(n.bundles_index !== undefined) {
      n.bundles = Object.keys(n.bundles_index).map(k => n.bundles_index[k])
    }
    else {
      n.bundles_index = {}
      n.bundles = []
    }
    n.bundles.forEach((b, i) => b.i = i)
  })

  links.forEach(l => {
    if(l.bundle.links === undefined) {
      l.bundle.links = []
    }
    l.bundle.links.push(l)
  })

  // layout
  const padding = 8
  const node_height = 22
  const node_width = 70
  const bundle_width = 14
  const level_y_padding = 16
  const metro_d = 4
  const c = 16
  const min_family_height = 16

  nodes.forEach(n => n.height = (Math.max(1, n.bundles.length)-1)*metro_d)

  var x_offset = padding
  var y_offset = padding
  levels.forEach(l => {
    x_offset += l.bundles.length*bundle_width
    y_offset += level_y_padding
    l.forEach((n, i) => {
      n.x = n.level*node_width + x_offset
      n.y = node_height + y_offset + n.height/2

      y_offset += node_height + n.height
    })
  })

  var i = 0
  levels.forEach(l => {
    l.bundles.forEach(b => {
      b.x = b.parents[0].x + node_width + (l.bundles.length-1-b.i)*bundle_width
      b.y = i*node_height
    })
    i += l.length
  })

  links.forEach(l => {
    l.xt = l.target.x
    l.yt = l.target.y + l.target.bundles_index[l.bundle.id].i*metro_d - l.target.bundles.length*metro_d/2 + metro_d/2
    l.xb = l.bundle.x
    l.xs = l.source.x
    l.ys = l.source.y
  })

  // compress vertical space
  var y_negative_offset = 0
  levels.forEach(l => {
    y_negative_offset += -min_family_height + d3.min(l.bundles, b => d3.min(b.links, link => (link.ys-c)-(link.yt+c))) || 0
    l.forEach(n => n.y -= y_negative_offset)
  })

  // very ugly, I know
  links.forEach(l => {
    l.yt = l.target.y + l.target.bundles_index[l.bundle.id].i*metro_d - l.target.bundles.length*metro_d/2 + metro_d/2
    l.ys = l.source.y
    l.c1 = l.source.level-l.target.level > 1 ? node_width+c : c
    l.c2 = c
  })

  const cluster = d3.cluster()
    .size([width, height]);

  const root = d3.hierarchy(links);
  cluster(root);

  var nodeG = svg.selectAll('.node')
      .data(root.links())
      .attr('class','node')
      .enter()
      .append('g');

      nodeG.append("path")
        .attr("class", "link")
        .attr("d", d3.linkHorizontal()
          .x(function(d) { return d.y; })
          .y(function(d) { return d.x; }))
        .style('stroke-width', '3px');
<script src="https://d3js.org/d3.v5.min.js"></script>

据我所知,所有的部分都已就位。我在 levels 中有我的数据,然后使用以下方法处理了必要的层次结构坐标:
  var links = []
  nodes.forEach(d => {
    d.parents.forEach(p => links.push({source: d, bundle: d.bundle, target: p}))
  })
  const cluster = d3.cluster()
    .size([width, height]);

  const root = d3.hierarchy(links);
  cluster(root);

  var nodeG = svg.selectAll('.node')
      .data(root.links())
      .attr('class','node')
      .enter()
      .append('g');
从这里开始,我将 d3.linkHorizontal() 用于链接函数:
      nodeG.append("path")
        .attr("class", "link")
        .attr("d", d3.linkHorizontal()
          .x(function(d) { return d.y; })
          .y(function(d) { return d.x; }))
        .style('stroke-width', '3px');
从概念上讲,我没有看到每个节点包含多个关系如何改变事情。而且,控制台日志中没有任何错误,我不确定如何进一步排除故障。
问题
是什么阻止了我的视觉效果(如上图所示)?如果可能的话,想要精确的复制品。
编辑
如果有帮助,这是 observable 上的视觉效果,但不能被视为独立的视觉效果。
https://observablehq.com/@nitaku/tangled-tree-visualization-ii?collection=@nitaku/tangled-trees

最佳答案

除了 svg 之外,graphGroup 元素中没有附加任何内容。显然 root.links() 返回一个空数组,并且 svg 中没有附加任何内容。这也是您没有收到任何错误的原因。
通过创建此数组并对其进行迭代,如果您还更改了您想要在树中实现的基本形状:

.attr("d", d3.linkHorizontal()
          .x(function(d) { return d.y; })
          .y(function(d) { return d.x; }))
和:
 .attr("d", d3.linkHorizontal()
          .source(d => [d.xs,d.ys] )
          .target(d => [d.xt,d.yt]))
您想要实现的树的基本形状可以在下面的代码片段中看到。尝试看看这个 example 是否可以帮助您根据需要设置树的样式。

var margins = {
  top: 20,
  bottom: 300,
  left: 30,
  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 levels = [
  [{
    id: 'Chaos'
  }],
  [{
      id: 'Gaea',
      parents: ['Chaos']
    },
    {
      id: 'Uranus'
    }
  ],
  [{
      id: 'Oceanus',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Thethys',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Pontus'
    },
    {
      id: 'Rhea',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Cronus',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Coeus',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Phoebe',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Crius',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Hyperion',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Iapetus',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Thea',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Themis',
      parents: ['Gaea', 'Uranus']
    },
    {
      id: 'Mnemosyne',
      parents: ['Gaea', 'Uranus']
    }
  ],
  [{
      id: 'Doris',
      parents: ['Oceanus', 'Thethys']
    },
    {
      id: 'Neures',
      parents: ['Pontus', 'Gaea']
    },
    {
      id: 'Dionne'
    },
    {
      id: 'Demeter',
      parents: ['Rhea', 'Cronus']
    },
    {
      id: 'Hades',
      parents: ['Rhea', 'Cronus']
    },
    {
      id: 'Hera',
      parents: ['Rhea', 'Cronus']
    },
    {
      id: 'Alcmene'
    },
    {
      id: 'Zeus',
      parents: ['Rhea', 'Cronus']
    },
    {
      id: 'Eris'
    },
    {
      id: 'Leto',
      parents: ['Coeus', 'Phoebe']
    },
    {
      id: 'Amphitrite'
    },
    {
      id: 'Medusa'
    },
    {
      id: 'Poseidon',
      parents: ['Rhea', 'Cronus']
    },
    {
      id: 'Hestia',
      parents: ['Rhea', 'Cronus']
    }
  ],
  [{
      id: 'Thetis',
      parents: ['Doris', 'Neures']
    },
    {
      id: 'Peleus'
    },
    {
      id: 'Anchises'
    },
    {
      id: 'Adonis'
    },
    {
      id: 'Aphrodite',
      parents: ['Zeus', 'Dionne']
    },
    {
      id: 'Persephone',
      parents: ['Zeus', 'Demeter']
    },
    {
      id: 'Ares',
      parents: ['Zeus', 'Hera']
    },
    {
      id: 'Hephaestus',
      parents: ['Zeus', 'Hera']
    },
    {
      id: 'Hebe',
      parents: ['Zeus', 'Hera']
    },
    {
      id: 'Hercules',
      parents: ['Zeus', 'Alcmene']
    },
    {
      id: 'Megara'
    },
    {
      id: 'Deianira'
    },
    {
      id: 'Eileithya',
      parents: ['Zeus', 'Hera']
    },
    {
      id: 'Ate',
      parents: ['Zeus', 'Eris']
    },
    {
      id: 'Leda'
    },
    {
      id: 'Athena',
      parents: ['Zeus']
    },
    {
      id: 'Apollo',
      parents: ['Zeus', 'Leto']
    },
    {
      id: 'Artemis',
      parents: ['Zeus', 'Leto']
    },
    {
      id: 'Triton',
      parents: ['Poseidon', 'Amphitrite']
    },
    {
      id: 'Pegasus',
      parents: ['Poseidon', 'Medusa']
    },
    {
      id: 'Orion',
      parents: ['Poseidon']
    },
    {
      id: 'Polyphemus',
      parents: ['Poseidon']
    }
  ],
  [{
      id: 'Deidamia'
    },
    {
      id: 'Achilles',
      parents: ['Peleus', 'Thetis']
    },
    {
      id: 'Creusa'
    },
    {
      id: 'Aeneas',
      parents: ['Anchises', 'Aphrodite']
    },
    {
      id: 'Lavinia'
    },
    {
      id: 'Eros',
      parents: ['Hephaestus', 'Aphrodite']
    },
    {
      id: 'Helen',
      parents: ['Leda', 'Zeus']
    },
    {
      id: 'Menelaus'
    },
    {
      id: 'Polydueces',
      parents: ['Leda', 'Zeus']
    }
  ],
  [{
      id: 'Andromache'
    },
    {
      id: 'Neoptolemus',
      parents: ['Deidamia', 'Achilles']
    },
    {
      id: 'Aeneas(2)',
      parents: ['Creusa', 'Aeneas']
    },
    {
      id: 'Pompilius',
      parents: ['Creusa', 'Aeneas']
    },
    {
      id: 'Iulus',
      parents: ['Lavinia', 'Aeneas']
    },
    {
      id: 'Hermione',
      parents: ['Helen', 'Menelaus']
    }
  ]
]

// precompute level depth
levels.forEach((l, i) => l.forEach(n => n.level = i));

var nodes = levels.reduce(((a, x) => a.concat(x)), []);
var nodes_index = {};
nodes.forEach(d => nodes_index[d.id] = d);

// objectification
nodes.forEach(d => {
  d.parents = (d.parents === undefined ? [] : d.parents).map(p => nodes_index[p])
})

// precompute bundles
levels.forEach((l, i) => {
  var index = {}
  l.forEach(n => {
    if (n.parents.length == 0) {
      return
    }

    var id = n.parents.map(d => d.id).sort().join('--')
    if (id in index) {
      index[id].parents = index[id].parents.concat(n.parents)
    } else {
      index[id] = {
        id: id,
        parents: n.parents.slice(),
        level: i
      }
    }
    n.bundle = index[id]
  })
  l.bundles = Object.keys(index).map(k => index[k])
  l.bundles.forEach((b, i) => b.i = i)
})

var links = []
nodes.forEach(d => {
  d.parents.forEach(p => links.push({
    source: d,
    bundle: d.bundle,
    target: p
  }))
})

var bundles = levels.reduce(((a, x) => a.concat(x.bundles)), [])

// reverse pointer from parent to bundles
bundles.forEach(b => b.parents.forEach(p => {
  if (p.bundles_index === undefined) {
    p.bundles_index = {}
  }
  if (!(b.id in p.bundles_index)) {
    p.bundles_index[b.id] = []
  }
  p.bundles_index[b.id].push(b)
}))

nodes.forEach(n => {
  if (n.bundles_index !== undefined) {
    n.bundles = Object.keys(n.bundles_index).map(k => n.bundles_index[k])
  } else {
    n.bundles_index = {}
    n.bundles = []
  }
  n.bundles.forEach((b, i) => b.i = i)
})

links.forEach(l => {
  if (l.bundle.links === undefined) {
    l.bundle.links = []
  }
  l.bundle.links.push(l)
})

// layout
const padding = 8
const node_height = 22
const node_width = 70
const bundle_width = 14
const level_y_padding = 16
const metro_d = 4
const c = 16
const min_family_height = 16

nodes.forEach(n => n.height = (Math.max(1, n.bundles.length) - 1) * metro_d)

var x_offset = padding
var y_offset = padding
levels.forEach(l => {
  x_offset += l.bundles.length * bundle_width
  y_offset += level_y_padding
  l.forEach((n, i) => {
    n.x = n.level * node_width + x_offset
    n.y = node_height + y_offset + n.height / 2

    y_offset += node_height + n.height
  })
})

var i = 0
levels.forEach(l => {
  l.bundles.forEach(b => {
    b.x = b.parents[0].x + node_width + (l.bundles.length - 1 - b.i) * bundle_width
    b.y = i * node_height
  })
  i += l.length
})

links.forEach(l => {
  l.xt = l.target.x
  l.yt = l.target.y + l.target.bundles_index[l.bundle.id].i * metro_d - l.target.bundles.length * metro_d / 2 + metro_d / 2
  l.xb = l.bundle.x
  l.xs = l.source.x
  l.ys = l.source.y
})

// compress vertical space
var y_negative_offset = 0
levels.forEach(l => {
  y_negative_offset += -min_family_height + d3.min(l.bundles, b => d3.min(b.links, link => (link.ys - c) - (link.yt + c))) || 0
  l.forEach(n => n.y -= y_negative_offset)
})

// very ugly, I know
links.forEach(l => {
  l.yt = l.target.y + l.target.bundles_index[l.bundle.id].i * metro_d - l.target.bundles.length * metro_d / 2 + metro_d / 2
  l.ys = l.source.y
  l.c1 = l.source.level - l.target.level > 1 ? node_width + c : c
  l.c2 = c
})

const cluster = d3.cluster()
  .size([width, height]);

const root = d3.hierarchy(links);
cluster(root);
let oValues = Object.values(root)[0];
let linkks = oValues.map(x => x.bundle.links);

linkks.forEach((linkk) => {
 let nodeG1 = svg.append("g")
    .selectAll("circle")
    .data(linkk)
    .join("circle")
    .attr("cx", d => d.target.x)
    .attr("cy", d => d.target.y)
    .attr("fill", "none")
    .attr("stroke", (d) => {
      return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.target.level) + 1)))).toString(16);
    })
    .attr("r", 6);
  let nodeG11 = svg.append("g")
    .selectAll("circle")
    .data(linkk)
    .join("circle")
    .attr("cx", d => d.source.x)
    .attr("cy", d => d.source.y)
    .attr("fill", "none")
    .attr("stroke", (d) => {
      return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.source.level) + 1)))).toString(16);
    })
    .attr("r", 6);


  let nodeG2 = svg.append("g")
    .attr("font-family", "sans-serif")
    .attr("font-size", 14)
    .selectAll("text")
    .data(linkk)
    .join("text")
    .attr("class", "text")
    .attr("x", d => d.target.x + padding)
    .attr("y", d => d.target.y)
    .text(d => d.target.id )
    .attr("fill", (d) => {
      return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.target.level) + 2)))).toString(16);
    });

 let nodeG22 = svg.append("g")
    .attr("font-family", "sans-serif")
    .attr("font-size", 14)
    .selectAll("text")
    .data(linkk)
    .join("text")
    .attr("class", "text")
    .attr("x", d => d.source.x + padding)
    .attr("y", d => d.source.y)
    .text(d => d.source.id )
    .attr("fill", (d) => {
      return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (5 * (parseInt(d.source.level) + 1)))).toString(16);
    });

  let nodeG = svg.append('g')
    .attr('class', 'node')
    .selectAll("path")
    .data(linkk)
    .join('path')
    .attr("class", "link")
    .attr("d", d3.linkHorizontal()
      .source(d => [d.xs, d.ys])
      .target(d => [d.xt, d.yt]))
    .attr("fill", "none")
    .attr("stroke-opacity", 0.325)
    .attr("stroke-width", 0.75)
    .attr("stroke", (d) => {
      return '#' + Math.floor(16777215 * Math.sin(3 * Math.PI / (4 * parseInt(d.source.level)))).toString(16);
    });
});
path {
  display: block;
  z-index: 0;
}

text,
circle {
  display: block;
  z-index: 1000;
}
<script src="https://d3js.org/d3.v5.min.js"></script>

关于javascript - d3.js 多重关系视觉/linkHorizo​​ntal()/纠结树,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/64423115/

10-09 16:18