Skip to content

fix(funnel/waterfall): respect textinfo token order#7752

Open
Abineshabee wants to merge 5 commits intoplotly:masterfrom
Abineshabee:fix/textinfo-token-order
Open

fix(funnel/waterfall): respect textinfo token order#7752
Abineshabee wants to merge 5 commits intoplotly:masterfrom
Abineshabee:fix/textinfo-token-order

Conversation

@Abineshabee
Copy link
Copy Markdown

@Abineshabee Abineshabee commented Apr 13, 2026

What this fixes

Fixes #7751

go.Funnel with textinfo="percent initial+value" was displaying
value before percent (e.g. "700 70.0%") regardless of the order
specified by the user.

Root cause

In src/traces/bar/plot.js, the calcTextinfo function pushed
funnel tokens (value, percent initial/previous/total) and
waterfall tokens (initial, delta, final) in a hardcoded
sequence, ignoring the order of tokens in the textinfo string.

Fix

Replaced hardcoded hasFlag() checks with a loop over parts[]
(the user-specified token order) for both funnel and waterfall traces.

Before

"percent initial+value" → "1000 100.0%" ❌
Screenshot 2026-04-13 230956

After

"percent initial+value" → "100.0% 1000" ✅
Screenshot 2026-04-13 232120

Notes

  • Backward compatible — existing behavior unchanged when token
    order was already correct
  • label and text tokens still use fixed ordering and are
    not modified in this PR
  • Fix applies to both funnel and waterfall traces as requested

@emilykl
Copy link
Copy Markdown
Contributor

emilykl commented Apr 13, 2026

Hi @Abineshabee ! Thanks for the PR. This looks like it's on the right track; I'll take a review pass through the code. Let us know if you would like assistance with the failing test.

Could you please update the PR description to include screenshots or a codepen demonstrating the fix?

Comment thread src/traces/bar/plot.js Outdated
Comment thread src/traces/bar/plot.js Outdated
Copy link
Copy Markdown
Contributor

@emilykl emilykl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few comments, I'll take another look once you're ready @Abineshabee !

@Abineshabee
Copy link
Copy Markdown
Author

@emilykl I've addressed both points:

  1. Switched to for (var part in parts) loop syntax
  2. Included label and text flags inside the ordered loop
    so all tokens now respect user-specified order consistently.
    Also normalized parts with .map(p => p.trim()) to handle
    any whitespace edge cases. Ready for another look!

@emilykl
Copy link
Copy Markdown
Contributor

emilykl commented Apr 13, 2026

Thanks @Abineshabee for the quick turnaround! Before I take another look, can you update the PR description to add screenshots of the fix in action, and also fix the failing test? I'm happy to assist with either, just let me know.

@Abineshabee
Copy link
Copy Markdown
Author

@emilykl Updated the test with proper safety checks. Ready for another look!

@Abineshabee
Copy link
Copy Markdown
Author

Abineshabee commented Apr 13, 2026

@emilykl The only remaining CI failure is isosurface_test.js @gl should display hover labels
which is a pre-existing flaky GL test unrelated to this PR.
It fails inconsistently across runs (different hover values each time)
and is not caused by my changes to src/traces/bar/plot.js.

All funnel and waterfall tests pass. Ready for review !

@emilykl
Copy link
Copy Markdown
Contributor

emilykl commented Apr 15, 2026

Great! Taking another look.

