D3.js(五):实现组织架构图

实现组织架构图

效果


初始化组织机构容器并实现缩放平移功能

效果

源码

ts 复制代码
import {useEffect} from 'react';
import TreeData from './json/tree-data.json';

interface ITreeConfig {
    k: number,
    x: number,
    y: number,
}

interface ITreeData {
    _id?: number,
    name: string,
    children?: ITreeData[] | null,
    _children?: ITreeData[] | null,
}

const d3 = window.d3;

const TreeConfig: ITreeConfig = {
    k: 1,
    x: 0,
    y: 0,
};

function OrgChart() {

    const init = (data: ITreeData, config: ITreeConfig) => {
        console.log(data);
        // 初始化svg
        const _svg = d3.select('#org-chart-svg').html('');
        // 获取初始化svg的宽高
        const {clientWidth: width, clientHeight: height} = _svg.node() as SVGElement;
        const svg = _svg.attr('viewBox', [0, 0, width, height]);
        // 渲染组织机构树数据的容器
        const treeGroup = svg.append('g').attr('class', 'tree-group');

        treeGroup.append('rect').attr('width', 200).attr('height', 100).attr('fill', 'yellow');

        // 缩放
        {
            // 缩放移动监听
            const zoom = d3.zoom().scaleExtent([.1, 5]).on('zoom', e => {
                config.k = e.transform.k;
                treeGroup.attr('transform', e.transform);
            });
            const transform = d3.zoomIdentity.translate(config.x, config.y).scale(config.k);
            zoom.transform(svg as never, transform, [config.x, config.y]);
            // 监听缩放拖拽并禁用双击放大功能
            svg.call(zoom as never).on('dblclick.zoom', null);
        }
    };

    const formatTree = (() => {
        let count = 0;
        return function callback(data: ITreeData): ITreeData {
            return {
                ...data,
                _id: count++,
                children: (data.children || []).map(d => callback(d)),
                _children: (data.children || []).map(d => callback(d)),
            };
        };
    })();

    useEffect(() => {
        init(formatTree(TreeData), TreeConfig);
    }, [formatTree]);

    return <>
        <svg id="org-chart-svg" className="tree-svg" width="100%" height="100%"></svg>
    </>;
}

export default OrgChart;

渲染节点

效果

源码

ts 复制代码
import {useEffect} from 'react';
import TreeData from './json/tree-data.json';

interface ITreeConfig {
    nodeWidth: number,
    nodeHeight: number,
    spaceX: number,
    spaceY: number,
    k: number,
    x: number,
    y: number,
    duration: number,
}

interface ITreeData {
    _id?: number,
    name: string,
    children?: ITreeData[] | null,
    _x0?: number,
    _y0?: number,
}

const d3 = window.d3;

const TreeConfig: ITreeConfig = {
    nodeWidth: 200,
    nodeHeight: 100,
    spaceX: 60,
    spaceY: 100,
    k: 1,
    x: 0,
    y: 0,
    duration: 500,
};

