added functionality, save buttons etc

This commit is contained in:
Ayzen
2025-09-26 13:00:05 +03:00
parent cdf48fd3e0
commit 926268733c
22 changed files with 13689 additions and 115 deletions

View File

@ -1,8 +1,10 @@
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from typing import List from typing import List
from pathlib import Path
import vna_system.core.singletons as singletons import vna_system.core.singletons as singletons
from vna_system.core.settings.calibration_manager import CalibrationStandard from vna_system.core.settings.calibration_manager import CalibrationStandard
from vna_system.core.visualization.magnitude_chart import generate_standards_magnitude_plots, generate_combined_standards_plot
from vna_system.api.models.settings import ( from vna_system.api.models.settings import (
PresetModel, PresetModel,
CalibrationModel, CalibrationModel,
@ -297,5 +299,120 @@ async def get_current_calibration():
"standards": [s.value for s in current_calib.standards.keys()], "standards": [s.value for s in current_calib.standards.keys()],
"is_complete": current_calib.is_complete() "is_complete": current_calib.is_complete()
} }
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/calibration/{calibration_name}/standards-plots")
async def get_calibration_standards_plots(calibration_name: str, preset_filename: str = None):
"""Get magnitude plots for all standards in a calibration set"""
try:
# Get preset
preset = None
if preset_filename:
presets = singletons.settings_manager.get_available_presets()
preset = next((p for p in presets if p.filename == preset_filename), None)
if not preset:
raise HTTPException(status_code=404, detail=f"Preset not found: {preset_filename}")
else:
preset = singletons.settings_manager.get_current_preset()
if not preset:
raise HTTPException(status_code=400, detail="No current preset selected")
# Get calibration directory
calibration_manager = singletons.settings_manager.calibration_manager
calibration_dir = calibration_manager._get_preset_calibration_dir(preset) / calibration_name
if not calibration_dir.exists():
raise HTTPException(status_code=404, detail=f"Calibration not found: {calibration_name}")
# Generate plots for each standard
individual_plots = generate_standards_magnitude_plots(calibration_dir, preset)
return {
"calibration_name": calibration_name,
"preset": {
"filename": preset.filename,
"mode": preset.mode.value
},
"individual_plots": individual_plots
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.get("/working-calibration/standards-plots")
async def get_working_calibration_standards_plots():
"""Get magnitude plots for standards in current working calibration"""
try:
working_calib = singletons.settings_manager.get_current_working_calibration()
if not working_calib:
raise HTTPException(status_code=404, detail="No working calibration active")
# Check if there are any standards captured
if not working_calib.standards:
raise HTTPException(status_code=404, detail="No standards captured in working calibration")
# Generate plots directly from in-memory sweep data
from vna_system.core.visualization.magnitude_chart import generate_magnitude_plot_from_sweep_data
individual_plots = {}
standard_colors = {
'open': '#2ca02c', # Green
'short': '#d62728', # Red
'load': '#ff7f0e', # Orange
'through': '#1f77b4' # Blue
}
for standard, sweep_data in working_calib.standards.items():
try:
# Generate plot for this standard
plot_config = generate_magnitude_plot_from_sweep_data(sweep_data, working_calib.preset)
if 'error' not in plot_config:
# Customize color and title for this standard
if plot_config.get('data'):
plot_config['data'][0]['line']['color'] = standard_colors.get(standard.value, '#1f77b4')
plot_config['data'][0]['name'] = f'{standard.value.upper()} Standard'
plot_config['layout']['title'] = f'{standard.value.upper()} Standard Magnitude (Working)'
# Include raw sweep data for download
plot_config['raw_sweep_data'] = {
'sweep_number': sweep_data.sweep_number,
'timestamp': sweep_data.timestamp,
'total_points': sweep_data.total_points,
'points': sweep_data.points, # Raw complex data points
'file_path': None # No file path for working calibration
}
# Add frequency information
plot_config['frequency_info'] = {
'start_freq': working_calib.preset.start_freq,
'stop_freq': working_calib.preset.stop_freq,
'points': working_calib.preset.points,
'bandwidth': working_calib.preset.bandwidth
}
individual_plots[standard.value] = plot_config
else:
individual_plots[standard.value] = plot_config
except Exception as e:
individual_plots[standard.value] = {'error': f'Failed to generate plot for {standard.value}: {str(e)}'}
if not individual_plots:
raise HTTPException(status_code=404, detail="No valid plots generated for working calibration")
return {
"calibration_name": "Working Calibration",
"preset": {
"filename": working_calib.preset.filename,
"mode": working_calib.preset.mode.value
},
"individual_plots": individual_plots,
"is_working": True,
"is_complete": working_calib.is_complete()
}
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))

View File

@ -1 +1 @@
config_inputs/s21_start100_stop8800_points1000_bw1khz.bin config_inputs/s11_start100_stop8800_points1000_bw1khz.bin

View File

@ -0,0 +1 @@
s11_start100_stop8800_points1000_bw1khz/lol

View File

