react和d3.js(v4)力導向圖force結合使用

前段時間因爲性能要求,需把項目d3的版本從v3升級到v4,據瞭解d3因爲在v4版本以前是沒有進行模塊化的,因此v3代碼的擴展性是比較差的,考慮到長遠之計,d3在v4版本算是對代碼進行了模塊化的重構吧,給開發者提供了一些可定製化的東西,全部api變化較大,這個坑還需各類研究文檔才能填完,好了,下面開始個人表演了。node

初始化force佈局

初始化函數從v3的d3.layout.force()變成v4的d3.forceSimulation(),部分參數設置方式以下:react

this.force = d3.forceSimulation().alphaDecay(0.1) // 設置alpha衰減係數
                .force("link", d3.forceLink().distance(100)) // distance爲連線的距離設置
                .force('collide', d3.forceCollide().radius(() => 30)) // collide 爲節點指定一個radius區域來防止節點重疊。
                .force("charge", d3.forceManyBody().strength(-400))  // 節點間的做用力

爲佈局添加點和線git

this.force.nodes(nodes)   // 節點數據
          .force('link', d3.forceLink(links).distance(150));  // 連線數據 distance爲連線的距離設置
          .alpha(1);  // 設置alpha值,讓裏導向圖有初始動力
          .restart();   // 啓動仿真計時器

因爲在v4版本中nodes的xy座標和加速度vxvy只在nodes中計算一次,全部在變成有節點或連線增長的時候,必須從新執行一次force.nodes(nodes)force('link', d3.forceLink(links)),初始化節點的數據結構。若是在v3版本中,只需在佈局初始化時執行便可,在d3會在每次force.start()方法執行時從新初始化一次節點和連線的數據結構,這是一個特別須要注意的地方,另外在v4版本中start方法被遺棄,需使用restart方法。github

react部分

將節點的dom結構交給react來控制,方便在節點上添加事件。如下爲svg渲染部分代碼。api

render() {
    const { width, height, nodes, links, scale, translate, selecting, grabbing } = this.props.store;
    return (
      <svg id="svg" ref="svg" width={width} height={height}
        className={cn({
          grab: !selecting && !grabbing,
          grabbing: !selecting && grabbing
        })}
        >
        <g id="outg" ref="outg" transform={`translate(${translate})scale(${scale})`}>
          <g ref="lines" className="lines">
            links.map(link => (
                <line
                  key={`${link.source.uid}_${link.target.uid}`}
                  ref={child => this.links[`${link.source.uid}_${link.target.uid}`] = child}
                  x1={link.source.x}
                  y1={link.source.y}
                  x2={link.target.x}
                  y2={link.target.y}/>
              ))
          </g>
          <g ref="nodes" className="nodes">
            {
              nodes.map(node => (
                <Node key={node.uid}
                  node={node}
                  store={this.props.store}
                  addRef={child => this.nodes[node.uid] = child}/>
              ))
            }
          </g>
        </g>
      </svg>
    );
  }

Node.js 節點
如下爲Node Component部分代碼數據結構

class Node extends Component {
  render() {
    const { node, addRef, store } = this.props;
    const { force } = store;
    return (
      <g className="node"
        ref={child => {
          this._node = child;
          addRef(child);
        }}
        transform={`translate(${node.x || width / 2},${node.y || height / 2})`}
        >
        <g id={node.nodeIndex}>
          // 節點圖片dom
        </g>
        {
          node.locked && (
            <Lock
              x={10}
              y={10}
              release={() => {   // 解鎖節點
                node.fixed = false;
                node.locked = false;
                node.fx = null;   // 當節點的fx、fy都爲null時,節點處於活動狀態
                node.fy = null;   
                force.alpha(0.3).restart();  // 釋放鎖定節點時需設置alpha值並重啓計時器,使得佈局能夠運動。
              }}
              />
          )
        }
      </g>
    );
  }

  componentDidMount() {
    this._node.__data__ = this.props.node;  // 將node節點在d3內部存一份引用,讓每次計時器更新的時候自動更改nodes列表中的數據
    d3.select(this._node)  // 各類事件
      .on('click', d => {
          // code
      })
  }
}

Lock.js 節點解除固定按鈕。dom

class Lock extends Component {
  render() {
    const { x, y, fixed } = this.props;
    return (
      <use
        ref="lock"
        xlinkHref="#lock"
        x={x}
        y={y}
        />
    );
  }

  componentDidMount() {
    const { release } = this.props;
    d3.select(this.refs.lock)
      .on('click', () => {
        d3.event.stopPropagation();
        release();
      });
  }
}

仿真計時器 tick

計時器函數,在仿真啓動的過程當中,計時器的每一幀都會改變一次以前咱們在內部存的引用(this._node.__data__ = this.props.node)的node的數據的x值和y值,這時咱們須要更新dom結構中的節點和線偏移量。ide

force.on('tick', () => {
  nodes.forEach(node => {
    if (!node.lock) {
      d3.select(self.nodes[node.uid]).attr('transform', () => `translate(${node.x},${node.y})`);
    }
  });
  links.forEach(link => {
    d3.select(self.links[`${link.source.uid}_${link.target.uid}`])
      .attr('x1', () => link.source.x)
      .attr('y1', () => link.source.y)
      .attr('x2', () => link.target.x)
      .attr('y2', () => link.target.y);
  });
});