function OrgChart() {

    const init = (data: ITreeData, config: ITreeConfig) => {
        console.log(data);
        // 初始化svg
        const _svg = d3.select('#org-chart-svg').html('');
        // 获取初始化svg的宽高
        const {clientWidth: width, clientHeight: height} = _svg.node() as SVGElement;
        const svg = _svg.attr('viewBox', [0, 0, width, height]);
        // 渲染组织机构树数据的容器
        const treeGroup = svg.append('g').attr('class', 'tree-group');
        // 节点组
        const nodeGroup = treeGroup.append('g').attr('class', 'node-group');
        // 节点组通用样式
        nodeGroup.attr('transform', `translate(${(width - config.nodeWidth) / 2}, 20)`).attr('fill', 'rgba(22,119,255,0.6)');

        // 缩放
        {
            // 缩放移动监听
            const zoom = d3.zoom().scaleExtent([.1, 5]).on('zoom', e => {
                treeGroup.attr('transform', e.transform);
            });
            const transform = d3.zoomIdentity.translate(config.x, config.y).scale(config.k);
            zoom.transform(svg as never, transform, [config.x, config.y]);
            // 监听缩放拖拽并禁用双击放大功能
            svg.call(zoom as never).on('dblclick.zoom', null);
        }

        // 将数据处理成存在位置信息的数据
        const root = d3.hierarchy(data);
        // 定义节点尺寸
        const tree = d3.tree().nodeSize([config.nodeWidth + config.spaceX, config.nodeHeight + config.spaceY]);

        // 初始化节点数据,默认仅展示一个节点
        root.data._x0 = 0;
        root.data._y0 = 0;
        root.descendants().forEach(d => {
            d.data.children = d.children as unknown as ITreeData[];
            d.children = undefined;
        });

        // 更新节点
        function update(source: d3.HierarchyNode<ITreeData>) {
            // 动画时间
            const transition = svg.transition().duration(config.duration);
            // 全部节点
            const nodes = root.descendants();
            // 处理数据添加坐标
            tree(root as never);
            // 处理渲染前数据
            root.eachBefore(d => {
                d.data._x0 = d.x || 0;
                d.data._y0 = d.y || 0;
            });

            // 节点处理
            {
                const node = nodeGroup.selectChildren('g').data(nodes, d => (d as never)['data']['_id']);
                const nodeEnter = node.enter().append('g')
                    .attr('opacity', 0)
                    .attr('transform', `translate(${source.data._x0},${source.data._y0})`);
                nodeEnter.append('rect')
                    .attr('width', config.nodeWidth)
                    .attr('height', config.nodeHeight)
                    .on('click', (_e, d) => {
                        (d.children as unknown) = d.children ? null : d.data.children;
                        update(d);
                    });
                nodeEnter.append('text')
                    .text(d => d.data.name)
                    .attr('x', 20)
                    .attr('y', 30)
                    .attr('fill', 'red');
                node.merge(nodeEnter as never).transition(transition)
                    .attr('opacity', 1)
                    .attr('transform', d => `translate(${d.x},${d.y})`);
                node.exit().transition(transition).remove()
                    .attr('opacity', 0)
                    .attr('transform', `translate(${source.x},${source.y})`);
            }
        }

        update(root);
    };

    const formatTree = (() => {
        let count = 0;
        return function callback(data: ITreeData): ITreeData {
            return {
                ...data,
                _id: count++,
                children: (data.children || []).map(d => callback(d)),
            };
        };
    })();

    useEffect(() => {
        init(formatTree(TreeData), TreeConfig);
    }, [formatTree]);

    return <>
        <svg id="org-chart-svg" className="tree-svg" width="100%" height="100%"></svg>
    </>;
}

export default OrgChart;

渲染连线

效果

源码

ts 复制代码
import {useEffect} from 'react';
import TreeData from './json/tree-data.json';

interface ITreeConfig {
    nodeWidth: number,
    nodeHeight: number,
    spaceX: number,
    spaceY: number,
    k: number,
    x: number,
    y: number,
    duration: number,
}

interface ITreeData {
    _id?: number,
    name: string,
    children?: ITreeData[] | null,
    _x0?: number,
    _y0?: number,
}

const d3 = window.d3;

const TreeConfig: ITreeConfig = {
    nodeWidth: 200,
    nodeHeight: 100,
    spaceX: 60,
    spaceY: 100,
    k: 1,
    x: 0,
    y: 0,
    duration: 500,
};

