Skip to content

feat: apply chart number formatting to CSV/Excel chart-data exports - #45817

Open
eugeneo17 wants to merge 18 commits into
apache:masterfrom
CloudGeometry:fix/45660-export-formatted-numbers
Open

eugeneo17 wants to merge 18 commits into
apache:masterfrom
CloudGeometry:fix/45660-export-formatted-numbers

Conversation

@eugeneo17

Copy link
Copy Markdown

SUMMARY

Chart-data CSV and Excel exports serialize raw numeric values and ignore the
number, currency and small-number formats configured on the chart, so exported
cells do not match what the chart displays (for example 19.7999992 instead of
19.8). Fixes #45660.

This adds a reusable apply_export_number_formats helper in
superset/charts/client_processing.py that resolves each numeric column's
configured d3 format, currency and small-number format the same way the Table
viz does -- column_config overrides first, then the datasource's saved
per-metric formats -- and applies it through the existing format_column
helper. Columns with nothing configured fall back to fixed-point text instead
of raw floats; non-numeric columns, nulls, NaN and infinity pass through
unchanged. The SQL Lab export path is left untouched.

The behavior is gated behind a new EXPORT_FORMATTED_NUMBERS feature flag
(default False), so existing exports are byte-for-byte unchanged unless it is
explicitly enabled.

TESTING INSTRUCTIONS

  1. Set EXPORT_FORMATTED_NUMBERS to True in your feature flags.
  2. Build a Table chart on a numeric column and configure a D3 number format or
    currency on it.
  3. From the chart menu, export the data as CSV or Excel.
  4. Numeric cells reflect the configured format instead of raw values. With the
    flag off, the exported bytes are unchanged.

New unit tests:

  • tests/unit_tests/charts/test_export_number_format.py
  • tests/unit_tests/common/test_query_context_processor_export_format.py

ADDITIONAL INFORMATION

Chart-data CSV and Excel exports serialize raw numeric values and ignore
the number, currency and small-number formats configured on the chart, so
exported cells do not match what the chart displays (e.g. 19.7999992
instead of 19.8). Fixes apache#45660.

Add a reusable apply_export_number_formats helper that resolves each
numeric column's configured d3 format, currency and small-number format
the same way the Table plugin does -- column_config overrides first, then
the datasource's saved per-metric formats -- and applies it through the
existing format_column helper. Columns with nothing configured fall back
to fixed-point text instead of raw floats; non-numeric columns, nulls,
NaN and infinity pass through unchanged.

The behavior is gated behind a new EXPORT_FORMATTED_NUMBERS feature flag
(default False), so exports are unchanged unless it is explicitly enabled.

Signed-off-by: CloudGeometry <eugeneo@cloudgeometry.com>
@netlify

netlify Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 5180710
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6aec9663feaf2562bd04db6b
😎 Deploy Preview https://deploy-preview-45817--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.

Comment thread superset/charts/client_processing.py Outdated
# treats as unset.
small_number_format = (config.get("d3SmallNumberFormat") or "").strip() or None

if number_format or currency.get("symbol") or small_number_format:

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.

Suggestion: Percent metrics default to PERCENT_3_POINT in the Table chart, but this condition sends them to the unconfigured fallback and exports fractions such as 0.3.

Assessment: 🟠 Major · 🔁 Occurrence: Sometimes · 🏷️ Logic error

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/charts/client_processing.py
**Line:** 1290:1290
**Comment:**
	*Logic Error: Percent metrics default to `PERCENT_3_POINT` in the Table chart, but this condition sends them to the unconfigured fallback and exports fractions such as `0.3`.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of e4c3bce.

Percent metric columns are detected by their prefixed labels and passed to format_column with PERCENT_3_POINT as the default format.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 8faccdf.

Percent metric columns are identified by their prefixed labels and formatted with number_format or PERCENT_3_POINT instead of falling through to the fixed-point fallback.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of dc15753.

Percent metric columns are identified by their prefixed labels and formatted with number_format or PERCENT_3_POINT instead of falling through to the fixed-point fallback.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of a934729.

Percent metric columns are now detected by their prefixed labels and formatted with PERCENT_3_POINT when no number format is configured.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 78351a9.

Percent metric columns are identified by their prefixed labels and explicitly formatted with PERCENT_3_POINT when no number format is configured.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of ecb3342.

