diff --git a/sample-data.json b/sample-data.json new file mode 100644 index 0000000..5bfcec8 --- /dev/null +++ b/sample-data.json @@ -0,0 +1,296 @@ +{ + "version": "1.0", + "exportedAt": "2025-10-19T17:08:27.358Z", + "exportedBy": "Astrolabe", + "snippets": [ + { + "id": 1760877277665.0476, + "name": "Inline Data Bar Chart (Sample)", + "created": "2025-10-19T12:34:36.985Z", + "modified": "2025-10-19T17:07:38.044Z", + "spec": { + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "description": "A simple bar chart with embedded data.", + "data": { + "values": [ + { + "category": "A", + "value": 28 + }, + { + "category": "B", + "value": 55 + }, + { + "category": "C", + "value": 43 + }, + { + "category": "D", + "value": 91 + }, + { + "category": "E", + "value": 81 + }, + { + "category": "F", + "value": 53 + }, + { + "category": "G", + "value": 19 + }, + { + "category": "H", + "value": 87 + } + ] + }, + "mark": "bar", + "encoding": { + "x": { + "field": "category", + "type": "nominal", + "axis": { + "labelAngle": 0 + } + }, + "y": { + "field": "value", + "type": "quantitative" + } + } + }, + "draftSpec": { + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "description": "A simple bar chart with embedded data.", + "data": { + "values": [ + { + "category": "A", + "value": 28 + }, + { + "category": "B", + "value": 55 + }, + { + "category": "C", + "value": 43 + }, + { + "category": "D", + "value": 91 + }, + { + "category": "E", + "value": 81 + }, + { + "category": "F", + "value": 53 + }, + { + "category": "G", + "value": 19 + }, + { + "category": "H", + "value": 87 + } + ] + }, + "mark": "bar", + "encoding": { + "x": { + "field": "category", + "type": "nominal", + "axis": { + "labelAngle": 0 + } + }, + "y": { + "field": "value", + "type": "quantitative" + } + } + }, + "comment": "", + "tags": [], + "datasetRefs": [], + "meta": {} + }, + { + "id": 1760891255058.404, + "name": "World Population Area Chart (Sample)", + "created": "2025-10-19T16:27:34.366Z", + "modified": "2025-10-19T16:44:48.633Z", + "spec": { + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": { + "name": "World Population (Sample)" + }, + "width": "container", + "height": "container", + "mark": "area", + "encoding": { + "x": { + "field": "Year", + "timeUnit": "year" + }, + "y": { + "field": "Growth", + "type": "quantitative" + } + } + }, + "draftSpec": { + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": { + "name": "World Population (Sample)" + }, + "width": "container", + "height": "container", + "mark": "area", + "encoding": { + "x": { + "field": "Year", + "timeUnit": "year" + }, + "y": { + "field": "Growth", + "type": "quantitative" + } + } + }, + "comment": "Visualization using dataset: World Population (Sample)", + "tags": [], + "datasetRefs": [ + "World Population (Sample)" + ], + "meta": {} + }, + { + "id": 1760891802060.8884, + "name": "CO2 Emissions Line Chart (Sample)", + "created": "2025-10-19T16:36:41.432Z", + "modified": "2025-10-19T16:45:32.295Z", + "spec": { + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": { + "name": "CO2 Concentration (Sample)" + }, + "width": 200, + "height": 600, + "mark": "line", + "encoding": { + "x": { + "field": "Date", + "timeUnit": "month" + }, + "color": { + "field": "Date", + "timeUnit": "year", + "type": "ordinal" + }, + "y": { + "field": "CO2", + "type": "quantitative", + "scale": { + "zero": false + }, + "aggregate": "average" + } + } + }, + "draftSpec": { + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": { + "name": "CO2 Concentration (Sample)" + }, + "width": 200, + "height": 600, + "mark": "line", + "encoding": { + "x": { + "field": "Date", + "timeUnit": "month" + }, + "color": { + "field": "Date", + "timeUnit": "year", + "type": "ordinal" + }, + "y": { + "field": "CO2", + "type": "quantitative", + "scale": { + "zero": false + }, + "aggregate": "average" + } + } + }, + "comment": "Visualization using dataset: CO2 Concentration (Sample)", + "tags": [], + "datasetRefs": [ + "CO2 Concentration (Sample)" + ], + "meta": {} + } + ], + "datasets": [ + { + "id": 1760891191833.647, + "name": "World Population (Sample)", + "created": "2025-10-19T16:26:31.174Z", + "modified": "2025-10-19T16:26:31.174Z", + "data": "Year\tPopulation\tGrowth\n1951\t2543130380\t0.0175\n1952\t2590270899\t0.0185\n1953\t2640278797\t0.0193\n1954\t2691979339\t0.0196\n1955\t2746072141\t0.0201\n1956\t2801002631\t0.0200\n1957\t2857866857\t0.0203\n1958\t2916108097\t0.0204\n1959\t2970292188\t0.0186\n1960\t3019233434\t0.0165\n1961\t3068370609\t0.0163\n1962\t3126686743\t0.0190\n1963\t3195779247\t0.0221\n1964\t3267212338\t0.0224\n1965\t3337111983\t0.0214\n1966\t3406417036\t0.0208\n1967\t3475448166\t0.0203\n1968\t3546810808\t0.0205\n1969\t3620655275\t0.0208\n1970\t3695390336\t0.0206\n1971\t3770163092\t0.0202\n1972\t3844800885\t0.0198\n1973\t3920251504\t0.0196\n1974\t3995517077\t0.0192\n1975\t4069437231\t0.0185\n1976\t4142505882\t0.0180\n1977\t4215772490\t0.0177\n1978\t4289657708\t0.0175\n1979\t4365582871\t0.0177\n1980\t4444007706\t0.0180\n1981\t4524627658\t0.0181\n1982\t4607984871\t0.0184\n1983\t4691884238\t0.0182\n1984\t4775836074\t0.0179\n1985\t4861730613\t0.0180\n1986\t4950063339\t0.0182\n1987\t5040984495\t0.0184\n1988\t5132293974\t0.0181\n1989\t5223704308\t0.0178\n1990\t5316175862\t0.0177\n1991\t5406245867\t0.0169\n1992\t5492686093\t0.0160\n1993\t5577433523\t0.0154\n1994\t5660727993\t0.0149\n1995\t5743219454\t0.0146\n1996\t5825145298\t0.0143\n1997\t5906481261\t0.0140\n1998\t5987312480\t0.0137\n1999\t6067758458\t0.0134\n2000\t6148898975\t0.0134\n2001\t6230746982\t0.0133\n2002\t6312407360\t0.0131\n2003\t6393898365\t0.0129\n2004\t6475751478\t0.0128\n2005\t6558176119\t0.0127\n2006\t6641416218\t0.0127\n2007\t6725948544\t0.0127\n2008\t6811597272\t0.0127\n2009\t6898305908\t0.0127\n2010\t6985603105\t0.0127\n2011\t7073125425\t0.0125\n2012\t7161697921\t0.0125\n2013\t7250593370\t0.0124\n2014\t7339013419\t0.0122\n2015\t7426597537\t0.0119\n2016\t7513474238\t0.0117\n2017\t7599822404\t0.0115\n2018\t7683789828\t0.0110\n2019\t7764951032\t0.0106\n2020\t7840952880\t0.0098\n2021\t7909295151\t0.0087\n2022\t7975105156\t0.0083\n2023\t8045311447\t0.0088", + "format": "tsv", + "source": "inline", + "comment": "Sample dataset from Wiki: https://en.wikipedia.org/wiki/World_population", + "rowCount": 73, + "columnCount": 3, + "columns": [ + "Year", + "Population", + "Growth" + ], + "columnTypes": [ + { + "name": "Year", + "type": "number" + }, + { + "name": "Population", + "type": "number" + }, + { + "name": "Growth", + "type": "number" + } + ], + "size": 1701 + }, + { + "id": 1760891542328.1973, + "name": "CO2 Concentration (Sample)", + "created": "2025-10-19T16:32:21.911Z", + "modified": "2025-10-19T16:32:46.230Z", + "data": "https://raw.githubusercontent.com/vega/vega-datasets/refs/heads/main/data/co2-concentration.csv", + "format": "csv", + "source": "url", + "comment": "", + "rowCount": 741, + "columnCount": 3, + "columns": [ + "Date", + "CO2", + "adjusted CO2" + ], + "columnTypes": [], + "size": 18547 + } + ] +} \ No newline at end of file diff --git a/src/js/app.js b/src/js/app.js index 8a72f0e..b568aaa 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -8,27 +8,27 @@ document.addEventListener('DOMContentLoaded', function () { const theme = getSetting('ui.theme') || 'light'; document.documentElement.setAttribute('data-theme', theme); - // Initialize snippet storage and render list - initializeSnippetsStorage(); + // Initialize snippet storage and render list (async) + initializeSnippetsStorage().then(() => { + // Initialize sort controls + initializeSortControls(); - // Initialize sort controls - initializeSortControls(); + // Initialize search controls + initializeSearchControls(); - // Initialize search controls - initializeSearchControls(); + renderSnippetList(); - renderSnippetList(); + // Update storage monitor + updateStorageMonitor(); - // Update storage monitor - updateStorageMonitor(); - - // Auto-select first snippet on page load (only if no hash in URL) - if (!window.location.hash) { - const firstSnippet = SnippetStorage.listSnippets()[0]; - if (firstSnippet) { - selectSnippet(firstSnippet.id); + // Auto-select first snippet on page load (only if no hash in URL) + if (!window.location.hash) { + const firstSnippet = SnippetStorage.listSnippets()[0]; + if (firstSnippet) { + selectSnippet(firstSnippet.id); + } } - } + }); // Load saved layout loadLayoutFromStorage(); diff --git a/src/js/snippet-manager.js b/src/js/snippet-manager.js index 650cdab..6641b56 100644 --- a/src/js/snippet-manager.js +++ b/src/js/snippet-manager.js @@ -280,12 +280,27 @@ const SnippetStorage = { } }; -// Initialize storage with default snippet if empty -function initializeSnippetsStorage() { +// Initialize storage with sample data from JSON file if empty +async function initializeSnippetsStorage() { const existingSnippets = SnippetStorage.loadSnippets(); if (existingSnippets.length === 0) { - // Create default snippet using the sample spec from config + // Try loading sample data from JSON file + try { + const response = await fetch('sample-data.json'); + if (response.ok) { + const sampleData = await response.json(); + const result = await processImportedData(sampleData, { silent: true }); + + if (result.success) { + return result.normalizedSnippets; + } + } + } catch (error) { + console.warn('Failed to load sample-data.json, using fallback:', error); + } + + // Fallback: create default snippet using the sample spec from config const defaultSnippet = createSnippet(sampleSpec, "Sample Bar Chart"); defaultSnippet.comment = "A simple bar chart showing category values"; @@ -1296,6 +1311,139 @@ function estimateImportFit(existingSnippets, newSnippets) { }; } +// Core logic to process imported data (shared between file import and initial sample data) +async function processImportedData(importedData, options = {}) { + const { silent = false } = options; + + // Detect format: legacy (array) or unified (object with version) + let snippetsToImport = []; + let datasetsToImport = []; + + if (Array.isArray(importedData)) { + // Legacy format: array of snippets only + snippetsToImport = importedData; + } else if (importedData.version && importedData.snippets) { + // New unified format + snippetsToImport = importedData.snippets || []; + datasetsToImport = importedData.datasets || []; + } else { + // Single snippet object + snippetsToImport = [importedData]; + } + + if (snippetsToImport.length === 0) { + if (!silent) Toast.info('No snippets found in file'); + return { success: false }; + } + + // Import datasets first (if any) + let datasetsImported = 0; + const renamedDatasets = []; // Track renamed datasets for warning + + for (const datasetData of datasetsToImport) { + try { + let datasetName = datasetData.name; + const originalName = datasetName; + + // Handle name conflicts by renaming + if (await DatasetStorage.nameExists(datasetName)) { + const timestamp = Date.now().toString().slice(-6); + datasetName = `${originalName}_${timestamp}`; + + // Unlikely, but ensure uniqueness + let counter = 1; + while (await DatasetStorage.nameExists(datasetName)) { + datasetName = `${originalName}_${timestamp}_${counter}`; + counter++; + } + + renamedDatasets.push({ from: originalName, to: datasetName }); + } + + await DatasetStorage.createDataset( + datasetName, + datasetData.data, + datasetData.format, + datasetData.source, + datasetData.comment || '' + ); + datasetsImported++; + } catch (error) { + console.warn(`Failed to import dataset ${datasetData.name}:`, error); + } + } + + // Import snippets (existing normalization logic) + const existingSnippets = SnippetStorage.loadSnippets(); + const existingIds = new Set(existingSnippets.map(s => s.id)); + + let snippetsImported = 0; + const normalizedSnippets = []; + + snippetsToImport.forEach(snippet => { + const normalized = normalizeSnippet(snippet); + + // Ensure no ID conflicts + while (existingIds.has(normalized.id)) { + normalized.id = generateSnippetId(); + } + + normalizedSnippets.push(normalized); + existingIds.add(normalized.id); + snippetsImported++; + }); + + // Estimate storage fit + const fit = estimateImportFit(existingSnippets, normalizedSnippets); + + if (!fit.willFit && !silent) { + Toast.warning( + `⚠️ Import is ${formatBytes(fit.overageBytes)} over the 5 MB limit. Attempting to load...`, + 5000 + ); + } + + // Save snippets + const allSnippets = existingSnippets.concat(normalizedSnippets); + + if (SnippetStorage.saveSnippets(allSnippets)) { + if (!silent) { + let message = `Imported ${snippetsImported} snippet${snippetsImported !== 1 ? 's' : ''}`; + if (datasetsImported > 0) { + message += ` and ${datasetsImported} dataset${datasetsImported !== 1 ? 's' : ''}`; + } + + // Warn about renamed datasets + if (renamedDatasets.length > 0) { + const renameList = renamedDatasets.map(r => `"${r.from}" → "${r.to}"`).join(', '); + Toast.warning( + `${message}. Some datasets were renamed due to conflicts: ${renameList}. You may need to update dataset references in affected snippets.`, + 8000 + ); + } else { + Toast.success(message); + } + + // Track event + Analytics.track('project-import', `Import ${snippetsImported} snippets, ${datasetsImported} datasets`); + } + + renderSnippetList(); + updateStorageMonitor(); + + return { success: true, snippetsImported, datasetsImported, normalizedSnippets }; + } else { + const overageBytes = fit.overageBytes > 0 ? fit.overageBytes : calculateDataSize(allSnippets) - STORAGE_LIMIT_BYTES; + if (!silent) { + Toast.error( + `Storage quota exceeded by ${formatBytes(overageBytes)}. Please delete some snippets and try again.`, + 6000 + ); + } + return { success: false }; + } +} + // Import snippets and datasets from JSON file function importSnippets(fileInput) { const file = fileInput.files[0]; @@ -1305,128 +1453,7 @@ function importSnippets(fileInput) { reader.onload = async function(e) { try { const importedData = JSON.parse(e.target.result); - - // Detect format: legacy (array) or unified (object with version) - let snippetsToImport = []; - let datasetsToImport = []; - - if (Array.isArray(importedData)) { - // Legacy format: array of snippets only - snippetsToImport = importedData; - } else if (importedData.version && importedData.snippets) { - // New unified format - snippetsToImport = importedData.snippets || []; - datasetsToImport = importedData.datasets || []; - } else { - // Single snippet object - snippetsToImport = [importedData]; - } - - if (snippetsToImport.length === 0) { - Toast.info('No snippets found in file'); - return; - } - - // Import datasets first (if any) - let datasetsImported = 0; - const renamedDatasets = []; // Track renamed datasets for warning - - for (const datasetData of datasetsToImport) { - try { - let datasetName = datasetData.name; - const originalName = datasetName; - - // Handle name conflicts by renaming - if (await DatasetStorage.nameExists(datasetName)) { - const timestamp = Date.now().toString().slice(-6); - datasetName = `${originalName}_${timestamp}`; - - // Unlikely, but ensure uniqueness - let counter = 1; - while (await DatasetStorage.nameExists(datasetName)) { - datasetName = `${originalName}_${timestamp}_${counter}`; - counter++; - } - - renamedDatasets.push({ from: originalName, to: datasetName }); - } - - await DatasetStorage.createDataset( - datasetName, - datasetData.data, - datasetData.format, - datasetData.source, - datasetData.comment || '' - ); - datasetsImported++; - } catch (error) { - console.warn(`Failed to import dataset ${datasetData.name}:`, error); - } - } - - // Import snippets (existing normalization logic) - const existingSnippets = SnippetStorage.loadSnippets(); - const existingIds = new Set(existingSnippets.map(s => s.id)); - - let snippetsImported = 0; - const normalizedSnippets = []; - - snippetsToImport.forEach(snippet => { - const normalized = normalizeSnippet(snippet); - - // Ensure no ID conflicts - while (existingIds.has(normalized.id)) { - normalized.id = generateSnippetId(); - } - - normalizedSnippets.push(normalized); - existingIds.add(normalized.id); - snippetsImported++; - }); - - // Estimate storage fit - const fit = estimateImportFit(existingSnippets, normalizedSnippets); - - if (!fit.willFit) { - Toast.warning( - `⚠️ Import is ${formatBytes(fit.overageBytes)} over the 5 MB limit. Attempting to load...`, - 5000 - ); - } - - // Save snippets - const allSnippets = existingSnippets.concat(normalizedSnippets); - - if (SnippetStorage.saveSnippets(allSnippets)) { - let message = `Imported ${snippetsImported} snippet${snippetsImported !== 1 ? 's' : ''}`; - if (datasetsImported > 0) { - message += ` and ${datasetsImported} dataset${datasetsImported !== 1 ? 's' : ''}`; - } - - // Warn about renamed datasets - if (renamedDatasets.length > 0) { - const renameList = renamedDatasets.map(r => `"${r.from}" → "${r.to}"`).join(', '); - Toast.warning( - `${message}. Some datasets were renamed due to conflicts: ${renameList}. You may need to update dataset references in affected snippets.`, - 8000 - ); - } else { - Toast.success(message); - } - - renderSnippetList(); - updateStorageMonitor(); - - // Track event - Analytics.track('project-import', `Import ${snippetsImported} snippets, ${datasetsImported} datasets`); - } else { - const overageBytes = fit.overageBytes > 0 ? fit.overageBytes : calculateDataSize(allSnippets) - STORAGE_LIMIT_BYTES; - Toast.error( - `Storage quota exceeded by ${formatBytes(overageBytes)}. Please delete some snippets and try again.`, - 6000 - ); - } - + await processImportedData(importedData); } catch (error) { console.error('Import error:', error); Toast.error('Failed to import. Please check that the file is valid JSON.');