2 * Copyright © 2016-2018 European Support Limited
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 import React, { Component } from 'react';
17 import PropTypes from 'prop-types';
18 import { select } from 'd3-selection';
19 import { tree, stratify } from 'd3-hierarchy';
21 function diagonal(d) {
29 (d.parent.y + offset) +
33 (d.parent.y + offset) +
44 const verticalSpaceBetweenNodes = 70;
45 const NARROW_HORIZONTAL_SPACES = 47;
46 const WIDE_HORIZONTAL_SPACES = 65;
48 const stratifyFn = stratify()
50 .parentId(d => d.parent);
52 class Tree extends Component {
54 // startingCoordinates: null,
59 name: PropTypes.string,
60 width: PropTypes.number,
61 allowScaleWidth: PropTypes.bool,
62 nodes: PropTypes.arrayOf(
65 name: PropTypes.string,
66 parent: PropTypes.string
69 selectedNodeId: PropTypes.string,
70 onNodeClick: PropTypes.func,
71 onRenderedBeyondWidth: PropTypes.func
74 static defaultProps = {
76 allowScaleWidth: true,
81 let { width, name, scrollable = false } = this.props;
84 className={`tree-view ${name}-container ${
85 scrollable ? 'scrollable' : ''
87 <svg width={width} className={name} />
96 // handleMouseMove(e) {
97 // if (!this.state.isDown) {
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);
106 // handleMouseDown(e) {
107 // let startingCoordinates = this.getCoordinates(e);
109 // startingCoordinates,
116 // startingCorrdinates: null,
121 // getCoordinates(e) {
122 // var bounds = e.target.getBoundingClientRect();
123 // var x = e.clientX - bounds.left;
124 // var y = e.clientY - bounds.top;
128 componentDidUpdate(prevProps) {
130 this.props.nodes.length !== prevProps.nodes.length ||
131 this.props.selectedNodeId !== prevProps.selectedNodeId
144 onRenderedBeyondWidth,
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)
159 verticalSpaceBetweenNodes * root.height + nodeRadius * 6;
163 let nodesXValue = root.descendants().map(node => node.x);
164 let maxX = Math.max(...nodesXValue);
165 let minX = Math.min(...nodesXValue);
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`);
173 svgEL.attr('height', svgHeight);
174 let canvasWidth = width;
175 if (svgTempWidth > width) {
176 if (allowScaleWidth) {
177 canvasWidth = svgTempWidth;
179 // we seems to have a margin of 25px that we can still see with text
181 svgTempWidth - 25 > width &&
182 onRenderedBeyondWidth !== undefined
184 onRenderedBeyondWidth();
187 svgEL.attr('width', canvasWidth);
188 let rootGroup = svgEL
192 `translate(${svgWidth / 2 + nodeRadius},${nodeRadius *
199 .data(root.descendants().slice(1))
202 .attr('class', 'link')
203 .attr('d', diagonal);
207 .data(root.descendants())
213 `node ${node.children ? ' has-children' : ' leaf'} ${
214 node.id === selectedNodeId ? 'selectedNode' : ''
215 } ${this.props.onNodeClick ? 'clickable' : ''}`
219 node => 'translate(' + node.y + ',' + node.x + ')'
221 .on('click', node => this.onNodeClick(node));
225 .attr('r', nodeRadius)
226 .attr('class', 'outer-circle');
229 .attr('r', nodeRadius - 3)
230 .attr('class', 'inner-circle');
234 .attr('y', nodeRadius / 4 + 1)
235 .attr('x', -nodeRadius * 1.8)
236 .text(node => node.data.name)
237 .attr('transform', 'rotate(-90)');
239 let selectedNode = selectedNodeId
240 ? root.descendants().find(node => node.id === selectedNodeId)
246 (svgWidth / 4 - 100) -
247 selectedNode.x / 30 * horizontalSpaceBetweenLeaves
251 selectedNode.y / 100 * verticalSpaceBetweenNodes
256 svgWidth / 4 + (svgWidth / 4 - 100)
263 if (this.props.onNodeClick) {
264 this.props.onNodeClick(node.data);