diff --git a/doc/index.qmd b/doc/index.qmd
index 3915d9a5..2302fd07 100644
--- a/doc/index.qmd
+++ b/doc/index.qmd
@@ -28,9 +28,26 @@ $$
for a nominal coverage level is $1-\alpha$.
The corresponding coverage results are highlighted according to the following color scheme:
-* Green if the deviation to the nominal level is below $5\%$
-* Yellow if the deviation to the nominal level is above $5\%$ and below $10\%$
-* Red if the deviation to the nominal level is above $10\%$
+```{python}
+#| echo: false
+#| output: asis
+from utils.styling import get_coverage_tier_html_span
+
+# Generate color legend using centralized configuration
+good_span = get_coverage_tier_html_span("good")
+medium_span = get_coverage_tier_html_span("medium")
+poor_span = get_coverage_tier_html_span("poor")
+
+from IPython.display import Markdown, display
+
+markdown_output = f"""
+* {good_span} if the deviation to the nominal level is below 5%
+* {medium_span} if the deviation to the nominal level is above 5% and below 10%
+* {poor_span} if the deviation to the nominal level is above 10%
+"""
+
+display(Markdown(markdown_output))
+```
For simulations with multiple parameters of interest, usually pointwise and uniform coverage is assessed.
@@ -247,5 +264,3 @@ fig.show()
```
:::
-
-:::
diff --git a/doc/styles.css b/doc/styles.css
index 2ddf50c7..951e1214 100644
--- a/doc/styles.css
+++ b/doc/styles.css
@@ -1 +1,146 @@
-/* css styles */
+/* Import Google Fonts */
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
+
+/* Root font variables */
+:root {
+ --font-family-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ --font-family-mono: 'JetBrains Mono', 'SF Mono', Monaco, Inconsolata, 'Roboto Mono', 'Source Code Pro', monospace;
+}
+
+/* Base typography */
+body {
+ font-family: var(--font-family-sans);
+ font-weight: 400;
+ line-height: 1.6;
+ font-feature-settings: 'kern' 1, 'liga' 1, 'calt' 1;
+}
+
+/* Headings */
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-family: var(--font-family-sans);
+ font-weight: 600;
+ line-height: 1.3;
+ letter-spacing: -0.025em;
+}
+
+h1 {
+ font-weight: 700;
+ font-size: 2.25rem;
+}
+
+h2 {
+ font-weight: 600;
+ font-size: 1.875rem;
+}
+
+h3 {
+ font-weight: 600;
+ font-size: 1.5rem;
+}
+
+h4 {
+ font-weight: 500;
+ font-size: 1.25rem;
+}
+
+/* Code and pre-formatted text */
+code,
+pre,
+.sourceCode {
+ font-family: var(--font-family-mono);
+ font-weight: 400;
+ font-feature-settings: 'liga' 1, 'calt' 1;
+}
+
+/* Inline code */
+code:not(pre code) {
+ font-size: 0.875em;
+ font-weight: 500;
+ padding: 0.125rem 0.25rem;
+ background-color: rgba(175, 184, 193, 0.2);
+ border-radius: 0.25rem;
+}
+
+/* Code blocks */
+pre {
+ font-size: 0.875rem;
+ line-height: 1.5;
+ padding: 1rem;
+ border-radius: 0.5rem;
+ overflow-x: auto;
+}
+
+/* Navigation and UI elements */
+.navbar-brand,
+.nav-link {
+ font-family: var(--font-family-sans);
+ font-weight: 500;
+}
+
+.sidebar .nav-link {
+ font-weight: 400;
+}
+
+.sidebar .nav-link.active {
+ font-weight: 500;
+}
+
+/* Tables */
+table {
+ font-family: var(--font-family-sans);
+ font-variant-numeric: tabular-nums;
+}
+
+th {
+ font-weight: 600;
+}
+
+/* Math equations - ensure good readability */
+.math {
+ font-family: 'STIX Two Math', 'Times New Roman', serif;
+}
+
+/* Buttons and interactive elements */
+.btn {
+ font-family: var(--font-family-sans);
+ font-weight: 500;
+ letter-spacing: 0.025em;
+}
+
+/* Improve readability for long text */
+.content {
+ max-width: none;
+}
+
+p {
+ margin-bottom: 1.25rem;
+}
+
+/* List styling */
+ul,
+ol {
+ margin-bottom: 1.25rem;
+}
+
+li {
+ margin-bottom: 0.5rem;
+}
+
+/* Better spacing for equations */
+.math.display {
+ margin: 1.5rem 0;
+}
+
+/* Blockquotes */
+blockquote {
+ font-style: italic;
+ border-left: 4px solid #e9ecef;
+ padding-left: 1rem;
+ margin-left: 0;
+ color: #6c757d;
+}
diff --git a/doc/utils/style_tables.py b/doc/utils/style_tables.py
index 5d734661..f932fe99 100644
--- a/doc/utils/style_tables.py
+++ b/doc/utils/style_tables.py
@@ -3,17 +3,21 @@
from pandas.io.formats.style import Styler
from typing import Union, Optional, List, Any
from itables import show
+from .styling import (
+ TABLE_STYLING,
+ COVERAGE_THRESHOLDS,
+ get_coverage_tier_css_props,
+)
-# Define highlighting tiers as a list of dictionaries or tuples
-# Each element defines: dist, props. Applied in order (later rules can override).
-# Order: from least specific (largest dist) to most specific (smallest dist)
-# or ensure the _apply_highlight_range logic correctly handles overlaps if props are different.
-# Current logic: more specific (smaller dist) rules are applied last and override.
+# Define highlighting tiers using centralized color configuration
HIGHLIGHT_TIERS = [
- {"dist": 1.0, "props": "color:black;background-color:red;"},
- {"dist": 0.1, "props": "color:black;background-color:yellow;"},
- {"dist": 0.05, "props": "color:white;background-color:darkgreen;"},
+ {"dist": COVERAGE_THRESHOLDS["poor"], "props": get_coverage_tier_css_props("poor")},
+ {
+ "dist": COVERAGE_THRESHOLDS["medium"],
+ "props": get_coverage_tier_css_props("medium", "500"),
+ },
+ {"dist": COVERAGE_THRESHOLDS["good"], "props": get_coverage_tier_css_props("good")},
]
@@ -27,19 +31,111 @@ def _apply_highlight_range(
s_numeric = pd.to_numeric(
s_col, errors="coerce"
) # Convert to numeric, non-convertibles become NaN
+
# Apply style ONLY if value is WITHIN the current dist from level
- # This means for tiered styling, the order of applying styles in the calling function matters.
- # If a value falls into multiple dist categories, the LAST applied style for that dist will win.
- condition = (s_numeric >= level - dist) & (s_numeric <= level + dist)
+ # Use absolute difference to determine which tier applies
+ abs_diff = np.abs(s_numeric - level)
+ condition = abs_diff <= dist
return np.where(condition, props, "")
+def _determine_coverage_tier(value: float, level: float) -> str:
+ """
+ Determine which coverage tier a value belongs to based on distance from level.
+ Returns the most specific (smallest distance) tier that applies.
+ """
+ if pd.isna(value):
+ return ""
+
+ abs_diff = abs(value - level)
+
+ # Check tiers from most specific to least specific
+ sorted_tiers = sorted(HIGHLIGHT_TIERS, key=lambda x: x["dist"])
+
+ for tier in sorted_tiers:
+ if abs_diff <= tier["dist"]:
+ return tier["props"]
+
+ return ""
+
+
+def _apply_base_table_styling(styler: Styler) -> Styler:
+ """
+ Apply base styling to the table including headers, borders, and overall appearance.
+ """
+ # Define CSS styles for clean table appearance using centralized colors
+ styles = [
+ # Table-wide styling
+ {
+ "selector": "table",
+ "props": [
+ ("border-collapse", "separate"),
+ ("border-spacing", "0"),
+ ("width", "100%"),
+ (
+ "font-family",
+ '"Segoe UI", -apple-system, BlinkMacSystemFont, "Roboto", sans-serif',
+ ),
+ ("font-size", "14px"),
+ ("line-height", "1.5"),
+ ("box-shadow", "0 2px 8px rgba(0,0,0,0.1)"),
+ ("border-radius", "8px"),
+ ("overflow", "hidden"),
+ ],
+ },
+ # Header styling
+ {
+ "selector": "thead th",
+ "props": [
+ ("background-color", TABLE_STYLING["header_bg"]),
+ ("color", TABLE_STYLING["header_text"]),
+ ("font-weight", "600"),
+ ("text-align", "center"),
+ ("padding", "12px 16px"),
+ ("border-bottom", f'2px solid {TABLE_STYLING["border"]}'),
+ ("position", "sticky"),
+ ("top", "0"),
+ ("z-index", "10"),
+ ],
+ },
+ # Cell styling
+ {
+ "selector": "tbody td",
+ "props": [
+ ("padding", "10px 16px"),
+ ("text-align", "center"),
+ ("border-bottom", f'1px solid {TABLE_STYLING["border"]}'),
+ ("transition", "background-color 0.2s ease"),
+ ],
+ },
+ # Row hover effect
+ {
+ "selector": "tbody tr:hover td",
+ "props": [("background-color", TABLE_STYLING["hover_bg"])],
+ },
+ # Caption styling
+ {
+ "selector": "caption",
+ "props": [
+ ("color", TABLE_STYLING["caption_color"]),
+ ("font-size", "16px"),
+ ("font-weight", "600"),
+ ("margin-bottom", "16px"),
+ ("text-align", "left"),
+ ("caption-side", "top"),
+ ],
+ },
+ ]
+
+ return styler.set_table_styles(styles)
+
+
def color_coverage_columns(
styler: Styler, level: float, coverage_cols: list[str] = ["Coverage"]
) -> Styler:
"""
Applies tiered highlighting to specified coverage columns of a Styler object.
- The order of application matters: more specific (narrower dist) rules are applied last to override.
+ Uses non-overlapping logic to prevent CSS conflicts.
"""
if not isinstance(styler, Styler):
raise TypeError("Expected a pandas Styler object.")
@@ -54,26 +150,28 @@ def color_coverage_columns(
if not valid_coverage_cols:
return styler # No valid columns to style
- # Apply highlighting rules from the defined tiers
- # The order in HIGHLIGHT_TIERS is important if props are meant to override.
- # Pandas Styler.apply applies styles sequentially. If a cell matches multiple
- # conditions from different .apply calls, the styles from later calls typically override
- # or merge with earlier ones, depending on the CSS properties.
- # For background-color, later calls will override.
- current_styler = styler
- for tier in HIGHLIGHT_TIERS:
- current_styler = current_styler.apply(
- _apply_highlight_range,
- level=level,
- dist=tier["dist"],
- props=tier["props"],
- subset=valid_coverage_cols,
- )
+ # Apply base styling first
+ current_styler = _apply_base_table_styling(styler)
- # Set font to bold for the coverage columns
+ # Apply single tier styling to prevent conflicts
+ def apply_coverage_tier_to_cell(s_col):
+ """Apply only the most appropriate coverage tier for each cell."""
+ return s_col.apply(lambda x: _determine_coverage_tier(x, level))
+
+ current_styler = current_styler.apply(
+ apply_coverage_tier_to_cell, subset=valid_coverage_cols
+ )
+
+ # Apply additional styling to coverage columns for emphasis
current_styler = current_styler.set_properties(
- **{"font-weight": "bold"}, subset=valid_coverage_cols
+ **{
+ "text-align": "center",
+ "font-family": "monospace",
+ "font-size": "13px",
+ },
+ subset=valid_coverage_cols,
)
+
return current_styler
diff --git a/doc/utils/styling.py b/doc/utils/styling.py
new file mode 100644
index 00000000..0362df67
--- /dev/null
+++ b/doc/utils/styling.py
@@ -0,0 +1,91 @@
+"""
+Styling utilities for DoubleML Coverage tables and documentation.
+
+This module provides helper functions for applying consistent styling
+based on the centralized theme configuration.
+"""
+
+import yaml
+from pathlib import Path
+from typing import Dict, Any
+import copy
+
+
+def _load_theme_config() -> Dict[str, Any]:
+ """Load theme configuration from YAML file."""
+ config_path = Path(__file__).parent / "theme.yml"
+ with open(config_path, "r") as f:
+ return yaml.safe_load(f)
+
+
+# Load configuration once at module import
+_THEME = _load_theme_config()
+
+# Expose configuration for backward compatibility and direct access
+COVERAGE_COLORS = _THEME["coverage_colors"]
+TABLE_STYLING = _THEME["table_styling"]
+COVERAGE_THRESHOLDS = _THEME["coverage_thresholds"]
+
+
+def get_coverage_tier_css_props(tier: str, font_weight: str = "600") -> str:
+ """
+ Generate CSS properties string for a coverage performance tier.
+
+ Args:
+ tier: One of 'good', 'medium', 'poor'
+ font_weight: CSS font-weight value
+
+ Returns:
+ CSS properties string for use with pandas Styler
+ """
+ if tier not in COVERAGE_COLORS:
+ raise ValueError(
+ f"Unknown tier '{tier}'. Must be one of: {list(COVERAGE_COLORS.keys())}"
+ )
+
+ colors = COVERAGE_COLORS[tier]
+ return (
+ f"color:{colors['text']};"
+ f"background-color:{colors['background']};"
+ f"border-left:4px solid {colors['border']};"
+ f"font-weight:{font_weight};"
+ )
+
+
+def get_coverage_tier_html_span(tier: str, text: str = None) -> str:
+ """
+ Generate HTML span element with coverage tier styling for documentation.
+
+ Args:
+ tier: One of 'good', 'medium', 'poor'
+ text: Text to display (defaults to tier description)
+
+ Returns:
+ HTML span element with inline styling
+ """
+ if tier not in COVERAGE_COLORS:
+ raise ValueError(
+ f"Unknown tier '{tier}'. Must be one of: {list(COVERAGE_COLORS.keys())}"
+ )
+
+ colors = COVERAGE_COLORS[tier]
+ display_text = text or colors["description"]
+
+ return (
+ f''
+ f"{display_text}"
+ )
+
+
+def get_theme_config() -> Dict[str, Any]:
+ """
+ Get the complete theme configuration.
+
+ Returns:
+ Dictionary containing all theme settings
+ """
+ return copy.deepcopy(_THEME)
diff --git a/doc/utils/theme.yml b/doc/utils/theme.yml
new file mode 100644
index 00000000..3e98bed3
--- /dev/null
+++ b/doc/utils/theme.yml
@@ -0,0 +1,31 @@
+# DoubleML Coverage Theme Configuration
+# Central color palette and styling settings
+
+coverage_colors:
+ good:
+ background: "#d1e7dd"
+ text: "#0f5132"
+ border: "#198754"
+ description: "Green"
+ medium:
+ background: "#fff3cd"
+ text: "#856404"
+ border: "#ffc107"
+ description: "Amber"
+ poor:
+ background: "#f8d7da"
+ text: "#721c24"
+ border: "#dc3545"
+ description: "Coral"
+
+table_styling:
+ header_bg: "#f8f9fa"
+ header_text: "#495057"
+ border: "#dee2e6"
+ caption_color: "#6c757d"
+ hover_bg: "#f5f5f5"
+
+coverage_thresholds:
+ good: 0.05 # Within 5% of nominal level
+ medium: 0.1 # Within 10% of nominal level
+ poor: 1.0 # Beyond 10% of nominal level