Skip to content

Commit f1d9b48

Browse files
committed
Improve HTML export: ELK layout for large diagrams and edge highlighting on hover
1 parent badb19e commit f1d9b48

19 files changed

+385
-48
lines changed

README.md

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,9 @@ Reqvire is now available as a plugin for Claude Code! Get AI-assisted requiremen
4242
---
4343

4444
<sub>Figure 1: Example Diagram (click image to browse requirements)</sub>
45-
[![Example Diagram](doc/diagram_1.png)](specifications/SpecificationsRequirements.md#specifications-requirements)
45+
[![Example Diagram](doc/diagram_1.png)](specifications/System/DiagramGeneration.md)
4646

4747
---
48-
49-
<sub>Figure 2: Example Change Impact Report (click image to read specifications)</sub>
50-
51-
[![Example Change Impact Report](doc/change_impact_report_pr.png)](specifications/SpecificationsRequirements.md#requirements-change-propagation)
52-
53-
---
54-
5548
## Get Started
5649

5750
### Installation

core/templates/base.html

Lines changed: 187 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@
66
<title>{title}</title>
77
{styles}
88
<!-- Enhanced mermaid configuration for Reqvire diagrams -->
9-
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
109
<script src="https://cdn.jsdelivr.net/npm/hammerjs@2.0.8/hammer.min.js"></script>
1110
<script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.5.0/dist/svg-pan-zoom.min.js"></script>
12-
<script>
11+
<script type="module">
12+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
13+
import elkLayouts from 'https://cdn.jsdelivr.net/npm/@mermaid-js/layout-elk/dist/mermaid-layout-elk.esm.min.mjs';
14+
15+
// Register ELK layout for large diagrams
16+
mermaid.registerLayoutLoaders(elkLayouts);
17+
1318
// Capture Mermaid errors
1419
mermaid.parseError = function(err, hash) {
1520
console.error('Mermaid Parse Error:', err);
@@ -24,21 +29,29 @@
2429
mermaid.initialize({
2530
startOnLoad: true,
2631
theme: 'neutral',
27-
maxTextSize: 120000,
28-
maxEdges: 2000,
32+
maxTextSize: 5000000,
33+
maxEdges: 50000,
2934
flowchart: {
3035
useMaxWidth: true,
3136
htmlLabels: true,
32-
curve: 'basis'
37+
curve: 'basis',
38+
defaultRenderer: 'elk'
39+
},
40+
layout: 'elk',
41+
elk: {
42+
mergeEdges: true,
43+
nodePlacementStrategy: 'SIMPLE'
3344
},
3445
securityLevel: 'loose',
3546
logLevel: 'error'
3647
});
3748

49+
await mermaid.run();
50+
window.mermaid = mermaid;
51+
3852
// Add pan/zoom to all Mermaid diagrams after rendering
39-
window.addEventListener('load', function() {
40-
setTimeout(function() {
41-
document.querySelectorAll('.mermaid').forEach(function(mermaidContainer) {
53+
setTimeout(function() {
54+
document.querySelectorAll('.mermaid').forEach(function(mermaidContainer) {
4255
var svg = mermaidContainer.querySelector('svg');
4356
if (!svg) return;
4457

@@ -137,9 +150,173 @@
137150
}
138151
});
139152
});
153+
});
154+
}, 500);
155+
156+
// Add CSS for edge highlighting (high specificity for SVG)
157+
var style = document.createElement('style');
158+
style.textContent = `
159+
path.edge-highlighted,
160+
.edge-highlighted path,
161+
svg path.edge-highlighted {
162+
stroke: #ff6b6b !important;
163+
stroke-width: 3px !important;
164+
}
165+
`;
166+
document.head.appendChild(style);
167+
168+
// Add edge highlighting on node hover
169+
setTimeout(function() {
170+
document.querySelectorAll('.mermaid svg').forEach(function(svg) {
171+
var nodes = svg.querySelectorAll('.node');
172+
173+
// Find flowchart-link paths (actual edges)
174+
var edgePaths = svg.querySelectorAll('path.flowchart-link');
175+
// Fallback: find paths in .edges group
176+
if (edgePaths.length === 0) {
177+
var edgesGroup = svg.querySelector('.edges');
178+
if (edgesGroup) {
179+
edgePaths = edgesGroup.querySelectorAll('path');
180+
}
181+
}
182+
183+
// Get SVG point for coordinate transforms
184+
var svgPoint = svg.createSVGPoint();
185+
186+
// Helper: transform point through element's CTM
187+
function getTransformedBBox(element) {
188+
var bbox = element.getBBox();
189+
var ctm = element.getCTM();
190+
if (!ctm) return bbox;
191+
192+
// Transform all four corners
193+
var corners = [
194+
{x: bbox.x, y: bbox.y},
195+
{x: bbox.x + bbox.width, y: bbox.y},
196+
{x: bbox.x, y: bbox.y + bbox.height},
197+
{x: bbox.x + bbox.width, y: bbox.y + bbox.height}
198+
];
199+
200+
var transformed = corners.map(function(c) {
201+
svgPoint.x = c.x;
202+
svgPoint.y = c.y;
203+
var tp = svgPoint.matrixTransform(ctm);
204+
return {x: tp.x, y: tp.y};
205+
});
206+
207+
var minX = Math.min.apply(null, transformed.map(function(p) { return p.x; }));
208+
var maxX = Math.max.apply(null, transformed.map(function(p) { return p.x; }));
209+
var minY = Math.min.apply(null, transformed.map(function(p) { return p.y; }));
210+
var maxY = Math.max.apply(null, transformed.map(function(p) { return p.y; }));
211+
212+
return {x: minX, y: minY, width: maxX - minX, height: maxY - minY};
213+
}
214+
215+
// Helper: get all points from path
216+
function getPathPoints(path) {
217+
var d = path.getAttribute('d') || '';
218+
var points = [];
219+
var ctm = path.getCTM();
220+
221+
// Get M (start) point
222+
var startMatch = d.match(/M\s*([\d.-]+)[,\s]+([\d.-]+)/i);
223+
if (startMatch) {
224+
var sx = parseFloat(startMatch[1]);
225+
var sy = parseFloat(startMatch[2]);
226+
if (ctm) {
227+
svgPoint.x = sx; svgPoint.y = sy;
228+
var tp = svgPoint.matrixTransform(ctm);
229+
points.push({x: tp.x, y: tp.y});
230+
} else {
231+
points.push({x: sx, y: sy});
232+
}
233+
}
234+
235+
// Get last point - find last number pair before Z or end
236+
var allNums = d.match(/[\d.-]+/g);
237+
if (allNums && allNums.length >= 2) {
238+
var ex = parseFloat(allNums[allNums.length - 2]);
239+
var ey = parseFloat(allNums[allNums.length - 1]);
240+
if (ctm) {
241+
svgPoint.x = ex; svgPoint.y = ey;
242+
var tp = svgPoint.matrixTransform(ctm);
243+
points.push({x: tp.x, y: tp.y});
244+
} else {
245+
points.push({x: ex, y: ey});
246+
}
247+
}
248+
249+
return points;
250+
}
251+
252+
// Build node bounding boxes with transforms
253+
var nodeData = {};
254+
nodes.forEach(function(node) {
255+
var nodeId = node.id || '';
256+
var parts = nodeId.split('-');
257+
var hashId = parts.length >= 2 ? parts[1] : nodeId;
258+
259+
nodeData[hashId] = {
260+
node: node,
261+
bbox: getTransformedBBox(node)
262+
};
263+
});
264+
265+
// Check if point is near bbox
266+
function isNearBbox(pt, bbox, margin) {
267+
margin = margin || 40;
268+
return pt.x >= bbox.x - margin && pt.x <= bbox.x + bbox.width + margin &&
269+
pt.y >= bbox.y - margin && pt.y <= bbox.y + bbox.height + margin;
270+
}
271+
272+
// Map edges to nodes
273+
var nodeEdgeMap = {};
274+
Object.keys(nodeData).forEach(function(hashId) {
275+
nodeEdgeMap[hashId] = [];
276+
var bbox = nodeData[hashId].bbox;
277+
278+
edgePaths.forEach(function(path) {
279+
var points = getPathPoints(path);
280+
for (var i = 0; i < points.length; i++) {
281+
if (isNearBbox(points[i], bbox, 40)) {
282+
if (!nodeEdgeMap[hashId].includes(path)) {
283+
nodeEdgeMap[hashId].push(path);
284+
}
285+
break;
286+
}
287+
}
288+
});
289+
});
290+
291+
// Attach hover handlers
292+
nodes.forEach(function(node) {
293+
var nodeId = node.id || '';
294+
var parts = nodeId.split('-');
295+
var hashId = parts.length >= 2 ? parts[1] : nodeId;
296+
297+
node.style.cursor = 'pointer';
298+
299+
node.addEventListener('mouseenter', function(e) {
300+
e.stopPropagation();
301+
nodes.forEach(function(n) { n.style.filter = ''; });
302+
node.style.filter = 'drop-shadow(0 0 6px #ff6b6b)';
303+
304+
var connectedEdges = nodeEdgeMap[hashId] || [];
305+
connectedEdges.forEach(function(path) {
306+
path.classList.add('edge-highlighted');
307+
});
308+
});
309+
310+
node.addEventListener('mouseleave', function() {
311+
node.style.filter = '';
312+
var connectedEdges = nodeEdgeMap[hashId] || [];
313+
connectedEdges.forEach(function(path) {
314+
path.classList.remove('edge-highlighted');
315+
});
316+
});
140317
});
141-
}, 1500);
142-
});
318+
});
319+
}, 1000);
143320
</script>
144321
</head>
145322
<body>

0 commit comments

Comments
 (0)