Skip to content

Commit 0286bc0

Browse files
committed
Add a graph visualisation page
Add a page for viewing the graph, using the Cytoscape.js library.
1 parent 23e0fd6 commit 0286bc0

4 files changed

Lines changed: 185 additions & 5 deletions

File tree

Dockerfile

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Use a slim Python image
2-
FROM python:3.12-slim
2+
FROM python:3.13-slim
33

44

55
RUN pip install uv
@@ -14,12 +14,11 @@ COPY pyproject.toml uv.lock ./
1414

1515
RUN uv sync --no-dev --no-install-project
1616

17-
1817
COPY src/ ./src/
18+
COPY static/ ./static/
19+
COPY templates/ ./templates/
1920
COPY app.py ./app.py
2021

21-
22-
2322
# Set environment variables
2423
ENV PATH="/app/.venv/bin:$PATH"
2524
ENV PORT=3000

app.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import os
22
import logging
3-
from flask import Flask, request, jsonify
3+
from flask import Flask, request, jsonify, render_template
44
from dotenv import load_dotenv
55
from src.generate_graph import generate_graph
66

@@ -19,6 +19,11 @@
1919
def create_app():
2020
app = Flask(__name__)
2121

22+
@app.route('/graph', methods=['GET'])
23+
def graph_page():
24+
"""Serve the Cytoscape graph viewer page."""
25+
return render_template('graph.html')
26+
2227
@app.route('/healthcheck/ready', methods=['GET'])
2328
def health_check():
2429
"""Simple health check endpoint."""

static/css/graph.css

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
body {
2+
margin: 0;
3+
font-family: Arial, sans-serif;
4+
background: #f3f4f6;
5+
color: #111827;
6+
}
7+
8+
header {
9+
padding: 1rem;
10+
background: #111827;
11+
color: #ffffff;
12+
}
13+
14+
main {
15+
display: grid;
16+
grid-template-columns: 1fr 320px;
17+
height: calc(100vh - 72px);
18+
}
19+
20+
#graph {
21+
width: 100%;
22+
height: 100%;
23+
background: #ffffff;
24+
}
25+
26+
.sidebar {
27+
padding: 1rem;
28+
border-left: 1px solid #d1d5db;
29+
background: #f9fafb;
30+
overflow-y: auto;
31+
}
32+
33+
.sidebar h2 {
34+
margin-top: 0;
35+
font-size: 1rem;
36+
}
37+
38+
.sidebar pre {
39+
white-space: pre-wrap;
40+
word-break: break-word;
41+
background: #ffffff;
42+
border: 1px solid #d1d5db;
43+
padding: 0.75rem;
44+
border-radius: 0.375rem;
45+
}
46+
47+
.status {
48+
margin-top: 0.5rem;
49+
font-size: 0.9rem;
50+
color: #374151;
51+
}

templates/graph.html

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>Graph Viewer</title>
7+
<link rel="stylesheet" href="{{ url_for('static', filename='css/graph.css') }}" />
8+
</head>
9+
<body>
10+
<header>
11+
<h1>Graph Viewer</h1>
12+
<p class="status">Loading graph from the /extract endpoint...</p>
13+
</header>
14+
<main>
15+
<div id="graph"></div>
16+
<aside class="sidebar">
17+
<h2></h2>
18+
<pre id="node-details">No node selected.</pre>
19+
</aside>
20+
</main>
21+
22+
<script src="https://unpkg.com/cytoscape@3.4.0/dist/cytoscape.min.js"></script>
23+
<script>
24+
const status = document.querySelector('.status');
25+
const details = document.getElementById('node-details');
26+
27+
function formatNodeData(node) {
28+
const data = node.data();
29+
return JSON.stringify(data, null, 2);
30+
}
31+
32+
async function loadGraph() {
33+
try {
34+
const response = await fetch('/extract');
35+
if (!response.ok) {
36+
throw new Error(`Failed to load graph: ${response.status} ${response.statusText}`);
37+
}
38+
39+
const graphData = await response.json();
40+
41+
status.textContent = 'Graph loaded successfully.';
42+
43+
const cy = cytoscape({
44+
container: document.getElementById('graph'),
45+
boxSelectionEnabled: false,
46+
autoungrabify: true,
47+
elements: graphData,
48+
style: [
49+
{
50+
selector: 'node[type="entity"]',
51+
style: {
52+
'background-color': '#2563eb',
53+
'label': 'data(label)',
54+
'color': '#ffffff',
55+
'text-valign': 'center',
56+
'text-halign': 'center',
57+
'width': 'label',
58+
'padding': '12px',
59+
'shape': 'roundrectangle'
60+
}
61+
},
62+
{
63+
selector: 'node[type="alias"]',
64+
style: {
65+
'background-color': '#f59e0b',
66+
'label': 'data(label)',
67+
'color': '#111827',
68+
'text-valign': 'center',
69+
'text-halign': 'center',
70+
'width': 'label',
71+
'padding': '10px',
72+
'shape': 'ellipse'
73+
}
74+
},
75+
{
76+
selector: 'edge',
77+
style: {
78+
'width': 2,
79+
'line-color': '#9ca3af',
80+
'target-arrow-color': '#9ca3af',
81+
'target-arrow-shape': 'triangle',
82+
'curve-style': 'bezier',
83+
'label': 'data(label)',
84+
'font-size': 10,
85+
'text-rotation': 'autorotate',
86+
'text-margin-y': -8
87+
}
88+
},
89+
{
90+
selector: ':selected',
91+
style: {
92+
'border-width': 3,
93+
'border-color': '#10b981'
94+
}
95+
}
96+
],
97+
layout: {
98+
name: 'cose',
99+
idealEdgeLength: 120,
100+
nodeOverlap: 20,
101+
refresh: 20,
102+
fit: true,
103+
}
104+
});
105+
106+
cy.on('tap', 'node', (event) => {
107+
details.textContent = formatNodeData(event.target);
108+
});
109+
110+
cy.on('tap', (event) => {
111+
if (event.target === cy) {
112+
details.textContent = 'No node selected.';
113+
}
114+
});
115+
} catch (error) {
116+
status.textContent = error.message;
117+
details.textContent = 'Unable to display graph.';
118+
console.error(error);
119+
}
120+
}
121+
122+
loadGraph();
123+
</script>
124+
</body>
125+
</html>

0 commit comments

Comments
 (0)