From 351355ffa79065083c7257f640b0cd609a863198 Mon Sep 17 00:00:00 2001 From: Prafyl <86287866+Prafyl@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:02:48 +0545 Subject: [PATCH 1/2] Support more TMath functions in tree draw expressions Only TMath::Exp, Abs, Prob and Gaus were mapped, so expressions like TMath::Sqrt(x) or TMath::Pi() failed with a syntax error on '::'. Map the common TMath functions to their Math equivalents and add TMath::Pi() and TMath::E(). --- modules/tree.mjs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/modules/tree.mjs b/modules/tree.mjs index af556d906..f1cc5a2d5 100644 --- a/modules/tree.mjs +++ b/modules/tree.mjs @@ -467,6 +467,15 @@ function getNumBranches(tree) { } */ +/** @summary JavaScript equivalents of TMath functions, used in draw expressions + * @private */ +const tmathFunctions = { + Abs: 'Math.abs', Sqrt: 'Math.sqrt', Exp: 'Math.exp', Log: 'Math.log', Log10: 'Math.log10', + Sin: 'Math.sin', Cos: 'Math.cos', Tan: 'Math.tan', ASin: 'Math.asin', ACos: 'Math.acos', ATan: 'Math.atan', + ATan2: 'Math.atan2', SinH: 'Math.sinh', CosH: 'Math.cosh', TanH: 'Math.tanh', + Ceil: 'Math.ceil', Floor: 'Math.floor', Power: 'Math.pow', Max: 'Math.max', Min: 'Math.min' +}; + /** * @summary object with single variable in TTree::Draw expression * @@ -664,11 +673,12 @@ class TDrawVariable { pos += replace.length; } - // support usage of some standard TMath functions - code = code.replace(/TMath::Exp\(/g, 'Math.exp(') - .replace(/TMath::Abs\(/g, 'Math.abs(') + // support usage of standard TMath functions + code = code.replace(/TMath::Pi\(\)/g, 'Math.PI') + .replace(/TMath::E\(\)/g, 'Math.E') .replace(/TMath::Prob\(/g, 'arg.$math.Prob(') - .replace(/TMath::Gaus\(/g, 'arg.$math.Gaus('); + .replace(/TMath::Gaus\(/g, 'arg.$math.Gaus(') + .replace(/TMath::(\w+)\(/g, (match, name) => (tmathFunctions[name] ? `${tmathFunctions[name]}(` : match)); this.func = new Function('arg', `return (${code})`); From 9b8bceeca4f5252f9d8a3ec6d2ae5eea5109dbda Mon Sep 17 00:00:00 2001 From: Prafyl <86287866+Prafyl@users.noreply.github.com> Date: Wed, 14 Oct 2026 23:03:04 +0545 Subject: [PATCH 2/2] Support ^ power operator in tree draw expressions In ROOT '^' is the power operator in TTree::Draw expressions, in JS it is bitwise XOR, so expressions like "x^2" silently gave wrong values. Rewrite '^' to '**' before compiling the expression. Operands are taken as names, numbers, brackets or calls, so "-x^2" is -(x**2) and "a^b^c" is evaluated from the left, as in ROOT. --- modules/tree.mjs | 63 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/modules/tree.mjs b/modules/tree.mjs index f1cc5a2d5..27fd70eaa 100644 --- a/modules/tree.mjs +++ b/modules/tree.mjs @@ -476,6 +476,66 @@ const tmathFunctions = { Ceil: 'Math.ceil', Floor: 'Math.floor', Power: 'Math.pow', Max: 'Math.max', Min: 'Math.min' }; +/** @summary Replace ROOT power operator 'a^b' by JavaScript 'a**b' + * @desc Operands are taken as primary expressions (names, numbers, brackets, calls), so unary minus + * stays outside of the power: '-x^2' is '-(x**2)', as in ROOT. Operators are processed from the left, + * so 'a^b^c' is '(a**b)**c', as ROOT evaluates it. + * @private */ +function convertPowerOperator(code) { + const isNameChar = c => (c !== undefined) && /[\w$.]/.test(c), + skipBrackets = (str, pos, open, close) => { // pos points to opening bracket, returns index after closing one + let cnt = 0; + for (let p = pos; p < str.length; ++p) { + if (str[p] === open) cnt++; + else if (str[p] === close && --cnt === 0) return p + 1; + } + return str.length; + }; + + let pos; + while ((pos = code.indexOf('^')) >= 0) { + // left operand: find its start + let start = pos - 1; + while ((start >= 0) && (code[start] === ' ')) start--; + if ((start < 0) || (!isNameChar(code[start]) && (code[start] !== ')') && (code[start] !== ']'))) + break; // no left operand + if (code[start] === ')' || code[start] === ']') { + // walk back to matching bracket + const close = code[start], open = (close === ')') ? '(' : '['; + let cnt = 0; + for (; start >= 0; start--) { + if (code[start] === close) cnt++; + else if (code[start] === open && --cnt === 0) break; + } + if (start < 0) + break; + } + while ((start > 0) && isNameChar(code[start - 1])) + start--; + + // right operand: optional unary signs, then a primary expression + let end = pos + 1; + while ((end < code.length) && (code[end] === ' ')) + end++; + while ((end < code.length) && ('+-!~'.indexOf(code[end]) >= 0)) + end++; + if (code[end] === '(') + end = skipBrackets(code, end, '(', ')'); + else { + const m = /^(?:\d+\.?\d*(?:[eE][+-]?\d+)?|\.\d+(?:[eE][+-]?\d+)?|[A-Za-z_$][\w$.]*)/.exec(code.slice(end)); + if (!m) + break; + end += m[0].length; + } + while ((end < code.length) && ((code[end] === '(') || (code[end] === '['))) + end = (code[end] === '(') ? skipBrackets(code, end, '(', ')') : skipBrackets(code, end, '[', ']'); + + const base = code.slice(start, pos).trim(), exp = code.slice(pos + 1, end).trim(); + code = `${code.slice(0, start)}((${base})**(${exp}))${code.slice(end)}`; + } + return code; +} + /** * @summary object with single variable in TTree::Draw expression * @@ -680,6 +740,9 @@ class TDrawVariable { .replace(/TMath::Gaus\(/g, 'arg.$math.Gaus(') .replace(/TMath::(\w+)\(/g, (match, name) => (tmathFunctions[name] ? `${tmathFunctions[name]}(` : match)); + // ROOT uses '^' as power operator, in JavaScript it is bitwise XOR + code = convertPowerOperator(code); + this.func = new Function('arg', `return (${code})`); return true;