jsroot
Version:
JavaScript ROOT
1,462 lines (1,230 loc) • 108 kB
JavaScript
import { BIT, settings, isArrayProto, isRootCollection, isObject, isFunc, isStr, getMethods,
create, createHistogram, createTGraph, prROOT,
clTObject, clTObjString, clTHashList, clTPolyMarker3D, clTH1, clTH2, clTH3, kNoStats } from './core.mjs';
import { kChar, kShort, kInt, kFloat,
kCharStar, kDouble, kDouble32,
kUChar, kUShort, kUInt,
kLong64, kULong64, kBool, kFloat16,
kOffsetL, kOffsetP, kObject, kAny, kObjectp, kTString,
kStreamer, kStreamLoop, kSTLp, kSTL, kBaseClass, clTBasket,
R__unzip, TBuffer, createStreamerElement, createMemberStreamer } from './io.mjs';
import * as jsroot_math from './base/math.mjs';
// branch types
const kLeafNode = 0, kBaseClassNode = 1, kObjectNode = 2, kClonesNode = 3,
kSTLNode = 4, kClonesMemberNode = 31, kSTLMemberNode = 41,
// branch bits
// kDoNotProcess = BIT(10), // Active bit for branches
// kIsClone = BIT(11), // to indicate a TBranchClones
// kBranchObject = BIT(12), // branch is a TObject*
// kBranchAny = BIT(17), // branch is an object*
// kAutoDelete = BIT(15),
kDoNotUseBufferMap = BIT(22), // If set, at least one of the entry in the branch will use the buffer's map of classname and objects.
clTBranchElement = 'TBranchElement', clTBranchFunc = 'TBranchFunc';
/**
* @summary Class to read data from TTree
*
* @desc Instance of TSelector can be used to access TTree data
*/
class TSelector {
/** @summary constructor */
constructor() {
this._branches = []; // list of branches to read
this._names = []; // list of member names for each branch in tgtobj
this._directs = []; // indication if only branch without any children should be read
this._break = 0;
this.tgtobj = {};
}
/** @summary Add branch to the selector
* @desc Either branch name or branch itself should be specified
* Second parameter defines member name in the tgtobj
* If selector.addBranch('px', 'read_px') is called,
* branch will be read into selector.tgtobj.read_px member
* If second parameter not specified, branch name (here 'px') will be used
* If branch object specified as first parameter and second parameter missing,
* then member like 'br0', 'br1' and so on will be assigned
* @param {string|Object} branch - name of branch (or branch object itself}
* @param {string} [name] - member name in tgtobj where data will be read
* @param {boolean} [direct] - if only branch without any children should be read */
addBranch(branch, name, direct) {
if (!name)
name = isStr(branch) ? branch : `br${this._branches.length}`;
this._branches.push(branch);
this._names.push(name);
this._directs.push(direct);
return this._branches.length - 1;
}
/** @summary returns number of branches used in selector */
numBranches() { return this._branches.length; }
/** @summary returns branch by index used in selector */
getBranch(indx) { return this._branches[indx]; }
/** @summary returns index of branch
* @private */
indexOfBranch(branch) { return this._branches.indexOf(branch); }
/** @summary returns name of branch
* @private */
nameOfBranch(indx) { return this._names[indx]; }
/** @summary function called during TTree processing
* @abstract
* @param {number} progress - current value between 0 and 1 */
ShowProgress(/* progress */) {}
/** @summary call this function to abort processing */
Abort() { this._break = -1111; }
/** @summary function called before start processing
* @abstract
* @param {object} tree - tree object */
Begin(/* tree */) {}
/** @summary function called when next entry extracted from the tree
* @abstract
* @param {number} entry - read entry number */
Process(/* entry */) {}
/** @summary function called at the very end of processing
* @abstract
* @param {boolean} res - true if all data were correctly processed */
Terminate(/* res */) {}
} // class TSelector
// =================================================================
/** @summary Checks array kind
* @desc return 0 when not array
* 1 - when arbitrary array
* 2 - when plain (1-dim) array with same-type content
* @private */
function checkArrayPrototype(arr, check_content) {
if (!isObject(arr)) return 0;
const arr_kind = isArrayProto(Object.prototype.toString.apply(arr));
if (!check_content || (arr_kind !== 1))
return arr_kind;
let typ, plain = true;
for (let k = 0; k < arr.length; ++k) {
const sub = typeof arr[k];
if (!typ)
typ = sub;
if ((sub !== typ) || (isObject(sub) && checkArrayPrototype(arr[k]))) {
plain = false;
break;
}
}
return plain ? 2 : 1;
}
/**
* @summary Class to iterate over array elements
*
* @private
*/
class ArrayIterator {
/** @summary constructor */
constructor(arr, select, tgtobj) {
this.object = arr;
this.value = 0; // value always used in iterator
this.arr = []; // all arrays
this.indx = []; // all indexes
this.cnt = -1; // current index counter
this.tgtobj = tgtobj;
if (isObject(select))
this.select = select; // remember indexes for selection
else
this.select = []; // empty array, undefined for each dimension means iterate over all indexes
}
/** @summary next element */
next() {
let obj, typ, cnt = this.cnt;
if (cnt >= 0) {
if (++this.fastindx < this.fastlimit) {
this.value = this.fastarr[this.fastindx];
return true;
}
while (--cnt >= 0) {
if ((this.select[cnt] === undefined) && (++this.indx[cnt] < this.arr[cnt].length))
break;
}
if (cnt < 0) return false;
}
while (true) {
obj = (cnt < 0) ? this.object : (this.arr[cnt])[this.indx[cnt]];
typ = obj ? typeof obj : 'any';
if (typ === 'object') {
if (obj._typename !== undefined) {
if (isRootCollection(obj)) {
obj = obj.arr;
typ = 'array';
} else
typ = 'any';
} else if (Number.isInteger(obj.length) && (checkArrayPrototype(obj) > 0))
typ = 'array';
else
typ = 'any';
}
if (this.select[cnt + 1] === '$self$') {
this.value = obj;
this.fastindx = this.fastlimit = 0;
this.cnt = cnt + 1;
return true;
}
if ((typ === 'any') && isStr(this.select[cnt + 1])) {
// this is extraction of the member from arbitrary class
this.arr[++cnt] = obj;
this.indx[cnt] = this.select[cnt]; // use member name as index
continue;
}
if ((typ === 'array') && ((obj.length > 0) || (this.select[cnt + 1] === '$size$'))) {
this.arr[++cnt] = obj;
switch (this.select[cnt]) {
case undefined: this.indx[cnt] = 0; break;
case '$last$': this.indx[cnt] = obj.length - 1; break;
case '$size$':
this.value = obj.length;
this.fastindx = this.fastlimit = 0;
this.cnt = cnt;
return true;
default:
if (Number.isInteger(this.select[cnt])) {
this.indx[cnt] = this.select[cnt];
if (this.indx[cnt] < 0) this.indx[cnt] = obj.length - 1;
} else {
// this is compile variable as array index - can be any expression
this.select[cnt].produce(this.tgtobj);
this.indx[cnt] = Math.round(this.select[cnt].get(0));
}
}
} else {
if (cnt < 0)
return false;
this.value = obj;
if (this.select[cnt] === undefined) {
this.fastarr = this.arr[cnt];
this.fastindx = this.indx[cnt];
this.fastlimit = this.fastarr.length;
} else
this.fastindx = this.fastlimit = 0; // no any iteration on that level
this.cnt = cnt;
return true;
}
}
// unreachable code
// return false;
}
/** @summary reset iterator */
reset() {
this.arr = [];
this.indx = [];
delete this.fastarr;
this.cnt = -1;
this.value = 0;
}
} // class ArrayIterator
/** @summary return TStreamerElement associated with the branch - if any
* @desc unfortunately, branch.fID is not number of element in streamer info
* @private */
function findBrachStreamerElement(branch, file) {
if (!branch || !file || (branch._typename !== clTBranchElement) || (branch.fID < 0) || (branch.fStreamerType < 0)) return null;
const s_i = file.findStreamerInfo(branch.fClassName, branch.fClassVersion, branch.fCheckSum),
arr = (s_i && s_i.fElements) ? s_i.fElements.arr : null;
if (!arr) return null;
let match_name = branch.fName,
pos = match_name.indexOf('[');
if (pos > 0) match_name = match_name.slice(0, pos);
pos = match_name.lastIndexOf('.');
if (pos > 0) match_name = match_name.slice(pos + 1);
function match_elem(elem) {
if (!elem) return false;
if (elem.fName !== match_name) return false;
if (elem.fType === branch.fStreamerType) return true;
if ((elem.fType === kBool) && (branch.fStreamerType === kUChar)) return true;
if (((branch.fStreamerType === kSTL) || (branch.fStreamerType === kSTL + kOffsetL) ||
(branch.fStreamerType === kSTLp) || (branch.fStreamerType === kSTLp + kOffsetL)) &&
(elem.fType === kStreamer)) return true;
console.warn(`Should match element ${elem.fType} with branch ${branch.fStreamerType}`);
return false;
}
// first check branch fID - in many cases gut guess
if (match_elem(arr[branch.fID]))
return arr[branch.fID];
for (let k = 0; k < arr.length; ++k) {
if ((k !== branch.fID) && match_elem(arr[k]))
return arr[k];
}
console.error(`Did not found/match element for branch ${branch.fName} class ${branch.fClassName}`);
return null;
}
/** @summary return class name of the object, stored in the branch
* @private */
function getBranchObjectClass(branch, tree, with_clones = false, with_leafs = false) {
if (!branch || (branch._typename !== clTBranchElement)) return '';
if ((branch.fType === kLeafNode) && (branch.fID === -2) && (branch.fStreamerType === -1)) {
// object where all sub-branches will be collected
return branch.fClassName;
}
if (with_clones && branch.fClonesName && ((branch.fType === kClonesNode) || (branch.fType === kSTLNode)))
return branch.fClonesName;
const s_elem = findBrachStreamerElement(branch, tree.$file);
if ((branch.fType === kBaseClassNode) && s_elem && (s_elem.fTypeName === kBaseClass))
return s_elem.fName;
if (branch.fType === kObjectNode) {
if (s_elem && ((s_elem.fType === kObject) || (s_elem.fType === kAny)))
return s_elem.fTypeName;
return clTObject;
}
if ((branch.fType === kLeafNode) && s_elem && with_leafs) {
if ((s_elem.fType === kObject) || (s_elem.fType === kAny))
return s_elem.fTypeName;
if (s_elem.fType === kObjectp)
return s_elem.fTypeName.slice(0, s_elem.fTypeName.length - 1);
}
return '';
}
/** @summary Get branch with specified id
* @desc All sub-branches checked as well
* @return {Object} branch
* @private */
function getTreeBranch(tree, id) {
if (!Number.isInteger(id)) return;
let res, seq = 0;
function scan(obj) {
obj?.fBranches?.arr.forEach(br => {
if (seq++ === id) res = br;
if (!res) scan(br);
});
}
scan(tree);
return res;
}
/** @summary Special branch search
* @desc Name can include extra part, which will be returned in the result
* @param {string} name - name of the branch
* @return {Object} with 'branch' and 'rest' members
* @private */
function findBranchComplex(tree, name, lst = undefined, only_search = false) {
let top_search = false, search = name, res = null;
if (!lst) {
top_search = true;
lst = tree.fBranches;
const pos = search.indexOf('[');
if (pos > 0) search = search.slice(0, pos);
}
if (!lst || (lst.arr.length === 0)) return null;
for (let n = 0; n < lst.arr.length; ++n) {
let brname = lst.arr[n].fName;
if (brname.at(-1) === ']')
brname = brname.slice(0, brname.indexOf('['));
// special case when branch name includes STL map name
if ((search.indexOf(brname) !== 0) && (brname.indexOf('<') > 0)) {
const p1 = brname.indexOf('<'), p2 = brname.lastIndexOf('>');
brname = brname.slice(0, p1) + brname.slice(p2 + 1);
}
if (brname === search) {
res = { branch: lst.arr[n], rest: '' };
break;
}
if (search.indexOf(brname) !== 0)
continue;
// this is a case when branch name is in the begin of the search string
// check where point is
let pnt = brname.length;
if (brname[pnt - 1] === '.') pnt--;
if (search[pnt] !== '.') continue;
res = findBranchComplex(tree, search, lst.arr[n].fBranches);
if (!res) res = findBranchComplex(tree, search.slice(pnt + 1), lst.arr[n].fBranches);
if (!res) res = { branch: lst.arr[n], rest: search.slice(pnt) };
break;
}
if (top_search && !only_search && !res && (search.indexOf('br_') === 0)) {
let p = 3;
while ((p < search.length) && (search[p] >= '0') && (search[p] <= '9')) ++p;
const br = (p > 3) ? getTreeBranch(tree, parseInt(search.slice(3, p))) : null;
if (br) res = { branch: br, rest: search.slice(p) };
}
if (!top_search || !res)
return res;
if (name.length > search.length)
res.rest += name.slice(search.length);
return res;
}
/** @summary Search branch with specified name
* @param {string} name - name of the branch
* @return {Object} found branch
* @private */
function findBranch(tree, name) {
const res = findBranchComplex(tree, name, tree.fBranches, true);
return (!res || res.rest) ? null : res.branch;
}
/** summary Returns number of branches in the TTree
* desc Checks also sub-branches in the branches
* return {number} number of branches
* private
function getNumBranches(tree) {
function count(obj) {
if (!obj?.fBranches) return 0;
let nchld = 0;
obj.fBranches.arr.forEach(sub => { nchld += count(sub); });
return obj.fBranches.arr.length + nchld;
}
return count(tree);
}
*/
/**
* @summary object with single variable in TTree::Draw expression
*
* @private
*/
class TDrawVariable {
/** @summary constructor */
constructor(globals) {
this.globals = globals;
this.code = '';
this.brindex = []; // index of used branches from selector
this.branches = []; // names of branches in target object
this.brarray = []; // array specifier for each branch
this.func = null; // generic function for variable calculation
this.kind = undefined;
this.buf = []; // buffer accumulates temporary values
}
/** @summary Parse variable
* @desc when only_branch specified, its placed in the front of the expression */
parse(tree, selector, code, only_branch, branch_mode) {
const is_start_symbol = symb => {
if ((symb >= 'A') && (symb <= 'Z')) return true;
if ((symb >= 'a') && (symb <= 'z')) return true;
return (symb === '_');
}, is_next_symbol = symb => {
if (is_start_symbol(symb)) return true;
if ((symb >= '0') && (symb <= '9')) return true;
return false;
};
if (!code) code = ''; // should be empty string at least
this.code = (only_branch?.fName ?? '') + code;
let pos = 0, pos2 = 0, br;
while ((pos < code.length) || only_branch) {
let arriter = [];
if (only_branch) {
br = only_branch;
only_branch = undefined;
} else {
// first try to find branch
pos2 = pos;
while ((pos2 < code.length) && (is_next_symbol(code[pos2]) || code[pos2] === '.')) pos2++;
if (code[pos2] === '$') {
let repl = '';
switch (code.slice(pos, pos2)) {
case 'LocalEntry':
case 'Entry': repl = 'arg.$globals.entry'; break;
case 'Entries': repl = 'arg.$globals.entries'; break;
}
if (repl) {
code = code.slice(0, pos) + repl + code.slice(pos2 + 1);
pos += repl.length;
continue;
}
}
br = findBranchComplex(tree, code.slice(pos, pos2));
if (!br) { pos = pos2 + 1; continue; }
// when full id includes branch name, replace only part of extracted expression
if (br.branch && (br.rest !== undefined)) {
pos2 -= br.rest.length;
branch_mode = undefined; // maybe selection of the sub-object done
br = br.branch;
}
// when code ends with the point - means object itself will be accessed
// sometime branch name itself ends with the point
if ((pos2 >= code.length - 1) && (code.at(-1) === '.')) {
arriter.push('$self$');
pos2 = code.length;
}
}
// now extract all levels of iterators
while (pos2 < code.length) {
if ((code[pos2] === '@') && (code.slice(pos2, pos2 + 5) === '@size') && (arriter.length === 0)) {
pos2 += 5;
branch_mode = true;
break;
}
if (code[pos2] === '.') {
// this is object member
const prev = ++pos2;
if ((code[prev] === '@') && (code.slice(prev, prev + 5) === '@size')) {
arriter.push('$size$');
pos2 += 5;
break;
}
if (!is_start_symbol(code[prev])) {
arriter.push('$self$'); // last point means extraction of object itself
break;
}
while ((pos2 < code.length) && is_next_symbol(code[pos2])) pos2++;
// this is looks like function call - do not need to extract member with
if (code[pos2] === '(') { pos2 = prev - 1; break; }
// this is selection of member, but probably we need to activate iterator for ROOT collection
if (arriter.length === 0) {
// TODO: if selected member is simple data type - no need to make other checks - just break here
if ((br.fType === kClonesNode) || (br.fType === kSTLNode))
arriter.push(undefined);
else {
const objclass = getBranchObjectClass(br, tree, false, true);
if (objclass && isRootCollection(null, objclass))
arriter.push(undefined);
}
}
arriter.push(code.slice(prev, pos2));
continue;
}
if (code[pos2] !== '[') break;
// simple []
if (code[pos2 + 1] === ']') { arriter.push(undefined); pos2 += 2; continue; }
const prev = pos2++;
let cnt = 0;
while ((pos2 < code.length) && ((code[pos2] !== ']') || (cnt > 0))) {
if (code[pos2] === '[') cnt++; else if (code[pos2] === ']') cnt--;
pos2++;
}
const sub = code.slice(prev + 1, pos2);
switch (sub) {
case '':
case '$all$': arriter.push(undefined); break;
case '$last$': arriter.push('$last$'); break;
case '$size$': arriter.push('$size$'); break;
case '$first$': arriter.push(0); break;
default:
if (Number.isInteger(parseInt(sub)))
arriter.push(parseInt(sub));
else {
// try to compile code as draw variable
const subvar = new TDrawVariable(this.globals);
if (!subvar.parse(tree, selector, sub)) return false;
arriter.push(subvar);
}
}
pos2++;
}
if (arriter.length === 0)
arriter = undefined;
else if ((arriter.length === 1) && (arriter[0] === undefined))
arriter = true;
let indx = selector.indexOfBranch(br);
if (indx < 0) indx = selector.addBranch(br, undefined, branch_mode);
branch_mode = undefined;
this.brindex.push(indx);
this.branches.push(selector.nameOfBranch(indx));
this.brarray.push(arriter);
// this is simple case of direct usage of the branch
if ((pos === 0) && (pos2 === code.length) && (this.branches.length === 1)) {
this.direct_branch = true; // remember that branch read as is
return true;
}
const replace = 'arg.var' + (this.branches.length - 1);
code = code.slice(0, pos) + replace + code.slice(pos2);
pos += replace.length;
}
// support usage of some standard TMath functions
code = code.replace(/TMath::Exp\(/g, 'Math.exp(')
.replace(/TMath::Abs\(/g, 'Math.abs(')
.replace(/TMath::Prob\(/g, 'arg.$math.Prob(')
.replace(/TMath::Gaus\(/g, 'arg.$math.Gaus(');
this.func = new Function('arg', `return (${code})`);
return true;
}
/** @summary Check if it is dummy variable */
is_dummy() { return (this.branches.length === 0) && !this.func; }
/** @summary Produce variable
* @desc after reading tree branches into the object, calculate variable value */
produce(obj) {
this.length = 1;
this.isarray = false;
if (this.is_dummy()) {
this.value = 1; // used as dummy weight variable
this.kind = 'number';
return;
}
const arg = { $globals: this.globals, $math: jsroot_math }, arrs = [];
let usearrlen = -1;
for (let n = 0; n < this.branches.length; ++n) {
const name = `var${n}`;
arg[name] = obj[this.branches[n]];
// try to check if branch is array and need to be iterated
if (this.brarray[n] === undefined)
this.brarray[n] = (checkArrayPrototype(arg[name]) > 0) || isRootCollection(arg[name]);
// no array - no pain
if (this.brarray[n] === false) continue;
// check if array can be used as is - one dimension and normal values
if ((this.brarray[n] === true) && (checkArrayPrototype(arg[name], true) === 2)) {
// plain array, can be used as is
arrs[n] = arg[name];
} else {
const iter = new ArrayIterator(arg[name], this.brarray[n], obj);
arrs[n] = [];
while (iter.next()) arrs[n].push(iter.value);
}
if ((usearrlen < 0) || (usearrlen < arrs[n].length)) usearrlen = arrs[n].length;
}
if (usearrlen < 0) {
this.value = this.direct_branch ? arg.var0 : this.func(arg);
if (!this.kind) this.kind = typeof this.value;
return;
}
if (usearrlen === 0) {
// empty array - no any histogram should be filled
this.length = 0;
this.value = 0;
return;
}
this.length = usearrlen;
this.isarray = true;
if (this.direct_branch)
this.value = arrs[0]; // just use array
else {
this.value = new Array(usearrlen);
for (let k = 0; k < usearrlen; ++k) {
for (let n = 0; n < this.branches.length; ++n) {
if (arrs[n])
arg[`var${n}`] = arrs[n][k];
}
this.value[k] = this.func(arg);
}
}
if (!this.kind) this.kind = typeof this.value[0];
}
/** @summary Get variable */
get(indx) { return this.isarray ? this.value[indx] : this.value; }
/** @summary Append array to the buffer */
appendArray(tgtarr) { this.buf = this.buf.concat(tgtarr[this.branches[0]]); }
} // class TDrawVariable
/**
* @summary Selector class for TTree::Draw function
*
* @private
*/
class TDrawSelector extends TSelector {
/** @summary constructor */
constructor() {
super();
this.ndim = 0;
this.vars = []; // array of expression variables
this.cut = null; // cut variable
this.hist = null;
this.drawopt = '';
this.hist_name = '$htemp';
this.draw_title = 'Result of TTree::Draw';
this.graph = false;
this.hist_args = []; // arguments for histogram creation
this.arr_limit = 1000; // number of accumulated items before create histogram
this.htype = 'F';
this.monitoring = 0;
this.globals = {}; // object with global parameters, which could be used in any draw expression
this.last_progress = 0;
this.aver_diff = 0;
}
/** @summary Set draw selector callbacks */
setCallback(result_callback, progress_callback) {
this.result_callback = result_callback;
this.progress_callback = progress_callback;
}
/** @summary Parse parameters */
parseParameters(tree, args, expr) {
if (!expr || !isStr(expr))
return '';
// parse parameters which defined at the end as expression;par1name:par1value;par2name:par2value
let pos = expr.lastIndexOf(';');
while (pos >= 0) {
let parname = expr.slice(pos + 1), parvalue;
expr = expr.slice(0, pos);
pos = expr.lastIndexOf(';');
const separ = parname.indexOf(':');
if (separ > 0) { parvalue = parname.slice(separ + 1); parname = parname.slice(0, separ); }
let intvalue = parseInt(parvalue);
if (!parvalue || !Number.isInteger(intvalue))
intvalue = undefined;
switch (parname) {
case 'elist':
if ((parvalue.at(0) === '[') && (parvalue.at(-1) === ']')) {
parvalue = parvalue.slice(1, parvalue.length - 1).replaceAll(/\s/g, '');
args.elist = [];
let p = 0, last_v = -1;
const getInt = () => {
const p0 = p;
while ((p < parvalue.length) && (parvalue.charCodeAt(p) >= 48) && (parvalue.charCodeAt(p) < 58))
p++;
return parseInt(parvalue.slice(p0, p));
};
while (p < parvalue.length) {
const v1 = getInt();
if (v1 <= last_v) {
console.log('position', p);
throw Error(`Wrong entry id ${v1} in elist last ${last_v}`);
}
let v2 = v1;
if (parvalue[p] === '.' && parvalue[p + 1] === '.') {
p += 2;
v2 = getInt();
if (v2 < v1)
throw Error(`Wrong entry id ${v2} in range ${v1}`);
}
if (parvalue[p] === ',' || p === parvalue.length) {
for (let v = v1; v <= v2; ++v) {
args.elist.push(v);
last_v = v;
}
p++;
} else
throw Error('Wrong syntax for elist');
}
}
break;
case 'entries':
case 'num':
case 'numentries':
if (parvalue === 'all')
args.numentries = tree.fEntries;
else if (parvalue === 'half')
args.numentries = Math.round(tree.fEntries / 2);
else if (intvalue !== undefined)
args.numentries = intvalue;
break;
case 'first':
if (intvalue !== undefined)
args.firstentry = intvalue;
break;
case 'nmatch':
if (intvalue !== undefined)
this.nmatch = intvalue;
break;
case 'mon':
case 'monitor':
args.monitoring = (intvalue !== undefined) ? intvalue : 5000;
break;
case 'player':
args.player = true;
break;
case 'dump':
args.dump = true;
break;
case 'staged':
args.staged = true;
break;
case 'maxseg':
case 'maxrange':
if (intvalue)
tree.$file.fMaxRanges = intvalue;
break;
case 'accum':
if (intvalue)
this.arr_limit = intvalue;
break;
case 'htype':
if (parvalue && (parvalue.length === 1)) {
this.htype = parvalue.toUpperCase();
if (['C', 'S', 'I', 'F', 'L', 'D'].indexOf(this.htype) < 0)
this.htype = 'F';
}
break;
case 'hbins':
this.hist_nbins = parseInt(parvalue);
if (!Number.isInteger(this.hist_nbins) || (this.hist_nbins <= 3))
delete this.hist_nbins;
else
this.want_hist = true;
break;
case 'drawopt':
args.drawopt = parvalue;
break;
case 'graph':
args.graph = intvalue || true;
break;
}
}
pos = expr.lastIndexOf('>>');
if (pos >= 0) {
let harg = expr.slice(pos + 2).trim();
expr = expr.slice(0, pos).trim();
pos = harg.indexOf('(');
if (pos > 0) {
this.hist_name = harg.slice(0, pos);
harg = harg.slice(pos);
}
if (harg === 'dump')
args.dump = true;
else if (harg === 'elist')
args.dump_entries = true;
else if (harg.indexOf('Graph') === 0)
args.graph = true;
else if (pos < 0) {
this.want_hist = true;
this.hist_name = harg;
} else if ((harg[0] === '(') && (harg.at(-1) === ')')) {
this.want_hist = true;
harg = harg.slice(1, harg.length - 1).split(',');
let isok = true;
for (let n = 0; n < harg.length; ++n) {
harg[n] = (n % 3 === 0) ? parseInt(harg[n]) : parseFloat(harg[n]);
if (!Number.isFinite(harg[n])) isok = false;
}
if (isok)
this.hist_args = harg;
}
}
if (args.dump) {
this.dump_values = true;
args.reallocate_objects = true;
if (args.numentries === undefined) {
args.numentries = 10;
args._dflt_entries = true;
}
}
return expr;
}
/** @summary Create draw expression for N-dim with cut */
createDrawExpression(tree, names, cut, args) {
if (args.dump && names.length === 1 && names[0] === 'Entry$') {
args.dump_entries = true;
args.dump = false;
}
if (args.dump_entries) {
this.dump_entries = true;
this.hist = [];
if (args._dflt_entries) {
delete args._dflt_entries;
delete args.numentries;
}
}
let is_direct = !cut && !this.dump_entries;
this.ndim = names.length;
for (let n = 0; n < this.ndim; ++n) {
this.vars[n] = new TDrawVariable(this.globals);
if (!this.vars[n].parse(tree, this, names[n])) return false;
if (!this.vars[n].direct_branch) is_direct = false;
}
this.cut = new TDrawVariable(this.globals);
if (cut && !this.cut.parse(tree, this, cut))
return false;
if (!this.numBranches()) {
console.warn('no any branch is selected');
return false;
}
if (is_direct)
this.ProcessArrays = this.ProcessArraysFunc;
this.monitoring = args.monitoring;
// force TPolyMarker3D drawing for 3D case
if ((this.ndim === 3) && !this.want_hist && !args.dump)
args.graph = true;
this.graph = args.graph;
if (args.drawopt !== undefined)
this.drawopt = args.drawopt;
else
args.drawopt = this.drawopt = this.graph ? 'P' : '';
return true;
}
/** @summary Parse draw expression */
parseDrawExpression(tree, args) {
// parse complete expression
let expr = this.parseParameters(tree, args, args.expr), cut = '';
// parse option for histogram creation
this.draw_title = `drawing '${expr}' from ${tree.fName}`;
let pos;
if (args.cut)
cut = args.cut;
else {
pos = expr.replace(/TMath::/g, 'TMath__').lastIndexOf('::'); // avoid confusion due-to :: in the namespace
if (pos >= 0) {
cut = expr.slice(pos + 2).trim();
expr = expr.slice(0, pos).trim();
}
}
args.parse_expr = expr;
args.parse_cut = cut;
// let names = expr.split(':'); // to allow usage of ? operator, we need to handle : as well
let names = [], nbr1 = 0, nbr2 = 0, prev = 0;
for (pos = 0; pos < expr.length; ++pos) {
switch (expr[pos]) {
case '(': nbr1++; break;
case ')': nbr1--; break;
case '[': nbr2++; break;
case ']': nbr2--; break;
case ':':
if (expr[pos + 1] === ':') { pos++; continue; }
if (!nbr1 && !nbr2 && (pos > prev)) names.push(expr.slice(prev, pos));
prev = pos + 1;
break;
}
}
if (!nbr1 && !nbr2 && (pos > prev))
names.push(expr.slice(prev, pos));
if (args.staged) {
args.staged_names = names;
names = ['Entry$'];
args.dump_entries = true;
} else if (cut && args.dump_entries)
names = ['Entry$'];
else if ((names.length < 1) || (names.length > 3))
return false;
return this.createDrawExpression(tree, names, cut, args);
}
/** @summary Draw only specified branch */
drawOnlyBranch(tree, branch, expr, args) {
this.ndim = 1;
if (expr.indexOf('dump') === 0)
expr = ';' + expr;
expr = this.parseParameters(tree, args, expr);
this.monitoring = args.monitoring;
if (args.dump) {
this.dump_values = true;
args.reallocate_objects = true;
}
if (this.dump_values) {
this.hist = []; // array of dump objects
this.leaf = args.leaf;
// branch object remains, therefore we need to copy fields to see them all
this.copy_fields = ((args.branch.fLeaves && (args.branch.fLeaves.arr.length > 1)) ||
(args.branch.fBranches && (args.branch.fBranches.arr.length > 0))) && !args.leaf;
this.addBranch(branch, 'br0', args.direct_branch); // add branch
this.Process = this.ProcessDump;
return true;
}
this.vars[0] = new TDrawVariable(this.globals);
if (!this.vars[0].parse(tree, this, expr, branch, args.direct_branch))
return false;
this.draw_title = `drawing branch ${branch.fName} ${expr?' expr:'+expr:''} from ${tree.fName}`;
this.cut = new TDrawVariable(this.globals);
if (this.vars[0].direct_branch)
this.ProcessArrays = this.ProcessArraysFunc;
return true;
}
/** @summary Begin processing */
Begin(tree) {
this.globals.entries = tree.fEntries;
if (this.monitoring)
this.lasttm = new Date().getTime();
}
/** @summary Show progress */
ShowProgress(/* value */) {}
/** @summary Get bins for bits histogram */
getBitsBins(nbits, res) {
res.nbins = res.max = nbits;
res.fLabels = create(clTHashList);
for (let k = 0; k < nbits; ++k) {
const s = create(clTObjString);
s.fString = k.toString();
s.fUniqueID = k + 1;
res.fLabels.Add(s);
}
return res;
}
/** @summary Get min.max bins */
getMinMaxBins(axisid, nbins) {
const res = { min: 0, max: 0, nbins, k: 1, fLabels: null, title: '' };
if (axisid >= this.ndim) return res;
const arr = this.vars[axisid].buf;
res.title = this.vars[axisid].code || '';
if (this.vars[axisid].kind === 'object') {
// this is any object type
let typename, similar = true, maxbits = 8;
for (let k = 0; k < arr.length; ++k) {
if (!arr[k]) continue;
if (!typename) typename = arr[k]._typename;
if (typename !== arr[k]._typename) similar = false; // check all object types
if (arr[k].fNbits) maxbits = Math.max(maxbits, arr[k].fNbits + 1);
}
if (typename && similar) {
if ((typename === 'TBits') && (axisid === 0)) {
this.fill1DHistogram = this.fillTBitsHistogram;
if (maxbits % 8) maxbits = (maxbits & 0xfff0) + 8;
if ((this.hist_name === 'bits') && (this.hist_args.length === 1) && this.hist_args[0])
maxbits = this.hist_args[0];
return this.getBitsBins(maxbits, res);
}
}
}
if (this.vars[axisid].kind === 'string') {
res.lbls = []; // all labels
for (let k = 0; k < arr.length; ++k) {
if (res.lbls.indexOf(arr[k]) < 0)
res.lbls.push(arr[k]);
}
res.lbls.sort();
res.max = res.nbins = res.lbls.length;
res.fLabels = create(clTHashList);
for (let k = 0; k < res.lbls.length; ++k) {
const s = create(clTObjString);
s.fString = res.lbls[k];
s.fUniqueID = k + 1;
if (s.fString === '') s.fString = '<empty>';
res.fLabels.Add(s);
}
} else if ((axisid === 0) && (this.hist_name === 'bits') && (this.hist_args.length <= 1)) {
this.fill1DHistogram = this.fillBitsHistogram;
return this.getBitsBins(this.hist_args[0] || 32, res);
} else if (axisid * 3 + 2 < this.hist_args.length) {
res.nbins = this.hist_args[axisid * 3];
res.min = this.hist_args[axisid * 3 + 1];
res.max = this.hist_args[axisid * 3 + 2];
} else {
let is_any = false;
for (let i = 1; i < arr.length; ++i) {
const v = arr[i];
if (!Number.isFinite(v)) continue;
if (is_any) {
res.min = Math.min(res.min, v);
res.max = Math.max(res.max, v);
} else {
res.min = res.max = v;
is_any = true;
}
}
if (!is_any) { res.min = 0; res.max = 1; }
if (this.hist_nbins)
nbins = res.nbins = this.hist_nbins;
res.isinteger = (Math.round(res.min) === res.min) && (Math.round(res.max) === res.max);
if (res.isinteger) {
for (let k = 0; k < arr.length; ++k)
if (arr[k] !== Math.round(arr[k])) { res.isinteger = false; break; }
}
if (res.isinteger) {
res.min = Math.round(res.min);
res.max = Math.round(res.max);
if (res.max - res.min < nbins * 5) {
res.min -= 1;
res.max += 2;
res.nbins = Math.round(res.max - res.min);
} else {
const range = (res.max - res.min + 2);
let step = Math.floor(range / nbins);
while (step * nbins < range) step++;
res.max = res.min + nbins * step;
}
} else if (res.min >= res.max) {
res.max = res.min;
if (Math.abs(res.min) < 100) { res.min -= 1; res.max += 1; } else
if (res.min > 0) { res.min *= 0.9; res.max *= 1.1; } else { res.min *= 1.1; res.max *= 0.9; }
} else
res.max += (res.max - res.min) / res.nbins;
}
res.k = res.nbins / (res.max - res.min);
res.GetBin = function(value) {
const bin = this.lbls?.indexOf(value) ?? Number.isFinite(value) ? Math.floor((value - this.min) * this.k) : this.nbins + 1;
return bin < 0 ? 0 : ((bin > this.nbins) ? this.nbins + 1 : bin + 1);
};
return res;
}
/** @summary Create histogram which matches value in dimensions */
createHistogram(nbins, set_hist = false) {
if (!nbins) nbins = 20;
const x = this.getMinMaxBins(0, nbins),
y = this.getMinMaxBins(1, nbins),
z = this.getMinMaxBins(2, nbins);
let hist = null;
switch (this.ndim) {
case 1: hist = createHistogram(clTH1 + this.htype, x.nbins); break;
case 2: hist = createHistogram(clTH2 + this.htype, x.nbins, y.nbins); break;
case 3: hist = createHistogram(clTH3 + this.htype, x.nbins, y.nbins, z.nbins); break;
}
hist.fXaxis.fTitle = x.title;
hist.fXaxis.fXmin = x.min;
hist.fXaxis.fXmax = x.max;
hist.fXaxis.fLabels = x.fLabels;
if (this.ndim > 1) hist.fYaxis.fTitle = y.title;
hist.fYaxis.fXmin = y.min;
hist.fYaxis.fXmax = y.max;
hist.fYaxis.fLabels = y.fLabels;
if (this.ndim > 2) hist.fZaxis.fTitle = z.title;
hist.fZaxis.fXmin = z.min;
hist.fZaxis.fXmax = z.max;
hist.fZaxis.fLabels = z.fLabels;
hist.fName = this.hist_name;
hist.fTitle = this.draw_title;
hist.fOption = this.drawopt;
hist.$custom_stat = (this.hist_name === '$htemp') ? 111110 : 111111;
if (set_hist) {
this.hist = hist;
this.x = x;
this.y = y;
this.z = z;
} else
hist.fBits |= kNoStats;
return hist;
}
/** @summary Create output object - histogram, graph, dump array */
createOutputObject() {
if (this.hist || !this.vars[0].buf)
return;
if (this.dump_values) {
// just create array where dumped values will be collected
this.hist = [];
// reassign fill method
this.fill1DHistogram = this.fill2DHistogram = this.fill3DHistogram = this.dumpValues;
} else if (this.graph) {
const N = this.vars[0].buf.length;
let res = null;
if (this.ndim === 1) {
// A 1-dimensional graph will just have the x axis as an index
res = createTGraph(N, Array.from(Array(N).keys()), this.vars[0].buf);
res.fName = 'Graph';
res.fTitle = this.draw_title;
} else if (this.ndim === 2) {
res = createTGraph(N, this.vars[0].buf, this.vars[1].buf);
res.fName = 'Graph';
res.fTitle = this.draw_title;
delete this.vars[1].buf;
} else if (this.ndim === 3) {
res = create(clTPolyMarker3D);
res.fN = N;
res.fLastPoint = N - 1;
const arr = new Array(N*3);
for (let k = 0; k< N; ++k) {
arr[k*3] = this.vars[0].buf[k];
arr[k*3+1] = this.vars[1].buf[k];
arr[k*3+2] = this.vars[2].buf[k];
}
res.fP = arr;
res.$hist = this.createHistogram(10);
delete this.vars[1].buf;
delete this.vars[2].buf;
res.fName = 'Points';
}
this.hist = res;
} else {
const nbins = [200, 50, 20];
this.createHistogram(nbins[this.ndim], true);
}
const var0 = this.vars[0].buf, cut = this.cut.buf, len = var0.length;
if (!this.graph) {
switch (this.ndim) {
case 1: {
for (let n = 0; n < len; ++n)
this.fill1DHistogram(var0[n], cut ? cut[n] : 1);
break;
}
case 2: {
const var1 = this.vars[1].buf;
for (let n = 0; n < len; ++n)
this.fill2DHistogram(var0[n], var1[n], cut ? cut[n] : 1);
delete this.vars[1].buf;
break;
}
case 3: {
const var1 = this.vars[1].buf, var2 = this.vars[2].buf;
for (let n = 0; n < len; ++n)
this.fill3DHistogram(var0[n], var1[n], var2[n], cut ? cut[n] : 1);
delete this.vars[1].buf;
delete this.vars[2].buf;
break;
}
}
}
delete this.vars[0].buf;
delete this.cut.buf;
}
/** @summary Fill TBits histogram */
fillTBitsHistogram(xvalue, weight) {
if (!weight || !xvalue || !xvalue.fNbits || !xvalue.fAllBits) return;
const sz = Math.min(xvalue.fNbits + 1, xvalue.fNbytes * 8);
for (let bit = 0, mask = 1, b = 0; bit < sz; ++bit) {
if (xvalue.fAllBits[b] && mask) {
if (bit <= this.x.nbins)
this.hist.fArray[bit + 1] += weight;
else
this.hist.fArray[this.x.nbins + 1] += weight;
}
mask *= 2;
if (mask >= 0x100) { mask = 1; ++b; }
}
}
/** @summary Fill bits histogram */
fillBitsHistogram(xvalue, weight) {
if (!weight) return;
for (let bit = 0, mask = 1; bit < this.x.nbins; ++bit) {
if (xvalue & mask) this.hist.fArray[bit + 1] += weight;
mask *= 2;
}
}
/** @summary Fill 1D histogram */
fill1DHistogram(xvalue, weight) {
const bin = this.x.GetBin(xvalue);
this.hist.fArray[bin] += weight;
if (!this.x.lbls && Number.isFinite(xvalue)) {
this.hist.fTsumw += weight;
this.hist.fTsumwx += weight * xvalue;
this.hist.fTsumwx2 += weight * xvalue * xvalue;
}
}
/** @summary Fill 2D histogram */
fill2DHistogram(xvalue, yvalue, weight) {
const xbin = this.x.GetBin(xvalue),
ybin = this.y.GetBin(yvalue);
this.hist.fArray[xbin + (this.x.nbins + 2) * ybin] += weight;
if (!this.x.lbls && !this.y.lbls && Number.isFinite(xvalue) && Number.isFinite(yvalue)) {
this.hist.fTsumw += weight;
this.hist.fTsumwx += weight * xvalue;
this.hist.fTsumwy += weight * yvalue;
this.hist.fTsumwx2 += weight * xvalue * xvalue;
this.hist.fTsumwxy += weight * xvalue * yvalue;
this.hist.fTsumwy2 += weight * yvalue * yvalue;
}
}
/** @summary Fill 3D histogram */
fill3DHistogram(xvalue, yvalue, zvalue, weight) {
const xbin = this.x.GetBin(xvalue),
ybin = this.y.GetBin(yvalue),
zbin = this.z.GetBin(zvalue);
this.hist.fArray[xbin + (this.x.nbins + 2) * (ybin + (this.y.nbins + 2) * zbin)] += weight;
if (!this.x.lbls && !this.y.lbls && !this.z.lbls && Number.isFinite(xvalue) && Number.isFinite(yvalue) && Number.isFinite(zvalue)) {
this.hist.fTsumw += weight;
this.hist.fTsumwx += weight * xvalue;
this.hist.fTsumwy += weight * yvalue;
this.hist.fTsumwz += weight * zvalue;
this.hist.fTsumwx2 += weight * xvalue * xvalue;
this.hist.fTsumwy2 += weight * yvalue * yvalue;
this.hist.fTsumwz2 += weight * zvalue * zvalue;
this.hist.fTsumwxy += weight * xvalue * yvalue;
this.hist.fTsumwxz += weight * xvalue * zvalue;
this.hist.fTsumwyz += weight * yvalue * zvalue;
}
}
/** @summary Dump values */
dumpValues(v1, v2, v3, v4) {
let obj;
switch (this.ndim) {
case 1: obj = { x: v1, weight: v2 }; break;
case 2: obj = { x: v1, y: v2, weight: v3 }; break;
case 3: obj = { x: v1, y: v2, z: v3, weight: v4 }; break;
}
if (this.cut.is_dummy()) {
if (this.ndim === 1)
obj = v1;
else
delete obj.weight;
}
this.hist.push(obj);
}
/** @summary function used when all branches can be read as array
* @desc most typical usage - histogram filling of single branch */
ProcessArraysFunc(/* entry */) {
if (this.arr_limit || this.graph) {
const var0 = this.vars[0],
var1 = this.vars[1],
var2 = this.vars[2],
len = this.tgtarr.br0.length;
if ((var0.buf.length === 0) && (len >= this.arr_limit) && !this.graph) {
// special use case - first array large enough to create histogram directly base on it
var0.buf = this.tgtarr.br0;
if (var1) var1.buf = this.tgtarr.br1;
if (var2) var2.buf = this.tgtarr.br2;
} else {
for (let k = 0; k < len; ++k) {
var0.buf.push(this.tgtarr.br0[k]);
if (var1) var1.buf.push(this.tgtarr.br1[k]);
if (var2) var2.buf.push(this.tgtarr.br2[k]);
}
}
var0.kind = 'number';
if (var1) var1.kind = 'number';
if (var2) var2.kind = 'number';
this.cut.buf = null; // do not create buffer for cuts
if (!this.graph && (var0.buf.length >= this.arr_limit)) {
this.createOutputObject();
this.arr_limit = 0;
}
} else {
const br0 = this.tgtarr.br0, len = br0.length;
switch (this.ndim) {
case 1: {
for (let k = 0; k < len; ++k)
this.fill1DHistogram(br0[k], 1);
break;
}
case 2: {
c