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 componentCustomComponent
Function, 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/