Skip to content

Commit 970d079

Browse files
authored
Merge pull request #65 from jvm123/feat-help-and-visualizer-ui-improvements
Feature: Visualizer UI improvements and related project file changes
2 parents ed874ab + 63180dd commit 970d079

File tree

9 files changed

+392
-52
lines changed

9 files changed

+392
-52
lines changed

Makefile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ docker-build:
5050
docker-run:
5151
docker run --rm -v $(PROJECT_DIR):/app --network="host" $(DOCKER_IMAGE) examples/function_minimization/initial_program.py examples/function_minimization/evaluator.py --config examples/function_minimization/config.yaml --iterations 1000
5252

53-
# Run the lm-eval benchmark
54-
.PHONY: lm-eval
55-
lm-eval:
56-
$(PYTHON) scripts/lm_eval/lm-eval.py
53+
# Run the visualization script
54+
.PHONY: visualizer
55+
visualizer:
56+
$(PYTHON) scripts/visualizer.py --path examples/

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,21 @@ The script in `scripts/visualize.py` allows you to visualize the evolution tree
137137
# Install requirements
138138
pip install -r scripts/requirements.txt
139139

140-
# Start the visualization web server
140+
# Start the visualization web server and have it watch the examples/ folder
141141
python scripts/visualizer.py
142+
143+
# Start the visualization web server with a specific checkpoint
144+
python scripts/visualizer.py --path examples/function_minimization/openevolve_output/checkpoints/checkpoint_100/
142145
```
146+
147+
In the visualization UI, you can
148+
- see the branching of your program evolution in a network visualization, with node radius chosen by the program fitness (= the currently selected metric),
149+
- see the parent-child relationship of nodes and click through them in the sidebar (use the yellow locator icon in the sidebar to center the node in the graph),
150+
- select the metric of interest (with the available metric choices depending on your data set),
151+
- highlight nodes, for example the top score (for the chosen metric) or the MAP-elites members,
152+
- click nodes to see their code and prompts (if available from the checkpoint data) in a sidebar,
153+
- in the "Performance" tab, see their selected metric score vs generation in a graph
154+
143155
![OpenEvolve Visualizer](openevolve-visualizer.png)
144156

145157
### Docker

openevolve-visualizer.png

32.3 KB
Loading

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ dependencies = [
1717
"pyyaml>=6.0",
1818
"numpy>=1.22.0",
1919
"tqdm>=4.64.0",
20+
"flask",
2021
]
2122

2223
[project.optional-dependencies]

scripts/static/js/graph.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export function updateGraphNodeSelection() {
5252
.attr('stroke', d => selectedProgramId === d.id ? 'red' : '#333')
5353
.attr('stroke-width', d => selectedProgramId === d.id ? 3 : 1.5)
5454
.classed('node-selected', d => selectedProgramId === d.id);
55+
updateGraphEdgeSelection(); // update edge highlight when node selection changes
5556
}
5657

5758
export function getNodeColor(d) {
@@ -104,6 +105,9 @@ export function selectProgram(programId) {
104105
}
105106
nodeElem.classed("node-hovered", false);
106107
});
108+
// Dispatch event for list view sync
109+
window.dispatchEvent(new CustomEvent('node-selected', { detail: { id: programId } }));
110+
updateGraphEdgeSelection(); // update edge highlight on selection
107111
}
108112

109113
let svg = null;
@@ -252,6 +256,7 @@ function renderGraph(data, options = {}) {
252256
node
253257
.attr("cx", d => d.x)
254258
.attr("cy", d => d.y);
259+
updateGraphEdgeSelection(); // update edge highlight on tick
255260
});
256261

257262
// Intelligent zoom/pan
@@ -309,6 +314,7 @@ function renderGraph(data, options = {}) {
309314
}
310315

311316
selectProgram(selectedProgramId);
317+
updateGraphEdgeSelection(); // update edge highlight after render
312318
applyDragHandlersToAllNodes();
313319

314320
svg.on("click", function(event) {
@@ -385,6 +391,14 @@ export function centerAndHighlightNodeInGraph(nodeId) {
385391
}
386392
}
387393

394+
export function updateGraphEdgeSelection() {
395+
if (!g) return;
396+
g.selectAll('line')
397+
.attr('stroke', d => (selectedProgramId && (d.source.id === selectedProgramId || d.target.id === selectedProgramId)) ? 'red' : '#999')
398+
.attr('stroke-width', d => (selectedProgramId && (d.source.id === selectedProgramId || d.target.id === selectedProgramId)) ? 4 : 2)
399+
.attr('stroke-opacity', d => (selectedProgramId && (d.source.id === selectedProgramId || d.target.id === selectedProgramId)) ? 0.95 : 0.6);
400+
}
401+
388402
function dragstarted(event, d) {
389403
if (!event.active && simulation) simulation.alphaTarget(0.3).restart(); // Keep simulation alive
390404
d.fx = d.x;
@@ -400,4 +414,9 @@ function dragended(event, d) {
400414
d.fy = null;
401415
}
402416

417+
window.addEventListener('node-selected', function(e) {
418+
// When node selection changes (e.g., from list view), update graph node selection
419+
updateGraphNodeSelection();
420+
});
421+
403422
export { renderGraph, g };

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 };

0 commit comments

Comments
 (0)