<!-- GENERATED by scripts/build-llms.mjs from llms/retrieval.md — do not edit this file. -->

# `lr-knowledge-graph-explorer`

- **Import** `import '@aceshooting/lyra-ui/components/lr-knowledge-graph-explorer.js';` (stable tag alias; registers the tag)
- **Class** `LyraKnowledgeGraphExplorer`, also available unregistered from `@aceshooting/lyra-ui/components/retrieval/knowledge-graph-explorer/knowledge-graph-explorer.class.js`
- **Family** `components/retrieval/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.1.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md); family-wide breaking-change summaries: [llms-full.txt](../../llms-full.txt)
- **Deprecations** none
- **Optional peers** `d3-drag`, `d3-force`, `d3-selection`, `d3-zoom` — see `llms/peers.md`
- **Themeable via** 13 parts, 1 custom property — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-knowledge-graph-explorer`

Orchestration-level knowledge-graph surface: the `lr-graph` canvas plus entity search, type filters,
neighborhood expansion, pinned nodes, path finding between pins, node selection, and a details
overlay. Composes `lr-graph`, `lr-graph-legend`, `lr-entity-card`, `lr-neighbor-list`,
`lr-path-strip`, and `lr-popover.showAt()`.

Removing `search-query` retains `null` property readback while clearing the search input, results
and search dimming; an explicitly empty value remains empty and later queries work normally.
Path-node `lr-entity-activate` is consumed by the explorer and enters the same selection, graph
focus and details flow as other entity activations, emitting one `lr-selection-change`.
`lr-relation-activate` continues to pass through unchanged. The canonical search event fields remain
`query`, `matchCount`, and `matchCountExact`.

**Properties:** (host-supplied data, identity-normalized before rendering)

- `nodes: LyraGraphNode[] = []`, `links: LyraGraphLink[] = []`, `nodeTypes: LyraNodeTypeStyle[] = []`,
  `communities: LyraGraphCommunity[] = []` (all attribute: false) — exactly `lr-graph`'s own types,
  projected through `lr-graph`'s shared nonblank, first-wins identity policy before derived lookups
  and forwarding: `LyraGraphNode { id: string; label?: string; accessibleLabel?: string;
  description?: string; radius?: number; color?: string; type?: string; expandable?: boolean;
  communityId?: string }`, `LyraGraphLink { id?: string; source: string; target: string; width?:
  number; label?: string; accessibleLabel?: string; description?: string; directed?: boolean;
  color?: string; dash?: number[] }` (source/target are node ids), `LyraNodeTypeStyle { id: string;
  label: string; color?: string; shape?: 'circle' | 'square' | 'diamond' }`, and
  `LyraGraphCommunity { id: string; label?: string; memberIds: string[]; color?: string }` — the full
  field-by-field semantics (color/shape resolution precedence, dangling-link handling, hull
  membership, etc.) are documented in `llms/components/lr-graph.md`'s own **Properties** list
- `entityDetails: Record<string, LyraKnowledgeGraphEntityDetails> = {}` (attribute: false) —
  `LyraKnowledgeGraphEntityDetails = Pick<LyraEntity, 'description' | 'properties' | 'degree'>`, i.e.
  `{ description?: string; properties?: Record<string, string | number>; degree?: number }`, keyed by
  node id. Merged onto the matching `LyraGraphNode` to build the entity shown in the details popover and
  neighbor rows. A node with no entry still renders: `degree` falls back to a live count derived from
  `links`, `description`/`properties` are omitted
- `path: LyraPathElement[] = []` (attribute: false) — host-supplied path-finding _result_, rendered
  via `lr-path-strip` (`{ kind: 'node'; node: LyraEntity } | { kind: 'edge'; relation: string;
directed?: boolean; reverse?: boolean }`). Empty renders no strip

(self-managed but presettable/observable — this component toggles its own copy on interaction, the
same self-toggle-then-emit contract `lr-graph-legend` uses, so every feature works with zero host wiring)

- `hiddenTypes: string[] = []` (attribute: false) — forwarded to both `lr-graph.hiddenTypes` and
  `lr-graph-legend.hiddenTypes`; hidden nodes are also excluded from explorer search results,
  neighbor rows, and activation
- `selectedNodeId: string | null = null` (attribute `selected-node-id`) — drives the details popover
  and `lr-graph.selectedNodeIds`; `null` shows no selection and keeps the popover closed