function OrgChart() {

    const init = (data: ITreeData, config: ITreeConfig) => {
        console.log(data);
        // 初始化svg
        const _svg = d3.select('#org-chart-svg').html('');
        // 获取初始化svg的宽高
        const {clientWidth: width, clientHeight: height} = _svg.node() as SVGElement;
        const svg = _svg.attr('viewBox', [0, 0, width, height]);
        // 渲染组织机构树数据的容器
        const treeGroup = svg.append('g').attr('class', 'tree-group');
        // 连线组
        const linkGroup = treeGroup.append('g').attr('class', 'link-group');
        // 连线组通用样式
        linkGroup.attr('transform', `translate(${(width - config.nodeWidth) / 2}, 20)`).attr('fill', 'none').attr('stroke', 'red').attr('stroke-width', 3);
        // 节点组
        const nodeGroup = treeGroup.append('g').attr('class', 'node-group');
        // 节点组通用样式
        nodeGroup.attr('transform', `translate(${(width - config.nodeWidth) / 2}, 20)`).attr('fill', 'rgba(22,119,255,0.6)');

        // 缩放
        {
            // 缩放移动监听
            const zoom = d3.zoom().scaleExtent([.1, 5]).on('zoom', e => {
                treeGroup.attr('transform', e.transform);
            });
            const transform = d3.zoomIdentity.translate(config.x, config.y).scale(config.k);
            zoom.transform(svg as never, transform, [config.x, config.y]);
            // 监听缩放拖拽并禁用双击放大功能
            svg.call(zoom as never).on('dblclick.zoom', null);
        }

        // 将数据处理成存在位置信息的数据
        const root = d3.hierarchy(data);
        // 定义节点尺寸
        const tree = d3.tree().nodeSize([config.nodeWidth + config.spaceX, config.nodeHeight + config.spaceY]);

        // 初始化节点数据,默认仅展示一个节点
        root.data._x0 = 0;
        root.data._y0 = 0;
        root.descendants().forEach(d => {
            d.data.children = d.children as unknown as ITreeData[];
            d.children = undefined;
        });

        // 更新节点
        function update(source: d3.HierarchyNode<ITreeData>) {
            // 动画时间
            const transition = svg.transition().duration(config.duration);
            // 全部节点
            const nodes = root.descendants();
            // 全部连线
            const links = root.links();
            // 处理数据添加坐标
            tree(root as never);
            // 处理渲染前数据
            root.eachBefore(d => {
                d.data._x0 = d.x || 0;
                d.data._y0 = d.y || 0;
            });

            // 节点处理
            {
                const node = nodeGroup.selectChildren('g').data(nodes, d => (d as never)['data']['_id']);
                const nodeEnter = node.enter().append('g')
                    .attr('opacity', 0)
                    .attr('transform', `translate(${source.data._x0},${source.data._y0})`);
                nodeEnter.append('rect')
                    .attr('width', config.nodeWidth)
                    .attr('height', config.nodeHeight)
                    .on('click', (_e, d) => {
                        (d.children as unknown) = d.children ? null : d.data.children;
                        update(d);
                    });
                nodeEnter.append('text')
                    .text(d => d.data.name)
                    .attr('x', 20)
                    .attr('y', 30)
                    .attr('fill', 'red');
                node.merge(nodeEnter as never).transition(transition)
                    .attr('opacity', 1)
                    .attr('transform', d => `translate(${d.x},${d.y})`);
                node.exit().transition(transition).remove()
                    .attr('opacity', 0)
                    .attr('transform', `translate(${source.x},${source.y})`);
            }
            // 连线处理
            {
                const link = linkGroup.selectChildren('path').data(links, d => (d as never)['target']['data']['_id']);
                const linkEnter = link.enter().append('path')
                    .attr('opacity', 0)
                    .attr('d', d => {
                        return [
                            `M${(d.source.data._x0 || 0) + config.nodeWidth / 2}, ${(d.source.data._y0 || 0) + config.nodeHeight}`, // 开始点
                            `L${(d.source.data._x0 || 0) + config.nodeWidth / 2}, ${(d.source.data._y0 || 0) + config.nodeHeight}`, // 开始的转折点
                            `L${(d.source.data._x0 || 0) + config.nodeWidth / 2}, ${(d.source.data._y0 || 0) + config.nodeHeight}`, // 结束的转折点
                            `L${(d.source.data._x0 || 0) + config.nodeWidth / 2}, ${(d.source.data._y0 || 0) + config.nodeHeight}`, // 结束点
                        ].join();
                    });
                link.merge(linkEnter as never).transition(transition)
                    .attr('opacity', 1)
                    .attr('d', d => {
                        return [
                            `M${(d.source.x || 0) + config.nodeWidth / 2}, ${(d.source.y || 0) + config.nodeHeight}`,                       // 开始点
                            `L${(d.source.x || 0) + config.nodeWidth / 2}, ${(d.source.y || 0) + config.nodeHeight + config.spaceY / 2}`,   // 开始点
                            `L${(d.target.x || 0) + config.nodeWidth / 2}, ${(d.source.y || 0) + config.nodeHeight + config.spaceY / 2}`,   // 结束的转折开始点
                            `L${(d.target.x || 0) + config.nodeWidth / 2}, ${(d.target.y || 0)}`,                                           // 结束点
                        ].join();
                    });
                link.exit().transition(transition).remove()
                    .attr('opacity', 0)
                    .attr('d', d => {
                        const t = d as d3.HierarchyLink<ITreeData>;
                        return [
                            `M${(t.source.x || 0) + config.nodeWidth / 2}, ${(t.source.y || 0) + config.nodeHeight}`, // 开始点
                            `L${(t.source.x || 0) + config.nodeWidth / 2}, ${(t.source.y || 0) + config.nodeHeight}`, // 开始的转折点
                            `L${(t.source.x || 0) + config.nodeWidth / 2}, ${(t.source.y || 0) + config.nodeHeight}`, // 结束的转折点
                            `L${(t.source.x || 0) + config.nodeWidth / 2}, ${(t.source.y || 0) + config.nodeHeight}`, // 结束点
                        ].join();
                    });
            }
        }

        update(root);
    };

    const formatTree = (() => {
        let count = 0;
        return function callback(data: ITreeData): ITreeData {
            return {
                ...data,
                _id: count++,
                children: (data.children || []).map(d => callback(d)),
            };
        };
    })();

    useEffect(() => {
        init(formatTree(TreeData), TreeConfig);
    }, [formatTree]);

    return <>
        <svg id="org-chart-svg" className="tree-svg" width="100%" height="100%"></svg>
    </>;
}

