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