D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
timelyportfolio
Full window
Github gist
rCharts + d3.js view of rpart with sankey-like link width
<!doctype HTML> <meta charset = 'utf-8'> <html> <head> <link rel='stylesheet' href='https://timelyportfolio.github.io/rCharts_rpart/css/treestyle.css'> <script src='https://d3js.org/d3.v3.min.js' type='text/javascript'></script> </head> <body > <div id = 'chart1efc2cc57150' class = 'rpart_tree' style = 'height:600px;width:800px'></div> <script> // Get JSON data (function(opts){ var opts = { "dom": "chart1efc2cc57150", "width": 400, "height": 400, "xAxis": { "type": "addCategoryAxis", "showPercent": false }, "yAxis": { "type": "addMeasureAxis", "showPercent": false }, "zAxis": [], "colorAxis": [], "defaultColors": [], "layers": [], "legend": [], "nodeHeight": 100, "maxLabelLength": 10, "id": "chart1efc2cc57150" }; var treeData = {"name":"mtcars","split":{"varid":2,"breaks":7,"index":[],"right":false,"prob":[1,0],"info":[]},"children":[{"name":" cyl < 7, mean = 98.06, n = 18","split":{"varid":4,"breaks":21.45,"index":[2,1],"right":false,"prob":[1,0],"info":[]},"children":[{"name":" mpg >= 21.45, mean = 80, n = 10","split":{"varid":3,"breaks":87.05,"index":[],"right":false,"prob":[0,1],"info":[]},"children":[{"name":" disp < 87.05, mean = 62.25, n = 4","split":[],"children":[],"surrogates":[],"info":[]},{"name":" disp >= 87.05, mean = 91.83, n = 6","split":[],"children":[],"surrogates":[],"info":[]}],"surrogates":[{"varid":4,"breaks":26.65,"index":[2,1],"right":false,"prob":[],"info":[]},{"varid":5,"breaks":4,"index":[2,1],"right":false,"prob":[],"info":[]},{"varid":6,"breaks":2.0375,"index":[],"right":false,"prob":[],"info":[]},{"varid":7,"breaks":19.95,"index":[],"right":false,"prob":[],"info":[]},{"varid":9,"breaks":0.5,"index":[2,1],"right":false,"prob":[],"info":[]}],"info":[]},{"name":" mpg < 21.45, mean = 120.62, n = 8","split":{"varid":7,"breaks":15.98,"index":[2,1],"right":false,"prob":[1,0],"info":[]},"children":[{"name":" qsec >= 15.98, mean = 112.86, n = 7","split":[],"children":[],"surrogates":[],"info":[]},{"name":" qsec < 15.98, mean = 175, n = 1","split":[],"children":[],"surrogates":[],"info":[]}],"surrogates":[],"info":[]}],"surrogates":[{"varid":2,"breaks":5,"index":[],"right":false,"prob":[],"info":[]},{"varid":3,"breaks":120.65,"index":[],"right":false,"prob":[],"info":[]},{"varid":6,"breaks":2.5425,"index":[],"right":false,"prob":[],"info":[]},{"varid":11,"breaks":3,"index":[],"right":false,"prob":[],"info":[]},{"varid":5,"breaks":3.655,"index":[2,1],"right":false,"prob":[],"info":[]}],"info":[]},{"name":" cyl >= 7, mean = 209.21, n = 14","split":{"varid":5,"breaks":3.18,"index":[],"right":false,"prob":[1,0],"info":[]},"children":[{"name":" drat < 3.18, mean = 178.89, n = 9","split":{"varid":4,"breaks":12.8,"index":[2,1],"right":false,"prob":[1,0],"info":[]},"children":[{"name":" mpg >= 12.8, mean = 170, n = 7","split":[],"children":[],"surrogates":[],"info":[]},{"name":" mpg < 12.8, mean = 210, n = 2","split":[],"children":[],"surrogates":[],"info":[]}],"surrogates":[{"varid":3,"breaks":430,"index":[],"right":false,"prob":[],"info":[]},{"varid":6,"breaks":4.66,"index":[],"right":false,"prob":[],"info":[]},{"varid":11,"breaks":3.5,"index":[],"right":false,"prob":[],"info":[]},{"varid":5,"breaks":3.035,"index":[2,1],"right":false,"prob":[],"info":[]},{"varid":7,"breaks":17.71,"index":[],"right":false,"prob":[],"info":[]}],"info":[]},{"name":" drat >= 3.18, mean = 263.8, n = 5","split":{"varid":11,"breaks":6,"index":[],"right":false,"prob":[1,0],"info":[]},"children":[{"name":" carb < 6, mean = 246, n = 4","split":[],"children":[],"surrogates":[],"info":[]},{"name":" carb >= 6, mean = 335, n = 1","split":[],"children":[],"surrogates":[],"info":[]}],"surrogates":[],"info":[]}],"surrogates":[{"varid":7,"breaks":16.355,"index":[2,1],"right":false,"prob":[],"info":[]},{"varid":11,"breaks":3.5,"index":[],"right":false,"prob":[],"info":[]},{"varid":4,"breaks":15.1,"index":[2,1],"right":false,"prob":[],"info":[]},{"varid":9,"breaks":0.5,"index":[],"right":false,"prob":[],"info":[]},{"varid":10,"breaks":4,"index":[],"right":false,"prob":[],"info":[]}],"info":[]}],"surrogates":[{"varid":3,"breaks":266.9,"index":[],"right":false,"prob":[],"info":[]},{"varid":4,"breaks":17.55,"index":[2,1],"right":false,"prob":[],"info":[]},{"varid":6,"breaks":3.49,"index":[],"right":false,"prob":[],"info":[]},{"varid":5,"breaks":3.58,"index":[2,1],"right":false,"prob":[],"info":[]},{"varid":8,"breaks":0.5,"index":[2,1],"right":false,"prob":[],"info":[]}],"info":{"data":{"hp":[],"cyl":[],"disp":[],"mpg":[],"drat":[],"wt":[],"qsec":[],"vs":[],"am":[],"gear":[],"carb":[]},"fitted":{"(fitted)":[7,7,5,7,11,7,14,5,5,7,7,11,11,11,12,12,14,4,4,4,5,11,11,14,11,4,5,5,14,8,15,7],"(response)":[110,110,93,110,175,105,245,62,95,123,123,180,180,180,205,215,230,66,52,65,97,150,150,245,175,66,91,113,264,175,335,109]},"terms":"hp~cyl + disp + mpg + drat + wt + qsec + vs + am + gear + carb","names":[],"info":{"method":"rpart"}}}; // 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; // define the baseSvg, attaching a class for styling and the zoomListener var baseSvg = d3.select("#" + opts.id).append("svg") .attr("width", "100%") .attr("height", "100%") .attr("class", "overlay") // size of the diagram var viewerWidth = d3.select("#" + opts.id)[0][0].getBoundingClientRect().width; var viewerHeight = d3.select("#" + opts.id)[0][0].getBoundingClientRect().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); } } } // Call visit function to establish maxLabelLength visit(treeData, function(d) { totalNodes++; maxLabelLength = opts.maxLabelLength || Math.max(d.name.length, maxLabelLength); }, function(d) { return d.children && d.children.length > 0 ? d.children : null; }); // sort the tree according to the node names function sortTree() { tree.sort(function(a, b) { return b.name.toLowerCase() < a.name.toLowerCase() ? 1 : -1; }); } // Sort the tree initially incase the JSON isn't in a sorted order. sortTree(); // 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); function initiateDrag(d, domNode) { draggingNode = d; d3.select(domNode).select('.ghostCircle').attr('pointer-events', 'none'); d3.selectAll('.ghostCircle').attr('class', 'ghostCircle show'); d3.select(domNode).attr('class', 'node activeDrag'); svgGroup.selectAll("g.node").sort(function(a, b) { // select the parent and sort the path's if (a.id != draggingNode.id) return 1; // a is not the hovered element, send "a" to the back else return -1; // a is the hovered element, bring "a" to the front }); // if nodes has children, remove the links and nodes if (nodes.length > 1) { // remove link paths links = tree.links(nodes); nodePaths = svgGroup.selectAll("path.link") .data(links, function(d) { return d.target.id; }).remove(); // remove child nodes nodesExit = svgGroup.selectAll("g.node") .data(nodes, function(d) { return d.id; }).filter(function(d, i) { if (d.id == draggingNode.id) { return false; } return true; }).remove(); } // remove parent link parentLink = tree.links(tree.nodes(draggingNode.parent)); svgGroup.selectAll('path.link').filter(function(d, i) { if (d.target.id == draggingNode.id) { return true; } return false; }).remove(); dragStarted = null; } baseSvg .call(zoomListener); /* // Define the drag listeners for drag/drop behaviour of nodes. dragListener = d3.behavior.drag() .on("dragstart", function(d) { if (d == root) { return; } dragStarted = true; nodes = tree.nodes(d); d3.event.sourceEvent.stopPropagation(); // it's important that we suppress the mouseover event on the node being dragged. Otherwise it will absorb the mouseover event and the underlying node will not detect it d3.select(this).attr('pointer-events', 'none'); }) .on("drag", function(d) { if (d == root) { return; } if (dragStarted) { domNode = this; initiateDrag(d, domNode); } // get coords of mouseEvent relative to svg container to allow for panning relCoords = d3.mouse(svgGroup[0][0]); if (relCoords[0] < panBoundary) { panTimer = true; pan(this, 'left'); } else if (relCoords[0] > (viewerWidth - panBoundary)) { panTimer = true; pan(this, 'right'); } else if (relCoords[1] < panBoundary) { panTimer = true; pan(this, 'up'); } else if (relCoords[1] > (viewerHeight - panBoundary)) { panTimer = true; pan(this, 'down'); } else { try { clearTimeout(panTimer); } catch (e) { } } d.x0 += d3.event.dy; d.y0 += d3.event.dx; var node = d3.select(this); node.attr("transform", "translate(" + d.y0 + "," + d.x0 + ")"); updateTempConnector(); }).on("dragend", function(d) { if (d == root) { return; } domNode = this; if (selectedNode) { // now remove the element from the parent, and insert it into the new elements children var index = draggingNode.parent.children.indexOf(draggingNode); if (index > -1) { draggingNode.parent.children.splice(index, 1); } if (typeof selectedNode.children !== 'undefined' || typeof selectedNode._children !== 'undefined') { if (typeof selectedNode.children !== 'undefined') { selectedNode.children.push(draggingNode); } else { selectedNode._children.push(draggingNode); } } else { selectedNode.children = []; selectedNode.children.push(draggingNode); } // Make sure that the node being added to is expanded so user can see added node is correctly moved expand(selectedNode); sortTree(); endDrag(); } else { endDrag(); } }); function endDrag() { selectedNode = null; d3.selectAll('.ghostCircle').attr('class', 'ghostCircle'); d3.select(domNode).attr('class', 'node'); // now restore the mouseover event or we won't be able to drag a 2nd time d3.select(domNode).select('.ghostCircle').attr('pointer-events', ''); updateTempConnector(); if (draggingNode !== null) { update(root); centerNode(draggingNode); draggingNode = null; } } */ // Helper functions for collapsing and expanding nodes. function collapse(d) { if (d.children) { d._children = d.children; d._children.forEach(collapse); d.children = null; } } function expand(d) { if (d._children) { d.children = d._children; d.children.forEach(expand); d._children = null; } } var overCircle = function(d) { selectedNode = d; updateTempConnector(); }; var outCircle = function(d) { selectedNode = null; updateTempConnector(); }; // Function to update the temporary connector indicating dragging affiliation var updateTempConnector = function() { var data = []; if (draggingNode !== null && selectedNode !== null) { // have to flip the source coordinates since we did this for the existing connectors on the original tree data = [{ source: { x: selectedNode.y0, y: selectedNode.x0 }, target: { x: draggingNode.y0, y: draggingNode.x0 } }]; } var link = svgGroup.selectAll(".templink").data(data); link.enter().append("path") .attr("class", "templink") .attr("d", d3.svg.diagonal()) .attr('pointer-events', 'none'); link.attr("d", d3.svg.diagonal()); link.exit().remove(); }; // 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 + ( source.name !== root.name ? viewerWidth / 2 : viewerWidth / 4 ); 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; } // Toggle children on click. function click(d) { if (d3.event.defaultPrevented) return; // click suppressed d = toggleChildren(d); update(d); 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) * ( opts.nodeHeight || 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 * 10)); //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); // phantom node to give us mouseover in a radius around it nodeEnter.append("circle") .attr('class', 'ghostCircle') .attr("r", 30) .attr("opacity", 0.2) // change this to zero to hide the target area .style("fill", "red") .attr('pointer-events', 'mouseover') .on("mouseover", function(node) { overCircle(node); }) .on("mouseout", function(node) { outCircle(node); }); // 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 + ")"; }); // 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 }); }); // Size link width according to n based on total n wscale = d3.scale.linear() .range([0,50]) .domain([0,treeData.info.fitted["(fitted)"].length]) link.style("stroke-width",function(d){ return wscale( + d.target.name.replace(/.*n = ([0-9]*)/,"$1") ) }) // 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. update(root); // since we can override node height and label length (width) // if zoom scale == 1 then auto scale to fit tree in container if (zoomListener.scale() == 1) { zoomListener.scale( viewerHeight/tree.size()[0] ); } centerNode(root); })(); </script> <script></script> </body> </html>
Modified
http://d3js.org/d3.v3.min.js
to a secure url
https://d3js.org/d3.v3.min.js