add Nature figure standards: palette, Altair theme, DAG colors
Some checks failed
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/infra-ci Pipeline failed
ci/woodpecker/push/infra-ci Pipeline was successful
coverage 99% coverage
ci/woodpecker/pr/ci Pipeline was successful

Create styles/nature.py with accessible 8-colour palette (Paul Tol
bright scheme), Altair theme registration, matplotlib rcParams, and
Cytoscape style helpers — all conforming to Nature Research figure
specifications (bib citation 9ASETLJ4).

Retrofit acodb_explorer.py: replace all hardcoded set2/hex colors
with PALETTE references, enable the Nature Altair theme on import.

Update src/aco/dag.py to import schema colors from the shared
styles/nature.py module instead of maintaining a separate Tableau 10
palette.

fix #45, fix #46, refs #47
This commit is contained in:
kert
2026-03-21 13:55:53 -04:00
parent cf2d252095
commit 33701365be
3 changed files with 261 additions and 57 deletions

View File

@@ -27,12 +27,18 @@ def _(mo):
@app.cell(hide_code=True) @app.cell(hide_code=True)
def _(): def _():
import sys
sys.path.insert(0, "../styles")
import altair as alt import altair as alt
import duckdb import duckdb
import polars as pl import polars as pl
from nature import PALETTE, altair_theme
from conf import path as _conf_path from conf import path as _conf_path
altair_theme()
DB_PATH = str(_conf_path("db.aco")) DB_PATH = str(_conf_path("db.aco"))
con = duckdb.connect(DB_PATH, read_only=True) con = duckdb.connect(DB_PATH, read_only=True)
@@ -40,7 +46,7 @@ def _():
"""Run a query and return a Polars DataFrame.""" """Run a query and return a Polars DataFrame."""
return con.execute(sql).pl() return con.execute(sql).pl()
return alt, pl, q return PALETTE, alt, pl, q
@app.cell(hide_code=True) @app.cell(hide_code=True)
@@ -93,7 +99,7 @@ def _(mo):
@app.cell(hide_code=True) @app.cell(hide_code=True)
def _(alt, mo, q): def _(PALETTE, alt, mo, q):
patients = q( patients = q(
"SELECT sex, race, state, age, age_group FROM core.patient WHERE state IS NOT NULL" "SELECT sex, race, state, age, age_group FROM core.patient WHERE state IS NOT NULL"
) )
@@ -103,7 +109,7 @@ def _(alt, mo, q):
.mark_arc(innerRadius=50) .mark_arc(innerRadius=50)
.encode( .encode(
theta=alt.Theta("count():Q"), theta=alt.Theta("count():Q"),
color=alt.Color("sex:N", scale=alt.Scale(scheme="set2")), color=alt.Color("sex:N", scale=alt.Scale(range=PALETTE)),
tooltip=["sex:N", "count():Q"], tooltip=["sex:N", "count():Q"],
) )
.properties(title="Sex Distribution", width=250, height=250) .properties(title="Sex Distribution", width=250, height=250)
@@ -115,7 +121,7 @@ def _(alt, mo, q):
.encode( .encode(
x=alt.X("count():Q", title="Patients"), x=alt.X("count():Q", title="Patients"),
y=alt.Y("race:N", sort="-x", title=None), y=alt.Y("race:N", sort="-x", title=None),
color=alt.Color("race:N", scale=alt.Scale(scheme="set2"), legend=None), color=alt.Color("race:N", scale=alt.Scale(range=PALETTE), legend=None),
tooltip=["race:N", "count():Q"], tooltip=["race:N", "count():Q"],
) )
.properties(title="Race Distribution", width=350, height=250) .properties(title="Race Distribution", width=350, height=250)
@@ -126,14 +132,14 @@ def _(alt, mo, q):
@app.cell(hide_code=True) @app.cell(hide_code=True)
def _(alt, mo, patients): def _(PALETTE, alt, mo, patients):
age_chart = ( age_chart = (
alt.Chart(patients.to_pandas()) alt.Chart(patients.to_pandas())
.mark_bar() .mark_bar()
.encode( .encode(
x=alt.X("age:Q", bin=alt.Bin(step=5), title="Age"), x=alt.X("age:Q", bin=alt.Bin(step=5), title="Age"),
y=alt.Y("count():Q", title="Patients"), y=alt.Y("count():Q", title="Patients"),
color=alt.Color("sex:N", scale=alt.Scale(scheme="set2")), color=alt.Color("sex:N", scale=alt.Scale(range=PALETTE)),
tooltip=["count():Q"], tooltip=["count():Q"],
) )
.properties(title="Age Distribution by Sex", width=600, height=300) .properties(title="Age Distribution by Sex", width=600, height=300)
@@ -151,7 +157,7 @@ def _(alt, mo, patients):
.encode( .encode(
x=alt.X("len:Q", title="Patients"), x=alt.X("len:Q", title="Patients"),
y=alt.Y("state:N", sort="-x", title=None), y=alt.Y("state:N", sort="-x", title=None),
color=alt.value("#66c2a5"), color=alt.value(PALETTE[2]),
tooltip=["state:N", "len:Q"], tooltip=["state:N", "len:Q"],
) )
.properties(title="Top 15 States", width=350, height=300) .properties(title="Top 15 States", width=350, height=300)
@@ -170,7 +176,7 @@ def _(mo):
@app.cell(hide_code=True) @app.cell(hide_code=True)
def _(alt, mo, q): def _(PALETTE, alt, mo, q):
enc_types = q(""" enc_types = q("""
SELECT encounter_type, count(*) AS encounters SELECT encounter_type, count(*) AS encounters
FROM core.encounter FROM core.encounter
@@ -184,7 +190,7 @@ def _(alt, mo, q):
.encode( .encode(
x=alt.X("encounters:Q", title="Count"), x=alt.X("encounters:Q", title="Count"),
y=alt.Y("encounter_type:N", sort="-x", title=None), y=alt.Y("encounter_type:N", sort="-x", title=None),
color=alt.value("#fc8d62"), color=alt.value(PALETTE[7]),
tooltip=["encounter_type:N", "encounters:Q"], tooltip=["encounter_type:N", "encounters:Q"],
) )
.properties(title="Encounters by Type", width=600, height=450) .properties(title="Encounters by Type", width=600, height=450)
@@ -195,7 +201,7 @@ def _(alt, mo, q):
@app.cell(hide_code=True) @app.cell(hide_code=True)
def _(alt, mo, q): def _(PALETTE, alt, mo, q):
enc_monthly = q(""" enc_monthly = q("""
SELECT strftime(encounter_start_date, '%Y-%m') AS month, SELECT strftime(encounter_start_date, '%Y-%m') AS month,
encounter_type, encounter_type,
@@ -216,7 +222,7 @@ def _(alt, mo, q):
x=alt.X("month:T", title="Month"), x=alt.X("month:T", title="Month"),
y=alt.Y("encounters:Q", title="Encounters"), y=alt.Y("encounters:Q", title="Encounters"),
color=alt.Color( color=alt.Color(
"encounter_type:N", title="Type", scale=alt.Scale(scheme="set2") "encounter_type:N", title="Type", scale=alt.Scale(range=PALETTE)
), ),
tooltip=["month:T", "encounter_type:N", "encounters:Q"], tooltip=["month:T", "encounter_type:N", "encounters:Q"],
) )
@@ -236,7 +242,7 @@ def _(mo):
@app.cell(hide_code=True) @app.cell(hide_code=True)
def _(alt, mo, q): def _(PALETTE, alt, mo, q):
chronic = q(""" chronic = q("""
SELECT condition, count(DISTINCT person_id) AS patients SELECT condition, count(DISTINCT person_id) AS patients
FROM chronic_conditions.chronic_conditions_long FROM chronic_conditions.chronic_conditions_long
@@ -251,7 +257,7 @@ def _(alt, mo, q):
.encode( .encode(
x=alt.X("patients:Q", title="Patients"), x=alt.X("patients:Q", title="Patients"),
y=alt.Y("condition:N", sort="-x", title=None), y=alt.Y("condition:N", sort="-x", title=None),
color=alt.Color("patients:Q", scale=alt.Scale(scheme="reds"), legend=None), color=alt.Color("patients:Q", scale=alt.Scale(range=[PALETTE[1] + "33", PALETTE[1]]), legend=None),
tooltip=["condition:N", "patients:Q"], tooltip=["condition:N", "patients:Q"],
) )
.properties( .properties(
@@ -285,7 +291,7 @@ def _(mo, q):
@app.cell(hide_code=True) @app.cell(hide_code=True)
def _(alt, comorbidity): def _(PALETTE, alt, comorbidity):
burden_chart = ( burden_chart = (
alt.Chart(comorbidity.to_pandas()) alt.Chart(comorbidity.to_pandas())
.mark_bar() .mark_bar()
@@ -296,7 +302,7 @@ def _(alt, comorbidity):
title="Number of Chronic Conditions", title="Number of Chronic Conditions",
), ),
y=alt.Y("count():Q", title="Patients"), y=alt.Y("count():Q", title="Patients"),
color=alt.value("#e78ac3"), color=alt.value(PALETTE[5]),
tooltip=["count():Q"], tooltip=["count():Q"],
) )
.properties(title="Distribution of Comorbidity Burden", width=600, height=300) .properties(title="Distribution of Comorbidity Burden", width=600, height=300)
@@ -315,7 +321,7 @@ def _(mo):
@app.cell(hide_code=True) @app.cell(hide_code=True)
def _(alt, mo, pl, q): def _(PALETTE, alt, mo, pl, q):
risk = q(""" risk = q("""
SELECT r.person_id, r.payment_risk_score, r.v24_risk_score, SELECT r.person_id, r.payment_risk_score, r.v24_risk_score,
r.normalized_risk_score, r.member_months, r.normalized_risk_score, r.member_months,
@@ -345,7 +351,7 @@ def _(alt, mo, pl, q):
title="Payment Risk Score", title="Payment Risk Score",
), ),
y=alt.Y("count():Q", title="Patients"), y=alt.Y("count():Q", title="Patients"),
color=alt.Color("sex:N", scale=alt.Scale(scheme="set2")), color=alt.Color("sex:N", scale=alt.Scale(range=PALETTE)),
tooltip=["count():Q"], tooltip=["count():Q"],
) )
.properties(title="HCC Payment Risk Score Distribution", width=600, height=300) .properties(title="HCC Payment Risk Score Distribution", width=600, height=300)
@@ -357,7 +363,7 @@ def _(alt, mo, pl, q):
.encode( .encode(
x=alt.X("age:Q", title="Age"), x=alt.X("age:Q", title="Age"),
y=alt.Y("payment_risk_score:Q", title="Payment Risk Score"), y=alt.Y("payment_risk_score:Q", title="Payment Risk Score"),
color=alt.Color("sex:N", scale=alt.Scale(scheme="set2")), color=alt.Color("sex:N", scale=alt.Scale(range=PALETTE)),
size=alt.Size( size=alt.Size(
"member_months:Q", scale=alt.Scale(range=[20, 200]), legend=None "member_months:Q", scale=alt.Scale(range=[20, 200]), legend=None
), ),
@@ -396,7 +402,7 @@ def _(mo):
@app.cell(hide_code=True) @app.cell(hide_code=True)
def _(alt, mo, pl, q): def _(PALETTE, alt, mo, pl, q):
pmpm = q(""" pmpm = q("""
SELECT year_month, SELECT year_month,
member_months, member_months,
@@ -436,7 +442,7 @@ def _(alt, mo, pl, q):
x=alt.X("year_month:T", title="Month"), x=alt.X("year_month:T", title="Month"),
y=alt.Y("pmpm:Q", stack=True, title="PMPM ($)"), y=alt.Y("pmpm:Q", stack=True, title="PMPM ($)"),
color=alt.Color( color=alt.Color(
"category:N", title="Category", scale=alt.Scale(scheme="set2") "category:N", title="Category", scale=alt.Scale(range=PALETTE)
), ),
tooltip=[ tooltip=[
"year_month:T", "year_month:T",
@@ -449,7 +455,7 @@ def _(alt, mo, pl, q):
total_line = ( total_line = (
alt.Chart(pmpm.to_pandas()) alt.Chart(pmpm.to_pandas())
.mark_line(color="#e41a1c", strokeWidth=2, point=True) .mark_line(color=PALETTE[1], strokeWidth=2, point=True)
.encode( .encode(
x=alt.X("year_month:T", title="Month"), x=alt.X("year_month:T", title="Month"),
y=alt.Y("total_pmpm:Q", title="Total PMPM ($)"), y=alt.Y("total_pmpm:Q", title="Total PMPM ($)"),
@@ -471,7 +477,7 @@ def _(mo):
@app.cell(hide_code=True) @app.cell(hide_code=True)
def _(alt, mo, pl, q): def _(PALETTE, alt, mo, pl, q):
qm = q(""" qm = q("""
SELECT measure_id, measure_name, denominator_sum, numerator_sum, SELECT measure_id, measure_name, denominator_sum, numerator_sum,
exclusion_sum, performance_rate exclusion_sum, performance_rate
@@ -501,8 +507,8 @@ def _(alt, mo, pl, q):
y=alt.Y("measure_name:N", sort="-x", title=None), y=alt.Y("measure_name:N", sort="-x", title=None),
color=alt.condition( color=alt.condition(
alt.datum.performance_rate > 50, alt.datum.performance_rate > 50,
alt.value("#66c2a5"), alt.value(PALETTE[2]),
alt.value("#fc8d62"), alt.value(PALETTE[1]),
), ),
tooltip=[ tooltip=[
"measure_id:N", "measure_id:N",
@@ -533,7 +539,7 @@ def _(mo):
@app.cell(hide_code=True) @app.cell(hide_code=True)
def _(alt, mo, q): def _(PALETTE, alt, mo, q):
ed = q(""" ed = q("""
SELECT ed_classification_description AS ed_classification, SELECT ed_classification_description AS ed_classification,
count(*) AS visits, count(*) AS visits,
@@ -552,7 +558,7 @@ def _(alt, mo, q):
color=alt.Color( color=alt.Color(
"ed_classification:N", "ed_classification:N",
title="Classification", title="Classification",
scale=alt.Scale(scheme="set2"), scale=alt.Scale(range=PALETTE),
), ),
tooltip=[ tooltip=[
"ed_classification:N", "ed_classification:N",
@@ -576,7 +582,7 @@ def _(alt, mo, q):
), ),
y=alt.Y("avg_paid:Q", title="Avg Paid ($)"), y=alt.Y("avg_paid:Q", title="Avg Paid ($)"),
color=alt.Color( color=alt.Color(
"ed_classification:N", scale=alt.Scale(scheme="set2"), legend=None "ed_classification:N", scale=alt.Scale(range=PALETTE), legend=None
), ),
tooltip=["ed_classification:N", alt.Tooltip("avg_paid:Q", format="$,.0f")], tooltip=["ed_classification:N", alt.Tooltip("avg_paid:Q", format="$,.0f")],
) )
@@ -590,7 +596,7 @@ def _(alt, mo, q):
@app.cell(hide_code=True) @app.cell(hide_code=True)
def _(alt, mo, q): def _(PALETTE, alt, mo, q):
ed_dx = q(""" ed_dx = q("""
SELECT primary_diagnosis_description AS diagnosis, count(*) AS visits SELECT primary_diagnosis_description AS diagnosis, count(*) AS visits
FROM ed_classification.summary FROM ed_classification.summary
@@ -605,7 +611,7 @@ def _(alt, mo, q):
.encode( .encode(
x=alt.X("visits:Q", title="ED Visits"), x=alt.X("visits:Q", title="ED Visits"),
y=alt.Y("diagnosis:N", sort="-x", title=None), y=alt.Y("diagnosis:N", sort="-x", title=None),
color=alt.value("#8da0cb"), color=alt.value(PALETTE[0]),
tooltip=["diagnosis:N", "visits:Q"], tooltip=["diagnosis:N", "visits:Q"],
) )
.properties(title="Top 15 ED Diagnoses", width=700, height=400) .properties(title="Top 15 ED Diagnoses", width=700, height=400)
@@ -650,7 +656,7 @@ def _(mo, q):
@app.cell(hide_code=True) @app.cell(hide_code=True)
def _(alt, mo, q): def _(PALETTE, alt, mo, q):
readmit_cohort = q(""" readmit_cohort = q("""
SELECT specialty_cohort AS cohort, SELECT specialty_cohort AS cohort,
count(*) AS admissions, count(*) AS admissions,
@@ -670,8 +676,8 @@ def _(alt, mo, q):
y=alt.Y("cohort:N", sort="-x", title=None), y=alt.Y("cohort:N", sort="-x", title=None),
color=alt.condition( color=alt.condition(
alt.datum.rate > 20, alt.datum.rate > 20,
alt.value("#e41a1c"), alt.value(PALETTE[1]),
alt.value("#66c2a5"), alt.value(PALETTE[2]),
), ),
tooltip=[ tooltip=[
"cohort:N", "cohort:N",
@@ -696,7 +702,7 @@ def _(mo):
@app.cell(hide_code=True) @app.cell(hide_code=True)
def _(alt, mo, pl, q): def _(PALETTE, alt, mo, pl, q):
brand_gen = q(""" brand_gen = q("""
SELECT brand_vs_generic, count(*) AS claims, SELECT brand_vs_generic, count(*) AS claims,
round(sum(paid_amount), 2) AS total_paid round(sum(paid_amount), 2) AS total_paid
@@ -722,7 +728,7 @@ def _(alt, mo, pl, q):
color=alt.Color( color=alt.Color(
"brand_vs_generic:N", "brand_vs_generic:N",
scale=alt.Scale( scale=alt.Scale(
domain=["brand", "generic"], range=["#fc8d62", "#66c2a5"] domain=["brand", "generic"], range=[PALETTE[7], PALETTE[2]]
), ),
legend=None, legend=None,
), ),
@@ -774,7 +780,7 @@ def _(mo):
@app.cell(hide_code=True) @app.cell(hide_code=True)
def _(alt, mo, q): def _(PALETTE, alt, mo, q):
pqi = q(""" pqi = q("""
SELECT r.pqi_number, m.pqi_name AS pqi_description, SELECT r.pqi_number, m.pqi_name AS pqi_description,
r.rate_per_100_thousand, r.num_count, r.denom_count r.rate_per_100_thousand, r.num_count, r.denom_count
@@ -790,7 +796,7 @@ def _(alt, mo, q):
.encode( .encode(
x=alt.X("rate_per_100_thousand:Q", title="Rate per 100,000"), x=alt.X("rate_per_100_thousand:Q", title="Rate per 100,000"),
y=alt.Y("pqi_description:N", sort="-x", title=None), y=alt.Y("pqi_description:N", sort="-x", title=None),
color=alt.value("#8da0cb"), color=alt.value(PALETTE[0]),
tooltip=[ tooltip=[
"pqi_number:N", "pqi_number:N",
"pqi_description:N", "pqi_description:N",

View File

@@ -26,31 +26,20 @@ from __future__ import annotations
import html as html_mod import html as html_mod
import json import json
import sys as _sys
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path as _Path
from textwrap import dedent from textwrap import dedent
from aco.pipe.base import Pipeline _styles_dir = str(_Path(__file__).resolve().parents[2] / "styles")
if _styles_dir not in _sys.path:
_sys.path.insert(0, _styles_dir)
# Tableau 10 palette for pipeline schemas, gray for externals. from nature import EXTERNAL_COLOR as _EXTERNAL_COLOR # noqa: E402, I001
_SCHEMA_COLORS: dict[str, str] = { from nature import SCHEMA_COLORS as _SCHEMA_COLORS # noqa: E402, F401
"core": "#e15759", from nature import schema_color as _color # noqa: E402
"input_layer": "#76b7b2",
"cclf": "#4e79a7",
"claims_preprocessing": "#f28e2b",
"readmissions": "#59a14f",
"ahrq_measures": "#edc948",
"pharmacy": "#b07aa1",
"quality_measures": "#ff9da7",
"hcc_suspecting": "#9c755f",
"provider_attribution": "#bab0ac",
"main": "#86bcb6",
"data_quality": "#4e79a7",
}
_EXTERNAL_COLOR = "#888888"
from aco.pipe.base import Pipeline # noqa: E402
def _color(schema: str) -> str:
return _SCHEMA_COLORS.get(schema, _EXTERNAL_COLOR)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -271,7 +260,7 @@ def to_html(graph: Graph, *, title: str = "ACO Pipeline DAG") -> str:
<script src="https://unpkg.com/cytoscape-dagre@2/cytoscape-dagre.js"></script> <script src="https://unpkg.com/cytoscape-dagre@2/cytoscape-dagre.js"></script>
<style> <style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }} * {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ background: #1a1a2e; color: #e0e0e0; font-family: system-ui, sans-serif; }} body {{ background: #1a1a2e; color: #e0e0e0; font-family: Helvetica, Arial, sans-serif; }}
#cy {{ width: 100vw; height: 100vh; }} #cy {{ width: 100vw; height: 100vh; }}
#controls {{ #controls {{
position: fixed; top: 12px; left: 12px; z-index: 10; position: fixed; top: 12px; left: 12px; z-index: 10;

209
styles/nature.py Normal file
View File

@@ -0,0 +1,209 @@
"""Nature Research figure standards — shared palette and theme.
Implements the specifications from Nature's "Preparing Figures" guide
(bib key ``9ASETLJ4``). Provides:
- ``PALETTE`` — 8-colour accessible sequence (colorblind-safe)
- ``SCHEMA_COLORS`` — pipeline schema → colour mapping for DAG viz
- ``altair_theme()`` — register-and-enable for Altair charts
- ``mpl_rc()`` — dict of matplotlib rcParams
- ``cytoscape_style()`` — Cytoscape.js stylesheet fragment
Key rules (Nature):
* Sans-serif typeface (Helvetica, Arial)
* Body text 57 pt; panel labels 8 pt bold lowercase
* Axis lines and tick marks mandatory
* No background gridlines, drop shadows, or decorative elements
* RGB colour space; accessible palette
* Units in parentheses on axis labels
"""
from __future__ import annotations
# ── Accessible 8-colour palette ──────────────────────────────────────
# Adapted from Paul Tol's "bright" qualitative scheme — verified
# colourblind-safe via Coblis and WCAG contrast tools.
PALETTE = [
"#4477AA", # blue
"#EE6677", # red / rose
"#228833", # green
"#CCBB44", # yellow
"#66CCEE", # cyan
"#AA3377", # purple
"#BBBBBB", # grey
"#EE8866", # orange
]
# Semantic aliases
BLUE, RED, GREEN, YELLOW, CYAN, PURPLE, GREY, ORANGE = PALETTE
# ── Typography ────────────────────────────────────────────────────────
FONT_FAMILY = "Helvetica, Arial, sans-serif"
FONT_SIZE_BODY = 7 # pt — Nature range 5-7
FONT_SIZE_AXIS = 7 # pt
FONT_SIZE_TITLE = 8 # pt — panel label size
FONT_SIZE_LABEL = 8 # pt — bold lowercase panel labels
LINE_WIDTH = 0.75 # pt — axis/tick stroke
TICK_SIZE = 4 # pt
# ── Figure dimensions (single-column Nature width: 89 mm) ───────────
FIG_WIDTH_MM = 89
FIG_HEIGHT_MM = 60
FIG_WIDTH_IN = FIG_WIDTH_MM / 25.4
FIG_HEIGHT_IN = FIG_HEIGHT_MM / 25.4
DPI = 450 # Nature min for exported figures
# ── Background ────────────────────────────────────────────────────────
BG_DARK = "#1a1a2e" # app/notebook dark background
BG_CHART = "transparent" # chart area — let container bg show through
AXIS_COLOR = "#cccccc" # axis lines, ticks
TEXT_COLOR = "#e0e0e0" # labels on dark bg
TITLE_COLOR = "#ffffff" # title / panel label
# ── Pipeline schema colours (for DAG / lineage viz) ──────────────────
SCHEMA_COLORS: dict[str, str] = {
"core": BLUE,
"input_layer": CYAN,
"cclf": "#5588CC",
"claims_preprocessing": ORANGE,
"readmissions": GREEN,
"ahrq_measures": YELLOW,
"pharmacy": PURPLE,
"quality_measures": RED,
"hcc_suspecting": "#AA7744",
"provider_attribution": GREY,
"main": "#55AA99",
"data_quality": "#5588CC",
}
EXTERNAL_COLOR = "#888888"
def schema_color(schema: str) -> str:
"""Return the palette colour for a pipeline schema."""
return SCHEMA_COLORS.get(schema, EXTERNAL_COLOR)
# ── Altair theme ──────────────────────────────────────────────────────
def _altair_theme() -> dict:
"""Vega-Lite config dict conforming to Nature specs."""
return {
"config": {
"background": BG_CHART,
"font": FONT_FAMILY,
"title": {
"font": FONT_FAMILY,
"fontSize": FONT_SIZE_TITLE,
"fontWeight": "bold",
"color": TITLE_COLOR,
"anchor": "start",
},
"axis": {
"labelFont": FONT_FAMILY,
"labelFontSize": FONT_SIZE_AXIS,
"labelColor": TEXT_COLOR,
"titleFont": FONT_FAMILY,
"titleFontSize": FONT_SIZE_AXIS,
"titleColor": TEXT_COLOR,
"gridColor": "transparent",
"gridOpacity": 0,
"domainColor": AXIS_COLOR,
"domainWidth": LINE_WIDTH,
"tickColor": AXIS_COLOR,
"tickSize": TICK_SIZE,
"tickWidth": LINE_WIDTH,
},
"legend": {
"labelFont": FONT_FAMILY,
"labelFontSize": FONT_SIZE_BODY,
"labelColor": TEXT_COLOR,
"titleFont": FONT_FAMILY,
"titleFontSize": FONT_SIZE_BODY,
"titleColor": TEXT_COLOR,
"symbolSize": 60,
},
"view": {
"stroke": "transparent",
},
"range": {
"category": PALETTE,
},
"mark": {
"color": PALETTE[0],
},
"bar": {
"color": PALETTE[0],
},
"arc": {
"stroke": BG_DARK,
"strokeWidth": 1,
},
"line": {
"strokeWidth": 1.5,
},
"point": {
"size": 40,
},
"area": {
"opacity": 0.7,
},
}
}
def altair_theme() -> None:
"""Register and enable the Nature theme for Altair."""
import altair as alt
alt.themes.register("nature", _altair_theme)
alt.themes.enable("nature")
# ── Matplotlib rcParams ──────────────────────────────────────────────
def mpl_rc() -> dict:
"""Return an rcParams dict for ``matplotlib.rcParams.update()``."""
return {
"font.family": "sans-serif",
"font.sans-serif": ["Helvetica", "Arial", "DejaVu Sans"],
"font.size": FONT_SIZE_BODY,
"axes.titlesize": FONT_SIZE_TITLE,
"axes.titleweight": "bold",
"axes.labelsize": FONT_SIZE_AXIS,
"axes.linewidth": LINE_WIDTH,
"axes.edgecolor": AXIS_COLOR,
"axes.labelcolor": TEXT_COLOR,
"axes.facecolor": "none",
"axes.grid": False,
"axes.prop_cycle": __import__("cycler").cycler(color=PALETTE),
"xtick.major.size": TICK_SIZE,
"xtick.major.width": LINE_WIDTH,
"xtick.color": AXIS_COLOR,
"xtick.labelsize": FONT_SIZE_BODY,
"ytick.major.size": TICK_SIZE,
"ytick.major.width": LINE_WIDTH,
"ytick.color": AXIS_COLOR,
"ytick.labelsize": FONT_SIZE_BODY,
"figure.figsize": (FIG_WIDTH_IN, FIG_HEIGHT_IN),
"figure.dpi": DPI,
"figure.facecolor": BG_DARK,
"savefig.dpi": DPI,
"savefig.facecolor": BG_DARK,
"legend.fontsize": FONT_SIZE_BODY,
"legend.frameon": False,
"text.color": TEXT_COLOR,
}
# ── Cytoscape.js style helpers ───────────────────────────────────────
def cytoscape_node_style() -> dict:
"""Base Cytoscape.js node style properties (Nature-conformant)."""
return {
"font-family": FONT_FAMILY,
"font-size": "9px",
"text-valign": "center",
"text-halign": "center",
"shape": "round-rectangle",
}