"""Pure gauge configuration dataclasses.""" from __future__ import annotations from collections.abc import Mapping, Sequence from dataclasses import dataclass, replace from typing import Any, Literal from .scales import validate_markers, validate_range, validate_thresholds from .themes import GaugeStyle, GaugeTheme, get_theme, merge_style from .types import ( AnimationConfig, Callback, GaugeMarker, GaugeValue, SmoothingConfig, StatusState, Tag, ThresholdBand, ValueFormatter, ) def _normalize_animation(value: AnimationConfig | bool | None) -> AnimationConfig | None: if isinstance(value, bool): return AnimationConfig(enabled=value) return value def _normalize_smoothing(value: SmoothingConfig | bool | None) -> SmoothingConfig | None: if isinstance(value, bool): return SmoothingConfig(enabled=value) return value @dataclass(frozen=True) class GaugeConfig: tag: Tag | None = None parent: Tag | None = None label: str | None = None value: GaugeValue = None min_value: float = 0.0 max_value: float = 100.0 unit: str = "" precision: int = 0 width: int = 0 height: int = 0 autosize_x: bool = False autosize_y: bool = False show: bool = True show_label: bool = True show_value: bool = True show_unit: bool = True tooltip: str | None = None callback: Callback | None = None user_data: Any = None theme: str | GaugeTheme | None = None style: GaugeStyle | None = None animation: AnimationConfig | bool | None = None smoothing: SmoothingConfig | bool | None = None clamp: bool = True format_value: ValueFormatter | None = None thresholds: Sequence[ThresholdBand] = () markers: Sequence[GaugeMarker] = () def __post_init__(self) -> None: validate_range(self.min_value, self.max_value) validate_thresholds(self.thresholds, self.min_value, self.max_value) validate_markers(self.markers, self.min_value, self.max_value) if self.precision < 0: msg = "precision must be non-negative" raise ValueError(msg) object.__setattr__(self, "theme", get_theme(self.theme)) object.__setattr__(self, "style", self.style or get_theme(self.theme).style) object.__setattr__(self, "animation", _normalize_animation(self.animation)) object.__setattr__(self, "smoothing", _normalize_smoothing(self.smoothing)) object.__setattr__(self, "thresholds", tuple(self.thresholds)) object.__setattr__(self, "markers", tuple(self.markers)) def with_style_overrides(self, **overrides: Any) -> GaugeConfig: return replace(self, style=merge_style(self.style, **overrides)) @dataclass(frozen=True) class AnalogGaugeConfig(GaugeConfig): start_angle: float = 225.0 end_angle: float = -45.0 major_ticks: int = 8 minor_ticks: int = 4 show_tick_labels: bool = True needle_length: float = 0.82 needle_width: float = 3.0 center_cap_radius: float = 7.0 arc_width: float = 8.0 maintain_aspect: bool = True redline_start: float | None = None @dataclass(frozen=True) class DigitalGaugeConfig(GaugeConfig): pass @dataclass(frozen=True) class BarGaugeConfig(GaugeConfig): orientation: Literal["horizontal", "vertical"] = "horizontal" fill_mode: Literal["clamp", "centered"] = "clamp" corner_radius: float = 4.0 show_scale: bool = True stretch: bool = True @dataclass(frozen=True) class LevelGaugeConfig(BarGaugeConfig): orientation: Literal["horizontal", "vertical"] = "vertical" @dataclass(frozen=True) class StatusLightConfig(GaugeConfig): state: str | bool | int = False states: Mapping[Any, StatusState] | None = None blink: bool = False pulse: bool = False @dataclass(frozen=True) class SegmentedGaugeConfig(BarGaugeConfig): segments: int = 10 @dataclass(frozen=True) class CompassGaugeConfig(AnalogGaugeConfig): min_value: float = 0.0 max_value: float = 360.0 start_angle: float = 90.0 end_angle: float = -270.0 @dataclass(frozen=True) class ThermometerGaugeConfig(LevelGaugeConfig): pass @dataclass(frozen=True) class BatteryGaugeConfig(LevelGaugeConfig): charging: bool | None = None @dataclass(frozen=True) class MultiValueGaugeConfig(GaugeConfig): values: Mapping[str, GaugeValue] | None = None