1 import React, {Component} from 'react';
2 import PropTypes from 'prop-types';
3 import {select} from 'd3-selection';
4 import {tree, stratify} from 'd3-hierarchy';
10 return 'M' + d.y + ',' + d.x
11 + 'C' + (d.parent.y + offset) + ',' + d.x
12 + ' ' + (d.parent.y + offset) + ',' + d.parent.x
13 + ' ' + d.parent.y + ',' + d.parent.x;
17 const verticalSpaceBetweenNodes = 70;
18 const NARROW_HORIZONTAL_SPACES = 47;
19 const WIDE_HORIZONTAL_SPACES = 65;
21 const stratifyFn = stratify().id(d => d.id).parentId(d => d.parent);
23 class Tree extends Component {
26 // startingCoordinates: null,
31 name: PropTypes.string,
32 width: PropTypes.number,
33 allowScaleWidth: PropTypes.bool,
34 nodes: PropTypes.arrayOf(PropTypes.shape({
36 name: PropTypes.string,
37 parent: PropTypes.string
39 selectedNodeId: PropTypes.string,
40 onNodeClick: PropTypes.func,
41 onRenderedBeyondWidth: PropTypes.func
44 static defaultProps = {
46 allowScaleWidth : true,
51 let {width, name, scrollable = false} = this.props;
54 className={`tree-view ${name}-container ${scrollable ? 'scrollable' : ''}`}>
55 <svg width={width} className={name}></svg>
64 // handleMouseMove(e) {
65 // if (!this.state.isDown) {
68 // const container = select(`.tree-view.${this.props.name}-container`);
69 // let coordinates = this.getCoordinates(e);
70 // container.property('scrollLeft' , container.property('scrollLeft') + coordinates.x - this.state.startingCoordinates.x);
71 // container.property('scrollTop' , container.property('scrollTop') + coordinates.y - this.state.startingCoordinates.y);
74 // handleMouseDown(e) {
75 // let startingCoordinates = this.getCoordinates(e);
77 // startingCoordinates,
84 // startingCorrdinates: null,
89 // getCoordinates(e) {
90 // var bounds = e.target.getBoundingClientRect();
91 // var x = e.clientX - bounds.left;
92 // var y = e.clientY - bounds.top;
96 componentDidUpdate(prevProps) {
97 if (this.props.nodes.length !== prevProps.nodes.length ||
98 this.props.selectedNodeId !== prevProps.selectedNodeId) {
99 console.log('update');
105 let {width, nodes, name, allowScaleWidth, selectedNodeId, onRenderedBeyondWidth, toWiden} = this.props;
106 if (nodes.length > 0) {
108 let horizontalSpaceBetweenLeaves = toWiden ? WIDE_HORIZONTAL_SPACES : NARROW_HORIZONTAL_SPACES;
109 const treeFn = tree().nodeSize([horizontalSpaceBetweenLeaves, verticalSpaceBetweenNodes]);//.size([width - 50, height - 50])
110 let root = stratifyFn(nodes).sort((a, b) => a.data.name.localeCompare(b.data.name));
111 let svgHeight = verticalSpaceBetweenNodes * root.height + nodeRadius * 6;
115 let nodesXValue = root.descendants().map(node => node.x);
116 let maxX = Math.max(...nodesXValue);
117 let minX = Math.min(...nodesXValue);
119 let svgTempWidth = (maxX - minX) / 30 * (horizontalSpaceBetweenLeaves);
120 let svgWidth = svgTempWidth < width ? (width - 5) : svgTempWidth;
121 const svgEL = select(`svg.${name}`);
122 const container = select(`.tree-view.${name}-container`);
124 svgEL.attr('height', svgHeight);
125 let canvasWidth = width;
126 if (svgTempWidth > width) {
127 if (allowScaleWidth) {
128 canvasWidth = svgTempWidth;
130 // we seems to have a margin of 25px that we can still see with text
131 if (((svgTempWidth - 25) > width) && onRenderedBeyondWidth !== undefined) {
132 onRenderedBeyondWidth();
135 svgEL.attr('width', canvasWidth);
136 let rootGroup = svgEL.append('g').attr('transform', `translate(${svgWidth / 2 + nodeRadius},${nodeRadius * 4}) rotate(90)`);
139 rootGroup.selectAll('.link')
140 .data(root.descendants().slice(1))
141 .enter().append('path')
142 .attr('class', 'link')
143 .attr('d', diagonal);
145 let node = rootGroup.selectAll('.node')
146 .data(root.descendants())
148 .attr('class', node => `node ${node.children ? ' has-children' : ' leaf'} ${node.id === selectedNodeId ? 'selectedNode' : ''} ${this.props.onNodeClick ? 'clickable' : ''}`)
149 .attr('transform', node => 'translate(' + node.y + ',' + node.x + ')')
150 .on('click', node => this.onNodeClick(node));
152 node.append('circle').attr('r', nodeRadius).attr('class', 'outer-circle');
153 node.append('circle').attr('r', nodeRadius - 3).attr('class', 'inner-circle');
156 .attr('y', nodeRadius / 4 + 1)
157 .attr('x', - nodeRadius * 1.8)
158 .text(node => node.data.name)
159 .attr('transform', 'rotate(-90)');
161 let selectedNode = selectedNodeId ? root.descendants().find(node => node.id === selectedNodeId) : null;
164 container.property('scrollLeft', (svgWidth / 4) + (svgWidth / 4 - 100) - (selectedNode.x / 30 * horizontalSpaceBetweenLeaves));
165 container.property('scrollTop', (selectedNode.y / 100 * verticalSpaceBetweenNodes));
168 container.property('scrollLeft', (svgWidth / 4) + (svgWidth / 4 - 100));
174 if (this.props.onNodeClick) {
175 this.props.onNodeClick(node.data);