返回 ppt-master
chartex.py
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
273 lines PYTHON