sayou-visualizer 0.0.9__py3-none-any.whl → 0.0.11__py3-none-any.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.
@@ -1,6 +1,7 @@
1
1
  from sayou.core.base_component import BaseComponent
2
2
 
3
- from .renderer.kg_renderer import KGRenderer
3
+ from .renderer.analytic_kg_renderer import AnalyticKGRenderer
4
+ from .renderer.showcase_kg_renderer import ShowcaseKGRenderer
4
5
  from .renderer.pyvis_renderer import PyVisRenderer
5
6
  from .tracer.graph_tracer import GraphTracer
6
7
  from .tracer.rich_tracer import RichConsoleTracer
@@ -81,11 +82,20 @@ class VisualizerPipeline(BaseComponent):
81
82
  self._renderer = PyVisRenderer()
82
83
  self._renderer.render(self._graph_tracer.graph, output_path, **kwargs)
83
84
 
84
- def render_kg(self, json_path: str, output_path: str = "kg_view.html"):
85
+ def render_analytic_kg(self, json_path: str, output_path: str = "kg_view.html"):
85
86
  """
86
87
  Visualizes the OUTPUT JSON (Knowledge Graph).
87
88
  """
88
- self._kg_renderer = KGRenderer()
89
+ self._kg_renderer = AnalyticKGRenderer()
90
+ self._kg_renderer.render(json_path, output_path)
91
+
92
+ def render_showcase_kg(
93
+ self, json_path: str, output_path: str = "showcase_kg_view.html"
94
+ ):
95
+ """
96
+ Visualizes the OUTPUT JSON (Knowledge Graph).
97
+ """
98
+ self._kg_renderer = ShowcaseKGRenderer()
89
99
  self._kg_renderer.render(json_path, output_path)
90
100
 
91
101
  def save_live_log(self, output_path="live_status.html"):
