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.
- package/index.js +1 -1
- package/package.json +1 -1
- package/hedgeboard/CLAUDE.md +0 -284
- package/hedgeboard/README.md +0 -64
- package/hedgeboard/brand/base.css +0 -419
- package/hedgeboard/brand/theme.json +0 -25
- package/hedgeboard/data/__init__.py +0 -1
- package/hedgeboard/data/sec.py +0 -218
- package/hedgeboard/modules/__init__.py +0 -1
- package/hedgeboard/modules/company_overview.py +0 -157
- package/hedgeboard/requirements.txt +0 -2
- package/hedgeboard/viz/__init__.py +0 -1
- package/hedgeboard/viz/charts.py +0 -198
- package/hedgeboard/viz/components.py +0 -391
- package/hedgeboard/viz/dashboard.py +0 -330
- package/hedgeboard/viz/tables.py +0 -140
|
@@ -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>'
|