refactoring 4
This commit is contained in:
@ -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,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"y_min": -80,
|
||||
"y_max": 30,
|
||||
"show_phase": true
|
||||
"y_max": 40,
|
||||
"show_phase": false
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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() {
|
||||
|
||||
176
vna_system/web_ui/static/js/modules/plotly-utils.js
Normal file
176
vna_system/web_ui/static/js/modules/plotly-utils.js
Normal 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 });
|
||||
}
|
||||
@ -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);
|
||||
|
||||
Reference in New Issue
Block a user