Skip to content

Commit 47535f4

Browse files
author
Test User
committed
Add TraceFlow view and fix containment diagram issues
- Add dedicated TraceFlow page with Sankey diagram for verification traceability - Remove Sankey diagram from traces view (now only in TraceFlow) - Add TraceFlow link to navigation after Traces - Fix icicle diagram not rendering by using fixed dimensions instead of clientWidth - Fix sunburst centering with viewBox centered on origin - Remove svg-pan-zoom library (caused touch zoom flickering) - Add TraceFlowView specification and verification test
1 parent 473d917 commit 47535f4

File tree

12 files changed

+1529
-70
lines changed

12 files changed

+1529
-70
lines changed

core/src/diagrams.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,3 +1248,61 @@ pub fn generate_containment_d3_tree(registry: &GraphRegistry, short: bool) -> Re
12481248

12491249
Ok(output)
12501250
}
1251+
1252+
/// Generate containment view as D3.js sunburst diagram
1253+
///
1254+
/// Generates a markdown code block with `d3-sunburst` language containing JSON data
1255+
/// that can be rendered as an interactive sunburst diagram in HTML export.
1256+
///
1257+
/// Uses the same hierarchical JSON format as the D3 tree.
1258+
pub fn generate_containment_d3_sunburst(registry: &GraphRegistry, short: bool) -> Result<String, ReqvireError> {
1259+
// Build containment hierarchy structure
1260+
let hierarchy = crate::containment::ContainmentHierarchy::build(registry, short)?;
1261+
1262+
// Convert to D3 tree format (same format works for sunburst)
1263+
let d3_tree = hierarchy.to_d3_tree();
1264+
1265+
// Serialize to JSON
1266+
let json = serde_json::to_string_pretty(&d3_tree)
1267+
.map_err(|e| ReqvireError::SerializationError(format!("Failed to serialize D3 sunburst: {}", e)))?;
1268+
1269+
let mut output = String::new();
1270+
1271+
// Note about element display mode
1272+
if short {
1273+
output.push_str("*Elements filtered to show only root elements (those without hierarchical parent relations within the same file).*\n\n");
1274+
}
1275+
1276+
// Output as d3-sunburst code block
1277+
output.push_str("```d3-sunburst\n");
1278+
output.push_str(&json);
1279+
output.push_str("\n```\n");
1280+
1281+
Ok(output)
1282+
}
1283+
1284+
/// Generate D3.js icicle/partition diagram for containment view
1285+
pub fn generate_containment_d3_icicle(registry: &GraphRegistry, short: bool) -> Result<String, ReqvireError> {
1286+
// Build containment hierarchy structure (same as sunburst/tree)
1287+
let hierarchy = crate::containment::ContainmentHierarchy::build(registry, short)?;
1288+
1289+
// Convert to D3 tree format (works for icicle too)
1290+
let d3_tree = hierarchy.to_d3_tree();
1291+
1292+
// Serialize to JSON
1293+
let json = serde_json::to_string_pretty(&d3_tree)
1294+
.map_err(|e| ReqvireError::SerializationError(format!("Failed to serialize D3 icicle: {}", e)))?;
1295+
1296+
let mut output = String::new();
1297+
1298+
if short {
1299+
output.push_str("*Elements filtered to show only root elements (those without hierarchical parent relations within the same file).*\n\n");
1300+
}
1301+
1302+
// Output as d3-icicle code block
1303+
output.push_str("```d3-icicle\n");
1304+
output.push_str(&json);
1305+
output.push_str("\n```\n");
1306+
1307+
Ok(output)
1308+
}

core/src/export.rs

