Skip to content

feat(conditional-formatting): add manual min/max color-range bounds - #43820

Open
EnxDev wants to merge 27 commits into
masterfrom
enxdev/feat/pivot-table-conditional-formatting
Open

feat(conditional-formatting): add manual min/max color-range bounds#43820
EnxDev wants to merge 27 commits into
masterfrom
enxdev/feat/pivot-table-conditional-formatting

Conversation

@EnxDev

@EnxDev EnxDev commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

SUMMARY

Adds three related conditional-formatting capabilities, unblocking OpenTable’s embedded-dashboard launch:

  1. Manual minBound and maxBound values Authors can fix the endpoints of a color scale instead of recalculating them from the currently loaded data. Today, the scale uses Math.min and Math.max from the column, or the entered values for the Between family. As a result, the same rule can produce different colors when filters, date ranges, or row counts change.

  2. Diverging low/mid/high color scales Authors can combine an optional centerValue with lowColor, midColor, and highColor to create a three-color scale, such as red → white → green. This makes it easy to see whether a value is above or below the center, rather than only how far it is from one end of the scale.

  3. Percent-of-column bounds minBound, maxBound, and centerValue can be defined as percentages of the column sum or maximum instead of fixed values. This allows the rule to adapt as the underlying data changes without requiring the author to enter new values.

The changes are implemented in the shared color-scale utility (getColorFormatters.ts) and conditional-formatting popover (FormattingPopoverContent.tsx). They are therefore available to all chart types that already support conditional formatting—Table, Pivot Table, and Big Number—without plugin-specific changes.

Design decisions

  • Bounds apply only to None, >, <, , and . They don’t apply to Equal or NotEqual, which perform exact matches, or to the Between family, which already uses targetValueLeft and targetValueRight. The popover only shows the bounds that affect the selected operator.

  • Diverging mode is available only for the None operator. It activates when centerValue and all three colors are set, and when the resolved center falls strictly between the resolved minimum and maximum. Incomplete or invalid configurations fall back to the existing single-color behavior, avoiding partially applied or broken scales.

  • Percent-of-column supports two denominators: Column sum and Column max. Pivot-specific row, column, and grand totals aren’t included because these two options have consistent meanings across Table and Pivot Table and cover the ticket’s requirements. Column max is the default, matching the behavior of data bars in Excel and Google Sheets.

  • The bound unit—Value or % of column applies to the entire rule, including minBound, maxBound, and centerValue. Using one shared unit prevents conflicting configurations within the same rule.

  • All new fields are optional and additive. Existing rules don’t contain them and continue to render as before, so no migration is required.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

  • Before
p3-before.mp4
  • After
p3-after.mp4
  • Before
p3-before-table p3-before-popover p3-before-popover-column
  • After
p3-after-popover-percent p3-after-colored-table p3-after-popover-filled

TESTING INSTRUCTIONS

  1. Create or open a Table (AG Grid) or Pivot Table (AG Grid) chart with a numeric column.
  2. Customize tab → Conditional Formatting → Add new color formatter.
  3. Manual bounds: leave Operator as None, set Min bound / Max bound to values wider than the data's actual range, Apply — confirm colors no longer reach full intensity (scaled against the fixed range, not the data).
  4. Diverging scale: with Operator None, set Center value plus Low/Mid/High colors — confirm the column renders as a genuine three-color gradient pivoting at the center value, not a single hue fading in and out.
  5. Percent-of-total: switch Bound unit to % of column, pick Column sum or Column max, set Min bound = 0 / Max bound = 100 — confirm the rule keeps coloring correctly relative to the chosen denominator, and that switching back to Value reverts to raw-number bounds.
  6. Switch Operator to >/<// and confirm only the relevant bound shows, and that Bound unit still applies.
  7. Save a chart with none of these fields set and confirm it renders unchanged from before this PR.

Automated coverage:

  • getColorFormatters.test.ts — 84 tests (manual bounds, diverging scale incl. operator-scoping and invalid-config fallback, percent resolution against sum/max/default/empty-column, percent-resolved centerValue feeding the diverging scale)
  • FormattingPopoverContent.test.tsx + ConditionalFormattingControl.test.tsx — 40 tests (field visibility per operator, cross-field validation, diverging color-picker independence, Bound unit/percent-denominator visibility and payload shape)
  • Full suite for both files: 124/124 passing

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags:
  • Changes UI
  • Includes DB Migration
  • Introduces new feature or API
  • Removes existing feature or API

