From 4c418a0d5455474769cbb22a8bd0a3320059fde2 Mon Sep 17 00:00:00 2001 From: Abinesh N Date: Mon, 13 Apr 2026 22:36:08 +0530 Subject: [PATCH 1/4] test(funnel): add test for textinfo token order --- src/traces/bar/plot.js | 42 +++++++++++++++++-------------- test/jasmine/tests/funnel_test.js | 25 ++++++++++++++++++ 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index 37653f9c3a8..5dc644d695b 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -1123,14 +1123,15 @@ function calcTextinfo(cd, index, xa, ya) { var final = cdi.v; var initial = final - delta; - if (hasFlag('initial')) text.push(formatNumber(initial)); - if (hasFlag('delta')) text.push(formatNumber(delta)); - if (hasFlag('final')) text.push(formatNumber(final)); + for (var j = 0; j < parts.length; j++) { + var wPart = parts[j].trim(); + if (wPart === 'initial') text.push(formatNumber(initial)); + else if (wPart === 'delta') text.push(formatNumber(delta)); + else if (wPart === 'final') text.push(formatNumber(final)); + } } if (isFunnel) { - if (hasFlag('value')) text.push(formatNumber(cdi.s)); - var nPercent = 0; if (hasFlag('percent initial')) nPercent++; if (hasFlag('percent previous')) nPercent++; @@ -1138,20 +1139,23 @@ function calcTextinfo(cd, index, xa, ya) { var hasMultiplePercents = nPercent > 1; - if (hasFlag('percent initial')) { - tx = Lib.formatPercent(cdi.begR); - if (hasMultiplePercents) tx += ' of initial'; - text.push(tx); - } - if (hasFlag('percent previous')) { - tx = Lib.formatPercent(cdi.difR); - if (hasMultiplePercents) tx += ' of previous'; - text.push(tx); - } - if (hasFlag('percent total')) { - tx = Lib.formatPercent(cdi.sumR); - if (hasMultiplePercents) tx += ' of total'; - text.push(tx); + for (var k = 0; k < parts.length; k++) { + var part = parts[k].trim(); + if (part === 'value') { + text.push(formatNumber(cdi.s)); + } else if (part === 'percent initial') { + tx = Lib.formatPercent(cdi.begR); + if (hasMultiplePercents) tx += ' of initial'; + text.push(tx); + } else if (part === 'percent previous') { + tx = Lib.formatPercent(cdi.difR); + if (hasMultiplePercents) tx += ' of previous'; + text.push(tx); + } else if (part === 'percent total') { + tx = Lib.formatPercent(cdi.sumR); + if (hasMultiplePercents) tx += ' of total'; + text.push(tx); + } } } diff --git a/test/jasmine/tests/funnel_test.js b/test/jasmine/tests/funnel_test.js index 0506aba9da3..c7dd0c770a8 100644 --- a/test/jasmine/tests/funnel_test.js +++ b/test/jasmine/tests/funnel_test.js @@ -1757,4 +1757,29 @@ describe('funnel uniformtext', function() { })) .then(done, done.fail); }); + + it('should respect textinfo token order', function() { + var mock = { + data: [{ + type: 'funnel', + y: ['Awareness', 'Interest', 'Action'], + x: [1000, 700, 400], + textinfo: 'percent initial+value' + }], + layout: {} + }; + + return Plotly.newPlot(createGraphDiv(), mock.data, mock.layout) + .then(function(gd) { + var texts = gd.calcdata[0].map(function(d) { return d.tx; }); + + // percent initial must come BEFORE value + // expected: "100%" before "1000", not "1000" before "100%" + expect(texts[0]).toMatch(/^[\d.]+%/); // starts with percent + expect(texts[1]).toMatch(/^[\d.]+%/); + expect(texts[2]).toMatch(/^[\d.]+%/); + }) + .then(destroyGraphDiv); + }); + }); From c05bb224bd82f9504beaf94e553b1ee4c1b13808 Mon Sep 17 00:00:00 2001 From: Abinesh N Date: Tue, 14 Apr 2026 02:01:04 +0530 Subject: [PATCH 2/4] fix: correct loop structure and restore hasFlag checks --- src/traces/bar/plot.js | 90 +++++++++++++++---------------- test/jasmine/tests/funnel_test.js | 35 ++++++------ 2 files changed, 63 insertions(+), 62 deletions(-) diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index 5dc644d695b..f7dd1e6cccc 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -1101,61 +1101,61 @@ function calcTextinfo(cd, index, xa, ya) { var textinfo = trace.textinfo; var cdi = cd[index]; - var parts = textinfo.split('+'); + var parts = textinfo.split('+').map(function(p) { + return p.trim(); + }); + var text = []; var tx; - var hasFlag = function (flag) { + var hasFlag = function(flag) { return parts.indexOf(flag) !== -1; }; - if (hasFlag('label')) { - text.push(formatLabel(cd[index].p)); - } - - if (hasFlag('text')) { - tx = Lib.castOption(trace, cdi.i, 'text'); - if (tx === 0 || tx) text.push(tx); + var delta, final, initial; + if(isWaterfall) { + delta = +cdi.rawS || cdi.s; + final = cdi.v; + initial = final - delta; } - if (isWaterfall) { - var delta = +cdi.rawS || cdi.s; - var final = cdi.v; - var initial = final - delta; - - for (var j = 0; j < parts.length; j++) { - var wPart = parts[j].trim(); - if (wPart === 'initial') text.push(formatNumber(initial)); - else if (wPart === 'delta') text.push(formatNumber(delta)); - else if (wPart === 'final') text.push(formatNumber(final)); - } + var nPercent = 0; + var hasMultiplePercents = false; + if(isFunnel) { + if(hasFlag('percent initial')) nPercent++; + if(hasFlag('percent previous')) nPercent++; + if(hasFlag('percent total')) nPercent++; + hasMultiplePercents = nPercent > 1; } - if (isFunnel) { - var nPercent = 0; - if (hasFlag('percent initial')) nPercent++; - if (hasFlag('percent previous')) nPercent++; - if (hasFlag('percent total')) nPercent++; - - var hasMultiplePercents = nPercent > 1; - - for (var k = 0; k < parts.length; k++) { - var part = parts[k].trim(); - if (part === 'value') { - text.push(formatNumber(cdi.s)); - } else if (part === 'percent initial') { - tx = Lib.formatPercent(cdi.begR); - if (hasMultiplePercents) tx += ' of initial'; - text.push(tx); - } else if (part === 'percent previous') { - tx = Lib.formatPercent(cdi.difR); - if (hasMultiplePercents) tx += ' of previous'; - text.push(tx); - } else if (part === 'percent total') { - tx = Lib.formatPercent(cdi.sumR); - if (hasMultiplePercents) tx += ' of total'; - text.push(tx); - } + for(var i in parts) { + var part = parts[i]; + + if(part === 'label' && hasFlag('label')) { + text.push(formatLabel(cdi.p)); + } else if(part === 'text' && hasFlag('text')) { + tx = Lib.castOption(trace, cdi.i, 'text'); + if(tx === 0 || tx) text.push(tx); + } else if(isWaterfall && part === 'initial' && hasFlag('initial')) { + text.push(formatNumber(initial)); + } else if(isWaterfall && part === 'delta' && hasFlag('delta')) { + text.push(formatNumber(delta)); + } else if(isWaterfall && part === 'final' && hasFlag('final')) { + text.push(formatNumber(final)); + } else if(isFunnel && part === 'value' && hasFlag('value')) { + text.push(formatNumber(cdi.s)); + } else if(isFunnel && part === 'percent initial' && hasFlag('percent initial')) { + tx = Lib.formatPercent(cdi.begR); + if(hasMultiplePercents) tx += ' of initial'; + text.push(tx); + } else if(isFunnel && part === 'percent previous' && hasFlag('percent previous')) { + tx = Lib.formatPercent(cdi.difR); + if(hasMultiplePercents) tx += ' of previous'; + text.push(tx); + } else if(isFunnel && part === 'percent total' && hasFlag('percent total')) { + tx = Lib.formatPercent(cdi.sumR); + if(hasMultiplePercents) tx += ' of total'; + text.push(tx); } } diff --git a/test/jasmine/tests/funnel_test.js b/test/jasmine/tests/funnel_test.js index c7dd0c770a8..b24b59a7709 100644 --- a/test/jasmine/tests/funnel_test.js +++ b/test/jasmine/tests/funnel_test.js @@ -1759,25 +1759,26 @@ describe('funnel uniformtext', function() { }); it('should respect textinfo token order', function() { - var mock = { - data: [{ - type: 'funnel', - y: ['Awareness', 'Interest', 'Action'], - x: [1000, 700, 400], - textinfo: 'percent initial+value' - }], - layout: {} - }; + var gd = createGraphDiv(); + + return Plotly.newPlot(gd, [{ + type: 'funnel', + y: ['Awareness', 'Interest', 'Action'], + x: [1000, 700, 400], + textinfo: 'percent initial+value' + }], {}) + .then(function() { + var traceNodes = gd.querySelectorAll('.bartext'); + var firstText = traceNodes[0] ? traceNodes[0].textContent : ''; + + var percentIndex = firstText.indexOf('%'); + expect(percentIndex).toBeGreaterThan(-1); - return Plotly.newPlot(createGraphDiv(), mock.data, mock.layout) - .then(function(gd) { - var texts = gd.calcdata[0].map(function(d) { return d.tx; }); + var numberMatch = firstText.match(/\d+/); + expect(numberMatch).not.toBeNull(); - // percent initial must come BEFORE value - // expected: "100%" before "1000", not "1000" before "100%" - expect(texts[0]).toMatch(/^[\d.]+%/); // starts with percent - expect(texts[1]).toMatch(/^[\d.]+%/); - expect(texts[2]).toMatch(/^[\d.]+%/); + var numberIndex = firstText.indexOf(numberMatch[0]); + expect(percentIndex).toBeLessThan(numberIndex); }) .then(destroyGraphDiv); }); From 4696313099d54a0ab77b74fe2b3fef325be800b8 Mon Sep 17 00:00:00 2001 From: Abinesh N Date: Tue, 14 Apr 2026 02:24:21 +0530 Subject: [PATCH 3/4] test: ensure robust validation of token order --- test/jasmine/tests/funnel_test.js | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/test/jasmine/tests/funnel_test.js b/test/jasmine/tests/funnel_test.js index b24b59a7709..4f178fa0abc 100644 --- a/test/jasmine/tests/funnel_test.js +++ b/test/jasmine/tests/funnel_test.js @@ -1768,17 +1768,25 @@ describe('funnel uniformtext', function() { textinfo: 'percent initial+value' }], {}) .then(function() { - var traceNodes = gd.querySelectorAll('.bartext'); - var firstText = traceNodes[0] ? traceNodes[0].textContent : ''; + var cd = gd.calcdata[0]; - var percentIndex = firstText.indexOf('%'); - expect(percentIndex).toBeGreaterThan(-1); + cd.forEach(function(d) { + var txt = d.tx; + expect(txt).toBeDefined(); - var numberMatch = firstText.match(/\d+/); - expect(numberMatch).not.toBeNull(); + var percentIndex = txt.indexOf('%'); + expect(percentIndex).toBeGreaterThan(-1); - var numberIndex = firstText.indexOf(numberMatch[0]); - expect(percentIndex).toBeLessThan(numberIndex); + var firstNumberIndex = txt.search(/\d/); + expect(firstNumberIndex).toBeGreaterThan(-1); + + // percent part starts early (e.g. "70%") + expect(percentIndex).toBeGreaterThan(firstNumberIndex); + + // value number must exist AFTER percent + var afterPercent = txt.slice(percentIndex + 1); + expect(afterPercent.match(/\d+/)).not.toBeNull(); + }); }) .then(destroyGraphDiv); }); From 4f5d538bc6bb707f673cc6a373488eafcbc61cf7 Mon Sep 17 00:00:00 2001 From: Abinesh N Date: Tue, 14 Apr 2026 02:38:51 +0530 Subject: [PATCH 4/4] test: use DOM text elements instead of calcdata tx --- test/jasmine/tests/funnel_test.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/jasmine/tests/funnel_test.js b/test/jasmine/tests/funnel_test.js index 4f178fa0abc..f4b770ea853 100644 --- a/test/jasmine/tests/funnel_test.js +++ b/test/jasmine/tests/funnel_test.js @@ -1768,11 +1768,13 @@ describe('funnel uniformtext', function() { textinfo: 'percent initial+value' }], {}) .then(function() { - var cd = gd.calcdata[0]; + var textEls = gd.querySelectorAll('text.bartext'); - cd.forEach(function(d) { - var txt = d.tx; - expect(txt).toBeDefined(); + expect(textEls.length).toBeGreaterThan(0); + + for(var i = 0; i < textEls.length; i++) { + var txt = textEls[i].textContent; + if(!txt || txt.length === 0) continue; var percentIndex = txt.indexOf('%'); expect(percentIndex).toBeGreaterThan(-1); @@ -1780,13 +1782,14 @@ describe('funnel uniformtext', function() { var firstNumberIndex = txt.search(/\d/); expect(firstNumberIndex).toBeGreaterThan(-1); - // percent part starts early (e.g. "70%") + // percent sign must appear AFTER first digit (e.g. "70%") + // but BEFORE the value number that follows expect(percentIndex).toBeGreaterThan(firstNumberIndex); // value number must exist AFTER percent var afterPercent = txt.slice(percentIndex + 1); expect(afterPercent.match(/\d+/)).not.toBeNull(); - }); + } }) .then(destroyGraphDiv); });