#javascript #svg #d3.js
Вопрос:
Я пытаюсь динамически добавлять новые узлы в график D3 с помощью nodes.push({«идентификатор»: элемент, «группа»: 1, «размер»: 30}); но когда я это делаю, возникает визуальная ошибка, из-за которой появляются дубликаты. Каждый раз, когда я обновляю (), я получаю двойную сумму того, что уже было там. У кого-нибудь есть какой-нибудь совет? Буду признателен.
var node;
var link;
var circles
var lables;
function update(){
node = svg.append("g")
.attr("class", "nodes")
.selectAll("g")
.data(nodes)
.enter().append("g")
link = svg.append("g")
.attr("class", "links")
.selectAll("line")
.data(links)
.enter().append("line")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); });
circles = node.append("circle")
.attr("r", function(d) { return (d.size / 10) 1})
.attr("fill", function(d) { return color(3); })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
.on("click", clicked);
lables = node.append("text")
.text(function(d) {
return d.id;
})
.attr('x', 6)
.attr('y', 3)
.style("font-size", "20px");
node.append("title")
.text(function(d) { return d.id; });
simulation
.nodes(nodes)
.on("tick", ticked);
simulation.force("link")
.links(links);
}
function ticked() {
link
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node
.attr("transform", function(d) {
return "translate(" d.x "," d.y ")";
})
}
Ответ №1:
Глядя только на узлы (ссылки, по сути, одна и та же проблема), каждый раз, когда вы обновляете свои данные, вы:
- Создайте нового родителя
g
(svg.append("g")
) - Выберите все дочерние
g
элементы этого нового родителяg
(.selectAll("g")
). Поскольку у этого нового родителяg
нет детей — вы только что сделали это, ничего не выбрано. - Свяжите данные с выбором (
.data(nodes)
) - Используя выбор ввода, добавьте a
g
для каждого элемента в массиве данных (поскольку в выборе нет элементов, вводится все (выбор ввода создает элемент в DOM для каждого элемента в массиве данных, для которого в выборе нет соответствующего элемента). - Добавьте кружок к каждому новому добавлению
g
. (.enter().append("g")
)
Нигде вы не выбираете уже существующие узлы — они просто отбрасываются в сторону. Они игнорируются функцией галочки, потому link
что и node
относятся к выборкам вновь созданных узлов и ссылок. Вы также не удаляете старые ссылки и узлы — поэтому они просто остаются там целую вечность или до тех пор, пока вы не закроете браузер.
Каноническое решение состоит в том, чтобы:
- Добавьте структурные элементы один раз. Я говорю «структурные» в отношении родительских
g
элементов: они не зависят от данных, они являются организационными. Они должны быть добавлены один раз за пределами функции обновления. - Используйте функцию обновления для управления (создания, обновления, удаления) элементами, зависящими от данных: самими узлами и ссылками. Все, что зависит от данных, должно быть изменено в функции обновления, и ничего больше.
Таким образом, мы могли бы добавить родительские элементы g вне функции обновления:
var nodeG = svg.append("g").attr("class", "nodes");
var linkG = svg.append("g").attr("class", "links");
Затем в функции обновления мы можем использовать эти параметры для выполнения цикла ввода/обновления/выхода. Это немного сложнее в вашем случае и во многих других, потому что у нас есть узлы, представленные a g
с дочерними элементами. Что-то вроде следующего:
function update() {
var node = nodeG.selectAll("g")
.data(nodes)
// remove excess nodes.
node.exit().remove();
// enter new nodes as required:
var nodeEnter = node.enter().append("g")
.attr(...
// append circles to new nodes:
nodeEnter.append("circle")
.attr(...
// merge update and enter.
node = nodeEnter.merge(node);
// do enter/update/exit with lines.
var link = linkG.selectAll("line")
.attr(...
link.exit().remove();
var linkEnter = link.enter().append("line")
.attr(...
link = linkEnter.merge(link);
...
Что в вашем случае может выглядеть так:
// Random data:
let graph = { nodes: [], links : [] }
function randomizeData(graph) {
// generate nodes:
let n = Math.floor(Math.random() * 10) 6;
let newNodes = [];
for(let i = 0; i < n; i ) {
if (graph.nodes[i]) newNodes.push(graph.nodes[i]);
else newNodes.push({ id: i,
color: Math.floor(Math.random()*10),
size: Math.floor(Math.random() * 10 2),
x: (Math.random() * width),
y: (Math.random() * height)
})
}
// generate links
let newLinks = [];
let m = Math.floor(Math.random() * n) 1;
for(let i = 0; i < m; i ) {
a = 0; b = 0;
while (a == b) {
a = Math.floor(Math.random() * n);
b = Math.floor(Math.random() * n);
}
newLinks.push({source: a, target: b})
if(i < newNodes.length - 2) newLinks.push({source: i, target: i 1})
}
return { nodes: newNodes, links: newLinks }
}
// On with main code:
// Set up the structure:
const svg = d3.select("svg"),
width = svg.attr("width"),
height = svg.attr("height");
const color = d3.scaleOrdinal(d3.schemeCategory10);
const simulation = d3.forceSimulation()
.force("link", d3.forceLink().id(function(d) { return d.id; }).strength(0.004))
.force("charge", d3.forceManyBody())
// to attract nodes to center, use forceX and forceY:
.force("x", d3.forceX().x(width/2).strength(0.01))
.force("y", d3.forceY().y(height/2).strength(0.01));
const nodeG = svg.append("g").attr("class","nodes")
const linkG = svg.append("g").attr("class", "links")
graph = randomizeData(graph);
update();
// Two variables to hold our links and nodes - declared outside the update function so that the tick function can access them.
var links;
var nodes;
// Update based on data:
function update() {
// Select all nodes and bind data:
nodes = nodeG.selectAll("g")
.data(graph.nodes);
// Remove excess nodes:
nodes.exit()
.transition()
.attr("opacity",0)
.remove();
// Enter new nodes:
var newnodes = nodes.enter().append("g")
.attr("opacity", 0)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
// for effect:
newnodes.transition()
.attr("opacity",1)
.attr("class", "nodes")
newnodes.append("circle")
.attr("r", function(d) { return (d.size * 2) 1})
.attr("fill", function(d) { return color(d.color); })
newnodes.append("text")
.text(function(d) { return d.id; })
.attr('x', 6)
.attr('y', 3)
.style("font-size", "20px");
newnodes.append("title")
.text(function(d) { return d.id; });
// Combine new nodes with old nodes:
nodes = newnodes.merge(nodes);
// Repeat but with links:
links = linkG.selectAll("line")
.data(graph.links)
// Remove excess links:
links.exit()
.transition()
.attr("opacity",0)
.remove();
// Add new links:
var newlinks = links.enter()
.append("line")
.attr("stroke-width", function(d) { return Math.sqrt(d.value); });
// for effect:
newlinks
.attr("opacity", 0)
.transition()
.attr("opacity",1)
// Combine new links with old:
links = newlinks.merge(links);
// Update the simualtion:
simulation
.nodes(graph.nodes) // the data array, not the selection of nodes.
.on("tick", ticked)
.force("link").links(graph.links)
simulation.alpha(1).restart();
}
function ticked() {
links // the selection of all links:
.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
nodes
.attr("transform", function(d) {
return "translate(" d.x "," d.y ")";
})
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
d3.select("button")
.on("click", function() {
graph = randomizeData(graph);
update();
})
.links line {
stroke: #999;
stroke-opacity: 0.6;
}
.nodes circle {
stroke: #fff;
stroke-width: 1.5px;
}
<button> Update</button>
<svg width="500" height="300"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
Примечание
Я немного обновил параметры силы, чтобы использовать forceX и forceY: силы, которые притягивают узлы к центру. Центрирующая сила обеспечивает только определенное значение центра тяжести, а не то, насколько близко должны быть узлы.
Альтернативные подходы:
Конечно, вы могли бы просто удалять все и добавлять каждый раз: но это ограничивает возможность перехода от одного набора данных к другому и, как правило, не является каноническим.
Если вам нужно ввести элементы только один раз (элементы не нужно добавлять или удалять во время обновлений), вы можете избежать использования полного цикла ввода/обновления/выхода и добавить один раз вне функции обновления, обновляя атрибуты узла/ссылки новыми данными при обновлении, а не используя более сложный цикл ввода/обновления/выхода во фрагменте выше.
Комментарии:
1. Извините, я действительно вышел из своей зоны комфорта с этим передним интерфейсом. Не удается прочитать свойство ‘selectAll’ неопределенного значения при выборе .selectAll(«g»). Вы имели в виду выбрать все из svg? Вот где я нахожусь до сих пор pastebin.pl/view/cac5c658 Мы очень ценим вашу помощь! Это мой последний проект :/
2. Я не вижу, откуда взялась бы эта конкретная ошибка — хотя я вижу несколько проблем в pastebin: exit должен быть exit() в строке 9. В строках 44 и 50
node
иlink
являются родительскимиg
элементами здесь, а не сами ссылки и узлы (links
иnodes
, но эти выборки здесь не ограничены — поэтому они могут быть массивом данных для узлов и ссылок, а не выбором элементов для ссылок и узлов). Тогда также похоже, что вы используете выделениеnodes
в качестве массива данныхsimulation.nodes()
, поэтому мы могли потерять ссылку на исходный массив данных.3. Я немного занят, но вернусь к этому позже вечером с рабочим примером и обновлю ответ.
4. Первоначально я загружал из json, затем вручную заполнял узлы и массив ссылок для создания графика. Спасибо, что уделили мне время. Вы можете ссылаться на все, что у меня есть здесь pastebin.pl/view/0ace449e (удаляется через 24 часа), чтобы вы могли видеть, чего я пытаюсь достичь. При вводе в поле узлы и ребра добавляются на график. Я работаю сегодня вечером, но с нетерпением жду вашего совета сегодня вечером или завтра утром. Еще раз спасибо! Это очень много значит.
5. Большое вам спасибо за помощь. Я не могу выразить словами, как много значит твоя помощь. У меня есть один последний вопрос. Метод update() отлично справляется с добавлением новых ссылок и узлов, но что, если я захочу изменить текущий узел? Например, функция щелкнула(событие, d) { если (событие.defaultPrevented) возвращает; // перетаскиваемый граф.узлы[d].размер = 10; консоль.журнал(граф.узлы[d].размер) ??? обновление не изменяет размер