Skip to content

Commit fef791f

Browse files
committed
functionality to combine different metrics into a single graph for a run
1 parent e9dffb4 commit fef791f

3 files changed

Lines changed: 251 additions & 3 deletions

File tree

stable_pretraining/web/assets/app.css

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,36 @@ body {
652652
}
653653
.chart-panel:hover .chart-download-btn { opacity: 1; }
654654

655+
.chart-combine-toggle {
656+
flex-shrink: 0;
657+
opacity: 0;
658+
transition: opacity 0.15s;
659+
font-size: 13px;
660+
font-weight: 700;
661+
padding: 1px 6px;
662+
}
663+
.chart-panel:hover .chart-combine-toggle { opacity: 0.6; }
664+
.chart-combine-toggle.active { opacity: 1; }
665+
666+
.chart-remove-btn {
667+
flex-shrink: 0;
668+
font-size: 14px;
669+
padding: 1px 7px;
670+
}
671+
672+
.combined-chart-panel {
673+
border-color: var(--accent);
674+
border-width: 2px;
675+
}
676+
677+
.combined-charts-section {
678+
display: flex;
679+
flex-direction: column;
680+
gap: 16px;
681+
margin-bottom: 16px;
682+
}
683+
.combined-charts-section:empty { display: none; }
684+
655685
.chart-zoom-reset {
656686
flex-shrink: 0;
657687
background: none;

stable_pretraining/web/assets/app.js

Lines changed: 220 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
scatterX: null, // 'hparams.lr' | 'summary.val_acc' | …
4040
scatterY: null,
4141
tableHideSame: false,
42+
combineSelection: new Set(), // metric names currently selected for combining
43+
combinedCharts: [], // [{id, metrics: string[]}]
4244
};
4345

4446
// setInterval handles for log auto-refresh; null when not running.
@@ -116,6 +118,7 @@
116118
tableHideSame: state.tableHideSame,
117119
activeTab: state.activeTab,
118120
theme: state.theme,
121+
combinedCharts: state.combinedCharts,
119122
};
120123
try { localStorage.setItem('spt-web-state', JSON.stringify(snap)); } catch {}
121124
const ids = [...state.visible].map(id => encodeURIComponent(id)).join(',');
@@ -162,6 +165,7 @@
162165
if (typeof saved.tableHideSame === 'boolean') state.tableHideSame = saved.tableHideSame;
163166
if (typeof saved.activeTab === 'string') state.activeTab = saved.activeTab;
164167
if (typeof saved.theme === 'string') state.theme = saved.theme;
168+
if (Array.isArray(saved.combinedCharts)) state.combinedCharts = saved.combinedCharts;
165169
if (Array.isArray(saved.visible)) state._pendingVisible = new Set(saved.visible);
166170
}
167171

@@ -1077,7 +1081,7 @@
10771081
const mediaTags = visibleMediaTags();
10781082
const allTags = [...new Set([...metrics, ...mediaTags.keys()])];
10791083

