diff --git a/vna_system/core/processors/configs/bscan_config.json b/vna_system/core/processors/configs/bscan_config.json index e7d0758..ac0eca8 100644 --- a/vna_system/core/processors/configs/bscan_config.json +++ b/vna_system/core/processors/configs/bscan_config.json @@ -1,8 +1,8 @@ { "open_air": true, - "axis": "abs", + "axis": "phase", "data_limitation": "ph_only_1", - "cut": 1.413, + "cut": 0.988, "max": 2.4, "gain": 1.2, "start_freq": 100.0, diff --git a/vna_system/core/processors/configs/magnitude_config.json b/vna_system/core/processors/configs/magnitude_config.json index a9e672d..d0aae19 100644 --- a/vna_system/core/processors/configs/magnitude_config.json +++ b/vna_system/core/processors/configs/magnitude_config.json @@ -1,5 +1,5 @@ { "y_min": -80, - "y_max": 30, - "show_phase": true + "y_max": 40, + "show_phase": false } \ No newline at end of file diff --git a/vna_system/web_ui/static/js/main.js b/vna_system/web_ui/static/js/main.js index 33b941b..c7b016c 100644 --- a/vna_system/web_ui/static/js/main.js +++ b/vna_system/web_ui/static/js/main.js @@ -199,12 +199,6 @@ class VNADashboard { * Handle keyboard shortcuts */ handleKeyboardShortcuts(event) { - // Ctrl/Cmd + E: Export data - if ((event.ctrlKey || event.metaKey) && event.key === 'e') { - event.preventDefault(); - this.ui.triggerExportData(); - } - // Ctrl/Cmd + Shift + R: Reconnect WebSocket if ((event.ctrlKey || event.metaKey) && event.key === 'r' && event.shiftKey) { event.preventDefault(); diff --git a/vna_system/web_ui/static/js/modules/charts.js b/vna_system/web_ui/static/js/modules/charts.js index fe4c55d..c7e2f70 100644 --- a/vna_system/web_ui/static/js/modules/charts.js +++ b/vna_system/web_ui/static/js/modules/charts.js @@ -5,6 +5,15 @@ import { formatProcessorName, safeClone, downloadJSON } from './utils.js'; import { ChartSettingsManager } from './charts/chart-settings.js'; +import { + defaultPlotlyLayout, + defaultPlotlyConfig, + createPlotlyPlot, + updatePlotlyPlot, + togglePlotlyFullscreen, + downloadPlotlyImage, + cleanupPlotly +} from './plotly-utils.js'; export class ChartManager { constructor(config, notifications) { @@ -30,38 +39,6 @@ export class ChartManager { }; this.settingsManager = new ChartSettingsManager(); - - this.plotlyConfig = { - displayModeBar: true, - modeBarButtonsToRemove: ['select2d','lasso2d','hoverClosestCartesian','hoverCompareCartesian','toggleSpikelines'], - displaylogo: false, - responsive: false, - doubleClick: 'reset', - toImageButtonOptions: { format: 'png', filename: 'vna_chart', height: 600, width: 800, scale: 1 } - }; - - this.plotlyLayout = { - plot_bgcolor: 'transparent', - paper_bgcolor: 'transparent', - font: { family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif', size: 12, color: '#f1f5f9' }, - colorway: ['#3b82f6','#22c55e','#eab308','#ef4444','#8b5cf6','#06b6d4','#f59e0b','#10b981'], - margin: { l: 60, r: 50, t: 50, b: 60 }, - showlegend: true, - legend: { - orientation: 'v', - x: 1.02, - y: 1, - xanchor: 'left', - yanchor: 'top', - bgcolor: 'rgba(30, 41, 59, 0.9)', - bordercolor: '#475569', - borderwidth: 1, - font: { size: 10, color: '#f1f5f9' } - }, - xaxis: { gridcolor: '#334155', zerolinecolor: '#475569', color: '#cbd5e1', fixedrange: false }, - yaxis: { gridcolor: '#334155', zerolinecolor: '#475569', color: '#cbd5e1', fixedrange: false }, - autosize: true, width: null, height: null - }; } async init() { @@ -110,21 +87,13 @@ export class ChartManager { this.chartsGrid.appendChild(card); const plotContainer = card.querySelector('.chart-card__plot'); - const layout = { - ...this.plotlyLayout, + const layoutOverrides = { title: { text: formatProcessorName(processorId), font: { size: 16, color: '#f1f5f9' } }, width: plotContainer.clientWidth || 500, height: plotContainer.clientHeight || 420 }; - Plotly.newPlot(plotContainer, [], layout, this.plotlyConfig); - if (window.ResizeObserver) { - const ro = new ResizeObserver(() => { - if (plotContainer && plotContainer.clientWidth > 0) Plotly.Plots.resize(plotContainer); - }); - ro.observe(plotContainer); - plotContainer._resizeObserver = ro; - } + createPlotlyPlot(plotContainer, [], layoutOverrides); this.charts.set(processorId, { element: card, plotContainer, isVisible: true, settingsInitialized: false }); this.performanceStats.chartsCreated++; @@ -146,15 +115,12 @@ export class ChartManager { try { const start = performance.now(); this.queueUpdate(processorId, async () => { - const updateLayout = { - ...this.plotlyLayout, + const layoutOverrides = { ...(plotlyConfig.layout || {}), title: { text: formatProcessorName(processorId), font: { size: 16, color: '#f1f5f9' } } }; - delete updateLayout.width; - delete updateLayout.height; - await Plotly.react(chart.plotContainer, plotlyConfig.data || [], updateLayout, this.plotlyConfig); + await updatePlotlyPlot(chart.plotContainer, plotlyConfig.data || [], layoutOverrides); this.updateChartMetadata(processorId); @@ -297,8 +263,7 @@ export class ChartManager { removeChart(id) { const c = this.charts.get(id); if (c) { - if (c.plotContainer?._resizeObserver) { c.plotContainer._resizeObserver.disconnect(); c.plotContainer._resizeObserver = null; } - if (c.plotContainer) Plotly.purge(c.plotContainer); + cleanupPlotly(c.plotContainer); c.element.remove(); this.charts.delete(id); this.chartData.delete(id); @@ -323,12 +288,7 @@ export class ChartManager { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const baseFilename = `${id}_${timestamp}`; - await Plotly.downloadImage(c.plotContainer, { - format: 'png', - width: 1200, - height: 800, - filename: `${baseFilename}_plot` - }); + await downloadPlotlyImage(c.plotContainer, `${baseFilename}_plot`); const processorData = this.prepareDownloadData(id); if (processorData) { @@ -371,24 +331,10 @@ export class ChartManager { }; } - toggleFullscreen(id) { + async toggleFullscreen(id) { const c = this.charts.get(id); if (!c?.element) return; - if (!document.fullscreenElement) { - c.element.requestFullscreen()?.then(() => { - setTimeout(() => { - if (c.plotContainer) { - const r = c.plotContainer.getBoundingClientRect(); - Plotly.relayout(c.plotContainer, { width: r.width, height: r.height }); - Plotly.Plots.resize(c.plotContainer); - } - }, 200); - }).catch(console.error); - } else { - document.exitFullscreen()?.then(() => { - setTimeout(() => c.plotContainer && Plotly.Plots.resize(c.plotContainer), 100); - }); - } + await togglePlotlyFullscreen(c.element, c.plotContainer); } hideEmptyState() { diff --git a/vna_system/web_ui/static/js/modules/plotly-utils.js b/vna_system/web_ui/static/js/modules/plotly-utils.js new file mode 100644 index 0000000..b7b1834 --- /dev/null +++ b/vna_system/web_ui/static/js/modules/plotly-utils.js @@ -0,0 +1,176 @@ +/** + * Plotly Utilities + * Shared functions for Plotly chart rendering and management + */ + +/** + * Default Plotly layout for dark theme + */ +export const defaultPlotlyLayout = { + plot_bgcolor: 'transparent', + paper_bgcolor: 'transparent', + font: { family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif', size: 12, color: '#f1f5f9' }, + colorway: ['#3b82f6','#22c55e','#eab308','#ef4444','#8b5cf6','#06b6d4','#f59e0b','#10b981'], + margin: { l: 60, r: 50, t: 50, b: 60 }, + showlegend: true, + legend: { + orientation: 'v', + x: 1.02, + y: 1, + xanchor: 'left', + yanchor: 'top', + bgcolor: 'rgba(30, 41, 59, 0.9)', + bordercolor: '#475569', + borderwidth: 1, + font: { size: 10, color: '#f1f5f9' } + }, + xaxis: { gridcolor: '#334155', zerolinecolor: '#475569', color: '#cbd5e1', fixedrange: false }, + yaxis: { gridcolor: '#334155', zerolinecolor: '#475569', color: '#cbd5e1', fixedrange: false }, + autosize: true, + width: null, + height: null +}; + +/** + * Default Plotly config + */ +export const defaultPlotlyConfig = { + displayModeBar: true, + modeBarButtonsToRemove: ['select2d','lasso2d','hoverClosestCartesian','hoverCompareCartesian','toggleSpikelines'], + displaylogo: false, + responsive: false, + doubleClick: 'reset', + toImageButtonOptions: { format: 'png', filename: 'vna_chart', height: 600, width: 800, scale: 1 } +}; + +/** + * Setup resize observer for Plotly plot + * @param {HTMLElement} container - Plot container element + */ +export function setupPlotlyResize(container) { + if (!window.ResizeObserver || !container) return; + + const ro = new ResizeObserver(() => { + if (container && container.clientWidth > 0 && typeof Plotly !== 'undefined') { + Plotly.Plots.resize(container); + } + }); + ro.observe(container); + container._resizeObserver = ro; +} + +/** + * Cleanup Plotly plot and observers + * @param {HTMLElement} container - Plot container element + */ +export function cleanupPlotly(container) { + if (!container) return; + + if (container._resizeObserver) { + container._resizeObserver.disconnect(); + container._resizeObserver = null; + } + + if (container._fullData && typeof Plotly !== 'undefined') { + Plotly.purge(container); + } +} + +/** + * Create Plotly plot with default styling + * @param {HTMLElement} container - Container element + * @param {Array} data - Plotly data traces + * @param {Object} layoutOverrides - Layout overrides + * @param {Object} configOverrides - Config overrides + */ +export function createPlotlyPlot(container, data = [], layoutOverrides = {}, configOverrides = {}) { + if (!container || typeof Plotly === 'undefined') return; + + const layout = { + ...defaultPlotlyLayout, + ...layoutOverrides + }; + + const config = { + ...defaultPlotlyConfig, + ...configOverrides + }; + + Plotly.newPlot(container, data, layout, config); + setupPlotlyResize(container); +} + +/** + * Update existing Plotly plot + * @param {HTMLElement} container - Container element + * @param {Array} data - Plotly data traces + * @param {Object} layoutOverrides - Layout overrides + */ +export async function updatePlotlyPlot(container, data = [], layoutOverrides = {}) { + if (!container || typeof Plotly === 'undefined') return; + + const layout = { + ...defaultPlotlyLayout, + ...layoutOverrides + }; + + // Remove width/height to allow autosize + delete layout.width; + delete layout.height; + + await Plotly.react(container, data, layout, defaultPlotlyConfig); +} + +/** + * Toggle fullscreen for element with Plotly plot + * @param {HTMLElement} element - Element to fullscreen + * @param {HTMLElement} plotContainer - Plot container inside element + */ +export async function togglePlotlyFullscreen(element, plotContainer) { + if (!element || typeof Plotly === 'undefined') return; + + if (!document.fullscreenElement) { + try { + await element.requestFullscreen?.(); + setTimeout(() => { + if (plotContainer) { + const rect = plotContainer.getBoundingClientRect(); + Plotly.relayout(plotContainer, { width: rect.width, height: rect.height }); + Plotly.Plots.resize(plotContainer); + } + }, 200); + } catch (error) { + console.error('Fullscreen request failed:', error); + } + } else { + try { + await document.exitFullscreen?.(); + setTimeout(() => { + if (plotContainer) { + Plotly.Plots.resize(plotContainer); + } + }, 100); + } catch (error) { + console.error('Exit fullscreen failed:', error); + } + } +} + +/** + * Download Plotly plot as image + * @param {HTMLElement} container - Plot container + * @param {string} filename - Output filename (without extension) + * @param {Object} options - Download options + */ +export async function downloadPlotlyImage(container, filename = 'plot', options = {}) { + if (!container || typeof Plotly === 'undefined') return; + + const defaultOptions = { + format: 'png', + width: 1200, + height: 800, + filename + }; + + await Plotly.downloadImage(container, { ...defaultOptions, ...options }); +} \ No newline at end of file diff --git a/vna_system/web_ui/static/js/modules/settings.js b/vna_system/web_ui/static/js/modules/settings.js index 3e4c5ce..79f418d 100644 --- a/vna_system/web_ui/static/js/modules/settings.js +++ b/vna_system/web_ui/static/js/modules/settings.js @@ -7,6 +7,12 @@ import { PresetManager } from './settings/preset-manager.js'; import { CalibrationManager } from './settings/calibration-manager.js'; import { ReferenceManager } from './settings/reference-manager.js'; import { Debouncer, ButtonState, downloadJSON } from './utils.js'; +import { + createPlotlyPlot, + togglePlotlyFullscreen, + downloadPlotlyImage, + cleanupPlotly +} from './plotly-utils.js'; export class SettingsManager { constructor(notifications, websocket, acquisition) { @@ -318,50 +324,12 @@ export class SettingsManager { return; } - const layout = { + const layoutOverrides = { ...plotConfig.layout, - title: { text: title, font: { size: 16, color: '#f1f5f9' } }, - plot_bgcolor: 'transparent', - paper_bgcolor: 'transparent', - font: { family: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif', size: 12, color: '#f1f5f9' }, - autosize: true, - width: null, - height: null, - margin: { l: 60, r: 50, t: 50, b: 60 }, - showlegend: true, - legend: { - orientation: 'v', - x: 1.02, - y: 1, - xanchor: 'left', - yanchor: 'top', - bgcolor: 'rgba(30, 41, 59, 0.9)', - bordercolor: '#475569', - borderwidth: 1, - font: { size: 10, color: '#f1f5f9' } - }, - xaxis: { - ...plotConfig.layout.xaxis, - gridcolor: '#334155', - zerolinecolor: '#475569', - color: '#cbd5e1', - fixedrange: false - }, - yaxis: { - ...plotConfig.layout.yaxis, - gridcolor: '#334155', - zerolinecolor: '#475569', - color: '#cbd5e1', - fixedrange: false - } + title: { text: title, font: { size: 16, color: '#f1f5f9' } } }; - const config = { - displayModeBar: true, - modeBarButtonsToRemove: ['select2d', 'lasso2d', 'hoverClosestCartesian', 'hoverCompareCartesian', 'toggleSpikelines'], - displaylogo: false, - responsive: false, - doubleClick: 'reset', + const configOverrides = { toImageButtonOptions: { format: 'png', filename: `calibration-plot-${Date.now()}`, @@ -371,41 +339,12 @@ export class SettingsManager { } }; - Plotly.newPlot(container, plotConfig.data, layout, config); - - if (window.ResizeObserver) { - const ro = new ResizeObserver(() => { - if (container && container.clientWidth > 0) { - Plotly.Plots.resize(container); - } - }); - ro.observe(container); - container._resizeObserver = ro; - } + createPlotlyPlot(container, plotConfig.data, layoutOverrides, configOverrides); } - toggleFullscreen(card) { - if (!document.fullscreenElement) { - card.requestFullscreen?.().then(() => { - setTimeout(() => { - const plot = card.querySelector('.chart-card__plot'); - if (plot && typeof Plotly !== 'undefined') { - const rect = plot.getBoundingClientRect(); - Plotly.relayout(plot, { width: rect.width, height: rect.height }); - Plotly.Plots.resize(plot); - } - }, 200); - }).catch(console.error); - } else { - document.exitFullscreen?.().then(() => { - setTimeout(() => { - const plot = card.querySelector('.chart-card__plot'); - if (plot && typeof Plotly !== 'undefined') { - Plotly.Plots.resize(plot); - } - }, 100); - }); - } + async toggleFullscreen(card) { + const plot = card.querySelector('.chart-card__plot'); + await togglePlotlyFullscreen(card, plot); } setupModalCloseHandlers(modal) { @@ -436,13 +375,8 @@ export class SettingsManager { modal.classList.remove('modal--active'); document.body.style.overflow = ''; - if (typeof Plotly !== 'undefined') { - const containers = modal.querySelectorAll('[id^="calibration-plot-"]'); - containers.forEach(c => { - if (c._resizeObserver) { c._resizeObserver.disconnect(); c._resizeObserver = null; } - if (c._fullData) Plotly.purge(c); - }); - } + const containers = modal.querySelectorAll('[id^="calibration-plot-"]'); + containers.forEach(c => cleanupPlotly(c)); this.currentPlotsData = null; } @@ -453,10 +387,8 @@ export class SettingsManager { const calibrationName = this.currentPlotsData?.calibration_name || 'unknown'; const base = `${calibrationName}_${standardName}_${ts}`; - if (plotContainer && typeof Plotly !== 'undefined') { - await Plotly.downloadImage(plotContainer, { - format: 'png', width: 1200, height: 800, filename: `${base}_plot` - }); + if (plotContainer) { + await downloadPlotlyImage(plotContainer, `${base}_plot`); } const data = this.prepareCalibrationDownloadData(standardName);