<!-- Pareto front demo — two-objective scatter plot with evolutionary selection -->
<p class="hint">Each dot is a solution. The goal: minimize <b>cost</b> (left is better) and maximize <b>quality</b> (up is better). Red dots on the bold curve are Pareto-optimal — no other solution beats them on both objectives at once.</p>
<canvas id="chart" width="380" height="300" aria-label="Scatter plot showing cost vs quality for the current population"></canvas>
<div class="status" id="status">Click Evolve to run a generation of selection.</div>
<div class="btns">
<button id="btn-evolve" type="button">Evolve</button>
<button id="btn-add" type="button">Add solutions</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="legend">
<span class="dot dominated"></span> Dominated
<span class="dot pareto"></span> Pareto front
</div>
/* Layout and color for the Pareto front demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
background: #f7f9fb; max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.info { color: #1d3557; }
.status.evolving { color: #0a7d33; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .4rem; }
button { font: 600 14px system-ui; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.legend { font-size: .82rem; color: #555; display: flex; align-items: center; gap: 4px; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
.dot.dominated { background: #adb1b8; }
.dot.pareto { background: #e63946; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Pareto front demo — two conflicting objectives (cost, quality).
// A solution is Pareto-optimal if no other solution is strictly better on at least one objective and at least as good on all others.
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Canvas padding for axes and labels
const PAD = { top: 20, right: 20, bottom: 40, left: 46 };
const IW = W - PAD.left - PAD.right;
const IH = H - PAD.top - PAD.bottom;
// Map objective values to canvas pixel coordinates.
// cost in [0,1] -> smaller is better (x axis)
// quality in [0,1] -> larger is better (y axis)
function toPixel(cost, quality) {
return [
PAD.left + cost * IW,
PAD.top + (1 - quality) * IH
];
}
// Population state
let pop = [];
let generation = 0;
function randSol() {
// Random solution with a soft cost-quality trade-off
const cost = Math.random();
const quality = Math.random() * (1 - cost * 0.6); // softer trade-off: high cost tends toward lower quality
return { cost, quality };
}
function initPop() {
// Initialize a random population of 30 solutions
pop = [];
for (let i = 0; i < 30; i++) pop.push(randSol());
generation = 0;
}
// Pareto dominance check.
// A dominates B if A is at least as good on all objectives and strictly better on one.
// cost: lower is better; quality: higher is better.
function dominates(a, b) {
return a.cost <= b.cost && a.quality >= b.quality &&
(a.cost < b.cost || a.quality > b.quality);
}
// Return the non-dominated subset (Pareto front) of the population.
function getParetoFront(solutions) {
return solutions.filter(s => !solutions.some(other => dominates(other, s)));
}
// Render axes, all population dots, and the Pareto-front curve.
function draw() {
ctx.clearRect(0, 0, W, H);
// Draw axes
ctx.strokeStyle = '#999';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.left, PAD.top);
ctx.lineTo(PAD.left, PAD.top + IH);
ctx.lineTo(PAD.left + IW, PAD.top + IH);
ctx.stroke();
// Axis labels
ctx.fillStyle = '#555';
ctx.font = '11px system-ui';
ctx.textAlign = 'center';
ctx.fillText('Cost → (lower is better)', PAD.left + IW / 2, H - 6);
ctx.save();
ctx.translate(13, PAD.top + IH / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText('Quality ↑ (higher is better)', 0, 0);
ctx.restore();
// Tick marks and values
ctx.fillStyle = '#aaa';
ctx.font = '9px system-ui';
for (let t = 0; t <= 1; t += 0.25) {
const [px] = toPixel(t, 0);
ctx.fillText(t.toFixed(2), px, PAD.top + IH + 12);
const [, py] = toPixel(0, t);
ctx.textAlign = 'right';
ctx.fillText(t.toFixed(2), PAD.left - 4, py + 3);
ctx.textAlign = 'center';
}
const front = getParetoFront(pop);
const frontSet = new Set(front.map((_, i) => front[i]));
// Draw population dots (red = Pareto front, gray = dominated)
for (const s of pop) {
const [x, y] = toPixel(s.cost, s.quality);
ctx.beginPath();
ctx.arc(x, y, 5, 0, Math.PI * 2);
ctx.fillStyle = frontSet.has(s) ? '#e63946' : '#adb1b8';
ctx.fill();
}
// Connect Pareto-front dots with a step line
const sorted = [...front].sort((a, b) => a.cost - b.cost);
if (sorted.length > 1) {
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i < sorted.length; i++) {
const [x, y] = toPixel(sorted[i].cost, sorted[i].quality);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
}
}
// One NSGA-II-inspired generation step.
// One generation: remove dominated solutions, add offspring around front members.
function evolve() {
const front = getParetoFront(pop);
// Mutate front members to produce offspring
const offspring = front.map(p => {
const noise = 0.06;
const cost = Math.max(0, Math.min(1, p.cost + (Math.random() - 0.5) * noise));
const quality = Math.max(0, Math.min(1, p.quality + (Math.random() - 0.5) * noise));
return { cost, quality };
});
// Keep only non-dominated solutions from the combined pool
const combined = [...pop, ...offspring];
const newFront = getParetoFront(combined);
// Fill back to population size with random solutions
while (newFront.length < 30) {
newFront.push(randSol());
}
pop = newFront.slice(0, 30);
generation++;
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status ' + cls;
}
// Evolve button handler
document.getElementById('btn-evolve').addEventListener('click', () => {
evolve();
draw();
setStatus(i18nFmt('Generation %gen% — Pareto front: %front% solutions.', { gen: generation, front: getParetoFront(pop).length }), 'evolving');
});
// Add-solutions button handler
document.getElementById('btn-add').addEventListener('click', () => {
for (let i = 0; i < 8; i++) pop.push(randSol());
draw();
setStatus(i18nFmt('Population now has %n% solutions.', { n: pop.length }), 'info');
});
// Reset button handler
document.getElementById('btn-reset').addEventListener('click', () => {
initPop();
draw();
setStatus('Click Evolve to run a generation of selection.', 'info');
});
initPop();
draw();
setStatus('Click Evolve to run a generation of selection.', 'info');
<!-- Pareto front demo — two-objective scatter plot with evolutionary selection -->
<p class="hint">Each dot is a solution. The goal: minimize <b>cost</b> (left is better) and maximize <b>quality</b> (up is better). Red dots on the bold curve are Pareto-optimal — no other solution beats them on both objectives at once.</p>
<canvas id="chart" width="380" height="300" aria-label="Scatter plot showing cost vs quality for the current population"></canvas>
<div class="status" id="status">Click Evolve to run a generation of selection.</div>
<div class="btns">
<button id="btn-evolve" type="button">Evolve</button>
<button id="btn-add" type="button">Add solutions</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="legend">
<span class="dot dominated"></span> Dominated
<span class="dot pareto"></span> Pareto front
</div>
/* Layout and color for the Pareto front demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
background: #f7f9fb; max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.info { color: #1d3557; }
.status.evolving { color: #0a7d33; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .4rem; }
button { font: 600 14px system-ui; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.legend { font-size: .82rem; color: #555; display: flex; align-items: center; gap: 4px; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; }
.dot.dominated { background: #adb1b8; }
.dot.pareto { background: #e63946; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Pareto front demo — two conflicting objectives (cost, quality).
// A solution is Pareto-optimal if no other solution is strictly better on at least one objective and at least as good on all others.
const canvas = document.getElementById('chart');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Canvas padding for axes and labels
const PAD = { top: 20, right: 20, bottom: 40, left: 46 };
const IW = W - PAD.left - PAD.right;
const IH = H - PAD.top - PAD.bottom;
// Map objective values to canvas pixel coordinates.
// cost in [0,1] -> smaller is better (x axis)
// quality in [0,1] -> larger is better (y axis)
function toPixel(cost, quality) {
return [
PAD.left + cost * IW,
PAD.top + (1 - quality) * IH
];
}
// Population state
let pop = [];
let generation = 0;
function randSol() {
// Random solution with a soft cost-quality trade-off
const cost = Math.random();
const quality = Math.random() * (1 - cost * 0.6); // softer trade-off: high cost tends toward lower quality
return { cost, quality };
}
function initPop() {
// Initialize a random population of 30 solutions
pop = [];
for (let i = 0; i < 30; i++) pop.push(randSol());
generation = 0;
}
// Pareto dominance check.
// A dominates B if A is at least as good on all objectives and strictly better on one.
// cost: lower is better; quality: higher is better.
function dominates(a, b) {
return a.cost <= b.cost && a.quality >= b.quality &&
(a.cost < b.cost || a.quality > b.quality);
}
// Return the non-dominated subset (Pareto front) of the population.
function getParetoFront(solutions) {
return solutions.filter(s => !solutions.some(other => dominates(other, s)));
}
// Render axes, all population dots, and the Pareto-front curve.
function draw() {
ctx.clearRect(0, 0, W, H);
// Draw axes
ctx.strokeStyle = '#999';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(PAD.left, PAD.top);
ctx.lineTo(PAD.left, PAD.top + IH);
ctx.lineTo(PAD.left + IW, PAD.top + IH);
ctx.stroke();
// Axis labels
ctx.fillStyle = '#555';
ctx.font = '11px system-ui';
ctx.textAlign = 'center';
ctx.fillText('Cost → (lower is better)', PAD.left + IW / 2, H - 6);
ctx.save();
ctx.translate(13, PAD.top + IH / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText('Quality ↑ (higher is better)', 0, 0);
ctx.restore();
// Tick marks and values
ctx.fillStyle = '#aaa';
ctx.font = '9px system-ui';
for (let t = 0; t <= 1; t += 0.25) {
const [px] = toPixel(t, 0);
ctx.fillText(t.toFixed(2), px, PAD.top + IH + 12);
const [, py] = toPixel(0, t);
ctx.textAlign = 'right';
ctx.fillText(t.toFixed(2), PAD.left - 4, py + 3);
ctx.textAlign = 'center';
}
const front = getParetoFront(pop);
const frontSet = new Set(front.map((_, i) => front[i]));
// Draw population dots (red = Pareto front, gray = dominated)
for (const s of pop) {
const [x, y] = toPixel(s.cost, s.quality);
ctx.beginPath();
ctx.arc(x, y, 5, 0, Math.PI * 2);
ctx.fillStyle = frontSet.has(s) ? '#e63946' : '#adb1b8';
ctx.fill();
}
// Connect Pareto-front dots with a step line
const sorted = [...front].sort((a, b) => a.cost - b.cost);
if (sorted.length > 1) {
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 2;
ctx.beginPath();
for (let i = 0; i < sorted.length; i++) {
const [x, y] = toPixel(sorted[i].cost, sorted[i].quality);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
}
ctx.stroke();
}
}
// One NSGA-II-inspired generation step.
// One generation: remove dominated solutions, add offspring around front members.
function evolve() {
const front = getParetoFront(pop);
// Mutate front members to produce offspring
const offspring = front.map(p => {
const noise = 0.06;
const cost = Math.max(0, Math.min(1, p.cost + (Math.random() - 0.5) * noise));
const quality = Math.max(0, Math.min(1, p.quality + (Math.random() - 0.5) * noise));
return { cost, quality };
});
// Keep only non-dominated solutions from the combined pool
const combined = [...pop, ...offspring];
const newFront = getParetoFront(combined);
// Fill back to population size with random solutions
while (newFront.length < 30) {
newFront.push(randSol());
}
pop = newFront.slice(0, 30);
generation++;
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status ' + cls;
}
// Evolve button handler
document.getElementById('btn-evolve').addEventListener('click', () => {
evolve();
draw();
setStatus(i18nFmt('Generation %gen% — Pareto front: %front% solutions.', { gen: generation, front: getParetoFront(pop).length }), 'evolving');
});
// Add-solutions button handler
document.getElementById('btn-add').addEventListener('click', () => {
for (let i = 0; i < 8; i++) pop.push(randSol());
draw();
setStatus(i18nFmt('Population now has %n% solutions.', { n: pop.length }), 'info');
});
// Reset button handler
document.getElementById('btn-reset').addEventListener('click', () => {
initPop();
draw();
setStatus('Click Evolve to run a generation of selection.', 'info');
});
initPop();
draw();
setStatus('Click Evolve to run a generation of selection.', 'info');
Comments
Loading comments...