@ -0,0 +1,18 @@
{
"preset": {
"filename": "s11_start100_stop8800_points1000_bw1khz.bin",
"mode": "s11",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "sdbsd",
"standards": [
"open",
"short",
"load"
],
"created_timestamp": "2025-09-25T17:53:37.990976",
"is_complete": true
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
{
"preset": {
"filename": "s11_start100_stop8800_points1000_bw1khz.bin",
"mode": "s11",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "sdbsd",
"standard": "load",
"sweep_number": 15,
"sweep_timestamp": 1758812011.9188073,
"created_timestamp": "2025-09-25T17:53:37.990927",
"total_points": 1000
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
{
"preset": {
"filename": "s11_start100_stop8800_points1000_bw1khz.bin",
"mode": "s11",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "sdbsd",
"standard": "open",
"sweep_number": 13,
"sweep_timestamp": 1758811971.8226106,
"created_timestamp": "2025-09-25T17:53:37.986902",
"total_points": 1000
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
{
"preset": {
"filename": "s11_start100_stop8800_points1000_bw1khz.bin",
"mode": "s11",
"start_freq": 100000000.0,
"stop_freq": 8800000000.0,
"points": 1000,
"bandwidth": 1000.0
},
"calibration_name": "sdbsd",
"standard": "short",
"sweep_number": 14,
"sweep_timestamp": 1758811996.130049,
"created_timestamp": "2025-09-25T17:53:37.988934",
"total_points": 1000
}

View File

@ -13,7 +13,7 @@ from vna_system.core.settings.preset_manager import ConfigPreset
class UIParameter: class UIParameter:
name: str name: str
label: str label: str
type: str # 'slider', 'toggle', 'select', 'input' type: str # 'slider', 'toggle', 'select', 'input', 'button'
value: Any value: Any
options: Optional[Dict[str, Any]] = None # min/max for slider, choices for select, etc. options: Optional[Dict[str, Any]] = None # min/max for slider, choices for select, etc.

View File

@ -1,9 +1,9 @@
{ {
"y_min": -110, "y_min": -80,
"y_max": 10, "y_max": 15,
"smoothing_enabled": false, "smoothing_enabled": false,
"smoothing_window": 17, "smoothing_window": 5,
"marker_enabled": true, "marker_enabled": true,
"marker_frequency": 1, "marker_frequency": 100000009,
"grid_enabled": false "grid_enabled": true
} }

View File

@ -2,11 +2,11 @@
"y_min": -210, "y_min": -210,
"y_max": 360, "y_max": 360,
"unwrap_phase": false, "unwrap_phase": false,
"phase_offset": -60, "phase_offset": 0,
"smoothing_enabled": true, "smoothing_enabled": true,
"smoothing_window": 5, "smoothing_window": 5,
"marker_enabled": false, "marker_enabled": false,
"marker_frequency": 2000000000, "marker_frequency": "asdasd",
"reference_line_enabled": false, "reference_line_enabled": false,
"reference_phase": 0, "reference_phase": 0,
"grid_enabled": true "grid_enabled": true

View File

@ -1,9 +1,9 @@
{ {
"impedance_mode": true, "impedance_mode": false,
"reference_impedance": 100, "reference_impedance": 100,
"marker_enabled": true, "marker_enabled": false,
"marker_frequency": 1000000000, "marker_frequency": 1000000000,
"grid_circles": true, "grid_circles": false,
"grid_radials": true, "grid_radials": false,
"trace_color_mode": "solid" "trace_color_mode": "solid"
} }

View File

@ -8,6 +8,8 @@ from ..base_processor import BaseProcessor, UIParameter
class MagnitudeProcessor(BaseProcessor): class MagnitudeProcessor(BaseProcessor):
def __init__(self, config_dir: Path): def __init__(self, config_dir: Path):
super().__init__("magnitude", config_dir) super().__init__("magnitude", config_dir)
# State for smoothing that can be reset by button
self._smoothing_history = []
def process_sweep(self, sweep_data: Any, calibrated_data: Any, vna_config: Dict[str, Any]) -> Dict[str, Any]: def process_sweep(self, sweep_data: Any, calibrated_data: Any, vna_config: Dict[str, Any]) -> Dict[str, Any]:
if not calibrated_data or not hasattr(calibrated_data, 'points'): if not calibrated_data or not hasattr(calibrated_data, 'points'):
@ -141,6 +143,13 @@ class MagnitudeProcessor(BaseProcessor):
label='Show Grid', label='Show Grid',
type='toggle', type='toggle',
value=self._config.get('grid_enabled', True) value=self._config.get('grid_enabled', True)
),
UIParameter(
name='reset_smoothing',
label='Reset Smoothing',
type='button',
value=False, # Always False for buttons, will be set to True temporarily when clicked
options={'action': 'Reset the smoothing filter state'}
) )
] ]
@ -155,6 +164,34 @@ class MagnitudeProcessor(BaseProcessor):
'grid_enabled': True 'grid_enabled': True
} }
def update_config(self, updates: Dict[str, Any]):
print(f"🔧 update_config called with: {updates}")
# Handle button parameters specially
button_actions = {}
config_updates = {}
for key, value in updates.items():
# Check if this is a button parameter
ui_params = {param.name: param for param in self.get_ui_parameters()}
if key in ui_params and ui_params[key].type == 'button':
if value: # Button was clicked
button_actions[key] = value
# Don't add button values to config
else:
config_updates[key] = value
# Update config with non-button parameters
if config_updates:
super().update_config(config_updates)
# Handle button actions
for action, pressed in button_actions.items():
if pressed and action == 'reset_smoothing':
# Reset smoothing state (could be a counter, filter state, etc.)
self._smoothing_history = [] # Reset any internal smoothing state
print(f"🔄 Smoothing state reset by button action")
# Note: recalculate() will be called automatically by the processing system
def _validate_config(self): def _validate_config(self):
required_keys = ['y_min', 'y_max', 'smoothing_enabled', 'smoothing_window', required_keys = ['y_min', 'y_max', 'smoothing_enabled', 'smoothing_window',
'marker_enabled', 'marker_frequency', 'grid_enabled'] 'marker_enabled', 'marker_frequency', 'grid_enabled']

View File

@ -0,0 +1,264 @@
import numpy as np
from typing import Dict, Any, List, Tuple
import json
from pathlib import Path
from vna_system.core.acquisition.sweep_buffer import SweepData
from vna_system.core.settings.preset_manager import ConfigPreset
def generate_magnitude_plot_from_sweep_data(sweep_data: SweepData, preset: ConfigPreset = None) -> Dict[str, Any]:
"""
Generate Plotly configuration for magnitude plot from SweepData
Args:
sweep_data: SweepData instance with points list of [real, imag] complex pairs
preset: Optional ConfigPreset with frequency info
Returns:
Plotly configuration dict for magnitude plot
"""
if not sweep_data or not sweep_data.points:
return {'error': 'Invalid sweep data'}
# Extract frequency range from preset or use defaults
start_freq = 100e6 # 100 MHz
stop_freq = 8.8e9 # 8.8 GHz
if preset:
start_freq = preset.start_freq or start_freq
stop_freq = preset.stop_freq or stop_freq
frequencies = []
magnitudes_db = []
# Calculate magnitude in dB for each point
for i, (real, imag) in enumerate(sweep_data.points):
complex_val = complex(real, imag)
magnitude_db = 20 * np.log10(abs(complex_val)) if abs(complex_val) > 0 else -120
# Calculate frequency based on point index
total_points = len(sweep_data.points)
frequency = start_freq + (stop_freq - start_freq) * i / (total_points - 1)
frequencies.append(frequency)
magnitudes_db.append(magnitude_db)
# Create Plotly trace
trace = {
'x': [f / 1e9 for f in frequencies], # Convert to GHz
'y': magnitudes_db,
'type': 'scatter',
'mode': 'lines',
'name': 'Magnitude',
'line': {'color': '#1f77b4', 'width': 2}
}
# Calculate reasonable Y-axis range
min_mag = min(magnitudes_db)
max_mag = max(magnitudes_db)
y_margin = (max_mag - min_mag) * 0.1
y_min = max(min_mag - y_margin, -120)
y_max = min(max_mag + y_margin, 20)
return {
'data': [trace],
'layout': {
'title': 'Magnitude Response',
'xaxis': {
'title': 'Frequency (GHz)',
'showgrid': True,
'gridcolor': '#e5e5e5',
'gridwidth': 1
},
'yaxis': {
'title': 'Magnitude (dB)',
'range': [y_min, y_max],
'showgrid': True,
'gridcolor': '#e5e5e5',
'gridwidth': 1
},
'plot_bgcolor': '#fafafa',
'paper_bgcolor': '#ffffff',
'font': {
'family': 'Arial, sans-serif',
'size': 12,
'color': '#333333'
},
'hovermode': 'x unified',
'showlegend': True,
'margin': {'l': 60, 'r': 40, 't': 60, 'b': 60}
}
}
def load_sweep_data_from_json(json_file: Path) -> SweepData:
"""
Load SweepData from JSON file
Args:
json_file: Path to JSON file containing sweep data
Returns:
SweepData instance
"""
with open(json_file, 'r') as f:
data = json.load(f)
return SweepData(
sweep_number=data.get('sweep_number', 0),
timestamp=data.get('timestamp', 0.0),
points=data.get('points', []),
total_points=data.get('total_points', len(data.get('points', [])))
)
def generate_standards_magnitude_plots(calibration_path: Path, preset: ConfigPreset = None) -> Dict[str, Any]:
"""
Generate magnitude plots for all calibration standards in a calibration set
Args:
calibration_path: Path to calibration directory
preset: Optional ConfigPreset
Returns:
Dictionary with plots for each standard, including raw data
"""
plots = {}
standard_colors = {
'open': '#2ca02c', # Green
'short': '#d62728', # Red
'load': '#ff7f0e', # Orange
'through': '#1f77b4' # Blue
}
# Find all standard JSON files
for standard_file in calibration_path.glob('*.json'):
standard_name = standard_file.stem
# Skip metadata files
if 'metadata' in standard_name:
continue
try:
sweep_data = load_sweep_data_from_json(standard_file)
plot_config = generate_magnitude_plot_from_sweep_data(sweep_data, preset)
if 'error' not in plot_config:
# Customize color and title for this standard
if plot_config.get('data'):
plot_config['data'][0]['line']['color'] = standard_colors.get(standard_name, '#1f77b4')
plot_config['data'][0]['name'] = f'{standard_name.upper()} Standard'
plot_config['layout']['title'] = f'{standard_name.upper()} Standard Magnitude'
# Include raw sweep data for download
plot_config['raw_sweep_data'] = {
'sweep_number': sweep_data.sweep_number,
'timestamp': sweep_data.timestamp,
'total_points': sweep_data.total_points,
'points': sweep_data.points, # Raw complex data points
'file_path': str(standard_file)
}
# Add frequency information if available
if preset:
plot_config['frequency_info'] = {
'start_freq': preset.start_freq,
'stop_freq': preset.stop_freq,
'points': preset.points,
'bandwidth': preset.bandwidth
}
plots[standard_name] = plot_config
except (json.JSONDecodeError, FileNotFoundError, KeyError) as e:
plots[standard_name] = {'error': f'Failed to load {standard_name}: {str(e)}'}
return plots
def generate_combined_standards_plot(calibration_path: Path, preset: ConfigPreset = None) -> Dict[str, Any]:
"""
Generate a combined plot showing all calibration standards
Args:
calibration_path: Path to calibration directory
preset: Optional ConfigPreset
Returns:
Plotly configuration dict with all standards overlaid
"""
traces = []
standard_colors = {
'open': '#2ca02c', # Green
'short': '#d62728', # Red
'load': '#ff7f0e', # Orange
'through': '#1f77b4' # Blue
}
y_min, y_max = 0, -120
# Process each standard
for standard_file in calibration_path.glob('*.json'):
standard_name = standard_file.stem
# Skip metadata files
if 'metadata' in standard_name:
continue
try:
sweep_data = load_sweep_data_from_json(standard_file)
plot_config = generate_magnitude_plot_from_sweep_data(sweep_data, preset)
if 'error' not in plot_config and plot_config.get('data'):
trace = plot_config['data'][0].copy()
trace['line']['color'] = standard_colors.get(standard_name, '#1f77b4')
trace['name'] = f'{standard_name.upper()} Standard'
traces.append(trace)
# Update Y range
if trace['y']:
trace_min = min(trace['y'])
trace_max = max(trace['y'])
y_min = min(y_min, trace_min)
y_max = max(y_max, trace_max)
except (json.JSONDecodeError, FileNotFoundError, KeyError):
continue
if not traces:
return {'error': 'No valid calibration standards found'}
# Add margin to Y range
y_margin = (y_max - y_min) * 0.1
y_min = max(y_min - y_margin, -120)
y_max = min(y_max + y_margin, 20)
return {
'data': traces,
'layout': {
'title': 'Calibration Standards Comparison',
'xaxis': {
'title': 'Frequency (GHz)',
'showgrid': True,
'gridcolor': '#e5e5e5',
'gridwidth': 1
},
'yaxis': {
'title': 'Magnitude (dB)',
'range': [y_min, y_max],
'showgrid': True,
'gridcolor': '#e5e5e5',
'gridwidth': 1
},
'plot_bgcolor': '#fafafa',
'paper_bgcolor': '#ffffff',
'font': {
'family': 'Arial, sans-serif',
'size': 12,
'color': '#333333'
},
'hovermode': 'x unified',
'showlegend': True,
'margin': {'l': 60, 'r': 40, 't': 60, 'b': 60}
}
}

