d3.js v4动态地将节点添加到整洁的树中

时间:2016-09-29 15:09:01

标签: d3.js tree

我正在尝试做与此问题类似的事情d3.js how to dynamically add nodes to a tree。但是,我发现很难在d3.js的v4中使用任何类型的解决方案。

我正在从一些预加载的json构建树,然后我希望能够在用户单击节点时将子节点添加到树中。添加子节点的过程将涉及对REST服务的调用,该服务将返回一些json。我的大多数代码都基于http://bl.ocks.org/mbostock/4339083,它是一个可扩展/可折叠的整洁树,使用v3和https://bl.ocks.org/mbostock/9d0899acb5d3b8d839d9d613a9e1fe04,这是一个使用v4的不可扩展/可折叠的整洁树。

我的代码在https://jsfiddle.net/rkian/3e73Lz6d/。我可以毫无问题地绘制树,但我确实在弄清楚如何向树中添加节点时遇到了麻烦。我怀疑这个:

import React from 'react'
import {
  StyleSheet,
  Text,
  TextInput,
  View,
  TouchableHighlight,
} from 'react-native'

export default class TabBar extends React.Component {
  state = {
    selectedTab: 'list'
  }

  render() {
    return (
      <TabBarIOS selectedTab={this.state.selectedTab}
        unselectedTintColor="#ffffff"
        tintColor="#ffe429"
        barTintColor="#294163">

        <TabBarIOS.Item 
          title="My List"
          systemIcon="bookmarks"
          selected={this.state.selectedTab==='list'}
          onPress={() => {
              this.setState({
                  selectedTab: 'list',
              });
          }}
          >
          <CreateUser />
        </TabBarIOS.Item>
      </TabBarIOS>
    );
  }
}


var styles = StyleSheet.create({
  tabContent: {
    flex: 1,
    alignItems: 'center',
  },
  tabText: {
    color: 'darkslategrey',
    margin: 50,
  },
});



export default class CreateUser extends React.Component{

    render(){
        return (
                <View style={styles.container}>
                    <TouchableHighlight style={styles.button}>
                        <Text style={styles.buttonText}>LOG IN</Text>
                    </TouchableHighlight>
                </View>
            )
    }

}



var styles = StyleSheet.create({
    container: {
        flex: 1,
        flexDirection: "column",
        justifyContent: "flex-end",
        alignItems: 'center',
    },
    button: {
        backgroundColor: "#ffe429",
        borderRadius: 3,
        height: 60,
        width: 200,
        margin: 7,
        //flex: 1,
        alignItems: "center",
        justifyContent: "center",
    },
    buttonText: {
        color: "#294163",
    }

})

是关键,但我只是无法弄清楚如何实现它。

1 个答案:

答案 0 :(得分:0)

有一些奇怪的谱系变量(平面数组要变成层次结构),我试图从中构建我的树,当我把它变成树时,它改变了它内的对象。我通过制作血统的深层副本来解决这个问题。我的所有代码都在这里,我希望它可以帮到某人:

var lineage = $.parseJSON('[{"id":739,"name":"Life","parent":null,"rank":9,"child_count":1},{"id":1,"name":"Eukaryota","parent":739,"rank":1,"child_count":3},{"id":43,"name":"Animalia","parent":1,"rank":2,"child_count":9},{"id":2,"name":"Archaeplastida","parent":1,"rank":2,"child_count":1},{"id":740,"name":"Plantae","parent":1,"rank":2,"child_count":0},{"id":417,"name":"Annelida","parent":43,"rank":3,"child_count":1},{"id":336,"name":"Arthropoda","parent":43,"rank":3,"child_count":6},{"id":228,"name":"Chordata","parent":43,"rank":3,"child_count":3},{"id":222,"name":"Cnidaria","parent":43,"rank":3,"child_count":4},{"id":328,"name":"Mollusca","parent":43,"rank":3,"child_count":1},{"id":548,"name":"Myzozoa","parent":43,"rank":3,"child_count":1},{"id":604,"name":"Nematoda","parent":43,"rank":3,"child_count":1},{"id":467,"name":"Platyhelminthes","parent":43,"rank":3,"child_count":2},{"id":275,"name":"Porifera","parent":43,"rank":3,"child_count":2},{"id":418,"name":"Polychaeta","parent":417,"rank":4,"child_count":3},{"id":419,"name":"Spionida","parent":418,"rank":5,"child_count":1},{"id":452,"name":"Sabellida","parent":418,"rank":5,"child_count":1},{"id":448,"name":"Terebellida","parent":418,"rank":5,"child_count":1},{"id":420,"name":"Spionidae","parent":419,"rank":6,"child_count":2},{"id":699,"name":"Boccardia","parent":420,"rank":7,"child_count":1},{"id":421,"name":"Polydora","parent":420,"rank":7,"child_count":1},{"id":700,"name":"Boccardia proboscidea","parent":699,"rank":8,"child_count":0}]');

