preface

Some time ago, the company had a requirement to develop a data relation interface, which is similar to the graphical interface between tables in UML modeling tool. Currently, the front-end framework used is React and RXJS, and the graphical interface has been decided to adopt the latest version of D3 V7, so now we need to develop this interface based on the React framework. Early to check some related information, based on the React, D3 V7 combined with the development of domestic less, almost all the V3, V4 version, V4 version and Chinese translation domestic stopped after V4, so combining individual under the background of the current demand and met some problems in the process of using record, on the one hand, people can draw lessons from, for the necessary On the one hand, it is also a summary for myself.

D3 version v7.0.0, need to develop features:

1. Drag and zoom functions

2. Lines with arrows, lines with text

3. Add nodes and delete nodes

4. When adding nodes, calculate their positions to ensure that they do not overlap

5. Update data between nodes

6. The background colors displayed at different layers of nodes are inconsistent

7. Nodes can be folded or expanded

The code structure

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

/ / the node is high
const nodeHalfHeight = 300 / 2;
// Node width
const nodeWidth = 240;
// The height after folding
const foldHeight = 85 / 2;
// Table data id is not selected
const NO_DATA = 'NO_DATA';
// Get the random ID
const getRandomId = () = > Math.random().toString(32).slice(2);


// Record the nodeId of the current operation
let nodeIds: Array<any> = [];

const D3DataModel = (props: any): React.ReactElement => {
  const refs = React.useRef(null);
  / / table data
  const [d3NodeData, setD3NodeData] = React.useState(() = > {
    // nodeId Specifies the ID used to build the line and generate the table area
    // level is used to draw the table background color according to the level
    // data_type is used to distinguish between rendering background images without data
    return [{ x: 10.y: 10.data_type: NO_DATA, nodeId: getRandomId(), level: 1 }];
  });
  // d3 scale range
  const [transformInfo, setTransformInfo] = React.useState<any>(null);

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

  const getD3Data = (): any= >{..3.. The Demo data};/** * Calculates line text position **@param {*} data
   * @return {*}* /
  const calcuLabelPoint = (data: any): number= >{..12..calculate text coordinates};/** * gets the scale object **@param {*} g
   * @return {*}* /
  const d3ZoomObj = (g: any): any= >{... 5. Zoom};/** * get the drag object **@param {*} Simulation force model *@return {*}  {object}* /
  const d3DragObj = (simulation: any): any= >{..6.. Drag and drop};/** * Build the table **@param {*} g
   * @param {*} data
   * @param {*} drag
   * @return {*}* /
  const buildTable = (g: any, data: any, drag: any): any= >{..7..construct table node};/** * Build line **@param {*} g
   * @param {*} data
   * @return {*}  {*}* /
  const buildLine = (g: any, data: any): any= >{..8..build lines};/** * Construct line text **@param {*} g
   * @param {*} data
   * @return {*}  {*}* /
  const buildLineLabel = (g: any, data: any): any= >{..9..construct line text};/** * build arrow **@param {*} g
   * @return {*}  {*}* /
  const buildArrow = (g: any): any= >{..10..build arrow};/** * painting ** /
  const drawModel = () = >{..2..draw function};/** * render table **@param {*} props* /
  const renderDataTable = (props: any) = >{..13.Render the React component into the image};return (
    <section className={'d3-dataModel-area'} >
      <div className={'popup-element'} / >
      <div className={'d3-element'} ref={refs} />
    </section>
  );
};

export default D3DataModel;

Copy the code

Code apart

1. The DOM node

This DOM node is used to mount the DOM generated by the Ant component Tooltip and Select. Because the ant component is used in the internal element DataTableComp of the node in the current way, some DOM nodes generated by Ant are not cleared during the D3 redraw, so they are mounted to this region and cleared.

<div className={'popup-element'} / >Copy the code

The graph nodes drawn by D3 are all in this div.

<div className={'d3-element'} ref={refs} />
Copy the code
<section className={'d3-dataModel-area'} > {/* Ant component popup element mounts node */}
      <div className={'popup-element'} / > {/* d3 draws the node */}
      <div className={'d3-element'} ref={refs} />
</section>
Copy the code

2. Draw the function

