Skip to content

Commit 80534f2

Browse files
committed
Add node label column selectors
1 parent 4aeb93e commit 80534f2

3 files changed

Lines changed: 155 additions & 13 deletions

File tree

src-tauri/src/lib.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ struct GraphNode {
3030
id: String,
3131
name: String,
3232
label: String,
33+
#[serde(skip_serializing_if = "HashMap::is_empty", default)]
34+
properties: HashMap<String, String>,
3335
#[serde(rename = "tableId", skip_serializing_if = "Option::is_none")]
3436
table_id: Option<u64>,
3537
#[serde(skip_serializing_if = "Option::is_none")]
@@ -362,6 +364,15 @@ fn node_display_name(props: &[(String, Value)]) -> String {
362364
.unwrap_or_else(|| "Node".to_string())
363365
}
364366

367+
fn node_display_properties(props: &[(String, Value)]) -> HashMap<String, String> {
368+
props
369+
.iter()
370+
.filter_map(|(key, value)| {
371+
non_empty_value_to_string(value).map(|value| (key.clone(), value))
372+
})
373+
.collect()
374+
}
375+
365376
fn value_to_score(val: &Value) -> f64 {
366377
match val {
367378
Value::Double(n) => *n,
@@ -385,11 +396,13 @@ fn graph_node_from_value(val: &Value) -> Option<GraphNode> {
385396

386397
let props = node_val.get_properties();
387398
let name = node_display_name(props);
399+
let properties = node_display_properties(props);
388400

389401
Some(GraphNode {
390402
id: id_to_string(node_val.get_node_id()),
391403
name,
392404
label: node_val.get_label_name().clone(),
405+
properties,
393406
table_id: Some(node_val.get_node_id().table_id),
394407
rowid: Some(node_val.get_node_id().offset),
395408
community: None,
@@ -495,6 +508,7 @@ fn make_expander_node(parent_id: &str, hidden_count: usize, offset: usize) -> Gr
495508
id: format!("{EXPANDER_PREFIX}node:{parent_id}:{offset}"),
496509
name: format!("+{hidden_count}"),
497510
label: "More".to_string(),
511+
properties: HashMap::new(),
498512
table_id: None,
499513
rowid: None,
500514
community: None,
@@ -1049,6 +1063,7 @@ fn compute_cluster_levels(
10491063
id: index.to_string(),
10501064
name: format!("Cluster {index}"),
10511065
label: "Cluster".to_string(),
1066+
properties: HashMap::new(),
10521067
table_id: None,
10531068
rowid: None,
10541069
community: Some(index as u64),
@@ -1268,12 +1283,14 @@ fn collect_edge_graph(conn: &Connection, limit: usize) -> Result<GraphData, Stri
12681283
for node_val in [source_node, target_node] {
12691284
let props = node_val.get_properties();
12701285
let name = node_display_name(props);
1286+
let properties = node_display_properties(props);
12711287
merge_node(
12721288
&mut nodes,
12731289
GraphNode {
12741290
id: id_to_string(node_val.get_node_id()),
12751291
name,
12761292
label: node_val.get_label_name().clone(),
1293+
properties,
12771294
table_id: Some(node_val.get_node_id().table_id),
12781295
rowid: Some(node_val.get_node_id().offset),
12791296
community: None,
@@ -1852,13 +1869,15 @@ fn execute_query(
18521869
node_id_set.insert(node_id.clone());
18531870
let props = node_val.get_properties();
18541871
let name = node_display_name(props);
1872+
let properties = node_display_properties(props);
18551873

18561874
let label = node_val.get_label_name().clone();
18571875

18581876
nodes.push(GraphNode {
18591877
id: node_id,
18601878
name,
18611879
label,
1880+
properties,
18621881
table_id: Some(node_val.get_node_id().table_id),
18631882
rowid: Some(node_val.get_node_id().offset),
18641883
community: None,

src/App.css

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,49 @@ body {
8787
border-top: 1px solid var(--border-color);
8888
}
8989

90+
.display-column-settings {
91+
margin-top: 20px;
92+
padding-top: 16px;
93+
border-top: 1px solid var(--border-color);
94+
}
95+
96+
.display-column-list {
97+
display: flex;
98+
flex-direction: column;
99+
gap: 8px;
100+
}
101+
102+
.display-column-row {
103+
display: grid;
104+
grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
105+
align-items: center;
106+
gap: 8px;
107+
color: var(--text-primary);
108+
font-size: 13px;
109+
}
110+
111+
.display-column-row span {
112+
overflow: hidden;
113+
text-overflow: ellipsis;
114+
white-space: nowrap;
115+
}
116+
117+
.display-column-row select {
118+
min-width: 0;
119+
width: 100%;
120+
padding: 7px 8px;
121+
background-color: var(--bg-tertiary);
122+
color: var(--text-primary);
123+
border: 1px solid var(--border-color);
124+
border-radius: 4px;
125+
font-size: 13px;
126+
}
127+
128+
.display-column-row select:focus {
129+
outline: none;
130+
border-color: var(--accent-color);
131+
}
132+
90133
.llm-cluster-settings {
91134
margin-top: 20px;
92135
padding-top: 16px;

src/App.tsx

Lines changed: 93 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface GraphNode {
1717
id: string
1818
name: string
1919
label: string
20+
properties?: Record<string, string>
2021
tableId?: number
2122
rowid?: number
2223
community?: number
@@ -144,6 +145,7 @@ interface SigmaGraphViewProps {
144145
labelNodeIds: Set<string>
145146
newlyExpandedNodeIds: Set<string>
146147
darkMode: boolean
148+
getNodeDisplayName: (node: GraphNode) => string
147149
getNodeColor: (node: GraphNode) => string
148150
getNodeSize: (node: GraphNode) => number
149151
getEdgeColor: (label: string) => string
@@ -193,9 +195,23 @@ function buildLlmClusterConfig(config: LlmClusterNamingConfig): LlmClusterNaming
193195
}
194196
}
195197

196-
function getNodeClusterLabel(node: GraphNode) {
198+
const AUTO_DISPLAY_COLUMN = '__auto__'
199+
200+
function getNodeDisplayName(node: GraphNode, displayColumns: Record<string, string>) {
201+
if (isExpanderNode(node) || isClusterNode(node)) return node.name || node.id
202+
203+
const selectedColumn = displayColumns[node.label]
204+
if (selectedColumn && selectedColumn !== AUTO_DISPLAY_COLUMN) {
205+
const selectedValue = node.properties?.[selectedColumn]
206+
if (selectedValue?.trim()) return selectedValue
207+
}
208+
209+
return node.name || node.id
210+
}
211+
212+
function getNodeClusterLabel(node: GraphNode, displayColumns: Record<string, string>) {
197213
const label = node.label.trim()
198-
const name = node.name.trim()
214+
const name = getNodeDisplayName(node, displayColumns).trim()
199215
return !name || name === label ? label : `${label}: ${name}`
200216
}
201217

@@ -699,7 +715,7 @@ function createInitialLayout(graphData: NormalizedGraphData) {
699715
return { degrees, positions }
700716
}
701717

702-
function SigmaGraphView({ graphData, labelNodeIds, newlyExpandedNodeIds, darkMode, getNodeColor, getNodeSize, getEdgeColor, onNodeClick }: SigmaGraphViewProps) {
718+
function SigmaGraphView({ graphData, labelNodeIds, newlyExpandedNodeIds, darkMode, getNodeDisplayName, getNodeColor, getNodeSize, getEdgeColor, onNodeClick }: SigmaGraphViewProps) {
703719
const containerRef = useRef<HTMLDivElement | null>(null)
704720
const rendererRef = useRef<Sigma | null>(null)
705721
const hoveredEdgeRef = useRef<string | null>(null)
@@ -708,15 +724,16 @@ function SigmaGraphView({ graphData, labelNodeIds, newlyExpandedNodeIds, darkMod
708724
const { positions } = createInitialLayout(graphData)
709725
const nodes = graphData.nodes.map(node => {
710726
const position = positions[node.id] || { x: 0, y: 0 }
727+
const displayName = getNodeDisplayName(node)
711728
return {
712729
key: node.id,
713730
attributes: {
714731
x: position.x,
715732
y: position.y,
716733
size: getNodeSize(node),
717734
color: isExpanderNode(node) ? '#f59e0b' : getNodeColor(node),
718-
label: isExpanderNode(node) || labelNodeIds.has(node.id) ? node.name || node.id : '',
719-
hoverLabel: node.name || node.id,
735+
label: isExpanderNode(node) || labelNodeIds.has(node.id) ? displayName : '',
736+
hoverLabel: displayName,
720737
isNewlyExpanded: newlyExpandedNodeIds.has(node.id),
721738
nodeType: node.label,
722739
},
@@ -752,7 +769,7 @@ function SigmaGraphView({ graphData, labelNodeIds, newlyExpandedNodeIds, darkMod
752769
edgeAttributes,
753770
edgeKeys,
754771
})
755-
}, [graphData, labelNodeIds, newlyExpandedNodeIds, getNodeColor, getNodeSize, getEdgeColor])
772+
}, [graphData, labelNodeIds, newlyExpandedNodeIds, getNodeDisplayName, getNodeColor, getNodeSize, getEdgeColor])
756773

757774
useEffect(() => {
758775
const container = containerRef.current
@@ -857,6 +874,7 @@ function App() {
857874
const [clusterViewEnabled, setClusterViewEnabled] = useState(true)
858875
const [nodeSearch, setNodeSearch] = useState('')
859876
const [searchResults, setSearchResults] = useState<GraphNode[]>([])
877+
const [displayColumnsByLabel, setDisplayColumnsByLabel] = useState<Record<string, string>>({})
860878
const [searching, setSearching] = useState(false)
861879
const [searchError, setSearchError] = useState<string | null>(null)
862880
const [focusedNodeId, setFocusedNodeId] = useState<string | null>(null)
@@ -1159,6 +1177,27 @@ function App() {
11591177
}, [graphData, selectedId, currentLlmClusterConfig, resetClusterNameRequests])
11601178

11611179
const normalizedGraphData = useMemo(() => normalizeGraphData(graphData), [graphData])
1180+
const displayColumnOptions = useMemo(() => {
1181+
const columnsByLabel = new Map<string, Set<string>>()
1182+
graphData.nodes.forEach(node => {
1183+
if (isExpanderNode(node) || isClusterNode(node)) return
1184+
const propertyNames = Object.keys(node.properties || {})
1185+
if (propertyNames.length === 0) return
1186+
const columns = columnsByLabel.get(node.label) || new Set<string>()
1187+
propertyNames.forEach(name => columns.add(name))
1188+
columnsByLabel.set(node.label, columns)
1189+
})
1190+
1191+
return [...columnsByLabel.entries()]
1192+
.map(([label, columns]) => ({
1193+
label,
1194+
columns: [...columns].sort((a, b) => a.localeCompare(b)),
1195+
}))
1196+
.sort((a, b) => a.label.localeCompare(b.label))
1197+
}, [graphData.nodes])
1198+
const getDisplayName = useCallback((node: GraphNode) => (
1199+
getNodeDisplayName(node, displayColumnsByLabel)
1200+
), [displayColumnsByLabel])
11621201
const clusterLevels = useMemo(() => buildCommunityClusterLevels(normalizedGraphData), [normalizedGraphData])
11631202
const coarsestClusterLevel = useMemo(() => getCoarsestClusterLevel(clusterLevels), [clusterLevels])
11641203
const currentClusterLevel = useMemo(() => {
@@ -1184,6 +1223,10 @@ function App() {
11841223
: normalizedGraphData
11851224
), [clusterViewEnabled, clusterLevels, normalizedGraphData, visibleClusterPath])
11861225

1226+
useEffect(() => {
1227+
resetClusterNameRequests()
1228+
}, [displayColumnsByLabel, resetClusterNameRequests])
1229+
11871230
useEffect(() => {
11881231
const llmConfig = currentLlmClusterConfig()
11891232
if (!llmConfig || !clusterViewEnabled || !currentClusterLevel) return
@@ -1207,7 +1250,7 @@ function App() {
12071250
&& currentClusterLevel.membership[index] === clusterId
12081251
&& nodeMatchesClusterPath(clusterLevels, index, visibleClusterPath)
12091252
))
1210-
.map(getNodeClusterLabel)
1253+
.map(node => getNodeClusterLabel(node, displayColumnsByLabel))
12111254
const sampledLabels = sampleLabels(labels, llmConfig.sampleSize)
12121255
if (sampledLabels.length === 0) return
12131256

@@ -1273,6 +1316,7 @@ function App() {
12731316
clusterViewEnabled,
12741317
currentClusterLevel,
12751318
currentLlmClusterConfig,
1319+
displayColumnsByLabel,
12761320
normalizedGraphData,
12771321
visibleClusterPath,
12781322
visibleGraphData.nodes,
@@ -1382,15 +1426,16 @@ function App() {
13821426
ctx.lineWidth = highlighted ? 3 : 1
13831427
ctx.stroke()
13841428

1385-
if ((isExpanderNode(node) || topLabelNodeIds.has(node.id)) && node.name) {
1429+
const displayName = getDisplayName(node)
1430+
if ((isExpanderNode(node) || topLabelNodeIds.has(node.id)) && displayName) {
13861431
const fontSize = 3
13871432
ctx.font = `${fontSize}px Sans-Serif`
13881433
ctx.textAlign = 'center'
13891434
ctx.textBaseline = 'middle'
13901435
ctx.fillStyle = highlighted ? '#f59e0b' : '#fff'
13911436

13921437
const maxWidth = size * 1.6
1393-
let label = node.name
1438+
let label = displayName
13941439
const measured = ctx.measureText(label)
13951440
if (measured.width > maxWidth) {
13961441
while (label.length > 1 && ctx.measureText(label + '\u2026').width > maxWidth) {
@@ -1400,7 +1445,7 @@ function App() {
14001445
}
14011446
ctx.fillText(label, node.x, node.y)
14021447
}
1403-
}, [getNodeSize, getNodeColor, darkMode, topLabelNodeIds, lastExpandedNodeIds])
1448+
}, [getNodeSize, getNodeColor, getDisplayName, darkMode, topLabelNodeIds, lastExpandedNodeIds])
14041449

14051450
return (
14061451
<div className="app-container">
@@ -1439,6 +1484,40 @@ function App() {
14391484
))}
14401485
</ul>
14411486
)}
1487+
{displayColumnOptions.length > 0 && (
1488+
<div className="display-column-settings">
1489+
<div className="panel-title">Node Labels</div>
1490+
<div className="display-column-list">
1491+
{displayColumnOptions.map(({ label, columns }) => {
1492+
const selectedColumn = displayColumnsByLabel[label]
1493+
const selectedValue = selectedColumn && columns.includes(selectedColumn)
1494+
? selectedColumn
1495+
: AUTO_DISPLAY_COLUMN
1496+
1497+
return (
1498+
<label className="display-column-row" key={label}>
1499+
<span title={label}>{label}</span>
1500+
<select
1501+
value={selectedValue}
1502+
onChange={event => {
1503+
const value = event.target.value
1504+
setDisplayColumnsByLabel(current => ({
1505+
...current,
1506+
[label]: value,
1507+
}))
1508+
}}
1509+
>
1510+
<option value={AUTO_DISPLAY_COLUMN}>Auto</option>
1511+
{columns.map(column => (
1512+
<option key={column} value={column}>{column}</option>
1513+
))}
1514+
</select>
1515+
</label>
1516+
)
1517+
})}
1518+
</div>
1519+
</div>
1520+
)}
14421521
<div className="node-search">
14431522
<div className="panel-title">Find Node</div>
14441523
<div className="node-search-row">
@@ -1464,9 +1543,9 @@ function App() {
14641543
key={result.id}
14651544
className="search-result"
14661545
onClick={() => exploreSearchResult(result)}
1467-
title={`${result.label}: ${result.name}`}
1546+
title={`${result.label}: ${getDisplayName(result)}`}
14681547
>
1469-
<span>{result.name}</span>
1548+
<span>{getDisplayName(result)}</span>
14701549
<small>{result.label} · {result.id}</small>
14711550
</button>
14721551
))}
@@ -1605,6 +1684,7 @@ function App() {
16051684
labelNodeIds={topLabelNodeIds}
16061685
newlyExpandedNodeIds={lastExpandedNodeIds}
16071686
darkMode={darkMode}
1687+
getNodeDisplayName={getDisplayName}
16081688
getNodeColor={getNodeColor}
16091689
getNodeSize={getNodeSize}
16101690
getEdgeColor={getEdgeColor}
@@ -1622,7 +1702,7 @@ function App() {
16221702
onNodeClick={(node) => handleVisibleNodeClick(String(node.id))}
16231703
nodeVal={(node) => { const s = getNodeSize(node); return s * s; }}
16241704
nodeRelSize={1}
1625-
nodeLabel={(node) => `${node.label}: ${node.name}`}
1705+
nodeLabel={(node) => `${node.label}: ${getDisplayName(node)}`}
16261706
linkLabel={(link) => link.label}
16271707
linkColor={(link) => getEdgeColor(link.label)}
16281708
linkWidth={2.5}

0 commit comments

Comments
 (0)