create-hedgeboard 1.0.7 → 1.0.8

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.
@@ -1,391 +0,0 @@
1
- """Dashboard components — reusable building blocks for financial dashboards.
2
-
3
- Provides KPI cards, callout boxes, scorecards, comparison tables, heatmaps,
4
- timelines, sparklines, section headers, source citations, and metric deltas.
5
-
6
- All components return HTML strings ready for render_dashboard().
7
-
8
- Usage:
9
- from viz.components import kpi_row, callout, grid, scorecard
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- import html as _html
15
- from typing import Literal
16
-
17
- # ---------------------------------------------------------------------------
18
- # Chart color palette — 8 distinct colors for multi-series data
19
- # ---------------------------------------------------------------------------
20
-
21
- COLORS = [
22
- "#EAEAEA", # primary (white-ish)
23
- "#4ADE80", # green
24
- "#60A5FA", # blue
25
- "#FBBF24", # amber
26
- "#F87171", # red
27
- "#A78BFA", # purple
28
- "#34D399", # teal
29
- "#FB923C", # orange
30
- ]
31
-
32
- COLORS_DIM = [
33
- "rgba(234,234,234,0.15)",
34
- "rgba(74,222,128,0.15)",
35
- "rgba(96,165,250,0.15)",
36
- "rgba(251,191,36,0.15)",
37
- "rgba(248,113,113,0.15)",
38
- "rgba(167,139,250,0.15)",
39
- "rgba(52,211,153,0.15)",
40
- "rgba(251,146,60,0.15)",
41
- ]
42
-
43
-
44
- def _esc(text: str) -> str:
45
- return _html.escape(str(text))
46
-
47
-
48
- # ---------------------------------------------------------------------------
49
- # Grid layout — put charts/components side by side
50
- # ---------------------------------------------------------------------------
51
-
52
- def grid(items: list[str], columns: int = 2) -> str:
53
- """Place items side-by-side in a responsive grid.
54
-
55
- Args:
56
- items: List of HTML strings (charts, tables, etc.).
57
- columns: Number of columns (2 or 3).
58
- """
59
- cells = "".join(f'<div class="hb-grid-cell">{item}</div>' for item in items)
60
- return f'<div class="hb-grid hb-grid-{columns}">{cells}</div>'
61
-
62
-
63
- # ---------------------------------------------------------------------------
64
- # KPI Row — top-level metrics with deltas
65
- # ---------------------------------------------------------------------------
66
-
67
- def kpi_row(metrics: list[dict]) -> str:
68
- """Render a row of KPI metric cards.
69
-
70
- Args:
71
- metrics: List of dicts with keys:
72
- - label: str (e.g. "Revenue")
73
- - value: str (e.g. "$901M")
74
- - delta: str, optional (e.g. "+15.5%")
75
- - trend: str, optional ("up", "down", "flat")
76
- - sparkline: list[float], optional (mini trend data)
77
- """
78
- cards = []
79
- for m in metrics:
80
- delta_html = ""
81
- if m.get("delta"):
82
- trend = m.get("trend", "")
83
- if not trend:
84
- d = m["delta"]
85
- trend = "up" if (d.startswith("+") or d.startswith("▲")) else "down" if (d.startswith("-") or d.startswith("▼")) else "flat"
86
- cls = "hb-delta-up" if trend == "up" else "hb-delta-down" if trend == "down" else "hb-delta-flat"
87
- arrow = "▲" if trend == "up" else "▼" if trend == "down" else "—"
88
- delta_html = f'<span class="hb-kpi-delta {cls}">{arrow} {_esc(m["delta"])}</span>'
89
-
90
- spark_html = ""
91
- if m.get("sparkline"):
92
- spark_html = sparkline(m["sparkline"])
93
-
94
- cards.append(f'''<div class="hb-kpi">
95
- <span class="hb-kpi-label">{_esc(m["label"])}</span>
96
- <span class="hb-kpi-value">{_esc(m["value"])}</span>
97
- {delta_html}
98
- {spark_html}
99
- </div>''')
100
-
101
- return f'<div class="hb-kpi-row">{" ".join(cards)}</div>'
102
-
103
-
104
- # ---------------------------------------------------------------------------
105
- # Callout / Alert boxes
106
- # ---------------------------------------------------------------------------
107
-
108
- def callout(
109
- text: str,
110
- type: Literal["info", "success", "warning", "danger"] = "info",
111
- title: str | None = None,
112
- ) -> str:
113
- """Render a callout box for key insights.
114
-
115
- Args:
116
- text: The callout message.
117
- type: "info", "success", "warning", or "danger".
118
- title: Optional bold title line.
119
- """
120
- icons = {"info": "ℹ", "success": "✓", "warning": "⚠", "danger": "✕"}
121
- icon = icons.get(type, "ℹ")
122
- title_html = f'<strong>{_esc(title)}</strong> ' if title else ""
123
- return f'''<div class="hb-callout hb-callout-{type}">
124
- <span class="hb-callout-icon">{icon}</span>
125
- <div>{title_html}{_esc(text)}</div>
126
- </div>'''
127
-
128
-
129
- # ---------------------------------------------------------------------------
130
- # Scorecard — bull/bear/neutral verdict
131
- # ---------------------------------------------------------------------------
132
-
133
- def scorecard(
134
- verdict: Literal["bullish", "neutral", "bearish"],
135
- reasoning: str,
136
- title: str = "Outlook",
137
- ) -> str:
138
- """Render a scorecard with a bullish/neutral/bearish verdict.
139
-
140
- Args:
141
- verdict: "bullish", "neutral", or "bearish".
142
- reasoning: Brief explanation.
143
- title: Card title.
144
- """
145
- colors = {"bullish": "#4ADE80", "neutral": "#FBBF24", "bearish": "#F87171"}
146
- labels = {"bullish": "▲ Bullish", "neutral": "— Neutral", "bearish": "▼ Bearish"}
147
- return f'''<div class="hb-scorecard">
148
- <span class="hb-section-lbl">{_esc(title)}</span>
149
- <div class="hb-scorecard-verdict" style="color:{colors[verdict]}">
150
- {labels[verdict]}
151
- </div>
152
- <p class="hb-scorecard-reason">{_esc(reasoning)}</p>
153
- </div>'''
154
-
155
-
156
- # ---------------------------------------------------------------------------
157
- # Comparison table — multi-company side-by-side with color coding
158
- # ---------------------------------------------------------------------------
159
-
160
- def comparison_table(
161
- headers: list[str],
162
- rows: list[list],
163
- title: str = "",
164
- highlight_best: bool = True,
165
- ) -> str:
166
- """Render a comparison table with color-coded best/worst values.
167
-
168
- Args:
169
- headers: Column headers (first is typically "Metric").
170
- rows: Each row is [metric_name, val1, val2, ...].
171
- Numeric values get color-coded.
172
- title: Optional table title.
173
- highlight_best: If True, highest numeric value gets green, lowest gets red.
174
- """
175
- title_html = f'<div class="hb-table-title">{_esc(title)}</div>' if title else ""
176
-
177
- # Header
178
- ths = "".join(f'<th>{_esc(h)}</th>' for h in headers)
179
- thead = f"<thead><tr>{ths}</tr></thead>"
180
-
181
- # Body with highlighting
182
- tbody_rows = []
183
- for row in rows:
184
- tds = []
185
- # Find numeric values for highlighting
186
- nums = []
187
- for i, val in enumerate(row):
188
- if i > 0:
189
- try:
190
- clean = str(val).replace("$", "").replace(",", "").replace("%", "").replace("B", "e9").replace("M", "e6").replace("K", "e3")
191
- nums.append((i, float(clean)))
192
- except (ValueError, TypeError):
193
- nums.append((i, None))
194
-
195
- best_idx = None
196
- worst_idx = None
197
- if highlight_best and len(nums) > 1:
198
- valid = [(i, n) for i, n in nums if n is not None]
199
- if valid:
200
- best_idx = max(valid, key=lambda x: x[1])[0]
201
- worst_idx = min(valid, key=lambda x: x[1])[0]
202
- if best_idx == worst_idx:
203
- best_idx = worst_idx = None
204
-
205
- for i, val in enumerate(row):
206
- cls = ""
207
- if i == best_idx:
208
- cls = ' class="hb-cell-best"'
209
- elif i == worst_idx:
210
- cls = ' class="hb-cell-worst"'
211
- elif i == 0:
212
- cls = ' class="hb-cell-label"'
213
- tds.append(f"<td{cls}>{_esc(val)}</td>")
214
-
215
- tbody_rows.append(f"<tr>{''.join(tds)}</tr>")
216
-
217
- return f'''{title_html}
218
- <div class="hb-table-wrap">
219
- <table class="hb-comp-table">
220
- {thead}
221
- <tbody>{''.join(tbody_rows)}</tbody>
222
- </table>
223
- </div>'''
224
-
225
-
226
- # ---------------------------------------------------------------------------
227
- # Heatmap table — cell intensity based on value
228
- # ---------------------------------------------------------------------------
229
-
230
- def heatmap_table(
231
- headers: list[str],
232
- rows: list[list],
233
- title: str = "",
234
- ) -> str:
235
- """Render a table where numeric cells have background intensity based on value.
236
-
237
- Args:
238
- headers: Column headers.
239
- rows: Each row is [label, val1, val2, ...].
240
- """
241
- title_html = f'<div class="hb-table-title">{_esc(title)}</div>' if title else ""
242
- ths = "".join(f'<th>{_esc(h)}</th>' for h in headers)
243
-
244
- # Collect all numeric values for normalization
245
- all_nums = []
246
- for row in rows:
247
- for val in row[1:]:
248
- try:
249
- all_nums.append(float(str(val).replace("$", "").replace(",", "").replace("%", "")))
250
- except (ValueError, TypeError):
251
- pass
252
-
253
- min_val = min(all_nums) if all_nums else 0
254
- max_val = max(all_nums) if all_nums else 1
255
- val_range = max_val - min_val or 1
256
-
257
- tbody_rows = []
258
- for row in rows:
259
- tds = [f'<td class="hb-cell-label">{_esc(row[0])}</td>']
260
- for val in row[1:]:
261
- try:
262
- num = float(str(val).replace("$", "").replace(",", "").replace("%", ""))
263
- intensity = (num - min_val) / val_range
264
- color = f"rgba(234,234,234,{intensity * 0.2 + 0.02})"
265
- tds.append(f'<td style="background:{color}">{_esc(val)}</td>')
266
- except (ValueError, TypeError):
267
- tds.append(f"<td>{_esc(val)}</td>")
268
- tbody_rows.append(f"<tr>{''.join(tds)}</tr>")
269
-
270
- return f'''{title_html}
271
- <div class="hb-table-wrap">
272
- <table class="hb-comp-table">
273
- <thead><tr>{ths}</tr></thead>
274
- <tbody>{''.join(tbody_rows)}</tbody>
275
- </table>
276
- </div>'''
277
-
278
-
279
- # ---------------------------------------------------------------------------
280
- # Sparklines — tiny inline SVG charts
281
- # ---------------------------------------------------------------------------
282
-
283
- def sparkline(data: list[float], width: int = 80, height: int = 24) -> str:
284
- """Render a tiny SVG sparkline chart.
285
-
286
- Args:
287
- data: List of numeric values.
288
- width: SVG width.
289
- height: SVG height.
290
- """
291
- if not data or len(data) < 2:
292
- return ""
293
-
294
- min_v = min(data)
295
- max_v = max(data)
296
- v_range = max_v - min_v or 1
297
- pad = 2
298
-
299
- points = []
300
- for i, v in enumerate(data):
301
- x = pad + (i / (len(data) - 1)) * (width - 2 * pad)
302
- y = pad + (1 - (v - min_v) / v_range) * (height - 2 * pad)
303
- points.append(f"{x:.1f},{y:.1f}")
304
-
305
- trend_color = "#4ADE80" if data[-1] >= data[0] else "#F87171"
306
-
307
- return f'''<svg class="hb-sparkline" width="{width}" height="{height}" viewBox="0 0 {width} {height}">
308
- <polyline points="{' '.join(points)}" fill="none"
309
- stroke="{trend_color}" stroke-width="1.5" stroke-linecap="round"/>
310
- </svg>'''
311
-
312
-
313
- # ---------------------------------------------------------------------------
314
- # Metric delta badge
315
- # ---------------------------------------------------------------------------
316
-
317
- def delta_badge(value: str, trend: Literal["up", "down", "flat"] | None = None) -> str:
318
- """Render an inline delta badge (▲ +15.5% in green, etc.).
319
-
320
- Args:
321
- value: The delta text (e.g. "+15.5%", "-3.2%").
322
- trend: "up", "down", or "flat". Auto-detected from value if not given.
323
- """
324
- if not trend:
325
- trend = "up" if value.startswith("+") else "down" if value.startswith("-") else "flat"
326
- arrow = "▲" if trend == "up" else "▼" if trend == "down" else "—"
327
- cls = f"hb-delta-{trend}"
328
- return f'<span class="hb-delta-badge {cls}">{arrow} {_esc(value)}</span>'
329
-
330
-
331
- # ---------------------------------------------------------------------------
332
- # Section header
333
- # ---------------------------------------------------------------------------
334
-
335
- def section_header(label: str, title: str = "", description: str = "") -> str:
336
- """Render a clean section divider with label and optional title.
337
-
338
- Args:
339
- label: Uppercase label (e.g. "REVENUE ANALYSIS").
340
- title: Optional larger title.
341
- description: Optional description text.
342
- """
343
- parts = [f'<span class="hb-section-lbl">{_esc(label)}</span>']
344
- if title:
345
- parts.append(f'<h2 class="hb-section-title">{_esc(title)}</h2>')
346
- if description:
347
- parts.append(f'<p class="hb-section-desc">{_esc(description)}</p>')
348
- return f'<div class="hb-section-header">{"".join(parts)}</div>'
349
-
350
-
351
- # ---------------------------------------------------------------------------
352
- # Source citation panel
353
- # ---------------------------------------------------------------------------
354
-
355
- def source_panel(sources: list[str]) -> str:
356
- """Render a collapsible source citation panel.
357
-
358
- Args:
359
- sources: List of source strings (e.g. "SEC 10-K, filed 2025-10-31").
360
- """
361
- items = "".join(f"<li>{_esc(s)}</li>" for s in sources)
362
- return f'''<details class="hb-sources">
363
- <summary class="hb-sources-toggle">Sources & Data ({len(sources)})</summary>
364
- <ul class="hb-sources-list">{items}</ul>
365
- </details>'''
366
-
367
-
368
- # ---------------------------------------------------------------------------
369
- # Timeline — horizontal milestones
370
- # ---------------------------------------------------------------------------
371
-
372
- def timeline(events: list[dict]) -> str:
373
- """Render a horizontal timeline of events.
374
-
375
- Args:
376
- events: List of dicts with keys:
377
- - date: str (e.g. "2025-10-31")
378
- - label: str (e.g. "10-K Filed")
379
- - detail: str, optional
380
- """
381
- items = []
382
- for ev in events:
383
- detail = f'<span class="hb-timeline-detail">{_esc(ev.get("detail", ""))}</span>' if ev.get("detail") else ""
384
- items.append(f'''<div class="hb-timeline-item">
385
- <span class="hb-timeline-date">{_esc(ev["date"])}</span>
386
- <span class="hb-timeline-dot"></span>
387
- <span class="hb-timeline-label">{_esc(ev["label"])}</span>
388
- {detail}
389
- </div>''')
390
-
391
- return f'<div class="hb-timeline">{"".join(items)}</div>'