EnxDev and others added 5 commits September 2, 2026 11:52
…orFunction

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ld validators

Swap the compare-function parameter order in minBoundValidator/maxBoundValidator
so targetValueValidator's actual call signature (compare(otherFieldValue,
ownFieldValue)) is honored, matching the working targetValueLeftValidator/
targetValueRightValidator pattern. Previously a valid pair (e.g. min=5, max=10)
was rejected while an invalid pair (min=10, max=5) passed silently. Adds two
tests that type into Min bound/Max bound, blur to trigger validation, and
assert on the resulting error state for both a valid and an invalid pair.
…r, fix vacuous tests, fix falsy-guard bug

- Redesign renderBoundFields via a new getBoundVisibility(operator) helper:
  None shows both Min/Max bound (with the cross-field validator), the four
  directional operators (>, >=, <, <=) show only the one bound
  getColorFunction actually reads for them, with no cross-field rule attached
  since there is nothing to cross-validate against. shouldFormItemUpdate now
  compares bound visibility directly so switching between e.g. `>` and `<`
  correctly swaps which field renders.
- Fix targetValueValidator's falsy guard (!targetValue || !compareValue) to
  use null/undefined checks instead, so a genuine 0 bound is no longer
  silently treated as "value absent, skip validation" — this also repairs
  the same latent hole for the pre-existing targetValueLeft/targetValueRight
  validators.
- Fix vacuous exact-match label queries in the "hides ..." tests and the
  "should display tooltip icon when extraColorChoices is provided" test
  (rescoped to the Color scheme field, matching its two already-fixed
  siblings, with an explicit toBeInTheDocument() check before use); factor
  the repeated `{ exact: false }` label-query pattern into shared
  boundLabelOptions/getBoundInputs test helpers.
- Update/add tests for the corrected per-operator bound visibility (only Max
  bound for `>`, only Min bound for `<`), and add a round-trip test
  confirming typed Min/Max bound values reach onChange under the literal
  `minBound`/`maxBound` field names Task 1's getColorFunction reads.
- Add a docs sentence on Min bound / Max bound to the conditional formatting
  section of creating-your-first-dashboard.mdx.
@bito-code-review

bito-code-review Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #10af74

Actionable Suggestions - 0
Additional Suggestions - 2
  • superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts - 1
    • Duplicated bound fallback logic · Line 122-123
      The `minBound ?? Math.min(...allValues)` / `maxBound ?? Math.max(...allValues)` fallback is repeated in five comparator cases (None, GreaterThan, LessThan, GreaterOrEqual, LessOrEqual). If the bound semantics change, all five must be updated in lockstep. Consider extracting `getCutoffValue`/`getExtremeValue` helpers to keep the fallback logic in one place.
  • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx - 1
    • Mock not cleared between tests · Line 603-606
      `mockOnChange` is a module-level `jest.fn()` never cleared between tests. The 'submits' test reads `mockOnChange.mock.calls[mockOnChange.mock.calls.length - 1]`, assuming the last call is from this test. Earlier tests (e.g. 'calls onChange when Apply button is clicked') also invoke `onChange`, so a late async call from a prior test could make this assertion stale and order-dependent. Add `beforeEach(() => mockOnChange.mockClear())`.
Review Details
  • Files reviewed - 7 · Commit Range: 1fd7d49..1dde110
    • docs/docs/using-superset/creating-your-first-dashboard.mdx
    • superset-frontend/packages/superset-ui-chart-controls/src/types.ts
    • superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts
    • superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts
    • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx
    • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx
    • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/types.ts
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • Eslint (Linter) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@github-actions github-actions Bot added doc Namespace | Anything related to documentation packages labels Sep 3, 2026
@netlify

netlify Bot commented Sep 3, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 61ae521
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a9abb3533aa7c0008b53a70
😎 Deploy Preview https://deploy-preview-43820--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.44751% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 79.43%. Comparing base (8d0637e) to head (61ae521).

Files with missing lines Patch % Lines
...-ui-chart-controls/src/utils/getColorFormatters.ts 98.70% 1 Missing ⚠️

