mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2025-12-21 13:12:24 +00:00
move scripts
This commit is contained in:
310
index.html
310
index.html
@@ -41,315 +41,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 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 =
|
||||
`<div style="color: red; padding: 1rem;">Error rendering visualization: ${err.message}</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
</script>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
308
script.js
Normal file
308
script.js
Normal file
@@ -0,0 +1,308 @@
|
||||
|
||||
// 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 =
|
||||
`<div style="color: red; padding: 1rem;">Error rendering visualization: ${err.message}</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
Reference in New Issue
Block a user