1###############################################################################
2#
3# ChartScatter - A class for writing the Excel XLSX Scatter 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 ChartScatter(chart.Chart):
16 """
17 A class for writing the Excel XLSX Scatter charts.
18
19
20 """
21
22 ###########################################################################
23 #
24 # Public API.
25 #
26 ###########################################################################
27
28 def __init__(self, options=None):
29 """
30 Constructor.
31
32 """
33 super().__init__()
34
35 if options is None:
36 options = {}
37
38 self.subtype = options.get("subtype")
39
40 if not self.subtype:
41 self.subtype = "marker_only"
42
43 self.cross_between = "midCat"
44 self.horiz_val_axis = 0
45 self.val_axis_position = "b"
46 self.smooth_allowed = True
47 self.requires_category = True
48
49 # Set the available data label positions for this chart type.
50 self.label_position_default = "right"
51 self.label_positions = {
52 "center": "ctr",
53 "right": "r",
54 "left": "l",
55 "above": "t",
56 "below": "b",
57 # For backward compatibility.
58 "top": "t",
59 "bottom": "b",
60 }
61
62 def combine(self, chart=None):
63 # pylint: disable=redefined-outer-name
64 """
65 Create a combination chart with a secondary chart.
66
67 Note: Override parent method to add a warning.
68
69 Args:
70 chart: The secondary chart to combine with the primary chart.
71
72 Returns:
73 Nothing.
74
75 """
76 if chart is None:
77 return
78
79 warn(
80 "Combined chart not currently supported with scatter chart "
81 "as the primary chart"
82 )
83
84 ###########################################################################
85 #
86 # Private API.
87 #
88 ###########################################################################
89
90 def _write_chart_type(self, args):
91 # Override the virtual superclass method with a chart specific method.
92 # Write the c:scatterChart element.
93 self._write_scatter_chart(args)
94
95 ###########################################################################
96 #
97 # XML methods.
98 #
99 ###########################################################################
100
101 def _write_scatter_chart(self, args):
102 # Write the <c:scatterChart> element.
103
104 if args["primary_axes"]:
105 series = self._get_primary_axes_series()
106 else:
107 series = self._get_secondary_axes_series()
108
109 if not series:
110 return
111
112 style = "lineMarker"
113 subtype = self.subtype
114
115 # Set the user defined chart subtype.
116 if subtype == "marker_only":
117 style = "lineMarker"
118
119 if subtype == "straight_with_markers":
120 style = "lineMarker"
121
122 if subtype == "straight":
123 style = "lineMarker"
124 self.default_marker = {"type": "none"}
125
126 if subtype == "smooth_with_markers":
127 style = "smoothMarker"
128
129 if subtype == "smooth":
130 style = "smoothMarker"
131 self.default_marker = {"type": "none"}
132
133 # Add default formatting to the series data.
134 self._modify_series_formatting()
135
136 self._xml_start_tag("c:scatterChart")
137
138 # Write the c:scatterStyle element.
139 self._write_scatter_style(style)
140
141 # Write the series elements.
142 for data in series:
143 self._write_ser(data)
144
145 # Write the c:axId elements
146 self._write_axis_ids(args)
147
148 self._xml_end_tag("c:scatterChart")
149
150 def _write_ser(self, series):
151 # Over-ridden to write c:xVal/c:yVal instead of c:cat/c:val elements.
152 # Write the <c:ser> element.
153
154 index = self.series_index
155 self.series_index += 1
156
157 self._xml_start_tag("c:ser")
158
159 # Write the c:idx element.
160 self._write_idx(index)
161
162 # Write the c:order element.
163 self._write_order(index)
164
165 # Write the series name.
166 self._write_series_name(series)
167
168 # Write the c:spPr element.
169 self._write_sp_pr(series)
170
171 # Write the c:marker element.
172 self._write_marker(series.get("marker"))
173
174 # Write the c:dPt element.
175 self._write_d_pt(series.get("points"))
176
177 # Write the c:dLbls element.
178 self._write_d_lbls(series.get("labels"))
179
180 # Write the c:trendline element.
181 self._write_trendline(series.get("trendline"))
182
183 # Write the c:errBars element.
184 self._write_error_bars(series.get("error_bars"))
185
186 # Write the c:xVal element.
187 self._write_x_val(series)
188
189 # Write the c:yVal element.
190 self._write_y_val(series)
191
192 # Write the c:smooth element.
193 if "smooth" in self.subtype and series["smooth"] is None:
194 # Default is on for smooth scatter charts.
195 self._write_c_smooth(True)
196 else:
197 self._write_c_smooth(series["smooth"])
198
199 self._xml_end_tag("c:ser")
200
201 def _write_plot_area(self):
202 # Over-ridden to have 2 valAx elements for scatter charts instead
203 # of catAx/valAx.
204 #
205 # Write the <c:plotArea> element.
206 self._xml_start_tag("c:plotArea")
207
208 # Write the c:layout element.
209 self._write_layout(self.plotarea.get("layout"), "plot")
210
211 # Write the subclass chart elements for primary and secondary axes.
212 self._write_chart_type({"primary_axes": 1})
213 self._write_chart_type({"primary_axes": 0})
214
215 # Write c:catAx and c:valAx elements for series using primary axes.
216 self._write_cat_val_axis(
217 {
218 "x_axis": self.x_axis,
219 "y_axis": self.y_axis,
220 "axis_ids": self.axis_ids,
221 "position": "b",
222 }
223 )
224
225 tmp = self.horiz_val_axis
226 self.horiz_val_axis = 1
227
228 self._write_val_axis(
229 {
230 "x_axis": self.x_axis,
231 "y_axis": self.y_axis,
232 "axis_ids": self.axis_ids,
233 "position": "l",
234 }
235 )
236
237 self.horiz_val_axis = tmp
238
239 # Write c:valAx and c:catAx elements for series using secondary axes
240 self._write_cat_val_axis(
241 {
242 "x_axis": self.x2_axis,
243 "y_axis": self.y2_axis,
244 "axis_ids": self.axis2_ids,
245 "position": "b",
246 }
247 )
248 self.horiz_val_axis = 1
249 self._write_val_axis(
250 {
251 "x_axis": self.x2_axis,
252 "y_axis": self.y2_axis,
253 "axis_ids": self.axis2_ids,
254 "position": "l",
255 }
256 )
257
258 # Write the c:spPr element for the plotarea formatting.
259 self._write_sp_pr(self.plotarea)
260
261 self._xml_end_tag("c:plotArea")
262
263 def _write_x_val(self, series):
264 # Write the <c:xVal> element.
265 formula = series.get("categories")
266 data_id = series.get("cat_data_id")
267 data = self.formula_data[data_id]
268
269 self._xml_start_tag("c:xVal")
270
271 # Check the type of cached data.
272 data_type = self._get_data_type(data)
273
274 if data_type == "str":
275 # Write the c:numRef element.
276 self._write_str_ref(formula, data, data_type)
277 else:
278 # Write the c:numRef element.
279 self._write_num_ref(formula, data, data_type)
280
281 self._xml_end_tag("c:xVal")
282
283 def _write_y_val(self, series):
284 # Write the <c:yVal> element.
285 formula = series.get("values")
286 data_id = series.get("val_data_id")
287 data = self.formula_data[data_id]
288
289 self._xml_start_tag("c:yVal")
290
291 # Unlike Cat axes data should only be numeric.
292 # Write the c:numRef element.
293 self._write_num_ref(formula, data, "num")
294
295 self._xml_end_tag("c:yVal")
296
297 def _write_scatter_style(self, val):
298 # Write the <c:scatterStyle> element.
299 attributes = [("val", val)]
300
301 self._xml_empty_tag("c:scatterStyle", attributes)
302
303 def _modify_series_formatting(self):
304 # Add default formatting to the series data unless it has already been
305 # specified by the user.
306 subtype = self.subtype
307
308 # The default scatter style "markers only" requires a line type.
309 if subtype == "marker_only":
310 # Go through each series and define default values.
311 for series in self.series:
312 # Set a line type unless there is already a user defined type.
313 if not series["line"]["defined"]:
314 series["line"] = {
315 "width": 2.25,
316 "none": 1,
317 "defined": 1,
318 }
319
320 def _write_d_pt_point(self, index, point):
321 # Write an individual <c:dPt> element. Override the parent method to
322 # add markers.
323
324 self._xml_start_tag("c:dPt")
325
326 # Write the c:idx element.
327 self._write_idx(index)
328
329 self._xml_start_tag("c:marker")
330
331 # Write the c:spPr element.
332 self._write_sp_pr(point)
333
334 self._xml_end_tag("c:marker")
335
336 self._xml_end_tag("c:dPt")