Percent metric columns are identified by their prefixed labels and explicitly formatted with PERCENT_3_POINT when no number format is configured.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 5180710.

Percent metric columns are identified by their prefixed labels and explicitly formatted with PERCENT_3_POINT when no number format is configured.

If that's not right, unresolve this thread and CodeAnt will leave it open.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Handled in the current revision. apply_export_number_formats matches percent-metric columns by their %-prefixed label (percent_metric_labels) and passes number_format or PERCENT_3_POINT to format_column, so a percent metric defaults to PERCENT_3_POINT like the Table plugin instead of falling through to the fixed-point fallback and exporting a fraction.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of e944e0a.

Percent-metric columns are identified by their prefixed labels and formatted with number_format or PERCENT_3_POINT instead of using the unconfigured fallback.

If that's not right, unresolve this thread and CodeAnt will leave it open.

Comment on lines +1291 to +1298
format_column(
df,
column,
number_format,
currency,
small_d3_format=small_number_format,
small_currency=small_currency,
)

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.

Suggestion: AUTO is passed without detected currency or row currency context, so it remains unresolved and exports amounts without the configured currency symbol.

Assessment: 🟠 Major · 🔁 Occurrence: Sometimes · 🏷️ Api mismatch

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/charts/client_processing.py
**Line:** 1291:1298
**Comment:**
	*Api Mismatch: `AUTO` is passed without detected currency or row currency context, so it remains unresolved and exports amounts without the configured currency symbol.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

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.

Agreed—this format_column call gets neither detected_currency nor a row currency context, whereas table() passes both, so an AUTO currency (from column_config or a saved metric) resolves to no symbol: 1234.5 with ,.2f + AUTO over a USD currency column exports 1,234.50 while the chart shows the $ symbol. Should the export resolve AUTO the way table() does?

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.

Yes. This is a real chart/export mismatch: these calls pass the currency configuration but omit the detected currency and row-currency context that table() uses, so AUTO cannot resolve to the chart’s symbol.

The export helper should receive and forward the same context as table() when calling format_column. Add a regression test with an AUTO format and USD currency context, asserting the export includes $ just as the chart does. If that context is unavailable for an export, it should follow table()’s fallback behavior rather than silently claiming to match the chart.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 8faccdf. apply_export_number_formats now mirrors table(): it builds a per-row currency context from the datasource's currency-code column and passes it to format_column, with the query-wide single currency (detected from the frame already in hand, no extra query) as the fallback_to_detected=False fallback. An AUTO currency now resolves to the row's symbol ($1,234.50) instead of exporting bare. Added test_auto_currency_resolves_from_row_context.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 8faccdf.

The formatting call now receives both detected_currency and row_currency_context, allowing AUTO currency formats to use the detected currency or per-row currency.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 78351a9.

format_column now receives both detected_currency and row_currency_context for configured currency formats.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of ecb3342.

format_column now receives both detected_currency and row_currency_context for configured currency formats.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 5180710.

format_column now receives both detected_currency and row_currency_context for configured currency formats.

If that's not right, unresolve this thread and CodeAnt will leave it open.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Handled in the current revision. apply_export_number_formats resolves AUTO the way table() does: a per-row currency context from the datasource currency-code column (row_currency_context), a query-wide detected_currency gated on auto_currency_configured, and the top-level currency_format fallback, all forwarded to format_column. So an AUTO currency exports with its symbol rather than a bare amount.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of e944e0a.

The formatting call now receives both detected_currency and row_currency_context when formatting configured currency columns.

If that's not right, unresolve this thread and CodeAnt will leave it open.

Comment on lines +650 to +655
apply_export_number_formats(
df,
coltypes,
self._query_context.form_data or {},
self._qc_datasource,
)

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.

Suggestion: On XLSX exports, excel.apply_column_types converts formatted strings like 3.50 back to numeric 3.5, so the exported cell loses its configured decimal places.

Assessment: 🟠 Major · 🔁 Occurrence: Sometimes · 🏷️ Type error

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** superset/common/query_context_processor.py
**Line:** 650:655
**Comment:**
	*Type Error: On XLSX exports, `excel.apply_column_types` converts formatted strings like `3.50` back to numeric `3.5`, so the exported cell loses its configured decimal places.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of e4c3bce.

For formatted XLSX exports, numeric columns are passed to excel.apply_column_types as strings, preventing formatted values such as 3.50 from being coerced back to numbers.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 8faccdf.

