2017-10-11 60 views
0

所以我試圖在D3中創建一棵樹(根據here改編),根據它們的值顯示一系列特定顏色的節點。問題是我以設定的時間間隔獲取新數據,可能會更改這些值。我希望樹在收到新信息時相應地更新顏色。我嘗試了許多不同的方法,導致整個樹重新繪製自己,飛到屏幕上,並自動擴展每個節點的子節點。我期望的效果是更新節點的顏色發生變化,而樹會尊重摺疊/未摺疊節點的狀態,而用戶不會看到整個樹自身重置。這可能嗎?D3 Tree動態更新節點的顏色

這是我到目前爲止有:

// Get JSON data 
 

 
var treeData = { 
 
    "name": "rootAlert", 
 
    "alert": "true", 
 
    "children": [{ 
 
    "name": "Child1", 
 
    "alert": "true", 
 
    "children": [{ 
 
     "name": "Child1-1", 
 
     "alert": "false" 
 
    }, { 
 
     "name": "Child1-2", 
 
     "alert": "false" 
 
    }, { 
 
     "name": "Child1-3", 
 
     "alert": "true" 
 
    }] 
 
    }, { 
 
    "name": "Child2", 
 
    "alert": "false", 
 
    "children": [{ 
 
     "name": "Child2-1", 
 
     "alert": "false" 
 
    }, { 
 
     "name": "Child2-2", 
 
     "alert": "false" 
 
    }, { 
 
     "name": "Child2-3", 
 
     "alert": "false" 
 
    }] 
 
    }, { 
 
    "name": "Child3", 
 
    "alert": "false" 
 
    }] 
 
} 
 

 

 

 

 

 

 
// Calculate total nodes, max label length 
 
var totalNodes = 0; 
 
var maxLabelLength = 0; 
 
// variables for drag/drop 
 
var selectedNode = null; 
 
var draggingNode = null; 
 
// panning variables 
 
var panSpeed = 200; 
 
var panBoundary = 20; // Within 20px from edges will pan when dragging. 
 
// Misc. variables 
 
var i = 0; 
 
var duration = 750; 
 
var root; 
 

 
// size of the diagram 
 
var viewerWidth = $(document).width(); 
 
var viewerHeight = $(document).height(); 
 

 
var tree = d3.layout.tree() 
 
    .size([viewerHeight, viewerWidth]); 
 

 
// define a d3 diagonal projection for use by the node paths later on. 
 
var diagonal = d3.svg.diagonal() 
 
    .projection(function(d) { 
 
    return [d.y, d.x]; 
 
    }); 
 

 
// A recursive helper function for performing some setup by walking through all nodes 
 

 
function visit(parent, visitFn, childrenFn) { 
 
    if (!parent) return; 
 

 
    visitFn(parent); 
 

 
    var children = childrenFn(parent); 
 
    if (children) { 
 
    var count = children.length; 
 
    for (var i = 0; i < count; i++) { 
 
     visit(children[i], visitFn, childrenFn); 
 
    } 
 
    } 
 
} 
 

 
function visit2(parent, visitFn, childrenFn) { 
 
    if (!parent) return; 
 

 
    visitFn(parent); 
 

 
    var children = childrenFn(parent); 
 
    if (children) { 
 
    var count = children.length; 
 
    for (var i = 0; i < count; i++) { 
 
     visit(children[i], visitFn, childrenFn); 
 
    } 
 
    } 
 
} 
 

 
// Call visit function to establish maxLabelLength 
 
