React與D3的結合

前言

前段時間公司有個需求要開發一個數據關係的界面,相似UML建模工具裏面表之間關係的圖形界面,目前用的前端框架是React、rxjs,圖形界面這塊定下來採用的是D3的最新版本V7,因此如今須要基於React框架下開發這個界面,前期查了一些相關資料,國內基於React、D3 V7版本結合開發的比較少,差很少都是V三、V4版本,V4版本國內還有中文翻譯V4以後就停了,因此結合我的在當前的需求背景下以及使用過程當中的碰到的一些問題記錄下來,一方面供有須要人的能夠借鑑下,一方面也是給本身作個總結。javascript

用的D3版本v7.0.0,須要開發的功能:css

1.拖拽、縮放功能html

2.連線並帶有箭頭,線條有文字前端

3.能添加節點、刪除結點java

4.添加節點需計算位置,儘可能保證不重疊node

5.節點與節點之間須要通訊更新數據react

6.節點不一樣層級展現的背景顏色不一致git

7.節點可摺疊、展開github

代碼結構

import * as d3 from 'd3';
import * as React from 'react';
import ReactDOM from 'react-dom';
import './index.scss';

// 節點高
const nodeHalfHeight = 300 / 2;
// 節點寬度
const nodeWidth = 240;
// 摺疊以後的高度
const foldHeight = 85 / 2;
// 未選擇表數據標識
const NO_DATA = 'NO_DATA';
// 獲取隨機ID
const getRandomId = () => Math.random().toString(32).slice(2);


// 記錄當前操做摺疊的nodeId
let nodeIds: Array<any> = [];

const D3DataModel = (props: any): React.ReactElement => {
  const refs = React.useRef(null);
  // 表數據
  const [d3NodeData, setD3NodeData] = React.useState(() => {
    // nodeId 用來構建連線以及生成表格區域的ID
    // level 用來根據層級繪畫表格背景色
    // data_type 用來區分是否渲染無數據背景圖片
    return [{ x: 10, y: 10, data_type: NO_DATA, nodeId: getRandomId(), level: 1 }];
  });
  // d3縮放範圍
  const [transformInfo, setTransformInfo] = React.useState<any>(null);

  React.useEffect(() => {
    drawModel();
  }, [d3NodeData.length]);

  const getD3Data = (): any => {
      ...3.Demo數據
  };

  /** * 計算線條文字位置 * * @param {*} data * @return {*} */
  const calcuLabelPoint = (data: any): number => {
      ...12.計算文字座標
  };

  /** * 獲取縮放對象 * * @param {*} g * @return {*} */
  const d3ZoomObj = (g: any): any => {
      ...5.縮放
  };

  /** * 獲取拖拽對象 * * @param {*} simulation 力模型 * @return {*} {object} */
  const d3DragObj = (simulation: any): any => {
      ...6.拖拽
  };

  /** * 構建表格 * * @param {*} g * @param {*} data * @param {*} drag * @return {*} */
  const buildTable = (g: any, data: any, drag: any): any => {
      ...7.構建表格節點
  };

  /** * 構建線條 * * @param {*} g * @param {*} data * @return {*} {*} */
  const buildLine = (g: any, data: any): any => {
      ...8.構建線條
  };

  /** * 構建線條文字 * * @param {*} g * @param {*} data * @return {*} {*} */
  const buildLineLabel = (g: any, data: any): any => {
      ...9.構建線條文字
  };

  /** * 構建箭頭 * * @param {*} g * @return {*} {*} */
  const buildArrow = (g: any): any => {
      ...10.構建箭頭
  };

  /** * 繪畫 * */
  const drawModel = () => {
      ...2.繪製函數
  };

  /** * 渲染數據表 * * @param {*} props */
  const renderDataTable = (props: any) => {
      ...13.渲染React組件到圖形中
  };

  return (
    <section className={'d3-dataModel-area'}> <div className={'popup-element'} /> <div className={'d3-element'} ref={refs} /> </section>
  );
};

export default D3DataModel;

複製代碼

代碼拆解

1.DOM節點

這個DOM節點用於掛載ant組件TooltipSelect生成的DOM,由於咱們當前這種方式節點內部元素DataTableComp中有使用到ant組件,致使D3重繪時ant生成的一些DOM節點沒有清除,統一掛載到這個區域統一清除。數組