For formatted numeric exports, numeric column types are changed to GenericDataType.STRING before excel.apply_column_types, preserving the formatted decimal places in XLSX.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of dc15753.

For formatted numeric exports, numeric column types are changed to GenericDataType.STRING before excel.apply_column_types, preserving the formatted decimal places in XLSX.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of a934729.

For XLSX exports with formatted numeric values, numeric column types are changed to strings before apply_column_types, preserving configured decimal places.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 78351a9.

For formatted XLSX exports, numeric column types are changed to string before apply_column_types, preserving the formatted decimal places.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of ecb3342.

For formatted XLSX exports, numeric column types are changed to string before apply_column_types, preserving the formatted decimal places.

If that's not right, unresolve this thread and CodeAnt will leave it open.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of 5180710.

For formatted XLSX exports, numeric column types are changed to string before apply_column_types, preserving the formatted decimal places.

If that's not right, unresolve this thread and CodeAnt will leave it open.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Handled in the current revision. In the XLSX branch of get_data, with the flag on, every numeric column is remapped to GenericDataType.STRING before apply_column_types precisely so "3.50" is not coerced back to the number 3.5. Covered by test_xlsx_export_keeps_configured_decimals_as_text, which reads the raw cell and asserts data_type 's' with value '3.50'.

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.

✅ CodeAnt verified this suggestion was addressed in subsequent commits and marked this thread resolved as of e944e0a.

For formatted XLSX exports, numeric columns are passed to apply_column_types as strings, preserving rendered decimal places.

If that's not right, unresolve this thread and CodeAnt will leave it open.

@codeant-ai-for-open-source

codeant-ai-for-open-source Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

CodeAnt  CodeAnt PR Risk

🟢  Low Risk

  • The PR appears safe to merge; percent formatting, AUTO currency resolution, and XLSX decimal preservation are addressed and covered by regression tests.

Assessed commit: e944e0ac97dc

Comment thread superset/charts/client_processing.py Resolved
Comment thread superset/charts/client_processing.py Resolved
Comment thread superset/config.py Resolved
@codecov

codecov Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 12.82051% with 68 lines in your changes missing coverage. Please review.
✅ Project coverage is 65.41%. Comparing base (230d9b0) to head (e944e0a).
⚠️ Report is 6 commits behind head on master.

Files with missing lines Patch % Lines
superset/charts/client_processing.py 7.69% 48 Missing ⚠️
superset/common/query_context_processor.py 21.42% 11 Missing ⚠️
superset/utils/excel.py 25.00% 9 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (230d9b0) and HEAD (e944e0a). Click for more details.

HEAD has 3 uploads less than BASE
Flag BASE (230d9b0) HEAD (e944e0a)
python 7 5
postgres 2 1
Additional details and impacted files
@@             Coverage Diff             @@
##           master   #45817       +/-   ##
===========================================
- Coverage   85.03%   65.41%   -19.63%     
===========================================
  Files        3080     3080               
  Lines      209437   209589      +152     
  Branches    49385    49402       +17     
===========================================
- Hits       178095   137097    -40998     
- Misses      27960    69360    +41400     
+ Partials     3382     3132      -250     
Flag Coverage Δ
hive 34.17% <12.82%> (-0.03%) ⬇️
mysql 51.97% <12.82%> (-0.05%) ⬇️
postgres 51.99% <12.82%> (-0.05%) ⬇️
presto 35.83% <12.82%> (-0.03%) ⬇️
python 52.29% <12.82%> (-35.67%) ⬇️
sqlite 51.74% <12.82%> (-0.05%) ⬇️
unit ?

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.

Apply ruff format to the two new unit test files and regenerate
docs/static/feature-flags.json so it includes the new
EXPORT_FORMATTED_NUMBERS flag, fixing the pre-commit CI job.

Signed-off-by: CloudGeometry <eugeneo@cloudgeometry.com>
@github-actions github-actions Bot added the doc Namespace | Anything related to documentation label Oct 28, 2026
eugeneo17 and others added 5 commits October 28, 2026 11:43
get_data() is typed as str | bytes | list[dict], so the export tests
needed to narrow that union to bytes before decoding / reading XLSX, and
the generic dict/list annotations needed type parameters. Also cast the
Explorable datasource to the helper's BaseDatasource | Query | None at
the single call site in get_data().

