为什么重置过滤器在 d3 js 强制定向图中不起作用?
Why reset filter is not working in d3 js force directed graph?
我正在尝试重新绘制力导向图,并在用户单击 "Reset Filter" 按钮时将其恢复到原始状态。
但它没有按预期工作。请参考下面的jsfiddle。
JSFiddle Link : Working Fiddle
var filter = document.querySelector('#filter');
filter.addEventListener('change', function(event) {
d3.select("svg").remove();
svg = d3.select("body").append("svg").attr("width","960").attr("height", "600");
filterData(event.target.value);
})
var resetFilter = document.querySelector('#reset');
resetFilter.addEventListener('click', function(event) {
d3.select("svg").remove();
svg = d3.select("body").append("svg").attr("width","960").attr("height", "600");
graph = Object.assign({}, store);
drawGraph(graph);
})
function filterData(id) {
g.html('');
graph = Object.assign({}, store);
graph.nodes = [];
graph.links = [];
dummyStore = [id];
store.links.forEach(function(link) {
if(link.source.id === id) {
graph.links.push(link);
dummyStore.push(link.target.id);
} else if (link.target.id === id) {
graph.links.push(link);
dummyStore.push(link.source.id)
}
});
store.nodes.forEach(function(node) {
if (dummyStore.includes(node.id)) {
graph.nodes.push(node);
}
})
drawGraph();
}
有人可以告诉我这里缺少什么吗?
目前你每次都在重新创建一个模拟,你也每次都在重新创建可视化:而不是做一个节点来来去去的 enter/update/exit 循环,你擦干净整个石板,从SVG。
现在,我们可以在过滤发生时添加一个enter/update/exit循环,但如果我们只需要隐藏已过滤的links和节点,我们可以只隐藏它们而不是删除它们他们。我在评论中澄清了这种方法可能令人满意,因为它使任务变得容易得多。
我们可以将不透明度设置为0
,将指针事件设置为none
,以过滤掉节点和link,并将这些值重置为1
和all
用于需要显示的 link 和节点。
尽可能多地使用您的代码,我们可以得到类似的东西:
// Re-apply the filter each time the input changes:
d3.select("input").on("keyup", function() {
// We want to know if we have a value of ""
var value = this.value.length ? this.value : undefined;
nodeElements.each(function(d) {
d.show = false; // by default don't show if a filter is applied.
})
// Go through each datum (d.target and d.source) and
// set a flag to show a node if it is connected (or is) included in the filter
// also show the link or hide it as needed:
linkElements.each(function(d) {
if(value && d.source.id.indexOf(value) == -1 && d.target.id.indexOf(value) == -1) {
d3.select(this).attr("opacity",0);
}
else {
d.source.show = d.target.show = true;
d3.select(this).attr("opacity",1);
}
})
// Now just hide/show text and circles as appropriate.
nodeElements.attr("opacity",function(d) { return d.show ? 1 : 0 });
textElements.attr("opacity",function(d) { return d.show ? 1 : 0 });
})
为了简洁起见,我没有在这里设置指针事件,使用 class 同时设置不透明度和指针事件会更简单。过滤器也是大写敏感的。
由于每对节点和文本的数据相同(并在 link 数据中引用),我们不需要分别更新每个数据。
隐藏节点继续受到作用于它们的力:它们继续在每个刻度中定位在背景中。如果我们删除了 SVG 元素,但没有重新定义模拟,模拟仍然会在每个刻度中计算它们的位置。如果我们两者都不想要,那么我们需要一种完全不同的方法。
这是一个片段形式的小例子:
var graph = {
'nodes':[
{'id':'Menu','group':0},
{'id':'Item1','group':1},
{'id':'Item2','group':1},
{'id':'Item3','group':1},
{'id':'Item4','group':1},
{'id':'Item5','group':1},
{'id':'SubItem1_Item1','group':2},
{'id':'SubItem2_Item1','group':2}],
'links':[
{'source':'Menu','target':'Item1','value':1,'type':'A'},
{'source':'Menu','target':'Item2','value':8,'type':'A'},
{'source':'Menu','target':'Item3','value':10,'type':'A'},
{'source':'Menu','target':'Item3','value':1,'type':'A'},
{'source':'Menu','target':'Item4','value':1,'type':'A'},
{'source':'Menu','target':'Item5','value':1,'type':'A'},
/* Item1 is linked to SubItems */
{'source':'Item1','target':'SubItem1_Item1','value':2,'type':'A'},
{'source':'Item1','target':'SubItem2_Item1','value':1,'type':'A'},
/* Interconnected Items */
{'source':'Item5','target':'Item4','value':2,'type':'A'},
{'source':'Item2','target':'Item3','value':1,'type':'A'},
]};
var width = 500;
var height= 300;
var color = d3.scaleOrdinal(d3.schemeCategory10);
var svg = d3.select("body").append("svg")
.attr("width",width)
.attr("height",height);
var grads = svg.append("defs").selectAll("radialGradient")
.data(graph.nodes)
.enter()
.append("radialGradient")
.attr("gradientUnits", "objectBoundingBox")
.attr("cx", 0)
.attr("fill", function(d) { return color(d.id); })
.attr("cy", 0)
.attr("r", "100%")
.attr("id", function(d, i) { return "grad" + i; });
grads.append("stop")
.attr("offset", "0%")
.style("stop-color", "white");
grads.append("stop")
.attr("offset", "100%")
.style("stop-color", function(d) { return color(d.id); });
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().distance(200).id(function(d) {
return d.id;
}))
.force("charge", d3.forceManyBody().strength(-1000))
.force("center", d3.forceCenter(width / 2, height / 2));
var g = svg.append("g")
.attr("class", "everything");
var linkElements = g.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line")
.style("stroke-width",5.5)
.style("stroke", "grey")
var nodeElements = g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("r", 60)
.attr("stroke", "#fff")
.attr('stroke-width', 21)
.attr("id", function(d) { return d.id })
.attr('fill', function(d, i) { return 'url(#grad' + i + ')'; })
.on('contextmenu', function(d){
d3.event.preventDefault();
menu(d3.mouse(svg.node())[0], d3.mouse(svg.node())[1]);
})
.on('mouseover', selectNode)
.on('mouseout', releaseNode)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
var textElements = g.append("g") // use g.append instead of svg.append to enable zoom
.attr("class", "texts")
.selectAll("text")
.data(graph.nodes)
.enter().append("text")
.attr("text-anchor", "end")
.text(function(node) {
return node.id
})
.attr("font-size", 55)
.attr("font-family", "sans-serif")
.attr("fill", "black")
.attr("style", "font-weight:bold;")
.attr("dx", 30)
.attr("dy", 80)
function ticked() {
linkElements
.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; });
nodeElements
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.each(d => { d3.select('#t_' + d.id).attr('x', d.x + 10).attr('y', d.y + 3); });
textElements
.attr('x', function(d) {
return d.x
})
.attr('y', function(d) {
return d.y
});
}
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
function zoom_actions() {
g.attr("transform", d3.event.transform)
}
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;
}
function selectNode(selectedNode) {
var neighbors = getNeighbors(selectedNode)
nodeElements
.attr('fill', function(node) {
return getNodeColor(node,neighbors,selectedNode);
})
.transition().duration(500)
.attr('r', function(node) {
return getNodeRadius(node,neighbors);
});
textElements.transition().duration(500).style('font-size', function(node) {
return getTextColor(node, neighbors)
})
}
function releaseNode() {
nodeElements.transition().duration(500)
.attr('r', 60);
nodeElements.attr('fill', function(d, i) { return 'url(#grad' + i + ')'; })
linkElements.style('stroke', 'grey');
}
function getNeighbors(node) {
return graph.links.reduce(function(neighbors, link) {
if (link.target.id === node.id) {
neighbors.push(link.source.id)
} else if (link.source.id === node.id) {
neighbors.push(link.target.id)
}
return neighbors
}, [node.id])
}
function getNodeColor(node, neighbors, selectedNode) {
// If is neighbor
if (Array.isArray(neighbors) && neighbors.indexOf(node.id) > -1) {
return 'url(#grad' + selectedNode.index + ')'
// return node.level === 1 ? '#9C4A9C' : 'rgba(251, 130, 30, 1)'
} else {
return 'url(#grad' + node.index + ')'
}
//return node.level === 0 ? '#91007B' : '#D8ABD8'
}
function getNodeRadius(node, neighbors) {
// If is neighbor
if ( neighbors.indexOf(node.id) > -1) {
return '100'
}
else {
return '60'
}
}
function getTextColor(node, neighbors) {
return Array.isArray(neighbors) && neighbors.indexOf(node.id) > -1 ? '40px' : '25px'
}
d3.select("input").on("keyup", function() {
var value = this.value.length ? this.value : undefined;
nodeElements.each(function(d) {
d.show = false; // by default don't show if a filter is applied.
})
linkElements.each(function(d) {
if(value && d.source.id.indexOf(value) == -1 && d.target.id.indexOf(value) == -1) {
d3.select(this).attr("opacity",0);
}
else {
d.source.show = d.target.show = true;
d3.select(this).attr("opacity",1);
}
})
nodeElements.attr("opacity",function(d) { return d.show ? 1 : 0 });
textElements.attr("opacity",function(d) { return d.show ? 1 : 0 });
})
d3.select("button").on("click", function() {
d3.select("input").property("value","");
g.selectAll("*").attr("opacity",1);
})
<script src="https://d3js.org/d3.v4.min.js"></script>
Filter: <input type="text" name="filter" id="filter"/>
<button id = 'reset'>Reset Filter</button><br />
这里是 fiddle。
我正在尝试重新绘制力导向图,并在用户单击 "Reset Filter" 按钮时将其恢复到原始状态。
但它没有按预期工作。请参考下面的jsfiddle。
JSFiddle Link : Working Fiddle
var filter = document.querySelector('#filter');
filter.addEventListener('change', function(event) {
d3.select("svg").remove();
svg = d3.select("body").append("svg").attr("width","960").attr("height", "600");
filterData(event.target.value);
})
var resetFilter = document.querySelector('#reset');
resetFilter.addEventListener('click', function(event) {
d3.select("svg").remove();
svg = d3.select("body").append("svg").attr("width","960").attr("height", "600");
graph = Object.assign({}, store);
drawGraph(graph);
})
function filterData(id) {
g.html('');
graph = Object.assign({}, store);
graph.nodes = [];
graph.links = [];
dummyStore = [id];
store.links.forEach(function(link) {
if(link.source.id === id) {
graph.links.push(link);
dummyStore.push(link.target.id);
} else if (link.target.id === id) {
graph.links.push(link);
dummyStore.push(link.source.id)
}
});
store.nodes.forEach(function(node) {
if (dummyStore.includes(node.id)) {
graph.nodes.push(node);
}
})
drawGraph();
}
有人可以告诉我这里缺少什么吗?
目前你每次都在重新创建一个模拟,你也每次都在重新创建可视化:而不是做一个节点来来去去的 enter/update/exit 循环,你擦干净整个石板,从SVG。
现在,我们可以在过滤发生时添加一个enter/update/exit循环,但如果我们只需要隐藏已过滤的links和节点,我们可以只隐藏它们而不是删除它们他们。我在评论中澄清了这种方法可能令人满意,因为它使任务变得容易得多。
我们可以将不透明度设置为0
,将指针事件设置为none
,以过滤掉节点和link,并将这些值重置为1
和all
用于需要显示的 link 和节点。
尽可能多地使用您的代码,我们可以得到类似的东西:
// Re-apply the filter each time the input changes:
d3.select("input").on("keyup", function() {
// We want to know if we have a value of ""
var value = this.value.length ? this.value : undefined;
nodeElements.each(function(d) {
d.show = false; // by default don't show if a filter is applied.
})
// Go through each datum (d.target and d.source) and
// set a flag to show a node if it is connected (or is) included in the filter
// also show the link or hide it as needed:
linkElements.each(function(d) {
if(value && d.source.id.indexOf(value) == -1 && d.target.id.indexOf(value) == -1) {
d3.select(this).attr("opacity",0);
}
else {
d.source.show = d.target.show = true;
d3.select(this).attr("opacity",1);
}
})
// Now just hide/show text and circles as appropriate.
nodeElements.attr("opacity",function(d) { return d.show ? 1 : 0 });
textElements.attr("opacity",function(d) { return d.show ? 1 : 0 });
})
为了简洁起见,我没有在这里设置指针事件,使用 class 同时设置不透明度和指针事件会更简单。过滤器也是大写敏感的。
由于每对节点和文本的数据相同(并在 link 数据中引用),我们不需要分别更新每个数据。
隐藏节点继续受到作用于它们的力:它们继续在每个刻度中定位在背景中。如果我们删除了 SVG 元素,但没有重新定义模拟,模拟仍然会在每个刻度中计算它们的位置。如果我们两者都不想要,那么我们需要一种完全不同的方法。
这是一个片段形式的小例子:
var graph = {
'nodes':[
{'id':'Menu','group':0},
{'id':'Item1','group':1},
{'id':'Item2','group':1},
{'id':'Item3','group':1},
{'id':'Item4','group':1},
{'id':'Item5','group':1},
{'id':'SubItem1_Item1','group':2},
{'id':'SubItem2_Item1','group':2}],
'links':[
{'source':'Menu','target':'Item1','value':1,'type':'A'},
{'source':'Menu','target':'Item2','value':8,'type':'A'},
{'source':'Menu','target':'Item3','value':10,'type':'A'},
{'source':'Menu','target':'Item3','value':1,'type':'A'},
{'source':'Menu','target':'Item4','value':1,'type':'A'},
{'source':'Menu','target':'Item5','value':1,'type':'A'},
/* Item1 is linked to SubItems */
{'source':'Item1','target':'SubItem1_Item1','value':2,'type':'A'},
{'source':'Item1','target':'SubItem2_Item1','value':1,'type':'A'},
/* Interconnected Items */
{'source':'Item5','target':'Item4','value':2,'type':'A'},
{'source':'Item2','target':'Item3','value':1,'type':'A'},
]};
var width = 500;
var height= 300;
var color = d3.scaleOrdinal(d3.schemeCategory10);
var svg = d3.select("body").append("svg")
.attr("width",width)
.attr("height",height);
var grads = svg.append("defs").selectAll("radialGradient")
.data(graph.nodes)
.enter()
.append("radialGradient")
.attr("gradientUnits", "objectBoundingBox")
.attr("cx", 0)
.attr("fill", function(d) { return color(d.id); })
.attr("cy", 0)
.attr("r", "100%")
.attr("id", function(d, i) { return "grad" + i; });
grads.append("stop")
.attr("offset", "0%")
.style("stop-color", "white");
grads.append("stop")
.attr("offset", "100%")
.style("stop-color", function(d) { return color(d.id); });
var simulation = d3.forceSimulation()
.force("link", d3.forceLink().distance(200).id(function(d) {
return d.id;
}))
.force("charge", d3.forceManyBody().strength(-1000))
.force("center", d3.forceCenter(width / 2, height / 2));
var g = svg.append("g")
.attr("class", "everything");
var linkElements = g.append("g")
.attr("class", "links")
.selectAll("line")
.data(graph.links)
.enter().append("line")
.style("stroke-width",5.5)
.style("stroke", "grey")
var nodeElements = g.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("r", 60)
.attr("stroke", "#fff")
.attr('stroke-width', 21)
.attr("id", function(d) { return d.id })
.attr('fill', function(d, i) { return 'url(#grad' + i + ')'; })
.on('contextmenu', function(d){
d3.event.preventDefault();
menu(d3.mouse(svg.node())[0], d3.mouse(svg.node())[1]);
})
.on('mouseover', selectNode)
.on('mouseout', releaseNode)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
var textElements = g.append("g") // use g.append instead of svg.append to enable zoom
.attr("class", "texts")
.selectAll("text")
.data(graph.nodes)
.enter().append("text")
.attr("text-anchor", "end")
.text(function(node) {
return node.id
})
.attr("font-size", 55)
.attr("font-family", "sans-serif")
.attr("fill", "black")
.attr("style", "font-weight:bold;")
.attr("dx", 30)
.attr("dy", 80)
function ticked() {
linkElements
.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; });
nodeElements
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.each(d => { d3.select('#t_' + d.id).attr('x', d.x + 10).attr('y', d.y + 3); });
textElements
.attr('x', function(d) {
return d.x
})
.attr('y', function(d) {
return d.y
});
}
simulation
.nodes(graph.nodes)
.on("tick", ticked);
simulation.force("link")
.links(graph.links);
function zoom_actions() {
g.attr("transform", d3.event.transform)
}
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;
}
function selectNode(selectedNode) {
var neighbors = getNeighbors(selectedNode)
nodeElements
.attr('fill', function(node) {
return getNodeColor(node,neighbors,selectedNode);
})
.transition().duration(500)
.attr('r', function(node) {
return getNodeRadius(node,neighbors);
});
textElements.transition().duration(500).style('font-size', function(node) {
return getTextColor(node, neighbors)
})
}
function releaseNode() {
nodeElements.transition().duration(500)
.attr('r', 60);
nodeElements.attr('fill', function(d, i) { return 'url(#grad' + i + ')'; })
linkElements.style('stroke', 'grey');
}
function getNeighbors(node) {
return graph.links.reduce(function(neighbors, link) {
if (link.target.id === node.id) {
neighbors.push(link.source.id)
} else if (link.source.id === node.id) {
neighbors.push(link.target.id)
}
return neighbors
}, [node.id])
}
function getNodeColor(node, neighbors, selectedNode) {
// If is neighbor
if (Array.isArray(neighbors) && neighbors.indexOf(node.id) > -1) {
return 'url(#grad' + selectedNode.index + ')'
// return node.level === 1 ? '#9C4A9C' : 'rgba(251, 130, 30, 1)'
} else {
return 'url(#grad' + node.index + ')'
}
//return node.level === 0 ? '#91007B' : '#D8ABD8'
}
function getNodeRadius(node, neighbors) {
// If is neighbor
if ( neighbors.indexOf(node.id) > -1) {
return '100'
}
else {
return '60'
}
}
function getTextColor(node, neighbors) {
return Array.isArray(neighbors) && neighbors.indexOf(node.id) > -1 ? '40px' : '25px'
}
d3.select("input").on("keyup", function() {
var value = this.value.length ? this.value : undefined;
nodeElements.each(function(d) {
d.show = false; // by default don't show if a filter is applied.
})
linkElements.each(function(d) {
if(value && d.source.id.indexOf(value) == -1 && d.target.id.indexOf(value) == -1) {
d3.select(this).attr("opacity",0);
}
else {
d.source.show = d.target.show = true;
d3.select(this).attr("opacity",1);
}
})
nodeElements.attr("opacity",function(d) { return d.show ? 1 : 0 });
textElements.attr("opacity",function(d) { return d.show ? 1 : 0 });
})
d3.select("button").on("click", function() {
d3.select("input").property("value","");
g.selectAll("*").attr("opacity",1);
})
<script src="https://d3js.org/d3.v4.min.js"></script>
Filter: <input type="text" name="filter" id="filter"/>
<button id = 'reset'>Reset Filter</button><br />
这里是 fiddle。