Skip to content

Taylor Series

Polynomials pretending to be functions.

Beginner / approximation / calculus

A Taylor polynomial rebuilds a function from its derivatives at a single point. Near that point a few terms are uncannily good; far away the approximation falls apart, and for functions like 1/(1 + x²) the failure line is the radius of convergence. Move the marker and read the error where you land.

Tagged: approximation, calculus, convergence, radius of convergence

Live simulation

Move the pointer to place the evaluation marker.

Speed

Move the pointer to place the evaluation marker.

Mathematics

A Taylor polynomial rebuilds a function near a point from its derivatives there: value, slope, curvature, and so on. Each additional term extends the region where the polynomial hugs the curve.

f(x) ≈ Pₙ(x) = Σ_{k=0}^{n} f⁽ᵏ⁾(0) / k! · xᵏ

Lagrange remainder: Rₙ(x) = f⁽ⁿ⁺¹⁾(ξ) / (n+1)! · xⁿ⁺¹ for some ξ between 0 and x

1/(1+x²): converges only for |x| < 1. The radius of convergence is real

The remainder bound explains what the plot shows: near the expansion point the factorial in the denominator crushes the error, and far away the polynomial is useless. For 1/(1 + x²) the failure at |x| = 1 has nothing to do with the function's smoothness at 0; it comes from poles hiding at x = ±i, which complex analysis makes visible in real calculus.

How it works

The function and its partial sum are evaluated point by point across the visible domain; the marker tracks the pointer and reports f, Pₙ, and their difference at that exact location. The max-error metric sweeps 201 sample points across the domain so the numbers cannot be cherry-picked.

Switch to 1/(1 + x²) and add terms one at a time: inside |x| < 1 the fit tightens, outside it gets dramatically worse. That is the radius of convergence doing exactly what the theorem says.

Implementation

src/engine/experiments/taylor-series.ts · 217 lines · the module running above

TypeScript · fixed-timestep engine

