如何在 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
数组没有 x1
和 x2
和 y1
和 y2
值,因为我们可以从 circles
和 labels
数组中获取它们。此外,如果可能的话,您可以像我的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 中对绑定到相同实例的两个列表视图进行不同选择
在 Force 有向图中将文本标签添加到 d3 节点并在悬停时调整大小