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)
OptionTypeDefaultDescription
semanticZoom.enabledbooleanfalseRe-tier node content as the tree is zoomed
semanticZoom.compactBelownumber90On-screen node width, in CSS pixels, at or below which a node drops to 'compact'
semanticZoom.dotBelownumber42On-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:

TierContent
'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.