Skip to content

Commit 0f07c8c

Browse files
committed
Added metric bars and children links in the sidebar ; added up/down keyboard shortcuts in the list view
1 parent 73de487 commit 0f07c8c

File tree

5 files changed

+191
-22
lines changed

5 files changed

+191
-22
lines changed

scripts/static/js/graph.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ export function selectProgram(programId) {
104104
}
105105
nodeElem.classed("node-hovered", false);
106106
});
107+
// Dispatch event for list view sync
108+
window.dispatchEvent(new CustomEvent('node-selected', { detail: { id: programId } }));
107109
}
108110

109111
let svg = null;

scripts/static/js/list.js

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ export function renderNodeList(nodes) {
5555
<span class="summary-label">Average</span>
5656
<span class="summary-value">${avgScore.toFixed(4)}</span>
5757
${renderMetricBar(avgScore, minScore, maxScore)}
58+
<span style="margin-left:1.2em;font-size:0.98em;color:#888;vertical-align:middle;">
59+
<span title="Total programs, generations, islands">📦</span> Total: ${nodes.length} programs, ${new Set(nodes.map(n => n.generation)).size} generations, ${new Set(nodes.map(n => n.island)).size} islands
60+
</span>
5861
</div>
5962
`;
6063
container.innerHTML = '';
@@ -151,6 +154,12 @@ export function renderNodeList(nodes) {
151154
}, 0);
152155
container.appendChild(row);
153156
});
157+
container.focus();
158+
// Scroll to selected node if present
159+
const selected = container.querySelector('.node-list-item.selected');
160+
if (selected) {
161+
selected.scrollIntoView({behavior: 'smooth', block: 'center'});
162+
}
154163
}
155164
export function selectListNodeById(id) {
156165
setSelectedProgramId(id);
@@ -200,4 +209,53 @@ function showSidebarListView() {
200209
} else {
201210
showSidebar();
202211
}
203-
}
212+
}
213+
214+
// Sync selection when switching to list tab
215+
const tabListBtn = document.getElementById('tab-list');
216+
if (tabListBtn) {
217+
tabListBtn.addEventListener('click', () => {
218+
renderNodeList(allNodeData);
219+
});
220+
}
221+
222+
// Keyboard navigation for up/down in list view
223+
const nodeListContainer = document.getElementById('node-list-container');
224+
if (nodeListContainer) {
225+
nodeListContainer.tabIndex = 0;
226+
nodeListContainer.addEventListener('keydown', function(e) {
227+
if (!['ArrowUp', 'ArrowDown'].includes(e.key)) return;
228+
e.preventDefault(); // Always prevent default to avoid browser scroll
229+
const items = Array.from(nodeListContainer.querySelectorAll('.node-list-item'));
230+
if (!items.length) return;
231+
let idx = items.findIndex(item => item.classList.contains('selected'));
232+
if (idx === -1) idx = 0;
233+
if (e.key === 'ArrowUp' && idx > 0) idx--;
234+
if (e.key === 'ArrowDown' && idx < items.length - 1) idx++;
235+
const nextItem = items[idx];
236+
if (nextItem) {
237+
const nodeId = nextItem.getAttribute('data-node-id');
238+
selectListNodeById(nodeId);
239+
nextItem.focus();
240+
nextItem.scrollIntoView({behavior: 'smooth', block: 'center'});
241+
// Also scroll the page if needed
242+
const rect = nextItem.getBoundingClientRect();
243+
if (rect.top < 0 || rect.bottom > window.innerHeight) {
244+
window.scrollTo({top: window.scrollY + rect.top - 100, behavior: 'smooth'});
245+
}
246+
}
247+
});
248+
// Focus container on click to enable keyboard nav
249+
nodeListContainer.addEventListener('click', function() {
250+
nodeListContainer.focus();
251+
});
252+
}
253+
254+
// Listen for node selection events from other views and sync selection in the list view
255+
window.addEventListener('node-selected', function(e) {
256+
// e.detail should contain the selected node id
257+
if (e.detail && e.detail.id) {
258+
setSelectedProgramId(e.detail.id);
259+
renderNodeList(allNodeData);
260+
}
261+
});

scripts/static/js/main.js

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { updateListSidebarLayout, renderNodeList } from './list.js';
55
import { renderGraph, g, getNodeRadius, animateGraphNodeAttributes } from './graph.js';
66

77
export let allNodeData = [];
8+
let metricMinMax = {};
89

910
let archiveProgramIds = [];
1011

@@ -13,15 +14,55 @@ const sidebarEl = document.getElementById('sidebar');
1314
let lastDataStr = null;
1415
let selectedProgramId = null;
1516

17+
function computeMetricMinMax(nodes) {
18+
metricMinMax = {};
19+
if (!nodes) return;
20+
nodes.forEach(n => {
21+
if (n.metrics && typeof n.metrics === 'object') {
22+
for (const [k, v] of Object.entries(n.metrics)) {
23+
if (typeof v === 'number' && isFinite(v)) {
24+
if (!(k in metricMinMax)) {
25+
metricMinMax[k] = {min: v, max: v};
26+
} else {
27+
metricMinMax[k].min = Math.min(metricMinMax[k].min, v);
28+
metricMinMax[k].max = Math.max(metricMinMax[k].max, v);
29+
}
30+
}
31+
}
32+
}
33+
});
34+
// Avoid min==max
35+
for (const k in metricMinMax) {
36+
if (metricMinMax[k].min === metricMinMax[k].max) {
37+
metricMinMax[k].min = 0;
38+
metricMinMax[k].max = 1;
39+
}
40+
}
41+
}
42+
1643
function formatMetrics(metrics) {
17-
return Object.entries(metrics).map(([k, v]) => `<b>${k}</b>: ${v}`).join('<br>');
44+
if (!metrics || typeof metrics !== 'object') return '';
45+
let rows = Object.entries(metrics).map(([k, v]) => {
46+
let min = 0, max = 1;
47+
if (metricMinMax[k]) {
48+
min = metricMinMax[k].min;
49+
max = metricMinMax[k].max;
50+
}
51+
let valStr = (typeof v === 'number' && isFinite(v)) ? v.toFixed(4) : v;
52+
return `<tr><td style='padding-right:0.7em;'><b>${k}</b></td><td style='padding-right:0.7em;'>${valStr}</td><td style='min-width:90px;'>${typeof v === 'number' ? renderMetricBar(v, min, max) : ''}</td></tr>`;
53+
}).join('');
54+
return `<table class='metrics-table'><tbody>${rows}</tbody></table>`;
1855
}
1956

2057
function renderMetricBar(value, min, max, opts={}) {
2158
let percent = 0;
22-
if (typeof value === 'number' && isFinite(value) && max > min) {
23-
percent = (value - min) / (max - min);
24-
percent = Math.max(0, Math.min(1, percent));
59+
if (typeof value === 'number' && isFinite(value)) {
60+
if (max > min) {
61+
percent = (value - min) / (max - min);
62+
percent = Math.max(0, Math.min(1, percent));
63+
} else if (max === min) {
64+
percent = 1; // Show as filled if min==max
65+
}
2566
}
2667
let minLabel = `<span class="metric-bar-min">${min.toFixed(2)}</span>`;
2768
let maxLabel = `<span class="metric-bar-max">${max.toFixed(2)}</span>`;
@@ -201,10 +242,11 @@ document.getElementById('tab-branching').addEventListener('click', function() {
201242
// Export all shared state and helpers for use in other modules
202243
export function setAllNodeData(nodes) {
203244
allNodeData = nodes;
245+
computeMetricMinMax(nodes);
204246
}
205247

206248
export function setSelectedProgramId(id) {
207249
selectedProgramId = id;
208250
}
209251

210-
export { archiveProgramIds, lastDataStr, selectedProgramId, formatMetrics, renderMetricBar, getHighlightNodes, getSelectedMetric };
252+
export { archiveProgramIds, lastDataStr, selectedProgramId, formatMetrics, renderMetricBar, getHighlightNodes, getSelectedMetric, metricMinMax };

scripts/static/js/performance.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ import { selectListNodeById } from './list.js';
172172
export function selectPerformanceNodeById(id, opts = {}) {
173173
setSelectedProgramId(id);
174174
setSidebarSticky(true);
175+
// Dispatch event for list view sync
176+
window.dispatchEvent(new CustomEvent('node-selected', { detail: { id } }));
175177
if (typeof allNodeData !== 'undefined' && allNodeData.length) {
176178
updatePerformanceGraph(allNodeData, opts);
177179
const node = allNodeData.find(n => n.id == id);

scripts/static/js/sidebar.js

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { scrollAndSelectNodeById } from './graph.js';
33

44
const sidebar = document.getElementById('sidebar');
55
export let sidebarSticky = false;
6+
let lastSidebarTab = null;
67

78
export function showSidebar() {
89
sidebar.style.transform = 'translateX(0)';
@@ -24,7 +25,6 @@ export function showSidebarContent(d, fromHover = false) {
2425
if (archiveProgramIds && archiveProgramIds.includes(d.id)) {
2526
starHtml = '<span style="position:relative;top:0.05em;left:0.15em;font-size:1.6em;color:#FFD600;z-index:10;" title="MAP-elites member" aria-label="MAP-elites member">★</span>';
2627
}
27-
// Locator icon button (left of close X)
2828
let locatorBtn = '<button id="sidebar-locator-btn" title="Locate selected node" aria-label="Locate selected node" style="position:absolute;top:0.05em;right:2.5em;font-size:1.5em;background:none;border:none;color:#FFD600;cursor:pointer;z-index:10;line-height:1;filter:drop-shadow(0 0 2px #FFD600);">⦿</button>';
2929
let closeBtn = '<button id="sidebar-close-btn" style="position:absolute;top:0.05em;right:0.15em;font-size:1.6em;background:none;border:none;color:#888;cursor:pointer;z-index:10;line-height:1;">&times;</button>';
3030
let openLink = '<div style="text-align:center;margin:-1em 0 1.2em 0;"><a href="/program/' + d.id + '" target="_blank" class="open-in-new" style="font-size:0.95em;">[open in new window]</a></div>';
@@ -33,17 +33,45 @@ export function showSidebarContent(d, fromHover = false) {
3333
let tabNames = [];
3434
if (d.code && typeof d.code === 'string' && d.code.trim() !== '') tabNames.push('Code');
3535
if (d.prompts && typeof d.prompts === 'object' && Object.keys(d.prompts).length > 0) tabNames.push('Prompts');
36-
if (tabNames.length > 0) {
37-
tabHtml = '<div id="sidebar-tab-bar" style="display:flex;gap:0.7em;margin-bottom:0.7em;">' +
38-
tabNames.map((name, i) => `<span class="sidebar-tab${i===0?' active':''}" data-tab="${name}">${name}</span>`).join('') + '</div>';
39-
tabContentHtml = '<div id="sidebar-tab-content">';
40-
if (tabNames[0] === 'Code') tabContentHtml += `<pre class="sidebar-code-pre">${d.code}</pre>`;
41-
if (tabNames[0] === 'Prompts') {
36+
const children = allNodeData.filter(n => n.parent_id === d.id);
37+
if (children.length > 0) tabNames.push('Children');
38+
let activeTab = lastSidebarTab && tabNames.includes(lastSidebarTab) ? lastSidebarTab : tabNames[0];
39+
40+
// Helper to render tab content
41+
function renderSidebarTabContent(tabName, d, children) {
42+
if (tabName === 'Code') {
43+
return `<pre class="sidebar-code-pre">${d.code}</pre>`;
44+
}
45+
if (tabName === 'Prompts') {
46+
let html = '';
4247
for (const [k, v] of Object.entries(d.prompts)) {
43-
tabContentHtml += `<div style="margin-bottom:0.7em;"><b>${k}:</b><pre class="sidebar-pre">${v}</pre></div>`;
48+
html += `<div style="margin-bottom:0.7em;"><b>${k}:</b><pre class="sidebar-pre">${v}</pre></div>`;
4449
}
50+
return html;
4551
}
46-
tabContentHtml += '</div>';
52+
if (tabName === 'Children') {
53+
const metric = (document.getElementById('metric-select') && document.getElementById('metric-select').value) || 'combined_score';
54+
let min = 0, max = 1;
55+
const vals = children.map(child => (child.metrics && typeof child.metrics[metric] === 'number') ? child.metrics[metric] : null).filter(x => x !== null);
56+
if (vals.length > 0) {
57+
min = Math.min(...vals);
58+
max = Math.max(...vals);
59+
}
60+
return `<div><ul style='margin:0.5em 0 0 1em;padding:0;'>` +
61+
children.map(child => {
62+
let val = (child.metrics && typeof child.metrics[metric] === 'number') ? child.metrics[metric].toFixed(4) : '(no value)';
63+
let bar = (child.metrics && typeof child.metrics[metric] === 'number') ? renderMetricBar(child.metrics[metric], min, max) : '';
64+
return `<li style='margin-bottom:0.3em;'><a href="#" class="child-link" data-child="${child.id}">${child.id}</a><br /><br /> <span style='margin-left:0.5em;'>${val}</span> ${bar}</li>`;
65+
}).join('') +
66+
`</ul></div>`;
67+
}
68+
return '';
69+
}
70+
71+
if (tabNames.length > 0) {
72+
tabHtml = '<div id="sidebar-tab-bar" style="display:flex;gap:0.7em;margin-bottom:0.7em;">' +
73+
tabNames.map((name) => `<span class="sidebar-tab${name===activeTab?' active':''}" data-tab="${name}">${name}</span>`).join('') + '</div>';
74+
tabContentHtml = `<div id="sidebar-tab-content">${renderSidebarTabContent(activeTab, d, children)}</div>`;
4775
}
4876
let parentIslandHtml = '';
4977
if (d.parent_id && d.parent_id !== 'None') {
@@ -72,18 +100,55 @@ export function showSidebarContent(d, fromHover = false) {
72100
Array.from(tabBar.children).forEach(e => e.classList.remove('active'));
73101
tabEl.classList.add('active');
74102
const tabName = tabEl.dataset.tab;
103+
lastSidebarTab = tabName;
75104
const tabContent = document.getElementById('sidebar-tab-content');
76-
if (tabName === 'Code') tabContent.innerHTML = `<pre class="sidebar-code-pre">${d.code}</pre>`;
77-
if (tabName === 'Prompts') {
78-
let html = '';
79-
for (const [k, v] of Object.entries(d.prompts)) {
80-
html += `<div style="margin-bottom:0.7em;"><b>${k}:</b><pre class="sidebar-pre">${v}</pre></div>`;
105+
tabContent.innerHTML = renderSidebarTabContent(tabName, d, children);
106+
setTimeout(() => {
107+
document.querySelectorAll('.child-link').forEach(link => {
108+
link.onclick = function(e) {
109+
e.preventDefault();
110+
const childNode = allNodeData.find(n => n.id == link.dataset.child);
111+
if (childNode) {
112+
window._lastSelectedNodeData = childNode;
113+
const perfTabBtn = document.getElementById('tab-performance');
114+
const perfTabView = document.getElementById('view-performance');
115+
if ((perfTabBtn && perfTabBtn.classList.contains('active')) || (perfTabView && perfTabView.classList.contains('active'))) {
116+
import('./performance.js').then(mod => {
117+
mod.selectPerformanceNodeById(childNode.id);
118+
showSidebar();
119+
});
120+
} else {
121+
scrollAndSelectNodeById(childNode.id);
122+
}
123+
}
124+
};
125+
});
126+
}, 0);
127+
};
128+
});
129+
}
130+
setTimeout(() => {
131+
document.querySelectorAll('.child-link').forEach(link => {
132+
link.onclick = function(e) {
133+
e.preventDefault();
134+
const childNode = allNodeData.find(n => n.id == link.dataset.child);
135+
if (childNode) {
136+
window._lastSelectedNodeData = childNode;
137+
// Check if performance tab is active
138+
const perfTabBtn = document.getElementById('tab-performance');
139+
const perfTabView = document.getElementById('view-performance');
140+
if ((perfTabBtn && perfTabBtn.classList.contains('active')) || (perfTabView && perfTabView.classList.contains('active'))) {
141+
import('./performance.js').then(mod => {
142+
mod.selectPerformanceNodeById(childNode.id);
143+
showSidebar();
144+
});
145+
} else {
146+
scrollAndSelectNodeById(childNode.id);
81147
}
82-
tabContent.innerHTML = html;
83148
}
84149
};
85150
});
86-
}
151+
}, 0);
87152
const closeBtnEl = document.getElementById('sidebar-close-btn');
88153
if (closeBtnEl) closeBtnEl.onclick = function() {
89154
setSelectedProgramId(null);

0 commit comments

Comments
 (0)