Signed-off-by: CloudGeometry <eugeneo@cloudgeometry.com>
eugeneo17 and others added 2 commits October 29, 2026 06:33
Resolve the correctness and scope issues raised in review:

- Do not double-process POST_PROCESSED exports. get_data() now skips the
  pre-serialization formatting for the post-processed path, where
  apply_client_processing re-parses the CSV and runs pivot/table
  arithmetic; formatting a thousands-separated value there coerced the
  pivot totals to NaN. The tabular export formats are instead formatted
  after post-processing, reusing the existing table()/pivot_table_v2()
  formatters.
- Keep XLSX cells as text once formatted, so apply_column_types no longer
  coerces e.g. "3.50" back to the number 3.5 and drops the decimals.
- Format finite floats and large integers as exact fixed-point text in the
  unconfigured fallback, so 1e-07 / 19.7999992 no longer leak and large
  ints are not rounded by a float64 re-inference.
- Honor the AG Grid small-number exemption and default percent-metric
  columns to PERCENT_3_POINT, mirroring the live Table plugin.
- Document the flag's scope: it does not cover the streaming CSV export
  path, and it only resolves the per-column formats a table-like viz
  carries (not chart-level keys such as a Time-series y_axis_format).

Add tests pinning config-over-saved-format precedence, small-number
forwarding, the float / large-int fallback, the AG Grid and percent-metric
behavior, the XLSX decimal retention, and the post-processed skip.

Signed-off-by: CloudGeometry <eugeneo@cloudgeometry.com>
- Only skip pre-serialization formatting for POST_PROCESSED exports whose
  viz_type has a post-processor (table / pivot_table_v2); other viz types
  (e.g. ag-grid-table in scheduled reports/alerts) have no post-processor,
  so apply_client_processing returns them unchanged and they must still be
  formatted in get_data.
- Resolve an AUTO currency in apply_export_number_formats the way table()
  does: a per-row currency context from the datasource currency-code column
  with the query-wide single currency (detected from the frame in hand) as
  fallback, so an AUTO currency exports with its symbol instead of bare.
- Tests: ag-grid POST_PROCESSED is formatted in get_data; round-trip proving
  apply_client_processing formats the aggregate after get_data skips it; AUTO
  currency resolves its symbol from the row context.

Signed-off-by: CloudGeometry <eugeneo@cloudgeometry.com>
The non-numeric/null cell test pinned the string column's null to None, but
under the pandas future.infer_string option a string column infers dtype str
and its null becomes NaN, so the DataFrame constructor itself (not this
helper) decides the marker. Assert the non-null cells are kept and the null
stays null via pd.isna, independent of the None/NaN representation.

Signed-off-by: CloudGeometry <eugeneo@cloudgeometry.com>
Comment thread superset/charts/client_processing.py Outdated
if coltype == GenericDataType.NUMERIC
}

for column in df.columns:

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.

If two numeric metrics share a verbose name (say both display as Total), get_data() renames both columns before this helper runs, so df[column] here is a two-column DataFrame. format_column then either hands whole Series to the per-cell formatter (the configured ,.2f is silently skipped and raw values ship) or, when a currency-code column is selected, zips the two columns against the per-row contexts and the strict=True zip raises ValueError, failing the whole CSV/XLSX export. Could the loop go by column position (or skip and dedupe duplicate labels) so a repeated verbose name can't change or abort the export?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in a934729. format_column now addresses the column by position (df.iloc[:, col_pos] / df.isetitem) instead of by label, so when two metrics share a verbose name the repeated label no longer turns df[label] into a two-column frame — it no longer silently skips the configured format, nor raises on the strict=True zip and aborts the export. Added two regression tests (the plain and the per-row-currency paths) that fail on the old label-based access.

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.

Positional writes fix the column lookup, but two same-label metrics still lose their own formats and values. number_format_by_label, currency_by_label and config_by_label are keyed by label, so with a saved as .2f and b as .0%, both shown as Total, a=1.5 exports as 150% instead of 1.50. For CSV the formatted columns are now strings, and df_to_escaped_csv writes them back with df.at[label, name], which assigns the first value to both same-named columns (1.50,2.50 exports as 1.50,1.50; I reproduced it on a two-column frame). Should format resolution key off the original column identity, and the string write-back avoid label-based .at?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done. apply_export_number_formats now iterates for col_pos, column in enumerate(df.columns) and addresses each column positionally (format_column(df, col_pos, ...), df.isetitem(col_pos, ...)), and format_column takes a positional col_pos: int. So two metrics sharing a verbose name no longer collapse into a two-column df[label] that skips formatting or aborts the export on the strict=True zip.

