Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save gkucmierz/2e0e2d1b778c32dd4fd0c9620e6f97ac to your computer and use it in GitHub Desktop.

Select an option

Save gkucmierz/2e0e2d1b778c32dd4fd0c9620e6f97ac to your computer and use it in GitHub Desktop.
Run this code instantly in your browser: https://instacode.app/gist/2e0e2d1b778c32dd4fd0c9620e6f97ac
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