<div className={'popup-element'} />
複製代碼

D3繪製的圖形節點所有在這個div中。

<div className={'d3-element'} ref={refs} />
複製代碼
<section className={'d3-dataModel-area'}>
      {/* ant組件彈框元素掛載節點 */}
      <div className={'popup-element'} />
      {/* d3繪製節點 */}
      <div className={'d3-element'} ref={refs} />
</section>
複製代碼

2.繪製函數

這個函數主要是整合其餘函數,統一入口。

React.useEffect(() => {
    drawModel();
  }, [d3NodeData.length]);

  /** * 繪畫 * */
  const drawModel = () => {
    const { edges } = getD3Data();
    // 先移除svg
    d3.selectAll('svg').remove();
    // 構建svg
    const svg = d3.select(refs.current).append('svg');
    // 構建容器g
    const g = svg.append('g').attr('transform', transformInfo);
    // 構建力模型,防止模型重疊
    const simulation = d3.forceSimulation(d3NodeData).force('collide', d3.forceCollide().radius(100));
    // 縮放
    const zoom = d3ZoomObj(g);
    // 獲取拖拽對象
    const drag = d3DragObj(simulation);
    // 構建表格區節點
    const d3DataTable = buildTable(g, d3NodeData, drag);
    // 構建線條
    const line = buildLine(g, edges);
    // 連線名稱
    const lineLabel = buildLineLabel(g, edges);
    // 繪製箭頭
    const arrows = buildArrow(g);

    simulation.on('tick', () => {
      // 更新節點位置
      d3DataTable.attr('transform', (d) => {
        return d && 'translate(' + d.x + ',' + d.y + ')';
      });
      // 更新連線位置
      line.attr('d', (d: any) => {
        // 節點的x+節點寬度
        const M1 = d.source.x + nodeWidth;
        // 節點的y+節點的一半高度
        let pathStr = `M ${M1} ${d.source.y + nodeHalfHeight} L ${d.target.x} ${d.target.y + nodeHalfHeight}`;
        // 起點摺疊
        if (nodeIds.includes(d.source.nodeId)) {
          pathStr = `M ${M1} ${d.source.y + foldHeight} L ${d.target.x} ${d.target.y + nodeHalfHeight}`;
        }
        // 終點摺疊
        if (nodeIds.includes(d.target.nodeId)) {
          pathStr = `M ${M1} ${d.source.y + nodeHalfHeight} L ${d.target.x} ${d.target.y + foldHeight}`;
        }
        // 起點重點同時摺疊
        if (nodeIds.includes(d.source.nodeId) && nodeIds.includes(d.target.nodeId)) {
          pathStr = `M ${M1} ${d.source.y + foldHeight} L ${d.target.x} ${d.target.y + foldHeight}`;
        }
        return pathStr;
      });
      // 更新線條文字
      lineLabel.attr('dx', (d: any) => calcuLabelPoint(d));
    });

    svg.call(zoom);
      
    /** * 摺疊 * * @param {string} nodeId * @param {boolean} status */
    const onFold = (nodeId: string, status: boolean) => {
      if (status) {
        g.select(`#foreign_${nodeId}`).attr('class', 'dataTable-class fold');
        // 記錄當前摺疊的id
        nodeIds.push(nodeId);
      } else {
        g.select(`#foreign_${nodeId}`).attr('class', 'dataTable-class');
        // 刪除存在的ID
        const currIndex = nodeIds.indexOf(nodeId);
        if (~currIndex) {
          nodeIds.splice(currIndex, 1);
        }
      }
      // 記錄當前節點摺疊狀態
      setD3NodeData(
        (prev: Array<any>) => {
          return prev.map((item: any) => {
            if (item.nodeId === nodeId) {
              item.foldStatus = status;
            }
            return item;
          });
        },
        () => {
          // 更新d3
          simulation.alpha(1).restart();
        }
      );
    };
      
	renderDataTable({ onFold });
  };
複製代碼

3.Demo數據

getD3Data函數主要是根據當前的數據生成線條數據,sNodeId存放的是開始節點的節點nodeId

getRandomId生成隨機ID,線條的id,會應用於連線的文字;

