// ============================================
// Privacy Compliance Checker
// Interactive Tool for IoT Education
// ============================================
{
// 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",
darkRed: "#C0392B",
darkGreen: "#1D8348"
};
// Data types configuration
const dataTypes = [
{ id: "location", label: "Location Data", sensitivity: "high", description: "GPS coordinates, movement patterns, geofencing data" },
{ id: "health", label: "Health Data", sensitivity: "very-high", description: "Heart rate, blood pressure, medical conditions, fitness metrics" },
{ id: "biometric", label: "Biometric Data", sensitivity: "very-high", description: "Fingerprints, facial recognition, voice patterns, iris scans" },
{ id: "financial", label: "Financial Data", sensitivity: "high", description: "Payment information, transaction history, account details" },
{ id: "personal_id", label: "Personal Identifiers", sensitivity: "high", description: "Name, email, phone number, device IDs, IP addresses" },
{ id: "behavioral", label: "Behavioral Data", sensitivity: "medium", description: "Usage patterns, preferences, interactions, browsing history" }
];
// User locations configuration
const userLocations = [
{ id: "eu", label: "European Union", flag: "EU", regulations: ["gdpr"], color: colors.blue },
{ id: "usa_ca", label: "USA (California)", flag: "CA", regulations: ["ccpa"], color: colors.orange },
{ id: "usa_other", label: "USA (Other States)", flag: "US", regulations: ["sectoral"], color: colors.gray },
{ id: "uk", label: "United Kingdom", flag: "UK", regulations: ["uk_gdpr"], color: colors.navy },
{ id: "canada", label: "Canada", flag: "CA", regulations: ["pipeda"], color: colors.red },
{ id: "australia", label: "Australia", flag: "AU", regulations: ["privacy_act"], color: colors.green }
];
// Processing activities
const processingActivities = [
{ id: "collection", label: "Collection", description: "Gathering data from users or IoT devices" },
{ id: "storage", label: "Storage", description: "Storing data in databases, cloud, or locally" },
{ id: "sharing", label: "Sharing", description: "Sharing with third parties, partners, or vendors" },
{ id: "analytics", label: "Analytics", description: "Analyzing data for insights and patterns" },
{ id: "profiling", label: "Profiling", description: "Creating user profiles, segmentation, or scoring" }
];
// Regulations database
const regulations = {
gdpr: {
name: "GDPR",
fullName: "General Data Protection Regulation",
region: "EU/EEA",
color: colors.blue,
penalty: "Up to 4% global revenue or 20M EUR",
requirements: {
consent: { required: true, standard: "Explicit, informed, freely given consent required", timeline: "Before processing" },
minimization: { required: true, standard: "Collect only data necessary for stated purpose", timeline: "Ongoing" },
deletion: { required: true, standard: "Right to erasure (right to be forgotten)", timeline: "Within 30 days" },
portability: { required: true, standard: "Provide data in machine-readable format", timeline: "Within 30 days" },
breach_notification: { required: true, standard: "Notify supervisory authority of breach", timeline: "Within 72 hours" },
dpo: { required: "conditional", standard: "Data Protection Officer required for large-scale processing", timeline: "Before processing" },
impact_assessment: { required: "conditional", standard: "DPIA required for high-risk processing", timeline: "Before processing" },
cross_border: { required: true, standard: "Adequacy decision or SCCs for transfers outside EU", timeline: "Before transfer" }
}
},
ccpa: {
name: "CCPA/CPRA",
fullName: "California Consumer Privacy Act / California Privacy Rights Act",
region: "California, USA",
color: colors.orange,
penalty: "$7,500 per intentional violation",
requirements: {
consent: { required: true, standard: "Opt-out for sale/sharing, opt-in for sensitive data", timeline: "At collection" },
minimization: { required: true, standard: "Collect only reasonably necessary data", timeline: "Ongoing" },
deletion: { required: true, standard: "Delete personal information on request", timeline: "Within 45 days" },
portability: { required: true, standard: "Provide portable copy of personal data", timeline: "Within 45 days" },
breach_notification: { required: true, standard: "Notify affected consumers of breach", timeline: "Most expedient time" },
dpo: { required: false, standard: "Not specifically required", timeline: "N/A" },
impact_assessment: { required: true, standard: "Annual cybersecurity audits for high-risk businesses", timeline: "Annually" },
cross_border: { required: false, standard: "No specific cross-border requirements", timeline: "N/A" }
}
},
hipaa: {
name: "HIPAA",
fullName: "Health Insurance Portability and Accountability Act",
region: "USA (Health Data)",
color: colors.red,
penalty: "Up to $1.5M per violation category per year",
requirements: {
consent: { required: true, standard: "Written authorization for PHI use/disclosure", timeline: "Before use" },
minimization: { required: true, standard: "Minimum necessary standard for PHI access", timeline: "Ongoing" },
deletion: { required: false, standard: "6-year retention required", timeline: "After 6 years minimum" },
portability: { required: true, standard: "Patient access to health records", timeline: "Within 30 days" },
breach_notification: { required: true, standard: "Notify individuals and HHS of breach", timeline: "60 days to individuals" },
dpo: { required: true, standard: "Privacy Officer and Security Officer required", timeline: "Before processing" },
impact_assessment: { required: true, standard: "Risk analysis and management required", timeline: "Ongoing" },
cross_border: { required: true, standard: "Business Associate Agreements required", timeline: "Before sharing" }
}
},
coppa: {
name: "COPPA",
fullName: "Children's Online Privacy Protection Act",
region: "USA (Children under 13)",
color: colors.purple,
penalty: "Up to $50,120 per violation",
requirements: {
consent: { required: true, standard: "Verifiable parental consent required", timeline: "Before collection" },
minimization: { required: true, standard: "Collect only data necessary for activity", timeline: "Ongoing" },
deletion: { required: true, standard: "Delete data at parent request", timeline: "Promptly" },
portability: { required: true, standard: "Allow parent to review collected data", timeline: "On request" },
breach_notification: { required: true, standard: "FTC notification guidelines apply", timeline: "As required" },
dpo: { required: false, standard: "Not specifically required", timeline: "N/A" },
impact_assessment: { required: false, standard: "Not specifically required", timeline: "N/A" },
cross_border: { required: false, standard: "Not specifically addressed", timeline: "N/A" }
}
},
uk_gdpr: {
name: "UK GDPR",
fullName: "UK General Data Protection Regulation",
region: "United Kingdom",
color: colors.navy,
penalty: "Up to 17.5M GBP or 4% global revenue",
requirements: {
consent: { required: true, standard: "Clear affirmative action required", timeline: "Before processing" },
minimization: { required: true, standard: "Adequate, relevant, limited to necessity", timeline: "Ongoing" },
deletion: { required: true, standard: "Right to erasure without undue delay", timeline: "Within 30 days" },
portability: { required: true, standard: "Commonly used, machine-readable format", timeline: "Within 30 days" },
breach_notification: { required: true, standard: "Notify ICO of personal data breach", timeline: "Within 72 hours" },
dpo: { required: "conditional", standard: "Required for large-scale processing", timeline: "Before processing" },
impact_assessment: { required: "conditional", standard: "Required for high-risk processing", timeline: "Before processing" },
cross_border: { required: true, standard: "International transfer assessment required", timeline: "Before transfer" }
}
},
pipeda: {
name: "PIPEDA",
fullName: "Personal Information Protection and Electronic Documents Act",
region: "Canada",
color: colors.teal,
penalty: "Up to $100,000 CAD per violation",
requirements: {
consent: { required: true, standard: "Meaningful consent for collection/use", timeline: "Before collection" },
minimization: { required: true, standard: "Limited to purposes identified at collection", timeline: "Ongoing" },
deletion: { required: true, standard: "Destroy when no longer needed", timeline: "Reasonable time" },
portability: { required: true, standard: "Individual access to personal information", timeline: "Within 30 days" },
breach_notification: { required: true, standard: "Notify OPC of significant breaches", timeline: "As soon as feasible" },
dpo: { required: true, standard: "Designate Privacy Officer", timeline: "Before processing" },
impact_assessment: { required: false, standard: "Recommended but not required", timeline: "N/A" },
cross_border: { required: true, standard: "Comparable protection required for transfers", timeline: "Before transfer" }
}
},
privacy_act: {
name: "Privacy Act 1988",
fullName: "Australian Privacy Act",
region: "Australia",
color: colors.green,
penalty: "Up to $50M AUD for serious violations",
requirements: {
consent: { required: true, standard: "Consent required for sensitive information", timeline: "Before collection" },
minimization: { required: true, standard: "Only collect necessary information", timeline: "Ongoing" },
deletion: { required: true, standard: "Reasonable steps to destroy unneeded data", timeline: "When no longer needed" },
portability: { required: true, standard: "Provide access on request", timeline: "Within 30 days" },
breach_notification: { required: true, standard: "Notify OAIC and individuals of eligible breaches", timeline: "As soon as practicable" },
dpo: { required: false, standard: "Not specifically required", timeline: "N/A" },
impact_assessment: { required: "conditional", standard: "PIA for high privacy risk activities", timeline: "Before processing" },
cross_border: { required: true, standard: "APPs must still apply overseas", timeline: "Before transfer" }
}
}
};
// Requirement labels and descriptions
const requirementLabels = {
consent: { label: "Consent Requirements", description: "How user consent must be obtained and managed" },
minimization: { label: "Data Minimization", description: "Limits on what data can be collected" },
deletion: { label: "Right to Deletion", description: "User's right to have data erased" },
portability: { label: "Data Portability", description: "User's right to receive their data" },
breach_notification: { label: "Breach Notification", description: "Requirements for reporting data breaches" },
dpo: { label: "Data Protection Officer", description: "Requirement for privacy leadership" },
impact_assessment: { label: "Privacy Impact Assessment", description: "Assessment of privacy risks" },
cross_border: { label: "Cross-Border Transfers", description: "Rules for international data transfers" }
};
// State management
let state = {
selectedDataTypes: new Set(),
selectedLocations: new Set(),
selectedActivities: new Set(),
checklistStatus: {},
complianceScore: 0
};
// Create main container
const container = d3.create("div")
.style("font-family", "system-ui, -apple-system, sans-serif")
.style("max-width", "1000px")
.style("margin", "0 auto");
// Title
container.append("div")
.style("text-align", "center")
.style("margin-bottom", "15px")
.append("h3")
.style("color", colors.navy)
.style("margin", "0")
.text("Privacy Compliance Checker");
// Main layout - two columns
const mainLayout = container.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "15px")
.style("margin-bottom", "15px");
// === LEFT COLUMN: Data Selection ===
const leftColumn = mainLayout.append("div")
.style("display", "flex")
.style("flex-direction", "column")
.style("gap", "15px");
// Data Types Section
const dataTypesSection = leftColumn.append("div")
.style("background", colors.lightGray)
.style("padding", "15px")
.style("border-radius", "8px");
dataTypesSection.append("div")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "12px")
.style("font-size", "14px")
.text("Data Types Collected");
const dataTypesGrid = dataTypesSection.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "8px");
dataTypes.forEach(dt => {
const sensitivityColor = dt.sensitivity === 'very-high' ? colors.red :
(dt.sensitivity === 'high' ? colors.orange : colors.gray);
const item = dataTypesGrid.append("label")
.style("display", "flex")
.style("align-items", "flex-start")
.style("gap", "8px")
.style("padding", "10px")
.style("background", colors.white)
.style("border-radius", "6px")
.style("cursor", "pointer")
.style("border", `2px solid transparent`)
.style("transition", "all 0.2s")
.style("min-height", "44px");
const checkbox = item.append("input")
.attr("type", "checkbox")
.attr("value", dt.id)
.style("width", "18px")
.style("height", "18px")
.style("cursor", "pointer")
.style("margin-top", "2px")
.style("flex-shrink", "0");
const textDiv = item.append("div")
.style("flex", "1");
textDiv.append("div")
.style("font-size", "12px")
.style("font-weight", "600")
.style("color", colors.navy)
.style("margin-bottom", "2px")
.text(dt.label);
textDiv.append("div")
.style("font-size", "10px")
.style("color", colors.gray)
.style("margin-bottom", "4px")
.text(dt.description);
textDiv.append("div")
.style("display", "inline-block")
.style("font-size", "9px")
.style("padding", "2px 6px")
.style("border-radius", "10px")
.style("background", sensitivityColor)
.style("color", colors.white)
.style("font-weight", "bold")
.text(dt.sensitivity.toUpperCase().replace('-', ' '));
checkbox.on("change", function() {
if (this.checked) {
state.selectedDataTypes.add(dt.id);
item.style("border-color", colors.teal);
} else {
state.selectedDataTypes.delete(dt.id);
item.style("border-color", "transparent");
}
updateResults();
});
});
// User Locations Section
const locationsSection = leftColumn.append("div")
.style("background", colors.lightGray)
.style("padding", "15px")
.style("border-radius", "8px");
locationsSection.append("div")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "12px")
.style("font-size", "14px")
.text("User Locations");
const locationsGrid = locationsSection.append("div")
.style("display", "grid")
.style("grid-template-columns", "1fr 1fr")
.style("gap", "8px");
userLocations.forEach(loc => {
const item = locationsGrid.append("label")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "8px")
.style("padding", "10px 12px")
.style("background", colors.white)
.style("border-radius", "6px")
.style("cursor", "pointer")
.style("border", `2px solid transparent`)
.style("transition", "all 0.2s")
.style("min-height", "44px");
const checkbox = item.append("input")
.attr("type", "checkbox")
.attr("value", loc.id)
.style("width", "18px")
.style("height", "18px")
.style("cursor", "pointer")
.style("flex-shrink", "0");
item.append("span")
.style("display", "inline-block")
.style("padding", "2px 6px")
.style("background", loc.color)
.style("color", colors.white)
.style("border-radius", "4px")
.style("font-size", "10px")
.style("font-weight", "bold")
.text(loc.flag);
item.append("span")
.style("font-size", "12px")
.style("font-weight", "600")
.style("color", colors.navy)
.text(loc.label);
checkbox.on("change", function() {
if (this.checked) {
state.selectedLocations.add(loc.id);
item.style("border-color", loc.color);
} else {
state.selectedLocations.delete(loc.id);
item.style("border-color", "transparent");
}
updateResults();
});
});
// Processing Activities Section
const activitiesSection = leftColumn.append("div")
.style("background", colors.lightGray)
.style("padding", "15px")
.style("border-radius", "8px");
activitiesSection.append("div")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "12px")
.style("font-size", "14px")
.text("Processing Activities");
const activitiesContainer = activitiesSection.append("div")
.style("display", "flex")
.style("flex-wrap", "wrap")
.style("gap", "8px");
processingActivities.forEach(activity => {
const item = activitiesContainer.append("label")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "6px")
.style("padding", "8px 14px")
.style("background", colors.white)
.style("border-radius", "20px")
.style("cursor", "pointer")
.style("border", `2px solid transparent`)
.style("transition", "all 0.2s")
.style("min-height", "44px");
const checkbox = item.append("input")
.attr("type", "checkbox")
.attr("value", activity.id)
.style("width", "16px")
.style("height", "16px")
.style("cursor", "pointer");
item.append("span")
.style("font-size", "12px")
.style("font-weight", "500")
.text(activity.label);
checkbox.on("change", function() {
if (this.checked) {
state.selectedActivities.add(activity.id);
item.style("border-color", colors.purple).style("background", colors.lightGray);
} else {
state.selectedActivities.delete(activity.id);
item.style("border-color", "transparent").style("background", colors.white);
}
updateResults();
});
});
// === RIGHT COLUMN: Results ===
const rightColumn = mainLayout.append("div")
.style("display", "flex")
.style("flex-direction", "column")
.style("gap", "15px");
// Applicable Regulations
const regulationsSection = rightColumn.append("div")
.style("background", colors.navy)
.style("padding", "15px")
.style("border-radius", "8px")
.style("color", colors.white);
regulationsSection.append("div")
.style("font-weight", "bold")
.style("margin-bottom", "12px")
.style("font-size", "14px")
.text("Applicable Regulations");
const regulationsDisplay = regulationsSection.append("div")
.attr("class", "regulations-display");
// Compliance Score
const scoreSection = rightColumn.append("div")
.style("background", colors.lightGray)
.style("padding", "15px")
.style("border-radius", "8px")
.style("text-align", "center");
scoreSection.append("div")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "10px")
.style("font-size", "14px")
.text("Compliance Score");
const scoreDisplay = scoreSection.append("div")
.attr("class", "score-display")
.style("font-size", "56px")
.style("font-weight", "bold")
.style("color", colors.gray)
.text("-%");
const scoreBar = scoreSection.append("div")
.style("background", colors.white)
.style("height", "16px")
.style("border-radius", "8px")
.style("margin-top", "12px")
.style("overflow", "hidden")
.style("border", `1px solid ${colors.gray}`);
const scoreBarFill = scoreBar.append("div")
.style("height", "100%")
.style("width", "0%")
.style("background", colors.gray)
.style("transition", "width 0.5s, background 0.5s")
.style("border-radius", "7px");
const scoreLabel = scoreSection.append("div")
.attr("class", "score-label")
.style("font-size", "12px")
.style("color", colors.gray)
.style("margin-top", "10px")
.text("Select data and locations to calculate");
// Score breakdown
const scoreBreakdown = scoreSection.append("div")
.attr("class", "score-breakdown")
.style("display", "none")
.style("margin-top", "15px")
.style("padding-top", "15px")
.style("border-top", `1px solid ${colors.gray}`);
// Requirements Checklist
const checklistSection = container.append("div")
.style("background", colors.lightGray)
.style("padding", "15px")
.style("border-radius", "8px")
.style("margin-bottom", "15px");
checklistSection.append("div")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "12px")
.style("font-size", "14px")
.text("Requirements Checklist");
const checklistContainer = checklistSection.append("div")
.attr("class", "checklist-container");
// Remediation Suggestions
const remediationSection = container.append("div")
.style("background", colors.white)
.style("border", `2px solid ${colors.orange}`)
.style("padding", "15px")
.style("border-radius", "8px")
.style("display", "none");
remediationSection.append("div")
.style("font-weight", "bold")
.style("color", colors.orange)
.style("margin-bottom", "12px")
.style("font-size", "14px")
.text("Remediation Suggestions");
const remediationContent = remediationSection.append("div")
.attr("class", "remediation-content");
// === UPDATE FUNCTIONS ===
function getApplicableRegulations() {
const applicable = new Set();
// Add regulations based on location
state.selectedLocations.forEach(locId => {
const location = userLocations.find(l => l.id === locId);
if (location) {
location.regulations.forEach(reg => {
if (regulations[reg]) {
applicable.add(reg);
}
});
}
});
// Add HIPAA if health data in US
if (state.selectedDataTypes.has('health') &&
(state.selectedLocations.has('usa_ca') || state.selectedLocations.has('usa_other'))) {
applicable.add('hipaa');
}
// COPPA is always potentially applicable in US for any data collection
if (state.selectedLocations.has('usa_ca') || state.selectedLocations.has('usa_other')) {
applicable.add('coppa');
}
return applicable;
}
function updateRegulationsDisplay() {
const applicable = getApplicableRegulations();
regulationsDisplay.selectAll("*").remove();
if (applicable.size === 0) {
regulationsDisplay.append("div")
.style("color", colors.lightGray)
.style("font-style", "italic")
.style("font-size", "12px")
.style("padding", "20px 0")
.style("text-align", "center")
.text("Select user locations to see applicable regulations");
return;
}
const regArray = Array.from(applicable);
regArray.forEach(regId => {
const reg = regulations[regId];
if (!reg) return;
const item = regulationsDisplay.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("justify-content", "space-between")
.style("padding", "12px")
.style("background", reg.color)
.style("border-radius", "6px")
.style("margin-bottom", "8px");
const leftPart = item.append("div");
leftPart.append("div")
.style("font-weight", "bold")
.style("font-size", "14px")
.text(reg.name);
leftPart.append("div")
.style("font-size", "10px")
.style("opacity", "0.9")
.text(reg.region);
item.append("div")
.style("font-size", "9px")
.style("text-align", "right")
.style("opacity", "0.8")
.style("max-width", "120px")
.text(reg.penalty);
});
}
function updateChecklist() {
const applicable = getApplicableRegulations();
checklistContainer.selectAll("*").remove();
if (applicable.size === 0) {
checklistContainer.append("div")
.style("color", colors.gray)
.style("font-style", "italic")
.style("font-size", "12px")
.style("padding", "20px 0")
.style("text-align", "center")
.text("Select data types and locations to generate checklist");
return;
}
// Group requirements across regulations
const requirementGroups = {};
Object.keys(requirementLabels).forEach(reqId => {
requirementGroups[reqId] = {
...requirementLabels[reqId],
items: []
};
});
applicable.forEach(regId => {
const reg = regulations[regId];
if (!reg) return;
Object.entries(reg.requirements).forEach(([reqId, req]) => {
if (requirementGroups[reqId]) {
requirementGroups[reqId].items.push({
regulation: reg.name,
regColor: reg.color,
required: req.required,
standard: req.standard,
timeline: req.timeline
});
}
});
});
// Render requirement groups
Object.entries(requirementGroups).forEach(([groupId, group]) => {
if (group.items.length === 0) return;
const groupDiv = checklistContainer.append("div")
.style("margin-bottom", "20px");
groupDiv.append("div")
.style("font-weight", "bold")
.style("color", colors.navy)
.style("margin-bottom", "6px")
.style("font-size", "13px")
.text(group.label);
groupDiv.append("div")
.style("font-size", "11px")
.style("color", colors.gray)
.style("margin-bottom", "10px")
.text(group.description);
const itemsContainer = groupDiv.append("div")
.style("display", "grid")
.style("gap", "8px");
group.items.forEach((item, idx) => {
const itemId = `${groupId}-${idx}`;
const isRequired = item.required === true;
const isConditional = item.required === 'conditional';
const itemDiv = itemsContainer.append("label")
.style("display", "flex")
.style("align-items", "flex-start")
.style("gap", "12px")
.style("padding", "12px")
.style("background", colors.white)
.style("border-radius", "6px")
.style("cursor", "pointer")
.style("border-left", `4px solid ${item.regColor}`)
.style("transition", "background 0.2s");
const checkbox = itemDiv.append("input")
.attr("type", "checkbox")
.attr("id", itemId)
.style("width", "20px")
.style("height", "20px")
.style("margin-top", "2px")
.style("cursor", "pointer")
.style("flex-shrink", "0");
if (state.checklistStatus[itemId]) {
checkbox.property("checked", true);
}
checkbox.on("change", function() {
state.checklistStatus[itemId] = this.checked;
if (this.checked) {
itemDiv.style("background", "#E8F8F5");
} else {
itemDiv.style("background", colors.white);
}
calculateScore();
});
// Apply background if already checked
if (state.checklistStatus[itemId]) {
itemDiv.style("background", "#E8F8F5");
}
const textDiv = itemDiv.append("div")
.style("flex", "1");
const headerDiv = textDiv.append("div")
.style("display", "flex")
.style("gap", "8px")
.style("align-items", "center")
.style("flex-wrap", "wrap")
.style("margin-bottom", "6px");
headerDiv.append("span")
.style("font-weight", "bold")
.style("font-size", "12px")
.style("color", item.regColor)
.text(item.regulation);
headerDiv.append("span")
.style("font-size", "9px")
.style("padding", "2px 8px")
.style("border-radius", "10px")
.style("background", isRequired ? colors.red : (isConditional ? colors.orange : colors.gray))
.style("color", colors.white)
.style("font-weight", "bold")
.text(isRequired ? 'REQUIRED' : (isConditional ? 'CONDITIONAL' : 'OPTIONAL'));
if (item.timeline && item.timeline !== 'N/A') {
headerDiv.append("span")
.style("font-size", "9px")
.style("padding", "2px 8px")
.style("border-radius", "10px")
.style("background", colors.lightGray)
.style("color", colors.gray)
.text(item.timeline);
}
textDiv.append("div")
.style("font-size", "11px")
.style("color", colors.gray)
.style("line-height", "1.4")
.text(item.standard);
});
});
}
function calculateScore() {
const applicable = getApplicableRegulations();
if (applicable.size === 0) {
scoreDisplay.text("-%").style("color", colors.gray);
scoreBarFill.style("width", "0%").style("background", colors.gray);
scoreLabel.text("Select data and locations to calculate");
scoreBreakdown.style("display", "none");
return;
}
// Count requirements by type and track completion
const requiredByType = {};
const completedByType = {};
applicable.forEach(regId => {
const reg = regulations[regId];
if (!reg) return;
Object.entries(reg.requirements).forEach(([reqId, req]) => {
if (req.required === true) {
if (!requiredByType[reqId]) {
requiredByType[reqId] = 0;
}
requiredByType[reqId]++;
}
});
});
// Check which requirements are completed
Object.entries(state.checklistStatus).forEach(([key, checked]) => {
if (checked) {
const type = key.split('-')[0];
if (!completedByType[type]) {
completedByType[type] = 0;
}
completedByType[type]++;
}
});
// Calculate score based on requirement types (not individual items)
const totalRequirementTypes = Object.keys(requiredByType).length;
let completedTypes = 0;
Object.keys(requiredByType).forEach(type => {
if (completedByType[type] && completedByType[type] > 0) {
completedTypes++;
}
});
const score = totalRequirementTypes > 0 ? Math.round((completedTypes / totalRequirementTypes) * 100) : 0;
state.complianceScore = score;
// Update display
const scoreColor = score >= 80 ? colors.green : (score >= 50 ? colors.orange : colors.red);
scoreDisplay.text(`${score}%`).style("color", scoreColor);
scoreBarFill.style("width", `${score}%`).style("background", scoreColor);
let labelText = "";
if (score >= 80) {
labelText = "Strong compliance posture";
} else if (score >= 50) {
labelText = "Moderate - improvements recommended";
} else if (score > 0) {
labelText = "Low compliance - significant gaps identified";
} else {
labelText = "No requirements completed yet";
}
scoreLabel.text(labelText);
// Update breakdown
scoreBreakdown.style("display", "block").html(`
<div style="font-size: 11px; color: ${colors.gray}; text-align: left;">
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span>Required categories:</span>
<strong>${totalRequirementTypes}</strong>
</div>
<div style="display: flex; justify-content: space-between; margin-bottom: 4px;">
<span>Categories addressed:</span>
<strong style="color: ${scoreColor};">${completedTypes}</strong>
</div>
<div style="display: flex; justify-content: space-between;">
<span>Regulations applicable:</span>
<strong>${applicable.size}</strong>
</div>
</div>
`);
// Update remediation
updateRemediation();
}
function updateRemediation() {
if (state.complianceScore >= 80 || getApplicableRegulations().size === 0) {
remediationSection.style("display", "none");
return;
}
remediationSection.style("display", "block");
const applicable = getApplicableRegulations();
const suggestions = [];
const checkedTypes = new Set();
// Find which requirement types have been addressed
Object.entries(state.checklistStatus).forEach(([key, checked]) => {
if (checked) {
const type = key.split('-')[0];
checkedTypes.add(type);
}
});
// Find uncompleted required requirements
applicable.forEach(regId => {
const reg = regulations[regId];
if (!reg) return;
Object.entries(reg.requirements).forEach(([reqId, req]) => {
if (req.required === true && !checkedTypes.has(reqId)) {
suggestions.push({
priority: "high",
regulation: reg.name,
requirement: reqId,
action: getRemediationAction(reqId),
timeline: req.timeline
});
}
});
});
// Deduplicate by requirement type
const uniqueSuggestions = [];
const seenTypes = new Set();
suggestions.forEach(s => {
if (!seenTypes.has(s.requirement)) {
seenTypes.add(s.requirement);
uniqueSuggestions.push(s);
}
});
if (uniqueSuggestions.length === 0) {
remediationSection.style("display", "none");
return;
}
remediationContent.html(
uniqueSuggestions.slice(0, 6).map((s, idx) => `
<div style="padding: 12px; background: ${colors.lightGray}; border-radius: 6px; margin-bottom: 10px; border-left: 4px solid ${idx < 2 ? colors.red : colors.orange};">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
<span style="font-weight: bold; font-size: 12px; color: ${colors.navy};">
${requirementLabels[s.requirement]?.label || s.requirement.replace(/_/g, ' ').toUpperCase()}
</span>
<span style="font-size: 9px; padding: 2px 6px; background: ${idx < 2 ? colors.red : colors.orange}; color: white; border-radius: 10px;">
${idx < 2 ? 'HIGH PRIORITY' : 'MEDIUM PRIORITY'}
</span>
</div>
<div style="font-size: 11px; color: ${colors.gray}; line-height: 1.5;">${s.action}</div>
${s.timeline && s.timeline !== 'N/A' ? `<div style="font-size: 10px; color: ${colors.teal}; margin-top: 6px;">Timeline: ${s.timeline}</div>` : ''}
</div>
`).join('')
);
}
function getRemediationAction(reqType) {
const actions = {
consent: "Implement clear, granular consent mechanisms with explicit opt-in. Provide privacy notices before data collection and make consent withdrawal as easy as giving it.",
minimization: "Audit data collection practices and remove unnecessary fields. Document the purpose for each data element collected and ensure proportionality.",
deletion: "Build automated data deletion workflows with verification. Create a process to handle deletion requests within regulatory timelines.",
portability: "Implement data export functionality in machine-readable formats (JSON, CSV). Test export capabilities for all user data types.",
breach_notification: "Establish incident response procedures with clear escalation paths. Create notification templates and train staff on breach protocols.",
dpo: "Designate a Data Protection Officer or privacy lead with appropriate authority. Ensure they have direct access to leadership and adequate resources.",
impact_assessment: "Conduct Privacy Impact Assessments for high-risk processing activities. Document risk mitigation measures and review periodically.",
cross_border: "Review international data transfers and implement Standard Contractual Clauses or other approved transfer mechanisms. Document adequacy assessments."
};
return actions[reqType] || "Review compliance requirements for this area and implement appropriate controls.";
}
function updateResults() {
updateRegulationsDisplay();
updateChecklist();
calculateScore();
}
// Initial render
updateResults();
// Legend
const legend = container.append("div")
.style("display", "flex")
.style("justify-content", "center")
.style("gap", "20px")
.style("margin-top", "15px")
.style("flex-wrap", "wrap")
.style("font-size", "11px")
.style("padding", "10px")
.style("background", colors.lightGray)
.style("border-radius", "6px");
const legendItems = [
{ color: colors.red, label: "Required" },
{ color: colors.orange, label: "Conditional" },
{ color: colors.gray, label: "Optional" },
{ color: colors.green, label: "Compliant (80%+)" }
];
legendItems.forEach(item => {
const legendItem = legend.append("div")
.style("display", "flex")
.style("align-items", "center")
.style("gap", "5px");
legendItem.append("div")
.style("width", "14px")
.style("height", "14px")
.style("background", item.color)
.style("border-radius", "3px");
legendItem.append("span")
.style("color", colors.navy)
.text(item.label);
});
return container.node();
}