@@ -0,0 +1,453 @@
1
+ import json
2
+ import os
3
+
4
+ from sayou.core.base_component import BaseComponent
5
+
6
+
7
+ class AnalyticKGRenderer(BaseComponent):
8
+ """
9
+ Renders an interactive 2D Knowledge Graph optimized for deep analysis and topology exploration.
10
+
11
+ Agnostic to data domains (Code, Subtitles, Text), it visualizes structural relationships
12
+ using a 'Hub & Spoke' layout. Features interactive filtering, search capabilities,
13
+ and detailed node inspection (Visual IDE/Viewer) to derive insights from complex connections.
14
+ """
15
+
16
+ component_name = "AnalyticKGRenderer"
17
+
18
+ STYLE_SHEET = [
19
+ # [Global Nodes]
20
+ {
21
+ "selector": "node",
22
+ "style": {
23
+ "label": "data(label)",
24
+ "color": "#ecf0f1",
25
+ "font-size": "10px",
26
+ "text-valign": "center",
27
+ "text-halign": "center",
28
+ "text-wrap": "wrap",
29
+ "text-max-width": "100px",
30
+ "background-color": "#95a5a6",
31
+ "border-width": 1,
32
+ "border-color": "#7f8c8d",
33
+ },
34
+ },
35
+ # [File Node]
36
+ {
37
+ "selector": "node[type='file']",
38
+ "style": {
39
+ "shape": "rectangle",
40
+ "background-color": "#2c3e50",
41
+ "width": 60,
42
+ "height": 60,
43
+ "font-size": "12px",
44
+ "font-weight": "bold",
45
+ "border-width": 2,
46
+ "border-color": "#00d2d3",
47
+ },
48
+ },
49
+ # [Class Node]
50
+ {
51
+ "selector": "node[type='class']",
52
+ "style": {
53
+ "shape": "diamond",
54
+ "background-color": "#8e44ad",
55
+ "width": 40,
56
+ "height": 40,
57
+ },
58
+ },
59
+ # [Method/Function]
60
+ {
61
+ "selector": "node[type='method'], node[type='function']",
62
+ "style": {
63
+ "shape": "ellipse",
64
+ "background-color": "#e67e22",
65
+ "width": 25,
66
+ "height": 25,
67
+ },
68
+ },
69
+ # [Code Chunk]
70
+ {
71
+ "selector": "node[type='code_block']",
72
+ "style": {
73
+ "shape": "round-rectangle",
74
+ "background-color": "#7f8c8d",
75
+ "width": 15,
76
+ "height": 15,
77
+ "label": "",
78
+ },
79
+ },
80
+ # [Package/Library]
81
+ {
82
+ "selector": "node[type='library'], node[type='package']",
83
+ "style": {
84
+ "shape": "hexagon",
85
+ "background-color": "#16a085",
86
+ "width": 50,
87
+ "height": 50,
88
+ },
89
+ },
90
+ # [Edges]
91
+ {
92
+ "selector": "edge",
93
+ "style": {
94
+ "width": 1,
95
+ "curve-style": "bezier",
96
+ "opacity": 0.6,
97
+ "arrow-scale": 1,
98
+ },
99
+ },
100
+ # 1. Structure Line (contains) -> Gray Dashed Line (Skeleton)
101
+ {
102
+ "selector": "edge[edgeType='sayou:contains']",
103
+ "style": {
104
+ "line-color": "#7f8c8d",
105
+ "target-arrow-color": "#7f8c8d",
106
+ "target-arrow-shape": "circle",
107
+ "width": 1.5,
108
+ "line-style": "dashed",
109
+ "opacity": 0.7,
110
+ },
111
+ },
112
+ # 2. Logic Line (imports) -> Cyan Dashed Line (Flow)
113
+ {
114
+ "selector": "edge[edgeType='sayou:imports']",
115
+ "style": {
116
+ "line-color": "#00d2d3",
117
+ "target-arrow-color": "#00d2d3",
118
+ "target-arrow-shape": "triangle",
119
+ "line-style": "dashed",
120
+ "width": 2,
121
+ "opacity": 0.9,
122
+ },
123
+ },
124
+ # 3. Inheritance Line (inherits) -> Red Solid Line
125
+ {
126
+ "selector": "edge[edgeType='sayou:inherits']",
127
+ "style": {
128
+ "line-color": "#ff6b6b",
129
+ "target-arrow-color": "#ff6b6b",
130
+ "target-arrow-shape": "triangle",
131
+ "width": 3,
132
+ },
133
+ },
134
+ # [Interaction]
135
+ {
136
+ "selector": ".highlighted",
137
+ "style": {
138
+ "background-color": "#f1c40f",
139
+ "line-color": "#f1c40f",
140
+ "target-arrow-color": "#f1c40f",
141
+ "opacity": 1,
142
+ "z-index": 999,
143
+ },
144
+ },
145
+ {
146
+ "selector": ".faded",
147
+ "style": {"opacity": 0.05, "label": ""},
148
+ },
149
+ {
150
+ "selector": ".found",
151
+ "style": {
152
+ "border-width": 4,
153
+ "border-color": "#e056fd",
154
+ "background-color": "#e056fd",
155
+ },
156
+ },
157
+ {
158
+ "selector": "node.no-label",
159
+ "style": {
160
+ "text-opacity": 0,
161
+ "text-background-opacity": 0,
162
+ "text-border-opacity": 0,
163
+ },
164
+ },
165
+ {
166
+ "selector": "edge.hidden-edge",
167
+ "style": {"display": "none"},
168
+ },
169
+ ]
170
+
171
+ def render(self, json_path: str, output_path: str = "sayou_analyst_view.html"):
172
+ if not os.path.exists(json_path):
173
+ return
174
+
175
+ with open(json_path, "r", encoding="utf-8") as f:
176
+ raw_data = json.load(f)
177
+
178
+ elements = []
179
+
180
+ # 1. Nodes (No Parents logic)
181
+ for node in raw_data.get("nodes", []):
182
+ node_id = node.get("node_id")
183
+ attrs = node.get("attributes", {})
184
+ n_cls = node.get("node_class", "unknown").lower()
185
+
186
+ # Type Check
187
+ cy_type = "unknown"
188
+ if "file" in n_cls:
189
+ cy_type = "file"
190
+ elif "class" in n_cls:
191
+ cy_type = "class"
192
+ elif "method" in n_cls:
193
+ cy_type = "method"
194
+ elif "function" in n_cls:
195
+ cy_type = "function"
196
+ elif "library" in n_cls:
197
+ cy_type = "library"
198
+ elif "code" in n_cls:
199
+ cy_type = "code_block"
200
+
201
+ # Labeling
202
+ label = attrs.get("label") or node.get("friendly_name") or node_id
203
+ if cy_type == "file":
204
+ label = os.path.basename(attrs.get("sayou:filePath", label))
205
+ elif cy_type == "class":
206
+ label = attrs.get("meta:class_name", label)
207
+ elif cy_type in ["method", "function"]:
208
+ label = attrs.get("function_name", label)
209
+
210
+ # Code Text
211
+ code_text = attrs.get("schema:text", "")
212
+ cy_data = {
213
+ "id": node_id,
214
+ "label": label,
215
+ "type": cy_type,
216
+ "code": code_text,
217
+ "meta": attrs,
218
+ }
219
+ elements.append({"group": "nodes", "data": cy_data})
220
+
221
+ # 2. Edges
222
+ for edge in raw_data.get("edges", []):
223
+ elements.append(
224
+ {
225
+ "group": "edges",
226
+ "data": {
227
+ "source": edge.get("source"),
228
+ "target": edge.get("target"),
229
+ "edgeType": edge.get("type", "relates"),
230
+ "label": edge.get("type", "").split(":")[-1],
231
+ },
232
+ }
233
+ )
234
+
235
+ self._generate_html(elements, output_path)
236
+ self._log(f"✅ Pure Graph View saved to: {output_path}")
237
+
238
+ def _generate_html(self, elements: list, output_path: str):
239
+ style_json = json.dumps(self.STYLE_SHEET)
240
+ elements_json = json.dumps(elements)
241
+
242
+ html_content = f"""
243
+ <!DOCTYPE html>
244
+ <html>
245
+ <head>
246
+ <meta charset="UTF-8">
247
+ <title>Sayou Code Universe</title>
248
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.28.1/cytoscape.min.js"></script>
249
+ <script src="https://unpkg.com/layout-base/layout-base.js"></script>
250
+ <script src="https://unpkg.com/cose-base/cose-base.js"></script>
251
+ <script src="https://unpkg.com/cytoscape-fcose/cytoscape-fcose.js"></script>
252
+
253
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
254
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
255
+
256
+ <style>
257
+ body {{ font-family: 'Segoe UI', sans-serif; margin: 0; background: #1e272e; color: #dcdde1; overflow: hidden; display: flex; }}
258
+ #cy {{ flex-grow: 1; height: 100vh; }}
259
+
260
+ /* Sidebar & Controls */
261
+ #controls {{
262
+ position: absolute; top: 20px; left: 20px; z-index: 100;
263
+ background: rgba(47, 54, 64, 0.95); padding: 15px; border-radius: 8px;
264
+ box-shadow: 0 4px 10px rgba(0,0,0,0.3); border: 1px solid #4b6584; width: 280px;
265
+ }}
266
+ #tooltip {{
267
+ position: absolute;
268
+ display: none;
269
+ background: rgba(0, 0, 0, 0.8);
270
+ color: #fff;
271
+ padding: 5px 10px;
272
+ border-radius: 4px;
273
+ font-size: 11px;
274
+ pointer-events: none;
275
+ z-index: 1000;
276
+ border: 1px solid #00d2d3;
277
+ }}
278
+ .search-group {{ display: flex; gap: 5px; margin-bottom: 10px; }}
279
+ input[type="text"] {{ flex-grow: 1; background: #222; border: 1px solid #57606f; color: white; padding: 6px; border-radius: 4px; }}
280
+ button {{ background: #4b6584; border: none; color: white; padding: 6px 12px; border-radius: 4px; cursor: pointer; }}
281
+ button:hover {{ background: #00d2d3; color: #000; }}
282
+
283
+ #sidebar {{
284
+ width: 450px; height: 100vh; background: #2f3640; border-left: 1px solid #4b6584;
285
+ display: flex; flex-direction: column; transform: translateX(450px); transition: 0.3s;
286
+ position: absolute; right: 0; top: 0; z-index: 200; box-shadow: -5px 0 15px rgba(0,0,0,0.5);
287
+ }}
288
+ #sidebar.open {{ transform: translateX(0); }}
289
+ .header {{ padding: 20px; border-bottom: 1px solid #4b6584; background: #252a34; }}
290
+ .node-title {{ font-size: 18px; color: #fff; font-weight: bold; margin: 0; }}
291
+ .content {{ flex-grow: 1; overflow-y: auto; padding: 0; background: #282c34; }}
292
+ pre {{ margin: 0; }}
293
+ </style>
294
+ </head>
295
+ <body>
296
+ <div id="controls">
297
+ <div style="font-weight:bold; color:#00d2d3; margin-bottom:10px;">Sayou Graph</div>
298
+ <div class="search-group">
299
+ <input type="text" id="search-input" placeholder="Search..." onkeyup="if(event.key === 'Enter') searchNode()">
300
+ <button onclick="searchNode()">🔍</button>
301
+ </div>
302
+ <div style="margin-bottom: 10px; display: flex; gap: 5px;">
303
+ <button id="btn-label" class="active" onclick="toggleLabels()" style="flex:1;">Labels ON</button>
304
+ <button id="btn-line" class="active" onclick="toggleLines()" style="flex:1;">Lines ON</button>
305
+ </div>
306
+ <div style="font-size:11px; color:#aaa; border-top:1px solid #57606f; padding-top:10px;">
307
+ <button onclick="runLayout()">Re-Layout</button>
308
+ <button onclick="resetView()">Reset Cam</button>
309
+ </div>
310
+ </div>
311
+
312
+ <div id="cy"></div>
313
+ <div id="tooltip"></div>
314
+
315
+ <div id="sidebar">
316
+ <div class="header">
317
+ <h2 class="node-title" id="sb-title">Details</h2>
318
+ <div id="sb-desc" style="font-size:12px; color:#aaa; margin-top:5px;"></div>
319
+ </div>
320
+ <div class="content">
321
+ <pre><code id="sb-code" class="language-python"></code></pre>
322
+ </div>
323
+ </div>
324
+
325
+ <script>
326
+ var cy = cytoscape({{
327
+ container: document.getElementById('cy'),
328
+ elements: {elements_json},
329
+ style: {style_json},
330
+ layout: {{
331
+ name: 'fcose',
332
+ quality: 'proof',
333
+ nodeSeparation: 75,
334
+ idealEdgeLength: edge => edge.data('edgeType') === 'sayou:contains' ? 50 : 200,
335
+ animate: false
336
+ }}
337
+ }});
338
+
339
+ function runLayout() {{
340
+ cy.layout({{ name: 'fcose', animate: true, animationDuration: 800 }}).run();
341
+ }}
342
+
343
+ // [Search]
344
+ function searchNode() {{
345
+ var query = document.getElementById('search-input').value.toLowerCase();
346
+ if(!query) return;
347
+ cy.elements().removeClass('found');
348
+ var found = cy.nodes().filter(ele => ele.data('label').toLowerCase().includes(query));
349
+ if(found.length > 0) {{
350
+ found.addClass('found');
351
+ // cy.animate({{ fit: {{ eles: found, padding: 50 }}, duration: 500 }});
352
+ // cy.center(found);
353
+ }}
354
+ }}
355
+
356
+ function resetView() {{
357
+ cy.elements().removeClass('highlighted faded found');
358
+ document.getElementById('sidebar').classList.remove('open');
359
+ cy.animate({{ fit: {{ padding: 50 }} }});
360
+ }}
361
+
362
+ // 1. Label Toggle (ON/OFF)
363
+ var labelsVisible = true;
364
+ function toggleLabels() {{
365
+ labelsVisible = !labelsVisible;
366
+ var btn = document.getElementById('btn-label');
367
+
368
+ if (labelsVisible) {{
369
+ cy.nodes().removeClass('no-label');
370
+ btn.innerText = "Labels ON";
371
+ btn.style.background = "#4b6584";
372
+ btn.style.color = "white";
373
+ }} else {{
374
+ cy.nodes().addClass('no-label');
375
+ btn.innerText = "Labels OFF";
376
+ btn.style.background = "#2f3542";
377
+ btn.style.color = "#747d8c";
378
+ }}
379
+ }}
380
+
381
+ // 2. Line Toggle (ON/OFF)
382
+ var linesVisible = true;
383
+ function toggleLines() {{
384
+ linesVisible = !linesVisible;
385
+ var btn = document.getElementById('btn-line');
386
+ // var edges = cy.edges('[edgeType="sayou:contains"]');
387
+ var edges = cy.edges();
388
+
389
+ if (linesVisible) {{
390
+ edges.removeClass('hidden-edge');
391
+ btn.innerText = "Lines ON";
392
+ btn.style.background = "#4b6584";
393
+ btn.style.color = "white";
394
+ }} else {{
395
+ edges.addClass('hidden-edge');
396
+ btn.innerText = "Lines OFF";
397
+ btn.style.background = "#2f3542";
398
+ btn.style.color = "#747d8c";
399
+ }}
400
+ }}
401
+
402
+ // [Click Interaction]
403
+ cy.on('tap', 'node', function(evt){{
404
+ var node = evt.target;
405
+
406
+ // Highlight neighbors
407
+ cy.elements().removeClass('highlighted faded');
408
+ var neighbors = node.neighborhood().add(node);
409
+ cy.elements().addClass('faded');
410
+ neighbors.removeClass('faded').addClass('highlighted');
411
+
412
+ // Sidebar
413
+ document.getElementById('sidebar').classList.add('open');
414
+ document.getElementById('sb-title').innerText = node.data('label');
415
+ document.getElementById('sb-desc').innerText = (node.data('type') || 'Unknown').toUpperCase() + ' | ' + node.id();
416
+
417
+ var codeArea = document.getElementById('sb-code');
418
+ codeArea.innerText = node.data('code') || JSON.stringify(node.data('meta'), null, 2);
419
+ hljs.highlightElement(codeArea);
420
+ }});
421
+
422
+ cy.on('tap', function(evt){{
423
+ if(evt.target === cy) resetView();
424
+ }});
425
+
426
+ var tooltip = document.getElementById('tooltip');
427
+
428
+ cy.on('mouseover', 'node', function(evt){{
429
+ var node = evt.target;
430
+ var label = node.data('label') || node.data('id');
431
+
432
+ tooltip.style.display = 'block';
433
+ tooltip.innerText = label;
434
+ tooltip.style.left = evt.renderedPosition.x + 'px';
435
+ // var pos = node.renderedPosition();
436
+ // tooltip.style.left = (pos.x + 10) + 'px';
437
+ // tooltip.style.top = (pos.y + 10) + 'px';
438
+ }});
439
+
440
+ cy.on('mousemove', function(evt){{
441
+ tooltip.style.left = (evt.renderedPosition.x + 15) + 'px';
442
+ tooltip.style.top = (evt.renderedPosition.y + 15) + 'px';
443
+ }});
444
+
445
+ cy.on('mouseout', 'node', function(){{
446
+ tooltip.style.display = 'none';
447
+ }});
448
+ </script>
449
+ </body>
450
+ </html>
451
+ """
452
+ with open(output_path, "w", encoding="utf-8") as f:
453
+ f.write(html_content)
@@ -0,0 +1,291 @@
1
+ import json
2
+ import os
3
+ from sayou.core.base_component import BaseComponent
4
+
5
+
6
+ class ShowcaseKGRenderer(BaseComponent):
7
+ """
8
+ Renders the Final Stable 3D Knowledge Graph.
9
+ - [Architecture] Removed unstable Post-Processing shaders to fix physics crash.
10
+ - [Visuals] Uses native 'Emissive' materials for Neon aesthetics.
11
+ - [Topology] Distinguishes 'Imports' (Gold/Bright) vs 'Hierarchy' (Dark/Blue).
12
+ """
13
+
14
+ component_name = "ShowcaseKGRenderer"
15
+
16
+ def render(self, json_path: str, output_path: str = "sayou_showcase_3d.html"):
17
+ if not os.path.exists(json_path):
18
+ return
19
+
20
+ with open(json_path, "r", encoding="utf-8") as f:
21
+ raw_data = json.load(f)
22
+
23
+ nodes = []
24
+ links = []
25
+ existing_ids = set()
26
+
27
+ for node in raw_data.get("nodes", []):
28
+ node_id = node.get("node_id")
29
+ existing_ids.add(node_id)
30
+ attrs = node.get("attributes", {})
31
+ n_cls = node.get("node_class", "unknown").lower()
32
+
33
+ group = "Chunk"
34
+ color = "#4a69bd"
35
+ val = 3
36
+
37
+ if "file" in n_cls or "package" in n_cls:
38
+ group = "Document"
39
+ color = "#00d2d3" # Cyan
40
+ val = 20
41
+ elif "class" in n_cls:
42
+ group = "Header"
43
+ color = "#ff6b81" # Pink
44
+ val = 12
45
+ elif "method" in n_cls or "function" in n_cls:
46
+ group = "Code"
47
+ color = "#feca57" # Gold
48
+ val = 6
49
+ elif "library" in n_cls:
50
+ group = "Library"
51
+ color = "#2ed573" # Green
52
+ val = 10
53
+
54
+ label = attrs.get("label") or node.get("friendly_name") or node_id
55
+ if group == "Document":
56
+ label = os.path.basename(attrs.get("sayou:filePath", label))
57
+
58
+ clean_attrs = {}
59
+ for k, v in attrs.items():
60
+ if isinstance(v, str) and len(v) > 200:
61
+ clean_attrs[k] = v[:200] + "..."
62
+ elif not k.startswith("sayou:"):
63
+ clean_attrs[k] = v
64
+
65
+ nodes.append(
66
+ {
67
+ "id": node_id,
68
+ "label": label,
69
+ "group": group,
70
+ "color": color,
71
+ "val": val,
72
+ "attributes": clean_attrs,
73
+ }
74
+ )
75
+
76
+ # [2] Edge data processing
77
+ for edge in raw_data.get("edges", []):
78
+ src = edge.get("source")
79
+ tgt = edge.get("target")
80
+
81
+ if src in existing_ids and tgt in existing_ids:
82
+ e_type = edge.get("type", "relates")
83
+ is_import = "import" in e_type or "calls" in e_type
84
+
85
+ links.append(
86
+ {
87
+ "source": src,
88
+ "target": tgt,
89
+ "type": e_type,
90
+ "is_import": is_import,
91
+ }
92
+ )
93
+
94
+ graph_data = {"nodes": nodes, "links": links}
95
+ self._generate_html(graph_data, output_path)
96
+ self._log(f"✅ Final Visual Showcase generated at: {output_path}")
97
+
98
+ def _generate_html(self, graph_data, output_path):
99
+ json_str = json.dumps(graph_data)
100
+
101
+ html_content = f"""
102
+ <!DOCTYPE html>
103
+ <html lang="en">
104
+ <head>
105
+ <meta charset="UTF-8">
106
+ <title>Sayou Dataverse</title>
107
+ <script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script>
108
+ <script src="https://unpkg.com/3d-force-graph@1.73.1/dist/3d-force-graph.min.js"></script>
109
+ <style>
110
+ body {{ margin: 0; background-color: #020202; overflow: hidden; font-family: sans-serif; }}
111
+ #graph {{ width: 100%; height: 100vh; }}
112
+ #info {{
113
+ position: absolute; top: 30px; right: 30px; width: 300px;
114
+ background: rgba(15, 20, 30, 0.9);
115
+ border: 1px solid #444; border-left: 4px solid #00d2d3;
116
+ box-shadow: 0 0 20px rgba(0,0,0,0.8);
117
+ color: #fff; padding: 20px; font-size:13px;
118
+ backdrop-filter: blur(5px); display: none; border-radius: 4px; pointer-events: none;
119
+ }}
120
+ .tag {{ display: inline-block; padding: 3px 6px; border-radius: 4px; color: #000; font-weight:bold; font-size:10px; margin-bottom:10px; }}
121
+ h2 {{ margin:0 0 10px 0; font-size:16px; color:#eee; }}
122
+ .row {{ margin-bottom:4px; color:#aaa; border-bottom:1px solid #333; padding-bottom:2px; }}
123
+ .key {{ color:#00d2d3; margin-right:5px; }}
124
+ </style>
125
+ </head>
126
+ <body>
127
+ <div id="graph"></div>
128
+ <div id="info"></div>
129
+
130
+ <script>
131
+ const gData = {json_str};
132
+ const infoDiv = document.getElementById('info');
133
+
134
+ // [State]
135
+ const highlightNodes = new Set();
136
+ const highlightLinks = new Set();
137
+ let hoverNode = null;
138
+ let isFlying = false;
139
+
140
+ // [Init] Big Bang
141
+ gData.nodes.forEach(node => {{
142
+ node.x = Math.random() * 2000 - 1000;
143
+ node.y = Math.random() * 2000 - 1000;
144
+ node.z = Math.random() * 2000 - 1000;
145
+ }});
146
+
147
+ // [Graph Init]
148
+ const Graph = ForceGraph3D()(document.getElementById('graph'))
149
+ .graphData(gData)
150
+ .backgroundColor('#050505')
151
+ .showNavInfo(false)
152
+ .nodeLabel(null)
153
+ .cooldownTicks(50);
154
+
155
+ // [Physics]
156
+ Graph.d3Force('charge').strength(-100);
157
+ Graph.d3Force('link').distance(link => link.is_import ? 10 : 100);
158
+
159
+ // [Visuals]
160
+ Graph
161
+ .nodeThreeObject(node => {{
162
+ let isDimmed = false;
163
+ let isTarget = false;
164
+ if (hoverNode) {{
165
+ if (hoverNode === node || highlightNodes.has(node)) isTarget = true;
166
+ else isDimmed = true;
167
+ }}
168
+ const baseColor = node.color;
169
+ const opacity = isDimmed ? 0.1 : 0.9;
170
+ const emissiveInt = isTarget ? 1.5 : (isDimmed ? 0 : 0.6);
171
+
172
+ const material = new THREE.MeshPhongMaterial({{
173
+ color: baseColor,
174
+ emissive: baseColor,
175
+ emissiveIntensity: emissiveInt,
176
+ transparent: true,
177
+ opacity: opacity,
178
+ shininess: 90
179
+ }});
180
+
181
+ if (node.group === "Document") {{
182
+ const s = node.val;
183
+ const geometry = new THREE.BoxGeometry(s, s, s);
184
+ const edges = new THREE.EdgesGeometry(geometry);
185
+ const lineMat = new THREE.LineBasicMaterial({{ color: baseColor, transparent:true, opacity: isDimmed ? 0.05 : 0.4 }});
186
+ const wireframe = new THREE.LineSegments(edges, lineMat);
187
+ wireframe.add(new THREE.Mesh(new THREE.BoxGeometry(s*0.4, s*0.4, s*0.4), material));
188
+ return wireframe;
189
+ }}
190
+ else if (node.group === "Header") return new THREE.Mesh(new THREE.OctahedronGeometry(node.val * 0.7), material);
191
+ else return new THREE.Mesh(new THREE.IcosahedronGeometry(node.val * 0.6, 2), material);
192
+ }})
193
+ .linkWidth(link => {{
194
+ if (highlightLinks.has(link)) return 3;
195
+ if (hoverNode && !highlightLinks.has(link)) return 0;
196
+ return link.is_import ? 1.5 : 0.5;
197
+ }})
198
+ .linkColor(link => {{
199
+ if (highlightLinks.has(link)) return link.is_import ? '#feca57' : '#00d2d3';
200
+ return link.is_import ? 'rgba(254, 202, 87, 0.4)' : 'rgba(44, 62, 80, 0.3)';
201
+ }})
202
+ .linkDirectionalParticles(link => highlightLinks.has(link) ? 3 : 0)
203
+ .linkDirectionalParticleWidth(3)
204
+ .onNodeHover(node => {{
205
+ if ((!node && !hoverNode) || (node && hoverNode === node)) return;
206
+
207
+ highlightNodes.clear(); highlightLinks.clear();
208
+ if (node) {{
209
+ highlightNodes.add(node);
210
+ gData.links.forEach(link => {{
211
+ if (link.source.id === node.id) {{ highlightNodes.add(link.target); highlightLinks.add(link); }}
212
+ else if (link.target.id === node.id) {{ highlightNodes.add(link.source); highlightLinks.add(link); }}
213
+ }});
214
+ }}
215
+ hoverNode = node || null;
216
+ requestAnimationFrame(() => {{
217
+ Graph
218
+ .nodeThreeObject(Graph.nodeThreeObject())
219
+ .linkWidth(Graph.linkWidth())
220
+ .linkColor(Graph.linkColor())
221
+ .linkDirectionalParticles(Graph.linkDirectionalParticles());
222
+ }});
223
+
224
+ document.body.style.cursor = node ? 'crosshair' : null;
225
+ if (node) {{
226
+ infoDiv.style.display = 'block';
227
+ let html = `<span class='tag' style='background:${{node.color}}'>${{node.group}}</span><h2>${{node.label}}</h2>`;
228
+ if (node.attributes) {{
229
+ for (const [k, v] of Object.entries(node.attributes)) {{
230
+ if(k!=='type') html += `<div class='row'><span class='key'>${{k.split(':').pop()}}:</span> ${{v}}</div>`;
231
+ }}
232
+ }}
233
+ infoDiv.innerHTML = html;
234
+ }} else infoDiv.style.display = 'none';
235
+ }})
236
+ .onNodeClick(node => {{
237
+ if (!node) return;
238
+ isFlying = true;
239
+ const dist = 150;
240
+ const distRatio = 1 + dist/Math.hypot(node.x, node.y, node.z);
241
+ Graph.cameraPosition(
242
+ {{ x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio }},
243
+ node,
244
+ 2000
245
+ );
246
+ setTimeout(() => {{ isFlying = false; }}, 2000);
247
+ }});
248
+
249
+ // [Env]
250
+ const scene = Graph.scene();
251
+ const starsGeo = new THREE.BufferGeometry();
252
+ const pos = new Float32Array(2000 * 3);
253
+ for(let i=0; i<2000*3; i++) pos[i] = (Math.random()-0.5) * 4000;
254
+ starsGeo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
255
+ scene.add(new THREE.Points(starsGeo, new THREE.PointsMaterial({{size:2, color:0xffffff, opacity:0.5, transparent:true}})));
256
+ scene.add(new THREE.AmbientLight(0x222222));
257
+ const light = new THREE.DirectionalLight(0xffffff, 1);
258
+ light.position.set(100, 100, 100);
259
+ scene.add(light);
260
+
261
+ // [Manual Orbit Engine]
262
+ setInterval(() => {{
263
+ if (hoverNode || isFlying) return;
264
+ const cam = Graph.camera();
265
+ const controls = Graph.controls();
266
+ if (!controls) return;
267
+
268
+ const target = controls.target;
269
+ const relX = cam.position.x - target.x;
270
+ const relZ = cam.position.z - target.z;
271
+ const r = Math.sqrt(relX*relX + relZ*relZ);
272
+ let theta = Math.atan2(relZ, relX);
273
+
274
+ theta += 0.0015;
275
+
276
+ const newX = target.x + r * Math.cos(theta);
277
+ const newZ = target.z + r * Math.sin(theta);
278
+
279
+ Graph.cameraPosition(
280
+ {{ x: newX, y: cam.position.y, z: newZ }},
281
+ target,
282
+ 0
283
+ );
284
+ }}, 15);
285
+
286
+ </script>
287
+ </body>
288
+ </html>
289
+ """
290
+ with open(output_path, "w", encoding="utf-8") as f:
291
+ f.write(html_content)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sayou-visualizer
3
- Version: 0.0.9
3
+ Version: 0.0.11
4
4
  Summary: Visualizer components for the Sayou Data Platform
