d19913a53d
Neues Modul libs/Farben.py mit Farbschema, das Gruppen Farben zuordnet, gesteuert durch Profile aus cfg/farben.cfg. Zwei Modi: "palette" (Farbliste zyklisch je Gruppe, Profil grosse_gruppen) und "kategorien" (Hochzeit: Familie/Verein/VIP getrennt ueber Farbverlaeufe, Profil hochzeit). Sitzplatzverteilung.alsSVG bekommt Parameter Farbschema; Farben werden nun fest fuer die vollstaendige Gruppenliste (nach Id) vergeben statt lazy in Zeichenreihenfolge, damit jede Gruppe variantenuebergreifend dieselbe Farbe hat. platz.py bekommt Schalter --farben (Vorgabe grosse_gruppen). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
166 lines
6.3 KiB
Python
166 lines
6.3 KiB
Python
"""
|
|
Farbschema fuer die SVG-Ausgabe (Sitzplatzverteilung.alsSVG).
|
|
|
|
Laedt ein Farbprofil aus cfg/farben.cfg und ordnet jeder Gruppe eine Farbe zu.
|
|
Zwei Modi (siehe cfg/farben.cfg fuer Details):
|
|
- 'palette': eine Farbliste, zyklisch je Gruppe (nach Gruppen-Id sortiert)
|
|
- 'kategorien': trennt nach Art (Hochzeit) in Familie / Verein / VIP und
|
|
faerbt sie ueber je einen Farbverlauf ein.
|
|
|
|
Waehlbar per --farben <Sektion> (libs/platz.py) bzw. PLATZ_FARBENART (web).
|
|
"""
|
|
|
|
__author__ = "Michael Stangl"
|
|
__copyright__ = "Copyright (c) 2026 Michael Stangl"
|
|
__license__ = "Python"
|
|
|
|
import configparser
|
|
import os
|
|
from collections import Counter, defaultdict
|
|
|
|
|
|
def _hex_zu_rgb(h):
|
|
h = h.strip().lstrip('#')
|
|
return tuple( int(h[i:i+2], 16) for i in (0, 2, 4) )
|
|
|
|
|
|
def _rgb_zu_hex(rgb):
|
|
return '#%02x%02x%02x' % tuple( max(0, min(255, int(round(c)))) for c in rgb )
|
|
|
|
|
|
def _verlauf(start, ende, t):
|
|
"""Farbe im linearen RGB-Verlauf start->ende an Position t in [0,1]"""
|
|
a, b = _hex_zu_rgb(start), _hex_zu_rgb(ende)
|
|
return _rgb_zu_hex( tuple( a[i] + (b[i] - a[i]) * t for i in range(3) ) )
|
|
|
|
|
|
def _verlauf_liste(start, ende, n):
|
|
"""n Farben gleichmaessig ueber den Verlauf start->ende (n>=1)"""
|
|
if n <= 1:
|
|
return [ _verlauf(start, ende, 0.0) ]
|
|
return [ _verlauf(start, ende, j / (n - 1)) for j in range(n) ]
|
|
|
|
|
|
class Farbschema:
|
|
"""ordnet Gruppen Farben zu, gesteuert durch ein Profil aus farben.cfg"""
|
|
|
|
# Rueckfall-Palette, falls keine Config / kein gueltiges Profil geladen wurde
|
|
Standard = [
|
|
'#e6194b', '#3cb44b', '#4363d8', '#f58231', '#911eb4',
|
|
'#42d4f4', '#f032e6', '#bfef45', '#fabed4', '#469990',
|
|
'#dcbeff', '#9a6324', '#800000', '#aaffc3', '#000075' ]
|
|
|
|
def __init__(self):
|
|
self.Modus = 'palette'
|
|
self.Palette = list(self.Standard)
|
|
self.FamilienVerlaeufe = [ ('#1f4e9c', '#2fae5f'), ('#f2c200', '#e8590c') ]
|
|
self.VereinVerlauf = ('#7b2fbf', '#9aa0a6')
|
|
self.VIPVerlauf = ('#e63946', '#8b0000')
|
|
|
|
def laden(self, FileName, Sektion='grosse_gruppen'):
|
|
config = configparser.ConfigParser()
|
|
config.read( os.path.expanduser(FileName) )
|
|
if not config.has_section( Sektion ):
|
|
raise ValueError(
|
|
"Farbprofil '%s' nicht in %s gefunden (vorhanden: %s)"
|
|
% ( Sektion, FileName, ', '.join(config.sections()) or '-' ) )
|
|
self.Modus = config.get( Sektion, 'Modus', fallback='palette' ).strip()
|
|
if config.has_option( Sektion, 'Palette' ):
|
|
self.Palette = self._farbliste( config.get( Sektion, 'Palette' ) )
|
|
if config.has_option( Sektion, 'FamilienVerlaeufe' ):
|
|
self.FamilienVerlaeufe = self._verlaeufe(
|
|
config.get( Sektion, 'FamilienVerlaeufe' ) )
|
|
if config.has_option( Sektion, 'VereinVerlauf' ):
|
|
self.VereinVerlauf = self._verlaeufe( config.get( Sektion, 'VereinVerlauf' ) )[0]
|
|
if config.has_option( Sektion, 'VIPVerlauf' ):
|
|
self.VIPVerlauf = self._verlaeufe( config.get( Sektion, 'VIPVerlauf' ) )[0]
|
|
return self
|
|
|
|
@staticmethod
|
|
def _farbliste(text):
|
|
return [ f.strip() for f in text.replace('\n', ',').split(',') if f.strip() ]
|
|
|
|
@staticmethod
|
|
def _verlaeufe(text):
|
|
"""'A -> B, C -> D' -> [(A,B),(C,D)]; ein einzelner Wert wird zu (v,v)"""
|
|
ergebnis = []
|
|
for teil in text.replace('\n', ',').split(','):
|
|
teil = teil.strip()
|
|
if not teil:
|
|
continue
|
|
if '->' in teil:
|
|
a, b = teil.split('->', 1)
|
|
ergebnis.append( (a.strip(), b.strip()) )
|
|
else:
|
|
ergebnis.append( (teil, teil) )
|
|
return ergebnis or [ ('#888888', '#888888') ]
|
|
|
|
def farben(self, Gruppen):
|
|
"""ordnet den Gruppen Farben zu.
|
|
|
|
Rueckgabe: (GidZuFarbe {Gruppen-Id: hexfarbe}, VIPFarbe hexfarbe).
|
|
VIPFarbe gilt fuer Personen ohne GA-Gruppe (feste Platzierung)."""
|
|
GruppenListe = sorted( Gruppen, key=lambda g: g.Id )
|
|
if self.Modus == 'kategorien':
|
|
return self._kategorien( GruppenListe )
|
|
return self._palette( GruppenListe )
|
|
|
|
def _palette(self, GruppenListe):
|
|
pal = self.Palette or self.Standard
|
|
GidZuFarbe = { G.Id: pal[i % len(pal)] for i, G in enumerate(GruppenListe) }
|
|
return GidZuFarbe, '#888888'
|
|
|
|
@staticmethod
|
|
def _nachname(G):
|
|
"""dominanter Nachname der Gruppe, wenn ihn die Mehrheit traegt (bei
|
|
>= 2 Mitgliedern) - sonst None (dann gilt die Gruppe als Verein/
|
|
gemischt). Anonyme Sammelbuchungen (alle Mitglieder derselbe Name,
|
|
z.B. 13x 'Gast Musikverein') sind keine Familie, sondern gelten als
|
|
Verein - daher werden mindestens zwei VERSCHIEDENE Personen verlangt."""
|
|
personen = list( G )
|
|
if len(personen) < 2:
|
|
return None
|
|
if len({ (P._Vorname, P._Name) for P in personen }) < 2:
|
|
return None # anonymer Block (alle identisch) -> kein Familienname
|
|
namen = [ P._Name for P in personen ]
|
|
name, anzahl = Counter(namen).most_common(1)[0]
|
|
return name if anzahl * 2 > len(namen) else None
|
|
|
|
def _kategorien(self, GruppenListe):
|
|
# 1. klassifizieren: Familien (nach Nachname) vs. Vereine
|
|
familien = defaultdict(list) # Nachname -> [Gruppen]
|
|
vereine = []
|
|
for G in GruppenListe:
|
|
nn = self._nachname( G )
|
|
if nn is not None:
|
|
familien[nn].append( G )
|
|
else:
|
|
vereine.append( G )
|
|
GidZuFarbe = {}
|
|
# 2. Familien: jeder Nachname kommt (alphabetisch, abwechselnd) in einen
|
|
# Farbbereich - Blau->Gruen ODER Gelb->Orange. Innerhalb eines
|
|
# Bereichs werden ALLE zugehoerigen Familiengruppen ueber den Verlauf
|
|
# gespreizt, damit verschiedene Familien verschiedene Toene haben;
|
|
# Gruppen desselben Nachnamens bleiben benachbart (ein zusammen-
|
|
# haengendes Band). So laeuft z.B. eine mehrteilige Familie Brandt
|
|
# ueber benachbarte Gelb->Orange-Toene, waehrend Familie Peters einen
|
|
# anderen Blau->Gruen-Ton bekommt.
|
|
Nachnamen = sorted( familien )
|
|
Bereich = { nn: i % len(self.FamilienVerlaeufe)
|
|
for i, nn in enumerate(Nachnamen) }
|
|
GruppenImBereich = defaultdict(list)
|
|
for nn in Nachnamen:
|
|
for G in sorted( familien[nn], key=lambda g: g.Id ):
|
|
GruppenImBereich[ Bereich[nn] ].append( G )
|
|
for b, gruppen in GruppenImBereich.items():
|
|
start, ende = self.FamilienVerlaeufe[b]
|
|
for farbe, G in zip( _verlauf_liste(start, ende, len(gruppen)), gruppen ):
|
|
GidZuFarbe[G.Id] = farbe
|
|
# 3. Vereine: ein Verlauf (z.B. Violett -> Grau) ueber alle Vereine
|
|
vstart, vende = self.VereinVerlauf
|
|
for farbe, G in zip( _verlauf_liste(vstart, vende, len(vereine)),
|
|
sorted(vereine, key=lambda g: g.Id) ):
|
|
GidZuFarbe[G.Id] = farbe
|
|
# 4. VIP-Farbe: Startfarbe des VIP-Verlaufs (i.d.R. Rot)
|
|
return GidZuFarbe, self.VIPVerlauf[0]
|