barwy kategorii

This commit is contained in:
Mateusz Gruszczyński
2025-10-18 00:15:06 +02:00
parent 57a553037b
commit 05d364bcd4

79
app.py
View File

@@ -886,39 +886,72 @@ def get_admin_expense_summary():
def category_to_color(name: str, min_hue_gap_deg: int = 18) -> str:
# Stabilny hash -> int
import hashlib, colorsys
from typing import List, Tuple
# Dozwolone segmenty hue (stopnie). Wykluczamy żółcie (5070°) i jasną zieleń (80160°).
# Zostawiamy wąski pas "butelkowej" zieleni (165185°) oraz ciemny pomarańcz (1840°).
ALLOWED_HUE_SEGMENTS: List[Tuple[int, int]] = [
(0, 10), # czerwienie
(18, 40), # ciemny pomarańcz
(165, 185), # butelkowa zieleń
(200, 260), # chłodne niebieskie
(270, 320), # fiolety
(330, 360), # czerwienie/karmin
]
def rotl(x: int, r: int, bits: int = 128) -> int:
r %= bits
return ((x << r) | (x >> (bits - r))) & ((1 << bits) - 1)
def pick_hue_from_segments(mix: int) -> int:
# Wybierz segment deterministycznie i rozłóż równomiernie w jego zakresie
seg_idx = mix % len(ALLOWED_HUE_SEGMENTS)
a, b = ALLOWED_HUE_SEGMENTS[seg_idx]
span = (b - a) % 360
# Użyj kolejnej porcji bitów do pozycji wewnątrz segmentu
t = (rotl(mix, 23) % 1000) / 1000.0 # [0,1)
hue = int(a + t * span) % 360
return hue
def category_to_color(name: str, min_hue_gap_deg: int = 14) -> str:
hv = int(hashlib.md5(name.encode("utf-8")).hexdigest(), 16)
# Proste mieszanie bitów, by uniknąć lokalnych skupień
def rotl(x, r, bits=128):
r %= bits
return ((x << r) | (x >> (bits - r))) & ((1 << bits) - 1)
mix = hv ^ rotl(hv, 37) ^ rotl(hv, 73) ^ rotl(hv, 91)
# Pełne pokrycie koła barw 0..360
hue_deg = mix % 360
# Wybór hue tylko z dozwolonych segmentów
hue_deg = pick_hue_from_segments(mix)
# Odpychanie lokalne po hue, by podobne nazwy nie lądowały zbyt blisko
gap = (rotl(mix, 17) % (2*min_hue_gap_deg)) - min_hue_gap_deg # [-gap, +gap]
# Lokalne odpychanie w ramach segmentu, by podobne nazwy nie dawały niemal tego samego koloru
gap = (rotl(mix, 17) % (2*min_hue_gap_deg)) - min_hue_gap_deg
hue_deg = (hue_deg + gap) % 360
# DARK profil: niższa jasność i nieco mniejsza saturacja
s = 0.70
l = 0.45
# Bardzo ciemny profil, unikamy prawie czarnych
# Saturation ograniczone, żeby nie było neonów; Lightness niskie, ale > 0.30
s_base, l_base = 0.66, 0.37
s_var = ((rotl(mix, 29) % 7) - 3) / 100.0 # ±0.03
l_var = ((rotl(mix, 53) % 9) - 4) / 100.0 # ±0.04
# Wąska wariacja, żeby uniknąć „neonów” i zachować spójność
s_var = ((rotl(mix, 29) % 5) - 2) / 100.0 # ±0.02
l_var = ((rotl(mix, 53) % 7) - 3) / 100.0 # ±0.03
s = min(0.76, max(0.62, s + s_var))
l = min(0.50, max(0.40, l + l_var))
s = min(0.72, max(0.58, s_base + s_var))
l = min(0.42, max(0.32, l_base + l_var))
# Konwersja HLS->RGB (colorsys: H,L,S w [0..1])
h = hue_deg / 360.0
# Dodatkowa korekta: dla niebiesko-fioletowych utrzymaj nieco wyższą L, żeby nie wpadały w czerń
if 200 <= hue_deg <= 320:
l = max(l, 0.35)
# Konwersja HLS->RGB (colorsys: h,l,s w [0..1])
h = (hue_deg % 360) / 360.0
r, g, b = colorsys.hls_to_rgb(h, l, s)
return f"#{int(round(r*255)):02x}{int(round(g*255)):02x}{int(round(b*255)):02x}"
# Unikaj prawie czarnych po RGB (wszystkie kanały < ~28)
rr, gg, bb = int(round(r*255)), int(round(g*255)), int(round(b*255))
if rr < 28 and gg < 28 and bb < 28:
# Podbij minimalnie L, zachowując hue/sat
l = min(0.44, l + 0.04)
r, g, b = colorsys.hls_to_rgb(h, l, s)
rr, gg, bb = int(round(r*255)), int(round(g*255)), int(round(b*255))
return f"#{rr:02x}{gg:02x}{bb:02x}"
def get_total_expenses_grouped_by_category(
show_all, range_type, start_date, end_date, user_id, category_id=None