茅侃侃
我认为你所做的很多事情,特别是围绕数据争论,都是没有必要的,特别是因为你打了电话,后来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 nodeslevels.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 partnersgetLinks(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 childrenroot .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>
开满天机
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 depthlevels.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);// objectificationnodes.forEach(d => { d.parents = (d.parents === undefined ? [] : d.parents).map(p => nodes_index[p])})// precompute bundleslevels.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 bundlesbundles.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)})// layoutconst padding = 8const node_height = 22const node_width = 70const bundle_width = 14const level_y_padding = 16const metro_d = 4const c = 16const min_family_height = 16nodes.forEach(n => n.height = (Math.max(1, n.bundles.length) - 1) * metro_d)var x_offset = paddingvar y_offset = paddinglevels.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 = 0levels.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 spacevar y_negative_offset = 0levels.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 knowlinks.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>