mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2025-12-21 21:22:25 +00:00
init
This commit is contained in:
452
index.html
Normal file
452
index.html
Normal file
@@ -0,0 +1,452 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Vega-Lite Editor Integration</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vega@5"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vega-lite@5"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vega-embed@6"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.47.0/min/vs/loader.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/jsonc-parser@3.2.0/lib/umd/main.js"></script>
|
||||||
|
<style>
|
||||||
|
#vis {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: var(--snippet-width, 1fr) 5px var(--editor-width, 1fr) 5px var(--preview-width, 2fr);
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: white;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
cursor: col-resize;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resize-handle:hover,
|
||||||
|
.resize-handle.active {
|
||||||
|
background-color: #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: #4CAF50;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:disabled {
|
||||||
|
background: #cccccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snippet-list {
|
||||||
|
padding: 1rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snippet-item {
|
||||||
|
padding: 0.75rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snippet-item:hover {
|
||||||
|
background: #eeeeee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.snippet-item.active {
|
||||||
|
background: #e3f2fd;
|
||||||
|
border-left: 4px solid #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
#monaco-editor {
|
||||||
|
height: 100%;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-panel>*:not(.panel-header) {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h2>Snippets</h2>
|
||||||
|
<button class="button" id="new-snippet">New Snippet</button>
|
||||||
|
</div>
|
||||||
|
<div class="snippet-list" id="snippet-list">
|
||||||
|
<!-- Snippets will be populated here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="resize-handle"></div>
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h2>Editor</h2>
|
||||||
|
<button class="button" id="save-snippet" disabled>Save Changes</button>
|
||||||
|
</div>
|
||||||
|
<div id="monaco-editor"></div>
|
||||||
|
</div>
|
||||||
|
<div class="resize-handle"></div>
|
||||||
|
<div class="panel preview-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h2>Preview</h2>
|
||||||
|
</div>
|
||||||
|
<div id="vis"></div>
|
||||||
|
</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>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user