Below is a small market with 3 goods and 4 buyers. Each buyer has a value for each good, and there is one unit of each good available. The current prices start low — too low, so multiple buyers want the same good at once (excess demand).
<!-- Market clearing demo — buyers demand the good with highest surplus; prices adjust until cleared -->
<div class="hint-box">Each buyer demands the good giving the highest surplus (value − price). <b>Green</b> = matched; <b>red</b> = contested. Prices adjust until every good has at most one buyer.</div>
<div class="market-wrap">
<table id="val-table" class="val-table">
<thead>
<tr>
<th>Buyer</th>
<th>Good A</th>
<th>Good B</th>
<th>Good C</th>
<th>Demands</th>
</tr>
</thead>
<tbody id="val-body"></tbody>
<tfoot>
<tr class="price-row" id="price-row">
<td>Price</td>
<td id="p0">0</td>
<td id="p1">0</td>
<td id="p2">0</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
<div id="status" class="status">Prices start at 0 — press Step or Auto to adjust.</div>
<div class="btns">
<button id="btn-step" type="button">Step</button>
<button id="btn-auto" type="button">Auto</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Styles for the matching-markets-clearing demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.hint-box { font-size: .85rem; color: #444; margin-bottom: .7rem; line-height: 1.5; }
.market-wrap { overflow-x: auto; margin-bottom: .6rem; }
.val-table { border-collapse: collapse; width: 100%; min-width: 280px; font-size: .9rem; }
.val-table th, .val-table td { border: 1px solid #cdd9e3; padding: .35rem .55rem; text-align: center; }
.val-table th { background: #e8eef3; color: #1d3557; font-weight: 700; }
.val-table td:first-child { font-weight: 600; text-align: left; }
/* Green = demanded by exactly one buyer; Red = contested (multiple buyers want it) */
td.demanded { background: #d4edda; font-weight: 700; }
td.contested { background: #f8d7da; }
td.surplus-zero { color: #888; }
.price-row td { background: #1d3557; color: #fff; font-weight: 700; }
.price-row td:first-child { text-align: left; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.5em; margin-bottom: .5rem; }
.status.cleared { color: #0a7d33; }
.status.adjusting { color: #b25000; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Tatonnement: raise prices of over-demanded goods, lower prices of under-demanded goods
// Three goods available in the market (one unit each)
const GOODS = ['A', 'B', 'C'];
// Buyer valuations matrix [buyer][good] — each cell is willingness-to-pay
// Row = buyer, Col = good. Chosen so clearing prices are A=3, B=5, C=4.
const VALUES = [
[8, 5, 3],
[4, 9, 6],
[7, 3, 8],
[5, 7, 4]
];
const BUYER_NAMES = ['Buyer 1', 'Buyer 2', 'Buyer 3', 'Buyer 4'];
let prices = [0, 0, 0];
let autoTimer = null;
let stepCount = 0;
// Each buyer demands the good with highest non-negative surplus (value minus price)
function computeDemands() {
const demands = [];
for (let b = 0; b < VALUES.length; b++) {
let bestSurplus = -Infinity;
let bestGoods = [];
for (let g = 0; g < GOODS.length; g++) {
const surplus = VALUES[b][g] - prices[g];
if (surplus > bestSurplus && surplus >= 0) { bestSurplus = surplus; bestGoods = [g]; }
else if (surplus === bestSurplus && surplus >= 0) { bestGoods.push(g); }
}
// Tie-break by lowest index (first good wins ties)
demands.push(bestGoods.length > 0 ? bestGoods[0] : -1);
}
return demands;
}
// Market clears when every good has at most one buyer demanding it
function isCleared(demands) {
const count = [0, 0, 0];
for (const d of demands) { if (d >= 0) count[d]++; }
return count.every(c => c <= 1);
}
function renderTable(demands) {
const tbody = document.getElementById('val-body');
tbody.innerHTML = '';
// Count how many buyers demand each good to detect oversubscription
const demandCount = [0, 0, 0];
for (const d of demands) { if (d >= 0) demandCount[d]++; }
for (let b = 0; b < VALUES.length; b++) {
const tr = document.createElement('tr');
const nameTd = document.createElement('td');
nameTd.textContent = BUYER_NAMES[b];
tr.appendChild(nameTd);
for (let g = 0; g < GOODS.length; g++) {
const td = document.createElement('td');
const surplus = VALUES[b][g] - prices[g];
td.textContent = surplus >= 0 ? `${VALUES[b][g]} (${surplus >= 0 ? '+' : ''}${surplus})` : `${VALUES[b][g]} (${surplus})`;
// Color-code cells: green = sole demander, red = contested
if (demands[b] === g) {
td.className = demandCount[g] > 1 ? 'contested' : 'demanded';
} else if (surplus === 0) {
td.className = 'surplus-zero';
}
tr.appendChild(td);
}
const demTd = document.createElement('td');
demTd.textContent = demands[b] >= 0 ? GOODS[demands[b]] : '—';
tr.appendChild(demTd);
tbody.appendChild(tr);
}
// Refresh displayed prices in the footer row
for (let g = 0; g < GOODS.length; g++) {
document.getElementById('p' + g).textContent = prices[g];
}
}
function setStatus(text, cls) {
const el = document.getElementById('status');
el.textContent = text;
el.className = 'status ' + (cls || '');
}
// One tatonnement step — adjust prices then re-check
function step() {
const demands = computeDemands();
if (isCleared(demands)) {
renderTable(demands);
setStatus('Market is already cleared.', 'cleared');
stopAuto();
return;
}
// Raise price of over-demanded goods (+1), lower price of under-demanded goods (-1, floor 0)
const demandCount = [0, 0, 0];
for (const d of demands) { if (d >= 0) demandCount[d]++; }
for (let g = 0; g < GOODS.length; g++) {
if (demandCount[g] > 1) prices[g]++;
else if (demandCount[g] === 0 && prices[g] > 0) prices[g]--;
}
stepCount++;
const newDemands = computeDemands();
renderTable(newDemands);
if (isCleared(newDemands)) {
setStatus(i18nFmt('Cleared in %steps% step(s)! Every good has at most one buyer.', { steps: stepCount }), 'cleared');
stopAuto();
document.getElementById('btn-step').disabled = true;
} else {
setStatus(i18nFmt('Step %step%: prices adjusted, market not yet clear.', { step: stepCount }), 'adjusting');
}
}
function stopAuto() {
if (autoTimer) { clearInterval(autoTimer); autoTimer = null; }
document.getElementById('btn-auto').textContent = 'Auto';
document.getElementById('btn-auto').disabled = false;
}
function toggleAuto() {
if (autoTimer) {
stopAuto();
} else {
document.getElementById('btn-auto').textContent = 'Stop';
autoTimer = setInterval(() => { step(); }, 600);
}
}
function reset() {
stopAuto();
prices = [0, 0, 0];
stepCount = 0;
const demands = computeDemands();
renderTable(demands);
setStatus('Prices start at 0 — press Step or Auto to adjust.', 'adjusting');
document.getElementById('btn-step').disabled = false;
}
document.getElementById('btn-step').addEventListener('click', step);
document.getElementById('btn-auto').addEventListener('click', toggleAuto);
document.getElementById('btn-reset').addEventListener('click', reset);
// Initial render — show starting state with all prices at 0
reset();
<!-- Market clearing demo — buyers demand the good with highest surplus; prices adjust until cleared -->
<div class="hint-box">Each buyer demands the good giving the highest surplus (value − price). <b>Green</b> = matched; <b>red</b> = contested. Prices adjust until every good has at most one buyer.</div>
<div class="market-wrap">
<table id="val-table" class="val-table">
<thead>
<tr>
<th>Buyer</th>
<th>Good A</th>
<th>Good B</th>
<th>Good C</th>
<th>Demands</th>
</tr>
</thead>
<tbody id="val-body"></tbody>
<tfoot>
<tr class="price-row" id="price-row">
<td>Price</td>
<td id="p0">0</td>
<td id="p1">0</td>
<td id="p2">0</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
<div id="status" class="status">Prices start at 0 — press Step or Auto to adjust.</div>
<div class="btns">
<button id="btn-step" type="button">Step</button>
<button id="btn-auto" type="button">Auto</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Styles for the matching-markets-clearing demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.hint-box { font-size: .85rem; color: #444; margin-bottom: .7rem; line-height: 1.5; }
.market-wrap { overflow-x: auto; margin-bottom: .6rem; }
.val-table { border-collapse: collapse; width: 100%; min-width: 280px; font-size: .9rem; }
.val-table th, .val-table td { border: 1px solid #cdd9e3; padding: .35rem .55rem; text-align: center; }
.val-table th { background: #e8eef3; color: #1d3557; font-weight: 700; }
.val-table td:first-child { font-weight: 600; text-align: left; }
/* Green = demanded by exactly one buyer; Red = contested (multiple buyers want it) */
td.demanded { background: #d4edda; font-weight: 700; }
td.contested { background: #f8d7da; }
td.surplus-zero { color: #888; }
.price-row td { background: #1d3557; color: #fff; font-weight: 700; }
.price-row td:first-child { text-align: left; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.5em; margin-bottom: .5rem; }
.status.cleared { color: #0a7d33; }
.status.adjusting { color: #b25000; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Tatonnement: raise prices of over-demanded goods, lower prices of under-demanded goods
// Three goods available in the market (one unit each)
const GOODS = ['A', 'B', 'C'];
// Buyer valuations matrix [buyer][good] — each cell is willingness-to-pay
// Row = buyer, Col = good. Chosen so clearing prices are A=3, B=5, C=4.
const VALUES = [
[8, 5, 3],
[4, 9, 6],
[7, 3, 8],
[5, 7, 4]
];
const BUYER_NAMES = ['Buyer 1', 'Buyer 2', 'Buyer 3', 'Buyer 4'];
let prices = [0, 0, 0];
let autoTimer = null;
let stepCount = 0;
// Each buyer demands the good with highest non-negative surplus (value minus price)
function computeDemands() {
const demands = [];
for (let b = 0; b < VALUES.length; b++) {
let bestSurplus = -Infinity;
let bestGoods = [];
for (let g = 0; g < GOODS.length; g++) {
const surplus = VALUES[b][g] - prices[g];
if (surplus > bestSurplus && surplus >= 0) { bestSurplus = surplus; bestGoods = [g]; }
else if (surplus === bestSurplus && surplus >= 0) { bestGoods.push(g); }
}
// Tie-break by lowest index (first good wins ties)
demands.push(bestGoods.length > 0 ? bestGoods[0] : -1);
}
return demands;
}
// Market clears when every good has at most one buyer demanding it
function isCleared(demands) {
const count = [0, 0, 0];
for (const d of demands) { if (d >= 0) count[d]++; }
return count.every(c => c <= 1);
}
function renderTable(demands) {
const tbody = document.getElementById('val-body');
tbody.innerHTML = '';
// Count how many buyers demand each good to detect oversubscription
const demandCount = [0, 0, 0];
for (const d of demands) { if (d >= 0) demandCount[d]++; }
for (let b = 0; b < VALUES.length; b++) {
const tr = document.createElement('tr');
const nameTd = document.createElement('td');
nameTd.textContent = BUYER_NAMES[b];
tr.appendChild(nameTd);
for (let g = 0; g < GOODS.length; g++) {
const td = document.createElement('td');
const surplus = VALUES[b][g] - prices[g];
td.textContent = surplus >= 0 ? `${VALUES[b][g]} (${surplus >= 0 ? '+' : ''}${surplus})` : `${VALUES[b][g]} (${surplus})`;
// Color-code cells: green = sole demander, red = contested
if (demands[b] === g) {
td.className = demandCount[g] > 1 ? 'contested' : 'demanded';
} else if (surplus === 0) {
td.className = 'surplus-zero';
}
tr.appendChild(td);
}
const demTd = document.createElement('td');
demTd.textContent = demands[b] >= 0 ? GOODS[demands[b]] : '—';
tr.appendChild(demTd);
tbody.appendChild(tr);
}
// Refresh displayed prices in the footer row
for (let g = 0; g < GOODS.length; g++) {
document.getElementById('p' + g).textContent = prices[g];
}
}
function setStatus(text, cls) {
const el = document.getElementById('status');
el.textContent = text;
el.className = 'status ' + (cls || '');
}
// One tatonnement step — adjust prices then re-check
function step() {
const demands = computeDemands();
if (isCleared(demands)) {
renderTable(demands);
setStatus('Market is already cleared.', 'cleared');
stopAuto();
return;
}
// Raise price of over-demanded goods (+1), lower price of under-demanded goods (-1, floor 0)
const demandCount = [0, 0, 0];
for (const d of demands) { if (d >= 0) demandCount[d]++; }
for (let g = 0; g < GOODS.length; g++) {
if (demandCount[g] > 1) prices[g]++;
else if (demandCount[g] === 0 && prices[g] > 0) prices[g]--;
}
stepCount++;
const newDemands = computeDemands();
renderTable(newDemands);
if (isCleared(newDemands)) {
setStatus(i18nFmt('Cleared in %steps% step(s)! Every good has at most one buyer.', { steps: stepCount }), 'cleared');
stopAuto();
document.getElementById('btn-step').disabled = true;
} else {
setStatus(i18nFmt('Step %step%: prices adjusted, market not yet clear.', { step: stepCount }), 'adjusting');
}
}
function stopAuto() {
if (autoTimer) { clearInterval(autoTimer); autoTimer = null; }
document.getElementById('btn-auto').textContent = 'Auto';
document.getElementById('btn-auto').disabled = false;
}
function toggleAuto() {
if (autoTimer) {
stopAuto();
} else {
document.getElementById('btn-auto').textContent = 'Stop';
autoTimer = setInterval(() => { step(); }, 600);
}
}
function reset() {
stopAuto();
prices = [0, 0, 0];
stepCount = 0;
const demands = computeDemands();
renderTable(demands);
setStatus('Prices start at 0 — press Step or Auto to adjust.', 'adjusting');
document.getElementById('btn-step').disabled = false;
}
document.getElementById('btn-step').addEventListener('click', step);
document.getElementById('btn-auto').addEventListener('click', toggleAuto);
document.getElementById('btn-reset').addEventListener('click', reset);
// Initial render — show starting state with all prices at 0
reset();
Press Step to run one round of the tatonnement process: prices of over-demanded goods rise by 1, prices of under-demanded goods fall by 1 (but never below 0). Press Auto to watch it converge on its own. When every good has at most one interested buyer — or none — the market clears.
Notice that each buyer's demand is simply the good that gives the highest surplus v−p (value minus price). When two goods tie, the buyer is indifferent; the demo breaks ties by index. Clearing is always reached in a finite number of steps for this type of market.
Comments
Loading comments...