sayou-visualizer 0.0.10__tar.gz → 0.0.12__tar.gz

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 (20) hide show
  1. {sayou_visualizer-0.0.10 → sayou_visualizer-0.0.12}/PKG-INFO +1 -1
  2. {sayou_visualizer-0.0.10 → sayou_visualizer-0.0.12}/pyproject.toml +1 -1
  3. sayou_visualizer-0.0.12/src/sayou/visualizer/core/styles.py +223 -0
  4. {sayou_visualizer-0.0.10 → sayou_visualizer-0.0.12}/src/sayou/visualizer/pipeline.py +13 -3
  5. {sayou_visualizer-0.0.10 → sayou_visualizer-0.0.12}/src/sayou/visualizer/renderer/analytic_kg_renderer.py +17 -185
  6. sayou_visualizer-0.0.12/src/sayou/visualizer/renderer/showcase_kg_renderer.py +288 -0
  7. sayou_visualizer-0.0.10/src/sayou/visualizer/renderer/showcase_kg_renderer.py +0 -303
  8. {sayou_visualizer-0.0.10 → sayou_visualizer-0.0.12}/.gitignore +0 -0
  9. {sayou_visualizer-0.0.10 → sayou_visualizer-0.0.12}/README.md +0 -0
  10. {sayou_visualizer-0.0.10 → sayou_visualizer-0.0.12}/examples/quick_start.py +0 -0
  11. {sayou_visualizer-0.0.10 → sayou_visualizer-0.0.12}/examples/quick_start_ws_client.py +0 -0
  12. {sayou_visualizer-0.0.10 → sayou_visualizer-0.0.12}/examples/quick_start_ws_server.py +0 -0
  13. {sayou_visualizer-0.0.10 → sayou_visualizer-0.0.12}/src/sayou/visualizer/__init__.py +0 -0
  14. {sayou_visualizer-0.0.10 → sayou_visualizer-0.0.12}/src/sayou/visualizer/core/exceptions.py +0 -0
  15. {sayou_visualizer-0.0.10 → sayou_visualizer-0.0.12}/src/sayou/visualizer/core/schemas.py +0 -0
  16. {sayou_visualizer-0.0.10 → sayou_visualizer-0.0.12}/src/sayou/visualizer/interfaces/base_renderer.py +0 -0
  17. {sayou_visualizer-0.0.10 → sayou_visualizer-0.0.12}/src/sayou/visualizer/renderer/pyvis_renderer.py +0 -0
  18. {sayou_visualizer-0.0.10 → sayou_visualizer-0.0.12}/src/sayou/visualizer/tracer/graph_tracer.py +0 -0
  19. {sayou_visualizer-0.0.10 → sayou_visualizer-0.0.12}/src/sayou/visualizer/tracer/rich_tracer.py +0 -0
  20. {sayou_visualizer-0.0.10 → sayou_visualizer-0.0.12}/src/sayou/visualizer/tracer/websocket_tracer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sayou-visualizer
3
- Version: 0.0.10
3
+ Version: 0.0.12
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/
@@ -7,7 +7,7 @@ build-backend = "hatchling.build"
7
7
  # -----------------
8
8
  [project]
9
9
  name = "sayou-visualizer"
10
- version = "0.0.10"
10
+ version = "0.0.12"
11
11
  authors = [
12
12
  { name = "Sayouzone", email = "contact@sayouzone.com" },
13
13
  ]
