From 226b10b5a1177bf48fccae08b0a0f33e0d397607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Sat, 18 Oct 2025 00:22:51 +0200 Subject: [PATCH] barwy kategorii --- app.py | 132 +++++++++++---------------------------------------------- 1 file changed, 25 insertions(+), 107 deletions(-) diff --git a/app.py b/app.py index 4777de9..0474504 100644 --- a/app.py +++ b/app.py @@ -886,121 +886,39 @@ def get_admin_expense_summary(): -from typing import List, Tuple - -# 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]] = [ - (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 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(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 = 18) -> str: + # Stabilny hash -> int 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) - hue_deg = pick_hue_rotating(mix, used_hues, min_hue_gap_deg) + # Pełne pokrycie koła barw 0..360 + hue_deg = mix % 360 - # 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.70, max(0.58, s_base + s_var)) - l = min(0.42, max(0.32, l_base + l_var)) + # 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] + hue_deg = (hue_deg + gap) % 360 - # Dla niebiesko‑fioletowych podbij minimalnie L, by nie wpadały w prawie czerń - if 200 <= hue_deg <= 320: - l = max(l, 0.35) + # DARK profil: niższa jasność i nieco mniejsza saturacja + s = 0.70 + l = 0.45 - h = (hue_deg % 360) / 360.0 + # 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)) + + # Konwersja HLS->RGB (colorsys: H,L,S w [0..1]) + h = hue_deg / 360.0 r, g, b = colorsys.hls_to_rgb(h, l, s) - # 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: - 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}" - + return f"#{int(round(r*255)):02x}{int(round(g*255)):02x}{int(round(b*255)):02x}" def get_total_expenses_grouped_by_category( show_all, range_type, start_date, end_date, user_id, category_id=None