$(document).ready(function() {  
  // WTF js.
  var lineage_flat;
  
	// Color manipulation functions and settings for the tree
	function shadeColor2(color, percent) {
		var f=parseInt(color.slice(1),16),t=percent<0?0:255,p=percent<0?percent*-1:percent,R=f>>16,G=f>>8&0x00FF,B=f&0x0000FF;
		return "#"+(0x1000000+(Math.round((t-R)*p)+R)*0x10000+(Math.round((t-G)*p)+G)*0x100+(Math.round((t-B)*p)+B)).toString(16).slice(1);
	}
	function blendColors(c0, c1, p) {
		var f=parseInt(c0.slice(1),16),t=parseInt(c1.slice(1),16),R1=f>>16,G1=f>>8&0x00FF,B1=f&0x0000FF,R2=t>>16,G2=t>>8&0x00FF,B2=t&0x0000FF;
		return "#"+(0x1000000+(Math.round((R2-R1)*p)+R1)*0x10000+(Math.round((G2-G1)*p)+G1)*0x100+(Math.round((B2-B1)*p)+B1)).toString(16).slice(1);
	}
  
  // Color variables
	var main_color = '#FFFFFF';
	var secondary_color = '#FFFFFF';
	var line_color = '#FFF793';
	var max_rank = lineage[lineage.length - 1]['rank'];

	// Used to assign colors to ranks
	function get_rank_colour(d) {
		rank = d.data.data.rank;
		if(rank == 9) {
			return main_color;
		}
		else {
			return blendColors(main_color, secondary_color, (d.data.rank / max_rank))
		}
	}
  
  // Draws a curve between two points
  function connector(d) {
    return 'M' + d.x + ',' + (d.y - 18) + 
      "C" + (d.x + d.parent.x) / 2 + "," + (d.y - 25) + 
      " " + (d.x + d.parent.x) / 2 + "," + (d.parent.y + 25) + 
      " " + d.parent.x + "," + (d.parent.y + 17);
  };   

	// Transition vars
	var duration = 500;

  // Get the width of the container element for the tree
	width = $('#svgcontainer').width();

	// Calculate the height required based on the number of node levels on the ancestry tree
	height = lineage[lineage.length - 1]['rank']* 150;

	// Set the svg element's height
	$('#lifetree').attr('height', height + 'px');

  // Select the svg element in the DOM
  var svg = d3.select("svg")

  // Insert a group container and move it 40 px to the right (to pad the tree contents in the svg container)
  var g = svg.append("g").attr("transform", "translate(40,40)");

  // Create and return a d3 tree object of the correct width and height, run the root hierarchy element through it
  var tree = d3.tree().size([width-200, height - 160]);


  var root = getTreeData(lineage);
  updateTree(root);

  // Get the data for the tree
  function getTreeData(json) {    
    // Save the flat lineage, we have to do this weird parse thing to make a deep copy
    lineage_flat = JSON.parse(JSON.stringify(json));
    
    // This seems to unflatten arrays of objects with parentIds and parents. Wish I'd known about it sooner.
    var dataTree = d3.stratify()
      .id(function(d){ return d.id; })
      .parentId(function(d){  return d.parent; })
      (JSON.parse(JSON.stringify(json)));

    // D3 requires a hierarchy object which then gets made into a tree
    var root = d3.hierarchy(dataTree);
    tree(root);
    
    // Normalize for fixed-depth, also we do some fancy transitions so save a copy of original xys
    root.each(function(d) { d.y = d.depth * 100; d.x0 = d.x; d.y0 = d.y; });
    return root;
  }

  function drawElements(node) {
    // Add circles above each node
    node.append("circle")
      .attr("r", 2)
      .attr("transform", function(d) { return "translate(0,-18)"; })
      .attr("class", "upper-circle")
      .style("stroke", get_rank_colour)
      .style("fill", get_rank_colour);

    // Add the circles below each node
    node.append("circle")
      .attr("r", 4)
      .attr("transform", function(d) { return "translate(0,16)"; })
      .attr("class", "lower-circle")
      .style("stroke", "#000000")
      .style("fill", function(d) {
        return d.data.data.child_count > 0 ? "#FFFFFF" : "#000000";
      });

    // Add text
    node.append("text")
      .attr("dy", 3)
      .style("fill", '#FFFFFF')
      .style("text-anchor", "middle")
      .text(function(d) {
        return d.data.data.name;
        if(d.data.rank == max_rank || d.data.name == "Life") {
          return d.data.name;
        }
        else if(d.children) {
          return d.data.name + ' (' + d.children.length + ")";
        }
        else {
          return d.data.name + ' (' + d.data.count + ")";
        }
      })
      .each(function(d) {
        d.textwidth = this.getBBox().width;
        d.textheight = this.getBBox().height;
      });

    // Add clickable background rectangle so it looks nicer
    node.insert("rect",":first-child")
      .style("fill", '#000000')
      .style("fill-opacity", function(d) {
          if(d.children || d.data.data.rank == max_rank) { return 0.5; }
          else { return 0.2; }
        }
      )
      .attr('height', function(d) { return d.textheight + 10; })
      .attr('width', function(d) { return d.textwidth + 10; })
      .attr("transform", function(d) {
        if(d.data.data.rank == 9) {
          return "translate(-" +  ((d.textwidth + 10) / 2) + ",-" +  ((d.textheight + 30) / 2) + ")";
        }
        return "translate(-" +  ((d.textwidth + 10) / 2) + ",-" +  ((d.textheight + 15) / 2) + ")";
      })
      .attr('rx', 10)
      .attr('ry', 10);
  }

  function updateTree(source, shallowestDepth = 0) {
    /* 
     * Nodes
     */
    // Data join with source data, keeping ids so it knows about the same nodes
    var node = g.selectAll(".node")
      .data(source.descendants() , function(d) { return d.data.id; });
    
    // Data enter, this starts doing things to all the new nodes
    var nodeEnter = node.enter()
      .append("g")
      .attr("class", function(d) { return "rank-" + d.data.data.rank + " node" + (d.children ? " node--internal" : " node--leaf"); })
      .attr("transform", function(d) {
        if(d.parent != null) {
          return "translate(" + d.parent.x + "," + d.parent.y + ")";
        }
        return "translate(" + d.x + "," + d.y + ")"; 
      })
      .on("click", click);
    
    // Add text + bg + circles to the nodes
    drawElements(nodeEnter);
    
    // Add pretty hover class for each taxon node
    $('g').hover(function() {
      $(this).children('rect').addClass('recthover');
    }, function() {
      $(this).children('rect').removeClass('recthover');
    });
    
    // Transition nodes to their new position.
    var nodeMerge = node.merge(nodeEnter).transition()
      .duration(duration)
      .attr('transform', function (d) {
        return 'translate(' + d.x + ',' + d.y + ')';
      });
    nodeMerge.select('rect', ':first-child').style("fill-opacity", function(d) {
      if(d.children || d.data.data.rank == max_rank) { return 0.5; }
      else { return 0.2; }
    });
      
    // Get the old elements for removal
    var oldNode = node.exit();
    
    // Find the shallowest depth in the old element, that's the parent
    oldNode.each(function(d) {
        var shallowestParent = d;
        do { shallowestParent = shallowestParent.parent; }
        while(shallowestParent.depth > shallowestDepth);
        d.shallowestParentX = shallowestParent.x;
        d.shallowestParentY = shallowestParent.y; 
    });
    
    // Transition the old nodes out
    var transitionedNodes = oldNode.transition()
      .duration(duration)
      .attr("transform", function(d) { 
        return "translate(" + d.shallowestParentX + "," + d.shallowestParentY + ")"; 
      });
    oldNode.selectAll('rect').transition()
      .style("fill-opacity", 0)
      .duration(duration/2)
    oldNode.selectAll('text').transition()
      .style("fill-opacity", 0)
      .duration(duration/2)
    oldNode.selectAll('circle').transition()
      .style("fill-opacity", 0)
      .duration(duration/3)
    transitionedNodes.remove();
    
    /* 
     * Links
     */
    var link = g.selectAll(".link")
      .data(source.descendants().slice(1).reverse(), function(d) { return d.data.id; })
    
    // Draw the links between nodes
    var linkEnter = link.enter()
      .insert("path",":first-child")
      .attr("class", "link")
      .style("stroke", function(d) {
        if(d.children) {
          return line_color;
        }
        return blendColors(main_color, secondary_color, (d.data.data.rank / max_rank))
      })
      .attr("d",  function (d) {
        var o = {x: d.parent.x0, y: d.parent.y0, parent: {x: d.parent.x0, y: d.parent.y0}};
        return connector(o);
      });
      
    // Transition links to their new position.
    var linkMerge = link.merge(linkEnter).transition()
      .duration(0)
      .attr('d', connector);
      
    // Style the links
    linkMerge.style("stroke", function(d) {
      if(d.children) {
        return line_color;
      }
      return blendColors(main_color, secondary_color, (d.data.data.rank / max_rank))
    })
    
    // Transition the old links out
    var oldLink = link.exit();
    oldLink.transition()
      .duration(duration/2)
      .attr("d", function(d) {
        var o = {x: d.x, y: d.y, parent: {x: d.x, y: d.y}};
        return connector(o);
      })
      .remove();
  }
  
  // Toggle children on click.
  function click(d) {
    // If the node does not have any pre-loaded children
    if (!d.children && !d._children) {
      var jsonPath = '/taxa/api/children/' + d.data.id;
      
      // Get the JSON lineage for it
      d3.json(jsonPath, function(error, json) {
        // Get the children
        children = json['children'];
        
        // Make a new lineage array, can't use the one previously stored because
        // of javascript variables mutability being weird
        new_lineage = [];
        lineage_flat.forEach(function(node, i) {
          recalcIndex = lineage.indexOf(node);
          // The node.rank 9 is in there because Life for some reason has rank 9
          // Basically we want to exclude all nodes of a lower rank than the one clicked
          if(node.rank > d.data.data.rank && node.rank != 9) {           
            // console.log(node); - we don't want these nodes
          }
          else { new_lineage.push(node); }
        });
        
        // Append the children to the new lineage
        children.forEach(function(child) {
          new_lineage.push(child);
        });
        
        // Javascript is weird. We need a deep copy of new_lineage
        temp = JSON.parse(JSON.stringify(new_lineage));
        new_lineage = JSON.parse(JSON.stringify(temp));
        
        // Turn it into a tree and update our svg
        root = getTreeData(new_lineage);
        updateTree(root, d.depth);
       });
    }
  }
});
.link {
  fill: none;
  stroke: orange;
  stroke-width: 1.5px;
	/* Transition. */
	-o-transition:.5s;
	-ms-transition:.5s;
	-moz-transition:.5s;
	-webkit-transition:.5s;
	/* ...and now for the proper property */
	transition:.5s;
}
.link:hover {
  fill: none;
  stroke: orange;
  stroke-width: 3px;
}
.link.warning{
  stroke: orange;
}
.lower-circle, .upper-circle {
  z-index: 1;
  stroke-width: 2px;
}
path.link {
  stroke-width: 3px;
}
.rank-9 .upper-circle {
  display: none;
}
.rank-9  text {
  font-size: 3em;
  font-weight: bold;
}
.rank-1 {
  font-size: 1.5em;
  font-weight: bold;
}

