nomograph-sysml-core 0.2.0

SysML v2 knowledge graph library -- parser, graph builder, queries, and rendering
Documentation
use crate::SysmlGraph;
use crate::core_traits::KnowledgeGraph;
use crate::core_types::CheckType;

pub struct BadgeData {
    pub label: String,
    pub value: String,
    pub color: String,
    pub completeness: f64,
    pub elements: usize,
    pub relationships: usize,
    pub findings: usize,
}

pub fn compute_badge_data(graph: &SysmlGraph) -> BadgeData {
    let elements = graph.element_count();
    let relationships = graph.relationship_count();

    let all_checks = vec![
        CheckType::OrphanRequirements,
        CheckType::UnverifiedRequirements,
        CheckType::MissingVerification,
        CheckType::UnconnectedPorts,
        CheckType::DanglingReferences,
    ];

    let mut total_findings = 0;
    let mut orphan_count = 0;
    let mut unverified_count = 0;

    for ct in &all_checks {
        let findings = graph.check(ct.clone());
        let count = findings.len();
        total_findings += count;
        match ct {
            CheckType::OrphanRequirements => orphan_count = count,
            CheckType::UnverifiedRequirements => unverified_count = count,
            _ => {}
        }
    }

    let total_requirements = graph
        .elements()
        .iter()
        .filter(|e| e.kind.to_lowercase().contains("requirement"))
        .count();

    let completeness = if total_requirements > 0 {
        let gap = (orphan_count + unverified_count).min(total_requirements);
        1.0 - (gap as f64 / total_requirements as f64)
    } else {
        1.0
    };

    let pct = (completeness * 100.0).round() as u32;
    let value = format!("{pct}% ({elements}E / {relationships}R)");

    let color = if total_findings == 0 {
        "#4c1".to_string()
    } else if completeness >= 0.8 {
        "#dfb317".to_string()
    } else if completeness >= 0.5 {
        "#fe7d37".to_string()
    } else {
        "#e05d44".to_string()
    };

    BadgeData {
        label: "model health".to_string(),
        value,
        color,
        completeness,
        elements,
        relationships,
        findings: total_findings,
    }
}

pub fn render_svg(data: &BadgeData) -> String {
    let label = &data.label;
    let value = &data.value;
    let color = &data.color;

    let label_width = label.len() as u32 * 7 + 12;
    let value_width = value.len() as u32 * 7 + 12;
    let total_width = label_width + value_width;

    let label_x = label_width as f32 / 2.0;
    let value_x = label_width as f32 + value_width as f32 / 2.0;

    format!(
        r##"<svg xmlns="http://www.w3.org/2000/svg" width="{total_width}" height="20" role="img" aria-label="{label}: {value}">
  <title>{label}: {value}</title>
  <linearGradient id="s" x2="0" y2="100%">
    <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
    <stop offset="1" stop-opacity=".1"/>
  </linearGradient>
  <clipPath id="r"><rect width="{total_width}" height="20" rx="3" fill="#fff"/></clipPath>
  <g clip-path="url(#r)">
    <rect width="{label_width}" height="20" fill="#555"/>
    <rect x="{label_width}" width="{value_width}" height="20" fill="{color}"/>
    <rect width="{total_width}" height="20" fill="url(#s)"/>
  </g>
  <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
    <text x="{label_x}" y="15" fill="#010101" fill-opacity=".3">{label}</text>
    <text x="{label_x}" y="14" fill="#fff">{label}</text>
    <text x="{value_x}" y="15" fill="#010101" fill-opacity=".3">{value}</text>
    <text x="{value_x}" y="14" fill="#fff">{value}</text>
  </g>
</svg>"##
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_render_svg_contains_label_and_value() {
        let data = BadgeData {
            label: "model health".to_string(),
            value: "85% (100E / 200R)".to_string(),
            color: "#dfb317".to_string(),
            completeness: 0.85,
            elements: 100,
            relationships: 200,
            findings: 3,
        };
        let svg = render_svg(&data);
        assert!(svg.contains("<svg"));
        assert!(svg.contains("model health"));
        assert!(svg.contains("85% (100E / 200R)"));
        assert!(svg.contains("#dfb317"));
    }

    #[test]
    fn test_color_thresholds() {
        let green = BadgeData {
            label: "x".into(),
            value: "x".into(),
            color: String::new(),
            completeness: 1.0,
            elements: 0,
            relationships: 0,
            findings: 0,
        };
        assert_eq!(compute_color(1.0, 0), "#4c1");
        assert_eq!(compute_color(0.9, 2), "#dfb317");
        assert_eq!(compute_color(0.6, 5), "#fe7d37");
        assert_eq!(compute_color(0.3, 10), "#e05d44");
        let _ = green;
    }

    fn compute_color(completeness: f64, findings: usize) -> &'static str {
        if findings == 0 {
            "#4c1"
        } else if completeness >= 0.8 {
            "#dfb317"
        } else if completeness >= 0.5 {
            "#fe7d37"
        } else {
            "#e05d44"
        }
    }

    #[test]
    fn test_svg_is_valid_xml() {
        let data = BadgeData {
            label: "model health".to_string(),
            value: "100% (50E / 80R)".to_string(),
            color: "#4c1".to_string(),
            completeness: 1.0,
            elements: 50,
            relationships: 80,
            findings: 0,
        };
        let svg = render_svg(&data);
        assert!(svg.starts_with("<svg"));
        assert!(svg.ends_with("</svg>"));
        assert!(svg.contains("</g>"));
    }

    #[test]
    fn test_badge_data_from_eve_model() {
        use crate::core_traits::KnowledgeGraph;
        let results = crate::graph::tests::parse_all_eve();
        let mut graph = crate::SysmlGraph::new();
        graph.index(results).unwrap();
        let data = compute_badge_data(&graph);
        assert!(data.elements > 0);
        assert!(data.relationships > 0);
        assert!(data.completeness >= 0.0);
        assert!(data.completeness <= 1.0);
        assert!(!data.value.is_empty());

        let svg = render_svg(&data);
        assert!(svg.contains("model health"));
        assert!(svg.contains(&format!("{}E", data.elements)));
    }
}