"""ChartEx native chart XML emitters."""
from __future__ import annotations
from typing import Any
from ..drawingml.utils import _xml_escape
from .marker_common import (
CHARTEX_URI,
CHART_COLOR_STYLE_REL_TYPE,
CHART_STYLE_REL_TYPE,
PACKAGE_REL_TYPE,
_clean_hex,
_compact_key,
_excel_col,
_first_present,
)
def _chart_ex_legend_xml(payload: dict[str, Any]) -> str:
style = payload.get("style") if isinstance(payload.get("style"), dict) else {}
show_legend = payload.get("show_legend", style.get("show_legend", False))
if not show_legend:
return ""
position_key = _compact_key(payload.get("legend_position") or style.get("legend_position") or "bottom")
positions = {
"bottom": "b",
"b": "b",
"left": "l",
"l": "l",
"right": "r",
"r": "r",
"top": "t",
"t": "t",
}
position = positions.get(position_key, "b")
return f''
def _cx_points(values: list[Any]) -> str:
return "".join(
f'{_xml_escape(str(value))}'
for idx, value in enumerate(values)
)
def _cx_col_range(start_col: int, col_count: int, row_count: int) -> str:
start = _excel_col(start_col)
end = _excel_col(start_col + col_count - 1)
return f"Sheet1!${start}$2:${end}${row_count + 1}"
def _cx_str_dim(levels: list[list[str]], *, start_col: int, row_count: int) -> str:
levels_xml = "".join(
f'{_cx_points(level)}'
for level in reversed(levels)
)
return (
''
f"{_cx_col_range(start_col, len(levels), row_count)}"
f"{levels_xml}"
""
)
def _cx_num_dim(
values: list[int | float],
*,
dim_type: str,
col: int,
) -> str:
return (
f''
f"Sheet1!${_excel_col(col)}$2:${_excel_col(col)}${len(values) + 1}"
f'{_cx_points(values)}'
""
)
def _chart_ex_data_xml(chart_data: dict[str, Any]) -> str:
chart_type = chart_data["type"]
if chart_type in {"sunburst", "treemap"}:
values = chart_data["values"]
levels = chart_data["levels"]
return (
''
f'{_cx_str_dim(levels, start_col=1, row_count=len(values))}'
f'{_cx_num_dim(values, dim_type="size", col=len(levels) + 1)}'
""
)
if chart_type == "histogram":
values = chart_data["values"]
return f'{_cx_num_dim(values, dim_type="val", col=1)}'
if chart_type in {"funnel", "pareto", "waterfall"}:
values = chart_data["values"]
categories = chart_data["categories"]
return (
''
f'{_cx_str_dim([categories], start_col=1, row_count=len(values))}'
f'{_cx_num_dim(values, dim_type="val", col=2)}'
""
)
if chart_type == "box_whisker":
parts: list[str] = []
for idx, item in enumerate(chart_data["series"]):
start_col = idx * 2 + 1
parts.append(
f''
f'{_cx_str_dim([item["categories"]], start_col=start_col, row_count=len(item["values"]))}'
f'{_cx_num_dim(item["values"], dim_type="val", col=start_col + 1)}'
""
)
return "".join(parts)
raise RuntimeError(f"Native PPTX {chart_type} chart is outside current basic chart support")
def _chart_ex_series_xml(chart_data: dict[str, Any]) -> str:
chart_type = chart_data["type"]
if chart_type in {"sunburst", "treemap"}:
layout_id = chart_type
label_pos = "ctr" if chart_type == "sunburst" else "inEnd"
layout_pr = ""
if chart_type == "treemap":
parent_labels = chart_data.get("parent_labels", "overlapping")
layout_pr = (
""
f''
""
)
return (
f''
'Sheet1!$A$1Series 1'
f''
''
f"{layout_pr}"
""
)
if chart_type == "histogram":
return (
''
'Sheet1!$A$1Series 1'
''
""
)
if chart_type == "pareto":
return (
''
'Sheet1!$B$1Series 1'
''
''
''
''
)
if chart_type == "waterfall":
subtotals_xml = ""
if chart_data.get("subtotals"):
subtotal_items = "".join(f'' for idx in chart_data["subtotals"])
subtotals_xml = f"{subtotal_items}"
return (
''
'Sheet1!$B$1Series 1'
''
f'{subtotals_xml}'
""
)
if chart_type == "funnel":
return (
''
'Sheet1!$B$1Series 1'
''
''
)
if chart_type == "box_whisker":
parts: list[str] = []
for idx, item in enumerate(chart_data["series"]):
value_col = _excel_col(idx * 2 + 2)
parts.append(
f''
f'Sheet1!${value_col}$1{_xml_escape(str(item["name"]))}'
f''
''
''
''
)
return "".join(parts)
raise RuntimeError(f"Native PPTX {chart_type} chart is outside current basic chart support")
def _chart_ex_axes_xml(chart_data: dict[str, Any]) -> str:
chart_type = chart_data["type"]
if chart_type in {"sunburst", "treemap"}:
return ""
if chart_type == "pareto":
return (
''
''
''
)
if chart_type == "funnel":
return ''
gap_width = "0.5" if chart_type == "waterfall" else "0"
if chart_type == "box_whisker":
gap_width = "1"
return (
f''
''
)
def _chart_ex_xml(
payload: dict[str, Any],
chart_data: dict[str, Any],
*,
chart_rels_id: str,
) -> bytes:
data_xml = _chart_ex_data_xml(chart_data)
series_xml = _chart_ex_series_xml(chart_data)
axes_xml = _chart_ex_axes_xml(chart_data)
xml = f'''
{data_xml}
{series_xml}{axes_xml}
{_chart_ex_legend_xml(payload)}
'''
return xml.encode("utf-8")
def _chart_ex_rels_xml(workbook_target: str, style_target: str, colors_target: str) -> bytes:
xml = f'''
'''
return xml.encode("utf-8")
def _chart_ex_style_xml() -> bytes:
return b''''''
def _chart_ex_colors_xml(payload: dict[str, Any]) -> bytes:
"""Return a ChartEx color style that consumes the marker palette when present."""
style = payload.get("style") if isinstance(payload.get("style"), dict) else {}
raw_colors = _first_present(style.get("colors"), payload.get("colors"))
colors = (
[_clean_hex(color, "#4472C4") for color in raw_colors]
if isinstance(raw_colors, list)
else []
)
if not colors:
return b'''
'''
color_entries = "".join(
f''
for color in colors
)
xml = f'''
{color_entries}
'''
return xml.encode("utf-8")