❌ Your project check has failed because the head coverage (99.97%) is below the target coverage (100.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##           master   #43820      +/-   ##
==========================================
+ Coverage   79.42%   79.43%   +0.01%     
==========================================
  Files        2895     2895              
  Lines      167997   168057      +60     
  Branches    38903    38982      +79     
==========================================
+ Hits       133437   133504      +67     
+ Misses      32061    32053       -8     
- Partials     2499     2500       +1     
Flag Coverage Δ
javascript 75.00% <99.44%> (+0.05%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@bito-code-review

bito-code-review Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #9aec91

Actionable Suggestions - 0
Additional Suggestions - 1
  • superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts - 1
    • None-op single bound drops color · Line 131-133
      The `cutoffValue > extremeValue` guard also fires when only one manual bound is set outside the data range (e.g. `maxBound: 30` with data `[50,100]` → `cutoffValue=50`, `extremeValue=30`), so no cells get colored. The UI validators only enforce `min < max` when both bounds are set, so this is reachable. Restrict the guard to `minBound !== undefined && maxBound !== undefined` so the `opacityValue` clamping branch still colors values.
Review Details
  • Files reviewed - 4 · Commit Range: 1dde110..24d3f1f
    • superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts
    • superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts
    • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx
    • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • Eslint (Linter) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

… prove per-field wiring

The previous version of this test only asserted each of lowColor/midColor/
highColor was "some string" after clicking a swatch on each picker. Since
none of the three ColorPickerControls pass distinct presets, clicking "the
first swatch" on all three picked the identical default color three times,
so the test could not have caught a swap between the three fields' `name`
props. Rewritten to click a different preset index per field and assert
the onChange payload against the exact color each click resolved to.
The fix round that renamed pickFirstPresetColor to pickPresetColorAt
appended a new doc comment without removing the old one, leaving a
paragraph describing the deleted helper's single-swatch behavior next
to a verbatim-duplicated paragraph, sitting above the actual code.
EnxDev and others added 3 commits September 3, 2026 19:20
…und/centerValue

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… percent-bound resolution

Math.max(...array) throws RangeError past ~125k elements; the Sum
denominator already used a safe reduce, so switch Max to match. Also
move the numeric-values filter inside the percent-mode guard so rules
that never use this feature don't pay its cost.
@bito-code-review

bito-code-review Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #786333

Actionable Suggestions - 0
Additional Suggestions - 1
  • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx - 1
    • Missing min-bound validator test · Line 113-116
      The max-bound validation branch is covered by a test, but the min-bound branch (`centerValueMinValidator`, lines 113-116) has no test. Add one mirroring the existing max-bound test to assert the rejection message and that `onChange` is not called, so both diverging validators are verified.
Review Details
  • Files reviewed - 6 · Commit Range: 24d3f1f..d693beb
    • superset-frontend/packages/superset-ui-chart-controls/src/types.ts
    • superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts
    • superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts
    • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx
    • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx
    • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/types.ts
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • Eslint (Linter) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@bito-code-review

bito-code-review Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #200270

Actionable Suggestions - 0
Additional Suggestions - 3
  • superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts - 1
    • Redundant filter recomputation · Line 157-159
      `numericColumnValues` is now recomputed on every `resolvePercentBound` call, which runs 3× (min/max/center) when `boundUnit` is Percent. On the large-column path this commit targets, that's 3 redundant O(n) filter passes. Compute it once before `resolvePercentBound`, still guarded by `boundUnit === BoundUnit.Percent`.
  • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx - 1
    • Stale percentDenominator preserved · Line 307-307
      The `percentDenominator` FormItem unmounts when `boundUnit` is switched back to 'Value', but antd's default `preserve=true` keeps its value in the store, so a stale `percentDenominator` is still submitted via `onChange`. `getColorFormatters.resolvePercentBound` ignores it when `boundUnit !== Percent`, so there's no rendering impact, but the stored config retains a meaningless value. Consider `preserve={false}`.
  • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx - 1
    • Label query consistency · Line 892-892
      These queries rely on the Select's `aria-label` matching exactly, since the rendered label text is 'Bound unit (optional)' (see the `boundLabelOptions = { exact: false }` comment above). This works today but diverges from the file's established pattern and would silently break if the Select's aria-label behavior changes. Consider reusing `boundLabelOptions` for consistency.
Review Details
  • Files reviewed - 7 · Commit Range: d693beb..5c73fb4
    • superset-frontend/packages/superset-ui-chart-controls/src/types.ts
    • superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts
    • superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts
    • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx
    • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx
    • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/constants.ts
    • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/types.ts
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • Eslint (Linter) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

…o Column max

percentDenominator had no initialValue, so switching Bound unit to
"% of column" left the denominator select blank even though
getColorFunction already treats an unset percentDenominator as Column
max. Make the default explicit in the UI (matching how boundUnit
already gets an initialValue) instead of only in the resolver.

Flagged in external PR review.
…nation

resolvePercentBound computes its sum/max denominator from whatever
rows are currently loaded. Under server pagination that's one page at
a time (AG Grid Table and legacy Table both build a distinct
row_limit/row_offset query per page), so the same rule would resolve
a different denominator -- and therefore different colors -- on each
page, defeating the whole point of "bounds" (keeping the scale fixed
against exactly this kind of data-window instability).

Thread a serverPagination flag from each plugin's control panel
(where the server_pagination control's value already lives) down
through ConditionalFormattingControl -> FormattingPopover ->
FormattingPopoverContent, and disable the "% of column" Bound unit
option in that mode rather than let it silently produce inconsistent
colors. An already-saved config keeps working and stays visible,
since disabling an option only blocks new selections.

Flagged in external PR review.
…semantics

resolvePercentBound previously used the raw signed sum/max as the
denominator. A negative denominator (e.g. an all-losses column) could
resolve minBound above maxBound, which the None branch's own guard
then treats as an invalid range and silently disables coloring
entirely. A zero denominator (e.g. a column summing to zero) collapsed
both bounds onto the same point, which getOpacity's
cutoffValue===extremeValue guard turns into full opacity for every
value -- coloring the whole column at maximum intensity regardless of
its actual spread.

Take the absolute magnitude of the denominator so the resolved bounds
stay correctly ordered, and treat a zero denominator the same as "no
numeric values" (bound stays unset, falling back to the column's own
data-derived range) instead of producing a degenerate single-point
scale.

Flagged in external PR review.
… active

The diverging low/mid/high scale always interpolates a color -- it has
no solid, non-gradient rendering to fall back to -- so the "Use
gradient" checkbox had no effect on it. It stayed visible regardless,
so a user could uncheck it while a complete diverging config was set
and see no change, with no indication why.

Watch the diverging fields via Form.useWatch (added to the shared
Form wrapper, matching its existing useForm/Item/List exposure) and
hide the checkbox once a config counts as diverging by the same
field-presence check getColorFunction uses, rather than gate diverging
on the checkbox itself: silently reverting a fully-configured
three-color rule back to plain opacity coloring because of an
unrelated, easily-left-over checkbox state would be more surprising,
not less.

Flagged in external PR review.
Tighten a few comments left overly long from the review-fix round,
and drop a comment's reference to the review finding it addressed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@amaannawab923 amaannawab923 left a comment

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.

Had a proper read through this one. Nice work, the shared-util approach means all the surfaces get it for free, and a couple of the edge cases are handled better than I expected. Disabling % of column under server pagination with the tooltip explaining why is a good catch, and using reduce instead of a spread for the denominator on big columns is the right instinct.

Five things I'd want a second look at, mostly around what happens silently when a config isn't quite valid. None of them block the approach.

Also checked two things that turned out to be fine, so ignore if you already knew: Pivot's rollup totals don't pollute the percent denominator since master already narrows colorScaleRows to leaf rows, and the fractional RGB out of getDivergingColor is safe because rgbaToHex rounds and clamps.

// % of column derives its denominator from the loaded rows, which under
// server pagination is just the current page -- disable picking it there
// so the scale doesn't drift per page. Existing saved configs keep working.
const boundUnitSelectOptions = serverPagination

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.

Disabling the option here is the right call and the comment explaining it is helpful.

Thing is it's only a UI guard. If someone saved a rule with boundUnit: percent and server pagination gets switched on afterwards, the rule still hits resolvePercentBound at runtime and resolves against whatever page happens to be loaded. There's nothing in the util that knows about pagination, so the colours just quietly differ page to page, which is the exact drift this disable is meant to stop.

The comment says an already-saved config keeps working, so this might be deliberate. If it is, worth saying so out loud somewhere, because the failure is invisible. Otherwise either the util bails out of percent mode, or turning on server pagination warns about existing rules.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. This is addressed in 3ae2f00. getColorFormatters now accepts a disablePercentBounds flag, and both Table and AG Grid pass serverPagination through to it. As a result, an already-saved percent rule is suppressed instead of being evaluated against the loaded page. The saved configuration remains visible and editable, and the tooltip explains why percent mode is unavailable under server pagination.

}
// Use the magnitude so a negative denominator doesn't flip the scale;
// a zero denominator falls back to unset rather than collapsing it.
const denominatorValue = Math.abs(

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.

The magnitude makes sense for the sum case, but Column max gets weird when every value in the column is negative.

Say the column is [-100, -5]. Max is -5, Math.abs makes it 5, so a maxBound of 100% resolves to +5. The scale ends up as [-100, 5] and every real value sits squashed in the lower part of it.

You could argue 100% of a column whose max is -5 should just be -5. Right now it's neither that nor a no-op, it's a third thing. Probably worth picking one deliberately.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. Since the option is Column max, preserving the actual signed maximum is the least surprising behavior. Addressed in f8c9745dc3: Column sum still uses its magnitude, while Column max retains its sign. A regression test with [-100, -5] verifies that 100% resolves to -5, and the documentation makes the distinction explicit. This commit will appear on the PR after the next push.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Follow-up in 792a456: testing the documented minBound=0 and maxBound=100 case showed that preserving a negative maximum reverses the resolved endpoints. A non-positive Column max denominator now treats the percentage bounds as unset and falls back to the ordered, data-derived range. The regression covers [-100, -5] with both 0% and 100%, and the docs describe the fallback.

cutoffValue: targetValue!,
extremeValue: Math.max(...allValues),
extremeValue:
maxBound !== undefined && maxBound > targetValue

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.

If someone sets a maxBound that isn't greater than targetValue, the bound gets dropped and it quietly falls back to Math.max(...allValues). Same at :267, and the mirror image for minBound on the < and <= branches.

Being defensive in the util is fine. It's more that from the author's side this is "I typed a bound and nothing happened". If the popover already catches it cross-field then this is just belt and braces and all good. If not, a config coming in through import or the API misbehaves with nothing surfaced anywhere.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The popover catches this for Value mode: > and >= require maxBound > targetValue, while < and <= require minBound < targetValue. In percent mode the direct comparison is intentionally skipped because the bound and target use different units until the column denominator is known. The formatter fallback remains as a defensive path for imported or API-provided configurations. The validation behavior is included in 3ae2f00.

typeof cutoffValue === 'number' &&
typeof extremeValue === 'number' &&
centerValue > cutoffValue &&
centerValue < extremeValue;

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.

Strict > and < here means a centerValue that lands exactly on the resolved min or max drops the whole thing back to single-hue, silently.

Easier to hit than it looks in percent mode, where the centre is computed rather than typed. A centre of 0% or 100% resolves straight onto the bound.

Falling back rather than rendering something broken is the right instinct. Just worth making sure the popover shows why, otherwise the author sets three colours, gets one, and has no idea what happened.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed that the popover surfaces this before submission. Its center validators use strict comparisons, so centerValue must be greater than minBound and smaller than maxBound; equality at either endpoint is rejected. The formatter fallback remains defensive for imported or API-provided invalid configurations.


Each rule has a **"Use gradient"** toggle: enabled applies a varying opacity (lighter = further from threshold), disabled applies a solid fill at full opacity regardless of value.

For numeric rules, the optional **"Min bound"** / **"Max bound"** fields let you override the auto-detected color range with fixed values instead of relying on the minimum/maximum found in the data — useful when you want consistent coloring across dashboards or data refreshes.

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.

This covers Min/Max bound well, but the diverging low/mid/high scale and the % of column bound unit aren't in here.

Those two are the harder ones to work out from the popover on your own, and percent has a real constraint worth writing down somewhere permanent, namely that it's not available under server pagination.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in 3ae2f00. The documentation now explains the diverging low/mid/high scale, how percent bounds use Column max or Column sum, and why percent mode is unavailable with server pagination.

@msyavuz msyavuz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Feature looks good and the tests are thorough. Two things inline, plus +1 on the docs and negative-max comments already raised.

) =>
columnConfig?.reduce(
(acc: ColorFormatters, config: ConditionalFormattingConfig) => {
if (disablePercentBounds && config.boundUnit === BoundUnit.Percent) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This drops the whole rule, so turning on server pagination silently removes all coloring for that column. Better to treat the percent bounds as unset here and fall back to data min/max instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 792a456. Enabling server pagination no longer drops the formatter. For a saved percentage rule, only minBound, maxBound, and centerValue are ignored and the rule falls back to the automatic data range, preserving comparator and solid-color behavior. The shared-util regression and the AG Grid transform regression now assert that the formatter remains present. The tooltip and docs also make this fallback explicit.

'Value: type the exact numbers used for coloring below. % of column is unavailable with Server pagination enabled, since each page would compute a different percentage.',
)
: t(
'Value: type the exact numbers used for coloring below. % of column: type a percentage of the column total instead, so the rule keeps working as the data changes.',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

"percentage of the column total" is wrong when the denominator is Column max (the default).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 792a456. The tooltip now says that percent bounds use the column maximum or sum selected below. The server-pagination version also explains that existing percentage rules use the automatic data range while pagination is enabled.

@bito-code-review

bito-code-review Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #7d3163

Actionable Suggestions - 0
Additional Suggestions - 1
  • superset-frontend/plugins/plugin-chart-table/src/transformProps.ts - 1
    • Missing test coverage · Line 763-764
      This change suppresses percent bounds under server pagination via `getColorFormatters(..., undefined, serverPagination)`, but unlike the identical ag-grid change it ships with no test. The table plugin has no `transformProps.test.ts` and `TableChart.test.tsx` never asserts `columnColorFormatters`. Add a case mirroring ag-grid's 'retains saved percentage rules with automatic bounds' to lock in this behavior.
Review Details
  • Files reviewed - 14 · Commit Range: 5c73fb4..792a456
    • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.test.tsx
    • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopoverContent.tsx
    • superset-frontend/plugins/plugin-chart-ag-grid-table/src/controlPanel.tsx
    • superset-frontend/plugins/plugin-chart-table/src/controlPanel.tsx
    • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/ConditionalFormattingControl.tsx
    • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/FormattingPopover.tsx
    • superset-frontend/src/explore/components/controls/ConditionalFormattingControl/types.ts
    • superset-frontend/packages/superset-ui-chart-controls/src/utils/getColorFormatters.ts
    • superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts
    • superset-frontend/packages/superset-ui-core/src/components/Form/Form.tsx
    • docs/docs/using-superset/creating-your-first-dashboard.mdx
    • superset-frontend/plugins/plugin-chart-ag-grid-table/src/transformProps.ts
    • superset-frontend/plugins/plugin-chart-ag-grid-table/test/transformProps.test.ts
    • superset-frontend/plugins/plugin-chart-table/src/transformProps.ts
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • Eslint (Linter) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers an incremental AI Review.

  • /review full - Manually triggers a full AI Review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@amaannawab923

Copy link
Copy Markdown
Contributor

did a round of manual testing on this branch with a small dataset and hit a few cases worth a look. all of these are on the table chart, so they go through the shared getColorFormatters path rather than anything plugin specific.

1. a column of all negative values gets no colour at all

same formatting rule on both columns, gradient with percent bounds, 0 to 100 percent of max. neg_col is -10 to -40, pos_col is 10 to 40.

cf-01-negative-column-no-color

pos_col gets the full gradient, neg_col gets nothing. resolvePercentBound returns (bound / 100) * denominatorValue with the denominator wrapped in Math.abs, so for any non negative bound percentage the resolved bound is always >= 0 and no bound can ever land below zero. every value in an all negative column then falls under the lower bound and comes out at zero alpha. dropping the Math.abs, or resolving the percent against the actual min/max range of the column, would probably cover it.

2. percent of sum saturates when the signed sum lands near zero

profit by region, same gradient but with percentDenominator: sum:

cf-02-percent-of-sum-saturates

the signed sum here is 1, so 100 percent resolves to a bound of 1 and both positive rows saturate to the same red. the 25 percent gap between north and east is invisible, and the two negative rows read as blank cells rather than as bad values. there is already a denominatorValue === 0 guard so the case looks anticipated, it just only catches the exact zero point. summing absolute values for the denominator would handle it, and would also make percent of sum read as share of total, which is probably closer to what someone picking that option expects.

3. center value sitting on the min or max drops the diverging config with no feedback

the description calls this fallback out as intended, so this one is more of a ux note than a bug. three colour diverging, bounds 10 to 40, only centerValue changes between the two runs.

center 25:

cf-03a-center-25-diverging-works

center 40:

cf-03b-center-40-diverging-dropped

the three colours are dropped and it falls back to the single hue colorScheme. isValidDivergingConfig uses strict > and < against cutoff and extreme, so a center sitting exactly on either end fails validation. the fallback itself is reasonable, but from the panel the rule still looks saved and correct, so there is nothing to tell you it was ignored. surfacing it in the control would save some head scratching.

repro data
-- cases 1 and 3
SELECT 'A' AS label, -10 AS neg_col, 10 AS pos_col
UNION ALL SELECT 'B', -20, 20
UNION ALL SELECT 'C', -30, 30
UNION ALL SELECT 'D', -40, 40;

-- case 2
SELECT 'North' AS region, 500 AS profit
UNION ALL SELECT 'South', -499
UNION ALL SELECT 'East', 400
UNION ALL SELECT 'West', -400;

@amaannawab923

Copy link
Copy Markdown
Contributor

quick follow up on the above. i had tested that against 3ae2f00 and missed the two commits that landed after it, so point 1 was already out of date when i posted. pulled the branch again and re-ran all three cases on 792a456.

point 1 (all negative column gets no colour) does not reproduce anymore. the signed percent maximum change covers it. a non positive column maximum now makes the percent bound resolve to undefined and the scale falls back to the automatic data range, so the column renders a normal gradient instead of nothing.

points 2 and 3 still reproduce on the latest head, re-checked just now:

  • percent of sum on 500 / -499 / 400 / -400 still gives north and east the exact same fully opaque red, and both negative rows still come out fully transparent. the sum branch still takes Math.abs of the total, and the signed sum of 1 passes the <= 0 guard.
  • diverging with the center sitting on the maximum still falls back to the single hue scale, isValidDivergingConfig is unchanged.

sorry for the noise on the first one.

@msyavuz msyavuz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks, the pagination degrade and tooltip look right now. Three small things on the latest commits.


export const percentDenominatorOptions = [
{ value: PercentDenominator.Max, label: t('Column max') },
{ value: PercentDenominator.Sum, label: t('Sum of magnitudes') },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

"Sum of magnitudes" is going to read as jargon to most authors; for the common all-positive column it's just the column sum. I'd keep Column sum as the label and mention the absolute-value handling in the tooltip.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed. Fixed in 61ae521: the author-facing label is back to Column sum. The Bound unit tooltip and documentation explain that this option adds the absolute values so mixed signs do not cancel into an unstable denominator.

passedData,
theme,
undefined,
serverPagination,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The AG Grid transform has a test for this pass-through but the legacy table doesn't; worth mirroring it here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in 61ae521. The legacy Table transform test now mirrors the AG Grid coverage: with server pagination enabled, a saved percentage rule remains present and its formatter uses the automatic data range rather than the percentage bounds.

normalize={normalizeOptionalNumber}
validateTrigger="onBlur"
tooltip={t(
'Optional. When set together with Low color, Mid color, and High color below, colors diverge from Mid color at this value toward Low color below it and High color above it, instead of a single color fading in and out.',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

With the sum denominator, a center of e.g. 50% only lands inside the data range when a single row holds more than half the total, so in practice it always falls back to single hue. Should center value be scoped to Column max, or at least called out here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Called this out in 61ae521 rather than restricting centerValue to Column max, because a sum-based center can still be valid when the configured range contains it. The Center value tooltip and documentation now state that a Column sum percentage must resolve inside the color range; otherwise the rule uses its single-color fallback.

@EnxDev

EnxDev commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

@amaannawab923 thanks for re-testing against the newer head. The two remaining points are addressed on 61ae521:

  • Point 2 was fixed in 2abe36b. Column sum now adds each value magnitude instead of taking the magnitude of the signed total, so 500 / -499 / 400 / -400 uses 1799 rather than 1 as its denominator. That exact shape has a regression test. With a configured minimum of 0%, negative values remain transparent by design; use a negative minimum or a diverging scale when they should receive distinct colors.
  • Point 3 is enforced in the control: a center equal to min or max is rejected on blur and Apply with a validation message. 2abe36b adds explicit center == min and center == max tests that verify Apply is blocked. 61ae521 also explains that a Column sum percentage center must resolve inside the color range; the runtime single-color fallback remains only as defense for imported or legacy invalid configurations.

@EnxDev
EnxDev requested a review from msyavuz September 4, 2026 12:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc Namespace | Anything related to documentation packages plugins size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants