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) { console.log('update'); 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;