意外的d3 v4树行为

时间:2016-12-02 10:50:51

标签: d3.js

以下d3.js(v4)交互式树布局我作为用户界面项目的概念证明放在一起并不符合预期。这是我的第一个d3.js可视化,我仍然围绕着所有的概念。

基本上,单击任何黄色节点应生成两个黄色子节点(& links)。按照从左到右,从上到下点击序列的方式工作正常,否则会显示意外行为。

通过示例运行您可能最容易,所以这是一个片段:



var data = {
    source: {
        type: 'dataSource',
        name: 'Data Source',
        silos: [
            { name: 'Silo 1', selected: true },
            { name: 'Silo 2', selected: false },
            { name: 'Silo 3', selected: false }
        ],
        union: {
            type: 'union',
            name: 'Union',
            count: null,
            cardinalities: [
                { type: 'cardinality', positive: false, name: 'Falsey', count: 40, cardinalities: [] },
                { type: 'cardinality', positive: true, name: 'Truthy', 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({ type: 'cardinality', positive: false, name: 'F ' + (new Date()).getSeconds(), count: 40, cardinalities: [] });
                d.data.cardinalities.push({ type: 'cardinality', positive: true, name: 'T ' + (new Date()).getSeconds(), 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();

    // store previous position for transitioning
    descendants.forEach(function (d) {
        d.x0 = d.x;
        d.y0 = d.y;
    });

    // ensure source is set when rendering for the first time (hierarch is the root, same as descendants[0])
    source = source || hierarchy;

    // render nodes
    var nodesUpdate = group.selectAll('.node').data(descendants);

    var nodesEnter = nodesUpdate.enter()
        .append('g')
            .attr('class', 'node')
            .attr('transform', 'translate(' + source.x0 + ',' + source.y0 + ')')
            .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));

    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.x0, y: source.y0 }; 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();
}

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>
&#13;
&#13;
&#13;

如果单击Falsey节点然后单击Truthy节点,您将看到每个节点下方出现两个子节点,如预期的那样。但是,如果首先单击Truthy节点,当您单击Falsey节点时,您将看到Truthy子节点在Falsey下移动,并且Falsey子节点在Truthy下移动。另外,Falsey和Truthy下面的子节点实际上是相同的两个节点,即使底层数据不同。

我确认在创建子项后数据对象的结构正确。从我所看到的,d3.hierarchy()和d3.tree()方法正常工作,所以我假设我构建选择的方式存在问题。

希望有人能发现问题。

可能与第一个问题相关的第二个问题是:第二次单击Falsey或Truthy会导致子节点(&amp; links)转换回父节点,但它不跟踪父节点&#39;的立场。希望有人也可以在这里发现问题。

谢谢!

1 个答案:

答案 0 :(得分:1)

在我看来,当您加入数据时需要一个关键功能:

  

如果未指定键功能,则数据中的第一个数据将分配给第一个选定元素,第二个数据分配给第二个选定元素,依此类推。可以指定一个关键函数来控制将哪个数据分配给哪个元素,替换默认的索引连接。

所以,这应该是你的数据绑定选择:

var nodesUpdate = group.selectAll('.node')
    .data(descendants, function(d){ return d.data.name});

检查代码段:

&#13;
&#13;
var data = {
    source: {
        type: 'dataSource',
        name: 'Data Source',
        silos: [
            { name: 'Silo 1', selected: true },
            { name: 'Silo 2', selected: false },
            { name: 'Silo 3', selected: false }
        ],
        union: {
            type: 'union',
            name: 'Union',
            count: null,
            cardinalities: [
                { type: 'cardinality', positive: false, name: 'Falsey', count: 40, cardinalities: [] },
                { type: 'cardinality', positive: true, name: 'Truthy', 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({ type: 'cardinality', positive: false, name: 'F ' + (new Date()).getSeconds(), count: 40, cardinalities: [] });
                d.data.cardinalities.push({ type: 'cardinality', positive: true, name: 'T ' + (new Date()).getSeconds(), 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();

    // store previous position for transitioning
    descendants.forEach(function (d) {
        d.x0 = d.x;
        d.y0 = d.y;
    });

    // ensure source is set when rendering for the first time (hierarch is the root, same as descendants[0])
    source = source || hierarchy;

    // render nodes
    var nodesUpdate = group.selectAll('.node').data(descendants, function(d){ return d.data.name});

    var nodesEnter = nodesUpdate.enter()
        .append('g')
            .attr('class', 'node')
            .attr('transform', 'translate(' + source.x0 + ',' + source.y0 + ')')
            .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));

    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.x0, y: source.y0 }; 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();
}

window.addEventListener('resize', render); // todo: use requestAnimationFrame (RAF) for this

render();
&#13;
.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
}
&#13;
<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>
&#13;
&#13;
&#13;