如何在 D3 中对绑定到不同数据(与 forcesimulatoin 一起使用)的元素进行分组,以便对它们进行排序

Posted

技术标签:

【中文标题】如何在 D3 中对绑定到不同数据(与 forcesimulatoin 一起使用)的元素进行分组,以便对它们进行排序【英文标题】:How can i group elements that are bound to different data (to be used with forcesimulatoin) in D3 so that I can sort them 【发布时间】:2020-10-24 21:13:30 【问题描述】:

我尝试创建一个散点图,它使用力模拟在我的数据点周围放置标签。到目前为止,这工作正常(感谢 *** 和一些博客的良好帮助:))。这是到目前为止的样子...... Scatter-Plot so far

但是,我现在被困在尝试重新排序我的元素,以便每个数据点的圆、线和文本元素在 z 轴上彼此相邻。

我现在的意思:

<g class="circles">
   <circle dp-1></circle>
   <circle dp-2></circle>
   ...
</g>
<g class="labels">
   <text dp-1></text>
   <text dp-2></text>
   ...
</g>
<g class="links">
   <line dp-1></line>
   <line dp-2></line>
   ...
</g>

我想去……

<g id="dp-1">
   <circle dp-1></circle>
   <text dp-1></text>
   <line dp-1></line>
</g>
<g id="dp-2">
   <circle dp-2></circle>
   <text dp-2></text>
   <line dp-2></line>
</g>
<g>
...

我知道在“静态”情况下如何在不使用力模拟的情况下做到这一点。但是,我不知道如何在我的情况下执行此操作,我在标签(节点)和线(链接)上运行力模拟,而不是在圆圈上。

如何在 D3 中正确实现这一点?以下是我的代码中最重要的 sn-ps。 我卡住的要点是我为我的圈子(数据)和节点(forceData)使用了不同的数据数组。后者基本上是一个两倍于数据长度的数组(每个数据点 2 个节点)。

我也不知道怎么做

让 D3 绘制一个绑定到两个不同数据的“g”,或者 根据数据数组进行力模拟(现在只有节点数组长度的一半)

当然也欢迎解决我的问题的其他想法。 感谢您的任何想法和帮助。

/**
* Updates the chart. To be used when the data stayed the same, but is sliced differently (filter, ...)
*/
public update() 
this.svg.select('.dataPoints')
  .selectAll("circle")
  .data(this.data,
    function (d: any)  return d.category 
  )
  .join(
    function (enter) 
      // what is to be done with new items ...
      return enter
        .append("circle")
        .style("opacity", 0)
    ,
    // function (update)  return update ,
  )
  .attr("cx", d => this.xScale()(d.x))
  .attr("cy", d => this.yScale()(d.y))
  .style("fill", d => this.color(d.color))
  .style("stroke-width", this.settings.dataPoints.stroke.width)
  .style("stroke-opacity", this.settings.dataPoints.stroke.opacity)
  .style("stroke", this.settings.dataPoints.stroke.color)
  .transition()
  .duration(this.settings.dataPoints.duration)
  .style('opacity', 1)
  .attr("r", d => this.rScale()(d.r))

if (this.settings.labels.show) 
  this.svg.select(".labels")
    .call(this.labelPlacement)


private labelPlacement = (g) => 
// we need to create our node and link array. We need two nodes per datapoint. One for the point
// itself which has a fixed x and y (fx/fy) and one for the label, which will be floating ...
var forceData = 
  'nodes': [],
  'links': [],
;

var myXscale = this.xScale()
var myYscale = this.yScale()

this.data.forEach(function (d, i) 
  // doing the two nodes per datapoint ...
  forceData.nodes.push(
    id: d.category,
    label: d.label,
    fx: myXscale(d.x),
    fy: myYscale(d.y)
  );
  forceData.nodes.push(
    id: d.category,
    label: d.label,
    x: myXscale(d.x),
    y: myYscale(d.y),
    dataX: myXscale(d.x),
    dataY: myYscale(d.y)
  );
  // and also adding a link between the datapoint and its label ...
  forceData.links.push(
    source: i * 2,
    target: i * 2 + 1,
  );
);

// now drawing them labels and links ...
if (this.settings.labels.showLinks) 
  var labelLink = this.svg.select('.label-links')
    .selectAll("line")
    .data(forceData.links, (d: any) =>  return (d.source + "-" + d.target) )
    .join("line")
    .attr("stroke", this.settings.labels.linkStroke.color)
    .attr("stroke-width", this.settings.labels.linkStroke.width)
    .attr("opacity", this.settings.labels.linkStroke.opacity)


var labelNode = this.svg.select('.labels')
  .selectAll("text")
  .data(forceData.nodes, (d: any) =>  return d.id )
  .join("text")
  .text((d, i) =>  return i % 2 == 0 ? "" : TextService.textLimit(d.label, this.settings.labels.maxTextLength) )
  .style("fill", this.settings.labels.label.fill)
  .style("font-family", this.settings.labels.label.fontFamily)
  .style("font-size", this.settings.labels.label.fontSize)
  .call(d3.drag()
    .on("drag", dragged)
    .on("end", dragended)
  )

