D3实现类似企查查股权穿透图

98 阅读2分钟
import * as d3 from 'd3';

class LegalPerson extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            data: {
                id: "abc1005",
                name: "山东吠舍科技有限责任公司",
                children: [
                    {
                        id: "abc1006",
                        name: "山东第一首陀罗科技服务有限公司",
                        percent: "100%",
                    },
                    {
                        id: "abc1007",
                        name: "山东第二首陀罗程技术有限公司",
                        percent: "100%",
                    },
                    {
                        id: "abc1008",
                        name: "山东第三首陀罗光伏材料有限公司",
                        percent: "100%",
                    },
                    {
                        id: "abc1009",
                        name: "山东第四首陀罗科技发展有限公司",
                        percent: "100%",
                        children: [
                            {
                                id: "abc1010",
                                name: "山东第一达利特瑞利分析仪器有限公司",
                                percent: "100%",
                                children: [
                                    {
                                        id: "abc1011",
                                        name: "山东瑞利的子公司一",
                                        percent: "80%",
                                    },
                                    {
                                        id: "abc1012",
                                        name: "山东瑞利的子公司二",
                                        percent: "90%",
                                    },
                                    {
                                        id: "abc1013",
                                        name: "山东瑞利的子公司三",
                                        percent: "100%",
                                    },
                                ],
                            },
                        ],
                    },
                    {
                        id: "abc1014",
                        name: "山东第五首陀罗电工科技有限公司",
                        percent: "100%",
                        children: [
                            {
                                id: "abc1015",
                                name: "山东第二达利特低自动化设备有限公司",
                                percent: "100%",
                                children: [
                                    {
                                        id: "abc1016",
                                        name: "山东敬业的子公司一",
                                        percent: "100%",
                                    },
                                    {
                                        id: "abc1017",
                                        name: "山东敬业的子公司二",
                                        percent: "90%",
                                    },
                                ],
                            },
                        ],
                    },
                    {
                        id: "abc1020",
                        name: "山东第六首陀罗分析仪器(集团)有限责任公司",
                        percent: "100%",
                        children: [
                            {
                                id: "abc1021",
                                name: "山东第三达利特分气体工业有限公司",
                            },
                        ],
                    },
                ],
                parents: [
                    {
                        id: "abc2001",
                        name: "山东刹帝利集团有限责任公司",
                        percent: "60%",
                        parents: [
                            {
                                id: "abc2000",
                                name: "山东婆罗门集团有限公司",
                                percent: "100%",
                            },
                        ],
                    },
                    {
                        id: "abc2002",
                        name: "吴小远",
                        percent: "40%",
                        parents: [
                            {
                                id: "abc1010",
                                name: "山东第一达利特瑞利分析仪器有限公司",
                                percent: "100%",
                                parents: [
                                    {
                                        id: "abc1011",
                                        name: "山东瑞利的子公司一",
                                        percent: "80%",
                                    },
                                    {
                                        id: "abc1012",
                                        name: "山东瑞利的子公司二",
                                        percent: "90%",
                                    },
                                    {
                                        id: "abc1013",
                                        name: "山东瑞利的子公司三",
                                        percent: "100%",
                                    },
                                ],
                            },
                        ],
                    },
                    {
                        id: "abc2003",
                        name: "测试数据",
                        percent: "40%",
                    },
                ],
            },
        };
        this.config = {
            dx: 200,
            dy: 170,
            width: 0,
            height: 500,
            rectWidth: 170,
            rectHeight: 70,
        };
        this.svg = null;
        this.gAll = null;
        this.gLinks = null;
        this.gNodes = null;
        this.tree = null;
        this.rootOfDown = null;
        this.rootOfUp = null;
    }

    componentDidMount() {
        this.initializeChart();
    }

    initializeChart = () => {
        this.drawChart({ type: "fold" });
    };

    drawChart = (options) => {
        const host = d3.select(this.container);
        const dom = this.container;
        const domRect = dom.getBoundingClientRect();

        this.config.width = domRect.width;
        this.config.height = domRect.height;

        d3.select(this.container).select("svg").remove();

        const svg = d3.create("svg")
           .attr("viewBox", () => {
                let parentsLength = this.state.data.parents
                   ? this.state.data.parents.length
                    : 0;
                return [
                    -this.config.width / 2,
                    parentsLength > 0
                       ? -this.config.height / 2
                        : -this.config.height / 3,
                    this.config.width,
                    this.config.height,
                ];
            })
           .style("user-select", "none")
           .style("cursor", "move");

        const gAll = svg.append("g").attr("id", "all");
        svg
           .call(
                d3.zoom()
                   .scaleExtent([0.2, 5])
                   .on("zoom", (e) => {
                        gAll.attr("transform", () => {
                            return `translate(${e.transform.x},${e.transform.y}) scale(${e.transform.k})`;
                        });
                    })
            )
           .on("dblclick.zoom", null);

        this.gAll = gAll;
        this.gLinks = gAll.append("g").attr("id", "linkGroup");
        this.gNodes = gAll.append("g").attr("id", "nodeGroup");

        this.tree = d3.tree().nodeSize([this.config.dx, this.config.dy]);
        this.rootOfDown = d3.hierarchy(
            this.state.data,
            (d) => d.children
        );
        this.rootOfUp = d3.hierarchy(this.state.data, (d) => d.parents);

        this.tree(this.rootOfDown);
        [this.rootOfDown.descendants(), this.rootOfUp.descendants()].forEach(
            (nodes) => {
                nodes.forEach((node) => {
                    node._children = node.children || null;
                    if (options.type === "all") {
                        node.children = node._children;
                    } else if (options.type === "fold") {
                        if (node.depth) {
                            node.children = null;
                        }
                    }
                });
            }
        );

        svg
           .append("marker")
           .attr("id", "markerOfDown")
           .attr("markerUnits", "userSpaceOnUse")
           .attr("viewBox", "0 -5 10 10")
           .attr("refX", 55)
           .attr("refY", 0)
           .attr("markerWidth", 10)
           .attr("markerHeight", 10)
           .attr("orient", "90")
           .attr("stroke-width", 2)
           .append("path")
           .attr("d", "M0,-5L10,0L0,5")
           .attr("fill", "#215af3");

        svg
           .append("marker")
           .attr("id", "markerOfUp")
           .attr("markerUnits", "userSpaceOnUse")
           .attr("viewBox", "0 -5 10 10")
           .attr("refX", -50)
           .attr("refY", 0)
           .attr("markerWidth", 10)
           .attr("markerHeight", 10)
           .attr("orient", "90")
           .attr("stroke-width", 2)
           .append("path")
           .attr("d", "M0,-5L10,0L0,5")
           .attr("fill", "#215af3");

        this.svg = svg;
        this.update();
        host.append(() => svg.node());
    };

    update = (source) => {
        if (!source) {
            source = {
                x0: 0,
                y0: 0,
            };
            this.rootOfDown.x0 = 0;
            this.rootOfDown.y0 = 0;
            this.rootOfUp.x0 = 0;
            this.rootOfUp.y0 = 0;
        }

        let nodesOfDown = this.rootOfDown.descendants().reverse();
        let linksOfDown = this.rootOfDown.links();
        let nodesOfUp = this.rootOfUp.descendants().reverse();
        let linksOfUp = this.rootOfUp.links();

        this.tree(this.rootOfDown);
        this.tree(this.rootOfUp);

        const myTransition = this.svg.transition().duration(500);

        const node1 = this.gNodes
           .selectAll("g.nodeOfDownItemGroup")
           .data(nodesOfDown, (d) => d.data.id);

        const node1Enter = node1
           .enter()
           .append("g")
           .attr("class", "nodeOfDownItemGroup")
           .attr("transform", (d) => `translate(${source.x0},${source.y0})`)
           .attr("fill-opacity", 0)
           .attr("stroke-opacity", 0)
           .style("cursor", "pointer");

        node1Enter
           .append("rect")
           .attr("width", (d) => {
                if (d.depth === 0) {
                    return (d.data.name.length + 2) * 16;
                }
                return this.config.rectWidth;
            })
           .attr("height", (d) => {
                if (d.depth === 0) {
                    return 30;
                }
                return this.config.rectHeight;
            })
           .attr("x", (d) => {
                if (d.depth === 0) {
                    return (-(d.data.name.length + 2) * 16) / 2;
                }
                return -this.config.rectWidth / 2;
            })
           .attr("y", (d) => {
                if (d.depth === 0) {
                    return -15;
                }
                return -this.config.rectHeight / 2;
            })
           .attr("rx", 5)
           .attr("stroke-width", 1)
           .attr("stroke", (d) => {
                if (d.depth === 0) {
                    return "#5682ec";
                }
                return "#7A9EFF";
            })
           .attr("fill", (d) => {
                if (d.depth === 0) {
                    return "#7A9EFF";
                }
                return "#FFFFFF";
            })
           .on("click", (e, d) => this.nodeClickEvent(e, d));

        node1Enter
           .append("text")
           .attr("class", "main-title")
           .attr("x", (d) => 0)
           .attr("y", (d) => {
                if (d.depth === 0) {
                    return 5;
                }
                return -14;
            })
           .attr("text-anchor", (d) => "middle")
           .text((d) => {
                if (d.depth === 0) {
                    return d.data.name;
                } else {
                    return d.data.name.length > 11
                       ? d.data.name.substring(0, 11)
                        : d.data.name;
                }
            })
           .attr("fill", (d) => {
                if (d.depth === 0) {
                    return "#FFFFFF";
                }
                return "#000000";
            })
           .style("font-size", (d) => (d.depth === 0? 16 : 14))
           .style("font-family", "黑体")
           .style("font-weight", "bold");

        node1Enter
           .append("text")
           .attr("class", "sub-title")
           .attr("x", (d) => 0)
           .attr("y", (d) => 5)
           .attr("text-anchor", (d) => "middle")
           .text((d) => {
                if (d.depth!== 0) {
                    let subTitle = d.data.name.substring(11);
                    if (subTitle.length > 10) {
                        return subTitle.substring(0, 10) + "...";
                    }
                    return subTitle;
                }
            })
           .style("font-size", (d) => 14)
           .style("font-family", "黑体")
           .style("font-weight", "bold");

        node1Enter
           .append("text")
           .attr("class", "percent")
           .attr("x", (d) => 12)
           .attr("y", (d) => -45)
           .text((d) => {
                if (d.depth!== 0) {
                    return d.data.percent;
                }
            })
           .attr("fill", "#000000")
           .style("font-family", "黑体")
           .style("font-size", (d) => 14);

        const expandBtnG = node1Enter
           .append("g")
           .attr("class", "expandBtn")
           .attr("transform", (d) => `translate(${0},${this.config.rectHeight / 2})`)
           .style("display", (d) => {
                if (d.depth === 0) {
                    return "none";
                }
                if (!d._children) {
                    return "none";
                }
            })
           .on("click", (e, d) => {
                if (d.children) {
                    d._children = d.children;
                    d.children = null;
                } else {
                    d.children = d._children;
                }
                this.update(d);
            });

        expandBtnG
           .append("circle")
           .attr("r", 8)
           .attr("fill", "#7A9EFF")
           .attr("cy", 8);

        expandBtnG
           .append("text")
           .attr("text-anchor", "middle")
           .attr("fill", "#ffffff")
           .attr("y", 13)
           .style("font-size", 16)
           .style("font-family", "微软雅黑")
           .text((d) => d.children? "-" : "+");

        const link1 = this.gLinks
           .selectAll("path.linkOfDownItem")
           .data(linksOfDown, (d) => d.target.data.id);

        const link1Enter = link1
           .enter()
           .append("path")
           .attr("class", "linkOfDownItem")
           .attr("d", (d) => {
                let o = {
                    source: {
                        x: source.x0,
                        y: source.y0,
                    },
                    target: {
                        x: source.x0,
                        y: source.y0,
                    },
                };
                return this.drawLink(o);
            })
           .attr("fill", "none")
           .attr("stroke", "#7A9EFF")
           .attr("stroke-width", 1)
           .attr("marker-end", "url(#markerOfDown)");

        node1
           .merge(node1Enter)
           .transition(myTransition)
           .attr("transform", (d) => `translate(${d.x},${d.y})`)
           .attr("fill-opacity", 1)
           .attr("stroke-opacity", 1);

        node1
           .exit()
           .transition(myTransition)
           .remove()
           .attr("transform", (d) => `translate(${source.x0},${source.y0})`)
           .attr("fill-opacity", 0)
           .attr("stroke-opacity", 0);

        link1.merge(link1Enter).transition(myTransition).attr("d", this.drawLink);

        link1
           .exit()
           .transition(myTransition)
           .remove()
           .attr("d", (d) => {
                let o = {
                    source: {
                        x: source.x,
                        y: source.y,
                    },
                    target: {
                        x: source.x,
                        y: source.y,
                    },
                };
                return this.drawLink(o);
            });

        nodesOfUp.forEach((node) => {
            node.y = -node.y;
        });

        const node2 = this.gNodes
           .selectAll("g.nodeOfUpItemGroup")
           .data(nodesOfUp, (d) => d.data.id);

        const node2Enter = node2
           .enter()
           .append("g")
           .attr("class", "nodeOfUpItemGroup")
           .attr("transform", (d) => `translate(${source.x0},${source.y0})`)
           .attr("fill-opacity", 0)
           .attr("stroke-opacity", 0)
           .style("cursor", "pointer");

        node2Enter
           .append("rect")
           .attr("width", (d) => {
                if (d.depth === 0) {
                    return (d.data.name.length + 2) * 16;
                }
                return this.config.rectWidth;
            })
           .attr("height", (d) => {
                if (d.depth === 0) {
                    return 30;
                }
                return this.config.rectHeight;
            })
           .attr("x", (d) => {
                if (d.depth === 0) {
                    return (-(d.data.name.length + 2) * 16) / 2;
                }
                return -this.config.rectWidth / 2;
            })
           .attr("y", (d) => {
                if (d.depth === 0) {
                    return -15;
                }
                return -this.config.rectHeight / 2;
            })
           .attr("rx", 5)
           .attr("stroke-width", 1)
           .attr("stroke", (d) => {
                if (d.depth === 0) {
                    return "#5682ec";
                }
                return "#7A9EFF";
            })
           .attr("fill", (d) => {
                if (d.depth === 0) {
                    return "#7A9EFF";
                }
                return "#FFFFFF";
            })
           .on("click", (e, d) => this.nodeClickEvent(e, d));

        node2Enter
           .append("text")
           .attr("class", "main-title")
           .attr("x", (d) => 0)
           .attr("y", (d) => {
                if (d.depth === 0) {
                    return 5;
                }
                return -14;
            })
           .attr("text-anchor", (d) => "middle")
           .text((d) => {
                if (d.depth === 0) {
                    return d.data.name;
                } else {
                    return d.data.name.length > 11
                       ? d.data.name.substring(0, 11)
                        : d.data.name;
                }
            })
           .attr("fill", (d) => {
                if (d.depth === 0) {
                    return "#FFFFFF";
                }
                return "#000000";
            })
           .style("font-size", (d) => (d.depth === 0? 16 : 14))
           .style("font-family", "黑体")
           .style("font-weight", "bold");

        node2Enter
           .append("text")
           .attr("class", "sub-title")
           .attr("x", (d) => 0)
           .attr("y", (d) => 5)
           .attr("text-anchor", (d) => "middle")
           .text((d) => {
                if (d.depth!== 0) {
                    let subTitle = d.data.name.substring(11);
                    if (subTitle.length > 10) {
                        return subTitle.substring(0, 10) + "...";
                    }
                    return subTitle;
                }
            })
           .style("font-size", (d) => 14)
           .style("font-family", "黑体")
           .style("font-weight", "bold");

        node2Enter
           .append("text")
           .attr("class", "percent")
           .attr("x", (d) => 12)
           .attr("y", (d) => 55)
           .text((d) => {
                if (d.depth!== 0) {
                    return d.data.percent;
                }
            })
           .attr("fill", "#000000")
           .style("font-family", "黑体")
           .style("font-size", (d) => 14);

        const expandBtnG2 = node2Enter
           .append("g")
           .attr("class", "expandBtn")
           .attr("transform", (d) => `translate(${0},${-this.config.rectHeight / 2})`)
           .style("display", (d) => {
                if (d.depth === 0) {
                    return "none";
                }
                if (!d._children) {
                    return "none";
                }
            })
           .on("click", (e, d) => {
                if (d.children) {
                    d._children = d.children;
                    d.children = null;
                } else {
                    d.children = d._children;
                }
                this.update(d);
            });

        expandBtnG2
           .append("circle")
           .attr("r", 8)
           .attr("fill", "#7A9EFF")
           .attr("cy", -8);

        expandBtnG2
           .append("text")
           .attr("text-anchor", "middle")
           .attr("fill", "#ffffff")
           .attr("y", -3)
           .style("font-size", 16)
           .style("font-family", "微软雅黑")
           .text((d) => d.children? "-" : "+");

        const link2 = this.gLinks
           .selectAll("path.linkOfUpItem")
           .data(linksOfUp, (d) => d.target.data.id);

        const link2Enter = link2
           .enter()
           .append("path")
           .attr("class", "linkOfUpItem")
           .attr("d", (d) => {
                let o = {
                    source: {
                        x: source.x0,
                        y: source.y0,
                    },
                    target: {
                        x: source.x0,
                        y: source.y0,
                    },
                };
                return this.drawLink(o);
            })
           .attr("fill", "none")
           .attr("stroke", "#7A9EFF")
           .attr("stroke-width", 1)
           .attr("marker-end", "url(#markerOfUp)");

        node2
           .merge(node2Enter)
           .transition(myTransition)
           .attr("transform", (d) => `translate(${d.x},${d.y})`)
           .attr("fill-opacity", 1)
           .attr("stroke-opacity", 1);

        node2
           .exit()
           .transition(myTransition)
           .remove()
           .attr("transform", (d) => `translate(${source.x0},${source.y0})`)
           .attr("fill-opacity", 0)
           .attr("stroke-opacity", 0);

        link2.merge(link2Enter).transition(myTransition).attr("d", this.drawLink);

        link2
           .exit()
           .transition(myTransition)
           .remove()
           .attr("d", (d) => {
                let o = {
                    source: {
                        x: source.x,
                        y: source.y,
                    },
                    target: {
                        x: source.x,
                        y: source.y,
                    },
                };
                return this.drawLink(o);
            });

        const expandButtonsSelection = d3.selectAll("g.expandBtn");
        expandButtonsSelection
           .select("text")
           .transition()
           .text((d) => d.children? "-" : "+");

        this.rootOfDown.eachBefore((d) => {
            d.x0 = d.x;
            d.y0 = d.y;
        });
        this.rootOfUp.eachBefore((d) => {
            d.x0 = d.x;
            d.y0 = d.y;
        });
    };

    drawLink = ({ source, target }) => {
        const halfDistance = (target.y - source.y) / 2;
        const halfY = source.y + halfDistance;
        return `M${source.x},${source.y} L${source.x},${halfY} ${target.x},${halfY} ${target.x},${target.y}`;
    };

    nodeClickEvent = (e, d) => {
        console.log("当前节点的数据:", d);
    };

    render() {
        return (
            <div
                ref={(el) => this.container = el}
                style={{ height: '650px' }}
            ></div>
        );
    }
}

export default LegalPerson;    

参考