- `pinnedNodeIds: string[] = []` (attribute: false) — exactly two pinned nodes reveals the "Find
  path" action
- `searchQuery: string = ''` (attribute `search-query`) — the filter applied to the visible node
  set, driving `[part="search-results"]` and the search-match dimming forwarded to `lr-graph`. A node
  matches when the query appears in **any** name it can be known by — its `id`, its `label` or its
  `accessibleLabel` — each folded with the active locale, so a node named only through
  `accessibleLabel` is findable by the very name the search results, pinned chips and details
  popover already display for it, and a node carrying both a `label` and an `accessibleLabel` matches
  either. Presettable, so a host can deep-link straight into a filtered view; the toolbar's search
  box keeps it up to date afterwards. A missing or nonstring `label`/`accessibleLabel` is skipped
  while the node's valid string id remains searchable

(presentation)

- `renderer: 'svg' | 'canvas' = 'svg'` — forwarded to `lr-graph.renderer`
- `fitTo: 'none' | 'container' = 'none'` (attribute `fit-to`) — forwarded to `lr-graph.fitTo`.
  `'container'` makes the composed graph draw at exactly the pane this component's own layout gave
  it — the reservation minus whatever the toolbar, search results, pinned row and path strip take,
  which is not derivable from `height` — and follow it live as the explorer is resized. `'none'`
  keeps forwarding the numeric `width`/`height` below unchanged. See `fitTo`'s own entry in
  `llms/components/lr-graph.md`
- `width: number = 800`, `height: number = 600` — `height` also sizes the composed graph's own
  rendered box (`[part="graph"]`) once the explorer's own layout gives it room, the same fallback
  chain `lr-graph.height` uses on its own host
- `nodeLabels?: 'always' | 'zoom' | 'none'` (attribute `node-labels`) — forwarded to
  `lr-graph.nodeLabels`. Unset (the default) leaves the composed `lr-graph` to apply its own
  per-renderer default — see `nodeLabels`'s own entry in `llms/components/lr-graph.md`
- `highlight: 'selection' | 'hover' | 'none' = 'selection'` — what drives the dimming forwarded to
  `lr-graph`'s `dimmedNodeIds`/`dimmedLinkIds`, on top of the always-active search-match dimming:
  `'selection'` dims by the selected node's immediate neighborhood; `'hover'` additionally dims by
  the pointer-hovered node (falling back to selection while nothing is hovered); `'none'` forwards
  empty arrays regardless of search/selection state, for a host driving dimming its own way
- `label?: string` — fallback name for the root group; omission uses localized
  `graphExplorerLabel`. A non-empty host `aria-label` makes the host the sole overall owner; an
  explicitly empty host label stays empty on the group

**Events:**

- `lr-selection-change` (`detail: { selectedNodeId: string | null }`) — emitted after the explorer
  changes its own selection through search, graph, keyboard/neighborhood/path activation, or
  closing/invalidating the details selection. Clearing reports `null`. Direct host assignments to
  `selectedNodeId` remain silent, and one interaction emits at most once even when a composed
  child also reports its primitive click event.
- `lr-path-request` (`detail: { sourceNodeId: string; targetNodeId: string }`) — the "Find path" action was
  activated with exactly two nodes pinned. This component has no traversal algorithm; the host
  computes/fetches the path and assigns it back through `path`.
- `lr-pin-change` (`detail: { pinnedNodeIds: string[] }`) — the complete updated array. Already
  self-applied before emitting, so reassigning back is optional.
- `lr-hidden-types-change` (`detail: { hiddenTypes: string[] }`) — a node type's visibility changed
  via the composed legend. Follows the same self-toggle-then-emit contract as `lr-pin-change`/
  `lr-search-change`, so reassigning back is optional.
- `lr-search-change` (`detail: { query: string; matchCount: number; matchCountExact: boolean }`) —
  the user typed in the toolbar's search box. `query` is the canonical `LyraSearchChangeDetail`
  field name. `matchCount` is the same live node-filter total the result list and its live-region
  announcement already compute (`0` while the query is empty). `matchCountExact` is always `true` —
  this component's node filter has no truncating ceiling, unlike a paginated text-search viewer.
  There is no `activeIndex`: this is a live node filter, not a cursor-based search. The component
  has already applied the query to its own `searchQuery` property before emitting, so reassigning
  it back is optional and a direct host assignment stays silent.
