Physics / Chaos Theory
BIFURCATION MAP
Explore the logistic map route to chaos, period-doubling cascade, and the Feigenbaum constant.
Bifurcation Control
Current behavior: Stable
Zoom: 1.0×
Key Constants
δ ≈ 4.67
Feigenbaum
0.500
Current x
1
Period
Logistic Map: xₙ₊₁ = r·xₙ·(1−xₙ)
How to use:
• Drag slider to change r
• Click-drag on diagram to zoom
• Double-click to zoom out
• Hover to see r, x values
• Watch cobweb respond in real-time
Bifurcation Diagram
r: 2.5 → 4.0
stable → chaos
Cobweb Plot
Parabola vs y=x iteration
Time Series (x_n vs n)
First 100 iterations (after 200 transients discarded)
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
// ===== BIFURCATION MAP SIMULATOR =====
const bifCanvas = document.getElementById('bifurcation-canvas');
const bifCtx = bifCanvas.getContext('2d');
const cobwebCanvas = document.getElementById('cobweb-canvas');
const cobwebCtx = cobwebCanvas.getContext('2d');
const timeseriesCanvas = document.getElementById('timeseries-canvas');
const timeseriesCtx = timeseriesCanvas.getContext('2d');
const rSlider = document.getElementById('r-slider');
const rNumeric = document.getElementById('r-numeric');
const rValue = document.getElementById('r-value');
const iterationsSelect = document.getElementById('iterations-select');
const colorModeSelect = document.getElementById('color-mode');
const cobwebSpeedSelect = document.getElementById('cobweb-speed');
const tooltip = document.getElementById('tooltip');
const infoBox = document.getElementById('info-box');
const behaviorText = document.getElementById('behavior-text');
const currentXStat = document.getElementById('current-x');
const attractorPeriodStat = document.getElementById('attractor-period');
const zoomIndicator = document.getElementById('zoom-indicator');
// State
let state = {
r: 3.0,
zoomBox: null,
zoomLevel: 1.0,
zoomOriginX: 2.5,
zoomOriginY: 0,
bifurcationData: null,
mousePos: { x: 0, y: 0 },
selecting: false,
selectionStart: null
};
const FEIGENBAUM = 4.669;
const TRANSIENTS = 200;
const MAX_ITERATIONS = 1000;
const COBWEB_SPEED = { fast: 50, normal: 100, slow: 200 };
// ===== LOGISTIC MAP =====
function logistic(x, r) {
return r * x * (1 - x);
}
function computeBifurcation(rMin, rMax, pointsPerR, maxIter) {
const data = [];
const step = (rMax - rMin) / 800;
for (let r = rMin; r <= rMax; r += step) {
let x = 0.5;
// Discard transients
for (let i = 0; i < TRANSIENTS; i++) {
x = logistic(x, r);
}
// Collect attractor points
const points = [];
for (let i = 0; i < maxIter; i++) {
x = logistic(x, r);
points.push(x);
}
data.push({ r, points });
}
return data;
}
// ===== ANALYSIS FUNCTIONS =====
function detectBehavior(r) {
if (r < 1) return "Dies out (r < 1)";
if (r < 3) return "Stable fixed point";
if (r < 3.449) return "Period-2 (bifurcation)";
if (r < 3.544) return "Period-4 (period doubling)";
if (r < 3.5644) return "Period-8 cascade";
if (r < 3.5699) return "Higher periods";
if (r < 3.5700) return "Onset of chaos (r* ≈ 3.5700)";
if (r < 3.83) return "Chaos with periodic windows";
if (r <= 4) return "Fully chaotic (r = 4)";
return "Beyond logistic map";
}
function estimateAttractorPeriod(r, maxIter = 500) {
let x = 0.5;
for (let i = 0; i < TRANSIENTS; i++) {
x = logistic(x, r);
}
const sequence = [];
for (let i = 0; i < maxIter; i++) {
x = logistic(x, r);
sequence.push(Math.round(x * 10000) / 10000);
}
// Try to detect period
for (let p = 1; p <= 64; p++) {
let isPeriodic = true;
for (let i = p; i < Math.min(200, sequence.length); i++) {
if (Math.abs(sequence[i] - sequence[i - p]) > 0.0001) {
isPeriodic = false;
break;
}
}
if (isPeriodic && p <= 64) return p;
}
return 0; // chaotic
}
// ===== DRAWING FUNCTIONS =====
function drawBifurcationDiagram(data, colorMode) {
bifCtx.fillStyle = '#000';
bifCtx.fillRect(0, 0, bifCanvas.width, bifCanvas.height);
const padding = 40;
const plotWidth = bifCanvas.width - 2 * padding;
const plotHeight = bifCanvas.height - 2 * padding;
// Draw axes
bifCtx.strokeStyle = '#333';
bifCtx.lineWidth = 1;
bifCtx.beginPath();
bifCtx.moveTo(padding, padding);
bifCtx.lineTo(padding, bifCanvas.height - padding);
bifCtx.lineTo(bifCanvas.width - padding, bifCanvas.height - padding);
bifCtx.stroke();
// Draw grid
bifCtx.strokeStyle = '#222';
bifCtx.lineWidth = 0.5;
for (let i = 0; i <= 5; i++) {
const y = padding + (plotHeight / 5) * i;
bifCtx.beginPath();
bifCtx.moveTo(padding, y);
bifCtx.lineTo(bifCanvas.width - padding, y);
bifCtx.stroke();
}
// Draw r slider line (red vertical line at current r)
const rPos = ((state.r - state.zoomOriginX) * state.zoomLevel / (4.0 - 2.5)) * plotWidth + padding;
if (rPos >= padding && rPos <= bifCanvas.width - padding) {
bifCtx.strokeStyle = '#ff2200';
bifCtx.lineWidth = 2;
bifCtx.beginPath();
bifCtx.moveTo(rPos, padding);
bifCtx.lineTo(rPos, bifCanvas.height - padding);
bifCtx.stroke();
}
// Draw points
const rMin = state.zoomOriginX;
const rMax = rMin + (4.0 - 2.5) / state.zoomLevel;
for (const datum of data) {
if (datum.r < rMin || datum.r > rMax) continue;
const rNorm = (datum.r - rMin) / (rMax - rMin);
const x = padding + rNorm * plotWidth;
for (const y of datum.points) {
if (y < 0 || y > 1) continue;
const yPixel = bifCanvas.height - padding - y * plotHeight;
let color;
if (colorMode === 'r-value') {
const h = (datum.r - 2.5) / 1.5 * 240;
color = `hsl(${h}, 100%, 50%)`;
} else {
color = '#ff2200';
}
bifCtx.fillStyle = color;
bifCtx.fillRect(x, yPixel, 1.2, 1.2);
}
}
// Draw axis labels
bifCtx.fillStyle = '#555';
bifCtx.font = '12px "DM Mono"';
bifCtx.textAlign = 'center';
bifCtx.fillText(`r: ${rMin.toFixed(2)}`, padding, bifCanvas.height - 10);
bifCtx.fillText(`${rMax.toFixed(2)}`, bifCanvas.width - padding, bifCanvas.height - 10);
bifCtx.textAlign = 'right';
bifCtx.fillText('x: 1.0', padding - 5, padding + 10);
bifCtx.fillText('0.0', padding - 5, bifCanvas.height - padding + 10);
}
function drawCobwebDiagram(r) {
cobwebCtx.fillStyle = '#000';
cobwebCtx.fillRect(0, 0, cobwebCanvas.width, cobwebCanvas.height);
const padding = 30;
const size = cobwebCanvas.width - 2 * padding;
// Draw axes
cobwebCtx.strokeStyle = '#333';
cobwebCtx.lineWidth = 1;
cobwebCtx.beginPath();
cobwebCtx.moveTo(padding, cobwebCanvas.height - padding);
cobwebCtx.lineTo(padding, padding);
cobwebCtx.lineTo(cobwebCanvas.width - padding, cobwebCanvas.height - padding);
cobwebCtx.stroke();
// Draw y=x line
cobwebCtx.strokeStyle = '#00c896';
cobwebCtx.lineWidth = 1.5;
cobwebCtx.beginPath();
cobwebCtx.moveTo(padding, cobwebCanvas.height - padding);
cobwebCtx.lineTo(cobwebCanvas.width - padding, padding);
cobwebCtx.stroke();
// Draw logistic parabola
cobwebCtx.strokeStyle = '#ff2200';
cobwebCtx.lineWidth = 2;
cobwebCtx.beginPath();
for (let xNorm = 0; xNorm <= 1; xNorm += 0.01) {
const y = r * xNorm * (1 - xNorm);
const xPixel = padding + xNorm * size;
const yPixel = cobwebCanvas.height - padding - y * size;
if (xNorm === 0) {
cobwebCtx.moveTo(xPixel, yPixel);
} else {
cobwebCtx.lineTo(xPixel, yPixel);
}
}
cobwebCtx.stroke();
// Draw cobweb trace
cobwebCtx.strokeStyle = '#ff8800';
cobwebCtx.lineWidth = 1;
let x = 0.5;
for (let i = 0; i < 30; i++) {
const xPixel = padding + x * size;
const yPixel = cobwebCanvas.height - padding - logistic(x, r) * size;
const xPixel2 = padding + logistic(x, r) * size;
const yPixel2 = yPixel;
cobwebCtx.beginPath();
cobwebCtx.moveTo(xPixel, cobwebCanvas.height - padding - x * size);
cobwebCtx.lineTo(xPixel2, yPixel2);
cobwebCtx.stroke();
x = logistic(x, r);
const xPixel3 = padding + x * size;
const yPixel3 = cobwebCanvas.height - padding - x * size;
cobwebCtx.beginPath();
cobwebCtx.moveTo(xPixel2, yPixel2);
cobwebCtx.lineTo(xPixel3, yPixel3);
cobwebCtx.stroke();
}
// Fixed point
cobwebCtx.fillStyle = '#00c896';
cobwebCtx.beginPath();
const xFixed = (r - 1) / r;
const xFixedPixel = padding + xFixed * size;
cobwebCtx.arc(xFixedPixel, cobwebCanvas.height - padding - xFixed * size, 3, 0, 2 * Math.PI);
cobwebCtx.fill();
// Axis labels
cobwebCtx.fillStyle = '#555';
cobwebCtx.font = '10px "DM Mono"';
cobwebCtx.textAlign = 'center';
cobwebCtx.fillText('0', padding, cobwebCanvas.height - padding + 15);
cobwebCtx.fillText('1', cobwebCanvas.width - padding, cobwebCanvas.height - padding + 15);
cobwebCtx.textAlign = 'right';
cobwebCtx.fillText('1', padding - 10, padding - 5);
cobwebCtx.fillText('0', padding - 10, cobwebCanvas.height - padding + 5);
}
function drawTimeSeriesPlot(r) {
timeseriesCtx.fillStyle = '#000';
timeseriesCtx.fillRect(0, 0, timeseriesCanvas.width, timeseriesCanvas.height);
const padding = 30;
const plotWidth = timeseriesCanvas.width - 2 * padding;
const plotHeight = timeseriesCanvas.height - 2 * padding;
// Draw axes
timeseriesCtx.strokeStyle = '#333';
timeseriesCtx.lineWidth = 1;
timeseriesCtx.beginPath();
timeseriesCtx.moveTo(padding, padding);
timeseriesCtx.lineTo(padding, timeseriesCanvas.height - padding);
timeseriesCtx.lineTo(timeseriesCanvas.width - padding, timeseriesCanvas.height - padding);
timeseriesCtx.stroke();
// Generate time series
let x = 0.5;
for (let i = 0; i < TRANSIENTS; i++) {
x = logistic(x, r);
}
const timeSeries = [];
for (let i = 0; i < 100; i++) {
x = logistic(x, r);
timeSeries.push(x);
}
// Draw time series line
timeseriesCtx.strokeStyle = '#ff2200';
timeseriesCtx.lineWidth = 1.5;
timeseriesCtx.beginPath();
for (let i = 0; i < timeSeries.length; i++) {
const xPixel = padding + (i / timeSeries.length) * plotWidth;
const yPixel = timeseriesCanvas.height - padding - timeSeries[i] * plotHeight;
if (i === 0) {
timeseriesCtx.moveTo(xPixel, yPixel);
} else {
timeseriesCtx.lineTo(xPixel, yPixel);
}
}
timeseriesCtx.stroke();
// Draw points
timeseriesCtx.fillStyle = '#ff8800';
for (let i = 0; i < timeSeries.length; i += 5) {
const xPixel = padding + (i / timeSeries.length) * plotWidth;
const yPixel = timeseriesCanvas.height - padding - timeSeries[i] * plotHeight;
timeseriesCtx.beginPath();
timeseriesCtx.arc(xPixel, yPixel, 2, 0, 2 * Math.PI);
timeseriesCtx.fill();
}
// Axis labels
timeseriesCtx.fillStyle = '#555';
timeseriesCtx.font = '11px "DM Mono"';
timeseriesCtx.textAlign = 'center';
timeseriesCtx.fillText('n (iterations)', timeseriesCanvas.width / 2, timeseriesCanvas.height - 5);
timeseriesCtx.textAlign = 'right';
timeseriesCtx.fillText('x', padding - 10, padding - 5);
currentXStat.textContent = timeSeries[timeSeries.length - 1].toFixed(3);
}
// ===== EVENT HANDLERS =====
function updateAll() {
rValue.textContent = state.r.toFixed(3);
rSlider.value = state.r;
rNumeric.value = state.r;
behaviorText.textContent = detectBehavior(state.r);
attractorPeriodStat.textContent = estimateAttractorPeriod(state.r);
drawBifurcationDiagram(state.bifurcationData, colorModeSelect.value);
drawCobwebDiagram(state.r);
drawTimeSeriesPlot(state.r);
}
rSlider.addEventListener('input', (e) => {
state.r = parseFloat(e.target.value);
updateAll();
});
rNumeric.addEventListener('input', (e) => {
state.r = Math.max(2.5, Math.min(4.0, parseFloat(e.target.value) || 2.5));
updateAll();
});
iterationsSelect.addEventListener('change', () => {
const iters = parseInt(iterationsSelect.value);
state.bifurcationData = computeBifurcation(2.5, 4.0, 800, iters);
updateAll();
});
colorModeSelect.addEventListener('change', () => {
updateAll();
});
// ===== ZOOM AND INTERACTION =====
bifCanvas.addEventListener('mousedown', (e) => {
const rect = bifCanvas.getBoundingClientRect();
state.selectionStart = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
state.selecting = true;
});
bifCanvas.addEventListener('mousemove', (e) => {
const rect = bifCanvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
state.mousePos = { x, y };
// Show tooltip
const padding = 40;
const plotWidth = bifCanvas.width - 2 * padding;
const plotHeight = bifCanvas.height - 2 * padding;
if (x > padding && x < bifCanvas.width - padding && y > padding && y < bifCanvas.height - padding) {
const rMin = state.zoomOriginX;
const rMax = rMin + (4.0 - 2.5) / state.zoomLevel;
const rNorm = (x - padding) / plotWidth;
const r = rMin + rNorm * (rMax - rMin);
const xVal = 1 - (y - padding) / plotHeight;
tooltip.innerHTML = `r: ${r.toFixed(3)}<br>x: ${xVal.toFixed(3)}`;
tooltip.style.display = 'block';
tooltip.style.left = (x + 10) + 'px';
tooltip.style.top = (y - 20) + 'px';
} else {
tooltip.style.display = 'none';
}
});
bifCanvas.addEventListener('mouseup', (e) => {
if (!state.selecting) return;
state.selecting = false;
const rect = bifCanvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const padding = 40;
const plotWidth = bifCanvas.width - 2 * padding;
const plotHeight = bifCanvas.height - 2 * padding;
const x1 = Math.min(state.selectionStart.x, x);
const x2 = Math.max(state.selectionStart.x, x);
if (x2 - x1 > 20 && x1 > padding && x2 < bifCanvas.width - padding) {
const rMin = state.zoomOriginX;
const rMax = rMin + (4.0 - 2.5) / state.zoomLevel;
const rNorm1 = (x1 - padding) / plotWidth;
const rNorm2 = (x2 - padding) / plotWidth;
const newRMin = rMin + rNorm1 * (rMax - rMin);
const newRMax = rMin + rNorm2 * (rMax - rMin);
state.zoomOriginX = newRMin;
state.zoomLevel *= (4.0 - 2.5) / (newRMax - newRMin);
zoomIndicator.textContent = `Zoom: ${state.zoomLevel.toFixed(1)}×`;
updateAll();
}
});
bifCanvas.addEventListener('dblclick', () => {
resetZoom();
});
function resetZoom() {
state.zoomOriginX = 2.5;
state.zoomLevel = 1.0;
zoomIndicator.textContent = 'Zoom: 1.0×';
updateAll();
}
// ===== INITIALIZATION =====
window.addEventListener('DOMContentLoaded', () => {
const iters = parseInt(iterationsSelect.value);
state.bifurcationData = computeBifurcation(2.5, 4.0, 800, iters);
updateAll();
});