Members stay visible inside a translucent parent disc. Double-click a disc to collapse or expand it; the members fan out inside without overlapping. Drag a disc to move the whole team.
hierarchical
// Nodes carry a team key; the application does not calculate parent discs or member slots.
// OGJS contains visible members, aggregates collapsed relationships, moves groups as a unit,
// and restores the expanded topology from stable ids.
import { Ogjs } from '../../src/index';
const og = new Ogjs({ container: '#viz', theme: 'light' });
const panel = document.querySelector('.panel');
const PAL = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'];
const TEAM = ['Platform', 'Payments', 'Identity', 'Data', 'Mobile'];
const nodes = [], edges = [];
let id = 0;
for (let t = 0; t < 5; t++) {
const first = id;
const count = 5 + t;
for (let k = 0; k < count; k++) {
nodes.push({ id: id, size: k === 0 ? 8 : 5, color: PAL[t],
label: TEAM[t].slice(0, 3).toLowerCase() + '-' + k, data: { team: TEAM[t] } });
if (k > 0) edges.push({ source: id, target: first + ((k - 1) >> 1) });
id++;
}
if (t > 0) edges.push({ source: first, target: 0, color: '#94a3b899' });
}
og.setGraph({ nodes, edges });
// Membership comes from domain data. OGJS creates and arranges the group discs.
await og.arrangeVisualGroups(({ data }) => data.team, { duration: 0 });
// Retained framing keeps every disc visible without placing a team beneath the controls.
og.camera.fit({ target: 'visible', avoid: panel, paddingPx: 24, retain: true });
const info = document.querySelector('#info');
info.textContent = 'double-click a disc to collapse or expand it · drag a disc to move the whole team';
og.on('nodedblclick', async ({ nodeId }) => {
const key = og.visualGroups.keyOfParent(nodeId) ?? og.visualGroups.keyOfMember(nodeId);
if (!key) return;
const grp = og.visualGroups.get(key);
info.textContent = (grp.collapsed ? 'expanding ' : 'collapsing ') + key + '…';
await og.toggleVisualGroup(key);
info.textContent = key + ' ' + (og.visualGroups.get(key).collapsed ? 'collapsed' : 'expanded');
});
document.querySelector('#collapseAll').onclick = async () => {
for (const grp of og.visualGroups.all) if (!grp.collapsed) await og.collapseVisualGroup(grp.key, 320);
og.camera.fit({ target: 'visible', avoid: panel, paddingPx: 24, retain: true });
info.textContent = 'all teams collapsed';
};
document.querySelector('#expandAll').onclick = async () => {
for (const grp of og.visualGroups.all) if (grp.collapsed) await og.expandVisualGroup(grp.key, 420);
og.camera.fit({ target: 'visible', avoid: panel, paddingPx: 24, retain: true });
info.textContent = 'all teams expanded — members laid out inside their disc';
};
document.querySelector('#addMember').onclick = async (event) => {
const button = event.currentTarget;
const group = og.visualGroups.get('Data');
if (!group || group.collapsed) {
info.textContent = 'expand Data before adding a member';
return;
}
button.disabled = true;
const memberId = id++;
await og.expandVisualGroupAsync('Data', async () => ({
nodes: [{
id: memberId, size: 5, color: PAL[3],
label: 'dat-' + group.members.length, data: { team: 'Data' },
}],
edges: [{ source: memberId, target: group.members[0] }],
}), 320);
info.textContent = `Data grew to ${group.members.length} members; other teams stayed fixed`;
button.disabled = false;
};