1080-
if (allTags.length === 0) {
1084+
if (allTags.length === 0 && state.combinedCharts.length === 0) {
10811085
for (const { plot } of state.charts.values()) plot?.destroy();
10821086
state.charts.clear();
10831087
for (const { panel } of state.mediaPanels.values()) panel.remove();
@@ -1088,15 +1092,19 @@
10881092
return;
10891093
}
10901094

1091-
// Tear down chart panels for metrics that vanished.
1095+
// Tear down chart panels for metrics that vanished (skip combined charts).
10921096
const metricSet = new Set(metrics);
1097+
let selectionChanged = false;
10931098
for (const [name, entry] of [...state.charts.entries()]) {
1099+
if (name.startsWith('__combined__/')) continue;
10941100
if (!metricSet.has(name) || mediaTags.has(name)) {
10951101
entry.plot?.destroy();
10961102
entry.panel.remove();
10971103
state.charts.delete(name);
1104+
if (state.combineSelection.delete(name)) selectionChanged = true;
10981105
}
10991106
}
1107+
if (selectionChanged) updateCombineSelectionUI();
11001108
// Tear down media panels for tags that vanished.
11011109
for (const [tag, entry] of [...state.mediaPanels.entries()]) {
11021110
if (!mediaTags.has(tag)) {
@@ -1105,6 +1113,14 @@
11051113
}
11061114
}
11071115

1116+
// Combined charts section — pinned above the metric tree.
1117+
let combinedSection = root.querySelector(':scope > .combined-charts-section');
1118+
if (!combinedSection) {
1119+
combinedSection = document.createElement('div');
1120+
combinedSection.className = 'combined-charts-section';
1121+
}
1122+
renderCombinedCharts(combinedSection);
1123+
11081124
// Skip the full tree rebuild when nothing structurally changed.
11091125
// Streaming-metrics chunks fire scheduleRerender many times per
11101126
// second; re-parenting a playing ``<video>`` (or just shifting page
@@ -1120,7 +1136,7 @@
11201136
const container = document.createElement('div');
11211137
container.className = 'metric-tree';
11221138
attachMetricTree(container, tree, '', mediaTags);
1123-
root.replaceChildren(container);
1139+
root.replaceChildren(combinedSection, container);
11241140
state._lastTreeKey = treeKey;
11251141

11261142
// After re-parenting, sizes may have changed. Resize each plot.
@@ -1131,6 +1147,8 @@
11311147
if (w) entry.plot.setSize({ width: w, height: 240 });
11321148
}
11331149
}
1150+
} else if (root.firstChild !== combinedSection) {
1151+
root.prepend(combinedSection);
11341152
}
11351153

11361154
for (const name of metrics) updateChart(name);
@@ -1360,6 +1378,21 @@
13601378
span.textContent = displayName || fullName;
13611379
span.title = fullName;
13621380
title.appendChild(span);
1381+
const toggleBtn = document.createElement('button');
1382+
toggleBtn.className = 'chart-combine-toggle icon-btn';
1383+
toggleBtn.type = 'button';
1384+
toggleBtn.title = 'select to combine into one chart';
1385+
toggleBtn.textContent = '+';
1386+
toggleBtn.addEventListener('click', (e) => {
1387+
e.stopPropagation();
1388+
if (state.combineSelection.has(fullName)) {
1389+
state.combineSelection.delete(fullName);
1390+
} else {
1391+
state.combineSelection.add(fullName);
1392+
}
1393+
updateCombineSelectionUI();
1394+
});
1395+
title.appendChild(toggleBtn);
13631396
const dlBtn = document.createElement('button');
13641397
dlBtn.className = 'chart-download-btn icon-btn';
13651398
dlBtn.type = 'button';
@@ -1690,6 +1723,179 @@
16901723
return n.includes('loss') || n.includes('err') || n.includes('perplexity') || n.includes('ppl');
16911724
}
16921725

