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;
相关推荐
2401_857439691 小时前
SSM 架构下 Vue 电脑测评系统:为电脑性能评估赋能
开发语言·php
SoraLuna1 小时前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
xlsw_1 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
燃先生._.2 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
Dream_Snowar3 小时前
速通Python 第三节
开发语言·python
高山我梦口香糖3 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
信号处理学渣4 小时前
matlab画图,选择性显示legend标签
开发语言·matlab
红龙创客4 小时前
某狐畅游24校招-C++开发岗笔试(单选题)
开发语言·c++
jasmine s4 小时前
Pandas
开发语言·python