Focus Mode and the Active Path

Two related ways to isolate part of a large tree: focus mode dims everything outside a node's lineage and subtree, and the active path flows a marching dash along the route from the root to a node. They work independently or together.

Focus mode

graph.focus(id) spotlights a node. Everything outside its lineage (its ancestors up to the root) and its visible subtree dims, and the camera springs to frame what is left:

const graph = tree.render(data)

graph.focus('eng')          // spotlight
graph.getFocusedNodeId()    // 'eng'
graph.clearFocus()          // restore

focus() returns false when the node is unknown or not currently rendered, which includes a node hidden inside a collapsed ancestor. Use the return value rather than assuming the call landed:

if (!graph.focus(nodeId)) {
  graph.expandToDepth(3)
  graph.focus(nodeId)
}

Escape, a re-click on the focused node, or clearFocus() all restore the full view. Focus survives collapse, expand and layout changes for as long as the node stays rendered.

Click to focus

const tree = new ApexTree(el, {
  focus: { clickToFocus: true, dimOpacity: 0.7 },
})

focus.clickToFocus (default false) focuses a node when its card is clicked. Clicks on the expand/collapse button keep toggling the subtree as usual, so the two gestures do not fight.

focus.dimOpacity (default 0.7) sets how strongly the rest of the tree is dimmed, from 0 (no dim) to 1 (fully hidden).

The dim is applied by paint order rather than by setting opacity on the cards themselves, which keeps Safari's foreignObject rendering intact.

The active path

graph.setActivePath(ids) lights up the lineage from the root down to each given node. Those edges recolor and a dashed pulse flows along them:

graph.setActivePath('ada')            // one lineage: root → ada
graph.setActivePath(['ada', 'kai'])   // two lineages

graph.getActivePath()   // ['ada', 'kai']
graph.clearActivePath()
graph.setActivePath([]) // same as clearActivePath()

The path is re-asserted after every render, so it survives collapse and expand.

Styling the flow

edgeFlow configures the marching dash:

OptionTypeDefaultDescription
edgeFlow.colorstring'#5C6BC0'Stroke color of a flowing edge
edgeFlow.widthnumber2Stroke width in pixels
edgeFlow.speednumber60Flow speed in SVG units per second
edgeFlow.dashLengthnumber8Length of each dash, in pixels
edgeFlow.gapLengthnumber6Gap between dashes, in pixels
edgeFlow.direction'toChild' | 'toParent''toChild'Which way the dashes travel
edgeFlow.followFocusbooleanfalseSet the active path automatically when focus mode spotlights a node
const tree = new ApexTree(el, {
  edgeFlow: {
    color: '#0EA5E9',
    width: 3,
    speed: 90,
    direction: 'toParent',
  },
})

'toChild' flows root to leaf, which reads as delegation flowing down. 'toParent' flows leaf to root, which reads as an escalation path.

Combining the two

edgeFlow.followFocus: true wires them together: focusing a node also traces its lineage, and clearFocus() clears the trace.

const tree = new ApexTree(el, {
  focus: { clickToFocus: true },
  edgeFlow: { followFocus: true },
})
const graph = tree.render(data)
// clicking any card now dims the rest AND flows its root path

Reduced motion

The flow is plain SVG plus injected keyframes, and it respects reduced motion: with the apextree-reduced-motion class on the container the path still highlights in edgeFlow.color, it simply does not animate. See Motion and Animation.

Complete example

const tree = new ApexTree(document.getElementById('chart'), {
  direction: 'top',
  nodeWidth: 160,
  nodeHeight: 60,
  focus: { clickToFocus: true, dimOpacity: 0.8 },
  edgeFlow: { color: '#5C6BC0', speed: 70, followFocus: true },
})
const graph = tree.render(data)

document.getElementById('search').oninput = (e) => {
  const matches = graph.findNodesByQuery(e.target.value)
  if (matches.length) {
    graph.setActivePath(matches)
    graph.centerOnNode(matches[0])
  } else {
    graph.clearActivePath()
  }
}