blob: 682f3b6d5090d15d68573bbbc865458929d67af4 [file] [log] [blame]
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {select} from 'd3-selection';
import {tree, stratify} from 'd3-hierarchy';
function diagonal(d) {
const offset = 50;
return 'M' + d.y + ',' + d.x
+ 'C' + (d.parent.y + offset) + ',' + d.x
+ ' ' + (d.parent.y + offset) + ',' + d.parent.x
+ ' ' + d.parent.y + ',' + d.parent.x;
}
const nodeRadius = 8;
const verticalSpaceBetweenNodes = 70;
const NARROW_HORIZONTAL_SPACES = 47;
const WIDE_HORIZONTAL_SPACES = 65;
const stratifyFn = stratify().id(d => d.id).parentId(d => d.parent);
class Tree extends Component {
// state = {
// startingCoordinates: null,
// isDown: false
// }
static propTypes = {
name: PropTypes.string,
width: PropTypes.number,
allowScaleWidth: PropTypes.bool,
nodes: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
name: PropTypes.string,
parent: PropTypes.string
})),
selectedNodeId: PropTypes.string,
onNodeClick: PropTypes.func,
onRenderedBeyondWidth: PropTypes.func
};
static defaultProps = {
width: 500,
allowScaleWidth : true,
name: 'default-name'
};
render() {
let {width, name, scrollable = false} = this.props;
return (
<div
className={`tree-view ${name}-container ${scrollable ? 'scrollable' : ''}`}>
<svg width={width} className={name}></svg>
</div>
);
}
componentDidMount() {
this.renderTree();
}
// handleMouseMove(e) {
// if (!this.state.isDown) {
// return;
// }
// const container = select(`.tree-view.${this.props.name}-container`);
// let coordinates = this.getCoordinates(e);
// container.property('scrollLeft' , container.property('scrollLeft') + coordinates.x - this.state.startingCoordinates.x);
// container.property('scrollTop' , container.property('scrollTop') + coordinates.y - this.state.startingCoordinates.y);
// }
// handleMouseDown(e) {
// let startingCoordinates = this.getCoordinates(e);
// this.setState({
// startingCoordinates,
// isDown: true
// });
// }
// handleMouseUp() {
// this.setState({
// startingCorrdinates: null,
// isDown: false
// });
// }
// getCoordinates(e) {
// var bounds = e.target.getBoundingClientRect();
// var x = e.clientX - bounds.left;
// var y = e.clientY - bounds.top;
// return {x, y};
// }
componentDidUpdate(prevProps) {
if (this.props.nodes.length !== prevProps.nodes.length ||
this.props.selectedNodeId !== prevProps.selectedNodeId) {
console.log('update');
this.renderTree();
}
}
renderTree() {
let {width, nodes, name, allowScaleWidth, selectedNodeId, onRenderedBeyondWidth, toWiden} = this.props;
if (nodes.length > 0) {
let horizontalSpaceBetweenLeaves = toWiden ? WIDE_HORIZONTAL_SPACES : NARROW_HORIZONTAL_SPACES;
const treeFn = tree().nodeSize([horizontalSpaceBetweenLeaves, verticalSpaceBetweenNodes]);//.size([width - 50, height - 50])
let root = stratifyFn(nodes).sort((a, b) => a.data.name.localeCompare(b.data.name));
let svgHeight = verticalSpaceBetweenNodes * root.height + nodeRadius * 6;
treeFn(root);
let nodesXValue = root.descendants().map(node => node.x);
let maxX = Math.max(...nodesXValue);
let minX = Math.min(...nodesXValue);
let svgTempWidth = (maxX - minX) / 30 * (horizontalSpaceBetweenLeaves);
let svgWidth = svgTempWidth < width ? (width - 5) : svgTempWidth;
const svgEL = select(`svg.${name}`);
const container = select(`.tree-view.${name}-container`);
svgEL.html('');
svgEL.attr('height', svgHeight);
let canvasWidth = width;
if (svgTempWidth > width) {
if (allowScaleWidth) {
canvasWidth = svgTempWidth;
}
// we seems to have a margin of 25px that we can still see with text
if (((svgTempWidth - 25) > width) && onRenderedBeyondWidth !== undefined) {
onRenderedBeyondWidth();
}
};
svgEL.attr('width', canvasWidth);
let rootGroup = svgEL.append('g').attr('transform', `translate(${svgWidth / 2 + nodeRadius},${nodeRadius * 4}) rotate(90)`);
// handle link
rootGroup.selectAll('.link')
.data(root.descendants().slice(1))
.enter().append('path')
.attr('class', 'link')
.attr('d', diagonal);
let node = rootGroup.selectAll('.node')
.data(root.descendants())
.enter().append('g')
.attr('class', node => `node ${node.children ? ' has-children' : ' leaf'} ${node.id === selectedNodeId ? 'selectedNode' : ''} ${this.props.onNodeClick ? 'clickable' : ''}`)
.attr('transform', node => 'translate(' + node.y + ',' + node.x + ')')
.on('click', node => this.onNodeClick(node));
node.append('circle').attr('r', nodeRadius).attr('class', 'outer-circle');
node.append('circle').attr('r', nodeRadius - 3).attr('class', 'inner-circle');
node.append('text')
.attr('y', nodeRadius / 4 + 1)
.attr('x', - nodeRadius * 1.8)
.text(node => node.data.name)
.attr('transform', 'rotate(-90)');
let selectedNode = selectedNodeId ? root.descendants().find(node => node.id === selectedNodeId) : null;
if (selectedNode) {
container.property('scrollLeft', (svgWidth / 4) + (svgWidth / 4 - 100) - (selectedNode.x / 30 * horizontalSpaceBetweenLeaves));
container.property('scrollTop', (selectedNode.y / 100 * verticalSpaceBetweenNodes));
} else {
container.property('scrollLeft', (svgWidth / 4) + (svgWidth / 4 - 100));
}
}
}
onNodeClick(node) {
if (this.props.onNodeClick) {
this.props.onNodeClick(node.data);
}
}
}
export default Tree;