实现组织架构图
效果
初始化组织机构容器并实现缩放平移功能
效果
源码
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;