React与D3的结合

·  阅读 563
React与D3的结合

前言

前段时间公司有个需求要开发一个数据关系的界面,类似UML建模工具里面表之间关系的图形界面,目前用的前端框架是React、rxjs,图形界面这块定下来采用的是D3的最新版本V7,所以现在需要基于React框架下开发这个界面,前期查了一些相关资料,国内基于React、D3 V7版本结合开发的比较少,差不多都是V3、V4版本,V4版本国内还有中文翻译V4之后就停了,所以结合个人在当前的需求背景下以及使用过程中的碰到的一些问题记录下来,一方面供有需要人的可以借鉴下,一方面也是给自己做个总结。

用的D3版本v7.0.0,需要开发的功能:

1.拖拽、缩放功能

2.连线并带有箭头,线条有文字

3.能添加节点、删除结点

4.添加节点需计算位置,尽量保证不重叠

5.节点与节点之间需要通信更新数据

6.节点不同层级展示的背景颜色不一致

7.节点可折叠、展开

代码结构

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/

分类:
前端
分类:
前端
收藏成功!
已添加到「」, 点击更改