"""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]
start_position: float # 0.0 to 1.0 along bar
[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