export default OrgChart;

完整源码

ts 复制代码
import {useEffect} from 'react';
import TreeData from './json/tree-data.json';

interface ITreeConfig {
    nodeWidth: number,
    nodeHeight: number,
    spaceX: number,
    spaceY: number,
    k: number,
    x: number,
    y: number,
    duration: number,
}

interface ITreeData {
    _id?: number,
    name: string,
    children?: ITreeData[] | null,
    _x0?: number,
    _y0?: number,
}

const d3 = window.d3;

const TreeConfig: ITreeConfig = {
    nodeWidth: 200,
    nodeHeight: 100,
    spaceX: 60,
    spaceY: 100,
    k: 1,
    x: 0,
    y: 0,
    duration: 500,
};

function OrgChart() {

    const init = (data: ITreeData, config: ITreeConfig) => {
        console.log(data);
        // 初始化svg
        const _svg = d3.select('#org-chart-svg').html('');
        // 获取初始化svg的宽高
        const {clientWidth: width, clientHeight: height} = _svg.node() as SVGElement;
        const svg = _svg.attr('viewBox', [0, 0, width, height]);
        // 渲染组织机构树数据的容器
        const treeGroup = svg.append('g').attr('class', 'tree-group');
        // 连线组
        const linkGroup = treeGroup.append('g').attr('class', 'link-group');
        // 连线组通用样式
        linkGroup.attr('transform', `translate(${(width - config.nodeWidth) / 2}, 20)`).attr('fill', 'none').attr('stroke', 'red').attr('stroke-width', 3);
        // 节点组
        const nodeGroup = treeGroup.append('g').attr('class', 'node-group');
        // 节点组通用样式
        nodeGroup.attr('transform', `translate(${(width - config.nodeWidth) / 2}, 20)`).attr('fill', 'rgba(22,119,255,0.6)');

        // 缩放
        {
            // 缩放移动监听
            const zoom = d3.zoom().scaleExtent([.1, 5]).on('zoom', e => {
                treeGroup.attr('transform', e.transform);
            });
            const transform = d3.zoomIdentity.translate(config.x, config.y).scale(config.k);
            zoom.transform(svg as never, transform, [config.x, config.y]);
            // 监听缩放拖拽并禁用双击放大功能
            svg.call(zoom as never).on('dblclick.zoom', null);
        }

        // 将数据处理成存在位置信息的数据
        const root = d3.hierarchy(data);
        // 定义节点尺寸
        const tree = d3.tree().nodeSize([config.nodeWidth + config.spaceX, config.nodeHeight + config.spaceY]);

        // 初始化节点数据,默认仅展示一个节点
        root.data._x0 = 0;
        root.data._y0 = 0;
        root.descendants().forEach(d => {
            d.data.children = d.children as unknown as ITreeData[];
            d.children = undefined;
        });

        // 更新节点
        function update(source: d3.HierarchyNode<ITreeData>) {
            // 动画时间
            const transition = svg.transition().duration(config.duration);
            // 全部节点
            const nodes = root.descendants();
            // 全部连线
            const links = root.links();
            // 处理数据添加坐标
            tree(root as never);
            // 处理渲染前数据
            root.eachBefore(d => {
                d.data._x0 = d.x || 0;
                d.data._y0 = d.y || 0;
            });

            // 节点处理
            {
                const node = nodeGroup.selectChildren('g').data(nodes, d => (d as never)['data']['_id']);
                const nodeEnter = node.enter().append('g')
                    .attr('opacity', 0)
                    .attr('transform', `translate(${source.data._x0},${source.data._y0})`);
                nodeEnter.append('rect')
                    .attr('width', config.nodeWidth)
                    .attr('height', config.nodeHeight)
                    .on('click', (_e, d) => {
                        (d.children as unknown) = d.children ? null : d.data.children;
                        update(d);
                    });
                nodeEnter.append('text')
                    .text(d => d.data.name)
                    .attr('x', 20)
                    .attr('y', 30)
                    .attr('fill', 'red');
                node.merge(nodeEnter as never).transition(transition)
                    .attr('opacity', 1)
                    .attr('transform', d => `translate(${d.x},${d.y})`);
                node.exit().transition(transition).remove()
                    .attr('opacity', 0)
                    .attr('transform', `translate(${source.x},${source.y})`);
            }
            // 连线处理
            {
                const link = linkGroup.selectChildren('path').data(links, d => (d as never)['target']['data']['_id']);
                const linkEnter = link.enter().append('path')
                    .attr('opacity', 0)
                    .attr('d', d => {
                        return [
                            `M${(d.source.data._x0 || 0) + config.nodeWidth / 2}, ${(d.source.data._y0 || 0) + config.nodeHeight}`, // 开始点
                            `L${(d.source.data._x0 || 0) + config.nodeWidth / 2}, ${(d.source.data._y0 || 0) + config.nodeHeight}`, // 开始的转折点
                            `L${(d.source.data._x0 || 0) + config.nodeWidth / 2}, ${(d.source.data._y0 || 0) + config.nodeHeight}`, // 结束的转折点
                            `L${(d.source.data._x0 || 0) + config.nodeWidth / 2}, ${(d.source.data._y0 || 0) + config.nodeHeight}`, // 结束点
                        ].join();
                    });
                link.merge(linkEnter as never).transition(transition)
                    .attr('opacity', 1)
                    .attr('d', d => {
                        return [
                            `M${(d.source.x || 0) + config.nodeWidth / 2}, ${(d.source.y || 0) + config.nodeHeight}`,                       // 开始点
                            `L${(d.source.x || 0) + config.nodeWidth / 2}, ${(d.source.y || 0) + config.nodeHeight + config.spaceY / 2}`,   // 开始点
                            `L${(d.target.x || 0) + config.nodeWidth / 2}, ${(d.source.y || 0) + config.nodeHeight + config.spaceY / 2}`,   // 结束的转折开始点
                            `L${(d.target.x || 0) + config.nodeWidth / 2}, ${(d.target.y || 0)}`,                                           // 结束点
                        ].join();
                    });
                link.exit().transition(transition).remove()
                    .attr('opacity', 0)
                    .attr('d', d => {
                        const t = d as d3.HierarchyLink<ITreeData>;
                        return [
                            `M${(t.source.x || 0) + config.nodeWidth / 2}, ${(t.source.y || 0) + config.nodeHeight}`, // 开始点
                            `L${(t.source.x || 0) + config.nodeWidth / 2}, ${(t.source.y || 0) + config.nodeHeight}`, // 开始的转折点
                            `L${(t.source.x || 0) + config.nodeWidth / 2}, ${(t.source.y || 0) + config.nodeHeight}`, // 结束的转折点
                            `L${(t.source.x || 0) + config.nodeWidth / 2}, ${(t.source.y || 0) + config.nodeHeight}`, // 结束点
                        ].join();
                    });
            }
        }

        update(root);
    };

    const formatTree = (() => {
        let count = 0;
        return function callback(data: ITreeData): ITreeData {
            return {
                ...data,
                _id: count++,
                children: (data.children || []).map(d => callback(d)),
            };
        };
    })();

    useEffect(() => {
        init(formatTree(TreeData), TreeConfig);
    }, [formatTree]);

    return <>
        <svg id="org-chart-svg" className="tree-svg" width="100%" height="100%"></svg>
    </>;
}

