为了账号安全,请及时绑定邮箱和手机立即绑定

d3.js 多重关系视觉/linkHorizo​​ntal()/纠结树

d3.js 多重关系视觉/linkHorizo​​ntal()/纠结树

慕无忌1623718 2023-05-19 19:47:13
我试图模仿一个按时间段描述多种关系的视觉效果,就像这样(时间段=世代):但是,到目前为止,我的努力还没有成功。我仍然在浏览器中得到空白输出。据我所知,所有的部分都已到位。我有我的数据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
查看完整描述

1 回答

?
茅侃侃

TA贡献1842条经验 获得超21个赞

我认为你所做的很多事情,特别是围绕数据争论,都是没有必要的,特别是因为你打了电话,后来d3.hierarchy()又打了d3.cluster()电话。我已将其替换为d3.stratify(处理尚未采用正确格式的分层数据)。

我也替换d3.clusterd3.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>


查看完整回答
反对 回复 2023-05-19
?
开满天机

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>


查看完整回答
反对 回复 2023-05-19
  • 1 回答
  • 0 关注
  • 162 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信