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,
"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,

View File

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

View File

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

View File

@ -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() {

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 { 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(() => {
async toggleFullscreen(card) {
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);
});
}
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);
});
}
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);