Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- **Fix: Explorer Distance Intelligence visible rendering** (PR #513 by @ZohaibHassan16, review fixes by @KaifAhmad1):
- Distance Intelligence now renders as a first-class visual state through the Sigma reducer/theme pipeline instead of mutating raw graph attributes directly.
- Ego mode fades and scales nodes by structural distance from the selected anchor; nodes outside `maxHops` are dimmed and label-suppressed.
- Heatmap mode renders a sampled local lens capped per ring (1-hop: ≤120, 2-hop: ≤650, 3-hop: ≤900 nodes shown); true counts remain visible in the status strip. Saturation detection reduces alpha for dense outer rings automatically.
- Structural mode highlights distance-aware context edges (colored by hop band) without breaking existing edge LOD.
- Semantic mode surfaces loading, unavailable, and error states visibly; edges colored by cosine similarity score.
- Trace Path inspector shows a distance band chip, hop count, and optional metric cards (confidence decay, semantic similarity, path coherence, bottleneck node) when path data is available.
- Added `GraphDistanceVisualState`, `GraphDistanceBucketCounts`, and `GraphHeatmapRenderSnapshot` types in `types.ts`; distance state flows through `GraphCanvas` → `buildReducerSceneState` → Sigma node/edge reducers.
- Added `buildStructuralDistanceSnapshot` (bounded BFS), `summarizeDistanceBuckets`, `buildHeatmapRenderSnapshot` (ring-capped deterministic sampling via `hashString` tiebreaker), `resolveDistanceNodeStyle`, and `resolveDistanceEdgeStyle` in `graphSceneState.ts`.
- Distance Intelligence status strip shows active mode, anchor label, per-ring node counts, sampled status, and a color legend.
- **Review blockers fixed** (follow-up by @KaifAhmad1 and @ZohaibHassan16): removed dead `if (anchorNodeId)` conditional in `buildHeatmapRenderSnapshot` (anchor always truthy past early-return guard); replaced O(n) `.includes()` call in the Sigma reducer hot path with a `WeakMap`-cached `Set.has()` lookup; renamed `GraphDistanceBucketCounts.threeHop → threeHopPlus` so the field accurately reflects ≥ 3 hops and updated status strip labels to "3+ hop"; restored `hasMetrics` guard in `PathDistanceIntelPanel` to suppress the empty metric grid `<div>` when a path result carries no optional metric fields.

- **Feature: Graph Explorer visual refresh** (PR #503 by @ZohaibHassan16, conflict resolution by @KaifAhmad1):
- Extracted all hardcoded `rgba(...)` color literals into a structured `ui.*` design-token namespace in `graphTheme.ts` — covering `ui.text`, `ui.surface`, `ui.scene`, `ui.control`, `ui.timeline`, and `ui.interaction`. Future theming is now a one-file change.
- Added `GraphEntityShapeVariant` type and per-shape config (`fillAlpha`, `shellAlpha`, `coreScale`, `borderBoost`, `minSize`) for biomolecule, condition, compound, process, community, and entity node kinds. Shell and fill colors now derive from per-entity-shape config rather than uniform overrides.
Expand Down
87 changes: 62 additions & 25 deletions explorer/src/workspaces/GraphWorkspace/GraphCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import {
mapFullEdgeClassToVisualState,
resolveEdgeElementStyle,
resolveEdgeVisualState,
resolveDistanceEdgeStyle,
resolveDistanceNodeStyle,
resolveNodeElementStyle,
resolveNodeVisualState,
} from "./graphSceneState";
Expand Down Expand Up @@ -61,6 +63,7 @@ import {
import type {
GraphAnalyticsSnapshot,
GraphCameraState,
GraphDistanceVisualState,
GraphDisplayMeta,
GraphDisplayStateSnapshot,
GraphDiagnosticsSnapshot,
Expand Down Expand Up @@ -101,6 +104,7 @@ export interface GraphCanvasProps {
selectedEdgeId: string;
activePath?: string[];
activePathEdgeIds?: string[];
distanceVisualState?: GraphDistanceVisualState;
effectsState: GraphEffectsState;
temporalState?: GraphTemporalState | null;
isLayoutRunning: boolean;
Expand Down Expand Up @@ -829,6 +833,7 @@ type ReducerSceneState = {
pathEdgeIds: Set<string>;
highlightedIncidentEdgeIds: Set<string>;
overviewBackboneEdgeIds: Set<string>;
distanceVisualState?: GraphDistanceVisualState;
};

const FULL_EDGE_CLASSES: GraphFullEdgeClass[] = [
Expand Down Expand Up @@ -952,6 +957,7 @@ function buildReducerSceneState(
interactionState: GraphInteractionState,
displayState?: GraphDisplayStateSnapshot,
analyticsSnapshot?: GraphAnalyticsSnapshot | null,
distanceVisualState?: GraphDistanceVisualState,
): ReducerSceneState {
const { viewMode, zoomTier, hoveredNodeId, selectedNodeId, selectedEdgeId, activePath } = interactionState;
const primaryNodeId = hoveredNodeId || selectedNodeId;
Expand All @@ -977,6 +983,7 @@ function buildReducerSceneState(
pathNodeIds: new Set(activePath),
pathEdgeIds,
overviewBackboneEdgeIds: new Set(analyticsSnapshot?.overviewBackbone.edgeIds ?? []),
distanceVisualState,
highlightedIncidentEdgeIds: buildHighlightedIncidentEdgeIds(
displayGraph,
interactionState,
Expand Down Expand Up @@ -1092,24 +1099,34 @@ function applySceneState(
data.label,
cameraRatio,
);
const distanceStyle = currentState.viewMode === "full"
? resolveDistanceNodeStyle(
GRAPH_THEME,
currentState.zoomTier,
style,
currentState.distanceVisualState,
String(node),
)
: {};
const resolvedStyle = { ...style, ...distanceStyle };

return {
...data,
color: style.color,
shellColor: style.shellColor,
coreScale: style.coreScale,
size: style.size,
forceLabel: style.forceLabel,
label: style.label,
zIndex: style.zIndex,
hidden: style.hidden,
borderColor: style.borderColor,
borderSize: style.borderSize,
ringColor: style.showRing ? style.ringColor : style.borderColor,
ringSize: style.ringSize,
entityShape: style.entityShape,
entityShapeKind: style.entityShapeKind,
entityAspectRatio: style.entityAspectRatio,
color: resolvedStyle.color,
shellColor: resolvedStyle.shellColor,
coreScale: resolvedStyle.coreScale,
size: resolvedStyle.size,
forceLabel: resolvedStyle.forceLabel,
label: resolvedStyle.label,
zIndex: resolvedStyle.zIndex,
hidden: resolvedStyle.hidden,
borderColor: resolvedStyle.borderColor,
borderSize: resolvedStyle.borderSize,
ringColor: resolvedStyle.showRing ? resolvedStyle.ringColor : resolvedStyle.borderColor,
ringSize: resolvedStyle.ringSize,
entityShape: resolvedStyle.entityShape,
entityShapeKind: resolvedStyle.entityShapeKind,
entityAspectRatio: resolvedStyle.entityAspectRatio,
};
});

Expand Down Expand Up @@ -1173,15 +1190,25 @@ function applySceneState(
stableEdgeId,
fullEdgeClass,
);
const distanceStyle = currentState.viewMode === "full"
? resolveDistanceEdgeStyle(
style,
currentState.distanceVisualState,
String(source),
String(target),
fullEdgeClass,
)
: {};
const resolvedStyle = { ...style, ...distanceStyle };

return {
...data,
hidden: style.hidden,
type: style.type,
color: style.color,
size: style.size,
zIndex: style.zIndex,
curvature: style.curvature,
hidden: resolvedStyle.hidden,
type: resolvedStyle.type,
color: resolvedStyle.color,
size: resolvedStyle.size,
zIndex: resolvedStyle.zIndex,
curvature: resolvedStyle.curvature,
};
});

Expand Down Expand Up @@ -1225,6 +1252,7 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle, GraphCanvasProps>(
selectedEdgeId,
activePath = [],
activePathEdgeIds = [],
distanceVisualState,
effectsState,
temporalState,
isLayoutRunning,
Expand Down Expand Up @@ -1259,6 +1287,7 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle, GraphCanvasProps>(
const graphVersionRef = useRef(graphVersion);
const selectedNodeIdRef = useRef(selectedNodeId);
const focusedNodeIdRef = useRef(focusedNodeId);
const distanceVisualStateRef = useRef(distanceVisualState);
const viewModeRef = useRef(viewMode);
const onNodeClickRef = useRef(onNodeClick);
const onEdgeClickRef = useRef(onEdgeClick);
Expand All @@ -1285,6 +1314,7 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle, GraphCanvasProps>(
graphVersionRef.current = graphVersion;
selectedNodeIdRef.current = selectedNodeId;
focusedNodeIdRef.current = focusedNodeId;
distanceVisualStateRef.current = distanceVisualState;
viewModeRef.current = viewMode;
onNodeClickRef.current = onNodeClick;
onEdgeClickRef.current = onEdgeClick;
Expand Down Expand Up @@ -1331,6 +1361,7 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle, GraphCanvasProps>(
const interactionStateRef = useRef<GraphInteractionState>(interactionState);
interactionStateRef.current = interactionState;
const previousInteractionStateRef = useRef<GraphInteractionState | null>(null);
const previousDistanceVisualStateRef = useRef<GraphDistanceVisualState | undefined>(undefined);
useEffect(() => {
if (!isLayoutRunning) {
setLayoutSettledEpoch((epoch) => epoch + 1);
Expand All @@ -1347,8 +1378,8 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle, GraphCanvasProps>(
[displayGraph, shouldComputeCentrality, shouldComputeCommunities],
);
const reducerSceneState = useMemo(
() => buildReducerSceneState(displayGraph, interactionState, displayState, analyticsSnapshot),
[analyticsSnapshot, displayGraph, displayState, interactionState],
() => buildReducerSceneState(displayGraph, interactionState, displayState, analyticsSnapshot, distanceVisualState),
[analyticsSnapshot, displayGraph, displayState, distanceVisualState, interactionState],
);
const reducerSceneStateRef = useRef<ReducerSceneState>(reducerSceneState);
reducerSceneStateRef.current = reducerSceneState;
Expand Down Expand Up @@ -1975,6 +2006,7 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle, GraphCanvasProps>(
appliedGraphVersionRef.current = graphVersion;
fittedDisplaySignatureRef.current = null;
previousInteractionStateRef.current = null;
previousDistanceVisualStateRef.current = undefined;
behaviorContextRef.current = getBehaviorContext(sigma);
if (runtimeRef.current) {
runtimeRef.current.displayGraph = displayGraph;
Expand Down Expand Up @@ -2094,6 +2126,7 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle, GraphCanvasProps>(
effectAvailability: availability,
edgeClasses: edgeClassDiagnostics,
structureLayer: structureLayerDiagnosticsRef.current ?? structureLayerDiagnostics,
distanceVisual: distanceVisualStateRef.current,
});
if (import.meta.env.DEV && effectsState.diagnosticsEnabled) {
console.debug("[Edge Truth]", edgeClassDiagnostics);
Expand All @@ -2107,10 +2140,12 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle, GraphCanvasProps>(
onDiagnosticsChange,
structureLayerDiagnostics,
temporalState,
distanceVisualState,
]);

useEffect(() => {
previousInteractionStateRef.current = null;
previousDistanceVisualStateRef.current = undefined;
}, [displayGraph]);

useEffect(() => {
Expand All @@ -2120,13 +2155,15 @@ export const GraphCanvas = forwardRef<GraphCanvasHandle, GraphCanvasProps>(
}

const previousInteractionState = previousInteractionStateRef.current;
const refreshTargets = previousInteractionState
const distanceVisualStateChanged = previousDistanceVisualStateRef.current !== distanceVisualState;
const refreshTargets = !distanceVisualStateChanged && previousInteractionState
? collectInteractionRefreshTargets(displayGraph, previousInteractionState, interactionState)
: undefined;

applySceneState(sigma, reducerSceneStateRef, reducerWarningStateRef, refreshTargets);
previousInteractionStateRef.current = interactionState;
}, [displayGraph, interactionState, reducerSceneStateRef]);
previousDistanceVisualStateRef.current = distanceVisualState;
}, [displayGraph, distanceVisualState, interactionState, reducerSceneStateRef]);

const drawStructureLayerFrame = useCallback(() => {
const sigma = sigmaRef.current;
Expand Down
11 changes: 4 additions & 7 deletions explorer/src/workspaces/GraphWorkspace/GraphInspectorPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,12 @@ const BAND_COLORS: Record<string, string> = {
};

function PathDistanceIntelPanel({ result }: { result: PathResponse }) {
const bandColor = BAND_COLORS[result.distance_band] ?? "#8b949e";
const hasMetrics =
result.confidence_decay != null ||
result.semantic_similarity != null ||
result.path_coherence_score != null ||
result.bottleneck_node ||
result.interpretation;
if (!hasMetrics) return null;

const bandColor = BAND_COLORS[result.distance_band] ?? "#8b949e";
result.bottleneck_node != null;

return (
<div style={{ display: "flex", flexDirection: "column", gap: 8, marginTop: 8 }}>
Expand All @@ -96,7 +93,7 @@ function PathDistanceIntelPanel({ result }: { result: PathResponse }) {
</div>

{/* metric grid */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
{hasMetrics && <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
{result.confidence_decay != null && (
<div style={metricCardStyle}>
<div style={metricLabelStyle}>Confidence Decay</div>
Expand Down Expand Up @@ -157,7 +154,7 @@ function PathDistanceIntelPanel({ result }: { result: PathResponse }) {
</div>
</div>
)}
</div>
</div>}

{/* interpretation */}
{result.interpretation && (
Expand Down
Loading
Loading