// Sparkline + TrendChart — hand-rolled SVG (matches design exactly).
// Both components read the active SERIES from React context.
const SeriesContext = React.createContext(null);
window.SeriesContext = SeriesContext;
function buildPath(vals, x, y) {
return vals.map((v, i) => `${i === 0 ? 'M' : 'L'} ${x(i)} ${y(v)}`).join(' ');
}
function buildAreaPath(top, bot, x, y) {
let d = '';
top.forEach((v, i) => { d += `${i === 0 ? 'M' : 'L'} ${x(i)} ${y(v)} `; });
for (let i = bot.length - 1; i >= 0; i--) d += `L ${x(i)} ${y(bot[i])} `;
return d + 'Z';
}
function Sparkline({
series, days = 14, width = 320, height = 60,
showTarget = false, showLast = false,
lastLabel = null, color, padX = 0, padY = 8,
targetLabel = null, annotation = null, fullWidth = false,
}) {
const theme = window.HC_THEME.useHCTheme();
const SERIES = React.useContext(SeriesContext);
const s = SERIES && SERIES[series];
if (!s) return null;
const c = color || s.color;
const ch = theme.chart;
const pal = theme.palette;
const raw = s.raw.slice(-days);
const avg = s.avg.slice(-days);
const std = s.std.slice(-days);
// Auto-fit Y to the trend + raw points only. The ±1σ band is excluded
// from the domain calc so the line uses the full vertical space — any
// band edges that extend past the card are clipped softly behind.
let allVals = [...avg, ...raw];
if (s.target != null && showTarget) allVals.push(s.target);
let min = Math.min(...allVals), max = Math.max(...allVals);
const range = max - min || 1;
min -= range * 0.08; max += range * 0.08;
if (s.invert) { [min, max] = [max, min]; }
const x = (i) => padX + (i / (days - 1)) * (width - 2 * padX);
const y = (v) => padY + ((max - v) / (max - min)) * (height - 2 * padY);
const upper = avg.map((v, i) => v + std[i]);
const lower = avg.map((v, i) => v - std[i]);
const lastY = y(avg[avg.length - 1]);
const clipId = `sp-clip-${series}-${width}-${height}`;
return (
);
}
function TrendChart({
series, width = 358, height = 160, refLine, refLabel,
invert = false, yFormat, showVolBand = false, phases = [],
annotations = [], range = 'full', windowDays = 60,
}) {
const theme = window.HC_THEME.useHCTheme();
const pal = theme.palette;
const ch = theme.chart;
const SERIES = React.useContext(SeriesContext);
const s = SERIES && SERIES[series];
if (!s) return null;
// Window: 4w shows 28 days, full shows windowDays (typically ~150).
const total = s.raw.length;
const days = range === '4w' ? Math.min(28, total) : Math.min(windowDays, total);
const offset = total - days; // index offset from full-series to window
const raw = s.raw.slice(-days);
const ewma = s.avg.slice(-days);
// 14-day SMA computed across the full series, then sliced.
const ma14 = s.raw.map((_, i) => {
const start = Math.max(0, i - 13);
const slice = s.raw.slice(start, i + 1);
return slice.reduce((a, b) => a + b, 0) / slice.length;
}).slice(-days);
const std = s.std.slice(-days);
let allVals = [...raw, ...ewma, ...ma14];
if (showVolBand || ch.showBand) {
allVals = allVals.concat(
ewma.map((v, i) => v + std[i]),
ewma.map((v, i) => v - std[i]),
);
}
if (refLine != null) allVals.push(refLine);
let min = Math.min(...allVals), max = Math.max(...allVals);
const r = max - min || 1;
min -= r * 0.1; max += r * 0.1;
if (invert) [min, max] = [max, min];
const padL = 32, padR = 12, padT = 14, padB = 22;
const innerW = width - padL - padR;
const innerH = height - padT - padB;
const x = (i) => padL + (i / Math.max(1, days - 1)) * innerW;
const y = (v) => padT + ((max - v) / (max - min)) * innerH;
const ticks = [];
for (let t = 0; t < 4; t++) {
const frac = t / 3;
const v = invert ? min + (max - min) * (1 - frac) : min + (max - min) * frac;
ticks.push({ v, y: padT + innerH * (1 - frac) });
}
const upper = ewma.map((v, i) => v + std[i]);
const lower = ewma.map((v, i) => v - std[i]);
return (
);
}
function DateTicks({ days, offset, x, height, pal }) {
const SERIES = React.useContext(SeriesContext);
const dates = window.HC_TREND_DATES || null; // injected by TrendsTab
const positions = [0, Math.floor(days / 2), days - 1];
return positions.map((i) => {
let label = '';
if (dates) {
const isoIdx = offset + i;
const iso = dates[isoIdx];
if (iso) {
const dt = new Date(iso + 'T00:00:00');
label = `${['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][dt.getMonth()]} ${dt.getDate()}`;
}
}
return (
{label}
);
});
}
window.Sparkline = Sparkline;
window.TrendChart = TrendChart;