实现组织架构图
- 效果
- 初始化组织机构容器并实现缩放平移功能
- 效果
- 源码
- 渲染节点
- 效果
- 源码
- 渲染连线
- 效果
- 源码
- 完整源码
效果
初始化组织机构容器并实现缩放平移功能
效果
源码
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);// 初始化svgconst _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;
渲染节点
效果
源码
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);// 初始化svgconst _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;
渲染连线
效果
源码
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);// 初始化svgconst _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;
完整源码
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);// 初始化svgconst _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;