<p class="hint">Pick a function below. Symbolic differentiation walks its expression tree from the root down, applying one fixed rule per node type — sum, product, quotient, power, or chain — and rebuilds an exact derivative tree, never a numeric approximation.</p>
<div class="row">
<label for="fnSelect" class="sr-only">Choose a function</label>
<select id="fnSelect"></select>
<button id="diff" type="button">Differentiate</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="panels">
<div class="panel">
<div class="panel-title">Original function</div>
<div id="original" class="expr"></div>
</div>
<div class="panel">
<div class="panel-title">Derivative</div>
<div id="derivative" class="expr placeholder">Press "Differentiate" to see the exact result.</div>
</div>
</div>
<div class="panel-title">Rule log</div>
<div id="log" class="log"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.row { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .7rem; }
select { font: 600 14px system-ui, sans-serif; padding: .4rem .6rem; border-radius: 8px; border: 1px solid #adb1b8; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.panels { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; margin-bottom: .7rem; }
@media (max-width: 520px) { .panels { grid-template-columns: 1fr; } }
.panel-title { font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
color: #6b7280; margin: 0 0 .3rem; }
.expr { font: 700 18px ui-monospace, monospace; background: #e8eef3; border: 1px solid #cdd9e3;
border-radius: 8px; padding: .7rem .8rem; min-height: 1.6em; color: #1d3557; word-break: break-word; }
.expr.placeholder { color: #8a97a6; font-weight: 500; font-family: system-ui, sans-serif; font-size: .9rem; }
.log { font: 13px ui-monospace, monospace; background: #16223a; color: #d7e2f0; border-radius: 8px;
padding: .6rem .7rem; min-height: 2.4em; max-height: 220px; overflow-y: auto; line-height: 1.55; }
.log .step { color: #8fd3a3; }
.log .rule { color: #ffd166; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// The expression is represented as an abstract syntax tree (AST), never as a text string.
// Node constructors: const/var are leaves; add/mul/div/pow/sin/cos are internal nodes with children.
const CONST = (v) => ({ t: 'const', v });
const VAR = () => ({ t: 'var' });
const ADD = (a, b) => ({ t: 'add', a, b });
const MUL = (a, b) => ({ t: 'mul', a, b });
const DIV = (a, b) => ({ t: 'div', a, b });
const POW = (a, n) => ({ t: 'pow', a, n });
const SIN = (a) => ({ t: 'sin', a });
const COS = (a) => ({ t: 'cos', a });
// A handful of built-in sample functions users can pick from.
const SAMPLES = [
{ key: 'sample_poly', build: () => ADD(POW(VAR(), 3), MUL(CONST(2), VAR())) },
{ key: 'sample_product', build: () => MUL(POW(VAR(), 2), SIN(VAR())) },
{ key: 'sample_chain', build: () => SIN(POW(VAR(), 2)) },
{ key: 'sample_quotient', build: () => DIV(VAR(), ADD(VAR(), CONST(1))) }
];
let stepCount = 0;
let logLines = [];
// Turn a tree back into a readable formula string (for display only, not used by derive()) .
function print(node) {
switch (node.t) {
case 'const': return String(node.v);
case 'var': return 'x';
case 'add': return '(' + print(node.a) + ' + ' + print(node.b) + ')';
case 'mul': return '(' + print(node.a) + ' * ' + print(node.b) + ')';
case 'div': return '(' + print(node.a) + ' / ' + print(node.b) + ')';
case 'pow': return print(node.a) + '^' + node.n;
case 'sin': return 'sin(' + print(node.a) + ')';
case 'cos': return 'cos(' + print(node.a) + ')';
default: return '?';
}
}
// Light post-pass: fold 0/1 identities and constant-constant arithmetic so the result stays readable.
function simplify(node) {
if (node.t === 'add') {
const a = simplify(node.a), b = simplify(node.b);
if (a.t === 'const' && a.v === 0) return b;
if (b.t === 'const' && b.v === 0) return a;
if (a.t === 'const' && b.t === 'const') return CONST(a.v + b.v);
return ADD(a, b);
}
if (node.t === 'mul') {
const a = simplify(node.a), b = simplify(node.b);
if ((a.t === 'const' && a.v === 0) || (b.t === 'const' && b.v === 0)) return CONST(0);
if (a.t === 'const' && a.v === 1) return b;
if (b.t === 'const' && b.v === 1) return a;
if (a.t === 'const' && b.t === 'const') return CONST(a.v * b.v);
return MUL(a, b);
}
if (node.t === 'div') {
const a = simplify(node.a), b = simplify(node.b);
if (a.t === 'const' && a.v === 0) return CONST(0);
return DIV(a, b);
}
if (node.t === 'pow') {
const a = simplify(node.a);
if (node.n === 1) return a;
if (node.n === 0) return CONST(1);
return POW(a, node.n);
}
if (node.t === 'sin' || node.t === 'cos') return { t: node.t, a: simplify(node.a) };
return node;
}
// The heart of the algorithm: one case per node type, each recursing into its children first.
// d/dx of `node`, logging the rule used at each recursive step.
function derive(node, depth) {
stepCount++;
switch (node.t) {
case 'const':
log(depth, 'rule_const', print(node));
return CONST(0);
case 'var':
log(depth, 'rule_var', print(node));
return CONST(1);
case 'add': {
log(depth, 'rule_sum', print(node));
const da = derive(node.a, depth + 1);
const db = derive(node.b, depth + 1);
return ADD(da, db);
}
case 'mul': {
log(depth, 'rule_product', print(node));
const da = derive(node.a, depth + 1);
const db = derive(node.b, depth + 1);
return ADD(MUL(da, node.b), MUL(node.a, db));
}
case 'div': {
log(depth, 'rule_quotient', print(node));
const da = derive(node.a, depth + 1);
const db = derive(node.b, depth + 1);
return DIV(ADD(MUL(da, node.b), MUL(CONST(-1), MUL(node.a, db))), POW(node.b, 2));
}
case 'pow': {
log(depth, 'rule_power', print(node));
const da = derive(node.a, depth + 1);
return MUL(MUL(CONST(node.n), POW(node.a, node.n - 1)), da);
}
case 'sin': {
log(depth, 'rule_chain_sin', print(node));
const da = derive(node.a, depth + 1);
return MUL(COS(node.a), da);
}
case 'cos': {
log(depth, 'rule_chain_cos', print(node));
const da = derive(node.a, depth + 1);
return MUL(MUL(CONST(-1), SIN(node.a)), da);
}
default:
return CONST(0);
}
}
// One human-readable label per differentiation rule, shown in the log panel.
// One human-readable label per differentiation rule, shown in the log panel.
const RULE_LABEL = {
rule_const: 'constant rule: d/dx[c] = 0 for',
rule_var: 'variable rule: d/dx[x] = 1 for',
rule_sum: 'sum rule: d/dx[u + v] = u’ + v’ for',
rule_product: 'product rule: d/dx[u*v] = u’v + uv’ for',
rule_quotient: 'quotient rule: d/dx[u/v] = (u’v - uv’)/v^2 for',
rule_power: 'power rule: d/dx[u^n] = n*u^(n-1)*u’ for',
rule_chain_sin: 'chain rule: d/dx[sin(u)] = cos(u)*u’ for',
rule_chain_cos: 'chain rule: d/dx[cos(u)] = -sin(u)*u’ for'
};
function log(depth, ruleKey, exprText) {
const indent = ' '.repeat(depth);
const line = i18nFmt('%indent%- %rule% %expr%', { indent, rule: RULE_LABEL[ruleKey], expr: exprText });
logLines.push(line);
}
// One human-readable label per sample function, shown in the dropdown.
// One human-readable label per sample function, shown in the dropdown.
const SAMPLE_LABEL = {
sample_poly: 'x^3 + 2x',
sample_product: 'x^2 * sin(x)',
sample_chain: 'sin(x^2)',
sample_quotient: 'x / (x + 1)'
};
const select = document.getElementById('fnSelect');
SAMPLES.forEach((s, i) => {
const opt = document.createElement('option');
opt.value = String(i);
opt.textContent = SAMPLE_LABEL[s.key];
select.appendChild(opt);
});
function renderOriginal() {
const idx = Number(select.value);
const tree = SAMPLES[idx].build();
document.getElementById('original').textContent = 'f(x) = ' + print(tree);
const dEl = document.getElementById('derivative');
dEl.textContent = 'Press "Differentiate" to see the exact result.';
dEl.classList.add('placeholder');
document.getElementById('log').innerHTML = '';
}
function runDifferentiate() {
const idx = Number(select.value);
const tree = SAMPLES[idx].build();
stepCount = 0;
logLines = [];
const raw = derive(tree, 0);
const clean = simplify(raw);
const dEl = document.getElementById('derivative');
dEl.textContent = "f'(x) = " + print(clean);
dEl.classList.remove('placeholder');
document.getElementById('log').innerHTML =
logLines.map((l) => '<div class="step">' + l + '</div>').join('') +
'<div class="rule">' + i18nFmt('Done — %n% recursive step(s), one rule application per tree node.', { n: stepCount }) + '</div>';
console.log('Symbolic derivative computed:', print(tree), '->', print(clean));
}
document.getElementById('diff').addEventListener('click', runDifferentiate);
document.getElementById('reset').addEventListener('click', renderOriginal);
select.addEventListener('change', renderOriginal);
renderOriginal();
<p class="hint">Pick a function below. Symbolic differentiation walks its expression tree from the root down, applying one fixed rule per node type — sum, product, quotient, power, or chain — and rebuilds an exact derivative tree, never a numeric approximation.</p>
<div class="row">
<label for="fnSelect" class="sr-only">Choose a function</label>
<select id="fnSelect"></select>
<button id="diff" type="button">Differentiate</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="panels">
<div class="panel">
<div class="panel-title">Original function</div>
<div id="original" class="expr"></div>
</div>
<div class="panel">
<div class="panel-title">Derivative</div>
<div id="derivative" class="expr placeholder">Press "Differentiate" to see the exact result.</div>
</div>
</div>
<div class="panel-title">Rule log</div>
<div id="log" class="log"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
.row { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; margin-bottom: .7rem; }
select { font: 600 14px system-ui, sans-serif; padding: .4rem .6rem; border-radius: 8px; border: 1px solid #adb1b8; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.panels { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; margin-bottom: .7rem; }
@media (max-width: 520px) { .panels { grid-template-columns: 1fr; } }
.panel-title { font-size: .78rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
color: #6b7280; margin: 0 0 .3rem; }
.expr { font: 700 18px ui-monospace, monospace; background: #e8eef3; border: 1px solid #cdd9e3;
border-radius: 8px; padding: .7rem .8rem; min-height: 1.6em; color: #1d3557; word-break: break-word; }
.expr.placeholder { color: #8a97a6; font-weight: 500; font-family: system-ui, sans-serif; font-size: .9rem; }
.log { font: 13px ui-monospace, monospace; background: #16223a; color: #d7e2f0; border-radius: 8px;
padding: .6rem .7rem; min-height: 2.4em; max-height: 220px; overflow-y: auto; line-height: 1.55; }
.log .step { color: #8fd3a3; }
.log .rule { color: #ffd166; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// The expression is represented as an abstract syntax tree (AST), never as a text string.
// Node constructors: const/var are leaves; add/mul/div/pow/sin/cos are internal nodes with children.
const CONST = (v) => ({ t: 'const', v });
const VAR = () => ({ t: 'var' });
const ADD = (a, b) => ({ t: 'add', a, b });
const MUL = (a, b) => ({ t: 'mul', a, b });
const DIV = (a, b) => ({ t: 'div', a, b });
const POW = (a, n) => ({ t: 'pow', a, n });
const SIN = (a) => ({ t: 'sin', a });
const COS = (a) => ({ t: 'cos', a });
// A handful of built-in sample functions users can pick from.
const SAMPLES = [
{ key: 'sample_poly', build: () => ADD(POW(VAR(), 3), MUL(CONST(2), VAR())) },
{ key: 'sample_product', build: () => MUL(POW(VAR(), 2), SIN(VAR())) },
{ key: 'sample_chain', build: () => SIN(POW(VAR(), 2)) },
{ key: 'sample_quotient', build: () => DIV(VAR(), ADD(VAR(), CONST(1))) }
];
let stepCount = 0;
let logLines = [];
// Turn a tree back into a readable formula string (for display only, not used by derive()) .
function print(node) {
switch (node.t) {
case 'const': return String(node.v);
case 'var': return 'x';
case 'add': return '(' + print(node.a) + ' + ' + print(node.b) + ')';
case 'mul': return '(' + print(node.a) + ' * ' + print(node.b) + ')';
case 'div': return '(' + print(node.a) + ' / ' + print(node.b) + ')';
case 'pow': return print(node.a) + '^' + node.n;
case 'sin': return 'sin(' + print(node.a) + ')';
case 'cos': return 'cos(' + print(node.a) + ')';
default: return '?';
}
}
// Light post-pass: fold 0/1 identities and constant-constant arithmetic so the result stays readable.
function simplify(node) {
if (node.t === 'add') {
const a = simplify(node.a), b = simplify(node.b);
if (a.t === 'const' && a.v === 0) return b;
if (b.t === 'const' && b.v === 0) return a;
if (a.t === 'const' && b.t === 'const') return CONST(a.v + b.v);
return ADD(a, b);
}
if (node.t === 'mul') {
const a = simplify(node.a), b = simplify(node.b);
if ((a.t === 'const' && a.v === 0) || (b.t === 'const' && b.v === 0)) return CONST(0);
if (a.t === 'const' && a.v === 1) return b;
if (b.t === 'const' && b.v === 1) return a;
if (a.t === 'const' && b.t === 'const') return CONST(a.v * b.v);
return MUL(a, b);
}
if (node.t === 'div') {
const a = simplify(node.a), b = simplify(node.b);
if (a.t === 'const' && a.v === 0) return CONST(0);
return DIV(a, b);
}
if (node.t === 'pow') {
const a = simplify(node.a);
if (node.n === 1) return a;
if (node.n === 0) return CONST(1);
return POW(a, node.n);
}
if (node.t === 'sin' || node.t === 'cos') return { t: node.t, a: simplify(node.a) };
return node;
}
// The heart of the algorithm: one case per node type, each recursing into its children first.
// d/dx of `node`, logging the rule used at each recursive step.
function derive(node, depth) {
stepCount++;
switch (node.t) {
case 'const':
log(depth, 'rule_const', print(node));
return CONST(0);
case 'var':
log(depth, 'rule_var', print(node));
return CONST(1);
case 'add': {
log(depth, 'rule_sum', print(node));
const da = derive(node.a, depth + 1);
const db = derive(node.b, depth + 1);
return ADD(da, db);
}
case 'mul': {
log(depth, 'rule_product', print(node));
const da = derive(node.a, depth + 1);
const db = derive(node.b, depth + 1);
return ADD(MUL(da, node.b), MUL(node.a, db));
}
case 'div': {
log(depth, 'rule_quotient', print(node));
const da = derive(node.a, depth + 1);
const db = derive(node.b, depth + 1);
return DIV(ADD(MUL(da, node.b), MUL(CONST(-1), MUL(node.a, db))), POW(node.b, 2));
}
case 'pow': {
log(depth, 'rule_power', print(node));
const da = derive(node.a, depth + 1);
return MUL(MUL(CONST(node.n), POW(node.a, node.n - 1)), da);
}
case 'sin': {
log(depth, 'rule_chain_sin', print(node));
const da = derive(node.a, depth + 1);
return MUL(COS(node.a), da);
}
case 'cos': {
log(depth, 'rule_chain_cos', print(node));
const da = derive(node.a, depth + 1);
return MUL(MUL(CONST(-1), SIN(node.a)), da);
}
default:
return CONST(0);
}
}
// One human-readable label per differentiation rule, shown in the log panel.
// One human-readable label per differentiation rule, shown in the log panel.
const RULE_LABEL = {
rule_const: 'constant rule: d/dx[c] = 0 for',
rule_var: 'variable rule: d/dx[x] = 1 for',
rule_sum: 'sum rule: d/dx[u + v] = u’ + v’ for',
rule_product: 'product rule: d/dx[u*v] = u’v + uv’ for',
rule_quotient: 'quotient rule: d/dx[u/v] = (u’v - uv’)/v^2 for',
rule_power: 'power rule: d/dx[u^n] = n*u^(n-1)*u’ for',
rule_chain_sin: 'chain rule: d/dx[sin(u)] = cos(u)*u’ for',
rule_chain_cos: 'chain rule: d/dx[cos(u)] = -sin(u)*u’ for'
};
function log(depth, ruleKey, exprText) {
const indent = ' '.repeat(depth);
const line = i18nFmt('%indent%- %rule% %expr%', { indent, rule: RULE_LABEL[ruleKey], expr: exprText });
logLines.push(line);
}
// One human-readable label per sample function, shown in the dropdown.
// One human-readable label per sample function, shown in the dropdown.
const SAMPLE_LABEL = {
sample_poly: 'x^3 + 2x',
sample_product: 'x^2 * sin(x)',
sample_chain: 'sin(x^2)',
sample_quotient: 'x / (x + 1)'
};
const select = document.getElementById('fnSelect');
SAMPLES.forEach((s, i) => {
const opt = document.createElement('option');
opt.value = String(i);
opt.textContent = SAMPLE_LABEL[s.key];
select.appendChild(opt);
});
function renderOriginal() {
const idx = Number(select.value);
const tree = SAMPLES[idx].build();
document.getElementById('original').textContent = 'f(x) = ' + print(tree);
const dEl = document.getElementById('derivative');
dEl.textContent = 'Press "Differentiate" to see the exact result.';
dEl.classList.add('placeholder');
document.getElementById('log').innerHTML = '';
}
function runDifferentiate() {
const idx = Number(select.value);
const tree = SAMPLES[idx].build();
stepCount = 0;
logLines = [];
const raw = derive(tree, 0);
const clean = simplify(raw);
const dEl = document.getElementById('derivative');
dEl.textContent = "f'(x) = " + print(clean);
dEl.classList.remove('placeholder');
document.getElementById('log').innerHTML =
logLines.map((l) => '<div class="step">' + l + '</div>').join('') +
'<div class="rule">' + i18nFmt('Done — %n% recursive step(s), one rule application per tree node.', { n: stepCount }) + '</div>';
console.log('Symbolic derivative computed:', print(tree), '->', print(clean));
}
document.getElementById('diff').addEventListener('click', runDifferentiate);
document.getElementById('reset').addEventListener('click', renderOriginal);
select.addEventListener('change', renderOriginal);
renderOriginal();
Comments
Loading comments...