英文:
Defining initial coordinates for nodes with children in a D3JS animation
问题
我有SVG节点,我将其定义为包含一个<circle>
和一个<text>
的<g>
元素。
这里是一个示例HTML:
<g class="nodes">
<g>
<circle r="63"></circle>
<text class="true">Croatia</text>
</g>
(...)
</g>
我正在使用d3js(v6)对它们进行动画处理,以实现圆形包装效果,使用力来实现。一切都运行正常,我只是无法定义元素的初始坐标。元素的起始位置是右上角,我希望它们在动画开始时位于SVG区域的中心。
我尝试给我的节点<g>
元素添加"x, y"属性,以及在动画开始之前使用transform: translate
对它们进行转换,但都没有成功。
我还尝试在将它们转换为节点之前初始化每个对象的"x"和"y"值,如下所示:
arrayOfCountries.forEach((country) => {
country["x"] = 300;
country["y"] = 700;
});
var simulation = d3
.forceSimulation()
//添加节点
.nodes(arrayOfCountries);
我已经创建了一个带有以下动画:
simulation.on("tick", tickActions);
并使用以下方式进行了动画处理:
function tickActions() {
//更新g的变换:
node.attr("transform", function (d) {
return "translate(" + [d.x, d.y] + ")";
});
}
有没有办法确保节点从中心开始向外进行动画处理?
这是我的代码概要(我正在使用Vue JS 3):
renderCountries() {
// #graph3是我的主SVG
var svg = d3.select("#graph3");
//清除先前的地图(如果有的话)
svg.selectAll("*").remove();
//创建用于放置力导向图的位置
let width = +svg.attr("width");
let height = +svg.attr("height");
let arrayOfCountries = this.euCountryList.values;
arrayOfCountries.forEach((country) => {
country["x"] = 300;
country["y"] = 700;
});
var simulation = d3
.forceSimulation()
//添加节点
.nodes(arrayOfCountries);
//添加力
//我们将为每个节点添加一个电荷
//还要添加一个居中力
simulation
.force("charge_force", d3.forceManyBody().strength(-200))
.force("center_force", d3.forceCenter(width / 2, height / 2))
.force(
"x",
d3
.forceX()
.x(function (d) {
let lon = parseFloat(d[2].replace(",", ".").replace(" ", ""));
//向右移动20个单位,以避免负数
let displayLon = (lon + 20) * 22;
return displayLon;
})
.strength(5)
)
.force(
"y",
d3
.forceY()
.y(function (d) {
let lat = parseFloat(d[1].replace(",", ".").replace(" ", ""));
let displayLat = height - lat * 10;
return displayLat;
})
.strength(5)
)
.force(
"collide",
d3
.forceCollide()
.strength(0.01)
.radius(function (d) {
if (d[14]) {
let radius = parseFloat(
d[14].replace(",", ".").replace(" ", "")
);
return radius;
} else {
return 50;
}
})
.iterations(35)
); //避免圆形重叠的力;
//添加tick指令
simulation.on("tick", tickActions);
(...)
//绘制圆圈
var node = svg
.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(arrayOfCountries)
.enter()
.append("g");
var circles = node
.append("circle")
.attr("r", function (d) {
if (d[14]) {
let radius = parseFloat(d[14].replace(",", ".").replace(" ", ""));
//如果半径不是数字,则退出;可能是君主制国家的总统选举,例如西班牙。
if (isNaN(radius)) {
return 0;
}
if (radius > 35) {
return radius / 1.1;
} else {
//定义最小半径
return 35;
}
} else {
return 0;
}
})
.attr("fill", "black")
.attr("filter", "url(#blurMe)")
.attr("ref", function (d) {
return d[0];
})
.attr("id", function (d) {
return d[0] + "-euparliament";
});
// eslint-disable-next-line no-unused-vars
var labels = node
.append("text")
.text(function (d) {
if (d[14] && d[14] != "nodata" && d[14] != "—") {
return d[3];
}
})
.attr("class", function (d) {
if (d[14]) {
let abstentionism = parseFloat(
d[14].replace(",", ".").replace(" ", "")
);
if (abstentionism) {
if (abstentionism < 35) {
return "small-country";
}
}
}
return true;
})
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("pointer-events", "none")
.attr("y", 0)
.attr("fill", "red");
function tickActions() {
//更新g的变换:
node.attr("transform", function (d) {
return "translate(" + [d.x, d.y] + ")";
});
}
}
英文:
I have SVG nodes that I defined as <g>
elements that contain one <circle>
and one <text>
inside them.
Here's an examples HTML:
<g class="nodes">
<g>
<circle r="63"></circle>
<text class="true">Croatia</text>
</g>
(...)
I am animating them in d3js (v6) as to achieve a circular packing effect, using forces. All is working well, I just am not at all able to define the initial coordinates for my elements. The elements starting position is top right, and I would like them to be in the center of my SVG area when the animation starts.
I have tried giving my node <g>
elements "x, y" attributes, as well as transform: translate
them before the animation starts, to no avail.
I also tried initializing each object "x" and "y" values, before turning them into nodes, as so:
arrayOfCountries.forEach((country) => {
country["x"] = 300;
country["y"] = 700;
});
var simulation = d3
.forceSimulation()
//add nodes
.nodes(arrayOfCountries);
I have created an animations with:
simulation.on("tick", tickActions);
and animated it using:
function tickActions() {
//update g transform:
node.attr("transform", function (d) {
return "translate(" + [d.x, d.y] + ")";
});
}
Any idea how I could make sure the nodes start animating from center outwards?
Here's an outline of my code (I'm using Vue JS 3):
renderCountries() {
// #graph3 is my main SVG
var svg = d3.select("#graph3");
// Clear the previous map, if any
svg.selectAll("*").remove();
// create somewhere to put the force directed graph
let width = +svg.attr("width");
let height = +svg.attr("height");
let arrayOfCountries = this.euCountryList.values;
arrayOfCountries.forEach((country) => {
country["x"] = 300;
country["y"] = 700;
});
var simulation = d3
.forceSimulation()
//add nodes
.nodes(arrayOfCountries);
// add forces
// we're going to add a charge to each node
// also going to add a centering force
simulation
.force("charge_force", d3.forceManyBody().strength(-200))
.force("center_force", d3.forceCenter(width / 2, height / 2))
.force(
"x",
d3
.forceX()
.x(function (d) {
let lon = parseFloat(d[2].replace(",", ".").replace(" ", ""));
// shift 20 units so as to avoid negative numbers
let displayLon = (lon + 20) * 22;
return displayLon;
})
.strength(5)
)
.force(
"y",
d3
.forceY()
.y(function (d) {
let lat = parseFloat(d[1].replace(",", ".").replace(" ", ""));
let displayLat = height - lat * 10;
// console.log("LAT: " + displayLat);
return displayLat;
})
.strength(5)
)
.force(
"collide",
d3
.forceCollide()
.strength(0.01)
.radius(function (d) {
if (d[14]) {
let radius = parseFloat(
d[14].replace(",", ".").replace(" ", "")
);
return radius;
} else {
return 50;
}
})
.iterations(35)
); // Force that avoids circle overlapping;
// add tick instructions
simulation.on("tick", tickActions);
(...)
// draw circles
var node = svg
.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(arrayOfCountries)
.enter()
.append("g");
var circles = node
.append("circle")
.attr("r", function (d) {
if (d[14]) {
let radius = parseFloat(d[14].replace(",", ".").replace(" ", ""));
// Bail if radius is Not A Number; possibly a presedential election in a monarchy, eg, Spain.
if (isNaN(radius)) {
return 0;
}
if (radius > 35) {
return radius / 1.1;
} else {
// define a minimum radius
return 35;
}
// No data for abstention in country. Hide country in map.
} else {
return 0;
}
})
.attr("fill", "black")
.attr("filter", "url(#blurMe)")
.attr("ref", function (d) {
return d[0];
})
// Access country's caption using Vue template Refs
.attr("id", function (d) {
return d[0] + "-euparliament";
});
// eslint-disable-next-line no-unused-vars
var labels = node
.append("text")
.text(function (d) {
if (d[14] && d[14] != "nodata" && d[14] != "—") {
return d[3];
}
})
.attr("class", function (d) {
if (d[14]) {
let abstentionism = parseFloat(
d[14].replace(",", ".").replace(" ", "")
);
if (abstentionism) {
if (abstentionism < 35) {
return "small-country";
// return short name
}
}
}
return true;
})
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("pointer-events", "none")
// .attr("x", function (d) {
// return -(d.abstentionism / 2);
// })
.attr("y", 0)
.attr("fill", "red");
function tickActions() {
//update g transform:
node.attr("transform", function (d) {
return "translate(" + [d.x, d.y] + ")";
});
}
}
答案1
得分: 1
你说过
> 我尝试过给予"x, y"属性
但不太清楚你在哪里做了这个。通常,节点应该是一个对象列表。力模拟会重置这些对象的x/y属性,但如果愿意,你可以将它们设置为一些初始位置。
通常,我会随机设置初始位置,而不是所有节点的默认左上角位置。在SVG中以这种方式创建n
个节点,宽度为w
,高度为h
的代码可能如下所示:
let nodes = d3.range(n).map((i) => ({
id: i,
x: d3.randomUniform(0, w)(),
y: d3.randomUniform(0, h)()
}));
如果你更喜欢从中间开始节点,只需这样做
x: w/2,
y: h/2,
如果你喜欢,你可以在此Observable笔记本中查看它的实际效果。
英文:
You say that
> I have tried giving "x, y" attributes
but it's not exactly clear where you've done that.
Typically, the nodes should be a list of objects. The force simulation will reset any x/y properties of those objects, but you can set them to some initial position if we like.
Often, I set the initial positions randomly, rather than the default upper left position for all nodes. The code to create n
nodes inside an SVG with width w
and height h
in this fashion might look something like so:
let nodes = d3.range(n).map((i) => ({
id: i,
x: d3.randomUniform(0, w)(),
y: d3.randomUniform(0, h)()
}));
If you prefer to start the nodes all from the middle, you just do
x: w/2,
y: h/2,
You can see this in action in this Observable notebook, if you like.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论