GIFT-Eval-CTX / src /static /explorer-plot.mjs
Xu Liu
update code
8479181
Raw History Blame Contribute Delete
6.54 kB
// Use related colors to distinguish input variants of the same model.
const methodColors = {
seasonal_naive: "#7f7f7f",
chronos_2: "#17becf",
toto_2_0_313m: "#8c564b",
moirai_2: "#d62728",
timesfm_3: "#393b79",
timebraid_6p7b_demo_rewrite_v1: "#3b82f6",
claude_opus_4_8_hist_only: "#e377c2",
claude_opus_4_8_hist_macro_micro: "#e377c2",
gpt_5_6_sol_hist_only: "#2ca02c",
gpt_5_6_sol_hist_macro: "#008c95",
gpt_5_6_sol_hist_macro_micro: "#2ca02c",
moirai_agent_hist_macro: "#bcbd22",
moirai_agent_hist_macro_micro: "#7c9636",
};
const eventColors = { impulse: "#c78463", sustained: "#8d76b5", recurrent: "#5a9d89", mean_shift: "#ba9953", amplitude: "#6f9cbd", onset: "#649b80", disappearance: "#ae8092", phase: "#ab9e58", period: "#858bc1" };
export function eventName(event) {
let name = [event.family, event.subtype, event.direction].filter((value) => value != null && value !== "").join(" / ").replaceAll("_", " ");
if (event.amplitude_trend) name += ` (inner ${event.amplitude_trend})`;
else if (event.envelope_change) name += ` (envelope ${event.envelope_change})`;
return name;
}
export function historySeries(history, context = 300) {
const count = context === "all" ? history.length : Math.min(history.length, Number(context));
return { x: Array.from({ length: count }, (_, index) => index - count), y: history.slice(-count) };
}
export function intervalGeometry(event) {
return { center: (event.start + event.end) / 2, width: event.end - event.start + 1 };
}
export function trendSegments(trend, horizon) {
let start = 0;
return (trend || []).map((segment, index) => {
const end = index === trend.length - 1 ? horizon : Math.max(start + 1, Math.min(horizon, start + Math.round(segment.duration * horizon)));
const result = { ...segment, start, end };
start = end;
return result;
});
}
export function rankedMethods(forecasts) {
return [...forecasts].sort((a, b) => a.mase - b.mase || a.id.localeCompare(b.id));
}
export function makePlot(sample, selected, context = 300) {
const history = historySeries(sample.history, context);
const futureX = Array.from({ length: sample.n_horizon }, (_, i) => i);
const predictions = rankedMethods(sample.forecasts).filter((method) => selected.has(method.id));
const nameCounts = new Map();
for (const method of sample.forecasts) nameCounts.set(method.name, (nameCounts.get(method.name) || 0) + 1);
const extent = [...history.y, ...sample.future, ...predictions.flatMap((method) => method.forecast), ...sample.tsfm_average_forecast].filter((value) => value != null && Number.isFinite(value));
const low = Math.min(...extent), high = Math.max(...extent);
const padding = (high - low || Math.abs(high) || 1) * .08;
const bottom = low - padding, top = high + padding;
const events = new Map();
for (const event of sample.micro) {
const name = eventName(event);
if (!events.has(name)) events.set(name, []);
events.get(name).push(event);
}
const traces = [...events].map(([name, group], index) => ({
type: "bar", name, legendgroup: name,
x: group.map((event) => intervalGeometry(event).center),
width: group.map((event) => intervalGeometry(event).width),
y: group.map(() => top - bottom), base: bottom,
marker: { color: eventColors[group[0].subtype || group[0].family] || "#8298b1", opacity: .15, line: { width: 0 } },
customdata: group.map((event) => [event.start, event.end, event.intensity == null ? "—" : Number(event.intensity).toFixed(3)]),
hovertemplate: `${name}<br>Future indices %{customdata[0]}–%{customdata[1]}<br>Intensity %{customdata[2]}<extra></extra>`,
legendrank: 3 + index,
}));
const line = (name, x, y, color, dash, legendrank, width = 1.7) => ({ type: "scatter", mode: "lines", name, x, y, legendrank, connectgaps: false, line: { color, width, dash }, hovertemplate: `${name}<br>Step %{x}<br>Value %{y:.4g}<extra></extra>` });
traces.push(line("History", history.x, history.y, "#1f77b4", "solid", 0, 1.6));
traces.push(line("Future", futureX, sample.future, "#ff7f0e", "dash", 1, 2.4));
traces.push(line("TSFM average forecast", futureX, sample.tsfm_average_forecast, "#9467bd", "dot", 2));
for (const [index, method] of predictions.entries()) {
const dash = method.id.endsWith("_hist_only") ? "dash" : "solid";
const name = nameCounts.get(method.name) > 1 ? `${method.name} (${method.input})` : method.name;
traces.push(line(name, futureX, method.forecast, methodColors[method.id] || "#8c564b", dash, 3 + events.size + index));
}
const shapes = [{ type: "line", x0: -.5, x1: -.5, y0: 0, y1: 1, yref: "paper", line: { color: "#a8b1bc", width: 1, dash: "dot" } }];
const annotations = [];
const trendBand = { y0: 1.005, y1: 1.08, yref: "paper" };
const segments = trendSegments(sample.macro.trend, sample.n_horizon);
if (segments.length) {
annotations.push({ x: -.5, xref: "x", y: (trendBand.y0 + trendBand.y1) / 2, yref: "paper", xanchor: "right", yanchor: "middle", xshift: -8, text: "Macro trend:", showarrow: false, font: { size: 11, color: "#5b6877" } });
}
for (const segment of segments) {
shapes.push({ ...trendBand, type: "rect", x0: segment.start - .5, x1: segment.end - .5, fillcolor: { up: "#e5f1ec", down: "#f7e9df", flat: "#eef0f4" }[segment.direction] || "#eef0f4", line: { color: "#fff", width: 1 } });
annotations.push({ x: (segment.start + segment.end - 1) / 2, y: (trendBand.y0 + trendBand.y1) / 2, yref: "paper", xanchor: "center", yanchor: "middle", text: segment.direction, hovertext: `Macro trend: ${segment.direction}`, showarrow: false, font: { size: 11, color: "#5b6877" } });
}
return {
data: traces,
layout: {
height: 545, margin: { l: 65, r: 22, t: 55, b: 90 },
paper_bgcolor: "#fff", plot_bgcolor: "#fff",
font: { family: "system-ui, sans-serif", color: "#687383", size: 11 },
xaxis: { title: { text: "Time Step" }, range: [-history.x.length - .5, sample.n_horizon - .5], gridcolor: "#eef1f4", zeroline: false },
yaxis: { title: { text: "Value" }, range: [bottom, top], gridcolor: "#eef1f4", zeroline: false },
legend: { orientation: "h", x: 0, y: -.17, yanchor: "top", font: { size: 10 }, traceorder: "normal", maxheight: .25 },
barmode: "overlay", bargap: 0, hovermode: "closest", shapes, annotations,
uirevision: `${sample.id}/${context}`, dragmode: "zoom",
},
config: { responsive: true, displaylogo: false, scrollZoom: false, modeBarButtonsToRemove: ["select2d", "lasso2d"] },
};
}