.rank-2 {
  font-size: 1em;
  font-weight: bold;
  margin-right: 20px;
  margin-left: 20px;
}
svg text {

}
.rank-3 {
  font-size: 1em;
}
#triangles {
  /*position: relative;
  top: -20px;
  margin-top: -50px;
  padding-top: 80px;

-webkit-transform: rotate(180deg);
-moz-transform: rotate(180deg);
-ms-transform: rotate(180deg);
-o-transform: rotate(180deg);
transform: rotate(180deg);*/
}
#svgcontainer {
  position: relative;
  padding-top: 80px;
/*-webkit-transform: rotate(180deg);
-moz-transform: rotate(180deg);
-ms-transform: rotate(180deg);
-o-transform: rotate(180deg);
transform: rotate(180deg);*/
}
#svgparent-disablethis {
  background: transparent url('static/img/carousel/1.jpg' 0 0 no-repeat );

  background: -moz-linear-gradient(top, rgba(255,255,255,0) 100%, rgba(130,91,0,1) 0%);   /* FF3.6+ */
  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,0)), color-stop(100%,rgba(130,91,0,1))); /* Chrome,Safari4+ */
  background: -webkit-linear-gradient(top, rgba(255,255,255,0) 0%,rgba(130,91,0,1) 100%); /* Chrome10+,Safari5.1+ */
  background: -o-linear-gradient(top, rgba(255,255,255,0) 0%,rgba(130,91,0,1) 100%); /* Opera 11.10+ */
  background: -ms-linear-gradient(top, rgba(255,255,255,0) 0%,rgba(130,91,0,1) 100%); /* IE10+ */
  background: linear-gradient(to bottom, rgba(255,255,255,0) 0%,rgba(130,91,0,1) 100%); /* W3C */
  filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00ffffff', endColorstr='#825b00',GradientType=0 ); /* IE6-9 */
}

svg { width: 100%; height: auto; }


rect {
	/* Transition. */
	-o-transition:.2s;
	-ms-transition:.2s;
	-moz-transition:.2s;
	-webkit-transition:.2s;
	/* ...and now for the proper property */
	transition:.2s;


}
text {
  cursor: pointer;
}

.lower-circle {
	cursor: pointer;
}
.lower-circle:hover {
	fill: #FFF !important;
}
.recthover {
	fill: #FFEC1C !important;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

  <div id="svgparent">
    <div id="svgcontainer">
      <svg id='lifetree'></svg>
    </div>
  </div>