- Format export columns by position, not by label. Two metrics can share a
  verbose name, so get_data() renames distinct columns to the same label;
  a label lookup then returned a two-column frame and either silently skipped
  the configured format or raised on zip(strict=True), aborting the whole
  CSV/XLSX export. format_column now takes the column position and reads/writes
  via df.iloc / df.isetitem, so a repeated verbose name formats each column once.

Tests:
- per-row AUTO currency with mixed USD/EUR rows, asserting each row's own
  symbol (fails if the per-row currency context is dropped);
- two regression tests for duplicate verbose names (plain and currency paths);
- XLSX export reads the raw cell via openpyxl and asserts the ",.2f" value is
  the text "3.50" (data_type "s"), pinning that apply_column_types does not
  coerce the formatted string back to the number 3.5.

Signed-off-by: Eugene Olkhovski <eugeneo@cloudgeometry.com>
def format_column(
df: pd.DataFrame,
column: Any,
col_pos: int,

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.

Switching format_column to a positional col_pos leaves its existing callers in tests/unit_tests/charts/test_client_processing.py (around lines 3915-3942) passing "amount", so df.iloc[:, "amount"] raises a ValueError. That is why unit-tests (current) fails on three test_format_column_* tests, with the flag off. Should those callers pass the column position (0)?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. The direct format_column callers in test_client_processing.py (~lines 3945-3972) now pass the column position 0 instead of "amount", so the three test_format_column_* tests pass with the flag off. Verified locally: pytest tests/unit_tests/charts/test_client_processing.py -k format_column -> 4 passed.

text = _csv_text(result)
# Sub-one value rendered by the small-number format, the other by the main.
assert "12.5%" in text
assert "5" in text

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.

assert "5" in text is already satisfied by the 12.5% on the line above, so it does not show the 5.0 row takes the main ,.0f format; if that cell stayed 5.0 or disappeared the test would still pass. Could you parse the CSV and assert the amount cells are exactly ["12.5%", "5"]?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good point. test_small_number_format_forwarded_to_export now parses the CSV and asserts the amount column is exactly ['12.5%', '5'] (pd.read_csv(io.StringIO(text), dtype=str)), so a 5.0 or a dropped cell would fail. In e944e0a.

The dup-verbose-name fix changed format_column's second parameter from a
column label to a positional index (col_pos: int). The pre-existing
direct-call unit tests still passed the label 'amount', so df.iloc[:, 'amount']
raised ValueError and failed unit-tests CI. Address the positional signature
(single-column frames -> position 0); the df['amount'] read-back assertions are
unchanged.

Signed-off-by: Eugene Olkhovski <eugeneo@cloudgeometry.com>
# treat them as strings here; otherwise apply_column_types
# would coerce e.g. "3.50" back to the number 3.5 and drop
# the configured decimals.
export_coltypes = [

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.

Mapping every numeric column to STRING here sends negative numbers through excel._quote_formula, because - is in FORMULA_PREFIXES. With the flag on, an unconfigured -3.5 (or -3.50 under ,.2f) is stored as the literal text '-3.5, so the apostrophe shows up as cell content and the value is no longer numeric. Should the formula quoting be skipped for the formatted numeric columns, or should only configured columns become text?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in e944e0a. The number-formatted column positions are threaded into df_to_excel -> quote_formulas, which exempts a formatted negative (-3.50) in those columns from formula-quoting, matching the CSV writer's escape_value/_is_negative_number. So it is stored as -3.50 (data_type 's'), not the literal '-3.50. A genuine formula prefix (=/+/@) in those columns, and every other column (headers, warehouse strings), still quote exactly as before: the exemption is an opt-in per-column flag, so the base _quote_formula/df_to_excel behavior and the injection guard are unchanged. New tests: XLSX and CSV keep -3.50 unquoted, and a warehouse =1+1 string is still quoted.

"Total (Sum)": "15",
}
assert unformatted["big"].tolist() == df["big"].tolist()
# Licensed to the Apache Software Foundation (ASF) under one

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 file shows as rewritten in full (5794 lines changed) because the last commit saved it with CRLF line endings; on master it is LF. That buries the five real format_column(df, 0, ...) edits near line 3915 and will make blame and rebases noisy. Could you restore LF endings so the diff is only the call-site changes?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed. The test file is back to LF (the committed blob has no CRLF), so the diff is just the five format_column(df, 0, ...) call-site edits rather than a full-file rewrite. Re-pushed clean.

@github-actions github-actions Bot added the requires:rebase Requires rebasing on top of current master label Nov 3, 2026
Resolve conflicts from apache#45865 (pandas 3 fixes) in
superset/charts/client_processing.py and its unit test:

- format_column: keep the positional col_pos addressing (this PR's
  dup-verbose-name fix) over master's label-based df[column] apply; take
  master's pivot_df SUPERSET_PANDAS_NAN replace fix as-is.
- test_client_processing.py: re-base on master's copy (restoring LF line
  endings after an earlier accidental CRLF rewrite) and re-apply only the
  five positional format_column call updates.

