fix(explorer): address grouped view review blockers

- Fix `import.meta.env.DEV` crash in graphSceneState.ts that broke the
  entire test:graph-workspace suite (module load fails in Node.js/tsx)
- Export `resolveGroupedDisplayNodeId` from graphSceneState.ts and
  remove the identical copy in GraphWorkspace.tsx
- Add `checkGroupedViewAvailability` helper (Louvain only, no centrality)
  so grouped view availability can be checked cheaply on every graph change
- Gate full community graph build (`groupedDisplayCandidate`) on
  `viewMode === 'grouped'` to avoid running Louvain + centrality on every
  graph version tick when the user is not in grouped view
- Remove dead ternary in `focusNode` where both branches returned `nodeId`
- Add 7 new tests covering resolveGroupedDisplayNodeId,
  resolveGroupedDisplayStateSnapshot, and checkGroupedViewAvailability

Co-Authored-By: ZohaibHassan16 <109234410+ZohaibHassan16@users.noreply.github.com>
Co-Authored-By: KaifAhmad1 <mohammadk78600@gmail.com>
This commit is contained in:
KaifAhmad1
2026-04-25 13:42:15 +05:30
co-authored by ZohaibHassan16 KaifAhmad1
parent b010ba68fa
commit 6ad1502224
3 changed files with 160 additions and 58 deletions
@@ -12,7 +12,7 @@ import { useLoadGraph, useReloadGraph } from "./useLoadGraph";
import { GraphLoadingOverlay } from "./GraphLoadingOverlay";
import { createGraphLoadProgress, getGraphLoadTitle } from "./graphLoading";
import { GRAPH_THEME, withAlpha } from "./graphTheme";
import { resolveDisplayGraph, resolveDisplayStateSnapshot, resolveGroupedDisplayStateSnapshot } from "./graphSceneState";
import { checkGroupedViewAvailability, resolveDisplayGraph, resolveDisplayStateSnapshot, resolveGroupedDisplayNodeId, resolveGroupedDisplayStateSnapshot } from "./graphSceneState";
import {
type GraphPlugin,
type GraphPluginActionRequest,
@@ -548,47 +548,6 @@ type FocusResolution = {
reason: string | null;
};
function resolveGroupedDisplayNodeId(
displayGraph: GraphSceneRuntime["displayGraph"],
nodeId: string,
): string | null {
if (!nodeId) {
return null;
}
if (displayGraph.hasNode(nodeId)) {
return nodeId;
}
let resolvedNodeId: string | null = null;
displayGraph.forEachNode((candidateId, attrs) => {
if (resolvedNodeId) {
return;
}
const communityGroup = (attrs as NodeAttributes).properties?.__communityGroup as
| {
anchorNodeId?: string | null;
memberNodeIds?: string[];
sampleNodeIds?: string[];
}
| undefined;
if (!communityGroup) {
return;
}
if (
communityGroup.anchorNodeId === nodeId
|| communityGroup.sampleNodeIds?.includes(nodeId)
|| communityGroup.memberNodeIds?.includes(nodeId)
) {
resolvedNodeId = candidateId;
}
});
return resolvedNodeId;
}
function buildSelectedEdgeState(
edgeId: string,
displayGraph: typeof graph | Graph<NodeAttributes, EdgeAttributes>,
@@ -953,15 +912,19 @@ export function GraphWorkspace() {
);
const inspectableNodeId = focusedSelectionResolution.resolvedNodeId ?? "";
const canActivateFocusedMode = Boolean(focusedSelectionResolution.resolvedNodeId);
const { available: groupedViewAvailable, reason: groupedViewReason } = useMemo(
() => checkGroupedViewAvailability(),
[graphVersion],
);
const groupedDisplayCandidate = useMemo(
() => resolveDisplayGraph("", EMPTY_PATH, EMPTY_PATH, "grouped", {
aggregationEnabled,
collapsedNeighborhoodNodeIds,
}),
[aggregationEnabled, collapsedNeighborhoodNodeIds, graphVersion],
() => viewMode === "grouped"
? resolveDisplayGraph("", EMPTY_PATH, EMPTY_PATH, "grouped", {
aggregationEnabled,
collapsedNeighborhoodNodeIds,
})
: null,
[viewMode, aggregationEnabled, collapsedNeighborhoodNodeIds, graphVersion],
);
const groupedViewAvailable = groupedDisplayCandidate.state.groupedViewAvailable;
const groupedViewReason = groupedDisplayCandidate.state.groupedViewReason;
const requestViewMode = useCallback((nextViewMode: GraphViewMode) => {
if (nextViewMode === "focused") {
@@ -986,7 +949,11 @@ export function GraphWorkspace() {
return;
}
const groupedDisplayGraph = groupedDisplayCandidate.graph;
const groupedDisplayGraph = groupedDisplayCandidate?.graph
?? resolveDisplayGraph("", EMPTY_PATH, EMPTY_PATH, "grouped", {
aggregationEnabled,
collapsedNeighborhoodNodeIds,
}).graph;
const nextGroupedSelection = [
lastGroupedSelectedNodeId,
selectedNodeId,
@@ -1016,7 +983,7 @@ export function GraphWorkspace() {
collapsedNeighborhoodNodeIds,
focusedNodeId,
graphVersion,
groupedDisplayCandidate.graph,
groupedDisplayCandidate,
groupedViewAvailable,
groupedViewReason,
lastGroupedSelectedNodeId,
@@ -1030,9 +997,7 @@ export function GraphWorkspace() {
}
const currentDisplayGraph = pluginRuntimeRef.current?.displayGraph ?? graph;
const nextSelectedNodeId = viewMode === "focused" && graph.hasNode(nodeId)
? nodeId
: nodeId;
const nextSelectedNodeId = nodeId;
if (!graph.hasNode(nodeId) && currentDisplayGraph.hasNode(nodeId)) {
setLastGroupedSelectedNodeId(nodeId);
@@ -1218,7 +1183,10 @@ export function GraphWorkspace() {
const displayResult = useMemo(
() => (
viewMode === "grouped"
? groupedDisplayCandidate
? (groupedDisplayCandidate ?? resolveDisplayGraph("", EMPTY_PATH, EMPTY_PATH, "grouped", {
aggregationEnabled,
collapsedNeighborhoodNodeIds,
}))
: resolveDisplayGraph(structuralSelectedNodeId, structuralActivePath, structuralActivePathEdgeIds, viewMode, {
aggregationEnabled,
collapsedNeighborhoodNodeIds,
@@ -35,7 +35,7 @@ const COLLAPSE_VISIBLE_NEIGHBORS = 8;
const GROUP_SAMPLE_MEMBERS = 8;
const AGGREGATED_EDGE_PREFIX = "__agg__:";
const COMMUNITY_NODE_PREFIX = "__community__:";
const DEBUG_GRAPH_SCENE_STATE = import.meta.env.DEV;
const DEBUG_GRAPH_SCENE_STATE = typeof import.meta !== "undefined" && import.meta.env?.DEV === true;
type GraphRef = typeof graph | Graph<NodeAttributes, EdgeAttributes>;
@@ -159,7 +159,7 @@ function createEmptyDisplayState(
};
}
function resolveGroupedDisplayNodeId(
export function resolveGroupedDisplayNodeId(
displayGraph: GraphRef,
nodeId: string,
): string | null {
@@ -201,6 +201,18 @@ function resolveGroupedDisplayNodeId(
return resolvedNodeId;
}
export function checkGroupedViewAvailability(): { available: boolean; reason: string | null } {
const base = computeGraphAnalyticsBase(graph, {
computeCommunities: true,
computeCentrality: false,
});
const available = base.communitiesByNode.size > 0;
return {
available,
reason: available ? null : "Grouped view is unavailable until communities can be detected.",
};
}
function rankGroupedNeighbors(
displayGraph: GraphRef,
nodeId: string,
+123 -1
View File
@@ -6,7 +6,12 @@ import {
batchMergeNodes,
clearGraph,
} from "../src/store/graphStore.ts";
import { resolveDisplayGraph } from "../src/workspaces/GraphWorkspace/graphSceneState.ts";
import {
checkGroupedViewAvailability,
resolveDisplayGraph,
resolveGroupedDisplayNodeId,
resolveGroupedDisplayStateSnapshot,
} from "../src/workspaces/GraphWorkspace/graphSceneState.ts";
function addNode(id: string, semanticGroup = "entity") {
batchMergeNodes([
@@ -135,3 +140,120 @@ test("resolveDisplayGraph grouped view emits community nodes and edges", () => {
assert.equal(hasCommunityEdge, true);
});
// ── resolveGroupedDisplayNodeId ──────────────────────────────────────────────
test("resolveGroupedDisplayNodeId returns null for empty nodeId", () => {
const { graph: displayGraph } = resolveDisplayGraph("", [], [], "full", { aggregationEnabled: false });
assert.equal(resolveGroupedDisplayNodeId(displayGraph, ""), null);
});
test("resolveGroupedDisplayNodeId returns nodeId when it exists directly in display graph", () => {
addNode("x");
const { graph: displayGraph } = resolveDisplayGraph("", [], [], "full", { aggregationEnabled: false });
assert.equal(resolveGroupedDisplayNodeId(displayGraph, "x"), "x");
});
test("resolveGroupedDisplayNodeId resolves base node to its community node", () => {
const left = ["a1", "a2", "a3", "a4"];
const right = ["b1", "b2", "b3", "b4"];
[...left, ...right].forEach((nodeId, index) => addNode(nodeId, index < left.length ? "left" : "right"));
let edgeIndex = 0;
for (let i = 0; i < left.length; i += 1) {
for (let j = 0; j < left.length; j += 1) {
if (i !== j) addEdge(`l-${edgeIndex++}`, left[i], left[j], 3);
}
}
for (let i = 0; i < right.length; i += 1) {
for (let j = 0; j < right.length; j += 1) {
if (i !== j) addEdge(`r-${edgeIndex++}`, right[i], right[j], 3);
}
}
addEdge("bridge-1", "a1", "b1", 0.1);
const { graph: displayGraph } = resolveDisplayGraph("", [], [], "grouped", { aggregationEnabled: true });
const communityNodes = displayGraph.nodes().filter((n) => n.startsWith("__community__"));
assert.ok(communityNodes.length >= 2, "expected community nodes");
const resolved = resolveGroupedDisplayNodeId(displayGraph, "a1");
assert.ok(resolved !== null, "should resolve a1 to a community node");
assert.ok(resolved!.startsWith("__community__"), "resolved id should be a community node");
});
// ── resolveGroupedDisplayStateSnapshot ──────────────────────────────────────
test("resolveGroupedDisplayStateSnapshot returns none-kind when no node selected", () => {
addNode("p");
addNode("q");
addEdge("e1", "p", "q");
const { graph: displayGraph } = resolveDisplayGraph("", [], [], "full", { aggregationEnabled: false });
const state = resolveGroupedDisplayStateSnapshot(displayGraph, "", {
groupedViewAvailable: true,
groupedViewReason: null,
});
assert.equal(state.selectedNodeKind, "none");
assert.equal(state.selectedRootNodeId, null);
});
test("resolveGroupedDisplayStateSnapshot maps selected base node to community in grouped graph", () => {
const left = ["c1", "c2", "c3", "c4"];
const right = ["d1", "d2", "d3", "d4"];
[...left, ...right].forEach((nodeId, index) => addNode(nodeId, index < left.length ? "left" : "right"));
let edgeIndex = 0;
for (let i = 0; i < left.length; i += 1) {
for (let j = 0; j < left.length; j += 1) {
if (i !== j) addEdge(`lc-${edgeIndex++}`, left[i], left[j], 3);
}
}
for (let i = 0; i < right.length; i += 1) {
for (let j = 0; j < right.length; j += 1) {
if (i !== j) addEdge(`rc-${edgeIndex++}`, right[i], right[j], 3);
}
}
addEdge("bridge-c1", "c1", "d1", 0.1);
addEdge("bridge-c2", "c2", "d2", 0.1);
const { graph: displayGraph } = resolveDisplayGraph("", [], [], "grouped", { aggregationEnabled: true });
const state = resolveGroupedDisplayStateSnapshot(displayGraph, "c1", {
groupedViewAvailable: true,
groupedViewReason: null,
selectedNodeKind: "grouped",
});
assert.ok(state.selectedRootNodeId !== null, "should resolve to a community node");
assert.ok(state.selectedRootNodeId!.startsWith("__community__"), "root should be a community node");
assert.equal(state.groupedViewAvailable, true);
});
// ── checkGroupedViewAvailability ─────────────────────────────────────────────
test("checkGroupedViewAvailability returns unavailable on empty graph", () => {
const result = checkGroupedViewAvailability();
assert.equal(result.available, false);
assert.ok(typeof result.reason === "string" && result.reason.length > 0);
});
test("checkGroupedViewAvailability returns available when communities exist", () => {
const left = ["e1", "e2", "e3", "e4"];
const right = ["f1", "f2", "f3", "f4"];
[...left, ...right].forEach((nodeId, index) => addNode(nodeId, index < left.length ? "left" : "right"));
let edgeIndex = 0;
for (let i = 0; i < left.length; i += 1) {
for (let j = 0; j < left.length; j += 1) {
if (i !== j) addEdge(`le-${edgeIndex++}`, left[i], left[j], 3);
}
}
for (let i = 0; i < right.length; i += 1) {
for (let j = 0; j < right.length; j += 1) {
if (i !== j) addEdge(`re-${edgeIndex++}`, right[i], right[j], 3);
}
}
addEdge("bridge-e1", "e1", "f1", 0.1);
const result = checkGroupedViewAvailability();
assert.equal(result.available, true);
assert.equal(result.reason, null);
});