React 基本上已經成為公司所有專案的前端必選框架了,
JavaScript
社群非常活躍,各種知名的庫幾乎都已經有了React
的版本。但是一些庫還沒有,比如dagre-d3
。dagre-d3
是一個來繪製關係圖的Javascript
庫。
參考了一下react-highcharts
,其實在React
中實現dagre-d3
也還好,並沒有那麼困難。
- 開始之前
npm i dagre-d3 -S
複製程式碼
- 準備一下資料
node
和edge
,也就是點和邊
const nodes = {
0: { label: 'ReactComponent', style: 'fill: rgb(80, 194, 138);' },
1: { label: 'props', style: 'fill: rgb(204, 230, 255);' },
2: { label: 'context', style: 'fill: rgb(204, 230, 255);' },
3: { label: 'refs', style: 'fill: rgb(204, 230, 255);' }
};
const edges = [
[0, 1, { style: 'stroke: rgb(214, 214, 214); fill: none', curve: d3.curveBasis }],
[0, 2, { style: 'stroke: rgb(214, 214, 214); fill: none', curve: d3.curveBasis }],
[0, 3, { style: 'stroke: rgb(214, 214, 214); fill: none', curve: d3.curveBasis }]
];
複製程式碼
- 獲取DOM node
這一步很重要,我們圖當然是要繪製在DOM上了
ref
簡介
React提供的ref
屬性,表示為對元件真正例項的引用,其實就是ReactDOM.render()返回的元件例項。
有一點要注意一下,ReactDOM.render()
渲染元件時返回的是元件例項;而渲染dom元素時,返回是具體的dom節點。
getDOMRef(ref) {
console.log(ref);
}
getComponentRef(ref) {
console.log(ref);
}
<svg ref={this.getDOMRef} />
<DagreD3 ref={this.getComponentRef} />
複製程式碼
這裡我們用nodeTree和nodeTreeInner來儲存具體的dom節點。
setNodeTree = (nodeTree) => {
this.nodeTree = nodeTree;
}
setNodeTreeInner = (nodeTreeInner) => {
this.nodeTreeInner = nodeTreeInner;
}
render() {
return (
<svg ref={this.setNodeTree}>
<g ref={this.setNodeTreeInner} />
</svg>
)
}
複製程式碼
- 渲染到DOM上(開始畫圖)
由於要獲取真正的DOM
節點,所以需要將上面程式碼的執行放到componentDidMount
裡。在props
我們定義一個renderDag
方法,放到componentDidMount
和componentDidUpdate
裡面
renderDag() {
const { nodes, edges } = this.props;
const g = new dagreD3.graphlib.Graph()
.setGraph({}) // Set an object for the graph label
.setDefaultNodeLabel(() => ({}))
.setDefaultEdgeLabel(() => ({})); // Default to assigning a new object as a label for each new edge.
Object.keys(nodes).forEach((id) => { // 畫點
g.setNode(id, nodes[id]);
});
edges.forEach((edge) => { // 畫線
edge[2] ? g.setEdge(edge[0], edge[1], edge[2]) : g.setEdge(edge[0], edge[1]);
});
// 渲染dag圖
const svg = d3.select(this.nodeTree);
const inner = d3.select(this.nodeTreeInner);
// 滑鼠滾動縮放
const zoom = d3.zoom().on('zoom', () => inner.attr('transform', d3.event.transform));
svg.call(zoom);
const render = new dagreD3.render(); // eslint-disable-line
render(inner, g);
}
複製程式碼
- 使用
import DagreD3, { d3 } from './react-dagre-d3';
const nodes = {
0: { label: 'ReactComponent', style: 'fill: rgb(80, 194, 138);' },
1: { label: 'props', style: 'fill: rgb(204, 230, 255);' },
2: { label: 'context', style: 'fill: rgb(204, 230, 255);' },
3: { label: 'refs', style: 'fill: rgb(204, 230, 255);' }
};
const edges = [
[0, 1, { style: 'stroke: rgb(214, 214, 214); fill: none', curve: d3.curveBasis }],
[0, 2, { style: 'stroke: rgb(214, 214, 214); fill: none', curve: d3.curveBasis }],
[0, 3, { style: 'stroke: rgb(214, 214, 214); fill: none', curve: d3.curveBasis }]
];
<DagreD3
fit
interactive
graph={{ rankdir: 'LR' }}
nodes={nodes}
edges={edges}
onNodeClick={this.onNodeClick}
/>
複製程式碼
- 效果圖
- 完整程式碼
import React from 'react';
import PropTypes from 'prop-types';
import * as dagreD3 from 'dagre-d3';
import * as d3 from 'd3';
class DagreD3 extends React.Component {
static defaultProps = {
width: '100%',
height: '100%',
nodes: {},
edges: [],
graph: {},
interactive: false,
onNodeClick: () => {}
}
static propTypes = {
width: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]),
height: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]),
nodes: PropTypes.object,
edges: PropTypes.array,
graph: PropTypes.object,
interactive: PropTypes.bool,
onNodeClick: PropTypes.func,
onNodeHover: PropTypes.func
}
componentDidMount() {
this.renderDag();
}
shouldComponentUpdate(nextProps) {
return !(this.props.nodes === nextProps.nodes)
|| !(this.props.edges === nextProps.edges);
}
componentDidUpdate() {
this.renderDag();
}
setNodeTree = (nodeTree) => {
this.nodeTree = nodeTree;
}
setNodeTreeInner = (nodeTreeInner) => {
this.nodeTreeInner = nodeTreeInner;
}
renderDag() {
const { nodes, edges, interactive, fit, onNodeClick, graph } = this.props;
const g = new dagreD3.graphlib.Graph()
.setGraph({ ...graph }) // Set an object for the graph label
.setDefaultNodeLabel(() => ({}))
.setDefaultEdgeLabel(() => ({})); // Default to assigning a new object as a label for each new edge.
Object.keys(nodes).forEach((id) => {
g.setNode(id, nodes[id]);
});
edges.forEach((edge) => {
edge[2] ? g.setEdge(edge[0], edge[1], edge[2]) : g.setEdge(edge[0], edge[1]);
});
const svg = d3.select(this.nodeTree);
const inner = d3.select(this.nodeTreeInner);
if (interactive) { // 自適應縮放
const zoom = d3.zoom().on('zoom', () => inner.attr('transform', d3.event.transform));
svg.call(zoom);
}
const render = new dagreD3.render(); // eslint-disable-line
render(inner, g);
render(inner, g);
// 自適應寬高
if (fit) {
const { height: gHeight, width: gWidth } = g.graph();
const { height, width } = this.nodeTree.getBBox();
const transX = width - gWidth;
const transY = height - gHeight;
svg.attr('viewBox', `0 0 ${width} ${height}`);
inner.attr('transform', d3.zoomIdentity.translate(transX, transY));
}
if (onNodeClick) { // 點選事件
svg.selectAll('g.node').on('click',
id => onNodeClick(id));
}
}
render() {
const { width, height } = this.props;
return (
<svg width={width} height={height} ref={this.setNodeTree}>
<g ref={this.setNodeTreeInner} />
</svg>
);
}
}
export { d3 };
export default DagreD3;
複製程式碼