export default OrgChart;
相关推荐
梦想科研社5 分钟前
【无人机设计与控制】红嘴蓝鹊优化器RBMO求解无人机路径规划MATLAB
开发语言·matlab·无人机
混迹网络的权某8 分钟前
每天一道C语言精选编程题之求数字的每⼀位之和
c语言·开发语言·考研·算法·改行学it·1024程序员节
一只特立独行的猪6111 小时前
Java面试题——微服务篇
java·开发语言·微服务
喵手3 小时前
Java 与 Oracle 数据泵实操:数据导入导出的全方位指南
java·开发语言·oracle
硬汉嵌入式4 小时前
H7-TOOL的LUA小程序教程第16期:脉冲测量,4路PWM,多路GPIO和波形打印(2024-10-25, 更新完毕)
开发语言·junit·小程序·lua
Wx120不知道取啥名4 小时前
C语言之长整型有符号数与短整型有符号数转换
c语言·开发语言·单片机·mcu·算法·1024程序员节
Python私教5 小时前
Flutter颜色和主题
开发语言·javascript·flutter
代码吐槽菌5 小时前
基于SSM的汽车客运站管理系统【附源码】
java·开发语言·数据库·spring boot·后端·汽车
Ws_5 小时前
蓝桥杯 python day01 第一题
开发语言·python·蓝桥杯
zdkdchao5 小时前
jdk,openjdk,oraclejdk
java·开发语言