Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Advanced JSON Data Viewer</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .data-type { | |
| display: inline-block; | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| font-size: 10px; | |
| font-weight: 500; | |
| text-transform: uppercase; | |
| margin-right: 4px; | |
| } | |
| .string { background-color: #93c5fd; color: #1e3a8a; } | |
| .number { background-color: #86efac; color: #166534; } | |
| .boolean { background-color: #fca5a5; color: #991b1b; } | |
| .object { background-color: #d8b4fe; color: #5b21b6; } | |
| .array { background-color: #fcd34d; color: #9a3412; } | |
| .null { background-color: #9ca3af; color: #1f2937; } | |
| .json-dropzone { | |
| border: 2px dashed #ccc; | |
| border-radius: 8px; | |
| min-height: 150px; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| padding: 20px; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .json-dropzone.active { | |
| border-color: #3b82f6; | |
| background-color: #f0f7ff; | |
| } | |
| .expand-icon { | |
| transition: transform 0.2s; | |
| cursor: pointer; | |
| } | |
| .expanded .expand-icon { | |
| transform: rotate(90deg); | |
| } | |
| .tree-node { | |
| margin-left: 16px; | |
| border-left: 1px dashed #d1d5db; | |
| padding-left: 8px; | |
| } | |
| .tree-node-header { | |
| display: flex; | |
| align-items: center; | |
| padding: 4px 0; | |
| cursor: pointer; | |
| } | |
| .tree-node-header:hover { | |
| background-color: #f3f4f6; | |
| } | |
| .highlight-schema { | |
| background-color: rgba(167, 243, 208, 0.3); | |
| } | |
| .sticky-header { | |
| position: sticky; | |
| top: 0; | |
| background-color: white; | |
| z-index: 10; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-50"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <h1 class="text-3xl font-bold text-gray-800 mb-6">Advanced JSON Data Viewer</h1> | |
| <!-- File Upload Section --> | |
| <div class="bg-white rounded-lg shadow-md p-6 mb-6"> | |
| <h2 class="text-xl font-semibold text-gray-700 mb-4">Upload JSON Files</h2> | |
| <div | |
| id="dropzone" | |
| class="json-dropzone" | |
| ondragover="event.preventDefault(); document.getElementById('dropzone').classList.add('active');" | |
| ondragleave="event.preventDefault(); document.getElementById('dropzone').classList.remove('active');" | |
| ondrop="event.preventDefault(); document.getElementById('dropzone').classList.remove('active'); handleFiles(event.dataTransfer.files);" | |
| > | |
| <i class="fas fa-file-upload text-4xl text-blue-500 mb-3"></i> | |
| <p class="text-gray-600 mb-2">Drag & Drop JSON files here</p> | |
| <p class="text-gray-400 text-sm mb-4">or</p> | |
| <label for="fileInput" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md cursor-pointer transition"> | |
| <span>Select Files</span> | |
| <input id="fileInput" type="file" accept=".json" multiple class="hidden" onchange="handleFiles(this.files)"> | |
| </label> | |
| </div> | |
| <div id="fileList" class="mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"></div> | |
| </div> | |
| <!-- Schema Visualization & Processing Section --> | |
| <div id="processingSection" class="hidden bg-white rounded-lg shadow-md p-6 mb-6"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <h2 class="text-xl font-semibold text-gray-700">Schema Analysis</h2> | |
| <div> | |
| <button id="analyzeBtn" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-md transition"> | |
| <i class="fas fa-cog mr-2"></i> Analyze Schemas | |
| </button> | |
| </div> | |
| </div> | |
| <div class="flex mb-4"> | |
| <div class="w-1/2 pr-4"> | |
| <h3 class="text-lg font-medium text-gray-700 mb-2">Detected Fields</h3> | |
| <div id="schemaTree" class="max-h-96 overflow-y-auto border border-gray-200 rounded-md p-2"></div> | |
| </div> | |
| <div class="w-1/2 pl-4"> | |
| <h3 class="text-lg font-medium text-gray-700 mb-2">Schema Details</h3> | |
| <div id="schemaDetails" class="max-h-96 overflow-y-auto border border-gray-200 rounded-md p-2"> | |
| <p class="text-gray-500 text-center py-8">Select a field to view details</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex items-center justify-center py-4" id="loadingIndicator"> | |
| <i class="fas fa-spinner fa-spin text-blue-500 text-3xl hidden"></i> | |
| </div> | |
| </div> | |
| <!-- Data Table Section --> | |
| <div id="resultSection" class="hidden bg-white rounded-lg shadow-md p-6"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <h2 class="text-xl font-semibold text-gray-700">Data View</h2> | |
| <div class="flex space-x-2"> | |
| <div class="relative"> | |
| <select id="viewMode" class="appearance-none bg-gray-100 border border-gray-300 rounded-md px-3 py-2 pr-8"> | |
| <option value="table">Table View</option> | |
| <option value="tree">Tree View</option> | |
| </select> | |
| <div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"> | |
| <i class="fas fa-chevron-down text-xs"></i> | |
| </div> | |
| </div> | |
| <button id="exportBtn" class="bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded-md transition"> | |
| <i class="fas fa-file-export mr-2"></i> Export | |
| </button> | |
| <button id="clearBtn" class="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md transition"> | |
| <i class="fas fa-trash mr-2"></i> Clear | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Table View --> | |
| <div id="tableView" class="overflow-x-auto"> | |
| <table id="resultTable" class="min-w-full border-collapse"> | |
| <thead> | |
| <tr class="sticky-header border-b border-gray-200"> | |
| <th class="bg-gray-100 px-4 py-2 text-left text-gray-700">#</th> | |
| <th class="bg-gray-100 px-4 py-2 text-left text-gray-700">Source</th> | |
| <!-- Columns will be added dynamically --> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <!-- Data will be added dynamically --> | |
| </tbody> | |
| </table> | |
| </div> | |
| <!-- Tree View --> | |
| <div id="treeView" class="hidden max-h-96 overflow-y-auto border border-gray-200 rounded-md p-2"> | |
| <!-- Data will be added dynamically --> | |
| </div> | |
| <!-- Summary Section --> | |
| <div class="mt-6 p-4 bg-gray-50 rounded-md"> | |
| <h3 class="font-medium text-gray-700 mb-3">Schema Summary</h3> | |
| <div id="summaryContent" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> | |
| <div class="bg-white p-3 rounded-md shadow-sm"> | |
| <h4 class="text-sm font-medium text-gray-500 mb-1">Files Processed</h4> | |
| <p id="fileCount" class="text-lg font-semibold text-gray-800">0</p> | |
| </div> | |
| <div class="bg-white p-3 rounded-md shadow-sm"> | |
| <h4 class="text-sm font-medium text-gray-500 mb-1">Total Records</h4> | |
| <p id="recordCount" class="text-lg font-semibold text-gray-800">0</p> | |
| </div> | |
| <div class="bg-white p-3 rounded-md shadow-sm"> | |
| <h4 class="text-sm font-medium text-gray-500 mb-1">Unique Fields</h4> | |
| <p id="fieldCount" class="text-lg font-semibold text-gray-800">0</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Global data store | |
| const jsonData = { | |
| files: [], // Array of loaded JSON files | |
| schema: {}, // Combined schema of all JSON files | |
| analysis: { // Results of schema analysis | |
| fieldStats: {}, // Statistics about each field | |
| commonFields: [],// List of fields common to all files | |
| uniqueFields: [] // List of fields unique to specific files | |
| }, | |
| records: [] // Flattened records for display | |
| }; | |
| // Handle file selection/drop | |
| function handleFiles(files) { | |
| const fileListContainer = document.getElementById('fileList'); | |
| fileListContainer.innerHTML = ''; | |
| jsonData.files = []; | |
| if (!files.length) return; | |
| // Show processing section | |
| document.getElementById('processingSection').classList.remove('hidden'); | |
| let filesLoaded = 0; | |
| for (let i = 0; i < files.length; i++) { | |
| const file = files[i]; | |
| if (file.type !== 'application/json' && !file.name.endsWith('.json')) { | |
| continue; | |
| } | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| try { | |
| const content = JSON.parse(e.target.result); | |
| jsonData.files.push({ | |
| name: file.name, | |
| data: content, | |
| schema: extractSchema(content) | |
| }); | |
| // Add to file list display | |
| const fileCard = document.createElement('div'); | |
| fileCard.className = 'bg-gray-50 p-3 rounded-md border border-gray-200 flex items-center justify-between'; | |
| fileCard.innerHTML = ` | |
| <div class="flex items-center"> | |
| <i class="fas fa-file-alt text-blue-400 mr-3"></i> | |
| <span class="text-gray-700 font-medium truncate" title="${file.name}">${file.name}</span> | |
| </div> | |
| <span class="text-xs text-green-600 bg-green-100 px-2 py-1 rounded-full">Loaded</span> | |
| `; | |
| fileListContainer.appendChild(fileCard); | |
| filesLoaded++; | |
| // When all files are loaded, analyze them | |
| if (filesLoaded === files.length) { | |
| analyzeSchemas(); | |
| } | |
| } catch (error) { | |
| alert(`Error parsing ${file.name}: ${error.message}`); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| } | |
| } | |
| // Extract schema from JSON data | |
| function extractSchema(data, prefix = '') { | |
| const schema = {}; | |
| if (data === null) { | |
| return { type: 'null', sample: null }; | |
| } | |
| const type = Array.isArray(data) ? 'array' : typeof data; | |
| if (type === 'object') { | |
| schema.type = 'object'; | |
| schema.properties = {}; | |
| Object.keys(data).forEach(key => { | |
| schema.properties[key] = extractSchema(data[key], prefix ? `${prefix}.${key}` : key); | |
| }); | |
| } | |
| else if (type === 'array') { | |
| schema.type = 'array'; | |
| schema.items = data.length > 0 ? extractSchema(data[0], `${prefix}[]`) : { type: 'unknown' }; | |
| } | |
| else { | |
| schema.type = type; | |
| schema.sample = data; | |
| } | |
| return schema; | |
| } | |
| // Analyze all loaded schemas | |
| function analyzeSchemas() { | |
| const loadingElement = document.getElementById('loadingIndicator').firstElementChild; | |
| loadingElement.classList.remove('hidden'); | |
| // Reset analysis | |
| jsonData.analysis = { | |
| fieldStats: {}, | |
| commonFields: [], | |
| uniqueFields: [] | |
| }; | |
| setTimeout(() => { | |
| // Combine all schemas into one big schema | |
| jsonData.schema = combineSchemas(jsonData.files.map(file => file.schema)); | |
| // Extract flattened field list | |
| const allFields = flattenSchema(jsonData.schema); | |
| // Calculate field statistics | |
| allFields.forEach(field => { | |
| jsonData.analysis.fieldStats[field.path] = { | |
| types: new Set(), | |
| samples: new Set(), | |
| presentIn: new Set(), | |
| path: field.path, | |
| ...field.info | |
| }; | |
| }); | |
| // Calculate which files contain which fields | |
| jsonData.files.forEach(file => { | |
| const fileFields = flattenSchema(file.schema).map(f => f.path); | |
| fileFields.forEach(fieldPath => { | |
| jsonData.analysis.fieldStats[fieldPath].presentIn.add(file.name); | |
| }); | |
| }); | |
| // Convert Sets to Arrays for easier display | |
| Object.values(jsonData.analysis.fieldStats).forEach(stats => { | |
| stats.types = Array.from(stats.types); | |
| stats.samples = Array.from(stats.samples); | |
| stats.presentIn = Array.from(stats.presentIn); | |
| }); | |
| // Identify common and unique fields | |
| const fileCount = jsonData.files.length; | |
| Object.entries(jsonData.analysis.fieldStats).forEach(([path, stats]) => { | |
| if (stats.presentIn.length === fileCount) { | |
| jsonData.analysis.commonFields.push(path); | |
| } else { | |
| jsonData.analysis.uniqueFields.push({ | |
| path: path, | |
| files: stats.presentIn | |
| }); | |
| } | |
| }); | |
| // Update UI | |
| displaySchemaTree(); | |
| updateSummaryStats(); | |
| loadingElement.classList.add('hidden'); | |
| document.getElementById('resultSection').classList.remove('hidden'); | |
| }, 500); | |
| } | |
| // Combine multiple schemas into one | |
| function combineSchemas(schemas) { | |
| if (schemas.length === 0) return {}; | |
| if (schemas.length === 1) return schemas[0]; | |
| const combined = JSON.parse(JSON.stringify(schemas[0])); | |
| for (let i = 1; i < schemas.length; i++) { | |
| mergeSchema(combined, schemas[i]); | |
| } | |
| return combined; | |
| } | |
| // Merge two schemas | |
| function mergeSchema(target, source) { | |
| // If types are different, mark as union type | |
| if (target.type !== source.type) { | |
| if (!target.types) target.types = new Set([target.type]); | |
| target.types.add(source.type); | |
| target.type = 'mixed'; | |
| return; | |
| } | |
| // Handle objects | |
| if (target.type === 'object' && source.type === 'object') { | |
| // Merge properties | |
| if (!target.properties) target.properties = {}; | |
| // Add all properties from source | |
| Object.keys(source.properties).forEach(key => { | |
| if (target.properties[key]) { | |
| mergeSchema(target.properties[key], source.properties[key]); | |
| } else { | |
| target.properties[key] = source.properties[key]; | |
| } | |
| }); | |
| } | |
| // Handle arrays | |
| else if (target.type === 'array' && source.type === 'array') { | |
| if (target.items && source.items) { | |
| mergeSchema(target.items, source.items); | |
| } else if (source.items) { | |
| target.items = source.items; | |
| } | |
| } | |
| // Handle primitive types | |
| else { | |
| // Keep samples for primitive types | |
| if (target.sample !== source.sample) { | |
| if (!Array.isArray(target.samples)) { | |
| target.samples = [target.sample]; | |
| } | |
| target.samples.push(source.sample); | |
| } | |
| } | |
| } | |
| // Flatten schema to get all paths | |
| function flattenSchema(schema, path = '', prefix = '', result = []) { | |
| if (schema === null || typeof schema !== 'object') return result; | |
| if (schema.type === 'object' && schema.properties) { | |
| Object.entries(schema.properties).forEach(([key, value]) => { | |
| const newPath = path ? `${path}.${key}` : key; | |
| const newPrefix = prefix ? `${prefix} > ${key}` : key; | |
| result.push({ | |
| path: newPath, | |
| displayPath: newPrefix, | |
| info: { | |
| type: value.type, | |
| sample: value.sample || (value.samples ? value.samples[0] : null), | |
| possibleTypes: value.types ? Array.from(value.types) : [value.type], | |
| samples: value.samples || (value.sample ? [value.sample] : []) | |
| } | |
| }); | |
| flattenSchema(value, newPath, newPrefix, result); | |
| }); | |
| } | |
| else if (schema.type === 'array' && schema.items) { | |
| const newPath = path ? `${path}[]` : '[]'; | |
| const newPrefix = prefix ? `${prefix} > [item]` : '[item]'; | |
| result.push({ | |
| path: newPath, | |
| displayPath: newPrefix, | |
| info: { | |
| type: `array<${schema.items.type}>`, | |
| sample: schema.items.sample || (schema.items.samples ? schema.items.samples[0] : null), | |
| possibleTypes: schema.items.types ? Array.from(schema.items.types) : [schema.items.type], | |
| samples: schema.items.samples || (schema.items.sample ? [schema.items.sample] : []) | |
| } | |
| }); | |
| flattenSchema(schema.items, newPath, newPrefix, result); | |
| } | |
| else if (schema.type) { | |
| result.push({ | |
| path: path, | |
| displayPath: prefix, | |
| info: { | |
| type: schema.type, | |
| sample: schema.sample || (schema.samples ? schema.samples[0] : null), | |
| possibleTypes: schema.types ? Array.from(schema.types) : [schema.type], | |
| samples: schema.samples || (schema.sample ? [schema.sample] : []) | |
| } | |
| }); | |
| } | |
| return result; | |
| } | |
| // Display schema as a collapsible tree | |
| function displaySchemaTree() { | |
| const treeContainer = document.getElementById('schemaTree'); | |
| treeContainer.innerHTML = ''; | |
| const rootNode = document.createElement('div'); | |
| rootNode.className = 'tree-root'; | |
| // Create tree for each top-level property | |
| const processNode = (schema, path, displayPath) => { | |
| const node = document.createElement('div'); | |
| node.className = 'tree-node'; | |
| node.dataset.path = path; | |
| const header = document.createElement('div'); | |
| header.className = 'tree-node-header'; | |
| if ((schema.type === 'object' && schema.properties && Object.keys(schema.properties).length > 0) || | |
| (schema.type === 'array' && schema.items && (schema.items.properties || schema.items.type !== 'unknown'))) { | |
| // Node with children - make expandable | |
| const expandIcon = document.createElement('i'); | |
| expandIcon.className = 'fas fa-chevron-right text-gray-400 text-xs mr-2 expand-icon'; | |
| header.appendChild(expandIcon); | |
| header.onclick = (e) => { | |
| e.stopPropagation(); | |
| node.classList.toggle('expanded'); | |
| if (node.classList.contains('expanded') && !node.children[1]) { | |
| // Populate children on first expand | |
| if (schema.type === 'object' && schema.properties) { | |
| Object.entries(schema.properties).forEach(([key, value]) => { | |
| node.appendChild(processNode(value, path ? `${path}.${key}` : key, `${displayPath} > ${key}`)); | |
| }); | |
| } | |
| else if (schema.type === 'array' && schema.items) { | |
| node.appendChild(processNode(schema.items, path ? `${path}[]` : '[]', `${displayPath} > [item]`)); | |
| } | |
| } | |
| }; | |
| } | |
| // Add type indicator | |
| const typeBadge = document.createElement('span'); | |
| typeBadge.className = 'data-type ' + (schema.type === 'array' ? schema.items.type : schema.type); | |
| typeBadge.textContent = schema.type === 'array' ? `array<${schema.items.type}>` : schema.type; | |
| // Add field name | |
| const nameSpan = document.createElement('span'); | |
| const lastPart = displayPath.split(' > ').pop(); | |
| name | |
| </html> |