Comment thread src/traces/bar/plot.js Outdated
Comment on lines +1104 to +1106
var parts = textinfo.split('+').map(function(p) {
return p.trim();
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I double-checked, and we already validate flaglist items during the coerce step, so this trim() is not needed.

Suggested change
var parts = textinfo.split('+').map(function(p) {
return p.trim();
});
var parts = textinfo.split('+');

Comment thread src/traces/bar/plot.js Outdated
for(var i in parts) {
var part = parts[i];

if(part === 'label' && hasFlag('label')) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need the hasFlag('...') checks at all, right? if(part === '...') should be sufficient, unless I'm missing something.

Comment thread src/traces/bar/plot.js Outdated
Comment on lines +1116 to +1119
if(isWaterfall) {
delta = +cdi.rawS || cdi.s;
final = cdi.v;
initial = final - delta;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any reason to calculate these separately at the top; might as well just put each calculation inside its corresponding if/else case, I think that's clearer.

Comment thread test/jasmine/tests/funnel_test.js Outdated
Comment on lines +1773 to +1792
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);

var firstNumberIndex = txt.search(/\d/);
expect(firstNumberIndex).toBeGreaterThan(-1);

// 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();
}
Copy link
Copy Markdown
Contributor

@emilykl emilykl Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Abineshabee Instead of this approach, let's just assert the exact text content we expect, I think that will be much clearer.

i.e. something like

var textContent = Array.from(textEls).map((el) => el.textContent);
expect(textContent).toBe([
    "100%1000",
    "70%700",
    "40%400"
]);

@emilykl
Copy link
Copy Markdown
Contributor

emilykl commented Apr 15, 2026

@Abineshabee This looks great! See my comments for additional small changes.

The new logic in calcTextInfo() I think is much easier to understand than the old logic, so that's a nice bonus to this fix.


There's one more chunk of work needed on this PR: Updating other trace types to respect token order in textinfo, so that textinfo works the same way for all traces.

Luckily there's only two other functions which need to be updated:

  • formatSliceLabel() in src/traces/pie/plot.js
  • formatSliceLabel() in src/traces/sunburst/plot.js

Both of those functions should be refactored similarly to calcTextInfo(), so that they also respect token order. Let me know if you run into any issues with that step, I'm happy to assist. Make sure to add a Jasmine test for sunburst and pie as well.

Thanks for working on this fix!

@emilykl
Copy link
Copy Markdown
Contributor

emilykl commented Apr 15, 2026

Also: You're correct that the one failing test is unrelated to these changes. That test is sometimes flaky. If it's still failing after your next commits I'll look into it.

@Abineshabee
Copy link
Copy Markdown
Author

Hi @emilykl — thanks for the detailed review and for guiding this fix in the right direction!

Here's a summary of everything updated in this latest push:

src/traces/bar/plot.js

  • Removed .trim() — validation already happens at coerce step
  • Removed hasFlag() — replaced with for(var part in parts) loop using part === '...' checks
  • Moved waterfall calculations (initial, delta, final) inline inside the loop
  • label and text tokens moved inside the loop so all tokens respect user-specified order consistently

src/traces/pie/plot.js

  • Refactored formatSliceLabel() — replaced hasFlag() checks with for(var part in parts) loop
  • All tokens (label, text, value, percent) now respect user-specified order

src/traces/sunburst/plot.js

  • Refactored formatSliceLabel() — replaced hasFlag() checks with for(var part in parts) loop
  • All tokens (label, value, text, current path, percent parent, percent entry, percent root) now respect user-specified order
  • Percent pre-count loop retained for hasMultiplePercents label logic

test/jasmine/tests/funnel_test.js

  • Replaced indirect positional assertions with exact textContent checks as suggested
  • Uses existing gd from beforeEach, follows done/done.fail pattern consistent with the rest of the suite

test/jasmine/tests/pie_test.js

  • Added test asserting percent+label renders in user-specified order

test/jasmine/tests/sunburst_test.js

  • Added test asserting value+label renders in user-specified order

All funnel, waterfall, pie and sunburst tests pass. The one remaining CI failure (isosurface_test.js) is the pre-existing flaky GL test unrelated to these changes.

Ready for another look!

@emilykl
Copy link
Copy Markdown
Contributor

emilykl commented Apr 16, 2026

Thanks @Abineshabee for the quick turnaround! I'll take another look.

Comment thread src/traces/bar/plot.js
Comment on lines +1117 to +1118
for(var part in parts) {
part = parts[part];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this back to how it was previously, it's confusing to reuse the same variable name. Likewise for the other loops.

(I realize I suggested for(var part in parts) originally; that's my bad, I forgot how for loops work in js!)

Suggested change
for(var part in parts) {
part = parts[part];
for(var i in parts) {
part = parts[i];

Comment thread src/traces/bar/plot.js
Comment on lines +1109 to 1114
if(isFunnel) {
for(var p = 0; p < parts.length; p++) {
var f = parts[p];
if(f === 'percent initial' || f === 'percent previous' || f === 'percent total') nPercent++;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer this version from your previous iteration as well, it's easier to understand.

Suggested change
if(isFunnel) {
for(var p = 0; p < parts.length; p++) {
var f = parts[p];
if(f === 'percent initial' || f === 'percent previous' || f === 'percent total') nPercent++;
}
}
if(isFunnel) {
if(hasFlag('percent initial')) nPercent++;
if(hasFlag('percent previous')) nPercent++;
if(hasFlag('percent total')) nPercent++;
hasMultiplePercents = nPercent > 1;
}

labels: ['Root', 'A', 'B'],
parents: ['', 'Root', 'Root'],
values: [0, 10, 20],
textinfo: 'value+label'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add percent root to flaglist to ensure the root is handled correctly (should not appear in root).

Make sure to update expected values.

Suggested change
textinfo: 'value+label'
textinfo: 'percent root+value+label'

Comment on lines +1773 to +1775
expect(textContent[0]).toBe('100%1000');
expect(textContent[1]).toBe('70%700');
expect(textContent[2]).toBe('40%400');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also check the length of textContent.

Same goes for the other new tests as well.

Suggested change
expect(textContent[0]).toBe('100%1000');
expect(textContent[1]).toBe('70%700');
expect(textContent[2]).toBe('40%400');
expect(textContent.length).toBe(3);
expect(textContent[0]).toBe('100%1000');
expect(textContent[1]).toBe('70%700');
expect(textContent[2]).toBe('40%400');

@Abineshabee
Copy link
Copy Markdown
Author

Thank for your review and suggestions , now I am working on updates

@emilykl
Copy link
Copy Markdown
Contributor

emilykl commented Apr 16, 2026

@Abineshabee I've finished my review pass! Just a few remaining items:

  • Some small tweaks to code (see comments)

  • Make sure to use the same for loop style in all 3 files where you have added one. I like the version below (for var i in parts) which you had used previously, but I don't care too much as long as it's consistent and doesn't reuse variable names.

for(var i in parts) {
        var part = parts[i];
  • There is a failing Jasmine test (should be able to restyle *textinfo*) which I think is expected due to this change. The test will need to be updated to expect the values in the new order.

  • There are some failing image tests in the test-baselines CI job which I believe are expected as well based on these changes. You will need to download the updated images from the CircleCI "Artifacts" tab (under test_images) and commit them to the repo in the test/image/baselines/ folder. Double-check that the changes are expected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: go.Funnel textinfo="percent initial+value" shows value before percent.

3 participants