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
- let baseLinkLength = Math.max(avgNodeSize + baseSeparation, 120);
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);