Files
ETPlot/plotstyle/colors.py
T
lars 36ae18e880 Add plotstyle: reusable KIT-branded matplotlib styling toolkit
Introduces a new plotstyle package providing consistent, presentation/thesis-ready
matplotlib figures: a KIT corporate-design color palette (categorical, sequential,
diverging, status), a theme applied via use() (KIT-black bottom spine only, left/bottom
ticks, horizontal gridlines, left-aligned titles, LaTeX text in Latin Modern Sans),
new_figure() with size presets and figure-level title/params subtitles, a
same-size colorbar() helper, style_legend() and panel_label() building blocks, and
savefig(). Ships as an optional "plotting" extra so core gallery installs stay
lightweight, with a full test suite and a runnable example notebook.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 14:19:30 +02:00

103 lines
3.9 KiB
Python

"""KIT (Karlsruhe Institute of Technology) corporate design color palette.
Hex values for KIT green, KIT blue, black 70%, and the corporate accent
colors are taken verbatim from the KIT corporate design guide
(https://kit-cd.km.kit.edu/english/341.php) — do not hand-edit them without
checking that page.
The categorical *order* below is not arbitrary: it was chosen by running
every hue through the CVD-safety/contrast checks described in the `dataviz`
skill (fixed hue order, OKLab CVD separation under simulated color-vision
deficiency, a normal-vision separation floor, contrast vs. a white surface)
and keeping the ordering that clears the adjacent-pair checks. KIT yellow
(#FCE500) is the one hue that cannot pass on its own (too light on a white
surface, ~1.3:1 contrast) — that is a property of the hex value itself, not
the ordering, so it is placed last and should always be paired with a
visible direct label rather than relied on as a fill alone.
"""
from __future__ import annotations
from matplotlib.colors import LinearSegmentedColormap
# Fixed-order categorical hues (KIT primary + accent colors). Order is the
# CVD-safety mechanism: never reorder, and never cycle past the last slot
# (fold extra series into "Other").
CATEGORICAL = [
"#009682", # 0 KIT green (primary)
"#DF9B1B", # 1 orange
"#4664AA", # 2 KIT blue (primary)
"#A78230", # 3 brown
"#23A1E0", # 4 cyan
"#A3107C", # 5 purple
"#8CB63C", # 6 pea green
"#A22223", # 7 red
"#FCE500", # 8 yellow — low contrast on white; always pair with a direct label
]
# Single-hue (KIT blue) sequential ramp, steps 100..700. Step 700 is the exact
# brand hex (the high/saturated end); lighter steps are tints blended toward
# white in sRGB — there is no darker-than-brand shade.
SEQUENTIAL_STEPS = [
"#e3e8f2", # 100
"#c9d2e6", # 200
"#afbcda", # 300
"#95a6ce", # 400
"#7a90c2", # 500
"#607ab6", # 600
"#4664AA", # 700 (KIT blue)
]
# Diverging KIT blue <-> KIT red, neutral gray midpoint.
DIVERGING = {
"low": "#4664AA",
"mid": "#f7f7f7",
"high": "#A22223",
}
# Fixed, reserved status scale — deliberately NOT re-themed to KIT colors:
# status is a small fixed scale with reserved meaning that must stay visually
# distinct from the categorical slots so it never impersonates a series.
# Never put these in the categorical cycle; always pair with an icon/label.
STATUS = {
"good": "#0ca30c",
"warning": "#fab219",
"serious": "#ec835a",
"critical": "#d03b3b",
}
# Chrome / ink roles, derived from KIT's specified "black 70%" (#404040,
# used by KIT for headings and continuous text) on a white surface.
INK = {
"surface": "#ffffff",
"primary": "#404040", # KIT black 70% — headings, titles, continuous text
"secondary": "#6a6a6a",
"muted": "#969696",
"gridline": "#ececec",
"baseline": "#c2c2c2",
}
def get_color(index: int) -> str:
"""Return the categorical color for series `index` (0-based).
Raises ValueError past the validated slots instead of silently wrapping
back to slot 0, which would collide two series on the same hue.
"""
if not 0 <= index < len(CATEGORICAL):
raise ValueError(
f"get_color({index}) out of range: only {len(CATEGORICAL)} validated categorical "
"colors exist. Fold extra series into an 'Other' bucket or facet instead of cycling."
)
return CATEGORICAL[index]
def sequential_cmap(name: str = "ps_sequential") -> LinearSegmentedColormap:
"""Continuous KIT-blue sequential colormap for magnitude encoding."""
return LinearSegmentedColormap.from_list(name, SEQUENTIAL_STEPS)
def diverging_cmap(name: str = "ps_diverging") -> LinearSegmentedColormap:
"""Continuous KIT blue-gray-red diverging colormap for polarity encoding."""
return LinearSegmentedColormap.from_list(name, [DIVERGING["low"], DIVERGING["mid"], DIVERGING["high"]])