PyDecisionGraph 0.3.2.post1__cp315-cp315t-win_amd64.whl

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.
Files changed (148) hide show
  1. decision_graph/__infra__.pxd +1 -0
  2. decision_graph/__init__.pxd +1 -0
  3. decision_graph/__init__.py +39 -0
  4. decision_graph/decision_tree/__init__.pxd +2 -0
  5. decision_graph/decision_tree/__init__.py +85 -0
  6. decision_graph/decision_tree/bake/__infra__.pxd +502 -0
  7. decision_graph/decision_tree/bake/__init__.pxd +502 -0
  8. decision_graph/decision_tree/bake/__init__.py +29 -0
  9. decision_graph/decision_tree/bake/c_action.cp315t-win_amd64.pyd +0 -0
  10. decision_graph/decision_tree/bake/c_action.h +324 -0
  11. decision_graph/decision_tree/bake/c_action.pxd +54 -0
  12. decision_graph/decision_tree/bake/c_action.pyi +126 -0
  13. decision_graph/decision_tree/bake/c_action.pyx +65 -0
  14. decision_graph/decision_tree/bake/c_allocator_protocol.cp315t-win_amd64.pyd +0 -0
  15. decision_graph/decision_tree/bake/c_allocator_protocol.pxd +24 -0
  16. decision_graph/decision_tree/bake/c_allocator_protocol.pyi +61 -0
  17. decision_graph/decision_tree/bake/c_allocator_protocol.pyx +109 -0
  18. decision_graph/decision_tree/bake/c_bake.cp315t-win_amd64.pyd +0 -0
  19. decision_graph/decision_tree/bake/c_bake.h +629 -0
  20. decision_graph/decision_tree/bake/c_bake.pxd +52 -0
  21. decision_graph/decision_tree/bake/c_bake.pyi +151 -0
  22. decision_graph/decision_tree/bake/c_bake.pyx +142 -0
  23. decision_graph/decision_tree/bake/c_collections.cp315t-win_amd64.pyd +0 -0
  24. decision_graph/decision_tree/bake/c_collections.h +692 -0
  25. decision_graph/decision_tree/bake/c_collections.pxd +52 -0
  26. decision_graph/decision_tree/bake/c_collections.pyi +193 -0
  27. decision_graph/decision_tree/bake/c_collections.pyx +161 -0
  28. decision_graph/decision_tree/bake/c_const.cp315t-win_amd64.pyd +0 -0
  29. decision_graph/decision_tree/bake/c_const.h +395 -0
  30. decision_graph/decision_tree/bake/c_const.pxd +56 -0
  31. decision_graph/decision_tree/bake/c_const.pyi +305 -0
  32. decision_graph/decision_tree/bake/c_const.pyx +278 -0
  33. decision_graph/decision_tree/bake/c_edge.cp315t-win_amd64.pyd +0 -0
  34. decision_graph/decision_tree/bake/c_edge.h +511 -0
  35. decision_graph/decision_tree/bake/c_edge.pxd +117 -0
  36. decision_graph/decision_tree/bake/c_edge.pyi +192 -0
  37. decision_graph/decision_tree/bake/c_edge.pyx +270 -0
  38. decision_graph/decision_tree/bake/c_eval.h +848 -0
  39. decision_graph/decision_tree/bake/c_eval.pxd +40 -0
  40. decision_graph/decision_tree/bake/c_expr.cp315t-win_amd64.pyd +0 -0
  41. decision_graph/decision_tree/bake/c_expr.h +1900 -0
  42. decision_graph/decision_tree/bake/c_expr.pxd +129 -0
  43. decision_graph/decision_tree/bake/c_expr.pyi +280 -0
  44. decision_graph/decision_tree/bake/c_expr.pyx +272 -0
  45. decision_graph/decision_tree/bake/c_hierarchy.cp315t-win_amd64.pyd +0 -0
  46. decision_graph/decision_tree/bake/c_hierarchy.h +731 -0
  47. decision_graph/decision_tree/bake/c_hierarchy.pxd +71 -0
  48. decision_graph/decision_tree/bake/c_hierarchy.pyi +409 -0
  49. decision_graph/decision_tree/bake/c_hierarchy.pyx +277 -0
  50. decision_graph/decision_tree/bake/c_logic_group.cp315t-win_amd64.pyd +0 -0
  51. decision_graph/decision_tree/bake/c_logic_group.h +1226 -0
  52. decision_graph/decision_tree/bake/c_logic_group.pxd +162 -0
  53. decision_graph/decision_tree/bake/c_logic_group.pyi +264 -0
  54. decision_graph/decision_tree/bake/c_logic_group.pyx +308 -0
  55. decision_graph/decision_tree/bake/c_node.cp315t-win_amd64.pyd +0 -0
  56. decision_graph/decision_tree/bake/c_node.h +2660 -0
  57. decision_graph/decision_tree/bake/c_node.pxd +381 -0
  58. decision_graph/decision_tree/bake/c_node.pyi +438 -0
  59. decision_graph/decision_tree/bake/c_node.pyx +639 -0
  60. decision_graph/decision_tree/bake/c_reconstruct.cp315t-win_amd64.pyd +0 -0
  61. decision_graph/decision_tree/bake/c_reconstruct.pxd +63 -0
  62. decision_graph/decision_tree/bake/c_reconstruct.pyi +60 -0
  63. decision_graph/decision_tree/bake/c_reconstruct.pyx +252 -0
  64. decision_graph/decision_tree/bake/c_var.cp315t-win_amd64.pyd +0 -0
  65. decision_graph/decision_tree/bake/c_var.h +2054 -0
  66. decision_graph/decision_tree/bake/c_var.pxd +205 -0
  67. decision_graph/decision_tree/bake/c_var.pyi +271 -0
  68. decision_graph/decision_tree/bake/c_var.pyx +228 -0
  69. decision_graph/decision_tree/capi/__infra__.pxd +33 -0
  70. decision_graph/decision_tree/capi/__init__.pxd +33 -0
  71. decision_graph/decision_tree/capi/__init__.py +57 -0
  72. decision_graph/decision_tree/capi/c_abc.cp315t-win_amd64.pyd +0 -0
  73. decision_graph/decision_tree/capi/c_abc.pxd +246 -0
  74. decision_graph/decision_tree/capi/c_abc.pyi +770 -0
  75. decision_graph/decision_tree/capi/c_abc.pyx +1532 -0
  76. decision_graph/decision_tree/capi/c_collection.cp315t-win_amd64.pyd +0 -0
  77. decision_graph/decision_tree/capi/c_collection.pxd +22 -0
  78. decision_graph/decision_tree/capi/c_collection.pyi +245 -0
  79. decision_graph/decision_tree/capi/c_collection.pyx +138 -0
  80. decision_graph/decision_tree/capi/c_node.cp315t-win_amd64.pyd +0 -0
  81. decision_graph/decision_tree/capi/c_node.pxd +95 -0
  82. decision_graph/decision_tree/capi/c_node.pyi +492 -0
  83. decision_graph/decision_tree/capi/c_node.pyx +551 -0
  84. decision_graph/decision_tree/exc.py +189 -0
  85. decision_graph/decision_tree/native/__init__.py +57 -0
  86. decision_graph/decision_tree/native/abc.py +1167 -0
  87. decision_graph/decision_tree/native/collection.py +134 -0
  88. decision_graph/decision_tree/native/node.py +521 -0
  89. decision_graph/includes/decision_graph/decision_tree/bake/c_action.c +16320 -0
  90. decision_graph/includes/decision_graph/decision_tree/bake/c_action.h +324 -0
  91. decision_graph/includes/decision_graph/decision_tree/bake/c_allocator_protocol.c +12473 -0
  92. decision_graph/includes/decision_graph/decision_tree/bake/c_bake.c +16485 -0
  93. decision_graph/includes/decision_graph/decision_tree/bake/c_bake.h +629 -0
  94. decision_graph/includes/decision_graph/decision_tree/bake/c_collections.c +17381 -0
  95. decision_graph/includes/decision_graph/decision_tree/bake/c_collections.h +692 -0
  96. decision_graph/includes/decision_graph/decision_tree/bake/c_const.c +20815 -0
  97. decision_graph/includes/decision_graph/decision_tree/bake/c_const.h +395 -0
  98. decision_graph/includes/decision_graph/decision_tree/bake/c_edge.c +20696 -0
  99. decision_graph/includes/decision_graph/decision_tree/bake/c_edge.h +511 -0
  100. decision_graph/includes/decision_graph/decision_tree/bake/c_eval.h +848 -0
  101. decision_graph/includes/decision_graph/decision_tree/bake/c_expr.c +22065 -0
  102. decision_graph/includes/decision_graph/decision_tree/bake/c_expr.h +1900 -0
  103. decision_graph/includes/decision_graph/decision_tree/bake/c_hierarchy.c +23267 -0
  104. decision_graph/includes/decision_graph/decision_tree/bake/c_hierarchy.h +731 -0
  105. decision_graph/includes/decision_graph/decision_tree/bake/c_logic_group.c +22434 -0
  106. decision_graph/includes/decision_graph/decision_tree/bake/c_logic_group.h +1226 -0
  107. decision_graph/includes/decision_graph/decision_tree/bake/c_node.c +28385 -0
  108. decision_graph/includes/decision_graph/decision_tree/bake/c_node.h +2660 -0
  109. decision_graph/includes/decision_graph/decision_tree/bake/c_reconstruct.c +15955 -0
  110. decision_graph/includes/decision_graph/decision_tree/bake/c_var.c +16881 -0
  111. decision_graph/includes/decision_graph/decision_tree/bake/c_var.h +2054 -0
  112. decision_graph/includes/decision_graph/decision_tree/capi/c_abc.c +59196 -0
  113. decision_graph/includes/decision_graph/decision_tree/capi/c_collection.c +19967 -0
  114. decision_graph/includes/decision_graph/decision_tree/capi/c_node.c +39229 -0
  115. decision_graph/logic_group/__init__.py +25 -0
  116. decision_graph/logic_group/base.py +100 -0
  117. decision_graph/logic_group/pending_request.py +384 -0
  118. decision_graph/webui/__init__.py +24 -0
  119. decision_graph/webui/bake/__init__.py +65 -0
  120. decision_graph/webui/bake/app.py +488 -0
  121. decision_graph/webui/bake/static/d3.v7.min.js +2 -0
  122. decision_graph/webui/bake/static/script.js +1440 -0
  123. decision_graph/webui/bake/static/style.css +920 -0
  124. decision_graph/webui/bake/templates/index.html +183 -0
  125. decision_graph/webui/bake/templates/offline.html +184 -0
  126. decision_graph/webui/capi/__init__.py +54 -0
  127. decision_graph/webui/capi/app.py +366 -0
  128. decision_graph/webui/capi/main.py +53 -0
  129. decision_graph/webui/capi/static/d3.v7.min.js +2 -0
  130. decision_graph/webui/capi/static/script.js +1026 -0
  131. decision_graph/webui/capi/static/style.css +656 -0
  132. decision_graph/webui/capi/templates/index.html +93 -0
  133. decision_graph/webui/capi/templates/offline.html +94 -0
  134. decision_graph/webui/native/__init__.py +54 -0
  135. decision_graph/webui/native/app.py +366 -0
  136. decision_graph/webui/native/main.py +53 -0
  137. decision_graph/webui/native/static/d3.v7.min.js +2 -0
  138. decision_graph/webui/native/static/script.js +1026 -0
  139. decision_graph/webui/native/static/style.css +656 -0
  140. decision_graph/webui/native/templates/index.html +93 -0
  141. decision_graph/webui/native/templates/offline.html +94 -0
  142. pydecisiongraph-0.3.2.post1.dist-info/METADATA +136 -0
  143. pydecisiongraph-0.3.2.post1.dist-info/RECORD +148 -0
  144. pydecisiongraph-0.3.2.post1.dist-info/WHEEL +5 -0
  145. pydecisiongraph-0.3.2.post1.dist-info/licenses/LICENSE +373 -0
  146. pydecisiongraph-0.3.2.post1.dist-info/top_level.txt +2 -0
  147. tests/bake_c_evaluator.cp315t-win_amd64.pyd +0 -0
  148. tests/bake_eval_probe.cp315t-win_amd64.pyd +0 -0
