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;
相关推荐
邹诗钰-电子信息工程9 分钟前
嵌入式自学第二十一天(5.14)
java·开发语言·算法
恋猫de小郭30 分钟前
如何查看项目是否支持最新 Android 16K Page Size 一文汇总
android·开发语言·javascript·kotlin
赵大仁1 小时前
React Native 与 Expo
javascript·react native·react.js
于壮士hoho1 小时前
Python | Dashboard制作
开发语言·python
程序员与背包客_CoderZ2 小时前
Node.js异步编程——Callback回调函数实现
前端·javascript·node.js·web
Asus.Blogs2 小时前
为什么go语言中返回的指针类型,不需要用*取值(解引用),就可以直接赋值呢?
开发语言·后端·golang
青瓦梦滋2 小时前
【语法】C++的多态
开发语言·c++
C_V_Better2 小时前
Java Spring Boot 控制器中处理用户数据详解
java·开发语言·spring boot·后端·spring
t198751282 小时前
基于Qt的OSG三维建模
java·开发语言
清灵xmf3 小时前
从 Set、Map 到 WeakSet、WeakMap 的进阶之旅
前端·javascript·set·map·weakset·weakmap