refactoring 4

This commit is contained in:
Ayzen
2025-09-30 14:32:15 +03:00
parent 32d5aa48d9
commit bfb82fce2a
6 changed files with 214 additions and 166 deletions

View File

@ -1,8 +1,8 @@
{ {
"open_air": true, "open_air": true,
"axis": "abs", "axis": "phase",
"data_limitation": "ph_only_1", "data_limitation": "ph_only_1",
"cut": 1.413, "cut": 0.988,
"max": 2.4, "max": 2.4,
"gain": 1.2, "gain": 1.2,
"start_freq": 100.0, "start_freq": 100.0,

View File

@ -1,5 +1,5 @@
{ {
"y_min": -80, "y_min": -80,
"y_max": 30, "y_max": 40,
"show_phase": true "show_phase": false
} }

View File

@ -199,12 +199,6 @@ class VNADashboard {
* Handle keyboard shortcuts * Handle keyboard shortcuts
*/ */
handleKeyboardShortcuts(event) { 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 // Ctrl/Cmd + Shift + R: Reconnect WebSocket
if ((event.ctrlKey || event.metaKey) && event.key === 'r' && event.shiftKey) { if ((event.ctrlKey || event.metaKey) && event.key === 'r' && event.shiftKey) {
event.preventDefault(); event.preventDefault();

View File

@ -5,6 +5,15 @@
import { formatProcessorName, safeClone, downloadJSON } from './utils.js'; import { formatProcessorName, safeClone, downloadJSON } from './utils.js';
import { ChartSettingsManager } from './charts/chart-settings.js'; import { ChartSettingsManager } from './charts/chart-settings.js';
import {
defaultPlotlyLayout,
defaultPlotlyConfig,
createPlotlyPlot,
updatePlotlyPlot,
togglePlotlyFullscreen,
downloadPlotlyImage,
cleanupPlotly
} from './plotly-utils.js';
export class ChartManager { export class ChartManager {
constructor(config, notifications) { constructor(config, notifications) {
@ -30,38 +39,6 @@ export class ChartManager {
}; };
this.settingsManager = new ChartSettingsManager(); 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() { async init() {
@ -110,21 +87,13 @@ export class ChartManager {
this.chartsGrid.appendChild(card); this.chartsGrid.appendChild(card);
const plotContainer = card.querySelector('.chart-card__plot'); const plotContainer = card.querySelector('.chart-card__plot');
const layout = { const layoutOverrides = {
...this.plotlyLayout,
title: { text: formatProcessorName(processorId), font: { size: 16, color: '#f1f5f9' } }, title: { text: formatProcessorName(processorId), font: { size: 16, color: '#f1f5f9' } },
width: plotContainer.clientWidth || 500, width: plotContainer.clientWidth || 500,
height: plotContainer.clientHeight || 420 height: plotContainer.clientHeight || 420
}; };
Plotly.newPlot(plotContainer, [], layout, this.plotlyConfig);
if (window.ResizeObserver) { createPlotlyPlot(plotContainer, [], layoutOverrides);
const ro = new ResizeObserver(() => {
if (plotContainer && plotContainer.clientWidth > 0) Plotly.Plots.resize(plotContainer);
});
ro.observe(plotContainer);
plotContainer._resizeObserver = ro;
}
this.charts.set(processorId, { element: card, plotContainer, isVisible: true, settingsInitialized: false }); this.charts.set(processorId, { element: card, plotContainer, isVisible: true, settingsInitialized: false });
this.performanceStats.chartsCreated++; this.performanceStats.chartsCreated++;
@ -146,15 +115,12 @@ export class ChartManager {
try { try {
const start = performance.now(); const start = performance.now();
this.queueUpdate(processorId, async () => { this.queueUpdate(processorId, async () => {
const updateLayout = { const layoutOverrides = {
...this.plotlyLayout,
...(plotlyConfig.layout || {}), ...(plotlyConfig.layout || {}),
title: { text: formatProcessorName(processorId), font: { size: 16, color: '#f1f5f9' } } 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); this.updateChartMetadata(processorId);
@ -297,8 +263,7 @@ export class ChartManager {
removeChart(id) { removeChart(id) {
const c = this.charts.get(id); const c = this.charts.get(id);
if (c) { if (c) {
if (c.plotContainer?._resizeObserver) { c.plotContainer._resizeObserver.disconnect(); c.plotContainer._resizeObserver = null; } cleanupPlotly(c.plotContainer);
if (c.plotContainer) Plotly.purge(c.plotContainer);
c.element.remove(); c.element.remove();
this.charts.delete(id); this.charts.delete(id);
this.chartData.delete(id); this.chartData.delete(id);
@ -323,12 +288,7 @@ export class ChartManager {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const baseFilename = `${id}_${timestamp}`; const baseFilename = `${id}_${timestamp}`;
await Plotly.downloadImage(c.plotContainer, { await downloadPlotlyImage(c.plotContainer, `${baseFilename}_plot`);
format: 'png',
width: 1200,
height: 800,
filename: `${baseFilename}_plot`
});
const processorData = this.prepareDownloadData(id); const processorData = this.prepareDownloadData(id);
if (processorData) { if (processorData) {
@ -371,24 +331,10 @@ export class ChartManager {
}; };
} }
toggleFullscreen(id) { async toggleFullscreen(id) {
const c = this.charts.get(id); const c = this.charts.get(id);
if (!c?.element) return; if (!c?.element) return;
if (!document.fullscreenElement) { await togglePlotlyFullscreen(c.element, c.plotContainer);
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);
});
}
} }
hideEmptyState() { hideEmptyState() {

View File

@ -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 });
}

View File

@ -7,6 +7,12 @@ import { PresetManager } from './settings/preset-manager.js';
import { CalibrationManager } from './settings/calibration-manager.js'; import { CalibrationManager } from './settings/calibration-manager.js';
import { ReferenceManager } from './settings/reference-manager.js'; import { ReferenceManager } from './settings/reference-manager.js';
import { Debouncer, ButtonState, downloadJSON } from './utils.js'; import { Debouncer, ButtonState, downloadJSON } from './utils.js';
import {
createPlotlyPlot,
togglePlotlyFullscreen,
downloadPlotlyImage,
cleanupPlotly
} from './plotly-utils.js';
export class SettingsManager { export class SettingsManager {
constructor(notifications, websocket, acquisition) { constructor(notifications, websocket, acquisition) {
@ -318,50 +324,12 @@ export class SettingsManager {
return; return;
} }
const layout = { const layoutOverrides = {
...plotConfig.layout, ...plotConfig.layout,
title: { text: title, font: { size: 16, color: '#f1f5f9' } }, 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
}
}; };
const config = { const configOverrides = {
displayModeBar: true,
modeBarButtonsToRemove: ['select2d', 'lasso2d', 'hoverClosestCartesian', 'hoverCompareCartesian', 'toggleSpikelines'],
displaylogo: false,
responsive: false,
doubleClick: 'reset',
toImageButtonOptions: { toImageButtonOptions: {
format: 'png', format: 'png',
filename: `calibration-plot-${Date.now()}`, filename: `calibration-plot-${Date.now()}`,
@ -371,41 +339,12 @@ export class SettingsManager {
} }
}; };
Plotly.newPlot(container, plotConfig.data, layout, config); createPlotlyPlot(container, plotConfig.data, layoutOverrides, configOverrides);
if (window.ResizeObserver) {
const ro = new ResizeObserver(() => {
if (container && container.clientWidth > 0) {
Plotly.Plots.resize(container);
}
});
ro.observe(container);
container._resizeObserver = ro;
}
} }
toggleFullscreen(card) { async toggleFullscreen(card) {
if (!document.fullscreenElement) { const plot = card.querySelector('.chart-card__plot');
card.requestFullscreen?.().then(() => { await togglePlotlyFullscreen(card, plot);
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);
});
}
} }
setupModalCloseHandlers(modal) { setupModalCloseHandlers(modal) {
@ -436,13 +375,8 @@ export class SettingsManager {
modal.classList.remove('modal--active'); modal.classList.remove('modal--active');
document.body.style.overflow = ''; document.body.style.overflow = '';
if (typeof Plotly !== 'undefined') { const containers = modal.querySelectorAll('[id^="calibration-plot-"]');
const containers = modal.querySelectorAll('[id^="calibration-plot-"]'); containers.forEach(c => cleanupPlotly(c));
containers.forEach(c => {
if (c._resizeObserver) { c._resizeObserver.disconnect(); c._resizeObserver = null; }
if (c._fullData) Plotly.purge(c);
});
}
this.currentPlotsData = null; this.currentPlotsData = null;
} }
@ -453,10 +387,8 @@ export class SettingsManager {
const calibrationName = this.currentPlotsData?.calibration_name || 'unknown'; const calibrationName = this.currentPlotsData?.calibration_name || 'unknown';
const base = `${calibrationName}_${standardName}_${ts}`; const base = `${calibrationName}_${standardName}_${ts}`;
if (plotContainer && typeof Plotly !== 'undefined') { if (plotContainer) {
await Plotly.downloadImage(plotContainer, { await downloadPlotlyImage(plotContainer, `${base}_plot`);
format: 'png', width: 1200, height: 800, filename: `${base}_plot`
});
} }
const data = this.prepareCalibrationDownloadData(standardName); const data = this.prepareCalibrationDownloadData(standardName);