codeant-ai-for-open-source[bot] commented on code in PR #44664:
URL: https://github.com/apache/superset/pull/44664#discussion_r4108139344
##########
superset/charts/client_processing.py:
##########
@@ -1454,8 +1473,15 @@ def apply_client_processing( # noqa: C901
),
**{
**current_app.config["EXCEL_EXPORT"],
- "index": show_default_index,
+ "index": include_index,
},
)
+ query["data"] = _apply_excel_explore_formats(
+ query["data"],
+ processed_df,
+ form_data,
+ viz_type,
+ include_index,
+ )
Review Comment:
**Suggestion:** `processed_df` uses verbose column labels while
conditional-formatting rules use raw column names, so `_column_index` cannot
find many configured columns and emits no rules.
**Assessment:** ๐ `Major` ยท ๐ `Occurrence: Sometimes` ยท ๐ท๏ธ `Api mismatch`
[](https://docs.codeant.ai/cli/resolve-pr-comments-skill)
[](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=cdabc145f67e4a7893fa84b4162387c7&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
[](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=cdabc145f67e4a7893fa84b4162387c7&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
<details>
<summary><b>Prompt for AI Agent ๐ค </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset/charts/client_processing.py
**Line:** 1479:1485
**Comment:**
*Api Mismatch: `processed_df` uses verbose column labels while
conditional-formatting rules use raw column names, so `_column_index` cannot
find many configured columns and emits no rules.
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
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F44664&comment_hash=943a57f057e7aa2deaa97c6691c34f07b7fe158b8b99aac0d6c281b6fa72773f&reaction=like'>๐</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F44664&comment_hash=943a57f057e7aa2deaa97c6691c34f07b7fe158b8b99aac0d6c281b6fa72773f&reaction=dislike'>๐</a>
##########
superset/utils/excel_conditional.py:
##########
@@ -0,0 +1,378 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Stamp Explore Table / Pivot Table v2 highlights onto an Excel workbook.
+
+Explore paints matching cells in the browser. Chart XLSX download is written
+in ``QueryContextProcessor.get_data`` before client post-processing, so this
+module is applied there as well as on the reports path.
+
+Rules from ``form_data["conditional_formatting"]`` become native Excel
+conditional formatting (CellIs / formula / color scale / data bar). When that
+list is empty and ``show_cell_bars`` is on, numeric columns get data bars so
+the download matches the Table chart's default gradient.
+"""
+
+from __future__ import annotations
+
+import io
+from typing import Any, Optional
+
+import pandas as pd
+from openpyxl import load_workbook
+from openpyxl.formatting.rule import (
+ CellIsRule,
+ ColorScaleRule,
+ DataBarRule,
+ FormulaRule,
+)
+from openpyxl.styles import Font, PatternFill
+from openpyxl.utils import get_column_letter
+from openpyxl.workbook import Workbook
+from openpyxl.worksheet.worksheet import Worksheet
+
+from superset.constants import SHOW_VALUES_AS_PERCENT_MODES
+from superset.utils.excel_display import (
+ apply_column_display,
+ refresh_sheet_bounds,
+ styles_from_pivot_form_data,
+ styles_from_table_form_data,
+)
+
+# Theme tokens used by Table / Pivot Table v2 pickers, plus CSS names.
+_NAMED_COLORS = {
+ "success": "52C41A",
+ "warning": "FAAD14",
+ "error": "FF4D4F",
+ "red": "FF4D4F",
+ "green": "52C41A",
+ "blue": "1890FF",
+ "yellow": "FAAD14",
+ "orange": "FA8C16",
+ "purple": "722ED1",
+ "cyan": "13C2C2",
+ "colorsuccess": "52C41A",
+ "colorwarning": "FAAD14",
+ "colorerror": "FF4D4F",
+ "colorsuccessbg": "F6FFED",
+ "colorwarningbg": "FFFBE6",
+ "colorerrorbg": "FFF2F0",
+}
+
+_CELL_IS_OPERATORS = {
+ ">": "greaterThan",
+ "<": "lessThan",
+ ">=": "greaterThanOrEqual",
+ "<=": "lessThanOrEqual",
+ "=": "equal",
+ "==": "equal",
+ "!=": "notEqual",
+ "โ ": "notEqual",
+ "โฅ": "greaterThanOrEqual",
+ "โค": "lessThanOrEqual",
+}
+
+_RANGE_OPERATORS = {
+ "< x <": (">", "<"),
+ "< x โค": (">", "<="),
+ "โค x <": (">=", "<"),
+ "โค x โค": (">=", "<="),
+}
+
+_DATA_BAR_POSITIVE = "63BE7B"
+_SCALE_LOW = "FFFFFF"
+_OBJECT_CELL_BAR = "CELL_BAR"
+_OBJECT_TEXT = "TEXT_COLOR"
+
+
+def _hex_rgb(color: Any) -> Optional[str]:
+ """Normalize a picker payload to a 6-digit RGB hex string."""
+ if isinstance(color, dict):
+ hex_value = color.get("hex")
+ if isinstance(hex_value, str):
+ return _hex_rgb(hex_value)
+ red, green, blue = color.get("r"), color.get("g"), color.get("b")
+ if None not in (red, green, blue):
+ return f"{int(red):02X}{int(green):02X}{int(blue):02X}"
+ return None
+ if not isinstance(color, str) or not color:
+ return None
+ token = color.strip().lstrip("#")
+ named = _NAMED_COLORS.get(token.lower().replace("_", "").replace("-", ""))
+ if named:
+ return named
+ if len(token) == 3 and all(ch in "0123456789abcdefABCDEF" for ch in token):
+ return "".join(ch * 2 for ch in token).upper()
+ if len(token) >= 6 and all(ch in "0123456789abcdefABCDEF" for ch in
token[:6]):
+ return token[:6].upper()
+ return None
+
+
+def _rule_color(rule: dict[str, Any]) -> str:
+ return _hex_rgb(rule.get("colorScheme")) or "52C41A"
+
+
+def _excel_literal(value: Any) -> str:
+ """Quote a comparison target so Excel treats it as a constant."""
+ if isinstance(value, bool):
+ return "TRUE" if value else "FALSE"
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
+ return str(value)
+ text = str(value).replace('"', '""')
+ return f'"{text}"'
+
+
+def _header_matches(sheet_header: str, column: str) -> bool:
+ """Match a rule column to a header, including Pivot ``SUM(col)`` titles."""
+ left = sheet_header.strip()
+ right = column.strip()
+ if left == right or left.lower() == right.lower():
+ return True
+ return left.endswith(f"({right})") or left.endswith(f"({right.lower()})")
+
+
+def _column_index(sheet: Worksheet, header_row: int, column: str) ->
Optional[int]:
+ for col_idx in range(1, sheet.max_column + 1):
+ header_label = ""
+ for row in range(header_row, 0, -1):
+ value = sheet.cell(row=row, column=col_idx).value
+ if value not in (None, ""):
+ header_label = str(value)
+ break
+ if header_label and _header_matches(header_label, column):
+ return col_idx
+ return None
+
+
+def _data_range(sheet: Worksheet, col_idx: int, header_row: int) ->
Optional[str]:
+ last_row = sheet.max_row
+ first_row = header_row + 1
+ if last_row < first_row:
+ return None
+ letter = get_column_letter(col_idx)
+ return f"{letter}{first_row}:{letter}{last_row}"
+
+
+def _solid_fill(rgb: str) -> PatternFill:
+ return PatternFill(start_color=rgb, end_color=rgb, fill_type="solid")
+
+
+def _add_formula_rule(
+ sheet: Worksheet,
+ cell_range: str,
+ formula: str,
+ rgb: str,
+ *,
+ text_color: bool,
+) -> None:
+ fill = None if text_color else _solid_fill(rgb)
+ font = Font(color=rgb) if text_color else None
+ sheet.conditional_formatting.add(
+ cell_range,
+ FormulaRule(formula=[formula], fill=fill, font=font),
+ )
+
+
+def _add_cell_is_rule(
+ sheet: Worksheet,
+ cell_range: str,
+ operator: str,
+ formula: list[str],
+ rgb: str,
+ *,
+ text_color: bool,
+) -> None:
+ fill = None if text_color else _solid_fill(rgb)
+ font = Font(color=rgb) if text_color else None
+ sheet.conditional_formatting.add(
+ cell_range,
+ CellIsRule(operator=operator, formula=formula, fill=fill, font=font),
+ )
+
+
+def _add_data_bar(sheet: Worksheet, cell_range: str, rgb: str) -> None:
+ sheet.conditional_formatting.add(
+ cell_range,
+ DataBarRule(
+ start_type="min",
+ end_type="max",
+ color=rgb,
+ showValue=True,
+ minLength=None,
+ maxLength=None,
+ ),
+ )
+
+
+def _apply_rule(sheet: Worksheet, header_row: int, rule: dict[str, Any]) ->
None:
+ column = rule.get("column")
+ if not isinstance(column, str) or not column:
+ return
+ col_idx = _column_index(sheet, header_row, column)
+ if col_idx is None:
+ return
+ cell_range = _data_range(sheet, col_idx, header_row)
+ if cell_range is None:
+ return
+
+ rgb = _rule_color(rule)
+ operator = rule.get("operator")
+ object_fmt = rule.get("objectFormatting") or ""
+ text_color = object_fmt == _OBJECT_TEXT
+ top_left = cell_range.split(":", 1)[0]
+
+ if object_fmt == _OBJECT_CELL_BAR:
+ _add_data_bar(sheet, cell_range, rgb)
+ return
Review Comment:
**Suggestion:** A cell-bar rule's comparator is discarded, so a rule such as
`>` 10 displays bars for every value instead of only matching values.
**Assessment:** ๐ `Major` ยท ๐ `Occurrence: Sometimes` ยท ๐ท๏ธ `Logic error`
[](https://docs.codeant.ai/cli/resolve-pr-comments-skill)
[](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=e7b0520738df459382cbf7ba3237ed52&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
[](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=e7b0520738df459382cbf7ba3237ed52&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
<details>
<summary><b>Prompt for AI Agent ๐ค </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset/utils/excel_conditional.py
**Line:** 237:239
**Comment:**
*Logic Error: A cell-bar rule's comparator is discarded, so a rule such
as `>` 10 displays bars for every value instead of only matching values.
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
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F44664&comment_hash=72711faf15881cad9244ff3e4c91477be36b3a606a35bef98c0ba3e6fb9a70c0&reaction=like'>๐</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F44664&comment_hash=72711faf15881cad9244ff3e4c91477be36b3a606a35bef98c0ba3e6fb9a70c0&reaction=dislike'>๐</a>
##########
superset/utils/excel_conditional.py:
##########
@@ -0,0 +1,378 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Stamp Explore Table / Pivot Table v2 highlights onto an Excel workbook.
+
+Explore paints matching cells in the browser. Chart XLSX download is written
+in ``QueryContextProcessor.get_data`` before client post-processing, so this
+module is applied there as well as on the reports path.
+
+Rules from ``form_data["conditional_formatting"]`` become native Excel
+conditional formatting (CellIs / formula / color scale / data bar). When that
+list is empty and ``show_cell_bars`` is on, numeric columns get data bars so
+the download matches the Table chart's default gradient.
+"""
+
+from __future__ import annotations
+
+import io
+from typing import Any, Optional
+
+import pandas as pd
+from openpyxl import load_workbook
+from openpyxl.formatting.rule import (
+ CellIsRule,
+ ColorScaleRule,
+ DataBarRule,
+ FormulaRule,
+)
+from openpyxl.styles import Font, PatternFill
+from openpyxl.utils import get_column_letter
+from openpyxl.workbook import Workbook
+from openpyxl.worksheet.worksheet import Worksheet
+
+from superset.constants import SHOW_VALUES_AS_PERCENT_MODES
+from superset.utils.excel_display import (
+ apply_column_display,
+ refresh_sheet_bounds,
+ styles_from_pivot_form_data,
+ styles_from_table_form_data,
+)
+
+# Theme tokens used by Table / Pivot Table v2 pickers, plus CSS names.
+_NAMED_COLORS = {
+ "success": "52C41A",
+ "warning": "FAAD14",
+ "error": "FF4D4F",
+ "red": "FF4D4F",
+ "green": "52C41A",
+ "blue": "1890FF",
+ "yellow": "FAAD14",
+ "orange": "FA8C16",
+ "purple": "722ED1",
+ "cyan": "13C2C2",
+ "colorsuccess": "52C41A",
+ "colorwarning": "FAAD14",
+ "colorerror": "FF4D4F",
+ "colorsuccessbg": "F6FFED",
+ "colorwarningbg": "FFFBE6",
+ "colorerrorbg": "FFF2F0",
+}
+
+_CELL_IS_OPERATORS = {
+ ">": "greaterThan",
+ "<": "lessThan",
+ ">=": "greaterThanOrEqual",
+ "<=": "lessThanOrEqual",
+ "=": "equal",
+ "==": "equal",
+ "!=": "notEqual",
+ "โ ": "notEqual",
+ "โฅ": "greaterThanOrEqual",
+ "โค": "lessThanOrEqual",
+}
+
+_RANGE_OPERATORS = {
+ "< x <": (">", "<"),
+ "< x โค": (">", "<="),
+ "โค x <": (">=", "<"),
+ "โค x โค": (">=", "<="),
+}
+
+_DATA_BAR_POSITIVE = "63BE7B"
+_SCALE_LOW = "FFFFFF"
+_OBJECT_CELL_BAR = "CELL_BAR"
+_OBJECT_TEXT = "TEXT_COLOR"
+
+
+def _hex_rgb(color: Any) -> Optional[str]:
+ """Normalize a picker payload to a 6-digit RGB hex string."""
+ if isinstance(color, dict):
+ hex_value = color.get("hex")
+ if isinstance(hex_value, str):
+ return _hex_rgb(hex_value)
+ red, green, blue = color.get("r"), color.get("g"), color.get("b")
+ if None not in (red, green, blue):
+ return f"{int(red):02X}{int(green):02X}{int(blue):02X}"
+ return None
+ if not isinstance(color, str) or not color:
+ return None
+ token = color.strip().lstrip("#")
+ named = _NAMED_COLORS.get(token.lower().replace("_", "").replace("-", ""))
+ if named:
+ return named
+ if len(token) == 3 and all(ch in "0123456789abcdefABCDEF" for ch in token):
+ return "".join(ch * 2 for ch in token).upper()
+ if len(token) >= 6 and all(ch in "0123456789abcdefABCDEF" for ch in
token[:6]):
+ return token[:6].upper()
+ return None
+
+
+def _rule_color(rule: dict[str, Any]) -> str:
+ return _hex_rgb(rule.get("colorScheme")) or "52C41A"
+
+
+def _excel_literal(value: Any) -> str:
+ """Quote a comparison target so Excel treats it as a constant."""
+ if isinstance(value, bool):
+ return "TRUE" if value else "FALSE"
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
+ return str(value)
+ text = str(value).replace('"', '""')
+ return f'"{text}"'
+
+
+def _header_matches(sheet_header: str, column: str) -> bool:
+ """Match a rule column to a header, including Pivot ``SUM(col)`` titles."""
+ left = sheet_header.strip()
+ right = column.strip()
+ if left == right or left.lower() == right.lower():
+ return True
+ return left.endswith(f"({right})") or left.endswith(f"({right.lower()})")
+
+
+def _column_index(sheet: Worksheet, header_row: int, column: str) ->
Optional[int]:
+ for col_idx in range(1, sheet.max_column + 1):
+ header_label = ""
+ for row in range(header_row, 0, -1):
+ value = sheet.cell(row=row, column=col_idx).value
+ if value not in (None, ""):
+ header_label = str(value)
+ break
+ if header_label and _header_matches(header_label, column):
+ return col_idx
+ return None
+
+
+def _data_range(sheet: Worksheet, col_idx: int, header_row: int) ->
Optional[str]:
+ last_row = sheet.max_row
+ first_row = header_row + 1
+ if last_row < first_row:
+ return None
+ letter = get_column_letter(col_idx)
+ return f"{letter}{first_row}:{letter}{last_row}"
+
+
+def _solid_fill(rgb: str) -> PatternFill:
+ return PatternFill(start_color=rgb, end_color=rgb, fill_type="solid")
+
+
+def _add_formula_rule(
+ sheet: Worksheet,
+ cell_range: str,
+ formula: str,
+ rgb: str,
+ *,
+ text_color: bool,
+) -> None:
+ fill = None if text_color else _solid_fill(rgb)
+ font = Font(color=rgb) if text_color else None
+ sheet.conditional_formatting.add(
+ cell_range,
+ FormulaRule(formula=[formula], fill=fill, font=font),
+ )
+
+
+def _add_cell_is_rule(
+ sheet: Worksheet,
+ cell_range: str,
+ operator: str,
+ formula: list[str],
+ rgb: str,
+ *,
+ text_color: bool,
+) -> None:
+ fill = None if text_color else _solid_fill(rgb)
+ font = Font(color=rgb) if text_color else None
+ sheet.conditional_formatting.add(
+ cell_range,
+ CellIsRule(operator=operator, formula=formula, fill=fill, font=font),
+ )
+
+
+def _add_data_bar(sheet: Worksheet, cell_range: str, rgb: str) -> None:
+ sheet.conditional_formatting.add(
+ cell_range,
+ DataBarRule(
+ start_type="min",
+ end_type="max",
+ color=rgb,
+ showValue=True,
+ minLength=None,
+ maxLength=None,
+ ),
+ )
+
+
+def _apply_rule(sheet: Worksheet, header_row: int, rule: dict[str, Any]) ->
None:
+ column = rule.get("column")
+ if not isinstance(column, str) or not column:
+ return
+ col_idx = _column_index(sheet, header_row, column)
+ if col_idx is None:
+ return
+ cell_range = _data_range(sheet, col_idx, header_row)
+ if cell_range is None:
+ return
+
+ rgb = _rule_color(rule)
+ operator = rule.get("operator")
+ object_fmt = rule.get("objectFormatting") or ""
+ text_color = object_fmt == _OBJECT_TEXT
+ top_left = cell_range.split(":", 1)[0]
+
+ if object_fmt == _OBJECT_CELL_BAR:
+ _add_data_bar(sheet, cell_range, rgb)
+ return
+
+ if operator in _CELL_IS_OPERATORS:
+ _add_cell_is_rule(
+ sheet,
+ cell_range,
+ _CELL_IS_OPERATORS[operator],
+ [_excel_literal(rule.get("targetValue"))],
+ rgb,
+ text_color=text_color,
+ )
+ return
+
+ if operator in _RANGE_OPERATORS:
+ left_op, right_op = _RANGE_OPERATORS[operator]
+ left = _excel_literal(rule.get("targetValueLeft"))
+ right = _excel_literal(rule.get("targetValueRight"))
+ _add_formula_rule(
+ sheet,
+ cell_range,
+ (
+ f"AND(NOT(ISBLANK({top_left})),"
+ f"{top_left}{left_op}{left},"
+ f"{top_left}{right_op}{right})"
+ ),
+ rgb,
+ text_color=text_color,
+ )
+ return
+
+ if operator in (None, "None", ""):
+ if text_color:
+ _add_formula_rule(
+ sheet,
+ cell_range,
+ f"NOT(ISBLANK({top_left}))",
+ rgb,
+ text_color=True,
+ )
+ return
+ sheet.conditional_formatting.add(
+ cell_range,
+ ColorScaleRule(
+ start_type="min",
+ start_color=_SCALE_LOW,
+ end_type="max",
+ end_color=rgb,
+ ),
Review Comment:
**Suggestion:** Color-scale rules discard configured `minBound` and
`maxBound`, causing Excel to scale against the data range instead of the bounds
used by Explore.
**Assessment:** ๐ `Major` ยท ๐ `Occurrence: Sometimes` ยท ๐ท๏ธ `Incomplete
implementation`
[](https://docs.codeant.ai/cli/resolve-pr-comments-skill)
[](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=898c4f8c6275452e8fd0953885eb622d&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
[](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=898c4f8c6275452e8fd0953885eb622d&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
<details>
<summary><b>Prompt for AI Agent ๐ค </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset/utils/excel_conditional.py
**Line:** 279:286
**Comment:**
*Incomplete Implementation: Color-scale rules discard configured
`minBound` and `maxBound`, causing Excel to scale against the data range
instead of the bounds used by Explore.
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
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F44664&comment_hash=f5576e733d6921fd15d24549b7234528b033f890e4b470549371a5e0a7c88914&reaction=like'>๐</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F44664&comment_hash=f5576e733d6921fd15d24549b7234528b033f890e4b470549371a5e0a7c88914&reaction=dislike'>๐</a>
##########
superset/utils/excel_display.py:
##########
@@ -0,0 +1,267 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""
+Map Explore d3 number/time formats onto Excel cell formats after a workbook
+has been written.
+
+The export path keeps values numeric (JSON reports stringify them). This module
+only changes how Excel *displays* those values so the sheet matches the chart.
+"""
+
+from __future__ import annotations
+
+import io
+import re
+from dataclasses import dataclass
+from datetime import date, datetime
+from typing import Any, Mapping
+
+from openpyxl.reader.excel import load_workbook
+from openpyxl.styles import Alignment
+
+# Same grammar as ``superset.utils.number_format.D3_FORMAT_RE``.
+D3_FORMAT_RE = re.compile(
+ r"^(?:(.)?([<>=^]))?([+\-(
])?([$#])?(0)?(\d+)?(,)?(?:\.(\d+))?(~)?([a-z%])?$",
+ re.IGNORECASE,
+)
+SMART_NUMBER = "SMART_NUMBER"
+SMART_NUMBER_SIGNED = "SMART_NUMBER_SIGNED"
+
+ALLOWED_ALIGNMENTS = frozenset({"left", "center", "right"})
+
+# strftime tokens used by Table/Pivot time format controls โ Excel format
codes.
+_STRFTIME_TO_EXCEL: tuple[tuple[str, str], ...] = (
+ ("%Y", "yyyy"),
+ ("%y", "yy"),
+ ("%m", "mm"),
+ ("%d", "dd"),
+ ("%H", "hh"),
+ ("%I", "hh"),
+ ("%M", "mm"),
+ ("%S", "ss"),
+ ("%p", "AM/PM"),
+ ("%b", "mmm"),
+ ("%B", "mmmm"),
+)
+
+_CURRENCY_EXCEL_SYMBOL = {
+ "USD": "$",
+ "EUR": "โฌ",
+ "GBP": "ยฃ",
+ "JPY": "ยฅ",
+ "CNY": "ยฅ",
+ "INR": "โน",
+ "RUB": "โฝ",
+ "MXN": "MX$",
+}
+
+
+@dataclass(frozen=True)
+class ExcelColumnDisplay:
+ """Display options for one exported column, keyed by header text."""
+
+ number_format: str | None = None
+ alignment: str | None = None
+
+
+def d3_number_to_excel(
+ d3_format: str | None,
+ currency: Mapping[str, Any] | None = None,
+) -> str | None:
+ """
+ Translate a d3-format specifier into an Excel ``numFmt``.
+
+ SMART_NUMBER / SI (``s``) have no Excel equivalent and return ``None`` so
+ the cell stays General. Unknown specifiers also return ``None``.
+ """
+ if not d3_format or d3_format in {SMART_NUMBER, SMART_NUMBER_SIGNED}:
+ return _currency_excel_format("#,##0.00", currency) if currency else
None
+
+ stripped = d3_format.replace("$", "")
+ match = D3_FORMAT_RE.fullmatch(stripped)
+ if not match:
+ return None
+
+ comma, precision, ntype = match.group(7), match.group(8), (match.group(10)
or "f")
+ ntype = ntype.lower()
+ if ntype in {"s", "e"}:
+ return None
+
+ digits = int(precision) if precision is not None else (0 if ntype in {"d",
"i"} else 2)
+ decimals = "" if digits == 0 else "." + ("0" * digits)
+ grouped = "#,##0" if comma else "0"
+ body = f"{grouped}{decimals}"
+
+ if ntype == "%":
+ body = f"{body}%"
+
+ sign = match.group(3)
+ if sign == "+":
+ body = f"+{body};-{body}"
+ elif sign == "(":
+ body = f"{body};({body})"
+
+ return _currency_excel_format(body, currency)
+
+
+def d3_time_to_excel(d3_time_format: str | None) -> str | None:
+ """Translate a Python/d3 strftime string into an Excel date format."""
+ if not d3_time_format:
+ return None
+ excel = d3_time_format
+ for token, replacement in _STRFTIME_TO_EXCEL:
+ excel = excel.replace(token, replacement)
+ if "%" in excel:
+ return None
+ return excel or None
+
+
+def _currency_excel_format(
+ number_body: str, currency: Mapping[str, Any] | None
+) -> str:
+ if not currency:
+ return number_body
+ code = str(currency.get("symbol") or "")
+ if not code or code == "AUTO":
+ return number_body
+ symbol = _CURRENCY_EXCEL_SYMBOL.get(code, code)
+ quoted = f'"{symbol}"'
+ if str(currency.get("symbolPosition") or "prefix").lower() == "suffix":
+ return f"{number_body}{quoted}"
+ return f"{quoted}{number_body}"
+
+
+def styles_from_table_form_data(
+ column_headers: list[Any],
+ form_data: Mapping[str, Any],
+) -> dict[str, ExcelColumnDisplay]:
+ """Build header โ display map from Table ``column_config``."""
+ column_config = form_data.get("column_config") or {}
+ if not isinstance(column_config, dict):
+ return {}
+
+ styles: dict[str, ExcelColumnDisplay] = {}
+ header_set = {str(header) for header in column_headers}
+ for name, config in column_config.items():
+ if not isinstance(config, dict):
+ continue
+ header = str(name)
+ if header not in header_set:
+ continue
+ alignment = config.get("horizontalAlign")
+ alignment = (
+ alignment.lower()
+ if isinstance(alignment, str) and alignment.lower() in
ALLOWED_ALIGNMENTS
+ else None
+ )
+ currency = config.get("currencyFormat")
+ currency = currency if isinstance(currency, dict) else None
+ number_format = d3_number_to_excel(
+ config.get("d3NumberFormat"), currency
+ ) or d3_time_to_excel(config.get("d3TimeFormat"))
Review Comment:
**Suggestion:** Table exports ignore datasource-level `columnFormats` and
`currencyFormats`, so columns without explicit `column_config` lose formats
that the chart still displays.
**Assessment:** ๐ `Major` ยท ๐ `Occurrence: Sometimes` ยท ๐ท๏ธ `Api mismatch`
[](https://docs.codeant.ai/cli/resolve-pr-comments-skill)
[](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=289830e9b3244f9b9cfc7e38f395bcbe&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
[](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=289830e9b3244f9b9cfc7e38f395bcbe&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
<details>
<summary><b>Prompt for AI Agent ๐ค </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset/utils/excel_display.py
**Line:** 171:175
**Comment:**
*Api Mismatch: Table exports ignore datasource-level `columnFormats`
and `currencyFormats`, so columns without explicit `column_config` lose formats
that the chart still displays.
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
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F44664&comment_hash=4d060d7e3ec256f22ffa838b3df341553e897f1835e057c566c930cc012446f4&reaction=like'>๐</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F44664&comment_hash=4d060d7e3ec256f22ffa838b3df341553e897f1835e057c566c930cc012446f4&reaction=dislike'>๐</a>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]