146 lines
4.7 KiB
Python
146 lines
4.7 KiB
Python
"""Pure scale, formatting, and value mapping helpers."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from collections.abc import Sequence
|
|
from typing import Literal
|
|
|
|
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 compass_heading(value: float) -> float:
|
|
if not math.isfinite(value):
|
|
msg = "heading value must be finite"
|
|
raise GaugeValueError(msg)
|
|
return value % 360.0
|
|
|
|
|
|
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 fill_span(
|
|
value: float,
|
|
min_value: float,
|
|
max_value: float,
|
|
*,
|
|
fill_mode: Literal["clamp", "centered"] = "clamp",
|
|
clamp: bool = True,
|
|
) -> tuple[float, float]:
|
|
fraction = fill_fraction(value, min_value, max_value, clamp=clamp)
|
|
if fill_mode == "clamp":
|
|
return 0.0, fraction
|
|
zero = fill_fraction(0.0, min_value, max_value, clamp=True)
|
|
return min(zero, fraction), max(zero, fraction)
|
|
|
|
|
|
def normalized_span(
|
|
start: float, end: float, min_value: float, max_value: float
|
|
) -> tuple[float, float]:
|
|
first = fill_fraction(start, min_value, max_value, clamp=True)
|
|
second = fill_fraction(end, min_value, max_value, clamp=True)
|
|
return min(first, second), max(first, second)
|
|
|
|
|
|
def active_segments(
|
|
value: float, min_value: float, max_value: float, segments: int, *, clamp: bool = True
|
|
) -> int:
|
|
if segments <= 0:
|
|
msg = "segments must be greater than zero"
|
|
raise GaugeConfigError(msg)
|
|
fraction = fill_fraction(value, min_value, max_value, clamp=clamp)
|
|
return min(max(math.ceil(fraction * segments), 0), segments)
|
|
|
|
|
|
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)
|