{
// Taylor series convergence disk — missed opportunity 1
// Draggable z₀, disk of radius R = distance to nearest singularity
const W = 480, H = 420, CX = 200, CY = 210, SCALE = 70;
const svgNS = "http://www.w3.org/2000/svg";
const funcSel = Inputs.select([
"1/(z²+1) — poles at ±i",
"tan z — poles at ±π/2, ±3π/2, …",
"log z — branch cut on Re≤0",
"e^z — entire (no singularity)"
], { value: "1/(z²+1) — poles at ±i", label: "f(z)" });
const nTermsSl = Inputs.range([1, 20], { value: 6, step: 1, label: "Partial sum N" });
// z₀ position via sliders (dragging complex in OJS needs canvas events; sliders are cleaner)
const z0reSl = Inputs.range([-2.5, 2.5], { value: 0.5, step: 0.05, label: "Re(z₀)" });
const z0imSl = Inputs.range([-2.5, 2.5], { value: 0.3, step: 0.05, label: "Im(z₀)" });
function singularities(fname) {
if (fname.startsWith("1/(z²+1)")) return [{re:0,im:1},{re:0,im:-1}];
if (fname.startsWith("tan")) {
const s = [];
for (let k = -3; k <= 3; k++) s.push({re: (k+0.5)*Math.PI, im: 0});
return s;
}
if (fname.startsWith("log")) return [{re:-99,im:0}]; // branch cut; treat as nearest on negative axis
return []; // entire
}
function nearestDist(fname, z0re, z0im) {
if (fname.startsWith("log")) {
// Distance to nearest branch-cut point on {Re≤0} = distance to point (-|z0re|, z0im) if z0re<0 else to (0, z0im)
// Approximate: distance to origin along the branch cut direction
// The branch cut is the negative real axis; nearest point is (min(z0re,0), 0) extended
// Actually: the branch point is at z=0, and the branch cut extends left.
// Convergence radius of Taylor series at z0 = distance to origin (the branch point)
return Math.sqrt(z0re*z0re + z0im*z0im);
}
const sings = singularities(fname);
if (sings.length === 0) return 999; // entire
let minD = Infinity;
for (const s of sings) {
const d = Math.sqrt((z0re-s.re)**2 + (z0im-s.im)**2);
if (d < minD) minD = d;
}
return minD;
}
function evalFunc(fname, re, im) {
if (fname.startsWith("1/(z²+1)")) {
const a = 1 + re*re - im*im, b = 2*re*im;
const d = a*a + b*b; if (d < 1e-10) return [NaN, NaN];
return [a/d, -b/d];
}
if (fname.startsWith("tan")) {
const sr = Math.sin(re)*Math.cosh(im), si = Math.cos(re)*Math.sinh(im);
const cr = Math.cos(re)*Math.cosh(im), ci = -Math.sin(re)*Math.sinh(im);
const d = cr*cr + ci*ci; if (d < 1e-10) return [NaN, NaN];
return [(sr*cr+si*ci)/d, (si*cr-sr*ci)/d];
}
if (fname.startsWith("log")) {
const r2 = re*re + im*im; if (r2 < 1e-10) return [NaN, NaN];
return [0.5*Math.log(r2), Math.atan2(im, re)];
}
// e^z
return [Math.exp(re)*Math.cos(im), Math.exp(re)*Math.sin(im)];
}
function taylorCoeffs(fname, z0re, z0im, N) {
// Numerical Taylor coefficients via finite differences (central diff for accuracy)
// c_n = f^(n)(z0) / n! — computed by repeated complex differentiation
// We use forward differences with h = 0.001 * R direction
const h = 0.001;
const coeffs = []; // [re, im] for n=0..N-1
// Use complex differentiation formula: f'(z) ≈ (f(z+h) - f(z-h)) / (2h)
// We'll just evaluate directly for known functions
if (fname.startsWith("1/(z²+1)")) {
// f(z) = 1/(z²+1); f^(n)(z0) computable
// Use the partial fraction decomposition: f = i/2 * (1/(z+i) - 1/(z-i))
// 1/(z-a)^{n+1} contributes (-1)^n * n! / (z0-a)^{n+1} to n-th derivative
const c = []; // coefficients c_n for Taylor at z0
const amre = z0re, amim = z0im + 1; // z0 - (-i) = z0 + i
const apre = z0re, apim = z0im - 1; // z0 - i
const amd = amre*amre + amim*amim;
const apd = apre*apre + apim*apim;
if (amd < 1e-10 || apd < 1e-10) return null;
// c_n = i/2 * [(-1)^n n! / (z0-(-i))^{n+1} - (-1)^n n! / (z0-i)^{n+1}]
// = i/2 * (-1)^n n! * [1/(z0+i)^{n+1} - 1/(z0-i)^{n+1}]
// Power of complex: (a+ib)^k
function cpow(re, im, k) {
if (k === 0) return [1, 0];
let r = Math.sqrt(re*re+im*im), theta = Math.atan2(im, re);
let rk = Math.pow(r, k), tk = theta * k;
return [rk*Math.cos(tk), rk*Math.sin(tk)];
}
function cdiv(ar, ai, br, bi) {
const d = br*br + bi*bi; if (d < 1e-12) return [NaN, NaN];
return [(ar*br+ai*bi)/d, (ai*br-ar*bi)/d];
}
for (let n = 0; n < N; n++) {
const [pm_re, pm_im] = cpow(amre, amim, n+1);
const [pp_re, pp_im] = cpow(apre, apim, n+1);
const [inv_am_re, inv_am_im] = cdiv(1, 0, pm_re, pm_im);
const [inv_ap_re, inv_ap_im] = cdiv(1, 0, pp_re, pp_im);
// diff = inv_am - inv_ap
const diffRe = inv_am_re - inv_ap_re, diffIm = inv_am_im - inv_ap_im;
// multiply by i/2 and (-1)^n (n! already absorbed into c_n = f^(n)(z0)/n!)
// c_n = i/2 * (-1)^n * [diff] (the n! cancels with 1/n!)
const sign = (n%2===0) ? 1 : -1;
// * i/2: (a+ib)*i = -b+ia
const cr = sign * (-diffIm) / 2;
const ci = sign * (diffRe) / 2;
c.push([cr, ci]);
}
return c;
}
// Fallback: numerical coefficients via repeated evaluation
// f^(n)(z0) ≈ (2h)^{-n} * sum_k C(n,k)*(-1)^{n-k} f(z0 + k*h) — forward differences
// For simplicity, use a simple numerical approach
const c = [];
for (let n = 0; n < N; n++) {
// n-th derivative via central finite differences (Richardson extrapolation would be better but expensive)
const hn = Math.pow(0.01, 1); // step
let sumRe = 0, sumIm = 0;
// Forward difference: f^(n)(z0) ≈ (1/h^n) sum_{k=0}^{n} (-1)^{n-k} C(n,k) f(z0 + k*h)
let fact = 1;
for (let k = 0; k <= n; k++) {
const sign = ((n-k)%2===0) ? 1 : -1;
const binom = fact / ((k <= n-k ? [1,1,2,6,24,120,720,5040][k]||1 : [1,1,2,6,24,120,720,5040][n-k]||1) * ([1,1,2,6,24,120,720,5040][Math.min(k,n-k)]||1));
const [fr, fi] = evalFunc(fname, z0re + k*hn, z0im);
// C(n,k) = n! / (k! * (n-k)!)
let cn = 1; for (let j=0;j<n;j++) cn*=(j<k?1:1); // just accumulate properly
// Simple binomial
let bk = 1; for (let j=0;j<k;j++) bk = bk*(n-j)/(j+1);
sumRe += sign * bk * fr;
sumIm += sign * bk * fi;
}
let nfact = 1; for (let j=1;j<=n;j++) nfact*=j;
c.push([sumRe / Math.pow(hn, n) / nfact, sumIm / Math.pow(hn, n) / nfact]);
}
return c;
}
function taylorPartialSum(coeffs, z0re, z0im, re, im, N) {
// Sum c_n (z - z0)^n
const dre = re - z0re, dim = im - z0im;
let sumRe = 0, sumIm = 0;
let powRe = 1, powIm = 0;
for (let n = 0; n < Math.min(coeffs.length, N); n++) {
const [cr, ci] = coeffs[n];
sumRe += cr*powRe - ci*powIm;
sumIm += cr*powIm + ci*powRe;
const newPowRe = powRe*dre - powIm*dim;
const newPowIm = powRe*dim + powIm*dre;
powRe = newPowRe; powIm = newPowIm;
}
return [sumRe, sumIm];
}
function hslToRgb(h, s, l) {
h = ((h%1)+1)%1;
const c = (1-Math.abs(2*l-1))*s, x = c*(1-Math.abs((h*6)%2-1)), m = l-c/2;
let r=0,g=0,b=0;
if(h<1/6){r=c;g=x;}else if(h<2/6){r=x;g=c;}else if(h<3/6){g=c;b=x;}
else if(h<4/6){g=x;b=c;}else if(h<5/6){r=x;b=c;}else{r=c;b=x;}
return [Math.round((r+m)*255),Math.round((g+m)*255),Math.round((b+m)*255)];
}
function toScreen(re, im) { return [CX + re*SCALE, CY - im*SCALE]; }
function render(fname, z0re, z0im, N) {
const R = nearestDist(fname, z0re, z0im);
const coeffs = taylorCoeffs(fname, z0re, z0im, N) || [];
// Left panel: exact function with disk overlay
const cL = document.createElement("canvas");
cL.width = W; cL.height = H;
const ctxL = cL.getContext("2d");
const imgL = ctxL.createImageData(W, H); const dL = imgL.data;
for (let px=0; px<W; px++) {
for (let py=0; py<H; py++) {
const re = (px-CX)/SCALE, im = -(py-CY)/SCALE;
const [wr, wi] = evalFunc(fname, re, im);
if (!isFinite(wr)||!isFinite(wi)||isNaN(wr)) {
const idx=(py*W+px)*4;
dL[idx]=255;dL[idx+1]=255;dL[idx+2]=255;dL[idx+3]=255; continue;
}
const arg = Math.atan2(wi, wr), mag = Math.sqrt(wr*wr+wi*wi);
const hue = (arg/(2*Math.PI)+1)%1;
const brightness = 0.5 + 0.35*Math.tanh(Math.log(mag+0.001)/2);
const [r,g,b] = hslToRgb(hue, 0.8, brightness);
const idx=(py*W+px)*4; dL[idx]=r;dL[idx+1]=g;dL[idx+2]=b;dL[idx+3]=255;
}
}
ctxL.putImageData(imgL, 0, 0);
// Convergence disk — gold dashed circle
const [z0sx, z0sy] = toScreen(z0re, z0im);
ctxL.setLineDash([6, 4]);
ctxL.strokeStyle = "#fbbf24"; ctxL.lineWidth = 2.5;
ctxL.beginPath();
if (R < 50) ctxL.arc(z0sx, z0sy, R*SCALE, 0, 2*Math.PI);
ctxL.stroke();
ctxL.setLineDash([]);
// Line to nearest singularity
const sings = singularities(fname);
if (sings.length > 0) {
let nearS = sings[0], nearD = Infinity;
for (const s of sings) {
const d = Math.sqrt((z0re-s.re)**2+(z0im-s.im)**2);
if (d < nearD) { nearD = d; nearS = s; }
}
const [nsx, nsy] = toScreen(nearS.re, nearS.im);
ctxL.strokeStyle = "rgba(251,191,36,0.6)"; ctxL.lineWidth = 1.5; ctxL.setLineDash([3,3]);
ctxL.beginPath(); ctxL.moveTo(z0sx, z0sy); ctxL.lineTo(nsx, nsy); ctxL.stroke();
ctxL.setLineDash([]);
// Nearest singularity ×
ctxL.strokeStyle="#ef4444"; ctxL.lineWidth=2;
ctxL.beginPath(); ctxL.moveTo(nsx-6,nsy-6); ctxL.lineTo(nsx+6,nsy+6); ctxL.stroke();
ctxL.beginPath(); ctxL.moveTo(nsx+6,nsy-6); ctxL.lineTo(nsx-6,nsy+6); ctxL.stroke();
}
// Axes
const [ox, oy] = toScreen(0, 0);
ctxL.strokeStyle="rgba(255,255,255,0.5)"; ctxL.lineWidth=0.8;
ctxL.beginPath(); ctxL.moveTo(0,oy); ctxL.lineTo(W,oy); ctxL.stroke();
ctxL.beginPath(); ctxL.moveTo(ox,0); ctxL.lineTo(ox,H); ctxL.stroke();
// z₀ dot
ctxL.fillStyle="#fbbf24"; ctxL.beginPath(); ctxL.arc(z0sx, z0sy, 6, 0, 2*Math.PI); ctxL.fill();
ctxL.strokeStyle="#fff"; ctxL.lineWidth=1.5; ctxL.stroke();
ctxL.fillStyle="#fff"; ctxL.font="11px sans-serif";
ctxL.fillText(`z₀`, z0sx+8, z0sy-4);
ctxL.fillText(`R = ${R < 50 ? R.toFixed(3) : "∞"}`, z0sx+8, z0sy+10);
ctxL.fillStyle="rgba(0,0,0,0.7)"; ctxL.font="11px sans-serif";
ctxL.fillText("exact f(z)", 6, 16);
// Right panel: partial sum
const cR = document.createElement("canvas");
cR.width = W; cR.height = H;
const ctxR = cR.getContext("2d");
const imgR = ctxR.createImageData(W, H); const dR = imgR.data;
for (let px=0; px<W; px++) {
for (let py=0; py<H; py++) {
const re = (px-CX)/SCALE, im = -(py-CY)/SCALE;
let wr, wi;
if (coeffs.length > 0) {
[wr, wi] = taylorPartialSum(coeffs, z0re, z0im, re, im, N);
} else {
[wr, wi] = evalFunc(fname, re, im);
}
if (!isFinite(wr)||!isFinite(wi)||isNaN(wr)||Math.abs(wr)>200||Math.abs(wi)>200) {
const idx=(py*W+px)*4;
dR[idx]=240;dR[idx+1]=240;dR[idx+2]=240;dR[idx+3]=255; continue;
}
const arg = Math.atan2(wi, wr), mag = Math.sqrt(wr*wr+wi*wi);
const hue = (arg/(2*Math.PI)+1)%1;
const brightness = 0.5 + 0.35*Math.tanh(Math.log(mag+0.001)/2);
const [r,g,b] = hslToRgb(hue, 0.8, brightness);
const idx=(py*W+px)*4; dR[idx]=r;dR[idx+1]=g;dR[idx+2]=b;dR[idx+3]=255;
}
}
ctxR.putImageData(imgR, 0, 0);
// Disk overlay on right panel
ctxR.setLineDash([6,4]);
ctxR.strokeStyle="#fbbf24"; ctxR.lineWidth=2.5;
ctxR.beginPath();
if (R < 50) ctxR.arc(z0sx, z0sy, R*SCALE, 0, 2*Math.PI);
ctxR.stroke(); ctxR.setLineDash([]);
ctxR.fillStyle="#fbbf24"; ctxR.beginPath(); ctxR.arc(z0sx, z0sy, 6, 0, 2*Math.PI); ctxR.fill();
ctxR.strokeStyle="#fff"; ctxR.lineWidth=1.5; ctxR.stroke();
const [ox2, oy2] = toScreen(0, 0);
ctxR.strokeStyle="rgba(255,255,255,0.5)"; ctxR.lineWidth=0.8;
ctxR.beginPath(); ctxR.moveTo(0,oy2); ctxR.lineTo(W,oy2); ctxR.stroke();
ctxR.beginPath(); ctxR.moveTo(ox2,0); ctxR.lineTo(ox2,H); ctxR.stroke();
ctxR.fillStyle="rgba(0,0,0,0.7)"; ctxR.font="11px sans-serif";
ctxR.fillText(`Taylor partial sum, N=${N} terms`, 6, 16);
ctxR.fillStyle="rgba(255,255,255,0.85)"; ctxR.font="10px sans-serif";
ctxR.fillText("Inside disk: series matches f. Outside: diverges.", 6, H-8);
const panelDiv = document.createElement("div");
panelDiv.style.cssText = "display:flex; gap:0.75rem; flex-wrap:wrap;";
panelDiv.appendChild(cL); panelDiv.appendChild(cR);
const infoDiv = document.createElement("div");
infoDiv.style.cssText = "margin-top:0.5rem; font-family:sans-serif; font-size:0.88em; background:#fefce8; border:1px solid #fde68a; border-radius:4px; padding:0.5rem 0.75rem;";
infoDiv.innerHTML = `<strong>Convergence radius R = ${R < 50 ? R.toFixed(4) : "∞"}</strong> — distance from z₀ = ${z0re.toFixed(2)}+${z0im.toFixed(2)}i to nearest singularity. The Taylor series converges inside the gold dashed disk and diverges outside it.`;
const wrap = document.createElement("div");
wrap.appendChild(panelDiv); wrap.appendChild(infoDiv);
return wrap;
}
const container = document.createElement("div");
container.style.cssText = "border:1px solid #e5e7eb; border-radius:8px; padding:1rem; margin:1rem 0;";
const title = document.createElement("div");
title.style.cssText = "font-weight:600; margin-bottom:0.5rem; font-family:sans-serif;";
title.textContent = "Taylor series convergence disk: R = distance to nearest singularity";
container.appendChild(title);
container.appendChild(funcSel);
container.appendChild(z0reSl);
container.appendChild(z0imSl);
container.appendChild(nTermsSl);
const vizDiv = document.createElement("div");
container.appendChild(vizDiv);
const note = document.createElement("div");
note.style.cssText = "margin-top:0.5rem; font-size:0.82em; color:#6b7280; font-style:italic;";
note.textContent = "Left: exact f(z) with domain colouring (hue = arg f, brightness = log|f|). Right: N-term Taylor partial sum at z₀. Gold dashed circle = convergence disk of radius R. Red × = nearest singularity. Move z₀ or change N to watch the disk and partial sum update.";
container.appendChild(note);
function update() {
vizDiv.innerHTML = "";
vizDiv.appendChild(render(funcSel.value, z0reSl.value, z0imSl.value, nTermsSl.value));
}
funcSel.addEventListener("input", update);
z0reSl.addEventListener("input", update);
z0imSl.addEventListener("input", update);
nTermsSl.addEventListener("input", update);
update();
return container;
}