@@ -0,0 +1,223 @@
1
+ from sayou.core.ontology import SayouClass
2
+
3
+ HIDDEN_ATTR_PREFIXES = ["sayou:", "meta:", "schema:"]
4
+
5
+ # =========================================================
6
+ # 1. 3D Showcase Styles (Original Design Restored)
7
+ # =========================================================
8
+ SHOWCASE_STYLE_MAP = {
9
+ "default": {"group": "Chunk", "color": "#4a69bd", "val": 3},
10
+ # [Original Design Colors]
11
+ SayouClass.FILE: {"group": "Document", "color": "#00d2d3", "val": 20}, # Cyan
12
+ SayouClass.CLASS: {"group": "Header", "color": "#ff6b81", "val": 12}, # Pink
13
+ SayouClass.METHOD: {"group": "Code", "color": "#feca57", "val": 6}, # Gold
14
+ SayouClass.FUNCTION: {"group": "Code", "color": "#feca57", "val": 6}, # Gold
15
+ SayouClass.LIBRARY: {"group": "Library", "color": "#2ed573", "val": 10}, # Green
16
+ # [New] YouTube Domain (Compatible Theme)
17
+ SayouClass.VIDEO: {"group": "Video", "color": "#e84118", "val": 40}, # Deep Red
18
+ SayouClass.VIDEO_SEGMENT: {
19
+ "group": "Segment",
20
+ "color": "#dcdde1",
21
+ "val": 4,
22
+ }, # Gray
23
+ }
24
+
25
+ # 동적 크기 계산 규칙
26
+ DYNAMIC_SIZING_RULES = {
27
+ SayouClass.VIDEO_SEGMENT: {
28
+ "attr_start": "sayou:startTime",
29
+ "attr_end": "sayou:endTime",
30
+ "base_val": 4,
31
+ "scale_factor": 0.5,
32
+ }
33
+ }
34
+
35
+ # =========================================================
36
+ # 2. 2D Analyst Styles (Original Design + Interactions Restored)
37
+ # =========================================================
38
+ ANALYST_TYPE_MAPPING = {
39
+ SayouClass.FILE: "file",
40
+ SayouClass.CLASS: "class",
41
+ SayouClass.METHOD: "method",
42
+ SayouClass.FUNCTION: "function",
43
+ SayouClass.LIBRARY: "library",
44
+ SayouClass.CODE_BLOCK: "code_block",
45
+ SayouClass.VIDEO: "video",
46
+ SayouClass.VIDEO_SEGMENT: "segment",
47
+ }
48
+
49
+ ANALYST_STYLE_SHEET = [
50
+ # [Global]
51
+ {
52
+ "selector": "node",
53
+ "style": {
54
+ "label": "data(label)",
55
+ "color": "#ecf0f1",
56
+ "font-size": "10px",
57
+ "text-valign": "center",
58
+ "text-halign": "center",
59
+ "text-wrap": "wrap",
60
+ "text-max-width": "100px",
61
+ "background-color": "#95a5a6",
62
+ "border-width": 1,
63
+ "border-color": "#7f8c8d",
64
+ },
65
+ },
66
+ # [Original Node Shapes/Colors]
67
+ {
68
+ "selector": "node[type='file']",
69
+ "style": {
70
+ "shape": "rectangle",
71
+ "background-color": "#2c3e50",
72
+ "width": 60,
73
+ "height": 60,
74
+ "font-size": "12px",
75
+ "font-weight": "bold",
76
+ "border-width": 2,
77
+ "border-color": "#00d2d3",
78
+ },
79
+ },
80
+ {
81
+ "selector": "node[type='class']",
82
+ "style": {
83
+ "shape": "diamond",
84
+ "background-color": "#8e44ad",
85
+ "width": 40,
86
+ "height": 40,
87
+ },
88
+ },
89
+ {
90
+ "selector": "node[type='method'], node[type='function']",
91
+ "style": {
92
+ "shape": "ellipse",
93
+ "background-color": "#e67e22",
94
+ "width": 25,
95
+ "height": 25,
96
+ },
97
+ },
98
+ {
99
+ "selector": "node[type='code_block']",
100
+ "style": {
101
+ "shape": "round-rectangle",
102
+ "background-color": "#7f8c8d",
103
+ "width": 15,
104
+ "height": 15,
105
+ "label": "",
106
+ },
107
+ },
108
+ {
109
+ "selector": "node[type='library']",
110
+ "style": {
111
+ "shape": "hexagon",
112
+ "background-color": "#16a085",
113
+ "width": 50,
114
+ "height": 50,
115
+ },
116
+ },
117
+ # [New YouTube Nodes]
118
+ {
119
+ "selector": "node[type='video']",
120
+ "style": {
121
+ "shape": "rectangle",
122
+ "background-color": "#c0392b",
123
+ "width": 80,
124
+ "height": 80,
125
+ "border-width": 4,
126
+ "border-color": "#e74c3c",
127
+ },
128
+ },
129
+ {
130
+ "selector": "node[type='segment']",
131
+ "style": {
132
+ "shape": "round-rectangle",
133
+ "background-color": "#bdc3c7",
134
+ "width": 40,
135
+ "height": 20,
136
+ "color": "#2c3e50",
137
+ "font-size": "8px",
138
+ },
139
+ },
140
+ # [Original Edges]
141
+ {
142
+ "selector": "edge",
143
+ "style": {
144
+ "width": 1,
145
+ "curve-style": "bezier",
146
+ "opacity": 0.6,
147
+ "arrow-scale": 1,
148
+ },
149
+ },
150
+ {
151
+ "selector": "edge[edgeType='sayou:contains']",
152
+ "style": {
153
+ "line-color": "#7f8c8d",
154
+ "target-arrow-shape": "circle",
155
+ "line-style": "dashed",
156
+ "width": 1.5,
157
+ "opacity": 0.7,
158
+ },
159
+ },
160
+ {
161
+ "selector": "edge[edgeType='sayou:imports']",
162
+ "style": {
163
+ "line-color": "#00d2d3",
164
+ "target-arrow-shape": "triangle",
165
+ "line-style": "dashed",
166
+ "width": 2,
167
+ "opacity": 0.9,
168
+ },
169
+ },
170
+ {
171
+ "selector": "edge[edgeType='sayou:inherits']",
172
+ "style": {
173
+ "line-color": "#ff6b6b",
174
+ "target-arrow-shape": "triangle",
175
+ "width": 3,
176
+ },
177
+ },
178
+ {
179
+ "selector": "edge[edgeType='sayou:next']",
180
+ "style": {
181
+ "line-color": "#f39c12",
182
+ "target-arrow-shape": "triangle",
183
+ "width": 2,
184
+ },
185
+ },
186
+ # =========================================================
187
+ # [CRITICAL FIX] Restored Interaction Styles
188
+ # =========================================================
189
+ {
190
+ "selector": ".highlighted",
191
+ "style": {
192
+ "background-color": "#f1c40f",
193
+ "line-color": "#f1c40f",
194
+ "target-arrow-color": "#f1c40f",
195
+ "opacity": 1,
196
+ "z-index": 999,
197
+ },
198
+ },
199
+ {
200
+ "selector": ".faded",
201
+ "style": {"opacity": 0.05, "label": ""},
202
+ },
203
+ {
204
+ "selector": ".found",
205
+ "style": {
206
+ "border-width": 4,
207
+ "border-color": "#e056fd",
208
+ "background-color": "#e056fd",
209
+ },
210
+ },
211
+ {
212
+ "selector": "node.no-label",
213
+ "style": {
214
+ "text-opacity": 0,
215
+ "text-background-opacity": 0,
216
+ "text-border-opacity": 0,
217
+ },
218
+ },
219
+ {
220
+ "selector": "edge.hidden-edge",
221
+ "style": {"display": "none"},
222
+ },
223
+ ]
@@ -1,7 +1,8 @@
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
4
  from .renderer.pyvis_renderer import PyVisRenderer
