Organization Chart
Loading example…
import { Badge, BaseBehavior, ExtensionCategory, Graph, GraphEvent, Label, Rect, register } from '@antv/g6';
const statusColors = {
online: '#17BEBB',
busy: '#E36397',
offline: '#B7AD99',
};
const DEFAULT_LEVEL = 'detailed';
/**
* Draw a chart node with different ui based on the zoom level.
*/
class ChartNode extends Rect {
get data() {
return this.context.model.getElementDataById(this.id).data;
}
get level() {
return this.data.level || DEFAULT_LEVEL;
}
getLabelStyle() {
const text = this.data.name;
const labelStyle =
this.level === 'overview'
? {
fill: '#fff',
fontSize: 20,
fontWeight: 600,
textAlign: 'center',
transform: [['translate', 0, 0]],
}
: {
fill: '#2078B4',
fontSize: 14,
fontWeight: 400,
textAlign: 'left',
transform: [['translate', -65, -15]],
};
return { text, ...labelStyle };
}
getKeyStyle(attributes) {
return {
...super.getKeyStyle(attributes),
fill: this.level === 'overview' ? statusColors[this.data.status] : '#fff',
};
}
getPositionStyle(attributes) {
if (this.level === 'overview') return false;
return {
text: this.data.position,
fontSize: 8,
fontWeight: 400,
textTransform: 'uppercase',
fill: '#343f4a',
textAlign: 'left',
transform: [['translate', -65, 0]],
};
}
drawPositionShape(attributes, container) {
const positionStyle = this.getPositionStyle(attributes);
this.upsert('position', Label, positionStyle, container);
}
getStatusStyle(attributes) {
if (this.level === 'overview') return false;
return {
text: this.data.status,
fontSize: 8,
textAlign: 'left',
transform: [['translate', 40, -16]],
padding: [0, 4],
fill: '#fff',
backgroundFill: statusColors[this.data.status],
};
}
drawStatusShape(attributes, container) {
const statusStyle = this.getStatusStyle(attributes);
this.upsert('status', Badge, statusStyle, container);
}
getPhoneStyle(attributes) {
if (this.level === 'overview') return false;
return {
text: this.data.phone,
fontSize: 8,
fontWeight: 300,
textAlign: 'left',
transform: [['translate', -65, 20]],
};
}
drawPhoneShape(attributes, container) {
const style = this.getPhoneStyle(attributes);
this.upsert('phone', Label, style, container);
}
render(attributes = this.parsedAttributes, container = this) {
super.render(attributes, container);
this.drawPositionShape(attributes, container);
this.drawStatusShape(attributes, container);
this.drawPhoneShape(attributes, container);
}
}
/**
* Implement a level of detail rendering, which will show different details based on the zoom level.
*/
class LevelOfDetail extends BaseBehavior {
prevLevel = DEFAULT_LEVEL;
levels = {
['overview']: [0, 0.6],
['detailed']: [0.6, Infinity],
};
constructor(context, options) {
super(context, options);
this.bindEvents();
}
update(options) {
this.unbindEvents();
super.update(options);
this.bindEvents();
}
updateZoomLevel = async (e) => {
if ('scale' in e.data) {
const scale = e.data.scale;
const level = Object.entries(this.levels).find(([key, [min, max]]) => scale > min && scale <= max)?.[0];
if (level && this.prevLevel !== level) {
const { graph } = this.context;
graph.updateNodeData((prev) => prev.map((node) => ({ ...node, data: { ...node.data, level } })));
await graph.draw();
this.prevLevel = level;
}
}
};
bindEvents() {
const { graph } = this.context;
graph.on(GraphEvent.AFTER_TRANSFORM, this.updateZoomLevel);
}
unbindEvents() {
const { graph } = this.context;
graph.off(GraphEvent.AFTER_TRANSFORM, this.updateZoomLevel);
}
destroy() {
this.unbindEvents();
super.destroy();
}
}
register(ExtensionCategory.NODE, 'chart-node', ChartNode);
register(ExtensionCategory.BEHAVIOR, 'level-of-detail', LevelOfDetail);
fetch('https://assets.antv.antgroup.com/g6/organization-chart.json')
.then((res) => res.json())
.then((data) => {
const graph = new Graph({
container: 'container',
data,
node: {
type: 'chart-node',
style: {
labelPlacement: 'center',
lineWidth: 1,
ports: [{ placement: 'top' }, { placement: 'bottom' }],
radius: 2,
shadowBlur: 10,
shadowColor: '#e0e0e0',
shadowOffsetX: 3,
size: [150, 60],
stroke: '#C0C0C0',
},
},
edge: {
type: 'polyline',
style: {
router: {
type: 'orth',
},
stroke: '#C0C0C0',
},
},
layout: {
type: 'dagre',
},
autoFit: 'view',
behaviors: ['level-of-detail', 'zoom-canvas', 'drag-canvas'],
});
graph.render();
});
import { Graph } from '@antv/g6';
import GUI from 'lil-gui';
export function addPanel(renderPanel: (gui: GUI) => void) {
const gui = new GUI({ container: document.body });
gui.title('Control');
Object.assign(gui.domElement.style, { position: 'absolute', top: '0', right: '0', zIndex: '10' });
renderPanel(gui);
addEventListener('pagehide', () => gui.destroy(), { once: true });
}
export async function createGraph(
options: ConstructorParameters<typeof Graph>[0],
size: { width?: number; height?: number } = {},
renderPanel?: (gui: GUI, graph: Graph) => void,
) {
const container = document.createElement('div');
Object.assign(container.style, {
width: '100%',
maxWidth: `${size.width || 600}px`,
height: `${size.height || 400}px`,
});
document.getElementById('container')!.append(container);
const graph = new Graph({ ...size, ...options, width: container.clientWidth, container, autoResize: true });
addEventListener('pagehide', () => graph.destroy(), { once: true });
await graph.render();
if (renderPanel) addPanel((gui) => renderPanel(gui, graph));
return container;
}