Skip to content

Commit 0f99a10

Browse files
authored
Support dynamic label (#44)
* support dynamic label * misc. fixes * add more selectable colors
1 parent 1380088 commit 0f99a10

File tree

10 files changed

+215
-36
lines changed

10 files changed

+215
-36
lines changed

frontend/src/models/edge.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ class GraphEdge extends GraphObject {
8080
* @param {EdgeData} params
8181
*/
8282
constructor(params) {
83-
const {source_node_identifier, destination_node_identifier, labels, properties, title, identifier} = params;
84-
super({labels, title, properties, identifier});
83+
const {source_node_identifier, destination_node_identifier, labels, properties, title, identifier, containsDynamicLabelElement, allSchemaStaticLabelSets} = params;
84+
super({labels, title, properties, identifier, containsDynamicLabelElement, allSchemaStaticLabelSets});
8585

8686
if (!this._validUid(source_node_identifier) || !this._validUid(destination_node_identifier)) {
8787
this.instantiationErrorReason = 'Edge destination or source invalid';

frontend/src/models/graph-object.js

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,10 @@ class GraphObject {
6767
* @param {string[]} params.labels - The labels for the object.
6868
* @param {Object} params.properties - The optional property:value map for the object.
6969
* @param {string} params.identifier - The unique identifier in Spanner
70+
* @param {boolean} params.containsDynamicLabelElement - True if there is dynamic label element type
71+
* @param {Set<Set<string>>} params.allSchemaStaticLabelSets - A set containing sets of static labels from all table definitions
7072
*/
71-
constructor({ labels, properties, key_property_names, identifier }) {
73+
constructor({ labels, properties, key_property_names, identifier, containsDynamicLabelElement, allSchemaStaticLabelSets }) {
7274
if (!Array.isArray(labels)) {
7375
throw new TypeError('labels must be an Array');
7476
}
@@ -77,7 +79,53 @@ class GraphObject {
7779
throw new TypeError('Invalid identifier');
7880
}
7981

80-
this.labels = labels;
82+
let displayLabels = [...labels]; // Start with a copy of the original labels
83+
84+
// Check if the instance's labels match any of the static label sets defined in the schema.
85+
// `allSchemaStaticLabelSets` is a Set of Sets (e.g., Set<Set<'LabelA', 'LabelB'>>).
86+
if (containsDynamicLabelElement && allSchemaStaticLabelSets instanceof Set && allSchemaStaticLabelSets.size > 0) {
87+
for (const schemaStaticSet of allSchemaStaticLabelSets) {
88+
// `schemaStaticSet` is one of the Set<string> from the schema (e.g., {'GraphNode'}).
89+
if (!schemaStaticSet instanceof Set) {
90+
continue;
91+
}
92+
if ( schemaStaticSet.size == 0) {
93+
continue;
94+
}
95+
if (schemaStaticSet.size >= labels.size) {
96+
continue;
97+
}
98+
// Check if all labels in `schemaStaticSet` are present in the `instanceLabelsSet`.
99+
let isSubset = true;
100+
for (const staticLabel of schemaStaticSet) {
101+
if (!labels.includes(staticLabel)) {
102+
isSubset = false;
103+
break;
104+
}
105+
}
106+
107+
if (isSubset) {
108+
// The instance's labels contain a complete static label set from the schema.
109+
// Filter these static labels out to get the "dynamic" or additional labels.
110+
const originalDisplayLabelsBeforeFilter = [...displayLabels];
111+
displayLabels = displayLabels.filter(l => !schemaStaticSet.has(l));
112+
113+
// If filtering resulted in an empty list, but the original list was not empty,
114+
// it implies that the instance *only* had the static labels.
115+
// In this case, to avoid an object with no label, revert to showing the static labels.
116+
// This is important if `dynamicLabelExpr` was not used or didn't yield a new label.
117+
if (displayLabels.length === 0 && originalDisplayLabelsBeforeFilter.length > 0) {
118+
displayLabels = originalDisplayLabelsBeforeFilter;
119+
}
120+
// Assuming an object's labels should only be filtered against one primary static set.
121+
// If multiple static sets could be subsets, this `break` might need re-evaluation
122+
// or a more complex priority system for which static set "wins" for filtering.
123+
break;
124+
}
125+
}
126+
}
127+
128+
this.labels = displayLabels;
81129
this.labelString = this.labels.join(' | ');
82130
this.properties = properties;
83131
this.key_property_names = key_property_names;

frontend/src/models/node.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,16 @@ class GraphNode extends GraphObject {
6464
* @property {string} color
6565
* @property {string} identifier
6666
* @property {bool} intermediate
67+
* @property {Set<Set<string>>} allSchemaStaticLabelSets - A set of static label sets from all node table definitions in the schema.
6768
*/
6869

6970
/**
7071
* A node on the graph
7172
* @param {NodeData} params
7273
*/
7374
constructor(params) {
74-
const { labels, title, properties, value, key_property_names, identifier, intermediate } = params;
75-
super({ labels, title, properties, key_property_names, identifier });
75+
const { labels, title, properties, value, key_property_names, identifier, intermediate, containsDynamicLabelElement, allSchemaStaticLabelSets} = params;
76+
super({ labels, title, properties, key_property_names, identifier, containsDynamicLabelElement, allSchemaStaticLabelSets});
7677

7778
this.value = value;
7879
this.instantiated = true;

frontend/src/models/schema.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ class Schema {
3838
* @property {string} baseSchemaName
3939
* @property {string} baseTableName
4040
* @property {EdgeDestinationNode} destinationNodeTable
41+
* @property {string} [dynamicLabelExpr] - Optional SQL expression for dynamic node labels.
42+
* @property {string} [dynamicPropertyExpr] - Optional SQL expression for dynamic node properties (JSON).
4143
* @property {Array<string>} keyColumns
4244
* @property {string} kind
4345
* @property {Array<string>} labelNames
@@ -51,6 +53,8 @@ class Schema {
5153
* @property {string} baseCatalogName
5254
* @property {string} baseSchemaName
5355
* @property {string} baseTableName
56+
* @property {string} [dynamicLabelExpr] - Optional SQL expression for dynamic node labels.
57+
* @property {string} [dynamicPropertyExpr] - Optional SQL expression for dynamic node properties (JSON).
5458
* @property {Array<string>} keyColumns
5559
* @property {string} kind
5660
* @property {Array<string>} labelNames
@@ -107,6 +111,22 @@ class Schema {
107111
*/
108112
rawSchema;
109113

114+
/**
115+
* Pre-calculated set of static label sets from all node table definitions.
116+
* Each inner Set contains the `labelNames` for a node table.
117+
* @type {Set<Set<string>>}
118+
* @private
119+
*/
120+
_allNodeTableStaticLabelSets = new Set();
121+
122+
/**
123+
* Pre-calculated set of static label sets from all edge table definitions.
124+
* Each inner Set contains the `labelNames` for an edge table.
125+
* @type {Set<Set<string>>}
126+
* @private
127+
*/
128+
_allEdgeTableStaticLabelSets = new Set();
129+
110130
/**
111131
* @param {RawSchema} rawSchemaObject
112132
*/
@@ -128,6 +148,71 @@ class Schema {
128148
if (!this.rawSchema.propertyDeclarations || !Array.isArray(this.rawSchema.propertyDeclarations)) {
129149
this.rawSchema.propertyDeclarations = [];
130150
}
151+
this._precalculateDynamicLabelSets();
152+
}
153+
154+
/**
155+
* Pre-calculates sets of static labels for tables using dynamic labeling.
156+
* This is called internally by the constructor.
157+
* @private
158+
*/
159+
_precalculateDynamicLabelSets() {
160+
this._allNodeTableStaticLabelSets = new Set();
161+
this._allEdgeTableStaticLabelSets = new Set();
162+
163+
// Process Node Tables
164+
(this.rawSchema.nodeTables || []).forEach(table => {
165+
// Collect labelNames if it's a valid array and non-empty
166+
if (Array.isArray(table.labelNames) && table.labelNames.length > 0) {
167+
this._allNodeTableStaticLabelSets.add(new Set(table.labelNames));
168+
}
169+
});
170+
171+
// Process Edge Tables
172+
(this.rawSchema.edgeTables || []).forEach(table => {
173+
// Collect labelNames if it's a valid array and non-empty
174+
if (Array.isArray(table.labelNames) && table.labelNames.length > 0) {
175+
this._allEdgeTableStaticLabelSets.add(new Set(table.labelNames));
176+
}
177+
});
178+
}
179+
180+
/**
181+
* Returns the pre-calculated set of static label sets from all node table definitions.
182+
* @returns {Set<Set<string>>} A Set of Sets, where each inner Set contains static string labels.
183+
*/
184+
getAllNodeTableStaticLabelSets() {
185+
return this._allNodeTableStaticLabelSets;
186+
}
187+
188+
/**
189+
* Returns the pre-calculated set of static label sets from all edge table definitions.
190+
* @returns {Set<Set<string>>} A Set of Sets, where each inner Set contains static string labels.
191+
*/
192+
getAllEdgeTableStaticLabelSets() {
193+
return this._allEdgeTableStaticLabelSets;
194+
}
195+
196+
/**
197+
* @returns {boolean} True if there is a dynamic label node table.
198+
*/
199+
containsDynamicLabelNode() {
200+
return (this.rawSchema.nodeTables || []).some(table =>
201+
table.dynamicLabelExpr &&
202+
typeof table.dynamicLabelExpr === "string" &&
203+
table.dynamicLabelExpr.length > 0
204+
);
205+
}
206+
207+
/**
208+
* @returns {boolean} True if there is a dynamic label edge table.
209+
*/
210+
containsDynamicLabelEdge() {
211+
return (this.rawSchema.edgeTables || []).some(table =>
212+
table.dynamicLabelExpr &&
213+
typeof table.dynamicLabelExpr === "string" &&
214+
table.dynamicLabelExpr.length > 0
215+
);
131216
}
132217

133218
/**

frontend/src/spanner-config.js

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,39 @@ class GraphConfig {
9090
colorScheme = GraphConfig.ColorScheme.NEIGHBORHOOD;
9191

9292
colorPalette = [
93-
'#1A73E8', '#E52592', '#12A4AF', '#F4511E',
94-
'#9334E6', '#689F38', '#3949AB', '#546E7A',
95-
'#EF6C00', '#D93025', '#1E8E3E', '#039BE5'
96-
];
93+
'#1A73E8',
94+
'#E52592',
95+
'#12A4AF',
96+
'#F4511E',
97+
'#9334E6',
98+
'#689F38',
99+
'#3949AB',
100+
'#546E7A',
101+
'#EF6C00',
102+
'#D93025',
103+
'#1E8E3E',
104+
'#039BE5',
105+
'#4682B4',
106+
'#F0E68C',
107+
'#00FFFF',
108+
'#FFB6C1',
109+
'#E6E6FA',
110+
'#7B68EE',
111+
'#CD853F',
112+
'#BDB76B',
113+
'#40E0D0',
114+
'#708090',
115+
'#DA70D6',
116+
'#32CD32',
117+
'#8B008B',
118+
'#B0E0E6',
119+
'#FF7F50',
120+
'#A0522D',
121+
'#6B8E23',
122+
'#DC143C',
123+
'#FFD700',
124+
'#DB7093',
125+
]
97126

98127
// [label: string]: colorString
99128
nodeColors = {};
@@ -181,10 +210,10 @@ class GraphConfig {
181210
* @param {RawSchema} config.schemaData - Raw schema data from Spanner
182211
*/
183212
constructor({ nodesData, edgesData, colorPalette, colorScheme, rowsData, schemaData, queryResult}) {
213+
this.parseSchema(schemaData);
184214
this.nodes = this.parseNodes(nodesData);
185215
this.nodeCount = Object.keys(this.nodes).length;
186216
this.edges = this.parseEdges(edgesData);
187-
this.parseSchema(schemaData);
188217

189218
this.nodeColors = {};
190219
this.assignColors();
@@ -205,8 +234,6 @@ class GraphConfig {
205234
* Assigns colors for node labels to the existing color map
206235
*/
207236
assignColors() {
208-
const colorPalette = this.colorPalette.map(color => color);
209-
210237
const labels = new Set();
211238

212239
for (const uid of Object.keys(this.nodes)) {
@@ -228,7 +255,7 @@ class GraphConfig {
228255
}
229256

230257
for (const label of labels) {
231-
if (colorPalette.length === 0) {
258+
if (this.colorPalette.length === 0) {
232259
console.error('Node labels exceed the color palette. Assigning default color.');
233260
continue;
234261
}
@@ -239,7 +266,7 @@ class GraphConfig {
239266
}
240267

241268
if (!this.nodeColors[label]) {
242-
this.nodeColors[label] = colorPalette.shift();
269+
this.nodeColors[label] = this.colorPalette.shift();
243270
}
244271
}
245272
}
@@ -251,6 +278,7 @@ class GraphConfig {
251278
*/
252279
parseSchema(schemaData) {
253280
if (!(schemaData instanceof Object)) {
281+
this.schema = new Schema({});
254282
return;
255283
}
256284

@@ -313,14 +341,17 @@ class GraphConfig {
313341

314342
/** @type {NodeMap} */
315343
const nodes = {};
344+
const allSchemaStaticLabelSets = this.schema.getAllNodeTableStaticLabelSets();
345+
const containsDynamicLabelElement = this.schema.containsDynamicLabelNode();
316346
nodesData.forEach(nodeData => {
317347
if (!(nodeData instanceof Object)) {
318348
console.error('Node data is not an object', nodeData);
319349
return;
320350
}
351+
const fullNodeData = { ...nodeData, containsDynamicLabelElement, allSchemaStaticLabelSets };
321352

322353
// Try to create a Node
323-
const node = new GraphNode(nodeData);
354+
const node = new GraphNode(fullNodeData);
324355
if (!node || !node.instantiated) {
325356
console.error('Unable to instantiate node', node.instantiationErrorReason);
326357
return;
@@ -332,7 +363,7 @@ class GraphConfig {
332363
}
333364
} else {
334365
node.instantiationErrorReason = 'Could not construct an instance of Node';
335-
console.error(node.instantiationErrorReason, { nodeData, node });
366+
console.error(node.instantiationErrorReason, { fullNodeData, node });
336367
}
337368
});
338369

@@ -353,14 +384,17 @@ class GraphConfig {
353384

354385
/** @type {EdgeMap} */
355386
const edges = {}
387+
const allSchemaStaticLabelSets = this.schema.getAllEdgeTableStaticLabelSets();
388+
const containsDynamicLabelElement = this.schema.containsDynamicLabelEdge();
356389
edgesData.forEach(edgeData => {
357390
if (!(edgeData instanceof Object)) {
358391
console.error('Edge data is not an object', edgeData);
359392
return;
360393
}
394+
const fullEdgeData = { ...edgeData, containsDynamicLabelElement, allSchemaStaticLabelSets };
361395

362396
// Try to create an Edge
363-
const edge = new GraphEdge(edgeData);
397+
const edge = new GraphEdge(fullEdgeData);
364398
if (!edge || !edge.instantiated) {
365399
console.error('Unable to instantiate edge', edge.instantiationErrorReason);
366400
return;
@@ -372,7 +406,7 @@ class GraphConfig {
372406
this._updateEdgeIndices(edge);
373407
} else {
374408
edge.instantiationErrorReason = 'Could not construct an instance of Edge';
375-
console.error(edge.instantiationErrorReason, { edgeData, edge });
409+
console.error(edge.instantiationErrorReason, { fullEdgeData, edge });
376410
}
377411
});
378412

frontend/tests/unit/spanner-config.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,6 @@ describe('GraphConfig', () => {
235235
schemaData: null
236236
});
237237

238-
expect(config.schema).toBeNull();
239238
expect(Object.keys(config.schemaNodes).length).toBe(0);
240239
expect(Object.keys(config.schemaEdges).length).toBe(0);
241240
});

frontend/tests/unit/spanner-store.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ describe('GraphStore', () => {
125125
labels: 2,
126126
nodeTables: [mockNodeTable, mockNodeTable2],
127127
edgeTables: [mockEdgeTable, mockEdgeTable2],
128-
propertyDeclarations: mockPropertyDeclarations
128+
propertyDeclarations: mockPropertyDeclarations,
129129
};
130130

131131
mockConfig = new GraphConfig({

spanner_graphs/graph_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ def execute_query(project: str, instance: str, database: str, query: str, mock =
211211

212212
try:
213213
query_result, fields, rows, schema_json, err = database.execute_query(query)
214-
if len(rows) == 0 : # if query returned an error
214+
if len(rows) == 0 and err : # if query returned an error
215215
if schema_json: # if the schema exists
216216
return {
217217
"response": {

0 commit comments

Comments
 (0)