barwy kategorii

This commit is contained in:
Mateusz Gruszczyński
2025-10-18 00:19:15 +02:00
parent 05d364bcd4
commit 11065cd007

117
app.py
View File

@@ -886,66 +886,115 @@ def get_admin_expense_summary():
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°).
# Dozwolone pasy hue (stopnie) z pominięciem żółci i jasnych zieleni,
# zostawiając wąski pas butelkowej zieleni i ciemny pomarańcz.
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
(200, 240), # chłodne niebieskie
(260, 300), # fiolety
(310, 350), # czerwienie/karmin
(10, 30), # czerwienie -> ciemny pomarańcz
(165, 185), # butelkowa zieleń (wąsko)
]
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 segment_length(seg):
a, b = seg
return (b - a) % 360 or 360
def hue_distance(a: int, b: int) -> int:
d = abs(a - b) % 360
return d if d <= 180 else 360 - d
def pick_hue_rotating(hv: int, used_hues: List[int], min_deg: int) -> int:
# Rotacyjny wybór segmentu na podstawie hasha, aby rozłożyć kolory między pasy
seg_order = []
for i in range(len(ALLOWED_HUE_SEGMENTS)):
m = rotl(hv, 13*i) ^ rotl(hv, 29*i)
seg_order.append((m, i))
seg_order.sort(key=lambda x: x[0])
# Spróbuj znaleźć hue w którymś segmencie z zachowaniem minimalnego kąta
for _, idx in seg_order:
a, b = ALLOWED_HUE_SEGMENTS[idx]
span = segment_length((a, b))
# wewnątrz segmentu rozkład równomierny deterministycznie
m = rotl(hv, 47) ^ rotl(hv, 71) ^ (idx * 0x9e3779b97f4a7c15)
t = (m % 1000) / 1000.0
h = int((a + t * span)) % 360
# lokalne odpychanie o ±min_deg/2
gap = ((rotl(hv, 17) % min_deg) - (min_deg // 2))
h = (h + gap) % 360
# weryfikacja dystansu kątowego
if all(hue_distance(h, uh) >= min_deg for uh in used_hues):
return h
# Jeśli nie znaleziono, zwiększ kąt i ponów w prosty sposób
h = (int(hv % 360))
for step in range(0, 360, max(8, min_deg // 2)):
cand = (h + step) % 360
# odrzuć kandydat spoza dozwolonych segmentów
ok = False
for a, b in ALLOWED_HUE_SEGMENTS:
# sprawdzenie wewnątrz segmentu
if a <= b:
ok |= (a <= cand <= b)
else:
ok |= (cand >= a or cand <= b)
if not ok:
continue
if all(hue_distance(cand, uh) >= (min_deg // 2) for uh in used_hues):
return cand
# Ostateczna rezygnacja: zwróć jakiś dopuszczalny
a, b = ALLOWED_HUE_SEGMENTS[0]
return a
def category_to_color_hls(name: str, used_hex_colors: List[str] = None,
min_hue_gap_deg: int = 28) -> str:
"""
Zwraca bardzo ciemny kolor HEX z dozwolonych pasów hue, tak by unikać żółci i jasnych zieleni,
a także czerni i prawie czerni; used_hex_colors: lista dotychczasowych kolorów, by zachować odstęp.
"""
used_hues = []
if used_hex_colors:
# jeśli masz już kolory, oszacuj ich hue przez przybliżenie HLS
for hx in used_hex_colors:
hx = hx.lstrip('#')
r = int(hx[0:2], 16)/255.0
g = int(hx[2:4], 16)/255.0
b = int(hx[4:6], 16)/255.0
# colorsys.rgb_to_hls -> (h, l, s) z h w [0..1]
h, l, s = colorsys.rgb_to_hls(r, g, b)
used_hues.append(int(round((h % 1.0) * 360)))
def category_to_color(name: str, min_hue_gap_deg: int = 14) -> str:
hv = int(hashlib.md5(name.encode("utf-8")).hexdigest(), 16)
mix = hv ^ rotl(hv, 37) ^ rotl(hv, 73) ^ rotl(hv, 91)
# Wybór hue tylko z dozwolonych segmentów
hue_deg = pick_hue_from_segments(mix)
hue_deg = pick_hue_rotating(mix, used_hues, min_hue_gap_deg)
# 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
# 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
# Bardzo ciemny profil bez „neonów” i bez czerni
s_base, l_base = 0.64, 0.36
s_var = ((rotl(mix, 29) % 7) - 3) / 100.0 # ±0.03
l_var = ((rotl(mix, 53) % 9) - 4) / 100.0 # ±0.04
s = min(0.72, max(0.58, s_base + s_var))
s = min(0.70, max(0.58, s_base + s_var))
l = min(0.42, max(0.32, l_base + l_var))
# Dodatkowa korekta: dla niebiesko-fioletowych utrzymaj nieco wyższą L, żeby nie wpadały w czerń
# Dla niebieskofioletowych podbij minimalnie L, by nie wpadały w prawie 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)
# Unikaj prawie czarnych po RGB (wszystkie kanały < ~28)
# Minimalny próg jasności w RGB, żeby uniknąć „prawie czarnych”
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))