fix(explorer): write diff highlights to displayGraph as well as store graph

fixed qodo review

applyDiffHighlight/clearDiffHighlight were writing baseColor only to
graphStore.graph (the store singleton), but Sigma is constructed with
displayGraphRef.current and the nodeReducer reads attributes from that
instance. When the display graph is a derived copy (aggregated,
focused, or grouped view), the store write has no effect on the
currently-rendered frame -- sigma.scheduleRefresh() flushes the
reducer over the display graph, which did not receive the mutation.

Fix: introduce writeBaseColor(context, nodeId, color) which writes to
BOTH the store graph (so the color propagates into the next display
graph rebuild via aggregateDisplayGraph's shallow attribute copy) AND
context.displayGraph (the live Graph instance currently bound to
Sigma, so the change is visible in the current frame immediately).

The dg !== graph guard skips the display-graph write when they happen
to be the same object (non-aggregated full view), avoiding a redundant
double-write in that case.

Original baseColor is still captured from the store graph (the
authoritative source, since aggregateDisplayGraph copies from there),
so restore remains correct across all view modes.
This commit is contained in:
Sameer6305
2026-08-04 16:32:04 +05:30
parent 47531d8365
commit 7d936d0f7c
@@ -1,7 +1,8 @@
import { useEffect, useRef, useState, type CSSProperties } from "react";
import { Loader2 } from "lucide-react";
import type Graph from "graphology";
import { graph } from "../../../store/graphStore";
import { graph, type NodeAttributes } from "../../../store/graphStore";
import { GRAPH_THEME } from "../graphTheme";
import { fetchTemporalDiff, type TemporalDiffResult } from "./temporalDiffState";
import type { GraphPlugin, GraphPluginContext } from "./types";
@@ -40,11 +41,42 @@ const DIFF_REMOVED_COLOR = GRAPH_THEME.ui.control.dangerText;
// state every diffed node is in here. baseColor is read unconditionally by resolveNodeColor's
// default branch regardless of interaction state or zoom tier, so it is the one attribute
// confirmed to actually render the diff highlight.
//
// Writes go to BOTH the store graph and context.displayGraph:
// - context.displayGraph is the live Graph instance currently bound to Sigma (updated by
// GraphCanvas.tsx via sigma.setGraph() / runtimeRef.current.displayGraph = displayGraph
// whenever the display graph is rebuilt). The nodeReducer reads attributes from this
// instance, so writing here makes the highlight visible in the currently-rendered frame.
// - graph (store singleton) carries the value into the *next* display graph rebuild:
// aggregateDisplayGraph copies node attributes shallowly from the store graph, so a
// mutation that only touches context.displayGraph would be lost on the next rebuild.
// Using a type assertion to Graph<NodeAttributes, EdgeAttributes> is consistent with how
// the rest of GraphCanvas/graphSceneState cast the same union when they need to call
// mutation methods; TypeScript cannot resolve setNodeAttribute across the union directly.
function toMutable(g: GraphPluginContext["displayGraph"]) {
return g as Graph<NodeAttributes>;
}
function writeBaseColor(
context: GraphPluginContext,
nodeId: string,
color: string | undefined,
): void {
// Write to the store graph first (survives display graph rebuilds).
if (graph.hasNode(nodeId)) {
graph.setNodeAttribute(nodeId, "baseColor", color);
}
// Write to the current display graph instance Sigma is rendering.
const dg = toMutable(context.displayGraph);
if (dg !== graph && dg.hasNode(nodeId)) {
dg.setNodeAttribute(nodeId, "baseColor", color);
}
}
function clearDiffHighlight(context: GraphPluginContext, previousColors: Map<string, string | undefined>) {
previousColors.forEach((color, nodeId) => {
if (graph.hasNode(nodeId)) {
graph.setNodeAttribute(nodeId, "baseColor", color);
}
writeBaseColor(context, nodeId, color);
});
context.scene?.requestRender();
}
@@ -52,9 +84,11 @@ function clearDiffHighlight(context: GraphPluginContext, previousColors: Map<str
function applyDiffHighlight(context: GraphPluginContext, result: TemporalDiffResult): Map<string, string | undefined> {
const previousColors = new Map<string, string | undefined>();
const paint = (nodeId: string, color: string) => {
// Capture from the store graph — this is the authoritative source for the node's
// original baseColor, since aggregateDisplayGraph copies from there.
if (graph.hasNode(nodeId)) {
previousColors.set(nodeId, graph.getNodeAttribute(nodeId, "baseColor"));
graph.setNodeAttribute(nodeId, "baseColor", color);
writeBaseColor(context, nodeId, color);
}
};
result.added_nodes.forEach((nodeId) => paint(nodeId, DIFF_ADDED_COLOR));