1 回答
TA贡献1842条经验 获得超21个赞
我认为你所做的很多事情,特别是围绕数据争论,都是没有必要的,特别是因为你打了电话,后来d3.hierarchy()
又打了d3.cluster()
电话。我已将其替换为d3.stratify
(处理尚未采用正确格式的分层数据)。
我也替换d3.cluster
为d3.tree()
因为我不清楚你为什么要d3.cluster
在这里使用。您的数据有多个父节点、多个根甚至浮动节点,而 d3 并不打算处理这些问题。我的解决方法是将伪节点附加到每一层,以确保只有一个节点并且所有节点始终处于正确的层级。为了确保正确绘制链接,我编写了一个自定义getLinks
函数,可以处理多个父母。
我还编写了一个自定义链接生成器,它以您希望的方式绘制链接。d3 在这里没有提供太多的灵活性,但您可以使用源代码来获得灵感。
编辑
我改变了逻辑,更关注哪些“伙伴”有孩子,所以指向同一个孩子的两个链接都处于同一级别 - 就像你的照片一样。我还根据他们有多少合作伙伴绘制了节点,并为每个链接提供了一个偏移量,以便线条更加清晰。
我对节点进行了排序,以便真正的专业创作者位于顶部(Zeus),这样可以提供更平衡且不那么拥挤的视图。
const margins = {
top: 20,
bottom: 300,
left: 30,
right: 100,
};
const height = 900;
const width = 900;
const totalWidth = width + margins.left + margins.right;
const totalHeight = height + margins.top + margins.bottom;
const svg = d3.select('body')
.append('svg')
.attr('width', totalWidth)
.attr('height', totalHeight);
const graphGroup = svg.append('g')
.attr('transform', "translate(" + margins.left + "," + margins.top + ")");
const 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'],
},
],
];
levels.unshift([]);
// We add one pseudo node to every level to deal with parentless nodes
levels.forEach((l, i) => {
l.forEach((n, j) => {
n.level = i;
if (n.parents !== undefined) {
n.parent = n.parents[0];
} else {
n.parent = `pseudo-${i - 1}`;
}
});
l.unshift({
id: `pseudo-${i}`,
parent: i > 0 ? `pseudo-${i - 1}` : "",
level: i
});
});
const nodes = levels.flat();
const colours = d3.scaleOrdinal()
.domain(nodes.filter(n => n.parents)
.map(n => n.parents.sort()
.join("-")))
.range(d3.schemePaired);
function getLinks(nodes) {
return nodes
.filter(n => n.data.parents !== undefined)
.map(n => n.data.parents.map(p => ({
source: nodes.find(n => n.id === p),
target: n
})))
.flat();
}
const offsetPerPartner = 3;
const drawNodePath = d => {
const radius = 5;
// The number of partners determines the node height
// But when a node has only one partner,
// treat it the same as when it has zero
const nPartners = (d.data.partners && d.data.partners.length > 1)
? d.data.partners.length
: 0;
// We want to centre each node
const straightLineOffset = (nPartners * offsetPerPartner) / 2;
const context = d3.path();
context.moveTo(-radius, 0);
context.lineTo(-radius, -straightLineOffset);
context.arc(0, -straightLineOffset, radius, -Math.PI, 0);
context.lineTo(radius, straightLineOffset);
context.arc(0, straightLineOffset, radius, 0, Math.PI);
context.closePath();
return context + "";
};
const drawLinkCurve = (x0, y0, x1, y1, offset, radius) => {
const context = d3.path();
context.moveTo(x0, y0);
context.lineTo(x1 - 2 * radius - offset, y0);
// If there is not enough space to draw two corners, reduce the corner radius
if (Math.abs(y0 - y1) < 2 * radius) {
radius = Math.abs(y0 - y1) / 2;
}
if (y0 < y1) {
context.arcTo(x1 - offset - radius, y0, x1 - offset - radius, y0 + radius, radius);
context.lineTo(x1 - offset - radius, y1 - radius);
context.arcTo(x1 - offset - radius, y1, x1 - offset, y1, radius);
} else if (y0 > y1) {
context.arcTo(x1 - offset - radius, y0, x1 - offset - radius, y0 - radius, radius);
context.lineTo(x1 - offset - radius, y1 + radius);
context.arcTo(x1 - offset - radius, y1, x1 - offset, y1, radius);
}
context.lineTo(x1, y1);
return context + "";
};
const partnershipsPerLevel = {};
const getPartnershipOffset = (parent, partner) => {
let partnershipId, level;
if(partner !== undefined) {
// On every level, every relationship gets its own offset. If a relationship
// spans multiple levels, the furthest level is chosen
level = Math.max(parent.depth, partner.level);
if(!partnershipsPerLevel[level]) { partnershipsPerLevel[level] = []; }
partnershipId = [parent.id, partner.id].sort().join("-");
} else {
level = parent.depth;
if(!partnershipsPerLevel[level]) { partnershipsPerLevel[level] = []; }
partnershipId = parent.id;
}
// Assume that the partnership already has a slot assigned
const partnershipOffset = partnershipsPerLevel[level].indexOf(partnershipId);
if(partnershipOffset === -1) {
// Apparently not
return partnershipsPerLevel[level].push(partnershipId) - 1;
}
return partnershipOffset;
}
const lineRadius = 10;
const offsetStep = 5;
const linkFn = link => {
const thisParent = link.source;
const partnerId = link.target.data.parents.find(p => p !== thisParent.id);
const partners = thisParent.data.partners || [];
// Let the first link start with this negative offset
// But when a node has only one partner,
// treat it the same as when it has zero
const startOffset = (partners.length > 1)
? -(partners.length * offsetPerPartner) / 2
: 0;
const partner = partners.find(p => p.id === partnerId);
// Chaos has no partner, nor Zeus with Athena
const nthPartner = partner !== undefined
? partners.indexOf(partner)
: (partners || []).length;
const partnershipOffset = getPartnershipOffset(thisParent, partner);
return drawLinkCurve(
thisParent.y,
thisParent.x + startOffset + offsetPerPartner * nthPartner,
link.target.y,
link.target.x,
offsetStep * partnershipOffset,
lineRadius
);
};
function draw(root) {
// Now every node has had it's position set, we can draw them now
const nodes = root.descendants()
.filter(n => !n.id.startsWith("pseudo-"));
const links = getLinks(nodes)
.filter(l => !l.source.id.startsWith("pseudo-"));
const link = graphGroup.selectAll(".link")
.data(links);
link.exit().remove();
link.enter()
.append("path")
.attr("class", "link")
.merge(link)
.attr("stroke", d => colours(d.target.data.parents.sort().join("-")))
.attr("d", linkFn);
const node = graphGroup.selectAll(".node")
.data(nodes);
node.exit().remove();
const newNode = node.enter()
.append("g")
.attr("class", "node");
newNode.append("path")
.attr("d", drawNodePath);
newNode.append("text")
.attr("dy", -3)
.attr("x", 6);
newNode.merge(node)
.attr("transform", d => `translate(${d.y},${d.x})`)
.selectAll("text")
.text(d => d.id);
}
const root = d3.stratify()
.parentId(d => d.parent)
(nodes);
// Map the different sets of parents,
// assigning each parent an array of partners
getLinks(root.descendants())
.filter(l => l.target.data.parents)
.forEach(l => {
const parentNames = l.target.data.parents;
if (parentNames.length > 1) {
const parentNodes = parentNames.map(p => nodes.find(n => n.id === p));
parentNodes.forEach(p => {
if (!p.partners) {
p.partners = [];
}
parentNodes
.filter(n => n !== p && !p.partners.includes(n))
.forEach(n => {
p.partners.push(n);
});
});
}
});
// Take nodes with more partners first,
// also counting the partners of the children
root
.sum(d => (d.value || 0) + (d.partners || []).length)
.sort((a, b) => b.value - a.value);
const tree = d3.tree()
.size([height, width])
.separation((a, b) => {
// More separation between nodes with many children
const totalPartners = (a.data.partners || []).length + (b.data.partners || []).length;
return 1 + (totalPartners / 5);
});
draw(tree(root));
.link {
fill: none;
stroke-width: 2;
}
.node path {
stroke: black;
stroke-width: 3;
fill: white;
}
<script src="https://d3js.org/d3.v5.js"></script>
TA贡献1786条经验 获得超12个赞
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]))
您要实现的树的基本形状可以在下面的代码片段中看到。尝试查看此示例是否有助于根据需要设置树的样式。
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>
添加回答
举报