This function is mainly to integrate other functions, unified entry.

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

  /** * painting ** /
  const drawModel = () = > {
    const { edges } = getD3Data();
    // Remove SVG first
    d3.selectAll('svg').remove();
    / / build SVG
    const svg = d3.select(refs.current).append('svg');
    // Build container g
    const g = svg.append('g').attr('transform', transformInfo);
    // Build a force model to prevent model overlap
    const simulation = d3.forceSimulation(d3NodeData).force('collide', d3.forceCollide().radius(100));
    / / zoom
    const zoom = d3ZoomObj(g);
    // Get the drag object
    const drag = d3DragObj(simulation);
    // Build the table area node
    const d3DataTable = buildTable(g, d3NodeData, drag);
    // Build lines
    const line = buildLine(g, edges);
    // The name of the connection
    const lineLabel = buildLineLabel(g, edges);
    // Draw arrows
    const arrows = buildArrow(g);

    simulation.on('tick'.() = > {
      // Update the node location
      d3DataTable.attr('transform'.(d) = > {
        return d && 'translate(' + d.x + ', ' + d.y + ') ';
      });
      // Update the connection position
      line.attr('d'.(d: any) = > {
        // The x+ node width of the node
        const M1 = d.source.x + nodeWidth;
        // Y of the node + half the height of the node
        let pathStr = `M ${M1} ${d.source.y + nodeHalfHeight} L ${d.target.x} ${d.target.y + nodeHalfHeight}`;
        // Start folding
        if (nodeIds.includes(d.source.nodeId)) {
          pathStr = `M ${M1} ${d.source.y + foldHeight} L ${d.target.x} ${d.target.y + nodeHalfHeight}`;
        }
        // End fold
        if (nodeIds.includes(d.target.nodeId)) {
          pathStr = `M ${M1} ${d.source.y + nodeHalfHeight} L ${d.target.x} ${d.target.y + foldHeight}`;
        }
        // Start point focus at the same time fold
        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;
      });
      // Update line text
      lineLabel.attr('dx'.(d: any) = > calcuLabelPoint(d));
    });

    svg.call(zoom);
      
    /** ** fold **@param {string} nodeId
     * @param {boolean} status* /
    const onFold = (nodeId: string, status: boolean) = > {
      if (status) {
        g.select(`#foreign_${nodeId}`).attr('class'.'dataTable-class fold');
        // Record the id of the current collapse
        nodeIds.push(nodeId);
      } else {
        g.select(`#foreign_${nodeId}`).attr('class'.'dataTable-class');
        // Delete an existing ID
        const currIndex = nodeIds.indexOf(nodeId);
        if (~currIndex) {
          nodeIds.splice(currIndex, 1); }}// Record the current node collapse status
      setD3NodeData(
        (prev: Array<any>) = > {
          return prev.map((item: any) = > {
            if (item.nodeId === nodeId) {
              item.foldStatus = status;
            }
            return item;
          });
        },
        () = > {
          / / update the d3
          simulation.alpha(1).restart(); }); }; renderDataTable({ onFold }); };Copy the code

3. The Demo data

The getD3Data function mainly generates line data according to the current data. SNodeId stores the node nodeId of the start node.

GetRandomId generates a random ID, the LINE ID, that will be applied to the line text;

  // Node data
  const [d3NodeData, setD3NodeData] = useCallbackState(() = > {
    // nodeId Specifies the ID used to build the line and generate the table area
    // level is used to draw the table background color according to the level
    // data_type is used to distinguish between rendering background images without data
    return [{ x: 10.y: 10.data_type: NO_DATA, nodeId: getRandomId(), level: 1 }];
  });
  
  const getD3Data = (): any= > {
    / / line
    let edges: Array<any> = [];
    d3NodeData.forEach((item) = > {
      if (item.sNodeId) {
        edges.push({
          lineId: getRandomId(), / / the attachment id
          source: d3NodeData.find(({ nodeId }) = > nodeId === item.sNodeId), // Start node
          target: d3NodeData.find(({ nodeId }) = > nodeId === item.nodeId), // End the node
          tag: ' '.// The name of the connection}); }});// console.log(d3NodeData, edges);
    return { edges };
  };
Copy the code

3. Generate SVG,G container

TransformInfo is used to record the zoom and drag information, which is used to keep the previous zoom and canvas position information when nodes are added or deleted and redrawn.

Now I need a position here. After redrawing, the interface will go back to the last zoom, and then dragging the canvas will reset the zoom.

  // d3 scale range
  const [transformInfo, setTransformInfo] = React.useState<any>(null
                                                                
    // Remove SVG first
    d3.selectAll('svg').remove();
    / / build SVG
    const svg = d3.select(refs.current).append('svg');
    // Build container g
    const g = svg.append('g').attr('transform', transformInfo);
Copy the code

4. Build force models

Collide: a circular area with a radius of 100 centered on node X to prevent overlap;

    // Build a force model to prevent model overlap
    const simulation = d3.forceSimulation(d3NodeData).force('collide', d3.forceCollide().radius(100));
Copy the code

5. The zoom

ScaleExtent: scale level

Filter: filters zooming and dragging events.

  /** * gets the scale object **@param {*} g
   * @return {*}* /
  const d3ZoomObj = (g: any): any= > {
    function zoomed(event: any) :void {
      const { transform } = event;
      g.attr('transform', transform);
      // Record the zoom
      setTransformInfo(transform);
    }
    const zoom = d3
      .zoom()
      .scaleExtent([0.10])
      .on('zoom', zoomed)
      .filter(function (event) {
        // Scrolling to zoom must be done while holding down Alt, drag is not required
        return (event.altKey && event.type === 'wheel') || event.type === 'mousedown';
      });

    return zoom;
  };

    / / zoom
    const zoom = d3ZoomObj(g);

    svg.call(zoom);
Copy the code

6. Drag and drop

After the drag, update x and y in data to prevent points X and y from being reset when nodes are added or deleted.

simulation.alpha(1).restart(); This function triggers a D3 reset, which is almost always needed to trigger a D3 reset;

  /** * get the drag object **@param {*} Simulation force model *@return {*}  {object}* /
  const d3DragObj = (simulation: any): any= > {
    /** * start dragging **@param {*} event
     * @param {*} data* /
    function onDragStart(event: any, data: any) :void {
      // d.x is the current position, d.fx is the rest position
      data.fx = data.x;
      data.fy = data.y;
    }

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

    /** * after drag **@param {*} data* /
    function onDragEnd(event: any, data: any) :void {
      // Remove the fixed coordinates during partying
      data.fx = null;
      data.fy = null;
      // Synchronously modify x and y in the data to prevent rerendering and position changes
      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'.() = > {})
      // Drag process
      .on('drag', dragging)
      .on('end', onDragEnd);
    return drag;
  };

    // Get the drag object
    const drag = d3DragObj(simulation);
Copy the code

7. Create a table node

Call (drag) is called where the drag is, and the ID is used for the renderReact component inside the element.

ForeignObject: This is an SVG node. The DOM contains HTML elements. If you want to add HTML elements to the DOM, write append(‘ XHTML :div’).

FoldStatus: indicates the foldStatus of a service scenario.

Join: enter Update c. -Serena: I’m leaving.

  /** * Build the table **@param {*} g
   * @param {*} data
   * @param {*} drag
   * @return {*}* /
  const buildTable = (g: any, data: any, drag: any): any= > {
    // Build the table area node
    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;
  };

    // Build the table area node
    const d3DataTable = buildTable(g, d3NodeData, drag);
Copy the code

8. Build lines

Id: line text is needed;

Marker -start: there are three attributes, you can view MDN, this attribute indicates the arrow at the beginning of the line;

The url(#arrow) marks the arrow by its ID;

  /** * Build line **@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')
              // Set the id to be used to line the text
              .attr('id'.(d: any) = > {
                return `line_${d.lineId}`;
              })
              // Mark the arrow according to the id of the arrow mark
              .attr('marker-start'.'url(#arrow)')
              / / color
              .style('stroke'.'#AAB7C4')
              / / the thickness
              .style('stroke-width'.1)); },(exit: any) = > exit.remove()
      );

    return line;
  };

    // Build lines
    const line = buildLine(g, edges);
Copy the code

9. Build line text

Dx,dy: position of line text;

Xlink :href: text placed on the line corresponding to the id;

  /** * Construct line text **@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')
      // The text is placed on the line corresponding to the id
      .attr('xlink:href'.(d: any) = > {
        return `#line_${d.lineId}`;
      })
      // Disable mouse events
      .style('pointer-events'.'none')
      // Set the text content
      .text((d: any) = > {
        return d && d.tag;
      });

    return lineLabel;
  };

    // The name of the connection
    const lineLabel = buildLineLabel(g, edges);
Copy the code

10. Build arrows

Id: the id of the arrow, used in the line URL (XXX);

  /** * build arrow **@param {*} g
   * @return {*}  {*}* /
  const buildArrow = (g: any): any= > {
    // defs defines reusable elements
    const defs = g.append('defs');
    const arrows = defs
      // Create arrows
      .append('marker')
      .attr('id'.'arrow')
      // Set the arrow to userSpaceOnUse to be unaffected by the connection element
      .attr('markerUnits'.'userSpaceOnUse')
      .attr('class'.'arrow-class')
      // viewport
      .attr('markerWidth'.20)
      // viewport
      .attr('markerHeight'.20)
      // viewBox
      .attr('viewBox'.'0 0 20 20')
      // From the center of the circle
      .attr('refX'.10)
      // From the center of the circle
      .attr('refY'.5)
      // Draw direction, can be set to: auto (automatically confirm direction) and Angle value
      .attr('orient'.'auto-start-reverse');

    arrows
      .append('path')
      // d: Path description, Bezier curve
      .attr('d'.'M0, 0 L0, 10 L10, 5 z')
      // Fill the color
      .attr('fill'.'#AAB7C4');

    return arrows;
  };

    // Draw arrows
    const arrows = buildArrow(g);
Copy the code

11. Graph element change response

The comment section is the parameter information originally provided to path.

    simulation.on('tick'.() = > {
      // Update the node location
      d3DataTable.attr('transform'.(d) = > {
        return d && 'translate(' + d.x + ', ' + d.y + ') ';
      });
      // Update the connection position
      line.attr('d'.(d: any) = > {
        // The x+ node width of the node
        const M1 = d.source.x + nodeWidth;
        // Y of the node + half the height of the node
        let pathStr = `M ${M1} ${d.source.y + nodeHalfHeight} L ${d.target.x} ${d.target.y + nodeHalfHeight}`;
        // Start folding
         if (nodeIds.includes(d.source.nodeId)) {
          pathStr = `M ${M1} ${d.source.y + foldHeight} L ${d.target.x} ${d.target.y + nodeHalfHeight}`;
        }
        // End fold
        if (nodeIds.includes(d.target.nodeId)) {
          pathStr = `M ${M1} ${d.source.y + nodeHalfHeight} L ${d.target.x} ${d.target.y + foldHeight}`;
        }
        // Start point focus at the same time fold
        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;
      });
      // Update line text
      lineLabel.attr('dx'.(d: any) = > calcuLabelPoint(d));
    });
Copy the code

12. Calculate text coordinates

It is mainly used to recalculate the center position of the line after dragging the node to make the text always in the center position of the line.

  /** * Calculates line text position **@param {*} data
   * @return {*}* /
  const calcuLabelPoint = (data: any): number= > {
    // Calculate the center point of the path rectangle object line
    // List the formulas for Pythagorean theorem. The formula is math.sqrt (math.pow (a,2)+Math.pow(b,2)), where A and b are the lengths of the right sides of a right triangle and C is the length of the hypotenuse of a right triangle.
    // Calculate the width target node x minus the source target node x+ the source target node's own width to get the rectangle width
    let rectWidth = data.target.x - (data.source.x + nodeWidth);
    // Calculate the height of the target node y minus the source target node y+ the source target node's own height half power
    let rectHeight = data.target.y + nodeHalfHeight - (data.source.y + nodeHalfHeight);
    rectHeight = Math.pow(rectHeight, 2);
    // A negative makes a positive
    if (rectWidth < 0) rectWidth = -rectWidth;
    if (rectHeight < 0) rectHeight = -rectHeight;
    // Compute the width to the power
    rectWidth = Math.pow(rectWidth, 2);
    // Calculate the square root
    const pathMidpoint = Math.sqrt(rectHeight + rectWidth) / 2;

    return Math.floor(pathMidpoint) - 20;
  };
Copy the code

13. Render the React component into the image

  /** * render table **@param {*} props* /
  const renderDataTable = (props: any) = > {
    if (d3NodeData && d3NodeData.length) {
      // Create a subscription to prevent it from being empty when the node is redrawn
      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}`) asHTMLElement ); }); }};Copy the code

Other features

This is all in the React custom componentCustomComponentFunction, directly on the code, if there is no need for similar functions can be directly skipped, this part of the logic for reference only;

1. Add or delete a node

This method of adding and removing nodes requires redrawing nodes for SVG, so you need to use React to trigger the update of the parent component and listen for the response to redraw SVG.

  /** * add child table ** /
  const addNode = (): void= > {
    props.subject.complete();
    const { newStartX, newStartY } = calcPoint();

    // Add to the new array summary
    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 });
    // Modify table data to trigger redraw
    props.setD3NodeData((prev: Array<any>) = > {
      newData.forEach((item: any) = > {
        // There is an update, but no new one
        const pIndex = prev.findIndex(({ nodeId }) = > item.nodeId === nodeId);
        if (~pIndex) {
          / / thereprev[pIndex] = { ... prev[pIndex], ... item, }; }else {
          / / does not existprev.push(item); }});return [...prev];
    });
  };

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

    // find all associated nodes over and over
    function iterationNode(data: any) {
      for (const item of props.d3NodeData) {
        if (item.sNodeId === data.nodeId) {
          iterationNode(item);
          delNodeIds.push(item.nodeId);
        }
      }
    }

    iterationNode(currNode);
    // Delete a node
    props.setD3NodeData((prev: Array<any>) = > {
      const newDatas = prev.filter(({ nodeId }) = >! delNodeIds.includes(nodeId));return [...newDatas];
    });
  };
Copy the code

2. Locate the node to be added

/ / the node is high
const nodeHeigth = 300;
// Node width
const nodeWidth = 240;
// The spacing between nodes
const spacWidth = 150;
const spacHeight = 30;
// Table data id is not selected
const NO_DATA = 'NO_DATA';

  /** * computes add position coordinates **@return {*}* /
  const calcPoint = (): any= > {
    let newStartX = currNode.x + nodeWidth + spacWidth;
    // Add node x Add node width + node spacing + new node width
    const newEndX = currNode.x + nodeWidth + spacWidth + nodeWidth;
    let newStartY = currNode.y;

    /** * 1. Filter nodes * 2 that are larger than the x-coordinate (from) of the added node and smaller than the x-coordinate (stop) of the new node. The Y-axis (check) in the data at point 1 was filtered out to be less than the y-coordinate (start) * 3 of the new node. The X-axis (check) in the data at point 2 was filtered out to be less than the x-coordinate (starting) * 4 of the new node. Find the node with the smallest y axis (starting) in the data of point 2 and calculate the distance from the y coordinate (starting) of the new node to the y axis (starting) of the data of point 3 * 5. If the space is enough, add * 6. If the space is not enough, find the space between the y axis (check) and the next y axis (up), and so on until the last node * */

    // 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;

    // Sort by Y-axis
    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;
      // The required height
      const needY = nodeHeigth + spacHeight;
      if (specY >= needY) {
        newStartY = prevStartY;
        break;
      }
      // Get the y axis of the next position (up)
      const nextY = spacDatas[index + 1]? .y ??'NO_NODE';
      // Calculate the space between prevStartY and nexY
      specY = nextY - prevStartY - nodeHeigth;
      if (specY >= needY) {
        // Y-axis (up) + node height + spacing height equals Y-axis (up) of the new node
        newStartY = prevStartY + nodeHeigth + spacHeight;
        break;
      } else {
        // Record the position of the last node on the y axis
        prevStartY = nextY === 'NO_NODE' ? item.y : nextY;
      }
      // If there is no next node, return the position of the last y axis (up)
      if (nextY === 'NO_NODE') {
        // Y-axis (up) + node height + spacing height equals Y-axis (up) of the new node
        newStartY = prevStartY + nodeHeigth + spacHeight;
        break; }}return { newStartX, newStartY };
  };