visit(treeData, function(d) { 
 
    totalNodes++; 
 
    maxLabelLength = Math.max(d.name.length, maxLabelLength); 
 

 
}, function(d) { 
 
    return d.children && d.children.length > 0 ? d.children : null; 
 
}); 
 

 
// TODO: Pan function, can be better implemented. 
 

 
function pan(domNode, direction) { 
 
    var speed = panSpeed; 
 
    if (panTimer) { 
 
    clearTimeout(panTimer); 
 
    translateCoords = d3.transform(svgGroup.attr("transform")); 
 
    if (direction == 'left' || direction == 'right') { 
 
     translateX = direction == 'left' ? translateCoords.translate[0] + speed : translateCoords.translate[0] - speed; 
 
     translateY = translateCoords.translate[1]; 
 
    } else if (direction == 'up' || direction == 'down') { 
 
     translateX = translateCoords.translate[0]; 
 
     translateY = direction == 'up' ? translateCoords.translate[1] + speed : translateCoords.translate[1] - speed; 
 
    } 
 
    scaleX = translateCoords.scale[0]; 
 
    scaleY = translateCoords.scale[1]; 
 
    scale = zoomListener.scale(); 
 
    svgGroup.transition().attr("transform", "translate(" + translateX + "," + translateY + ")scale(" + scale + ")"); 
 
    d3.select(domNode).select('g.node').attr("transform", "translate(" + translateX + "," + translateY + ")"); 
 
    zoomListener.scale(zoomListener.scale()); 
 
    zoomListener.translate([translateX, translateY]); 
 
    panTimer = setTimeout(function() { 
 
     pan(domNode, speed, direction); 
 
    }, 50); 
 
    } 
 
} 
 

 
// Define the zoom function for the zoomable tree 
 

 
function zoom() { 
 
    svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")"); 
 
} 
 

 

 
// define the zoomListener which calls the zoom function on the "zoom" event constrained within the scaleExtents 
 
var zoomListener = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom); 
 

 
// define the baseSvg, attaching a class for styling and the zoomListener 
 
var baseSvg = d3.select("#tree-container").append("svg") 
 
    .attr("width", viewerWidth) 
 
    .attr("height", viewerHeight) 
 
    .attr("class", "overlay") 
 
    .call(zoomListener); 
 

 

 