Signed-off-by: Eugene Olkhovski <eugeneo@cloudgeometry.com>
@eugeneo17

Copy link
Copy Markdown
Author

Synced the branch with master to clear the merge conflict introduced by #45865 (pandas 3 fixes).

Resolution:

  • format_column: kept this PR's positional col_pos addressing (the duplicate-verbose-name fix) rather than master's label-based df[column] apply, and took fix(charts): pandas 3 fixes for box plot counts, pivot CSV totals and calendar labels #45865's pivot_df SUPERSET_PANDAS_NAN replacement as-is.
  • tests/unit_tests/charts/test_client_processing.py: restored LF line endings after an accidental CRLF rewrite in an earlier commit, so the diff vs master is back to just the five format_column positional call updates.

The branch is mergeable again; re-running CI after the sync.

…xport

Addresses the open apache#45817 review thread on the XLSX export path. With
EXPORT_FORMATTED_NUMBERS on, numeric columns are rendered to display text and
typed as strings so apply_column_types keeps their decimals. That routed a
formatted negative like -3.50 through excel._quote_formula, which (because '-'
is in FORMULA_PREFIXES) stored it as the literal text '-3.50 and dropped its
numeric meaning.

The CSV writer already exempts negative numbers (escape_value /
_is_negative_number); XLSX did not, so the two diverged. Thread the
number-formatted column positions into df_to_excel / quote_formulas and exempt
those columns' formatted negatives from quoting, matching CSV. A genuine
formula prefix (=/+/@) in those columns, and every other column (headers,
warehouse strings), still quote exactly as before, so the base df_to_excel
behavior and formula-injection guard are unchanged.

Tests: XLSX and CSV keep a formatted -3.50 unquoted; a warehouse '=1+1' string
is still quoted; and the small-number-format test now parses the column and
pins both cells exactly instead of a substring match.

Signed-off-by: Eugene Olkhovski <eugeneo@cloudgeometry.com>
@eugeneo17

Copy link
Copy Markdown
Author

@sadpandajoe thanks for the detailed review. All open threads are addressed; the branch is synced with master and green.

New in e944e0a:

  • Formatted negative numbers no longer formula-quoted in XLSX. The number-formatted column positions are threaded into df_to_excel/quote_formulas, which exempts a formatted negative (e.g. -3.50) from quoting in those columns only, matching the CSV writer. A genuine =/+/@ prefix there and every other column still quote, so the base _quote_formula/df_to_excel behavior and the injection guard are unchanged.
  • Small-number-format test now parses the column and pins both cells exactly (['12.5%', '5']).

Already in the current revision (replied inline on each):

  • Percent metrics default to PERCENT_3_POINT; AUTO currency fully resolved; XLSX keeps formatted decimals as text (numeric->STRING before apply_column_types); duplicate verbose names handled by positional col_pos; the three test_format_column_* callers pass 0; test file restored to LF (diff is just the five call-site edits).

Ran locally: test_query_context_processor_export_format (24), excel_tests (17), test_client_processing -k format_column (4) all green. Ready for another pass once CI re-runs (it re-gates to action_required on each push until a committer approves).

@github-actions github-actions Bot removed the requires:rebase Requires rebasing on top of current master label Nov 5, 2026

This branch has not been deployed

No deployments
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 size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Exported CSV/Excel files ignore locale-specific number formatting and may export incorrect numeric values

2 participants