barwy kategorii

This commit is contained in:
Mateusz Gruszczyński
2025-10-18 00:22:51 +02:00
parent b24748a7b6
commit 226b10b5a1

132
app.py
View File

@@ -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 niebieskofioletowych 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