From 23878de0b26389a608ad98a0f48378436856de02 Mon Sep 17 00:00:00 2001 From: mathleur Date: Thu, 26 Feb 2026 16:28:12 +0100 Subject: [PATCH 1/2] start to add jupyter lite to work on the data --- stac_server/static/app.js | 309 ++++++++++++++++++++++++++++++- stac_server/static/styles.css | 221 ++++++++++++++++++++++ stac_server/templates/index.html | 58 ++++++ 3 files changed, 585 insertions(+), 3 deletions(-) diff --git a/stac_server/static/app.js b/stac_server/static/app.js index 23899c8..73f5d1e 100644 --- a/stac_server/static/app.js +++ b/stac_server/static/app.js @@ -910,6 +910,9 @@ async function queryPolytope() { polytopeStatus.textContent = `Successfully submitted ${result.total} request(s). ${result.successful} succeeded, ${result.failed} failed.`; polytopeStatus.className = 'polytope-status success'; + // Store results globally for notebook access + window.polytopeResults = result.results; + // Display detailed results if (result.results && result.results.length > 0) { polytopeResults.innerHTML = result.results.map((res, idx) => ` @@ -925,9 +928,17 @@ async function queryPolytope() { ${res.message ? `
${res.message}
` : ''} ${res.success && res.json_data ? ` - +
+ + +
` : ''} `).join(''); @@ -943,6 +954,17 @@ async function queryPolytope() { } }); }); + + // Add event listeners to notebook buttons + document.querySelectorAll('.open-notebook-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + const idx = parseInt(e.target.closest('.open-notebook-btn').getAttribute('data-request-idx')); + const resultData = result.results[idx]; + if (resultData && resultData.json_data) { + openInNotebook(resultData.json_data, idx); + } + }); + }); } polytopeBtnText.textContent = 'Query Complete'; @@ -959,6 +981,269 @@ async function queryPolytope() { } } +// ============================================ +// JupyterLite Notebook Integration +// ============================================ + +let pyodideInstance = null; +let codeEditor = null; +let currentNotebookData = null; + +async function initPyodide() { + if (pyodideInstance) { + return pyodideInstance; + } + + console.log('Initializing Pyodide...'); + pyodideInstance = await loadPyodide({ + indexURL: "https://cdn.jsdelivr.net/pyodide/v0.25.0/full/" + }); + + // Load commonly used packages + await pyodideInstance.loadPackage(['numpy', 'matplotlib']); + console.log('Pyodide initialized successfully!'); + + return pyodideInstance; +} + +function initCodeEditor() { + if (codeEditor) { + return codeEditor; + } + + const editorElement = document.getElementById('code-editor'); + codeEditor = CodeMirror(editorElement, { + value: getDefaultNotebookCode(), + mode: 'python', + theme: 'monokai', + lineNumbers: true, + indentUnit: 4, + tabSize: 4, + indentWithTabs: false, + lineWrapping: true, + }); + + return codeEditor; +} + +function getDefaultNotebookCode() { + return `# Polytope Data Visualization +# The data is available in the 'polytope_data' variable + +import json + +# Print data structure +print("Data type:", type(polytope_data)) +print("\\nData preview:") +if isinstance(polytope_data, dict): + print("Keys:", list(polytope_data.keys())) + for key, value in list(polytope_data.items())[:3]: + print(f" {key}: {type(value)}") +elif isinstance(polytope_data, list): + print(f"List with {len(polytope_data)} items") + if len(polytope_data) > 0: + print("First item:", polytope_data[0]) +else: + print(polytope_data) + +# Check if this is COVJSON format +if isinstance(polytope_data, dict) and 'domain' in polytope_data: + print("\\nāœ“ COVJSON format detected!") + domain = polytope_data.get('domain', {}) + axes = domain.get('axes', {}) + print(f"Available axes: {list(axes.keys())}") + + if 'ranges' in polytope_data: + ranges = polytope_data.get('ranges', {}) + print(f"Available parameters: {list(ranges.keys())}") +`; +} + +async function openInNotebook(jsonData, requestIdx) { + const notebookSection = document.getElementById('notebook-section'); + + // Show notebook section + notebookSection.style.display = 'block'; + notebookSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); + + // Store data globally + currentNotebookData = jsonData; + window.currentNotebookRequestIdx = requestIdx; + + // Initialize code editor if not already done + if (!codeEditor) { + initCodeEditor(); + } + + // Update the default code with the request index + const defaultCode = `# Polytope Data Visualization - Request ${requestIdx + 1} +# The data is available in the 'polytope_data' variable + +import json + +# Print data structure +print("Data type:", type(polytope_data)) +print("\\nData preview:") +if isinstance(polytope_data, dict): + print("Keys:", list(polytope_data.keys())) + for key, value in list(polytope_data.items())[:5]: + if isinstance(value, (dict, list)): + print(f" {key}: {type(value).__name__} (length: {len(value)})") + else: + print(f" {key}: {value}") +elif isinstance(polytope_data, list): + print(f"List with {len(polytope_data)} items") + if len(polytope_data) > 0: + print("First item:", polytope_data[0]) +else: + print(str(polytope_data)[:500]) + +# Check if this is COVJSON format +if isinstance(polytope_data, dict) and 'domain' in polytope_data: + print("\\nāœ“ COVJSON format detected!") + domain = polytope_data.get('domain', {}) + axes = domain.get('axes', {}) + print(f"Available axes: {list(axes.keys())}") + + if 'ranges' in polytope_data: + ranges = polytope_data.get('ranges', {}) + print(f"Available parameters: {list(ranges.keys())}") +`; + + codeEditor.setValue(defaultCode); +} + +async function runPythonCode() { + const runBtn = document.getElementById('run-code-btn'); + const runBtnText = document.getElementById('run-code-text'); + const outputDiv = document.getElementById('notebook-output'); + const outputContent = document.getElementById('output-content'); + const loadingDiv = document.getElementById('notebook-loading-exec'); + + if (!currentNotebookData) { + outputContent.textContent = 'Error: No data available. Please query Polytope first.'; + outputDiv.style.display = 'block'; + return; + } + + // Disable button and show loading + runBtn.disabled = true; + runBtnText.textContent = 'Executing...'; + loadingDiv.style.display = 'flex'; + outputDiv.style.display = 'none'; + + try { + // Initialize Pyodide if not already done + if (!pyodideInstance) { + await initPyodide(); + } + + // Get code from editor + const code = codeEditor.getValue(); + + // Convert data to JSON string and inject into Python namespace + const dataJsonString = JSON.stringify(currentNotebookData); + pyodideInstance.globals.set('polytope_data_json', dataJsonString); + + // Parse JSON in Python to get native Python object + await pyodideInstance.runPythonAsync(` +import json +polytope_data = json.loads(polytope_data_json) +`); + + // Capture stdout + await pyodideInstance.runPythonAsync(` +import sys +from io import StringIO +sys.stdout = StringIO() +`); + + // Run user code + try { + await pyodideInstance.runPythonAsync(code); + } catch (error) { + // Capture the error + await pyodideInstance.runPythonAsync(` +import traceback +sys.stdout.write("\\n\\nError:\\n") +sys.stdout.write(traceback.format_exc()) +`); + } + + // Get output + const output = await pyodideInstance.runPythonAsync('sys.stdout.getvalue()'); + + // Display output + outputContent.textContent = output || '(No output)'; + outputDiv.style.display = 'block'; + loadingDiv.style.display = 'none'; + + runBtnText.textContent = 'Run Code'; + runBtn.disabled = false; + + } catch (error) { + console.error('Python execution error:', error); + outputContent.textContent = `Error: ${error.message}`; + outputDiv.style.display = 'block'; + loadingDiv.style.display = 'none'; + runBtnText.textContent = 'Run Code'; + runBtn.disabled = false; + } +} + +function resetCode() { + if (codeEditor) { + const requestIdx = window.currentNotebookRequestIdx || 0; + const defaultCode = `# Polytope Data Visualization - Request ${requestIdx + 1} +# The data is available in the 'polytope_data' variable + +import json + +# Print data structure +print("Data type:", type(polytope_data)) +print("\\nData preview:") +if isinstance(polytope_data, dict): + print("Keys:", list(polytope_data.keys())) + for key, value in list(polytope_data.items())[:5]: + if isinstance(value, (dict, list)): + print(f" {key}: {type(value).__name__} (length: {len(value)})") + else: + print(f" {key}: {value}") +elif isinstance(polytope_data, list): + print(f"List with {len(polytope_data)} items") + if len(polytope_data) > 0: + print("First item:", polytope_data[0]) +else: + print(str(polytope_data)[:500]) + +# Check if this is COVJSON format +if isinstance(polytope_data, dict) and 'domain' in polytope_data: + print("\\nāœ“ COVJSON format detected!") + domain = polytope_data.get('domain', {}) + axes = domain.get('axes', {}) + print(f"Available axes: {list(axes.keys())}") + + if 'ranges' in polytope_data: + ranges = polytope_data.get('ranges', {}) + print(f"Available parameters: {list(ranges.keys())}") +`; + codeEditor.setValue(defaultCode); + } + + // Clear output + const outputDiv = document.getElementById('notebook-output'); + outputDiv.style.display = 'none'; +} + +function closeNotebook() { + const notebookSection = document.getElementById('notebook-section'); + notebookSection.style.display = 'none'; + + // Clear output + const outputDiv = document.getElementById('notebook-output'); + outputDiv.style.display = 'none'; +} + // Call initializeViewer on page load initializeViewer(); @@ -974,4 +1259,22 @@ document.addEventListener("DOMContentLoaded", () => { if (polytopeBtn) { polytopeBtn.addEventListener('click', queryPolytope); } + + // Add event listener for close notebook button + const closeNotebookBtn = document.getElementById('close-notebook-btn'); + if (closeNotebookBtn) { + closeNotebookBtn.addEventListener('click', closeNotebook); + } + + // Add event listener for run code button + const runCodeBtn = document.getElementById('run-code-btn'); + if (runCodeBtn) { + runCodeBtn.addEventListener('click', runPythonCode); + } + + // Add event listener for reset code button + const resetCodeBtn = document.getElementById('reset-code-btn'); + if (resetCodeBtn) { + resetCodeBtn.addEventListener('click', resetCode); + } }); diff --git a/stac_server/static/styles.css b/stac_server/static/styles.css index 31f2c14..7de05d7 100644 --- a/stac_server/static/styles.css +++ b/stac_server/static/styles.css @@ -1396,3 +1396,224 @@ canvas { .download-json-btn:active { transform: translateY(0); } +/* ============================================ + JupyterLite Notebook Section + ============================================ */ + +.notebook-section { + background: var(--bg-primary); + padding: 2rem; + border-radius: var(--radius-lg); + border: 1px solid var(--border-light); + margin-bottom: 2rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.notebook-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.notebook-title { + font-size: 1.3rem; + font-weight: 600; + color: var(--text-primary); + display: flex; + align-items: center; + gap: 0.75rem; + margin: 0; +} + +.notebook-title svg { + color: var(--primary-color); +} + +.close-notebook-btn { + background: transparent; + border: 1px solid var(--border-light); + color: var(--text-secondary); + padding: 0.5rem; + border-radius: var(--radius-sm); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.close-notebook-btn:hover { + background: var(--bg-secondary); + color: var(--text-primary); + border-color: var(--primary-color); +} + +.notebook-description { + color: var(--text-secondary); + margin-bottom: 1.5rem; + line-height: 1.6; +} + +.notebook-description code { + background: var(--bg-secondary); + padding: 0.2rem 0.5rem; + border-radius: 3px; + font-size: 0.9em; + color: var(--primary-color); + font-family: 'Monaco', 'Courier New', monospace; +} + +.notebook-controls { + display: flex; + gap: 0.75rem; + margin-bottom: 1rem; +} + +.run-code-btn, +.reset-code-btn { + padding: 0.6rem 1.2rem; + border: none; + border-radius: var(--radius-md); + cursor: pointer; + font-size: 0.9rem; + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 0.5rem; + transition: all 0.2s ease; +} + +.run-code-btn { + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + color: white; +} + +.run-code-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(0, 115, 230, 0.4); +} + +.run-code-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; +} + +.reset-code-btn { + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--border-light); +} + +.reset-code-btn:hover { + background: var(--bg-tertiary); + border-color: var(--primary-color); +} + +.code-editor { + border: 1px solid var(--border-light); + border-radius: var(--radius-md); + overflow: hidden; + margin-bottom: 1rem; +} + +.CodeMirror { + height: 400px; + font-size: 14px; + font-family: 'Monaco', 'Courier New', monospace; +} + +.notebook-output { + background: var(--bg-secondary); + border: 1px solid var(--border-light); + border-radius: var(--radius-md); + padding: 1rem; + margin-top: 1rem; +} + +.output-title { + font-size: 0.9rem; + font-weight: 600; + color: var(--text-primary); + margin: 0 0 0.75rem 0; +} + +.output-content { + margin: 0; + padding: 0.75rem; + background: #1e1e1e; + color: #d4d4d4; + border-radius: var(--radius-sm); + font-family: 'Monaco', 'Courier New', monospace; + font-size: 0.85rem; + line-height: 1.5; + overflow-x: auto; + white-space: pre-wrap; + word-wrap: break-word; +} + +.notebook-loading-exec { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 1rem; + background: var(--bg-secondary); + border-radius: var(--radius-md); + color: var(--text-secondary); + margin-top: 1rem; +} + +.loading-spinner-small { + width: 20px; + height: 20px; + border: 3px solid var(--border-light); + border-top: 3px solid var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +.loading-spinner { + width: 40px; + height: 40px; + border: 4px solid var(--border-light); + border-top: 4px solid var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 1rem; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.open-notebook-btn { + margin-top: 0.5rem; + padding: 0.5rem 1rem; + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + color: white; + border: none; + border-radius: var(--radius-md); + cursor: pointer; + font-size: 0.9rem; + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 0.5rem; + transition: all 0.2s ease; +} + +.open-notebook-btn:hover { + transform: translateY(-2px); + box-shadow: 0 4px 16px rgba(0, 115, 230, 0.4); +} + +.open-notebook-btn:active { + transform: translateY(0); +} + +.open-notebook-btn svg { + width: 16px; + height: 16px; +} diff --git a/stac_server/templates/index.html b/stac_server/templates/index.html index 8aeb9a8..8c2d30b 100644 --- a/stac_server/templates/index.html +++ b/stac_server/templates/index.html @@ -25,6 +25,15 @@ + + + + + + + + + - - - @@ -247,7 +244,7 @@

Write Python code to visualize and analyze the data. The data is available in the polytope_data variable. - Click "Run Code" to execute (first run takes 10-15 seconds to load Python). + Pre-installed packages: numpy, covjsonkit, earthkit-plots, xarray, matplotlib.

@@ -271,6 +268,7 @@