Phase 1 hardened the runtime: Web Worker offloading, IndexedDB persistence, responsive layout. Phase 2 surfaces the intelligence that was already running silently.
In Phase 1, we made the visualizer fast and stable. In Phase 2, we make it observable. The profiler, schema validator, and quality engine have always produced rich diagnostics—but they lived in a collapsed drawer. Phase 2 elevates them:
- Quality Banner: Critical signals (missing data, outliers, schema drift, duplicate keys, schema violations) now appear in a dismissible banner directly above the table—no drawer click required.
- Schema Error Inspector: JSON Schema violations are mapped from JSON Pointer paths to exact table cells via a new
pointer-utils.jsmodule. Invalid cells show inline badges; the drawer panel groups violations by cell with human-readable messages. - Automatic Re-validation on Share Restore: Opening a shared link with an embedded schema now re-runs validation immediately, so the banner and cell highlights appear without user action.
All three upgrades are live at webdevelopmentor.com/json-nexus/ and in every share link (/s/{token}/).
Upgrade 1: Quality Banner — From Drawer to Toolbar
The Problem
The profiler’s qualitySnapshot() computes null rates, IQR-based numeric outliers, schema drift, duplicate JSON keys, and a composite issueCount. The schema validator produces violations with JSON Pointer paths. But both rendered only inside the Data Quality & Schema drawer disclosure—hidden by default, easy to miss.
The Fix: Elevated Banner Component
Signal Aggregation Logic
function renderQualityBanner(quality, schemaValidation) {
if (!els.qualityBanner || !els.qualityBannerText) return;
const signals = [];
const details = [];
if (quality) {
// Null/missing columns
const nullCols = (quality.nullRates || []).filter(item => item.missingRate > 0);
if (nullCols.length) {
const highNulls = nullCols.filter(item => item.missingRate > 0.5).length;
signals.push(`${nullCols.length} column${nullCols.length === 1 ? '' : 's'} with missing data${highNulls ? ` (${highNulls} >50%)` : ''}`);
details.push({ label: 'Missing', value: `${nullCols.length} columns` });
}
// Numeric outliers (IQR)
if (quality.numericOutliers?.length) {
const totalOutliers = quality.numericOutliers.reduce((sum, item) => sum + item.count, 0);
signals.push(`${totalOutliers} numeric outlier${totalOutliers === 1 ? '' : 's'} (IQR)`);
details.push({ label: 'Outliers', value: totalOutliers });
}
// Schema drift
if (quality.schemaDrift?.changed) {
const added = quality.schemaDrift.added?.length || 0;
const removed = quality.schemaDrift.removed?.length || 0;
signals.push(`Schema drift: +${added} / −${removed} columns`);
details.push({ label: 'Drift', value: `+${added} added, −${removed} removed` });
}
// Duplicate keys
if (quality.duplicateKeyCount) {
signals.push(`${quality.duplicateKeyCount} duplicate key${quality.duplicateKeyCount === 1 ? '' : 's'}`);
details.push({ label: 'Dupes', value: quality.duplicateKeyCount });
}
}
// Schema validation violations
if (schemaValidation.validSchema && schemaValidation.result?.violations?.length) {
const violations = schemaValidation.result.violations.length;
signals.push(`${violations} schema violation${violations === 1 ? '' : 's'}`);
details.push({ label: 'Schema', value: violations });
}
if (signals.length) {
els.qualityBannerText.textContent = signals.join(' · ');
els.qualityBanner.hidden = false;
els.qualityBannerDetails.innerHTML = details.map(d =>
`${d.label}${d.value}`
).join('');
els.qualityBannerDetails.hidden = false;
} else {
els.qualityBanner.hidden = true;
}
}
// Called from renderQualityPanel() and applySchemaToCurrentPayload()
function renderQualityPanel() {
// ... existing drawer rendering ...
renderQualityBanner(activeProfile?.quality, schemaValidation);
}
Styling
.quality-banner {
border: 1px solid var(--warning);
background: color-mix(in srgb, var(--warning) 12%, var(--panel));
border-radius: 10px;
margin: 10px 16px;
box-shadow: 0 4px 16px rgba(0,0,0,.15);
animation: qualityBannerSlide 240ms ease-out;
}
@keyframes qualityBannerSlide {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
.quality-banner-content {
display: flex; align-items: center; gap: 10px; padding: 12px 14px;
}
.quality-banner-icon { color: var(--warning); font-size: 14px; }
.quality-banner-text { flex: 1; font-size: 11px; line-height: 1.5; }
.quality-banner-dismiss {
width: 28px; height: 28px; border-radius: 6px;
border: 1px solid var(--line); background: var(--panel-2);
display: grid; place-items: center; font-size: 14px;
}
.quality-banner-details {
border-top: 1px solid var(--line); padding: 12px 14px;
font-size: 10px; color: var(--muted);
}
.quality-banner-details .quality-detail-row {
display: flex; gap: 16px; flex-wrap: wrap; margin-top: 8px;
}
.quality-detail-item {
display: flex; align-items: center; gap: 6px;
padding: 4px 8px; border: 1px solid var(--line);
border-radius: 6px; background: var(--panel-2);
}
.quality-detail-label { color: var(--muted); font-size: 9px; text-transform: uppercase; letter-spacing: .07em; }
.quality-detail-value { font-weight: 600; color: var(--text); }
Upgrade 2: JSON Schema Error Inspector — Pointer to Cell Mapping
The Problem
The schema validator returns violations with JSON Pointer paths (e.g., /items/2/email). The table model uses flattened dot-notation keys (e.g., email or items.2.email). Bridging this gap required a dedicated utility.
pointer-utils.js — New Module
// assets/bbs/js/pointer-utils.js
function jsonPointerToPath(pointer) {
// "/items/2/email" -> ['items', '2', 'email']
if (!pointer || pointer === '#') return [];
return pointer.split('/').slice(1).map(seg =>
seg.replace(/~1/g, '/').replace(/~0/g, '~')
);
}
function pathToCellKey(pathSegments) {
// ['items', '2', 'email'] -> 'items.2.email' (matches flattened column key)
if (!pathSegments.length) return null;
return pathSegments.join('.');
}
function groupViolationsByCell(violations, rowCount) {
const cellViolations = new Map();
violations.forEach(violation => {
const path = jsonPointerToPath(violation.instanceLocation || violation.path);
let rowIdx = 0;
for (let i = 0; i < path.length; i++) {
if (/^\d+$/.test(path[i])) { rowIdx = parseInt(path[i], 10); break; }
}
if (rowIdx >= rowCount) rowIdx = rowCount - 1;
const cellKey = pathToCellKey(path);
if (!cellViolations.has(cellKey)) cellViolations.set(cellKey, []);
cellViolations.get(cellKey).push({ ...violation, rowIndex: rowIdx });
});
return cellViolations;
}
function formatViolationMessage(violation) {
// Human-readable messages per keyword
const keyword = violation.keyword || 'validation';
switch (keyword) {
case 'type': return `Expected ${violation.expected || 'valid type'}, got ${violation.actual || 'invalid'}`;
case 'required': return `Missing required property: ${violation.missingProperty || 'unknown'}`;
case 'format': return `Invalid ${violation.format || 'format'}: ${violation.message}`;
case 'minimum': return `Value must be >= ${violation.limit || violation.minimum}`;
case 'maximum': return `Value must be <= ${violation.limit || violation.maximum}`;
case 'minLength': return `Must be at least ${violation.limit} characters`;
case 'maxLength': return `Must be at most ${violation.limit} characters`;
case 'pattern': return `Must match pattern: ${violation.pattern}`;
case 'enum': return `Must be one of: ${(violation.allowedValues || []).join(', ')}`;
default: return violation.message || 'Validation failed';
}
}
// Exported to window.JSONNexusPointer
Integration: deriveSchemaHighlights
// In app.js — called from applySchemaToCurrentPayload()
function deriveSchemaHighlights(result) {
const cells = new Set();
const rows = new Set();
if (!result?.violations?.length) return { cells, rows };
const violationsByCell = window.JSONNexusPointer.groupViolationsByCell(
result.violations, tableModel.rows.length
);
violationsByCell.forEach((violations, cellKey) => {
cells.add(cellKey);
violations.forEach(v => rows.add(v.rowIndex));
});
return { cells, rows };
}
// In renderTable() — apply visual treatment to flagged cells
function renderTable() {
// ... for each cell ...
if (schemaValidation.cells.has(schemaCellKey(rowIndex, column))) {
td.classList.add('schema-invalid-cell');
td.title = 'Schema violation: ' + violationsForCell.map(v =>
window.JSONNexusPointer.formatViolationMessage(v)
).join('; ');
}
}
// In renderQualitySchemaValidation() — show grouped violations
function renderQualitySchemaValidation() {
// ... existing logic ...
if (result?.valid) { /* PASS */ }
else {
const violationsByCell = window.JSONNexusPointer.groupViolationsByCell(
result.violations, tableModel.rows.length
);
let html = '';
violationsByCell.forEach((violations, cellKey) => {
html += `
${cellKey} (row ${violations[0].rowIndex + 1})
${violations.map(v => `- ${window.JSONNexusPointer.formatViolationMessage(v)}
`).join('')}
`;
});
els.qualitySchemaValidation.innerHTML = html;
}
}
Visual Treatment
.schema-invalid-cell {
background: color-mix(in srgb, var(--danger) 14%, var(--panel)) !important;
box-shadow: inset 3px 0 0 var(--danger), inset 0 0 0 1px color-mix(in srgb, var(--danger) 38%, transparent);
position: relative;
}
.schema-invalid-cell::after {
content: '!'; position: absolute; top: 5px; right: 5px;
width: 15px; height: 15px; border-radius: 50%;
display: grid; place-items: center;
background: var(--danger); color: #fff; font: 800 9px/1 var(--font);
}
.quality-schema-cell-group {
margin: 8px 0; padding: 8px; border: 1px solid var(--line);
border-radius: 8px; background: var(--panel-2);
}
.quality-schema-cell-group strong { color: var(--accent); }
.quality-schema-cell-group ul { margin: 6px 0 0 18px; font-size: 9px; }
.quality-schema-cell-group li { color: var(--muted); line-height: 1.5; }
Upgrade 3: Automatic Schema Re-validation on Share Restore
Share payloads include uiState.schemaInput and uiState.schemaEnabled. Previously, opening a shared link loaded the JSON but left the schema un-validated—the user had to click "Validate schema" manually.
// In loadShareFromPath() — after parsing JSON and hydrating state
await tryParseInput(data.rawJson, { show: true, autoProfile: false });
// Re-apply schema validation if schema was configured
if (state.schemaInput && state.schemaEnabled) {
console.log('[JNV Share] Re-applying schema validation...');
await validateSchemaInput({ notify: false });
console.log('[JNV Share] schemaValidation:', schemaValidation);
}
showToast('Shared visualization restored.', 'success');
Now when a recipient opens a shared link with an embedded schema, the validation runs automatically. The quality banner appears instantly with schema violations, invalid cells are highlighted, and the drawer panel shows grouped violations—no clicks required.
Verification Checklist
- Quality banner appears: Drop JSON with nulls/outliers/drift → banner slides down above table with signal summary.
- Banner details expand: Click banner → detail chips show "Missing: 3 columns", "Outliers: 12", "Drift: +2 added", etc.
- Dismiss works: Click × → banner hides; reload → banner reappears (persistent until issues fixed).
- Schema violations highlight cells: Add schema with
format: "email"+ invalid email → cell gets red badge +!marker. - Violation grouping in drawer: Open Data Quality & Schema → Schema Validation shows violations grouped by cell with formatted messages.
- Share link auto-validates: Open
/s/{token}/with schema → banner + cell highlights appear without clicking Validate. - Pointer mapping accuracy: Violation at
/items/2/emailhighlights row 2, columnemailin flat table. - Responsive banner: Resize to mobile (≤480px) → banner stacks correctly, details wrap.
Phase 2 Architecture Summary
| Layer | File | Purpose |
|---|---|---|
| Pointer Utils | assets/bbs/js/pointer-utils.js | JSON Pointer ↔ cell key mapping, violation grouping, message formatting |
| Quality Banner | assets/app.js (renderQualityBanner), templates/visualizer-embed.php, assets/styles.css | Toolbar banner with signal summary + detail chips, dismiss, animation |
| Schema Inspector | assets/app.js (deriveSchemaHighlights, renderQualitySchemaValidation), assets/bbs/js/pointer-utils.js | Cell-level violation mapping, inline badges, grouped drawer panel |
| Auto-Revalidation | assets/app.js (loadShareFromPath) | Runs validateSchemaInput() on share restore |
| Styles | assets/styles.css | Banner, badges, detail chips, invalid-cell treatment, responsive |
| WP Plugin | json-nexus-visualizer.php | Enqueues json-nexus-pointer-utils script |
Try the Phase 2 Tool
Drop the JSON below into the visualizer to see the quality banner and schema violations in action:
[
{ "id": 1, "name": "Alice", "email": "alice@example.com", "age": 30, "score": 95.5 },
{ "id": 2, "name": "Bob", "email": "not-an-email", "age": 25, "score": 88.0 },
{ "id": 3, "name": "Carol", "email": "carol@example.com", "age": -5, "score": 92.0 },
{ "id": 4, "name": "Dave", "email": "dave@example.com", "age": 40, "score": 150.0 }
]
Then paste this schema and click Validate schema (or open this pre-configured share link to see auto-validation):
{
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"email": { "type": "string", "format": "email" },
"age": { "type": "integer", "minimum": 0 },
"score": { "type": "number" }
},
"required": ["id", "name", "email", "age", "score"]
}
}
You'll see: Quality banner (negative age, outlier score, invalid email), red badges on the invalid cells, grouped violations in the drawer, and auto-validation on the share link.
The complete plugin (GPLv3) includes the PHP PDO SQLite backend, 90-day cryptographic share links, REST monitoring, JSON Schema validation, Phase 1 client-side upgrades, and Phase 2 observability features. Download the plugin ZIP from the tool page or clone the repository for self-hosted deployment.