`;
for (const [colName, base64] of Object.entries(columns)) {
html += `
-
+
${colName}
`;
@@ -1989,26 +2182,47 @@ function renderWorkspaceHistograms(histograms) {
container.innerHTML = html;
}
-// ==================== Workspace Init ====================
+/**
+ * Render categorical distribution pie charts (base64 PNG per column).
+ */
+function renderCategoricalPieCharts(charts, containerId) {
+ const container = document.getElementById(containerId);
+ if (!container || !charts) return;
+
+ const entries = Object.entries(charts);
+ if (entries.length === 0) {
+ container.innerHTML = "";
+ return;
+ }
+
+ let html = '
';
+ for (const [colName, base64] of entries) {
+ html += `
+
+
+
`;
+ }
+ html += "
";
+ container.innerHTML = html;
+}
/**
- * Initialize the workspace after file upload.
- * Fetches summary statistics and populates feature dropdowns.
+ * Fetch summary statistics from the backend and render the stat cards,
+ * summary table, and feature-distribution histograms into the given
+ * containers. Reused by both the Data Overview panel and the
+ * Readiness Report panel.
+ *
+ * @param {string} summaryContainerId - element ID for the stats/table.
+ * @param {string} histogramsContainerId - element ID for the histograms.
*/
-function initWorkspace() {
- // Restore panel from URL hash, or default to data-overview
- const hash = location.hash.replace("#", "");
- const initialPanel =
- hash && document.getElementById("panel-" + hash) ? hash : "data-overview";
- showPanel(initialPanel, false); // false = don't push to history on init
- // Replace current history entry so back button works from the first panel
- history.replaceState({ panel: initialPanel }, "", "#" + initialPanel);
+function loadDataOverview(summaryContainerId, histogramsContainerId) {
+ const summaryId = summaryContainerId || "workspace-summary";
+ const histogramsId = histogramsContainerId || "workspace-histograms";
- // Fetch summary statistics
fetch("/summary-statistics")
.then((r) => r.json())
.then((data) => {
- const container = document.getElementById("workspace-summary");
+ const container = document.getElementById(summaryId);
if (!container) return;
if (data.success) {
@@ -2084,9 +2298,9 @@ function initWorkspace() {
container.innerHTML = html;
- // Render histograms in the data overview panel
+ // Render histograms below the summary table
if (data.histograms) {
- renderWorkspaceHistograms(data.histograms);
+ renderWorkspaceHistograms(data.histograms, histogramsId);
}
} else {
container.innerHTML = `
@@ -2097,73 +2311,2058 @@ function initWorkspace() {
}
})
.catch((err) => {
- const container = document.getElementById("workspace-summary");
+ const container = document.getElementById(summaryId);
if (container)
container.innerHTML = `
Error loading summary: ${err.message}
`;
});
+}
- // Populate feature dropdowns via /feature-set (same as metric.js does)
- fetch("/feature-set", { method: "POST" })
+/**
+ * Map a readiness status string to Tailwind color classes.
+ */
+function _dqStatusClasses(status) {
+ switch (status) {
+ case "good":
+ return {
+ text: "text-green-700 dark:text-green-400",
+ bar: "bg-green-500",
+ badge:
+ "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400",
+ };
+ case "warning":
+ return {
+ text: "text-amber-700 dark:text-amber-400",
+ bar: "bg-amber-500",
+ badge:
+ "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400",
+ };
+ case "poor":
+ return {
+ text: "text-red-700 dark:text-red-400",
+ bar: "bg-red-500",
+ badge: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400",
+ };
+ default:
+ return {
+ text: "text-gray-500 dark:text-gray-400",
+ bar: "bg-gray-400",
+ badge:
+ "bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-300",
+ };
+ }
+}
+
+/** Coerce readiness metric values to finite numbers. */
+function _readinessNums(values) {
+ return (values || [])
+ .map((v) => (typeof v === "number" ? v : parseFloat(v)))
+ .filter((v) => !Number.isNaN(v));
+}
+
+/**
+ * Decimal places for a group: default 2; if every value rounds to 0.00, use more
+ * (up to maxDecimals) so small non-zero values remain visible. Same precision
+ * is used for every value in the group.
+ */
+function _readinessDecimalPlaces(values, { minDecimals = 2, maxDecimals = 6 } = {}) {
+ const nums = _readinessNums(values);
+ if (!nums.length) return minDecimals;
+ if (nums.every((v) => v === 0)) return minDecimals;
+
+ for (let d = minDecimals; d <= maxDecimals; d++) {
+ const allRoundedZero = nums.every((v) => Number(v.toFixed(d)) === 0);
+ if (!allRoundedZero) return d;
+ }
+ return maxDecimals;
+}
+
+/** Decimal places for 0–1 ratios shown as percentages (value × 100). */
+function _readinessPctDecimals(values, options) {
+ return _readinessDecimalPlaces(
+ _readinessNums(values).map((v) => v * 100),
+ options,
+ );
+}
+
+/** Format a number with fixed decimals; integers omit the fractional part. */
+function _readinessNum(value, decimals = 2) {
+ if (value === null || value === undefined || Number.isNaN(value)) return "N/A";
+ if (typeof value !== "number") return String(value);
+ if (Number.isInteger(value)) return value.toLocaleString();
+ return value.toFixed(decimals);
+}
+
+/** Build a formatter that uses one decimal precision for every value in *values*. */
+function _readinessNumFormatter(values, options) {
+ const nums = _readinessNums(values);
+ if (nums.length && nums.every((v) => Number.isInteger(v))) {
+ return (value) => {
+ if (value === null || value === undefined || Number.isNaN(value)) return "N/A";
+ return Number(value).toLocaleString();
+ };
+ }
+ const decimals = _readinessDecimalPlaces(nums, options);
+ return (value) => _readinessNum(value, decimals);
+}
+
+/** Build a percentage formatter (0–1 input) with group-consistent decimals. */
+function _readinessPctFormatter(values, options) {
+ const decimals = _readinessPctDecimals(values, options);
+ return (value) => _pct(value, decimals);
+}
+
+/** Format a 0–1 ratio as a percentage, or "N/A" if missing. */
+function _pct(value, decimals = 2) {
+ if (value === null || value === undefined || Number.isNaN(value)) return "N/A";
+ return `${(value * 100).toFixed(decimals)}%`;
+}
+
+/** Show an error message inside one readiness section container. */
+function _readinessSectionError(container, message) {
+ if (!container) return;
+ container.classList.add("text-center", "py-8");
+ container.innerHTML = `
${message}
`;
+}
+
+/** Human-readable section build duration (matches server log precision). */
+function _formatReadinessBuildTime(seconds) {
+ if (seconds === null || seconds === undefined || Number.isNaN(Number(seconds))) {
+ return "";
+ }
+ return `Prepared in ${Number(seconds).toFixed(2)} seconds`;
+}
+
+/** Append build-time footer at the bottom of a readiness section container. */
+function _appendReadinessBuildTimeFooter(container, seconds) {
+ if (!container) return;
+ const label = _formatReadinessBuildTime(seconds);
+ if (!label) return;
+ container.querySelector(".readiness-build-time")?.remove();
+ const el = document.createElement("p");
+ el.className =
+ "readiness-build-time text-xs text-gray-400 dark:text-gray-500 mt-4 pt-3 border-t border-gray-200 dark:border-gray-700 text-right";
+ el.textContent = label;
+ container.appendChild(el);
+}
+
+const _readinessVizCache = {};
+
+/** Placeholder for a chart that loads when the details panel is opened. */
+function _readinessVizSlot(section, vizKey, title) {
+ return `
+
${title}
+
Open this section to load chart…
+
`;
+}
+
+function _readinessVizSpinnerHtml() {
+ return `
`;
+}
+
+function _applyReadinessVisualizations(section, root, vizMap) {
+ if (!root || !vizMap) return;
+
+ if (section === "dataset-overview") {
+ if (vizMap.categorical_charts) {
+ const catHost = root.querySelector("#readiness-categorical-charts");
+ if (catHost) {
+ renderCategoricalPieCharts(vizMap.categorical_charts, "readiness-categorical-charts");
+ }
+ }
+ if (vizMap.histograms) {
+ const histHost = root.querySelector("#readiness-histograms-inner");
+ if (histHost) {
+ renderWorkspaceHistograms(
+ vizMap.histograms,
+ "readiness-histograms-inner",
+ true,
+ "large",
+ );
+ }
+ }
+ return;
+ }
+
+ root.querySelectorAll(".readiness-viz-slot").forEach((slot) => {
+ const key = slot.dataset.readinessViz;
+ const target = slot.querySelector(".readiness-viz-content");
+ if (!target || !key) return;
+ const b64 = vizMap[key];
+ if (b64) {
+ target.innerHTML = `
`;
+ } else {
+ target.innerHTML = `
Chart unavailable.
`;
+ }
+ });
+}
+
+function _loadReadinessSectionVisualizations(section, root) {
+ if (!root) return Promise.resolve();
+ const slots = root.querySelectorAll(".readiness-viz-slot");
+ if (!section || (!slots.length && section !== "dataset-overview")) {
+ return Promise.resolve();
+ }
+
+ if (_readinessVizCache[section]) {
+ _applyReadinessVisualizations(section, root, _readinessVizCache[section]);
+ return Promise.resolve();
+ }
+
+ slots.forEach((slot) => {
+ const target = slot.querySelector(".readiness-viz-content");
+ if (target) target.innerHTML = _readinessVizSpinnerHtml();
+ });
+ const catHost = root.querySelector("#readiness-categorical-charts");
+ const histHost = root.querySelector("#readiness-histograms-inner");
+ if (catHost && !catHost.childElementCount) catHost.innerHTML = _readinessVizSpinnerHtml();
+ if (histHost && !histHost.childElementCount) histHost.innerHTML = _readinessVizSpinnerHtml();
+
+ return fetch(`/readiness-report/${section}/visualizations`)
.then((r) => r.json())
- .then((data) => {
- if (data.success && typeof populateWorkspaceDropdowns === "function") {
- populateWorkspaceDropdowns(data);
+ .then((resp) => {
+ if (!resp.success) {
+ const msg = resp.message || "Could not load charts";
+ slots.forEach((slot) => {
+ const target = slot.querySelector(".readiness-viz-content");
+ if (target) target.innerHTML = `
${_escapeHtml(msg)}
`;
+ });
+ return;
}
+ _readinessVizCache[section] = resp.visualizations || {};
+ _applyReadinessVisualizations(section, root, _readinessVizCache[section]);
})
- .catch((err) => console.error("Error fetching features:", err));
+ .catch((err) => {
+ slots.forEach((slot) => {
+ const target = slot.querySelector(".readiness-viz-content");
+ if (target) {
+ target.innerHTML = `
${_escapeHtml(err.message)}
`;
+ }
+ });
+ });
+}
- // Feature relevance: disable target feature in checkbox lists
- const targetDropdown = document.getElementById(
- "all-features-dropdown-feature-relevance",
+/** Fetch charts the first time a readiness details panel is expanded. */
+function _wireReadinessDetailsViz(detailsEl, section) {
+ if (!detailsEl || detailsEl.dataset.vizWired === "1") return;
+ detailsEl.dataset.vizWired = "1";
+ detailsEl.addEventListener("toggle", () => {
+ if (!detailsEl.open) return;
+ _loadReadinessSectionVisualizations(section, detailsEl);
+ });
+}
+
+/**
+ * Fetch one readiness-report section and render it when ready.
+ * @param {string} section - URL slug (e.g. "data-quality")
+ * @param {HTMLElement} container
+ * @param {Function} renderFn - (container, data) => void
+ * @returns {Promise
}
+ */
+function _getReadinessSectionRenderers() {
+ return [
+ {
+ section: "dataset-overview",
+ container: document.getElementById("readiness-summary"),
+ render: renderReadinessDatasetOverview,
+ },
+ {
+ section: "data-quality",
+ container: document.getElementById("readiness-data-quality"),
+ render: renderReadinessDataQuality,
+ },
+ {
+ section: "impact-on-ai",
+ container: document.getElementById("readiness-impact"),
+ render: renderReadinessImpact,
+ },
+ {
+ section: "fairness-bias",
+ container: document.getElementById("readiness-fairness"),
+ render: renderReadinessFairness,
+ },
+ {
+ section: "data-governance",
+ container: document.getElementById("readiness-governance"),
+ render: renderReadinessGovernance,
+ },
+ ];
+}
+
+function _initReadinessReportShell() {
+ _wireReadinessExportButton();
+ _resetReadinessFairSection();
+ initReadinessFairSection();
+ _READINESS_REPORT_SECTIONS.forEach((section) => {
+ _readinessSectionStatus[section] = "pending";
+ });
+ _updateReadinessExportButton();
+}
+
+function _applyReadinessSection(container, section, data, renderFn, buildTimeSeconds) {
+ if (!container) {
+ _readinessSectionStatus[section] = "error";
+ return false;
+ }
+ if (data.error) {
+ _readinessSectionStatus[section] = "error";
+ _readinessSectionError(container, data.error);
+ return false;
+ }
+ _readinessSectionStatus[section] = "ok";
+ container.classList.remove("text-center", "py-8");
+ renderFn(container, data);
+ _appendReadinessBuildTimeFooter(
+ container,
+ buildTimeSeconds ?? data.build_time_seconds,
);
- if (targetDropdown) {
- targetDropdown.addEventListener("change", function () {
- const target = this.value;
- // In both cat and num checkbox containers, disable the checkbox matching the target
- ["catFeaturesCheckbox1", "numFeaturesCheckbox1"].forEach(
- (containerId) => {
- const container = document.getElementById(containerId);
- if (!container) return;
- container.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
- if (cb.value === target) {
- cb.checked = false;
- cb.disabled = true;
- cb.closest("label").style.opacity = "0.4";
- } else {
- cb.disabled = false;
- cb.closest("label").style.opacity = "1";
- }
- });
- },
+ return true;
+}
+
+function _fetchReadinessSection(section, container, renderFn) {
+ if (!container) {
+ _readinessSectionStatus[section] = "error";
+ _updateReadinessExportButton();
+ return Promise.resolve();
+ }
+ return fetch(`/readiness-report/${section}`)
+ .then((r) => r.json())
+ .then((resp) => {
+ if (!resp.success) {
+ _readinessSectionStatus[section] = "error";
+ _readinessSectionError(
+ container,
+ `Could not load ${section.replace(/-/g, " ")}: ${resp.message || "unknown error"}`,
+ );
+ return;
+ }
+ const data = resp.data || {};
+ _applyReadinessSection(
+ container,
+ section,
+ data,
+ renderFn,
+ resp.build_time_seconds,
);
+ })
+ .catch((err) => {
+ _readinessSectionStatus[section] = "error";
+ _readinessSectionError(container, `Error loading section: ${err.message}`);
+ })
+ .finally(() => {
+ _updateReadinessExportButton();
});
- }
+}
- // Handle FAIR assessment file input UI
- const fairFile = document.getElementById("fair-file");
- const fairLabel = document.getElementById("fairFileLabel");
- const fairIcon = document.getElementById("fairUploadIcon");
- if (fairFile && fairLabel) {
- fairFile.addEventListener("change", () => {
- if (fairFile.files.length) {
- fairLabel.textContent = fairFile.files[0].name;
- if (fairIcon) {
- fairIcon.innerHTML =
- ' ';
- fairIcon.classList.remove("text-gray-400");
- fairIcon.classList.add("text-green-500");
+/**
+ * Restore the readiness report from the aggregated server cache (one request).
+ * @returns {Promise} true when all sections were restored from cache
+ */
+function _restoreCachedReadinessReport() {
+ return fetch("/cached-result/readiness_report")
+ .then((r) => r.json())
+ .then((resp) => {
+ if (!resp.cached || !resp.sections || activePanel !== "readiness-report") {
+ return false;
+ }
+ _initReadinessReportShell();
+ let allOk = true;
+ _getReadinessSectionRenderers().forEach(({ section, container, render }) => {
+ const data = resp.sections[section];
+ if (!data) {
+ _readinessSectionStatus[section] = "error";
+ allOk = false;
+ return;
+ }
+ if (
+ !_applyReadinessSection(
+ container,
+ section,
+ data,
+ render,
+ data.build_time_seconds,
+ )
+ ) {
+ allOk = false;
+ }
+ });
+ _updateReadinessExportButton();
+ if (resp.fair_compliance) {
+ _restoreReadinessFairFromCache(resp.fair_compliance);
+ }
+ return allOk;
+ })
+ .catch((err) => {
+ debugLog("Readiness cache restore error:", err);
+ return false;
+ });
+}
+
+/**
+ * Load the readiness report with hybrid progressive rendering:
+ * dataset overview first, then remaining sections in parallel.
+ */
+function loadReadinessReport() {
+ _initReadinessReportShell();
+ _tryRestoreCachedReadinessFair();
+
+ const overviewEntry = _getReadinessSectionRenderers().find(
+ ({ section }) => section === "dataset-overview",
+ );
+ const parallelSections = _getReadinessSectionRenderers().filter(
+ ({ section }) => section !== "dataset-overview",
+ );
+
+ const loadParallelSections = () => {
+ Promise.all(
+ parallelSections.map(({ section, container, render }) =>
+ _fetchReadinessSection(section, container, render),
+ ),
+ );
+ };
+
+ if (!overviewEntry?.container) {
+ loadParallelSections();
+ return;
+ }
+
+ // Hybrid: overview first, then parallel for the rest (spinners stay until each resolves).
+ _fetchReadinessSection(
+ overviewEntry.section,
+ overviewEntry.container,
+ overviewEntry.render,
+ ).finally(loadParallelSections);
+}
+
+function _readinessAllSectionsReady() {
+ return _READINESS_REPORT_SECTIONS.every(
+ (section) => _readinessSectionStatus[section] === "ok",
+ );
+}
+
+function _updateReadinessExportButton() {
+ const bar = document.getElementById("readiness-export-bar");
+ const scorecardBtn = document.getElementById("readiness-export-scorecard-pdf-btn");
+ const fullBtn = document.getElementById("readiness-export-full-pdf-btn");
+ if (!bar) return;
+ bar.classList.remove("hidden");
+ [scorecardBtn, fullBtn].forEach((btn) => {
+ if (btn && !btn.hasAttribute("aria-busy")) {
+ btn.disabled = false;
+ }
+ });
+}
+
+function _wireReadinessExportButton() {
+ const scorecardBtn = document.getElementById("readiness-export-scorecard-pdf-btn");
+ const fullBtn = document.getElementById("readiness-export-full-pdf-btn");
+ if (scorecardBtn && scorecardBtn.dataset.wired !== "1") {
+ scorecardBtn.dataset.wired = "1";
+ scorecardBtn.addEventListener("click", () => {
+ exportReadinessReportPdf("scorecard");
+ });
+ }
+ if (fullBtn && fullBtn.dataset.wired !== "1") {
+ fullBtn.dataset.wired = "1";
+ fullBtn.addEventListener("click", () => {
+ exportReadinessReportPdf("full");
+ });
+ }
+}
+
+function _readinessPdfFilename() {
+ const panel = document.getElementById("panel-readiness-report");
+ const raw = panel?.dataset?.datasetName || window.AIDRIN_DATASET_NAME || "dataset";
+ const stem = String(raw).replace(/\.[^.]+$/, "").replace(/[^\w.-]+/g, "_");
+ const date = new Date().toISOString().slice(0, 10);
+ return `readiness-report-${stem || "dataset"}-${date}.pdf`;
+}
+
+function _filenameFromContentDisposition(header) {
+ if (!header) return null;
+ const match = /filename\*?=(?:UTF-8''|")?([^";]+)/i.exec(header);
+ if (!match) return null;
+ try {
+ return decodeURIComponent(match[1].replace(/"/g, ""));
+ } catch (_e) {
+ return match[1].replace(/"/g, "");
+ }
+}
+
+/** Download readiness report PDF from the server (sync build-on-miss). */
+function exportReadinessReportPdf(mode = "scorecard") {
+ const isFull = mode === "full";
+ const scorecardBtn = document.getElementById("readiness-export-scorecard-pdf-btn");
+ const fullBtn = document.getElementById("readiness-export-full-pdf-btn");
+ const scorecardLabel = document.getElementById("readiness-export-scorecard-pdf-label");
+ const fullLabel = document.getElementById("readiness-export-full-pdf-label");
+ const buttons = [scorecardBtn, fullBtn].filter(Boolean);
+
+ buttons.forEach((el) => {
+ el.disabled = true;
+ el.setAttribute("aria-busy", "true");
+ });
+ if (isFull && fullLabel) fullLabel.textContent = "Preparing full PDF…";
+ if (!isFull && scorecardLabel) scorecardLabel.textContent = "Preparing PDF…";
+
+ const url = isFull ? "/readiness-report/pdf?mode=full" : "/readiness-report/pdf";
+
+ return fetch(url)
+ .then(async (response) => {
+ if (!response.ok) {
+ let message = `PDF export failed (${response.status})`;
+ try {
+ const data = await response.json();
+ if (data?.message) message = data.message;
+ } catch (_e) {
+ /* ignore */
}
+ throw new Error(message);
+ }
+ const filename =
+ _filenameFromContentDisposition(response.headers.get("Content-Disposition")) ||
+ _readinessPdfFilename();
+ const blob = await response.blob();
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = filename;
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ URL.revokeObjectURL(url);
+ })
+ .catch((err) => {
+ console.error("Readiness PDF export failed:", err);
+ alert(err.message || "Could not generate PDF.");
+ })
+ .finally(() => {
+ buttons.forEach((el) => {
+ el.disabled = false;
+ el.removeAttribute("aria-busy");
+ });
+ if (scorecardLabel) scorecardLabel.textContent = "Scorecard PDF";
+ if (fullLabel) fullLabel.textContent = "Full report PDF";
+ });
+}
+
+function _exportReadinessReportPdf() {
+ return exportReadinessReportPdf("scorecard");
+}
+
+/** Escape text for safe inclusion in readiness info tooltips. */
+function _escapeHtml(s) {
+ return String(s)
+ .replace(/&/g, "&")
+ .replace(/ `${k}=${v}`)
+ .join(", ");
+}
+
+/**
+ * One needs-attention list row with optional secondary line (context/detail).
+ * *primary* and *secondary* may contain safe HTML built by callers; user text
+ * must be escaped before passing in.
+ */
+function _readinessNaRow(primary, secondary, value) {
+ const valHtml = value
+ ? `${_escapeHtml(value)} `
+ : "";
+ const sub =
+ secondary
+ ? `${secondary}
`
+ : "";
+ return `
+
+ ${primary}
+ ${valHtml}
+
${sub}
+ `;
+}
+
+/** Needs-attention subsection with title and list body. */
+function _readinessNaBlock(title, infoKey, contextHtml, listHtml, tone) {
+ const titleCls =
+ tone === "red"
+ ? "text-red-700 dark:text-red-400"
+ : tone === "amber"
+ ? "text-amber-700 dark:text-amber-400"
+ : "text-gray-600 dark:text-gray-300";
+ const ctx = contextHtml
+ ? `${contextHtml}
`
+ : "";
+ return `
+
${title}${infoKey ? _readinessInfoIcon(infoKey) : ""}
+ ${ctx}
+
+
`;
+}
+
+/** Wrap needs-attention blocks in the standard amber/green panel. */
+function _renderReadinessNeedsAttentionPanel(naItems, emptyMessage) {
+ if (naItems.length) {
+ return `
+
Needs attention
+
${naItems.join("")}
+
`;
+ }
+ return ``;
+}
+
+/** Readiness metric definitions shown in info-icon tooltips. */
+const _READINESS_METRIC_INFO = {
+ feature_profile:
+ "A per-column snapshot of whether each feature is usable for modeling. Combines missingness, cardinality, and value balance into a readiness status (Good / Warning / Poor).",
+ pct_missing:
+ "Share of rows where this feature is missing (null/NaN). High missingness reduces reliability and may require imputation or dropping the column.",
+ n_unique:
+ "Number of distinct non-missing values. Very low values suggest constants; very high values relative to row count may indicate IDs or free text.",
+ pct_dominant:
+ "Share of rows taken by the most frequent value (the mode). Values near 100% mean the column is almost constant and usually carries little signal.",
+ profile_status:
+ "Readiness verdict for this feature. Poor: high missingness, constant, or ID-like. Warning: moderate issues. Good: no major issues detected.",
+ overall_dq_grade:
+ "Average of the data-quality KPIs (completeness, uniqueness, outlier-cleanliness). Higher is better — indicates how clean the dataset is overall.",
+ analysis_scope:
+ "This section evaluates every column automatically; no features or targets are chosen by the user.",
+ completeness:
+ "Overall share of non-missing values across all features. Same measure as the Completeness metric on the Data Quality tab.",
+ uniqueness:
+ "1 minus the proportion of duplicate rows. Low uniqueness means many exact duplicate records, which can bias models and inflate metrics.",
+ outlier_cleanliness:
+ "1 minus the mean outlier proportion across numerical features (IQR method). Lower values mean more extreme values that may need review.",
+ features_analyzed:
+ "Number of columns included in the automated correlation scan after pruning constants, ID-like fields, and high-cardinality categoricals.",
+ leakage_risk_pairs:
+ "Feature pairs with correlation |score| ≥ 0.95 — nearly duplicate or derived from each other. Can inflate model performance or indicate redundant inputs (not necessarily target leakage).",
+ redundant_pairs:
+ "Feature pairs with correlation |score| between 0.8 and 0.95 — strongly related and likely redundant. Consider keeping only one from each pair.",
+ isolated_features:
+ "Features whose strongest correlation to any other feature is below 0.1. May be uninformative noise, identifiers, or weakly related fields worth reviewing.",
+ most_related_pairs:
+ "The feature pairs with the highest absolute correlation scores from the automated scan — quick view of the strongest relationships in the data.",
+ overall_impact_grade:
+ "Average of impact KPIs (leakage safety, redundancy, informativeness). Higher suggests healthier feature structure for modeling.",
+ leakage_safety:
+ "Whether any feature pairs exceed the leakage-risk correlation threshold (|score| ≥ 0.95). Fewer or no pairs is better.",
+ redundancy:
+ "Derived from the count of highly correlated redundant pairs (|score| ≥ 0.8). Lower redundancy is generally preferable.",
+ informativeness:
+ "Share of analyzed features that have at least one meaningful correlation to another feature — flags isolated or uninformative columns.",
+ overall_fairness_grade:
+ "Average of fairness KPIs (representation balance, label balance, outcome parity). Higher suggests more balanced representation and outcomes under the automated checks.",
+ representation_balance:
+ "1 divided by the worst group probability ratio across auto-selected sensitive attributes. Low values mean some groups are much more represented than others.",
+ label_balance:
+ "Derived from the Imbalance Degree of the auto-selected target column. 0 means perfectly balanced classes; higher imbalance degree means a skewed label distribution.",
+ outcome_parity:
+ "1 minus the maximum TSD (standard deviation of class rates across sensitive groups). Flags when outcome rates differ substantially by group.",
+ representation_imbalance:
+ "Sensitive attributes where the largest group probability ratio exceeds the threshold — one category dominates representation.",
+ minority_classes:
+ "Target classes that make up less than 5% of rows. Rare classes are harder to learn and can hurt model fairness and recall.",
+ outcome_disparities:
+ "Target classes whose outcome rates vary most across sensitive groups (high TSD). Suggests uneven outcomes by group.",
+ cdd_disparities:
+ "Sensitive groups flagged by Conditional Demographic Disparity — rejected outcomes outweigh accepted ones disproportionately (positive class is auto-selected as the most frequent target value).",
+ overall_governance_grade:
+ "Average of governance KPIs (anonymity, diversity, distribution leakage, linkage risk, PHI exposure). Higher suggests lower privacy and compliance risk under automated checks.",
+ anonymity_k:
+ "Minimum equivalence-class size (k) on auto-selected quasi-identifiers. Higher k means each QI combination appears in at least k rows — harder to re-identify individuals.",
+ diversity_l:
+ "Minimum l-diversity on the auto-selected sensitive attribute within QI groups. Higher l means more distinct sensitive values per group — harder to infer a specific sensitive value.",
+ distribution_t:
+ "Maximum t-closeness (TVD) between group and global sensitive-attribute distributions. Lower t means groups do not reveal unusually skewed sensitive information.",
+ single_linkage_risk:
+ "Worst mean Marketer/Prosecutor re-identification risk across single quasi-identifiers. Higher risk means one field alone can identify many individuals.",
+ linkage_risk:
+ "Mean MM re-identification risk when all auto-selected quasi-identifiers are combined — the realistic linkage-attack scenario.",
+ phi_exposure:
+ "HIPAA-style pattern scan on auto-selected text columns. Flags potential SSNs, medical IDs, postal codes, emails, etc. Not a full regulatory certification.",
+ low_anonymity:
+ "Privacy metrics (e.g. k-Anonymity) below warning thresholds — small equivalence classes increase re-identification risk.",
+ hipaa_phi:
+ "Columns where HIPAA-like identifier patterns were detected during the automated scan.",
+ high_linkage_risk:
+ "Quasi-identifiers or QI combinations with high Marketer/Prosecutor re-identification risk scores.",
+ attribute_disclosure:
+ "l-Diversity or t-Closeness signals suggesting sensitive-attribute values may be inferable within QI groups.",
+};
+
+/**
+ * Info-icon tooltip matching existing metric panels (see theme.css .info-icon).
+ * @param {string} key - key in _READINESS_METRIC_INFO
+ */
+function _readinessInfoIcon(key) {
+ const text = _READINESS_METRIC_INFO[key];
+ if (!text) return "";
+ return `i${_escapeHtml(text)} `;
+}
+
+/** Table header cell with label + info tooltip. */
+function _readinessTh(label, infoKey, alignRight) {
+ const align = alignRight ? " text-right" : "";
+ return `${label}${_readinessInfoIcon(infoKey)} `;
+}
+
+/** Format byte count as a human-readable size string. */
+function _formatBytes(bytes) {
+ if (bytes == null || isNaN(bytes)) return "—";
+ if (bytes < 1024) return `${bytes} B`;
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
+}
+
+/** Badge HTML for per-feature readiness status. */
+function _profileStatusBadge(status) {
+ const cls = _dqStatusClasses(status);
+ const label = status === "good" ? "Good" : status === "warning" ? "Warning" : status === "poor" ? "Poor" : "—";
+ return `${label} `;
+}
+
+/**
+ * Render the dataset overview: file metadata, KPI tiles, per-feature readiness
+ * profile, and collapsible detailed statistics / distributions / histograms.
+ */
+function renderReadinessDatasetOverview(container, overview) {
+ if (overview.error) {
+ container.innerHTML = `Dataset overview unavailable: ${overview.error}
`;
+ return;
+ }
+
+ const meta = overview.file_metadata || {};
+ const profiles = overview.feature_profiles || [];
+ const profileMeta = overview.feature_profiles_meta || {};
+ const statusCounts = profileMeta.status_counts || {};
+ const poorCount =
+ statusCounts.poor ?? profiles.filter((p) => p.status === "poor").length;
+ const warnCount =
+ statusCounts.warning ?? profiles.filter((p) => p.status === "warning").length;
+
+ // --- File metadata ---
+ let html = `
+
+
${meta.file_name || "Dataset"}
+
+
Type: ${meta.file_type || "—"}
+
Size: ${_formatBytes(meta.file_size_bytes)}
+
Memory: ${_formatBytes(meta.memory_bytes)}
+
Rows: ${(meta.rows || 0).toLocaleString()}
+
Columns: ${meta.columns || 0}
+
Numerical: ${meta.numerical_count || 0}
+
Categorical: ${meta.categorical_count || 0}
+
Other: ${(meta.datetime_count || 0) + (meta.boolean_count || 0)}
+
+
`;
+
+ // --- KPI tiles ---
+ html += `
+
+
+
${(meta.rows || 0).toLocaleString()}
+
Records
+
+
+
${meta.columns || 0}
+
Features
+
+
+
${meta.numerical_count || 0}
+
Numerical
+
+
+
${meta.categorical_count || 0}
+
Categorical
+
+
`;
+
+ // --- Per-feature readiness profile ---
+ html += `
+
+
Per-feature readiness profile${_readinessInfoIcon("feature_profile")}
+
+ ${poorCount ? `${poorCount} poor ` : ""}
+ ${warnCount ? `${poorCount ? " · " : ""}${warnCount} warning ` : ""}
+ ${!poorCount && !warnCount ? "all good" : ""}
+
+
`;
+
+ if (profileMeta.truncated) {
+ html += `Showing ${profileMeta.shown.toLocaleString()} of ${profileMeta.total.toLocaleString()} features (prioritized: poor → warning → good).
`;
+ }
+
+ html += ``;
+ html += `
`;
+ html += ``;
+ html += `Feature Type Dtype `;
+ html += _readinessTh("% missing", "pct_missing", true);
+ html += _readinessTh("# unique", "n_unique", true);
+ html += _readinessTh("% dominant", "pct_dominant", true);
+ html += _readinessTh("Status", "profile_status", false);
+ html += `Summary `;
+ html += ` `;
+
+ const profilePctValues = profiles.flatMap((p) =>
+ [p.pct_missing, p.pct_dominant].filter((v) => v != null),
+ );
+ const fmtProfilePct = _readinessPctFormatter(profilePctValues);
+
+ profiles.forEach((p, i) => {
+ const stripe = i % 2 === 0 ? "bg-white dark:bg-gray-800" : "bg-gray-50 dark:bg-gray-700/50";
+ html += ``;
+ html += `${p.feature} `;
+ html += `${p.type} `;
+ html += `${p.dtype} `;
+ html += `${fmtProfilePct(p.pct_missing)} `;
+ html += `${p.n_unique} `;
+ html += `${p.pct_dominant != null ? fmtProfilePct(p.pct_dominant) : "—"} `;
+ html += `${_profileStatusBadge(p.status)} `;
+ html += `${p.summary || "—"} `;
+ html += ` `;
+ });
+ html += `
`;
+
+ // --- Collapsible detailed statistics ---
+ let detailsInner = "";
+
+ const numSummary = overview.numerical_summary || {};
+ const numMeta = overview.numerical_summary_meta || {};
+ const allNumFeatures = Object.keys(numSummary);
+ const numFeatures = allNumFeatures.slice(0, _READINESS_MAX_DETAIL_TABLE_ROWS);
+ if (numFeatures.length > 0) {
+ const allStats = Object.keys(numSummary[numFeatures[0]] || {});
+ const preferredOrder = [
+ "count", "min", "25th percentile", "50th percentile", "mean",
+ "75th percentile", "max", "std",
+ ];
+ const statKeys = preferredOrder
+ .filter((s) => allStats.includes(s))
+ .concat(allStats.filter((s) => !preferredOrder.includes(s)));
+
+ const statFormatters = {};
+ statKeys.forEach((s) => {
+ const colVals = numFeatures.map((feat) => numSummary[feat][s]);
+ statFormatters[s] = _readinessNumFormatter(colVals);
+ });
+
+ detailsInner += `Numerical summary statistics
`;
+ detailsInner += ``;
+ detailsInner += `Feature `;
+ statKeys.forEach((s) => {
+ detailsInner += `${s} `;
+ });
+ detailsInner += ``;
+ numFeatures.forEach((feat, i) => {
+ const stripe = i % 2 === 0 ? "bg-white dark:bg-gray-800" : "bg-gray-50 dark:bg-gray-700/50";
+ detailsInner += `${feat} `;
+ statKeys.forEach((s) => {
+ const raw = numSummary[feat][s];
+ const display =
+ raw === null || raw === undefined
+ ? "—"
+ : typeof raw === "number"
+ ? statFormatters[s](raw)
+ : raw;
+ detailsInner += `${display} `;
+ });
+ detailsInner += ` `;
+ });
+ detailsInner += `
`;
+ if (numMeta.truncated || allNumFeatures.length > numFeatures.length) {
+ const total = numMeta.total || allNumFeatures.length;
+ const shown = numMeta.shown || numFeatures.length;
+ detailsInner += `Showing first ${shown} of ${total} numerical features. Profile table lists prioritized features.
`;
+ }
+ }
+
+ const catCharts = overview.categorical_charts || {};
+ const catChartCols = Object.keys(catCharts);
+ const hasHistograms =
+ overview.histograms && Object.keys(overview.histograms).length > 0;
+ const vizDeferred = overview.visualizations_deferred;
+ const profileNumericalCount = profiles.filter((p) => p.type === "numerical").length;
+ const profileCategoricalCount = profiles.filter((p) => p.type === "categorical").length;
+ const showCatCharts =
+ catChartCols.length > 0 ||
+ (vizDeferred && profileCategoricalCount > 0);
+ const showHistograms =
+ hasHistograms || (vizDeferred && profileNumericalCount > 0);
+
+ if ((showCatCharts || showHistograms) && profileMeta.truncated) {
+ detailsInner += `Distribution charts use the same ${profileMeta.shown.toLocaleString()} features shown in the profile table above (prioritized: poor → warning → good).
`;
+ }
+
+ if (showCatCharts) {
+ detailsInner += `Categorical value distributions
`;
+ detailsInner += `
`;
+ }
+
+ if (showHistograms) {
+ detailsInner += `Feature distributions (numerical)
`;
+ detailsInner += `
`;
+ }
+
+ if (detailsInner) {
+ html += `
+
+
+ Show detailed statistics & distributions
+
+ ${detailsInner}
+ `;
+ }
+
+ container.classList.remove("text-center", "py-8");
+ container.innerHTML = html;
+
+ if (vizDeferred) {
+ const detailsEl = container.querySelector("details");
+ if (detailsEl && (showCatCharts || showHistograms)) {
+ _wireReadinessDetailsViz(detailsEl, "dataset-overview");
+ }
+ } else {
+ if (catChartCols.length > 0) {
+ renderCategoricalPieCharts(overview.categorical_charts, "readiness-categorical-charts");
+ }
+ if (hasHistograms) {
+ renderWorkspaceHistograms(
+ overview.histograms,
+ "readiness-histograms-inner",
+ true,
+ "large",
+ );
+ }
+ }
+}
+
+/**
+ * Render the Data Quality scorecard into the given container.
+ */
+function renderReadinessDataQuality(container, dq) {
+ if (dq.error) {
+ container.innerHTML = `Data quality unavailable: ${dq.error}
`;
+ return;
+ }
+ const kpis = dq.kpis || [];
+ const gradeCls = _dqStatusClasses(dq.grade_status);
+ const scopeCrit =
+ (dq.auto_selection || {}).selection_criteria?.analysis_scope || {};
+ const dqPctValues = [dq.grade, ...kpis.map((k) => k.value)].filter((v) => v != null);
+ const fmtDqPct = _readinessPctFormatter(dqPctValues);
+
+ // --- Analysis scope (no column auto-selection required) ---
+ let html = `
+
+
Auto-selection criteria
+
+ Analysis scope: ${scopeCrit.selected || "all columns"}${_readinessInfoIcon("analysis_scope")}
+ ${scopeCrit.rule || "All columns are evaluated automatically."}
+
+
`;
+
+ // --- Overall grade + KPI tiles ---
+ html += `
+
+ Overall data quality grade${_readinessInfoIcon("overall_dq_grade")}
+ ${fmtDqPct(dq.grade)}
+
+ `;
+
+ kpis.forEach((k) => {
+ const cls = _dqStatusClasses(k.status);
+ const widthPct =
+ k.value === null || k.value === undefined
+ ? 0
+ : Math.max(0, Math.min(100, Math.round(k.value * 100)));
+ html += `
+
+
+ ${k.label}${_readinessInfoIcon(k.id)}
+ ${fmtDqPct(k.value)}
+
+
+
${k.hint || ""}
+
`;
+ });
+ html += "
";
+
+ // --- Needs attention ---
+ const na = dq.needs_attention || {};
+ const incomplete = na.incomplete_features || [];
+ const outlierFeats = na.outlier_features || [];
+ const dupRows = na.duplicate_rows || 0;
+ const naPctValues = [
+ ...incomplete.map((f) => f.completeness),
+ ...outlierFeats.map((f) => f.outlier_proportion),
+ dupRows > 0 ? dupRows : null,
+ ].filter((v) => v != null);
+ const fmtNaPct = _readinessPctFormatter(naPctValues);
+
+ const naItems = [];
+ if (incomplete.length) {
+ const top = incomplete
+ .slice(0, 6)
+ .map(
+ (f) =>
+ `${f.feature} ${fmtNaPct(f.completeness)} complete `,
+ )
+ .join("");
+ const more =
+ incomplete.length > 6
+ ? `+${incomplete.length - 6} more `
+ : "";
+ naItems.push(`
+
+
Incomplete features (${incomplete.length})${_readinessInfoIcon("completeness")}
+
+
`);
+ }
+ if (outlierFeats.length) {
+ const top = outlierFeats
+ .slice(0, 6)
+ .map(
+ (f) =>
+ `${f.feature} ${fmtNaPct(f.outlier_proportion)} outliers `,
+ )
+ .join("");
+ const more =
+ outlierFeats.length > 6
+ ? `+${outlierFeats.length - 6} more `
+ : "";
+ naItems.push(`
+
+
Features with outliers (${outlierFeats.length})${_readinessInfoIcon("outlier_cleanliness")}
+
+
`);
+ }
+ if (dupRows && dupRows > 0) {
+ naItems.push(`
+
+
Duplicate rows${_readinessInfoIcon("uniqueness")}
+
${fmtNaPct(dupRows)} of rows are exact duplicates.
+
`);
+ }
+
+ if (naItems.length) {
+ html += `
+
+
Needs attention
+
${naItems.join("")}
+
`;
} else {
- fairLabel.textContent = "JSON metadata file";
- if (fairIcon) {
- fairIcon.innerHTML =
- ' ';
- fairIcon.classList.remove("text-green-500");
- fairIcon.classList.add("text-gray-400");
+ html += `
+
+
No data quality issues detected — all features complete, no duplicates, no outliers.
+
`;
+ }
+
+ // --- Collapsible details (original charts) ---
+ const det = dq.details || {};
+ const vizDeferred = dq.visualizations_deferred;
+ let detailsInner = "";
+ if (det.completeness) {
+ if (det.completeness.visualization) {
+ detailsInner += `
+
+
Completeness by feature
+
+
`;
+ } else if (vizDeferred || det.completeness.visualization_deferred) {
+ detailsInner += _readinessVizSlot("data-quality", "completeness", "Completeness by feature");
+ }
+ }
+ if (det.outliers) {
+ if (det.outliers.visualization) {
+ detailsInner += `
+
+
Outliers by feature
+
+
`;
+ } else if (det.outliers.error) {
+ detailsInner += `Outliers: ${det.outliers.error}
`;
+ } else if (vizDeferred || det.outliers.visualization_deferred) {
+ detailsInner += _readinessVizSlot("data-quality", "outliers", "Outliers by feature");
+ }
+ }
+
+ if (detailsInner) {
+ html += `
+
+
+ Show detailed charts
+
+ ${detailsInner}
+ `;
+ }
+
+ container.classList.remove("text-center", "py-8");
+ container.innerHTML = html;
+
+ if (vizDeferred) {
+ const detailsEl = container.querySelector("details");
+ if (detailsEl) _wireReadinessDetailsViz(detailsEl, "data-quality");
+ }
+}
+
+/**
+ * Render the Impact on AI scorecard (automated all-pairs correlation signals:
+ * redundancy, leakage risk, isolated features) into the given container.
+ */
+function renderReadinessImpact(container, impact) {
+ if (impact.error) {
+ container.innerHTML = `Impact on AI unavailable: ${impact.error}
`;
+ return;
+ }
+
+ const autoSel = impact.auto_selection || {};
+ const crit = autoSel.selection_criteria || {};
+ const colCrit = crit.columns_analyzed || {};
+ const thresholds = crit.thresholds || {};
+ const kpis = impact.kpis || [];
+ const na = impact.needs_attention || {};
+ const gradeCls = _dqStatusClasses(impact.grade_status);
+
+ const leakage = na.leakage_pairs || impact.leakage_pairs || [];
+ const redundant = na.redundant_pairs || impact.redundant_pairs || [];
+ const isolated = na.isolated_features || impact.isolated_features || [];
+ const topPairs = impact.top_pairs || [];
+ const dropped = colCrit.excluded || impact.columns_dropped || [];
+ const analyzed = impact.columns_analyzed || (colCrit.selected || []).length;
+
+ const impactScoreValues = [
+ ...leakage.map((p) => p.score),
+ ...redundant.map((p) => p.score),
+ ...topPairs.map((p) => p.score),
+ ];
+ const fmtScore = _readinessNumFormatter(impactScoreValues);
+ const impactPctValues = [impact.grade, ...kpis.map((k) => k.value)].filter((v) => v != null);
+ const fmtImpactPct = _readinessPctFormatter(impactPctValues);
+ const fmtPair = (p) =>
+ _readinessNaRow(
+ `${_escapeHtml(p.a)} ↔ ${_escapeHtml(p.b)} `,
+ "Correlated feature pair",
+ `|score| ${fmtScore(p.score)}`,
+ );
+
+ const selectedCols = colCrit.selected || [];
+ const selectedPreview =
+ selectedCols.length > 0
+ ? `${selectedCols.slice(0, 8).join(", ")}${selectedCols.length > 8 ? "…" : ""}`
+ : "none";
+
+ // --- Auto-selection criteria ---
+ let html = `
+
+
Auto-selection criteria
+
+ Columns analyzed (${analyzed}): ${selectedPreview}
+ ${colCrit.rule || ""}
+ Excluded columns: ${dropped.length}
+ Thresholds:
+ redundant |score| ≥ ${_readinessNum(thresholds.redundant_threshold)},
+ leakage |score| ≥ ${_readinessNum(thresholds.leakage_threshold)},
+ isolated max |score| < ${_readinessNum(thresholds.isolated_threshold)}
+
+
+
`;
+
+ // --- Overall grade + KPI tiles ---
+ html += `
+
+ Overall impact grade${_readinessInfoIcon("overall_impact_grade")}
+ ${fmtImpactPct(impact.grade)}
+
+ `;
+
+ kpis.forEach((k) => {
+ const cls = _dqStatusClasses(k.status);
+ const displayVal =
+ k.raw_count != null ? `${k.raw_count} flagged` : fmtImpactPct(k.value);
+ const widthPct =
+ k.value === null || k.value === undefined
+ ? 0
+ : Math.max(0, Math.min(100, Math.round(k.value * 100)));
+ html += `
+
+
+ ${k.label}${_readinessInfoIcon(k.id)}
+ ${displayVal}
+
+
+
${k.hint || ""}
+
`;
+ });
+ html += "
";
+
+ // --- Needs attention ---
+ const naItems = [];
+ if (leakage.length) {
+ const items = leakage.slice(0, 6).map(fmtPair).join("");
+ const more =
+ leakage.length > 6
+ ? `+${leakage.length - 6} more `
+ : "";
+ naItems.push(
+ _readinessNaBlock(
+ `Leakage risk (|score| ≥ 0.95) (${leakage.length})`,
+ "leakage_risk_pairs",
+ null,
+ items + more,
+ "red",
+ ),
+ );
+ }
+ if (redundant.length) {
+ const items = redundant.slice(0, 6).map(fmtPair).join("");
+ const more =
+ redundant.length > 6
+ ? `+${redundant.length - 6} more `
+ : "";
+ naItems.push(
+ _readinessNaBlock(
+ `Redundant pairs (|score| ≥ 0.8) (${redundant.length})`,
+ "redundant_pairs",
+ null,
+ items + more,
+ "amber",
+ ),
+ );
+ }
+ if (isolated.length) {
+ const items = isolated
+ .slice(0, 10)
+ .map((f) => {
+ const name = typeof f === "string" ? f : f.feature || f;
+ return _readinessNaRow(
+ `${_escapeHtml(name)} `,
+ "No strong correlation to other analyzed features",
+ null,
+ );
+ })
+ .join("");
+ const more =
+ isolated.length > 10
+ ? `+${isolated.length - 10} more `
+ : "";
+ naItems.push(
+ _readinessNaBlock(
+ `Isolated features (${isolated.length})`,
+ "isolated_features",
+ null,
+ items + more,
+ "amber",
+ ),
+ );
+ }
+
+ html += _renderReadinessNeedsAttentionPanel(
+ naItems,
+ "No redundancy, leakage risk, or isolated features detected.",
+ );
+
+ // --- Collapsible details ---
+ const det = impact.details || {};
+ const vizDeferred = impact.visualizations_deferred || det.visualizations_deferred;
+ let detailsInner = "";
+
+ if (topPairs.length) {
+ detailsInner +=
+ 'Most-related feature pairs' +
+ _readinessInfoIcon("most_related_pairs") +
+ "
";
+ detailsInner +=
+ '';
+ detailsInner +=
+ 'Feature A Feature B Score ';
+ topPairs.forEach((p, i) => {
+ const stripe =
+ i % 2 === 0
+ ? "bg-white dark:bg-gray-800"
+ : "bg-gray-50 dark:bg-gray-700/50";
+ detailsInner += `${p.a} ${p.b} ${fmtScore(p.score)} `;
+ });
+ detailsInner += "
";
+ }
+
+ if (det.numerical_visualization) {
+ const method = det.numerical_method ? ` (${det.numerical_method})` : "";
+ detailsInner += `
+
+
Numerical correlation${method}
+
+
`;
+ } else if (vizDeferred && analyzed >= 2) {
+ const method = det.numerical_method ? ` (${det.numerical_method})` : "";
+ detailsInner += _readinessVizSlot(
+ "impact-on-ai",
+ "numerical_correlation",
+ `Numerical correlation${method}`,
+ );
+ }
+ if (det.categorical_visualization) {
+ detailsInner += `
+
+
Categorical correlation (Theil's U)
+
+
`;
+ } else if (vizDeferred && analyzed >= 2) {
+ detailsInner += _readinessVizSlot(
+ "impact-on-ai",
+ "categorical_correlation",
+ "Categorical correlation (Theil's U)",
+ );
+ }
+ if (dropped.length) {
+ const excludedMeta = colCrit.excluded_meta || {};
+ const excludedTotal = excludedMeta.total || dropped.length;
+ const { html: items } = _readinessTruncatedListItems(
+ dropped,
+ _READINESS_MAX_DETAIL_LIST_ITEMS,
+ (d) =>
+ `${d.feature} ${d.reason} `,
+ );
+ detailsInner += `
+
+
Excluded columns (${excludedTotal})
+
+
`;
+ }
+
+ if (detailsInner) {
+ html += `
+
+
+ Show detailed charts & tables
+
+ ${detailsInner}
+ `;
+ }
+
+ container.classList.remove("text-center", "py-8");
+ container.innerHTML = html;
+
+ if (vizDeferred) {
+ const detailsEl = container.querySelector("details");
+ if (detailsEl) _wireReadinessDetailsViz(detailsEl, "impact-on-ai");
+ }
+}
+
+/**
+ * Render the Fairness & Bias scorecard (auto-selected columns, four metrics,
+ * selection criteria, needs-attention lists, collapsible charts).
+ */
+function renderReadinessFairness(container, fb) {
+ if (fb.error) {
+ container.innerHTML = `Fairness & Bias unavailable: ${fb.error}
`;
+ return;
+ }
+
+ const sel = fb.auto_selection || {};
+ const criteria = sel.selection_criteria || {};
+ const sensCrit = criteria.sensitive_attributes || {};
+ const targetCrit = criteria.target_column || {};
+ const posCrit = criteria.positive_class || {};
+ const thresholds = criteria.thresholds || {};
+ const kpis = fb.kpis || [];
+ const na = fb.needs_attention || {};
+ const gradeCls = _dqStatusClasses(fb.grade_status);
+ const fairnessPctValues = [fb.grade, ...kpis.map((k) => k.value)].filter((v) => v != null);
+ const fmtFairnessPct = _readinessPctFormatter(fairnessPctValues);
+ const fmtThresholdPct = _readinessPctFormatter(
+ thresholds.minority_class_share != null ? [thresholds.minority_class_share] : [],
+ );
+ const imbalanceVals = kpis
+ .filter((k) => k.id === "label_balance" && k.raw_imbalance_degree != null)
+ .map((k) => k.raw_imbalance_degree);
+ const fmtImbalance = _readinessNumFormatter(imbalanceVals);
+
+ // --- Auto-selection criteria (transparent) ---
+ let html = `
+
+
Auto-selection criteria
+
+ Sensitive attributes: ${sensCrit.selected?.length ? sensCrit.selected.join(", ") : "none"}
+ ${sensCrit.rule || ""}
+ Target column: ${targetCrit.selected || "none"}${targetCrit.reason ? ` (${targetCrit.reason})` : ""}
+ ${targetCrit.rule || ""}
+ CDD positive class: ${posCrit.selected ?? "none"}${posCrit.reason ? ` (${posCrit.reason})` : ""}
+ ${posCrit.rule || ""}
+ Primary sensitive (statistical rate & CDD): ${sel.primary_sensitive || "none"}
+ Flags:
+ representation ratio ≥ ${_readinessNum(thresholds.representation_ratio_flag)},
+ minority class < ${fmtThresholdPct(thresholds.minority_class_share)},
+ TSD ≥ ${_readinessNum(thresholds.tsd_disparity_flag)},
+ imbalance degree good/warning < ${_readinessNum(thresholds.imbalance_degree_good)} / ${_readinessNum(thresholds.imbalance_degree_warning)}
+
+
+
`;
+
+ // --- Overall grade + KPI tiles ---
+ html += `
+
+ Overall fairness grade${_readinessInfoIcon("overall_fairness_grade")}
+ ${fmtFairnessPct(fb.grade)}
+
+ `;
+
+ kpis.forEach((k) => {
+ const cls = _dqStatusClasses(k.status);
+ const displayVal =
+ k.id === "label_balance" && k.raw_imbalance_degree != null
+ ? `ID ${fmtImbalance(k.raw_imbalance_degree)}`
+ : fmtFairnessPct(k.value);
+ const widthPct =
+ k.value === null || k.value === undefined
+ ? 0
+ : Math.max(0, Math.min(100, Math.round(k.value * 100)));
+ html += `
+
+
+ ${k.label}${_readinessInfoIcon(k.id)}
+ ${displayVal}
+
+
+
${k.hint || ""}
+
`;
+ });
+ html += "
";
+
+ // --- Needs attention ---
+ const naItems = [];
+ const repImbalance = na.representation_imbalance || [];
+ if (repImbalance.length) {
+ const ratioValues = repImbalance.flatMap((s) => [
+ s.max_ratio,
+ ...(s.flagged_pairs || []).map((p) => p.ratio),
+ ]);
+ const fmtRatio = _readinessNumFormatter(ratioValues);
+ const items = repImbalance
+ .slice(0, 5)
+ .map((s) => {
+ const pairHint =
+ s.flagged_pairs && s.flagged_pairs.length
+ ? `Worst pair: ${s.flagged_pairs[0].pair} (ratio ${fmtRatio(s.flagged_pairs[0].ratio)})`
+ : "";
+ return _readinessNaRow(
+ `${_escapeHtml(s.column)} `,
+ pairHint,
+ `max ratio ${fmtRatio(s.max_ratio)}`,
+ );
+ })
+ .join("");
+ naItems.push(
+ _readinessNaBlock(
+ `Representation imbalance (${repImbalance.length})`,
+ "representation_imbalance",
+ "Sensitive attributes with extreme category probability ratios",
+ items,
+ "amber",
+ ),
+ );
+ }
+
+ const minorities = na.minority_classes || [];
+ if (minorities.length) {
+ const targetCol =
+ minorities[0].target_column || targetCrit.selected || "target";
+ const fmtMinorityShare = _readinessPctFormatter(minorities.map((m) => m.share));
+ const items = minorities
+ .map((m) =>
+ _readinessNaRow(
+ `${_escapeHtml(m.class)} `,
+ `Class in ${_escapeHtml(targetCol)} `,
+ `${fmtMinorityShare(m.share)} share`,
+ ),
+ )
+ .join("");
+ naItems.push(
+ _readinessNaBlock(
+ `Minority classes (${minorities.length})`,
+ "minority_classes",
+ `Target column: ${_escapeHtml(targetCol)} `,
+ items,
+ "amber",
+ ),
+ );
+ }
+
+ const outcomeDisp = na.outcome_disparities || [];
+ if (outcomeDisp.length) {
+ const sensCol = outcomeDisp[0].sensitive_column || sel.primary_sensitive || "—";
+ const tgtCol = outcomeDisp[0].target_column || targetCrit.selected || "—";
+ const fmtTsd = _readinessNumFormatter(outcomeDisp.map((d) => d.tsd));
+ const items = outcomeDisp
+ .map((d) =>
+ _readinessNaRow(
+ `${_escapeHtml(d.target_column || tgtCol)} = ${_escapeHtml(d.class)}`,
+ `Outcome rates vary by sensitive ${_escapeHtml(d.sensitive_column || sensCol)} `,
+ `TSD ${fmtTsd(d.tsd)}`,
+ ),
+ )
+ .join("");
+ naItems.push(
+ _readinessNaBlock(
+ `Outcome-rate disparities (${outcomeDisp.length})`,
+ "outcome_disparities",
+ `Sensitive ${_escapeHtml(sensCol)} × target ${_escapeHtml(tgtCol)} `,
+ items,
+ "amber",
+ ),
+ );
+ }
+
+ const cddDisp = na.cdd_disparities || [];
+ if (cddDisp.length) {
+ const sensCol = cddDisp[0].sensitive_column || sel.primary_sensitive || "—";
+ const tgtCol = cddDisp[0].target_column || targetCrit.selected || "—";
+ const posClass = cddDisp[0].positive_class || posCrit.selected || "—";
+ const items = cddDisp
+ .map((d) =>
+ _readinessNaRow(
+ `${_escapeHtml(d.sensitive_column || sensCol)} = ${_escapeHtml(d.group)}`,
+ `CDD vs target ${_escapeHtml(d.target_column || tgtCol)} (positive: ${_escapeHtml(String(d.positive_class ?? posClass))})`,
+ null,
+ ),
+ )
+ .join("");
+ naItems.push(
+ _readinessNaBlock(
+ `CDD flagged groups (${cddDisp.length})`,
+ "cdd_disparities",
+ `Sensitive ${_escapeHtml(sensCol)} × target ${_escapeHtml(tgtCol)} `,
+ items,
+ "red",
+ ),
+ );
+ }
+
+ html += _renderReadinessNeedsAttentionPanel(
+ naItems,
+ "No fairness issues detected under the automated thresholds.",
+ );
+
+ // --- Collapsible details (charts) ---
+ const det = fb.details || {};
+ const vizDeferred =
+ fb.visualizations_deferred ||
+ det.representation_rate?.visualizations_deferred;
+ let detailsInner = "";
+
+ const repVis = det.representation_rate?.visualizations || {};
+ const sensCols = sensCrit.selected || [];
+ if (vizDeferred && sensCols.length && !det.representation_rate?.error) {
+ sensCols.forEach((col) => {
+ detailsInner += _readinessVizSlot(
+ "fairness-bias",
+ `representation_rate.${col}`,
+ `Representation rate — ${col}`,
+ );
+ });
+ } else {
+ for (const [col, b64] of Object.entries(repVis)) {
+ detailsInner += `
+
+
Representation rate — ${col}
+
+
`;
+ }
+ }
+ if (det.representation_rate?.error && !Object.keys(repVis).length) {
+ detailsInner += `Representation rate: ${det.representation_rate.error}
`;
+ }
+
+ if (det.class_imbalance?.visualization) {
+ detailsInner += `
+
+
Class imbalance — ${targetCrit.selected || "target"}
+
+
`;
+ } else if (det.class_imbalance?.error) {
+ detailsInner += `Class imbalance: ${det.class_imbalance.error}
`;
+ } else if (
+ (vizDeferred || det.class_imbalance?.visualization_deferred) &&
+ targetCrit.selected
+ ) {
+ detailsInner += _readinessVizSlot(
+ "fairness-bias",
+ "class_imbalance",
+ `Class imbalance — ${targetCrit.selected || "target"}`,
+ );
+ }
+
+ if (det.statistical_rate?.visualization) {
+ detailsInner += `
+
+
Statistical rate — ${det.statistical_rate.sensitive} × ${det.statistical_rate.target}
+
+
`;
+ } else if (det.statistical_rate?.error) {
+ detailsInner += `Statistical rate: ${det.statistical_rate.error}
`;
+ } else if (
+ (vizDeferred || det.statistical_rate?.visualization_deferred) &&
+ sel.primary_sensitive &&
+ targetCrit.selected
+ ) {
+ detailsInner += _readinessVizSlot(
+ "fairness-bias",
+ "statistical_rate",
+ `Statistical rate — ${sel.primary_sensitive} × ${targetCrit.selected}`,
+ );
+ }
+
+ if (det.cdd?.disparities && !det.cdd.error) {
+ const rows = Object.entries(det.cdd.disparities)
+ .map(
+ ([grp, info]) =>
+ `${grp} ${info.disparity} `,
+ )
+ .join("");
+ detailsInner += `
+
+
Conditional demographic disparity (positive: ${det.cdd.positive_class})
+
+
`;
+ } else if (det.cdd?.error) {
+ detailsInner += `CDD: ${det.cdd.error}
`;
+ }
+
+ const excluded = sensCrit.excluded || [];
+ if (excluded.length) {
+ const excludedMeta = sensCrit.excluded_meta || {};
+ const excludedTotal = excludedMeta.total || excluded.length;
+ const { html: items } = _readinessTruncatedListItems(
+ excluded,
+ _READINESS_MAX_DETAIL_LIST_ITEMS,
+ (d) =>
+ `${d.feature} ${d.reason} `,
+ );
+ detailsInner += `
+
+
Excluded sensitive candidates (${excludedTotal})
+
+
`;
+ }
+
+ if (detailsInner) {
+ html += `
+
+
+ Show detailed charts & CDD table
+
+ ${detailsInner}
+ `;
+ }
+
+ container.classList.remove("text-center", "py-8");
+ container.innerHTML = html;
+
+ if (vizDeferred) {
+ const detailsEl = container.querySelector("details");
+ if (detailsEl) _wireReadinessDetailsViz(detailsEl, "fairness-bias");
+ }
+}
+
+/**
+ * Render the Data Governance scorecard (auto-selected columns, privacy KPIs,
+ * HIPAA flags, needs-attention lists, collapsible charts).
+ */
+function renderReadinessGovernance(container, gov) {
+ if (gov.error) {
+ container.innerHTML = `Data Governance unavailable: ${gov.error}
`;
+ return;
+ }
+
+ const sel = gov.auto_selection || {};
+ const criteria = sel.selection_criteria || {};
+ const qiCrit = criteria.quasi_identifiers || {};
+ const sensCrit = criteria.sensitive_attribute || {};
+ const idCrit = criteria.id_column || {};
+ const hipaaCrit = criteria.hipaa_scan_columns || {};
+ const dpCrit = criteria.dp_features || {};
+ const thresholds = criteria.thresholds || {};
+ const kpis = gov.kpis || [];
+ const na = gov.needs_attention || {};
+ const det = gov.details || {};
+ const singleRisk = det.single_attribute_risk?.by_quasi_identifier || {};
+ const gradeCls = _dqStatusClasses(gov.grade_status);
+ const lowAnon = na.low_anonymity || [];
+ const linkage = na.high_linkage_risk || [];
+ const attrDisc = na.attribute_disclosure || [];
+ const govPctValues = [gov.grade, ...kpis.map((k) => k.value)].filter((v) => v != null);
+ const fmtGovPct = _readinessPctFormatter(govPctValues);
+ const govRiskValues = [
+ ...kpis.map((k) => k.raw_worst_mean ?? k.raw_mean).filter((v) => v != null),
+ ...linkage.map((x) => x.mean_risk),
+ ...lowAnon.flatMap((x) =>
+ x.worst_single_qi ? [x.worst_single_qi.mean_risk] : [],
+ ),
+ ...Object.values(singleRisk).map((v) => v.mean_risk).filter((v) => v != null),
+ ];
+ const govMetricValues = [
+ ...kpis.filter((k) => k.id === "distribution_t" && k.raw_t != null).map((k) => k.raw_t),
+ ...attrDisc.map((x) => x.value),
+ ];
+ const fmtGovRisk = _readinessNumFormatter(govRiskValues);
+ const fmtGovMetric = _readinessNumFormatter(govMetricValues);
+
+ let html = `
+
+
Auto-selection criteria
+
+ Quasi-identifiers: ${qiCrit.selected?.length ? qiCrit.selected.join(", ") : "none"}
+ ${qiCrit.rule || ""}
+ Sensitive attribute: ${sensCrit.selected || "none"}
+ ${sensCrit.rule || ""}
+ ID column: ${idCrit.selected || "none"}${idCrit.synthetic ? " (synthetic row index)" : ""}
+ ${idCrit.rule || ""}
+ HIPAA scan columns: ${(hipaaCrit.selected || []).length} column(s)${hipaaCrit.selected?.length ? ` — ${hipaaCrit.selected.slice(0, 5).join(", ")}${hipaaCrit.selected.length > 5 ? "…" : ""}` : ""}
+ Thresholds:
+ k ≥ ${thresholds.k_good ?? "—"}/${thresholds.k_warning ?? "—"},
+ l ≥ ${thresholds.l_good ?? "—"}/${thresholds.l_warning ?? "—"},
+ t ≤ ${thresholds.t_good ?? "—"}/${thresholds.t_warning ?? "—"},
+ MM single < ${thresholds.mm_single_good ?? "—"}/${thresholds.mm_single_warning ?? "—"},
+ MM combined < ${thresholds.mm_multi_good ?? "—"}/${thresholds.mm_multi_warning ?? "—"}
+
+ ${
+ gov.small_sample_warning
+ ? `Note: small dataset (< ${thresholds.small_sample_rows ?? 30} rows) — privacy metrics may be unstable. `
+ : ""
}
+
+
`;
+
+ html += `
+
+ Overall governance grade${_readinessInfoIcon("overall_governance_grade")}
+ ${fmtGovPct(gov.grade)}
+
+ `;
+
+ kpis.forEach((k) => {
+ const cls = _dqStatusClasses(k.status);
+ let displayVal = fmtGovPct(k.value);
+ if (k.id === "anonymity_k" && k.raw_k != null) displayVal = `k=${k.raw_k}`;
+ else if (k.id === "diversity_l" && k.raw_l != null) displayVal = `l=${k.raw_l}`;
+ else if (k.id === "distribution_t" && k.raw_t != null)
+ displayVal = `t=${fmtGovMetric(k.raw_t)}`;
+ else if (k.id === "single_linkage_risk" && k.raw_worst_mean != null)
+ displayVal = `${fmtGovRisk(k.raw_worst_mean)} risk`;
+ else if (k.id === "linkage_risk" && k.raw_mean != null)
+ displayVal = `${fmtGovRisk(k.raw_mean)} risk`;
+ else if (k.id === "phi_exposure" && k.columns_flagged != null)
+ displayVal = k.columns_flagged === 0 ? "None" : `${k.columns_flagged} col(s)`;
+
+ const widthPct =
+ k.value === null || k.value === undefined
+ ? 0
+ : Math.max(0, Math.min(100, Math.round(k.value * 100)));
+ html += `
+
+
+ ${k.label}${_readinessInfoIcon(k.id)}
+ ${displayVal}
+
+
+
${k.hint || ""}
+
`;
+ });
+ html += "
";
+
+ const naItems = [];
+
+ if (lowAnon.length) {
+ let items = "";
+ lowAnon.forEach((x) => {
+ const qiList = (x.quasi_identifiers || []).join(", ");
+ items += _readinessNaRow(
+ `${_escapeHtml(x.metric)}: k = ${x.value}`,
+ x.detail || (qiList ? `Quasi-identifiers: ${qiList}` : null),
+ x.singleton_count != null ? `${x.singleton_count} singleton group(s)` : null,
+ );
+ (x.worst_groups || []).slice(0, 3).forEach((g) => {
+ items += _readinessNaRow(
+ `Smallest group (size ${g.size})`,
+ _formatQiValues(g.qi_values),
+ null,
+ );
+ });
+ if (x.worst_single_qi) {
+ items += _readinessNaRow(
+ `Highest single-QI risk: ${_escapeHtml(x.worst_single_qi.feature)} `,
+ "May contribute to low k when combined with other quasi-identifiers",
+ `risk ${fmtGovRisk(x.worst_single_qi.mean_risk)}`,
+ );
+ }
+ });
+ naItems.push(
+ _readinessNaBlock(
+ `Low anonymity (${lowAnon.length})`,
+ "low_anonymity",
+ lowAnon[0].quasi_identifiers?.length
+ ? `Quasi-identifiers: ${_escapeHtml(lowAnon[0].quasi_identifiers.join(", "))} `
+ : null,
+ items,
+ "red",
+ ),
+ );
+ }
+
+ const hipaaPhi = na.hipaa_phi || [];
+ if (hipaaPhi.length) {
+ const items = hipaaPhi
+ .slice(0, 6)
+ .map((x) =>
+ _readinessNaRow(
+ `${_escapeHtml(x.column)} `,
+ (x.types || []).join(", ") || "Pattern match",
+ `${x.total_flags} flag(s)`,
+ ),
+ )
+ .join("");
+ naItems.push(
+ _readinessNaBlock(
+ `HIPAA pattern matches (${hipaaPhi.length})`,
+ "hipaa_phi",
+ "Scanned text-like columns for HIPAA-style identifier patterns",
+ items,
+ "red",
+ ),
+ );
+ }
+
+ const linkageNa = na.high_linkage_risk || [];
+ if (linkageNa.length) {
+ const items = linkageNa
+ .map((x) => {
+ const qis = x.quasi_identifiers || x.features || [];
+ const featLabel = x.feature
+ ? `${_escapeHtml(x.feature)} `
+ : `${_escapeHtml(qis.join(", "))} `;
+ return _readinessNaRow(
+ `${_escapeHtml(x.metric)}: ${featLabel}`,
+ x.detail || (qis.length ? `Quasi-identifiers: ${qis.join(", ")}` : null),
+ `risk ${fmtGovRisk(x.mean_risk)}`,
+ );
+ })
+ .join("");
+ naItems.push(
+ _readinessNaBlock(
+ `High linkage risk (${linkageNa.length})`,
+ "high_linkage_risk",
+ null,
+ items,
+ "amber",
+ ),
+ );
+ }
+
+ const attrDiscNa = na.attribute_disclosure || [];
+ if (attrDiscNa.length) {
+ const items = attrDiscNa
+ .map((x) => {
+ const qiList = (x.quasi_identifiers || []).join(", ");
+ const sens = x.sensitive_attribute || "—";
+ return _readinessNaRow(
+ `${_escapeHtml(x.metric)} = ${fmtGovMetric(x.value)}`,
+ x.detail ||
+ `Sensitive ${_escapeHtml(sens)} within groups of (${qiList})`,
+ null,
+ );
+ })
+ .join("");
+ naItems.push(
+ _readinessNaBlock(
+ `Attribute disclosure risk (${attrDiscNa.length})`,
+ "attribute_disclosure",
+ attrDiscNa[0].sensitive_attribute
+ ? `Sensitive: ${_escapeHtml(attrDiscNa[0].sensitive_attribute)} `
+ : null,
+ items,
+ "amber",
+ ),
+ );
+ }
+
+ html += _renderReadinessNeedsAttentionPanel(
+ naItems,
+ "No governance issues detected under the automated thresholds.",
+ );
+
+ let detailsInner = "";
+ const vizDeferred = gov.visualizations_deferred;
+
+ const chartMetrics = [
+ ["k_anonymity", "k-Anonymity", "visualization"],
+ ["l_diversity", "l-Diversity", "visualization"],
+ ["t_closeness", "t-Closeness", "visualization"],
+ ["entropy_risk", "Entropy risk", "visualization"],
+ ["multiple_attribute_risk", "Multiple-attribute linkage risk", "visualization"],
+ ["differential_privacy", "Differential privacy (illustrative)", "visualization"],
+ ];
+ chartMetrics.forEach(([key, title, visKey]) => {
+ const block = det[key];
+ if (block?.[visKey]) {
+ detailsInner += `
+
+
${title}
+
+
`;
+ } else if (block?.error) {
+ detailsInner += `${title}: ${block.error}
`;
+ } else if (block && (vizDeferred || block.visualization_deferred)) {
+ detailsInner += _readinessVizSlot("data-governance", key, title);
+ }
+ });
+
+ const singleRows = Object.entries(singleRisk)
+ .filter(([, v]) => v.mean_risk != null)
+ .map(
+ ([q, v]) =>
+ `${q} ${fmtGovRisk(v.mean_risk)} `,
+ )
+ .join("");
+ if (singleRows) {
+ detailsInner += `
+
+
Single-attribute MM risk by quasi-identifier
+
Quasi-identifier Mean risk ${singleRows}
+
`;
+ }
+
+ const hipaaDet = det.hipaa?.detected || {};
+ const hipaaRows = Object.entries(hipaaDet)
+ .map(
+ ([col, info]) =>
+ `${col} ${(info.potential_types_detected || []).join(", ")} ${info.total_flags} `,
+ )
+ .join("");
+ if (hipaaRows) {
+ detailsInner += `
+
+
HIPAA scan results
+
Column Types Flags ${hipaaRows}
+
`;
+ }
+
+ const qiExcluded = qiCrit.excluded || [];
+ if (qiExcluded.length) {
+ const excludedMeta = qiCrit.excluded_meta || {};
+ const excludedTotal = excludedMeta.total || qiExcluded.length;
+ const { html: items } = _readinessTruncatedListItems(
+ qiExcluded,
+ _READINESS_MAX_DETAIL_LIST_ITEMS,
+ (d) =>
+ `${d.feature} ${d.reason} `,
+ );
+ detailsInner += `
+
+
Excluded quasi-identifier candidates (${excludedTotal})
+
+
`;
+ }
+
+ if (dpCrit.selected?.length) {
+ detailsInner += `DP demo features: ${dpCrit.selected.join(", ")} (ε=${dpCrit.epsilon ?? "—"}, illustrative only).
`;
+ }
+
+ if (detailsInner) {
+ html += `
+
+
+ Show detailed charts & tables
+
+ ${detailsInner}
+ `;
+ }
+
+ container.classList.remove("text-center", "py-8");
+ container.innerHTML = html;
+
+ if (vizDeferred) {
+ const detailsEl = container.querySelector("details");
+ if (detailsEl) _wireReadinessDetailsViz(detailsEl, "data-governance");
+ }
+}
+
+// ==================== Workspace Init ====================
+
+/**
+ * Initialize the workspace after file upload.
+ * Fetches summary statistics and populates feature dropdowns.
+ */
+function initWorkspace() {
+ // Restore panel from URL hash, or default to data-overview
+ const hash = location.hash.replace("#", "");
+ const initialPanel =
+ hash && document.getElementById("panel-" + hash) ? hash : "data-overview";
+ showPanel(initialPanel, false); // false = don't push to history on init
+ // Replace current history entry so back button works from the first panel
+ history.replaceState({ panel: initialPanel }, "", "#" + initialPanel);
+
+ // Fetch + render summary statistics into the Data Overview panel
+ loadDataOverview();
+
+ // Populate feature dropdowns via /feature-set (same as metric.js does)
+ fetch("/feature-set", { method: "POST" })
+ .then((r) => r.json())
+ .then((data) => {
+ if (data.success && typeof populateWorkspaceDropdowns === "function") {
+ populateWorkspaceDropdowns(data);
}
+ })
+ .catch((err) => console.error("Error fetching features:", err));
+
+ // Feature relevance: disable target feature in checkbox lists
+ const targetDropdown = document.getElementById(
+ "all-features-dropdown-feature-relevance",
+ );
+ if (targetDropdown) {
+ targetDropdown.addEventListener("change", function () {
+ const target = this.value;
+ // In both cat and num checkbox containers, disable the checkbox matching the target
+ ["catFeaturesCheckbox1", "numFeaturesCheckbox1"].forEach(
+ (containerId) => {
+ const container = document.getElementById(containerId);
+ if (!container) return;
+ container.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
+ if (cb.value === target) {
+ cb.checked = false;
+ cb.disabled = true;
+ cb.closest("label").style.opacity = "0.4";
+ } else {
+ cb.disabled = false;
+ cb.closest("label").style.opacity = "1";
+ }
+ });
+ },
+ );
});
}
+
+ // Handle FAIR assessment file input UI
+ _wireFairFileInput(
+ document.getElementById("fair-file"),
+ document.getElementById("fairFileLabel"),
+ document.getElementById("fairUploadIcon"),
+ );
}
/**
diff --git a/web/templates/_components/sidebar.html b/web/templates/_components/sidebar.html
index f7c6dd3f..35ab5af2 100644
--- a/web/templates/_components/sidebar.html
+++ b/web/templates/_components/sidebar.html
@@ -18,6 +18,14 @@
Data Overview
+
+
+
diff --git a/web/templates/_panels/_readiness_report.html b/web/templates/_panels/_readiness_report.html
new file mode 100644
index 00000000..4646a888
--- /dev/null
+++ b/web/templates/_panels/_readiness_report.html
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+ Scorecard PDF
+
+
+
+
+
+ Full report PDF
+
+
+
+
Readiness Report
+
+ An at-a-glance data readiness report for the uploaded dataset.
+
+
+
+
+
Loading dataset overview...
+
+
+
+
+
+
Data Quality
+
+
+
Computing data quality metrics...
+
+
+
+
+
+
Impact on AI
+
+ Automated all-pairs correlation across features (no target required) to surface redundancy, leakage risk, and uninformative features.
+
+
+
+
Computing feature correlations...
+
+
+
+
+
+
Fairness & Bias
+
+ Automated fairness checks with auto-selected sensitive attributes and target (no user input).
+
+
+
+
Computing fairness metrics...
+
+
+
+
+
+
Data Governance
+
+ Automated privacy and compliance checks with auto-selected quasi-identifiers, sensitive attributes, and HIPAA scan columns (no user input).
+
+
+
+
Computing governance metrics...
+
+
+
+
+
+
+
FAIR Compliance
+ Optional · requires metadata
+
+
+ Upload a JSON metadata file (DCAT or Datacite) to evaluate FAIR compliance. This section is not computed automatically.
+
+
+
+
+
diff --git a/web/templates/inspector.html b/web/templates/inspector.html
index 5587b48e..6f60c6c8 100644
--- a/web/templates/inspector.html
+++ b/web/templates/inspector.html
@@ -56,6 +56,7 @@
{% include '_panels/_data_overview.html' %}
+ {% include '_panels/_readiness_report.html' %}
{% include '_panels/_data_quality.html' %}
{% include '_panels/_feature_relevance.html' %}
{% include '_panels/_correlation_analysis.html' %}
@@ -85,6 +86,7 @@
{% endif %}
{% if uploaded_file_path and not globus_mode %}
+ window.AIDRIN_DATASET_NAME = {{ uploaded_file_name | tojson }};
initWorkspace();
{% elif globus_mode %}
// Globus mode — flag for workspaceSubmit to route through Globus
@@ -92,6 +94,7 @@
window.AIDRIN_GLOBUS_ENDPOINT = '{{ globus_endpoint_id }}';
window.AIDRIN_GLOBUS_FILE_PATH = '{{ uploaded_file_path }}';
window.AIDRIN_GLOBUS_FILE_NAME = '{{ uploaded_file_name }}';
+ window.AIDRIN_DATASET_NAME = {{ uploaded_file_name | tojson }};
window.AIDRIN_GLOBUS_FILE_TYPE = '{{ file_type }}';
showPanel('data-overview');
// Show Globus info banner + loading spinner for summary stats
diff --git a/web/templates/readiness_report/_pdf_fair_details.html b/web/templates/readiness_report/_pdf_fair_details.html
new file mode 100644
index 00000000..c2903d0a
--- /dev/null
+++ b/web/templates/readiness_report/_pdf_fair_details.html
@@ -0,0 +1,35 @@
+
+
+ {% for p in fair_compliance.principles %}
+
+
+
+ {% if p.rows %}
+
+
+ {% for row in p.rows %}
+ {% if row.is_group %}
+ {{ row.label }}
+ {% else %}
+
+ {{ row.label }}
+
+ {{ row.status_label }}
+
+
+ {% endif %}
+ {% endfor %}
+
+
+ {% else %}
+
No detail available.
+ {% endif %}
+
+
+ {% if loop.index % 2 == 0 and not loop.last %} {% endif %}
+ {% endfor %}
+
+
diff --git a/web/templates/readiness_report/_pdf_needs_attention.html b/web/templates/readiness_report/_pdf_needs_attention.html
new file mode 100644
index 00000000..5b437e8a
--- /dev/null
+++ b/web/templates/readiness_report/_pdf_needs_attention.html
@@ -0,0 +1,35 @@
+{% if needs_attention and needs_attention|length > 0 %}
+
+
Needs attention
+
+ {% for block in needs_attention %}
+
+
+ {{ block.title }}{% if block.glossary_key %}{{ fn(block.glossary_key) }}{% endif %}
+
+ {% if block.context %}
{{ block.context }}
{% endif %}
+ {% if block.message %}
+
{{ block.message }}
+ {% else %}
+
+ {% endif %}
+
+ {% endfor %}
+
+
+{% else %}
+
+{% endif %}
diff --git a/web/templates/readiness_report/_pdf_section_details.html b/web/templates/readiness_report/_pdf_section_details.html
new file mode 100644
index 00000000..d62966fb
--- /dev/null
+++ b/web/templates/readiness_report/_pdf_section_details.html
@@ -0,0 +1,58 @@
+{% if detail_block and detail_block.blocks %}
+
+
{{ detail_block.heading }}
+ {% for block in detail_block.blocks %}
+ {% if block.type == 'subheading' %}
+
{{ block.text }}
+ {% elif block.type == 'note' %}
+
{{ block.text }}
+ {% elif block.type == 'table' %}
+
{{ block.title }}
+ {% if block.note %}
{{ block.note }}
{% endif %}
+
+
+
+ {% for h in block.headers %}
+ 1 %} class="num"{% endif %}>{{ h }}
+ {% endfor %}
+
+
+
+ {% for row in block.rows %}
+
+ {% for cell in row %}
+ 1 %} class="num"{% else %} class="mono"{% endif %}>{{ cell }}
+ {% endfor %}
+
+ {% endfor %}
+
+
+ {% elif block.type == 'list' %}
+
{{ block.title }}
+
+ {% for item in block.entries %}
+
+ {{ item.primary }}
+ {% if item.secondary %} — {{ item.secondary }} {% endif %}
+
+ {% endfor %}
+
+ {% elif block.type == 'chart_group' %}
+
{{ block.title }}
+
+ {% for chart in block.charts %}
+
+ {{ chart.label }}
+
+
+ {% endfor %}
+
+ {% elif block.type == 'chart_wide' %}
+
+ {{ block.title }}
+
+
+ {% endif %}
+ {% endfor %}
+
+{% endif %}
diff --git a/web/templates/readiness_report/_pdf_section_footnotes.html b/web/templates/readiness_report/_pdf_section_footnotes.html
new file mode 100644
index 00000000..3cfc6f9f
--- /dev/null
+++ b/web/templates/readiness_report/_pdf_section_footnotes.html
@@ -0,0 +1,9 @@
+{% if footnotes.section(section_name).entries() %}
+
+{% endif %}
diff --git a/web/templates/readiness_report/pdf.html b/web/templates/readiness_report/pdf.html
new file mode 100644
index 00000000..39ef6b67
--- /dev/null
+++ b/web/templates/readiness_report/pdf.html
@@ -0,0 +1,367 @@
+
+
+
+
+
Readiness Report — {{ file_name }}
+
+
+
+
+
+
+
+
+
{% if include_details %}AI Data Readiness Report (Full){% else %}AI Data Readiness Report{% endif %}
+
Dataset: {{ file_name }}
+
Generated {{ generated_at }} · AIDRIN {{ app_version }}
+
+ {% if include_details %}
+ Full readiness report including scorecard summaries and detailed charts & tables
+ (the same content as each section’s “Show details” panel in the interactive report).
+ {% else %}
+ Automated scorecard for the uploaded dataset. Five pillars are computed without user column selection.
+ {% endif %}
+ Overall grades use the mean of available KPI values on a 0–1 scale.
+ Good ≥ 90%, Warning ≥ 70%, Poor < 70%.
+ Higher KPI values indicate better readiness unless noted otherwise.
+
+
+
+ {# --- Dataset Overview --- #}
+
+ {% set fn = footnotes.section('overview').ref %}
+
Dataset Overview
+ {% set meta = overview.meta %}
+
+
{{ meta.file_name or file_name }}
+
+
+
+
+ {{ '{:,}'.format(meta.rows or 0) }}
Records
+ {{ meta.columns or 0 }}
Features
+ {{ meta.numerical_count or 0 }}
Numerical
+ {{ meta.categorical_count or 0 }}
Categorical
+
+
+
+ {% if overview.profile_meta.truncated %}
+
Showing {{ '{:,}'.format(overview.profile_meta.shown) }} of {{ '{:,}'.format(overview.profile_meta.total) }} features (prioritized: poor → warning → good).
+ {% endif %}
+
+
+
+
+ Feature
+ Type{{ fn('feature_type_codes') }}
+ Dtype
+ % missing{{ fn('pct_missing') }}
+ # unique{{ fn('n_unique') }}
+ % dominant{{ fn('pct_dominant') }}
+ Status{{ fn('profile_status') }}
+ Summary
+
+
+
+ {% for p in overview.profiles %}
+
+ {{ p.feature }}
+ {{ p.type_abbr }}
+ {{ p.dtype }}
+ {{ fmt_pct(p.pct_missing) }}
+ {{ p.n_unique }}
+ {{ fmt_pct(p.pct_dominant) }}
+ {{ p.status_label }}
+ {{ p.summary or '—' }}
+
+ {% endfor %}
+
+
+ {% set section_name = 'overview' %}
+ {% include "readiness_report/_pdf_section_footnotes.html" %}
+ {% if include_details %}
+ {% set detail_block = section_details.overview %}
+ {% include "readiness_report/_pdf_section_details.html" %}
+ {% endif %}
+
+
+ {# --- Data Quality --- #}
+
+ {% set fn = footnotes.section('data_quality').ref %}
+
Data Quality
+
+
Auto-selection criteria
+
+
+ Analysis scope: {{ data_quality.auto_selection.selected or 'all columns' }}{{ fn('analysis_scope') }}
+ {{ data_quality.auto_selection.rule or 'All columns are evaluated automatically.' }}
+
+
+
+
+ Overall data quality grade{{ fn('overall_dq_grade') }}
+ {{ fmt_pct(data_quality.grade) }}
+
+
+ {% for kpi in data_quality.kpis %}
+
+ {{ kpi.label }}{{ fn(kpi.id) }}
+ {{ kpi.display }}
+
+ {{ kpi.hint }}
+
+ {% if loop.index % 3 == 0 and not loop.last %} {% endif %}
+ {% endfor %}
+
+ {% set needs_attention = data_quality.needs_attention %}
+ {% set empty_message = data_quality.empty_message %}
+ {% include "readiness_report/_pdf_needs_attention.html" %}
+ {% set section_name = 'data_quality' %}
+ {% include "readiness_report/_pdf_section_footnotes.html" %}
+ {% if include_details %}
+ {% set detail_block = section_details.data_quality %}
+ {% include "readiness_report/_pdf_section_details.html" %}
+ {% endif %}
+
+
+ {# --- Impact on AI --- #}
+
+ {% set fn = footnotes.section('impact').ref %}
+
Impact on AI
+
Automated all-pairs correlation across features to surface redundancy, leakage risk, and uninformative features.
+
+
Auto-selection criteria
+
+
+ Columns analyzed ({{ impact.columns_analyzed }}): {{ impact.columns_preview }}{{ fn('features_analyzed') }}
+ {{ impact.columns_rule }}
+
+ Excluded columns: {{ impact.excluded_count }}
+
+ Thresholds:
+ redundant |score| ≥ {{ fmt_num(impact.thresholds.redundant_threshold) }},
+ leakage |score| ≥ {{ fmt_num(impact.thresholds.leakage_threshold) }},
+ isolated max |score| < {{ fmt_num(impact.thresholds.isolated_threshold) }}
+
+
+
+
+ Overall impact grade{{ fn('overall_impact_grade') }}
+ {{ fmt_pct(impact.grade) }}
+
+
+ {% for kpi in impact.kpis %}
+
+ {{ kpi.label }}{{ fn(kpi.id) }}
+ {{ kpi.display }}
+
+ {{ kpi.hint }}
+
+ {% if loop.index % 3 == 0 and not loop.last %} {% endif %}
+ {% endfor %}
+
+ {% set needs_attention = impact.needs_attention %}
+ {% set empty_message = impact.empty_message %}
+ {% include "readiness_report/_pdf_needs_attention.html" %}
+ {% set section_name = 'impact' %}
+ {% include "readiness_report/_pdf_section_footnotes.html" %}
+ {% if include_details %}
+ {% set detail_block = section_details.impact %}
+ {% include "readiness_report/_pdf_section_details.html" %}
+ {% endif %}
+
+
+ {# --- Fairness & Bias --- #}
+
+ {% set fn = footnotes.section('fairness').ref %}
+
Fairness & Bias
+
+
Auto-selection criteria
+
+
+ Sensitive attributes:
+ {{ fairness.sensitive_attributes.selected | join(', ') if fairness.sensitive_attributes.selected else 'none' }}
+ {{ fairness.sensitive_attributes.rule or '' }}
+
+
+ Target column:
+ {{ fairness.target_column.selected or 'none' }}{% if fairness.target_column.reason %} ({{ fairness.target_column.reason }}){% endif %}
+ {{ fairness.target_column.rule or '' }}
+
+
+ CDD positive class:
+ {{ fairness.positive_class.selected if fairness.positive_class.selected is not none else 'none' }}{% if fairness.positive_class.reason %} ({{ fairness.positive_class.reason }}){% endif %}
+ {{ fairness.positive_class.rule or '' }}
+
+ Primary sensitive (statistical rate & CDD): {{ fairness.primary_sensitive or 'none' }}
+
+ Flags:
+ representation ratio ≥ {{ fmt_num(fairness.thresholds.representation_ratio_flag) }},
+ minority class < {{ fmt_pct(fairness.thresholds.minority_class_share) }},
+ TSD ≥ {{ fmt_num(fairness.thresholds.tsd_disparity_flag) }},
+ imbalance degree good/warning < {{ fmt_num(fairness.thresholds.imbalance_degree_good) }} / {{ fmt_num(fairness.thresholds.imbalance_degree_warning) }}
+
+
+
+
+ Overall fairness grade{{ fn('overall_fairness_grade') }}
+ {{ fmt_pct(fairness.grade) }}
+
+
+ {% for kpi in fairness.kpis %}
+
+ {{ kpi.label }}{{ fn(kpi.id) }}
+ {{ kpi.display }}
+
+ {{ kpi.hint }}
+
+ {% if loop.index % 3 == 0 and not loop.last %} {% endif %}
+ {% endfor %}
+
+ {% set needs_attention = fairness.needs_attention %}
+ {% set empty_message = fairness.empty_message %}
+ {% include "readiness_report/_pdf_needs_attention.html" %}
+ {% set section_name = 'fairness' %}
+ {% include "readiness_report/_pdf_section_footnotes.html" %}
+ {% if include_details %}
+ {% set detail_block = section_details.fairness %}
+ {% include "readiness_report/_pdf_section_details.html" %}
+ {% endif %}
+
+
+ {# --- Data Governance --- #}
+
+ {% set fn = footnotes.section('governance').ref %}
+
Data Governance
+
+
Auto-selection criteria
+
+
+ Quasi-identifiers:
+ {{ governance.quasi_identifiers.selected | join(', ') if governance.quasi_identifiers.selected else 'none' }}
+ {{ governance.quasi_identifiers.rule or '' }}
+
+
+ Sensitive attribute: {{ governance.sensitive_attribute.selected or 'none' }}
+ {{ governance.sensitive_attribute.rule or '' }}
+
+
+ ID column:
+ {{ governance.id_column.selected or 'none' }}{% if governance.id_column.synthetic %} (synthetic row index){% endif %}
+ {{ governance.id_column.rule or '' }}
+
+
+ HIPAA scan columns:
+ {{ governance.hipaa_scan | length }} column(s){% if governance.hipaa_scan %} — {{ governance.hipaa_scan[:5] | join(', ') }}{% if governance.hipaa_scan | length > 5 %}…{% endif %}{% endif %}
+
+
+ Thresholds:
+ k ≥ {{ governance.thresholds.k_good | default('—') }}/{{ governance.thresholds.k_warning | default('—') }},
+ l ≥ {{ governance.thresholds.l_good | default('—') }}/{{ governance.thresholds.l_warning | default('—') }},
+ t ≤ {{ governance.thresholds.t_good | default('—') }}/{{ governance.thresholds.t_warning | default('—') }},
+ MM single < {{ governance.thresholds.mm_single_good | default('—') }}/{{ governance.thresholds.mm_single_warning | default('—') }},
+ MM combined < {{ governance.thresholds.mm_multi_good | default('—') }}/{{ governance.thresholds.mm_multi_warning | default('—') }}
+
+ {% if governance.small_sample_warning %}
+ Note: small dataset (< {{ governance.thresholds.small_sample_rows | default(30) }} rows) — privacy metrics may be unstable.
+ {% endif %}
+
+
+
+ Overall governance grade{{ fn('overall_governance_grade') }}
+ {{ fmt_pct(governance.grade) }}
+
+
+ {% for kpi in governance.kpis %}
+
+ {{ kpi.label }}{{ fn(kpi.id) }}
+ {{ kpi.display }}
+
+ {{ kpi.hint }}
+
+ {% if loop.index % 2 == 0 and not loop.last %} {% endif %}
+ {% endfor %}
+
+ {% set needs_attention = governance.needs_attention %}
+ {% set empty_message = governance.empty_message %}
+ {% include "readiness_report/_pdf_needs_attention.html" %}
+ {% set section_name = 'governance' %}
+ {% include "readiness_report/_pdf_section_footnotes.html" %}
+ {% if include_details %}
+ {% set detail_block = section_details.governance %}
+ {% include "readiness_report/_pdf_section_details.html" %}
+ {% endif %}
+
+
+ {% if fair_compliance %}
+
+ {% set fn = footnotes.section('fair').ref %}
+
FAIR Compliance{{ fn('fair_compliance') }} (optional metadata assessment)
+
+ {{ fair_compliance.total_passed }}/{{ fair_compliance.total_expected }} checks passed
+ {{ fair_compliance.total_pct }}%
+
+
+
+ {% for p in fair_compliance.principles %}
+
+ {{ p.name }}
+ {{ p.passed }}/{{ p.total }}
+
+
+ {% endfor %}
+
+ {% include "readiness_report/_pdf_fair_details.html" %}
+ {% set section_name = 'fair' %}
+ {% include "readiness_report/_pdf_section_footnotes.html" %}
+
+ {% endif %}
+
+
+
Metric glossary
+
Definitions for metrics appearing in this report.
+
+
+
+
+
+ Term Definition
+
+ {% for item in glossary %}
+ {{ item.term }} {{ item.definition }}
+ {% endfor %}
+
+
+
+ HIPAA and PHI findings are automated pattern scans and do not constitute regulatory certification.
+ FAIR compliance evaluates uploaded metadata JSON, not the tabular dataset file.
+
+
+
+