@@ -0,0 +1,1026 @@
1
+ let GLOBAL_TREE_ROOT = null;
2
+ let GLOBAL_TREE_DATA = null;
3
+ let GLOBAL_SELECTED_GROUP = "*";
4
+ let GLOBAL_VIRTUAL_LINK_DEFS = [];
5
+ let LAST_ACTIVE_IDS = [];
6
+
7
+ const ANIM_SLOW = 500;
8
+ const ANIM_FAST = 120;
9
+ const ROW_VERTICAL_SPACE_FACTOR = 8;
10
+ let GLOBAL_HORIZONTAL_SPACING_BASE = null;
11
+ let GLOBAL_VERTICAL_SPACING = null;
12
+
13
+ const nodeInfoPinBtn = document.getElementById('node-info-pin');
14
+ const nodeInfoPanel = document.getElementById('node-info');
15
+ let nodeInfoPinned = false;
16
+ let nodeInfoDragging = false;
17
+ let nodeInfoOffset = {x: 0, y: 0};
18
+ let nodeInfoLastPos = null;
19
+ let nodeInfoCurrentNodeId = null;
20
+
21
+ function visualizeTree(treeData) {
22
+ GLOBAL_TREE_DATA = treeData;
23
+ GLOBAL_VIRTUAL_LINK_DEFS = treeData.virtual_links || [];
24
+ if (typeof initTheme === 'function') initTheme();
25
+
26
+ const allGroups = new Set();
27
+
28
+ function collectGroups(node) {
29
+ if (node.labels && Array.isArray(node.labels)) {
30
+ node.labels.forEach(label => allGroups.add(label));
31
+ }
32
+ if (node._children) {
33
+ node._children.forEach(collectGroups);
34
+ }
35
+ }
36
+
37
+ collectGroups(treeData.root);
38
+
39
+ const groups = ["*"].concat(Array.from(allGroups).sort());
40
+
41
+ const tabContainer = d3.select("#logic-group-tabs");
42
+ tabContainer.selectAll("button").remove();
43
+ const tabs = tabContainer.selectAll("button")
44
+ .data(groups)
45
+ .enter()
46
+ .append("button")
47
+ .attr("class", d => `tab-button ${d === "*" ? "active" : ""}`)
48
+ .attr("data-group", String)
49
+ .text(d => d === "*" ? "All" : d)
50
+ .on("click", function (event, group) {
51
+ d3.selectAll(".tab-button").classed("active", false);
52
+ d3.select(this).classed("active", true);
53
+
54
+ GLOBAL_SELECTED_GROUP = group;
55
+ renderFilteredTree();
56
+ });
57
+
58
+ renderFilteredTree();
59
+ addExportButtons();
60
+ }
61
+
62
+ function getAllCSS() {
63
+ let css = "";
64
+ for (const sheet of document.styleSheets) {
65
+ try {
66
+ if (!sheet.cssRules) continue;
67
+ for (const rule of sheet.cssRules) {
68
+ css += rule.cssText + "\n";
69
+ }
70
+ } catch (e) {
71
+ }
72
+ }
73
+ return css;
74
+ }
75
+
76
+ function downloadBlob(blob, filename) {
77
+ const url = URL.createObjectURL(blob);
78
+ const a = document.createElement('a');
79
+ a.href = url;
80
+ a.download = filename;
81
+ document.body.appendChild(a);
82
+ a.click();
83
+ a.remove();
84
+ setTimeout(() => URL.revokeObjectURL(url), 100);
85
+ }
86
+
87
+ function getFullTreeBounds() {
88
+ let xMin = Infinity, xMax = -Infinity, yMin = Infinity, yMax = -Infinity;
89
+ if (!GLOBAL_TREE_ROOT) return {xMin: 0, xMax: 1000, yMin: 0, yMax: 800};
90
+ GLOBAL_TREE_ROOT.each(d => {
91
+ const bbox = d.data._nodeBBox || {width: 80, height: 32};
92
+
93
+ const left = d.x - bbox.width / 2;
94
+ const right = d.x + bbox.width / 2;
95
+ const top = d.y - bbox.height / 2;
96
+ const bottom = d.y + bbox.height / 2;
97
+ if (left < xMin) xMin = left;
98
+ if (right > xMax) xMax = right;
99
+ if (top < yMin) yMin = top;
100
+ if (bottom > yMax) yMax = bottom;
101
+ });
102
+
103
+ const pad = 40;
104
+ return {
105
+ xMin: xMin - pad, xMax: xMax + pad, yMin: yMin - pad, yMax: yMax + pad
106
+ };
107
+ }
108
+
109
+ function exportSVG() {
110
+ const svgNode = document.querySelector('#tree-container svg');
111
+ if (!svgNode) return alert('No SVG found to export');
112
+
113
+ const clone = svgNode.cloneNode(true);
114
+ const bounds = getFullTreeBounds();
115
+ clone.setAttribute('viewBox', `${bounds.xMin} ${bounds.yMin} ${bounds.xMax - bounds.xMin} ${bounds.yMax - bounds.yMin}`);
116
+
117
+ const viewport = clone.querySelector('g.dg-viewport');
118
+ if (viewport) viewport.removeAttribute('transform');
119
+ const content = viewport ? viewport.querySelector('g.dg-content') : null;
120
+ if (content) content.removeAttribute('transform');
121
+
122
+ const cssVarNames = ['--bg', '--panel-bg', '--text-color', '--header-bg', '--header-text', '--tabs-bg', '--tab-active-bg', '--tab-active-text', '--muted-border'];
123
+ const comp = getComputedStyle(document.documentElement);
124
+ let varCss = ':root {';
125
+ cssVarNames.forEach(name => {
126
+ const v = comp.getPropertyValue(name).trim();
127
+ if (v) varCss += `${name}: ${v};`;
128
+ });
129
+ varCss += '}\n';
130
+ const cssText = varCss + getAllCSS();
131
+ const styleEl = document.createElement('style');
132
+ styleEl.setAttribute('type', 'text/css');
133
+ styleEl.innerHTML = cssText;
134
+ clone.insertBefore(styleEl, clone.firstChild);
135
+ if (!clone.getAttribute('xmlns')) {
136
+ clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
137
+ }
138
+ const svgText = new XMLSerializer().serializeToString(clone);
139
+ const blob = new Blob([svgText], {type: 'image/svg+xml;charset=utf-8'});
140
+ downloadBlob(blob, 'decision-tree.svg');
141
+ }
142
+
143
+ function exportPNG() {
144
+ const svgNode = document.querySelector('#tree-container svg');
145
+ if (!svgNode) return alert('No SVG found to export');
146
+
147
+ const clone = svgNode.cloneNode(true);
148
+ const bounds = getFullTreeBounds();
149
+ clone.setAttribute('viewBox', `${bounds.xMin} ${bounds.yMin} ${bounds.xMax - bounds.xMin} ${bounds.yMax - bounds.yMin}`);
150
+
151
+ const viewport = clone.querySelector('g.dg-viewport');
152
+ if (viewport) viewport.removeAttribute('transform');
153
+ const content = viewport ? viewport.querySelector('g.dg-content') : null;
154
+ if (content) content.removeAttribute('transform');
155
+
156
+ const cssVarNames = ['--bg', '--panel-bg', '--text-color', '--header-bg', '--header-text', '--tabs-bg', '--tab-active-bg', '--tab-active-text', '--muted-border'];
157
+ const comp = getComputedStyle(document.documentElement);
158
+ let varCss = ':root {';
159
+ cssVarNames.forEach(name => {
160
+ const v = comp.getPropertyValue(name).trim();
161
+ if (v) varCss += `${name}: ${v};`;
162
+ });
163
+ varCss += '}\n';
164
+ const cssText = varCss + getAllCSS();
165
+ const styleEl = document.createElement('style');
166
+ styleEl.setAttribute('type', 'text/css');
167
+ styleEl.innerHTML = cssText;
168
+ clone.insertBefore(styleEl, clone.firstChild);
169
+ if (!clone.getAttribute('xmlns')) {
170
+ clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
171
+ }
172
+ const svgText = new XMLSerializer().serializeToString(clone);
173
+ const svgBlob = new Blob([svgText], {type: 'image/svg+xml'});
174
+ const url = URL.createObjectURL(svgBlob);
175
+ const img = new Image();
176
+ img.onload = () => {
177
+ try {
178
+ const w = bounds.xMax - bounds.xMin;
179
+ const h = bounds.yMax - bounds.yMin;
180
+ const scale = window.devicePixelRatio || 1;
181
+ const canvas = document.createElement('canvas');
182
+ canvas.width = Math.round(w * scale);
183
+ canvas.height = Math.round(h * scale);
184
+ const ctx = canvas.getContext('2d');
185
+ ctx.setTransform(scale, 0, 0, scale, 0, 0);
186
+ const comp = getComputedStyle(document.documentElement);
187
+ const panelBg = comp.getPropertyValue('--panel-bg') || '#ffffff';
188
+ ctx.fillStyle = panelBg.trim() || '#ffffff';
189
+ ctx.fillRect(0, 0, w, h);
190
+ ctx.drawImage(img, 0, 0, w, h);
191
+ canvas.toBlob((blob) => {
192
+ if (blob) downloadBlob(blob, 'decision-tree.png');
193
+ }, 'image/png');
194
+ } finally {
195
+ URL.revokeObjectURL(url);
196
+ }
197
+ };
198
+ img.onerror = (e) => {
199
+ URL.revokeObjectURL(url);
200
+ alert('Failed to render SVG to PNG');
201
+ };
202
+ img.src = url;
203
+ }
204
+
205
+ function addExportButtons() {
206
+ const controlsBar = d3.select('#controls-bar');
207
+ if (!controlsBar.empty() && controlsBar.select('.right-controls').empty()) {
208
+ const wrap = controlsBar.append('div').attr('class', 'right-controls');
209
+ wrap.append('button').attr('id', 'export-png-btn').attr('class', 'control-button').text('Export PNG').on('click', exportPNG);
210
+ wrap.append('button').attr('id', 'export-svg-btn').attr('class', 'control-button').text('Export SVG').on('click', exportSVG);
211
+ wrap.append('button').attr('id', 'theme-toggle-btn').attr('class', 'control-button').text(getCurrentTheme() === 'dark' ? 'Light' : 'Dark').on('click', () => {
212
+ toggleTheme();
213
+ const btn = document.getElementById('theme-toggle-btn');
214
+ if (btn) btn.textContent = getCurrentTheme() === 'dark' ? 'Light' : 'Dark';
215
+ });
216
+ }
217
+ }
218
+
219
+ function applyTheme(theme) {
220
+ const root = document.documentElement;
221
+ if (theme === 'dark') root.classList.add('dark-mode'); else root.classList.remove('dark-mode');
222
+ }
223
+
224
+ function getCurrentTheme() {
225
+ return localStorage.getItem('dg-theme') || 'light';
226
+ }
227
+
228
+ function toggleTheme() {
229
+ const cur = getCurrentTheme();
230
+ const next = cur === 'dark' ? 'light' : 'dark';
231
+ localStorage.setItem('dg-theme', next);
232
+ applyTheme(next);
233
+ }
234
+
235
+ function initTheme() {
236
+ const stored = getCurrentTheme();
237
+ applyTheme(stored);
238
+ }
239
+
240
+ function buildNodeMap(root) {
241
+ const nodeMap = new Map();
242
+ root.each(d => nodeMap.set(d.data.id, d));
243
+ return nodeMap;
244
+ }
245
+
246
+ function buildVirtualLinks(virtualLinkDefs, nodeMap) {
247
+ return virtualLinkDefs
248
+ .map(link => {
249
+ const src = nodeMap.get(link.source);
250
+ const tgt = nodeMap.get(link.target);
251
+ return src && tgt ? {
252
+ source: src, target: tgt, condition: "virtual", condition_type: "virtual", type: link.type
253
+ } : null;
254
+ })
255
+ .filter(Boolean);
256
+ }
257
+
258
+ function measureNodeBBox(nodeData) {
259
+ let svg = document.getElementById('dg-measure-svg');
260
+ if (!svg) {
261
+ svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
262
+ svg.setAttribute('id', 'dg-measure-svg');
263
+ svg.setAttribute('style', 'position:absolute; left:-9999px; top:-9999px; width:0; height:0; overflow:visible;');
264
+ document.body.appendChild(svg);
265
+ }
266
+ if (svg._dg_node_group) {
267
+ svg.removeChild(svg._dg_node_group);
268
+ svg._dg_node_group = null;
269
+ }
270
+ const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
271
+ g.setAttribute('class', `node ${nodeData.type || ''}`);
272
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
273
+ rect.setAttribute('class', 'node-rect');
274
+ rect.setAttribute('rx', 6);
275
+ rect.setAttribute('ry', 6);
276
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
277
+ text.setAttribute('class', 'node-text');
278
+ text.setAttribute('text-anchor', 'middle');
279
+ text.setAttribute('dy', '0.35em');
280
+ text.textContent = nodeData.name || nodeData.id || 'unnamed';
281
+ g.appendChild(text);
282
+ svg.appendChild(g);
283
+ const textBBox = text.getBBox();
284
+ const pad = 8;
285
+ const w = Math.max(textBBox.width + pad, 40);
286
+ const h = Math.max(textBBox.height + pad, 16);
287
+ rect.setAttribute('x', -w / 2);
288
+ rect.setAttribute('y', -h / 2);
289
+ rect.setAttribute('width', w);
290
+ rect.setAttribute('height', h);
291
+ g.insertBefore(rect, text);
292
+ const groupBBox = g.getBBox();
293
+ svg.removeChild(g);
294
+ svg._dg_node_group = null;
295
+ return {width: groupBBox.width, height: groupBBox.height};
296
+ }
297
+
298
+ function updateTreeLayout(root) {
299
+ let nodeBBoxes = [];
300
+ let nodeHeights = [];
301
+ root.each(d => {
302
+ const bbox = measureNodeBBox({
303
+ name: d.data.name, id: d.data.id, type: d.data.type, depth: d.depth
304
+ });
305
+ d.data._nodeBBox = bbox;
306
+ nodeBBoxes.push(bbox.width);
307
+ nodeHeights.push(bbox.height);
308
+ });
309
+ const avgWidth = nodeBBoxes.length ? Math.round(nodeBBoxes.reduce((a, b) => a + b, 0) / nodeBBoxes.length) : 80;
310
+ const avgHeight = nodeHeights.length ? Math.round(nodeHeights.reduce((a, b) => a + b, 0) / nodeHeights.length) : 32;
311
+
312
+ if (GLOBAL_HORIZONTAL_SPACING_BASE === null) {
313
+ GLOBAL_HORIZONTAL_SPACING_BASE = Math.max(50, Math.min(400, avgWidth + 40));
314
+ GLOBAL_VERTICAL_SPACING = avgHeight * ROW_VERTICAL_SPACE_FACTOR;
315
+ }
316
+ const treeLayout = d3.tree()
317
+ .nodeSize([GLOBAL_HORIZONTAL_SPACING_BASE, 1])
318
+ .separation((a, b) => {
319
+ const wa = (a && a.data && a.data._nodeBBox) ? a.data._nodeBBox.width : avgWidth;
320
+ const wb = (b && b.data && b.data._nodeBBox) ? b.data._nodeBBox.width : avgWidth;
321
+ const desiredPx = (wa + wb) / 2 + 20;
322
+ const factor = desiredPx / GLOBAL_HORIZONTAL_SPACING_BASE;
323
+ return (a.parent === b.parent) ? Math.max(0.5, factor) : Math.max(2, factor * 1.2);
324
+ });
325
+ treeLayout(root);
326
+ root.each(d => {
327
+ d.y = d.depth * GLOBAL_VERTICAL_SPACING;
328
+ });
329
+ }
330
+
331
+ function updateVisualization(root, g, virtualLinkDefs, nodeMap, animate = true) {
332
+ const nodes = root.descendants();
333
+
334
+ const nodeSelection = g.selectAll("g.node").data(nodes, d => d.data.id);
335
+ const nodeEnter = nodeSelection.enter().append("g")
336
+ .attr("class", d => `node ${d.data.type}`)
337
+ .attr("data-id", d => d.data.id)
338
+ .attr("transform", d => `translate(${d.x0},${d.y0})`)
339
+ .on("click", toggleChildren)
340
+ .on("mouseover", showNodeInfo)
341
+ .on("mouseout", hideNodeInfo);
342
+
343
+ nodeEnter.append("rect")
344
+ .attr("class", "node-rect")
345
+ .attr("rx", 6)
346
+ .attr("ry", 6);
347
+
348
+ nodeEnter.append("text")
349
+ .attr("class", "node-text")
350
+ .attr("text-anchor", "middle")
351
+ .attr("dy", "0.35em");
352
+
353
+ const nodeUpdate = nodeSelection.merge(nodeEnter);
354
+
355
+ nodeUpdate.select("text.node-text")
356
+ .text(d => d.data.name || d.data.id || "unnamed");
357
+
358
+ nodeUpdate.each(function (d) {
359
+ const text = d3.select(this).select("text").node();
360
+ if (!text) return;
361
+ let w, h;
362
+ const bbox = text.getBBox();
363
+ const pad = 8;
364
+ w = Math.max(bbox.width + pad, 40);
365
+ h = Math.max(bbox.height + pad, 16);
366
+ d3.select(this).select("rect")
367
+ .attr("x", -w / 2)
368
+ .attr("y", -h / 2)
369
+ .attr("width", w)
370
+ .attr("height", h);
371
+ });
372
+
373
+ const highlightToggle = document.getElementById('highlight-toggle');
374
+ const shouldDim = highlightToggle ? highlightToggle.checked : false;
375
+ nodeUpdate.select("rect.node-rect")
376
+ .classed("node-rect-inactive", d => shouldDim && d.data.activated === false);
377
+
378
+ nodeUpdate.select("text.node-text")
379
+ .classed("node-text-inactive", d => shouldDim && d.data.activated === false);
380
+
381
+ if (animate) {
382
+ nodeUpdate.transition().delay(ANIM_FAST).duration(ANIM_SLOW)
383
+ .attr("transform", d => `translate(${d.x},${d.y})`);
384
+ } else {
385
+ nodeUpdate.attr("transform", d => `translate(${d.x},${d.y})`);
386
+ }
387
+
388
+ if (animate) {
389
+ nodeSelection.exit().transition().delay(ANIM_FAST).duration(ANIM_SLOW)
390
+ .attr("transform", d => {
391
+ const parent = d.parent || d;
392
+ return `translate(${parent.x},${parent.y})`;
393
+ })
394
+ .style("opacity", 0)
395
+ .remove();
396
+ } else {
397
+ nodeSelection.exit().remove();
398
+ }
399
+
400
+ const parentChildLinks = [];
401
+ root.each(d => {
402
+ if (d.children) {
403
+ d.children.forEach(child => {
404
+ const isLinkActivated = (d.data.activated !== false) && (child.data.activated !== false);
405
+ parentChildLinks.push({
406
+ source: d, target: child, condition: child.data.condition_to_child || "", condition_type: child.data.condition_type || "default", activated: isLinkActivated
407
+ });
408
+ });
409
+ }
410
+ });
411
+
412
+ const virtualLinks = buildVirtualLinks(virtualLinkDefs, nodeMap).map(link => {
413
+ const srcActivated = link.source.data.activated !== false;
414
+ const tgtActivated = link.target.data.activated !== false;
415
+ return {
416
+ ...link, activated: srcActivated && tgtActivated
417
+ };
418
+ });
419
+
420
+ const allLinks = [...parentChildLinks, ...virtualLinks];
421
+
422
+ const linkSelection = g.selectAll("path.link").data(allLinks, d => `${d.source.data.id}-${d.target.data.id}`);
423
+ const linkEnter = linkSelection.enter().insert("path", "g")
424
+ .attr("class", "link")
425
+ .attr("fill", "none")
426
+ .attr("stroke", d => d.type === "virtual_parent" ? "red" : "gray")
427
+ .attr("stroke-width", 1)
428
+ .attr("stroke-dasharray", d => d.type === "virtual_parent" ? "5,5" : null)
429
+ .attr("opacity", 0);
430
+
431
+ const linkUpdate = linkSelection.merge(linkEnter);
432
+ linkUpdate.classed("link-inactive", d => shouldDim && d.activated === false);
433
+
434
+ const linkGenerator = d3.linkVertical().x(d => d.x).y(d => d.y);
435
+ if (animate) {
436
+ linkUpdate.transition().duration(ANIM_FAST)
437
+ .attr("d", d => linkGenerator({source: d.source, target: d.target}))
438
+ .attr("opacity", 1);
439
+ } else {
440
+ linkUpdate.attr("d", d => linkGenerator({source: d.source, target: d.target})).attr("opacity", 1);
441
+ }
442
+
443
+ if (animate) {
444
+ linkSelection.exit().transition().duration(ANIM_FAST)
445
+ .attr("opacity", 0)
446
+ .remove();
447
+ } else {
448
+ linkSelection.exit().remove();
449
+ }
450
+
451
+ const labelSelection = g.selectAll("g.link-condition-group").data(allLinks, d => `${d.source.data.id}-${d.target.data.id}`);
452
+ const labelEnter = labelSelection.enter().append("g")
453
+ .attr("class", d => `link-condition-group ${d.condition_type || "default"}`)
454
+ .attr("data-link", d => `${d.source.data.id}-${d.target.data.id}`)
455
+ .style("opacity", 0);
456
+
457
+ labelEnter.append("rect")
458
+ .attr("class", "link-condition-bg")
459
+ .attr("rx", 4)
460
+ .attr("ry", 4);
461
+
462
+ labelEnter.append("text").attr("class", "link-condition");
463
+
464
+ const labelUpdate = labelSelection.merge(labelEnter);
465
+ labelUpdate.select("text.link-condition")
466
+ .text(d => d.condition || "")
467
+ .attr("text-anchor", "middle")
468
+ .attr("dominant-baseline", "middle")
469
+ .attr("font-size", "10px")
470
+ .attr("pointer-events", "none");
471
+
472
+ labelUpdate.each(function (d) {
473
+ const text = d3.select(this).select("text").node();
474
+ if (!text) return;
475
+ const bbox = text.getBBox();
476
+ const pad = 6;
477
+ const w = bbox.width + pad;
478
+ const h = bbox.height + pad;
479
+ d3.select(this).select("rect.link-condition-bg")
480
+ .attr("x", -w / 2)
481
+ .attr("y", -h / 2)
482
+ .attr("width", w)
483
+ .attr("height", h);
484
+ const midX = (d.source.x + d.target.x) / 2;
485
+ const midY = (d.source.y + d.target.y) / 2;
486
+ d3.select(this).attr("transform", `translate(${midX},${midY})`);
487
+ });
488
+
489
+ labelUpdate.select("rect.link-condition-bg")
490
+ .classed("link-condition-bg-inactive", d => shouldDim && d.activated === false);
491
+
492
+ labelUpdate.select("text.link-condition")
493
+ .classed("link-condition-text-inactive", d => shouldDim && d.activated === false);
494
+
495
+ if (animate) {
496
+ labelUpdate.transition().duration(ANIM_FAST).style("opacity", 1);
497
+ labelSelection.exit().transition().duration(ANIM_FAST).style("opacity", 0).remove();
498
+ } else {
499
+ labelUpdate.style("opacity", 1);
500
+ labelSelection.exit().remove();
501
+ }
502
+
503
+ nodes.forEach(n => {
504
+ n.x0 = n.x;
505
+ n.y0 = n.y;
506
+ });
507
+ }
508
+
509
+ function toggleChildren(event, d) {
510
+ event.stopPropagation();
511
+
512
+ if (d.children) {
513
+ d._children = d.children;
514
+ d.children = null;
515
+ } else if (d._children) {
516
+ d.children = d._children;
517
+ d._children = null;
518
+ } else {
519
+ return;
520
+ }
521
+
522
+ const container = d3.select("#tree-container");
523
+ const g = container.select("svg").select("g.dg-viewport").select("g.dg-content");
524
+ const nodeMap = new Map();
525
+ GLOBAL_TREE_ROOT.each(n => nodeMap.set(n.data.id, n));
526
+ const virtualLinkDefs = GLOBAL_VIRTUAL_LINK_DEFS || [];
527
+ updateVisualization(GLOBAL_TREE_ROOT, g, virtualLinkDefs, nodeMap, true);
528
+ }
529
+
530
+ function showNodeInfo(event, d) {
531
+ const info = d.data;
532
+ d3.select("#info-id").text(info.id || "N/A");
533
+ d3.select("#info-name").text(info.name || "N/A");
534
+ d3.select("#info-repr").text(info.repr || "N/A");
535
+ d3.select("#info-type").text(info.type || "N/A");
536
+ d3.select("#info-labels").text(Array.isArray(info.labels) ? info.labels.join(", ") : String(info.labels || "N/A"));
537
+ const autogenElem = d3.select("#info-autogen");
538
+ autogenElem.classed("autogen-true", false).classed("autogen-false", false);
539
+ if (info.autogen === undefined) {
540
+ autogenElem.text("N/A");
541
+ } else if (info.autogen) {
542
+ autogenElem.text("True").classed("autogen-true", true);
543
+ } else {
544
+ autogenElem.text("False").classed("autogen-false", true);
545
+ }
546
+ let expr = "N/A";
547
+ if (info.expression !== undefined) {
548
+ expr = info.expression;
549
+ } else if (info.condition_to_child) {
550
+ expr = info.condition_to_child;
551
+ }
552
+ d3.select("#info-expr").text(expr);
553
+
554
+ const panel = d3.select("#node-info");
555
+ panel.style("display", "block");
556
+ nodeInfoCurrentNodeId = info.id;
557
+
558
+ if (!nodeInfoPinned && !nodeInfoDragging && !isMouseOverNodeInfo()) {
559
+ const mouseX = event.pageX;
560
+ const mouseY = event.pageY;
561
+ const panelNode = panel.node();
562
+ const panelWidth = panelNode.offsetWidth;
563
+ const panelHeight = panelNode.offsetHeight;
564
+ const container = document.getElementById('tree-container');
565
+ const containerRect = container.getBoundingClientRect();
566
+ let x = mouseX + 10;
567
+ let y = mouseY + 10;
568
+ if (x + panelWidth > containerRect.right || y + panelHeight > containerRect.bottom) {
569
+ x = containerRect.right - panelWidth - 10;
570
+ y = containerRect.top + 10;
571
+ }
572
+ panel.style("left", x + "px").style("top", y + "px").style("position", "fixed");
573
+ nodeInfoLastPos = {x, y};
574
+ }
575
+ }
576
+
577
+ function hideNodeInfo() {
578
+ if (nodeInfoPinned || isMouseOverNodeInfo() || nodeInfoDragging) return;
579
+ d3.select("#node-info").style("display", "none");
580
+ nodeInfoCurrentNodeId = null;
581
+ }
582
+
583
+ function isMouseOverNodeInfo() {
584
+ const panel = document.getElementById('node-info');
585
+ const rect = panel.getBoundingClientRect();
586
+ const x = window.event ? window.event.clientX : 0;
587
+ const y = window.event ? window.event.clientY : 0;
588
+ return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
589
+ }
590
+
591
+ function renderFilteredTree(include_all_children = false) {
592
+ const container = d3.select("#tree-container");
593
+ container.select("svg").remove();
594
+
595
+ if (!GLOBAL_TREE_DATA) return;
596
+
597
+ const group = GLOBAL_SELECTED_GROUP;
598
+ const treeData = GLOBAL_TREE_DATA;
599
+
600
+ function shouldInclude(node) {
601
+ if (group === "*") return true;
602
+ return node.labels && Array.isArray(node.labels) && node.labels.includes(group);
603
+ }
604
+
605
+ function cloneAndFilter(node, includeSelf) {
606
+ const include = includeSelf || shouldInclude(node);
607
+ const copy = {...node};
608
+
609
+ if (node._children && node._children.length > 0) {
610
+ const filteredChildren = node._children
611
+ .map(child => include_all_children ? cloneAndFilter(child, include || shouldInclude(child)) : cloneAndFilter(child, false))
612
+ .filter(Boolean);
613
+
614
+ if (filteredChildren.length > 0) {
615
+ copy._children = filteredChildren;
616
+ copy.children = copy._children;
617
+ } else {
618
+ copy._children = [];
619
+ copy.children = null;
620
+ }
621
+ } else {
622
+ copy._children = [];
623
+ copy.children = null;
624
+ }
625
+
626
+ return include || (copy.children && copy.children.length > 0) ? copy : null;
627
+ }
628
+
629
+ const filteredRoot = cloneAndFilter(treeData.root, shouldInclude(treeData.root));
630
+ if (!filteredRoot) {
631
+ container.append("div").text("No nodes match the selected logic group.");
632
+ return;
633
+ }
634
+
635
+ const svg = container.append("svg");
636
+ const viewport = svg.append("g").attr("class", "dg-viewport");
637
+ const g = viewport.append("g").attr("class", "dg-content");
638
+
639
+ const root = d3.hierarchy(filteredRoot, d => d.children);
640
+ GLOBAL_TREE_ROOT = root;
641
+
642
+ const nodeMap = buildNodeMap(root);
643
+
644
+ updateTreeLayout(root);
645
+
646
+ const zoom = d3.zoom()
647
+ .scaleExtent([0.0, Number.POSITIVE_INFINITY])
648
+ .on("zoom", (event) => {
649
+ viewport.attr("transform", event.transform);
650
+ });
651
+
652
+ svg.call(zoom);
653
+
654
+ const cw = container.node() ? container.node().clientWidth : null;
655
+ const ch = container.node() ? container.node().clientHeight : null;
656
+ if (cw && ch) {
657
+ const rootX = typeof root.x === 'number' ? root.x : 0;
658
+ const rootY = typeof root.y === 'number' ? root.y : 0;
659
+ const tx = (cw / 2) - rootX;
660
+ const ty = ((ch * 0.1) - rootY);
661
+ const initialTransform = d3.zoomIdentity.translate(tx, ty).scale(1);
662
+ svg.call(zoom.transform, initialTransform);
663
+ }
664
+
665
+ root.each(d => {
666
+ d.x0 = d.x;
667
+ d.y0 = d.y;
668
+ });
669
+
670
+ updateVisualization(root, g, GLOBAL_VIRTUAL_LINK_DEFS, nodeMap, false);
671
+ }
672
+
673
+ function applyActivationDiff(diff) {
674
+ diff.added.forEach(function (id) {
675
+ let rectSel = d3.select(`g.node[data-id='${id}'] rect.node-rect`);
676
+ let textSel = d3.select(`g.node[data-id='${id}'] text.node-text`);
677
+ if (!rectSel.empty()) {
678
+ rectSel.classed('node-rect-inactive', false);
679
+ }
680
+ if (!textSel.empty()) {
681
+ textSel.classed('node-text-inactive', false);
682
+ }
683
+ });
684
+ diff.removed.forEach(function (id) {
685
+ let rectSel = d3.select(`g.node[data-id='${id}'] rect.node-rect`);
686
+ let textSel = d3.select(`g.node[data-id='${id}'] text.node-text`);
687
+ if (!rectSel.empty()) {
688
+ rectSel.classed('node-rect-inactive', true);
689
+ }
690
+ if (!textSel.empty()) {
691
+ textSel.classed('node-text-inactive', true);
692
+ }
693
+ });
694
+
695
+ d3.selectAll('path.link').each(function (d) {
696
+ if (!d || !d.source || !d.target) return;
697
+ let srcRect = d3.select(`g.node[data-id='${d.source.data.id}'] rect.node-rect`);
698
+ let tgtRect = d3.select(`g.node[data-id='${d.target.data.id}'] rect.node-rect`);
699
+ const srcActive = !srcRect.empty() && !srcRect.classed('node-rect-inactive');
700
+ const tgtActive = !tgtRect.empty() && !tgtRect.classed('node-rect-inactive');
701
+ const linkActive = srcActive && tgtActive;
702
+ d3.select(this).classed('link-inactive', !linkActive);
703
+ d3.select(this).classed('link-active', linkActive);
704
+
705
+ let labelGroup = d3.select(`g.link-condition-group[data-link='${d.source.data.id}-${d.target.data.id}']`);
706
+ let labelBg = labelGroup.select('rect.link-condition-bg');
707
+ let labelText = labelGroup.select('text.link-condition');
708
+ if (!labelBg.empty()) {
709
+ labelBg.classed('link-condition-bg-inactive', !linkActive);
710
+ }
711
+ if (!labelText.empty()) {
712
+ labelText.classed('link-condition-text-inactive', !linkActive);
713
+ }
714
+ });
715
+ }
716
+
717
+ function updateHighlightClasses() {
718
+ const highlightToggle = document.getElementById('highlight-toggle');
719
+ const shouldDim = highlightToggle ? highlightToggle.checked : false;
720
+ LAST_ACTIVE_IDS.length = 0;
721
+ d3.selectAll('g.node').each(function (d) {
722
+ if (d && d.data) {
723
+ const rectSel = d3.select(this).select('rect.node-rect');
724
+ const textSel = d3.select(this).select('text.node-text');
725
+ if (d.data.activated === false) {
726
+ rectSel.classed('node-rect-inactive', true);
727
+ textSel.classed('node-text-inactive', true);
728
+ } else {
729
+ rectSel.classed('node-rect-inactive', false);
730
+ textSel.classed('node-text-inactive', false);
731
+ LAST_ACTIVE_IDS.push(d.data.id);
732
+ }
733
+ }
734
+ });
735
+
736
+ d3.selectAll('rect.node-rect')
737
+ .classed('node-rect-inactive', function () {
738
+ const d = d3.select(this.parentNode).datum();
739
+ if (!d || !d.data) return false;
740
+ return shouldDim && d.data.activated === false;
741
+ });
742
+ d3.selectAll('text.node-text')
743
+ .classed('node-text-inactive', function () {
744
+ const d = d3.select(this.parentNode).datum();
745
+ if (!d || !d.data) return false;
746
+ return shouldDim && d.data.activated === false;
747
+ });
748
+ d3.selectAll('path.link')
749
+ .classed('link-inactive', function () {
750
+ const d = d3.select(this).datum();
751
+ if (!d) return false;
752
+ return shouldDim && d.activated === false;
753
+ })
754
+ .classed('link-active', function () {
755
+ const d = d3.select(this).datum();
756
+ if (!d) return false;
757
+ return shouldDim && d.activated !== false;
758
+ });
759
+ d3.selectAll('rect.link-condition-bg')
760
+ .classed('link-condition-bg-inactive', function () {
761
+ const d = d3.select(this.parentNode).datum();
762
+ if (!d) return false;
763
+ return shouldDim && d.activated === false;
764
+ });
765
+ d3.selectAll('text.link-condition')
766
+ .classed('link-condition-text-inactive', function () {
767
+ const d = d3.select(this.parentNode).datum();
768
+ if (!d) return false;
769
+ return shouldDim && d.activated === false;
770
+ });
771
+ }
772
+
773
+ function pollActiveNodes() {
774
+ const highlightToggle = document.getElementById('highlight-toggle');
775
+ const watchToggle = document.getElementById('watch-toggle');
776
+ if ((highlightToggle && !highlightToggle.checked) || (watchToggle && !watchToggle.checked)) {
777
+
778
+ return;
779
+ }
780
+ fetch('/api/active_nodes')
781
+ .then(response => response.json())
782
+ .then(data => {
783
+ if (!data || !Array.isArray(data.active_ids)) return;
784
+ const newActive = data.active_ids;
785
+ const added = newActive.filter(id => !LAST_ACTIVE_IDS.includes(id));
786
+ const removed = LAST_ACTIVE_IDS.filter(id => !newActive.includes(id));
787
+ if (added.length > 0 || removed.length > 0) {
788
+ applyActivationDiff({added, removed});
789
+ }
790
+ LAST_ACTIVE_IDS = newActive;
791
+ })
792
+ .catch(err => {
793
+ console.warn('Failed to fetch active nodes:', err);
794
+ });
795
+ }
796
+
797
+ function setActiveIds(ids) {
798
+ if (!Array.isArray(ids)) return;
799
+ const idSet = new Set(ids.map(String));
800
+
801
+ // Update the underlying GLOBAL_TREE_DATA structure (if present)
802
+ function recurseUpdate(node) {
803
+ if (!node || !node.id) return;
804
+ if (idSet.has(String(node.id))) {
805
+ // mark as active: remove explicit false flag
806
+ if (node.hasOwnProperty('activated')) delete node.activated;
807
+ } else {
808
+ node.activated = false;
809
+ }
810
+ if (node._children && Array.isArray(node._children)) node._children.forEach(recurseUpdate);
811
+ if (node.children && Array.isArray(node.children)) node.children.forEach(recurseUpdate);
812
+ }
813
+
814
+ if (GLOBAL_TREE_DATA && GLOBAL_TREE_DATA.root) {
815
+ recurseUpdate(GLOBAL_TREE_DATA.root);
816
+ }
817
+
818
+ // Update currently rendered nodes' bound data (d.data) so UI reflects change
819
+ try {
820
+ d3.selectAll('g.node').each(function (d) {
821
+ if (!d || !d.data || !d.data.id) return;
822
+ if (idSet.has(String(d.data.id))) {
823
+ if (d.data.hasOwnProperty('activated')) delete d.data.activated;
824
+ } else {
825
+ d.data.activated = false;
826
+ }
827
+ });
828
+ } catch (e) {
829
+ console.warn('Failed to update rendered node data:', e);
830
+ }
831
+
832
+ // Refresh highlight classes
833
+ try {
834
+ updateHighlightClasses();
835
+ } catch (e) {
836
+ console.warn('Failed to update highlight classes:', e);
837
+ }
838
+ }
839
+
840
+ async function from_clipboard() {
841
+ const inputEl = document.getElementById('from-clipboard-input');
842
+ let raw = inputEl && inputEl.value ? inputEl.value.trim() : '';
843
+
844
+ if (!raw) {
845
+ if (!navigator.clipboard || !navigator.clipboard.readText) {
846
+ alert('Clipboard API not available and input is empty');
847
+ return;
848
+ }
849
+ try {
850
+ raw = await navigator.clipboard.readText();
851
+ } catch (e) {
852
+ alert('Failed to read clipboard: ' + (e && e.message ? e.message : e));
853
+ return;
854
+ }
855
+ }
856
+
857
+ let parsed;
858
+ try {
859
+ parsed = JSON.parse(raw);
860
+ } catch (e) {
861
+ alert('Failed to parse JSON from input/clipboard: ' + (e && e.message ? e.message : e));
862
+ return;
863
+ }
864
+
865
+ if (!Array.isArray(parsed)) {
866
+ alert('Parsed content is not a JSON array of UIDs');
867
+ return;
868
+ }
869
+
870
+ // Apply the active ids
871
+ try {
872
+ setActiveIds(parsed);
873
+ } catch (e) {
874
+ console.error('Error applying active ids:', e);
875
+ }
876
+ }
877
+
878
+ if (typeof window.with_eval !== 'undefined' && window.with_eval) {
879
+ document.addEventListener('DOMContentLoaded', function () {
880
+ var controlsBar = document.getElementById('controls-bar');
881
+
882
+ if (controlsBar && !document.getElementById('from-clipboard-control')) {
883
+ var fc = document.createElement('div');
884
+ fc.id = 'from-clipboard-control';
885
+ fc.innerHTML = `
886
+ <button id="from-clipboard-btn" class="control-button from-clipboard-btn">Load Active</button>
887
+ <input id="from-clipboard-input" class="from-clipboard-input" type="text" placeholder='["uid1","uid2",...]'>
888
+ `;
889
+ controlsBar.prepend(fc);
890
+
891
+ // Wire up events after element creation
892
+ var inputEl = document.getElementById('from-clipboard-input');
893
+ var btn = document.getElementById('from-clipboard-btn');
894
+ if (btn) {
895
+ btn.addEventListener('click', function () {
896
+ // call the async function; ignore returned promise
897
+ from_clipboard();
898
+ });
899
+ }
900
+ if (inputEl) {
901
+ inputEl.addEventListener('keydown', function (e) {
902
+ if (e.key === 'Enter') {
903
+ e.preventDefault();
904
+ from_clipboard();
905
+ }
906
+ });
907
+ }
908
+ }
909
+
910
+ if (controlsBar && !document.getElementById('highlight-control')) {
911
+ var div = document.createElement('div');
912
+ div.id = 'highlight-control';
913
+ div.innerHTML = `<label>
914
+ <span>Highlight Active Path</span>
915
+ <input type="checkbox" id="highlight-toggle" checked>
916
+ <span class="highlight-switch-slider"></span>
917
+ </label>`;
918
+ controlsBar.prepend(div);
919
+ }
920
+ });
921
+ }
922
+
923
+ if (typeof window.with_watch !== 'undefined' && window.with_watch) {
924
+ document.addEventListener('DOMContentLoaded', function () {
925
+ var controlsBar = document.getElementById('controls-bar');
926
+ if (controlsBar && !document.getElementById('watch-control')) {
927
+ var div = document.createElement('div');
928
+ div.id = 'watch-control';
929
+ div.innerHTML = `<label>
930
+ <span>Watch Active Path</span>
931
+ <input type="checkbox" id="watch-toggle" checked>
932
+ <span class="watch-switch-slider"></span>
933
+ </label>`;
934
+ controlsBar.prepend(div);
935
+ }
936
+ });
937
+
938
+ setInterval(pollActiveNodes, 500);
939
+ }
940
+
941
+ document.addEventListener('DOMContentLoaded', function () {
942
+ const highlight_toggle = document.getElementById('highlight-toggle');
943
+ if (highlight_toggle) {
944
+ highlight_toggle.addEventListener('change', function () {
945
+ updateHighlightClasses();
946
+ });
947
+ }
948
+
949
+ updateHighlightClasses();
950
+
951
+ initTheme();
952
+ });
953
+
954
+ document.addEventListener('keydown', function (e) {
955
+ if (e.code === 'Space' || e.key === ' ') {
956
+ const nodeInfoPanel = document.getElementById('node-info');
957
+ const pinBtn = document.getElementById('node-info-pin');
958
+ if (nodeInfoPanel) {
959
+ if (nodeInfoPinned) {
960
+ nodeInfoPinned = false;
961
+ if (pinBtn) {
962
+ pinBtn.classList.remove('active');
963
+ pinBtn.setAttribute('title', 'Pin');
964
+ }
965
+ nodeInfoPanel.style.display = 'none';
966
+ } else {
967
+ nodeInfoPanel.style.display = 'block';
968
+ nodeInfoPinned = true;
969
+ if (pinBtn) {
970
+ pinBtn.classList.add('active');
971
+ pinBtn.setAttribute('title', 'Unpin');
972
+ }
973
+ }
974
+ }
975
+ e.preventDefault();
976
+ }
977
+ });
978
+
979
+ if (nodeInfoPinBtn) {
980
+ nodeInfoPinBtn.addEventListener('click', function (e) {
981
+ nodeInfoPinned = !nodeInfoPinned;
982
+ this.classList.toggle('active', nodeInfoPinned);
983
+ if (nodeInfoPinned) {
984
+ this.setAttribute('title', 'Unpin');
985
+ } else {
986
+ this.setAttribute('title', 'Pin');
987
+ }
988
+ });
989
+ }
990
+
991
+ if (nodeInfoPanel) {
992
+ nodeInfoPanel.addEventListener('mousedown', function (e) {
993
+ if (e.target.id === 'node-info-pin') return;
994
+ nodeInfoDragging = true;
995
+ nodeInfoPanel.classList.add('dragging');
996
+ nodeInfoOffset.x = e.clientX - nodeInfoPanel.getBoundingClientRect().left;
997
+ nodeInfoOffset.y = e.clientY - nodeInfoPanel.getBoundingClientRect().top;
998
+ document.body.style.userSelect = 'none';
999
+ });
1000
+ document.addEventListener('mousemove', function (e) {
1001
+ if (!nodeInfoDragging) return;
1002
+ let x = e.clientX - nodeInfoOffset.x;
1003
+ let y = e.clientY - nodeInfoOffset.y;
1004
+ nodeInfoPanel.style.left = x + 'px';
1005
+ nodeInfoPanel.style.top = y + 'px';
1006
+ nodeInfoLastPos = {x, y};
1007
+ });
1008
+ document.addEventListener('mouseup', function (e) {
1009
+ if (nodeInfoDragging) {
1010
+ nodeInfoDragging = false;
1011
+ nodeInfoPanel.classList.remove('dragging');
1012
+ document.body.style.userSelect = '';
1013
+ }
1014
+ });
1015
+ nodeInfoPanel.addEventListener('mouseenter', function (e) {
1016
+ if (nodeInfoCurrentNodeId) {
1017
+ d3.select('#node-info').style('display', 'block');
1018
+ }
1019
+ });
1020
+ nodeInfoPanel.addEventListener('mouseleave', function (e) {
1021
+ if (!nodeInfoPinned && !nodeInfoDragging) {
1022
+ d3.select('#node-info').style('display', 'none');
1023
+ nodeInfoCurrentNodeId = null;
1024
+ }
1025
+ });
1026
+ }