feat: Update node color and legent after entity_type changed
- Move color constants to utils module - Extract resolveNodeColor function - Update node colors on type changes - Simplify hook color logic
This commit is contained in:
parent
79a17c3f7f
commit
4cbd876126
3 changed files with 247 additions and 215 deletions
|
|
@ -10,225 +10,18 @@ import { useBackendState } from '@/stores/state'
|
|||
import { useSettingsStore } from '@/stores/settings'
|
||||
|
||||
import seedrandom from 'seedrandom'
|
||||
|
||||
const TYPE_SYNONYMS: Record<string, string> = {
|
||||
'unknown': 'unknown',
|
||||
'未知': 'unknown',
|
||||
|
||||
'other': 'other',
|
||||
'其它': 'other',
|
||||
|
||||
'concept': 'concept',
|
||||
'object': 'concept',
|
||||
'type': 'concept',
|
||||
'category': 'concept',
|
||||
'model': 'concept',
|
||||
'project': 'concept',
|
||||
'condition': 'concept',
|
||||
'rule': 'concept',
|
||||
'regulation': 'concept',
|
||||
'article': 'concept',
|
||||
'law': 'concept',
|
||||
'legalclause': 'concept',
|
||||
'policy': 'concept',
|
||||
'disease': 'concept',
|
||||
'概念': 'concept',
|
||||
'对象': 'concept',
|
||||
'类别': 'concept',
|
||||
'分类': 'concept',
|
||||
'模型': 'concept',
|
||||
'项目': 'concept',
|
||||
'条件': 'concept',
|
||||
'规则': 'concept',
|
||||
'法律': 'concept',
|
||||
'法律条款': 'concept',
|
||||
'条文': 'concept',
|
||||
'政策': 'policy',
|
||||
'疾病': 'concept',
|
||||
|
||||
'method': 'method',
|
||||
'process': 'method',
|
||||
'方法': 'method',
|
||||
'过程': 'method',
|
||||
|
||||
'artifact': 'artifact',
|
||||
'technology': 'artifact',
|
||||
'tech': 'artifact',
|
||||
'product': 'artifact',
|
||||
'equipment': 'artifact',
|
||||
'device': 'artifact',
|
||||
'stuff': 'artifact',
|
||||
'component': 'artifact',
|
||||
'material': 'artifact',
|
||||
'chemical': 'artifact',
|
||||
'drug': 'artifact',
|
||||
'medicine': 'artifact',
|
||||
'food': 'artifact',
|
||||
'weapon': 'artifact',
|
||||
'arms': 'artifact',
|
||||
'人工制品': 'artifact',
|
||||
'人造物品': 'artifact',
|
||||
'技术': 'technology',
|
||||
'科技': 'technology',
|
||||
'产品': 'artifact',
|
||||
'设备': 'artifact',
|
||||
'装备': 'artifact',
|
||||
'物品': 'artifact',
|
||||
'材料': 'artifact',
|
||||
'化学': 'artifact',
|
||||
'药物': 'artifact',
|
||||
'食品': 'artifact',
|
||||
'武器': 'artifact',
|
||||
'军火': 'artifact',
|
||||
|
||||
'naturalobject': 'naturalobject',
|
||||
'natural': 'naturalobject',
|
||||
'phenomena': 'naturalobject',
|
||||
'substance': 'naturalobject',
|
||||
'plant': 'naturalobject',
|
||||
'自然对象': 'naturalobject',
|
||||
'自然物体': 'naturalobject',
|
||||
'自然现象': 'naturalobject',
|
||||
'物质': 'naturalobject',
|
||||
'物体': 'naturalobject',
|
||||
|
||||
'data': 'data',
|
||||
'figure': 'data',
|
||||
'value': 'data',
|
||||
'数据': 'data',
|
||||
'数字': 'data',
|
||||
'数值': 'data',
|
||||
|
||||
'content': 'content',
|
||||
'book': 'content',
|
||||
'video': 'content',
|
||||
'内容': 'content',
|
||||
'作品': 'content',
|
||||
'书籍': 'content',
|
||||
'视频': 'content',
|
||||
|
||||
'organization': 'organization',
|
||||
'org': 'organization',
|
||||
'company': 'organization',
|
||||
'组织': 'organization',
|
||||
'公司': 'organization',
|
||||
'机构': 'organization',
|
||||
'组织机构': 'organization',
|
||||
|
||||
'event': 'event',
|
||||
'事件': 'event',
|
||||
'activity': 'event',
|
||||
'活动': 'event',
|
||||
|
||||
'person': 'person',
|
||||
'people': 'person',
|
||||
'human': 'person',
|
||||
'role': 'person',
|
||||
'人物': 'person',
|
||||
'人类': 'person',
|
||||
'人': 'person',
|
||||
'角色': 'person',
|
||||
|
||||
'creature': 'creature',
|
||||
'animal': 'creature',
|
||||
'beings': 'creature',
|
||||
'being': 'creature',
|
||||
'alien': 'creature',
|
||||
'ghost': 'creature',
|
||||
'动物': 'creature',
|
||||
'生物': 'creature',
|
||||
'神仙': 'creature',
|
||||
'鬼怪': 'creature',
|
||||
'妖怪': 'creature',
|
||||
|
||||
'location': 'location',
|
||||
'geography': 'location',
|
||||
'geo': 'location',
|
||||
'place': 'location',
|
||||
'address': 'location',
|
||||
'地点': 'location',
|
||||
'位置': 'location',
|
||||
'地址': 'location',
|
||||
'地理': 'location',
|
||||
'地域': 'location',
|
||||
};
|
||||
|
||||
// node type to color mapping
|
||||
const NODE_TYPE_COLORS: Record<string, string> = {
|
||||
'person': '#4169E1', // RoyalBlue
|
||||
'creature': '#bd7ebe', // LightViolet
|
||||
'organization': '#00cc00', // LightGreen
|
||||
'location': '#cf6d17', // Carrot
|
||||
'event': '#00bfa0', // Turquoise
|
||||
'concept': '#e3493b', // GoogleRed
|
||||
'method': '#b71c1c', // red
|
||||
'content': '#0f558a', // NavyBlue
|
||||
'data': '#0000ff', // Blue
|
||||
'artifact': '#4421af', // DeepPurple
|
||||
'naturalobject': '#b2e061', // YellowGreen
|
||||
'other': '#f4d371', // Yellow
|
||||
'unknown': '#b0b0b0', // Yellow
|
||||
};
|
||||
|
||||
// Extended colors pool - Used for unknown node types
|
||||
const EXTENDED_COLORS = [
|
||||
'#84a3e1', // SkyBlue
|
||||
'#5a2c6d', // DeepViolet
|
||||
'#2F4F4F', // DarkSlateGray
|
||||
'#003366', // DarkBlue
|
||||
'#9b3a31', // DarkBrown
|
||||
'#00CED1', // DarkTurquoise
|
||||
'#b300b3', // Purple
|
||||
'#0f705d', // Green
|
||||
'#ff99cc', // Pale Pink
|
||||
'#6ef7b3', // LightGreen
|
||||
'#cd071e', // ChinaRed
|
||||
];
|
||||
import { resolveNodeColor, DEFAULT_NODE_COLOR } from '@/utils/graphColor'
|
||||
|
||||
// Select color based on node type
|
||||
const getNodeColorByType = (nodeType: string | undefined): string => {
|
||||
const state = useGraphStore.getState()
|
||||
const { color, map, updated } = resolveNodeColor(nodeType, state.typeColorMap)
|
||||
|
||||
const defaultColor = '#5D6D7E';
|
||||
|
||||
const normalizedType = nodeType ? nodeType.toLowerCase() : 'unknown';
|
||||
const typeColorMap = useGraphStore.getState().typeColorMap;
|
||||
|
||||
// First try to find standard type
|
||||
const standardType = TYPE_SYNONYMS[normalizedType];
|
||||
|
||||
// Check cache using standard type if available, otherwise use normalized type
|
||||
const cacheKey = standardType || normalizedType;
|
||||
if (typeColorMap.has(cacheKey)) {
|
||||
return typeColorMap.get(cacheKey) || defaultColor;
|
||||
if (updated) {
|
||||
useGraphStore.setState({ typeColorMap: map })
|
||||
}
|
||||
|
||||
if (standardType) {
|
||||
const color = NODE_TYPE_COLORS[standardType];
|
||||
// Store using standard type name as key
|
||||
const newMap = new Map(typeColorMap);
|
||||
newMap.set(standardType, color);
|
||||
useGraphStore.setState({ typeColorMap: newMap });
|
||||
return color;
|
||||
}
|
||||
|
||||
// For unpredefind nodeTypes, use extended colors
|
||||
// Find used extended colors
|
||||
const usedExtendedColors = new Set(
|
||||
Array.from(typeColorMap.entries())
|
||||
.filter(([, color]) => !Object.values(NODE_TYPE_COLORS).includes(color))
|
||||
.map(([, color]) => color)
|
||||
);
|
||||
|
||||
// Find and use the first unused extended color
|
||||
const unusedColor = EXTENDED_COLORS.find(color => !usedExtendedColors.has(color));
|
||||
const newColor = unusedColor || defaultColor;
|
||||
|
||||
// Update color mapping - use normalized type for unknown types
|
||||
const newMap = new Map(typeColorMap);
|
||||
newMap.set(normalizedType, newColor);
|
||||
useGraphStore.setState({ typeColorMap: newMap });
|
||||
|
||||
return newColor;
|
||||
return color || DEFAULT_NODE_COLOR
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { create } from 'zustand'
|
|||
import { createSelectors } from '@/lib/utils'
|
||||
import { DirectedGraph } from 'graphology'
|
||||
import MiniSearch from 'minisearch'
|
||||
import { resolveNodeColor, DEFAULT_NODE_COLOR } from '@/utils/graphColor'
|
||||
|
||||
export type RawNodeType = {
|
||||
// for NetworkX: id is identical to properties['entity_id']
|
||||
|
|
@ -319,11 +320,21 @@ const useGraphStoreBase = create<GraphState>()((set, get) => ({
|
|||
// For non-NetworkX nodes or non-entity_id changes
|
||||
const nodeIndex = rawGraph.nodeIdMap[String(nodeId)]
|
||||
if (nodeIndex !== undefined) {
|
||||
rawGraph.nodes[nodeIndex].properties[propertyName] = newValue
|
||||
const nodeRef = rawGraph.nodes[nodeIndex]
|
||||
nodeRef.properties[propertyName] = newValue
|
||||
if (propertyName === 'entity_id') {
|
||||
rawGraph.nodes[nodeIndex].labels = [newValue]
|
||||
nodeRef.labels = [newValue]
|
||||
sigmaGraph.setNodeAttribute(String(nodeId), 'label', newValue)
|
||||
}
|
||||
if (propertyName === 'entity_type') {
|
||||
const { color, map, updated } = resolveNodeColor(newValue, state.typeColorMap)
|
||||
const resolvedColor = color || DEFAULT_NODE_COLOR
|
||||
nodeRef.color = resolvedColor
|
||||
sigmaGraph.setNodeAttribute(String(nodeId), 'color', resolvedColor)
|
||||
if (updated) {
|
||||
set({ typeColorMap: map })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger a re-render by incrementing the version counter
|
||||
|
|
|
|||
228
lightrag_webui/src/utils/graphColor.ts
Normal file
228
lightrag_webui/src/utils/graphColor.ts
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
const DEFAULT_NODE_COLOR = '#5D6D7E'
|
||||
|
||||
const TYPE_SYNONYMS: Record<string, string> = {
|
||||
unknown: 'unknown',
|
||||
未知: 'unknown',
|
||||
|
||||
other: 'other',
|
||||
其它: 'other',
|
||||
|
||||
concept: 'concept',
|
||||
object: 'concept',
|
||||
type: 'concept',
|
||||
category: 'concept',
|
||||
model: 'concept',
|
||||
project: 'concept',
|
||||
condition: 'concept',
|
||||
rule: 'concept',
|
||||
regulation: 'concept',
|
||||
article: 'concept',
|
||||
law: 'concept',
|
||||
legalclause: 'concept',
|
||||
policy: 'concept',
|
||||
disease: 'concept',
|
||||
概念: 'concept',
|
||||
对象: 'concept',
|
||||
类别: 'concept',
|
||||
分类: 'concept',
|
||||
模型: 'concept',
|
||||
项目: 'concept',
|
||||
条件: 'concept',
|
||||
规则: 'concept',
|
||||
法律: 'concept',
|
||||
法律条款: 'concept',
|
||||
条文: 'concept',
|
||||
政策: 'policy',
|
||||
疾病: 'concept',
|
||||
|
||||
method: 'method',
|
||||
process: 'method',
|
||||
方法: 'method',
|
||||
过程: 'method',
|
||||
|
||||
artifact: 'artifact',
|
||||
technology: 'artifact',
|
||||
tech: 'artifact',
|
||||
product: 'artifact',
|
||||
equipment: 'artifact',
|
||||
device: 'artifact',
|
||||
stuff: 'artifact',
|
||||
component: 'artifact',
|
||||
material: 'artifact',
|
||||
chemical: 'artifact',
|
||||
drug: 'artifact',
|
||||
medicine: 'artifact',
|
||||
food: 'artifact',
|
||||
weapon: 'artifact',
|
||||
arms: 'artifact',
|
||||
人工制品: 'artifact',
|
||||
人造物品: 'artifact',
|
||||
技术: 'technology',
|
||||
科技: 'technology',
|
||||
产品: 'artifact',
|
||||
设备: 'artifact',
|
||||
装备: 'artifact',
|
||||
物品: 'artifact',
|
||||
材料: 'artifact',
|
||||
化学: 'artifact',
|
||||
药物: 'artifact',
|
||||
食品: 'artifact',
|
||||
武器: 'artifact',
|
||||
军火: 'artifact',
|
||||
|
||||
naturalobject: 'naturalobject',
|
||||
natural: 'naturalobject',
|
||||
phenomena: 'naturalobject',
|
||||
substance: 'naturalobject',
|
||||
plant: 'naturalobject',
|
||||
自然对象: 'naturalobject',
|
||||
自然物体: 'naturalobject',
|
||||
自然现象: 'naturalobject',
|
||||
物质: 'naturalobject',
|
||||
物体: 'naturalobject',
|
||||
|
||||
data: 'data',
|
||||
figure: 'data',
|
||||
value: 'data',
|
||||
数据: 'data',
|
||||
数字: 'data',
|
||||
数值: 'data',
|
||||
|
||||
content: 'content',
|
||||
book: 'content',
|
||||
video: 'content',
|
||||
内容: 'content',
|
||||
作品: 'content',
|
||||
书籍: 'content',
|
||||
视频: 'content',
|
||||
|
||||
organization: 'organization',
|
||||
org: 'organization',
|
||||
company: 'organization',
|
||||
组织: 'organization',
|
||||
公司: 'organization',
|
||||
机构: 'organization',
|
||||
组织机构: 'organization',
|
||||
|
||||
event: 'event',
|
||||
事件: 'event',
|
||||
activity: 'event',
|
||||
活动: 'event',
|
||||
|
||||
person: 'person',
|
||||
people: 'person',
|
||||
human: 'person',
|
||||
role: 'person',
|
||||
人物: 'person',
|
||||
人类: 'person',
|
||||
人: 'person',
|
||||
角色: 'person',
|
||||
|
||||
creature: 'creature',
|
||||
animal: 'creature',
|
||||
beings: 'creature',
|
||||
being: 'creature',
|
||||
alien: 'creature',
|
||||
ghost: 'creature',
|
||||
动物: 'creature',
|
||||
生物: 'creature',
|
||||
神仙: 'creature',
|
||||
鬼怪: 'creature',
|
||||
妖怪: 'creature',
|
||||
|
||||
location: 'location',
|
||||
geography: 'location',
|
||||
geo: 'location',
|
||||
place: 'location',
|
||||
address: 'location',
|
||||
地点: 'location',
|
||||
位置: 'location',
|
||||
地址: 'location',
|
||||
地理: 'location',
|
||||
地域: 'location'
|
||||
}
|
||||
|
||||
const NODE_TYPE_COLORS: Record<string, string> = {
|
||||
person: '#4169E1',
|
||||
creature: '#bd7ebe',
|
||||
organization: '#00cc00',
|
||||
location: '#cf6d17',
|
||||
event: '#00bfa0',
|
||||
concept: '#e3493b',
|
||||
method: '#b71c1c',
|
||||
content: '#0f558a',
|
||||
data: '#0000ff',
|
||||
artifact: '#4421af',
|
||||
naturalobject: '#b2e061',
|
||||
other: '#f4d371',
|
||||
unknown: '#b0b0b0'
|
||||
}
|
||||
|
||||
const EXTENDED_COLORS = [
|
||||
'#84a3e1',
|
||||
'#5a2c6d',
|
||||
'#2F4F4F',
|
||||
'#003366',
|
||||
'#9b3a31',
|
||||
'#00CED1',
|
||||
'#b300b3',
|
||||
'#0f705d',
|
||||
'#ff99cc',
|
||||
'#6ef7b3',
|
||||
'#cd071e'
|
||||
]
|
||||
|
||||
const PREDEFINED_COLOR_SET = new Set(Object.values(NODE_TYPE_COLORS))
|
||||
|
||||
interface ResolveNodeColorResult {
|
||||
color: string
|
||||
map: Map<string, string>
|
||||
updated: boolean
|
||||
}
|
||||
|
||||
export const resolveNodeColor = (
|
||||
nodeType: string | undefined,
|
||||
currentMap: Map<string, string> | undefined
|
||||
): ResolveNodeColorResult => {
|
||||
const typeColorMap = currentMap ?? new Map<string, string>()
|
||||
const normalizedType = nodeType ? nodeType.toLowerCase() : 'unknown'
|
||||
const standardType = TYPE_SYNONYMS[normalizedType]
|
||||
const cacheKey = standardType || normalizedType
|
||||
|
||||
if (typeColorMap.has(cacheKey)) {
|
||||
return {
|
||||
color: typeColorMap.get(cacheKey) || DEFAULT_NODE_COLOR,
|
||||
map: typeColorMap,
|
||||
updated: false
|
||||
}
|
||||
}
|
||||
|
||||
if (standardType) {
|
||||
const color = NODE_TYPE_COLORS[standardType] || DEFAULT_NODE_COLOR
|
||||
const newMap = new Map(typeColorMap)
|
||||
newMap.set(standardType, color)
|
||||
return {
|
||||
color,
|
||||
map: newMap,
|
||||
updated: true
|
||||
}
|
||||
}
|
||||
|
||||
const usedExtendedColors = new Set(
|
||||
Array.from(typeColorMap.values()).filter((color) => !PREDEFINED_COLOR_SET.has(color))
|
||||
)
|
||||
|
||||
const unusedColor = EXTENDED_COLORS.find((color) => !usedExtendedColors.has(color))
|
||||
const color = unusedColor || DEFAULT_NODE_COLOR
|
||||
|
||||
const newMap = new Map(typeColorMap)
|
||||
newMap.set(normalizedType, color)
|
||||
|
||||
return {
|
||||
color,
|
||||
map: newMap,
|
||||
updated: true
|
||||
}
|
||||
}
|
||||
|
||||
export { DEFAULT_NODE_COLOR }
|
||||
Loading…
Add table
Reference in a new issue