将新的 json 数据合并到 d3.js 和 cola.js 中的现有图表
Merge new json data to existing graph in d3.js and cola.js
我有一个函数 buildTree,它将 json 数据作为输入,并使用 d3.js 和 cola.js 将它们可视化。我的想法是,我单击一个按钮并将 json 添加到 d3。然后通过单击另一个按钮我添加另一个 js 并保留旧的,所以我最终得到两棵树。在我的示例中,我有一个节点存在于两个 json 文件中,所以我想结束连接这两棵树并且该节点出现一次。
我设法获取现有树,添加新树,删除存在两次的节点并更新 links 但一个 link 从未连接到共存节点。
JSON 文件具有以下格式:
{
"nodes": [
{"id": "a", "name": "a", "type": "tool", "width": 60, "height": 40},
{"id": "b", "name": "b", "type": "tool", "width": 60, "height": 40},
{"id": "c", "name": "c", "type": "tool", "width": 60, "height": 40}
],
"links": [
{"source": 0, "target": 1},
{"source": 0, "target": 2}
],
"groups": []
}
第二个:
{
"nodes": [
{"id": "h", "name": "h", "type": "tool", "width": 60, "height": 40},
{"id": "i", "name": "i", "type": "tool", "width": 60, "height": 40},
{"id": "c", "name": "c", "type": "tool", "width": 60, "height": 40}
],
"links": [
{"source": 0, "target": 1},
{"source": 0, "target": 2}
],
"groups": []
}
所以 c 是一个节点,它存在于两个 JSON 文件中,应该只出现在树中一次,但同时有两个 links。
buildTree 类似于:
function buildTree(jsonSource) {
d3.json(jsonSource, function (error, graph) {
//get existing data if any and merge them with new data
myNodes = svg.selectAll(".node");
myLinks = svg.selectAll(".link");
//update the existing nodes with the new ones, remove duplications and store them in removedNodes
allNodes = graph.nodes.concat(myNodes.data());
var uniqueIds = [];
var allNodesUnique = [];
var removedNodes = [];
for (var i = 0; i < allNodes.length; i++) {
var id = allNodes[i].id;
if (uniqueIds.indexOf(id) == -1) {
uniqueIds.push(id);
allNodesUnique.push(allNodes[i]);
} else {
removedNodes.push(allNodes[i]);
}
}
allNodes = allNodesUnique;
//update links
allLinks = graph.links.concat(myLinks.data());
d3.selectAll("svg > *").remove();
cola
.nodes(allNodes)
.links(allLinks)
.groups(graph.groups)
.start();
...
我认为您的问题是您传递的链接引用了它们所在数组中节点的索引。当您将这些节点合并到 1 个数组中时,索引现在不再匹配。
您必须将旧索引从新数据映射到这些节点现在在节点数组中的位置。
我还建议您使用 Map 数据结构来删除重复项。
即你通过你的节点将它们全部放在一个 Map by ids 中。然后浏览地图并提取您现在重复的空闲列表
例如(请原谅我可能犯的任何愚蠢错误)
// map indexes of nodes to their IDs
const indexIDMap = graph.links.map(d=>d.id);
// remove duplicates
const nodeMap = new Map();
// add old nodes to map
myNodes.data().forEach(d=> nodeMap.set(d.id, d));
//note this would over write any data contained in the new node that isn't on the old node
myNodes.links.forEach(d=> nodeMap.set(d.id, d));
// Maps maintain insertion order which is important
const allNodes = [...myNodes.values()]
// links
const newIndices = indexIdMap.map(id => allNodes.findIndex(d => d.id === id))
const newLinks = graph.links.map(l => ({
source: newIndices[l.source],
target: newIndices[l.target]
}))
const allLinks = myLinks.data().concat(newLinks)
最后我通过正确更新链接解决了这个问题,javascript 代码可以在这里找到:
<script src="d3v4.js"></script>
<script src="cola.min.js"></script>
<script>
//function on button click
function myFunction() {
buildTree("Data1.json");
document.getElementById('graph').style.visibility="visible";
}
//function on button click
function myFunction2() {
buildTree("Data2.json");
document.getElementById('graph').style.visibility="visible";
}
<!-- initialize cola -->
var width = 960,
height = 500; //these are the dimensions of the graph
// map colors for nodes to their type
var color = d3.scaleOrdinal()
.domain(["workflow", "tool", "task", "exposed variable"])
.range(["#009933", "#E3a322", "#E05B2B", "#81D42F"]);
var cola = cola.d3adaptor(d3)
.linkDistance(100)
.avoidOverlaps(true)
.handleDisconnected(false)
.size([width, height]);
var svg = d3.select("#graph").append("svg")
.attr("width", width)
.attr("height", height);
<!-- end of initialize cola -->
/**
This function takes as an iput a json with nodes and links and creates a tree.
If another tree already exists it merges the json data and redraws old trees and new ones
**/
function buildTree(jsonSource){
var myNodes =[];
var myLinks=[];
var allNodes=[];
var allLinks=[];
d3.json(jsonSource, function (error, graph) {
//console.log(error);
<!-- Data Merging -->
//get existing data if any and merge them with new data
myNodes = svg.selectAll(".node").data();
myLinks = svg.selectAll(".link").data();
//update the existing NODES with the new ones, remove duplications and store them in removedNodes
allNodes = graph.nodes.concat(myNodes);
var uniqueIds=[];
var allNodesUnique=[];
var removedNodes=[];
var oldIds=[];
for (var i=0; i < allNodes.length; i++){
var currentId = allNodes[i].id;
if(uniqueIds.indexOf(currentId) == -1){
uniqueIds.push(currentId);
allNodesUnique.push(allNodes[i]);
}else{
oldIds.push(currentId);
removedNodes.push(allNodes[i]);
}
}
allNodes=allNodesUnique;
var remainedNodes=[];
for (var j=0; j < oldIds.length; j++){
for (var i=0; i < allNodes.length; i++){
if(oldIds[j]!='undefined' && oldIds[j]==allNodes[i].id){
remainedNode = allNodes[i];
remainedNodes.push(allNodes[i]);
}
}
}
//update LINKS (remove dublications)
var myCount = (myNodes.length);
if(myCount>-1){
for (var j=0; j < remainedNodes.length; j++){
for (var i=0; i < myLinks.length; i++){
if(myLinks[i].source.id == remainedNodes[j].id){
myLinks[i].source = remainedNodes[j];
}
if(myLinks[i].target.id == remainedNodes[j].id){
myLinks[i].target = remainedNodes[j];
}
myLinks[i].source.index=myLinks[i].source.index+myCount;
myLinks[i].target.index=myLinks[i].target.index+myCount;
}
}
}
allLinks = graph.links.concat(myLinks);
//update removed info
//search for the removed node
tempLinks=[];
for(var j=0; j<removedNodes.length; j++){
for (var i=0; i < allLinks.length; i++){
if(allLinks[i].source.id==removedNodes[j].id){
allLinks[i].source.index = allLinks[i].source.index - myCount
}
if(allLinks[i].target.id==removedNodes[j].id){
allLinks[i].target.index = allLinks[i].target.index - myCount
}
}
}
<!-- End of Data Merging -->
d3.selectAll("svg > *").remove();
cola
.nodes(allNodes)
.links(allLinks)
.groups(graph.groups)
.start();
var group = svg.selectAll(".group")
.data(graph.groups)
.enter().append("rect")
.attr("rx", 8).attr("ry", 8)
.attr("class", "group")
.style("fill", function (d, i) { return color(i);})
.call(cola.drag);
var link = svg.selectAll(".link")
.data(allLinks)
.enter().append("line")
.attr("class", function(d){ return ["link", d.source.name, d.target.name].join(" "); });
var pad = 3;
var node = svg.selectAll(".node")
.data(allNodes)
.enter().append("rect")
.attr("class", "node")
.attr("width", function (d) { return d.width - 2 * pad; })
.attr("height", function (d) { return d.height - 2 * pad; })
.attr("rx", 5).attr("ry", 5)
.style("fill", function(d) { return color(d.type); }) //color based on type
.call(cola.drag);
node.append("title")
.text(function (d) { return d.name; });
cola.on("tick", function () {
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("x", function (d) { return d.x - d.width / 2 + pad; })
.attr("y", function (d) { return d.y - d.height / 2 + pad; });
group.attr("x", function (d) { return d.bounds.x; })
.attr("y", function (d) { return d.bounds.y; })
.attr("width", function (d) { return d.bounds.width(); })
.attr("height", function (d) { return d.bounds.height(); });
});
});
}
</script>
我有一个函数 buildTree,它将 json 数据作为输入,并使用 d3.js 和 cola.js 将它们可视化。我的想法是,我单击一个按钮并将 json 添加到 d3。然后通过单击另一个按钮我添加另一个 js 并保留旧的,所以我最终得到两棵树。在我的示例中,我有一个节点存在于两个 json 文件中,所以我想结束连接这两棵树并且该节点出现一次。
我设法获取现有树,添加新树,删除存在两次的节点并更新 links 但一个 link 从未连接到共存节点。
JSON 文件具有以下格式:
{
"nodes": [
{"id": "a", "name": "a", "type": "tool", "width": 60, "height": 40},
{"id": "b", "name": "b", "type": "tool", "width": 60, "height": 40},
{"id": "c", "name": "c", "type": "tool", "width": 60, "height": 40}
],
"links": [
{"source": 0, "target": 1},
{"source": 0, "target": 2}
],
"groups": []
}
第二个:
{
"nodes": [
{"id": "h", "name": "h", "type": "tool", "width": 60, "height": 40},
{"id": "i", "name": "i", "type": "tool", "width": 60, "height": 40},
{"id": "c", "name": "c", "type": "tool", "width": 60, "height": 40}
],
"links": [
{"source": 0, "target": 1},
{"source": 0, "target": 2}
],
"groups": []
}
所以 c 是一个节点,它存在于两个 JSON 文件中,应该只出现在树中一次,但同时有两个 links。
buildTree 类似于:
function buildTree(jsonSource) {
d3.json(jsonSource, function (error, graph) {
//get existing data if any and merge them with new data
myNodes = svg.selectAll(".node");
myLinks = svg.selectAll(".link");
//update the existing nodes with the new ones, remove duplications and store them in removedNodes
allNodes = graph.nodes.concat(myNodes.data());
var uniqueIds = [];
var allNodesUnique = [];
var removedNodes = [];
for (var i = 0; i < allNodes.length; i++) {
var id = allNodes[i].id;
if (uniqueIds.indexOf(id) == -1) {
uniqueIds.push(id);
allNodesUnique.push(allNodes[i]);
} else {
removedNodes.push(allNodes[i]);
}
}
allNodes = allNodesUnique;
//update links
allLinks = graph.links.concat(myLinks.data());
d3.selectAll("svg > *").remove();
cola
.nodes(allNodes)
.links(allLinks)
.groups(graph.groups)
.start();
...
我认为您的问题是您传递的链接引用了它们所在数组中节点的索引。当您将这些节点合并到 1 个数组中时,索引现在不再匹配。 您必须将旧索引从新数据映射到这些节点现在在节点数组中的位置。
我还建议您使用 Map 数据结构来删除重复项。 即你通过你的节点将它们全部放在一个 Map by ids 中。然后浏览地图并提取您现在重复的空闲列表
例如(请原谅我可能犯的任何愚蠢错误)
// map indexes of nodes to their IDs
const indexIDMap = graph.links.map(d=>d.id);
// remove duplicates
const nodeMap = new Map();
// add old nodes to map
myNodes.data().forEach(d=> nodeMap.set(d.id, d));
//note this would over write any data contained in the new node that isn't on the old node
myNodes.links.forEach(d=> nodeMap.set(d.id, d));
// Maps maintain insertion order which is important
const allNodes = [...myNodes.values()]
// links
const newIndices = indexIdMap.map(id => allNodes.findIndex(d => d.id === id))
const newLinks = graph.links.map(l => ({
source: newIndices[l.source],
target: newIndices[l.target]
}))
const allLinks = myLinks.data().concat(newLinks)
最后我通过正确更新链接解决了这个问题,javascript 代码可以在这里找到:
<script src="d3v4.js"></script>
<script src="cola.min.js"></script>
<script>
//function on button click
function myFunction() {
buildTree("Data1.json");
document.getElementById('graph').style.visibility="visible";
}
//function on button click
function myFunction2() {
buildTree("Data2.json");
document.getElementById('graph').style.visibility="visible";
}
<!-- initialize cola -->
var width = 960,
height = 500; //these are the dimensions of the graph
// map colors for nodes to their type
var color = d3.scaleOrdinal()
.domain(["workflow", "tool", "task", "exposed variable"])
.range(["#009933", "#E3a322", "#E05B2B", "#81D42F"]);
var cola = cola.d3adaptor(d3)
.linkDistance(100)
.avoidOverlaps(true)
.handleDisconnected(false)
.size([width, height]);
var svg = d3.select("#graph").append("svg")
.attr("width", width)
.attr("height", height);
<!-- end of initialize cola -->
/**
This function takes as an iput a json with nodes and links and creates a tree.
If another tree already exists it merges the json data and redraws old trees and new ones
**/
function buildTree(jsonSource){
var myNodes =[];
var myLinks=[];
var allNodes=[];
var allLinks=[];
d3.json(jsonSource, function (error, graph) {
//console.log(error);
<!-- Data Merging -->
//get existing data if any and merge them with new data
myNodes = svg.selectAll(".node").data();
myLinks = svg.selectAll(".link").data();
//update the existing NODES with the new ones, remove duplications and store them in removedNodes
allNodes = graph.nodes.concat(myNodes);
var uniqueIds=[];
var allNodesUnique=[];
var removedNodes=[];
var oldIds=[];
for (var i=0; i < allNodes.length; i++){
var currentId = allNodes[i].id;
if(uniqueIds.indexOf(currentId) == -1){
uniqueIds.push(currentId);
allNodesUnique.push(allNodes[i]);
}else{
oldIds.push(currentId);
removedNodes.push(allNodes[i]);
}
}
allNodes=allNodesUnique;
var remainedNodes=[];
for (var j=0; j < oldIds.length; j++){
for (var i=0; i < allNodes.length; i++){
if(oldIds[j]!='undefined' && oldIds[j]==allNodes[i].id){
remainedNode = allNodes[i];
remainedNodes.push(allNodes[i]);
}
}
}
//update LINKS (remove dublications)
var myCount = (myNodes.length);
if(myCount>-1){
for (var j=0; j < remainedNodes.length; j++){
for (var i=0; i < myLinks.length; i++){
if(myLinks[i].source.id == remainedNodes[j].id){
myLinks[i].source = remainedNodes[j];
}
if(myLinks[i].target.id == remainedNodes[j].id){
myLinks[i].target = remainedNodes[j];
}
myLinks[i].source.index=myLinks[i].source.index+myCount;
myLinks[i].target.index=myLinks[i].target.index+myCount;
}
}
}
allLinks = graph.links.concat(myLinks);
//update removed info
//search for the removed node
tempLinks=[];
for(var j=0; j<removedNodes.length; j++){
for (var i=0; i < allLinks.length; i++){
if(allLinks[i].source.id==removedNodes[j].id){
allLinks[i].source.index = allLinks[i].source.index - myCount
}
if(allLinks[i].target.id==removedNodes[j].id){
allLinks[i].target.index = allLinks[i].target.index - myCount
}
}
}
<!-- End of Data Merging -->
d3.selectAll("svg > *").remove();
cola
.nodes(allNodes)
.links(allLinks)
.groups(graph.groups)
.start();
var group = svg.selectAll(".group")
.data(graph.groups)
.enter().append("rect")
.attr("rx", 8).attr("ry", 8)
.attr("class", "group")
.style("fill", function (d, i) { return color(i);})
.call(cola.drag);
var link = svg.selectAll(".link")
.data(allLinks)
.enter().append("line")
.attr("class", function(d){ return ["link", d.source.name, d.target.name].join(" "); });
var pad = 3;
var node = svg.selectAll(".node")
.data(allNodes)
.enter().append("rect")
.attr("class", "node")
.attr("width", function (d) { return d.width - 2 * pad; })
.attr("height", function (d) { return d.height - 2 * pad; })
.attr("rx", 5).attr("ry", 5)
.style("fill", function(d) { return color(d.type); }) //color based on type
.call(cola.drag);
node.append("title")
.text(function (d) { return d.name; });
cola.on("tick", function () {
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("x", function (d) { return d.x - d.width / 2 + pad; })
.attr("y", function (d) { return d.y - d.height / 2 + pad; });
group.attr("x", function (d) { return d.bounds.x; })
.attr("y", function (d) { return d.bounds.y; })
.attr("width", function (d) { return d.bounds.width(); })
.attr("height", function (d) { return d.bounds.height(); });
});
});
}
</script>