-
+
0 ? clusterColors : DEFAULT_CLUSTER_COLORS
+ return palette[hashString(key) % palette.length] as string
}
export function computeClusterAssignments(
documents: GraphApiDocument[],
+ clusterColors: readonly string[] = DEFAULT_CLUSTER_COLORS,
): Map {
const adjacency = new Map>()
const docByMemory = new Map()
@@ -180,7 +174,7 @@ export function computeClusterAssignments(
: `relation:${firstDocId}:${firstId}`
const assignment = {
key,
- color: getClusterColor(key),
+ color: getClusterColor(key, clusterColors),
size: component.length,
}
@@ -205,6 +199,7 @@ function getMemoryRelationTargets(mem: GraphApiMemory): Record {
function getDocumentClusterAssignment(
doc: GraphApiDocument,
assignments: Map,
+ clusterColors: readonly string[] = DEFAULT_CLUSTER_COLORS,
): ClusterAssignment {
const counts = new Map<
string,
@@ -229,7 +224,7 @@ function getDocumentClusterAssignment(
return (
best?.assignment ?? {
key: `doc:${doc.id}`,
- color: getClusterColor(`doc:${doc.id}`),
+ color: getClusterColor(`doc:${doc.id}`, clusterColors),
size: 1,
}
)
@@ -492,7 +487,10 @@ export function useGraphData(
? buildAppendSpatialGrid(appendPlacementNodes)
: null
let appendIndex = 0
- const clusterAssignments = computeClusterAssignments(documents)
+ const clusterAssignments = computeClusterAssignments(
+ documents,
+ colors.clusterColors,
+ )
const result: GraphNode[] = []
// Spiral layout: documents form a compact spiral core, memories orbit
@@ -509,7 +507,11 @@ export function useGraphData(
for (let docIdx = 0; docIdx < docCount; docIdx++) {
const doc = documents[docIdx]
- const docCluster = getDocumentClusterAssignment(doc, clusterAssignments)
+ const docCluster = getDocumentClusterAssignment(
+ doc,
+ clusterAssignments,
+ colors.clusterColors,
+ )
const angle = docIdx * goldenAngle
const radius = spiralScale * Math.sqrt((docIdx + 1) / docCount)
const initialX = cx + Math.cos(angle) * radius
diff --git a/packages/memory-graph/src/hooks/use-graph-theme.ts b/packages/memory-graph/src/hooks/use-graph-theme.ts
index f045830a1..9f3cdb474 100644
--- a/packages/memory-graph/src/hooks/use-graph-theme.ts
+++ b/packages/memory-graph/src/hooks/use-graph-theme.ts
@@ -10,6 +10,19 @@ function readCssVar(name: string, fallback: string): string {
return val || fallback
}
+function readCssColorList(name: string, fallback: string[]): string[] {
+ if (typeof document === "undefined") return fallback
+ const val = getComputedStyle(document.documentElement)
+ .getPropertyValue(name)
+ .trim()
+ if (!val) return fallback
+ const colors = val
+ .split(",")
+ .map((color) => color.trim())
+ .filter(Boolean)
+ return colors.length > 0 ? colors : fallback
+}
+
function resolveColors(): GraphThemeColors {
return {
bg: readCssVar("--graph-bg", DEFAULT_COLORS.bg),
@@ -71,6 +84,10 @@ function resolveColors(): GraphThemeColors {
"--graph-control-border",
DEFAULT_COLORS.controlBorder,
),
+ clusterColors: readCssColorList(
+ "--graph-cluster-colors",
+ DEFAULT_COLORS.clusterColors,
+ ),
}
}
@@ -107,13 +124,13 @@ export function useGraphTheme(
const overrideKey = overrides
? Object.entries(overrides)
.sort(([a], [b]) => a.localeCompare(b))
- .map(([k, v]) => `${k}:${v}`)
+ .map(([k, v]) => `${k}:${Array.isArray(v) ? v.join("|") : v}`)
.join(",")
: ""
+ // biome-ignore lint/correctness/useExhaustiveDependencies: overrideKey tracks overrides by value
const merged = useMemo(
() => (overrides ? { ...colors, ...overrides } : colors),
- // biome-ignore lint/correctness/useExhaustiveDependencies: overrideKey tracks overrides by value
[colors, overrideKey],
)
diff --git a/packages/memory-graph/src/types.ts b/packages/memory-graph/src/types.ts
index b446a0232..9fd640737 100644
--- a/packages/memory-graph/src/types.ts
+++ b/packages/memory-graph/src/types.ts
@@ -130,6 +130,7 @@ export interface GraphThemeColors {
popoverTextMuted: string
controlBg: string
controlBorder: string
+ clusterColors: string[]
}
export interface GraphCanvasProps {