added new preview feature
This commit is contained in:
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"open_air": false,
|
"open_air": false,
|
||||||
"axis": "abs",
|
"axis": "abs",
|
||||||
"cut": 0.279,
|
"cut": 0.0,
|
||||||
"max": 1.5,
|
"max": 5.0,
|
||||||
"gain": 0.7,
|
"gain": 0.0,
|
||||||
"start_freq": 2130.0,
|
"start_freq": 100.0,
|
||||||
"stop_freq": 8230.0,
|
"stop_freq": 8800.0,
|
||||||
"clear_history": false,
|
"clear_history": false,
|
||||||
"data_limit": 500
|
"data_limit": 500
|
||||||
}
|
}
|
||||||
@ -4,5 +4,5 @@
|
|||||||
"autoscale": true,
|
"autoscale": true,
|
||||||
"show_magnitude": true,
|
"show_magnitude": true,
|
||||||
"show_phase": false,
|
"show_phase": false,
|
||||||
"open_air": true
|
"open_air": false
|
||||||
}
|
}
|
||||||
@ -268,4 +268,26 @@
|
|||||||
.chart-card__content {
|
.chart-card__content {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* Keyboard shortcuts display */
|
||||||
|
.chart-card__shortcuts {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-card__shortcuts kbd {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: 'Monaco', 'Courier New', monospace;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
color: #1e293b;
|
||||||
|
background: linear-gradient(180deg, #e2e8f0 0%, #cbd5e1 100%);
|
||||||
|
border: 1px solid #94a3b8;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 2px #1e293b inset;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|||||||
@ -100,14 +100,11 @@ export class ChartManager {
|
|||||||
height: plotContainer.clientHeight || 420
|
height: plotContainer.clientHeight || 420
|
||||||
};
|
};
|
||||||
|
|
||||||
// Disable interactivity for bscan processor
|
// Keep interactivity for bscan processor but disable some features
|
||||||
const configOverrides = processorId === 'bscan' ? {
|
const configOverrides = processorId === 'bscan' ? {
|
||||||
staticPlot: false,
|
displayModeBar: true,
|
||||||
displayModeBar: false,
|
modeBarButtonsToRemove: ['select2d', 'lasso2d'],
|
||||||
scrollZoom: false,
|
scrollZoom: false
|
||||||
doubleClick: false,
|
|
||||||
showTips: false,
|
|
||||||
editable: false
|
|
||||||
} : {};
|
} : {};
|
||||||
|
|
||||||
createPlotlyPlot(plotContainer, [], layoutOverrides, configOverrides);
|
createPlotlyPlot(plotContainer, [], layoutOverrides, configOverrides);
|
||||||
@ -142,14 +139,11 @@ export class ChartManager {
|
|||||||
title: { text: formatProcessorName(processorId), font: { size: 16, color: '#f1f5f9' } }
|
title: { text: formatProcessorName(processorId), font: { size: 16, color: '#f1f5f9' } }
|
||||||
};
|
};
|
||||||
|
|
||||||
// Disable interactivity for bscan processor
|
// Keep interactivity for bscan processor but disable some features
|
||||||
const configOverrides = processorId === 'bscan' ? {
|
const configOverrides = processorId === 'bscan' ? {
|
||||||
staticPlot: false,
|
displayModeBar: true,
|
||||||
displayModeBar: false,
|
modeBarButtonsToRemove: ['select2d', 'lasso2d'],
|
||||||
scrollZoom: false,
|
scrollZoom: false
|
||||||
doubleClick: false,
|
|
||||||
showTips: false,
|
|
||||||
editable: false
|
|
||||||
} : {};
|
} : {};
|
||||||
|
|
||||||
await updatePlotlyPlot(chart.plotContainer, plotlyConfig.data || [], layoutOverrides, configOverrides);
|
await updatePlotlyPlot(chart.plotContainer, plotlyConfig.data || [], layoutOverrides, configOverrides);
|
||||||
@ -163,6 +157,11 @@ export class ChartManager {
|
|||||||
this.updateChartSettings(processorId);
|
this.updateChartSettings(processorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear selection for bscan when data updates
|
||||||
|
if (processorId === 'bscan') {
|
||||||
|
this.bscanClickHandler.onDataUpdate(processorId);
|
||||||
|
}
|
||||||
|
|
||||||
const dt = performance.now() - start;
|
const dt = performance.now() - start;
|
||||||
this.updatePerformanceStats(dt);
|
this.updatePerformanceStats(dt);
|
||||||
});
|
});
|
||||||
@ -236,6 +235,11 @@ export class ChartManager {
|
|||||||
<div class="chart-card__meta">
|
<div class="chart-card__meta">
|
||||||
<div class="chart-card__timestamp" data-timestamp="">Last update: --</div>
|
<div class="chart-card__timestamp" data-timestamp="">Last update: --</div>
|
||||||
<div class="chart-card__sweep" data-sweep=""></div>
|
<div class="chart-card__sweep" data-sweep=""></div>
|
||||||
|
${processorId === 'bscan' ? `
|
||||||
|
<div class="chart-card__shortcuts" style="font-size: 11px; color: #94a3b8; margin-top: 4px;">
|
||||||
|
Клавиши: <kbd>Клик</kbd> - выбрать | <kbd>D</kbd> - удалить | <kbd>P</kbd> - предпросмотр | <kbd>Esc</kbd> - отмена
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@ -3,11 +3,16 @@
|
|||||||
* Handles column deletion clicks on the B-Scan heatmap
|
* Handles column deletion clicks on the B-Scan heatmap
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { createPlotlyPlot, cleanupPlotly } from '../plotly-utils.js';
|
||||||
|
|
||||||
export class BScanClickHandler {
|
export class BScanClickHandler {
|
||||||
constructor(websocket, notifications) {
|
constructor(websocket, notifications) {
|
||||||
this.websocket = websocket;
|
this.websocket = websocket;
|
||||||
this.notifications = notifications;
|
this.notifications = notifications;
|
||||||
this.activeListeners = new Map();
|
this.activeListeners = new Map();
|
||||||
|
this.selectedColumn = null;
|
||||||
|
this.selectedProcessorId = null;
|
||||||
|
this.keyboardListener = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,13 +38,18 @@ export class BScanClickHandler {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show confirmation dialog
|
// Select/highlight the column
|
||||||
this.showDeleteConfirmation(processorId, columnIndex);
|
this.selectColumn(processorId, columnIndex, plotContainer);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Attach Plotly click event
|
// Attach Plotly click event
|
||||||
plotContainer.on('plotly_click', clickHandler);
|
plotContainer.on('plotly_click', clickHandler);
|
||||||
this.activeListeners.set(processorId, clickHandler);
|
this.activeListeners.set(processorId, { clickHandler, plotContainer });
|
||||||
|
|
||||||
|
// Setup keyboard listener if not already done
|
||||||
|
if (!this.keyboardListener) {
|
||||||
|
this.setupKeyboardListener();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -48,11 +58,132 @@ export class BScanClickHandler {
|
|||||||
* @param {HTMLElement} plotContainer - Plot container element
|
* @param {HTMLElement} plotContainer - Plot container element
|
||||||
*/
|
*/
|
||||||
detachClickHandler(processorId, plotContainer) {
|
detachClickHandler(processorId, plotContainer) {
|
||||||
const handler = this.activeListeners.get(processorId);
|
const listenerData = this.activeListeners.get(processorId);
|
||||||
if (handler && plotContainer) {
|
if (listenerData && plotContainer) {
|
||||||
plotContainer.removeListener('plotly_click', handler);
|
plotContainer.removeListener('plotly_click', listenerData.clickHandler);
|
||||||
this.activeListeners.delete(processorId);
|
this.activeListeners.delete(processorId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear selection if this processor was selected
|
||||||
|
if (this.selectedProcessorId === processorId) {
|
||||||
|
this.clearSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select and highlight a column
|
||||||
|
* @param {string} processorId - Processor ID
|
||||||
|
* @param {number} columnIndex - Column index (1-based)
|
||||||
|
* @param {HTMLElement} plotContainer - Plot container element
|
||||||
|
*/
|
||||||
|
selectColumn(processorId, columnIndex, plotContainer) {
|
||||||
|
this.selectedColumn = columnIndex;
|
||||||
|
this.selectedProcessorId = processorId;
|
||||||
|
|
||||||
|
// Add visual highlight by adding a vertical line shape
|
||||||
|
this.highlightColumn(plotContainer, columnIndex);
|
||||||
|
|
||||||
|
console.log(`Column ${columnIndex} selected. Press 'D' to delete.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add visual highlight to the selected column
|
||||||
|
* @param {HTMLElement} plotContainer - Plot container element
|
||||||
|
* @param {number} columnIndex - Column index (1-based)
|
||||||
|
*/
|
||||||
|
highlightColumn(plotContainer, columnIndex) {
|
||||||
|
if (!plotContainer || typeof Plotly === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a vertical line shape to highlight the column
|
||||||
|
const update = {
|
||||||
|
shapes: [{
|
||||||
|
type: 'line',
|
||||||
|
x0: columnIndex - 0.5,
|
||||||
|
x1: columnIndex - 0.5,
|
||||||
|
y0: 0,
|
||||||
|
y1: 1,
|
||||||
|
yref: 'paper',
|
||||||
|
line: {
|
||||||
|
color: 'rgba(255, 0, 0, 0.8)',
|
||||||
|
width: 3,
|
||||||
|
dash: 'solid'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
type: 'line',
|
||||||
|
x0: columnIndex + 0.5,
|
||||||
|
x1: columnIndex + 0.5,
|
||||||
|
y0: 0,
|
||||||
|
y1: 1,
|
||||||
|
yref: 'paper',
|
||||||
|
line: {
|
||||||
|
color: 'rgba(255, 0, 0, 0.8)',
|
||||||
|
width: 3,
|
||||||
|
dash: 'solid'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
Plotly.relayout(plotContainer, update);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear column selection and highlighting
|
||||||
|
*/
|
||||||
|
clearSelection() {
|
||||||
|
if (!this.selectedProcessorId || !this.selectedColumn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listenerData = this.activeListeners.get(this.selectedProcessorId);
|
||||||
|
if (listenerData && listenerData.plotContainer && typeof Plotly !== 'undefined') {
|
||||||
|
// Remove shapes (highlighting)
|
||||||
|
Plotly.relayout(listenerData.plotContainer, { shapes: [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedColumn = null;
|
||||||
|
this.selectedProcessorId = null;
|
||||||
|
|
||||||
|
console.log('Selection cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup keyboard listener for 'D' key
|
||||||
|
*/
|
||||||
|
setupKeyboardListener() {
|
||||||
|
this.keyboardListener = (event) => {
|
||||||
|
// Ignore if user is typing in an input field
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
if (activeElement && (
|
||||||
|
activeElement.tagName === 'INPUT' ||
|
||||||
|
activeElement.tagName === 'TEXTAREA' ||
|
||||||
|
activeElement.isContentEditable
|
||||||
|
)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if 'D' key is pressed (case insensitive)
|
||||||
|
if (event.key === 'd' || event.key === 'D') {
|
||||||
|
// Delete selected column if any
|
||||||
|
if (this.selectedColumn !== null && this.selectedProcessorId !== null) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.showDeleteConfirmation(this.selectedProcessorId, this.selectedColumn);
|
||||||
|
}
|
||||||
|
} else if (event.key === 'p' || event.key === 'P') {
|
||||||
|
// Show sweep preview modal
|
||||||
|
if (this.selectedColumn !== null && this.selectedProcessorId !== null) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.showSweepPreview(this.selectedProcessorId, this.selectedColumn);
|
||||||
|
}
|
||||||
|
} else if (event.key === 'Escape') {
|
||||||
|
// Clear selection on Escape
|
||||||
|
this.clearSelection();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', this.keyboardListener);
|
||||||
|
console.log('Keyboard shortcuts: D - delete column, P - preview sweep, Escape - deselect');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,7 +205,19 @@ export class BScanClickHandler {
|
|||||||
* @param {number} columnIndex - Column index to delete (1-based)
|
* @param {number} columnIndex - Column index to delete (1-based)
|
||||||
*/
|
*/
|
||||||
deleteColumn(processorId, columnIndex) {
|
deleteColumn(processorId, columnIndex) {
|
||||||
if (!this.websocket || !this.websocket.ws || this.websocket.ws.readyState !== WebSocket.OPEN) {
|
// Get websocket from global window object if not available in instance
|
||||||
|
const websocket = this.websocket || window.vnaDashboard?.websocket;
|
||||||
|
|
||||||
|
console.log('deleteColumn called:', { processorId, columnIndex, websocket, ws: websocket?.ws, readyState: websocket?.ws?.readyState });
|
||||||
|
|
||||||
|
if (!websocket || !websocket.ws || websocket.ws.readyState !== WebSocket.OPEN) {
|
||||||
|
console.error('WebSocket not available or not open:', {
|
||||||
|
hasWebsocket: !!websocket,
|
||||||
|
hasWs: !!websocket?.ws,
|
||||||
|
readyState: websocket?.ws?.readyState,
|
||||||
|
OPEN: WebSocket.OPEN
|
||||||
|
});
|
||||||
|
|
||||||
this.notifications?.show?.({
|
this.notifications?.show?.({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
title: 'Ошибка подключения',
|
title: 'Ошибка подключения',
|
||||||
@ -90,7 +233,8 @@ export class BScanClickHandler {
|
|||||||
column_index: columnIndex
|
column_index: columnIndex
|
||||||
};
|
};
|
||||||
|
|
||||||
this.websocket.ws.send(JSON.stringify(message));
|
console.log('Sending delete column message:', message);
|
||||||
|
websocket.ws.send(JSON.stringify(message));
|
||||||
|
|
||||||
this.notifications?.show?.({
|
this.notifications?.show?.({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
@ -108,10 +252,324 @@ export class BScanClickHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show sweep preview modal for selected column
|
||||||
|
* @param {string} processorId - Processor ID
|
||||||
|
* @param {number} columnIndex - Column index (1-based)
|
||||||
|
*/
|
||||||
|
showSweepPreview(processorId, columnIndex) {
|
||||||
|
// Get websocket from global window object if not available in instance
|
||||||
|
const websocket = this.websocket || window.vnaDashboard?.websocket;
|
||||||
|
|
||||||
|
if (!websocket || !websocket.ws || websocket.ws.readyState !== WebSocket.OPEN) {
|
||||||
|
this.notifications?.show?.({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Ошибка подключения',
|
||||||
|
message: 'WebSocket не подключен'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Request processor state to get sweep data
|
||||||
|
const message = {
|
||||||
|
type: 'get_processor_state',
|
||||||
|
processor_id: processorId
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('Requesting processor state for sweep preview:', message);
|
||||||
|
|
||||||
|
// Set up one-time listener for the response
|
||||||
|
const handleResponse = (event) => {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(event.data);
|
||||||
|
|
||||||
|
if (response.type === 'processor_state' && response.processor_id === processorId) {
|
||||||
|
// Remove this listener
|
||||||
|
websocket.ws.removeEventListener('message', handleResponse);
|
||||||
|
|
||||||
|
// Extract sweep data for the selected column
|
||||||
|
this.displaySweepModal(response, columnIndex);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing processor state response:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.ws.addEventListener('message', handleResponse);
|
||||||
|
websocket.ws.send(JSON.stringify(message));
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to request sweep preview:', error);
|
||||||
|
this.notifications?.show?.({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Ошибка предпросмотра',
|
||||||
|
message: 'Не удалось загрузить данные свипа'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display sweep data in modal window
|
||||||
|
* @param {Object} processorState - Processor state from backend
|
||||||
|
* @param {number} columnIndex - Column index (1-based, same as sweep number)
|
||||||
|
*/
|
||||||
|
displaySweepModal(processorState, columnIndex) {
|
||||||
|
const sweepHistory = processorState.state?.sweep_history || [];
|
||||||
|
const config = processorState.state?.config || {};
|
||||||
|
|
||||||
|
// Convert 1-based column index to 0-based array index
|
||||||
|
const sweepIndex = columnIndex - 1;
|
||||||
|
|
||||||
|
if (sweepIndex < 0 || sweepIndex >= sweepHistory.length) {
|
||||||
|
this.notifications?.show?.({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Ошибка',
|
||||||
|
message: `Свип ${columnIndex} не найден в истории`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sweep = sweepHistory[sweepIndex];
|
||||||
|
|
||||||
|
// Use same logic as BScanProcessor: calibrated_data or sweep_data
|
||||||
|
const hasCalibrated = sweep.calibrated_points && sweep.calibrated_points.length > 0;
|
||||||
|
const dataToProcess = hasCalibrated ? sweep.calibrated_points : (sweep.sweep_points || []);
|
||||||
|
const referencePoints = sweep.reference_points || [];
|
||||||
|
const openAirEnabled = config.open_air || false;
|
||||||
|
|
||||||
|
if (dataToProcess.length === 0) {
|
||||||
|
this.notifications?.show?.({
|
||||||
|
type: 'warning',
|
||||||
|
title: 'Нет данных',
|
||||||
|
message: `Свип ${columnIndex} не содержит данных для отображения`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show modal with plot
|
||||||
|
this.showSweepPlotModal(
|
||||||
|
columnIndex,
|
||||||
|
dataToProcess,
|
||||||
|
referencePoints,
|
||||||
|
openAirEnabled,
|
||||||
|
hasCalibrated,
|
||||||
|
sweep.vna_config
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show modal with sweep plot
|
||||||
|
* @param {number} sweepNumber - Sweep number (1-based)
|
||||||
|
* @param {Array} points - Sweep points (calibrated or raw)
|
||||||
|
* @param {Array} referencePoints - Reference points for open air subtraction
|
||||||
|
* @param {boolean} openAirEnabled - Whether to subtract reference
|
||||||
|
* @param {boolean} hasCalibrated - Whether the data is calibrated (true) or raw (false)
|
||||||
|
* @param {Object} vnaConfig - VNA configuration
|
||||||
|
*/
|
||||||
|
showSweepPlotModal(sweepNumber, points, referencePoints, openAirEnabled, hasCalibrated, vnaConfig) {
|
||||||
|
// Get modal element
|
||||||
|
const modal = document.getElementById('plotsModal');
|
||||||
|
if (!modal) {
|
||||||
|
console.error('Plots modal not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Prepare data for Plotly
|
||||||
|
const frequencies = [];
|
||||||
|
const magnitudes = [];
|
||||||
|
const phases = [];
|
||||||
|
|
||||||
|
points.forEach((point, index) => {
|
||||||
|
const freq = vnaConfig?.start_freq + (index / (points.length - 1)) * (vnaConfig?.stop_freq - vnaConfig?.start_freq);
|
||||||
|
frequencies.push(freq / 1e6); // Convert to MHz
|
||||||
|
|
||||||
|
let real = point[0] || point.real || point.r || 0;
|
||||||
|
let imag = point[1] || point.imag || point.i || 0;
|
||||||
|
|
||||||
|
// Apply reference subtraction if enabled (same as BScanProcessor)
|
||||||
|
if (openAirEnabled && referencePoints && referencePoints.length > index) {
|
||||||
|
const refPoint = referencePoints[index];
|
||||||
|
const refReal = refPoint[0] || refPoint.real || refPoint.r || 0;
|
||||||
|
const refImag = refPoint[1] || refPoint.imag || refPoint.i || 0;
|
||||||
|
|
||||||
|
// Subtract reference: complex_data - reference_complex
|
||||||
|
real = real - refReal;
|
||||||
|
imag = imag - refImag;
|
||||||
|
}
|
||||||
|
|
||||||
|
const magnitude = Math.sqrt(real * real + imag * imag);
|
||||||
|
const phase = Math.atan2(imag, real) * (180 / Math.PI);
|
||||||
|
|
||||||
|
magnitudes.push(magnitude);
|
||||||
|
phases.push(phase);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create Plotly traces
|
||||||
|
const traces = [
|
||||||
|
{
|
||||||
|
x: frequencies,
|
||||||
|
y: magnitudes,
|
||||||
|
type: 'scatter',
|
||||||
|
mode: 'lines',
|
||||||
|
name: 'Амплитуда',
|
||||||
|
line: { color: '#3b82f6', width: 2 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x: frequencies,
|
||||||
|
y: phases,
|
||||||
|
type: 'scatter',
|
||||||
|
mode: 'lines',
|
||||||
|
name: 'Фаза',
|
||||||
|
yaxis: 'y2',
|
||||||
|
line: { color: '#f59e0b', width: 2 }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Build descriptive title
|
||||||
|
const dataType = hasCalibrated ? 'откалиброванные' : 'сырые';
|
||||||
|
const refStatus = openAirEnabled && referencePoints && referencePoints.length > 0 ? 'с вычетом референса' : 'без вычета референса';
|
||||||
|
const titleText = `Свип ${sweepNumber} (${dataType}, ${refStatus})`;
|
||||||
|
|
||||||
|
const layoutOverrides = {
|
||||||
|
title: { text: titleText, font: { size: 16, color: '#f1f5f9' } },
|
||||||
|
xaxis: {
|
||||||
|
title: 'Частота (МГц)',
|
||||||
|
gridcolor: '#334155',
|
||||||
|
zerolinecolor: '#475569',
|
||||||
|
color: '#cbd5e1',
|
||||||
|
fixedrange: false
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
title: 'Амплитуда',
|
||||||
|
side: 'left',
|
||||||
|
gridcolor: '#334155',
|
||||||
|
zerolinecolor: '#475569',
|
||||||
|
color: '#cbd5e1',
|
||||||
|
fixedrange: false
|
||||||
|
},
|
||||||
|
yaxis2: {
|
||||||
|
title: 'Фаза (°)',
|
||||||
|
overlaying: 'y',
|
||||||
|
side: 'right',
|
||||||
|
gridcolor: '#334155',
|
||||||
|
zerolinecolor: '#475569',
|
||||||
|
color: '#cbd5e1',
|
||||||
|
fixedrange: false
|
||||||
|
},
|
||||||
|
showlegend: true,
|
||||||
|
legend: { x: 0.01, y: 0.99 },
|
||||||
|
height: 500
|
||||||
|
};
|
||||||
|
|
||||||
|
const configOverrides = {
|
||||||
|
toImageButtonOptions: {
|
||||||
|
format: 'png',
|
||||||
|
filename: `sweep-${sweepNumber}-preview`,
|
||||||
|
height: 600,
|
||||||
|
width: 800,
|
||||||
|
scale: 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update modal title
|
||||||
|
const title = modal.querySelector('.modal__title');
|
||||||
|
if (title) {
|
||||||
|
title.textContent = titleText;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get plots grid container and clear it
|
||||||
|
const container = document.getElementById('plotsGrid');
|
||||||
|
if (!container) {
|
||||||
|
console.error('Plots grid container not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
// Create card wrapper with proper styling
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'chart-card';
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="chart-card__header">
|
||||||
|
<div class="chart-card__title">${titleText}</div>
|
||||||
|
</div>
|
||||||
|
<div class="chart-card__content">
|
||||||
|
<div class="chart-card__plot" id="sweep-preview-plot"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
container.appendChild(card);
|
||||||
|
|
||||||
|
// Setup close handlers
|
||||||
|
this.setupModalCloseHandlers(modal);
|
||||||
|
|
||||||
|
// Show modal
|
||||||
|
modal.classList.add('modal--active');
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
// Create plot after modal is shown
|
||||||
|
setTimeout(() => {
|
||||||
|
const plotElement = document.getElementById('sweep-preview-plot');
|
||||||
|
if (plotElement) {
|
||||||
|
createPlotlyPlot(plotElement, traces, layoutOverrides, configOverrides);
|
||||||
|
} else {
|
||||||
|
console.error('Plot element not found');
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup modal close handlers
|
||||||
|
* @param {HTMLElement} modal - Modal element
|
||||||
|
*/
|
||||||
|
setupModalCloseHandlers(modal) {
|
||||||
|
const closeElements = modal.querySelectorAll('[data-modal-close]');
|
||||||
|
closeElements.forEach(el => {
|
||||||
|
el.onclick = () => this.closeModal(modal);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close modal
|
||||||
|
* @param {HTMLElement} modal - Modal element
|
||||||
|
*/
|
||||||
|
closeModal(modal) {
|
||||||
|
modal.classList.remove('modal--active');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
|
||||||
|
// Cleanup Plotly plot
|
||||||
|
const plotElement = document.getElementById('sweep-preview-plot');
|
||||||
|
if (plotElement) {
|
||||||
|
cleanupPlotly(plotElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when data is updated (new columns added, imported, etc.)
|
||||||
|
* Clears the current selection
|
||||||
|
* @param {string} processorId - Processor ID
|
||||||
|
*/
|
||||||
|
onDataUpdate(processorId) {
|
||||||
|
// Clear selection if this processor's data was updated
|
||||||
|
if (this.selectedProcessorId === processorId) {
|
||||||
|
this.clearSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clean up all listeners
|
* Clean up all listeners
|
||||||
*/
|
*/
|
||||||
destroy() {
|
destroy() {
|
||||||
|
// Remove keyboard listener
|
||||||
|
if (this.keyboardListener) {
|
||||||
|
document.removeEventListener('keydown', this.keyboardListener);
|
||||||
|
this.keyboardListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear selection
|
||||||
|
this.clearSelection();
|
||||||
|
|
||||||
|
// Clear active listeners
|
||||||
this.activeListeners.clear();
|
this.activeListeners.clear();
|
||||||
|
|
||||||
|
console.log('BScanClickHandler destroyed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user