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