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;
相关推荐
用户22152044278006 分钟前
new、原型和原型链浅析
前端·javascript
阿星做前端6 分钟前
coze源码解读: space develop 页面
前端·javascript
叫我小窝吧6 分钟前
Promise 的使用
前端·javascript
前端康师傅1 小时前
JavaScript 作用域
前端·javascript
云枫晖2 小时前
JS核心知识-事件循环
前端·javascript
eason_fan2 小时前
Git 大小写敏感性问题:一次组件重命名引发的CI构建失败
前端·javascript
前端付豪4 小时前
1、震惊!99% 前端都没搞懂的 JavaScript 类型细节
前端·javascript·面试
朝与暮4 小时前
js符号(Symbol)
前端·javascript
大怪v5 小时前
前端:人工智能?我也会啊!来个花活,😎😎😎“自动驾驶”整起!
前端·javascript·算法
遂心_6 小时前
为什么 '1'.toString() 可以调用?深入理解 JavaScript 包装对象机制
前端·javascript