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