Lines changed: 114 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const ASSETS: &[(&str, &[u8])] = &[
3131
/// Page descriptions for HTML export pages
3232
const PAGE_DESCRIPTION_CONTAINMENT: &str = r#"# Containment
3333
34-
Interactive tree view showing the physical organization of the model—how elements are structured within folders and files. Click on folders and files to expand/collapse, or click on elements to navigate to their definitions. Use the Expand All/Collapse All buttons to control the tree view."#;
34+
The containment view shows the physical organization of the model—how requirements, verifications, and other elements are structured within folders and files. This hierarchical view helps you understand the model's file structure and navigate to specific elements."#;
3535

3636

3737
const PAGE_DESCRIPTION_MODEL: &str = r#"# Model
@@ -54,6 +54,12 @@ const PAGE_DESCRIPTION_COVERAGE: &str = r#"# Verification Coverage
5454
5555
Coverage analysis focuses on **leaf requirements**—the lowest-level requirements that don't derive others. In MBSE, these are the implementable specifications. The **roll-up strategy** means verifying leaves provides automatic coverage to their ancestors through derivedFrom chains. This report shows verified vs. unverified leaf percentages by file and type, identifying where verification effort is needed."#;
5656

57+
const PAGE_DESCRIPTION_TRACEFLOW: &str = r#"# TraceFlow
58+
59+
The TraceFlow view visualizes the verification traceability flow as an interactive Sankey diagram. It shows how requirements flow from stakeholder needs (user requirements) through system specifications (system requirements) to verifications. Link width indicates the number of connections between elements. Use this view to understand the overall traceability architecture and identify gaps in requirement coverage.
60+
61+
**Instructions:** Use mouse wheel to zoom, drag to pan. Click on nodes to navigate to element definitions. Use the +/-/reset buttons for precise control."#;
62+
5763
/// Copies assets folder to output directory
5864
fn copy_assets_folder(output_dir: &Path) -> Result<(), ReqvireError> {
5965
let assets_dir = output_dir.join("assets");
@@ -232,7 +238,13 @@ fn copy_html_and_assets(src: &Path, dst: &Path, temp_root: &Path) -> Result<(),
232238
/// Post-processes generated HTML files to convert .md references to .html in display text
233239
/// This fixes text like "File: path/to/file.md" that appears in HTML content
234240
fn post_process_html_files(temp_dir: &Path) -> Result<(), ReqvireError> {
235-
let html_files = vec!["index.html", "traces.html", "coverage.html", "containment.html"];
241+
use regex::Regex;
242+
243+
let html_files = vec!["index.html", "traces.html", "traceflow.html", "coverage.html", "containment.html"];
244+
245+
// Only convert .md to .html in heading id attributes and heading text content
246+
// IMPORTANT: Do NOT convert .md in script tags - D3 JSON data and JS code need .md preserved
247+
let id_attr_regex = Regex::new(r#"(id="[^"]*?)\.md""#).unwrap();
236248

237249
for file_name in html_files {
238250
let file_path = temp_dir.join(file_name);
@@ -243,12 +255,15 @@ fn post_process_html_files(temp_dir: &Path) -> Result<(), ReqvireError> {
243255
let content = fs::read_to_string(&file_path)
244256
.map_err(|e| ReqvireError::IoError(e))?;
245257

246-
// Convert .md references to .html in HTML text content and id attributes
247-
// This handles heading text and other display text containing file paths
248-
// Example: <h2 id="file:-path/to/file.md">File: path/to/file.md</h2>
249-
// becomes: <h2 id="file:-path/to/file.html">File: path/to/file.html</h2>
250-
let processed = content
251-
.replace(".md\"", ".html\"") // Fix id attributes and quoted strings
258+
// Convert .md references to .html ONLY in specific contexts:
259+
// 1. ID attributes: id="file:-path/file.md" → id="file:-path/file.html"
260+
// 2. Heading text ending tags: .md</h1>, .md</h2>, etc.
261+
//
262+
// We must NOT convert:
263+
// - Script content (D3 JSON data with "name": "file.md")
264+
// - JavaScript code with .replace(".html", ".md")
265+
let processed = id_attr_regex.replace_all(&content, r#"${1}.html""#);
266+
let processed = processed
252267
.replace(".md</h1>", ".html</h1>")
253268
.replace(".md</h2>", ".html</h2>")
254269
.replace(".md</h3>", ".html</h3>")
@@ -383,13 +398,23 @@ pub fn generate_artifacts_in_temp(
383398
);
384399
let trace_report = trace_generator.generate();
385400
let traces_markdown = trace_generator.generate_markdown(&trace_report);
401+
let sankey_markdown = crate::verification_trace::generate_sankey_markdown(&trace_report);
386402
let traces_content = format!(
387403
"{}\n\n{}",
388404
PAGE_DESCRIPTION_TRACES,
389405
traces_markdown
390406
);
391407
filesystem::write_file("traces.md", traces_content.as_bytes())?;
392408

409+
// Generate traceflow.md (dedicated TraceFlow page with just Sankey diagram)
410+
info!("Generating traceflow.md...");
411+
let traceflow_content = format!(
412+
"{}\n\n{}",
413+
PAGE_DESCRIPTION_TRACEFLOW,
414+
sankey_markdown
415+
);
416+
filesystem::write_file("traceflow.md", traceflow_content.as_bytes())?;
417+
393418
info!("Generating coverage.md...");
394419
let coverage_report = crate::report_coverage::generate_coverage_report(&temp_model_manager.graph_registry);
395420
let coverage_text = coverage_report.format_text();
@@ -400,13 +425,88 @@ pub fn generate_artifacts_in_temp(
400425
);
401426
filesystem::write_file("coverage.md", coverage_content.as_bytes())?;
402427

403-
// Generate containment.md (D3 tree - containment view)
404-
info!("Generating containment.md (D3 tree - containment view)...");
405-
let d3_tree_content = crate::diagrams::generate_containment_d3_tree(&temp_model_manager.graph_registry, false)?;
428+
// Generate containment.md (D3 visualizations - containment view with toggle)
429+
info!("Generating containment.md (D3 visualizations - containment view)...");
430+
let d3_sunburst_content = crate::diagrams::generate_containment_d3_sunburst(&temp_model_manager.graph_registry, false)?;
431+
let d3_icicle_content = crate::diagrams::generate_containment_d3_icicle(&temp_model_manager.graph_registry, false)?;
432+
433+
// Create containment page with view toggle
406434
let containment_content = format!(
407-
"{}\n\n{}",
408-
PAGE_DESCRIPTION_CONTAINMENT,
409-
d3_tree_content
435+
r#"{description}
436+
437+
<div class="view-toggle">
438+
<button id="btn-sunburst" class="view-btn active" onclick="showView('sunburst')">Sunburst</button>
439+
<button id="btn-icicle" class="view-btn" onclick="showView('icicle')">Icicle</button>
440+
</div>
441+
442+
<div id="view-sunburst" class="containment-view">
443+
444+
<p class="view-instructions">Click on segments to zoom in. Click center circle to zoom out. Click the center name link to navigate to the element.</p>
445+
446+
{sunburst}
447+
448+
</div>
449+
450+
<div id="view-icicle" class="containment-view">
451+
452+
<p class="view-instructions">Click on bars to zoom in. Click breadcrumb path to navigate back. Click the element link to open it.</p>
453+
454+
{icicle}
455+
456+
</div>
457+
458+
<script>
459+
// Hide icicle view after page loads (both render first so D3 can calculate dimensions)
460+
document.addEventListener('DOMContentLoaded', function() {{
461+
document.getElementById('view-icicle').style.display = 'none';
462+
}});
463+
464+
function showView(view) {{
465+
// Hide all views
466+
document.querySelectorAll('.containment-view').forEach(el => el.style.display = 'none');
467+
// Remove active from all buttons
468+
document.querySelectorAll('.view-btn').forEach(btn => btn.classList.remove('active'));
469+
// Show selected view
470+
document.getElementById('view-' + view).style.display = 'block';
471+
// Mark button as active
472+
document.getElementById('btn-' + view).classList.add('active');
473+
}}
474+
</script>
475+
476+
<style>
477+
.view-toggle {{
478+
display: flex;
479+
gap: 8px;
480+
margin-bottom: 16px;
481+
}}
482+
.view-btn {{
483+
padding: 8px 20px;
484+
border: 1px solid #BDBDBD;
485+
background: #fff;
486+
border-radius: 4px;
487+
cursor: pointer;
488+
font-size: 14px;
489+
transition: all 0.2s;
490+
}}
491+
.view-btn:hover {{
492+
background: #F5F5F5;
493+
}}
494+
.view-btn.active {{
495+
background: var(--color-primary, #3F51B5);
496+
color: #fff;
497+
border-color: var(--color-primary, #3F51B5);
498+
}}
499+
.view-instructions {{
500+
color: #757575;
501+
font-size: 13px;
502+
margin: 0 0 12px 0;
503+
font-style: italic;
504+
}}
505+
</style>
506+
"#,
507+
description = PAGE_DESCRIPTION_CONTAINMENT,
508+
sunburst = d3_sunburst_content,
509+
icicle = d3_icicle_content
410510
);
411511
filesystem::write_file("containment.md", containment_content.as_bytes())?;
412512

0 commit comments

Comments
 (0)