"""Inline SVG charts, so the same markup renders in the browser and in WeasyPrint."""
import math
from html import escape
BLUE = "#2f6fd6"
ORANGE = "#e8632b"
INK = "#1d1d1f"
MUTED = "#6b6b70"
GRID = "#e4e4e8"
def nice_ticks(lo: float, hi: float, count: int = 8) -> list[float]:
if hi <= lo:
hi = lo + 1
raw = (hi - lo) / count
mag = 10 ** math.floor(math.log10(raw))
step = next(m * mag for m in (1, 2, 2.5, 5, 10) if m * mag >= raw)
start = math.floor(lo / step) * step
ticks, v = [], start
while v <= hi + step * 0.001:
ticks.append(round(v, 10))
v += step
if ticks[-1] < hi:
ticks.append(ticks[-1] + step)
return ticks
def _wrap(text: str, width: int = 30, lines: int = 2) -> list[str]:
words, out, cur = text.split(), [], ""
for w in words:
if len(cur) + len(w) + (1 if cur else 0) <= width:
cur = f"{cur} {w}".strip()
else:
out.append(cur)
cur = w
out.append(cur)
out = [o for o in out if o]
if len(out) > lines:
out = out[:lines]
out[-1] = out[-1][: width - 1].rstrip() + "…"
return [o if len(o) <= width else o[: width - 1] + "…" for o in out] or [""]
def grouped_bars(rows: list[tuple[str, float, float]], fmt_tick, fmt_value=None, width: int = 900,
height: int = 490) -> str:
"""Horizontal bars, two per row (prior in blue, adjusted in orange), sized to fill width x height."""
if not rows:
return ""
label_w = 250
top, bottom = 8, 30
row_h = min(130.0, (height - top - bottom) / len(rows))
bar_h = min(row_h * 0.34, 36)
plot_w = width - label_w - (80 if fmt_value else 24)
height = round(top + bottom + row_h * len(rows))
hi = max(max(p, a) for _, p, a in rows) or 1
ticks = nice_ticks(0, hi, 8)
x = lambda v: label_w + plot_w * v / ticks[-1]
parts = [f'")
return "".join(parts)
def change_bars(rows: list[tuple[str, float]], fmt_value, width: int = 900, height: int = 520) -> str:
"""Ranked % change per category; increases orange, decreases blue."""
if not rows:
return ""
label_w, top, bottom = 270, 8, 30
row_h = min(60.0, (height - top - bottom) / len(rows))
bar = min(row_h * 0.6, 28)
plot_w = width - label_w - 90
height = round(top + bottom + row_h * len(rows))
lo = min(0.0, min(v for _, v in rows))
hi = max(0.0, max(v for _, v in rows)) or 1
ticks = nice_ticks(lo, hi, 8)
span = ticks[-1] - ticks[0]
x = lambda v: label_w + plot_w * (v - ticks[0]) / span
parts = [f'")
return "".join(parts)