ml4t-diagnostic 0.1.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. ml4t/diagnostic/AGENT.md +25 -0
  2. ml4t/diagnostic/__init__.py +166 -0
  3. ml4t/diagnostic/backends/__init__.py +10 -0
  4. ml4t/diagnostic/backends/adapter.py +192 -0
  5. ml4t/diagnostic/backends/polars_backend.py +899 -0
  6. ml4t/diagnostic/caching/__init__.py +40 -0
  7. ml4t/diagnostic/caching/cache.py +331 -0
  8. ml4t/diagnostic/caching/decorators.py +131 -0
  9. ml4t/diagnostic/caching/smart_cache.py +339 -0
  10. ml4t/diagnostic/config/AGENT.md +24 -0
  11. ml4t/diagnostic/config/README.md +267 -0
  12. ml4t/diagnostic/config/__init__.py +219 -0
  13. ml4t/diagnostic/config/barrier_config.py +277 -0
  14. ml4t/diagnostic/config/base.py +301 -0
  15. ml4t/diagnostic/config/event_config.py +148 -0
  16. ml4t/diagnostic/config/feature_config.py +404 -0
  17. ml4t/diagnostic/config/multi_signal_config.py +55 -0
  18. ml4t/diagnostic/config/portfolio_config.py +215 -0
  19. ml4t/diagnostic/config/report_config.py +391 -0
  20. ml4t/diagnostic/config/sharpe_config.py +202 -0
  21. ml4t/diagnostic/config/signal_config.py +206 -0
  22. ml4t/diagnostic/config/trade_analysis_config.py +310 -0
  23. ml4t/diagnostic/config/validation.py +279 -0
  24. ml4t/diagnostic/core/__init__.py +29 -0
  25. ml4t/diagnostic/core/numba_utils.py +315 -0
  26. ml4t/diagnostic/core/purging.py +372 -0
  27. ml4t/diagnostic/core/sampling.py +471 -0
  28. ml4t/diagnostic/errors/__init__.py +205 -0
  29. ml4t/diagnostic/evaluation/AGENT.md +26 -0
  30. ml4t/diagnostic/evaluation/__init__.py +437 -0
  31. ml4t/diagnostic/evaluation/autocorrelation.py +531 -0
  32. ml4t/diagnostic/evaluation/barrier_analysis.py +1050 -0
  33. ml4t/diagnostic/evaluation/binary_metrics.py +910 -0
  34. ml4t/diagnostic/evaluation/dashboard.py +715 -0
  35. ml4t/diagnostic/evaluation/diagnostic_plots.py +1037 -0
  36. ml4t/diagnostic/evaluation/distribution/__init__.py +499 -0
  37. ml4t/diagnostic/evaluation/distribution/moments.py +299 -0
  38. ml4t/diagnostic/evaluation/distribution/tails.py +777 -0
  39. ml4t/diagnostic/evaluation/distribution/tests.py +470 -0
  40. ml4t/diagnostic/evaluation/drift/__init__.py +139 -0
  41. ml4t/diagnostic/evaluation/drift/analysis.py +432 -0
  42. ml4t/diagnostic/evaluation/drift/domain_classifier.py +517 -0
  43. ml4t/diagnostic/evaluation/drift/population_stability_index.py +310 -0
  44. ml4t/diagnostic/evaluation/drift/wasserstein.py +388 -0
  45. ml4t/diagnostic/evaluation/event_analysis.py +647 -0
  46. ml4t/diagnostic/evaluation/excursion.py +390 -0
  47. ml4t/diagnostic/evaluation/feature_diagnostics.py +873 -0
  48. ml4t/diagnostic/evaluation/feature_outcome.py +666 -0
  49. ml4t/diagnostic/evaluation/framework.py +935 -0
  50. ml4t/diagnostic/evaluation/metric_registry.py +255 -0
  51. ml4t/diagnostic/evaluation/metrics/AGENT.md +23 -0
  52. ml4t/diagnostic/evaluation/metrics/__init__.py +133 -0
  53. ml4t/diagnostic/evaluation/metrics/basic.py +160 -0
  54. ml4t/diagnostic/evaluation/metrics/conditional_ic.py +469 -0
  55. ml4t/diagnostic/evaluation/metrics/feature_outcome.py +475 -0
  56. ml4t/diagnostic/evaluation/metrics/ic_statistics.py +446 -0
  57. ml4t/diagnostic/evaluation/metrics/importance_analysis.py +338 -0
  58. ml4t/diagnostic/evaluation/metrics/importance_classical.py +375 -0
  59. ml4t/diagnostic/evaluation/metrics/importance_mda.py +371 -0
  60. ml4t/diagnostic/evaluation/metrics/importance_shap.py +715 -0
  61. ml4t/diagnostic/evaluation/metrics/information_coefficient.py +527 -0
  62. ml4t/diagnostic/evaluation/metrics/interactions.py +772 -0
  63. ml4t/diagnostic/evaluation/metrics/monotonicity.py +226 -0
  64. ml4t/diagnostic/evaluation/metrics/risk_adjusted.py +324 -0
  65. ml4t/diagnostic/evaluation/multi_signal.py +550 -0
  66. ml4t/diagnostic/evaluation/portfolio_analysis/__init__.py +83 -0
  67. ml4t/diagnostic/evaluation/portfolio_analysis/analysis.py +734 -0
  68. ml4t/diagnostic/evaluation/portfolio_analysis/metrics.py +589 -0
  69. ml4t/diagnostic/evaluation/portfolio_analysis/results.py +334 -0
  70. ml4t/diagnostic/evaluation/report_generation.py +824 -0
  71. ml4t/diagnostic/evaluation/signal_selector.py +452 -0
  72. ml4t/diagnostic/evaluation/stat_registry.py +139 -0
  73. ml4t/diagnostic/evaluation/stationarity/__init__.py +97 -0
  74. ml4t/diagnostic/evaluation/stationarity/analysis.py +518 -0
  75. ml4t/diagnostic/evaluation/stationarity/augmented_dickey_fuller.py +296 -0
  76. ml4t/diagnostic/evaluation/stationarity/kpss_test.py +308 -0
  77. ml4t/diagnostic/evaluation/stationarity/phillips_perron.py +365 -0
  78. ml4t/diagnostic/evaluation/stats/AGENT.md +43 -0
  79. ml4t/diagnostic/evaluation/stats/__init__.py +191 -0
  80. ml4t/diagnostic/evaluation/stats/backtest_overfitting.py +219 -0
  81. ml4t/diagnostic/evaluation/stats/bootstrap.py +228 -0
  82. ml4t/diagnostic/evaluation/stats/deflated_sharpe_ratio.py +591 -0
  83. ml4t/diagnostic/evaluation/stats/false_discovery_rate.py +295 -0
  84. ml4t/diagnostic/evaluation/stats/hac_standard_errors.py +108 -0
  85. ml4t/diagnostic/evaluation/stats/minimum_track_record.py +408 -0
  86. ml4t/diagnostic/evaluation/stats/moments.py +164 -0
  87. ml4t/diagnostic/evaluation/stats/rademacher_adjustment.py +436 -0
  88. ml4t/diagnostic/evaluation/stats/reality_check.py +155 -0
  89. ml4t/diagnostic/evaluation/stats/sharpe_inference.py +219 -0
  90. ml4t/diagnostic/evaluation/themes.py +330 -0
  91. ml4t/diagnostic/evaluation/threshold_analysis.py +957 -0
  92. ml4t/diagnostic/evaluation/trade_analysis.py +1136 -0
  93. ml4t/diagnostic/evaluation/trade_dashboard/__init__.py +32 -0
  94. ml4t/diagnostic/evaluation/trade_dashboard/app.py +315 -0
  95. ml4t/diagnostic/evaluation/trade_dashboard/export/__init__.py +18 -0
  96. ml4t/diagnostic/evaluation/trade_dashboard/export/csv.py +82 -0
  97. ml4t/diagnostic/evaluation/trade_dashboard/export/html.py +276 -0
  98. ml4t/diagnostic/evaluation/trade_dashboard/io.py +166 -0
  99. ml4t/diagnostic/evaluation/trade_dashboard/normalize.py +304 -0
  100. ml4t/diagnostic/evaluation/trade_dashboard/stats.py +386 -0
  101. ml4t/diagnostic/evaluation/trade_dashboard/style.py +79 -0
  102. ml4t/diagnostic/evaluation/trade_dashboard/tabs/__init__.py +21 -0
  103. ml4t/diagnostic/evaluation/trade_dashboard/tabs/patterns.py +354 -0
  104. ml4t/diagnostic/evaluation/trade_dashboard/tabs/shap_analysis.py +280 -0
  105. ml4t/diagnostic/evaluation/trade_dashboard/tabs/stat_validation.py +186 -0
  106. ml4t/diagnostic/evaluation/trade_dashboard/tabs/worst_trades.py +236 -0
  107. ml4t/diagnostic/evaluation/trade_dashboard/types.py +129 -0
  108. ml4t/diagnostic/evaluation/trade_shap/__init__.py +102 -0
  109. ml4t/diagnostic/evaluation/trade_shap/alignment.py +188 -0
  110. ml4t/diagnostic/evaluation/trade_shap/characterize.py +413 -0
  111. ml4t/diagnostic/evaluation/trade_shap/cluster.py +302 -0
  112. ml4t/diagnostic/evaluation/trade_shap/explain.py +208 -0
  113. ml4t/diagnostic/evaluation/trade_shap/hypotheses/__init__.py +23 -0
  114. ml4t/diagnostic/evaluation/trade_shap/hypotheses/generator.py +290 -0
  115. ml4t/diagnostic/evaluation/trade_shap/hypotheses/matcher.py +251 -0
  116. ml4t/diagnostic/evaluation/trade_shap/hypotheses/templates.yaml +467 -0
  117. ml4t/diagnostic/evaluation/trade_shap/models.py +386 -0
  118. ml4t/diagnostic/evaluation/trade_shap/normalize.py +116 -0
  119. ml4t/diagnostic/evaluation/trade_shap/pipeline.py +263 -0
  120. ml4t/diagnostic/evaluation/trade_shap_dashboard.py +283 -0
  121. ml4t/diagnostic/evaluation/trade_shap_diagnostics.py +588 -0
  122. ml4t/diagnostic/evaluation/validated_cv.py +535 -0
  123. ml4t/diagnostic/evaluation/visualization.py +1050 -0
  124. ml4t/diagnostic/evaluation/volatility/__init__.py +45 -0
  125. ml4t/diagnostic/evaluation/volatility/analysis.py +351 -0
  126. ml4t/diagnostic/evaluation/volatility/arch.py +258 -0
  127. ml4t/diagnostic/evaluation/volatility/garch.py +460 -0
  128. ml4t/diagnostic/integration/__init__.py +48 -0
  129. ml4t/diagnostic/integration/backtest_contract.py +671 -0
  130. ml4t/diagnostic/integration/data_contract.py +316 -0
  131. ml4t/diagnostic/integration/engineer_contract.py +226 -0
  132. ml4t/diagnostic/logging/__init__.py +77 -0
  133. ml4t/diagnostic/logging/logger.py +245 -0
  134. ml4t/diagnostic/logging/performance.py +234 -0
  135. ml4t/diagnostic/logging/progress.py +234 -0
  136. ml4t/diagnostic/logging/wandb.py +412 -0
  137. ml4t/diagnostic/metrics/__init__.py +9 -0
  138. ml4t/diagnostic/metrics/percentiles.py +128 -0
  139. ml4t/diagnostic/py.typed +1 -0
  140. ml4t/diagnostic/reporting/__init__.py +43 -0
  141. ml4t/diagnostic/reporting/base.py +130 -0
  142. ml4t/diagnostic/reporting/html_renderer.py +275 -0
  143. ml4t/diagnostic/reporting/json_renderer.py +51 -0
  144. ml4t/diagnostic/reporting/markdown_renderer.py +117 -0
  145. ml4t/diagnostic/results/AGENT.md +24 -0
  146. ml4t/diagnostic/results/__init__.py +105 -0
  147. ml4t/diagnostic/results/barrier_results/__init__.py +36 -0
  148. ml4t/diagnostic/results/barrier_results/hit_rate.py +304 -0
  149. ml4t/diagnostic/results/barrier_results/precision_recall.py +266 -0
  150. ml4t/diagnostic/results/barrier_results/profit_factor.py +297 -0
  151. ml4t/diagnostic/results/barrier_results/tearsheet.py +397 -0
  152. ml4t/diagnostic/results/barrier_results/time_to_target.py +305 -0
  153. ml4t/diagnostic/results/barrier_results/validation.py +38 -0
  154. ml4t/diagnostic/results/base.py +177 -0
  155. ml4t/diagnostic/results/event_results.py +349 -0
  156. ml4t/diagnostic/results/feature_results.py +787 -0
  157. ml4t/diagnostic/results/multi_signal_results.py +431 -0
  158. ml4t/diagnostic/results/portfolio_results.py +281 -0
  159. ml4t/diagnostic/results/sharpe_results.py +448 -0
  160. ml4t/diagnostic/results/signal_results/__init__.py +74 -0
  161. ml4t/diagnostic/results/signal_results/ic.py +581 -0
  162. ml4t/diagnostic/results/signal_results/irtc.py +110 -0
  163. ml4t/diagnostic/results/signal_results/quantile.py +392 -0
  164. ml4t/diagnostic/results/signal_results/tearsheet.py +456 -0
  165. ml4t/diagnostic/results/signal_results/turnover.py +213 -0
  166. ml4t/diagnostic/results/signal_results/validation.py +147 -0
  167. ml4t/diagnostic/signal/AGENT.md +17 -0
  168. ml4t/diagnostic/signal/__init__.py +69 -0
  169. ml4t/diagnostic/signal/_report.py +152 -0
  170. ml4t/diagnostic/signal/_utils.py +261 -0
  171. ml4t/diagnostic/signal/core.py +275 -0
  172. ml4t/diagnostic/signal/quantile.py +148 -0
  173. ml4t/diagnostic/signal/result.py +214 -0
  174. ml4t/diagnostic/signal/signal_ic.py +129 -0
  175. ml4t/diagnostic/signal/turnover.py +182 -0
  176. ml4t/diagnostic/splitters/AGENT.md +19 -0
  177. ml4t/diagnostic/splitters/__init__.py +36 -0
  178. ml4t/diagnostic/splitters/base.py +501 -0
  179. ml4t/diagnostic/splitters/calendar.py +421 -0
  180. ml4t/diagnostic/splitters/calendar_config.py +91 -0
  181. ml4t/diagnostic/splitters/combinatorial.py +1064 -0
  182. ml4t/diagnostic/splitters/config.py +322 -0
  183. ml4t/diagnostic/splitters/cpcv/__init__.py +57 -0
  184. ml4t/diagnostic/splitters/cpcv/combinations.py +119 -0
  185. ml4t/diagnostic/splitters/cpcv/partitioning.py +263 -0
  186. ml4t/diagnostic/splitters/cpcv/purge_engine.py +379 -0
  187. ml4t/diagnostic/splitters/cpcv/windows.py +190 -0
  188. ml4t/diagnostic/splitters/group_isolation.py +329 -0
  189. ml4t/diagnostic/splitters/persistence.py +316 -0
  190. ml4t/diagnostic/splitters/utils.py +207 -0
  191. ml4t/diagnostic/splitters/walk_forward.py +757 -0
  192. ml4t/diagnostic/utils/__init__.py +42 -0
  193. ml4t/diagnostic/utils/config.py +542 -0
  194. ml4t/diagnostic/utils/dependencies.py +318 -0
  195. ml4t/diagnostic/utils/sessions.py +127 -0
  196. ml4t/diagnostic/validation/__init__.py +54 -0
  197. ml4t/diagnostic/validation/dataframe.py +274 -0
  198. ml4t/diagnostic/validation/returns.py +280 -0
  199. ml4t/diagnostic/validation/timeseries.py +299 -0
  200. ml4t/diagnostic/visualization/AGENT.md +19 -0
  201. ml4t/diagnostic/visualization/__init__.py +223 -0
  202. ml4t/diagnostic/visualization/backtest/__init__.py +98 -0
  203. ml4t/diagnostic/visualization/backtest/cost_attribution.py +762 -0
  204. ml4t/diagnostic/visualization/backtest/executive_summary.py +895 -0
  205. ml4t/diagnostic/visualization/backtest/interactive_controls.py +673 -0
  206. ml4t/diagnostic/visualization/backtest/statistical_validity.py +874 -0
  207. ml4t/diagnostic/visualization/backtest/tearsheet.py +565 -0
  208. ml4t/diagnostic/visualization/backtest/template_system.py +373 -0
  209. ml4t/diagnostic/visualization/backtest/trade_plots.py +1172 -0
  210. ml4t/diagnostic/visualization/barrier_plots.py +782 -0
  211. ml4t/diagnostic/visualization/core.py +1060 -0
  212. ml4t/diagnostic/visualization/dashboards/__init__.py +36 -0
  213. ml4t/diagnostic/visualization/dashboards/base.py +582 -0
  214. ml4t/diagnostic/visualization/dashboards/importance.py +801 -0
  215. ml4t/diagnostic/visualization/dashboards/interaction.py +263 -0
  216. ml4t/diagnostic/visualization/dashboards.py +43 -0
  217. ml4t/diagnostic/visualization/data_extraction/__init__.py +48 -0
  218. ml4t/diagnostic/visualization/data_extraction/importance.py +649 -0
  219. ml4t/diagnostic/visualization/data_extraction/interaction.py +504 -0
  220. ml4t/diagnostic/visualization/data_extraction/types.py +113 -0
  221. ml4t/diagnostic/visualization/data_extraction/validation.py +66 -0
  222. ml4t/diagnostic/visualization/feature_plots.py +888 -0
  223. ml4t/diagnostic/visualization/interaction_plots.py +618 -0
  224. ml4t/diagnostic/visualization/portfolio/__init__.py +41 -0
  225. ml4t/diagnostic/visualization/portfolio/dashboard.py +514 -0
  226. ml4t/diagnostic/visualization/portfolio/drawdown_plots.py +341 -0
  227. ml4t/diagnostic/visualization/portfolio/returns_plots.py +487 -0
  228. ml4t/diagnostic/visualization/portfolio/risk_plots.py +301 -0
  229. ml4t/diagnostic/visualization/report_generation.py +1343 -0
  230. ml4t/diagnostic/visualization/signal/__init__.py +103 -0
  231. ml4t/diagnostic/visualization/signal/dashboard.py +911 -0
  232. ml4t/diagnostic/visualization/signal/event_plots.py +514 -0
  233. ml4t/diagnostic/visualization/signal/ic_plots.py +635 -0
  234. ml4t/diagnostic/visualization/signal/multi_signal_dashboard.py +974 -0
  235. ml4t/diagnostic/visualization/signal/multi_signal_plots.py +603 -0
  236. ml4t/diagnostic/visualization/signal/quantile_plots.py +625 -0
  237. ml4t/diagnostic/visualization/signal/turnover_plots.py +400 -0
  238. ml4t/diagnostic/visualization/trade_shap/__init__.py +90 -0
  239. ml4t_diagnostic-0.1.0a1.dist-info/METADATA +1044 -0
  240. ml4t_diagnostic-0.1.0a1.dist-info/RECORD +242 -0
  241. ml4t_diagnostic-0.1.0a1.dist-info/WHEEL +4 -0
  242. ml4t_diagnostic-0.1.0a1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,673 @@
