В настоящее время я пытаюсь построить диаграмму в своем приложении React, используя d3 (d3 -ierarchy / d3-force), и у меня возникла проблема с пониманием того, как работает общее обновление шаблона.
I прочитал много статей об этом, и все же, я запутался с тем, как реализовать это в моем коде. Для получения дополнительной информации моя текущая версия d3 - 5.15.
Ниже кода, который я сейчас использую для рисования моего графика:
drawChart(data) {
const root = d3.hierarchy(data);
const links = root.links();
const nodes = root.descendants();
const width = this.props.width;
const height = this.props.height;
const leavesNumber = root.leaves().length;
const initScale = 1 / Math.log(root.descendants().length);
const initTransX = 1;
const initTransY = initTransX * initScale;
console.log("root =>", root);
console.log("links =>", links);
console.log("nodes =>", nodes);
console.log("leavesNumber =>", leavesNumber);
console.log("initScale =>", initScale)
console.log("ancestors =>", root.depth);
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id).distance(
d => {
if (d.source.parent === null) {
return 400
} else {
return 0
}
})
.strength(1))
.force("charge", d3.forceManyBody().strength(-50))
.force("x", d3.forceX())
.force("y", d3.forceY())
//forceCollide create a radius around the node which will reject elements entering in
.force("collide", d3.forceCollide(d => d.depth === 0 ? 400 : d.depth === 1 ? 200 : 100))
.force('center', d3.forceCenter(0, 50))
const zoom = d3.zoom()
.scaleExtent([1, 5])
.on("zoom", () => {
const currentTransform = d3.event.transform;
g.attr("transform", currentTransform.scale(initScale));
slider.property("value", currentTransform.k);
});
const svg = d3.select("#container").append("svg")
.attr("viewBox", [-width / 2, -height / 2, width, height])
.attr("width", '100%')
.attr("height", '100%')
.attr("class", "carto-svg-container")
.call(zoom)
let g = svg.append("g").attr("id", "main-g-container").attr("transform","translate("+initTransX+","+initTransY+")scale("+initScale+","+initScale+")");
function slided(d) {
zoom.scaleTo(svg, d3.select(this).property("value"));
}
const slider = d3.select('#container').append("p").append("input")
.datum({})
.attr("type", "range")
.attr("value", zoom.scaleExtent()[0])
.attr("min", zoom.scaleExtent()[0])
.attr("max", zoom.scaleExtent()[1])
.attr("step", (zoom.scaleExtent()[1] - zoom.scaleExtent()[0]) / 100)
.attr("class", "carto-slidebar")
.on("input", slided)
d3.select('#container').append("svg").attr("width", 24).attr("height", 24)
.attr("class", "carto-zoom-out-button")
.on("click", () => zoom.scaleBy(svg.transition().duration(750), 0.70))
.append("g").attr("stroke", 'none').attr("stroke-width", "1").attr("fill", "none").attr("fill-rule", "evenodd")
.append("g")
.append("polygon").attr("fill", "#9AA5B8").attr("fille-rule", "nonzero").attr("points", "19 13 5 13 5 11 19 11")
.append("polygon").attr("points", "0 0 24 0 24 24 0 24")
d3.select('#container').append("svg").attr("width", 24).attr("height", 24)
.attr("class", "carto-zoom-in-button")
.on("click", () => zoom.scaleBy(svg.transition().duration(750), 1.5))
.append("g").attr("stroke", 'none').attr("stroke-width", "1").attr("fill", "none").attr("fill-rule", "evenodd")
.append("g")
.append("polygon").attr("fill", "#647592").attr("fille-rule", "nonzero").attr("points", "19 13 13 13 13 19 11 19 11 13 5 13 5 11 11 11 11 5 13 5 13 11 19 11")
.append("polygon").attr("points", "0 0 24 0 24 24 0 24")
let location = d3.select('#container')
.append("svg")
.attr("viewBox", [0, 0, 24, 24])
.attr("focusable", "false")
.attr("aria-hidden", "true")
.attr("role", "presentation")
.attr("class", "carto-reset-zoom-button")
.on("click", () => svg.transition().duration(750).call(zoom.transform, d3.zoomIdentity.scale(1)))
location.append("path").attr("fill", "none").attr("d", "M0 0h24v24H0z")
location.append("path").attr("d", "M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm8.94 3c-.46-4.17-3.77-7.48-7.94-7.94V1h-2v2.06C6.83 3.52 3.52 6.83 3.06 11H1v2h2.06c.46 4.17 3.77 7.48 7.94 7.94V23h2v-2.06c4.17-.46 7.48-3.77 7.94-7.94H23v-2h-2.06zM12 19c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z")
const link = g.append("g")
.attr("id", "links-container")
.data(nodes)
.attr("stroke", "#999")
.selectAll("line")
.data(links)
.join("line");
link.each(function(d, i) {
d3.select(this)
.style("stroke-dasharray", d => {
if (d.target.data.children) {
return ("5, 5")
} else {
return "none"
}
})
.attr("stroke", d => {
if (d.target.data.children) {
return "#333857";
}
})
.attr("stroke-width", d => {
if (d.target.data.children) {
return 2;
} else {
return 1
}
})
})
const node = g.append("g")
.attr("id", "nodes-container")
.attr("stroke-width", 2)
.selectAll("g")
.data(nodes)
.enter()
.append("g")
.call(this.drag(simulation));
node.filter((d, i) => i === 0 || d.data.children)
.append("circle")
.attr("id", "node-circle")
.attr("r", (d, i) => i === 0 ? 114 : 90)
.attr("fill", (d, i) => i === 0 ? "#333857" : !d.data.children ? "transparent" : "#505D73")
.append("title").text(d => d.data.name)
node.filter(d => !d.data.children)
.append("rect")
.attr("fill", "#E0E3E9")
.attr("stroke", "#647592")
.attr("stroke-width", 1)
.attr("width", 141)
.attr("height", 24)
.append("title").text(d => d.data.name)
node.append("text")
.attr("dy", ".35em")
.style("text-anchor", (d, i) => i === 0 || d.data.children ? "middle" : "left")
.style("font-size", (d, i) => i === 0 ? "32px" : d.data.children ? '14px' : "12px")
.style("font-family", (d, i) => i === 0 ? "Roboto Medium" : !d.data.children ? "Roboto" : "Roboto Regular")
.style("fill", (d, i) => !(i === 0 || d.data.children) ? "#647592" : "#fff")
.text((d, i) => i === 0 ? leavesNumber + " results" : d.data.name)
simulation.on("tick", () => {
// console.log("this.props.data in tick=>", this.props.data)
let textsWidth = [];
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
d3.selectAll("#node-circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y);
const texts = d3.selectAll("text")
// + 10 because + 20 in the final width to make padding
.attr("x", d => d.data.children ? d.x : d.x + 10)
// + 12 to center the text in height, rect height is currently 24
.attr("y", d => d.data.children ? d.y : d.y + 12)
texts.each(function(d, i) {
if (!d.data.children) {
textsWidth.push({width: this.getBBox().width, name: d.data.name})
}})
texts.each(function(d, i) {
d3.select(this)
.attr("x", d => {
if (d.data.children) {
return d.x
} else {
// set text position after rect width has been set
const currentElement = textsWidth.filter(item => d.data.name === item.name)
return ((d.x) - (Math.round(currentElement[0].width) / 2))
}
})
})
d3.selectAll("rect")
.attr("x", d => d.x)
.attr("y", d => d.y)
.each(function(d, i) {
// re set rect size after txt size has been known
let currentElement = textsWidth.filter(item => d.data.name === item.name)
d3.select(this)
.attr("width", Math.round(currentElement[0].width) + 20)
.attr("x", d => (d.x) - ((Math.round(currentElement[0].width) + 20) / 2))
.attr("height", 24)
})
});
// invalidation.then(() => simulation.stop());
return svg.node();
}
А здесь функция, которая должна обновлять данные ( данные в параметре, отправленные этой функции, являются новыми данными, которые я хочу отобразить на моем графике):
updateData(data) {
const root = d3.hierarchy(data);
const links = root.links();
const nodes = root.descendants();
console.log("nodes =>", nodes)
const leavesNumber = root.leaves().length;
const initScale = 1 / Math.log(root.descendants().length);
const initTransX = 1;
const initTransY = initTransX * initScale;
const simulation = d3.forceSimulation(nodes)
.force("link", d3.forceLink(links).id(d => d.id).distance(
d => {
if (d.source.parent === null) {
return 400
} else {
return 0
}
})
.strength(1))
.force("charge", d3.forceManyBody().strength(-50))
.force("x", d3.forceX())
.force("y", d3.forceY())
//forceCollide create a radius around the node which will reject elements entering in
.force("collide", d3.forceCollide(d => d.depth === 0 ? 400 : d.depth === 1 ? 200 : 100))
.force('center', d3.forceCenter(0, 50))
console.log("root 2 =>", root)
console.log("links 2 =>", links)
// get main element
let g = d3.select("#main-g-container")
.attr("transform","translate("+initTransX+","+initTransY+")scale("+initScale+","+initScale+")")
d3.select("#main-g-container")
.join("#main-g-container")
.attr("transform","translate("+initTransX+","+initTransY+")scale("+initScale+","+initScale+")")
d3.select("#nodes-container")
.selectAll("g")
.data(nodes)
.join(
enter => enter.append("g")
.attr("id", "node-element")
.call(this.drag(simulation))
.filter((d, i) => i === 0 || d.data.children)
.append("circle"),
update => update,
exit => exit.remove(),
)
данные, которые я хочу отобразить и обновить =>
const mockData = {
"name": "Eve",
"children": [
{
"name": "Cain",
"children": [
{
"name": "Ragnar",
},
{
"name": "Freya",
}
]
},
{
"name": "Seth",
"children": [
{
"name": "Enos le castor des prairies",
},
{
"name": "Noam",
}
]
},
{
"name": "Awan",
"children": [
{
"name": "Enoch",
}
]
},
]
}
I В настоящее время я пытаюсь обновить круги внутри моего svg (как начало, перед обновлением оставшихся элементов). До сих пор я могу создавать элементы g и помещать в них окружность, когда захочу. Из-за этого я заблудился, я не понимаю, как я могу назначать позиции через .join () своим кругам, так как я не знаю, как выбрать эти круги. Должен ли я создать d3.selectAll ("# node-circle") или выбрать родителя и затем найти способ обновить его дочерние элементы?
Я видел, что выход будет содержать любой элемент, который не будет обновлен update blo c, поэтому я понимаю важность обновления в join и думаю, что получил глобальные принципы; но, к сожалению, я все еще в замешательстве.
Я немного новичок в d3, поэтому любые советы будут оценены,
Заранее спасибо,