// ============================================
// Industry 4.0 Maturity Assessment Tool
// Complete Implementation with All Features
// ============================================
viewof industry40Tool = {
// IEEE Color palette
const colors = {
navy: "#2C3E50",
teal: "#16A085",
orange: "#E67E22",
gray: "#7F8C8D",
lightGray: "#ECF0F1",
white: "#FFFFFF",
red: "#E74C3C",
green: "#27AE60",
purple: "#9B59B6",
yellow: "#F1C40F",
blue: "#3498DB",
lightTeal: "#1ABC9C"
};
// Maturity levels with descriptions
const maturityLevels = [
{ level: 1, name: "Computerization", description: "Basic IT systems, isolated processes", color: colors.red, range: [1, 1.8] },
{ level: 2, name: "Connectivity", description: "Connected systems, data exchange beginning", color: colors.orange, range: [1.8, 2.6] },
{ level: 3, name: "Visibility", description: "Real-time monitoring, digital shadow", color: colors.yellow, range: [2.6, 3.4] },
{ level: 4, name: "Transparency", description: "Root cause analysis, predictive insights", color: colors.teal, range: [3.4, 4.2] },
{ level: 5, name: "Predictability", description: "Autonomous optimization, AI-driven decisions", color: colors.green, range: [4.2, 5.1] }
];
// Six dimensions with benchmarks and investment data
const dimensions = [
{
id: "strategy",
name: "Strategy & Organization",
shortName: "Strategy",
icon: "📊",
descriptions: [
"No digital strategy",
"Basic IT roadmap",
"Digital transformation plan",
"Integrated digital strategy",
"AI-first organization"
],
benchmarks: { manufacturing: 2.8, logistics: 2.5, healthcare: 2.3, energy: 3.1, retail: 2.7 },
investments: {
low: { min: 30000, max: 80000 },
medium: { min: 100000, max: 250000 },
high: { min: 300000, max: 600000 }
},
actions: [
"Develop comprehensive digital transformation roadmap",
"Establish cross-functional governance committee",
"Define digital KPIs and success metrics",
"Create innovation budget allocation",
"Build strategic partnerships with technology providers"
]
},
{
id: "factory",
name: "Smart Factory",
shortName: "Factory",
icon: "🏭",
descriptions: [
"Manual processes",
"Basic PLCs/automation",
"Connected production lines",
"Flexible manufacturing cells",
"Autonomous factory operations"
],
benchmarks: { manufacturing: 3.2, logistics: 2.1, healthcare: 2.0, energy: 2.8, retail: 1.8 },
investments: {
low: { min: 150000, max: 400000 },
medium: { min: 500000, max: 1200000 },
high: { min: 1500000, max: 4000000 }
},
actions: [
"Implement IIoT sensors on critical equipment",
"Deploy collaborative robots (cobots)",
"Integrate MES with production systems",
"Enable flexible manufacturing capabilities",
"Implement digital twin for production optimization"
]
},
{
id: "operations",
name: "Smart Operations",
shortName: "Operations",
icon: "⚙️",
descriptions: [
"Reactive maintenance",
"Scheduled maintenance",
"Condition monitoring",
"Predictive maintenance",
"Prescriptive operations"
],
benchmarks: { manufacturing: 2.9, logistics: 3.0, healthcare: 2.4, energy: 3.3, retail: 2.2 },
investments: {
low: { min: 80000, max: 200000 },
medium: { min: 250000, max: 600000 },
high: { min: 800000, max: 1800000 }
},
actions: [
"Deploy real-time monitoring dashboards",
"Implement condition-based monitoring",
"Build predictive maintenance models",
"Automate quality control processes",
"Enable prescriptive analytics for optimization"
]
},
{
id: "products",
name: "Smart Products",
shortName: "Products",
icon: "📱",
descriptions: [
"No connectivity",
"Basic data logging",
"Connected products",
"Digital twin enabled",
"Autonomous product services"
],
benchmarks: { manufacturing: 2.6, logistics: 2.2, healthcare: 2.8, energy: 2.5, retail: 3.0 },
investments: {
low: { min: 100000, max: 300000 },
medium: { min: 350000, max: 800000 },
high: { min: 1000000, max: 2500000 }
},
actions: [
"Add connectivity to existing products",
"Implement product usage analytics",
"Develop digital twin capabilities",
"Enable remote diagnostics and updates",
"Create product-as-a-service models"
]
},
{
id: "services",
name: "Data-Driven Services",
shortName: "Services",
icon: "📈",
descriptions: [
"Manual reporting",
"Basic dashboards",
"Analytics platform",
"AI/ML insights",
"Autonomous decision systems"
],
benchmarks: { manufacturing: 2.4, logistics: 2.7, healthcare: 2.6, energy: 2.9, retail: 3.2 },
investments: {
low: { min: 60000, max: 150000 },
medium: { min: 180000, max: 400000 },
high: { min: 500000, max: 1200000 }
},
actions: [
"Build centralized analytics platform",
"Develop data monetization strategy",
"Create customer insight dashboards",
"Implement ML-based recommendations",
"Enable automated decision systems"
]
},
{
id: "culture",
name: "Employees & Culture",
shortName: "Culture",
icon: "👥",
descriptions: [
"No digital training",
"Basic IT skills",
"Digital literacy programs",
"Advanced digital skills",
"Innovation-first culture"
],
benchmarks: { manufacturing: 2.5, logistics: 2.4, healthcare: 2.2, energy: 2.6, retail: 2.8 },
investments: {
low: { min: 20000, max: 60000 },
medium: { min: 80000, max: 180000 },
high: { min: 220000, max: 500000 }
},
actions: [
"Launch digital skills training program",
"Create AR-assisted work instructions",
"Establish internal innovation lab",
"Implement change management program",
"Build culture of continuous improvement"
]
}
];
// State management
let state = {
industry: "manufacturing",
companySize: "medium",
scores: { strategy: 2.5, factory: 2, operations: 2.5, products: 2, services: 1.5, culture: 2 },
targetLevel: 4
};
// Company size multipliers
const sizeMultipliers = {
small: { label: "Small (<250 employees)", factor: 0.4 },
medium: { label: "Medium (250-1000)", factor: 1.0 },
large: { label: "Large (>1000)", factor: 2.5 },
enterprise: { label: "Enterprise (>5000)", factor: 5.0 }
};
// Create main container
const container = d3.create("div")
.style("width", "100%")
.style("max-width", "1200px")
.style("margin", "0 auto")
.style("font-family", "system-ui, -apple-system, sans-serif");
// =====================
// HEADER SECTION
// =====================
const header = container.append("div")
.style("background", `linear-gradient(135deg, ${colors.navy} 0%, ${colors.teal} 100%)`)
.style("color", colors.white)
.style("padding", "25px 30px")
.style("border-radius", "12px 12px 0 0")
.style("text-align", "center");
header.append("h2")
.style("margin", "0 0 10px 0")
.style("font-size", "26px")
.text("Industry 4.0 Maturity Assessment");
header.append("p")
.style("margin", "0")
.style("opacity", "0.9")
.style("font-size", "14px")
.text("Evaluate your digital transformation readiness across six key dimensions");
// =====================
// CONFIGURATION SECTION
// =====================
const configSection = container.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(3, 1fr)")
.style("gap", "15px")
.style("padding", "20px")
.style("background", colors.lightGray);
// Industry selector
const industryBox = configSection.append("div")
.style("background", colors.white)
.style("padding", "15px")
.style("border-radius", "8px");
industryBox.append("label")
.style("display", "block")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "8px")
.style("font-size", "13px")
.text("Industry Sector");
const industrySelect = industryBox.append("select")
.style("width", "100%")
.style("padding", "10px")
.style("border", `2px solid ${colors.teal}`)
.style("border-radius", "6px")
.style("font-size", "13px")
.on("change", function() {
state.industry = this.value;
updateAll();
});
[
{ value: "manufacturing", label: "Manufacturing" },
{ value: "logistics", label: "Logistics & Supply Chain" },
{ value: "energy", label: "Energy & Utilities" },
{ value: "healthcare", label: "Healthcare" },
{ value: "retail", label: "Retail & Consumer" }
].forEach(opt => {
industrySelect.append("option")
.attr("value", opt.value)
.text(opt.label);
});
// Company size selector
const sizeBox = configSection.append("div")
.style("background", colors.white)
.style("padding", "15px")
.style("border-radius", "8px");
sizeBox.append("label")
.style("display", "block")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "8px")
.style("font-size", "13px")
.text("Company Size");
const sizeSelect = sizeBox.append("select")
.style("width", "100%")
.style("padding", "10px")
.style("border", `2px solid ${colors.orange}`)
.style("border-radius", "6px")
.style("font-size", "13px")
.on("change", function() {
state.companySize = this.value;
updateAll();
});
Object.entries(sizeMultipliers).forEach(([key, val]) => {
sizeSelect.append("option")
.attr("value", key)
.attr("selected", key === "medium" ? true : null)
.text(val.label);
});
// Target level selector
const targetBox = configSection.append("div")
.style("background", colors.white)
.style("padding", "15px")
.style("border-radius", "8px");
targetBox.append("label")
.style("display", "block")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "8px")
.style("font-size", "13px")
.text("Target Maturity Level");
const targetSlider = targetBox.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "10px");
targetSlider.append("input")
.attr("type", "range")
.attr("min", "2")
.attr("max", "5")
.attr("step", "0.5")
.attr("value", "4")
.style("flex", "1")
.style("accent-color", colors.purple)
.on("input", function() {
state.targetLevel = parseFloat(this.value);
d3.select(".target-value").text(state.targetLevel.toFixed(1));
updateAll();
});
targetSlider.append("span")
.attr("class", "target-value")
.style("font-weight", "bold")
.style("color", colors.purple)
.style("min-width", "30px")
.text("4.0");
// =====================
// MAIN CONTENT AREA
// =====================
const mainContent = container.append("div")
.style("display", "grid")
.style("grid-template-columns", "350px 1fr")
.style("gap", "0")
.style("background", colors.lightGray);
// =====================
// LEFT PANEL - SLIDERS
// =====================
const leftPanel = mainContent.append("div")
.style("background", colors.white)
.style("padding", "20px")
.style("border-right", `1px solid ${colors.lightGray}`);
leftPanel.append("h3")
.style("color", colors.navy)
.style("margin", "0 0 20px 0")
.style("font-size", "16px")
.text("Rate Your Organization (1-5)");
const slidersContainer = leftPanel.append("div");
dimensions.forEach(dim => {
const sliderGroup = slidersContainer.append("div")
.style("margin-bottom", "20px")
.style("padding", "12px")
.style("background", colors.lightGray)
.style("border-radius", "8px");
const labelRow = sliderGroup.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("align-items", "center")
.style("margin-bottom", "8px");
labelRow.append("span")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("font-size", "13px")
.html(`${dim.icon} ${dim.name}`);
const valueDisplay = labelRow.append("span")
.attr("class", `value-${dim.id}`)
.style("background", colors.teal)
.style("color", colors.white)
.style("padding", "3px 12px")
.style("border-radius", "12px")
.style("font-size", "12px")
.style("font-weight", "bold")
.text(state.scores[dim.id].toFixed(1));
sliderGroup.append("input")
.attr("type", "range")
.attr("min", "1")
.attr("max", "5")
.attr("step", "0.5")
.attr("value", state.scores[dim.id])
.style("width", "100%")
.style("height", "8px")
.style("cursor", "pointer")
.style("accent-color", colors.teal)
.on("input", function() {
state.scores[dim.id] = parseFloat(this.value);
d3.select(`.value-${dim.id}`).text(state.scores[dim.id].toFixed(1));
d3.select(`.desc-${dim.id}`).text(dim.descriptions[Math.floor(this.value) - 1]);
updateAll();
});
sliderGroup.append("div")
.attr("class", `desc-${dim.id}`)
.style("font-size", "11px")
.style("color", colors.gray)
.style("margin-top", "6px")
.style("font-style", "italic")
.text(dim.descriptions[Math.floor(state.scores[dim.id]) - 1]);
});
// =====================
// RIGHT PANEL - VISUALIZATIONS
// =====================
const rightPanel = mainContent.append("div")
.style("padding", "20px")
.style("display", "flex")
.style("flex-direction", "column")
.style("gap", "20px");
// Maturity Overview Cards
const overviewCards = rightPanel.append("div")
.attr("class", "overview-cards")
.style("display", "grid")
.style("grid-template-columns", "repeat(3, 1fr)")
.style("gap", "15px");
// Radar Chart Container
const radarContainer = rightPanel.append("div")
.style("background", colors.white)
.style("border-radius", "10px")
.style("padding", "20px")
.style("box-shadow", "0 2px 10px rgba(0,0,0,0.08)");
radarContainer.append("h3")
.style("color", colors.navy)
.style("margin", "0 0 15px 0")
.style("font-size", "16px")
.style("text-align", "center")
.text("Maturity Radar: Current vs Target vs Benchmark");
const radarSvgContainer = radarContainer.append("div")
.attr("class", "radar-chart")
.style("display", "flex")
.style("justify-content", "center");
// Maturity Level Bar
const levelContainer = rightPanel.append("div")
.style("background", colors.white)
.style("border-radius", "10px")
.style("padding", "20px")
.style("box-shadow", "0 2px 10px rgba(0,0,0,0.08)");
levelContainer.append("h3")
.style("color", colors.navy)
.style("margin", "0 0 15px 0")
.style("font-size", "16px")
.text("Overall Maturity Level");
const levelBarContainer = levelContainer.append("div")
.attr("class", "level-bar-container");
// =====================
// BOTTOM SECTION
// =====================
const bottomSection = container.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "20px")
.style("padding", "20px")
.style("background", colors.lightGray);
// Gap Analysis Panel
const gapPanel = bottomSection.append("div")
.style("background", colors.white)
.style("border-radius", "10px")
.style("padding", "20px")
.style("box-shadow", "0 2px 10px rgba(0,0,0,0.08)");
gapPanel.append("h3")
.style("color", colors.navy)
.style("margin", "0 0 15px 0")
.style("font-size", "16px")
.text("Gap Analysis vs Target State");
const gapChartContainer = gapPanel.append("div")
.attr("class", "gap-chart");
// Priority Recommendations Panel
const recPanel = bottomSection.append("div")
.style("background", colors.white)
.style("border-radius", "10px")
.style("padding", "20px")
.style("box-shadow", "0 2px 10px rgba(0,0,0,0.08)");
recPanel.append("h3")
.style("color", colors.navy)
.style("margin", "0 0 15px 0")
.style("font-size", "16px")
.text("Priority Recommendations");
const recContainer = recPanel.append("div")
.attr("class", "recommendations");
// =====================
// ROADMAP & INVESTMENT
// =====================
const roadmapSection = container.append("div")
.style("padding", "20px")
.style("background", colors.lightGray);
const roadmapPanel = roadmapSection.append("div")
.style("background", colors.white)
.style("border-radius", "10px")
.style("padding", "20px")
.style("box-shadow", "0 2px 10px rgba(0,0,0,0.08)");
roadmapPanel.append("h3")
.style("color", colors.navy)
.style("margin", "0 0 15px 0")
.style("font-size", "16px")
.text("Improvement Roadmap & Investment Estimate");
const roadmapContainer = roadmapPanel.append("div")
.attr("class", "roadmap-container");
// Investment Summary
const investmentSummary = roadmapSection.append("div")
.attr("class", "investment-summary")
.style("margin-top", "20px");
// =====================
// FOOTER
// =====================
const footer = container.append("div")
.style("background", colors.navy)
.style("color", colors.white)
.style("padding", "15px 20px")
.style("border-radius", "0 0 12px 12px")
.style("text-align", "center")
.style("font-size", "12px");
footer.append("span")
.text("Based on the ACATECH Industry 4.0 Maturity Index | ");
footer.append("span")
.style("opacity", "0.8")
.text("Investment estimates are indicative only");
// =====================
// UPDATE FUNCTIONS
// =====================
function calculateOverallMaturity() {
const scores = Object.values(state.scores);
return scores.reduce((a, b) => a + b, 0) / scores.length;
}
function getCurrentLevel(score) {
return maturityLevels.find(l => score >= l.range[0] && score < l.range[1]) || maturityLevels[4];
}
function updateOverviewCards() {
const overall = calculateOverallMaturity();
const currentLevel = getCurrentLevel(overall);
const gap = state.targetLevel - overall;
const monthsToTarget = Math.max(0, Math.ceil(gap * 10));
overviewCards.selectAll("*").remove();
// Current Score Card
const currentCard = overviewCards.append("div")
.style("background", `linear-gradient(135deg, ${currentLevel.color}, ${colors.navy})`)
.style("color", colors.white)
.style("padding", "20px")
.style("border-radius", "10px")
.style("text-align", "center");
currentCard.append("div")
.style("font-size", "36px")
.style("font-weight", "bold")
.text(overall.toFixed(1));
currentCard.append("div")
.style("font-size", "12px")
.style("opacity", "0.9")
.text("Current Score");
currentCard.append("div")
.style("font-size", "14px")
.style("font-weight", "600")
.style("margin-top", "5px")
.text(currentLevel.name);
// Target Card
const targetCard = overviewCards.append("div")
.style("background", colors.purple)
.style("color", colors.white)
.style("padding", "20px")
.style("border-radius", "10px")
.style("text-align", "center");
targetCard.append("div")
.style("font-size", "36px")
.style("font-weight", "bold")
.text(state.targetLevel.toFixed(1));
targetCard.append("div")
.style("font-size", "12px")
.style("opacity", "0.9")
.text("Target Level");
const targetLevelName = getCurrentLevel(state.targetLevel).name;
targetCard.append("div")
.style("font-size", "14px")
.style("font-weight", "600")
.style("margin-top", "5px")
.text(targetLevelName);
// Gap Card
const gapCard = overviewCards.append("div")
.style("background", gap > 1.5 ? colors.red : gap > 0.5 ? colors.orange : colors.green)
.style("color", colors.white)
.style("padding", "20px")
.style("border-radius", "10px")
.style("text-align", "center");
gapCard.append("div")
.style("font-size", "36px")
.style("font-weight", "bold")
.text(gap > 0 ? gap.toFixed(1) : "0.0");
gapCard.append("div")
.style("font-size", "12px")
.style("opacity", "0.9")
.text("Gap to Target");
gapCard.append("div")
.style("font-size", "14px")
.style("font-weight", "600")
.style("margin-top", "5px")
.text(`~${monthsToTarget} months`);
}
function updateRadarChart() {
radarSvgContainer.selectAll("*").remove();
const width = 450;
const height = 400;
const centerX = width / 2;
const centerY = height / 2;
const radius = 140;
const svg = radarSvgContainer.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
.attr("width", "100%")
.style("max-height", "400px");
const numDimensions = dimensions.length;
const angleSlice = (2 * Math.PI) / numDimensions;
// Draw grid circles
for (let i = 1; i <= 5; i++) {
const r = (radius / 5) * i;
const points = dimensions.map((_, idx) => {
const angle = angleSlice * idx - Math.PI / 2;
return `${centerX + r * Math.cos(angle)},${centerY + r * Math.sin(angle)}`;
}).join(" ");
svg.append("polygon")
.attr("points", points)
.attr("fill", "none")
.attr("stroke", colors.lightGray)
.attr("stroke-width", 1);
svg.append("text")
.attr("x", centerX + 5)
.attr("y", centerY - r + 4)
.attr("font-size", "9px")
.attr("fill", colors.gray)
.text(i);
}
// Draw axes and labels
dimensions.forEach((dim, idx) => {
const angle = angleSlice * idx - Math.PI / 2;
const x = centerX + radius * Math.cos(angle);
const y = centerY + radius * Math.sin(angle);
svg.append("line")
.attr("x1", centerX)
.attr("y1", centerY)
.attr("x2", x)
.attr("y2", y)
.attr("stroke", colors.lightGray)
.attr("stroke-width", 1);
const labelRadius = radius + 30;
const labelX = centerX + labelRadius * Math.cos(angle);
const labelY = centerY + labelRadius * Math.sin(angle);
svg.append("text")
.attr("x", labelX)
.attr("y", labelY)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("font-size", "11px")
.attr("font-weight", "600")
.attr("fill", colors.navy)
.text(dim.shortName);
});
// Line generator for radar polygons
const lineGen = d3.lineRadial()
.radius(d => (radius / 5) * d.value)
.angle((d, i) => angleSlice * i)
.curve(d3.curveLinearClosed);
// Prepare data
const currentData = dimensions.map(d => ({ value: state.scores[d.id] }));
const targetData = dimensions.map(() => ({ value: state.targetLevel }));
const benchmarkData = dimensions.map(d => ({ value: d.benchmarks[state.industry] }));
// Draw benchmark polygon
svg.append("path")
.datum(benchmarkData)
.attr("d", lineGen)
.attr("transform", `translate(${centerX}, ${centerY})`)
.attr("fill", colors.gray)
.attr("fill-opacity", 0.1)
.attr("stroke", colors.gray)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "5,5");
// Draw target polygon
svg.append("path")
.datum(targetData)
.attr("d", lineGen)
.attr("transform", `translate(${centerX}, ${centerY})`)
.attr("fill", colors.purple)
.attr("fill-opacity", 0.1)
.attr("stroke", colors.purple)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "8,4");
// Draw current state polygon
svg.append("path")
.datum(currentData)
.attr("d", lineGen)
.attr("transform", `translate(${centerX}, ${centerY})`)
.attr("fill", colors.teal)
.attr("fill-opacity", 0.3)
.attr("stroke", colors.teal)
.attr("stroke-width", 3);
// Draw points for current state
dimensions.forEach((dim, idx) => {
const angle = angleSlice * idx - Math.PI / 2;
const r = (radius / 5) * state.scores[dim.id];
const x = centerX + r * Math.cos(angle);
const y = centerY + r * Math.sin(angle);
svg.append("circle")
.attr("cx", x)
.attr("cy", y)
.attr("r", 6)
.attr("fill", colors.teal)
.attr("stroke", colors.white)
.attr("stroke-width", 2);
});
// Legend
const legend = svg.append("g")
.attr("transform", `translate(${width - 130}, 20)`);
const legendItems = [
{ label: "Current", color: colors.teal, dash: "" },
{ label: "Target", color: colors.purple, dash: "8,4" },
{ label: "Benchmark", color: colors.gray, dash: "5,5" }
];
legendItems.forEach((item, i) => {
const ly = i * 22;
legend.append("line")
.attr("x1", 0)
.attr("x2", 25)
.attr("y1", ly + 8)
.attr("y2", ly + 8)
.attr("stroke", item.color)
.attr("stroke-width", 3)
.attr("stroke-dasharray", item.dash);
legend.append("text")
.attr("x", 32)
.attr("y", ly + 12)
.attr("font-size", "11px")
.attr("fill", colors.navy)
.text(item.label);
});
}
function updateLevelBar() {
levelBarContainer.selectAll("*").remove();
const overall = calculateOverallMaturity();
const currentLevel = getCurrentLevel(overall);
const levelBar = levelBarContainer.append("div")
.style("display", "flex")
.style("gap", "4px")
.style("margin-bottom", "15px");
maturityLevels.forEach((ml, idx) => {
const isActive = overall >= ml.range[0];
const isCurrent = ml.level === currentLevel.level;
levelBar.append("div")
.style("flex", "1")
.style("height", "40px")
.style("background", isActive ? ml.color : colors.lightGray)
.style("border-radius", idx === 0 ? "8px 0 0 8px" : idx === 4 ? "0 8px 8px 0" : "0")
.style("display", "flex")
.style("flex-direction", "column")
.style("align-items", "center")
.style("justify-content", "center")
.style("color", isActive ? colors.white : colors.gray)
.style("font-weight", "bold")
.style("font-size", "12px")
.style("border", isCurrent ? `3px solid ${colors.navy}` : "none")
.style("box-sizing", "border-box")
.html(`<span style="font-size: 16px;">${ml.level}</span><span style="font-size: 9px;">${ml.name}</span>`);
});
levelBarContainer.append("div")
.style("text-align", "center")
.style("color", colors.navy)
.style("font-size", "14px")
.html(`<strong>Level ${currentLevel.level}: ${currentLevel.name}</strong> - ${currentLevel.description}`);
}
function updateGapChart() {
gapChartContainer.selectAll("*").remove();
const gapData = dimensions.map(d => ({
name: d.shortName,
fullName: d.name,
current: state.scores[d.id],
target: state.targetLevel,
benchmark: d.benchmarks[state.industry],
gap: state.targetLevel - state.scores[d.id]
})).sort((a, b) => b.gap - a.gap);
const svgWidth = 450;
const svgHeight = 280;
const margin = { top: 20, right: 80, bottom: 40, left: 100 };
const svg = gapChartContainer.append("svg")
.attr("viewBox", `0 0 ${svgWidth} ${svgHeight}`)
.attr("width", "100%");
const y = d3.scaleBand()
.domain(gapData.map(d => d.name))
.range([margin.top, svgHeight - margin.bottom])
.padding(0.3);
const x = d3.scaleLinear()
.domain([0, 5])
.range([margin.left, svgWidth - margin.right]);
// Background bars (target)
svg.selectAll(".target-bar")
.data(gapData)
.join("rect")
.attr("x", margin.left)
.attr("y", d => y(d.name))
.attr("width", d => x(d.target) - margin.left)
.attr("height", y.bandwidth())
.attr("fill", colors.lightGray);
// Current bars
svg.selectAll(".current-bar")
.data(gapData)
.join("rect")
.attr("x", margin.left)
.attr("y", d => y(d.name))
.attr("width", d => x(d.current) - margin.left)
.attr("height", y.bandwidth())
.attr("fill", d => d.gap > 1.5 ? colors.red : d.gap > 0.5 ? colors.orange : colors.green)
.attr("rx", 4);
// Benchmark markers
svg.selectAll(".benchmark")
.data(gapData)
.join("line")
.attr("x1", d => x(d.benchmark))
.attr("x2", d => x(d.benchmark))
.attr("y1", d => y(d.name))
.attr("y2", d => y(d.name) + y.bandwidth())
.attr("stroke", colors.gray)
.attr("stroke-width", 2)
.attr("stroke-dasharray", "3,3");
// Labels
svg.selectAll(".label")
.data(gapData)
.join("text")
.attr("x", margin.left - 8)
.attr("y", d => y(d.name) + y.bandwidth() / 2)
.attr("text-anchor", "end")
.attr("dominant-baseline", "middle")
.attr("font-size", "11px")
.attr("fill", colors.navy)
.text(d => d.name);
// Gap labels
svg.selectAll(".gap-label")
.data(gapData)
.join("text")
.attr("x", svgWidth - margin.right + 10)
.attr("y", d => y(d.name) + y.bandwidth() / 2)
.attr("dominant-baseline", "middle")
.attr("font-size", "11px")
.attr("font-weight", "bold")
.attr("fill", d => d.gap > 1.5 ? colors.red : d.gap > 0.5 ? colors.orange : colors.green)
.text(d => d.gap > 0 ? `Gap: ${d.gap.toFixed(1)}` : "On track");
// X axis
svg.append("g")
.attr("transform", `translate(0, ${svgHeight - margin.bottom})`)
.call(d3.axisBottom(x).ticks(5))
.call(g => g.select(".domain").attr("stroke", colors.gray))
.call(g => g.selectAll(".tick text").attr("fill", colors.navy).attr("font-size", "10px"));
}
function updateRecommendations() {
recContainer.selectAll("*").remove();
const gaps = dimensions.map(d => ({
dimension: d,
current: state.scores[d.id],
gap: state.targetLevel - state.scores[d.id],
benchmark: d.benchmarks[state.industry]
})).filter(g => g.gap > 0).sort((a, b) => b.gap - a.gap);
if (gaps.length === 0) {
recContainer.append("div")
.style("padding", "20px")
.style("text-align", "center")
.style("color", colors.green)
.style("font-weight", "bold")
.text("Congratulations! You have reached your target maturity level.");
return;
}
gaps.slice(0, 4).forEach((item, idx) => {
const priority = item.gap > 1.5 ? "Critical" : item.gap > 0.5 ? "High" : "Medium";
const priorityColor = item.gap > 1.5 ? colors.red : item.gap > 0.5 ? colors.orange : colors.teal;
const rec = recContainer.append("div")
.style("padding", "12px")
.style("margin-bottom", "10px")
.style("background", colors.lightGray)
.style("border-radius", "8px")
.style("border-left", `4px solid ${priorityColor}`);
const header = rec.append("div")
.style("display", "flex")
.style("justify-content", "space-between")
.style("align-items", "center")
.style("margin-bottom", "8px");
header.append("span")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("font-size", "13px")
.text(`#${idx + 1} ${item.dimension.name}`);
header.append("span")
.style("background", priorityColor)
.style("color", colors.white)
.style("padding", "2px 10px")
.style("border-radius", "10px")
.style("font-size", "10px")
.style("font-weight", "bold")
.text(priority);
rec.append("div")
.style("font-size", "12px")
.style("color", colors.gray)
.style("margin-bottom", "6px")
.text(`Current: ${item.current.toFixed(1)} → Target: ${state.targetLevel.toFixed(1)} (Gap: ${item.gap.toFixed(1)})`);
const actionIdx = Math.min(Math.floor(item.current), 4);
rec.append("div")
.style("font-size", "11px")
.style("color", colors.navy)
.html(`<strong>Action:</strong> ${item.dimension.actions[actionIdx]}`);
});
}
function updateRoadmapAndInvestment() {
roadmapContainer.selectAll("*").remove();
investmentSummary.selectAll("*").remove();
const gaps = dimensions.map(d => ({
dimension: d,
gap: Math.max(0, state.targetLevel - state.scores[d.id]),
current: state.scores[d.id]
})).filter(g => g.gap > 0).sort((a, b) => b.gap - a.gap);
// Assign phases and calculate investments
let totalInvestmentMin = 0;
let totalInvestmentMax = 0;
const sizeMultiplier = sizeMultipliers[state.companySize].factor;
const phaseData = gaps.map((item, idx) => {
let phase, startMonth, duration;
if (item.gap > 1.5) {
phase = 1;
startMonth = 0;
duration = 6;
} else if (item.gap > 0.5) {
phase = 2;
startMonth = 6;
duration = 6;
} else {
phase = 3;
startMonth = 12;
duration = 6;
}
const investLevel = item.gap > 1.5 ? "high" : item.gap > 0.5 ? "medium" : "low";
const baseMin = item.dimension.investments[investLevel].min * sizeMultiplier * (item.gap / 2);
const baseMax = item.dimension.investments[investLevel].max * sizeMultiplier * (item.gap / 2);
totalInvestmentMin += baseMin;
totalInvestmentMax += baseMax;
return {
...item,
phase,
startMonth,
duration,
investmentMin: baseMin,
investmentMax: baseMax
};
});
// Draw Gantt-style roadmap
const svgWidth = 800;
const svgHeight = 50 + phaseData.length * 45;
const margin = { top: 40, right: 40, bottom: 30, left: 150 };
const svg = roadmapContainer.append("svg")
.attr("viewBox", `0 0 ${svgWidth} ${svgHeight}`)
.attr("width", "100%");
const x = d3.scaleLinear()
.domain([0, 18])
.range([margin.left, svgWidth - margin.right]);
const y = d3.scaleBand()
.domain(phaseData.map(d => d.dimension.shortName))
.range([margin.top, svgHeight - margin.bottom])
.padding(0.25);
// Phase backgrounds
const phases = [
{ name: "Phase 1", start: 0, end: 6, color: colors.red },
{ name: "Phase 2", start: 6, end: 12, color: colors.orange },
{ name: "Phase 3", start: 12, end: 18, color: colors.teal }
];
phases.forEach(p => {
svg.append("rect")
.attr("x", x(p.start))
.attr("y", margin.top - 25)
.attr("width", x(p.end) - x(p.start))
.attr("height", svgHeight - margin.top - margin.bottom + 40)
.attr("fill", p.color)
.attr("opacity", 0.08);
svg.append("text")
.attr("x", x((p.start + p.end) / 2))
.attr("y", margin.top - 10)
.attr("text-anchor", "middle")
.attr("font-size", "11px")
.attr("font-weight", "bold")
.attr("fill", p.color)
.text(p.name);
});
// Timeline bars
svg.selectAll(".timeline-bar")
.data(phaseData)
.join("rect")
.attr("x", d => x(d.startMonth))
.attr("y", d => y(d.dimension.shortName))
.attr("width", d => x(d.startMonth + d.duration) - x(d.startMonth))
.attr("height", y.bandwidth())
.attr("fill", d => d.phase === 1 ? colors.red : d.phase === 2 ? colors.orange : colors.teal)
.attr("rx", 4)
.attr("opacity", 0.85);
// Labels
svg.selectAll(".dim-label")
.data(phaseData)
.join("text")
.attr("x", margin.left - 10)
.attr("y", d => y(d.dimension.shortName) + y.bandwidth() / 2)
.attr("text-anchor", "end")
.attr("dominant-baseline", "middle")
.attr("font-size", "11px")
.attr("fill", colors.navy)
.text(d => d.dimension.shortName);
// Gap labels on bars
svg.selectAll(".bar-label")
.data(phaseData)
.join("text")
.attr("x", d => x(d.startMonth) + (x(d.startMonth + d.duration) - x(d.startMonth)) / 2)
.attr("y", d => y(d.dimension.shortName) + y.bandwidth() / 2)
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("font-size", "10px")
.attr("font-weight", "bold")
.attr("fill", colors.white)
.text(d => `Gap: ${d.gap.toFixed(1)}`);
// X axis
svg.append("g")
.attr("transform", `translate(0, ${svgHeight - margin.bottom})`)
.call(d3.axisBottom(x).ticks(6).tickFormat(d => `Month ${d}`))
.call(g => g.select(".domain").attr("stroke", colors.gray))
.call(g => g.selectAll(".tick text").attr("fill", colors.navy).attr("font-size", "10px"));
// Investment Summary Card
const investCard = investmentSummary.append("div")
.style("background", `linear-gradient(135deg, ${colors.navy}, ${colors.teal})`)
.style("color", colors.white)
.style("padding", "25px")
.style("border-radius", "12px");
investCard.append("h3")
.style("margin", "0 0 15px 0")
.style("font-size", "18px")
.text("Estimated Total Investment");
const investRange = investCard.append("div")
.style("font-size", "32px")
.style("font-weight", "bold")
.style("margin-bottom", "10px");
const formatCurrency = (val) => {
if (val >= 1000000) return `$${(val / 1000000).toFixed(1)}M`;
return `$${(val / 1000).toFixed(0)}K`;
};
investRange.text(`${formatCurrency(totalInvestmentMin)} - ${formatCurrency(totalInvestmentMax)}`);
investCard.append("div")
.style("opacity", "0.9")
.style("margin-bottom", "20px")
.style("font-size", "13px")
.text(`For ${sizeMultipliers[state.companySize].label.toLowerCase()} company in ${state.industry}`);
// Investment breakdown by dimension
const breakdownGrid = investCard.append("div")
.style("display", "grid")
.style("grid-template-columns", "repeat(3, 1fr)")
.style("gap", "12px");
phaseData.slice(0, 6).forEach(item => {
breakdownGrid.append("div")
.style("background", "rgba(255,255,255,0.15)")
.style("padding", "12px")
.style("border-radius", "8px")
.html(`
<div style="font-size: 11px; opacity: 0.8;">${item.dimension.shortName}</div>
<div style="font-size: 14px; font-weight: bold;">${formatCurrency(item.investmentMin)} - ${formatCurrency(item.investmentMax)}</div>
`);
});
}
function updateAll() {
updateOverviewCards();
updateRadarChart();
updateLevelBar();
updateGapChart();
updateRecommendations();
updateRoadmapAndInvestment();
}
// Initial render
updateAll();
return container.node();
}