5
+ from .renderer.showcase_kg_renderer import ShowcaseKGRenderer
5
6
  from .tracer.graph_tracer import GraphTracer
6
7
  from .tracer.rich_tracer import RichConsoleTracer
7
8
  from .tracer.websocket_tracer import WebSocketTracer
@@ -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"):
@@ -3,6 +3,8 @@ import os
3
3
 
4
4
  from sayou.core.base_component import BaseComponent
5
5
 
6
+ from ..core.styles import ANALYST_STYLE_SHEET, ANALYST_TYPE_MAPPING
7
+
6
8
 
7
9
  class AnalyticKGRenderer(BaseComponent):
8
10
  """
@@ -15,158 +17,7 @@ class AnalyticKGRenderer(BaseComponent):
15
17
 
16
18
  component_name = "AnalyticKGRenderer"
17
19
 
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
- ]
20
+ STYLE_SHEET = ANALYST_STYLE_SHEET
170
21
 
171
22
  def render(self, json_path: str, output_path: str = "sayou_analyst_view.html"):
172
23
  if not os.path.exists(json_path):
@@ -177,57 +28,38 @@ class AnalyticKGRenderer(BaseComponent):
177
28
 
178
29
  elements = []
179
30
 
180
- # 1. Nodes (No Parents logic)
31
+ # 1. Nodes Processing
181
32
  for node in raw_data.get("nodes", []):
182
33
  node_id = node.get("node_id")
183
34
  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", "")
35
+ n_cls = node.get("node_class", "unknown")
36
+
37
+ # [Pure Config Lookup]
38
+ cy_type = ANALYST_TYPE_MAPPING.get(n_cls, "unknown")
39
+
40
+ # [No Logic] Builder가 준 라벨을 그대로 사용
41
+ label = node.get("friendly_name") or attrs.get("label") or node_id
42
+
212
43
  cy_data = {
213
44
  "id": node_id,
214
45
  "label": label,
215
46
  "type": cy_type,
216
- "code": code_text,
47
+ "code": attrs.get("schema:text", ""),
217
48
  "meta": attrs,
218
49
  }
219
50
  elements.append({"group": "nodes", "data": cy_data})
220
51
 
221
- # 2. Edges
52
+ # 2. Edges Processing
222
53
  for edge in raw_data.get("edges", []):
54
+ e_type = edge.get("type", "relates")
223
55
  elements.append(
224
56
  {
225
57
  "group": "edges",
226
58
  "data": {
227
59
  "source": edge.get("source"),
228
60
  "target": edge.get("target"),
229
- "edgeType": edge.get("type", "relates"),
230
- "label": edge.get("type", "").split(":")[-1],
61
+ "edgeType": e_type,
62
+ "label": e_type.split(":")[-1],
231
63
  },
232
64
  }
233
65
  )
@@ -0,0 +1,288 @@
1
+ import json
2
+ import os
3
+
4
+ from sayou.core.base_component import BaseComponent
5
+
6
+ from ..core.styles import HIDDEN_ATTR_PREFIXES, SHOWCASE_STYLE_MAP
7
+
8
+
9
+ class ShowcaseKGRenderer(BaseComponent):
10
+ """
11
+ Renders the Final Stable 3D Knowledge Graph.
12
+ - [Architecture] Removed unstable Post-Processing shaders to fix physics crash.
13
+ - [Visuals] Uses native 'Emissive' materials for Neon aesthetics.
14
+ - [Topology] Distinguishes 'Imports' (Gold/Bright) vs 'Hierarchy' (Dark/Blue).
15
+ """
16
+
17
+ component_name = "ShowcaseKGRenderer"
18
+
19
+ def render(self, json_path: str, output_path: str = "sayou_showcase_3d.html"):
20
+ if not os.path.exists(json_path):
21
+ self._log(f"❌ File not found: {json_path}", level="error")
22
+ return
23
+
24
+ with open(json_path, "r", encoding="utf-8") as f:
25
+ raw_data = json.load(f)
26
+
27
+ nodes = []
28
+ links = []
29
+ existing_ids = set()
30
+
31
+ # 1. Nodes Processing
32
+ for node in raw_data.get("nodes", []):
33
+ node_id = node.get("node_id")
34
+ if not node_id:
35
+ continue
36
+ existing_ids.add(node_id)
37
+
38
+ attrs = node.get("attributes", {})
39
+ n_cls = node.get("node_class", "unknown")
40
+
41
+ style = SHOWCASE_STYLE_MAP.get(n_cls, SHOWCASE_STYLE_MAP["default"])
42
+
43
+ val = attrs.get("val", style["val"])
44
+
45
+ label = node.get("friendly_name") or attrs.get("label") or node_id
46
+
47
+ clean_attrs = self._clean_attributes(attrs)
48
+
49
+ nodes.append(
50
+ {
51
+ "id": node_id,
52
+ "label": label,
53
+ "group": style["group"],
54
+ "color": style["color"],
55
+ "val": val,
56
+ "attributes": clean_attrs,
57
+ }
58
+ )
59
+
60
+ # 2. Edges Processing
61
+ for edge in raw_data.get("edges", []):
62
+ src = edge.get("source")
63
+ tgt = edge.get("target")
64
+ if src in existing_ids and tgt in existing_ids:
65
+ e_type = edge.get("type", "relates")
66
+ is_import = any(
67
+ k in e_type for k in ["import", "calls", "next", "contains"]
68
+ )
69
+ links.append(
70
+ {
71
+ "source": src,
72
+ "target": tgt,
73
+ "type": e_type,
74
+ "is_import": is_import,
75
+ }
76
+ )
77
+
78
+ graph_data = {"nodes": nodes, "links": links}
79
+ self._generate_html(graph_data, output_path)
80
+ self._log(f"✅ Final Visual Showcase generated at: {output_path}")
81
+
82
+ def _clean_attributes(self, attrs):
83
+ """설정 파일 기반 속성 정제"""
84
+ clean = {}
85
+ for k, v in attrs.items():
86
+ if any(k.startswith(prefix) for prefix in HIDDEN_ATTR_PREFIXES):
87
+ continue
88
+ s = str(v)
89
+ if len(s) > 200:
90
+ clean[k] = s[:200] + "..."
91
+ else:
92
+ clean[k] = v
93
+ return clean
94
+
95
+ def _generate_html(self, graph_data, output_path):
96
+ json_str = json.dumps(graph_data)
97
+
98
+ html_content = f"""
99
+ <!DOCTYPE html>
100
+ <html lang="en">
101
+ <head>
102
+ <meta charset="UTF-8">
103
+ <title>Sayou Dataverse</title>
104
+ <script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script>
105
+ <script src="https://unpkg.com/3d-force-graph@1.73.1/dist/3d-force-graph.min.js"></script>
106
+ <style>
107
+ body {{ margin: 0; background-color: #020202; overflow: hidden; font-family: sans-serif; }}
108
+ #graph {{ width: 100%; height: 100vh; }}
109
+ #info {{
110
+ position: absolute; top: 30px; right: 30px; width: 300px;
111
+ background: rgba(15, 20, 30, 0.9);
112
+ border: 1px solid #444; border-left: 4px solid #00d2d3;
113
+ box-shadow: 0 0 20px rgba(0,0,0,0.8);
114
+ color: #fff; padding: 20px; font-size:13px;
115
+ backdrop-filter: blur(5px); display: none; border-radius: 4px; pointer-events: none;
116
+ }}
117
+ .tag {{ display: inline-block; padding: 3px 6px; border-radius: 4px; color: #000; font-weight:bold; font-size:10px; margin-bottom:10px; }}
118
+ h2 {{ margin:0 0 10px 0; font-size:16px; color:#eee; }}
119
+ .row {{ margin-bottom:4px; color:#aaa; border-bottom:1px solid #333; padding-bottom:2px; }}
120
+ .key {{ color:#00d2d3; margin-right:5px; }}
121
+ </style>
122
+ </head>
123
+ <body>
124
+ <div id="graph"></div>
125
+ <div id="info"></div>
126
+
127
+ <script>
128
+ const gData = {json_str};
129
+ const infoDiv = document.getElementById('info');
130
+
131
+ // [State]
132
+ const highlightNodes = new Set();
133
+ const highlightLinks = new Set();
134
+ let hoverNode = null;
135
+ let isFlying = false;
136
+
137
+ // [Init] Big Bang
138
+ gData.nodes.forEach(node => {{
139
+ node.x = Math.random() * 2000 - 1000;
140
+ node.y = Math.random() * 2000 - 1000;
141
+ node.z = Math.random() * 2000 - 1000;
142
+ }});
143
+
144
+ // [Graph Init]
145
+ const Graph = ForceGraph3D()(document.getElementById('graph'))
146
+ .graphData(gData)
147
+ .backgroundColor('#050505')
148
+ .showNavInfo(false)
149
+ .nodeLabel(null)
150
+ .cooldownTicks(50);
151
+
152
+ // [Physics]
153
+ Graph.d3Force('charge').strength(-100);
154
+ Graph.d3Force('link').distance(link => link.is_import ? 10 : 100);
155
+
156
+ // [Visuals]
157
+ Graph
158
+ .nodeThreeObject(node => {{
159
+ let isDimmed = false;
160
+ let isTarget = false;
161
+ if (hoverNode) {{
162
+ if (hoverNode === node || highlightNodes.has(node)) isTarget = true;
163
+ else isDimmed = true;
164
+ }}
165
+ const baseColor = node.color;
166
+ const opacity = isDimmed ? 0.1 : 0.9;
167
+ const emissiveInt = isTarget ? 1.5 : (isDimmed ? 0 : 0.6);
168
+
169
+ const material = new THREE.MeshPhongMaterial({{
170
+ color: baseColor,
171
+ emissive: baseColor,
172
+ emissiveIntensity: emissiveInt,
173
+ transparent: true,
174
+ opacity: opacity,
175
+ shininess: 90
176
+ }});
177
+
178
+ if (node.group === "Document") {{
179
+ const s = node.val;
180
+ const geometry = new THREE.BoxGeometry(s, s, s);
181
+ const edges = new THREE.EdgesGeometry(geometry);
182
+ const lineMat = new THREE.LineBasicMaterial({{ color: baseColor, transparent:true, opacity: isDimmed ? 0.05 : 0.4 }});
183
+ const wireframe = new THREE.LineSegments(edges, lineMat);
184
+ wireframe.add(new THREE.Mesh(new THREE.BoxGeometry(s*0.4, s*0.4, s*0.4), material));
185
+ return wireframe;
186
+ }}
187
+ else if (node.group === "Header") return new THREE.Mesh(new THREE.OctahedronGeometry(node.val * 0.7), material);
188
+ else return new THREE.Mesh(new THREE.IcosahedronGeometry(node.val * 0.6, 2), material);
189
+ }})
190
+ .linkWidth(link => {{
191
+ if (highlightLinks.has(link)) return 3;
192
+ if (hoverNode && !highlightLinks.has(link)) return 0;
193
+ return link.is_import ? 1.5 : 0.5;
194
+ }})
195
+ .linkColor(link => {{
196
+ if (highlightLinks.has(link)) return link.is_import ? '#feca57' : '#00d2d3';
197
+ return link.is_import ? 'rgba(254, 202, 87, 0.4)' : 'rgba(44, 62, 80, 0.3)';
198
+ }})
199
+ .linkDirectionalParticles(link => highlightLinks.has(link) ? 3 : 0)
200
+ .linkDirectionalParticleWidth(3)
201
+ .onNodeHover(node => {{
202
+ if ((!node && !hoverNode) || (node && hoverNode === node)) return;
203
+
204
+ highlightNodes.clear(); highlightLinks.clear();
205
+ if (node) {{
206
+ highlightNodes.add(node);
207
+ gData.links.forEach(link => {{
208
+ if (link.source.id === node.id) {{ highlightNodes.add(link.target); highlightLinks.add(link); }}
209
+ else if (link.target.id === node.id) {{ highlightNodes.add(link.source); highlightLinks.add(link); }}
210
+ }});
211
+ }}
212
+ hoverNode = node || null;
213
+ requestAnimationFrame(() => {{
214
+ Graph
215
+ .nodeThreeObject(Graph.nodeThreeObject())
216
+ .linkWidth(Graph.linkWidth())
217
+ .linkColor(Graph.linkColor())
218
+ .linkDirectionalParticles(Graph.linkDirectionalParticles());
219
+ }});
220
+
221
+ document.body.style.cursor = node ? 'crosshair' : null;
222
+ if (node) {{
223
+ infoDiv.style.display = 'block';
224
+ let html = `<span class='tag' style='background:${{node.color}}'>${{node.group}}</span><h2>${{node.label}}</h2>`;
225
+ if (node.attributes) {{
226
+ for (const [k, v] of Object.entries(node.attributes)) {{
227
+ if(k!=='type') html += `<div class='row'><span class='key'>${{k.split(':').pop()}}:</span> ${{v}}</div>`;
228
+ }}
229
+ }}
230
+ infoDiv.innerHTML = html;
231
+ }} else infoDiv.style.display = 'none';
232
+ }})
233
+ .onNodeClick(node => {{
234
+ if (!node) return;
235
+ isFlying = true;
236
+ const dist = 150;
237
+ const distRatio = 1 + dist/Math.hypot(node.x, node.y, node.z);
238
+ Graph.cameraPosition(
239
+ {{ x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio }},
240
+ node,
241
+ 2000
242
+ );
243
+ setTimeout(() => {{ isFlying = false; }}, 2000);
244
+ }});
245
+
246
+ // [Env]
247
+ const scene = Graph.scene();
248
+ const starsGeo = new THREE.BufferGeometry();
249
+ const pos = new Float32Array(2000 * 3);
250
+ for(let i=0; i<2000*3; i++) pos[i] = (Math.random()-0.5) * 4000;
251
+ starsGeo.setAttribute('position', new THREE.BufferAttribute(pos, 3));
252
+ scene.add(new THREE.Points(starsGeo, new THREE.PointsMaterial({{size:2, color:0xffffff, opacity:0.5, transparent:true}})));
253
+ scene.add(new THREE.AmbientLight(0x222222));
254
+ const light = new THREE.DirectionalLight(0xffffff, 1);
255
+ light.position.set(100, 100, 100);
256
+ scene.add(light);
257
+
258
+ // [Manual Orbit Engine]
259
+ setInterval(() => {{
260
+ if (hoverNode || isFlying) return;
261
+ const cam = Graph.camera();
262
+ const controls = Graph.controls();
263
+ if (!controls) return;
264
+
265
+ const target = controls.target;
266
+ const relX = cam.position.x - target.x;
267
+ const relZ = cam.position.z - target.z;
268
+ const r = Math.sqrt(relX*relX + relZ*relZ);
269
+ let theta = Math.atan2(relZ, relX);
270
+
271
+ theta += 0.0015;
272
+
273
+ const newX = target.x + r * Math.cos(theta);
274
+ const newZ = target.z + r * Math.sin(theta);
275
+
276
+ Graph.cameraPosition(
277
+ {{ x: newX, y: cam.position.y, z: newZ }},
278
+ target,
279
+ 0
280
+ );
281
+ }}, 15);
282
+
283
+ </script>
284
+ </body>
285
+ </html>
286
+ """
287
+ with open(output_path, "w", encoding="utf-8") as f:
288
+ f.write(html_content)
@@ -1,303 +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 ShowcaseKGRenderer(BaseComponent):
9
- """
10
- Renders an immersive 3D Knowledge Graph designed for high-level visualization and presentation.
11
-
12
- Focuses on the 'Big Picture' and aesthetic impact using spatial layouts and semantic coloring.
13
- Ideal for demonstrating data scale, clustering patterns, and global connectivity (Wow Effect),
14
- rather than granular node analysis.
15
- """
16
-
17
- component_name = "ShowcaseKGRenderer"
18
-
19
- def render(self, json_path: str, output_path: str = "kg_view_3d.html"):
20
- if not os.path.exists(json_path):
21
- self._log(f"Output file not found: {json_path}", level="error")
22
- return
23
-
24
- with open(json_path, "r", encoding="utf-8") as f:
25
- raw_data = json.load(f)
26
-
27
- # ---------------------------------------------------------
28
- # [Python Logic]
29
- # ---------------------------------------------------------
30
- nodes_by_source = defaultdict(list)
31
- final_nodes = []
32
- final_links = []
33
-
34
- existing_ids = set()
35
-
36
- for node in raw_data.get("nodes", []):
37
- node_id = node.get("node_id")
38
- existing_ids.add(node_id)
39
- attrs = node.get("attributes", {})
40
- meta = node.get("metadata", {})
41
-
42
- source = meta.get("source") or attrs.get("sayou:source") or "Unknown Source"
43
- nodes_by_source[source].append(node_id)
44
-
45
- sem_type = attrs.get("sayou:semanticType", "text")
46
-
47
- clean_attrs = {}
48
- for k, v in attrs.items():
49
- if isinstance(v, str) and len(v) > 300:
50
- clean_attrs[k] = v[:200] + "..."
51
- else:
52
- clean_attrs[k] = v
53
-
54
- display_label = attrs.get("schema:text", node_id)
55
- if len(display_label) > 30:
56
- display_label = display_label[:30] + "..."
57
-
58
- group = "Chunk"
59
- color = "#1e90ff"
60
- val = 5
61
-
62
- if sem_type in ["h1", "h2", "h3", "title"]:
63
- group = "Header"
64
- color = "#ffa502"
65
- val = 12
66
- elif "list" in sem_type:
67
- group = "List"
68
- color = "#2ed573"
69
- val = 4
70
- elif "table" in sem_type:
71
- group = "Table"
72
- color = "#a55eea"
73
- val = 10
74
- elif "code" in sem_type:
75
- group = "Code"
76
- color = "#ff4757"
77
- val = 8
78
-
79
- final_nodes.append(
80
- {
81
- "id": node_id,
82
- "label": display_label,
83
- "group": group,
84
- "sem_type": sem_type,
85
- "color": color,
86
- "val": val,
87
- "attributes": clean_attrs,
88
- "source": source,
89
- }
90
- )
91
-
92
- for edge in raw_data.get("links", []) + raw_data.get("edges", []):
93
- final_links.append(
94
- {
95
- "source": edge.get("source"),
96
- "target": edge.get("target"),
97
- "relation": edge.get("relation", "relates_to"),
98
- }
99
- )
100
-
101
- for source_name, child_ids in nodes_by_source.items():
102
- if source_name in existing_ids:
103
- continue
104
-
105
- virtual_doc_id = f"VIRTUAL_DOC:{source_name}"
106
-
107
- final_nodes.append(
108
- {
109
- "id": virtual_doc_id,
110
- "label": source_name,
111
- "group": "Document",
112
- "color": "#ff4757",
113
- "val": 30,
114
- "attributes": {
115
- "type": "Virtual Parent",
116
- "child_count": len(child_ids),
117
- },
118
- "is_virtual": True,
119
- }
120
- )
121
-
122
- for child_id in child_ids:
123
- final_links.append(
124
- {
125
- "source": virtual_doc_id,
126
- "target": child_id,
127
- "relation": "CONTAINS",
128
- }
129
- )
130
-
131
- graph_data = {"nodes": final_nodes, "links": final_links}
132
- self._log(f"Rendering KG with Virtual Hierarchy ({len(final_nodes)} nodes)...")
133
-
134
- # ---------------------------------------------------------
135
- # [JS Logic] Renderer
136
- # ---------------------------------------------------------
137
- html_content = f"""
138
- <!DOCTYPE html>
139
- <html lang="en">
140
- <head>
141
- <meta charset="UTF-8">
142
- <title>Sayou Dataverse</title>
143
- <style>
144
- body {{ margin: 0; background-color: #000205; overflow: hidden; }}
145
- #graph {{ width: 100%; height: 100vh; }}
146
-
147
- #info {{
148
- position: absolute; top: 20px; right: 20px; width: 300px;
149
- background: rgba(5, 10, 20, 0.85);
150
- border: 1px solid rgba(0, 242, 255, 0.3);
151
- border-left: 3px solid #00f2ff;
152
- box-shadow: 0 0 20px rgba(0, 242, 255, 0.1);
153
- color: #dff9fb; padding: 20px;
154
- font-family: 'Consolas', 'Monaco', monospace;
155
- backdrop-filter: blur(5px);
156
- display: none; pointer-events: none;
157
- border-radius: 0 10px 10px 0;
158
- }}
159
- .tag {{
160
- display: inline-block; padding: 2px 8px; border-radius: 2px;
161
- font-size: 9px; font-weight: 800; color: #000; margin-bottom: 12px;
162
- text-transform: uppercase; letter-spacing: 1px;
163
- }}
164
- h3 {{
165
- margin: 0 0 12px 0; color: #fff;
166
- border-bottom: 1px dashed #576574; padding-bottom: 8px;
167
- font-size: 14px; line-height: 1.4;
168
- }}
169
- .row {{ font-size: 11px; margin-bottom: 4px; color: #a4b0be; }}
170
- .key {{ color: #00d2d3; margin-right: 5px; }}
171
- </style>
172
- <script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script>
173
- <script src="https://unpkg.com/3d-force-graph@1.73.1/dist/3d-force-graph.min.js"></script>
174
- </head>
175
- <body>
176
- <div id="graph"></div>
177
- <div id="info"></div>
178
-
179
- <script>
180
- const gData = {json.dumps(graph_data)};
181
- const infoDiv = document.getElementById('info');
182
-
183
- const Graph = ForceGraph3D()(document.getElementById('graph'))
184
- .graphData(gData)
185
- .backgroundColor('#000205')
186
- .showNavInfo(false)
187
-
188
- // [Design Logic]
189
- .nodeThreeObject(node => {{
190
- let geometry, material;
191
-
192
- // 1. Virtual Document
193
- if (node.group === "Document") {{
194
- const size = node.val;
195
- geometry = new THREE.BoxGeometry(size, size, size);
196
-
197
- const edges = new THREE.EdgesGeometry(geometry);
198
- material = new THREE.LineBasicMaterial({{
199
- color: node.color,
200
- transparent: true,
201
- opacity: 0.4
202
- }});
203
- const wireframe = new THREE.LineSegments(edges, material);
204
-
205
- const coreGeo = new THREE.BoxGeometry(size*0.2, size*0.2, size*0.2);
206
- const coreMat = new THREE.MeshBasicMaterial({{ color: node.color, wireframe: true }});
207
- wireframe.add(new THREE.Mesh(coreGeo, coreMat));
208
-
209
- return wireframe;
210
- }}
211
-
212
- // 2. Headers (H1~H3): [발광 다이아몬드]
213
- else if (node.group === "Header") {{
214
- geometry = new THREE.OctahedronGeometry(4);
215
- material = new THREE.MeshPhongMaterial({{
216
- color: node.color,
217
- emissive: node.color,
218
- emissiveIntensity: 0.5,
219
- shininess: 100,
220
- flatShading: true
221
- }});
222
- }}
223
-
224
- // 3. Chunk / Text: [데이터 오브]
225
- else {{
226
- // 단순 구 대신 Icosahedron(정이십면체)을 써서 디지털 느낌
227
- geometry = new THREE.IcosahedronGeometry(3, 1);
228
- material = new THREE.MeshLambertMaterial({{
229
- color: node.color,
230
- transparent: true,
231
- opacity: 0.8
232
- }});
233
- }}
234
-
235
- return new THREE.Mesh(geometry, material);
236
- }})
237
-
238
- // [Link Design]
239
- .linkWidth(link => link.relation === "CONTAINS" ? 0 : 0.5)
240
- .linkColor(() => '#2f3542')
241
- .linkDirectionalParticles(link => link.relation === "CONTAINS" ? 1 : 3)
242
- .linkDirectionalParticleWidth(1.2)
243
- .linkDirectionalParticleSpeed(0.006)
244
- .linkDirectionalParticleColor(link => link.relation === "CONTAINS" ? '#57606f' : '#00f2ff')
245
-
246
- // [Interaction]
247
- .onNodeHover(node => {{
248
- document.body.style.cursor = node ? 'crosshair' : null;
249
- if (node) {{
250
- infoDiv.style.display = 'block';
251
-
252
- let tagColor = node.color;
253
- if(node.group === 'Document') tagColor = '#ff4757';
254
-
255
- let html = `<span class='tag' style='background:${{tagColor}}'>${{node.group}}</span>`;
256
- if(node.sem_type) html += `<span class='tag' style='background:#2f3542; color:#fff; margin-left:5px'>${{node.sem_type}}</span>`;
257
-
258
- html += `<h3>${{node.label}}</h3>`;
259
-
260
- if (node.attributes) {{
261
- for (const [k, v] of Object.entries(node.attributes)) {{
262
- if(k === 'type' || k === 'child_count') continue;
263
- let key = k.includes(':') ? k.split(':').pop() : k;
264
- html += `<div class='row'><span class='key'>${{key}}:</span> ${{v}}</div>`;
265
- }}
266
- }}
267
- infoDiv.innerHTML = html;
268
- }} else {{
269
- infoDiv.style.display = 'none';
270
- }}
271
- }})
272
- .onNodeClick(node => {{
273
- const distance = 70;
274
- const distRatio = 1 + distance/Math.hypot(node.x, node.y, node.z);
275
- const newPos = node.x || node.y || node.z
276
- ? {{ x: node.x * distRatio, y: node.y * distRatio, z: node.z * distRatio }}
277
- : {{ x: 0, y: 0, z: distance }};
278
- Graph.cameraPosition(newPos, node, 1500);
279
- }});
280
-
281
- // [Lighting]
282
- const ambientLight = new THREE.AmbientLight(0x222222); // 어두운 기본광
283
- Graph.scene().add(ambientLight);
284
-
285
- const blueLight = new THREE.PointLight(0x00f2ff, 1, 100); // 청록색 포인트 조명
286
- blueLight.position.set(50, 50, 50);
287
- Graph.scene().add(blueLight);
288
-
289
- const pinkLight = new THREE.PointLight(0xff00ff, 0.5, 100); // 핑크색 포인트 조명 (반대편)
290
- pinkLight.position.set(-50, -50, -50);
291
- Graph.scene().add(pinkLight);
292
-
293
- // [Physics]
294
- Graph.d3Force('charge').strength(-50);
295
- Graph.d3Force('link').distance(link => link.relation === "CONTAINS" ? 60 : 30);
296
-
297
- </script>
298
- </body>
299
- </html>
300
- """
301
- with open(output_path, "w", encoding="utf-8") as f:
302
- f.write(html_content)
303
- self._log(f"✅ Semantic 3D KG Showcase saved to: {output_path}")