5
5
  Project-URL: Homepage, https://www.sayouzone.com/
6
6
  Project-URL: Documentation, https://sayouzone.github.io/sayou-fabric/
@@ -1,13 +1,14 @@
1
1
  sayou/visualizer/__init__.py,sha256=DRv-5qzP6nal7qXVf7Zl67CQx3Kf3mTZhlLKl-vs_G0,82
2
- sayou/visualizer/pipeline.py,sha256=HCNxVEVO_XfhCIX5ZmTlab36EDmSx22J_916enSVGL8,3727
2
+ sayou/visualizer/pipeline.py,sha256=LduZlz5jGqTfzRRuv3W-IFfj1VxjjTkXAwXL6zs-yQE,4116
3
3
  sayou/visualizer/core/exceptions.py,sha256=Mk5UtIfim7i9688c4qAKP7kB1GpLPM29t94HbQM9fhw,99
4
4
  sayou/visualizer/core/schemas.py,sha256=qn44BINevFZF_ALBhh20DS4GyMo5HV3UzqY4UTh_p3A,381
5
5
  sayou/visualizer/interfaces/base_renderer.py,sha256=orllTXlqM4-wDemOWbcZX8zF708KOdWFgoqZh8MeAzE,760
6
- sayou/visualizer/renderer/kg_renderer.py,sha256=78co3QBwmEjcLGiDkEt1s_wPYlCa1tIQQ7-6uPjhK20,11573
6
+ sayou/visualizer/renderer/analytic_kg_renderer.py,sha256=I5ccv7JUCGvPE79c2d1O-XuYwrK3fxk9qj4Xzyx_WLg,16404
7
7
  sayou/visualizer/renderer/pyvis_renderer.py,sha256=2HKv_qAKKNHMKLwC7xoIn3EZ5oFLpgds7LPgK4EeXfA,2408
