| 1 | """ChartEx native chart XML emitters.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | from typing import Any |
| 6 | |
| 7 | from ..drawingml.utils import _xml_escape |
| 8 | from .marker_common import ( |
| 9 | CHARTEX_URI, |
| 10 | CHART_COLOR_STYLE_REL_TYPE, |
| 11 | CHART_STYLE_REL_TYPE, |
| 12 | PACKAGE_REL_TYPE, |
| 13 | _clean_hex, |
| 14 | _compact_key, |
| 15 | _excel_col, |
| 16 | _first_present, |
| 17 | ) |
| 18 | |
| 19 | |
| 20 | def _chart_ex_legend_xml(payload: dict[str, Any]) -> str: |
| 21 | style = payload.get("style") if isinstance(payload.get("style"), dict) else {} |
| 22 | show_legend = payload.get("show_legend", style.get("show_legend", False)) |
| 23 | if not show_legend: |
| 24 | return "" |
| 25 | position_key = _compact_key(payload.get("legend_position") or style.get("legend_position") or "bottom") |
| 26 | positions = { |
| 27 | "bottom": "b", |
| 28 | "b": "b", |
| 29 | "left": "l", |
| 30 | "l": "l", |
| 31 | "right": "r", |
| 32 | "r": "r", |
| 33 | "top": "t", |
| 34 | "t": "t", |
| 35 | } |
| 36 | position = positions.get(position_key, "b") |
| 37 | return f'<cx:legend pos="{position}" align="ctr" overlay="0"/>' |
| 38 | |
| 39 | |
| 40 | def _cx_points(values: list[Any]) -> str: |
| 41 | return "".join( |
| 42 | f'<cx:pt idx="{idx}">{_xml_escape(str(value))}</cx:pt>' |
| 43 | for idx, value in enumerate(values) |
| 44 | ) |
| 45 | |
| 46 | |
| 47 | def _cx_col_range(start_col: int, col_count: int, row_count: int) -> str: |
| 48 | start = _excel_col(start_col) |
| 49 | end = _excel_col(start_col + col_count - 1) |
| 50 | return f"Sheet1!${start}$2:${end}${row_count + 1}" |
| 51 | |
| 52 | |
| 53 | def _cx_str_dim(levels: list[list[str]], *, start_col: int, row_count: int) -> str: |
| 54 | levels_xml = "".join( |
| 55 | f'<cx:lvl ptCount="{len(level)}">{_cx_points(level)}</cx:lvl>' |
| 56 | for level in reversed(levels) |
| 57 | ) |
| 58 | return ( |
| 59 | '<cx:strDim type="cat">' |
| 60 | f"<cx:f>{_cx_col_range(start_col, len(levels), row_count)}</cx:f>" |
| 61 | f"{levels_xml}" |
| 62 | "</cx:strDim>" |
| 63 | ) |
| 64 | |
| 65 | |
| 66 | def _cx_num_dim( |
| 67 | values: list[int | float], |
| 68 | *, |
| 69 | dim_type: str, |
| 70 | col: int, |
| 71 | ) -> str: |
| 72 | return ( |
| 73 | f'<cx:numDim type="{dim_type}">' |
| 74 | f"<cx:f>Sheet1!${_excel_col(col)}$2:${_excel_col(col)}${len(values) + 1}</cx:f>" |
| 75 | f'<cx:lvl ptCount="{len(values)}" formatCode="General">{_cx_points(values)}</cx:lvl>' |
| 76 | "</cx:numDim>" |
| 77 | ) |
| 78 | |
| 79 | |
| 80 | def _chart_ex_data_xml(chart_data: dict[str, Any]) -> str: |
| 81 | chart_type = chart_data["type"] |
| 82 | if chart_type in {"sunburst", "treemap"}: |
| 83 | values = chart_data["values"] |
| 84 | levels = chart_data["levels"] |
| 85 | return ( |
| 86 | '<cx:data id="0">' |
| 87 | f'{_cx_str_dim(levels, start_col=1, row_count=len(values))}' |
| 88 | f'{_cx_num_dim(values, dim_type="size", col=len(levels) + 1)}' |
| 89 | "</cx:data>" |
| 90 | ) |
| 91 | if chart_type == "histogram": |
| 92 | values = chart_data["values"] |
| 93 | return f'<cx:data id="0">{_cx_num_dim(values, dim_type="val", col=1)}</cx:data>' |
| 94 | if chart_type in {"funnel", "pareto", "waterfall"}: |
| 95 | values = chart_data["values"] |
| 96 | categories = chart_data["categories"] |
| 97 | return ( |
| 98 | '<cx:data id="0">' |
| 99 | f'{_cx_str_dim([categories], start_col=1, row_count=len(values))}' |
| 100 | f'{_cx_num_dim(values, dim_type="val", col=2)}' |
| 101 | "</cx:data>" |
| 102 | ) |
| 103 | if chart_type == "box_whisker": |
| 104 | parts: list[str] = [] |
| 105 | for idx, item in enumerate(chart_data["series"]): |
| 106 | start_col = idx * 2 + 1 |
| 107 | parts.append( |
| 108 | f'<cx:data id="{idx}">' |
| 109 | f'{_cx_str_dim([item["categories"]], start_col=start_col, row_count=len(item["values"]))}' |
| 110 | f'{_cx_num_dim(item["values"], dim_type="val", col=start_col + 1)}' |
| 111 | "</cx:data>" |
| 112 | ) |
| 113 | return "".join(parts) |
| 114 | raise RuntimeError(f"Native PPTX {chart_type} chart is outside current basic chart support") |
| 115 | |
| 116 | |
| 117 | def _chart_ex_series_xml(chart_data: dict[str, Any]) -> str: |
| 118 | chart_type = chart_data["type"] |
| 119 | if chart_type in {"sunburst", "treemap"}: |
| 120 | layout_id = chart_type |
| 121 | label_pos = "ctr" if chart_type == "sunburst" else "inEnd" |
| 122 | layout_pr = "" |
| 123 | if chart_type == "treemap": |
| 124 | parent_labels = chart_data.get("parent_labels", "overlapping") |
| 125 | layout_pr = ( |
| 126 | "<cx:layoutPr>" |
| 127 | f'<cx:parentLabelLayout val="{parent_labels}"/>' |
| 128 | "</cx:layoutPr>" |
| 129 | ) |
| 130 | return ( |
| 131 | f'<cx:series layoutId="{layout_id}" uniqueId="{{00000000-0000-4000-8000-000000000001}}">' |
| 132 | '<cx:tx><cx:txData><cx:f>Sheet1!$A$1</cx:f><cx:v>Series 1</cx:v></cx:txData></cx:tx>' |
| 133 | f'<cx:dataLabels pos="{label_pos}"><cx:visibility seriesName="0" categoryName="1" value="1"/></cx:dataLabels>' |
| 134 | '<cx:dataId val="0"/>' |
| 135 | f"{layout_pr}" |
| 136 | "</cx:series>" |
| 137 | ) |
| 138 | if chart_type == "histogram": |
| 139 | return ( |
| 140 | '<cx:series layoutId="clusteredColumn" uniqueId="{00000000-0000-4000-8000-000000000001}">' |
| 141 | '<cx:tx><cx:txData><cx:f>Sheet1!$A$1</cx:f><cx:v>Series 1</cx:v></cx:txData></cx:tx>' |
| 142 | '<cx:dataId val="0"/><cx:layoutPr><cx:binning intervalClosed="r"/></cx:layoutPr>' |
| 143 | "</cx:series>" |
| 144 | ) |
| 145 | if chart_type == "pareto": |
| 146 | return ( |
| 147 | '<cx:series layoutId="clusteredColumn" uniqueId="{00000000-0000-4000-8000-000000000001}">' |
| 148 | '<cx:tx><cx:txData><cx:f>Sheet1!$B$1</cx:f><cx:v>Series 1</cx:v></cx:txData></cx:tx>' |
| 149 | '<cx:dataId val="0"/><cx:layoutPr><cx:aggregation/></cx:layoutPr>' |
| 150 | '<cx:axisId val="1"/></cx:series>' |
| 151 | '<cx:series layoutId="paretoLine" ownerIdx="0" uniqueId="{00000000-0000-4000-8000-000000000002}">' |
| 152 | '<cx:axisId val="2"/></cx:series>' |
| 153 | ) |
| 154 | if chart_type == "waterfall": |
| 155 | subtotals_xml = "" |
| 156 | if chart_data.get("subtotals"): |
| 157 | subtotal_items = "".join(f'<cx:idx val="{idx}"/>' for idx in chart_data["subtotals"]) |
| 158 | subtotals_xml = f"<cx:subtotals>{subtotal_items}</cx:subtotals>" |
| 159 | return ( |
| 160 | '<cx:series layoutId="waterfall" uniqueId="{00000000-0000-4000-8000-000000000001}">' |
| 161 | '<cx:tx><cx:txData><cx:f>Sheet1!$B$1</cx:f><cx:v>Series 1</cx:v></cx:txData></cx:tx>' |
| 162 | '<cx:dataLabels pos="outEnd"><cx:visibility seriesName="0" categoryName="0" value="1"/></cx:dataLabels>' |
| 163 | f'<cx:dataId val="0"/><cx:layoutPr>{subtotals_xml}</cx:layoutPr>' |
| 164 | "</cx:series>" |
| 165 | ) |
| 166 | if chart_type == "funnel": |
| 167 | return ( |
| 168 | '<cx:series layoutId="funnel" uniqueId="{00000000-0000-4000-8000-000000000001}">' |
| 169 | '<cx:tx><cx:txData><cx:f>Sheet1!$B$1</cx:f><cx:v>Series 1</cx:v></cx:txData></cx:tx>' |
| 170 | '<cx:dataLabels><cx:visibility seriesName="0" categoryName="0" value="1"/></cx:dataLabels>' |
| 171 | '<cx:dataId val="0"/></cx:series>' |
| 172 | ) |
| 173 | if chart_type == "box_whisker": |
| 174 | parts: list[str] = [] |
| 175 | for idx, item in enumerate(chart_data["series"]): |
| 176 | value_col = _excel_col(idx * 2 + 2) |
| 177 | parts.append( |
| 178 | f'<cx:series layoutId="boxWhisker" uniqueId="{{00000000-0000-4000-8000-{idx + 1:012d}}}">' |
| 179 | f'<cx:tx><cx:txData><cx:f>Sheet1!${value_col}$1</cx:f><cx:v>{_xml_escape(str(item["name"]))}</cx:v></cx:txData></cx:tx>' |
| 180 | f'<cx:dataId val="{idx}"/><cx:layoutPr>' |
| 181 | '<cx:visibility meanMarker="1" outliers="1"/>' |
| 182 | '<cx:statistics quartileMethod="exclusive"/>' |
| 183 | '</cx:layoutPr></cx:series>' |
| 184 | ) |
| 185 | return "".join(parts) |
| 186 | raise RuntimeError(f"Native PPTX {chart_type} chart is outside current basic chart support") |
| 187 | |
| 188 | |
| 189 | def _chart_ex_axes_xml(chart_data: dict[str, Any]) -> str: |
| 190 | chart_type = chart_data["type"] |
| 191 | if chart_type in {"sunburst", "treemap"}: |
| 192 | return "" |
| 193 | if chart_type == "pareto": |
| 194 | return ( |
| 195 | '<cx:axis id="0"><cx:catScaling gapWidth="0"/><cx:tickLabels/></cx:axis>' |
| 196 | '<cx:axis id="1"><cx:valScaling/><cx:majorGridlines/><cx:tickLabels/></cx:axis>' |
| 197 | '<cx:axis id="2"><cx:valScaling max="1" min="0"/><cx:units unit="percentage"/><cx:tickLabels/></cx:axis>' |
| 198 | ) |
| 199 | if chart_type == "funnel": |
| 200 | return '<cx:axis id="1"><cx:catScaling gapWidth="0.06"/><cx:tickLabels/></cx:axis>' |
| 201 | gap_width = "0.5" if chart_type == "waterfall" else "0" |
| 202 | if chart_type == "box_whisker": |
| 203 | gap_width = "1" |
| 204 | return ( |
| 205 | f'<cx:axis id="0"><cx:catScaling gapWidth="{gap_width}"/><cx:tickLabels/></cx:axis>' |
| 206 | '<cx:axis id="1"><cx:valScaling/><cx:majorGridlines/><cx:tickLabels/></cx:axis>' |
| 207 | ) |
| 208 | |
| 209 | |
| 210 | def _chart_ex_xml( |
| 211 | payload: dict[str, Any], |
| 212 | chart_data: dict[str, Any], |
| 213 | *, |
| 214 | chart_rels_id: str, |
| 215 | ) -> bytes: |
| 216 | data_xml = _chart_ex_data_xml(chart_data) |
| 217 | series_xml = _chart_ex_series_xml(chart_data) |
| 218 | axes_xml = _chart_ex_axes_xml(chart_data) |
| 219 | xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?> |
| 220 | <cx:chartSpace xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" |
| 221 | xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" |
| 222 | xmlns:cx="{CHARTEX_URI}"> |
| 223 | <cx:chartData><cx:externalData r:id="{chart_rels_id}" cx:autoUpdate="0"/>{data_xml}</cx:chartData> |
| 224 | <cx:chart><cx:title pos="t" align="ctr" overlay="0"/> |
| 225 | <cx:plotArea><cx:plotAreaRegion>{series_xml}</cx:plotAreaRegion>{axes_xml}</cx:plotArea> |
| 226 | {_chart_ex_legend_xml(payload)}</cx:chart> |
| 227 | </cx:chartSpace>''' |
| 228 | return xml.encode("utf-8") |
| 229 | |
| 230 | |
| 231 | def _chart_ex_rels_xml(workbook_target: str, style_target: str, colors_target: str) -> bytes: |
| 232 | xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?> |
| 233 | <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"> |
| 234 | <Relationship Id="rId1" Type="{PACKAGE_REL_TYPE}" Target="{_xml_escape(workbook_target)}"/> |
| 235 | <Relationship Id="rId2" Type="{CHART_STYLE_REL_TYPE}" Target="{_xml_escape(style_target)}"/> |
| 236 | <Relationship Id="rId3" Type="{CHART_COLOR_STYLE_REL_TYPE}" Target="{_xml_escape(colors_target)}"/> |
| 237 | </Relationships>''' |
| 238 | return xml.encode("utf-8") |
| 239 | |
| 240 | |
| 241 | def _chart_ex_style_xml() -> bytes: |
| 242 | return b'''<cs:chartStyle xmlns:cs="http://schemas.microsoft.com/office/drawing/2012/chartStyle" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" id="410"><cs:axisTitle><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"><a:lumMod val="65000"/><a:lumOff val="35000"/></a:schemeClr></cs:fontRef><cs:spPr><a:solidFill><a:schemeClr val="bg1"><a:lumMod val="65000"/></a:schemeClr></a:solidFill><a:ln w="19050"><a:solidFill><a:schemeClr val="bg1"/></a:solidFill></a:ln></cs:spPr><cs:defRPr sz="1197"/></cs:axisTitle><cs:categoryAxis><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"><a:lumMod val="65000"/><a:lumOff val="35000"/></a:schemeClr></cs:fontRef><cs:spPr><a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="tx1"><a:lumMod val="15000"/><a:lumOff val="85000"/></a:schemeClr></a:solidFill><a:round/></a:ln></cs:spPr><cs:defRPr sz="1197"/></cs:categoryAxis><cs:chartArea mods="allowNoFillOverride allowNoLineOverride"><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"/></cs:fontRef><cs:spPr><a:solidFill><a:schemeClr val="bg1"/></a:solidFill><a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="tx1"><a:lumMod val="15000"/><a:lumOff val="85000"/></a:schemeClr></a:solidFill><a:round/></a:ln></cs:spPr><cs:defRPr sz="1330"/></cs:chartArea><cs:dataLabel><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="lt1"/></cs:fontRef><cs:defRPr sz="1197"/></cs:dataLabel><cs:dataLabelCallout><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="dk1"><a:lumMod val="65000"/><a:lumOff val="35000"/></a:schemeClr></cs:fontRef><cs:spPr><a:solidFill><a:schemeClr val="lt1"/></a:solidFill><a:ln><a:solidFill><a:schemeClr val="dk1"><a:lumMod val="25000"/><a:lumOff val="75000"/></a:schemeClr></a:solidFill></a:ln></cs:spPr><cs:defRPr sz="1197"/><cs:bodyPr rot="0" spcFirstLastPara="1" vertOverflow="clip" horzOverflow="clip" vert="horz" wrap="square" lIns="36576" tIns="18288" rIns="36576" bIns="18288" anchor="ctr" anchorCtr="1"><a:spAutoFit/></cs:bodyPr></cs:dataLabelCallout><cs:dataPoint><cs:lnRef idx="0"/><cs:fillRef idx="0"><cs:styleClr val="auto"/></cs:fillRef><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"/></cs:fontRef><cs:spPr><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:ln w="19050"><a:solidFill><a:schemeClr val="lt1"/></a:solidFill></a:ln></cs:spPr></cs:dataPoint><cs:dataPoint3D><cs:lnRef idx="0"/><cs:fillRef idx="0"><cs:styleClr val="auto"/></cs:fillRef><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"/></cs:fontRef><cs:spPr><a:solidFill><a:schemeClr val="phClr"/></a:solidFill></cs:spPr></cs:dataPoint3D><cs:dataPointLine><cs:lnRef idx="0"><cs:styleClr val="auto"/></cs:lnRef><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"/></cs:fontRef><cs:spPr><a:ln w="28575" cap="rnd"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:round/></a:ln></cs:spPr></cs:dataPointLine><cs:dataPointMarker><cs:lnRef idx="0"/><cs:fillRef idx="0"><cs:styleClr val="auto"/></cs:fillRef><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"/></cs:fontRef><cs:spPr><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:ln w="9525"><a:solidFill><a:schemeClr val="lt1"/></a:solidFill></a:ln></cs:spPr></cs:dataPointMarker><cs:dataPointMarkerLayout symbol="circle" size="5"/><cs:dataPointWireframe><cs:lnRef idx="0"><cs:styleClr val="auto"/></cs:lnRef><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"/></cs:fontRef><cs:spPr><a:ln w="28575" cap="rnd"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:round/></a:ln></cs:spPr></cs:dataPointWireframe><cs:dataTable><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"><a:lumMod val="65000"/><a:lumOff val="35000"/></a:schemeClr></cs:fontRef><cs:spPr><a:ln w="9525"><a:solidFill><a:schemeClr val="tx1"><a:lumMod val="15000"/><a:lumOff val="85000"/></a:schemeClr></a:solidFill></a:ln></cs:spPr><cs:defRPr sz="1197"/></cs:dataTable><cs:downBar><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="dk1"/></cs:fontRef><cs:spPr><a:solidFill><a:schemeClr val="dk1"><a:lumMod val="65000"/><a:lumOff val="35000"/></a:schemeClr></a:solidFill><a:ln w="9525"><a:solidFill><a:schemeClr val="tx1"><a:lumMod val="65000"/><a:lumOff val="35000"/></a:schemeClr></a:solidFill></a:ln></cs:spPr></cs:downBar><cs:dropLine><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"/></cs:fontRef><cs:spPr><a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="tx1"><a:lumMod val="35000"/><a:lumOff val="65000"/></a:schemeClr></a:solidFill><a:round/></a:ln></cs:spPr></cs:dropLine><cs:errorBar><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"/></cs:fontRef><cs:spPr><a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="tx1"><a:lumMod val="65000"/><a:lumOff val="35000"/></a:schemeClr></a:solidFill><a:round/></a:ln></cs:spPr></cs:errorBar><cs:floor><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"/></cs:fontRef></cs:floor><cs:gridlineMajor><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"/></cs:fontRef><cs:spPr><a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="tx1"><a:lumMod val="15000"/><a:lumOff val="85000"/></a:schemeClr></a:solidFill><a:round/></a:ln></cs:spPr></cs:gridlineMajor><cs:gridlineMinor><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"/></cs:fontRef><cs:spPr><a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="tx1"><a:lumMod val="15000"/><a:lumOff val="85000"/></a:schemeClr></a:solidFill><a:round/></a:ln></cs:spPr></cs:gridlineMinor><cs:hiLoLine><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"/></cs:fontRef><cs:spPr><a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="tx1"><a:lumMod val="75000"/><a:lumOff val="25000"/></a:schemeClr></a:solidFill><a:round/></a:ln></cs:spPr></cs:hiLoLine><cs:leaderLine><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"/></cs:fontRef><cs:spPr><a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="tx1"><a:lumMod val="35000"/><a:lumOff val="65000"/></a:schemeClr></a:solidFill><a:round/></a:ln></cs:spPr></cs:leaderLine><cs:legend><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"><a:lumMod val="65000"/><a:lumOff val="35000"/></a:schemeClr></cs:fontRef><cs:defRPr sz="1197"/></cs:legend><cs:plotArea mods="allowNoFillOverride allowNoLineOverride"><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"/></cs:fontRef></cs:plotArea><cs:plotArea3D mods="allowNoFillOverride allowNoLineOverride"><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"/></cs:fontRef></cs:plotArea3D><cs:seriesAxis><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"><a:lumMod val="65000"/><a:lumOff val="35000"/></a:schemeClr></cs:fontRef><cs:spPr><a:ln w="9525" cap="flat" cmpd="sng" algn="ctr"><a:solidFill><a:schemeClr val="tx1"><a:lumMod val="15000"/><a:lumOff val="85000"/></a:schemeClr></a:solidFill><a:round/></a:ln></cs:spPr><cs:defRPr sz="1197"/></cs:seriesAxis><cs:seriesLine><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"/></cs:fontRef><cs:spPr><a:ln w="9525" cap="flat"><a:solidFill><a:srgbClr val="D9D9D9"/></a:solidFill><a:round/></a:ln></cs:spPr></cs:seriesLine><cs:title><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"><a:lumMod val="65000"/><a:lumOff val="35000"/></a:schemeClr></cs:fontRef><cs:defRPr sz="1862"/></cs:title><cs:trendline><cs:lnRef idx="0"><cs:styleClr val="auto"/></cs:lnRef><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"/></cs:fontRef><cs:spPr><a:ln w="19050" cap="rnd"><a:solidFill><a:schemeClr val="phClr"/></a:solidFill><a:prstDash val="sysDash"/></a:ln></cs:spPr></cs:trendline><cs:trendlineLabel><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"><a:lumMod val="65000"/><a:lumOff val="35000"/></a:schemeClr></cs:fontRef><cs:defRPr sz="1197"/></cs:trendlineLabel><cs:upBar><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="dk1"/></cs:fontRef><cs:spPr><a:solidFill><a:schemeClr val="lt1"/></a:solidFill><a:ln w="9525"><a:solidFill><a:schemeClr val="tx1"><a:lumMod val="15000"/><a:lumOff val="85000"/></a:schemeClr></a:solidFill></a:ln></cs:spPr></cs:upBar><cs:valueAxis><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"><a:lumMod val="65000"/><a:lumOff val="35000"/></a:schemeClr></cs:fontRef><cs:defRPr sz="1197"/></cs:valueAxis><cs:wall><cs:lnRef idx="0"/><cs:fillRef idx="0"/><cs:effectRef idx="0"/><cs:fontRef idx="minor"><a:schemeClr val="tx1"/></cs:fontRef></cs:wall></cs:chartStyle>''' |
| 243 | |
| 244 | |
| 245 | def _chart_ex_colors_xml(payload: dict[str, Any]) -> bytes: |
| 246 | """Return a ChartEx color style that consumes the marker palette when present.""" |
| 247 | style = payload.get("style") if isinstance(payload.get("style"), dict) else {} |
| 248 | raw_colors = _first_present(style.get("colors"), payload.get("colors")) |
| 249 | colors = ( |
| 250 | [_clean_hex(color, "#4472C4") for color in raw_colors] |
| 251 | if isinstance(raw_colors, list) |
| 252 | else [] |
| 253 | ) |
| 254 | if not colors: |
| 255 | return b'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?> |
| 256 | <cs:colorStyle xmlns:cs="http://schemas.microsoft.com/office/drawing/2012/chartStyle" |
| 257 | xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" |
| 258 | meth="cycle" id="10"> |
| 259 | <a:schemeClr val="accent1"/><a:schemeClr val="accent2"/><a:schemeClr val="accent3"/> |
| 260 | <a:schemeClr val="accent4"/><a:schemeClr val="accent5"/><a:schemeClr val="accent6"/> |
| 261 | </cs:colorStyle>''' |
| 262 | color_entries = "".join( |
| 263 | f'<a:srgbClr val="{color}"/>' |
| 264 | for color in colors |
| 265 | ) |
| 266 | xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?> |
| 267 | <cs:colorStyle xmlns:cs="http://schemas.microsoft.com/office/drawing/2012/chartStyle" |
| 268 | xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" |
| 269 | meth="cycle" id="10"> |
| 270 | {color_entries} |
| 271 | </cs:colorStyle>''' |
| 272 | return xml.encode("utf-8") |
| 273 |