mirror of
https://github.com/olehomelchenko/astrolabe-nvc.git
synced 2025-12-21 21:22:23 +00:00
refactor: enhance dataset overview layout and statistics display
This commit is contained in:
58
index.html
58
index.html
@@ -213,33 +213,47 @@
|
|||||||
<textarea id="dataset-detail-comment" class="input textarea" placeholder="Add a comment..." rows="3"></textarea>
|
<textarea id="dataset-detail-comment" class="input textarea" placeholder="Add a comment..." rows="3"></textarea>
|
||||||
|
|
||||||
<div class="dataset-detail-header-row">
|
<div class="dataset-detail-header-row">
|
||||||
<span class="dataset-detail-header">Statistics</span>
|
<span class="dataset-detail-header">Overview</span>
|
||||||
<button class="btn btn-icon large" id="refresh-metadata-btn" style="display: none;" title="Refresh metadata from URL">🔄</button>
|
<button class="btn btn-icon large" id="refresh-metadata-btn" style="display: none;" title="Refresh metadata from URL">🔄</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="stats-box">
|
<div class="dataset-overview-grid">
|
||||||
<div class="stat-item">
|
<div class="overview-section">
|
||||||
<span class="stat-label">Rows:</span>
|
<div class="overview-section-title">Statistics</div>
|
||||||
<span id="dataset-detail-rows">0</span>
|
<div class="stats-box">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Rows:</span>
|
||||||
|
<span id="dataset-detail-rows">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Columns:</span>
|
||||||
|
<span id="dataset-detail-columns">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Size:</span>
|
||||||
|
<span id="dataset-detail-size">0 B</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
|
||||||
<span class="stat-label">Columns:</span>
|
|
||||||
<span id="dataset-detail-columns">0</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-item">
|
|
||||||
<span class="stat-label">Size:</span>
|
|
||||||
<span id="dataset-detail-size">0 B</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="dataset-detail-header">Timestamps</div>
|
<div class="overview-section" id="columns-section" style="display: none;">
|
||||||
<div class="stats-box">
|
<div class="overview-section-title">Columns</div>
|
||||||
<div class="stat-item">
|
<div class="columns-list" id="dataset-detail-columns-list">
|
||||||
<span class="stat-label">Created:</span>
|
<!-- Dynamically populated with column names and types -->
|
||||||
<span id="dataset-detail-created">-</span>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-item">
|
|
||||||
<span class="stat-label">Modified:</span>
|
<div class="overview-section">
|
||||||
<span id="dataset-detail-modified">-</span>
|
<div class="overview-section-title">Timestamps</div>
|
||||||
|
<div class="stats-box">
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Created:</span>
|
||||||
|
<span id="dataset-detail-created">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<span class="stat-label">Modified:</span>
|
||||||
|
<span id="dataset-detail-modified">-</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -40,21 +40,32 @@ function calculateDatasetStats(data, format, source) {
|
|||||||
let rowCount = 0;
|
let rowCount = 0;
|
||||||
let columnCount = 0;
|
let columnCount = 0;
|
||||||
let columns = [];
|
let columns = [];
|
||||||
|
let columnTypes = [];
|
||||||
let size = 0;
|
let size = 0;
|
||||||
|
|
||||||
// For URL sources, we can't calculate stats without fetching
|
// For URL sources, we can't calculate stats without fetching
|
||||||
if (source === 'url') {
|
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 (format === 'json' || format === 'topojson') {
|
||||||
if (!Array.isArray(data) || data.length === 0) {
|
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;
|
rowCount = data.length;
|
||||||
const firstRow = data[0];
|
const firstRow = data[0];
|
||||||
columns = typeof firstRow === 'object' ? Object.keys(firstRow) : [];
|
columns = typeof firstRow === 'object' ? Object.keys(firstRow) : [];
|
||||||
columnCount = columns.length;
|
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;
|
size = new Blob([JSON.stringify(data)]).size;
|
||||||
} else if (format === 'csv' || format === 'tsv') {
|
} else if (format === 'csv' || format === 'tsv') {
|
||||||
// For CSV/TSV, data is stored as raw text
|
// For CSV/TSV, data is stored as raw text
|
||||||
@@ -64,11 +75,23 @@ function calculateDatasetStats(data, format, source) {
|
|||||||
const separator = format === 'csv' ? ',' : '\t';
|
const separator = format === 'csv' ? ',' : '\t';
|
||||||
columns = lines[0].split(separator).map(h => h.trim().replace(/^"|"$/g, ''));
|
columns = lines[0].split(separator).map(h => h.trim().replace(/^"|"$/g, ''));
|
||||||
columnCount = columns.length;
|
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;
|
size = new Blob([data]).size;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { rowCount, columnCount, columns, size };
|
return { rowCount, columnCount, columns, columnTypes, size };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dataset Storage API
|
// Dataset Storage API
|
||||||
@@ -243,6 +266,7 @@ async function fetchURLMetadata(url, format) {
|
|||||||
let rowCount = 0;
|
let rowCount = 0;
|
||||||
let columnCount = 0;
|
let columnCount = 0;
|
||||||
let columns = [];
|
let columns = [];
|
||||||
|
let columnTypes = [];
|
||||||
let size = contentLength ? parseInt(contentLength) : new Blob([text]).size;
|
let size = contentLength ? parseInt(contentLength) : new Blob([text]).size;
|
||||||
|
|
||||||
// Parse based on format
|
// Parse based on format
|
||||||
@@ -253,6 +277,15 @@ async function fetchURLMetadata(url, format) {
|
|||||||
if (data.length > 0 && typeof data[0] === 'object') {
|
if (data.length > 0 && typeof data[0] === 'object') {
|
||||||
columns = Object.keys(data[0]);
|
columns = Object.keys(data[0]);
|
||||||
columnCount = columns.length;
|
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') {
|
} else if (format === 'csv' || format === 'tsv') {
|
||||||
@@ -262,6 +295,18 @@ async function fetchURLMetadata(url, format) {
|
|||||||
const separator = format === 'csv' ? ',' : '\t';
|
const separator = format === 'csv' ? ',' : '\t';
|
||||||
columns = lines[0].split(separator).map(h => h.trim().replace(/^"|"$/g, ''));
|
columns = lines[0].split(separator).map(h => h.trim().replace(/^"|"$/g, ''));
|
||||||
columnCount = columns.length;
|
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') {
|
} else if (format === 'topojson') {
|
||||||
// TopoJSON structure is complex, just note it exists
|
// TopoJSON structure is complex, just note it exists
|
||||||
@@ -269,7 +314,7 @@ async function fetchURLMetadata(url, format) {
|
|||||||
columnCount = null;
|
columnCount = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { rowCount, columnCount, columns, size };
|
return { rowCount, columnCount, columns, columnTypes, size };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to fetch URL metadata: ${error.message}`);
|
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-created').textContent = new Date(dataset.created).toLocaleString();
|
||||||
document.getElementById('dataset-detail-modified').textContent = new Date(dataset.modified).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 `
|
||||||
|
<div class="column-item">
|
||||||
|
<span class="column-type-icon">${icon}</span>
|
||||||
|
<span class="column-name">${col.name}</span>
|
||||||
|
<span class="column-type">${col.type}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
columnsList.innerHTML = columnsHTML;
|
||||||
|
} else {
|
||||||
|
columnsSection.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
// Show/hide preview toggle based on data type
|
// Show/hide preview toggle based on data type
|
||||||
const toggleGroup = document.getElementById('preview-toggle-group');
|
const toggleGroup = document.getElementById('preview-toggle-group');
|
||||||
const canShowTable = (dataset.format === 'json' || dataset.format === 'csv' || dataset.format === 'tsv');
|
const canShowTable = (dataset.format === 'json' || dataset.format === 'csv' || dataset.format === 'tsv');
|
||||||
|
|||||||
@@ -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: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-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 & Preview Boxes */
|
||||||
.stats-box { background: var(--bg-light); border: 1px inset var(--win-gray); padding: 8px; font-size: 10px; color: var(--text-primary); }
|
.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 { display: flex; justify-content: space-between; margin-bottom: 4px; }
|
||||||
.stat-item:last-child { margin-bottom: 0; }
|
.stat-item:last-child { margin-bottom: 0; }
|
||||||
.stat-label { font-weight: bold; }
|
.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 { 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.medium { max-height: 150px; }
|
||||||
.preview-box.large { max-height: 200px; }
|
.preview-box.large { max-height: 200px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user