8
+ sayou/visualizer/renderer/showcase_kg_renderer.py,sha256=_K9GjAPuKzA-BKbJYDK0Ku0U8tThusuqgwAwhdtaprU,11643
8
9
  sayou/visualizer/tracer/graph_tracer.py,sha256=j0dqd0_67ZnQCNjn5siKHXXsXasWim9olYqUQA2jKxk,3638
9
10
  sayou/visualizer/tracer/rich_tracer.py,sha256=ik7J1P7AMTN47lkjMLE7iTlRuksUpwSKluvWdpY6kdQ,2406
10
11
  sayou/visualizer/tracer/websocket_tracer.py,sha256=OZLg4jTfuxp6IwDacmAACKZ_0FirZFhyLysqh9QyrJA,1626
11
- sayou_visualizer-0.0.9.dist-info/METADATA,sha256=ygbD1biZ6YTS54Q4C_UwWoonAZHuXShezzgmMLL0vv0,16680
12
- sayou_visualizer-0.0.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
13
- sayou_visualizer-0.0.9.dist-info/RECORD,,
12
+ sayou_visualizer-0.0.11.dist-info/METADATA,sha256=0wlBaqJxYAqGoTx4B7vRusfBxQIwSKJFnrt36ad74bw,16681
13
+ sayou_visualizer-0.0.11.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
+ sayou_visualizer-0.0.11.dist-info/RECORD,,
@@ -1,300 +0,0 @@
1
- import json
2
- import os
3
- from collections import defaultdict
4
-
5
- from sayou.core.base_component import BaseComponent
6
-
7
-
8
- class KGRenderer(BaseComponent):
9
- """
10
- Renders 3D KG with 'Virtual Hierarchy'.
11
- Injects missing Parent Nodes based on metadata and applies Semantic Styling.
12
- """
13
-
14
- component_name = "KGRenderer"
15
-
16
- def render(self, json_path: str, output_path: str = "kg_view_3d.html"):
17
- if not os.path.exists(json_path):
18
- self._log(f"Output file not found: {json_path}", level="error")
19
- return
20
-
21
- with open(json_path, "r", encoding="utf-8") as f:
22
- raw_data = json.load(f)
23
-
24
- # ---------------------------------------------------------
25
- # [Python Logic]
26
- # ---------------------------------------------------------
27
- nodes_by_source = defaultdict(list)
28
- final_nodes = []
29
- final_links = []
30
-
31
- existing_ids = set()
32
-
33
- for node in raw_data.get("nodes", []):
34
- node_id = node.get("node_id")
35
- existing_ids.add(node_id)
36
- attrs = node.get("attributes", {})
37
- meta = node.get("metadata", {})
38
-
39
- source = meta.get("source") or attrs.get("sayou:source") or "Unknown Source"
40
- nodes_by_source[source].append(node_id)
41
-
42
- sem_type = attrs.get("sayou:semanticType", "text")
43
-
44
- clean_attrs = {}
45
- for k, v in attrs.items():
46
- if isinstance(v, str) and len(v) > 300:
47
- clean_attrs[k] = v[:200] + "..."
48
- else:
49
- clean_attrs[k] = v
50
-
51
- display_label = attrs.get("schema:text", node_id)
52
- if len(display_label) > 30:
53
- display_label = display_label[:30] + "..."
54
-
55
- group = "Chunk"
56
- color = "#1e90ff"
57
- val = 5
58
-
59
- if sem_type in ["h1", "h2", "h3", "title"]:
60
- group = "Header"
61
- color = "#ffa502"
62
- val = 12
63
- elif "list" in sem_type:
64
- group = "List"
65
- color = "#2ed573"
66
- val = 4
67
- elif "table" in sem_type:
68
- group = "Table"
69
- color = "#a55eea"
70
- val = 10
71
- elif "code" in sem_type:
72
- group = "Code"
73
- color = "#ff4757"
74
- val = 8
75
-
76
- final_nodes.append(
77
- {
78
- "id": node_id,
79
- "label": display_label,
80
- "group": group,
81
- "sem_type": sem_type,
82
- "color": color,
83
- "val": val,
84
- "attributes": clean_attrs,
85
- "source": source,
86
- }
87
- )
88
-
89
- for edge in raw_data.get("links", []) + raw_data.get("edges", []):
90
- final_links.append(
91
- {
92
- "source": edge.get("source"),
93
- "target": edge.get("target"),
94
- "relation": edge.get("relation", "relates_to"),
95
- }
96
- )
97
-
98
- for source_name, child_ids in nodes_by_source.items():
99
- if source_name in existing_ids:
100
- continue
101
-
102
- virtual_doc_id = f"VIRTUAL_DOC:{source_name}"
103
-
104
- final_nodes.append(
105
- {
106
- "id": virtual_doc_id,
107
- "label": source_name,
108
- "group": "Document",
109
- "color": "#ff4757",
110
- "val": 30,
111
- "attributes": {
112
- "type": "Virtual Parent",
113
- "child_count": len(child_ids),
114
- },
115
- "is_virtual": True,
116
- }
117
- )
118
-
119
- for child_id in child_ids:
120
- final_links.append(
121
- {
122
- "source": virtual_doc_id,
123
- "target": child_id,
124
- "relation": "CONTAINS",
125
- }
126
- )
127
-
128
- graph_data = {"nodes": final_nodes, "links": final_links}
129
- self._log(f"Rendering KG with Virtual Hierarchy ({len(final_nodes)} nodes)...")
130
-
131
- # ---------------------------------------------------------
132
- # [JS Logic] Renderer
133
- # ---------------------------------------------------------
134
- html_content = f"""
135
- <!DOCTYPE html>
136
- <html lang="en">
137
- <head>
138
- <meta charset="UTF-8">
139
- <title>Sayou Dataverse</title>
140
- <style>
141
- body {{ margin: 0; background-color: #000205; overflow: hidden; }}
142
- #graph {{ width: 100%; height: 100vh; }}
143
-
144
- #info {{
145
- position: absolute; top: 20px; right: 20px; width: 300px;
146
- background: rgba(5, 10, 20, 0.85);
147
- border: 1px solid rgba(0, 242, 255, 0.3);
148
- border-left: 3px solid #00f2ff;
149
- box-shadow: 0 0 20px rgba(0, 242, 255, 0.1);
150
- color: #dff9fb; padding: 20px;
151
- font-family: 'Consolas', 'Monaco', monospace;
152
- backdrop-filter: blur(5px);
153
- display: none; pointer-events: none;
154
- border-radius: 0 10px 10px 0;
155
- }}
156
- .tag {{
157
- display: inline-block; padding: 2px 8px; border-radius: 2px;
158
- font-size: 9px; font-weight: 800; color: #000; margin-bottom: 12px;
159
- text-transform: uppercase; letter-spacing: 1px;
160
- }}
161
- h3 {{
162
- margin: 0 0 12px 0; color: #fff;
163
- border-bottom: 1px dashed #576574; padding-bottom: 8px;
164
- font-size: 14px; line-height: 1.4;
165
- }}
166
- .row {{ font-size: 11px; margin-bottom: 4px; color: #a4b0be; }}
167
- .key {{ color: #00d2d3; margin-right: 5px; }}
168
- </style>
169
- <script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script>
170
- <script src="https://unpkg.com/3d-force-graph@1.73.1/dist/3d-force-graph.min.js"></script>
171
- </head>
172
- <body>
173
- <div id="graph"></div>
174
- <div id="info"></div>
175
-
176
- <script>
177
- const gData = {json.dumps(graph_data)};
178
- const infoDiv = document.getElementById('info');
179
-
180
- const Graph = ForceGraph3D()(document.getElementById('graph'))
181
- .graphData(gData)
182
- .backgroundColor('#000205')
183
- .showNavInfo(false)
184
-
185
- // [Design Logic]
186
- .nodeThreeObject(node => {{
187
- let geometry, material;
188
-
189
- // 1. Virtual Document
190
- if (node.group === "Document") {{
191
- const size = node.val;
192
- geometry = new THREE.BoxGeometry(size, size, size);
193
-
194
- const edges = new THREE.EdgesGeometry(geometry);
195
- material = new THREE.LineBasicMaterial({{
196
- color: node.color,
197
- transparent: true,
198
- opacity: 0.4
199
- }});
200
- const wireframe = new THREE.LineSegments(edges, material);
201
-
202
- const coreGeo = new THREE.BoxGeometry(size*0.2, size*0.2, size*0.2);
203
- const coreMat = new THREE.MeshBasicMaterial({{ color: node.color, wireframe: true }});
204
- wireframe.add(new THREE.Mesh(coreGeo, coreMat));
205
-
206
- return wireframe;
207
- }}
208
-
209
- // 2. Headers (H1~H3): [발광 다이아몬드]
210
- else if (node.group === "Header") {{
211
- geometry = new THREE.OctahedronGeometry(4);
212
- material = new THREE.MeshPhongMaterial({{
213
- color: node.color,
214
- emissive: node.color,
215
- emissiveIntensity: 0.5,
216
- shininess: 100,
217
- flatShading: true
218
- }});
219
- }}
220
-
221
- // 3. Chunk / Text: [데이터 오브]
222
- else {{
223
- // 단순 구 대신 Icosahedron(정이십면체)을 써서 디지털 느낌
224
- geometry = new THREE.IcosahedronGeometry(3, 1);
225
- material = new THREE.MeshLambertMaterial({{
226
- color: node.color,
227
- transparent: true,
228
- opacity: 0.8
229
- }});
230
- }}
231
-
232
- return new THREE.Mesh(geometry, material);
233
- }})
234
-
235
- // [Link Design]
236
- .linkWidth(link => link.relation === "CONTAINS" ? 0 : 0.5)
237
- .linkColor(() => '#2f3542')
238
- .linkDirectionalParticles(link => link.relation === "CONTAINS" ? 1 : 3)
239
- .linkDirectionalParticleWidth(1.2)
240
- .linkDirectionalParticleSpeed(0.006)
241
- .linkDirectionalParticleColor(link => link.relation === "CONTAINS" ? '#57606f' : '#00f2ff')
242
-
243
- // [Interaction]
244
- .onNodeHover(node => {{
245
- document.body.style.cursor = node ? 'crosshair' : null;
246
- if (node) {{
247
- infoDiv.style.display = 'block';
248
-
249
- let tagColor = node.color;
250
- if(node.group === 'Document') tagColor = '#ff4757';
251
-
252
- let html = `<span class='tag' style='background:${{tagColor}}'>${{node.group}}</span>`;
253
- if(node.sem_type) html += `<span class='tag' style='background:#2f3542; color:#fff; margin-left:5px'>${{node.sem_type}}</span>`;
254
-
255
- html += `<h3>${{node.label}}</h3>`;
256
-
257
- if (node.attributes) {{
258
- for (const [k, v] of Object.entries(node.attributes)) {{
259
- if(k === 'type' || k === 'child_count') continue;
260
- let key = k.includes(':') ? k.split(':').pop() : k;
261
- html += `<div class='row'><span class='key'>${{key}}:</span> ${{v}}</div>`;
262
- }}
263
- }}
264
- infoDiv.innerHTML = html;
265
- }} else {{
266
- infoDiv.style.display = 'none';
267
- }}
268
- }})
269
- .onNodeClick(node => {{
270
- const distance = 70;
271
- const distRatio = 1 + distance/Math.hypot(node.x, node.y, node.z);
272
- const newPos = node.x || node.y || node.z
273
- ? {{ x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio }}
274
- : {{ x: 0, y: 0, z: distance }};
275
- Graph.cameraPosition(newPos, node, 1500);
276
- }});
277
-
278
- // [Lighting]
279
- const ambientLight = new THREE.AmbientLight(0x222222); // 어두운 기본광
280
- Graph.scene().add(ambientLight);
281
-
282
- const blueLight = new THREE.PointLight(0x00f2ff, 1, 100); // 청록색 포인트 조명
283
- blueLight.position.set(50, 50, 50);
284
- Graph.scene().add(blueLight);
285
-
286
- const pinkLight = new THREE.PointLight(0xff00ff, 0.5, 100); // 핑크색 포인트 조명 (반대편)
287
- pinkLight.position.set(-50, -50, -50);
288
- Graph.scene().add(pinkLight);
289
-
290
- // [Physics]
291
- Graph.d3Force('charge').strength(-50);
292
- Graph.d3Force('link').distance(link => link.relation === "CONTAINS" ? 60 : 30);
293
-
294
- </script>
295
- </body>
296
- </html>
297
- """
298
- with open(output_path, "w", encoding="utf-8") as f:
299
- f.write(html_content)
300
- self._log(f"✅ Semantic 3D KG Showcase saved to: {output_path}")