// 節點數據
  const [d3NodeData, setD3NodeData] = useCallbackState(() => {
    // nodeId 用來構建連線以及生成表格區域的ID
    // level 用來根據層級繪畫表格背景色
    // data_type 用來區分是否渲染無數據背景圖片
    return [{ x: 10, y: 10, data_type: NO_DATA, nodeId: getRandomId(), level: 1 }];
  });
  
  const getD3Data = (): any => {
    // 線條
    let edges: Array<any> = [];
    d3NodeData.forEach((item) => {
      if (item.sNodeId) {
        edges.push({
          lineId: getRandomId(), // 連線id
          source: d3NodeData.find(({ nodeId }) => nodeId === item.sNodeId), // 開始節點
          target: d3NodeData.find(({ nodeId }) => nodeId === item.nodeId), // 結束節點
          tag: '', // 連線名稱
        });
      }
    });
    // console.log(d3NodeData, edges);
    return { edges };
  };
複製代碼

3.生成SVG,G容器

這裏須要先移除SVG的內容在生成,transformInfo是記錄的縮放、拖動信息,用於添加節點刪除節點發生重繪時保持以前的縮放與畫布位置信息;

這裏目前還要個位置,重繪以後,界面回到上一次的縮放以後,再拖動畫布會重置縮放,暫時還沒解決。

