Source code for wolfhece.assets.bar.distribution

"""Distribution model for bar chart segment fractions.

Computes normalized fractions from values, similar to pie distribution
but tailored for consecutive bar segments.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Sequence

import numpy as np


@dataclass(frozen=True)
[docs] class BarSegment: """One normalized bar segment definition."""
[docs] value: float
[docs] fraction: float
[docs] start_position: float # 0.0 to 1.0 along bar
[docs] end_position: float
[docs] label: str
[docs] class BarDistributionModel: """Compute and store normalized bar fractions from values.""" def __init__(self, values: Sequence[float], labels: Sequence[str] | None = None) -> None:
[docs] self._values = np.array([], dtype=np.float64)
[docs] self._fractions = np.array([], dtype=np.float64)
[docs] self._labels: list[str] = []
self.set_values(values, labels=labels) @property
[docs] def values(self) -> np.ndarray: return self._values.copy()
@property
[docs] def fractions(self) -> np.ndarray: return self._fractions.copy()
@property
[docs] def labels(self) -> list[str]: return self._labels.copy()
[docs] def set_values(self, values: Sequence[float], labels: Sequence[str] | None = None) -> None: arr = np.asarray(values, dtype=np.float64).ravel() if arr.size == 0: raise ValueError("Bar distribution needs at least one value") if not np.isfinite(arr).all(): raise ValueError("All bar values must be finite") if np.any(arr < 0.0): raise ValueError("Bar values must be >= 0") total = float(arr.sum()) if total <= 0.0: raise ValueError("Total bar value must be > 0") fracs = arr / total self._values = arr self._fractions = fracs if labels is None: self._labels = [f"Segment {i + 1}" for i in range(len(arr))] else: label_list = list(labels) if len(label_list) != len(arr): raise ValueError( f"Number of labels ({len(label_list)}) must match values" f" ({len(arr)})" ) self._labels = label_list
[docs] def segments(self) -> list[BarSegment]: """Compute bar segments with normalized positions.""" result = [] pos = 0.0 for i, (value, fraction, label) in enumerate( zip(self._values, self._fractions, self._labels) ): start = pos end = pos + fraction result.append( BarSegment( value=float(value), fraction=float(fraction), start_position=start, end_position=end, label=label, ) ) pos = end return result