1import { BaseExperiment } from "../core/base-experiment";
2import type { ParameterDef, PointerState, ThemeColors } from "../core/types";
3
4type TaylorFn = {
5 label: string;
6 domain: [number, number];
7 /** f at x; NaN outside its domain. */
8 f: (x: number) => number;
9 /** k-th Taylor coefficient (about 0), k >= 0. */
10 coefficient: (k: number) => number;
11};
12
13const FUNCTIONS: Record<string, TaylorFn> = {
14 sin: {
15 label: "sin(x)",
16 domain: [-9, 9],
17 f: Math.sin,
18 coefficient: (k) => (k % 2 === 0 ? 0 : (k === 1 ? 1 : ((-1) ** ((k - 1) / 2)) / factorial(k))),
19 },
20 "1/(1+x^2)": {
21 label: "1 / (1 + x²)",
22 domain: [-4, 4],
23 f: (x) => 1 / (1 + x * x),
24 coefficient: (k) => (k % 2 === 0 ? ((-1) ** (k / 2)) : 0),
25 },
26 "ln(1+x)": {
27 label: "ln(1 + x)",
28 domain: [-0.95, 3],
29 f: (x) => (x > -1 ? Math.log(1 + x) : NaN),
30 coefficient: (k) => (k === 0 ? 0 : ((-1) ** (k + 1)) / k),
31 },
32};
33
34function factorial(n: number): number {
35 let out = 1;
36 for (let i = 2; i <= n; i++) out *= i;
37 return out;
38}
39
40/**
41 * Taylor polynomials with a live error readout. The marker position is set by
42 * the pointer; the max-error metric is sampled across the visible domain.
43 */
44export class TaylorSeriesExperiment extends BaseExperiment {
45 readonly id = "taylor-series";
46
47 private fn = FUNCTIONS.sin;
48 private markerX = 2;
49 private pointer = { x: 0, y: 0, down: false, inside: false };
50
51 protected params(): ParameterDef[] {
52 return [
53 {
54 key: "fn",
55 label: "Function",
56 options: [
57 { value: "sin", label: "sin(x) about 0" },
58 { value: "1/(1+x^2)", label: "1/(1 + x²) about 0" },
59 { value: "ln(1+x)", label: "ln(1 + x) about 0" },
60 ],
61 defaultValue: "sin",
62 },
63 { key: "terms", label: "Polynomial terms", min: 1, max: 21, step: 2, defaultValue: 5 },
64 ];
65 }
66
67 protected onReset(): void {
68 this.fn = FUNCTIONS[this.str("fn")];
69 this.markerX = 2;
70 }
71
72 protected onParameterChange(key: string): void {
73 if (key === "fn") this.reset();
74 }
75
76 /** Static visualization: state changes only through parameters and pointer. */
77 protected onUpdate(): void {}
78
79 private partialSum(x: number, terms: number): number {
80 let sum = 0;
81 let power = 1;
82 for (let k = 0; k < terms; k++) {
83 sum += this.fn.coefficient(k) * power;
84 power *= x;
85 }
86 return sum;
87 }
88
89 onPointer(state: PointerState): void {
90 this.pointer = state;
91 if (state.inside) {
92 const [x0, x1] = this.fn.domain;
93 this.markerX = x0 + (state.x / this.width) * (x1 - x0);
94 }
95 }
96
97 render(ctx: CanvasRenderingContext2D, theme: ThemeColors): void {
98 ctx.fillStyle = theme.bg;
99 ctx.fillRect(0, 0, this.width, this.height);
100 const [x0, x1] = this.fn.domain;
101 const yRange: [number, number] = [-2.4, 2.4];
102 const toX = (x: number) => ((x - x0) / (x1 - x0)) * this.width;
103 const toY = (y: number) => this.height / 2 - (y / (yRange[1] - yRange[0])) * this.height;
104 // Axes
105 ctx.strokeStyle = theme.line;
106 ctx.lineWidth = 1;
107 ctx.beginPath();
108 ctx.moveTo(0, toY(0));
109 ctx.lineTo(this.width, toY(0));
110 ctx.stroke();
111 if (x0 < 0 && x1 > 0) {
112 ctx.beginPath();
113 ctx.moveTo(toX(0), 0);
114 ctx.lineTo(toX(0), this.height);
115 ctx.stroke();
116 }
117 const terms = this.num("terms");
118 let maxError = 0;
119 // f
120 ctx.strokeStyle = theme.fgSecondary;
121 ctx.lineWidth = 1.5;
122 ctx.beginPath();
123 let pen = false;
124 for (let px = 0; px <= this.width; px += 2) {
125 const xv = x0 + (px / this.width) * (x1 - x0);
126 const yv = this.fn.f(xv);
127 if (Number.isNaN(yv) || Math.abs(yv) > 12) {
128 pen = false;
129 continue;
130 }
131 const py = toY(yv);
132 if (pen) ctx.lineTo(px, py);
133 else ctx.moveTo(px, py);
134 pen = true;
135 }
136 ctx.stroke();
137 // Partial sum
138 ctx.strokeStyle = theme.accent;
139 ctx.lineWidth = 2;
140 ctx.beginPath();
141 pen = false;
142 for (let px = 0; px <= this.width; px += 2) {
143 const xv = x0 + (px / this.width) * (x1 - x0);
144 const yv = this.partialSum(xv, terms);
145 if (Number.isNaN(yv) || Math.abs(yv) > 12) {
146 pen = false;
147 continue;
148 }
149 if (Number.isFinite(this.fn.f(xv))) {
150 maxError = Math.max(maxError, Math.abs(yv - this.fn.f(xv)));
151 }
152 const py = toY(yv);
153 if (pen) ctx.lineTo(px, py);
154 else ctx.moveTo(px, py);
155 pen = true;
156 }
157 ctx.stroke();
158 // Marker
159 const mx = toX(this.markerX);
160 const fy = this.fn.f(this.markerX);
161 const py = this.partialSum(this.markerX, terms);
162 ctx.strokeStyle = theme.fgTertiary;
163 ctx.setLineDash([3, 3]);
164 ctx.beginPath();
165 ctx.moveTo(mx, 0);
166 ctx.lineTo(mx, this.height);
167 ctx.stroke();
168 ctx.setLineDash([]);
169 ctx.fillStyle = theme.fg;
170 ctx.beginPath();
171 ctx.arc(mx, toY(fy), 3.5, 0, Math.PI * 2);
172 ctx.fill();
173 ctx.fillStyle = theme.accent;
174 ctx.beginPath();
175 ctx.arc(mx, toY(py), 3.5, 0, Math.PI * 2);
176 ctx.fill();
177 // Legend
178 ctx.font = "11px ui-monospace, monospace";
179 ctx.fillStyle = theme.fgSecondary;
180 ctx.fillText(this.fn.label, 10, 16);
181 ctx.fillStyle = theme.accent;
182 ctx.fillText(`P${terms}(x), ${terms} TERMS`, 10, 30);
183 }
184
185 getMetrics() {
186 const terms = this.num("terms");
187 const f = this.fn.f(this.markerX);
188 const p = this.partialSum(this.markerX, terms);
189 let maxError = 0;
190 const [x0, x1] = this.fn.domain;
191 for (let i = 0; i <= 200; i++) {
192 const xv = x0 + ((x1 - x0) * i) / 200;
193 const fv = this.fn.f(xv);
194 if (Number.isNaN(fv)) continue;
195 maxError = Math.max(maxError, Math.abs(this.partialSum(xv, terms) - fv));
196 }
197 return {
198 function: this.fn.label,
199 terms,
200 markerX: this.markerX.toFixed(2),
201 fAtMarker: Number.isNaN(f) ? "undefined" : f.toFixed(4),
202 pAtMarker: p.toExponential(3),
203 errorAtMarker: Number.isNaN(f) ? "n/a" : Math.abs(f - p).toExponential(2),
204 maxError: maxError.toExponential(2),
205 };
206 }
207
208 describe(): string {
209 const terms = this.num("terms");
210 return `The order-${terms} Taylor polynomial of ${this.fn.label} compared against the true curve.`;
211 }
212
213 entities(): number {
214 return this.num("terms");
215 }
216}
217

Performance

Time per tick
O(terms) per evaluated point
Space
None beyond the coefficient sequence
Notes
Coefficients are generated from each function's derivative recurrence, not hardcoded tables.

Live numbers (FPS, frame time, steps per second, microseconds per step, entity count) are in the performance overlay on the simulation above: the pulse icon in its top-right corner.