View File

@ -866,6 +866,37 @@ input:checked + .processor-param__toggle-slider:before {
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
} }
.chart-setting__button {
padding: var(--space-2) var(--space-3);
font-size: var(--text-sm);
font-weight: 500;
color: var(--color-text-light);
background: var(--color-primary-600);
border: 1px solid var(--color-primary-600);
border-radius: var(--radius-md);
cursor: pointer;
transition: all var(--transition-fast);
width: 100%;
max-width: 200px;
}
.chart-setting__button:hover {
background: var(--color-primary-700);
border-color: var(--color-primary-700);
}
.chart-setting__button:active {
background: var(--color-primary-800);
border-color: var(--color-primary-800);
}
.chart-setting__button:disabled {
background: var(--color-bg-muted);
border-color: var(--color-border-muted);
color: var(--color-text-muted);
cursor: not-allowed;
}
/* Mobile Responsive Styles for Chart Settings */ /* Mobile Responsive Styles for Chart Settings */
@media (max-width: 1024px) { @media (max-width: 1024px) {
.chart-card__content { .chart-card__content {
@ -899,4 +930,148 @@ input:checked + .processor-param__toggle-slider:before {
.chart-card__settings { .chart-card__settings {
max-height: 150px; max-height: 150px;
} }
}
/* Modal */
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: none;
align-items: center;
justify-content: center;
padding: var(--space-4);
}
.modal--active {
display: flex;
}
.modal__backdrop {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(4px);
cursor: pointer;
}
.modal__content {
position: relative;
background-color: var(--color-surface);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-2xl);
border: 1px solid var(--color-border);
max-width: 90vw;
max-height: 90vh;
width: 600px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.modal__content--large {
width: 1200px;
max-width: 95vw;
height: 80vh;
}
.modal__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-6);
border-bottom: 1px solid var(--color-border);
background-color: var(--color-surface-accent);
}
.modal__actions {
display: flex;
align-items: center;
gap: var(--space-2);
}
.modal__title {
display: flex;
align-items: center;
gap: var(--space-2);
margin: 0;
font-size: var(--font-size-lg);
font-weight: var(--font-weight-semibold);
color: var(--color-text);
}
.modal__close {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: var(--radius-md);
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
}
.modal__close:hover {
background-color: var(--color-surface-hover);
color: var(--color-text);
}
.modal__body {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* Plots Container */
.plots-container {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
}
.plots-content {
flex: 1;
overflow: auto;
padding: var(--space-4);
}
.plots-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
gap: var(--space-4);
min-height: 100%;
}
/* Calibration Actions */
.calibration-actions {
display: flex;
gap: var(--space-2);
margin-top: var(--space-3);
}
/* Plot Error Styles */
.plot-error {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
min-height: 200px;
color: var(--color-text-secondary);
font-style: italic;
text-align: center;
padding: var(--space-4);
background-color: var(--color-surface-accent);
border-radius: var(--radius-lg);
border: 1px dashed var(--color-border);
} }

View File

