/* global React */ /* Brave Alpha Capital — Diligence chart primitives. * Hand-built SVG so they sit inside white cards with hairline grids, mono axis * labels and the brand palette. Multi-series uses the categorical palette from * data.js. Legends toggle series. Single-series charts get a hover readout. */ // CSS custom properties, not hex — inline SVG resolves them, so every chart follows // the light/dark theme for free. Series palettes stay hex (legible on both). const C = { line: "var(--line)", ink: "var(--ink)", muted: "var(--muted)", gold: "var(--gold)", green: "var(--green)", red: "var(--red)", paper: "var(--paper)", }; const MONO = "var(--font-mono), 'IBM Plex Mono', monospace"; function fmtPct(v, dp = 0) { return (v >= 0 ? "+" : "\u2212") + Math.abs(v).toFixed(dp) + "%"; } function niceTicks(min, max, n) { if (min === max) { min -= 1; max += 1; } const span = max - min, step0 = span / n, mag = Math.pow(10, Math.floor(Math.log10(step0))); const norm = step0 / mag, step = (norm < 1.5 ? 1 : norm < 3 ? 2 : norm < 7 ? 5 : 10) * mag; const lo = Math.floor(min / step) * step, hi = Math.ceil(max / step) * step, out = []; for (let v = lo; v <= hi + 1e-9; v += step) out.push(Math.round(v * 1e6) / 1e6); return out; } // Shared compact hover tile used by line and bar charts. Keeping one component // prevents chart tooltips from drifting into different sizes and visual styles. function ChartHoverTile({ left, top, flip = false, title, value, tone = C.ink, metaLeft, metaRight, sub }) { const hasMeta = metaLeft || metaRight; return
{String(title || "POINT").toUpperCase()}
{value}
{hasMeta && <>
{metaLeft || ""}{metaRight || ""}
} {sub &&
{sub}
}
; } // Keyboard-first searchable selector for long strategy registries. Typing ranks // exact/prefix matches before word and partial matches; arrows + Enter select. function SearchSelect({ value, onChange, options, placeholder = "Search…", ariaLabel, style }) { const list = options || []; const selected = list.find((o) => String(o.value) === String(value)); const [open, setOpen] = React.useState(false); const [query, setQuery] = React.useState(""); const [active, setActive] = React.useState(0); const q = query.trim().toLowerCase(); const ranked = list.map((o, index) => { const hay = `${o.label || ""} ${o.keywords || ""} ${o.value || ""}`.toLowerCase(); const words = hay.split(/[^a-z0-9]+/).filter(Boolean); const score = !q ? index : hay === q ? -30 : hay.startsWith(q) ? -20 : words.some((w) => w.startsWith(q)) ? -10 : hay.includes(q) ? 0 : 10000; return { o, index, score }; }).filter((x) => x.score < 10000).sort((a, b) => a.score - b.score || a.index - b.index).map((x) => x.o); React.useEffect(() => { setActive(0); }, [query, open]); function choose(o) { if (!o) return; onChange(o.value); setQuery(""); setOpen(false); } function keyDown(e) { if (e.key === "ArrowDown") { e.preventDefault(); if (!open) setOpen(true); else setActive((i) => Math.min(ranked.length - 1, i + 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); if (!open) setOpen(true); else setActive((i) => Math.max(0, i - 1)); } else if (e.key === "Enter") { if (open && ranked.length) { e.preventDefault(); choose(ranked[active] || ranked[0]); } } else if (e.key === "Escape") { setOpen(false); setQuery(""); } } return (
{ setOpen(true); setQuery(""); }} onChange={(e) => { setQuery(e.target.value); setOpen(true); }} onKeyDown={keyDown} onBlur={() => setTimeout(() => { setOpen(false); setQuery(""); }, 120)} style={{ width: "100%", boxSizing: "border-box", fontFamily: "var(--font-mono)", fontSize: 12, padding: "8px 28px 8px 10px", borderRadius: 7, border: "1px solid var(--line)", background: "var(--paper)", color: "var(--ink)", outline: "none" }} /> {open && (
{ranked.slice(0, 80).map((o, i) => ( ))} {!ranked.length &&
No matching strategies
}
)}
); } // ---- Legend (HTML) --------------------------------------------------------- function Legend({ items, hidden, onToggle, small }) { return (
{items.map((it) => { const off = hidden && hidden.has(it.id); return ( ); })}
); } // ---- Multi-series line chart ---------------------------------------------- // markers: [{ i, label, color? }] — vertical annotation lines (e.g. "paper starts", // "live fills", a strategy switch) so a regime change reads off the curve itself. function LineChart({ series, hidden, yDomain, yFmt, xLabels, xCount, height = 280, area, accent, baseline, hover, markers, hoverCard, initialHoverIndex = null }) { const W = 900, H = height, padL = 52, padT = 14, padB = 28; const vis = series.filter((s) => !(hidden && hidden.has(s.id))); const showEnd = vis.length >= 2 && vis.length <= 10; // name each line at its right endpoint const padR = showEnd ? 96 : 16; const maxLen = xCount || Math.max(1, ...series.map((s) => s.data.length)); let lo, hi; if (yDomain) { [lo, hi] = yDomain; } else { let mn = Infinity, mx = -Infinity; for (const s of vis) for (const v of s.data) { if (v < mn) mn = v; if (v > mx) mx = v; } if (!isFinite(mn)) { mn = 0; mx = 1; } const pad = (mx - mn) * 0.08 || 1; lo = mn - pad; hi = mx + pad; } const yTicks = niceTicks(lo, hi, 5); lo = Math.min(lo, yTicks[0]); hi = Math.max(hi, yTicks[yTicks.length - 1]); const X = (i) => padL + (maxLen <= 1 ? 0 : i / (maxLen - 1)) * (W - padL - padR); const Y = (v) => padT + (1 - (v - lo) / (hi - lo)) * (H - padT - padB); const safeInitialHover = initialHoverIndex == null ? null : Math.max(0, Math.min(maxLen - 1, Number(initialHoverIndex))); const [hi_, setHi] = React.useState(safeInitialHover); const svgRef = React.useRef(null); const chartId = React.useId().replace(/:/g, ""); React.useEffect(() => { setHi(safeInitialHover); }, [safeInitialHover, maxLen]); function path(data) { return data.map((v, i) => (i ? "L" : "M") + X(i).toFixed(1) + "," + Y(v).toFixed(1)).join(" "); } function smoothPath(data) { if (!data || data.length < 2) return path(data || []); let d = `M${X(0).toFixed(1)},${Y(data[0]).toFixed(1)}`; for (let i = 0; i < data.length - 1; i++) { const p0x = X(Math.max(0, i - 1)), p0y = Y(data[Math.max(0, i - 1)]); const p1x = X(i), p1y = Y(data[i]); const p2x = X(i + 1), p2y = Y(data[i + 1]); const p3x = X(Math.min(data.length - 1, i + 2)), p3y = Y(data[Math.min(data.length - 1, i + 2)]); const c1x = p1x + (p2x - p0x) / 6, c1y = p1y + (p2y - p0y) / 6; const c2x = p2x - (p3x - p1x) / 6, c2y = p2y - (p3y - p1y) / 6; d += ` C${c1x.toFixed(1)},${c1y.toFixed(1)} ${c2x.toFixed(1)},${c2y.toFixed(1)} ${p2x.toFixed(1)},${p2y.toFixed(1)}`; } return d; } function onMove(e) { if (!hover) return; const r = svgRef.current.getBoundingClientRect(); const px = ((e.clientX - r.left) / r.width) * W; const idx = Math.round(((px - padL) / (W - padL - padR)) * (maxLen - 1)); setHi(Math.max(0, Math.min(maxLen - 1, idx))); } const xt = []; const nLabels = Math.min(8, maxLen); for (let k = 0; k < nLabels; k++) { const i = Math.round((k / (nLabels - 1)) * (maxLen - 1)); xt.push(i); } return (
setHi(safeInitialHover)}> {area && ( )} {yTicks.map((t) => ( {yFmt ? yFmt(t) : t} ))} {baseline !== undefined && } {xt.map((i) => ( {xLabels ? xLabels(i) : "T" + (i + 1)} ))} {area && vis.length === 1 && ( (() => { const base = baseline == null ? 0 : baseline; const curve = smoothPath(vis[0].data); const areaPath = curve + ` L${X(vis[0].data.length - 1).toFixed(1)},${Y(base).toFixed(1)} L${X(0).toFixed(1)},${Y(base).toFixed(1)} Z`; return <> ; })() )} {vis.map((s) => area && vis.length === 1 ? ( ) : ( ))} {showEnd && (() => { // name each line beside its right endpoint (de-overlapped, with a connector) const pts = vis.map((s) => { const di = s.data.length - 1; return { id: s.id, label: String(s.label || s.id).slice(0, 16), color: s.color, ex: di >= 0 ? X(di) : null, ey: (di >= 0 && s.data[di] != null) ? Y(s.data[di]) : null }; }).filter((p) => p.ey != null).sort((a, b) => a.ey - b.ey); pts.forEach((p, i) => { p.y = i ? Math.max(p.ey, pts[i - 1].y + 12.5) : p.ey; }); return pts.map((p) => ( {p.label} )); })()} {(markers || []).filter((m) => m.i > 0 && m.i < maxLen).map((m, mi) => ( {m.label} ))} {hover && hi_ != null && ( {vis.map((s) => s.data[hi_] != null && ( = (baseline == null ? 0 : baseline) ? C.green : C.red} stroke="#fff" strokeWidth="1.5" /> ))} {vis.length === 1 && vis[0].data[hi_] != null && ( !hoverCard && {yFmt ? yFmt(vis[0].data[hi_]) : vis[0].data[hi_].toFixed(1)} )} {vis.length > 1 && (() => { // floating readout: every visible series' value at the cursor, largest first, // so a dense overlay is readable without chasing endpoint labels. const at = vis.map((s) => ({ id: s.id, label: String(s.label || s.id).slice(0, 14), color: s.color, v: s.data[hi_] })) .filter((p) => p.v != null).sort((a, b) => b.v - a.v).slice(0, 8); if (!at.length) return null; const bw = 128, bh = at.length * 13.5 + 20; const bx = X(hi_) + 12 + bw > W - padR ? X(hi_) - bw - 12 : X(hi_) + 12; return ( {xLabels ? xLabels(hi_) : "T" + (hi_ + 1)} {at.map((p, pi) => ( {p.label} · {yFmt ? yFmt(p.v) : p.v.toFixed(1)} ))} ); })()} )} {hover && hi_ != null && hoverCard && hoverCard[hi_] && (() => { const d = hoverCard[hi_]; const px = X(hi_) / W * 100; const py = Math.max(2, Math.min(64, Y(vis[0].data[hi_]) / H * 100 - 8)); const tone = d.pnl == null ? C.ink : Number(d.pnl) >= 0 ? C.green : C.red; return 62} title={d.title || "Point"} value={d.value || (yFmt ? yFmt(vis[0].data[hi_]) : vis[0].data[hi_].toFixed(2))} tone={tone} metaLeft={d.detail} metaRight={d.pnlLabel} sub={d.sub} />; })()}
); } // ---- Monthly P&L bars (single strategy, green/red) ------------------------- function MonthBars({ months, values, height = 240, barRatio = 0.56 }) { const W = 900, H = height, padL = 48, padR = 12, padT = 14, padB = 28; let mn = Math.min(0, ...values), mx = Math.max(0, ...values); const ticks = niceTicks(mn, mx, 4); mn = Math.min(mn, ticks[0]); mx = Math.max(mx, ticks[ticks.length - 1]); const Y = (v) => padT + (1 - (v - mn) / (mx - mn)) * (H - padT - padB); const bw = (W - padL - padR) / months.length; const [hover, setHover] = React.useState(null); return (
setHover(null)}> {ticks.map((t) => ( {fmtPct(t, 0)} ))} {values.map((v, i) => { const x = padL + i * bw + bw * (1 - barRatio) / 2, w = bw * barRatio; const y0 = Y(0), y1 = Y(v); return ( setHover(i)}> = 0 ? C.green : C.red} opacity=".9" /> {months[i]} ); })} {hover != null && values[hover] != null && (() => { const value = Number(values[hover]); const px = (padL + hover * bw + bw / 2) / W * 100; const py = Math.max(2, Math.min(62, Math.min(Y(0), Y(value)) / H * 100 - 6)); return 62} title={months[hover] || "Monthly P&L"} value={fmtPct(value, 2)} tone={value >= 0 ? C.green : C.red} metaLeft="Calendar month" metaRight={value >= 0 ? "GAIN" : "LOSS"} />; })()}
); } // ---- Grouped monthly bars (multi-series) ----------------------------------- function GroupedBars({ months, series, hidden, height = 260, yFmt }) { const W = 900, H = height, padL = 48, padR = 12, padT = 14, padB = 28; const vis = series.filter((s) => !(hidden && hidden.has(s.id))); let mn = 0, mx = 0; for (const s of vis) for (const v of s.data) { if (v < mn) mn = v; if (v > mx) mx = v; } const ticks = niceTicks(mn, mx, 4); mn = Math.min(mn, ticks[0]); mx = Math.max(mx, ticks[ticks.length - 1]); const Y = (v) => padT + (1 - (v - mn) / (mx - mn)) * (H - padT - padB); const gw = (W - padL - padR) / months.length, bw = (gw * 0.7) / Math.max(1, vis.length); return ( {ticks.map((t) => ( {yFmt ? yFmt(t) : t} ))} {months.map((m, mi) => ( {vis.map((s, si) => { const v = s.data[mi] || 0, x = padL + mi * gw + gw * 0.15 + si * bw; const y0 = Y(0), y1 = Y(v); return ; })} {m} ))} ); } // ---- Histogram (grouped multi-series by P&L bucket) ------------------------ function Histogram({ bins, labels, series, hidden, height = 240 }) { const W = 900, H = height, padL = 40, padR = 12, padT = 14, padB = 30; const vis = series.filter((s) => !(hidden && hidden.has(s.id))); let mx = 1; for (const s of vis) for (const v of s.data) if (v > mx) mx = v; const ticks = niceTicks(0, mx, 4); mx = Math.max(mx, ticks[ticks.length - 1]); const Y = (v) => padT + (1 - v / mx) * (H - padT - padB); const gw = (W - padL - padR) / labels.length, bw = (gw * 0.74) / Math.max(1, vis.length); return ( {ticks.map((t) => ( {t} ))} {labels.map((lb, bi) => ( {vis.map((s, si) => { const v = s.data[bi] || 0, x = padL + bi * gw + gw * 0.13 + si * bw; return ; })} {lb} ))} ); } // ---- Sparkline ------------------------------------------------------------- function Sparkline({ data, color = C.ink, width = 120, height = 32, area }) { if (!data || data.length < 2) return ; const mn = Math.min(...data), mx = Math.max(...data), sp = mx - mn || 1, pad = 3; const X = (i) => pad + (i / (data.length - 1)) * (width - pad * 2); const Y = (v) => pad + (1 - (v - mn) / sp) * (height - pad * 2); const d = data.map((v, i) => (i ? "L" : "M") + X(i).toFixed(1) + "," + Y(v).toFixed(1)).join(" "); return ( {area && } ); } // ---- Horizontal bar (composite breakdown / win-rate meter) ----------------- function MeterBar({ value, max = 100, color = C.gold, height = 7, track = "var(--line)" }) { return (
); } // Long / short profile bars. The visual height is always measured upward from // the bottom baseline; the signed value remains explicit in labels and hover. function ModeBars({ data, height = 190, metric = "return" }) { const W = 900, H = height, padL = 48, padR = 12, padT = 30, padB = 38; const vals = (data || []).map((d) => Number(d[metric] || 0)); const magnitudes = vals.map((v) => metric === "return" ? Math.abs(v) : Math.max(0, v)); const rawMax = Math.max(1, ...magnitudes); const hi = Math.max(1, Math.ceil(rawMax * 1.12)); const Y = (v) => padT + (1 - v / hi) * (H - padT - padB); const yTicks = niceTicks(0, hi, 4).filter((t) => t >= 0 && t <= hi); const bw = (W - padL - padR) / Math.max(1, data.length); const [hover, setHover] = React.useState(null); const fmt = (v) => metric === "trades" ? String(Math.round(v)) : (metric === "return" ? (v >= 0 ? "+" : "−") + Math.abs(v).toFixed(1) : v.toFixed(1)) + "%"; const axisFmt = (v) => metric === "trades" ? String(Math.round(v)) : v.toFixed(0) + "%"; return (
setHover(null)}> {yTicks.map((t) => {axisFmt(t)} )} {(data || []).map((d, i) => { const v = Number(d[metric] || 0), magnitude = metric === "return" ? Math.abs(v) : Math.max(0, v); const x = padL + i * bw + bw * 0.12, w = bw * 0.76; const y0 = Y(0), y1 = Y(magnitude); const fill = d.mode === "Both" ? C.gold : d.mode === "Short" ? C.red : C.green; return setHover(i)} style={{ cursor: "crosshair" }}> {d.mode} ; })} {hover != null && data[hover] && (() => { const d = data[hover]; const v = Number(d[metric] || 0); const px = (padL + hover * bw + bw / 2) / W * 100; const tone = metric === "return" ? (v >= 0 ? C.green : C.red) : (d.mode === "Both" ? C.gold : d.mode === "Short" ? C.red : C.green); return 62} title={d.mode + " · " + (metric === "return" ? "Compounded return" : metric === "winRate" ? "Win rate" : "Closed trades")} value={fmt(v)} tone={tone} metaLeft={metric === "return" ? "Signed strategy result" : metric === "winRate" ? "Winning trades / total" : "Completed positions"} />; })()}
); } Object.assign(window, { Legend, LineChart, MonthBars, GroupedBars, Histogram, Sparkline, MeterBar, ModeBars, SearchSelect, CHART_C: C, fmtPct });