This is an automated email from the ASF dual-hosted git repository.

rusackas pushed a commit to branch fix/issue-32369-pivot-rows-csv-wf612d443b
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 9f93591a0644a3880eded623611dd00597282cb4
Author: rusackas <[email protected]>
AuthorDate: Sun Jul 26 19:09:26 2026 -0700

    fix(pivot-table): keep each Rows field as its own CSV/XLSX column
    
    When a pivot table has multiple "Rows" fields selected, exporting to
    pivoted CSV (or scheduled XLSX reports) collapsed all of them into a
    single space-joined column instead of one column per field, unlike
    the table rendered in Explore.
    
    Fixes #32369
---
 superset/charts/client_processing.py              | 44 ++++++++++++++----
 tests/unit_tests/charts/test_client_processing.py | 55 ++++++++++++++++++++++-
 2 files changed, 89 insertions(+), 10 deletions(-)

diff --git a/superset/charts/client_processing.py 
b/superset/charts/client_processing.py
index e0c2db71af7..c765a534c7f 100644
--- a/superset/charts/client_processing.py
+++ b/superset/charts/client_processing.py
@@ -403,7 +403,7 @@ def apply_client_processing(  # noqa: C901
         # Check if the DataFrame has a default RangeIndex, which should not be 
shown
         show_default_index = not isinstance(processed_df.index, pd.RangeIndex)
 
-        # Flatten hierarchical columns/index since they are represented as
+        # Flatten hierarchical columns since they are represented as
         # `Tuple[str]`. Otherwise encoding to JSON later will fail because
         # maps cannot have tuples as their keys in JSON.
         processed_df.columns = [
@@ -414,14 +414,40 @@ def apply_client_processing(  # noqa: C901
             )
             for column in processed_df.columns
         ]
-        processed_df.index = [
-            (
-                " ".join(str(name) for name in index).strip()
-                if isinstance(index, tuple)
-                else index
-            )
-            for index in processed_df.index
-        ]
+
+        if query["result_format"] == ChartDataResultFormat.JSON:
+            # JSON object keys must be strings, so a hierarchical (multi-level)
+            # row index has to be flattened into a single string per row.
+            processed_df.index = [
+                (
+                    " ".join(str(name) for name in index).strip()
+                    if isinstance(index, tuple)
+                    else index
+                )
+                for index in processed_df.index
+            ]
+        elif (
+            isinstance(processed_df.index, pd.MultiIndex)
+            and processed_df.index.nlevels > 1
+        ):
+            # For tabular formats (CSV/XLSX) keep each "Rows" field as its own
+            # column instead of collapsing them into a single joined string.
+            # Previously, when a pivot table had multiple "Rows" fields, they
+            # were merged into a single column on export; pandas natively
+            # writes a MultiIndex as one column per level when serializing to
+            # CSV/Excel. See: https://github.com/apache/superset/issues/32369
+            processed_df.index.names = [
+                name if name is not None else "" for name in 
processed_df.index.names
+            ]
+        else:
+            processed_df.index = [
+                (
+                    " ".join(str(name) for name in index).strip()
+                    if isinstance(index, tuple)
+                    else index
+                )
+                for index in processed_df.index
+            ]
 
         if query["result_format"] == ChartDataResultFormat.JSON:
             query["data"] = processed_df.to_dict()
diff --git a/tests/unit_tests/charts/test_client_processing.py 
b/tests/unit_tests/charts/test_client_processing.py
index 600c7cf4c7f..179b4647b7d 100644
--- a/tests/unit_tests/charts/test_client_processing.py
+++ b/tests/unit_tests/charts/test_client_processing.py
@@ -15,7 +15,7 @@
 # specific language governing permissions and limitations
 # under the License.
 
-from io import BytesIO
+from io import BytesIO, StringIO
 
 import pandas as pd
 import pytest
@@ -3150,3 +3150,56 @@ def 
test_apply_client_processing_csv_format_partial_config_decimal_only():
     assert lines[0] == "name,value", f"Expected comma-separated header, got: 
{lines[0]}"
     assert "foo" in lines[1]
     assert "bar" in lines[2]
+
+
+def test_apply_client_processing_csv_format_pivot_table_multiple_rows():
+    """
+    When multiple "Rows" fields are selected in a pivot table, exporting to
+    pivoted CSV should keep each field in its own column instead of merging
+    them into a single column.
+
+    See: https://github.com/apache/superset/issues/32369
+    """
+    csv_data = (
+        "city,segment,value\n"
+        "Paris,Consumer,10\n"
+        "Paris,Corporate,20\n"
+        "London,Consumer,30\n"
+        "London,Corporate,40\n"
+    )
+
+    result = {
+        "queries": [
+            {
+                "result_format": ChartDataResultFormat.CSV,
+                "data": csv_data,
+            }
+        ]
+    }
+    form_data = {
+        "viz_type": "pivot_table_v2",
+        "groupbyColumns": [],
+        "groupbyRows": ["city", "segment"],
+        "metrics": ["value"],
+        "metricsLayout": "COLUMNS",
+    }
+
+    processed_result = apply_client_processing(result, form_data)
+    query = processed_result["queries"][0]
+
+    output_data = query["data"]
+    lines = output_data.strip().split("\n")
+
+    # the header should have a separate column for each "Rows" field, instead
+    # of a single merged column
+    assert lines[0] == "city,segment,value"
+
+    # each data row should have the city and segment in their own columns
+    output_df = pd.read_csv(StringIO(output_data))
+    assert list(output_df.columns) == ["city", "segment", "value"]
+    assert set(zip(output_df["city"], output_df["segment"], strict=False)) == {
+        ("Paris", "Consumer"),
+        ("Paris", "Corporate"),
+        ("London", "Consumer"),
+        ("London", "Corporate"),
+    }

Reply via email to