codeant-ai-for-open-source[bot] commented on code in PR #44664:
URL: https://github.com/apache/superset/pull/44664#discussion_r4112156231
##########
superset/common/query_context_processor.py:
##########
@@ -566,6 +566,15 @@ def get_data(
result = excel.df_to_excel(
df, index=include_index,
**current_app.config["EXCEL_EXPORT"]
)
+ # Explore Table / Pivot download uses this path (RESULTS +
XLSX),
+ # not apply_client_processing. Stamp formats and CF here.
+ form_data = self._query_context.form_data
+ if isinstance(form_data, dict) and isinstance(result, bytes):
+ from superset.utils.excel_conditional import
polish_explore_xlsx
+
+ result = polish_explore_xlsx(
+ result, df, form_data, include_index=include_index
+ )
Review Comment:
✅ **CodeAnt verified this suggestion was addressed in subsequent commits and
marked this thread resolved** as of `b811d26`.
Pivot XLSX exports in percentage modes now pass `number_format="0.0%"` to
`excel.df_to_excel`, keeping fractional values numeric while displaying them as
percentages.
<sub>If that's not right, unresolve this thread and CodeAnt will leave it
open.</sub>
<!-- codeant-auto-resolve-reply -->
##########
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:
✅ **CodeAnt verified this suggestion was addressed in subsequent commits and
marked this thread resolved** as of `b811d26`.
Excel formatting now passes the datasource `verbose_map` alongside
`processed_df` to `_apply_excel_explore_formats`, allowing configured raw
column names to resolve against verbose headers.
<sub>If that's not right, unresolve this thread and CodeAnt will leave it
open.</sub>
<!-- codeant-auto-resolve-reply -->
##########
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:
✅ **CodeAnt verified this suggestion was addressed in subsequent commits and
marked this thread resolved** as of `b811d26`.
Cell-bar rules now derive a matching range via `_matching_cell_range(sheet,
col_idx, header_row, rule)` before adding the data bar, so the rule comparator
is applied.
<sub>If that's not right, unresolve this thread and CodeAnt will leave it
open.</sub>
<!-- codeant-auto-resolve-reply -->
##########
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:
✅ **CodeAnt verified this suggestion was addressed in subsequent commits and
marked this thread resolved** as of `b811d26`.
Color-scale rules now convert `minBound` and `maxBound` to numeric values
and pass them as `start_value` and `end_value`, using `min`/`max` only when
bounds are absent.
<sub>If that's not right, unresolve this thread and CodeAnt will leave it
open.</sub>
<!-- codeant-auto-resolve-reply -->
--
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]