barwy kategorii
This commit is contained in:
132
app.py
132
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
|
||||
|
Reference in New Issue
Block a user