SVG-Farbprofile: --farben mit palette- und kategorien-Modus

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>
This commit is contained in:
Michael Stangl
2026-07-27 10:10:10 +02:00
parent d4844ded67
commit d19913a53d
4 changed files with 243 additions and 14 deletions
+165
View File
@@ -0,0 +1,165 @@
"""
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]