@ -161,11 +161,6 @@ class VNADashboard {
this.savePreferences(); this.savePreferences();
}); });
// Export
this.ui.onExportData(() => {
console.log('📊 Exporting chart data...');
this.exportData();
});
} }
/** /**
@ -217,37 +212,6 @@ class VNADashboard {
} }
} }
/**
* Export data functionality
*/
exportData() {
try {
const data = this.charts.exportData();
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `vna-data-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.notifications.show({
type: 'success',
title: 'Export Complete',
message: 'Chart data exported successfully'
});
} catch (error) {
console.error('❌ Export failed:', error);
this.notifications.show({
type: 'error',
title: 'Export Failed',
message: 'Failed to export chart data'
});
}
}
/** /**
* Clean up old/invalid preferences * Clean up old/invalid preferences

View File

@ -408,6 +408,23 @@ export class ChartManager {
</div> </div>
`; `;
} }
case 'button':
const buttonText = param.label || 'Click';
const actionDesc = opts.action ? `title="${opts.action}"` : '';
return `
<div class="chart-setting" data-param="${param.name}">
<button
type="button"
class="chart-setting__button"
id="${paramId}"
data-processor="${processorId}"
data-param="${param.name}"
${actionDesc}
>
${buttonText}
</button>
</div>
`;
default: default:
return ` return `
<div class="chart-setting" data-param="${param.name}"> <div class="chart-setting" data-param="${param.name}">
@ -432,8 +449,14 @@ export class ChartManager {
this.handleSettingChange(e, processorId); this.handleSettingChange(e, processorId);
}; };
const onButtonClick = (e) => {
if (!e.target.classList.contains('chart-setting__button')) return;
this.handleButtonClick(e, processorId);
};
settingsContainer.addEventListener('input', onParamChange); settingsContainer.addEventListener('input', onParamChange);
settingsContainer.addEventListener('change', onParamChange); settingsContainer.addEventListener('change', onParamChange);
settingsContainer.addEventListener('click', onButtonClick);
} }
/** /**
@ -504,6 +527,47 @@ export class ChartManager {
}, 300); // 300ms delay to prevent rapid updates }, 300); // 300ms delay to prevent rapid updates
} }
/**
* Handle button click in settings
*/
handleButtonClick(event, processorId) {
const button = event.target;
const paramName = button.dataset.param;
if (!paramName) {
console.warn('⚠️ Button missing param data:', button);
return;
}
// Prevent multiple clicks while processing
if (button.disabled) {
console.log('🔘 Button already processing, ignoring click');
return;
}
console.log(`🔘 Button clicked: ${processorId}.${paramName}`);
// Temporarily disable button and show feedback
const originalText = button.textContent;
button.disabled = true;
button.textContent = '...';
// Send button action via WebSocket (set to true to trigger action) - only once
const websocket = window.vnaDashboard?.websocket;
if (websocket && websocket.recalculate) {
console.log(`📤 Sending button action: ${processorId}.${paramName} = true`);
websocket.recalculate(processorId, { [paramName]: true });
} else {
console.warn('⚠️ WebSocket not available for button action');
}
// Re-enable button after a short delay
setTimeout(() => {
button.disabled = false;
button.textContent = originalText;
}, 1000);
}
toggleProcessor(id, enabled) { enabled ? this.showChart(id) : this.hideChart(id); } toggleProcessor(id, enabled) { enabled ? this.showChart(id) : this.hideChart(id); }
showChart(id) { showChart(id) {
const c = this.charts.get(id); const c = this.charts.get(id);
@ -541,18 +605,195 @@ export class ChartManager {
this.updateEmptyStateVisibility(); this.updateEmptyStateVisibility();
} }
downloadChart(id) { async downloadChart(id) {
const c = this.charts.get(id); const c = this.charts.get(id);
if (!c?.plotContainer) return; if (!c?.plotContainer) return;
try { try {
Plotly.downloadImage(c.plotContainer, { format: 'png', width: 1200, height: 800, filename: `${id}-${Date.now()}` }); const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
this.notifications?.show?.({ type: 'success', title: 'Download Started', message: 'Chart image download started' }); const baseFilename = `${id}_${timestamp}`;
// Download image
await Plotly.downloadImage(c.plotContainer, {
format: 'png',
width: 1200,
height: 800,
filename: `${baseFilename}_plot`
});
// Prepare and download processor data
const processorData = this.prepareProcessorDownloadData(id);
if (processorData) {
this.downloadJSON(processorData, `${baseFilename}_data.json`);
}
this.notifications?.show?.({
type: 'success',
title: 'Download Complete',
message: `Downloaded ${this.formatProcessorName(id)} plot and data`
});
} catch (e) { } catch (e) {
console.error('❌ Chart download failed:', e); console.error('❌ Chart download failed:', e);
this.notifications?.show?.({ type: 'error', title: 'Download Failed', message: 'Failed to download chart image' }); this.notifications?.show?.({
type: 'error',
title: 'Download Failed',
message: 'Failed to download chart data'
});
} }
} }
prepareProcessorDownloadData(processorId) {
const chart = this.charts.get(processorId);
const latestData = this.chartData.get(processorId);
if (!chart || !latestData) return null;
// Safe copy function to avoid circular references
const safeClone = (obj, seen = new WeakSet()) => {
if (obj === null || typeof obj !== 'object') return obj;
if (seen.has(obj)) return '[Circular Reference]';
seen.add(obj);
if (Array.isArray(obj)) {
return obj.map(item => safeClone(item, seen));
}
const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
try {
cloned[key] = safeClone(obj[key], seen);
} catch (e) {
cloned[key] = `[Error: ${e.message}]`;
}
}
}
return cloned;
};
return {
processor_info: {
processor_id: processorId,
processor_name: this.formatProcessorName(processorId),
download_timestamp: new Date().toISOString(),
is_visible: chart.isVisible
},
current_data: {
data: safeClone(latestData.data),
metadata: safeClone(latestData.metadata),
timestamp: latestData.timestamp instanceof Date ? latestData.timestamp.toISOString() : latestData.timestamp,
plotly_config: safeClone(latestData.plotly_config)
},
plot_config: this.getCurrentPlotlyDataSafe(processorId),
ui_parameters: safeClone(this.getProcessorSettings(processorId)),
raw_sweep_data: this.extractProcessorRawData(latestData),
metadata: {
description: `VNA processor data export - ${this.formatProcessorName(processorId)}`,
format_version: "1.0",
exported_by: "VNA System Dashboard",
export_type: "processor_data",
contains: [
"Current processor data and metadata",
"Plot configuration (Plotly format)",
"UI parameter settings",
"Raw measurement data if available",
"Processing results and statistics"
]
}
};
}
extractProcessorRawData(latestData) {
// Extract raw data from processor results
if (!latestData || !latestData.data) return null;
try {
const rawData = {
processor_results: this.safeStringify(latestData.data),
metadata: this.safeStringify(latestData.metadata)
};
// If this is magnitude processor, extract frequency/magnitude data
if (latestData.plotly_config && latestData.plotly_config.data) {
const plotData = latestData.plotly_config.data[0];
if (plotData && plotData.x && plotData.y) {
rawData.processed_measurements = [];
const maxPoints = Math.min(plotData.x.length, plotData.y.length, 1000); // Limit to 1000 points
for (let i = 0; i < maxPoints; i++) {
rawData.processed_measurements.push({
point_index: i,
x_value: plotData.x[i],
y_value: plotData.y[i],
x_unit: this.getAxisUnit(plotData, 'x'),
y_unit: this.getAxisUnit(plotData, 'y')
});
}
}
}
return rawData;
} catch (error) {
console.warn('⚠️ Error extracting processor raw data:', error);
return { error: 'Failed to extract raw data' };
}
}
safeStringify(obj) {
try {
// Simple objects - try direct JSON
return JSON.parse(JSON.stringify(obj));
} catch (e) {
// If that fails, create a safe representation
if (obj === null || typeof obj !== 'object') return obj;
if (Array.isArray(obj)) return obj.slice(0, 100); // Limit array size
const safe = {};
Object.keys(obj).forEach(key => {
try {
if (typeof obj[key] === 'function') {
safe[key] = '[Function]';
} else if (typeof obj[key] === 'object') {
safe[key] = '[Object]';
} else {
safe[key] = obj[key];
}
} catch (err) {
safe[key] = '[Error accessing property]';
}
});
return safe;
}
}
getAxisUnit(plotData, axis) {
// Try to determine units from plot data or layout
if (axis === 'x') {
// For frequency data, usually GHz
return plotData.name && plotData.name.includes('Frequency') ? 'GHz' : 'unknown';
} else if (axis === 'y') {
// For magnitude data, usually dB
return plotData.name && plotData.name.includes('Magnitude') ? 'dB' : 'unknown';
}
return 'unknown';
}
downloadJSON(data, filename) {
const jsonString = JSON.stringify(data, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Clean up the URL object
URL.revokeObjectURL(url);
}
toggleFullscreen(id) { toggleFullscreen(id) {
const c = this.charts.get(id); const c = this.charts.get(id);
if (!c?.element) return; if (!c?.element) return;
@ -592,41 +833,6 @@ export class ChartManager {
pause() { this.isPaused = true; console.log('⏸️ Chart updates paused'); } pause() { this.isPaused = true; console.log('⏸️ Chart updates paused'); }
resume() { this.isPaused = false; console.log('▶️ Chart updates resumed'); if (this.updateQueue.size) this.processUpdateQueue(); } resume() { this.isPaused = false; console.log('▶️ Chart updates resumed'); if (this.updateQueue.size) this.processUpdateQueue(); }
exportData() {
const exportData = {
timestamp: new Date().toISOString(),
charts: {},
stats: this.performanceStats,
description: "Current chart data snapshot - latest data from each visible processor"
};
// Export only the latest data from each visible chart (not all historical data)
for (const [processorId, chart] of this.charts) {
// Skip hidden/disabled processors
if (!chart.isVisible || this.disabledProcessors.has(processorId)) {
continue;
}
const latestData = this.chartData.get(processorId);
if (!latestData) {
continue;
}
exportData.charts[processorId] = {
processor_name: this.formatProcessorName(processorId),
current_data: {
data: latestData.data,
metadata: latestData.metadata,
timestamp: latestData.timestamp.toISOString()
},
plotly_config: this.getCurrentPlotlyData(processorId),
settings: this.getProcessorSettings(processorId)
};
}
console.log(`📊 Exporting current data for ${Object.keys(exportData.charts).length} visible charts`);
return exportData;
}
/** /**
* Get current Plotly data/layout for a processor * Get current Plotly data/layout for a processor
@ -654,6 +860,60 @@ export class ChartManager {
} }
} }
/**
* Safe version of getCurrentPlotlyData that avoids circular references
*/
getCurrentPlotlyDataSafe(processorId) {
const chart = this.charts.get(processorId);
if (!chart?.plotContainer?._fullData || !chart?.plotContainer?._fullLayout) {
return null;
}
try {
// Extract only essential plot data to avoid circular references
const data = [];
if (chart.plotContainer._fullData) {
chart.plotContainer._fullData.forEach(trace => {
data.push({
x: trace.x ? Array.from(trace.x) : null,
y: trace.y ? Array.from(trace.y) : null,
type: trace.type,
mode: trace.mode,
name: trace.name,
line: trace.line ? {
color: trace.line.color,
width: trace.line.width
} : null,
marker: trace.marker ? {
color: trace.marker.color,
size: trace.marker.size
} : null
});
});
}
const layout = {};
if (chart.plotContainer._fullLayout) {
const fullLayout = chart.plotContainer._fullLayout;
layout.title = typeof fullLayout.title === 'string' ? fullLayout.title : fullLayout.title?.text;
layout.xaxis = {
title: fullLayout.xaxis?.title,
range: fullLayout.xaxis?.range
};
layout.yaxis = {
title: fullLayout.yaxis?.title,
range: fullLayout.yaxis?.range
};
layout.showlegend = fullLayout.showlegend;
}
return { data, layout };
} catch (error) {
console.warn(`⚠️ Could not extract safe Plotly data for ${processorId}:`, error);
return null;
}
}
/** /**
* Get current processor settings (UI parameters) * Get current processor settings (UI parameters)
*/ */

View File

@ -29,6 +29,11 @@ export class SettingsManager {
this.handleSaveCalibration = this.handleSaveCalibration.bind(this); this.handleSaveCalibration = this.handleSaveCalibration.bind(this);
this.handleSetCalibration = this.handleSetCalibration.bind(this); this.handleSetCalibration = this.handleSetCalibration.bind(this);
this.handleCalibrationChange = this.handleCalibrationChange.bind(this); this.handleCalibrationChange = this.handleCalibrationChange.bind(this);
this.handleViewPlots = this.handleViewPlots.bind(this);
this.handleViewCurrentPlots = this.handleViewCurrentPlots.bind(this);
// Store current plots data for download
this.currentPlotsData = null;
} }
async init() { async init() {
@ -73,6 +78,13 @@ export class SettingsManager {
saveCalibrationBtn: document.getElementById('saveCalibrationBtn'), saveCalibrationBtn: document.getElementById('saveCalibrationBtn'),
calibrationDropdown: document.getElementById('calibrationDropdown'), calibrationDropdown: document.getElementById('calibrationDropdown'),
setCalibrationBtn: document.getElementById('setCalibrationBtn'), setCalibrationBtn: document.getElementById('setCalibrationBtn'),
viewPlotsBtn: document.getElementById('viewPlotsBtn'),
viewCurrentPlotsBtn: document.getElementById('viewCurrentPlotsBtn'),
// Modal elements
plotsModal: document.getElementById('plotsModal'),
plotsGrid: document.getElementById('plotsGrid'),
downloadAllBtn: document.getElementById('downloadAllBtn'),
// Status elements // Status elements
presetCount: document.getElementById('presetCount'), presetCount: document.getElementById('presetCount'),
@ -91,6 +103,8 @@ export class SettingsManager {
this.elements.saveCalibrationBtn?.addEventListener('click', this.handleSaveCalibration); this.elements.saveCalibrationBtn?.addEventListener('click', this.handleSaveCalibration);
this.elements.calibrationDropdown?.addEventListener('change', this.handleCalibrationChange); this.elements.calibrationDropdown?.addEventListener('change', this.handleCalibrationChange);
this.elements.setCalibrationBtn?.addEventListener('click', this.handleSetCalibration); this.elements.setCalibrationBtn?.addEventListener('click', this.handleSetCalibration);
this.elements.viewPlotsBtn?.addEventListener('click', this.handleViewPlots);
this.elements.viewCurrentPlotsBtn?.addEventListener('click', this.handleViewCurrentPlots);
// Calibration name input // Calibration name input
this.elements.calibrationNameInput?.addEventListener('input', () => { this.elements.calibrationNameInput?.addEventListener('input', () => {
@ -211,6 +225,7 @@ export class SettingsManager {
dropdown.disabled = false; dropdown.disabled = false;
this.elements.setCalibrationBtn.disabled = true; this.elements.setCalibrationBtn.disabled = true;
this.elements.viewPlotsBtn.disabled = true;
} }
formatPresetDisplay(preset) { formatPresetDisplay(preset) {
@ -280,11 +295,22 @@ export class SettingsManager {
const hasName = this.elements.calibrationNameInput.value.trim().length > 0; const hasName = this.elements.calibrationNameInput.value.trim().length > 0;
this.elements.saveCalibrationBtn.disabled = !hasName || !workingCalibration.is_complete; this.elements.saveCalibrationBtn.disabled = !hasName || !workingCalibration.is_complete;
this.elements.calibrationNameInput.disabled = false; this.elements.calibrationNameInput.disabled = false;
// Enable "View Current Plots" button if there are any completed standards
const hasCompletedStandards = workingCalibration.completed_standards && workingCalibration.completed_standards.length > 0;
if (this.elements.viewCurrentPlotsBtn) {
this.elements.viewCurrentPlotsBtn.disabled = !hasCompletedStandards;
}
} }
hideCalibrationSteps() { hideCalibrationSteps() {
this.elements.calibrationSteps.style.display = 'none'; this.elements.calibrationSteps.style.display = 'none';
this.elements.calibrationStandards.innerHTML = ''; this.elements.calibrationStandards.innerHTML = '';
// Disable "View Current Plots" button
if (this.elements.viewCurrentPlotsBtn) {
this.elements.viewCurrentPlotsBtn.disabled = true;
}
} }
resetCalibrationState() { resetCalibrationState() {
@ -592,6 +618,7 @@ export class SettingsManager {
handleCalibrationChange() { handleCalibrationChange() {
const selectedValue = this.elements.calibrationDropdown.value; const selectedValue = this.elements.calibrationDropdown.value;
this.elements.setCalibrationBtn.disabled = !selectedValue; this.elements.setCalibrationBtn.disabled = !selectedValue;
this.elements.viewPlotsBtn.disabled = !selectedValue;
} }
async handleSetCalibration() { async handleSetCalibration() {
@ -638,6 +665,629 @@ export class SettingsManager {
} }
} }
async handleViewPlots() {
const calibrationName = this.elements.calibrationDropdown.value;
if (!calibrationName || !this.currentPreset) return;
try {
this.elements.viewPlotsBtn.disabled = true;
this.elements.viewPlotsBtn.innerHTML = '<i data-lucide="loader"></i> Loading...';
// Fetch plots data
const response = await fetch(`/api/v1/settings/calibration/${encodeURIComponent(calibrationName)}/standards-plots?preset_filename=${encodeURIComponent(this.currentPreset.filename)}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const plotsData = await response.json();
// Show modal with plots
this.showPlotsModal(plotsData);
} catch (error) {
console.error('Failed to load calibration plots:', error);
this.notifications.show({
type: 'error',
title: 'Plots Error',
message: 'Failed to load calibration plots'
});
} finally {
this.elements.viewPlotsBtn.disabled = false;
this.elements.viewPlotsBtn.innerHTML = '<i data-lucide="bar-chart-3"></i> View Plots';
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
async handleViewCurrentPlots() {
if (!this.workingCalibration || !this.workingCalibration.active) return;
try {
this.elements.viewCurrentPlotsBtn.disabled = true;
this.elements.viewCurrentPlotsBtn.innerHTML = '<i data-lucide="loader"></i> Loading...';
// Fetch current working calibration plots
const response = await fetch('/api/v1/settings/working-calibration/standards-plots');
if (!response.ok) {
if (response.status === 404) {
this.notifications.show({
type: 'warning',
title: 'No Data',
message: 'No working calibration or standards available to plot'
});
return;
}
throw new Error(`HTTP ${response.status}`);
}
const plotsData = await response.json();
// Show modal with plots
this.showPlotsModal(plotsData);
} catch (error) {
console.error('Failed to load current calibration plots:', error);
this.notifications.show({
type: 'error',
title: 'Plots Error',
message: 'Failed to load current calibration plots'
});
} finally {
this.elements.viewCurrentPlotsBtn.disabled = false;
this.elements.viewCurrentPlotsBtn.innerHTML = '<i data-lucide="bar-chart-3"></i> View Current Plots';
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
showPlotsModal(plotsData) {
const modal = this.elements.plotsModal;
if (!modal) return;
// Setup modal close handlers
this.setupModalCloseHandlers(modal);
// Store plots data for download
this.currentPlotsData = plotsData;
// Render plots using chart manager style
this.renderCalibrationPlots(plotsData.individual_plots, plotsData.preset);
// Show modal
modal.classList.add('modal--active');
document.body.style.overflow = 'hidden';
// Update modal title
const title = modal.querySelector('.modal__title');
if (title) {
title.innerHTML = `
<i data-lucide="bar-chart-3"></i>
${plotsData.calibration_name} - ${plotsData.preset.mode.toUpperCase()} Standards
`;
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
setupModalCloseHandlers(modal) {
const closeElements = modal.querySelectorAll('[data-modal-close]');
closeElements.forEach(element => {
element.addEventListener('click', () => this.closePlotsModal());
});
// Setup download all button
const downloadAllBtn = modal.querySelector('#downloadAllBtn');
if (downloadAllBtn) {
downloadAllBtn.addEventListener('click', () => this.downloadAllCalibrationData());
}
// Close on Escape key
const escapeHandler = (e) => {
if (e.key === 'Escape') {
this.closePlotsModal();
document.removeEventListener('keydown', escapeHandler);
}
};
document.addEventListener('keydown', escapeHandler);
}
renderCalibrationPlots(individualPlots, preset) {
const container = this.elements.plotsGrid;
if (!container) return;
container.innerHTML = '';
if (!individualPlots || Object.keys(individualPlots).length === 0) {
container.innerHTML = '<div class="plot-error">No calibration plots available</div>';
return;
}
Object.entries(individualPlots).forEach(([standardName, plotConfig]) => {
if (plotConfig.error) {
const errorDiv = document.createElement('div');
errorDiv.className = 'chart-card';
errorDiv.innerHTML = `
<div class="chart-card__header">
<div class="chart-card__title">
<i data-lucide="alert-circle" class="chart-card__icon"></i>
${standardName.toUpperCase()} Standard
</div>
</div>
<div class="chart-card__content">
<div class="plot-error">Error: ${plotConfig.error}</div>
</div>
`;
container.appendChild(errorDiv);
return;
}
const card = this.createCalibrationChartCard(standardName, plotConfig, preset);
container.appendChild(card);
});
if (typeof lucide !== 'undefined') {
lucide.createIcons({ attrs: { 'stroke-width': 1.5 } });
}
}
createCalibrationChartCard(standardName, plotConfig, preset) {
const card = document.createElement('div');
card.className = 'chart-card';
card.dataset.standard = standardName;
const standardTitle = `${standardName.toUpperCase()} Standard`;
card.innerHTML = `
<div class="chart-card__header">
<div class="chart-card__title">
<i data-lucide="bar-chart-3" class="chart-card__icon"></i>
${standardTitle}
</div>
<div class="chart-card__actions">
<button class="chart-card__action" data-action="fullscreen" title="Fullscreen">
<i data-lucide="expand"></i>
</button>
<button class="chart-card__action" data-action="download" title="Download">
<i data-lucide="download"></i>
</button>
</div>
</div>
<div class="chart-card__content">
<div class="chart-card__plot" id="calibration-plot-${standardName}"></div>
</div>
<div class="chart-card__meta">
<div class="chart-card__timestamp">Standard: ${standardName.toUpperCase()}</div>
<div class="chart-card__sweep">Preset: ${preset?.filename || 'Unknown'}</div>
</div>
`;
// Setup event handlers
this.setupCalibrationChartEvents(card, standardName);
// Render the plot
const plotContainer = card.querySelector('.chart-card__plot');
this.renderCalibrationChart(plotContainer, plotConfig, standardTitle);
return card;
}
setupCalibrationChartEvents(card, standardName) {
card.addEventListener('click', (e) => {
const action = e.target.closest('[data-action]')?.dataset.action;
if (!action) return;
e.stopPropagation();
const plotContainer = card.querySelector('.chart-card__plot');
switch (action) {
case 'fullscreen':
this.toggleCalibrationFullscreen(card);
break;
case 'download':
this.downloadCalibrationStandard(standardName, plotContainer);
break;
}
});
}
renderCalibrationChart(container, plotConfig, title) {
if (!container || !plotConfig || plotConfig.error) {
container.innerHTML = `<div class="plot-error">Failed to load plot: ${plotConfig?.error || 'Unknown error'}</div>`;
return;
}
const layout = {
...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
}
};
const config = {
displayModeBar: true,
modeBarButtonsToRemove: ['select2d','lasso2d','hoverClosestCartesian','hoverCompareCartesian','toggleSpikelines'],
displaylogo: false,
responsive: false,
doubleClick: 'reset',
toImageButtonOptions: {
format: 'png',
filename: `calibration-plot-${Date.now()}`,
height: 600,
width: 800,
scale: 1
}
};
Plotly.newPlot(container, plotConfig.data, layout, config);
// Setup resize observer
if (window.ResizeObserver) {
const ro = new ResizeObserver(() => {
if (container && container.clientWidth > 0) {
Plotly.Plots.resize(container);
}
});
ro.observe(container);
container._resizeObserver = ro;
}
}
toggleCalibrationFullscreen(card) {
if (!document.fullscreenElement) {
card.requestFullscreen()?.then(() => {
setTimeout(() => {
const plotContainer = card.querySelector('.chart-card__plot');
if (plotContainer && typeof Plotly !== 'undefined') {
const rect = plotContainer.getBoundingClientRect();
Plotly.relayout(plotContainer, { width: rect.width, height: rect.height });
Plotly.Plots.resize(plotContainer);
}
}, 200);
}).catch(console.error);
} else {
document.exitFullscreen()?.then(() => {
setTimeout(() => {
const plotContainer = card.querySelector('.chart-card__plot');
if (plotContainer && typeof Plotly !== 'undefined') {
Plotly.Plots.resize(plotContainer);
}
}, 100);
});
}
}
async downloadCalibrationStandard(standardName, plotContainer) {
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const calibrationName = this.currentPlotsData?.calibration_name || 'unknown';
const presetName = this.currentPlotsData?.preset?.filename || 'unknown';
const baseFilename = `${calibrationName}_${standardName}_${timestamp}`;
// Download image
if (plotContainer && typeof Plotly !== 'undefined') {
await Plotly.downloadImage(plotContainer, {
format: 'png',
width: 1200,
height: 800,
filename: `${baseFilename}_plot`
});
}
// Prepare calibration data for download
const calibrationData = this.prepareCalibrationDownloadData(standardName);
// Download data as JSON
this.downloadJSON(calibrationData, `${baseFilename}_data.json`);
// Show success notification
this.notifications.show({
type: 'success',
title: 'Download Complete',
message: `Downloaded ${standardName.toUpperCase()} standard plot and data`
});
} catch (error) {
console.error('Failed to download calibration standard:', error);
this.notifications.show({
type: 'error',
title: 'Download Failed',
message: 'Failed to download calibration data'
});
}
}
prepareCalibrationDownloadData(standardName) {
if (!this.currentPlotsData) return null;
const standardPlot = this.currentPlotsData.individual_plots[standardName];
return {
calibration_info: {
calibration_name: this.currentPlotsData.calibration_name,
preset: this.currentPlotsData.preset,
standard_name: standardName,
download_timestamp: new Date().toISOString()
},
plot_data: standardPlot ? {
data: standardPlot.data,
layout: standardPlot.layout,
error: standardPlot.error
} : null,
raw_sweep_data: this.extractRawSweepData(standardName),
metadata: {
description: `VNA calibration standard data export - ${standardName.toUpperCase()}`,
format_version: "1.0",
exported_by: "VNA System Dashboard",
contains: [
"Calibration information",
"Plot configuration (Plotly format)",
"Raw sweep measurements",
"Frequency and magnitude data"
]
}
};
}
extractRawSweepData(standardName) {
const standardPlot = this.currentPlotsData?.individual_plots?.[standardName];
if (!standardPlot || !standardPlot.raw_sweep_data) {
return null;
}
const rawData = standardPlot.raw_sweep_data;
const frequencyInfo = standardPlot.frequency_info;
// Process raw complex data points into detailed format
const processedPoints = [];
if (rawData.points && rawData.points.length > 0) {
for (let i = 0; i < rawData.points.length; i++) {
const [real, imag] = rawData.points[i];
const magnitude_linear = Math.sqrt(real * real + imag * imag);
const magnitude_db = magnitude_linear > 0 ? 20 * Math.log10(magnitude_linear) : -120;
const phase_radians = Math.atan2(imag, real);
const phase_degrees = phase_radians * (180 / Math.PI);
// Calculate frequency
let frequency_hz = 0;
if (frequencyInfo && frequencyInfo.start_freq && frequencyInfo.stop_freq) {
frequency_hz = frequencyInfo.start_freq +
(frequencyInfo.stop_freq - frequencyInfo.start_freq) * i / (rawData.points.length - 1);
}
processedPoints.push({
point_index: i,
frequency_hz: frequency_hz,
frequency_ghz: frequency_hz / 1e9,
complex_data: {
real: real,
imaginary: imag
},
magnitude: {
linear: magnitude_linear,
db: magnitude_db
},
phase: {
radians: phase_radians,
degrees: phase_degrees
}
});
}
}
return {
standard_name: standardName,
sweep_info: {
sweep_number: rawData.sweep_number,
timestamp: rawData.timestamp,
total_points: rawData.total_points,
file_path: rawData.file_path
},
frequency_info: frequencyInfo,
measurement_points: processedPoints,
statistics: {
total_points: processedPoints.length,
frequency_range: {
start_hz: processedPoints[0]?.frequency_hz || 0,
stop_hz: processedPoints[processedPoints.length - 1]?.frequency_hz || 0
},
magnitude_range_db: {
min: Math.min(...processedPoints.map(p => p.magnitude.db)),
max: Math.max(...processedPoints.map(p => p.magnitude.db))
}
}
};
}
async downloadAllCalibrationData() {
if (!this.currentPlotsData) return;
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const calibrationName = this.currentPlotsData.calibration_name || 'unknown';
const baseFilename = `${calibrationName}_complete_${timestamp}`;
// Show loading state
const downloadAllBtn = this.elements.downloadAllBtn;
if (downloadAllBtn) {
downloadAllBtn.disabled = true;
downloadAllBtn.innerHTML = '<i data-lucide="loader"></i> Downloading...';
if (typeof lucide !== 'undefined') lucide.createIcons();
}
// Prepare complete calibration export
const completeData = this.prepareCompleteCalibrationData();
// Download complete data as JSON
this.downloadJSON(completeData, `${baseFilename}.json`);
// Also download individual plots as images
await this.downloadAllPlotImages(baseFilename);
// Show success notification
this.notifications.show({
type: 'success',
title: 'Complete Download',
message: `Downloaded complete calibration data and all plots for ${calibrationName}`
});
} catch (error) {
console.error('Failed to download complete calibration data:', error);
this.notifications.show({
type: 'error',
title: 'Download Failed',
message: 'Failed to download complete calibration data'
});
} finally {
// Restore button state
const downloadAllBtn = this.elements.downloadAllBtn;
if (downloadAllBtn) {
downloadAllBtn.disabled = false;
downloadAllBtn.innerHTML = '<i data-lucide="download-cloud"></i> Download All';
if (typeof lucide !== 'undefined') lucide.createIcons();
}
}
}
prepareCompleteCalibrationData() {
if (!this.currentPlotsData) return null;
const allStandardsData = {};
Object.keys(this.currentPlotsData.individual_plots).forEach(standardName => {
allStandardsData[standardName] = this.prepareCalibrationDownloadData(standardName);
});
return {
export_info: {
export_timestamp: new Date().toISOString(),
export_type: "complete_calibration",
calibration_name: this.currentPlotsData.calibration_name,
preset: this.currentPlotsData.preset,
standards_included: Object.keys(allStandardsData),
format_version: "1.0"
},
calibration_summary: {
name: this.currentPlotsData.calibration_name,
preset: this.currentPlotsData.preset,
total_standards: Object.keys(allStandardsData).length,
standards: Object.keys(allStandardsData).map(name => ({
name: name,
has_data: allStandardsData[name] !== null,
has_error: allStandardsData[name]?.plot_data?.error ? true : false
}))
},
standards_data: allStandardsData,
metadata: {
description: "Complete VNA calibration data export including all standards",
exported_by: "VNA System Dashboard",
contains: [
"Complete calibration information",
"All calibration standards data",
"Raw sweep measurements for each standard",
"Plot configurations (Plotly format)",
"Frequency and magnitude data",
"Complex impedance measurements"
],
usage_notes: [
"This file contains all data for the calibration set",
"Individual standard data is available in the 'standards_data' section",
"Raw complex measurements are in [real, imaginary] format",
"Frequencies are provided in both Hz and GHz",
"Magnitudes are provided in both linear and dB scales"
]
}
};
}
async downloadAllPlotImages(baseFilename) {
const plotContainers = this.elements.plotsModal.querySelectorAll('[id^="calibration-plot-"]');
const downloadPromises = [];
plotContainers.forEach(container => {
if (container && typeof Plotly !== 'undefined' && container._fullData) {
const standardName = container.id.replace('calibration-plot-', '');
const promise = Plotly.downloadImage(container, {
format: 'png',
width: 1200,
height: 800,
filename: `${baseFilename}_${standardName}_plot`
});
downloadPromises.push(promise);
}
});
// Wait for all image downloads to complete
await Promise.all(downloadPromises);
}
downloadJSON(data, filename) {
const jsonString = JSON.stringify(data, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// Clean up the URL object
URL.revokeObjectURL(url);
}
closePlotsModal() {
const modal = this.elements.plotsModal;
if (!modal) return;
modal.classList.remove('modal--active');
document.body.style.overflow = '';
// Clean up plots and resize observers
if (typeof Plotly !== 'undefined') {
const plotContainers = modal.querySelectorAll('[id^="calibration-plot-"]');
plotContainers.forEach(container => {
if (container._resizeObserver) {
container._resizeObserver.disconnect();
container._resizeObserver = null;
}
if (container._fullData) {
Plotly.purge(container);
}
});
}
// Clear plots data
this.currentPlotsData = null;
}
// Public methods for external use // Public methods for external use
async refresh() { async refresh() {
if (!this.isInitialized) return; if (!this.isInitialized) return;

View File

@ -15,7 +15,6 @@ export class UIManager {
processorToggles: null, processorToggles: null,
sweepCount: null, sweepCount: null,
dataRate: null, dataRate: null,
exportBtn: null,
navButtons: null, navButtons: null,
views: null views: null
}; };
@ -30,7 +29,6 @@ export class UIManager {
this.eventHandlers = { this.eventHandlers = {
viewChange: [], viewChange: [],
processorToggle: [], processorToggle: [],
exportData: []
}; };
} }
@ -70,12 +68,11 @@ export class UIManager {
findElements() { findElements() {
this.elements.connectionStatus = document.getElementById('connectionStatus'); this.elements.connectionStatus = document.getElementById('connectionStatus');
this.elements.processorToggles = document.getElementById('processorToggles'); this.elements.processorToggles = document.getElementById('processorToggles');
this.elements.exportBtn = document.getElementById('exportBtn');
this.elements.navButtons = document.querySelectorAll('.nav-btn[data-view]'); this.elements.navButtons = document.querySelectorAll('.nav-btn[data-view]');
this.elements.views = document.querySelectorAll('.view'); this.elements.views = document.querySelectorAll('.view');
// Validate required elements // Validate required elements
const required = ['connectionStatus', 'processorToggles', 'exportBtn']; const required = ['connectionStatus', 'processorToggles'];
for (const key of required) { for (const key of required) {
if (!this.elements[key]) { if (!this.elements[key]) {
throw new Error(`Required UI element not found: ${key}`); throw new Error(`Required UI element not found: ${key}`);
@ -95,11 +92,6 @@ export class UIManager {
}); });
}); });
// Export button
this.elements.exportBtn.addEventListener('click', () => {
this.triggerExportData();
});
// Processor toggles container (event delegation) // Processor toggles container (event delegation)
this.elements.processorToggles.addEventListener('click', (e) => { this.elements.processorToggles.addEventListener('click', (e) => {
const toggle = e.target.closest('.processor-toggle'); const toggle = e.target.closest('.processor-toggle');
@ -402,10 +394,6 @@ export class UIManager {
.join(' '); .join(' ');
} }
triggerExportData() {
this.emitEvent('exportData');
}
handleResize() { handleResize() {
console.log('📱 Window resized'); console.log('📱 Window resized');
// Charts handle their own resize // Charts handle their own resize
@ -419,7 +407,6 @@ export class UIManager {
// Events // Events
onViewChange(callback) { this.eventHandlers.viewChange.push(callback); } onViewChange(callback) { this.eventHandlers.viewChange.push(callback); }
onProcessorToggle(callback) { this.eventHandlers.processorToggle.push(callback); } onProcessorToggle(callback) { this.eventHandlers.processorToggle.push(callback); }
onExportData(callback) { this.eventHandlers.exportData.push(callback); }
emitEvent(eventType, ...args) { emitEvent(eventType, ...args) {
if (this.eventHandlers[eventType]) { if (this.eventHandlers[eventType]) {

View File

@ -107,15 +107,6 @@
</div> </div>
</div> </div>
<div class="controls-group">
<label class="controls-label">Actions</label>
<div class="display-controls">
<button class="btn btn--primary btn--bordered" id="exportBtn">
<i data-lucide="download"></i>
Export Data
</button>
</div>
</div>
</div> </div>
</div> </div>
@ -219,6 +210,10 @@
</div> </div>
<div class="calibration-actions"> <div class="calibration-actions">
<button class="btn btn--secondary" id="viewCurrentPlotsBtn" disabled>
<i data-lucide="bar-chart-3"></i>
View Current Plots
</button>
<input type="text" class="calibration-name-input" id="calibrationNameInput" placeholder="Enter calibration name" disabled> <input type="text" class="calibration-name-input" id="calibrationNameInput" placeholder="Enter calibration name" disabled>
<button class="btn btn--success" id="saveCalibrationBtn" disabled> <button class="btn btn--success" id="saveCalibrationBtn" disabled>
<i data-lucide="save"></i> <i data-lucide="save"></i>
@ -234,10 +229,16 @@
<select class="calibration-dropdown" id="calibrationDropdown" disabled> <select class="calibration-dropdown" id="calibrationDropdown" disabled>
<option value="">No calibrations available</option> <option value="">No calibrations available</option>
</select> </select>
<button class="btn btn--primary" id="setCalibrationBtn" disabled> <div class="calibration-actions">
<i data-lucide="check"></i> <button class="btn btn--primary" id="setCalibrationBtn" disabled>
Set Active <i data-lucide="check"></i>
</button> Set Active
</button>
<button class="btn btn--secondary" id="viewPlotsBtn" disabled>
<i data-lucide="bar-chart-3"></i>
View Plots
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -266,6 +267,37 @@
</div> </div>
</main> </main>
<!-- Calibration Plots Modal -->
<div class="modal" id="plotsModal">
<div class="modal__backdrop" data-modal-close></div>
<div class="modal__content modal__content--large">
<div class="modal__header">
<h2 class="modal__title">
<i data-lucide="bar-chart-3"></i>
Calibration Standards Plots
</h2>
<div class="modal__actions">
<button class="btn btn--secondary btn--sm" id="downloadAllBtn" title="Download all calibration data">
<i data-lucide="download-cloud"></i>
Download All
</button>
<button class="modal__close" data-modal-close title="Close">
<i data-lucide="x"></i>
</button>
</div>
</div>
<div class="modal__body" id="plotsModalBody">
<div class="plots-container">
<div class="plots-content">
<div class="plots-grid" id="plotsGrid">
<!-- Individual plots will be populated here -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Notification System --> <!-- Notification System -->
<div class="notifications" id="notifications"></div> <div class="notifications" id="notifications"></div>