Hover a node for a camera-tracked tooltip. Right-click anywhere for a fixed interactive context card.
// Cards are opened against a node id or a pointer location, not manually calculated canvas
// coordinates. OGJS tracks node-anchored cards through pan and zoom and owns their disposal
// when the card closes or the graph instance is destroyed.
import { Ogjs } from '../../src/index';
const og = new Ogjs({ container:'#viz',theme:'light' });
const cardState = { clicks:0,contextCloses:0 };
// Node ids are valid card anchors. OGJS follows those nodes through camera and graph movement,
// so the application never translates their world positions into CSS coordinates.
og.setGraph({ nodes:[
{ id:'account',label:'Account',x:-160,y:35,size:17,color:'#2563eb' },
{ id:'device',label:'Device',x:-35,y:125,size:14,color:'#7c3aed' },
{ id:'merchant',label:'Merchant',x:130,y:75,size:15,color:'#059669' },
{ id:'alert',label:'Alert',x:35,y:-70,size:15,color:'#dc2626' },
{ id:'case',label:'Case',x:-125,y:-115,size:14,color:'#f59e0b' },
{ id:'owner',label:'Owner',x:185,y:-105,size:14,color:'#0891b2' },
], edges:[
{ source:'account',target:'device' },{ source:'device',target:'merchant' },
{ source:'merchant',target:'alert' },{ source:'alert',target:'case' },
{ source:'alert',target:'owner' },{ source:'account',target:'case' },
] });
og.camera.fit();
const retargetElement = document.createElement('div');
retargetElement.className = 'graph-card retarget-card';
retargetElement.dataset.card = 'retarget';
retargetElement.textContent = 'Retargeted card';
const retargetHandle = og.cards.open({
id:'retarget',element:retargetElement,anchor:{ kind:'node',id:'missing' },order:9,
});
function retargetCard(anchor) {
// Retarget the existing handle to preserve the card element and any UI state it contains;
// invalid anchors fail at the public boundary instead of creating an orphaned overlay.
try { retargetHandle.setAnchor(anchor);return 'ok'; }
catch (error) { return error.message; }
}
let tooltip;
og.on('nodehover', ({ nodeIndex }) => {
// Closing the previous handle releases both its DOM element and camera tracking before the
// next transient tooltip is opened.
tooltip?.close();
const element = document.createElement('div');
element.className = 'graph-card tooltip-card';
element.dataset.card = 'tooltip';
element.textContent = og.graph.getLabel(nodeIndex) || String(og.graph.getNodeId(nodeIndex));
tooltip = og.cards.open({
id:'tooltip',element,anchor:{ kind:'node',id:og.graph.getNodeId(nodeIndex) },offset:[0,-24],order:10,
});
});
og.on('nodeout', () => { tooltip?.close();tooltip = null; });
let contextCard;
const viz = document.querySelector('#viz');
viz.addEventListener('contextmenu', (event) => {
event.preventDefault();
contextCard?.close();
const element = document.createElement('div');
element.className = 'graph-card context-card';
element.dataset.card = 'context';
const button = document.createElement('button');
button.type = 'button';button.textContent = 'Inspect card';
button.addEventListener('click', () => { cardState.clicks += 1; });
element.appendChild(button);
// Passing the event itself tells OGJS to preserve the exact click location. The Cards facade
// converts it to the graph surface coordinate system and keeps the fixed card independent of
// later camera movement.
contextCard = og.cards.open({ id:'context',element,interactive:true,order:11,
anchor:event });
});
window.addEventListener('keydown', (event) => {
if (event.key !== 'Escape' || !contextCard) return;
contextCard.close();contextCard = null;cardState.contextCloses += 1;
});