mirror of
https://github.com/olehomelchenko/astrolabe-nvc.git
synced 2025-12-21 21:22:23 +00:00
refactor: migrate chart builder UI to Alpine.js for reactive state management
This commit is contained in:
@@ -1,10 +1,299 @@
|
||||
// Chart Builder - Visual chart construction from datasets
|
||||
|
||||
// Global state for chart builder
|
||||
window.chartBuilderState = null;
|
||||
/**
|
||||
* Alpine.js component for Chart Builder
|
||||
* Manages reactive state for chart configuration and preview
|
||||
*/
|
||||
function chartBuilder() {
|
||||
return {
|
||||
// Dataset info
|
||||
datasetId: null,
|
||||
datasetName: null,
|
||||
dataset: null,
|
||||
|
||||
// Timeout for debounced preview updates
|
||||
let previewUpdateTimeout = null;
|
||||
// Chart configuration
|
||||
markType: 'bar',
|
||||
encodings: {
|
||||
x: { field: '', type: 'nominal' },
|
||||
y: { field: '', type: 'quantitative' },
|
||||
color: { field: '', type: 'nominal' },
|
||||
size: { field: '', type: 'quantitative' }
|
||||
},
|
||||
width: null,
|
||||
height: null,
|
||||
|
||||
// UI state
|
||||
previewTimeout: null,
|
||||
|
||||
// Computed: Generate Vega-Lite spec from current state
|
||||
get spec() {
|
||||
const spec = {
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
||||
"data": { "name": this.datasetName },
|
||||
"mark": { "type": this.markType, "tooltip": true },
|
||||
"encoding": {}
|
||||
};
|
||||
|
||||
// Add encodings
|
||||
['x', 'y', 'color', 'size'].forEach(channel => {
|
||||
const enc = this.encodings[channel];
|
||||
if (enc.field) {
|
||||
spec.encoding[channel] = {
|
||||
field: enc.field,
|
||||
type: enc.type
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Add dimensions if specified
|
||||
if (this.width) spec.width = parseInt(this.width);
|
||||
if (this.height) spec.height = parseInt(this.height);
|
||||
|
||||
// Remove empty encoding object
|
||||
if (Object.keys(spec.encoding).length === 0) {
|
||||
delete spec.encoding;
|
||||
}
|
||||
|
||||
return spec;
|
||||
},
|
||||
|
||||
// Computed: Check if configuration is valid
|
||||
get isValid() {
|
||||
return Object.values(this.encodings).some(enc => enc.field !== '');
|
||||
},
|
||||
|
||||
// Initialize component with dataset
|
||||
async init(datasetId) {
|
||||
try {
|
||||
// Validate datasetId is provided
|
||||
if (!datasetId || isNaN(datasetId)) {
|
||||
console.warn('Chart builder init called without valid datasetId');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fetch dataset from IndexedDB
|
||||
this.dataset = await DatasetStorage.getDataset(datasetId);
|
||||
if (!this.dataset) {
|
||||
Toast.error('Dataset not found');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.datasetId = datasetId;
|
||||
this.datasetName = this.dataset.name;
|
||||
|
||||
// Populate field dropdowns
|
||||
populateFieldDropdowns(this.dataset);
|
||||
|
||||
// Auto-select smart defaults
|
||||
this.autoSelectDefaults();
|
||||
|
||||
// Trigger initial preview
|
||||
this.$nextTick(() => {
|
||||
this.updatePreview();
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error initializing chart builder:', error);
|
||||
Toast.error('Error opening chart builder');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Auto-select smart defaults based on column types
|
||||
autoSelectDefaults() {
|
||||
const columns = this.dataset.columns || [];
|
||||
const columnTypes = this.dataset.columnTypes || [];
|
||||
|
||||
if (columns.length === 0) return;
|
||||
|
||||
// Select first column for X axis
|
||||
if (columns.length >= 1) {
|
||||
const firstCol = columns[0];
|
||||
const firstColType = columnTypes.find(ct => ct.name === firstCol);
|
||||
this.encodings.x.field = firstCol;
|
||||
this.encodings.x.type = firstColType ? mapColumnTypeToVegaType(firstColType.type) : 'nominal';
|
||||
}
|
||||
|
||||
// Select second column for Y axis (if exists)
|
||||
if (columns.length >= 2) {
|
||||
const secondCol = columns[1];
|
||||
const secondColType = columnTypes.find(ct => ct.name === secondCol);
|
||||
this.encodings.y.field = secondCol;
|
||||
this.encodings.y.type = secondColType ? mapColumnTypeToVegaType(secondColType.type) : 'quantitative';
|
||||
}
|
||||
},
|
||||
|
||||
// Set mark type and update preview
|
||||
setMarkType(type) {
|
||||
this.markType = type;
|
||||
this.updatePreview();
|
||||
},
|
||||
|
||||
// Set encoding field and auto-detect type if needed
|
||||
async setEncodingField(channel, field) {
|
||||
this.encodings[channel].field = field;
|
||||
|
||||
if (field && this.dataset) {
|
||||
// Auto-detect type from column type
|
||||
const columnTypes = this.dataset.columnTypes || [];
|
||||
const colType = columnTypes.find(ct => ct.name === field);
|
||||
if (colType) {
|
||||
this.encodings[channel].type = mapColumnTypeToVegaType(colType.type);
|
||||
}
|
||||
}
|
||||
|
||||
this.updatePreview();
|
||||
},
|
||||
|
||||
// Set encoding type and update preview
|
||||
setEncodingType(channel, type) {
|
||||
if (this.encodings[channel].field) {
|
||||
this.encodings[channel].type = type;
|
||||
this.updatePreview();
|
||||
}
|
||||
},
|
||||
|
||||
// Update preview with debouncing
|
||||
updatePreview() {
|
||||
clearTimeout(this.previewTimeout);
|
||||
|
||||
// Get debounce time from settings (default 1500ms)
|
||||
const debounceTime = getSetting('performance.renderDebounce') || 1500;
|
||||
|
||||
this.previewTimeout = setTimeout(async () => {
|
||||
await this.renderPreview();
|
||||
}, debounceTime);
|
||||
},
|
||||
|
||||
// Render preview in chart builder
|
||||
async renderPreview() {
|
||||
const previewContainer = document.getElementById('chart-builder-preview');
|
||||
const errorDiv = document.getElementById('chart-builder-error');
|
||||
|
||||
if (!previewContainer) return;
|
||||
|
||||
try {
|
||||
// Validate configuration
|
||||
if (!this.isValid) {
|
||||
previewContainer.innerHTML = '<div class="chart-preview-placeholder">Configure at least one encoding to see preview</div>';
|
||||
if (errorDiv) errorDiv.textContent = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Resolve dataset references
|
||||
const resolvedSpec = await resolveDatasetReferences(JSON.parse(JSON.stringify(this.spec)));
|
||||
|
||||
// Clear container
|
||||
previewContainer.innerHTML = '';
|
||||
|
||||
// Render with Vega-Embed
|
||||
await window.vegaEmbed('#chart-builder-preview', resolvedSpec, {
|
||||
actions: false,
|
||||
renderer: 'svg'
|
||||
});
|
||||
|
||||
// Clear error
|
||||
if (errorDiv) errorDiv.textContent = '';
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error rendering chart preview:', error);
|
||||
|
||||
// Show error message
|
||||
if (errorDiv) {
|
||||
errorDiv.textContent = error.message || 'Error rendering chart';
|
||||
}
|
||||
|
||||
// Show error in preview
|
||||
previewContainer.innerHTML = `<div class="chart-preview-placeholder" style="color: #d32f2f;">Error: ${error.message || 'Failed to render chart'}</div>`;
|
||||
}
|
||||
},
|
||||
|
||||
// Create snippet from current chart configuration
|
||||
async createSnippet() {
|
||||
if (!this.isValid) return;
|
||||
|
||||
try {
|
||||
// Create snippet with auto-generated name
|
||||
const snippetName = generateSnippetName();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const snippet = {
|
||||
id: generateSnippetId(),
|
||||
name: snippetName,
|
||||
created: now,
|
||||
modified: now,
|
||||
spec: this.spec,
|
||||
draftSpec: null,
|
||||
comment: `Chart built from dataset: ${this.datasetName}`,
|
||||
tags: [],
|
||||
datasetRefs: [this.datasetName],
|
||||
meta: {}
|
||||
};
|
||||
|
||||
// Save snippet
|
||||
SnippetStorage.saveSnippet(snippet);
|
||||
|
||||
// Close modals
|
||||
this.close();
|
||||
const datasetModal = document.getElementById('dataset-modal');
|
||||
if (datasetModal) datasetModal.style.display = 'none';
|
||||
|
||||
// Select and open the new snippet
|
||||
selectSnippet(snippet.id);
|
||||
|
||||
// Show success message
|
||||
Toast.success(`Created snippet: ${snippetName}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating snippet from builder:', error);
|
||||
Toast.error('Error creating snippet');
|
||||
}
|
||||
},
|
||||
|
||||
// Close chart builder and cleanup
|
||||
close() {
|
||||
const modal = document.getElementById('chart-builder-modal');
|
||||
modal.style.display = 'none';
|
||||
|
||||
// Update URL - go back to dataset view
|
||||
if (this.datasetId) {
|
||||
URLState.update({
|
||||
view: 'datasets',
|
||||
datasetId: this.datasetId,
|
||||
action: null
|
||||
});
|
||||
}
|
||||
|
||||
// Clear timeout
|
||||
clearTimeout(this.previewTimeout);
|
||||
|
||||
// Reset state
|
||||
this.datasetId = null;
|
||||
this.datasetName = null;
|
||||
this.dataset = null;
|
||||
this.markType = 'bar';
|
||||
this.encodings = {
|
||||
x: { field: '', type: 'nominal' },
|
||||
y: { field: '', type: 'quantitative' },
|
||||
color: { field: '', type: 'nominal' },
|
||||
size: { field: '', type: 'quantitative' }
|
||||
};
|
||||
this.width = null;
|
||||
this.height = null;
|
||||
|
||||
// Clear preview
|
||||
const previewContainer = document.getElementById('chart-builder-preview');
|
||||
if (previewContainer) {
|
||||
previewContainer.innerHTML = '<div class="chart-preview-placeholder">Configure chart to see preview</div>';
|
||||
}
|
||||
|
||||
// Clear error
|
||||
const errorDiv = document.getElementById('chart-builder-error');
|
||||
if (errorDiv) errorDiv.textContent = '';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Map column types to Vega-Lite types
|
||||
function mapColumnTypeToVegaType(columnType) {
|
||||
@@ -23,38 +312,26 @@ function setActiveToggle(buttons, activeButton) {
|
||||
activeButton.classList.add('active');
|
||||
}
|
||||
|
||||
|
||||
// Open chart builder modal with dataset
|
||||
async function openChartBuilder(datasetId) {
|
||||
try {
|
||||
// Fetch dataset from IndexedDB
|
||||
const dataset = await DatasetStorage.getDataset(datasetId);
|
||||
if (!dataset) {
|
||||
showToast('Dataset not found', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize state with defaults
|
||||
window.chartBuilderState = {
|
||||
datasetId: datasetId,
|
||||
datasetName: dataset.name,
|
||||
spec: {
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
||||
"data": {"name": dataset.name},
|
||||
"mark": {"type": "bar", "tooltip": true},
|
||||
"encoding": {}
|
||||
}
|
||||
};
|
||||
|
||||
// Populate field dropdowns with dataset columns BEFORE showing modal
|
||||
populateFieldDropdowns(dataset);
|
||||
|
||||
// Auto-select smart defaults
|
||||
autoSelectDefaults(dataset);
|
||||
|
||||
// Show modal
|
||||
// Show modal first
|
||||
const modal = document.getElementById('chart-builder-modal');
|
||||
modal.style.display = 'flex';
|
||||
|
||||
// Get Alpine component instance and initialize it
|
||||
const chartBuilderView = document.getElementById('chart-builder-view');
|
||||
if (chartBuilderView && chartBuilderView._x_dataStack) {
|
||||
const component = chartBuilderView._x_dataStack[0];
|
||||
const success = await component.init(datasetId);
|
||||
|
||||
if (!success) {
|
||||
modal.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update URL to reflect chart builder state
|
||||
URLState.update({
|
||||
view: 'datasets',
|
||||
@@ -62,18 +339,15 @@ async function openChartBuilder(datasetId) {
|
||||
action: 'build'
|
||||
});
|
||||
|
||||
// Initial preview update (with a small delay to ensure DOM is ready)
|
||||
setTimeout(() => {
|
||||
updateChartBuilderPreview();
|
||||
}, 50);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error opening chart builder:', error);
|
||||
showToast('Error opening chart builder', 'error');
|
||||
Toast.error('Error opening chart builder');
|
||||
}
|
||||
}
|
||||
|
||||
// Populate field dropdowns with dataset columns
|
||||
|
||||
|
||||
// Populate field dropdowns with dataset columns (utility function)
|
||||
function populateFieldDropdowns(dataset) {
|
||||
const encodings = ['x', 'y', 'color', 'size'];
|
||||
const columns = dataset.columns || [];
|
||||
@@ -95,363 +369,11 @@ function populateFieldDropdowns(dataset) {
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-select smart defaults based on column types
|
||||
function autoSelectDefaults(dataset) {
|
||||
const columns = dataset.columns || [];
|
||||
const columnTypes = dataset.columnTypes || [];
|
||||
|
||||
if (columns.length === 0) return;
|
||||
|
||||
// Select first column for X axis
|
||||
if (columns.length >= 1) {
|
||||
const firstCol = columns[0];
|
||||
const firstColType = columnTypes.find(ct => ct.name === firstCol);
|
||||
setEncoding('x', firstCol, firstColType ? mapColumnTypeToVegaType(firstColType.type) : 'nominal');
|
||||
}
|
||||
|
||||
// Select second column for Y axis (if exists)
|
||||
if (columns.length >= 2) {
|
||||
const secondCol = columns[1];
|
||||
const secondColType = columnTypes.find(ct => ct.name === secondCol);
|
||||
setEncoding('y', secondCol, secondColType ? mapColumnTypeToVegaType(secondColType.type) : 'quantitative');
|
||||
}
|
||||
}
|
||||
|
||||
// Set encoding field and type in UI and state
|
||||
function setEncoding(channel, field, type) {
|
||||
// Update dropdown
|
||||
const select = document.getElementById(`encoding-${channel}-field`);
|
||||
if (select) {
|
||||
select.value = field;
|
||||
}
|
||||
|
||||
// Update type toggle buttons
|
||||
const typeButtons = document.querySelectorAll(`[data-encoding="${channel}"][data-type]`);
|
||||
const activeButton = Array.from(typeButtons).find(btn => btn.dataset.type === type);
|
||||
if (activeButton) {
|
||||
setActiveToggle(typeButtons, activeButton);
|
||||
}
|
||||
|
||||
// Update state
|
||||
if (!window.chartBuilderState) return;
|
||||
|
||||
if (field) {
|
||||
window.chartBuilderState.spec.encoding[channel] = {
|
||||
field: field,
|
||||
type: type
|
||||
};
|
||||
} else {
|
||||
delete window.chartBuilderState.spec.encoding[channel];
|
||||
}
|
||||
}
|
||||
|
||||
// Generate Vega-Lite spec from current state
|
||||
function generateVegaLiteSpec() {
|
||||
if (!window.chartBuilderState) return null;
|
||||
|
||||
const state = window.chartBuilderState;
|
||||
const spec = JSON.parse(JSON.stringify(state.spec)); // Deep clone
|
||||
|
||||
// Add width/height if specified
|
||||
const width = document.getElementById('chart-width');
|
||||
const height = document.getElementById('chart-height');
|
||||
|
||||
if (width && width.value) {
|
||||
spec.width = parseInt(width.value);
|
||||
}
|
||||
|
||||
if (height && height.value) {
|
||||
spec.height = parseInt(height.value);
|
||||
}
|
||||
|
||||
// Remove empty encodings
|
||||
if (Object.keys(spec.encoding).length === 0) {
|
||||
delete spec.encoding;
|
||||
}
|
||||
|
||||
return spec;
|
||||
}
|
||||
|
||||
// Validate chart configuration
|
||||
function validateChartConfig() {
|
||||
if (!window.chartBuilderState) return false;
|
||||
|
||||
const spec = window.chartBuilderState.spec;
|
||||
const encoding = spec.encoding || {};
|
||||
|
||||
// At least one encoding must be set
|
||||
const hasEncoding = Object.keys(encoding).length > 0;
|
||||
|
||||
return hasEncoding;
|
||||
}
|
||||
|
||||
// Update preview with debouncing
|
||||
function updateChartBuilderPreview() {
|
||||
clearTimeout(previewUpdateTimeout);
|
||||
|
||||
// Get debounce time from settings (default 1500ms)
|
||||
const debounceTime = getSetting('performance.renderDebounce') || 1500;
|
||||
|
||||
previewUpdateTimeout = setTimeout(async () => {
|
||||
await renderChartBuilderPreview();
|
||||
}, debounceTime);
|
||||
}
|
||||
|
||||
// Render preview in chart builder
|
||||
async function renderChartBuilderPreview() {
|
||||
const previewContainer = document.getElementById('chart-builder-preview');
|
||||
const errorDiv = document.getElementById('chart-builder-error');
|
||||
const createBtn = document.getElementById('chart-builder-create-btn');
|
||||
|
||||
if (!previewContainer) return;
|
||||
|
||||
try {
|
||||
// Validate configuration
|
||||
const isValid = validateChartConfig();
|
||||
|
||||
if (!isValid) {
|
||||
// Show placeholder
|
||||
previewContainer.innerHTML = '<div class="chart-preview-placeholder">Configure at least one encoding to see preview</div>';
|
||||
if (errorDiv) errorDiv.textContent = '';
|
||||
if (createBtn) createBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate spec
|
||||
const spec = generateVegaLiteSpec();
|
||||
if (!spec) return;
|
||||
|
||||
// Resolve dataset references (reuse existing function from editor.js)
|
||||
const resolvedSpec = await resolveDatasetReferences(JSON.parse(JSON.stringify(spec)));
|
||||
|
||||
// Clear container
|
||||
previewContainer.innerHTML = '';
|
||||
|
||||
// Render with Vega-Embed
|
||||
await window.vegaEmbed('#chart-builder-preview', resolvedSpec, {
|
||||
actions: false,
|
||||
renderer: 'svg'
|
||||
});
|
||||
|
||||
// Clear error and enable create button
|
||||
if (errorDiv) errorDiv.textContent = '';
|
||||
if (createBtn) createBtn.disabled = false;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error rendering chart preview:', error);
|
||||
|
||||
// Show error message
|
||||
if (errorDiv) {
|
||||
errorDiv.textContent = error.message || 'Error rendering chart';
|
||||
}
|
||||
|
||||
// Show error in preview
|
||||
previewContainer.innerHTML = `<div class="chart-preview-placeholder" style="color: #d32f2f;">Error: ${error.message || 'Failed to render chart'}</div>`;
|
||||
|
||||
// Disable create button
|
||||
if (createBtn) createBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Create snippet from chart builder
|
||||
async function createSnippetFromBuilder() {
|
||||
if (!window.chartBuilderState) return;
|
||||
|
||||
try {
|
||||
// Generate final spec
|
||||
const spec = generateVegaLiteSpec();
|
||||
if (!spec) return;
|
||||
|
||||
// Create snippet with auto-generated name
|
||||
const snippetName = generateSnippetName();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
const snippet = {
|
||||
id: generateSnippetId(),
|
||||
name: snippetName,
|
||||
created: now,
|
||||
modified: now,
|
||||
spec: spec,
|
||||
draftSpec: null,
|
||||
comment: `Chart built from dataset: ${window.chartBuilderState.datasetName}`,
|
||||
tags: [],
|
||||
datasetRefs: [window.chartBuilderState.datasetName],
|
||||
meta: {}
|
||||
};
|
||||
|
||||
// Save snippet
|
||||
SnippetStorage.saveSnippet(snippet);
|
||||
|
||||
// Close chart builder
|
||||
closeChartBuilder();
|
||||
|
||||
// Close dataset modal if open
|
||||
const datasetModal = document.getElementById('dataset-modal');
|
||||
if (datasetModal) {
|
||||
datasetModal.style.display = 'none';
|
||||
}
|
||||
|
||||
// Select and open the new snippet
|
||||
selectSnippet(snippet.id);
|
||||
|
||||
// Show success message
|
||||
showToast(`Created snippet: ${snippetName}`, 'success');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error creating snippet from builder:', error);
|
||||
showToast('Error creating snippet', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Close chart builder modal
|
||||
// closeChartBuilder - now calls Alpine component's close() method
|
||||
function closeChartBuilder() {
|
||||
const modal = document.getElementById('chart-builder-modal');
|
||||
modal.style.display = 'none';
|
||||
|
||||
// Update URL - go back to dataset view
|
||||
if (window.chartBuilderState && window.chartBuilderState.datasetId) {
|
||||
URLState.update({
|
||||
view: 'datasets',
|
||||
datasetId: window.chartBuilderState.datasetId,
|
||||
action: null
|
||||
});
|
||||
}
|
||||
|
||||
// Clear timeout
|
||||
clearTimeout(previewUpdateTimeout);
|
||||
|
||||
// Clear state
|
||||
window.chartBuilderState = null;
|
||||
|
||||
// Clear preview
|
||||
const previewContainer = document.getElementById('chart-builder-preview');
|
||||
if (previewContainer) {
|
||||
previewContainer.innerHTML = '<div class="chart-preview-placeholder">Configure chart to see preview</div>';
|
||||
}
|
||||
|
||||
// Clear error
|
||||
const errorDiv = document.getElementById('chart-builder-error');
|
||||
if (errorDiv) {
|
||||
errorDiv.textContent = '';
|
||||
}
|
||||
|
||||
// Reset create button
|
||||
const createBtn = document.getElementById('chart-builder-create-btn');
|
||||
if (createBtn) {
|
||||
createBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize chart builder event listeners
|
||||
function initializeChartBuilder() {
|
||||
// Close button
|
||||
const closeBtn = document.getElementById('chart-builder-modal-close');
|
||||
if (closeBtn) {
|
||||
closeBtn.addEventListener('click', closeChartBuilder);
|
||||
}
|
||||
|
||||
// Cancel button
|
||||
const cancelBtn = document.getElementById('chart-builder-cancel-btn');
|
||||
if (cancelBtn) {
|
||||
cancelBtn.addEventListener('click', closeChartBuilder);
|
||||
}
|
||||
|
||||
// Back button
|
||||
const backBtn = document.getElementById('chart-builder-back-btn');
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', () => {
|
||||
closeChartBuilder();
|
||||
// Dataset modal should still be open
|
||||
});
|
||||
}
|
||||
|
||||
// Create snippet button
|
||||
const createBtn = document.getElementById('chart-builder-create-btn');
|
||||
if (createBtn) {
|
||||
createBtn.addEventListener('click', createSnippetFromBuilder);
|
||||
}
|
||||
|
||||
// Mark type toggle buttons
|
||||
const markButtons = document.querySelectorAll('.mark-toggle-group .btn-toggle');
|
||||
markButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
setActiveToggle(markButtons, btn);
|
||||
|
||||
if (window.chartBuilderState) {
|
||||
window.chartBuilderState.spec.mark.type = btn.dataset.mark;
|
||||
updateChartBuilderPreview();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Encoding field dropdowns
|
||||
const encodings = ['x', 'y', 'color', 'size'];
|
||||
encodings.forEach(encoding => {
|
||||
const select = document.getElementById(`encoding-${encoding}-field`);
|
||||
if (select) {
|
||||
select.addEventListener('change', async (e) => {
|
||||
const field = e.target.value;
|
||||
|
||||
if (!window.chartBuilderState) return;
|
||||
|
||||
if (field) {
|
||||
// Try to get active type button, or auto-detect from dataset
|
||||
const activeTypeBtn = document.querySelector(`[data-encoding="${encoding}"][data-type].active`);
|
||||
let type = activeTypeBtn ? activeTypeBtn.dataset.type : 'nominal';
|
||||
|
||||
// If no active type button, auto-detect from column type
|
||||
if (!activeTypeBtn && window.chartBuilderState.datasetId) {
|
||||
const dataset = await DatasetStorage.getDataset(window.chartBuilderState.datasetId);
|
||||
const columnTypes = dataset.columnTypes || [];
|
||||
const colType = columnTypes.find(ct => ct.name === field);
|
||||
if (colType) {
|
||||
type = mapColumnTypeToVegaType(colType.type);
|
||||
}
|
||||
}
|
||||
|
||||
setEncoding(encoding, field, type);
|
||||
} else {
|
||||
// Remove encoding when "None" is selected
|
||||
setEncoding(encoding, '', '');
|
||||
}
|
||||
|
||||
updateChartBuilderPreview();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Encoding type toggle buttons
|
||||
const typeButtons = document.querySelectorAll('.type-toggle-group .btn-toggle');
|
||||
typeButtons.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const encoding = btn.dataset.encoding;
|
||||
const type = btn.dataset.type;
|
||||
|
||||
// Update active state for this encoding's buttons
|
||||
const encodingButtons = document.querySelectorAll(`[data-encoding="${encoding}"][data-type]`);
|
||||
setActiveToggle(encodingButtons, btn);
|
||||
|
||||
// Update state
|
||||
if (window.chartBuilderState && window.chartBuilderState.spec.encoding[encoding]) {
|
||||
window.chartBuilderState.spec.encoding[encoding].type = type;
|
||||
updateChartBuilderPreview();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Dimension inputs
|
||||
const widthInput = document.getElementById('chart-width');
|
||||
const heightInput = document.getElementById('chart-height');
|
||||
|
||||
if (widthInput) {
|
||||
widthInput.addEventListener('input', () => {
|
||||
updateChartBuilderPreview();
|
||||
});
|
||||
}
|
||||
|
||||
if (heightInput) {
|
||||
heightInput.addEventListener('input', () => {
|
||||
updateChartBuilderPreview();
|
||||
});
|
||||
const chartBuilderView = document.getElementById('chart-builder-view');
|
||||
if (chartBuilderView && chartBuilderView._x_dataStack) {
|
||||
const component = chartBuilderView._x_dataStack[0];
|
||||
component.close();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user