1726+
// ---- combine-metrics feature ------------------------------------------
1727+
1728+
function updateCombineSelectionUI() {
1729+
const btn = document.getElementById('combine-btn');
1730+
if (!btn) return;
1731+
const n = state.combineSelection.size;
1732+
btn.textContent = `Combine (${n})`;
1733+
btn.hidden = n < 2;
1734+
for (const [name, entry] of state.charts.entries()) {
1735+
if (name.startsWith('__combined__/')) continue;
1736+
const toggleBtn = entry.panel.querySelector('.chart-combine-toggle');
1737+
if (toggleBtn) toggleBtn.classList.toggle('active', state.combineSelection.has(name));
1738+
}
1739+
}
1740+
1741+
function buildCombinedChartData(metricNames, runIds) {
1742+
const series = [];
1743+
const xUnion = new Set();
1744+
const multiRun = runIds.length > 1;
1745+
for (const metricName of metricNames) {
1746+
for (const id of runIds) {
1747+
const m = state.metrics.get(id);
1748+
if (!m || !m.metrics[metricName]) continue;
1749+
const col = m.metrics[metricName];
1750+
const xs = col[state.xAxis];
1751+
const fallback = (state.xAxis === 'epoch' && !xs?.some(v => v != null)) ? col.step : xs;
1752+
const xArr = fallback || col.step;
1753+
const filtered = { id, metricName, xs: [], ys: [] };
1754+
for (let i = 0; i < col.y.length; i++) {
1755+
const x = xArr ? xArr[i] : i;
1756+
if (x == null || !isFinite(x)) continue;
1757+
filtered.xs.push(x);
1758+
filtered.ys.push(col.y[i]);
1759+
xUnion.add(x);
1760+
}
1761+
if (filtered.ys.length) series.push(filtered);
1762+
}
1763+
}
1764+
const xs = [...xUnion].sort((a, b) => a - b);
1765+
const xIdx = new Map();
1766+
for (let i = 0; i < xs.length; i++) xIdx.set(xs[i], i);
1767+
const seriesCfg = [{}];
1768+
const data = [xs];
1769+
for (const s of series) {
1770+
const arr = new Array(xs.length).fill(null);
1771+
for (let i = 0; i < s.xs.length; i++) arr[xIdx.get(s.xs[i])] = s.ys[i];
1772+
data.push(emaSmooth(arr, state.smoothing));
1773+
const label = multiRun ? `${s.metricName} · ${s.id}` : s.metricName;
1774+
seriesCfg.push({
1775+
label,
1776+
runId: label,
1777+
stroke: runColor(multiRun ? `${s.metricName}:${s.id}` : s.metricName),
1778+
width: 1.6,
1779+
points: { show: false },
1780+
spanGaps: true,
1781+
});
1782+
}
1783+
const seriesStats = series.map(s => {
1784+
const raw = s.ys.filter(v => v != null && isFinite(v));
1785+
const lastVal = raw.length ? raw[raw.length - 1] : null;
1786+
const bestVal = raw.length ? Math.max(...raw) : null;
1787+
const label = multiRun ? `${s.metricName} · ${s.id}` : s.metricName;
1788+
return { id: label, lastVal, bestVal };
1789+
});
1790+
return { data, seriesCfg, seriesStats };
1791+
}
1792+
1793+
function makeCombinedPanel(combinedId, metricNames) {
1794+
const panel = document.createElement('div');
1795+
panel.className = 'chart-panel combined-chart-panel';
1796+
const title = document.createElement('div');
1797+
title.className = 'chart-title';
1798+
const span = document.createElement('span');
1799+
span.className = 'name';
1800+
const shortNames = metricNames.map(n => n.split('/').pop());
1801+
span.textContent = 'Combined: ' + shortNames.join(', ');
1802+
span.title = metricNames.join(', ');
1803+
title.appendChild(span);
1804+
const removeBtn = document.createElement('button');
1805+
removeBtn.className = 'chart-remove-btn icon-btn';
1806+
removeBtn.type = 'button';
1807+
removeBtn.title = 'remove combined chart';
1808+
removeBtn.textContent = '×';
1809+
removeBtn.addEventListener('click', () => {
1810+
state.combinedCharts = state.combinedCharts.filter(c => c.id !== combinedId);
1811+
const key = '__combined__/' + combinedId;
1812+
const entry = state.charts.get(key);
1813+
if (entry) { entry.plot?.destroy(); state.charts.delete(key); }
1814+
panel.remove();
1815+
saveState();
1816+
updateCombineSelectionUI();
1817+
});
1818+
title.appendChild(removeBtn);
1819+
const dlBtn = document.createElement('button');
1820+
dlBtn.className = 'chart-download-btn icon-btn';
1821+
dlBtn.type = 'button';
1822+
dlBtn.title = 'download chart as PNG';
1823+
dlBtn.textContent = '⬇';
1824+
dlBtn.addEventListener('click', () => {
1825+
downloadChartPNG('__combined__/' + combinedId, 'Combined: ' + shortNames.join(', '));
1826+
});
1827+
title.appendChild(dlBtn);
1828+
const resetBtn = document.createElement('button');
1829+
resetBtn.className = 'chart-zoom-reset';
1830+
resetBtn.type = 'button';
1831+
resetBtn.title = 'reset zoom';
1832+
resetBtn.textContent = '⤢';
1833+
resetBtn.hidden = true;
1834+
title.appendChild(resetBtn);
1835+
panel.appendChild(title);
1836+
const body = document.createElement('div');
1837+
body.className = 'chart-body';
1838+
panel.appendChild(body);
1839+
const annot = document.createElement('div');
1840+
annot.className = 'chart-annot';
1841+
panel.appendChild(annot);
1842+
return panel;
1843+
}
1844+
1845+
function renderCombinedCharts(container) {
1846+
const visibleIds = effectivelyVisible().sort();
1847+
const validIds = new Set(state.combinedCharts.map(c => c.id));
1848+
for (const [key, entry] of [...state.charts.entries()]) {
1849+
if (!key.startsWith('__combined__/')) continue;
1850+
const id = key.slice('__combined__/'.length);
1851+
if (!validIds.has(id)) {
1852+
entry.plot?.destroy();
1853+
entry.panel.remove();
1854+
state.charts.delete(key);
1855+
}
1856+
}
1857+
for (const combined of state.combinedCharts) {
1858+
const key = '__combined__/' + combined.id;
1859+
let entry = state.charts.get(key);
1860+
if (!entry) {
1861+
const panel = makeCombinedPanel(combined.id, combined.metrics);
1862+
entry = { panel, plot: null, configKey: '' };
1863+
state.charts.set(key, entry);
1864+
}
1865+
container.appendChild(entry.panel);
1866+
const { data, seriesCfg, seriesStats } = buildCombinedChartData(combined.metrics, visibleIds);
1867+
const configKey = JSON.stringify([visibleIds, combined.metrics, state.xAxis, state.logY, state.theme]);
1868+
if (seriesCfg.length <= 1) {
1869+
if (entry.plot) { entry.plot.destroy(); entry.plot = null; entry.configKey = ''; }
1870+
updateAnnotTable(key, entry, [], []);
1871+
continue;
1872+
}
1873+
if (entry.plot && entry.configKey === configKey) {
1874+
entry.plot.setData(data);
1875+
updateAnnotTable(key, entry, seriesCfg, seriesStats);
1876+
continue;
1877+
}
1878+
if (entry.plot) entry.plot.destroy();
1879+
const body = entry.panel.querySelector('.chart-body');
1880+
const resetBtn = entry.panel.querySelector('.chart-zoom-reset');
1881+
if (resetBtn) resetBtn.hidden = true;
1882+
const width = body.clientWidth || 400;
1883+
entry.plot = new uPlot(makeUplotOpts(key, width, seriesCfg, resetBtn), data, body);
1884+
if (resetBtn) {
1885+
resetBtn.onclick = () => {
1886+
for (const [, e] of state.charts) {
1887+
if (!e.plot) continue;
1888+
const xData = e.plot.data[0];
1889+
if (!xData || !xData.length) continue;
1890+
e.plot.setScale('x', { min: xData[0], max: xData[xData.length - 1] });
1891+
}
1892+
};
1893+
}
1894+
entry.configKey = configKey;
1895+
updateAnnotTable(key, entry, seriesCfg, seriesStats);
1896+
}
1897+
}
1898+
16931899
function fmtTooltipNum(v) {
16941900
if (v == null || !isFinite(v)) return '—';
16951901
if (Number.isInteger(v)) return String(v);
@@ -3187,6 +3393,17 @@
31873393

31883394
document.getElementById('export-csv').addEventListener('click', exportMetricsCSV);
31893395

3396+
document.getElementById('combine-btn').addEventListener('click', () => {
3397+
if (state.combineSelection.size < 2) return;
3398+
const id = String(Date.now());
3399+
const metrics = [...state.combineSelection];
3400+
state.combinedCharts.push({ id, metrics });
3401+
state.combineSelection.clear();
3402+
saveState();
3403+
scheduleRerender();
3404+
updateCombineSelectionUI();
3405+
});
3406+
31903407
document.getElementById('add-filter-btn').addEventListener('click', () => openFilterDraft(null));
31913408

31923409
document.getElementById('group-by').addEventListener('change', e => {

stable_pretraining/web/assets/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
autocomplete="off" spellcheck="false">
7474
<span id="metric-search-status" class="metric-search-status"></span>
7575
<button id="export-csv" class="icon-btn" type="button" title="export visible metrics as CSV">⬇ CSV</button>
76+
<button id="combine-btn" class="icon-btn" type="button" title="combine selected metrics into one chart" hidden>Combine (0)</button>
7677
</div>
7778
<div id="charts"><div class="empty-state">select a run on the left to plot its metrics</div></div>
7879
</div>

0 commit comments

Comments
 (0)