added functionality, save buttons etc
This commit is contained in:
@ -1,8 +1,10 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
|
||||
import vna_system.core.singletons as singletons
|
||||
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 (
|
||||
PresetModel,
|
||||
CalibrationModel,
|
||||
@ -299,3 +301,118 @@ async def get_current_calibration():
|
||||
}
|
||||
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:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
@ -1 +1 @@
|
||||
config_inputs/s21_start100_stop8800_points1000_bw1khz.bin
|
||||
config_inputs/s11_start100_stop8800_points1000_bw1khz.bin
|
||||
1
vna_system/calibration/current_calibration
Symbolic link
1
vna_system/calibration/current_calibration
Symbolic link
@ -0,0 +1 @@
|
||||
s11_start100_stop8800_points1000_bw1khz/lol
|
||||
@ -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
@ -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
@ -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
@ -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
|
||||
}
|
||||
@ -13,7 +13,7 @@ from vna_system.core.settings.preset_manager import ConfigPreset
|
||||
class UIParameter:
|
||||
name: str
|
||||
label: str
|
||||
type: str # 'slider', 'toggle', 'select', 'input'
|
||||
type: str # 'slider', 'toggle', 'select', 'input', 'button'
|
||||
value: Any
|
||||
options: Optional[Dict[str, Any]] = None # min/max for slider, choices for select, etc.
|
||||
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
{
|
||||
"y_min": -110,
|
||||
"y_max": 10,
|
||||
"y_min": -80,
|
||||
"y_max": 15,
|
||||
"smoothing_enabled": false,
|
||||
"smoothing_window": 17,
|
||||
"smoothing_window": 5,
|
||||
"marker_enabled": true,
|
||||
"marker_frequency": 1,
|
||||
"grid_enabled": false
|
||||
"marker_frequency": 100000009,
|
||||
"grid_enabled": true
|
||||
}
|
||||
@ -2,11 +2,11 @@
|
||||
"y_min": -210,
|
||||
"y_max": 360,
|
||||
"unwrap_phase": false,
|
||||
"phase_offset": -60,
|
||||
"phase_offset": 0,
|
||||
"smoothing_enabled": true,
|
||||
"smoothing_window": 5,
|
||||
"marker_enabled": false,
|
||||
"marker_frequency": 2000000000,
|
||||
"marker_frequency": "asdasd",
|
||||
"reference_line_enabled": false,
|
||||
"reference_phase": 0,
|
||||
"grid_enabled": true
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
{
|
||||
"impedance_mode": true,
|
||||
"impedance_mode": false,
|
||||
"reference_impedance": 100,
|
||||
"marker_enabled": true,
|
||||
"marker_enabled": false,
|
||||
"marker_frequency": 1000000000,
|
||||
"grid_circles": true,
|
||||
"grid_radials": true,
|
||||
"grid_circles": false,
|
||||
"grid_radials": false,
|
||||
"trace_color_mode": "solid"
|
||||
}
|
||||
@ -8,6 +8,8 @@ from ..base_processor import BaseProcessor, UIParameter
|
||||
class MagnitudeProcessor(BaseProcessor):
|
||||
def __init__(self, config_dir: Path):
|
||||
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]:
|
||||
if not calibrated_data or not hasattr(calibrated_data, 'points'):
|
||||
@ -141,6 +143,13 @@ class MagnitudeProcessor(BaseProcessor):
|
||||
label='Show Grid',
|
||||
type='toggle',
|
||||
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
|
||||
}
|
||||
|
||||
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):
|
||||
required_keys = ['y_min', 'y_max', 'smoothing_enabled', 'smoothing_window',
|
||||
'marker_enabled', 'marker_frequency', 'grid_enabled']
|
||||
|
||||
264
vna_system/core/visualization/magnitude_chart.py
Normal file
264
vna_system/core/visualization/magnitude_chart.py
Normal 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}
|
||||
}
|
||||
}
|
||||
@ -866,6 +866,37 @@ input:checked + .processor-param__toggle-slider:before {
|
||||
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 */
|
||||
@media (max-width: 1024px) {
|
||||
.chart-card__content {
|
||||
@ -900,3 +931,147 @@ input:checked + .processor-param__toggle-slider:before {
|
||||
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);
|
||||
}
|
||||
@ -161,11 +161,6 @@ class VNADashboard {
|
||||
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
|
||||
|
||||
@ -408,6 +408,23 @@ export class ChartManager {
|
||||
</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:
|
||||
return `
|
||||
<div class="chart-setting" data-param="${param.name}">
|
||||
@ -432,8 +449,14 @@ export class ChartManager {
|
||||
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('change', onParamChange);
|
||||
settingsContainer.addEventListener('click', onButtonClick);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -504,6 +527,47 @@ export class ChartManager {
|
||||
}, 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); }
|
||||
showChart(id) {
|
||||
const c = this.charts.get(id);
|
||||
@ -541,18 +605,195 @@ export class ChartManager {
|
||||
this.updateEmptyStateVisibility();
|
||||
}
|
||||
|
||||
downloadChart(id) {
|
||||
async downloadChart(id) {
|
||||
const c = this.charts.get(id);
|
||||
if (!c?.plotContainer) return;
|
||||
|
||||
try {
|
||||
Plotly.downloadImage(c.plotContainer, { format: 'png', width: 1200, height: 800, filename: `${id}-${Date.now()}` });
|
||||
this.notifications?.show?.({ type: 'success', title: 'Download Started', message: 'Chart image download started' });
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
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) {
|
||||
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) {
|
||||
const c = this.charts.get(id);
|
||||
if (!c?.element) return;
|
||||
@ -592,41 +833,6 @@ export class ChartManager {
|
||||
pause() { this.isPaused = true; console.log('⏸️ Chart updates paused'); }
|
||||
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
|
||||
@ -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)
|
||||
*/
|
||||
|
||||
@ -29,6 +29,11 @@ export class SettingsManager {
|
||||
this.handleSaveCalibration = this.handleSaveCalibration.bind(this);
|
||||
this.handleSetCalibration = this.handleSetCalibration.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() {
|
||||
@ -73,6 +78,13 @@ export class SettingsManager {
|
||||
saveCalibrationBtn: document.getElementById('saveCalibrationBtn'),
|
||||
calibrationDropdown: document.getElementById('calibrationDropdown'),
|
||||
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
|
||||
presetCount: document.getElementById('presetCount'),
|
||||
@ -91,6 +103,8 @@ export class SettingsManager {
|
||||
this.elements.saveCalibrationBtn?.addEventListener('click', this.handleSaveCalibration);
|
||||
this.elements.calibrationDropdown?.addEventListener('change', this.handleCalibrationChange);
|
||||
this.elements.setCalibrationBtn?.addEventListener('click', this.handleSetCalibration);
|
||||
this.elements.viewPlotsBtn?.addEventListener('click', this.handleViewPlots);
|
||||
this.elements.viewCurrentPlotsBtn?.addEventListener('click', this.handleViewCurrentPlots);
|
||||
|
||||
// Calibration name input
|
||||
this.elements.calibrationNameInput?.addEventListener('input', () => {
|
||||
@ -211,6 +225,7 @@ export class SettingsManager {
|
||||
|
||||
dropdown.disabled = false;
|
||||
this.elements.setCalibrationBtn.disabled = true;
|
||||
this.elements.viewPlotsBtn.disabled = true;
|
||||
}
|
||||
|
||||
formatPresetDisplay(preset) {
|
||||
@ -280,11 +295,22 @@ export class SettingsManager {
|
||||
const hasName = this.elements.calibrationNameInput.value.trim().length > 0;
|
||||
this.elements.saveCalibrationBtn.disabled = !hasName || !workingCalibration.is_complete;
|
||||
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() {
|
||||
this.elements.calibrationSteps.style.display = 'none';
|
||||
this.elements.calibrationStandards.innerHTML = '';
|
||||
|
||||
// Disable "View Current Plots" button
|
||||
if (this.elements.viewCurrentPlotsBtn) {
|
||||
this.elements.viewCurrentPlotsBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
resetCalibrationState() {
|
||||
@ -592,6 +618,7 @@ export class SettingsManager {
|
||||
handleCalibrationChange() {
|
||||
const selectedValue = this.elements.calibrationDropdown.value;
|
||||
this.elements.setCalibrationBtn.disabled = !selectedValue;
|
||||
this.elements.viewPlotsBtn.disabled = !selectedValue;
|
||||
}
|
||||
|
||||
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
|
||||
async refresh() {
|
||||
if (!this.isInitialized) return;
|
||||
|
||||
@ -15,7 +15,6 @@ export class UIManager {
|
||||
processorToggles: null,
|
||||
sweepCount: null,
|
||||
dataRate: null,
|
||||
exportBtn: null,
|
||||
navButtons: null,
|
||||
views: null
|
||||
};
|
||||
@ -30,7 +29,6 @@ export class UIManager {
|
||||
this.eventHandlers = {
|
||||
viewChange: [],
|
||||
processorToggle: [],
|
||||
exportData: []
|
||||
};
|
||||
|
||||
}
|
||||
@ -70,12 +68,11 @@ export class UIManager {
|
||||
findElements() {
|
||||
this.elements.connectionStatus = document.getElementById('connectionStatus');
|
||||
this.elements.processorToggles = document.getElementById('processorToggles');
|
||||
this.elements.exportBtn = document.getElementById('exportBtn');
|
||||
this.elements.navButtons = document.querySelectorAll('.nav-btn[data-view]');
|
||||
this.elements.views = document.querySelectorAll('.view');
|
||||
|
||||
// Validate required elements
|
||||
const required = ['connectionStatus', 'processorToggles', 'exportBtn'];
|
||||
const required = ['connectionStatus', 'processorToggles'];
|
||||
for (const key of required) {
|
||||
if (!this.elements[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)
|
||||
this.elements.processorToggles.addEventListener('click', (e) => {
|
||||
const toggle = e.target.closest('.processor-toggle');
|
||||
@ -402,10 +394,6 @@ export class UIManager {
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
triggerExportData() {
|
||||
this.emitEvent('exportData');
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
console.log('📱 Window resized');
|
||||
// Charts handle their own resize
|
||||
@ -419,7 +407,6 @@ export class UIManager {
|
||||
// Events
|
||||
onViewChange(callback) { this.eventHandlers.viewChange.push(callback); }
|
||||
onProcessorToggle(callback) { this.eventHandlers.processorToggle.push(callback); }
|
||||
onExportData(callback) { this.eventHandlers.exportData.push(callback); }
|
||||
|
||||
emitEvent(eventType, ...args) {
|
||||
if (this.eventHandlers[eventType]) {
|
||||
|
||||
@ -107,15 +107,6 @@
|
||||
</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>
|
||||
|
||||
@ -219,6 +210,10 @@
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<button class="btn btn--success" id="saveCalibrationBtn" disabled>
|
||||
<i data-lucide="save"></i>
|
||||
@ -234,10 +229,16 @@
|
||||
<select class="calibration-dropdown" id="calibrationDropdown" disabled>
|
||||
<option value="">No calibrations available</option>
|
||||
</select>
|
||||
<button class="btn btn--primary" id="setCalibrationBtn" disabled>
|
||||
<i data-lucide="check"></i>
|
||||
Set Active
|
||||
</button>
|
||||
<div class="calibration-actions">
|
||||
<button class="btn btn--primary" id="setCalibrationBtn" disabled>
|
||||
<i data-lucide="check"></i>
|
||||
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>
|
||||
@ -266,6 +267,37 @@
|
||||
</div>
|
||||
</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 -->
|
||||
<div class="notifications" id="notifications"></div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user