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