blob: 682f3b6d5090d15d68573bbbc865458929d67af4 [file] [log] [blame]
talig8e9c0652017-12-20 14:30:43 +02001import React, {Component} from 'react';
2import PropTypes from 'prop-types';
3import {select} from 'd3-selection';
4import {tree, stratify} from 'd3-hierarchy';
5
6
7function diagonal(d) {
8
9 const offset = 50;
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;
14}
15
16const nodeRadius = 8;
17const verticalSpaceBetweenNodes = 70;
18const NARROW_HORIZONTAL_SPACES = 47;
19const WIDE_HORIZONTAL_SPACES = 65;
20
21const stratifyFn = stratify().id(d => d.id).parentId(d => d.parent);
22
23class Tree extends Component {
24
25 // state = {
26 // startingCoordinates: null,
27 // isDown: false
28 // }
29
30 static propTypes = {
31 name: PropTypes.string,
32 width: PropTypes.number,
33 allowScaleWidth: PropTypes.bool,
34 nodes: PropTypes.arrayOf(PropTypes.shape({
35 id: PropTypes.string,
36 name: PropTypes.string,
37 parent: PropTypes.string
38 })),
39 selectedNodeId: PropTypes.string,
40 onNodeClick: PropTypes.func,
41 onRenderedBeyondWidth: PropTypes.func
42 };
43
44 static defaultProps = {
45 width: 500,
46 allowScaleWidth : true,
47 name: 'default-name'
48 };
49
50 render() {
51 let {width, name, scrollable = false} = this.props;
52 return (
53 <div
54 className={`tree-view ${name}-container ${scrollable ? 'scrollable' : ''}`}>
55 <svg width={width} className={name}></svg>
56 </div>
57 );
58 }
59
60 componentDidMount() {
61 this.renderTree();
62 }
63
64 // handleMouseMove(e) {
65 // if (!this.state.isDown) {
66 // return;
67 // }
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);
72 // }
73
74 // handleMouseDown(e) {
75 // let startingCoordinates = this.getCoordinates(e);
76 // this.setState({
77 // startingCoordinates,
78 // isDown: true
79 // });
80 // }
81
82 // handleMouseUp() {
83 // this.setState({
84 // startingCorrdinates: null,
85 // isDown: false
86 // });
87 // }
88
89 // getCoordinates(e) {
90 // var bounds = e.target.getBoundingClientRect();
91 // var x = e.clientX - bounds.left;
92 // var y = e.clientY - bounds.top;
93 // return {x, y};
94 // }
95
96 componentDidUpdate(prevProps) {
97 if (this.props.nodes.length !== prevProps.nodes.length ||
98 this.props.selectedNodeId !== prevProps.selectedNodeId) {
99 console.log('update');
100 this.renderTree();
101 }
102 }
103
104 renderTree() {
105 let {width, nodes, name, allowScaleWidth, selectedNodeId, onRenderedBeyondWidth, toWiden} = this.props;
106 if (nodes.length > 0) {
107
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;
112
113 treeFn(root);
114
115 let nodesXValue = root.descendants().map(node => node.x);
116 let maxX = Math.max(...nodesXValue);
117 let minX = Math.min(...nodesXValue);
118
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`);
123 svgEL.html('');
124 svgEL.attr('height', svgHeight);
125 let canvasWidth = width;
126 if (svgTempWidth > width) {
127 if (allowScaleWidth) {
128 canvasWidth = svgTempWidth;
129 }
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();
133 }
134 };
135 svgEL.attr('width', canvasWidth);
136 let rootGroup = svgEL.append('g').attr('transform', `translate(${svgWidth / 2 + nodeRadius},${nodeRadius * 4}) rotate(90)`);
137
138 // handle link
139 rootGroup.selectAll('.link')
140 .data(root.descendants().slice(1))
141 .enter().append('path')
142 .attr('class', 'link')
143 .attr('d', diagonal);
144
145 let node = rootGroup.selectAll('.node')
146 .data(root.descendants())
147 .enter().append('g')
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));
151
152 node.append('circle').attr('r', nodeRadius).attr('class', 'outer-circle');
153 node.append('circle').attr('r', nodeRadius - 3).attr('class', 'inner-circle');
154
155 node.append('text')
156 .attr('y', nodeRadius / 4 + 1)
157 .attr('x', - nodeRadius * 1.8)
158 .text(node => node.data.name)
159 .attr('transform', 'rotate(-90)');
160
161 let selectedNode = selectedNodeId ? root.descendants().find(node => node.id === selectedNodeId) : null;
162 if (selectedNode) {
163
164 container.property('scrollLeft', (svgWidth / 4) + (svgWidth / 4 - 100) - (selectedNode.x / 30 * horizontalSpaceBetweenLeaves));
165 container.property('scrollTop', (selectedNode.y / 100 * verticalSpaceBetweenNodes));
166
167 } else {
168 container.property('scrollLeft', (svgWidth / 4) + (svgWidth / 4 - 100));
169 }
170 }
171 }
172
173 onNodeClick(node) {
174 if (this.props.onNodeClick) {
175 this.props.onNodeClick(node.data);
176 }
177 }
178
179}
180
181export default Tree;