我试图模仿一个按时间段描绘多种关系的视觉效果,如下所示(时间段 = 代):
然而,到目前为止,我的努力还没有成功。我仍然在浏览器中得到空白输出。片段中的硬编码数据和代码:
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 多重关系视觉/linkHorizontal()/纠结树,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/64423115/