diff --git a/index.html b/index.html index abdc17e..f7039a4 100644 --- a/index.html +++ b/index.html @@ -41,7 +41,7 @@ - + \ No newline at end of file diff --git a/script.js b/script.js deleted file mode 100644 index 19290be..0000000 --- a/script.js +++ /dev/null @@ -1,308 +0,0 @@ - -// Default snippets -const defaultSnippets = [ - { - id: 'simple-bar', - name: 'Simple Bar Chart', - content: { - "$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 } - ] - }, - "mark": "bar", - "encoding": { - "x": { "field": "category", "type": "nominal" }, - "y": { "field": "value", "type": "quantitative" } - } - } - }, - { - id: 'scatter-plot', - name: 'Basic Scatter Plot', - content: { - "$schema": "https://vega.github.io/schema/vega-lite/v5.json", - "description": "A scatter plot example.", - "data": { - "values": [ - { "x": 1, "y": 28 }, { "x": 2, "y": 55 }, { "x": 3, "y": 43 } - ] - }, - "mark": "point", - "encoding": { - "x": { "field": "x", "type": "quantitative" }, - "y": { "field": "y", "type": "quantitative" } - } - } - } -]; - -class PanelResizer { - constructor() { - this.handleDragStart = this.handleDragStart.bind(this); - this.handleDrag = this.handleDrag.bind(this); - this.handleDragEnd = this.handleDragEnd.bind(this); - this.loadLayout(); - this.initializeResizeHandles(); - } - - loadLayout() { - const stored = localStorage.getItem('panelLayout'); - if (stored) { - const layout = JSON.parse(stored); - document.documentElement.style.setProperty('--snippet-width', layout.snippetWidth); - document.documentElement.style.setProperty('--editor-width', layout.editorWidth); - document.documentElement.style.setProperty('--preview-width', layout.previewWidth); - } - } - - saveLayout() { - const layout = { - snippetWidth: document.documentElement.style.getPropertyValue('--snippet-width'), - editorWidth: document.documentElement.style.getPropertyValue('--editor-width'), - previewWidth: document.documentElement.style.getPropertyValue('--preview-width') - }; - localStorage.setItem('panelLayout', JSON.stringify(layout)); - } - - initializeResizeHandles() { - const handles = document.querySelectorAll('.resize-handle'); - handles.forEach((handle, index) => { - handle.addEventListener('mousedown', (e) => this.handleDragStart(e, index)); - }); - } - - handleDragStart(e, handleIndex) { - this.activeHandle = handleIndex; - this.startX = e.clientX; - this.handle = e.target; - this.handle.classList.add('active'); - - // Get the panels adjacent to the handle - const panels = document.querySelectorAll('.panel'); - this.leftPanel = panels[handleIndex]; - this.rightPanel = panels[handleIndex + 1]; - - // Store initial widths - this.leftWidth = this.leftPanel.getBoundingClientRect().width; - this.rightWidth = this.rightPanel.getBoundingClientRect().width; - - document.addEventListener('mousemove', this.handleDrag); - document.addEventListener('mouseup', this.handleDragEnd); - } - - handleDrag(e) { - if (!this.handle) return; - - const dx = e.clientX - this.startX; - const containerWidth = document.querySelector('.container').getBoundingClientRect().width; - - // Calculate new widths as fractions - const newLeftWidth = `${(this.leftWidth + dx) / containerWidth}fr`; - const newRightWidth = `${(this.rightWidth - dx) / containerWidth}fr`; - - // Apply new widths based on which handle is being dragged - if (this.activeHandle === 0) { - document.documentElement.style.setProperty('--snippet-width', newLeftWidth); - document.documentElement.style.setProperty('--editor-width', newRightWidth); - } else { - document.documentElement.style.setProperty('--editor-width', newLeftWidth); - document.documentElement.style.setProperty('--preview-width', newRightWidth); - } - } - - handleDragEnd() { - if (!this.handle) return; - - this.handle.classList.remove('active'); - this.handle = null; - this.saveLayout(); - - document.removeEventListener('mousemove', this.handleDrag); - document.removeEventListener('mouseup', this.handleDragEnd); - - // Trigger Monaco editor resize - if (window.editor) { - window.editor.layout(); - } - } -} - -// Snippet management -class SnippetManager { - constructor() { - this.currentSnippetId = null; - this.hasUnsavedChanges = false; - this.loadSnippets(); - this.setupUI(); - } - - loadSnippets() { - // Try to load from localStorage - const stored = localStorage.getItem('vegaSnippets'); - this.snippets = stored ? JSON.parse(stored) : defaultSnippets; - - // Initialize localStorage if empty - if (!stored) { - this.saveToStorage(); - } - } - - saveToStorage() { - localStorage.setItem('vegaSnippets', JSON.stringify(this.snippets)); - } - - renderSnippetList() { - const container = document.getElementById('snippet-list'); - container.innerHTML = ''; - - this.snippets.forEach(snippet => { - const div = document.createElement('div'); - div.className = `snippet-item ${snippet.id === this.currentSnippetId ? 'active' : ''}`; - div.textContent = snippet.name; - div.onclick = () => this.loadSnippet(snippet.id); - container.appendChild(div); - }); - } - - loadSnippet(id) { - if (this.hasUnsavedChanges) { - if (!confirm('You have unsaved changes. Do you want to discard them?')) { - return; - } - } - - const snippet = this.snippets.find(s => s.id === id); - if (snippet) { - this.currentSnippetId = id; - this.editor.setValue(JSON.stringify(snippet.content, null, 2)); - this.hasUnsavedChanges = false; - this.updateSaveButton(); - this.renderSnippetList(); - } - } - - createNewSnippet() { - const name = prompt('Enter snippet name:', 'New Snippet'); - if (!name) return; - - const id = 'snippet-' + Date.now(); - const newSnippet = { - id, - name, - content: { - "$schema": "https://vega.github.io/schema/vega-lite/v5.json", - "description": "New visualization", - "mark": "bar" - } - }; - - this.snippets.push(newSnippet); - this.saveToStorage(); - this.loadSnippet(id); - } - - saveCurrentSnippet() { - if (!this.currentSnippetId) return; - - try { - const content = JSON.parse(this.editor.getValue()); - const snippetIndex = this.snippets.findIndex(s => s.id === this.currentSnippetId); - - if (snippetIndex !== -1) { - this.snippets[snippetIndex].content = content; - this.saveToStorage(); - this.hasUnsavedChanges = false; - this.updateSaveButton(); - } - } catch (e) { - alert('Invalid JSON in editor'); - } - } - - updateSaveButton() { - const saveButton = document.getElementById('save-snippet'); - saveButton.disabled = !this.hasUnsavedChanges; - } - - setupUI() { - // New snippet button - document.getElementById('new-snippet').onclick = () => this.createNewSnippet(); - - // Save button - document.getElementById('save-snippet').onclick = () => this.saveCurrentSnippet(); - - // Initial render - this.renderSnippetList(); - } - - setEditor(editor) { - this.editor = editor; - - // Setup change tracking and visualization update - let timeoutId = null; - editor.onDidChangeModelContent(() => { - this.hasUnsavedChanges = true; - this.updateSaveButton(); - - // Debounce visualization updates - if (timeoutId) clearTimeout(timeoutId); - timeoutId = setTimeout(() => { - try { - const value = editor.getValue(); - this.updateVisualization(value); - } catch (e) { - console.error('Invalid JSON:', e); - } - }, 300); - }); - - // Load first snippet if available - if (this.snippets.length > 0) { - this.loadSnippet(this.snippets[0].id); - } - } - - async updateVisualization(spec) { - try { - const parsedSpec = typeof spec === 'string' ? JSON.parse(spec) : spec; - await vegaEmbed('#vis', parsedSpec, { - actions: true, // This adds the export/view source buttons - theme: 'light' - }); - } catch (err) { - console.error('Error rendering visualization:', err); - // Optionally show error in the preview panel - document.getElementById('vis').innerHTML = - `
Error rendering visualization: ${err.message}
`; - } - } -} - -// Initialize Monaco -require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.47.0/min/vs' } }); - -const snippetManager = new SnippetManager(); - -require(['vs/editor/editor.main'], async function () { - // Create editor instance - const editor = monaco.editor.create(document.getElementById('monaco-editor'), { - language: 'json', - theme: 'vs-light', - wordWrap: 'on', - minimap: { enabled: false }, - automaticLayout: true, - formatOnPaste: true, - formatOnType: true - }); - - const resizer = new PanelResizer(); - window.editor = editor; - - // Connect editor to snippet manager - snippetManager.setEditor(editor); -}); diff --git a/src/PanelResizer.js b/src/PanelResizer.js new file mode 100644 index 0000000..9b571b3 --- /dev/null +++ b/src/PanelResizer.js @@ -0,0 +1,90 @@ +export class PanelResizer { + constructor() { + this.handleDragStart = this.handleDragStart.bind(this); + this.handleDrag = this.handleDrag.bind(this); + this.handleDragEnd = this.handleDragEnd.bind(this); + this.loadLayout(); + this.initializeResizeHandles(); + } + + loadLayout() { + const stored = localStorage.getItem('panelLayout'); + if (stored) { + const layout = JSON.parse(stored); + document.documentElement.style.setProperty('--snippet-width', layout.snippetWidth); + document.documentElement.style.setProperty('--editor-width', layout.editorWidth); + document.documentElement.style.setProperty('--preview-width', layout.previewWidth); + } + } + + saveLayout() { + const layout = { + snippetWidth: document.documentElement.style.getPropertyValue('--snippet-width'), + editorWidth: document.documentElement.style.getPropertyValue('--editor-width'), + previewWidth: document.documentElement.style.getPropertyValue('--preview-width') + }; + localStorage.setItem('panelLayout', JSON.stringify(layout)); + } + + initializeResizeHandles() { + const handles = document.querySelectorAll('.resize-handle'); + handles.forEach((handle, index) => { + handle.addEventListener('mousedown', (e) => this.handleDragStart(e, index)); + }); + } + + handleDragStart(e, handleIndex) { + this.activeHandle = handleIndex; + this.startX = e.clientX; + this.handle = e.target; + this.handle.classList.add('active'); + + // Get the panels adjacent to the handle + const panels = document.querySelectorAll('.panel'); + this.leftPanel = panels[handleIndex]; + this.rightPanel = panels[handleIndex + 1]; + + // Store initial widths + this.leftWidth = this.leftPanel.getBoundingClientRect().width; + this.rightWidth = this.rightPanel.getBoundingClientRect().width; + + document.addEventListener('mousemove', this.handleDrag); + document.addEventListener('mouseup', this.handleDragEnd); + } + + handleDrag(e) { + if (!this.handle) return; + + const dx = e.clientX - this.startX; + const containerWidth = document.querySelector('.container').getBoundingClientRect().width; + + // Calculate new widths as fractions + const newLeftWidth = `${(this.leftWidth + dx) / containerWidth}fr`; + const newRightWidth = `${(this.rightWidth - dx) / containerWidth}fr`; + + // Apply new widths based on which handle is being dragged + if (this.activeHandle === 0) { + document.documentElement.style.setProperty('--snippet-width', newLeftWidth); + document.documentElement.style.setProperty('--editor-width', newRightWidth); + } else { + document.documentElement.style.setProperty('--editor-width', newLeftWidth); + document.documentElement.style.setProperty('--preview-width', newRightWidth); + } + } + + handleDragEnd() { + if (!this.handle) return; + + this.handle.classList.remove('active'); + this.handle = null; + this.saveLayout(); + + document.removeEventListener('mousemove', this.handleDrag); + document.removeEventListener('mouseup', this.handleDragEnd); + + // Trigger Monaco editor resize + if (window.editor) { + window.editor.layout(); + } + } +} \ No newline at end of file diff --git a/src/SnippetManager.js b/src/SnippetManager.js new file mode 100644 index 0000000..7ada9df --- /dev/null +++ b/src/SnippetManager.js @@ -0,0 +1,149 @@ +export class SnippetManager { + constructor() { + this.currentSnippetId = null; + this.hasUnsavedChanges = false; + this.loadSnippets(); + this.setupUI(); + } + + loadSnippets() { + // Try to load from localStorage + const stored = localStorage.getItem('vegaSnippets'); + this.snippets = stored ? JSON.parse(stored) : defaultSnippets; + + // Initialize localStorage if empty + if (!stored) { + this.saveToStorage(); + } + } + + saveToStorage() { + localStorage.setItem('vegaSnippets', JSON.stringify(this.snippets)); + } + + renderSnippetList() { + const container = document.getElementById('snippet-list'); + container.innerHTML = ''; + + this.snippets.forEach(snippet => { + const div = document.createElement('div'); + div.className = `snippet-item ${snippet.id === this.currentSnippetId ? 'active' : ''}`; + div.textContent = snippet.name; + div.onclick = () => this.loadSnippet(snippet.id); + container.appendChild(div); + }); + } + + loadSnippet(id) { + if (this.hasUnsavedChanges) { + if (!confirm('You have unsaved changes. Do you want to discard them?')) { + return; + } + } + + const snippet = this.snippets.find(s => s.id === id); + if (snippet) { + this.currentSnippetId = id; + this.editor.setValue(JSON.stringify(snippet.content, null, 2)); + this.hasUnsavedChanges = false; + this.updateSaveButton(); + this.renderSnippetList(); + } + } + + createNewSnippet() { + const name = prompt('Enter snippet name:', 'New Snippet'); + if (!name) return; + + const id = 'snippet-' + Date.now(); + const newSnippet = { + id, + name, + content: { + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "description": "New visualization", + "mark": "bar" + } + }; + + this.snippets.push(newSnippet); + this.saveToStorage(); + this.loadSnippet(id); + } + + saveCurrentSnippet() { + if (!this.currentSnippetId) return; + + try { + const content = JSON.parse(this.editor.getValue()); + const snippetIndex = this.snippets.findIndex(s => s.id === this.currentSnippetId); + + if (snippetIndex !== -1) { + this.snippets[snippetIndex].content = content; + this.saveToStorage(); + this.hasUnsavedChanges = false; + this.updateSaveButton(); + } + } catch (e) { + alert('Invalid JSON in editor'); + } + } + + updateSaveButton() { + const saveButton = document.getElementById('save-snippet'); + saveButton.disabled = !this.hasUnsavedChanges; + } + + setupUI() { + // New snippet button + document.getElementById('new-snippet').onclick = () => this.createNewSnippet(); + + // Save button + document.getElementById('save-snippet').onclick = () => this.saveCurrentSnippet(); + + // Initial render + this.renderSnippetList(); + } + + setEditor(editor) { + this.editor = editor; + + // Setup change tracking and visualization update + let timeoutId = null; + editor.onDidChangeModelContent(() => { + this.hasUnsavedChanges = true; + this.updateSaveButton(); + + // Debounce visualization updates + if (timeoutId) clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + try { + const value = editor.getValue(); + this.updateVisualization(value); + } catch (e) { + console.error('Invalid JSON:', e); + } + }, 300); + }); + + // Load first snippet if available + if (this.snippets.length > 0) { + this.loadSnippet(this.snippets[0].id); + } + } + + async updateVisualization(spec) { + try { + const parsedSpec = typeof spec === 'string' ? JSON.parse(spec) : spec; + await vegaEmbed('#vis', parsedSpec, { + actions: true, // This adds the export/view source buttons + theme: 'light' + }); + } catch (err) { + console.error('Error rendering visualization:', err); + // Optionally show error in the preview panel + document.getElementById('vis').innerHTML = + `
Error rendering visualization: ${err.message}
`; + } + } +} \ No newline at end of file diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..53ea9de --- /dev/null +++ b/src/config.js @@ -0,0 +1,40 @@ +export const defaultSnippets = [ + { + id: 'simple-bar', + name: 'Simple Bar Chart', + content: { + "$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 } + ] + }, + "mark": "bar", + "encoding": { + "x": { "field": "category", "type": "nominal" }, + "y": { "field": "value", "type": "quantitative" } + } + } + }, + { + id: 'scatter-plot', + name: 'Basic Scatter Plot', + content: { + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "description": "A scatter plot example.", + "data": { + "values": [ + { "x": 1, "y": 28 }, { "x": 2, "y": 55 }, { "x": 3, "y": 43 } + ] + }, + "mark": "point", + "encoding": { + "x": { "field": "x", "type": "quantitative" }, + "y": { "field": "y", "type": "quantitative" } + } + } + } +]; diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..a48bcce --- /dev/null +++ b/src/main.js @@ -0,0 +1,24 @@ +import { PanelResizer } from './PanelResizer.js'; +import { SnippetManager } from './SnippetManager.js'; + +// Initialize Monaco +require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.47.0/min/vs' } }); + +const snippetManager = new SnippetManager(); + +require(['vs/editor/editor.main'], async function () { + const editor = monaco.editor.create(document.getElementById('monaco-editor'), { + language: 'json', + theme: 'vs-light', + wordWrap: 'on', + minimap: { enabled: false }, + automaticLayout: true, + formatOnPaste: true, + formatOnType: true + }); + + const resizer = new PanelResizer(); + window.editor = editor; + + snippetManager.setEditor(editor); +});