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