// adding and doing the force simulation ...
if (this.settings.labels.force) 
  d3.forceSimulation(forceData.nodes)
    .alphaTarget(this.settings.labels.alphaTarget)
    .alphaDecay(this.settings.labels.alphaDecay)
    .force("charge", d3.forceManyBody().strength(this.settings.labels.chargeStrength))
    .force("link", d3.forceLink(forceData.links)
      .distance(this.settings.labels.linkDistance)
      .strength(this.settings.labels.linkStrength))
    .on("tick", ticked);

【问题讨论】:

您能否发布足够的代码以便我们测试您的演示等?理想情况下,将您的问题作为可重复的最小示例 (***.com/help/minimal-reproducible-example) - 您可以在此处设置“sn-p”或添加指向codepen.io 的链接等。由于 D3 是“数据驱动文档”,我认为我们在最少需要查看您的数据(或其简化版本)。我的第一个想法是使用 map/reduce 等数组方法合并/转换数据。 嗨,亚历克斯,感谢您的回复。我担心,可能有点难以理解我的要求。幸运的是,我找到了一种通过使用稍微不同的方法来解决问题的方法。我将其作为单独的答案发布。 不用担心。如果您想看看我将如何解决它,我现在也在下面发布了我的答案。在我的回答中,我只使用.append() - 我实现它的方式是通过数据合并/分组,然后以相当常见的方式使用 d3,将我们的分组数据绑定到我们创建的每个“g”元素,然后附加到每个我们想要的形状。总有很多方法可以解决问题 :) 很高兴你找到了一个。 【参考方案1】:

由于你没有包含你的数据,我可以给你一个在数据方面解决它的高级概念方法:

本质上,使用 reduce 将 3 个数组合并为一个按“颜色”属性(可以是任何属性)分组的对象。然后将每个圆圈、线条和文本附加到我们为每种颜色创建的每个“g”元素中。

注意:links 数组没有 x1x2y1y2 值,因为我们可以从 circleslabels 数组中获取它们。此外,如果可能的话,您可以像我的combinedData 一样从一开始就定义您的数据。

const circles = [
  shape: "circle", color: "green", x: 2, y: 2, r: 0.5,
  shape: "circle", color: "blue", x: 4, y: 4, r: 1,
  shape: "circle", color: "red", x: 8, y: 8, r: 1.5,
];
const links = [
  shape: "line", color: "green", x1: 2, y1: 2, x2: 1, y2: 1,
  shape: "line", color: "blue", x1: 4, y1: 4, x2: 2, y2: 6,
  shape: "line", color: "red", x1: 8, y1: 8, x2: 9, y2: 4,
];
const labels = [
  shape: "text", color: "green", x: 1, y: 1, text: "A",
  shape: "text", color: "blue", x: 2, y: 6, text: "B",
  shape: "text", color: "red", x: 9, y: 4, text: "C",
];

const combinedData = [...circles, ...links, ...labels].reduce((aggObj, item) => 
  
  if (!aggObj[item.color]) aggObj[item.color] = ;
  
  aggObj[item.color][item.shape] = item;
  
  return aggObj;
, );

//console.log(combinedData);

const groups = d3.select('svg').selectAll('g')
  .data(Object.entries(combinedData))
  .enter()
  .append('g')
  .attr('class', ([k,v]) => k);
  
groups  
    .append('circle')
    .attr('fill', ([k,v]) => v.circle.color)
    .attr('r', ([k,v]) => v.circle.r)
    .attr('cx', ([k,v]) => v.circle.x)
    .attr('cy', ([k,v]) => v.circle.y)
    
groups  
    .append('line')
    .attr('stroke', ([k,v]) => v.line.color)
    .attr('stroke-width', 0.1)
    .attr('x1', ([k,v]) => v.line.x1)
    .attr('y1', ([k,v]) => v.line.y1) 
    .attr('x2', ([k,v]) => v.line.x2)
    .attr('y2', ([k,v]) => v.line.y2)     
    
groups  
    .append('rect')
    .attr('fill', "#cfcfcf")
    .attr('x', ([k,v]) => v.text.x - 0.6)
    .attr('y', ([k,v]) => v.text.y - 0.6)
    .attr('width', 1.1)
    .attr('height', 1.1)
    
groups  
    .append('text')
    .attr('alignment-baseline', "middle")
    .attr('text-anchor', "middle")
    .attr('fill', ([k,v]) => v.text.color)
    .attr('font-size', 1)
    .attr('x', ([k,v]) => v.text.x)
    .attr('y', ([k,v]) => v.text.y)
    .text(([k,v]) => v.text.text)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg  viewbox="0 0 12 12">