- Bubbling straight through from composed children, unmodified: `lr-node-click`
  (`detail: { nodeId, x, y }`), `lr-link-click` (`detail: { sourceNodeId, targetNodeId, linkId? }`), `lr-community-click`
  (`detail: { communityId }`), `lr-node-expand` (`detail: { nodeId }`, from `lr-graph` and/or `lr-neighbor-list`),
  `lr-relation-activate` (`detail: { relation, sourceNodeId?, targetNodeId?, occurrenceIndex }`, from `lr-path-strip`).

**Slots:** `details` — overrides the details popover's default content (an `lr-entity-card` with a
nested `lr-neighbor-list` and a pin toggle) entirely, including the two additive slots below.
Receives no data; an overriding consumer reads the selected entity from `selectedNodeId`/`nodes`
itself. `detail-body` — additive content appended inside the default `lr-entity-card`'s body,
alongside its `lr-neighbor-list`; no effect while `details` is overridden. `detail-actions` —
additive content appended into the default `lr-entity-card`'s `actions` slot, beside its built-in
pin toggle; no effect while `details` is overridden.

**CSS parts:** `base` (`role="group"` unless a non-empty host label owns the component), `toolbar`,
`search` (the search `lr-input`), `legend` (the
composed `lr-graph-legend`), `search-results` (only while `searchQuery` is non-empty),
`search-result` (`role="listitem"` wrapping a `<button>`), `search-empty`, `pinned` (only while
`pinnedNodeIds` is non-empty), `pinned-heading`, `graph` (the composed `lr-graph`), `path` (only
while `path` is non-empty), `detail-popover`, `detail-card`.

**Themeable custom properties:** `--lr-canvas-reserved-height` (default
`var(--lr-size-24rem)`) sets the explorer's host block size and matches its pre-upgrade
reservation. The composed graph's own `[part="graph"]` box additionally falls back to the
explorer's `height` property (through a private custom property) beneath this same reservation
name, so setting `--lr-canvas-reserved-height` anywhere above the explorer overrides `height` for
the composed graph too. Retheme the composed graph through `lr-graph`'s own custom properties — `--lr-node-fill`,
`--lr-link-color`, `--lr-graph-cat-1` through `-8`, `--lr-graph-edge-label-halo`,
`--lr-graph-focus-halo-color`, `--lr-graph-selected-color`, `--lr-graph-dimmed-opacity`, and
`--lr-graph-hull-fill`/`-opacity` — documented in `llms/components/lr-graph.md`'s own
**Themeable custom properties** list.

**Optional peer deps:** `lr-graph`'s `d3-force`/`d3-drag`/`d3-zoom`/`d3-selection` set, transitively.

**Known gotchas:**

- The host defaults to `--lr-canvas-reserved-height` (`24rem`). An explicit height on the host
  bounds the whole explorer: `[part="base"]` fills it and
  `[part="graph"]` takes whatever the toolbar, search results, pinned row and path strip leave over,
  rather than the graph sizing itself from its own intrinsic aspect ratio. Within that allocation,
  the composed graph's own `height` property still contributes its configured block size (subject
  to `--lr-canvas-reserved-height` winning, and to flex-shrink if the host itself has no room to
  grow) — it is not purely cosmetic even though the host's own footprint is governed separately.
- A node's displayed name (search results, pinned chips, the details popover's accessible name)
  resolves `LyraGraphNode.label || LyraGraphNode.accessibleLabel || LyraGraphNode.id` — a node with
  only a spoken `accessibleLabel` (no visible `label`) still shows that text instead of falling
  straight through to its raw id.
- `lr-graph.getNodePosition()` and `lr-node-click`'s `{ x, y }` are graph-_local_ drawing
  coordinates, never viewport pixels. For `renderer="svg"` this component resolves the real viewport
  rect from `event.composedPath()`'s `[part="node"]` element; for `renderer="canvas"` (no per-node
  DOM) it uses the click's `clientX`/`clientY`. While an svg-click popover stays open it re-anchors
  on every `lr-viewport-change` from the graph — no `requestAnimationFrame` polling loop.
- Selecting a node any _other_ way (search result, neighbor row, path element, keyboard Enter/Space
  on a graph node — which dispatches no native `click`) has no rect to read, so it calls
  `lr-graph.focusNode(id)` and anchors at the graph element's own bounding-box center instead; no
  continuous tracking applies on that path.

---
