step 2: add gauge state sizing and animation model
This commit is contained in:
104
src/dpg_gauges/scales.py
Normal file
104
src/dpg_gauges/scales.py
Normal 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)
|
||||
Reference in New Issue
Block a user