spytial-core 1.4.15-beta.1 → 1.4.15-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -146,6 +146,55 @@ See [webcola-demo/projection-controls-demo-vanilla.html](./webcola-demo/projecti
|
|
|
146
146
|
|
|
147
147
|
---
|
|
148
148
|
|
|
149
|
+
## Node Highlighting
|
|
150
|
+
|
|
151
|
+
Visualize selector and evaluator results by highlighting nodes directly in the graph. This feature allows you to examine selector results in context without triggering a layout refresh.
|
|
152
|
+
|
|
153
|
+
### Unary Selectors (Single Nodes)
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
// Evaluate a unary selector
|
|
157
|
+
const result = evaluator.evaluate('Student');
|
|
158
|
+
const nodeIds = result.selectedAtoms();
|
|
159
|
+
|
|
160
|
+
// Highlight the nodes
|
|
161
|
+
const graph = document.querySelector('webcola-cnd-graph');
|
|
162
|
+
graph.highlightNodes(nodeIds);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Binary Selectors (Node Pairs)
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// Evaluate a binary selector
|
|
169
|
+
const result = evaluator.evaluate('friend');
|
|
170
|
+
const pairs = result.selectedTwoples(); // [["Alice", "Bob"], ["Charlie", "Diana"]]
|
|
171
|
+
|
|
172
|
+
// Highlight with visual correspondence
|
|
173
|
+
graph.highlightNodePairs(pairs);
|
|
174
|
+
|
|
175
|
+
// Or with badges showing 1/2 correspondence
|
|
176
|
+
graph.highlightNodePairs(pairs, { showBadges: true });
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Clear Highlights
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// Remove all node highlights
|
|
183
|
+
graph.clearNodeHighlights();
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Visual Styling
|
|
187
|
+
|
|
188
|
+
- **Unary selectors**: Orange border with glow effect
|
|
189
|
+
- **Binary selectors**:
|
|
190
|
+
- First elements: Blue border (e.g., the source of a relation)
|
|
191
|
+
- Second elements: Red border (e.g., the target of a relation)
|
|
192
|
+
- Optional badges: Shows "1" and "2" to indicate correspondence
|
|
193
|
+
|
|
194
|
+
See [webcola-demo/node-highlighter-demo.html](./webcola-demo/node-highlighter-demo.html) for an interactive demo.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
149
198
|
## CDN
|
|
150
199
|
|
|
151
200
|
You can use the browser bundle directly from a CDN:
|
|
@@ -197,6 +246,25 @@ Once loaded, use via the global `CndCore` object:
|
|
|
197
246
|
- **`SGraphQueryEvaluator`** - Evaluate selector expressions
|
|
198
247
|
- **`AlloyDataInstance`**, **`JSONDataInstance`**, etc. - Data format adapters
|
|
199
248
|
|
|
249
|
+
### WebCola Graph API
|
|
250
|
+
|
|
251
|
+
The `<webcola-cnd-graph>` custom element provides methods for interacting with the rendered graph:
|
|
252
|
+
|
|
253
|
+
#### Node Highlighting
|
|
254
|
+
- **`highlightNodes(nodeIds: string[])`** - Highlight nodes by ID (unary selectors)
|
|
255
|
+
- **`highlightNodePairs(pairs: string[][], options?)`** - Highlight node pairs with first/second correspondence (binary selectors)
|
|
256
|
+
- **`clearNodeHighlights()`** - Remove all node highlights
|
|
257
|
+
|
|
258
|
+
#### Relation Highlighting
|
|
259
|
+
- **`getAllRelations()`** - Get all unique relation names
|
|
260
|
+
- **`highlightRelation(relName: string)`** - Highlight edges by relation name
|
|
261
|
+
- **`clearHighlightRelation(relName: string)`** - Clear relation highlighting
|
|
262
|
+
|
|
263
|
+
#### Layout Management
|
|
264
|
+
- **`renderLayout(instanceLayout, options?)`** - Render a layout with optional prior positions
|
|
265
|
+
- **`clear()`** - Clear the graph and reset state
|
|
266
|
+
- **`getNodePositions()`** - Get current positions of all nodes
|
|
267
|
+
|
|
200
268
|
See [docs/](./docs/) for detailed documentation.
|
|
201
269
|
|
|
202
270
|
---
|
|
@@ -91263,9 +91263,9 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
|
|
|
91263
91263
|
return group.name.startsWith(_WebColaCnDGraph.DISCONNECTED_NODE_PREFIX);
|
|
91264
91264
|
}
|
|
91265
91265
|
/**
|
|
91266
|
-
* Computes adaptive link length based on actual node dimensions and graph density
|
|
91266
|
+
* Computes adaptive link length based on actual node dimensions, edge labels, and graph density
|
|
91267
91267
|
*/
|
|
91268
|
-
computeAdaptiveLinkLength(nodes, scaleFactor) {
|
|
91268
|
+
computeAdaptiveLinkLength(nodes, scaleFactor, links) {
|
|
91269
91269
|
if (!nodes || nodes.length === 0) {
|
|
91270
91270
|
return 150;
|
|
91271
91271
|
}
|
|
@@ -91285,20 +91285,32 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
|
|
|
91285
91285
|
const avgWidth = totalWidth / validNodes;
|
|
91286
91286
|
const avgHeight = totalHeight / validNodes;
|
|
91287
91287
|
const avgNodeSize = Math.max(avgWidth, avgHeight);
|
|
91288
|
+
let maxLabelWidth = 0;
|
|
91289
|
+
if (links && links.length > 0) {
|
|
91290
|
+
const fontSize = 12;
|
|
91291
|
+
links.forEach((link) => {
|
|
91292
|
+
if (link && link.label) {
|
|
91293
|
+
const labelWidth = this.measureTextWidth(link.label, fontSize, "system-ui");
|
|
91294
|
+
maxLabelWidth = Math.max(maxLabelWidth, labelWidth);
|
|
91295
|
+
}
|
|
91296
|
+
});
|
|
91297
|
+
}
|
|
91298
|
+
const markerSize = 15;
|
|
91288
91299
|
const baseSeparation = 50;
|
|
91289
|
-
|
|
91300
|
+
const labelAndMarkerSpace = maxLabelWidth + markerSize + 20;
|
|
91301
|
+
let baseLinkLength = Math.max(avgNodeSize + baseSeparation + labelAndMarkerSpace, 120);
|
|
91290
91302
|
const densityFactor = Math.max(0.7, 1 - Math.log10(validNodes) * 0.1);
|
|
91291
91303
|
baseLinkLength *= densityFactor;
|
|
91292
91304
|
const adjustedScaleFactor = scaleFactor / 5;
|
|
91293
91305
|
const scaledLinkLength = baseLinkLength / adjustedScaleFactor;
|
|
91294
91306
|
return Math.max(60, Math.min(scaledLinkLength, 350));
|
|
91295
91307
|
}
|
|
91296
|
-
getScaledDetails(constraints, scaleFactor = DEFAULT_SCALE_FACTOR, nodes, groups) {
|
|
91308
|
+
getScaledDetails(constraints, scaleFactor = DEFAULT_SCALE_FACTOR, nodes, groups, links) {
|
|
91297
91309
|
const adjustedScaleFactor = scaleFactor / 5;
|
|
91298
91310
|
let groupCompactness = this.calculateAdaptiveGroupCompactness(groups || [], nodes?.length || 0, adjustedScaleFactor);
|
|
91299
91311
|
let linkLength;
|
|
91300
91312
|
if (nodes && nodes.length > 0) {
|
|
91301
|
-
linkLength = this.computeAdaptiveLinkLength(nodes, scaleFactor);
|
|
91313
|
+
linkLength = this.computeAdaptiveLinkLength(nodes, scaleFactor, links);
|
|
91302
91314
|
} else {
|
|
91303
91315
|
const min_sep = 150;
|
|
91304
91316
|
const default_node_width = 100;
|
|
@@ -91407,11 +91419,11 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
|
|
|
91407
91419
|
<span id="error-icon" title="This graph is depicting an error state">\u26A0\uFE0F</span>
|
|
91408
91420
|
<svg id="svg" viewBox="0 0 ${containerWidth} ${containerHeight}" preserveAspectRatio="xMidYMid meet">
|
|
91409
91421
|
<defs>
|
|
91410
|
-
<marker id="end-arrow" markerWidth="15" markerHeight="10" refX="12" refY="5" orient="auto">
|
|
91411
|
-
<polygon points="0 0, 15 5, 0 10" />
|
|
91422
|
+
<marker id="end-arrow" markerWidth="15" markerHeight="10" refX="12" refY="5" orient="auto" markerUnits="userSpaceOnUse">
|
|
91423
|
+
<polygon points="0 0, 15 5, 0 10" fill="context-stroke" />
|
|
91412
91424
|
</marker>
|
|
91413
|
-
<marker id="start-arrow" markerWidth="15" markerHeight="10" refX="3" refY="5" orient="auto">
|
|
91414
|
-
<polygon points="15 0, 0 5, 15 10" />
|
|
91425
|
+
<marker id="start-arrow" markerWidth="15" markerHeight="10" refX="3" refY="5" orient="auto" markerUnits="userSpaceOnUse">
|
|
91426
|
+
<polygon points="15 0, 0 5, 15 10" fill="context-stroke" />
|
|
91415
91427
|
</marker>
|
|
91416
91428
|
</defs>
|
|
91417
91429
|
<g class="zoomable"></g>
|
|
@@ -91899,7 +91911,8 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
|
|
|
91899
91911
|
webcolaLayout.constraints,
|
|
91900
91912
|
DEFAULT_SCALE_FACTOR,
|
|
91901
91913
|
webcolaLayout.nodes,
|
|
91902
|
-
webcolaLayout.groups
|
|
91914
|
+
webcolaLayout.groups,
|
|
91915
|
+
webcolaLayout.links
|
|
91903
91916
|
);
|
|
91904
91917
|
this.updateLoadingProgress("Applying constraints and initializing...");
|
|
91905
91918
|
const convergenceThreshold = hasPriorPositions ? 0.1 : 1e-3;
|
|
@@ -93604,6 +93617,124 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
|
|
|
93604
93617
|
this.svgLinkGroups.filter((d) => d.relName === relName && !this.isAlignmentEdge(d)).selectAll("path").classed("highlighted", false);
|
|
93605
93618
|
return true;
|
|
93606
93619
|
}
|
|
93620
|
+
/** Public API for node highlighting */
|
|
93621
|
+
/**
|
|
93622
|
+
* Highlights nodes based on their IDs (for unary selector results).
|
|
93623
|
+
* This is useful for visualizing the results of a selector expression.
|
|
93624
|
+
*
|
|
93625
|
+
* @param nodeIds - Array of node IDs to highlight (e.g., from evaluator.selectedAtoms())
|
|
93626
|
+
* @returns True if any nodes were highlighted, false otherwise
|
|
93627
|
+
*
|
|
93628
|
+
* @example
|
|
93629
|
+
* ```typescript
|
|
93630
|
+
* const result = evaluator.evaluate('Student');
|
|
93631
|
+
* const nodeIds = result.selectedAtoms();
|
|
93632
|
+
* graph.highlightNodes(nodeIds);
|
|
93633
|
+
* ```
|
|
93634
|
+
*/
|
|
93635
|
+
highlightNodes(nodeIds) {
|
|
93636
|
+
if (!this.currentLayout?.nodes || !this.svgNodes) return false;
|
|
93637
|
+
if (!nodeIds || nodeIds.length === 0) return false;
|
|
93638
|
+
const nodeIdSet = new Set(nodeIds);
|
|
93639
|
+
let highlighted = false;
|
|
93640
|
+
this.svgNodes.each((d, i, nodes) => {
|
|
93641
|
+
if (nodeIdSet.has(d.id)) {
|
|
93642
|
+
d32.select(nodes[i]).classed("highlighted", true);
|
|
93643
|
+
highlighted = true;
|
|
93644
|
+
}
|
|
93645
|
+
});
|
|
93646
|
+
return highlighted;
|
|
93647
|
+
}
|
|
93648
|
+
/**
|
|
93649
|
+
* Highlights node pairs based on binary selector results.
|
|
93650
|
+
* Shows visual correspondence between first and second elements using different colors.
|
|
93651
|
+
*
|
|
93652
|
+
* Note: If a node appears in multiple pairs with different roles (both first and second),
|
|
93653
|
+
* it will receive both 'highlighted-first' and 'highlighted-second' classes, and if badges
|
|
93654
|
+
* are enabled, only the last badge will be visible (this is intentional to avoid cluttering).
|
|
93655
|
+
*
|
|
93656
|
+
* @param nodePairs - Array of [first, second] node ID pairs (e.g., from evaluator.selectedTwoples())
|
|
93657
|
+
* @param options - Optional configuration for highlighting
|
|
93658
|
+
* @param options.showBadges - If true, shows "1" and "2" badges on nodes (default: false)
|
|
93659
|
+
* @returns True if any nodes were highlighted, false otherwise
|
|
93660
|
+
*
|
|
93661
|
+
* @example
|
|
93662
|
+
* ```typescript
|
|
93663
|
+
* const result = evaluator.evaluate('friend');
|
|
93664
|
+
* const pairs = result.selectedTwoples(); // [["Alice", "Bob"], ["Charlie", "Diana"]]
|
|
93665
|
+
* graph.highlightNodePairs(pairs, { showBadges: true });
|
|
93666
|
+
* ```
|
|
93667
|
+
*/
|
|
93668
|
+
highlightNodePairs(nodePairs, options = {}) {
|
|
93669
|
+
if (!this.currentLayout?.nodes || !this.svgNodes) return false;
|
|
93670
|
+
if (!nodePairs || nodePairs.length === 0) return false;
|
|
93671
|
+
const { showBadges = false } = options;
|
|
93672
|
+
const firstNodeIds = /* @__PURE__ */ new Set();
|
|
93673
|
+
const secondNodeIds = /* @__PURE__ */ new Set();
|
|
93674
|
+
nodePairs.forEach((pair, index) => {
|
|
93675
|
+
if (!Array.isArray(pair)) {
|
|
93676
|
+
console.warn(`highlightNodePairs: Pair at index ${index} is not an array, skipping`);
|
|
93677
|
+
return;
|
|
93678
|
+
}
|
|
93679
|
+
if (pair.length !== 2) {
|
|
93680
|
+
console.warn(`highlightNodePairs: Pair at index ${index} has ${pair.length} elements (expected 2), skipping`);
|
|
93681
|
+
return;
|
|
93682
|
+
}
|
|
93683
|
+
const [first, second] = pair;
|
|
93684
|
+
if (first) firstNodeIds.add(first);
|
|
93685
|
+
if (second) secondNodeIds.add(second);
|
|
93686
|
+
});
|
|
93687
|
+
let highlighted = false;
|
|
93688
|
+
this.svgNodes.each((d, i, nodes) => {
|
|
93689
|
+
const nodeGroup = d32.select(nodes[i]);
|
|
93690
|
+
if (firstNodeIds.has(d.id)) {
|
|
93691
|
+
nodeGroup.classed("highlighted-first", true);
|
|
93692
|
+
highlighted = true;
|
|
93693
|
+
if (showBadges) {
|
|
93694
|
+
this.addHighlightBadge(nodeGroup, d, "1", "#007aff");
|
|
93695
|
+
}
|
|
93696
|
+
}
|
|
93697
|
+
if (secondNodeIds.has(d.id)) {
|
|
93698
|
+
nodeGroup.classed("highlighted-second", true);
|
|
93699
|
+
highlighted = true;
|
|
93700
|
+
if (showBadges) {
|
|
93701
|
+
if (firstNodeIds.has(d.id)) {
|
|
93702
|
+
this.addHighlightBadge(nodeGroup, d, "1,2", "#9B59B6");
|
|
93703
|
+
} else {
|
|
93704
|
+
this.addHighlightBadge(nodeGroup, d, "2", "#ff3b30");
|
|
93705
|
+
}
|
|
93706
|
+
}
|
|
93707
|
+
}
|
|
93708
|
+
});
|
|
93709
|
+
return highlighted;
|
|
93710
|
+
}
|
|
93711
|
+
/**
|
|
93712
|
+
* Clears all node highlights (both unary and binary).
|
|
93713
|
+
*
|
|
93714
|
+
* @returns True if the operation completed successfully
|
|
93715
|
+
*/
|
|
93716
|
+
clearNodeHighlights() {
|
|
93717
|
+
if (!this.svgNodes) return false;
|
|
93718
|
+
this.svgNodes.classed("highlighted", false).classed("highlighted-first", false).classed("highlighted-second", false).selectAll(".highlight-badge, .highlight-badge-bg").remove();
|
|
93719
|
+
return true;
|
|
93720
|
+
}
|
|
93721
|
+
/**
|
|
93722
|
+
* Adds a visual badge to a highlighted node to show first/second correspondence.
|
|
93723
|
+
*
|
|
93724
|
+
* @param nodeGroup - D3 selection of the node group
|
|
93725
|
+
* @param nodeData - Node data containing position and dimensions
|
|
93726
|
+
* @param label - Badge label text ('1' or '2')
|
|
93727
|
+
* @param color - Badge background color
|
|
93728
|
+
*/
|
|
93729
|
+
addHighlightBadge(nodeGroup, nodeData, label, color) {
|
|
93730
|
+
nodeGroup.selectAll(".highlight-badge, .highlight-badge-bg").remove();
|
|
93731
|
+
const badgeSize = 16;
|
|
93732
|
+
const padding = 4;
|
|
93733
|
+
const badgeX = (nodeData.width || 0) / 2 - badgeSize / 2 - padding;
|
|
93734
|
+
const badgeY = -(nodeData.height || 0) / 2 + badgeSize / 2 + padding;
|
|
93735
|
+
nodeGroup.append("circle").attr("class", "highlight-badge-bg").attr("cx", badgeX).attr("cy", badgeY).attr("r", badgeSize / 2).attr("fill", color);
|
|
93736
|
+
nodeGroup.append("text").attr("class", "highlight-badge").attr("x", badgeX).attr("y", badgeY).attr("dy", "0.35em").text(label);
|
|
93737
|
+
}
|
|
93607
93738
|
/**
|
|
93608
93739
|
* Shows a runtime alert for edge routing errors.
|
|
93609
93740
|
*
|
|
@@ -93710,6 +93841,38 @@ VVbdfjptxz|~\x80\x82\x84\xA6\xA8W\b
|
|
|
93710
93841
|
stroke:#666666; /* Change this to your desired highlight color */
|
|
93711
93842
|
stroke-width: 3px; /* Change this to your desired highlight width */
|
|
93712
93843
|
}
|
|
93844
|
+
|
|
93845
|
+
/* Node highlighting styles */
|
|
93846
|
+
.node.highlighted rect {
|
|
93847
|
+
stroke: #ff9500;
|
|
93848
|
+
stroke-width: 3px;
|
|
93849
|
+
filter: drop-shadow(0 0 6px rgba(255, 149, 0, 0.6));
|
|
93850
|
+
}
|
|
93851
|
+
|
|
93852
|
+
.node.highlighted-first rect {
|
|
93853
|
+
stroke: #007aff;
|
|
93854
|
+
stroke-width: 3px;
|
|
93855
|
+
filter: drop-shadow(0 0 6px rgba(0, 122, 255, 0.6));
|
|
93856
|
+
}
|
|
93857
|
+
|
|
93858
|
+
.node.highlighted-second rect {
|
|
93859
|
+
stroke: #ff3b30;
|
|
93860
|
+
stroke-width: 3px;
|
|
93861
|
+
filter: drop-shadow(0 0 6px rgba(255, 59, 48, 0.6));
|
|
93862
|
+
}
|
|
93863
|
+
|
|
93864
|
+
/* Add a badge indicator for first/second in binary selectors */
|
|
93865
|
+
.highlight-badge {
|
|
93866
|
+
font-size: 10px;
|
|
93867
|
+
font-weight: bold;
|
|
93868
|
+
fill: white;
|
|
93869
|
+
text-anchor: middle;
|
|
93870
|
+
pointer-events: none;
|
|
93871
|
+
}
|
|
93872
|
+
|
|
93873
|
+
.highlight-badge-bg {
|
|
93874
|
+
pointer-events: none;
|
|
93875
|
+
}
|
|
93713
93876
|
|
|
93714
93877
|
.group {
|
|
93715
93878
|
fill: rgba(200, 200, 200, 0.3);
|