// Function to center node when clicked/dropped so node doesn't get lost when collapsing/moving with large amount of children. 
 

 
function centerNode(source) { 
 
    scale = zoomListener.scale(); 
 
    x = -source.y0; 
 
    y = -source.x0; 
 
    x = x * scale + viewerWidth/2; 
 
    y = y * scale + viewerHeight/2; 
 
    d3.select('g').transition() 
 
    .duration(duration) 
 
    .attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")"); 
 
    zoomListener.scale(scale); 
 
    zoomListener.translate([x, y]); 
 
} 
 

 
function leftAlignNode(source) { 
 
    scale = zoomListener.scale(); 
 
    x = -source.y0; 
 
    y = -source.x0; 
 
    x = (x * scale) + 100; 
 
    y = y * scale + viewerHeight/2; 
 
    d3.select('g').transition() 
 
    .duration(duration) 
 
    .attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")"); 
 
    zoomListener.scale(scale); 
 
    zoomListener.translate([x, y]); 
 
} 
 

 
// Toggle children function 
 

 
function toggleChildren(d) { 
 
    if (d.children) { 
 
    d._children = d.children; 
 
    d.children = null; 
 
    } else if (d._children) { 
 
    d.children = d._children; 
 
    d._children = null; 
 
    } 
 
    return d; 
 
} 
 

 
function toggle(d) { 
 
    if (d.children) { 
 
    d._children = d.children; 
 
    d.children = null; 
 
    } else { 
 
    d.children = d._children; 
 
    d._children = null; 
 
    } 
 
} 
 

 
// Toggle children on click. 
 

 
function click(d) { 
 
    if (d3.event.defaultPrevented) return; // click suppressed 
 

 
    if (d._children != null) { 
 
    var isCollapsed = true 
 
    } else { 
 
    var isCollapsed = false; 
 
    } 
 

 
    d = toggleChildren(d); 
 
    update(d); 
 

 
    if (isCollapsed) { 
 
    leftAlignNode(d); 
 
    } else { 
 
    centerNode(d); 
 
    } 
 

 
} 
 

 
function update(source) { 
 
    // Compute the new height, function counts total children of root node and sets tree height accordingly. 
 
    // This prevents the layout looking squashed when new nodes are made visible or looking sparse when nodes are removed 
 
    // This makes the layout more consistent. 
 
    var levelWidth = [1]; 
 
    var childCount = function(level, n) { 
 

 
    if (n.children && n.children.length > 0) { 
 
     if (levelWidth.length <= level + 1) levelWidth.push(0); 
 

 
     levelWidth[level + 1] += n.children.length; 
 
     n.children.forEach(function(d) { 
 
     childCount(level + 1, d); 
 
     }); 
 
    } 
 
    }; 
 
    childCount(0, root); 
 
    var newHeight = d3.max(levelWidth) * 25; // 25 pixels per line 
 
    tree = tree.size([newHeight, viewerWidth]); 
 

 
    // Compute the new tree layout. 
 
    var nodes = tree.nodes(root).reverse(), 
 
    links = tree.links(nodes); 
 

 
    // Set widths between levels based on maxLabelLength. 
 
    nodes.forEach(function(d) { 
 
    d.y = (d.depth * (maxLabelLength * 5)); //maxLabelLength * 10px 
 
    // alternatively to keep a fixed scale one can set a fixed depth per level 
 
    // Normalize for fixed-depth by commenting out below line 
 
    // d.y = (d.depth * 500); //500px per level. 
 
    }); 
 

 
    // Update the nodes… 
 
    node = svgGroup.selectAll("g.node") 
 
    .data(nodes, function(d) { 
 
     return d.id || (d.id = ++i); 
 
    }); 
 

 
    // Enter any new nodes at the parent's previous position. 
 
    var nodeEnter = node.enter().append("g") 
 
    //.call(dragListener) 
 
    .attr("class", "node") 
 
    .attr("transform", function(d) { 
 
     return "translate(" + source.y0 + "," + source.x0 + ")"; 
 
    }) 
 
    .on('click', click); 
 

 
    nodeEnter.append("circle") 
 
    .attr('class', 'nodeCircle') 
 
    .attr("r", 0) 
 
    .style("fill", function(d) { 
 
     return d._children ? "lightsteelblue" : "#fff"; 
 
    }); 
 

 
    nodeEnter.append("text") 
 
    .attr("x", function(d) { 
 
     return d.children || d._children ? -10 : 10; 
 
    }) 
 
    .attr("dy", ".35em") 
 
    .attr('class', 'nodeText') 
 
    .attr("text-anchor", function(d) { 
 
     return d.children || d._children ? "end" : "start"; 
 
    }) 
 
    .text(function(d) { 
 
     return d.name; 
 
    }) 
 
    .style("fill-opacity", 0); 
 

 

 
    // Update the text to reflect whether node has children or not. 
 
    node.select('text') 
 
    .attr("x", function(d) { 
 
     return d.children || d._children ? -10 : 10; 
 
    }) 
 
    .attr("text-anchor", function(d) { 
 
     return d.children || d._children ? "end" : "start"; 
 
    }) 
 
    .text(function(d) { 
 
     return d.name; 
 
    }); 
 

 
    // Change the circle fill depending on whether it has children and is collapsed 
 
    node.select("circle.nodeCircle") 
 
    .attr("r", 4.5) 
 
    .style("fill", function(d) { 
 
     return d._children ? "lightsteelblue" : "#fff"; 
 
    }); 
 

 
    // Transition nodes to their new position. 
 
    var nodeUpdate = node.transition() 
 
    .duration(duration) 
 
    .attr("transform", function(d) { 
 
     return "translate(" + d.y + "," + d.x + ")"; 
 
    }); 
 
    nodeUpdate.select("circle") 
 
    .attr("r", 4.5) 
 
    .style("fill", function(d) { // alert(d.alert); 
 
     //console.log(d.name + ' is ' + d.alert) 
 
     if (d.alert == 'true') //if alert == true 
 
     return "red"; 
 
     else return d._children ? "green" : "green"; 
 
    }); 
 

 
    // Fade the text in 
 
    nodeUpdate.select("text") 
 
    .style("fill-opacity", 1); 
 

 
    // Transition exiting nodes to the parent's new position. 
 
    var nodeExit = node.exit().transition() 
 
    .duration(duration) 
 
    .attr("transform", function(d) { 
 
     return "translate(" + source.y + "," + source.x + ")"; 
 
    }) 
 
    .remove(); 
 

 
    nodeExit.select("circle") 
 
    .attr("r", 0); 
 

 
    nodeExit.select("text") 
 
    .style("fill-opacity", 0); 
 

 
    // Update the links… 
 
    var link = svgGroup.selectAll("path.link") 
 
    .data(links, function(d) { 
 
     return d.target.id; 
 
    }); 
 

 
    // Enter any new links at the parent's previous position. 
 
    link.enter().insert("path", "g") 
 
    .attr("class", "link") 
 
    .attr("d", function(d) { 
 
     var o = { 
 
     x: source.x0, 
 
     y: source.y0 
 
     }; 
 
     return diagonal({ 
 
     source: o, 
 
     target: o 
 
     }); 
 
    }); 
 

 
    // Transition links to their new position. 
 
    link.transition() 
 
    .duration(duration) 
 
    .attr("d", diagonal); 
 

 
    // Transition exiting nodes to the parent's new position. 
 
    link.exit().transition() 
 
    .duration(duration) 
 
    .attr("d", function(d) { 
 
     var o = { 
 
     x: source.x, 
 
     y: source.y 
 
     }; 
 
     return diagonal({ 
 
     source: o, 
 
     target: o 
 
     }); 
 
    }) 
 
    .remove(); 
 

 
    // Stash the old positions for transition. 
 
    nodes.forEach(function(d) { 
 
    d.x0 = d.x; 
 
    d.y0 = d.y; 
 
    }); 
 
} 
 

 
// Append a group which holds all nodes and which the zoom Listener can act upon. 
 
