diff --git a/app.py b/app.py index b90fc30..ae783c0 100644 --- a/app.py +++ b/app.py @@ -886,66 +886,115 @@ def get_admin_expense_summary(): -import hashlib, colorsys from typing import List, Tuple -# Dozwolone segmenty hue (stopnie). Wykluczamy żółcie (50–70°) i jasną zieleń (80–160°). -# Zostawiamy wąski pas "butelkowej" zieleni (165–185°) oraz ciemny pomarańcz (18–40°). +# 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 niebiesko‑fioletowych 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))