Copy the code

3. Communication between nodes

  React.useEffect(() = > {
    // Subscribe to the change action of other tables to filter the dropdown data
    props.subject.subscribe(function (aciton: any) {
      const { type, data } = aciton;
      if (type === 'table-change') {
        // Update is not triggered if it is the current node
        if(data.nodeId ! == currNode.nodeId) {// Listen for other changes to filter data
          setTableData((prev: Array<any>) = > {
            return prev.filter((item: any) = > {
              const val = `${item.value}-${item.title}`;
              returnval ! = data.changeVal; }); }); }}}); } []);/** * select table **@param {*} val* /
  const onChange = (val: any): void= > {
    // Publish the message
    props.subject.next({
      type: 'table-change'.data: {
        changeVal: val,
        nodeId: currNode.nodeId,
      },
    });
  };
Copy the code

4. Different colors for different levels

This is useful for functions like skin peels, showing different colors according to different classes, and levels are controlled by levels.

Afile/** d3 table color **/
@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)));

-----------------------------------------------------------
Bfile/** Different levels of the table different colors **/
    @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'); }}Copy the code

The last

This part of the business function is still in the development stage, there may be some logic problems not considered, if you find anything, please point out in the comment section, thank you;

The resources

Other related articles: juejin.cn/post/684490…

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

D3 official website: d3js.org/