import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { select } from 'd3-selection'; import { tree, stratify } from 'd3-hierarchy'; function diagonal(d) { const offset = 50; return ( 'M' + d.y + ',' + d.x + 'C' + (d.parent.y + offset) + ',' + d.x + ' ' + (d.parent.y + offset) + ',' + d.parent.x + ' ' + d.parent.y + ',' + d.parent.x ); } const nodeRadius = 8; const verticalSpaceBetweenNodes = 70; const NARROW_HORIZONTAL_SPACES = 47; const WIDE_HORIZONTAL_SPACES = 65; const stratifyFn = stratify() .id(d => d.id) .parentId(d => d.parent); class Tree extends Component { // state = { // startingCoordinates: null, // isDown: false // } static propTypes = { name: PropTypes.string, width: PropTypes.number, allowScaleWidth: PropTypes.bool, nodes: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.string, name: PropTypes.string, parent: PropTypes.string }) ), selectedNodeId: PropTypes.string, onNodeClick: PropTypes.func, onRenderedBeyondWidth: PropTypes.func }; static defaultProps = { width: 500, allowScaleWidth: true, name: 'default-name' }; render() { let { width, name, scrollable = false } = this.props; return (
); } componentDidMount() { this.renderTree(); } // handleMouseMove(e) { // if (!this.state.isDown) { // return; // } // const container = select(`.tree-view.${this.props.name}-container`); // let coordinates = this.getCoordinates(e); // container.property('scrollLeft' , container.property('scrollLeft') + coordinates.x - this.state.startingCoordinates.x); // container.property('scrollTop' , container.property('scrollTop') + coordinates.y - this.state.startingCoordinates.y); // } // handleMouseDown(e) { // let startingCoordinates = this.getCoordinates(e); // this.setState({ // startingCoordinates, // isDown: true // }); // } // handleMouseUp() { // this.setState({ // startingCorrdinates: null, // isDown: false // }); // } // getCoordinates(e) { // var bounds = e.target.getBoundingClientRect(); // var x = e.clientX - bounds.left; // var y = e.clientY - bounds.top; // return {x, y}; // } componentDidUpdate(prevProps) { if ( this.props.nodes.length !== prevProps.nodes.length || this.props.selectedNodeId !== prevProps.selectedNodeId ) { this.renderTree(); } } renderTree() { let { width, nodes, name, allowScaleWidth, selectedNodeId, onRenderedBeyondWidth, toWiden } = this.props; if (nodes.length > 0) { let horizontalSpaceBetweenLeaves = toWiden ? WIDE_HORIZONTAL_SPACES : NARROW_HORIZONTAL_SPACES; const treeFn = tree().nodeSize([ horizontalSpaceBetweenLeaves, verticalSpaceBetweenNodes ]); //.size([width - 50, height - 50]) let root = stratifyFn(nodes).sort((a, b) => a.data.name.localeCompare(b.data.name) ); let svgHeight = verticalSpaceBetweenNodes * root.height + nodeRadius * 6; treeFn(root); let nodesXValue = root.descendants().map(node => node.x); let maxX = Math.max(...nodesXValue); let minX = Math.min(...nodesXValue); let svgTempWidth = (maxX - minX) / 30 * horizontalSpaceBetweenLeaves; let svgWidth = svgTempWidth < width ? width - 5 : svgTempWidth; const svgEL = select(`svg.${name}`); const container = select(`.tree-view.${name}-container`); svgEL.html(''); svgEL.attr('height', svgHeight); let canvasWidth = width; if (svgTempWidth > width) { if (allowScaleWidth) { canvasWidth = svgTempWidth; } // we seems to have a margin of 25px that we can still see with text if ( svgTempWidth - 25 > width && onRenderedBeyondWidth !== undefined ) { onRenderedBeyondWidth(); } } svgEL.attr('width', canvasWidth); let rootGroup = svgEL .append('g') .attr( 'transform', `translate(${svgWidth / 2 + nodeRadius},${nodeRadius * 4}) rotate(90)` ); // handle link rootGroup .selectAll('.link') .data(root.descendants().slice(1)) .enter() .append('path') .attr('class', 'link') .attr('d', diagonal); let node = rootGroup .selectAll('.node') .data(root.descendants()) .enter() .append('g') .attr( 'class', node => `node ${node.children ? ' has-children' : ' leaf'} ${ node.id === selectedNodeId ? 'selectedNode' : '' } ${this.props.onNodeClick ? 'clickable' : ''}` ) .attr( 'transform', node => 'translate(' + node.y + ',' + node.x + ')' ) .on('click', node => this.onNodeClick(node)); node .append('circle') .attr('r', nodeRadius) .attr('class', 'outer-circle'); node .append('circle') .attr('r', nodeRadius - 3) .attr('class', 'inner-circle'); node .append('text') .attr('y', nodeRadius / 4 + 1) .attr('x', -nodeRadius * 1.8) .text(node => node.data.name) .attr('transform', 'rotate(-90)'); let selectedNode = selectedNodeId ? root.descendants().find(node => node.id === selectedNodeId) : null; if (selectedNode) { container.property( 'scrollLeft', svgWidth / 4 + (svgWidth / 4 - 100) - selectedNode.x / 30 * horizontalSpaceBetweenLeaves ); container.property( 'scrollTop', selectedNode.y / 100 * verticalSpaceBetweenNodes ); } else { container.property( 'scrollLeft', svgWidth / 4 + (svgWidth / 4 - 100) ); } } } onNodeClick(node) { if (this.props.onNodeClick) { this.props.onNodeClick(node.data); } } } export default Tree;