// d3縮放範圍
  const [transformInfo, setTransformInfo] = React.useState<any>(null
                                                                
    // 先移除svg
    d3.selectAll('svg').remove();
    // 構建svg
    const svg = d3.select(refs.current).append('svg');
    // 構建容器g
    const g = svg.append('g').attr('transform', transformInfo);
複製代碼

4.構建力模型

collide:表示以x節點爲中心半徑100的圓形區域防止重疊;

// 構建力模型,防止模型重疊
    const simulation = d3.forceSimulation(d3NodeData).force('collide', d3.forceCollide().radius(100));
複製代碼

5.縮放

scaleExtent:縮放級別

filter:過濾縮放、拖動事件;

/** * 獲取縮放對象 * * @param {*} g * @return {*} */
  const d3ZoomObj = (g: any): any => {
    function zoomed(event: any): void {
      const { transform } = event;
      g.attr('transform', transform);
      // 記錄縮放
      setTransformInfo(transform);
    }
    const zoom = d3
      .zoom()
      .scaleExtent([0, 10])
      .on('zoom', zoomed)
      .filter(function (event) {
        // 滾動縮放必須同時按住`Alt`鍵,拖拽不須要
        return (event.altKey && event.type === 'wheel') || event.type === 'mousedown';
      });

    return zoom;
  };

    // 縮放
    const zoom = d3ZoomObj(g);

    svg.call(zoom);
複製代碼

6.拖拽

拖拽後須要同步更新數據中的x,y,防止添加節點、刪除節點時節點x,y被重置。

simulation.alpha(1).restart();這個函數會觸發D3重置,若是要觸發D3重置基本都要用到這個函數;

/** * 獲取拖拽對象 * * @param {*} simulation 力模型 * @return {*} {object} */
  const d3DragObj = (simulation: any): any => {
    /** * 開始拖拽 * * @param {*} event * @param {*} data */
    function onDragStart(event: any, data: any): void {
      // d.x是當前位置,d.fx是靜止時位置
      data.fx = data.x;
      data.fy = data.y;
    }

    /** * 拖拽中 * * @param {*} event * @param {*} data */
    function dragging(event: any, data: any): void {
      data.fx = event.x;
      data.fy = event.y;
      simulation.alpha(1).restart();
    }

    /** * 拖拽後 * * @param {*} data */
    function onDragEnd(event: any, data: any): void {
      // 解除dragged中固定的座標
      data.fx = null;
      data.fy = null;
      // 同步修改數據中的x,y,防止再次渲染,位置發生變化
      setD3NodeData((perv: Array<any>) => {
        return perv.map((item: any) => {
          if (item.nodeId === data.nodeId) {
            item.x = data.x;
            item.y = data.y;
          }
          return item;
        });
      });
    }

    const drag = d3
      .drag()
      .on('start', () => {})
      // 拖拽過程
      .on('drag', dragging)
      .on('end', onDragEnd);
    return drag;
  };

    // 獲取拖拽對象
    const drag = d3DragObj(simulation);
複製代碼

7.構建表格節點

call(drag)在哪裏調用就表示哪裏帶拖動,id用於renderReact組件到元素內部。

foreignObject:這個是SVG的節點,DOM內是html元素,若是須要在該DOM添加html元素,須要寫爲append('xhtml:div')

foldStatus:業務場景,摺疊狀態;

join:enter-入場;update:更新;exit:退場;

/** * 構建表格 * * @param {*} g * @param {*} data * @param {*} drag * @return {*} */
  const buildTable = (g: any, data: any, drag: any): any => {
    // 構建表格區節點
    const dataTable = g
      .selectAll('.dataTable-class')
      .data(data)
      .join(
        (enter: any) =>
          enter
            .append('foreignObject')
            .call(drag)
            .attr('class', (d) => {
              return `dataTable-class ${d.foldStatus ? 'fold' : ''}`;
            })
            .attr('id', function (d) {
              return `foreign_${d.nodeId}`;
            })
            .attr('transform', (d) => {
              return d && `translate(${d.x},${d.y})`;
            }),
        (update: any) => {
          return update;
        },
        (exit: any) => exit.remove()
      );

    return dataTable;
  };

    // 構建表格區節點
    const d3DataTable = buildTable(g, d3NodeData, drag);
複製代碼

8.構建線條

id:連線文字須要用到;

marker-start:有三個屬性,能夠查看MDN,這個屬性表示箭頭在線條的開始;

url(#arrow)根據箭頭的ID標記箭頭;

/** * 構建線條 * * @param {*} g * @param {*} data * @return {*} {*} */
  const buildLine = (g: any, data: any): any => {
    const line = g
      .selectAll('.line-class')
      .data(data)
      .join(
        (enter: any) => {
          return (
            enter
              .append('path')
              .attr('class', 'line-class')
              // 設置id,用於連線文字
              .attr('id', (d: any) => {
                return `line_${d.lineId}`;
              })
              // 根據箭頭標記的id號標記箭頭
              .attr('marker-start', 'url(#arrow)')
              // 顏色
              .style('stroke', '#AAB7C4')
              // 粗細
              .style('stroke-width', 1)
          );
        },
        (exit: any) => exit.remove()
      );

    return line;
  };

    // 構建線條
    const line = buildLine(g, edges);
複製代碼

9.構建線條文字

dx,dy:線條文字的位置;

xlink:href:文字佈置在對應id的連線上;

/** * 構建線條文字 * * @param {*} g * @param {*} data * @return {*} {*} */
  const buildLineLabel = (g: any, data: any): any => {
    const lineLabel = g
      .selectAll('.lineLabel-class')
      .data(data)
      .join(
        (enter: any) => {
          return enter
            .append('text')
            .attr('class', 'lineLabel-class')
            .attr('dx', (d: any) => calcuLabelPoint(d))
            .attr('dy', -5);
        },
        (exit: any) => exit.remove()
      );

    lineLabel
      .append('textPath')
      // 文字佈置在對應id的連線上
      .attr('xlink:href', (d: any) => {
        return `#line_${d.lineId}`;
      })
      // 禁止鼠標事件
      .style('pointer-events', 'none')
      // 設置文字內容
      .text((d: any) => {
        return d && d.tag;
      });

    return lineLabel;
  };

    // 連線名稱
    const lineLabel = buildLineLabel(g, edges);
複製代碼

10.構建箭頭

id:箭頭的ID,在線條的url(xxx)需用到;

/** * 構建箭頭 * * @param {*} g * @return {*} {*} */
  const buildArrow = (g: any): any => {
    // defs定義可重複使用的元素
    const defs = g.append('defs');
    const arrows = defs
      // 建立箭頭
      .append('marker')
      .attr('id', 'arrow')
      // 設置爲userSpaceOnUse箭頭不受鏈接元素的影響
      .attr('markerUnits', 'userSpaceOnUse')
      .attr('class', 'arrow-class')
      // viewport
      .attr('markerWidth', 20)
      // viewport
      .attr('markerHeight', 20)
      // viewBox
      .attr('viewBox', '0 0 20 20')
      // 偏離圓心距離
      .attr('refX', 10)
      // 偏離圓心距離
      .attr('refY', 5)
      // 繪製方向,可設定爲:auto(自動確認方向)和 角度值
      .attr('orient', 'auto-start-reverse');

    arrows
      .append('path')
      // d: 路徑描述,貝塞爾曲線
      .attr('d', 'M0,0 L0,10 L10,5 z')
      // 填充顏色
      .attr('fill', '#AAB7C4');

    return arrows;
  };

    // 繪製箭頭
    const arrows = buildArrow(g);
複製代碼

11.圖元素變化響應

註釋部分是原需提供給path的參數信息;

simulation.on('tick', () => {
      // 更新節點位置
      d3DataTable.attr('transform', (d) => {
        return d && 'translate(' + d.x + ',' + d.y + ')';
      });
      // 更新連線位置
      line.attr('d', (d: any) => {
        // 節點的x+節點寬度
        const M1 = d.source.x + nodeWidth;
        // 節點的y+節點的一半高度
        let pathStr = `M ${M1} ${d.source.y + nodeHalfHeight} L ${d.target.x} ${d.target.y + nodeHalfHeight}`;
        // 起點摺疊
         if (nodeIds.includes(d.source.nodeId)) {
          pathStr = `M ${M1} ${d.source.y + foldHeight} L ${d.target.x} ${d.target.y + nodeHalfHeight}`;
        }
        // 終點摺疊
        if (nodeIds.includes(d.target.nodeId)) {
          pathStr = `M ${M1} ${d.source.y + nodeHalfHeight} L ${d.target.x} ${d.target.y + foldHeight}`;
        }
        // 起點重點同時摺疊
        if (nodeIds.includes(d.source.nodeId) && nodeIds.includes(d.target.nodeId)) {
          pathStr = `M ${M1} ${d.source.y + foldHeight} L ${d.target.x} ${d.target.y + foldHeight}`;
        }
        // const pathStr = 'M ' + d.source.x + ' ' + d.source.y + ' L ' + d.target.x + ' ' + d.target.y;
        return pathStr;
      });
      // 更新線條文字
      lineLabel.attr('dx', (d: any) => calcuLabelPoint(d));
    });
複製代碼

12.計算文字座標

主要用於拖動節點後線條拉長從新計算線條中心位置,使文字始終處於線條的中心位置;

/** * 計算線條文字位置 * * @param {*} data * @return {*} */
  const calcuLabelPoint = (data: any): number => {
    // 計算path矩形對象線的中心點
    // 列出勾股定理的公式。該公式是Math.sqrt(Math.pow(a,2)+Math.pow(b,2)),其中a和b是直角三角形直角邊的邊長,而c是直角三角形的斜邊長度。
    // 計算寬度 目標節點x 減去 源目標節點x+源目標節點的自身寬度 獲得 矩形寬度
    let rectWidth = data.target.x - (data.source.x + nodeWidth);
    // 計算高度 目標節點y 減去 源目標節點y+源目標節點的自身高度一半 再 次冪
    let rectHeight = data.target.y + nodeHalfHeight - (data.source.y + nodeHalfHeight);
    rectHeight = Math.pow(rectHeight, 2);
    // 負負得正
    if (rectWidth < 0) rectWidth = -rectWidth;
    if (rectHeight < 0) rectHeight = -rectHeight;
    // 計算寬度 次冪
    rectWidth = Math.pow(rectWidth, 2);
    // 計算平方根
    const pathMidpoint = Math.sqrt(rectHeight + rectWidth) / 2;

    return Math.floor(pathMidpoint) - 20;
  };
複製代碼

13.渲染React組件到圖形中

/** * 渲染數據表 * * @param {*} props */
  const renderDataTable = (props: any) => {
    if (d3NodeData && d3NodeData.length) {
      // 建立訂閱,防止節點重繪時,沒法清空訂閱
      const subject = new Subject<any>();

      d3NodeData.forEach((item: any) => {
        const foreignId = `foreign_${item.nodeId}`;
        ReactDOM.render(
          <CustomComponent currNode={item} {...props} setD3NodeData={setD3NodeData} d3NodeData={d3NodeData} subject={subject} />,
          document.querySelector(`#${foreignId}`) as HTMLElement
        );
      });
    }
  };
複製代碼

其餘功能

這部分都是在React自定義組件中CustomComponent的功能了,就直接上代碼了,若是沒有須要相似的功能能夠直接跳過,這部分邏輯僅供參考;

1.添加節點、刪除節點

這種添加刪除節點,對於SVG來講須要從新繪製節點,因此須要用React的方式觸發父組件的更新,監聽響應重繪SVG;

/** * 添加子表 * */
  const addNode = (): void => {
    props.subject.complete();
    const { newStartX, newStartY } = calcPoint();

    // 添加到新數組彙總
    let newData: Array<any> = [];
    newData.push(currNode);
    newData.push({ nodeId: getRandomId(), x: newStartX, y: newStartY, data_type: NO_DATA, sNodeId: currNode.nodeId, level: currNode.level + 1 });
    // 修改表數據,觸發重繪
    props.setD3NodeData((prev: Array<any>) => {
      newData.forEach((item: any) => {
        // 存在更新,不存在新增
        const pIndex = prev.findIndex(({ nodeId }) => item.nodeId === nodeId);
        if (~pIndex) {
          // 存在
          prev[pIndex] = {
            ...prev[pIndex],
            ...item,
          };
        } else {
          // 不存在
          prev.push(item);
        }
      });
      return [...prev];
    });
  };

  /** * 刪除結點 * */
  const delNode = (): void => {
    props.subject.complete();
    let delNodeIds: Array<any> = [currNode.nodeId];

    // 迭歸查找全部關聯節點
    function iterationNode(data: any) {
      for (const item of props.d3NodeData) {
        if (item.sNodeId === data.nodeId) {
          iterationNode(item);
          delNodeIds.push(item.nodeId);
        }
      }
    }

    iterationNode(currNode);
    // 刪除節點
    props.setD3NodeData((prev: Array<any>) => {
      const newDatas = prev.filter(({ nodeId }) => !delNodeIds.includes(nodeId));
      return [...newDatas];
    });
  };
複製代碼

2.計算添加節點位置

// 節點高
const nodeHeigth = 300;
// 節點寬度
const nodeWidth = 240;
// 節點之間的間距
const spacWidth = 150;
const spacHeight = 30;
// 未選擇表數據標識
const NO_DATA = 'NO_DATA';

  /** * 計算添加位置座標 * * @return {*} */
  const calcPoint = (): any => {
    let newStartX = currNode.x + nodeWidth + spacWidth;
    // 添加節點x添加節點寬度+節點間距+新增節點寬度
    const newEndX = currNode.x + nodeWidth + spacWidth + nodeWidth;
    let newStartY = currNode.y;

    /** * 1.篩選大於添加節點x座標(起)與小於新增節點x座標(止)區間的節點 * 2.過濾掉第1點數據中y軸(止)小於新增節點y座標(起) * 3.過濾掉第2點數據中x軸(止)小於新增節點x座標(起) * 4.查找第2點數據中y軸(起)最小的節點並計算新增節點y座標(起)至第3點數據y軸(起)的間距 * 5.間距足夠放下新增數據就追加 * 6.間距不夠,就查找4點y軸(止)與下一個y軸(起)之間的間距,依次類推,直到最後一個節點 * */

    // step 1
    let spacDatas = props.d3NodeData.filter((item: any) => {
      return item.x >= currNode.x && item.x <= newEndX;
    });
    // step 2
    spacDatas = spacDatas.filter((item: any) => {
      const oldEndY = item.y + nodeHeigth;
      return oldEndY >= newStartY;
    });
    // step 3
    spacDatas = spacDatas.filter((item: any) => {
      const oldEndX = item.x + nodeWidth;
      return oldEndX >= newStartX;
    });
    // step 4,step5,step6
    let prevStartY = newStartY;

    // 根據y軸進行排序
    spacDatas.sort(({ y: y1 }, { y: y2 }) => y1 - y2);

    for (let index = 0; index < spacDatas.length; index++) {
      const item = spacDatas[index];
      let specY = item.y - prevStartY;
      // 須要的高度
      const needY = nodeHeigth + spacHeight;
      if (specY >= needY) {
        newStartY = prevStartY;
        break;
      }
      // 獲取下一個位置的y軸(起)
      const nextY = spacDatas[index + 1]?.y ?? 'NO_NODE';
      // 計算prevStartY與nexY之間的間距
      specY = nextY - prevStartY - nodeHeigth;
      if (specY >= needY) {
        // y軸(起)+節點高度+間距高度等於新增節點y軸(起)
        newStartY = prevStartY + nodeHeigth + spacHeight;
        break;
      } else {
        // 記錄y軸(起)上一個節點的位置
        prevStartY = nextY === 'NO_NODE' ? item.y : nextY;
      }
      // 若是沒有下一個節點,則返回最後一個y軸(起)的位置
      if (nextY === 'NO_NODE') {
        // y軸(起)+節點高度+間距高度等於新增節點y軸(起)
        newStartY = prevStartY + nodeHeigth + spacHeight;
        break;
      }
    }
    return { newStartX, newStartY };
  };
複製代碼

3.節點間的通訊

React.useEffect(() => {
    // 訂閱其餘table的change動做,篩選下拉框數據
    props.subject.subscribe(function (aciton: any) {
      const { type, data } = aciton;
      if (type === 'table-change') {
        // 若是是當前節點則不觸發更新
        if (data.nodeId !== currNode.nodeId) {
          // 監聽其餘change再過濾數據
          setTableData((prev: Array<any>) => {
            return prev.filter((item: any) => {
              const val = `${item.value}-${item.title}`;
              return val != data.changeVal;
            });
          });
        }
      }
    });
  }, []);

  /** * 選擇表 * * @param {*} val */
  const onChange = (val: any): void => {
    // 發佈消息
    props.subject.next({
      type: 'table-change',
      data: {
        changeVal: val,
        nodeId: currNode.nodeId,
      },
    });
  };
複製代碼

4.不一樣層級不一樣顏色

這個有用到相似換膚的功能,根據不一樣的class展現不一樣的顏色,層級的話是經過level控制的。

A文件
/** d3 table顏色 **/
@mixin tableTheme($tableThemes: $tableThemes) {

    @each $class-name,
    $map in $tableThemes {
        &.#{$class-name} {
            $color-map: () !global;

            @each $key,
            $value in $map {
                $color-map: map-merge($color-map, ($key: $value)) !global;
            }

            @content;

            $color-map: null !global;
        }
    }
}

@function colord($key) {
    @return map-get($color-map, $key);
}

$tableThemes: (mian-table: (table-border:rgba(239, 177, 91, 1),
        table-background:rgba(254, 251, 247, 1),
        table-header-background:rgba(239, 177, 91, 0.15),
        table-header-border:rgba(239, 177, 91, 0.5),
        table-foot-background:rgba(239, 177, 91, 0.2)),

    child-table: (table-border:rgba(91, 143, 249, 1),
        table-background:rgba(238, 243, 254, 1),
        table-header-background:rgba(91, 143, 249, 0.2),
        table-header-border:rgba(91, 143, 249, 0.5),
        table-foot-background:rgba(91, 143, 249, 0.25)),

    grandson-table: (table-border:rgba(38, 154, 153, 1),
        table-background:rgba(238, 247, 247, 1),
        table-header-background:rgba(38, 154, 153, 0.2),
        table-header-border:rgba(38, 154, 153, 0.5),
        table-foot-background:rgba(38, 154, 153, 0.25)),

    other-table: (table-border:rgba(153, 173, 208, 1),
        table-background:rgba(244, 246, 250, 1),
        table-header-background:rgba(153, 173, 208, 0.2),
        table-header-border:rgba(153, 173, 208, 0.5),
        table-foot-background:rgba(153, 173, 208, 0.25)));

-----------------------------------------------------------
B文件
    /** 不一樣層級表不一樣顏色 **/
    @include tableTheme($tableThemes) {
        border: 1px solid colord('table-border');
        background-color: colord('table-background');

        .icon.iconfont {
            color: colord('table-border')
        }

        >.table-header {
            background-color: colord('table-header-background');
            border-bottom: 1px solid colord('table-header-border');
        }

        >.table-body {
            >div:first-child {
                background-color: colord('table-header-background');
            }

            >section:last-child {
                >:first-child {
                    border-top: 5px solid colord('table-background');
                }
            }
        }

        >.table-foot {
            background-color: colord('table-foot-background');
        }
    }
複製代碼

最後

這部分業務功能還在開發階段,可能有些邏輯問題沒有考慮到,若是你們有發現還請在評論區指出,謝謝;

參考資料

其餘相關文章:juejin.cn/post/684490…

D3 API:github.com/d3/d3/blob/…

D3官網:d3js.org/

相關文章
相關標籤/搜索