115 lines
3.9 KiB
Python
115 lines
3.9 KiB
Python
"""
|
||
viz_theme.py — Shared matplotlib setup for levineuwirth.org figures.
|
||
|
||
Usage in a figure script:
|
||
|
||
import sys
|
||
sys.path.insert(0, 'tools') # relative to project root (where cabal runs)
|
||
from viz_theme import apply_monochrome, save_svg
|
||
|
||
apply_monochrome()
|
||
|
||
import matplotlib.pyplot as plt
|
||
fig, ax = plt.subplots()
|
||
ax.plot([1, 2, 3], [4, 5, 6])
|
||
ax.set_xlabel("x")
|
||
ax.set_ylabel("y")
|
||
|
||
save_svg(fig) # writes SVG to stdout; Viz.hs captures it
|
||
|
||
Design constraints
|
||
------------------
|
||
- Use pure black (#000000) for all drawn elements (lines, markers, text,
|
||
spines, ticks). Filters.Viz.processColors replaces these with
|
||
`currentColor` so the SVG adapts to light/dark mode via CSS.
|
||
- Use transparent backgrounds (figure and axes). The page background
|
||
shows through, so the figure integrates cleanly in both modes.
|
||
- For greyscale fills (bars, areas), use values in the range #333–#ccc.
|
||
These do NOT get replaced by processColors, so choose mid-greys that
|
||
remain legible in both light (#faf8f4) and dark (#121212) contexts.
|
||
- For multi-series charts, distinguish series by linestyle (solid, dashed,
|
||
dotted, dash-dot) rather than colour.
|
||
|
||
Font note: matplotlib's SVG output uses the font names configured here, but
|
||
those fonts are not available in the browser SVG renderer — the browser falls
|
||
back to its default serif. Do not rely on font metrics for sizing.
|
||
"""
|
||
|
||
import sys
|
||
import io
|
||
import matplotlib as mpl
|
||
import matplotlib.pyplot as plt
|
||
|
||
# Greyscale linestyle cycle for multi-series charts.
|
||
# Each entry: (color, linestyle) — all black, distinguished by dash pattern.
|
||
LINESTYLE_CYCLE = [
|
||
{'color': '#000000', 'linestyle': 'solid'},
|
||
{'color': '#000000', 'linestyle': 'dashed'},
|
||
{'color': '#000000', 'linestyle': 'dotted'},
|
||
{'color': '#000000', 'linestyle': (0, (5, 2, 1, 2))}, # dash-dot
|
||
{'color': '#555555', 'linestyle': 'solid'},
|
||
{'color': '#555555', 'linestyle': 'dashed'},
|
||
]
|
||
|
||
|
||
def apply_monochrome():
|
||
"""Configure matplotlib for monochrome, transparent, dark-mode-safe output.
|
||
|
||
Call this before creating any figures. All element colours are set to
|
||
pure black (#000000) so Filters.Viz.processColors can replace them with
|
||
CSS currentColor. Backgrounds are transparent.
|
||
"""
|
||
mpl.rcParams.update({
|
||
# Transparent backgrounds — CSS page background shows through.
|
||
'figure.facecolor': 'none',
|
||
'axes.facecolor': 'none',
|
||
'savefig.facecolor': 'none',
|
||
'savefig.edgecolor': 'none',
|
||
|
||
# All text and structural elements: pure black → currentColor.
|
||
'text.color': 'black',
|
||
'axes.labelcolor': 'black',
|
||
'axes.edgecolor': 'black',
|
||
'xtick.color': 'black',
|
||
'ytick.color': 'black',
|
||
|
||
# Grid: mid-grey, stays legible in both modes (not replaced).
|
||
'axes.grid': False,
|
||
'grid.color': '#cccccc',
|
||
'grid.linewidth': 0.6,
|
||
|
||
# Lines and patches: black → currentColor.
|
||
'lines.color': 'black',
|
||
'patch.edgecolor': 'black',
|
||
|
||
# Legend: no box frame; background transparent.
|
||
'legend.frameon': False,
|
||
'legend.facecolor': 'none',
|
||
'legend.edgecolor': 'none',
|
||
|
||
# Use linestyle cycle instead of colour cycle for series distinction.
|
||
'axes.prop_cycle': mpl.cycler(
|
||
color=[c['color'] for c in LINESTYLE_CYCLE],
|
||
linestyle=[c['linestyle'] for c in LINESTYLE_CYCLE],
|
||
),
|
||
})
|
||
|
||
|
||
def save_svg(fig, tight=True):
|
||
"""Write *fig* as SVG to stdout and close it.
|
||
|
||
Hakyll's Viz filter captures stdout and inlines the SVG.
|
||
|
||
Parameters
|
||
----------
|
||
fig : matplotlib.figure.Figure
|
||
tight : bool
|
||
If True (default), call fig.tight_layout() before saving.
|
||
"""
|
||
if tight:
|
||
fig.tight_layout()
|
||
buf = io.StringIO()
|
||
fig.savefig(buf, format='svg', bbox_inches='tight', transparent=True)
|
||
plt.close(fig)
|
||
sys.stdout.write(buf.getvalue())
|