blob: 1bb05ff2f119d08bd56acf37e86617817e18af4e [file] [log] [blame]
svishnev3e9f4cc2018-10-21 10:59:00 +03001/*
2 * Copyright © 2016-2018 European Support Limited
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020016import React, { Component } from 'react';
talig8e9c0652017-12-20 14:30:43 +020017import PropTypes from 'prop-types';
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020018import { select } from 'd3-selection';
19import { tree, stratify } from 'd3-hierarchy';
talig8e9c0652017-12-20 14:30:43 +020020
21function diagonal(d) {
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020022 const offset = 50;
23 return (
24 'M' +
25 d.y +
26 ',' +
27 d.x +
28 'C' +
29 (d.parent.y + offset) +
30 ',' +
31 d.x +
32 ' ' +
33 (d.parent.y + offset) +
34 ',' +
35 d.parent.x +
36 ' ' +
37 d.parent.y +
38 ',' +
39 d.parent.x
40 );
talig8e9c0652017-12-20 14:30:43 +020041}
42
43const nodeRadius = 8;
44const verticalSpaceBetweenNodes = 70;
45const NARROW_HORIZONTAL_SPACES = 47;
46const WIDE_HORIZONTAL_SPACES = 65;
47
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020048const stratifyFn = stratify()
49 .id(d => d.id)
50 .parentId(d => d.parent);
talig8e9c0652017-12-20 14:30:43 +020051
52class Tree extends Component {
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020053 // state = {
54 // startingCoordinates: null,
55 // isDown: false
56 // }
talig8e9c0652017-12-20 14:30:43 +020057
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020058 static propTypes = {
59 name: PropTypes.string,
60 width: PropTypes.number,
61 allowScaleWidth: PropTypes.bool,
62 nodes: PropTypes.arrayOf(
63 PropTypes.shape({
64 id: PropTypes.string,
65 name: PropTypes.string,
66 parent: PropTypes.string
67 })
68 ),
69 selectedNodeId: PropTypes.string,
70 onNodeClick: PropTypes.func,
71 onRenderedBeyondWidth: PropTypes.func
72 };
talig8e9c0652017-12-20 14:30:43 +020073
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020074 static defaultProps = {
75 width: 500,
76 allowScaleWidth: true,
77 name: 'default-name'
78 };
talig8e9c0652017-12-20 14:30:43 +020079
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020080 render() {
81 let { width, name, scrollable = false } = this.props;
82 return (
83 <div
84 className={`tree-view ${name}-container ${
85 scrollable ? 'scrollable' : ''
86 }`}>
87 <svg width={width} className={name} />
88 </div>
89 );
90 }
talig8e9c0652017-12-20 14:30:43 +020091
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020092 componentDidMount() {
93 this.renderTree();
94 }
talig8e9c0652017-12-20 14:30:43 +020095
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +020096 // handleMouseMove(e) {
97 // if (!this.state.isDown) {
98 // return;
99 // }
100 // const container = select(`.tree-view.${this.props.name}-container`);
101 // let coordinates = this.getCoordinates(e);
102 // container.property('scrollLeft' , container.property('scrollLeft') + coordinates.x - this.state.startingCoordinates.x);
103 // container.property('scrollTop' , container.property('scrollTop') + coordinates.y - this.state.startingCoordinates.y);
104 // }
talig8e9c0652017-12-20 14:30:43 +0200105
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200106 // handleMouseDown(e) {
107 // let startingCoordinates = this.getCoordinates(e);
108 // this.setState({
109 // startingCoordinates,
110 // isDown: true
111 // });
112 // }
talig8e9c0652017-12-20 14:30:43 +0200113
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200114 // handleMouseUp() {
115 // this.setState({
116 // startingCorrdinates: null,
117 // isDown: false
118 // });
119 // }
talig8e9c0652017-12-20 14:30:43 +0200120
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200121 // getCoordinates(e) {
122 // var bounds = e.target.getBoundingClientRect();
123 // var x = e.clientX - bounds.left;
124 // var y = e.clientY - bounds.top;
125 // return {x, y};
126 // }
talig8e9c0652017-12-20 14:30:43 +0200127
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200128 componentDidUpdate(prevProps) {
129 if (
130 this.props.nodes.length !== prevProps.nodes.length ||
131 this.props.selectedNodeId !== prevProps.selectedNodeId
132 ) {
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200133 this.renderTree();
134 }
135 }
talig8e9c0652017-12-20 14:30:43 +0200136
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200137 renderTree() {
138 let {
139 width,
140 nodes,
141 name,
142 allowScaleWidth,
143 selectedNodeId,
144 onRenderedBeyondWidth,
145 toWiden
146 } = this.props;
147 if (nodes.length > 0) {
148 let horizontalSpaceBetweenLeaves = toWiden
149 ? WIDE_HORIZONTAL_SPACES
150 : NARROW_HORIZONTAL_SPACES;
151 const treeFn = tree().nodeSize([
152 horizontalSpaceBetweenLeaves,
153 verticalSpaceBetweenNodes
154 ]); //.size([width - 50, height - 50])
155 let root = stratifyFn(nodes).sort((a, b) =>
156 a.data.name.localeCompare(b.data.name)
157 );
158 let svgHeight =
159 verticalSpaceBetweenNodes * root.height + nodeRadius * 6;
talig8e9c0652017-12-20 14:30:43 +0200160
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200161 treeFn(root);
talig8e9c0652017-12-20 14:30:43 +0200162
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200163 let nodesXValue = root.descendants().map(node => node.x);
164 let maxX = Math.max(...nodesXValue);
165 let minX = Math.min(...nodesXValue);
talig8e9c0652017-12-20 14:30:43 +0200166
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200167 let svgTempWidth =
168 (maxX - minX) / 30 * horizontalSpaceBetweenLeaves;
169 let svgWidth = svgTempWidth < width ? width - 5 : svgTempWidth;
170 const svgEL = select(`svg.${name}`);
171 const container = select(`.tree-view.${name}-container`);
172 svgEL.html('');
173 svgEL.attr('height', svgHeight);
174 let canvasWidth = width;
175 if (svgTempWidth > width) {
176 if (allowScaleWidth) {
177 canvasWidth = svgTempWidth;
178 }
179 // we seems to have a margin of 25px that we can still see with text
180 if (
181 svgTempWidth - 25 > width &&
182 onRenderedBeyondWidth !== undefined
183 ) {
184 onRenderedBeyondWidth();
185 }
186 }
187 svgEL.attr('width', canvasWidth);
188 let rootGroup = svgEL
189 .append('g')
190 .attr(
191 'transform',
192 `translate(${svgWidth / 2 + nodeRadius},${nodeRadius *
193 4}) rotate(90)`
194 );
talig8e9c0652017-12-20 14:30:43 +0200195
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200196 // handle link
197 rootGroup
198 .selectAll('.link')
199 .data(root.descendants().slice(1))
200 .enter()
201 .append('path')
202 .attr('class', 'link')
203 .attr('d', diagonal);
talig8e9c0652017-12-20 14:30:43 +0200204
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200205 let node = rootGroup
206 .selectAll('.node')
207 .data(root.descendants())
208 .enter()
209 .append('g')
210 .attr(
211 'class',
212 node =>
213 `node ${node.children ? ' has-children' : ' leaf'} ${
214 node.id === selectedNodeId ? 'selectedNode' : ''
215 } ${this.props.onNodeClick ? 'clickable' : ''}`
216 )
217 .attr(
218 'transform',
219 node => 'translate(' + node.y + ',' + node.x + ')'
220 )
221 .on('click', node => this.onNodeClick(node));
talig8e9c0652017-12-20 14:30:43 +0200222
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200223 node
224 .append('circle')
225 .attr('r', nodeRadius)
226 .attr('class', 'outer-circle');
227 node
228 .append('circle')
229 .attr('r', nodeRadius - 3)
230 .attr('class', 'inner-circle');
talig8e9c0652017-12-20 14:30:43 +0200231
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200232 node
233 .append('text')
234 .attr('y', nodeRadius / 4 + 1)
235 .attr('x', -nodeRadius * 1.8)
236 .text(node => node.data.name)
237 .attr('transform', 'rotate(-90)');
talig8e9c0652017-12-20 14:30:43 +0200238
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200239 let selectedNode = selectedNodeId
240 ? root.descendants().find(node => node.id === selectedNodeId)
241 : null;
242 if (selectedNode) {
243 container.property(
244 'scrollLeft',
245 svgWidth / 4 +
246 (svgWidth / 4 - 100) -
247 selectedNode.x / 30 * horizontalSpaceBetweenLeaves
248 );
249 container.property(
250 'scrollTop',
251 selectedNode.y / 100 * verticalSpaceBetweenNodes
252 );
253 } else {
254 container.property(
255 'scrollLeft',
256 svgWidth / 4 + (svgWidth / 4 - 100)
257 );
258 }
259 }
260 }
talig8e9c0652017-12-20 14:30:43 +0200261
Einav Weiss Keidar7fdf7332018-03-20 14:45:40 +0200262 onNodeClick(node) {
263 if (this.props.onNodeClick) {
264 this.props.onNodeClick(node.data);
265 }
266 }
talig8e9c0652017-12-20 14:30:43 +0200267}
268
269export default Tree;