blob: 39434fcdf15e27fdd7c296258cbacb298b3e574d [file] [log] [blame]
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +02001import React, { Component } from 'react';
talig8e9c0652017-12-20 14:30:43 +02002import PropTypes from 'prop-types';
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +02003import { select } from 'd3-selection';
4import { tree, stratify } from 'd3-hierarchy';
talig8e9c0652017-12-20 14:30:43 +02005
6function diagonal(d) {
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +02007 const offset = 50;
8 return (
9 'M' +
10 d.y +
11 ',' +
12 d.x +
13 'C' +
14 (d.parent.y + offset) +
15 ',' +
16 d.x +
17 ' ' +
18 (d.parent.y + offset) +
19 ',' +
20 d.parent.x +
21 ' ' +
22 d.parent.y +
23 ',' +
24 d.parent.x
25 );
talig8e9c0652017-12-20 14:30:43 +020026}
27
28const nodeRadius = 8;
29const verticalSpaceBetweenNodes = 70;
30const NARROW_HORIZONTAL_SPACES = 47;
31const WIDE_HORIZONTAL_SPACES = 65;
32
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020033const stratifyFn = stratify()
34 .id(d => d.id)
35 .parentId(d => d.parent);
talig8e9c0652017-12-20 14:30:43 +020036
37class Tree extends Component {
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020038 // state = {
39 // startingCoordinates: null,
40 // isDown: false
41 // }
talig8e9c0652017-12-20 14:30:43 +020042
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020043 static propTypes = {
44 name: PropTypes.string,
45 width: PropTypes.number,
46 allowScaleWidth: PropTypes.bool,
47 nodes: PropTypes.arrayOf(
48 PropTypes.shape({
49 id: PropTypes.string,
50 name: PropTypes.string,
51 parent: PropTypes.string
52 })
53 ),
54 selectedNodeId: PropTypes.string,
55 onNodeClick: PropTypes.func,
56 onRenderedBeyondWidth: PropTypes.func
57 };
talig8e9c0652017-12-20 14:30:43 +020058
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020059 static defaultProps = {
60 width: 500,
61 allowScaleWidth: true,
62 name: 'default-name'
63 };
talig8e9c0652017-12-20 14:30:43 +020064
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020065 render() {
66 let { width, name, scrollable = false } = this.props;
67 return (
68 <div
69 className={`tree-view ${name}-container ${
70 scrollable ? 'scrollable' : ''
71 }`}>
72 <svg width={width} className={name} />
73 </div>
74 );
75 }
talig8e9c0652017-12-20 14:30:43 +020076
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020077 componentDidMount() {
78 this.renderTree();
79 }
talig8e9c0652017-12-20 14:30:43 +020080
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020081 // handleMouseMove(e) {
82 // if (!this.state.isDown) {
83 // return;
84 // }
85 // const container = select(`.tree-view.${this.props.name}-container`);
86 // let coordinates = this.getCoordinates(e);
87 // container.property('scrollLeft' , container.property('scrollLeft') + coordinates.x - this.state.startingCoordinates.x);
88 // container.property('scrollTop' , container.property('scrollTop') + coordinates.y - this.state.startingCoordinates.y);
89 // }
talig8e9c0652017-12-20 14:30:43 +020090
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020091 // handleMouseDown(e) {
92 // let startingCoordinates = this.getCoordinates(e);
93 // this.setState({
94 // startingCoordinates,
95 // isDown: true
96 // });
97 // }
talig8e9c0652017-12-20 14:30:43 +020098
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020099 // handleMouseUp() {
100 // this.setState({
101 // startingCorrdinates: null,
102 // isDown: false
103 // });
104 // }
talig8e9c0652017-12-20 14:30:43 +0200105
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200106 // getCoordinates(e) {
107 // var bounds = e.target.getBoundingClientRect();
108 // var x = e.clientX - bounds.left;
109 // var y = e.clientY - bounds.top;
110 // return {x, y};
111 // }
talig8e9c0652017-12-20 14:30:43 +0200112
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200113 componentDidUpdate(prevProps) {
114 if (
115 this.props.nodes.length !== prevProps.nodes.length ||
116 this.props.selectedNodeId !== prevProps.selectedNodeId
117 ) {
118 console.log('update');
119 this.renderTree();
120 }
121 }
talig8e9c0652017-12-20 14:30:43 +0200122
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200123 renderTree() {
124 let {
125 width,
126 nodes,
127 name,
128 allowScaleWidth,
129 selectedNodeId,
130 onRenderedBeyondWidth,
131 toWiden
132 } = this.props;
133 if (nodes.length > 0) {
134 let horizontalSpaceBetweenLeaves = toWiden
135 ? WIDE_HORIZONTAL_SPACES
136 : NARROW_HORIZONTAL_SPACES;
137 const treeFn = tree().nodeSize([
138 horizontalSpaceBetweenLeaves,
139 verticalSpaceBetweenNodes
140 ]); //.size([width - 50, height - 50])
141 let root = stratifyFn(nodes).sort((a, b) =>
142 a.data.name.localeCompare(b.data.name)
143 );
144 let svgHeight =
145 verticalSpaceBetweenNodes * root.height + nodeRadius * 6;
talig8e9c0652017-12-20 14:30:43 +0200146
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200147 treeFn(root);
talig8e9c0652017-12-20 14:30:43 +0200148
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200149 let nodesXValue = root.descendants().map(node => node.x);
150 let maxX = Math.max(...nodesXValue);
151 let minX = Math.min(...nodesXValue);
talig8e9c0652017-12-20 14:30:43 +0200152
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200153 let svgTempWidth =
154 (maxX - minX) / 30 * horizontalSpaceBetweenLeaves;
155 let svgWidth = svgTempWidth < width ? width - 5 : svgTempWidth;
156 const svgEL = select(`svg.${name}`);
157 const container = select(`.tree-view.${name}-container`);
158 svgEL.html('');
159 svgEL.attr('height', svgHeight);
160 let canvasWidth = width;
161 if (svgTempWidth > width) {
162 if (allowScaleWidth) {
163 canvasWidth = svgTempWidth;
164 }
165 // we seems to have a margin of 25px that we can still see with text
166 if (
167 svgTempWidth - 25 > width &&
168 onRenderedBeyondWidth !== undefined
169 ) {
170 onRenderedBeyondWidth();
171 }
172 }
173 svgEL.attr('width', canvasWidth);
174 let rootGroup = svgEL
175 .append('g')
176 .attr(
177 'transform',
178 `translate(${svgWidth / 2 + nodeRadius},${nodeRadius *
179 4}) rotate(90)`
180 );
talig8e9c0652017-12-20 14:30:43 +0200181
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200182 // handle link
183 rootGroup
184 .selectAll('.link')
185 .data(root.descendants().slice(1))
186 .enter()
187 .append('path')
188 .attr('class', 'link')
189 .attr('d', diagonal);
talig8e9c0652017-12-20 14:30:43 +0200190
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200191 let node = rootGroup
192 .selectAll('.node')
193 .data(root.descendants())
194 .enter()
195 .append('g')
196 .attr(
197 'class',
198 node =>
199 `node ${node.children ? ' has-children' : ' leaf'} ${
200 node.id === selectedNodeId ? 'selectedNode' : ''
201 } ${this.props.onNodeClick ? 'clickable' : ''}`
202 )
203 .attr(
204 'transform',
205 node => 'translate(' + node.y + ',' + node.x + ')'
206 )
207 .on('click', node => this.onNodeClick(node));
talig8e9c0652017-12-20 14:30:43 +0200208
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200209 node
210 .append('circle')
211 .attr('r', nodeRadius)
212 .attr('class', 'outer-circle');
213 node
214 .append('circle')
215 .attr('r', nodeRadius - 3)
216 .attr('class', 'inner-circle');
talig8e9c0652017-12-20 14:30:43 +0200217
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200218 node
219 .append('text')
220 .attr('y', nodeRadius / 4 + 1)
221 .attr('x', -nodeRadius * 1.8)
222 .text(node => node.data.name)
223 .attr('transform', 'rotate(-90)');
talig8e9c0652017-12-20 14:30:43 +0200224
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200225 let selectedNode = selectedNodeId
226 ? root.descendants().find(node => node.id === selectedNodeId)
227 : null;
228 if (selectedNode) {
229 container.property(
230 'scrollLeft',
231 svgWidth / 4 +
232 (svgWidth / 4 - 100) -
233 selectedNode.x / 30 * horizontalSpaceBetweenLeaves
234 );
235 container.property(
236 'scrollTop',
237 selectedNode.y / 100 * verticalSpaceBetweenNodes
238 );
239 } else {
240 container.property(
241 'scrollLeft',
242 svgWidth / 4 + (svgWidth / 4 - 100)
243 );
244 }
245 }
246 }
talig8e9c0652017-12-20 14:30:43 +0200247
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200248 onNodeClick(node) {
249 if (this.props.onNodeClick) {
250 this.props.onNodeClick(node.data);
251 }
252 }
talig8e9c0652017-12-20 14:30:43 +0200253}
254
255export default Tree;