Created
June 2, 2026 01:01
-
-
Save gkucmierz/2e0e2d1b778c32dd4fd0c9620e6f97ac to your computer and use it in GitHub Desktop.
Run this code instantly in your browser: https://instacode.app/gist/2e0e2d1b778c32dd4fd0c9620e6f97ac
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { canvas, onResize } from 'canvas'; | |
| import { mobius } from '@gkucmierz/utils@3.2.0'; | |
| const ctx = canvas.getContext('2d'); | |
| // --- Configuration Constants --- | |
| const BG_COLOR = '#0f172a'; // Background color of the canvas (Slate 900) | |
| const MAX_X = 250; | |
| const MAX_Y = 0; // Maximum limit on Y axis (set to 0 or -1 for auto-scaling) | |
| const STEP_X = 20; // Grid step size for X axis (set to 0 for auto-scaling) | |
| const STEP_Y = 0; // Grid step size for Y axis (set to 0 for auto-scaling) | |
| const RIEMANN_TERMS = 10; // Number of terms to compute in Riemann's R(x) approximation | |
| // --- Mathematical Helper Functions --- | |
| // Check if a number is prime | |
| function isPrime(n) { | |
| if (n < 2) return false; | |
| for (let i = 2; i <= Math.sqrt(n); i++) { | |
| if (n % i === 0) return false; | |
| } | |
| return true; | |
| } | |
| // Prime counting function pi(x) | |
| function pi(x) { | |
| let count = 0; | |
| for (let i = 2; i <= x; i++) { | |
| if (isPrime(i)) count++; | |
| } | |
| return count; | |
| } | |
| // Logarithmic integral function Li(x) approximated numerically | |
| // Using Li(2) ≈ 1.04516378 as a baseline to avoid singularity at t = 1 | |
| function Li(x) { | |
| if (x <= 1.0001) return 0; | |
| const Li2 = 1.04516378; | |
| if (x === 2) return Li2; | |
| // Integrate 1 / ln(t) from 2 to x | |
| let sum = 0; | |
| const steps = 500; | |
| const dt = (x - 2) / steps; | |
| for (let i = 0; i < steps; i++) { | |
| const t = 2 + i * dt + dt / 2; | |
| sum += 1 / Math.log(t); | |
| } | |
| return Li2 + sum * dt; | |
| } | |
| // Riemann's prime-counting approximation R(x) | |
| // Using Mobius inversion formula terms up to RIEMANN_TERMS | |
| function R(x) { | |
| if (x <= 1.0001) return 0; | |
| let sum = 0; | |
| for (let n = 1; n <= RIEMANN_TERMS; n++) { | |
| const mu = mobius(n); | |
| if (mu === 0) continue; | |
| const power = Math.pow(x, 1 / n); | |
| sum += (mu / n) * Li(power); | |
| } | |
| return sum; | |
| } | |
| // --- Canvas Rendering logic --- | |
| onResize((width, height) => { | |
| canvas.width = width; | |
| canvas.height = height; | |
| // Clear background | |
| ctx.fillStyle = BG_COLOR; | |
| ctx.fillRect(0, 0, width, height); | |
| // Layout dimensions | |
| const paddingLeft = 50; | |
| const paddingRight = 130; // Extra space for the legend on the right | |
| const paddingTop = 60; | |
| const paddingBottom = 45; | |
| const graphWidth = width - paddingLeft - paddingRight; | |
| const graphHeight = height - paddingTop - paddingBottom; | |
| if (graphWidth <= 0 || graphHeight <= 0) return; | |
| // --- Dynamic Axis Calibration --- | |
| // 1. Calculate MAX_Y automatically if needed | |
| let maxYValue = MAX_Y; | |
| if (maxYValue <= 0) { | |
| const computedMaxY = Math.max(MAX_X / Math.log(MAX_X), Li(MAX_X), R(MAX_X), pi(MAX_X)); | |
| maxYValue = Math.ceil(computedMaxY * 1.1); | |
| } | |
| // 2. Calculate STEP_X automatically if needed | |
| let stepXValue = STEP_X; | |
| if (stepXValue <= 0) { | |
| const rawStep = MAX_X / 5; | |
| if (rawStep <= 1) stepXValue = 1; | |
| else if (rawStep <= 2) stepXValue = 2; | |
| else if (rawStep <= 5) stepXValue = 5; | |
| else if (rawStep <= 10) stepXValue = 10; | |
| else if (rawStep <= 20) stepXValue = 20; | |
| else if (rawStep <= 50) stepXValue = 50; | |
| else if (rawStep <= 100) stepXValue = 100; | |
| else stepXValue = Math.ceil(rawStep / 50) * 50; | |
| } | |
| // 3. Calculate STEP_Y automatically if needed | |
| let stepYValue = STEP_Y; | |
| if (stepYValue <= 0) { | |
| const rawStep = maxYValue / 5; | |
| if (rawStep <= 1) stepYValue = 1; | |
| else if (rawStep <= 2) stepYValue = 2; | |
| else if (rawStep <= 5) stepYValue = 5; | |
| else if (rawStep <= 10) stepYValue = 10; | |
| else if (rawStep <= 20) stepYValue = 20; | |
| else if (rawStep <= 50) stepYValue = 50; | |
| else if (rawStep <= 100) stepYValue = 100; | |
| else stepYValue = Math.ceil(rawStep / 50) * 50; | |
| } | |
| // Coordinate conversion helpers | |
| const toScreenX = (valX) => paddingLeft + (valX / MAX_X) * graphWidth; | |
| const toScreenY = (valY) => height - paddingBottom - (valY / maxYValue) * graphHeight; | |
| // 1. Draw Title | |
| ctx.fillStyle = '#f8fafc'; | |
| ctx.font = 'bold 15px sans-serif'; | |
| ctx.textAlign = 'left'; | |
| ctx.fillText('Primes & Riemann Zeta Zeros Relation', paddingLeft, 30); | |
| // 2. Draw Axes & Grid | |
| ctx.strokeStyle = '#334155'; // Slate 700 | |
| ctx.lineWidth = 1; | |
| // X grid and labels | |
| ctx.fillStyle = '#94a3b8'; | |
| ctx.font = '10px monospace'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'top'; | |
| for (let x = 0; x <= MAX_X; x += stepXValue) { | |
| const sx = toScreenX(x); | |
| // Grid line | |
| ctx.strokeStyle = x === 0 ? '#64748b' : '#1e293b'; | |
| ctx.beginPath(); | |
| ctx.moveTo(sx, toScreenY(0)); | |
| ctx.lineTo(sx, toScreenY(maxYValue)); | |
| ctx.stroke(); | |
| // Label | |
| ctx.fillStyle = '#94a3b8'; | |
| ctx.fillText(String(x), sx, toScreenY(0) + 6); | |
| } | |
| // Y grid and labels | |
| ctx.textAlign = 'right'; | |
| ctx.textBaseline = 'middle'; | |
| for (let y = 0; y <= maxYValue; y += stepYValue) { | |
| const sy = toScreenY(y); | |
| // Grid line | |
| ctx.strokeStyle = y === 0 ? '#64748b' : '#1e293b'; | |
| ctx.beginPath(); | |
| ctx.moveTo(toScreenX(0), sy); | |
| ctx.lineTo(toScreenX(MAX_X), sy); | |
| ctx.stroke(); | |
| // Label | |
| ctx.fillStyle = '#94a3b8'; | |
| ctx.fillText(String(y), toScreenX(0) - 8, sy); | |
| } | |
| // 3. Draw Functions | |
| // A. Gauss approximation: x / ln(x) (Green Line) | |
| ctx.strokeStyle = '#22c55e'; // Green 500 | |
| ctx.lineWidth = 2; | |
| ctx.beginPath(); | |
| let first = true; | |
| for (let x = 2.0; x <= MAX_X; x += 0.5) { | |
| const y = x / Math.log(x); | |
| const sx = toScreenX(x); | |
| const sy = toScreenY(y); | |
| if (first) { | |
| ctx.moveTo(sx, sy); | |
| first = false; | |
| } else { | |
| ctx.lineTo(sx, sy); | |
| } | |
| } | |
| ctx.stroke(); | |
| // B. Logarithmic integral: Li(x) (Purple Line) | |
| ctx.strokeStyle = '#d946ef'; // Fuchsia 500 | |
| ctx.lineWidth = 2; | |
| ctx.beginPath(); | |
| first = true; | |
| for (let x = 2.0; x <= MAX_X; x += 0.5) { | |
| const y = Li(x); | |
| const sx = toScreenX(x); | |
| const sy = toScreenY(y); | |
| if (first) { | |
| ctx.moveTo(sx, sy); | |
| first = false; | |
| } else { | |
| ctx.lineTo(sx, sy); | |
| } | |
| } | |
| ctx.stroke(); | |
| // C. Riemann approximation: R(x) (Blue Line) | |
| ctx.strokeStyle = '#3b82f6'; // Blue 500 | |
| ctx.lineWidth = 2; | |
| ctx.beginPath(); | |
| first = true; | |
| for (let x = 2.0; x <= MAX_X; x += 0.5) { | |
| const y = R(x); | |
| const sx = toScreenX(x); | |
| const sy = toScreenY(y); | |
| if (first) { | |
| ctx.moveTo(sx, sy); | |
| first = false; | |
| } else { | |
| ctx.lineTo(sx, sy); | |
| } | |
| } | |
| ctx.stroke(); | |
| // D. Prime counting function: pi(x) (Red Step-like Line) | |
| ctx.strokeStyle = '#ef4444'; // Red 500 | |
| ctx.lineWidth = 2.5; | |
| ctx.beginPath(); | |
| ctx.moveTo(toScreenX(0), toScreenY(0)); | |
| let currentY = 0; | |
| for (let x = 1; x <= MAX_X; x++) { | |
| const nextY = pi(x); | |
| const sxCurrent = toScreenX(x); | |
| const syCurrent = toScreenY(currentY); | |
| const syNext = toScreenY(nextY); | |
| // Draw horizontal step to current X, then vertical jump to next Y value | |
| ctx.lineTo(sxCurrent, syCurrent); | |
| ctx.lineTo(sxCurrent, syNext); | |
| currentY = nextY; | |
| } | |
| ctx.stroke(); | |
| // 5. Draw Legend Box on the Right | |
| const legendX = toScreenX(MAX_X) + 15; | |
| const legendY = paddingTop + 10; | |
| const itemHeight = 22; | |
| ctx.fillStyle = '#1e293b'; // Slate 800 | |
| ctx.strokeStyle = '#334155'; | |
| ctx.lineWidth = 1; | |
| ctx.beginPath(); | |
| ctx.roundRect(legendX - 5, legendY - 8, 105, 100, 4); | |
| ctx.fill(); | |
| ctx.stroke(); | |
| const legendItems = [ | |
| { label: 'π(x)', color: '#ef4444' }, | |
| { label: 'x/ln(x)', color: '#22c55e' }, | |
| { label: 'Li(x)', color: '#d946ef' }, | |
| { label: 'R(x)', color: '#3b82f6' } | |
| ]; | |
| ctx.textAlign = 'left'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.font = '11px "JetBrains Mono", monospace'; | |
| legendItems.forEach((item, idx) => { | |
| const cy = legendY + idx * itemHeight; | |
| // Draw color line indicator | |
| ctx.strokeStyle = item.color; | |
| ctx.lineWidth = 3; | |
| ctx.beginPath(); | |
| ctx.moveTo(legendX, cy); | |
| ctx.lineTo(legendX + 15, cy); | |
| ctx.stroke(); | |
| // Draw label | |
| ctx.fillStyle = '#cbd5e1'; | |
| ctx.fillText(item.label, legendX + 22, cy); | |
| }); | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment