| | """View state serialization and deserialization for saving/loading plot configurations.""" |
| |
|
| | import json |
| | from typing import Dict, Any, Optional, List |
| | from dataclasses import dataclass, asdict |
| | from datetime import datetime |
| | import orjson |
| | from pydantic import BaseModel, Field, validator |
| |
|
| |
|
| | class PlotConfig(BaseModel): |
| | """Plot configuration schema.""" |
| | plot_type: str = Field(..., description="Type of plot (1d, 2d, map)") |
| | x_dim: Optional[str] = Field(None, description="X-axis dimension") |
| | y_dim: Optional[str] = Field(None, description="Y-axis dimension") |
| | projection: str = Field("PlateCarree", description="Map projection") |
| | colormap: str = Field("viridis", description="Colormap name") |
| | vmin: Optional[float] = Field(None, description="Color scale minimum") |
| | vmax: Optional[float] = Field(None, description="Color scale maximum") |
| | levels: Optional[List[float]] = Field(None, description="Contour levels") |
| | style: Dict[str, Any] = Field(default_factory=dict, description="Additional style parameters") |
| | |
| | @validator('plot_type') |
| | def validate_plot_type(cls, v): |
| | allowed = ['1d', '2d', 'map', 'contour'] |
| | if v not in allowed: |
| | raise ValueError(f"plot_type must be one of {allowed}") |
| | return v |
| |
|
| |
|
| | class DataConfig(BaseModel): |
| | """Data configuration schema.""" |
| | uri: str = Field(..., description="Data source URI") |
| | engine: Optional[str] = Field(None, description="Data engine") |
| | variable_a: str = Field(..., description="Primary variable") |
| | variable_b: Optional[str] = Field(None, description="Secondary variable for operations") |
| | operation: str = Field("none", description="Operation between variables") |
| | selections: Dict[str, Any] = Field(default_factory=dict, description="Dimension selections") |
| | |
| | @validator('operation') |
| | def validate_operation(cls, v): |
| | allowed = ['none', 'sum', 'avg', 'diff'] |
| | if v not in allowed: |
| | raise ValueError(f"operation must be one of {allowed}") |
| | return v |
| |
|
| |
|
| | class ViewState(BaseModel): |
| | """Complete view state schema.""" |
| | version: str = Field("1.0", description="State schema version") |
| | created: str = Field(default_factory=lambda: datetime.utcnow().isoformat(), description="Creation timestamp") |
| | title: str = Field("", description="User-defined title") |
| | description: str = Field("", description="User-defined description") |
| | data_config: DataConfig = Field(..., description="Data configuration") |
| | plot_config: PlotConfig = Field(..., description="Plot configuration") |
| | animation: Optional[Dict[str, Any]] = Field(None, description="Animation settings") |
| | exports: List[str] = Field(default_factory=list, description="Export history") |
| | |
| | class Config: |
| | extra = "allow" |
| |
|
| |
|
| | def create_view_state(data_config: Dict[str, Any], plot_config: Dict[str, Any], |
| | title: str = "", description: str = "", |
| | animation: Optional[Dict[str, Any]] = None) -> ViewState: |
| | """ |
| | Create a new view state object. |
| | |
| | Args: |
| | data_config: Data configuration dictionary |
| | plot_config: Plot configuration dictionary |
| | title: Optional title |
| | description: Optional description |
| | animation: Optional animation settings |
| | |
| | Returns: |
| | ViewState object |
| | """ |
| | data_cfg = DataConfig(**data_config) |
| | plot_cfg = PlotConfig(**plot_config) |
| | |
| | return ViewState( |
| | title=title, |
| | description=description, |
| | data_config=data_cfg, |
| | plot_config=plot_cfg, |
| | animation=animation |
| | ) |
| |
|
| |
|
| | def dump_state(state: ViewState) -> str: |
| | """ |
| | Serialize a view state to JSON string. |
| | |
| | Args: |
| | state: ViewState object |
| | |
| | Returns: |
| | JSON string |
| | """ |
| | |
| | return orjson.dumps(state.dict(), option=orjson.OPT_INDENT_2).decode('utf-8') |
| |
|
| |
|
| | def load_state(state_json: str) -> ViewState: |
| | """ |
| | Deserialize a view state from JSON string. |
| | |
| | Args: |
| | state_json: JSON string |
| | |
| | Returns: |
| | ViewState object |
| | """ |
| | try: |
| | data = orjson.loads(state_json) |
| | return ViewState(**data) |
| | except Exception as e: |
| | raise ValueError(f"Failed to parse view state: {str(e)}") |
| |
|
| |
|
| | def save_state_file(state: ViewState, filepath: str) -> str: |
| | """ |
| | Save view state to a file. |
| | |
| | Args: |
| | state: ViewState object |
| | filepath: Output file path |
| | |
| | Returns: |
| | File path |
| | """ |
| | state_json = dump_state(state) |
| | |
| | with open(filepath, 'w', encoding='utf-8') as f: |
| | f.write(state_json) |
| | |
| | return filepath |
| |
|
| |
|
| | def load_state_file(filepath: str) -> ViewState: |
| | """ |
| | Load view state from a file. |
| | |
| | Args: |
| | filepath: Input file path |
| | |
| | Returns: |
| | ViewState object |
| | """ |
| | with open(filepath, 'r', encoding='utf-8') as f: |
| | state_json = f.read() |
| | |
| | return load_state(state_json) |
| |
|
| |
|
| | def merge_states(base_state: ViewState, updates: Dict[str, Any]) -> ViewState: |
| | """ |
| | Merge updates into a base state. |
| | |
| | Args: |
| | base_state: Base ViewState object |
| | updates: Dictionary of updates |
| | |
| | Returns: |
| | New ViewState object with updates applied |
| | """ |
| | |
| | state_dict = base_state.dict() |
| | |
| | |
| | def deep_merge(base_dict, update_dict): |
| | for key, value in update_dict.items(): |
| | if key in base_dict and isinstance(base_dict[key], dict) and isinstance(value, dict): |
| | deep_merge(base_dict[key], value) |
| | else: |
| | base_dict[key] = value |
| | |
| | deep_merge(state_dict, updates) |
| | return ViewState(**state_dict) |
| |
|
| |
|
| | def validate_state_compatibility(state: ViewState) -> List[str]: |
| | """ |
| | Check state compatibility and return any warnings. |
| | |
| | Args: |
| | state: ViewState object |
| | |
| | Returns: |
| | List of warning messages |
| | """ |
| | warnings = [] |
| | |
| | |
| | current_version = "1.0" |
| | if state.version != current_version: |
| | warnings.append(f"State version {state.version} may not be fully compatible with current version {current_version}") |
| | |
| | |
| | plot_config = state.plot_config |
| | data_config = state.data_config |
| | |
| | if plot_config.plot_type == "map": |
| | if not plot_config.x_dim or not plot_config.y_dim: |
| | warnings.append("Map plots require both x_dim and y_dim to be specified") |
| | |
| | elif plot_config.plot_type == "2d": |
| | if not plot_config.x_dim or not plot_config.y_dim: |
| | warnings.append("2D plots require both x_dim and y_dim to be specified") |
| | |
| | elif plot_config.plot_type == "1d": |
| | if not plot_config.x_dim: |
| | warnings.append("1D plots require x_dim to be specified") |
| | |
| | |
| | if data_config.operation != "none" and not data_config.variable_b: |
| | warnings.append(f"Operation '{data_config.operation}' requires variable_b to be specified") |
| | |
| | return warnings |
| |
|
| |
|
| | def create_default_state(uri: str, variable: str) -> ViewState: |
| | """ |
| | Create a default view state for a given data source and variable. |
| | |
| | Args: |
| | uri: Data source URI |
| | variable: Variable name |
| | |
| | Returns: |
| | Default ViewState object |
| | """ |
| | data_config = { |
| | 'uri': uri, |
| | 'variable_a': variable, |
| | 'operation': 'none', |
| | 'selections': {} |
| | } |
| | |
| | plot_config = { |
| | 'plot_type': '2d', |
| | 'colormap': 'viridis', |
| | 'style': { |
| | 'colorbar': True, |
| | 'grid': True |
| | } |
| | } |
| | |
| | return create_view_state(data_config, plot_config, title=f"View of {variable}") |
| |
|
| |
|
| | def export_state_summary(state: ViewState) -> Dict[str, Any]: |
| | """ |
| | Create a human-readable summary of a view state. |
| | |
| | Args: |
| | state: ViewState object |
| | |
| | Returns: |
| | Summary dictionary |
| | """ |
| | summary = { |
| | 'title': state.title or "Untitled View", |
| | 'created': state.created, |
| | 'data_source': state.data_config.uri, |
| | 'primary_variable': state.data_config.variable_a, |
| | 'plot_type': state.plot_config.plot_type, |
| | 'has_secondary_variable': state.data_config.variable_b is not None, |
| | 'operation': state.data_config.operation, |
| | 'colormap': state.plot_config.colormap, |
| | 'has_animation': state.animation is not None, |
| | 'export_count': len(state.exports) |
| | } |
| | |
| | |
| | selections = state.data_config.selections |
| | if selections: |
| | summary['fixed_dimensions'] = list(selections.keys()) |
| | summary['selection_count'] = len(selections) |
| | |
| | |
| | if state.plot_config.plot_type == "map": |
| | summary['projection'] = state.plot_config.projection |
| | |
| | return summary |