1###############################################################################
2#
3# ChartPie - A class for writing the Excel XLSX Pie charts.
4#
5# SPDX-License-Identifier: BSD-2-Clause
6#
7# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org
8#
9
10from warnings import warn
11
12from . import chart
13
14
15class ChartPie(chart.Chart):
16 """
17 A class for writing the Excel XLSX Pie charts.
18
19
20 """
21
22 ###########################################################################
23 #
24 # Public API.
25 #
26 ###########################################################################
27
28 def __init__(self):
29 """
30 Constructor.
31
32 """
33 super().__init__()
34
35 self.vary_data_color = 1
36 self.rotation = 0
37
38 # Set the available data label positions for this chart type.
39 self.label_position_default = "best_fit"
40 self.label_positions = {
41 "center": "ctr",
42 "inside_end": "inEnd",
43 "outside_end": "outEnd",
44 "best_fit": "bestFit",
45 }
46
47 def set_rotation(self, rotation):
48 """
49 Set the Pie/Doughnut chart rotation: the angle of the first slice.
50
51 Args:
52 rotation: First segment angle: 0 <= rotation <= 360.
53
54 Returns:
55 Nothing.
56
57 """
58 if rotation is None:
59 return
60
61 # Ensure the rotation is in Excel's range.
62 if rotation < 0 or rotation > 360:
63 warn(
64 f"Chart rotation '{rotation}' outside Excel range: 0 <= rotation <= 360"
65 )
66 return
67
68 self.rotation = int(rotation)
69
70 ###########################################################################
71 #
72 # Private API.
73 #
74 ###########################################################################
75
76 def _write_chart_type(self, args):
77 # Override the virtual superclass method with a chart specific method.
78 # Write the c:pieChart element.
79 self._write_pie_chart()
80
81 ###########################################################################
82 #
83 # XML methods.
84 #
85 ###########################################################################
86
87 def _write_pie_chart(self):
88 # Write the <c:pieChart> element. Over-ridden method to remove
89 # axis_id code since Pie charts don't require val and cat axes.
90 self._xml_start_tag("c:pieChart")
91
92 # Write the c:varyColors element.
93 self._write_vary_colors()
94
95 # Write the series elements.
96 for data in self.series:
97 self._write_ser(data)
98
99 # Write the c:firstSliceAng element.
100 self._write_first_slice_ang()
101
102 self._xml_end_tag("c:pieChart")
103
104 def _write_plot_area(self):
105 # Over-ridden method to remove the cat_axis() and val_axis() code
106 # since Pie charts don't require those axes.
107 #
108 # Write the <c:plotArea> element.
109
110 self._xml_start_tag("c:plotArea")
111
112 # Write the c:layout element.
113 self._write_layout(self.plotarea.get("layout"), "plot")
114
115 # Write the subclass chart type element.
116 self._write_chart_type(None)
117 # Configure a combined chart if present.
118 second_chart = self.combined
119
120 if second_chart:
121 # Secondary axis has unique id otherwise use same as primary.
122 if second_chart.is_secondary:
123 second_chart.id = 1000 + self.id
124 else:
125 second_chart.id = self.id
126
127 # Share the same filehandle for writing.
128 second_chart.fh = self.fh
129
130 # Share series index with primary chart.
131 second_chart.series_index = self.series_index
132
133 # Write the subclass chart type elements for combined chart.
134 # pylint: disable-next=protected-access
135 second_chart._write_chart_type(None)
136
137 # Write the c:spPr element for the plotarea formatting.
138 self._write_sp_pr(self.plotarea)
139
140 self._xml_end_tag("c:plotArea")
141
142 def _write_legend(self):
143 # Over-ridden method to add <c:txPr> to legend.
144 # Write the <c:legend> element.
145 legend = self.legend
146 position = legend.get("position", "right")
147 font = legend.get("font")
148 delete_series = []
149 overlay = 0
150
151 if legend.get("delete_series") and isinstance(legend["delete_series"], list):
152 delete_series = legend["delete_series"]
153
154 if position.startswith("overlay_"):
155 position = position.replace("overlay_", "")
156 overlay = 1
157
158 allowed = {
159 "right": "r",
160 "left": "l",
161 "top": "t",
162 "bottom": "b",
163 "top_right": "tr",
164 }
165
166 if position == "none":
167 return
168
169 if position not in allowed:
170 return
171
172 position = allowed[position]
173
174 self._xml_start_tag("c:legend")
175
176 # Write the c:legendPos element.
177 self._write_legend_pos(position)
178
179 # Remove series labels from the legend.
180 for index in delete_series:
181 # Write the c:legendEntry element.
182 self._write_legend_entry(index)
183
184 # Write the c:layout element.
185 self._write_layout(legend.get("layout"), "legend")
186
187 # Write the c:overlay element.
188 if overlay:
189 self._write_overlay()
190
191 # Write the c:spPr element.
192 self._write_sp_pr(legend)
193
194 # Write the c:txPr element. Over-ridden.
195 self._write_tx_pr_legend(None, font)
196
197 self._xml_end_tag("c:legend")
198
199 def _write_tx_pr_legend(self, horiz, font):
200 # Write the <c:txPr> element for legends.
201
202 if font and font.get("rotation"):
203 rotation = font["rotation"]
204 else:
205 rotation = None
206
207 self._xml_start_tag("c:txPr")
208
209 # Write the a:bodyPr element.
210 self._write_a_body_pr(rotation, horiz)
211
212 # Write the a:lstStyle element.
213 self._write_a_lst_style()
214
215 # Write the a:p element.
216 self._write_a_p_legend(font)
217
218 self._xml_end_tag("c:txPr")
219
220 def _write_a_p_legend(self, font):
221 # Write the <a:p> element for legends.
222
223 self._xml_start_tag("a:p")
224
225 # Write the a:pPr element.
226 self._write_a_p_pr_legend(font)
227
228 # Write the a:endParaRPr element.
229 self._write_a_end_para_rpr()
230
231 self._xml_end_tag("a:p")
232
233 def _write_a_p_pr_legend(self, font):
234 # Write the <a:pPr> element for legends.
235 attributes = [("rtl", 0)]
236
237 self._xml_start_tag("a:pPr", attributes)
238
239 # Write the a:defRPr element.
240 self._write_a_def_rpr(font)
241
242 self._xml_end_tag("a:pPr")
243
244 def _write_vary_colors(self):
245 # Write the <c:varyColors> element.
246 attributes = [("val", 1)]
247
248 self._xml_empty_tag("c:varyColors", attributes)
249
250 def _write_first_slice_ang(self):
251 # Write the <c:firstSliceAng> element.
252 attributes = [("val", self.rotation)]
253
254 self._xml_empty_tag("c:firstSliceAng", attributes)
255
256 def _write_show_leader_lines(self):
257 # Write the <c:showLeaderLines> element.
258 #
259 # This is for Pie/Doughnut charts. Other chart types only supported
260 # leader lines after Excel 2015 via an extension element.
261 attributes = [("val", 1)]
262
263 self._xml_empty_tag("c:showLeaderLines", attributes)