1
+ """Interactive controls for backtest tearsheets.
2
+
3
+ Provides JavaScript-based UI components for interactive tearsheets:
4
+ - Date range selector with presets (1M, 3M, YTD, 1Y, All)
5
+ - Metric filter dropdown
6
+ - Section navigation
7
+ - Drill-down functionality
8
+
9
+ These controls enhance the HTML tearsheet with client-side interactivity.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from datetime import date
15
+ from typing import TYPE_CHECKING
16
+
17
+ if TYPE_CHECKING:
18
+ pass
19
+
20
+
21
+ # =============================================================================
22
+ # Date Range Selector
23
+ # =============================================================================
24
+
25
+ DATE_RANGE_PRESETS = {
26
+ "1M": 30,
27
+ "3M": 90,
28
+ "6M": 180,
29
+ "YTD": "ytd",
30
+ "1Y": 365,
31
+ "3Y": 1095,
32
+ "5Y": 1825,
33
+ "All": None,
34
+ }
35
+
36
+
37
+ def get_date_range_html(
38
+ start_date: date | str | None = None,
39
+ end_date: date | str | None = None,
40
+ presets: list[str] | None = None,
41
+ default_preset: str = "All",
42
+ on_change_callback: str = "onDateRangeChange",
43
+ ) -> str:
44
+ """Generate HTML/JS for a date range selector with presets.
45
+
46
+ Parameters
47
+ ----------
48
+ start_date : date or str, optional
49
+ Minimum selectable date (data start)
50
+ end_date : date or str, optional
51
+ Maximum selectable date (data end)
52
+ presets : list[str], optional
53
+ Preset buttons to show (default: ["1M", "3M", "YTD", "1Y", "All"])
54
+ default_preset : str
55
+ Initially selected preset
56
+ on_change_callback : str
57
+ JavaScript function name to call on date change
58
+
59
+ Returns
60
+ -------
61
+ str
62
+ HTML string with date range selector
63
+ """
64
+ if presets is None:
65
+ presets = ["1M", "3M", "YTD", "1Y", "All"]
66
+
67
+ # Convert dates to strings
68
+ if isinstance(start_date, date):
69
+ start_date = start_date.isoformat()
70
+ if isinstance(end_date, date):
71
+ end_date = end_date.isoformat()
72
+
73
+ # Build preset buttons
74
+ preset_buttons = []
75
+ for preset in presets:
76
+ active = "active" if preset == default_preset else ""
77
+ preset_buttons.append(
78
+ f'<button class="date-preset-btn {active}" data-preset="{preset}">{preset}</button>'
79
+ )
80
+
81
+ return f"""
82
+ <div class="date-range-selector">
83
+ <div class="preset-buttons">
84
+ {"".join(preset_buttons)}
85
+ </div>
86
+ <div class="custom-range">
87
+ <input type="date" id="start-date" value="{start_date or ""}" min="{start_date or ""}" max="{end_date or ""}">
88
+ <span>to</span>
89
+ <input type="date" id="end-date" value="{end_date or ""}" min="{start_date or ""}" max="{end_date or ""}">
90
+ </div>
91
+ </div>
92
+
93
+ <style>
94
+ .date-range-selector {{
95
+ display: flex;
96
+ align-items: center;
97
+ gap: 20px;
98
+ padding: 10px 0;
99
+ margin-bottom: 20px;
100
+ }}
101
+ .preset-buttons {{
102
+ display: flex;
103
+ gap: 5px;
104
+ }}
105
+ .date-preset-btn {{
106
+ padding: 6px 12px;
107
+ border: 1px solid #ddd;
108
+ background: #f8f9fa;
109
+ border-radius: 4px;
110
+ cursor: pointer;
111
+ font-size: 13px;
112
+ transition: all 0.2s;
113
+ }}
114
+ .date-preset-btn:hover {{
115
+ background: #e9ecef;
116
+ }}
117
+ .date-preset-btn.active {{
118
+ background: #636EFA;
119
+ color: white;
120
+ border-color: #636EFA;
121
+ }}
122
+ .custom-range {{
123
+ display: flex;
124
+ align-items: center;
125
+ gap: 8px;
126
+ }}
127
+ .custom-range input {{
128
+ padding: 6px 10px;
129
+ border: 1px solid #ddd;
130
+ border-radius: 4px;
131
+ font-size: 13px;
132
+ }}
133
+ </style>
134
+
135
+ <script>
136
+ (function() {{
137
+ const startDate = '{start_date or ""}';
138
+ const endDate = '{end_date or ""}';
139
+
140
+ document.querySelectorAll('.date-preset-btn').forEach(btn => {{
141
+ btn.addEventListener('click', function() {{
142
+ // Update active state
143
+ document.querySelectorAll('.date-preset-btn').forEach(b => b.classList.remove('active'));
144
+ this.classList.add('active');
145
+
146
+ const preset = this.dataset.preset;
147
+ let newStart, newEnd = endDate;
148
+
149
+ if (preset === 'All') {{
150
+ newStart = startDate;
151
+ }} else if (preset === 'YTD') {{
152
+ const now = new Date(endDate);
153
+ newStart = new Date(now.getFullYear(), 0, 1).toISOString().split('T')[0];
154
+ }} else {{
155
+ const days = {{"1M": 30, "3M": 90, "6M": 180, "1Y": 365, "3Y": 1095, "5Y": 1825}}[preset];
156
+ if (days) {{
157
+ const end = new Date(endDate);
158
+ newStart = new Date(end - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
159
+ }}
160
+ }}
161
+
162
+ document.getElementById('start-date').value = newStart;
163
+ document.getElementById('end-date').value = newEnd;
164
+
165
+ if (typeof {on_change_callback} === 'function') {{
166
+ {on_change_callback}(newStart, newEnd);
167
+ }}
168
+ }});
169
+ }});
170
+
171
+ // Custom date inputs
172
+ ['start-date', 'end-date'].forEach(id => {{
173
+ document.getElementById(id).addEventListener('change', function() {{
174
+ const newStart = document.getElementById('start-date').value;
175
+ const newEnd = document.getElementById('end-date').value;
176
+
177
+ // Clear preset active state
178
+ document.querySelectorAll('.date-preset-btn').forEach(b => b.classList.remove('active'));
179
+
180
+ if (typeof {on_change_callback} === 'function') {{
181
+ {on_change_callback}(newStart, newEnd);
182
+ }}
183
+ }});
184
+ }});
185
+ }})();
186
+ </script>
187
+ """
188
+
189
+
190
+ # =============================================================================
191
+ # Metric Filter Dropdown
192
+ # =============================================================================
193
+
194
+
195
+ def get_metric_filter_html(
196
+ metrics: list[str],
197
+ default_metric: str | None = None,
198
+ multi_select: bool = False,
199
+ on_change_callback: str = "onMetricFilterChange",
200
+ ) -> str:
201
+ """Generate HTML/JS for a metric filter dropdown.
202
+
203
+ Parameters
204
+ ----------
205
+ metrics : list[str]
206
+ Available metric names
207
+ default_metric : str, optional
208
+ Initially selected metric
209
+ multi_select : bool
210
+ Whether to allow multiple selection
211
+ on_change_callback : str
212
+ JavaScript function name to call on change
213
+
214
+ Returns
215
+ -------
216
+ str
217
+ HTML string with metric filter dropdown
218
+ """
219
+ if default_metric is None and metrics:
220
+ default_metric = metrics[0]
221
+
222
+ options = []
223
+ for metric in metrics:
224
+ selected = "selected" if metric == default_metric else ""
225
+ options.append(f'<option value="{metric}" {selected}>{metric}</option>')
226
+
227
+ multiple_attr = "multiple" if multi_select else ""
228
+
229
+ return f"""
230
+ <div class="metric-filter">
231
+ <label for="metric-select">Metric:</label>
232
+ <select id="metric-select" {multiple_attr}>
233
+ {"".join(options)}
234
+ </select>
235
+ </div>
236
+
237
+ <style>
238
+ .metric-filter {{
239
+ display: flex;
240
+ align-items: center;
241
+ gap: 10px;
242
+ margin-bottom: 15px;
243
+ }}
244
+ .metric-filter label {{
245
+ font-weight: 500;
246
+ font-size: 14px;
247
+ }}
248
+ .metric-filter select {{
249
+ padding: 6px 12px;
250
+ border: 1px solid #ddd;
251
+ border-radius: 4px;
252
+ font-size: 14px;
253
+ min-width: 150px;
254
+ }}
255
+ </style>
256
+
257
+ <script>
258
+ document.getElementById('metric-select').addEventListener('change', function() {{
259
+ const isMulti = {"true" if multi_select else "false"};
260
+ const selected = isMulti
261
+ ? Array.from(this.selectedOptions).map(o => o.value)
262
+ : this.value;
263
+ if (typeof {on_change_callback} === 'function') {{
264
+ {on_change_callback}(selected);
265
+ }}
266
+ }});
267
+ </script>
268
+ """
269
+
270
+
271
+ # =============================================================================
272
+ # Section Navigation
273
+ # =============================================================================
274
+
275
+
276
+ def get_section_navigation_html(
277
+ sections: list[dict[str, str]],
278
+ sticky: bool = True,
279
+ ) -> str:
280
+ """Generate HTML/JS for section navigation sidebar.
281
+
282
+ Parameters
283
+ ----------
284
+ sections : list[dict]
285
+ List of {"id": "section-id", "title": "Section Title"}
286
+ sticky : bool
287
+ Whether navigation should stick to viewport
288
+
289
+ Returns
290
+ -------
291
+ str
292
+ HTML string with section navigation
293
+ """
294
+ nav_items = []
295
+ for section in sections:
296
+ nav_items.append(f'<a href="#{section["id"]}" class="nav-item">{section["title"]}</a>')
297
+
298
+ position_style = "position: sticky; top: 20px;" if sticky else ""
299
+
300
+ return f"""
301
+ <nav class="section-nav" style="{position_style}">
302
+ <div class="nav-title">Contents</div>
303
+ {"".join(nav_items)}
304
+ </nav>
305
+
306
+ <style>
307
+ .section-nav {{
308
+ width: 200px;
309
+ padding: 15px;
310
+ background: #f8f9fa;
311
+ border-radius: 8px;
312
+ border: 1px solid #dee2e6;
313
+ }}
314
+ .nav-title {{
315
+ font-weight: 600;
316
+ font-size: 14px;
317
+ margin-bottom: 12px;
318
+ color: #495057;
319
+ }}
320
+ .nav-item {{
321
+ display: block;
322
+ padding: 6px 10px;
323
+ margin: 2px 0;
324
+ color: #666;
325
+ text-decoration: none;
326
+ font-size: 13px;
327
+ border-radius: 4px;
328
+ transition: all 0.2s;
329
+ }}
330
+ .nav-item:hover {{
331
+ background: #e9ecef;
332
+ color: #333;
333
+ }}
334
+ .nav-item.active {{
335
+ background: #636EFA;
336
+ color: white;
337
+ }}
338
+ </style>
339
+
340
+ <script>
341
+ // Highlight current section in navigation
342
+ const observer = new IntersectionObserver((entries) => {{
343
+ entries.forEach(entry => {{
344
+ if (entry.isIntersecting) {{
345
+ document.querySelectorAll('.nav-item').forEach(item => {{
346
+ item.classList.remove('active');
347
+ if (item.getAttribute('href') === '#' + entry.target.id) {{
348
+ item.classList.add('active');
349
+ }}
350
+ }});
351
+ }}
352
+ }});
353
+ }}, {{ threshold: 0.3 }});
354
+
355
+ document.querySelectorAll('section').forEach(section => {{
356
+ observer.observe(section);
357
+ }});
358
+ </script>
359
+ """
360
+
361
+
362
+ # =============================================================================
363
+ # Drill-Down Modal
364
+ # =============================================================================
365
+
366
+
367
+ def get_drill_down_modal_html() -> str:
368
+ """Generate HTML/JS for drill-down modal functionality.
369
+
370
+ Returns
371
+ -------
372
+ str
373
+ HTML string with modal component
374
+ """
375
+ return """
376
+ <div id="drill-down-modal" class="modal">
377
+ <div class="modal-content">
378
+ <div class="modal-header">
379
+ <h3 id="modal-title">Details</h3>
380
+ <span class="modal-close">&times;</span>
381
+ </div>
382
+ <div id="modal-body" class="modal-body">
383
+ <!-- Dynamic content loaded here -->
384
+ </div>
385
+ </div>
386
+ </div>
387
+
388
+ <style>
389
+ .modal {
390
+ display: none;
391
+ position: fixed;
392
+ z-index: 1000;
393
+ left: 0;
394
+ top: 0;
395
+ width: 100%;
396
+ height: 100%;
397
+ background-color: rgba(0,0,0,0.5);
398
+ }
399
+ .modal-content {
400
+ background-color: #fff;
401
+ margin: 5% auto;
402
+ padding: 0;
403
+ border-radius: 8px;
404
+ width: 80%;
405
+ max-width: 900px;
406
+ max-height: 80vh;
407
+ overflow: hidden;
408
+ box-shadow: 0 4px 20px rgba(0,0,0,0.3);
409
+ }
410
+ .modal-header {
411
+ display: flex;
412
+ justify-content: space-between;
413
+ align-items: center;
414
+ padding: 15px 20px;
415
+ border-bottom: 1px solid #dee2e6;
416
+ background: #f8f9fa;
417
+ }
418
+ .modal-header h3 {
419
+ margin: 0;
420
+ font-size: 18px;
421
+ }
422
+ .modal-close {
423
+ font-size: 28px;
424
+ font-weight: bold;
425
+ color: #666;
426
+ cursor: pointer;
427
+ }
428
+ .modal-close:hover {
429
+ color: #333;
430
+ }
431
+ .modal-body {
432
+ padding: 20px;
433
+ overflow-y: auto;
434
+ max-height: calc(80vh - 60px);
435
+ }
436
+ </style>
437
+
438
+ <script>
439
+ const modal = document.getElementById('drill-down-modal');
440
+ const modalTitle = document.getElementById('modal-title');
441
+ const modalBody = document.getElementById('modal-body');
442
+ const closeBtn = document.querySelector('.modal-close');
443
+
444
+ function showDrillDown(title, content) {
445
+ modalTitle.textContent = title;
446
+ modalBody.innerHTML = content;
447
+ modal.style.display = 'block';
448
+ }
449
+
450
+ function hideDrillDown() {
451
+ modal.style.display = 'none';
452
+ }
453
+
454
+ closeBtn.onclick = hideDrillDown;
455
+
456
+ window.onclick = function(event) {
457
+ if (event.target === modal) {
458
+ hideDrillDown();
459
+ }
460
+ };
461
+
462
+ document.addEventListener('keydown', function(event) {
463
+ if (event.key === 'Escape') {
464
+ hideDrillDown();
465
+ }
466
+ });
467
+ </script>
468
+ """
469
+
470
+
471
+ # =============================================================================
472
+ # Complete Interactive Toolbar
473
+ # =============================================================================
474
+
475
+
476
+ def get_interactive_toolbar_html(
477
+ start_date: date | str | None = None,
478
+ end_date: date | str | None = None,
479
+ metrics: list[str] | None = None,
480
+ show_date_range: bool = True,
481
+ show_metric_filter: bool = True,
482
+ show_export_button: bool = True,
483
+ ) -> str:
484
+ """Generate a complete interactive toolbar for the tearsheet.
485
+
486
+ Parameters
487
+ ----------
488
+ start_date : date or str, optional
489
+ Data start date
490
+ end_date : date or str, optional
491
+ Data end date
492
+ metrics : list[str], optional
493
+ Available metrics for filtering
494
+ show_date_range : bool
495
+ Whether to show date range selector
496
+ show_metric_filter : bool
497
+ Whether to show metric filter
498
+ show_export_button : bool
499
+ Whether to show export button
500
+
501
+ Returns
502
+ -------
503
+ str
504
+ HTML string with complete toolbar
505
+ """
506
+ components = []
507
+
508
+ if show_date_range:
509
+ components.append(get_date_range_html(start_date, end_date))
510
+
511
+ if show_metric_filter and metrics:
512
+ components.append(get_metric_filter_html(metrics))
513
+
514
+ export_btn = ""
515
+ if show_export_button:
516
+ export_btn = """
517
+ <button class="export-btn" onclick="window.print()">
518
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
519
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
520
+ <polyline points="7 10 12 15 17 10"/>
521
+ <line x1="12" y1="15" x2="12" y2="3"/>
522
+ </svg>
523
+ Export
524
+ </button>
525
+ """
526
+
527
+ return f"""
528
+ <div class="tearsheet-toolbar">
529
+ <div class="toolbar-left">
530
+ {"".join(components)}
531
+ </div>
532
+ <div class="toolbar-right">
533
+ {export_btn}
534
+ </div>
535
+ </div>
536
+
537
+ <style>
538
+ .tearsheet-toolbar {{
539
+ display: flex;
540
+ justify-content: space-between;
541
+ align-items: center;
542
+ padding: 15px 0;
543
+ margin-bottom: 20px;
544
+ border-bottom: 1px solid #dee2e6;
545
+ }}
546
+ .toolbar-left {{
547
+ display: flex;
548
+ gap: 30px;
549
+ align-items: center;
550
+ }}
551
+ .toolbar-right {{
552
+ display: flex;
553
+ gap: 10px;
554
+ }}
555
+ .export-btn {{
556
+ display: flex;
557
+ align-items: center;
558
+ gap: 6px;
559
+ padding: 8px 16px;
560
+ background: #636EFA;
561
+ color: white;
562
+ border: none;
563
+ border-radius: 6px;
564
+ cursor: pointer;
565
+ font-size: 14px;
566
+ transition: background 0.2s;
567
+ }}
568
+ .export-btn:hover {{
569
+ background: #5258d9;
570
+ }}
571
+
572
+ @media print {{
573
+ .tearsheet-toolbar {{
574
+ display: none;
575
+ }}
576
+ }}
577
+ </style>
578
+ """
579
+
580
+
581
+ # =============================================================================
582
+ # Theme Switcher
583
+ # =============================================================================
584
+
585
+
586
+ def get_theme_switcher_html(
587
+ themes: list[str] | None = None,
588
+ default_theme: str = "default",
589
+ ) -> str:
590
+ """Generate HTML/JS for theme switcher.
591
+
592
+ Parameters
593
+ ----------
594
+ themes : list[str], optional
595
+ Available themes (default: ["default", "dark", "print"])
596
+ default_theme : str
597
+ Initially selected theme
598
+
599
+ Returns
600
+ -------
601
+ str
602
+ HTML string with theme switcher
603
+ """
604
+ if themes is None:
605
+ themes = ["default", "dark", "print"]
606
+
607
+ theme_labels = {
608
+ "default": "Light",
609
+ "dark": "Dark",
610
+ "print": "Print",
611
+ "presentation": "Present",
612
+ }
613
+
614
+ buttons = []
615
+ for theme in themes:
616
+ active = "active" if theme == default_theme else ""
617
+ label = theme_labels.get(theme, theme.title())
618
+ buttons.append(f'<button class="theme-btn {active}" data-theme="{theme}">{label}</button>')
619
+
620
+ return f"""
621
+ <div class="theme-switcher">
622
+ {"".join(buttons)}
623
+ </div>
624
+
625
+ <style>
626
+ .theme-switcher {{
627
+ display: flex;
628
+ gap: 5px;
629
+ }}
630
+ .theme-btn {{
631
+ padding: 5px 12px;
632
+ border: 1px solid #ddd;
633
+ background: #fff;
634
+ border-radius: 4px;
635
+ cursor: pointer;
636
+ font-size: 12px;
637
+ transition: all 0.2s;
638
+ }}
639
+ .theme-btn:hover {{
640
+ background: #f0f0f0;
641
+ }}
642
+ .theme-btn.active {{
643
+ background: #636EFA;
644
+ color: white;
645
+ border-color: #636EFA;
646
+ }}
647
+ </style>
648
+
649
+ <script>
650
+ document.querySelectorAll('.theme-btn').forEach(btn => {{
651
+ btn.addEventListener('click', function() {{
652
+ document.querySelectorAll('.theme-btn').forEach(b => b.classList.remove('active'));
653
+ this.classList.add('active');
654
+
655
+ const theme = this.dataset.theme;
656
+ document.documentElement.setAttribute('data-theme', theme === 'dark' ? 'dark' : 'light');
657
+
658
+ // Notify Plotly charts to update
659
+ if (typeof Plotly !== 'undefined') {{
660
+ document.querySelectorAll('.js-plotly-plot').forEach(plot => {{
661
+ const bgColor = theme === 'dark' ? '#1E1E1E' : '#FFFFFF';
662
+ const textColor = theme === 'dark' ? '#E0E0E0' : '#2E2E2E';
663
+ Plotly.relayout(plot, {{
664
+ 'paper_bgcolor': bgColor,
665
+ 'plot_bgcolor': bgColor,
666
+ 'font.color': textColor,
667
+ }});
668
+ }});
669
+ }}
670
+ }});
671
+ }});
672
+ </script>
673
+ """