diff --git a/index.html b/index.html index cd03d17..8526b1e 100644 --- a/index.html +++ b/index.html @@ -213,33 +213,47 @@
- Statistics + Overview
-
-
- Rows: - 0 +
+
+
Statistics
+
+
+ Rows: + 0 +
+
+ Columns: + 0 +
+
+ Size: + 0 B +
+
-
- Columns: - 0 -
-
- Size: - 0 B -
-
-
Timestamps
-
-
- Created: - - + -
- Modified: - - + +
+
Timestamps
+
+
+ Created: + - +
+
+ Modified: + - +
+
diff --git a/src/js/dataset-manager.js b/src/js/dataset-manager.js index 044208a..66cf953 100644 --- a/src/js/dataset-manager.js +++ b/src/js/dataset-manager.js @@ -40,21 +40,32 @@ function calculateDatasetStats(data, format, source) { let rowCount = 0; let columnCount = 0; let columns = []; + let columnTypes = []; let size = 0; // For URL sources, we can't calculate stats without fetching if (source === 'url') { - return { rowCount: null, columnCount: null, columns: [], size: null }; + return { rowCount: null, columnCount: null, columns: [], columnTypes: [], size: null }; } if (format === 'json' || format === 'topojson') { if (!Array.isArray(data) || data.length === 0) { - return { rowCount: 0, columnCount: 0, columns: [], size: 0 }; + return { rowCount: 0, columnCount: 0, columns: [], columnTypes: [], size: 0 }; } rowCount = data.length; const firstRow = data[0]; columns = typeof firstRow === 'object' ? Object.keys(firstRow) : []; columnCount = columns.length; + + // Infer column types + if (columns.length > 0) { + columnTypes = columns.map(col => { + const values = data.map(row => row[col]); + const type = detectColumnType(values); + return { name: col, type }; + }); + } + size = new Blob([JSON.stringify(data)]).size; } else if (format === 'csv' || format === 'tsv') { // For CSV/TSV, data is stored as raw text @@ -64,11 +75,23 @@ function calculateDatasetStats(data, format, source) { const separator = format === 'csv' ? ',' : '\t'; columns = lines[0].split(separator).map(h => h.trim().replace(/^"|"$/g, '')); columnCount = columns.length; + + // Infer column types from all rows + if (columns.length > 0 && lines.length > 1) { + columnTypes = columns.map((col, colIndex) => { + const values = lines.slice(1).map(line => { + const cells = line.split(separator); + return cells[colIndex] ? cells[colIndex].trim().replace(/^"|"$/g, '') : ''; + }); + const type = detectColumnType(values); + return { name: col, type }; + }); + } } size = new Blob([data]).size; } - return { rowCount, columnCount, columns, size }; + return { rowCount, columnCount, columns, columnTypes, size }; } // Dataset Storage API @@ -243,6 +266,7 @@ async function fetchURLMetadata(url, format) { let rowCount = 0; let columnCount = 0; let columns = []; + let columnTypes = []; let size = contentLength ? parseInt(contentLength) : new Blob([text]).size; // Parse based on format @@ -253,6 +277,15 @@ async function fetchURLMetadata(url, format) { if (data.length > 0 && typeof data[0] === 'object') { columns = Object.keys(data[0]); columnCount = columns.length; + + // Infer column types + if (columns.length > 0) { + columnTypes = columns.map(col => { + const values = data.map(row => row[col]); + const type = detectColumnType(values); + return { name: col, type }; + }); + } } } } else if (format === 'csv' || format === 'tsv') { @@ -262,6 +295,18 @@ async function fetchURLMetadata(url, format) { const separator = format === 'csv' ? ',' : '\t'; columns = lines[0].split(separator).map(h => h.trim().replace(/^"|"$/g, '')); columnCount = columns.length; + + // Infer column types from all rows + if (columns.length > 0 && lines.length > 1) { + columnTypes = columns.map((col, colIndex) => { + const values = lines.slice(1).map(line => { + const cells = line.split(separator); + return cells[colIndex] ? cells[colIndex].trim().replace(/^"|"$/g, '') : ''; + }); + const type = detectColumnType(values); + return { name: col, type }; + }); + } } } else if (format === 'topojson') { // TopoJSON structure is complex, just note it exists @@ -269,7 +314,7 @@ async function fetchURLMetadata(url, format) { columnCount = null; } - return { rowCount, columnCount, columns, size }; + return { rowCount, columnCount, columns, columnTypes, size }; } catch (error) { throw new Error(`Failed to fetch URL metadata: ${error.message}`); } @@ -362,6 +407,29 @@ async function selectDataset(datasetId, updateURL = true) { document.getElementById('dataset-detail-created').textContent = new Date(dataset.created).toLocaleString(); document.getElementById('dataset-detail-modified').textContent = new Date(dataset.modified).toLocaleString(); + // Populate columns list with types + const columnsSection = document.getElementById('columns-section'); + const columnsList = document.getElementById('dataset-detail-columns-list'); + + if (dataset.columnTypes && dataset.columnTypes.length > 0) { + columnsSection.style.display = 'block'; + + const columnsHTML = dataset.columnTypes.map(col => { + const icon = getTypeIcon(col.type); + return ` +
+ ${icon} + ${col.name} + ${col.type} +
+ `; + }).join(''); + + columnsList.innerHTML = columnsHTML; + } else { + columnsSection.style.display = 'none'; + } + // Show/hide preview toggle based on data type const toggleGroup = document.getElementById('preview-toggle-group'); const canShowTable = (dataset.format === 'json' || dataset.format === 'csv' || dataset.format === 'tsv'); diff --git a/src/styles.css b/src/styles.css index 4fea136..4bc8d01 100644 --- a/src/styles.css +++ b/src/styles.css @@ -299,12 +299,30 @@ body { font-family: var(--font-main); height: 100vh; overflow: hidden; backgroun .dataset-detail-header:first-child { margin-top: 0; } .dataset-detail-header-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; margin-top: 12px; color: var(--text-primary); } +/* Dataset Overview Grid */ +.dataset-overview-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); background: var(--bg-light); border: 1px inset var(--win-gray); padding: 8px; } +.overview-section { display: flex; flex-direction: column; padding: 0 8px; } +.overview-section:not(:last-child) { border-right: 1px solid var(--win-gray); } +.overview-section:first-child { padding-left: 0; } +.overview-section:last-child { padding-right: 0; } +.overview-section-title { font-size: 10px; font-weight: bold; margin-bottom: 8px; color: var(--text-secondary); } + /* Stats & Preview Boxes */ .stats-box { background: var(--bg-light); border: 1px inset var(--win-gray); padding: 8px; font-size: 10px; color: var(--text-primary); } +.overview-section .stats-box { background: none; border: none; padding: 0; } .stat-item { display: flex; justify-content: space-between; margin-bottom: 4px; } .stat-item:last-child { margin-bottom: 0; } .stat-label { font-weight: bold; } +/* Columns List */ +.columns-list { background: var(--bg-light); border: 1px inset var(--win-gray); padding: 8px; font-size: 10px; color: var(--text-primary); max-height: 200px; overflow-y: auto; } +.overview-section .columns-list { background: none; border: none; padding: 0; max-height: 150px; } +.column-item { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; } +.column-item:last-child { margin-bottom: 0; } +.column-type-icon { flex-shrink: 0; font-size: 11px; } +.column-name { font-family: var(--font-mono); color: var(--text-primary); } +.column-type { color: var(--text-secondary); font-style: italic; margin-left: auto; } + .preview-box { background: var(--bg-light); border: 2px inset var(--win-gray); padding: 8px; font-family: var(--font-mono); font-size: 10px; overflow: auto; margin: 0; color: var(--text-primary); } .preview-box.medium { max-height: 150px; } .preview-box.large { max-height: 200px; }