Semantic Zoom
A card that reads well at full size becomes unreadable noise at 20% zoom. Semantic zoom re-tiers what renders inside each node based on how wide that node appears on screen, so a zoomed-out tree shows shape and structure instead of illegible text.
Enabling it
const tree = new ApexTree(document.getElementById('chart'), {
nodeWidth: 180,
nodeHeight: 72,
semanticZoom: {
enabled: true,
compactBelow: 90,
dotBelow: 42,
},
})
const graph = tree.render(data)
| Option | Type | Default | Description |
|---|---|---|---|
semanticZoom.enabled | boolean | false | Re-tier node content as the tree is zoomed |
semanticZoom.compactBelow | number | 90 | On-screen node width, in CSS pixels, at or below which a node drops to 'compact' |
semanticZoom.dotBelow | number | 42 | On-screen node width below which a node drops to 'dot' |
compactBelow must be greater than dotBelow. Both thresholds are measured in CSS pixels on screen, not in layout units, so they track the actual zoom level.
The tiers
The built-in card renders three tiers:
| Tier | Content |
|---|---|
'full' | The complete card: avatar, name, title, subtitle, chips, stripe |
'compact' | A name and role plate |
'dot' | A color slab carrying just the name |
Geometry never moves
The important property: node geometry is fixed across tiers. The layout is computed once, and crossing a threshold only swaps what renders inside each node's existing box. Nothing moves, the camera is untouched, and there is nothing per-tier to configure.
That is what makes semantic zoom safe on dynamic data of unknown shape. A tier change can never reflow the tree, so it can never fight a spring that is already in flight or shift the node under the user's pointer.
Custom templates
A custom nodeTemplate receives the current tier as lod on its second context argument, alongside expanded, direction and cardImagePosition. The context is typed as optional, so read it defensively:
const tree = new ApexTree(el, {
contentKey: 'data',
semanticZoom: { enabled: true },
nodeTemplate: (content, context) => {
const lod = context?.lod ?? 'full'
if (lod === 'dot') {
return `<div class="node-dot">${content.name}</div>`
}
if (lod === 'compact') {
return `
<div class="node-compact">
<strong>${content.name}</strong>
<span>${content.title ?? ''}</span>
</div>
`
}
return `
<div class="node-full">
<img src="${content.imageURL}" alt="" />
<strong>${content.name}</strong>
<span>${content.title ?? ''}</span>
<p>${content.subtitle ?? ''}</p>
</div>
`
},
})
With semanticZoom.enabled: false, lod is always 'full', so a template written this way works either way.
Keep every branch inside the same box. A tier that needs more room than nodeHeight allows will be clipped, because the geometry is not recomputed.
Zoom interaction
Wheel zoom, drag pan, the toolbar buttons and graph.zoom() all re-check the tier once the camera settles. Both gestures release the camera spring first, so a wheel zoom or a drag is never swallowed by a fit or focus animation still in flight.
See Zoom, Pan and Export for the camera API.