</svg>

组中的元素:

扩展:

<svg  viewBox="0 0 12 12">
  <g class="green">
    <circle fill="green" r="0.5" cx="2" cy="2"></circle>
    <line stroke="green" stroke- x1="2" y1="2" x2="1" y2="1"></line>
    <rect fill="#cfcfcf" x="0.4" y="0.4"  ></rect>
    <text alignment-baseline="middle" text-anchor="middle" fill="green" font-size="1" x="1" y="1">A</text>
  </g>
  <g class="blue">
    <circle fill="blue" r="1" cx="4" cy="4"></circle>
    <line stroke="blue" stroke- x1="4" y1="4" x2="2" y2="6"></line>
    <rect fill="#cfcfcf" x="1.4" y="5.4"  ></rect>
    <text alignment-baseline="middle" text-anchor="middle" fill="blue" font-size="1" x="2" y="6">B</text>
  </g>
  <g class="red">
    <circle fill="red" r="1.5" cx="8" cy="8"></circle>
    <line stroke="red" stroke- x1="8" y1="8" x2="9" y2="4"></line>
    <rect fill="#cfcfcf" x="8.4" y="3.4"  ></rect>
    <text alignment-baseline="middle" text-anchor="middle" fill="red" font-size="1" x="9" y="4">C</text>
  </g>
</svg>

输出(粗略示例):

【讨论】:

【参考方案2】:

感谢大家的阅读。又过了 1 个不眠之夜,我终于解决了这个问题 :)

我确实跳过了“g”元素,因为它们并不是真正需要的,试图获得这样的结构:

<line dp-1></line>
<circle dp-1></circle>
<line dp-2></line>
<circle dp-2></line>
...

这样做我可以使用两个单独的数据绑定和数据数组(这是使用力模拟所必需的)。 为了实现更正排序(线 - 圆 - 线 - 圆 - ...),我使用“d3.insert”而不是附加动态计算的“之前”元素。 以下是代码中最重要的部分。希望这最终对某人有所帮助。

问候

// Drawing the data ...
public update() 
this.dataPoints
  .selectAll("circle")
  .data(this.data,
    function (d: any)  return d.category 
  )
  .join(
    enter => enter.append("circle")
      .style('opacity', 0)
  )
  .call(this.drawData)

if (this.settings.labels.showLinks && this.settings.labels.showLinks) 
  this.dataPoints
    .selectAll("line")
    .data(this.forceData.links)
    .join(
      enter => enter.insert('line', (d, i) => 
        console.log("JOIN", d)
        return document.getElementById('dP_' + d.id)
      )
        // .style('opacity', 0)
    )
    .call(this.drawLabelLine)




/**
   * Draws the data circles ...
   * @param circle 
*/
  private drawData = (circle) => 
circle
  .attr("id", (d, i) =>  return 'dP_' + d.category )
  .attr("class", "dataPoint")
  .style("fill", d => this.color(d.color))
  .style("stroke-width", this.settings.dataPoints.stroke.width)
  .style("stroke-opacity", this.settings.dataPoints.stroke.opacity)
  .style("stroke", this.settings.dataPoints.stroke.color)
  .transition()
  .duration(this.settings.dataPoints.duration)
  .style('opacity', 1)
  .attr("r", d => this.rScale()(d.r))
  .attr("cx", d => this.xScale()(d.x))
  .attr("cy", d => this.yScale()(d.y))
  

/**
   * draws the lines to connect labels to data points
   * @param g 
   */
private drawLabelLine = (line) => 
line
  .attr("class", "label-link")
  .attr("stroke", this.settings.labels.linkStroke.color)
  .attr("stroke-width", this.settings.labels.linkStroke.width)
  .attr("opacity", this.settings.labels.linkStroke.opacity)


// adding and doing the force simulation ...
if (this.settings.labels.force) 
  d3.forceSimulation(forceData.nodes)
    .alphaTarget(this.settings.labels.alphaTarget)
    .alphaDecay(this.settings.labels.alphaDecay)
    .force("charge", d3.forceManyBody().strength(this.settings.labels.chargeStrength))
    .force("link", d3.forceLink(forceData.links)
      .distance(this.settings.labels.linkDistance)
      .strength(this.settings.labels.linkStrength))
    .on("tick", ticked);

【讨论】:

以上是关于如何在 D3 中对绑定到不同数据(与 forcesimulatoin 一起使用)的元素进行分组,以便对它们进行排序的主要内容,如果未能解决你的问题,请参考以下文章

如何在 WPF / MVVM 中对绑定到相同实例的两个列表视图进行不同选择

了解 D3.js 如何将数据绑定到节点

在 Force 有向图中将文本标签添加到 d3 节点并在悬停时调整大小

在版本 4 中将节点动态添加到 D3 Force Layout

使用新数据重新渲染 D3 强制定向布局

D3.js中Force-Directed Graph详解