Skip to content

Commit 10a5f4b

Browse files
authored
Merge pull request #25 from DoubleML/s-improve-styling
Update theme configuration and styling utilities
2 parents ddb008c + 1275da0 commit 10a5f4b

File tree

5 files changed

+415
-35
lines changed

5 files changed

+415
-35
lines changed

doc/index.qmd

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,26 @@ $$
2828
for a nominal coverage level is $1-\alpha$.
2929
The corresponding coverage results are highlighted according to the following color scheme:
3030

31-
* <span style="background-color: #00FF00; color: black; padding: 2px 5px; border-radius: 3px;">Green</span> if the deviation to the nominal level is below $5\%$
32-
* <span style="background-color: #FFFF00; color: black; padding: 2px 5px; border-radius: 3px;">Yellow</span> if the deviation to the nominal level is above $5\%$ and below $10\%$
33-
* <span style="background-color: #FF0000; color: black; padding: 2px 5px; border-radius: 3px;">Red</span> if the deviation to the nominal level is above $10\%$
31+
```{python}
32+
#| echo: false
33+
#| output: asis
34+
from utils.styling import get_coverage_tier_html_span
35+
36+
# Generate color legend using centralized configuration
37+
good_span = get_coverage_tier_html_span("good")
38+
medium_span = get_coverage_tier_html_span("medium")
39+
poor_span = get_coverage_tier_html_span("poor")
40+
41+
from IPython.display import Markdown, display
42+
43+
markdown_output = f"""
44+
* {good_span} if the deviation to the nominal level is below 5%
45+
* {medium_span} if the deviation to the nominal level is above 5% and below 10%
46+
* {poor_span} if the deviation to the nominal level is above 10%
47+
"""
48+
49+
display(Markdown(markdown_output))
50+
```
3451

3552
For simulations with multiple parameters of interest, usually pointwise and uniform coverage is assessed.
3653

