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.
@@ -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."""