Repository navigation
Releases
v0.10.0
Layered graphs: a component family that shows one graph split into layers by a node attribute (3D
layer stack, lanes, adjacency matrix and hive plot) on shared selection, link-following and
filter state, with controls, an inspector and a ready-made explorer. Also faster organic and
orthogonal layouts and cleaner ux-navigation routes.
Added
- Layered graph framework (#330). Input is the
GraphCanvasnode and
edge model; one node attribute (layerBy) assigns layers, withmultiValue(first,last,
exclude,all) andmissingValuepolicies.buildLayeredIndexbundles parallel edges into links and derives categories, cross-layer
flags, layer pairs, degrees and two-letter predicate codes; bad input goes toissues
instead of throwing.computeReach: selecting a node lights its direct links, then follows cross-layer links
away from the clicked layer forfollowmore hops.- Shared state:
useLayeredGraphandLayeredGraphContextown selection, follow, filters,
predicate-label mode, layer focus and plane layout, controlled or uncontrolled, with
snapshot/restore. One instance can drive several views. - Helpers:
isLinkVisible,linkEmphasis,nodeEmphasis,layoutLayers,
mixLayerPositions, orderings,placeEdgeLabelswith sampledlineCurve/quadCurve/
cubicCurve, andarrowHeadPoints. - Tokens:
--graph-hop-1…3(amber reach ramp),--graph-link-1…6(link categories),
--graph-layer-1…12(layer palette),--graph-pill-bgand--graph-plane-fill-alpha, for
light and dark canvas. GraphNodeDataandGraphEdgegain an optionalattributesbag (GraphAttributeValue),
whichGraphCanvasignores.
LayerStack(#332): layers as stacked translucent planes in an SVG
perspective projection, with cross-layer links between them and a drill-through column under
the selection.- Camera presets (
stack,exploded,top,profile), drag to orbit, wheel zoom once the
scene has focus, a turntable (off under reduced motion), layer gap, layer focus and an
animated independent/coupled plane layout. - An imperative handle (
applyPreset,setCamera,zoomBy,fit,setTurntable,
focusLayer,select). - Keyboard navigation of node labels. The projection math (
createProjector, presets, …) and
useTweenare exported.
- Camera presets (
GraphCanvas layout="layer-lanes"(#332): one swimlane per layer,
nodes packed in rows ordered so linked nodes line up, arcs within a lane and curves between
lanes, horizontal or vertical. Keeps tooltips, popovers,renderNode, pan/zoom and the
toolbar. New propslayeredGraph,layering,layeredGraphOptionsandlaneOptions; the
geometry is exported aslayoutLanesandlaneLinkCurve.AdjacencyMatrix(#330): a directed block matrix grouped by layer,
with cross-layer block counts, category-coloured cells, the reach ramp, predicate codes and a
code legend.HivePlot(#332): one axis per layer, doubled so links within a layer
span the gap; nodes ranked outward; keyboard navigation along and between axes. Geometry
exported ashiveLayoutandhiveLinkGeometry.- Controls and inspector (#332):
FollowControl,
PredicateLabelControl,PlaneLayoutControl,LinkCategoryFilter,LayerList,
LayerPairList,BridgeNodeListandReachInspector(withreachRelationships). Each reads
agraphprop or the context. LayeredGraphExplorer(#332): the views, controls, inspector and a
status line in one component, withViewSwitcherandCameraControls. Acontrolsprop turns
each piece of chrome on or off. Selection, follow and filters carry across view switches, and
the stack keeps its camera.GraphInspector:RelationshipLinkgains optionalhop,viaandlayerfields
(layer swatch,+nhop badge, "via X · predicate" line). Existing output is unchanged.- Docs: a "Layered graphs" section with a page per component
(#333).
Changed
organicLayoutis about 3x faster on large graphs (#326): 1000-node
random graph 2.1 s → 0.6 s, DR spec graph 140 ms → 62 ms. A stress polish now runs before
overlap removal on components of 300 nodes or fewer, so small layouts can shift slightly;
incremental re-solves are unchanged.orthogonalLayoutis several times faster on large graphs (#327):
DR spec graph 14.6 s → 5.2 s (39 s → 3.5 s with all predicates); a 400-node community graph
and a 401-node star that did not finish in 180 s now take 12 s and 0.5 s. Output is unchanged
except on components with more than 64 candidate roots (for example long chains), where the
root search is now capped.
Fixed
layout="ux-navigation"navigation routes go through free gaps between boxes and take the
cheapest clear path, instead of detouring round the whole drawing (#324).
Routes leave and arrive perpendicular to their port side, parallel routes are kept apart, and
routing cost is bounded (400 boxes / 400 routes: about 140 ms).- Large ux-navigation sitemaps: routes no longer pass through other nodes or take long
detours (#325). On a 582-node sitemap, segments through nodes go from 26
to 0 and the worst route-length ratio from 8.1 to 1.2. A shifted segment no longer lands on
another route's line.
v0.9.5
Two pan inertia fixes in usePanZoom, so GraphCanvas and MapCanvas drags settle when they look settled.
Fixed
usePanZoom— a drag held still and then released no longer flings
(#321). The release velocity came from the lastpointermove, however
long ago it was, so a drag that had come to rest still glided on release. It is now the
average over the last 100 ms before the release, counting time the pointer was held still: a
100 ms hold gives no glide, and a slowdown just before the release gives a gentler one.usePanZoom— an inertia glide now ends when it visibly stops
(#320). It ran until the speed fell below 0.1 px/s, about a second of
sub-pixel creep after the view looked still, which delayed the'end'gesture phase and so
GraphCanvas'sonViewportDwell(about 4.6 s after a flick withdwellMs1500). The glide
now ends once under half a pixel of travel is left; a 2000 px/s flick ends after about 2.3 s
instead of 3.2 s, and the view comes to rest within half a pixel of where it did before.
v0.9.4
Galaxy drag performance, a layout-busy state, and viewport history for GraphCanvas.
Changed
GraphCanvasengine layouts now apply one frame after their inputs change, not during the
same commit (#307). This lets the new busy state paint before a slow
engine (for exampleorthogonal) blocks the main thread. Code or tests that read node
positions immediately after rendering or after changinglayout,nodesoredgesmust
now wait: fordata-layout-state="idle"on.graph-canvas, or foronLayoutStateChange
to report'idle'. While a new layout computes, the previous positions stay on screen. Before
the first layout, nodes and edges without a position are hidden rather than stacked at the
origin, so the first visible frame is the laid-out (and, withfitView, fitted) graph. If
inputs change again before the deferred run starts, the stale run is dropped. An engine error
still reaches the nearest error boundary.
Fixed
GraphCanvas(layout="galaxy", live simulation) — dragging a node no longer stalls on
larger graphs (#287). Each drag frame re-resolved overlaps around the
dragged system with an all-pairs pass repeated up to 400 times: about 260 ms per frame at 252
nodes and 30 s at 3000. Overlaps are now found through a spatial grid, only boxes that moved
are re-checked, and a live frame spends a bounded amount of work on them, finishing any
remainder over the next frames. A drag frame now costs about 1 ms at 252 nodes and under 10 ms
at 3000. The settled layout, and staticgalaxyLayout()output, are unchanged.
Added
GraphCanvas— layout busy state (#307).- Reporting:
onLayoutStateChangeand adata-layout-stateroot attribute report
'computing'while an engine layout runs,'simulating'while galaxy live simulation moves
nodes, and'idle'otherwise. - Indicator: a "Laying out…" status chip (
role="status",aria-live="polite") appears
once a layout has been busy for 150 ms. Turn it off withshowLayoutIndicator={false}. - Exported type:
GraphLayoutState. - Not included: running engines in a Web Worker.
- Reporting:
GraphCanvas— viewport history (#309).onViewportDwell(withdwellMs, default 1500) fires once the viewport has been still
after a user pan/zoom (drag, wheel, pinch, keyboard, toolbar+/-). It never fires
mid-gesture or while inertia is still gliding.onViewportNavigatereports moves the canvas or the app made, with a
GraphViewportCause:center-on-select(withnodeId),fit(mount,toolbar,
fullscreenorresize;layoutis reserved) orimperative.initialViewportreplaces the mount fit and centring, for deep links and reloads.setViewport(viewport, { animate, silent })andgetViewport()are on a new
viewportRefhandle and onuseGraphCanvas().silentsuppressesonViewportNavigate,
so restoring a history entry doesn't record a new one.animateis a 150 ms ease, a jump
underprefers-reduced-motion.- Snapshots are in world coordinates (
center,zoom,bounds). - New types:
GraphViewportSnapshot,GraphViewport,GraphViewportCause,
GraphViewportHandle,GraphSetViewportOptions.
usePanZoom— optionalonUserGesture('start' | 'change' | 'end')option and a
getViewport()return value. Existing callers are unaffected.
v0.9.3
Canvas interaction and accessibility fixes from downstream testing.
Fixed
usePanZoom(GraphCanvas,MapCanvas) — wheel or pinch zoom right after a drag-pan now
zooms around the cursor (#306). The pan inertia glide kept writing its own
pan every frame and overwrote the zoom's cursor-anchored pan, so the zoom pivoted around an
unrelated point. Wheel, keyboard,zoomTo,panToandresetnow stop the glide, and wheel
events that arrive within one frame compound instead of all but the last being dropped.usePanZoom(GraphCanvas,MapCanvas) —+/-keyboard zoom now scales about the
view centre instead of the top-left corner (#302). Each press now
multiplies or divides the zoom by 1.2 instead of adding or subtracting 0.1, so it is the same
relative step at any zoom.GraphCanvas—fitViewand the toolbar's zoom-to-fit now frame cluster boundary circles
(showClusterBoundaries) and radial-tree hierarchy rings (showHierarchyRings), not just the
nodes (#305). Radial-tree trunk boundaries were clipped by up to ~176px
after fit, and force-clustered bubbles slightly. The initial fit also waits for the current
layout's positions and boundaries, so it never frames a previous layout's.GraphCanvas— interactive edges andnodePopover/edgePopoverdialogs get their
accessible names from node labels instead of ids, so screen readers no longer announce opaque
ids such as UUIDs (#304). An edge is announced as "monitors: WebSocket
Disconnect Alert to Connection State Gauge", a popover as "Details for " or
"Details for ". Each name falls back to the id when a node has no label.
Added
GraphEdge— optionalsourceLabel/targetLabelprops for the interactive edge's
accessible name, falling back tosourceId/targetId.GraphCanvaspasses them
automatically; pass them yourself when renderingGraphEdgestandalone with opaque ids.
v0.9.2
Hotfix: the stylesheet import works again.
Fixed
- Package
./cssexport —import '@tinkermonkey/heimdall-ui/css'failed to resolve in 0.9.0
and 0.9.1 (#303). The build-tool upgrade in 0.9.0 renamed the emitted
stylesheet fromdist/style.csstodist/heimdall-ui.css, and theexportsmap still pointed
at the old name. The stylesheet is back atdist/style.css.dist/heimdall-ui.cssremains as
a deprecated one-line forwarder, so apps that aliased that file directly as a workaround keep
building; switch back to the documented import.
Added
npm run check:package(scripts/check-package-exports.mjs) — fails when a path in
package.json'sexports,mainortypesis missing from the packed tarball. The release
workflow runs it after building, before the visual tests, so a release can't publish an export
consumers can't resolve.
v0.9.1
Bug fixes for MapCanvas, LineChart and KanbanBoard, and agent skills regenerated to cover
every public export.
Fixed
MapCanvas— the initial view now fits the data (#296). It used to open
on an empty grid at the top-left of the world with pins, tracks and heatmaps off-screen: the fit
misread its own zoom calculation, never applied the zoom it computed, and panned in a different
coordinate space from the one content is drawn in. Content is now projected once, at the level
that frames the data (orbounds) with 60px padding at scale 1, and centred; a single point
opens at level 12, the fit stops at level 16, and no data shows the whole world. Zooming only
scales that projection, so it no longer jumps as you zoom. Tiles come from the integer level
nearest the on-screen zoom.onViewportChange'scenteris now the map point under the middle
of the view;zoomis still the scale factor, 1 being the fitted view.usePanZoom—onViewportChangeno longer fires again just because the callback's identity
changed. A caller that set state from an inline callback, the usual way to write it, re-rendered
forever (MapCanvaswith an inlineonViewportChangelogged "Maximum update depth exceeded").LineChart— no longer throws whenseriesswitches between empty and non-empty data
(#297). A hook ran after the empty-data early return, so the hook count
changed between renders.KanbanBoard—onMoveCard'sindexis now the card's final position for pointer drags
downward within the card's own column too (#298). It was one slot too high,
so a remove-then-insert handler landed the card one slot low. Keyboard and cross-column moves
were already correct.
Added
agent-skills/heimdall-observability— new skill forTraceViewer(with thefromOTLP/
fromXRayadapters and the trace-model helpers),LogStream,DiffViewer,HashSetDiff,
SideBySideDiff,VersionTimelineandLineageRail.agent-skills/heimdall-planning— new skill forCalendar,MiniCalendarandKanbanBoard.npm run check:skills(scripts/check-skill-coverage.mjs) — fails when an exported
component has no section in its assigned skill, a hook or utility export is never named, a skill
documents a component that is not exported, or a props table names a prop the component type
does not accept or leaves out one declared insrc/. The release workflow runs it before the
visual tests.
Changed
- Every agent skill regenerated against the current source. The skills were written in May
and had drifted: 22 public components were in no skill, and many props tables were incomplete.
All 13 skills now cover every value exported fromsrc/index.ts. Notable corrections:heimdall-chartsdocumented aPieChartthat does not exist; it is removed, and the pie case
is described underDonut.ChartWrapperandBarChartgained sections.heimdall-inputsdescribedSelectas a native<select>wrapper. It is a custom dropdown
with single and multi modes andSelect.Item/Select.CheckboxItem/Select.Separator/
Select.SectionTitlechildren.heimdall-data-displayimpliedTablesorts its rows; it only tracks the sort header, and
the consumer sorts inonSort. AddedResultCard,AssetCard,AssetGridandMapCanvas.heimdall-shellno longer tells consumers to pad theShellLayoutcanvas (it pads itself),
and documents the mobile drawer behavior behindmobileBreakpoint.- Added
Avatar(primitives),TooltipandPopover(overlays),ChatMarkdownContent
(chat),useMediaQuery(layout), and inheimdall-graph:GraphEdgeShape,GraphToolbar,
GraphCollapseControl,GraphEdgeInspector,DetailDrawer,usePanZoomand the geometry
utilities. - Missing props added across
Sidebar,Topbar,AppTitle,Statusbar,VersionPill,
Drawer,FilterDropdown,EntityPicker,RowMenu,PipelineCard,ChatComposer,
GraphCanvas,GraphNode,GraphEdgeand six charts.Iconpoints atICONSinstead of a
stale list of names.
/refine-component-skills— the category table now assigns every public export to a skill
(two new categories: observability, planning). Prop tables use backticked names so the checker
can read them.- README — new "Agent skills" section with setup instructions and the skill list.
agent-skills/heimdall-graph— brought up to date with 0.9.0. The skill now covers all
eightGraphCanvaslayouts (what data each suits, the props it reads, and its limits;organic
andorthogonalare marked proof of concept), the hierarchy and collapse props,
nodePopover/edgePopoverand the tooltip triggers, therenderNodefourthpropsargument
a custom renderer must wire for popovers, click tooltips andaria-describedby, the new
GraphNodeprops, and the headlessorganicLayout,orthogonalLayout,radialTreeLayout,
uxNavLayoutandgalaxyLayoutfunctions.
v0.9.0
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 toforce/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 andnodeMargin(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 explicitx/yare 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 droveforcetoNaN, 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 toux-navigation. A headless, deterministicorthogonalLayout()(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
isStructuralEdgeis 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;isNavigationRouteedges 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 fromorthogonalLayout()only, not through GraphCanvas.
- Rows come from the structural forest when
GraphCanvas—layout="radial-tree": each structural tree (perisStructuralEdge) 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
collapsedNodeIdsand draws a boundary per trunk (showClusterBoundaries). Headless
radialTreeLayout()exported, withRadialTreeLayoutOptions(ringSpacing,
nodeSeparationGap,trunkPadding) andRingGeometry.GraphCanvas—showHierarchyRingsprop (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 propsisPageNode(page vs. view) andisNavigationRoute(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. HeadlessuxNavLayout()andvisibleNavigationEdgeIds()exported.GraphCanvas—structuralEdgeCurvatureprop (default 0.22): curvature for structural
edges without their owncurvature; 0 draws them straight.GraphCanvas—nodePopover/edgePopoverrender 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—renderNodereceives a fourth argument,props: { nodeId, tooltipId, onSelect, onPopoverOpen }. A custom renderer must callonPopoverOpen(element)on click when
it is defined, ornodePopoverand click-triggered tooltips never open;tooltipIdis the id
to put in the node'saria-describedby.GraphNode—popoverOpen,popoverPanelId,tooltipId,onFocusandonBlurprops,
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 aPopoverso it can hold buttons and
links. Node and edge tooltips are now wired to their targets witharia-describedby.Popover— new compound component (Popover,Popover.Trigger,Popover.Panel) for
interactive, accessible content: controlled (open+onOpenChange) or uncontrolled,
placement,offset,disableOutsideClick,panelId, andautoFocus(move focus to the
panel's first focusable element on open; defaultfalse).GraphCollapseControl— the collapse/expand button GraphCanvas shows beside a hovered node
that has structural children, exported for custom overlays.Tooltip—idprop, so a trigger elsewhere in the DOM can reference it with
aria-describedby.GraphNodeData.weight— optional size weight forlayout="galaxy"(ignored by every other
layout; unrelated toGraphEdge.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—bodyRadiusprop: 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—enterBehaviorprop ('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 whenenterBehavior='newline'.
Changed
GraphCanvasContextValue.layout— the union now includes'organic','orthogonal',
'ux-navigation'and'radial-tree'; an exhaustiveswitchover it needs the new cases.findClearLabelPosition— newisPolylineparameter 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 bydomainColor, 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 aweight, 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 (
showClusterBoundariesstill 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:
startAnglenow defaults to π (first satellite at 9 o'clock; was −π/2).
nodeSpreadis now the default gap as a multiple of the median body radius (default 0.6;
was an orbit-distance multiplier, default 3.2).nodeMarginreplaces that gap when given (0
means touching).settleCycles,separateGroups,resolvedAspectScaleand
resolveAspectRatioScaleare deprecated and have no effect.
- Without a
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.
v0.8.0
Added
Tooltip— new component (content,children,placement,delay,offset,
disabled,open). Positions relative to its trigger with automatic edge-flip, and supports an
openprop for external/controlled visibility (used byGraphCanvas's node/edge tooltip
overlays, whose hover state lives on the node or edge rather than on whatever wraps the
tooltip) — when set, the trigger's own pointer/focus tracking anddelayare bypassed.GraphCanvas—nodeTooltipandedgeTooltiprender-prop options to show aTooltipover
a hovered node or edge.GraphCanvas—onEdgeHoverevent, mirroring the existingonNodeHover.GraphCanvas—renderEdgerender-prop (paired with the new exportedEdgeGeometrytype
andGraphEdgeShapecomponent) for fully custom edge rendering, mirroring the existing
renderNode.useMediaQuery— new hook: subscribes to a media query string and returns whether it
currently matches, updating on change. SSR-safe (returnsfalseuntil mounted).ShellLayout—mobileBreakpointprop (default768, matchingTopbar/Statusbar; pass
falseto disable): below the breakpoint the sidebar collapses into a slide-over overlay opened
via a mobile menu toggle instead of the persistent rail.Drawer—keepMountedprop: keeps the drawer in the DOM (hidden) while closed instead of
unmounting it, for consumers that need to preserve its internal state across opens.
Fixed
Tooltip— pointer moving from the trigger onto the tooltip's own content no longer hides
it immediately; hover now persists across that boundary as intended.
v0.7.0
Added
GraphCanvas—centerOnSelectprop (defaultfalse): pans to keepselectedNodeId
centered in the viewport whenever it changes, preserving the current zoom (never re-fits or
re-zooms). Off by default so existing consumers relying onselectedNodeIdpurely for
highlighting/inspector wiring see no behavior change. Motivating case: an external selection
origin — a sidebar, a nav tree — can select a node that's currently off-screen, unlike clicking
a node directly on the canvas (you had to see it to click it);centerOnSelectcloses that gap.
Skipped until the initial mount center/fit has run and while pan/zoom islocked, and only acts
on a genuine change toselectedNodeId— it never fights a user's own subsequent pan away from
the selected node on a re-render. CombiningcenterOnSelectwithfitViewand an already-set
selectedNodeIdat mount correctly uses the post-fit zoom rather than a stale pre-fit one; a
selectedNodeIdthat isn't resolvable yet (e.g. inside a currently-collapsed subtree) is
retried on a later render instead of being silently given up on.GraphCanvas—fullscreenContainerRefprop: pass a ref to an ancestor element and the
built-in toolbar's Fullscreen button (andtoggleFullscreen/isFullscreenfrom
useGraphCanvas()) request/track fullscreen on that ancestor instead of GraphCanvas's own root.
Motivating case: GraphCanvas doesn't acceptchildren— it fully owns its internal tree — so a
consumer composing sibling overlay content next to it (a control strip above it, a
DetailDrawerbeside it, the same pattern GraphLayoutsShowcase itself uses) previously lost
that content the instant Fullscreen was used, since the native Fullscreen API only renders the
fullscreened element's own DOM subtree. GraphCanvas still measures and lays out against its own
container's size regardless of which ancestor is actually fullscreened, so nothing else about
its behavior changes; omitting the prop is identical to previous behavior. Entering/exiting
fullscreen is bounded and failure-safe: a rejectedrequestFullscreen()/exitFullscreen()(no
permissions-policy grant, a detached or mid-unmount target, ...) is caught and logged via
console.warninstead of surfacing as an unhandled rejection, and the fit applied on entering
fullscreen no longer stays armed indefinitely waiting for a container resize that may never
come (a target that's already viewport-sized on entry, say) — it's bounded to a short window
after entry, so it can't fire later against an unrelated resize instead.
v0.6.0
Added
GraphCanvas— opt-in nested bubble-packing layout:layout="force-clustered"
groups nodes into clusters (and clusters of clusters) derived purely from
edge structure via deterministic Louvain modularity clustering, packs
them withd3-hierarchy, then runs the existing spring simulation within
each bubble. Trades a larger canvas for a less even, more legible node
distribution. Renders an additional.graph-cluster-boundarycircle per
top-level cluster (see the newshowClusterBoundariesprop below). New exports:clusteredForceLayout,
ClusteredLayoutOptions,ClusteredLayoutResult,louvainCluster,
ClusterEdge,ClusterTreeNode,LouvainOptions,packClusters,
PackedCircle,PackOptions. New dependency:d3-hierarchy.GraphCanvas—onBackgroundClickprop fires the typical "click empty
space to deselect" gesture: a press-and-release within a few px of each
other on genuine canvas background (not a node, edge, or opted-out
element). A real pan drag never fires it, and it works independently of
usePanZoom's own drag tracking so it still works while pan/zoom is locked.DetailDrawer— an auto-hiding overlay panel: floats over its nearest
position: relativeancestor (aGraphCanvas, for instance) instead of
reserving dedicated layout space, expanding only onceopenand its
children are truthy. Translucent/blurred background, no border by default,
and a left-edge resize handle that highlights in the accent color on hover
so users can tell it's resizable without a visible seam at rest. New
exports:DetailDrawer,DetailDrawerProps.GraphToolbar— a built-in zoom/pan/lock control cluster for
GraphCanvas, shown by default (showToolbar, defaulttrue) in any of
the 4 corners or 4 edge-centers (toolbarPosition, default
'bottom-right'): zoom in, zoom out, zoom to fit, and lock pan/zoom (backed
byusePanZoom's newlockedoption, which gates wheel/drag/keyboard
input but not imperativesetZoom/setPan/zoomToFitcalls). New
exports:GraphToolbar,GraphToolbarProps,GraphToolbarPosition.galaxyLayout— opt-innodeMarginoption, andGraphCanvas's
existingnodeMarginprop (previouslylayout="force"only) now applies
tolayout="galaxy"andlayout="force-clustered"too. Padding is applied
only during galaxy's settle-cycle separation passes, never its final
cleanup pass, so the "zero literal overlap" guarantee still holds
regardless of the margin value. Defaults to0(unpadded) for"galaxy"
specifically — unlike"force"/"force-clustered", which default to each
node's own rendered width — since galaxy's radial placement already spaces
most layouts generously and defaulting padding on would shift every
existing galaxy layout's node positions for what's a narrow, opt-in-worthy
edge case (verified on a real 57-node/4-level hierarchy with heavy size
variance: the tightest sibling pairs went from under 1px apart to 8px+
withnodeMargin={12}, zero overlaps either way).GraphCanvas—showClusterBoundariesprop (defaulttrue) and
boundary circles now also render underlayout="galaxy", not just
"force-clustered"— one per independent root subtree. A root with
children delegates its group-headship down to each of its own children
rather than drawing one circle around its entire subtree, so a single deep
hierarchy (e.g. one root with a dozen direct branches) still reads as
several legible groups instead of one all-encompassing circle; a childless
root (an orphan, or a node reachable only via a relational edge) keeps its
own single-node boundary. Both engines share the same underlying helper,
newly exported asboundingCirclesByGroup.GraphCanvas— non-structural edges (perisStructuralEdge) are now
hidden entirely by default instead of dimmed to a low opacity — line,
arrow marker, and label all disappear, and the edge isn't clickable while
hidden. Hovering/selecting a touching node, orshowAllRelations, still
reveals them at full opacity, same as before. Removes the internal
NON_STRUCTURAL_DIM_OPACITYconstant this replaces.GraphToolbar— a fifth button toggles the canvas container in and out
of the Fullscreen API. Tracks the platform's own fullscreen state (not
just its own clicks), so Esc or any other exit path updates the icon too.
Entering fullscreen also zooms/pans to fit the current nodes once the
container has actually resized to its new (larger) fullscreen dimensions
— a previously zoomed-in view no longer stays zoomed into a random corner
when the canvas suddenly has much more room. Skipped while pan/zoom is
locked, same as every other automatic viewport change inGraphCanvas.galaxyLayout— top-level group boundary circles (the same ones
showClusterBoundariesrenders) no longer overlap each other, on by
default. After the existing per-node settle cycles, a macro separation
pass treats each group's boundary circle as a single pseudo-node, pushes
apart any that overlap, then rigidly shifts each group's members by the
resulting delta — preserving each group's already-correct internal
arrangement. Computed unconditionally, independent of whether boundaries
are actually rendered. NewGalaxyLayoutOptions.separateGroupsoption
(defaulttrue) opts back out. New export:galaxyGroupHeads(from
utils/graphHierarchy) — the "a root with children delegates
group-headship to each of its own children" rule, now shared between this
pass andGraphCanvas's own boundary rendering so the two always agree on
what a "group" is.GraphCanvas— opt-in live simulation forlayout="galaxy": instead
of a one-shot computed-then-frozen layout, run a continuous elastic
simulation. Drag any node ("sun") and its descendants follow in real time,
spring-pulled toward their algorithmic home position with collision
detection keeping siblings apart throughout; releasing a node leaves it
exactly where it was dropped, permanently pinned, same as a static-mode
drag. A sixthGraphToolbarbutton (galaxy layout only) toggles it —
purely internal state, no controlling prop. The simulation self-idles
(stops scheduling animation frames) once movement drops below a small
threshold for about half a second, and wakes on the next drag. New
exports:galaxySimulationStep(fromutils/galaxyLayout, the
single-step primitivegalaxyLayoutitself now loops over),
useGalaxySimulation(new hook,hooks/useGalaxySimulation), and a new
orbiticon.GraphCanvasContextValuegainslayout(read-only),
liveSimulation, andsetLiveSimulation.
New exports onGraphCanvasContextValue:isFullscreen,
toggleFullscreen. New icons:fullscreen,fullscreenExit.galaxyLayout— aspect-ratio-aware placement: a new
GalaxyLayoutOptions.aspectRatiooption (container width/height) warps
every orbital ring from a circle into an ellipse so the layout's overall
shape leans toward the container's own proportions, instead of always
producing a footprint that a wide-short or tall-narrow container then has
to letterbox — the established technique radial tree layouts (e.g.
Perforce'sIlvTree, thed3-radiallibrary) use for the same problem.
The correction is computed relative to the tree's own natural, unwarped
shape (measured with a first placement pass) rather than relative to a
hypothetical circle — a tree with a long single-child chain, in
particular, is often already far from circular on its own, and correcting
against the wrong baseline can misjudge the needed warp badly enough to
move the result in the wrong direction entirely. Both the target ratio and
the resulting correction factor are clamped to[1/8, 8]; the correction
is then bounded, not by a fixed damping constant, but by a binary search
over how strong a correction the graph can actually take —separationPass's
collision floor resists compression but not expansion, so an
undamped correction doesn't trade width for height the way it looks like
it should, it mostly just adds width, and how much depends heavily on the
graph's own structure (a single fixed damping constant measurably
under-corrected some real graphs while over-inflating others). The search
finds the strongest correction whose actual rendered area — measured via
one real settle step per trial, not just the raw elliptical placement,
which meaningfully overestimates distortion for graphs with several small
sibling groups — stays within roughly double the natural (unwarped) area.
New exportresolveAspectRatioScaleperforms this resolution and is what
galaxyLayoutcalls once per invocation (rather than once per settle
cycle) and what a live-simulation caller drivinggalaxySimulationStep
directly should call once per structural/target-ratio change (via its own
newresolvedAspectScaleoption) rather than paying the search's cost on
every animation frame. OmittedaspectRatio(or exactly1) reproduces
prior circular placement bit-for-bit.GraphCanvasfeeds its own real
container aspect ratio in automatically forlayout="galaxy"(rounded to
the nearest 5% so sub-pixel resize noise doesn't churn it) and pre-resolves
it the same way for its own live-simulation loop, and redraws the layout —
respecting a currently-pinned/dropped node's position exactly — whenever
that ratio changes meaningfully and pan/zoom isn't locked; live-simulation
mode eases toward a new shape smoothly via its existing spring mechanism
instead of snapping. No newGraphCanvasprop — the existing lock toggle
is the complete opt-out. Known limitation: even the strongest correction
the search allows has a practical ceiling —separationPassrefuses to
compress nodes past their own rendered size — so a layout dominated by on...