step 2: add gauge state sizing and animation model

This commit is contained in:
2026-07-02 13:54:58 +02:00
parent 44e4cbd246
commit 34fd60c34e
27 changed files with 1732 additions and 5 deletions

104
src/dpg_gauges/scales.py Normal file
View File

@@ -0,0 +1,104 @@
"""Pure scale, formatting, and value mapping helpers."""
from __future__ import annotations
import math
from collections.abc import Sequence
from .exceptions import GaugeConfigError, GaugeValueError
from .types import GaugeMarker, ThresholdBand
def validate_range(min_value: float, max_value: float) -> None:
if not math.isfinite(min_value) or not math.isfinite(max_value):
msg = "gauge range values must be finite"
raise GaugeConfigError(msg)
if min_value >= max_value:
msg = "min_value must be less than max_value"
raise GaugeConfigError(msg)
def is_finite_number(value: object) -> bool:
return isinstance(value, int | float) and not isinstance(value, bool) and math.isfinite(value)
def clamp_value(value: float, min_value: float, max_value: float) -> float:
validate_range(min_value, max_value)
if not math.isfinite(value):
msg = "value must be finite"
raise GaugeValueError(msg)
return min(max(value, min_value), max_value)
def normalize_value(
value: float, min_value: float, max_value: float, *, clamp: bool = True
) -> float:
validate_range(min_value, max_value)
if clamp:
value = clamp_value(value, min_value, max_value)
if not math.isfinite(value):
msg = "value must be finite"
raise GaugeValueError(msg)
return (value - min_value) / (max_value - min_value)
def value_to_angle(
value: float,
min_value: float,
max_value: float,
start_angle: float,
end_angle: float,
*,
clamp: bool = True,
) -> float:
fraction = normalize_value(value, min_value, max_value, clamp=clamp)
return start_angle + (end_angle - start_angle) * fraction
def fill_fraction(value: float, min_value: float, max_value: float, *, clamp: bool = True) -> float:
return normalize_value(value, min_value, max_value, clamp=clamp)
def generate_ticks(min_value: float, max_value: float, count: int) -> list[float]:
validate_range(min_value, max_value)
if count < 2:
msg = "tick count must be at least 2"
raise GaugeConfigError(msg)
step = (max_value - min_value) / (count - 1)
return [min_value + step * index for index in range(count)]
def format_gauge_value(value: float, precision: int = 0, unit: str = "") -> str:
if precision < 0:
msg = "precision must be non-negative"
raise GaugeConfigError(msg)
if not math.isfinite(value):
return "--"
formatted = f"{value:.{precision}f}"
return f"{formatted} {unit}" if unit else formatted
def validate_thresholds(
thresholds: Sequence[ThresholdBand], min_value: float, max_value: float
) -> None:
validate_range(min_value, max_value)
for threshold in thresholds:
if not math.isfinite(threshold.start) or not math.isfinite(threshold.end):
msg = "threshold values must be finite"
raise GaugeConfigError(msg)
def validate_markers(markers: Sequence[GaugeMarker], min_value: float, max_value: float) -> None:
validate_range(min_value, max_value)
for marker in markers:
if not math.isfinite(marker.value):
msg = "marker values must be finite"
raise GaugeConfigError(msg)
def clamped_threshold(
threshold: ThresholdBand, min_value: float, max_value: float
) -> tuple[float, float]:
start = clamp_value(threshold.start, min_value, max_value)
end = clamp_value(threshold.end, min_value, max_value)
return min(start, end), max(start, end)