Skip to content

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 26 Oct 19:27
· 76 commits to main since this release
f4a1231

Graph layout release: four new GraphCanvas layout engines (organic, orthogonal,
radial-tree, ux-navigation), a rewritten galaxy, a fixed force-clustered, and
interactive popovers.

Added

  • GraphCanvas — layout="organic", the first mode of the layout-engine rework and the
    intended successor to force / force-clustered. A headless, deterministic
    organicLayout() (also exported): stress majorization seeded by classical MDS, with a
    GTree-style overlap-removal stage that makes zero overlap and nodeMargin (as a minimum
    box-to-box gap, default 24 px) hard post-conditions, and a warm-start rule under which
    re-running the engine on its own output is a no-op. Nodes with explicit x/y are pinned
    and held exactly. On the 31-node technology-layer benchmark, neighborhood preservation goes
    from 0.34 (force) to 0.62 and scale-fitted stress from 0.151 to 0.039; 400-leaf stars,
    which drove force to NaN, stay finite. Proof of concept: no clustering option yet, and
    isolates are circle-packed rather than gridded.
  • GraphCanvas — layout="orthogonal", the second mode of the rework and the intended
    successor to ux-navigation. A headless, deterministic orthogonalLayout() (also exported)
    places nodes on a cell grid and routes every edge as an axis-parallel polyline over a lattice
    of routing lanes: routes never pass through a node, never share a segment with another route,
    and are returned boundary to boundary, index-aligned with the edges. GraphCanvas renders them
    in place of bezier paths (edges touching a dragged node fall back to a live bezier).
    • Rows come from the structural forest when isStructuralEdge is given, otherwise from the
      BFS layering with the fewest crossings; rows are crossing-minimized.
    • Honors the ux-navigation hooks: with isPageNode, a page's views fan out to its right in
      the same row; isNavigationRoute edges are routed last, around everything else.
    • Re-running on its own output keeps every node's cell; pinned nodes hold their cell.
    • On the harness's Stage 1 exemplar (22 types / 33 relationships): 1 crossing, 0.73 bends
      per edge, every hard check green.
    • Proof of concept: no incremental routing, and trunk sharing for tree edges
      (busTreeEdges) is available from orthogonalLayout() only, not through GraphCanvas.
  • GraphCanvas — layout="radial-tree": each structural tree (per isStructuralEdge) is
    laid out around its root, with each depth on its own ring and rings sized from their own
    population; several trees are packed as separate trunks without overlap. Supports
    collapsedNodeIds and draws a boundary per trunk (showClusterBoundaries). Headless
    radialTreeLayout() exported, with RadialTreeLayoutOptions (ringSpacing,
    nodeSeparationGap, trunkPadding) and RingGeometry.
  • GraphCanvas — showHierarchyRings prop (layout="radial-tree" only, default false):
    draws a ring per occupied depth around each trunk. Visual only.
  • GraphCanvas — layout="ux-navigation" for app screen maps: page nodes form top-to-bottom
    trees, a page's views fan out beside it, independent trees and orphan views are arranged in
    rows. New props isPageNode (page vs. view) and isNavigationRoute (route edges, hidden until
    a page or view touching them is hovered; which routes show depends on whether the hovered node
    is a collapsed page, an expanded page or a view). Structural-edge and route labels show only
    when the edge is in focus. Headless uxNavLayout() and visibleNavigationEdgeIds() exported.
  • GraphCanvas — structuralEdgeCurvature prop (default 0.22): curvature for structural
    edges without their own curvature; 0 draws them straight.
  • GraphCanvas — nodePopover / edgePopover render props: interactive content in a
    role="dialog" popover anchored to a clicked or focused node or edge, tracking pan and zoom,
    closed on Escape, background click or click outside. Opening moves focus to the popover's
    first focusable element; Escape closes it from anywhere in the canvas, including while focus
    is still on the node or edge (content with nothing focusable, such as a click tooltip).
  • GraphCanvas — renderNode receives a fourth argument, props: { nodeId, tooltipId, onSelect, onPopoverOpen }. A custom renderer must call onPopoverOpen(element) on click when
    it is defined, or nodePopover and click-triggered tooltips never open; tooltipId is the id
    to put in the node's aria-describedby.
  • GraphNode — popoverOpen, popoverPanelId, tooltipId, onFocus and onBlur props,
    used by GraphCanvas to wire popovers and tooltips to the default node.
  • GraphCanvas — nodeTooltipTrigger / edgeTooltipTrigger ('hover' | 'click', default
    'hover'). 'click' renders the tooltip content in a Popover so it can hold buttons and
    links. Node and edge tooltips are now wired to their targets with aria-describedby.
  • Popover — new compound component (Popover, Popover.Trigger, Popover.Panel) for
    interactive, accessible content: controlled (open + onOpenChange) or uncontrolled,
    placement, offset, disableOutsideClick, panelId, and autoFocus (move focus to the
    panel's first focusable element on open; default false).
  • GraphCollapseControl — the collapse/expand button GraphCanvas shows beside a hovered node
    that has structural children, exported for custom overlays.
  • Tooltip — id prop, so a trigger elsewhere in the DOM can reference it with
    aria-describedby.
  • GraphNodeData.weight — optional size weight for layout="galaxy" (ignored by every other
    layout; unrelated to GraphEdge.weight). A node without one weighs the sum of its structural
    children; when no node has a weight, each node weighs its subtree leaf count. Weights are
    resolved over the full tree, so collapsing a subtree never resizes its parent.
  • GraphNode — bodyRadius prop: renders the node as a round body of that radius.
  • galaxyLayout — GalaxyLayoutNode.weight (orbit order) and .radius (true radius for a
    round body, used instead of the box half-diagonal).
  • ChatComposer — enterBehavior prop ('send' | 'newline', default 'send'): controls Enter key behavior in the message textarea. When set to 'send', bare Enter submits the message and Shift+Enter inserts a newline (default behavior). When set to 'newline', bare Enter inserts a newline and Shift+Enter also inserts a newline — Submit only via the Send button. Enables touch-friendly and mobile-optimized input modes without forking or DOM-patching the component. The shortcut hint text automatically hides desktop-specific wording when enterBehavior='newline'.

Changed

  • GraphCanvasContextValue.layout — the union now includes 'organic', 'orthogonal',
    'ux-navigation' and 'radial-tree'; an exhaustive switch over it needs the new cases.
  • findClearLabelPosition — new isPolyline parameter samples the points as a polyline
    (orthogonal routes). Without it, only exactly four points are read as a cubic bezier; five or
    more points are now sampled as a quadratic from the first three, where they were previously
    read as a cubic from the first four.
  • GraphCanvas — layout="galaxy" rewritten to follow the original galaxy charts it was
    built to recreate (Risacher's D3 galaxy chart, Galaxies of the Guardian):
    • Without a renderNode, nodes are drawn as round bodies whose area grows with weight
      (with the square root of weight, so items spanning several orders of magnitude stay
      distinguishable), coloured by domainColor, instead of label cards. Labels sit inside bodies
      big enough to hold them, wrapping between words; smaller bodies are named on hover. When no
      node has a weight, sizes come from subtree leaf counts with a higher floor. Selection,
      keyboard, popover and collapse behave exactly as before.
    • Each parent's children orbit it heaviest first in a full, evenly spaced circle. Each
      satellite sits just far enough out for its whole subtree to clear its parent and stay within
      its own slot, so no two bodies overlap anywhere, by construction rather than by a settle
      loop. Only children curl like a galaxy arm instead of running off in a line.
    • Independent trees are separate galaxies packed together compactly; lone nodes are packed as
      their own group; a wide container spreads whole galaxies, never a galaxy's own orbits.
    • Boundary discs are off by default for galaxy (showClusterBoundaries still turns them
      on; they are now the smallest circle around each system). Structural (orbit) edge labels show
      only when the edge or one of its nodes is hovered or selected. Edges meet round bodies at the
      circle rather than the bounding box.
    • Live simulation eases toward the same construction; a dragged node carries its subtree and a
      held drag settles. Static layout is ~1-30 ms for typical graphs (was ~300 ms for 250 nodes).
    • Options: startAngle now defaults to π (first satellite at 9 o'clock; was −π/2).
      nodeSpread is now the default gap as a multiple of the median body radius (default 0.6;
      was an orbit-distance multiplier, default 3.2). nodeMargin replaces that gap when given (0
      means touching). settleCycles, separateGroups, resolvedAspectScale and
      resolveAspectRatioScale are deprecated and have no effect.

Fixed

  • GraphCanvas — layout="force-clustered" no longer smears clusters into each other on
    dense graphs. Each Louvain cluster is now laid out on its own (internal edges only), clusters
    are spaced by their real size, and each cluster is rotated to face the clusters it connects
    to. On the college-football benchmark (115 nodes, 613 edges) node overlaps go from 53 to 0
    and overlapping cluster bubbles from 93% to 2%; layout is also 2-10x faster.