2 * Copyright © 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.
17 import React, { Component } from 'react';
18 import PropTypes from 'prop-types';
19 import { select } from 'd3-selection';
20 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 name: PropTypes.string,
55 width: PropTypes.number,
56 allowScaleWidth: PropTypes.bool,
57 nodes: PropTypes.arrayOf(
60 name: PropTypes.string,
61 parent: PropTypes.string
64 selectedNodeId: PropTypes.string,
65 onNodeClick: PropTypes.func,
66 onRenderedBeyondWidth: PropTypes.func,
67 scrollable: PropTypes.bool,
68 toWiden: PropTypes.bool
71 static defaultProps = {
73 allowScaleWidth: true,
78 let { width, name, scrollable = false } = this.props;
81 className={`tree-view ${name}-container ${
82 scrollable ? 'scrollable' : ''
84 <svg width={width} className={name} />
93 componentDidUpdate(prevProps) {
95 this.props.nodes.length !== prevProps.nodes.length ||
96 this.props.selectedNodeId !== prevProps.selectedNodeId
109 onRenderedBeyondWidth,
112 if (nodes.length > 0) {
113 let horizontalSpaceBetweenLeaves = toWiden
114 ? WIDE_HORIZONTAL_SPACES
115 : NARROW_HORIZONTAL_SPACES;
116 const treeFn = tree().nodeSize([
117 horizontalSpaceBetweenLeaves,
118 verticalSpaceBetweenNodes
119 ]); //.size([width - 50, height - 50])
120 let root = stratifyFn(nodes).sort((a, b) =>
121 a.data.name.localeCompare(b.data.name)
124 verticalSpaceBetweenNodes * root.height + nodeRadius * 6;
128 let nodesXValue = root.descendants().map(node => node.x);
129 let maxX = Math.max(...nodesXValue);
130 let minX = Math.min(...nodesXValue);
133 ((maxX - minX) / 30) * horizontalSpaceBetweenLeaves;
134 let svgWidth = svgTempWidth < width ? width - 5 : svgTempWidth;
135 const svgEL = select(`svg.${name}`);
136 const container = select(`.tree-view.${name}-container`);
138 svgEL.attr('height', svgHeight);
139 let canvasWidth = width;
140 if (svgTempWidth > width) {
141 if (allowScaleWidth) {
142 canvasWidth = svgTempWidth;
144 // we seems to have a margin of 25px that we can still see with text
146 svgTempWidth - 25 > width &&
147 onRenderedBeyondWidth !== undefined
149 onRenderedBeyondWidth();
152 svgEL.attr('width', canvasWidth);
153 let rootGroup = svgEL
157 `translate(${svgWidth / 2 + nodeRadius},${nodeRadius *
164 .data(root.descendants().slice(1))
167 .attr('class', 'link')
168 .attr('d', diagonal);
172 .data(root.descendants())
178 `node ${node.children ? ' has-children' : ' leaf'} ${
179 node.id === selectedNodeId ? 'selectedNode' : ''
180 } ${this.props.onNodeClick ? 'clickable' : ''}`
184 node => 'translate(' + node.y + ',' + node.x + ')'
186 .on('click', node => this.onNodeClick(node));
188 node.append('circle')
189 .attr('r', nodeRadius)
190 .attr('class', 'outer-circle');
191 node.append('circle')
192 .attr('r', nodeRadius - 3)
193 .attr('class', 'inner-circle');
196 .attr('y', nodeRadius / 4 + 1)
197 .attr('x', -nodeRadius * 1.8)
198 .text(node => node.data.name)
199 .attr('transform', 'rotate(-90)');
201 let selectedNode = selectedNodeId
202 ? root.descendants().find(node => node.id === selectedNodeId)
208 (svgWidth / 4 - 100) -
209 (selectedNode.x / 30) * horizontalSpaceBetweenLeaves
213 (selectedNode.y / 100) * verticalSpaceBetweenNodes
218 svgWidth / 4 + (svgWidth / 4 - 100)
225 if (this.props.onNodeClick) {
226 this.props.onNodeClick(node.data);