以下d3.js(v4)交互式树布局我已经放在一起作为用户界面项目的概念证明,其行为不符合预期。这是我的第一个d3.js可视化,我仍然围绕着所有的概念。
该片段突出了我正在努力解决的问题。如果你点击标有“F:3”的黄色节点,我希望孩子们(“F:4”,“T:5”和它的两个链接)转换回实时x,y坐标“F” :3“。但是,它似乎只转换到“F:3”节点的初始x,y坐标。知道我错过了什么吗?
var id = 0;
var data = {
source: {
id: id++,
type: 'dataSource',
name: 'Data Source: ' + id,
silos: [
{ name: 'Silo 1', selected: true },
{ name: 'Silo 2', selected: false },
{ name: 'Silo 3', selected: false }
],
union: {
id: id++,
type: 'union',
name: 'Union:' + id,
count: null,
cardinalities: [
{
id: id++, type: 'cardinality', positive: false, name: 'F: ' + id, count: 40, cardinalities: [
{ id: id++, type: 'cardinality', positive: false, name: 'F: ' + id, count: 40, cardinalities: [] },
{ id: id++, type: 'cardinality', positive: true, name: 'T: ' + id, count: 60, cardinalities: [] }
]
},
{ id: id++, type: 'cardinality', positive: true, name: 'T: ' + id, count: 60, cardinalities: [] }
]
}
}
}
// global variables
var containerPadding = 20;
var container = d3.select('#container').style('padding', containerPadding + 'px'); // contains the structured search svg
var svg = container.select('svg'); // the canvas that displays the structured search
var group = svg.append('g'); // contains the tree elements (nodes & links)
var nodeWidth = 40, nodeHeight = 30, nodeCornerRadius = 3, verticalNodeSeparation = 150, transitionDuration = 600;
var tree = d3.tree().nodeSize([nodeWidth, nodeHeight]);
var source;
function nodeClicked(d) {
source = d;
switch (d.data.type) {
case 'dataSource':
// todo: show the data source popup and update the selected values
d.data.silos[0].selected = !d.data.silos[0].selected;
break;
default:
// todo: show the operation popup and update the selected values
if (d.data.cardinalities && d.data.cardinalities.length) {
d.data.cardinalities.splice(-2, 2);
}
else {
d.data.cardinalities.push({ id: id++, type: 'cardinality', positive: false, name: 'F: ' + id, count: 40, cardinalities: [] });
d.data.cardinalities.push({ id: id++, type: 'cardinality', positive: true, name: 'T: ' + id, count: 60, cardinalities: [] });
}
break;
}
render();
}
function renderLink(source, destination) {
var x = destination.x + nodeWidth / 2;
var y = destination.y;
var px = source.x + nodeWidth / 2;
var py = source.y + nodeHeight;
return 'M' + x + ',' + y
+ 'C' + x + ',' + (y + py) / 2
+ ' ' + x + ',' + (y + py) / 2
+ ' ' + px + ',' + py;
}
function render() {
// map the data source to a heirarchy that d3.tree requires
// d3.tree instance needs the data structured in a specific way to generate the required layout of nodes & links (lines)
var hierarchy = d3.hierarchy(data.source, function (d) {
switch (d.type) {
case 'dataSource':
return d.silos.some(function (e) { return e.selected; }) ? [d.union] : undefined;
default:
return d.cardinalities;
}
});
// set the layout parameters (all required for resizing)
var containerBoundingRect = container.node().getBoundingClientRect();
var width = containerBoundingRect.width - containerPadding * 2;
var height = verticalNodeSeparation * hierarchy.height;
svg.transition().duration(transitionDuration).attr('width', width).attr('height', height + nodeHeight);
tree.size([width - nodeWidth, height]);
// tree() assigns the (x, y) coords, depth, etc, to the nodes in the hierarchy
tree(hierarchy);
// get the descendants
var descendants = hierarchy.descendants();
// ensure source is set when rendering for the first time (hierarchy is the root, same as descendants[0])
source = source || hierarchy;
// render nodes
var nodesUpdate = group.selectAll('.node').data(descendants, keyFunction);
var nodesEnter = nodesUpdate.enter()
.append('g')
.attr('class', 'node')
.attr('transform', 'translate(' + source.x + ',' + source.y + ')')
.style('opacity', 0)
.on('click', nodeClicked);
nodesEnter.append('rect')
.attr('rx', nodeCornerRadius)
.attr('width', nodeWidth)
.attr('height', nodeHeight)
.attr('class', function (d) { return 'box ' + d.data.type; });
nodesEnter.append('text')
.attr('dx', nodeWidth / 2 + 5)
.attr('dy', function (d) { return d.parent ? -5 : nodeHeight + 15; })
.text(function (d) { return d.data.name; });
nodesUpdate
.merge(nodesEnter)
.transition().duration(transitionDuration)
.attr('transform', function (d) { return 'translate(' + d.x + ',' + d.y + ')'; })
.style('opacity', 1);
nodesUpdate.exit()
.transition().duration(transitionDuration)
.attr('transform', function (d) { return 'translate(' + source.x + ',' + source.y + ')'; })
.style('opacity', 0)
.remove();
// render links
var linksUpdate = group.selectAll('.link').data(descendants.slice(1), keyFunction);
var linksEnter = linksUpdate.enter()
.append('path')
.attr('class', 'link')
.classed('falsey', function (d) { return d.data.positive === false })
.classed('truthy', function (d) { return d.data.positive === true })
.attr('d', function (d) { var o = { x: source.x, y: source.y }; return renderLink(o, o); })
.style('opacity', 0);
linksUpdate
.merge(linksEnter)
.transition().duration(transitionDuration)
.attr('d', function (d) { return renderLink({ x: d.parent.x, y: d.parent.y }, d); })
.style('opacity', 1);
linksUpdate.exit()
.transition().duration(transitionDuration)
.attr('d', function (d) { var o = { x: source.x, y: source.y }; return renderLink(o, o); })
.style('opacity', 0)
.remove();
}
function keyFunction(d) { return d.data.id; }
window.addEventListener('resize', render); // todo: use requestAnimationFrame (RAF) for this
render();
.link {
fill:none;
stroke:#555;
stroke-opacity:0.4;
stroke-width:1.5px
}
.truthy {
stroke:green
}
.falsey {
stroke:red
}
.box {
stroke:black;
stroke-width:1;
cursor:pointer
}
.dataSource {
fill:blue
}
.union {
fill:orange
}
.cardinality {
fill:yellow
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="container" style="background-color:gray">
<svg style="background-color:#fff" width="0" height="0"></svg>
</div>