From 9196549438746fef1c3827847765d0a145493be0 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Thu, 27 Nov 2025 07:52:53 +0100 Subject: [PATCH 1/7] Let configure alternative draw function After invoking: ``` addDrawFunc({ name: '*', func: (dom,obj,opt) => console.log('drawing', obj._typename) }); ``` Provided function will be invoked every time when some object is drawn If function returns value - this will be the only action then Allows catch and redefine drawing of any object in jsroot. But TTree::Draw will be performed and only produced histogram drawing will be alternated --- modules/draw.mjs | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/modules/draw.mjs b/modules/draw.mjs index d2b793423..1417ae14b 100644 --- a/modules/draw.mjs +++ b/modules/draw.mjs @@ -130,14 +130,14 @@ const drawFuncs = { lst: [ { name: 'kind:Command', icon: 'img_execute', execute: true }, { name: 'TFolder', icon: 'img_folder', icon2: 'img_folderopen', noinspect: true, get_expand: () => import_h().then(h => h.folderHierarchy) }, { name: 'TTask', icon: 'img_task', get_expand: () => import_h().then(h => h.taskHierarchy), for_derived: true }, - { name: clTTree, icon: 'img_tree', get_expand: () => import('./tree.mjs').then(h => h.treeHierarchy), draw: () => import_tree().then(h => h.drawTree), dflt: 'expand', opt: 'player;testio', shift: kInspect, pm: true }, + { name: clTTree, icon: 'img_tree', get_expand: () => import('./tree.mjs').then(h => h.treeHierarchy), draw: () => import_tree().then(h => h.drawTree), dflt: 'expand', opt: 'player;testio', shift: kInspect, pm: true, transform: true }, { name: 'TNtuple', sameas: clTTree }, { name: 'TNtupleD', sameas: clTTree }, - { name: clTBranchFunc, icon: 'img_leaf_method', draw: () => import_tree().then(h => h.drawTree), opt: ';dump', noinspect: true }, - { name: /^TBranch/, icon: 'img_branch', draw: () => import_tree().then(h => h.drawTree), dflt: 'expand', opt: ';dump', ctrl: 'dump', shift: kInspect, ignore_online: true, always_draw: true }, - { name: /^TLeaf/, icon: 'img_leaf', noexpand: true, draw: () => import_tree().then(h => h.drawTree), opt: ';dump', ctrl: 'dump', ignore_online: true, always_draw: true }, - { name: 'ROOT::RNTuple', icon: 'img_tree', get_expand: () => import('./rntuple.mjs').then(h => h.tupleHierarchy), draw: () => import('./draw/RNTuple.mjs').then(h => h.drawRNTuple), dflt: 'expand', pm: true }, - { name: 'ROOT::RNTupleField', icon: 'img_leaf', draw: () => import('./draw/RNTuple.mjs').then(h => h.drawRNTuple), opt: ';dump', ctrl: 'dump', shift: kInspect, ignore_online: true, always_draw: true }, + { name: clTBranchFunc, icon: 'img_leaf_method', draw: () => import_tree().then(h => h.drawTree), opt: ';dump', noinspect: true, transform: true }, + { name: /^TBranch/, icon: 'img_branch', draw: () => import_tree().then(h => h.drawTree), dflt: 'expand', opt: ';dump', ctrl: 'dump', shift: kInspect, ignore_online: true, always_draw: true, transform: true }, + { name: /^TLeaf/, icon: 'img_leaf', noexpand: true, draw: () => import_tree().then(h => h.drawTree), opt: ';dump', ctrl: 'dump', ignore_online: true, always_draw: true, transform: true }, + { name: 'ROOT::RNTuple', icon: 'img_tree', get_expand: () => import('./rntuple.mjs').then(h => h.tupleHierarchy), draw: () => import('./draw/RNTuple.mjs').then(h => h.drawRNTuple), dflt: 'expand', pm: true, transform: true }, + { name: 'ROOT::RNTupleField', icon: 'img_leaf', draw: () => import('./draw/RNTuple.mjs').then(h => h.drawRNTuple), opt: ';dump', ctrl: 'dump', shift: kInspect, ignore_online: true, always_draw: true, transform: true }, { name: clTList, icon: 'img_list', draw: () => import_h().then(h => h.drawList), get_expand: () => import_h().then(h => h.listHierarchy), dflt: 'expand' }, { name: clTHashList, sameas: clTList }, { name: clTObjArray, sameas: clTList }, @@ -165,13 +165,12 @@ const drawFuncs = { lst: [ { name: nsREX + 'RFont', icon: 'img_text', draw: () => import_v7().then(h => h.drawRFont), opt: '', direct: 'v7', csstype: 'font' }, { name: nsREX + 'RAxisDrawable', icon: 'img_frame', draw: () => import_v7().then(h => h.drawRAxis), opt: '' }, { name: nsREX + 'RTreeMapPainter', class: () => import('./draw/RTreeMapPainter.mjs').then(h => h.RTreeMapPainter), opt: '' } -], cache: {} }; +], cache: {}, alt: null }; /** @summary Register draw function for the class - * @desc List of supported draw options could be provided, separated with ';' * @param {object} args - arguments - * @param {string|regexp} args.name - class name or regexp pattern + * @param {string|regexp} args.name - class name or regexp pattern or '*' * @param {function} [args.func] - draw function * @param {function} [args.draw] - async function to load draw function * @param {function} [args.class] - async function to load painter class with static draw function @@ -179,9 +178,15 @@ const drawFuncs = { lst: [ * @param {string} [args.opt] - list of supported draw options (separated with semicolon) like 'col;scat;' * @param {string} [args.icon] - icon name shown for the class in hierarchy browser * @param {string} [args.draw_field] - draw only data member from object, like fHistogram + * @desc List of supported draw options could be provided, separated with ';' + * If args.name parameter is '*', function will be invoked before object drawing. + * If such function does not return value - normal drawing will be continued. * @protected */ function addDrawFunc(args) { - drawFuncs.lst.push(args); + if (args?.name === '*') + drawFuncs.alt = isFunc(args.func) ? args.func : null; + else + drawFuncs.lst.push(args); return args; } @@ -392,6 +397,12 @@ async function draw(dom, obj, opt) { if (handle.draw_field && obj[handle.draw_field]) return draw(dom, obj[handle.draw_field], opt || handle.draw_field_opt); + if (drawFuncs.alt && !handle.transform) { + const v = drawFuncs.alt(dom, obj, opt); + if (v) + return v; + } + if (!canDrawHandle(handle)) { if (opt && (opt.indexOf('same') >= 0)) { const main_painter = getElementMainPainter(dom); From 9a2c6c3104c11ac1a202c99940cb1a6e00a519b5 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Thu, 27 Nov 2025 08:02:06 +0100 Subject: [PATCH 2/7] Remove setDrawFunc for hpainter --- modules/gui/HierarchyPainter.mjs | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/modules/gui/HierarchyPainter.mjs b/modules/gui/HierarchyPainter.mjs index e0978e9ce..7d5065bf6 100644 --- a/modules/gui/HierarchyPainter.mjs +++ b/modules/gui/HierarchyPainter.mjs @@ -825,8 +825,6 @@ class HierarchyPainter extends BasePainter { #one_by_one; // process drop items one by one #topname; // top item name #cached_draw_object; // cached object for first draw - #draw_func; // alternative draw function - #redraw_func; // alternative redraw function /** @summary Create painter * @param {string} name - symbolic name @@ -857,23 +855,6 @@ class HierarchyPainter extends BasePainter { this.textcolor = settings.DarkMode ? '#eee' : '#111'; } - /** @summary Set alternative draw/redraw functions - * @desc If only only draw function specified - it also will be used for re-drawing - * @protected */ - setDrawFunc(_draw, _redraw) { - if (isFunc(_draw)) { - this.#draw_func = _draw; - this.#redraw_func = isFunc(_redraw) ? _redraw : _draw; - } - } - - /** @summary Invoke configured draw or redraw function - * @protected */ - async callDrawFunc(dom, obj, opt, doredraw) { - const func = doredraw ? (this.#redraw_func || redraw) : (this.#draw_func || draw); - return func(dom, obj, opt); - } - /** @summary Cleanup hierarchy painter * @desc clear drawing and browser */ cleanup() { @@ -2313,7 +2294,7 @@ class HierarchyPainter extends BasePainter { drawopt = handle.dflt; if (dom) - return this.callDrawFunc(dom, obj, drawopt, updating).then(p => complete(p)).catch(err => complete(null, err)); + return (updating ? redraw : draw)(dom, obj, drawopt).then(p => complete(p)).catch(err => complete(null, err)); let did_activate = false; const arr = []; @@ -2357,7 +2338,7 @@ class HierarchyPainter extends BasePainter { cleanup(frame); mdi.activateFrame(frame); - return this.callDrawFunc(frame, obj, drawopt) + return draw(frame, obj, drawopt) .then(p => complete(p)) .catch(err => complete(null, err)); }); @@ -2465,7 +2446,7 @@ class HierarchyPainter extends BasePainter { if (isFunc(dom?.addPadButtons)) dom.addPadButtons(); - return this.callDrawFunc(dom, res.obj, opt).then(p => drop_complete(p, mp === p)); + return draw(dom, res.obj, opt).then(p => drop_complete(p, mp === p)); }); } From e8b52ebffbcfd8cd3d2e8d1edfcfb316822134fb Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Thu, 27 Nov 2025 08:15:02 +0100 Subject: [PATCH 3/7] Use internals to store alternative draw function --- modules/draw.mjs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/draw.mjs b/modules/draw.mjs index 1417ae14b..9f1ce98f9 100644 --- a/modules/draw.mjs +++ b/modules/draw.mjs @@ -165,7 +165,7 @@ const drawFuncs = { lst: [ { name: nsREX + 'RFont', icon: 'img_text', draw: () => import_v7().then(h => h.drawRFont), opt: '', direct: 'v7', csstype: 'font' }, { name: nsREX + 'RAxisDrawable', icon: 'img_frame', draw: () => import_v7().then(h => h.drawRAxis), opt: '' }, { name: nsREX + 'RTreeMapPainter', class: () => import('./draw/RTreeMapPainter.mjs').then(h => h.RTreeMapPainter), opt: '' } -], cache: {}, alt: null }; +], cache: {} }; /** @summary Register draw function for the class @@ -184,7 +184,7 @@ const drawFuncs = { lst: [ * @protected */ function addDrawFunc(args) { if (args?.name === '*') - drawFuncs.alt = isFunc(args.func) ? args.func : null; + internals._alt_draw = isFunc(args.func) ? args.func : null; else drawFuncs.lst.push(args); return args; @@ -397,8 +397,8 @@ async function draw(dom, obj, opt) { if (handle.draw_field && obj[handle.draw_field]) return draw(dom, obj[handle.draw_field], opt || handle.draw_field_opt); - if (drawFuncs.alt && !handle.transform) { - const v = drawFuncs.alt(dom, obj, opt); + if (internals._alt_draw && !handle.transform) { + const v = internals._alt_draw(dom, obj, opt); if (v) return v; } From c50da30840cb4318eae245ef94c49c2c72b89972 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Thu, 27 Nov 2025 08:15:17 +0100 Subject: [PATCH 4/7] Check alternative draw function in TTree drawing --- modules/draw/TTree.mjs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modules/draw/TTree.mjs b/modules/draw/TTree.mjs index 667a05715..3716e10b2 100644 --- a/modules/draw/TTree.mjs +++ b/modules/draw/TTree.mjs @@ -56,6 +56,12 @@ async function drawTreeDrawResult(dom, obj, opt) { if (!typ || !isStr(typ)) return Promise.reject(Error('Object without type cannot be draw with TTree')); + if (internals._alt_draw) { + const v = internals._alt_draw(dom, obj, opt); + if (v) + return v; + } + if (typ.indexOf(clTH1) === 0) return TH1Painter.draw(dom, obj, opt); if (typ.indexOf(clTH2) === 0) From 686a51dc6371f8041f20e0f7e25c086aaa5b2aa0 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Thu, 27 Nov 2025 08:15:26 +0100 Subject: [PATCH 5/7] Update hpainter example --- demo/node/hpainter.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/demo/node/hpainter.js b/demo/node/hpainter.js index 6f47452c9..5924b9918 100644 --- a/demo/node/hpainter.js +++ b/demo/node/hpainter.js @@ -2,8 +2,8 @@ // in batch display one just able to create images -import { version, HierarchyPainter, draw } from 'jsroot'; - +import { version, HierarchyPainter, draw, addDrawFunc } from 'jsroot'; +import { writeFileSync } from 'fs'; console.log(`JSROOT version ${version}`); @@ -14,22 +14,28 @@ const hp = new HierarchyPainter('hpainter'); hp.setDisplay('batch'); // catch draw function calls -hp.setDrawFunc((dom, obj, opt) => { - console.log(`trying to draw ${obj._typename}`); - return draw(dom, obj, opt); +addDrawFunc({ + name: '*', + func: (dom, obj, opt) => { + console.log(`Actual draw of ${obj._typename}`); + // if function return true no normal drawing will be performed + // do not try to call `draw` function from here !!! + // return true; + } }); await hp.openRootFile('https://root.cern/js/files/hsimple.root'); // display of TH2 histogram +console.log('Invoke histogram drawing'); await hp.display('hpxpy'); await hp.expandItem('ntuple'); // invoking TTree::Draw +console.log('Invoke TLeaf drawing'); await hp.display('ntuple/pz'); - // should be BatchDisplay const disp = hp.getDisplay(); @@ -38,5 +44,5 @@ for (let id = 0; id < disp.numFrames(); ++id) { console.log(`Frame ${id} create svg size ${svg.length}`); // one can save svg plain file - // writeFileSync(`frame${id}.svg`, svg); + writeFileSync(`frame${id}.svg`, svg); } From 6a208cabba7f0adf134de8f6ecd367147848922c Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Thu, 27 Nov 2025 08:19:40 +0100 Subject: [PATCH 6/7] Add more info to addDrawFunc --- modules/draw.mjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/draw.mjs b/modules/draw.mjs index 9f1ce98f9..9f763bd66 100644 --- a/modules/draw.mjs +++ b/modules/draw.mjs @@ -172,12 +172,16 @@ const drawFuncs = { lst: [ * @param {object} args - arguments * @param {string|regexp} args.name - class name or regexp pattern or '*' * @param {function} [args.func] - draw function + * @param {string} [args.sameas] - let behave same as specified class * @param {function} [args.draw] - async function to load draw function * @param {function} [args.class] - async function to load painter class with static draw function * @param {boolean} [args.direct] - if true, function is just Redraw() method of ObjectPainter * @param {string} [args.opt] - list of supported draw options (separated with semicolon) like 'col;scat;' * @param {string} [args.icon] - icon name shown for the class in hierarchy browser * @param {string} [args.draw_field] - draw only data member from object, like fHistogram + * @param {string} [args.noinspect] - disable inspect + * @param {string} [args.noexpand] - disable expand + * @param {string} [args.pm] - always show plus or minus sign even when no child items exists * @desc List of supported draw options could be provided, separated with ';' * If args.name parameter is '*', function will be invoked before object drawing. * If such function does not return value - normal drawing will be continued. From 6a09f2fba922e00bd801866eba16391bfbc28220 Mon Sep 17 00:00:00 2001 From: Sergey Linev Date: Thu, 27 Nov 2025 08:20:57 +0100 Subject: [PATCH 7/7] Comment out svg file saving --- demo/node/hpainter.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/demo/node/hpainter.js b/demo/node/hpainter.js index 5924b9918..bbcf3b97e 100644 --- a/demo/node/hpainter.js +++ b/demo/node/hpainter.js @@ -3,7 +3,7 @@ import { version, HierarchyPainter, draw, addDrawFunc } from 'jsroot'; -import { writeFileSync } from 'fs'; +// import { writeFileSync } from 'fs'; console.log(`JSROOT version ${version}`); @@ -33,7 +33,7 @@ await hp.display('hpxpy'); await hp.expandItem('ntuple'); // invoking TTree::Draw -console.log('Invoke TLeaf drawing'); +console.log('Invoke TBranch drawing'); await hp.display('ntuple/pz'); // should be BatchDisplay @@ -41,8 +41,9 @@ const disp = hp.getDisplay(); for (let id = 0; id < disp.numFrames(); ++id) { const svg = await disp.makeSVG(id); + console.log(`Frame ${id} create svg size ${svg.length}`); // one can save svg plain file - writeFileSync(`frame${id}.svg`, svg); + // writeFileSync(`frame${id}.svg`, svg); }