"""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")