在計時器的每一幀中,仿真的alpha係數會不斷削減,可經過force.alpha()來獲取和設置alpha係數,削減速度由alphaDecay來決定,默認值爲0.0228…,衰減係數可經過force.alphaDecay()來獲取和設置,當alpha到達一個係數時,仿真將會中止,也就是alpha的目標係數alphaTarget,該值區間爲[0,1]. 默認爲0,可經過force.alphaTarget()來獲取和設置,另外還有一個速度衰減系統velocityDecay ,至關於摩擦力。區間爲[0,1], 默認爲0.4。在每次tick以後,節點的速度都會等於當前速度乘以1-velocityDecay,和alpha衰減相似,速度衰減越慢最終的效果越好,可是若是速度衰減過慢,可能會致使震盪。以上爲tick過程的發生。須要注意的是,在v4版本中,tick事件的callback中不帶任何參數,在v3版本的'tick'事件中,咱們可經過callback(e)中的e.alpha來獲取alpha值,而在v4版本中,alpha值只能經過force.alpha()來獲取。svg

拖拽 Drag

建立拖拽操做模塊化

let startTime = 0;
this.drag = d3.drag()
      .on('start', (d) => {
        startTime = (new Date()).getTime();
        d3.event.sourceEvent.stopPropagation();
        if (!d3.event.active) {
           this.force.alphaTarget(0.3).restart();  // 當前alpha值爲0,需設置alphaTarget讓節點動起來
        }
        d.fx = d.x;
        d.fy = d.y;
      })
      .on('drag', d => {
        this.grabbing = true;
        d.fx = d3.event.x;
        d.fy = d3.event.y;
      })
      .on('end', d => {
        const nowTime = (new Date()).getTime();
        if (!d3.event.active) {
           this.force.alphaTarget(0);  // 讓alpha目標值值恢復爲默認值0
        }
        if (nowTime - startTime >= 150) {  // 操做150毫秒的拖拽固定節點
          d.fixed = true;
          d.locked = true;
        }
        this.grabbing = false;
      });

將拖拽操做應用到指定的選擇集。

d3.select('#outg').selectAll('.node').call(this.drag);

在內部,拖拽操做經過selection.on來爲元素添加監聽事件. 事件監聽器使用 .drag 來標識這是一個拖拽事件。拖拽drag的v4版本與v3不一樣的是,v3經過force.drag()建立拖拽操做,拖拽過程事件使用dragstartdragdragend,在拖拽過程當中d3內部自動設置alpha相關係數讓節點運動起來,而在v4中版本中須要手動設置。

縮放 Zoom

在v4版本中,縮放操做經過transform對象進行,能夠經過d3.zoomTransform(selection.node())獲取指定節點的縮放狀態,也能夠經過d3.event.transform來獲取當前正在縮放的節點的縮放狀態。
與拖拽相似,須要先建立縮放操做。

const self = this;
const outg = d3.select('#outg');
this.zoomObj = d3.zoom()
      .scaleExtent([0.2, 4]) // 縮放範圍
      .on('zoom',() => {
        const transform = d3.event.transform;
        self.scale = transform.k;  // 保存當前縮放大小
        self.translate = [transform.x, transform.y];  // 保存當前便宜量
        outg.attr('transform', transform);   // 設置縮放和偏移量 transform對象自帶toString()方法
      })
      .on('end', () => {
        // code
      })

將縮放操做應用於選擇集,並取消雙擊操做

const svg = d3.select('#svg');
svg.call(this.zoomObj).on('dblclick.zoom', null);

若是要禁止滾輪滾動縮放,能夠在講zoom事件應用於選擇集以後移除zoom事件中的滾輪事件:

svg.call(this.zoomObj).on("wheel.zoom", null);

當縮放事件被調用,d3.event會被設置爲當前的zoom事件,zoom event對象由如下幾部分組成:

  • target - 當前的縮放zoom behavior。
  • type - 事件類型:「start」, 「zoom」 或者 「end」,參考 zoom.on。
  • transform - 當前的zoom transform(縮放變換)。
  • sourceEvent - 原始事件, 好比 mousemove 或 touchmove。

經過按鈕縮放、定位視圖。

this.zoomObj.transform(d3.select('#svg'), d3.zoomIdentity.translate(newX,newY).scale(newScale))

在v3版本中,能夠經過zoom.scale(s)zoom.translate(x, y)設置縮放和偏移量後經過使用'zoom.event(selection)'方法應用到指定選擇節點,而在v4中版本須要經過d3.zoomIdentity建立新transform對象,並經過translate(x, y)scale(s)方法設置偏移量和縮放級別,而後將該transform應用到選擇集中。另外也能夠經過zoom.translateBy(selection, x, y)zoom.translateTo(selection, x, y)zoom.scaleBy(selection, k)zoom.scaleTo(selection, k)方法進行變換。

小結

因爲api變更較大,v3升級v4須要耐心看api,查看各個部分的變化,因此,升級需謹慎。最後附上d3.js v4.0中文api


閒適有空一把以上內容封裝成組件,詳情請見https://github.com/yacan8/d3-react-force

相關文章
相關標籤/搜索