@@ -247,5 +264,3 @@ fig.show()
247264
```
248265

249266
:::
250-
251-
:::

doc/styles.css

Lines changed: 146 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,146 @@
1-
/* css styles */
1+
/* Import Google Fonts */
2+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap');
3+
4+
/* Root font variables */
5+
:root {
6+
--font-family-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
7+
--font-family-mono: 'JetBrains Mono', 'SF Mono', Monaco, Inconsolata, 'Roboto Mono', 'Source Code Pro', monospace;
8+
}
9+
10+
/* Base typography */
11+
body {
12+
font-family: var(--font-family-sans);
13+
font-weight: 400;
14+
line-height: 1.6;
15+
font-feature-settings: 'kern' 1, 'liga' 1, 'calt' 1;
16+
}
17+
18+
/* Headings */
19+
h1,
20+
h2,
21+
h3,
22+
h4,
23+
h5,
24+
h6 {
25+
font-family: var(--font-family-sans);
26+
font-weight: 600;
27+
line-height: 1.3;
28+
letter-spacing: -0.025em;
29+
}
30+
31+
h1 {
32+
font-weight: 700;
33+
font-size: 2.25rem;
34+
}
35+
36+
h2 {
37+
font-weight: 600;
38+
font-size: 1.875rem;
39+
}
40+
41+
h3 {
42+
font-weight: 600;
43+
font-size: 1.5rem;
44+
}
45+
46+
h4 {
47+
font-weight: 500;
48+
font-size: 1.25rem;
49+
}
50+
51+
/* Code and pre-formatted text */
52+
code,
53+
pre,
54+
.sourceCode {
55+
font-family: var(--font-family-mono);
56+
font-weight: 400;
57+
font-feature-settings: 'liga' 1, 'calt' 1;
58+
}
59+
60+
/* Inline code */
61+
code:not(pre code) {
62+
font-size: 0.875em;
63+
font-weight: 500;
64+
padding: 0.125rem 0.25rem;
65+
background-color: rgba(175, 184, 193, 0.2);
66+
border-radius: 0.25rem;
67+
}
68+
69+
/* Code blocks */
70+
pre {
71+
font-size: 0.875rem;
72+
line-height: 1.5;
73+
padding: 1rem;
74+
border-radius: 0.5rem;
75+
overflow-x: auto;
76+
}
77+
78+
/* Navigation and UI elements */
79+
.navbar-brand,
80+
.nav-link {
81+
font-family: var(--font-family-sans);
82+
font-weight: 500;
83+
}
84+
85+
.sidebar .nav-link {
86+
font-weight: 400;
87+
}
88+
89+
.sidebar .nav-link.active {
90+
font-weight: 500;
91+
}
92+
93+
/* Tables */
94+
table {
95+
font-family: var(--font-family-sans);
96+
font-variant-numeric: tabular-nums;
97+
}
98+
99+
th {
100+
font-weight: 600;
101+
}
102+
103+
/* Math equations - ensure good readability */
104+
.math {
105+
font-family: 'STIX Two Math', 'Times New Roman', serif;
106+
}
107+
108+
/* Buttons and interactive elements */
109+
.btn {
110+
font-family: var(--font-family-sans);
111+
font-weight: 500;
112+
letter-spacing: 0.025em;
113+
}
114+
115+
/* Improve readability for long text */
116+
.content {
117+
max-width: none;
118+
}
119+
120+
p {
121+
margin-bottom: 1.25rem;
122+
}
123+
124+
/* List styling */
125+
ul,
126+
ol {
127+
margin-bottom: 1.25rem;
128+
}
129+
130+
li {
131+
margin-bottom: 0.5rem;
132+
}
133+
134+
/* Better spacing for equations */
135+
.math.display {
136+
margin: 1.5rem 0;
137+
}
138+
139+
/* Blockquotes */
140+
blockquote {
141+
font-style: italic;
142+
border-left: 4px solid #e9ecef;
143+
padding-left: 1rem;
144+
margin-left: 0;
145+
color: #6c757d;
146+
}

doc/utils/style_tables.py

Lines changed: 127 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,21 @@
33
from pandas.io.formats.style import Styler
44
from typing import Union, Optional, List, Any
55
from itables import show
6+
from .styling import (
7+
TABLE_STYLING,
8+
COVERAGE_THRESHOLDS,
9+
get_coverage_tier_css_props,
10+
)
611

712

8-
# Define highlighting tiers as a list of dictionaries or tuples
9-
# Each element defines: dist, props. Applied in order (later rules can override).
10-
# Order: from least specific (largest dist) to most specific (smallest dist)
11-
# or ensure the _apply_highlight_range logic correctly handles overlaps if props are different.
12-
# Current logic: more specific (smaller dist) rules are applied last and override.
13+
# Define highlighting tiers using centralized color configuration
1314
HIGHLIGHT_TIERS = [
14-
{"dist": 1.0, "props": "color:black;background-color:red;"},
15-
{"dist": 0.1, "props": "color:black;background-color:yellow;"},
16-
{"dist": 0.05, "props": "color:white;background-color:darkgreen;"},
15+
{"dist": COVERAGE_THRESHOLDS["poor"], "props": get_coverage_tier_css_props("poor")},
16+
{
17+
"dist": COVERAGE_THRESHOLDS["medium"],
18+
"props": get_coverage_tier_css_props("medium", "500"),
19+
},
20+
{"dist": COVERAGE_THRESHOLDS["good"], "props": get_coverage_tier_css_props("good")},
1721
]
1822

1923

@@ -27,19 +31,111 @@ def _apply_highlight_range(
2731
s_numeric = pd.to_numeric(
2832
s_col, errors="coerce"
2933
) # Convert to numeric, non-convertibles become NaN
34+
3035
# Apply style ONLY if value is WITHIN the current dist from level
31-
# This means for tiered styling, the order of applying styles in the calling function matters.
32-
# If a value falls into multiple dist categories, the LAST applied style for that dist will win.
33-
condition = (s_numeric >= level - dist) & (s_numeric <= level + dist)
36+
# Use absolute difference to determine which tier applies
37+
abs_diff = np.abs(s_numeric - level)
38+
condition = abs_diff <= dist
3439
return np.where(condition, props, "")
3540

3641

42+
def _determine_coverage_tier(value: float, level: float) -> str:
43+
"""
44+
Determine which coverage tier a value belongs to based on distance from level.
45+
Returns the most specific (smallest distance) tier that applies.
46+
"""
47+
if pd.isna(value):
48+
return ""
49+
50+
abs_diff = abs(value - level)
51+
52+
# Check tiers from most specific to least specific
53+
sorted_tiers = sorted(HIGHLIGHT_TIERS, key=lambda x: x["dist"])
54+
55+
for tier in sorted_tiers:
56+
if abs_diff <= tier["dist"]:
57+
return tier["props"]
58+
59+
return ""
60+
61+
62+
def _apply_base_table_styling(styler: Styler) -> Styler:
63+
"""
64+
Apply base styling to the table including headers, borders, and overall appearance.
65+
"""
66+
# Define CSS styles for clean table appearance using centralized colors
67+
styles = [
68+
# Table-wide styling
69+
{
70+
"selector": "table",
71+
"props": [
72+
("border-collapse", "separate"),
73+
("border-spacing", "0"),
74+
("width", "100%"),
75+
(
76+
"font-family",
77+
'"Segoe UI", -apple-system, BlinkMacSystemFont, "Roboto", sans-serif',
78+
),
79+
("font-size", "14px"),
80+
("line-height", "1.5"),
81+
("box-shadow", "0 2px 8px rgba(0,0,0,0.1)"),
82+
("border-radius", "8px"),
83+
("overflow", "hidden"),
84+
],
85+
},
86+
# Header styling
87+
{
88+
"selector": "thead th",
89+
"props": [
90+
("background-color", TABLE_STYLING["header_bg"]),
91+
("color", TABLE_STYLING["header_text"]),
92+
("font-weight", "600"),
93+
("text-align", "center"),
94+
("padding", "12px 16px"),
95+
("border-bottom", f'2px solid {TABLE_STYLING["border"]}'),
96+
("position", "sticky"),
97+
("top", "0"),
98+
("z-index", "10"),
99+
],
100+
},
101+
# Cell styling
102+
{
103+
"selector": "tbody td",
104+
"props": [
105+
("padding", "10px 16px"),
106+
("text-align", "center"),
107+
("border-bottom", f'1px solid {TABLE_STYLING["border"]}'),
108+
("transition", "background-color 0.2s ease"),
109+
],
110+
},
111+
# Row hover effect
112+
{
113+
"selector": "tbody tr:hover td",
114+
"props": [("background-color", TABLE_STYLING["hover_bg"])],
115+
},
116+
# Caption styling
117+
{
118+
"selector": "caption",
119+
"props": [
120+
("color", TABLE_STYLING["caption_color"]),
121+
("font-size", "16px"),
122+
("font-weight", "600"),
123+
("margin-bottom", "16px"),
124+
("text-align", "left"),
125+
("caption-side", "top"),
126+
],
127+
},
128+
]
129+
130+
return styler.set_table_styles(styles)
131+
132+
37133
def color_coverage_columns(
38134
styler: Styler, level: float, coverage_cols: list[str] = ["Coverage"]
39135
) -> Styler:
40136
"""
41137
Applies tiered highlighting to specified coverage columns of a Styler object.
42-
The order of application matters: more specific (narrower dist) rules are applied last to override.
138+
Uses non-overlapping logic to prevent CSS conflicts.
43139
"""
44140
if not isinstance(styler, Styler):
45141
raise TypeError("Expected a pandas Styler object.")
@@ -54,26 +150,28 @@ def color_coverage_columns(
54150
if not valid_coverage_cols:
55151
return styler # No valid columns to style
56152

57-
# Apply highlighting rules from the defined tiers
58-
# The order in HIGHLIGHT_TIERS is important if props are meant to override.
59-
# Pandas Styler.apply applies styles sequentially. If a cell matches multiple
60-
# conditions from different .apply calls, the styles from later calls typically override
61-
# or merge with earlier ones, depending on the CSS properties.
62-
# For background-color, later calls will override.
63-
current_styler = styler
64-
for tier in HIGHLIGHT_TIERS:
65-
current_styler = current_styler.apply(
66-
_apply_highlight_range,
67-
level=level,
68-
dist=tier["dist"],
69-
props=tier["props"],
70-
subset=valid_coverage_cols,
71-
)
153+
# Apply base styling first
154+
current_styler = _apply_base_table_styling(styler)
72155

73-
# Set font to bold for the coverage columns
156+
# Apply single tier styling to prevent conflicts
157+
def apply_coverage_tier_to_cell(s_col):
158+
"""Apply only the most appropriate coverage tier for each cell."""
159+
return s_col.apply(lambda x: _determine_coverage_tier(x, level))
160+
161+
current_styler = current_styler.apply(
162+
apply_coverage_tier_to_cell, subset=valid_coverage_cols
163+
)
164+
165+
# Apply additional styling to coverage columns for emphasis
74166
current_styler = current_styler.set_properties(
75-
**{"font-weight": "bold"}, subset=valid_coverage_cols
167+
**{
168+
"text-align": "center",
169+
"font-family": "monospace",
170+
"font-size": "13px",
171+
},
172+
subset=valid_coverage_cols,
76173
)
174+
77175
return current_styler
78176

79177

0 commit comments

Comments
 (0)