React專案中使用dagre-d3

我就什麼話也不說發表於2019-03-04

React 基本上已經成為公司所有專案的前端必選框架了, JavaScript社群非常活躍,各種知名的庫幾乎都已經有了React的版本。但是一些庫還沒有,比如dagre-d3dagre-d3是一個來繪製關係圖的Javascript庫。

參考了一下react-highcharts,其實在React中實現dagre-d3也還好,並沒有那麼困難。

  • 開始之前
npm i dagre-d3 -S
複製程式碼
  • 準備一下資料nodeedge,也就是點和邊
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 簡介

Refs and the DOM

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} />
複製程式碼
React專案中使用dagre-d3

這裡我們用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方法,放到componentDidMountcomponentDidUpdate裡面

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}
/>
複製程式碼
  • 效果圖
React專案中使用dagre-d3
  • 完整程式碼
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;
複製程式碼

相關文章