var svgGroup = baseSvg.append("g"); 
 

 
// Define the root 
 
root = treeData; 
 
root.x0 = viewerHeight/2; 
 
root.y0 = 0; 
 

 
// Layout the tree initially and center on the root node. 
 
tree.nodes(root).forEach(function(n) { 
 
    toggle(n); 
 
}); 
 
update(root); 
 
leftAlignNode(root); 
 

 

 
setInterval(function() { 
 
    //update the color of each node 
 
}, 2000);
.node { 
 
    cursor: pointer; 
 
} 
 

 
.overlay { 
 
    background-color: #EEE; 
 
} 
 

 
.node circle { 
 
    fill: #fff; 
 
    stroke: gray; 
 
    stroke-width: 1.5px; 
 
} 
 

 
.node text { 
 
    font: 10px sans-serif; 
 
} 
 

 
.link { 
 
    fill: none; 
 
    stroke: #ccc; 
 
    stroke-width: 1.5px; 
 
} 
 

 
.templink { 
 
    fill: none; 
 
    stroke: red; 
 
    stroke-width: 3px; 
 
} 
 

 
.ghostCircle.show { 
 
    display: block; 
 
} 
 

 
.ghostCircle, 
 
.activeDrag .ghostCircle { 
 
    display: none; 
 
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script> 
 
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script> 
 
<div id="tree-container"></div>

我找到了一個很好的例子here

它基本上正是我想要的。但是,在進行選擇時不是更新樹,而是希望在接收到新數據時自動更新樹。我也想看看它不在AngularJS中。我試圖從這個例子中實現同樣類型的更新函數,但是我仍然從左上角進入,而例子非常流暢!

回答

0

當您的數據管道更新時,最好重新呈現可視化文件。

您必須保存可視化狀態,保留重新呈現可視化所需的所有信息,以便您的用戶不再明智。

+0

是的,這就是我想要做的。我在原文中包含了這個工作的一個例子,但我似乎無法自行復制它。我已經實現了該示例所具有的大部分更改(減去角色),並且仍然彈出並重新繪製了整個事物 – jzeef