grai-build 0.3.0__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.
- grai/__init__.py +11 -0
- grai/cli/__init__.py +5 -0
- grai/cli/main.py +2546 -0
- grai/core/__init__.py +1 -0
- grai/core/cache/__init__.py +33 -0
- grai/core/cache/build_cache.py +352 -0
- grai/core/compiler/__init__.py +23 -0
- grai/core/compiler/cypher_compiler.py +426 -0
- grai/core/exporter/__init__.py +13 -0
- grai/core/exporter/ir_exporter.py +343 -0
- grai/core/lineage/__init__.py +42 -0
- grai/core/lineage/lineage_tracker.py +685 -0
- grai/core/loader/__init__.py +21 -0
- grai/core/loader/neo4j_loader.py +514 -0
- grai/core/models.py +344 -0
- grai/core/parser/__init__.py +25 -0
- grai/core/parser/yaml_parser.py +375 -0
- grai/core/validator/__init__.py +25 -0
- grai/core/validator/validator.py +475 -0
- grai/core/visualizer/__init__.py +650 -0
- grai/core/visualizer/visualizer.py +15 -0
- grai/templates/__init__.py +1 -0
- grai_build-0.3.0.dist-info/METADATA +374 -0
- grai_build-0.3.0.dist-info/RECORD +28 -0
- grai_build-0.3.0.dist-info/WHEEL +5 -0
- grai_build-0.3.0.dist-info/entry_points.txt +2 -0
- grai_build-0.3.0.dist-info/licenses/LICENSE +21 -0
- grai_build-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive visualization module for knowledge graphs.
|
|
3
|
+
|
|
4
|
+
Provides HTML-based interactive visualizations using D3.js and other web technologies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, Optional
|
|
9
|
+
|
|
10
|
+
from grai.core.lineage import (
|
|
11
|
+
build_lineage_graph,
|
|
12
|
+
export_lineage_to_dict,
|
|
13
|
+
get_lineage_statistics,
|
|
14
|
+
)
|
|
15
|
+
from grai.core.models import Project
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def generate_d3_visualization(
|
|
19
|
+
project: Project,
|
|
20
|
+
output_path: Path,
|
|
21
|
+
title: Optional[str] = None,
|
|
22
|
+
width: int = 1200,
|
|
23
|
+
height: int = 800,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Generate interactive D3.js visualization of the knowledge graph.
|
|
27
|
+
|
|
28
|
+
Creates an HTML file with an interactive force-directed graph using D3.js.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
project: The Project to visualize
|
|
32
|
+
output_path: Path to save the HTML file
|
|
33
|
+
title: Optional title for the visualization (defaults to project name)
|
|
34
|
+
width: Width of the visualization canvas in pixels
|
|
35
|
+
height: Height of the visualization canvas in pixels
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> from grai.core.parser.yaml_parser import load_project
|
|
39
|
+
>>> project = load_project(Path("."))
|
|
40
|
+
>>> generate_d3_visualization(project, Path("graph.html"))
|
|
41
|
+
"""
|
|
42
|
+
# Build lineage graph
|
|
43
|
+
graph = build_lineage_graph(project)
|
|
44
|
+
graph_data = export_lineage_to_dict(graph)
|
|
45
|
+
stats = get_lineage_statistics(graph)
|
|
46
|
+
|
|
47
|
+
# Use project name as default title
|
|
48
|
+
if title is None:
|
|
49
|
+
title = project.name
|
|
50
|
+
|
|
51
|
+
# Generate HTML with embedded D3.js visualization
|
|
52
|
+
html_content = _generate_d3_html(
|
|
53
|
+
title=title,
|
|
54
|
+
graph_data=graph_data,
|
|
55
|
+
stats=stats,
|
|
56
|
+
width=width,
|
|
57
|
+
height=height,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Write to file
|
|
61
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
output_path.write_text(html_content, encoding="utf-8")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def generate_cytoscape_visualization(
|
|
66
|
+
project: Project,
|
|
67
|
+
output_path: Path,
|
|
68
|
+
title: Optional[str] = None,
|
|
69
|
+
width: int = 1200,
|
|
70
|
+
height: int = 800,
|
|
71
|
+
) -> None:
|
|
72
|
+
"""
|
|
73
|
+
Generate interactive Cytoscape.js visualization of the knowledge graph.
|
|
74
|
+
|
|
75
|
+
Creates an HTML file with an interactive graph using Cytoscape.js.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
project: The Project to visualize
|
|
79
|
+
output_path: Path to save the HTML file
|
|
80
|
+
title: Optional title for the visualization (defaults to project name)
|
|
81
|
+
width: Width of the visualization canvas in pixels
|
|
82
|
+
height: Height of the visualization canvas in pixels
|
|
83
|
+
|
|
84
|
+
Example:
|
|
85
|
+
>>> from grai.core.parser.yaml_parser import load_project
|
|
86
|
+
>>> project = load_project(Path("."))
|
|
87
|
+
>>> generate_cytoscape_visualization(project, Path("graph.html"))
|
|
88
|
+
"""
|
|
89
|
+
# Build lineage graph
|
|
90
|
+
graph = build_lineage_graph(project)
|
|
91
|
+
graph_data = export_lineage_to_dict(graph)
|
|
92
|
+
stats = get_lineage_statistics(graph)
|
|
93
|
+
|
|
94
|
+
# Use project name as default title
|
|
95
|
+
if title is None:
|
|
96
|
+
title = project.name
|
|
97
|
+
|
|
98
|
+
# Generate HTML with embedded Cytoscape.js visualization
|
|
99
|
+
html_content = _generate_cytoscape_html(
|
|
100
|
+
title=title,
|
|
101
|
+
graph_data=graph_data,
|
|
102
|
+
stats=stats,
|
|
103
|
+
width=width,
|
|
104
|
+
height=height,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Write to file
|
|
108
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
output_path.write_text(html_content, encoding="utf-8")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _generate_d3_html(
|
|
113
|
+
title: str,
|
|
114
|
+
graph_data: Dict,
|
|
115
|
+
stats: Dict,
|
|
116
|
+
width: int,
|
|
117
|
+
height: int,
|
|
118
|
+
) -> str:
|
|
119
|
+
"""Generate HTML with D3.js force-directed graph."""
|
|
120
|
+
|
|
121
|
+
# Convert graph data to D3 format
|
|
122
|
+
nodes = []
|
|
123
|
+
links = []
|
|
124
|
+
|
|
125
|
+
for node in graph_data["nodes"]:
|
|
126
|
+
nodes.append(
|
|
127
|
+
{
|
|
128
|
+
"id": node["id"],
|
|
129
|
+
"name": node["name"],
|
|
130
|
+
"type": node["type"],
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
for edge in graph_data["edges"]:
|
|
135
|
+
links.append(
|
|
136
|
+
{
|
|
137
|
+
"source": edge["from"],
|
|
138
|
+
"target": edge["to"],
|
|
139
|
+
"type": edge["type"],
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
import json
|
|
144
|
+
|
|
145
|
+
nodes_json = json.dumps(nodes)
|
|
146
|
+
links_json = json.dumps(links)
|
|
147
|
+
|
|
148
|
+
return f"""<!DOCTYPE html>
|
|
149
|
+
<html lang="en">
|
|
150
|
+
<head>
|
|
151
|
+
<meta charset="UTF-8">
|
|
152
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
153
|
+
<title>{title} - Knowledge Graph Visualization</title>
|
|
154
|
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
|
155
|
+
<style>
|
|
156
|
+
body {{
|
|
157
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
158
|
+
margin: 0;
|
|
159
|
+
padding: 20px;
|
|
160
|
+
background: #f5f5f5;
|
|
161
|
+
}}
|
|
162
|
+
.container {{
|
|
163
|
+
max-width: {width + 40}px;
|
|
164
|
+
margin: 0 auto;
|
|
165
|
+
background: white;
|
|
166
|
+
border-radius: 8px;
|
|
167
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
168
|
+
overflow: hidden;
|
|
169
|
+
}}
|
|
170
|
+
.header {{
|
|
171
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
172
|
+
color: white;
|
|
173
|
+
padding: 20px;
|
|
174
|
+
}}
|
|
175
|
+
.header h1 {{
|
|
176
|
+
margin: 0 0 10px 0;
|
|
177
|
+
font-size: 24px;
|
|
178
|
+
}}
|
|
179
|
+
.stats {{
|
|
180
|
+
display: flex;
|
|
181
|
+
gap: 20px;
|
|
182
|
+
font-size: 14px;
|
|
183
|
+
opacity: 0.9;
|
|
184
|
+
}}
|
|
185
|
+
.stat {{
|
|
186
|
+
display: flex;
|
|
187
|
+
align-items: center;
|
|
188
|
+
gap: 5px;
|
|
189
|
+
}}
|
|
190
|
+
#graph {{
|
|
191
|
+
background: white;
|
|
192
|
+
}}
|
|
193
|
+
.legend {{
|
|
194
|
+
padding: 20px;
|
|
195
|
+
background: #f9f9f9;
|
|
196
|
+
border-top: 1px solid #e0e0e0;
|
|
197
|
+
}}
|
|
198
|
+
.legend-title {{
|
|
199
|
+
font-weight: 600;
|
|
200
|
+
margin-bottom: 10px;
|
|
201
|
+
}}
|
|
202
|
+
.legend-items {{
|
|
203
|
+
display: flex;
|
|
204
|
+
gap: 20px;
|
|
205
|
+
flex-wrap: wrap;
|
|
206
|
+
}}
|
|
207
|
+
.legend-item {{
|
|
208
|
+
display: flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
gap: 8px;
|
|
211
|
+
font-size: 14px;
|
|
212
|
+
}}
|
|
213
|
+
.legend-color {{
|
|
214
|
+
width: 16px;
|
|
215
|
+
height: 16px;
|
|
216
|
+
border-radius: 3px;
|
|
217
|
+
}}
|
|
218
|
+
.node {{
|
|
219
|
+
cursor: pointer;
|
|
220
|
+
stroke: #fff;
|
|
221
|
+
stroke-width: 2px;
|
|
222
|
+
}}
|
|
223
|
+
.node.entity {{
|
|
224
|
+
fill: #4fc3f7;
|
|
225
|
+
}}
|
|
226
|
+
.node.relation {{
|
|
227
|
+
fill: #ffd54f;
|
|
228
|
+
}}
|
|
229
|
+
.node.source {{
|
|
230
|
+
fill: #ba68c8;
|
|
231
|
+
}}
|
|
232
|
+
.link {{
|
|
233
|
+
stroke: #999;
|
|
234
|
+
stroke-opacity: 0.6;
|
|
235
|
+
stroke-width: 2px;
|
|
236
|
+
}}
|
|
237
|
+
.node-label {{
|
|
238
|
+
font-size: 12px;
|
|
239
|
+
pointer-events: none;
|
|
240
|
+
text-anchor: middle;
|
|
241
|
+
fill: #333;
|
|
242
|
+
}}
|
|
243
|
+
.tooltip {{
|
|
244
|
+
position: absolute;
|
|
245
|
+
padding: 8px 12px;
|
|
246
|
+
background: rgba(0, 0, 0, 0.8);
|
|
247
|
+
color: white;
|
|
248
|
+
border-radius: 4px;
|
|
249
|
+
font-size: 12px;
|
|
250
|
+
pointer-events: none;
|
|
251
|
+
opacity: 0;
|
|
252
|
+
transition: opacity 0.2s;
|
|
253
|
+
}}
|
|
254
|
+
</style>
|
|
255
|
+
</head>
|
|
256
|
+
<body>
|
|
257
|
+
<div class="container">
|
|
258
|
+
<div class="header">
|
|
259
|
+
<h1>🔍 {title}</h1>
|
|
260
|
+
<div class="stats">
|
|
261
|
+
<div class="stat">
|
|
262
|
+
<span>📊</span>
|
|
263
|
+
<span>{stats['total_nodes']} nodes</span>
|
|
264
|
+
</div>
|
|
265
|
+
<div class="stat">
|
|
266
|
+
<span>🔗</span>
|
|
267
|
+
<span>{stats['total_edges']} edges</span>
|
|
268
|
+
</div>
|
|
269
|
+
<div class="stat">
|
|
270
|
+
<span>🏢</span>
|
|
271
|
+
<span>{stats['entity_count']} entities</span>
|
|
272
|
+
</div>
|
|
273
|
+
<div class="stat">
|
|
274
|
+
<span>↔️</span>
|
|
275
|
+
<span>{stats['relation_count']} relations</span>
|
|
276
|
+
</div>
|
|
277
|
+
<div class="stat">
|
|
278
|
+
<span>📁</span>
|
|
279
|
+
<span>{stats['source_count']} sources</span>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<svg id="graph" width="{width}" height="{height}"></svg>
|
|
285
|
+
|
|
286
|
+
<div class="legend">
|
|
287
|
+
<div class="legend-title">Legend</div>
|
|
288
|
+
<div class="legend-items">
|
|
289
|
+
<div class="legend-item">
|
|
290
|
+
<div class="legend-color" style="background: #4fc3f7;"></div>
|
|
291
|
+
<span>Entity</span>
|
|
292
|
+
</div>
|
|
293
|
+
<div class="legend-item">
|
|
294
|
+
<div class="legend-color" style="background: #ffd54f;"></div>
|
|
295
|
+
<span>Relation</span>
|
|
296
|
+
</div>
|
|
297
|
+
<div class="legend-item">
|
|
298
|
+
<div class="legend-color" style="background: #ba68c8;"></div>
|
|
299
|
+
<span>Source</span>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
<div class="tooltip" id="tooltip"></div>
|
|
306
|
+
|
|
307
|
+
<script>
|
|
308
|
+
const nodes = {nodes_json};
|
|
309
|
+
const links = {links_json};
|
|
310
|
+
|
|
311
|
+
const svg = d3.select("#graph");
|
|
312
|
+
const width = {width};
|
|
313
|
+
const height = {height};
|
|
314
|
+
|
|
315
|
+
// Create force simulation
|
|
316
|
+
const simulation = d3.forceSimulation(nodes)
|
|
317
|
+
.force("link", d3.forceLink(links).id(d => d.id).distance(100))
|
|
318
|
+
.force("charge", d3.forceManyBody().strength(-300))
|
|
319
|
+
.force("center", d3.forceCenter(width / 2, height / 2))
|
|
320
|
+
.force("collision", d3.forceCollide().radius(40));
|
|
321
|
+
|
|
322
|
+
// Create links
|
|
323
|
+
const link = svg.append("g")
|
|
324
|
+
.selectAll("line")
|
|
325
|
+
.data(links)
|
|
326
|
+
.join("line")
|
|
327
|
+
.attr("class", "link");
|
|
328
|
+
|
|
329
|
+
// Create nodes
|
|
330
|
+
const node = svg.append("g")
|
|
331
|
+
.selectAll("circle")
|
|
332
|
+
.data(nodes)
|
|
333
|
+
.join("circle")
|
|
334
|
+
.attr("class", d => `node ${{d.type}}`)
|
|
335
|
+
.attr("r", 15)
|
|
336
|
+
.call(drag(simulation))
|
|
337
|
+
.on("mouseover", showTooltip)
|
|
338
|
+
.on("mouseout", hideTooltip);
|
|
339
|
+
|
|
340
|
+
// Create labels
|
|
341
|
+
const label = svg.append("g")
|
|
342
|
+
.selectAll("text")
|
|
343
|
+
.data(nodes)
|
|
344
|
+
.join("text")
|
|
345
|
+
.attr("class", "node-label")
|
|
346
|
+
.attr("dy", 30)
|
|
347
|
+
.text(d => d.name);
|
|
348
|
+
|
|
349
|
+
// Update positions on tick
|
|
350
|
+
simulation.on("tick", () => {{
|
|
351
|
+
link
|
|
352
|
+
.attr("x1", d => d.source.x)
|
|
353
|
+
.attr("y1", d => d.source.y)
|
|
354
|
+
.attr("x2", d => d.target.x)
|
|
355
|
+
.attr("y2", d => d.target.y);
|
|
356
|
+
|
|
357
|
+
node
|
|
358
|
+
.attr("cx", d => d.x)
|
|
359
|
+
.attr("cy", d => d.y);
|
|
360
|
+
|
|
361
|
+
label
|
|
362
|
+
.attr("x", d => d.x)
|
|
363
|
+
.attr("y", d => d.y);
|
|
364
|
+
}});
|
|
365
|
+
|
|
366
|
+
// Drag functionality
|
|
367
|
+
function drag(simulation) {{
|
|
368
|
+
function dragstarted(event) {{
|
|
369
|
+
if (!event.active) simulation.alphaTarget(0.3).restart();
|
|
370
|
+
event.subject.fx = event.subject.x;
|
|
371
|
+
event.subject.fy = event.subject.y;
|
|
372
|
+
}}
|
|
373
|
+
|
|
374
|
+
function dragged(event) {{
|
|
375
|
+
event.subject.fx = event.x;
|
|
376
|
+
event.subject.fy = event.y;
|
|
377
|
+
}}
|
|
378
|
+
|
|
379
|
+
function dragended(event) {{
|
|
380
|
+
if (!event.active) simulation.alphaTarget(0);
|
|
381
|
+
event.subject.fx = null;
|
|
382
|
+
event.subject.fy = null;
|
|
383
|
+
}}
|
|
384
|
+
|
|
385
|
+
return d3.drag()
|
|
386
|
+
.on("start", dragstarted)
|
|
387
|
+
.on("drag", dragged)
|
|
388
|
+
.on("end", dragended);
|
|
389
|
+
}}
|
|
390
|
+
|
|
391
|
+
// Tooltip functions
|
|
392
|
+
function showTooltip(event, d) {{
|
|
393
|
+
const tooltip = d3.select("#tooltip");
|
|
394
|
+
tooltip
|
|
395
|
+
.style("opacity", 1)
|
|
396
|
+
.style("left", (event.pageX + 10) + "px")
|
|
397
|
+
.style("top", (event.pageY - 10) + "px")
|
|
398
|
+
.html(`<strong>${{d.name}}</strong><br>Type: ${{d.type}}`);
|
|
399
|
+
}}
|
|
400
|
+
|
|
401
|
+
function hideTooltip() {{
|
|
402
|
+
d3.select("#tooltip").style("opacity", 0);
|
|
403
|
+
}}
|
|
404
|
+
</script>
|
|
405
|
+
</body>
|
|
406
|
+
</html>
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _generate_cytoscape_html(
|
|
411
|
+
title: str,
|
|
412
|
+
graph_data: Dict,
|
|
413
|
+
stats: Dict,
|
|
414
|
+
width: int,
|
|
415
|
+
height: int,
|
|
416
|
+
) -> str:
|
|
417
|
+
"""Generate HTML with Cytoscape.js graph."""
|
|
418
|
+
|
|
419
|
+
# Convert graph data to Cytoscape format
|
|
420
|
+
elements = []
|
|
421
|
+
|
|
422
|
+
for node in graph_data["nodes"]:
|
|
423
|
+
elements.append(
|
|
424
|
+
{
|
|
425
|
+
"data": {
|
|
426
|
+
"id": node["id"],
|
|
427
|
+
"label": node["name"],
|
|
428
|
+
"type": node["type"],
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
for edge in graph_data["edges"]:
|
|
434
|
+
elements.append(
|
|
435
|
+
{
|
|
436
|
+
"data": {
|
|
437
|
+
"source": edge["from"],
|
|
438
|
+
"target": edge["to"],
|
|
439
|
+
"label": edge["type"],
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
import json
|
|
445
|
+
|
|
446
|
+
elements_json = json.dumps(elements)
|
|
447
|
+
|
|
448
|
+
return f"""<!DOCTYPE html>
|
|
449
|
+
<html lang="en">
|
|
450
|
+
<head>
|
|
451
|
+
<meta charset="UTF-8">
|
|
452
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
453
|
+
<title>{title} - Knowledge Graph Visualization</title>
|
|
454
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/cytoscape/3.26.0/cytoscape.min.js"></script>
|
|
455
|
+
<style>
|
|
456
|
+
body {{
|
|
457
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
458
|
+
margin: 0;
|
|
459
|
+
padding: 20px;
|
|
460
|
+
background: #f5f5f5;
|
|
461
|
+
}}
|
|
462
|
+
.container {{
|
|
463
|
+
max-width: {width + 40}px;
|
|
464
|
+
margin: 0 auto;
|
|
465
|
+
background: white;
|
|
466
|
+
border-radius: 8px;
|
|
467
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
468
|
+
overflow: hidden;
|
|
469
|
+
}}
|
|
470
|
+
.header {{
|
|
471
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
472
|
+
color: white;
|
|
473
|
+
padding: 20px;
|
|
474
|
+
}}
|
|
475
|
+
.header h1 {{
|
|
476
|
+
margin: 0 0 10px 0;
|
|
477
|
+
font-size: 24px;
|
|
478
|
+
}}
|
|
479
|
+
.stats {{
|
|
480
|
+
display: flex;
|
|
481
|
+
gap: 20px;
|
|
482
|
+
font-size: 14px;
|
|
483
|
+
opacity: 0.9;
|
|
484
|
+
}}
|
|
485
|
+
.stat {{
|
|
486
|
+
display: flex;
|
|
487
|
+
align-items: center;
|
|
488
|
+
gap: 5px;
|
|
489
|
+
}}
|
|
490
|
+
#cy {{
|
|
491
|
+
width: {width}px;
|
|
492
|
+
height: {height}px;
|
|
493
|
+
background: white;
|
|
494
|
+
}}
|
|
495
|
+
.legend {{
|
|
496
|
+
padding: 20px;
|
|
497
|
+
background: #f9f9f9;
|
|
498
|
+
border-top: 1px solid #e0e0e0;
|
|
499
|
+
}}
|
|
500
|
+
.legend-title {{
|
|
501
|
+
font-weight: 600;
|
|
502
|
+
margin-bottom: 10px;
|
|
503
|
+
}}
|
|
504
|
+
.legend-items {{
|
|
505
|
+
display: flex;
|
|
506
|
+
gap: 20px;
|
|
507
|
+
flex-wrap: wrap;
|
|
508
|
+
}}
|
|
509
|
+
.legend-item {{
|
|
510
|
+
display: flex;
|
|
511
|
+
align-items: center;
|
|
512
|
+
gap: 8px;
|
|
513
|
+
font-size: 14px;
|
|
514
|
+
}}
|
|
515
|
+
.legend-color {{
|
|
516
|
+
width: 16px;
|
|
517
|
+
height: 16px;
|
|
518
|
+
border-radius: 3px;
|
|
519
|
+
}}
|
|
520
|
+
</style>
|
|
521
|
+
</head>
|
|
522
|
+
<body>
|
|
523
|
+
<div class="container">
|
|
524
|
+
<div class="header">
|
|
525
|
+
<h1>🔍 {title}</h1>
|
|
526
|
+
<div class="stats">
|
|
527
|
+
<div class="stat">
|
|
528
|
+
<span>📊</span>
|
|
529
|
+
<span>{stats['total_nodes']} nodes</span>
|
|
530
|
+
</div>
|
|
531
|
+
<div class="stat">
|
|
532
|
+
<span>🔗</span>
|
|
533
|
+
<span>{stats['total_edges']} edges</span>
|
|
534
|
+
</div>
|
|
535
|
+
<div class="stat">
|
|
536
|
+
<span>🏢</span>
|
|
537
|
+
<span>{stats['entity_count']} entities</span>
|
|
538
|
+
</div>
|
|
539
|
+
<div class="stat">
|
|
540
|
+
<span>↔️</span>
|
|
541
|
+
<span>{stats['relation_count']} relations</span>
|
|
542
|
+
</div>
|
|
543
|
+
<div class="stat">
|
|
544
|
+
<span>📁</span>
|
|
545
|
+
<span>{stats['source_count']} sources</span>
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
<div id="cy"></div>
|
|
551
|
+
|
|
552
|
+
<div class="legend">
|
|
553
|
+
<div class="legend-title">Legend</div>
|
|
554
|
+
<div class="legend-items">
|
|
555
|
+
<div class="legend-item">
|
|
556
|
+
<div class="legend-color" style="background: #4fc3f7;"></div>
|
|
557
|
+
<span>Entity</span>
|
|
558
|
+
</div>
|
|
559
|
+
<div class="legend-item">
|
|
560
|
+
<div class="legend-color" style="background: #ffd54f;"></div>
|
|
561
|
+
<span>Relation</span>
|
|
562
|
+
</div>
|
|
563
|
+
<div class="legend-item">
|
|
564
|
+
<div class="legend-color" style="background: #ba68c8;"></div>
|
|
565
|
+
<span>Source</span>
|
|
566
|
+
</div>
|
|
567
|
+
</div>
|
|
568
|
+
</div>
|
|
569
|
+
</div>
|
|
570
|
+
|
|
571
|
+
<script>
|
|
572
|
+
const cy = cytoscape({{
|
|
573
|
+
container: document.getElementById('cy'),
|
|
574
|
+
elements: {elements_json},
|
|
575
|
+
style: [
|
|
576
|
+
{{
|
|
577
|
+
selector: 'node',
|
|
578
|
+
style: {{
|
|
579
|
+
'label': 'data(label)',
|
|
580
|
+
'text-valign': 'center',
|
|
581
|
+
'text-halign': 'center',
|
|
582
|
+
'font-size': '12px',
|
|
583
|
+
'background-color': '#4fc3f7',
|
|
584
|
+
'border-width': 2,
|
|
585
|
+
'border-color': '#fff',
|
|
586
|
+
'width': 40,
|
|
587
|
+
'height': 40
|
|
588
|
+
}}
|
|
589
|
+
}},
|
|
590
|
+
{{
|
|
591
|
+
selector: 'node[type="entity"]',
|
|
592
|
+
style: {{
|
|
593
|
+
'background-color': '#4fc3f7',
|
|
594
|
+
'shape': 'roundrectangle'
|
|
595
|
+
}}
|
|
596
|
+
}},
|
|
597
|
+
{{
|
|
598
|
+
selector: 'node[type="relation"]',
|
|
599
|
+
style: {{
|
|
600
|
+
'background-color': '#ffd54f',
|
|
601
|
+
'shape': 'diamond'
|
|
602
|
+
}}
|
|
603
|
+
}},
|
|
604
|
+
{{
|
|
605
|
+
selector: 'node[type="source"]',
|
|
606
|
+
style: {{
|
|
607
|
+
'background-color': '#ba68c8',
|
|
608
|
+
'shape': 'ellipse'
|
|
609
|
+
}}
|
|
610
|
+
}},
|
|
611
|
+
{{
|
|
612
|
+
selector: 'edge',
|
|
613
|
+
style: {{
|
|
614
|
+
'width': 2,
|
|
615
|
+
'line-color': '#999',
|
|
616
|
+
'target-arrow-color': '#999',
|
|
617
|
+
'target-arrow-shape': 'triangle',
|
|
618
|
+
'curve-style': 'bezier',
|
|
619
|
+
'label': 'data(label)',
|
|
620
|
+
'font-size': '10px',
|
|
621
|
+
'text-rotation': 'autorotate',
|
|
622
|
+
'text-margin-y': -10
|
|
623
|
+
}}
|
|
624
|
+
}}
|
|
625
|
+
],
|
|
626
|
+
layout: {{
|
|
627
|
+
name: 'cose',
|
|
628
|
+
animate: true,
|
|
629
|
+
animationDuration: 1000,
|
|
630
|
+
nodeRepulsion: 8000,
|
|
631
|
+
idealEdgeLength: 100,
|
|
632
|
+
edgeElasticity: 100,
|
|
633
|
+
nestingFactor: 5,
|
|
634
|
+
gravity: 80,
|
|
635
|
+
numIter: 1000,
|
|
636
|
+
initialTemp: 200,
|
|
637
|
+
coolingFactor: 0.95,
|
|
638
|
+
minTemp: 1.0
|
|
639
|
+
}}
|
|
640
|
+
}});
|
|
641
|
+
|
|
642
|
+
// Add click handlers
|
|
643
|
+
cy.on('tap', 'node', function(evt) {{
|
|
644
|
+
const node = evt.target;
|
|
645
|
+
console.log('Clicked node:', node.data());
|
|
646
|
+
}});
|
|
647
|
+
</script>
|
|
648
|
+
</body>
|
|
649
|
+
</html>
|
|
650
|
+
"""
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive visualization generator for knowledge graphs.
|
|
3
|
+
|
|
4
|
+
Provides functions to generate interactive HTML visualizations using D3.js and Cytoscape.js.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from grai.core.visualizer import (
|
|
8
|
+
generate_cytoscape_visualization,
|
|
9
|
+
generate_d3_visualization,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"generate_d3_visualization",
|
|
14
|
+
"generate_cytoscape_visualization",
|
|
15
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Example YAML templates for grai.build projects."""
|