Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/xlsxwriter/chart.py: 11%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

2012 statements  

1############################################################################### 

2# 

3# Chart - A class for writing the Excel XLSX Worksheet file. 

4# 

5# SPDX-License-Identifier: BSD-2-Clause 

6# 

7# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org 

8# 

9 

10import copy 

11import re 

12from typing import Any, Dict, Optional 

13from warnings import warn 

14 

15from xlsxwriter.color import Color, ColorTypes 

16 

17from . import xmlwriter 

18from .shape import Shape 

19from .utility import ( 

20 _datetime_to_excel_datetime, 

21 _supported_datetime, 

22 quote_sheetname, 

23 xl_range_formula, 

24 xl_rowcol_to_cell, 

25) 

26 

27 

28class Chart(xmlwriter.XMLwriter): 

29 """ 

30 A class for writing the Excel XLSX Chart file. 

31 

32 

33 """ 

34 

35 ########################################################################### 

36 # 

37 # Public API. 

38 # 

39 ########################################################################### 

40 

41 def __init__(self) -> None: 

42 """ 

43 Constructor. 

44 

45 """ 

46 

47 super().__init__() 

48 

49 self.subtype = None 

50 self.sheet_type = 0x0200 

51 self.orientation = 0x0 

52 self.series = [] 

53 self.embedded = 0 

54 self.id = -1 

55 self.series_index = 0 

56 self.style_id = 2 

57 self.axis_ids = [] 

58 self.axis2_ids = [] 

59 self.cat_has_num_fmt = False 

60 self.requires_category = False 

61 self.legend = {} 

62 self.cat_axis_position = "b" 

63 self.val_axis_position = "l" 

64 self.formula_ids = {} 

65 self.formula_data = [] 

66 self.horiz_cat_axis = 0 

67 self.horiz_val_axis = 1 

68 self.protection = 0 

69 self.chartarea = {} 

70 self.plotarea = {} 

71 self.x_axis = {} 

72 self.y_axis = {} 

73 self.y2_axis = {} 

74 self.x2_axis = {} 

75 self.chart_name = "" 

76 self.show_blanks = "gap" 

77 self.show_na_as_empty = False 

78 self.show_hidden = False 

79 self.show_crosses = True 

80 self.width = 480 

81 self.height = 288 

82 self.x_scale = 1 

83 self.y_scale = 1 

84 self.x_offset = 0 

85 self.y_offset = 0 

86 self.table = None 

87 self.cross_between = "between" 

88 self.default_marker = None 

89 self.series_gap_1 = None 

90 self.series_gap_2 = None 

91 self.series_overlap_1 = None 

92 self.series_overlap_2 = None 

93 self.drop_lines = None 

94 self.hi_low_lines = None 

95 self.up_down_bars = None 

96 self.smooth_allowed = False 

97 self.title_font = None 

98 self.title_name = None 

99 self.title_formula = None 

100 self.title_data_id = None 

101 self.title_layout = None 

102 self.title_overlay = None 

103 self.title_none = False 

104 self.date_category = False 

105 self.date_1904 = False 

106 self.remove_timezone = False 

107 self.label_positions = {} 

108 self.label_position_default = "" 

109 self.already_inserted = False 

110 self.combined = None 

111 self.is_secondary = False 

112 self.warn_sheetname = True 

113 self._set_default_properties() 

114 self.fill = {} 

115 

116 def add_series(self, options: Optional[Dict[str, Any]] = None) -> None: 

117 """ 

118 Add a data series to a chart. 

119 

120 Args: 

121 options: A dictionary of chart series options. 

122 

123 Returns: 

124 Nothing. 

125 

126 """ 

127 # Add a series and it's properties to a chart. 

128 if options is None: 

129 options = {} 

130 

131 # Check that the required input has been specified. 

132 if "values" not in options: 

133 warn("Must specify 'values' in add_series()") 

134 return 

135 

136 if self.requires_category and "categories" not in options: 

137 warn("Must specify 'categories' in add_series() for this chart type") 

138 return 

139 

140 if len(self.series) == 255: 

141 warn( 

142 "The maximum number of series that can be added to an " 

143 "Excel Chart is 255" 

144 ) 

145 return 

146 

147 # Convert list into a formula string. 

148 values = self._list_to_formula(options.get("values")) 

149 categories = self._list_to_formula(options.get("categories")) 

150 

151 # Switch name and name_formula parameters if required. 

152 name, name_formula = self._process_names( 

153 options.get("name"), options.get("name_formula") 

154 ) 

155 

156 # Get an id for the data equivalent to the range formula. 

157 cat_id = self._get_data_id(categories, options.get("categories_data")) 

158 val_id = self._get_data_id(values, options.get("values_data")) 

159 name_id = self._get_data_id(name_formula, options.get("name_data")) 

160 

161 # Set the line properties for the series. 

162 line = Shape._get_line_properties(options.get("line")) 

163 

164 # Allow 'border' as a synonym for 'line' in bar/column style charts. 

165 if options.get("border"): 

166 line = Shape._get_line_properties(options["border"]) 

167 

168 # Set the fill properties for the series. 

169 fill = Shape._get_fill_properties(options.get("fill")) 

170 

171 # Set the pattern fill properties for the series. 

172 pattern = Shape._get_pattern_properties(options.get("pattern")) 

173 

174 # Set the gradient fill properties for the series. 

175 gradient = Shape._get_gradient_properties(options.get("gradient")) 

176 

177 # Pattern fill overrides solid fill. 

178 if pattern: 

179 self.fill = None 

180 

181 # Gradient fill overrides the solid and pattern fill. 

182 if gradient: 

183 pattern = None 

184 fill = None 

185 

186 # Set the marker properties for the series. 

187 marker = self._get_marker_properties(options.get("marker")) 

188 

189 # Set the trendline properties for the series. 

190 trendline = self._get_trendline_properties(options.get("trendline")) 

191 

192 # Set the line smooth property for the series. 

193 smooth = options.get("smooth") 

194 

195 # Set the error bars properties for the series. 

196 y_error_bars = self._get_error_bars_props(options.get("y_error_bars")) 

197 x_error_bars = self._get_error_bars_props(options.get("x_error_bars")) 

198 

199 error_bars = {"x_error_bars": x_error_bars, "y_error_bars": y_error_bars} 

200 

201 # Set the point properties for the series. 

202 points = self._get_points_properties(options.get("points")) 

203 

204 # Set the labels properties for the series. 

205 labels = self._get_labels_properties(options.get("data_labels")) 

206 

207 # Set the "invert if negative" fill property. 

208 invert_if_neg = options.get("invert_if_negative", False) 

209 inverted_color = options.get("invert_if_negative_color") 

210 

211 if inverted_color: 

212 inverted_color = Color._from_value(inverted_color) 

213 

214 # Set the secondary axis properties. 

215 x2_axis = options.get("x2_axis") 

216 y2_axis = options.get("y2_axis") 

217 

218 # Store secondary status for combined charts. 

219 if x2_axis or y2_axis: 

220 self.is_secondary = True 

221 

222 # Set the gap for Bar/Column charts. 

223 if options.get("gap") is not None: 

224 if y2_axis: 

225 self.series_gap_2 = options["gap"] 

226 else: 

227 self.series_gap_1 = options["gap"] 

228 

229 # Set the overlap for Bar/Column charts. 

230 if options.get("overlap"): 

231 if y2_axis: 

232 self.series_overlap_2 = options["overlap"] 

233 else: 

234 self.series_overlap_1 = options["overlap"] 

235 

236 # Add the user supplied data to the internal structures. 

237 series = { 

238 "values": values, 

239 "categories": categories, 

240 "name": name, 

241 "name_formula": name_formula, 

242 "name_id": name_id, 

243 "val_data_id": val_id, 

244 "cat_data_id": cat_id, 

245 "line": line, 

246 "fill": fill, 

247 "pattern": pattern, 

248 "gradient": gradient, 

249 "marker": marker, 

250 "trendline": trendline, 

251 "labels": labels, 

252 "invert_if_neg": invert_if_neg, 

253 "inverted_color": inverted_color, 

254 "x2_axis": x2_axis, 

255 "y2_axis": y2_axis, 

256 "points": points, 

257 "error_bars": error_bars, 

258 "smooth": smooth, 

259 } 

260 

261 self.series.append(series) 

262 

263 def set_x_axis(self, options: Dict[str, Any]) -> None: 

264 """ 

265 Set the chart X axis options. 

266 

267 Args: 

268 options: A dictionary of axis options. 

269 

270 Returns: 

271 Nothing. 

272 

273 """ 

274 axis = self._convert_axis_args(self.x_axis, options) 

275 

276 self.x_axis = axis 

277 

278 def set_y_axis(self, options: Dict[str, Any]) -> None: 

279 """ 

280 Set the chart Y axis options. 

281 

282 Args: 

283 options: A dictionary of axis options. 

284 

285 Returns: 

286 Nothing. 

287 

288 """ 

289 axis = self._convert_axis_args(self.y_axis, options) 

290 

291 self.y_axis = axis 

292 

293 def set_x2_axis(self, options: Dict[str, Any]) -> None: 

294 """ 

295 Set the chart secondary X axis options. 

296 

297 Args: 

298 options: A dictionary of axis options. 

299 

300 Returns: 

301 Nothing. 

302 

303 """ 

304 axis = self._convert_axis_args(self.x2_axis, options) 

305 

306 self.x2_axis = axis 

307 

308 def set_y2_axis(self, options: Dict[str, Any]) -> None: 

309 """ 

310 Set the chart secondary Y axis options. 

311 

312 Args: 

313 options: A dictionary of axis options. 

314 

315 Returns: 

316 Nothing. 

317 

318 """ 

319 axis = self._convert_axis_args(self.y2_axis, options) 

320 

321 self.y2_axis = axis 

322 

323 def set_title(self, options: Optional[Dict[str, Any]] = None) -> None: 

324 """ 

325 Set the chart title options. 

326 

327 Args: 

328 options: A dictionary of chart title options. 

329 

330 Returns: 

331 Nothing. 

332 

333 """ 

334 if options is None: 

335 options = {} 

336 

337 name, name_formula = self._process_names( 

338 options.get("name"), options.get("name_formula") 

339 ) 

340 

341 data_id = self._get_data_id(name_formula, options.get("data")) 

342 

343 self.title_name = name 

344 self.title_formula = name_formula 

345 self.title_data_id = data_id 

346 

347 # Set the font properties if present. 

348 self.title_font = self._convert_font_args(options.get("name_font")) 

349 

350 # Set the axis name layout. 

351 self.title_layout = self._get_layout_properties(options.get("layout"), True) 

352 # Set the title overlay option. 

353 self.title_overlay = options.get("overlay") 

354 

355 # Set the automatic title option. 

356 self.title_none = options.get("none") 

357 

358 def set_legend(self, options: Dict[str, Any]) -> None: 

359 """ 

360 Set the chart legend options. 

361 

362 Args: 

363 options: A dictionary of chart legend options. 

364 

365 Returns: 

366 Nothing. 

367 """ 

368 # Convert the user defined properties to internal properties. 

369 self.legend = self._get_legend_properties(options) 

370 

371 def set_plotarea(self, options: Dict[str, Any]) -> None: 

372 """ 

373 Set the chart plot area options. 

374 

375 Args: 

376 options: A dictionary of chart plot area options. 

377 

378 Returns: 

379 Nothing. 

380 """ 

381 # Convert the user defined properties to internal properties. 

382 self.plotarea = self._get_area_properties(options) 

383 

384 def set_chartarea(self, options: Dict[str, Any]) -> None: 

385 """ 

386 Set the chart area options. 

387 

388 Args: 

389 options: A dictionary of chart area options. 

390 

391 Returns: 

392 Nothing. 

393 """ 

394 # Convert the user defined properties to internal properties. 

395 self.chartarea = self._get_area_properties(options) 

396 

397 def set_style(self, style_id: int = 2) -> None: 

398 """ 

399 Set the chart style type. 

400 

401 Args: 

402 style_id: An int representing the chart style. 

403 

404 Returns: 

405 Nothing. 

406 """ 

407 # Set one of the 48 built-in Excel chart styles. The default is 2. 

408 if style_id is None: 

409 style_id = 2 

410 

411 if style_id < 1 or style_id > 48: 

412 style_id = 2 

413 

414 self.style_id = style_id 

415 

416 def show_blanks_as(self, option: str) -> None: 

417 """ 

418 Set the option for displaying blank data in a chart. 

419 

420 Args: 

421 option: A string representing the display option. 

422 

423 Returns: 

424 Nothing. 

425 """ 

426 if not option: 

427 return 

428 

429 valid_options = { 

430 "gap": 1, 

431 "zero": 1, 

432 "span": 1, 

433 } 

434 

435 if option not in valid_options: 

436 warn(f"Unknown show_blanks_as() option '{option}'") 

437 return 

438 

439 self.show_blanks = option 

440 

441 def show_na_as_empty_cell(self) -> None: 

442 """ 

443 Display ``#N/A`` on charts as blank/empty cells. 

444 

445 Args: 

446 None. 

447 

448 Returns: 

449 Nothing. 

450 """ 

451 self.show_na_as_empty = True 

452 

453 def show_hidden_data(self) -> None: 

454 """ 

455 Display data on charts from hidden rows or columns. 

456 

457 Args: 

458 None. 

459 

460 Returns: 

461 Nothing. 

462 """ 

463 self.show_hidden = True 

464 

465 def set_size(self, options: Optional[Dict[str, Any]] = None) -> None: 

466 """ 

467 Set size or scale of the chart. 

468 

469 Args: 

470 options: A dictionary of chart size options. 

471 

472 Returns: 

473 Nothing. 

474 """ 

475 if options is None: 

476 options = {} 

477 

478 # Set dimensions or scale for the chart. 

479 self.width = options.get("width", self.width) 

480 self.height = options.get("height", self.height) 

481 self.x_scale = options.get("x_scale", 1) 

482 self.y_scale = options.get("y_scale", 1) 

483 self.x_offset = options.get("x_offset", 0) 

484 self.y_offset = options.get("y_offset", 0) 

485 

486 def set_table(self, options: Optional[Dict[str, Any]] = None) -> None: 

487 """ 

488 Set properties for an axis data table. 

489 

490 Args: 

491 options: A dictionary of axis table options. 

492 

493 Returns: 

494 Nothing. 

495 

496 """ 

497 if options is None: 

498 options = {} 

499 

500 table = {} 

501 

502 table["horizontal"] = options.get("horizontal", 1) 

503 table["vertical"] = options.get("vertical", 1) 

504 table["outline"] = options.get("outline", 1) 

505 table["show_keys"] = options.get("show_keys", 0) 

506 table["font"] = self._convert_font_args(options.get("font")) 

507 

508 self.table = table 

509 

510 def set_up_down_bars(self, options: Optional[Dict[str, Any]] = None) -> None: 

511 """ 

512 Set properties for the chart up-down bars. 

513 

514 Args: 

515 options: A dictionary of options. 

516 

517 Returns: 

518 Nothing. 

519 

520 """ 

521 if options is None: 

522 options = {} 

523 

524 # Defaults. 

525 up_line = None 

526 up_fill = None 

527 down_line = None 

528 down_fill = None 

529 

530 # Set properties for 'up' bar. 

531 if options.get("up"): 

532 if "border" in options["up"]: 

533 # Map border to line. 

534 up_line = Shape._get_line_properties(options["up"]["border"]) 

535 

536 if "line" in options["up"]: 

537 up_line = Shape._get_line_properties(options["up"]["line"]) 

538 

539 if "fill" in options["up"]: 

540 up_fill = Shape._get_fill_properties(options["up"]["fill"]) 

541 

542 # Set properties for 'down' bar. 

543 if options.get("down"): 

544 if "border" in options["down"]: 

545 # Map border to line. 

546 down_line = Shape._get_line_properties(options["down"]["border"]) 

547 

548 if "line" in options["down"]: 

549 down_line = Shape._get_line_properties(options["down"]["line"]) 

550 

551 if "fill" in options["down"]: 

552 down_fill = Shape._get_fill_properties(options["down"]["fill"]) 

553 

554 self.up_down_bars = { 

555 "up": { 

556 "line": up_line, 

557 "fill": up_fill, 

558 }, 

559 "down": { 

560 "line": down_line, 

561 "fill": down_fill, 

562 }, 

563 } 

564 

565 def set_drop_lines(self, options: Optional[Dict[str, Any]] = None) -> None: 

566 """ 

567 Set properties for the chart drop lines. 

568 

569 Args: 

570 options: A dictionary of options. 

571 

572 Returns: 

573 Nothing. 

574 

575 """ 

576 if options is None: 

577 options = {} 

578 

579 line = Shape._get_line_properties(options.get("line")) 

580 fill = Shape._get_fill_properties(options.get("fill")) 

581 

582 # Set the pattern fill properties for the series. 

583 pattern = Shape._get_pattern_properties(options.get("pattern")) 

584 

585 # Set the gradient fill properties for the series. 

586 gradient = Shape._get_gradient_properties(options.get("gradient")) 

587 

588 # Pattern fill overrides solid fill. 

589 if pattern: 

590 self.fill = None 

591 

592 # Gradient fill overrides the solid and pattern fill. 

593 if gradient: 

594 pattern = None 

595 fill = None 

596 

597 self.drop_lines = { 

598 "line": line, 

599 "fill": fill, 

600 "pattern": pattern, 

601 "gradient": gradient, 

602 } 

603 

604 def set_high_low_lines(self, options: Optional[Dict[str, Any]] = None) -> None: 

605 """ 

606 Set properties for the chart high-low lines. 

607 

608 Args: 

609 options: A dictionary of options. 

610 

611 Returns: 

612 Nothing. 

613 

614 """ 

615 if options is None: 

616 options = {} 

617 

618 line = Shape._get_line_properties(options.get("line")) 

619 fill = Shape._get_fill_properties(options.get("fill")) 

620 

621 # Set the pattern fill properties for the series. 

622 pattern = Shape._get_pattern_properties(options.get("pattern")) 

623 

624 # Set the gradient fill properties for the series. 

625 gradient = Shape._get_gradient_properties(options.get("gradient")) 

626 

627 # Pattern fill overrides solid fill. 

628 if pattern: 

629 self.fill = None 

630 

631 # Gradient fill overrides the solid and pattern fill. 

632 if gradient: 

633 pattern = None 

634 fill = None 

635 

636 self.hi_low_lines = { 

637 "line": line, 

638 "fill": fill, 

639 "pattern": pattern, 

640 "gradient": gradient, 

641 } 

642 

643 def combine(self, chart: Optional["Chart"] = None) -> None: 

644 """ 

645 Create a combination chart with a secondary chart. 

646 

647 Args: 

648 chart: The secondary chart to combine with the primary chart. 

649 

650 Returns: 

651 Nothing. 

652 

653 """ 

654 if chart is None: 

655 return 

656 

657 self.combined = chart 

658 

659 ########################################################################### 

660 # 

661 # Private API. 

662 # 

663 ########################################################################### 

664 

665 def _assemble_xml_file(self) -> None: 

666 # Assemble and write the XML file. 

667 

668 # Write the XML declaration. 

669 self._xml_declaration() 

670 

671 # Write the c:chartSpace element. 

672 self._write_chart_space() 

673 

674 # Write the c:lang element. 

675 self._write_lang() 

676 

677 # Write the c:style element. 

678 self._write_style() 

679 

680 # Write the c:protection element. 

681 self._write_protection() 

682 

683 # Write the c:chart element. 

684 self._write_chart() 

685 

686 # Write the c:spPr element for the chartarea formatting. 

687 self._write_sp_pr(self.chartarea) 

688 

689 # Write the c:printSettings element. 

690 if self.embedded: 

691 self._write_print_settings() 

692 

693 # Close the worksheet tag. 

694 self._xml_end_tag("c:chartSpace") 

695 # Close the file. 

696 self._xml_close() 

697 

698 def _convert_axis_args(self, axis, user_options): 

699 # Convert user defined axis values into private hash values. 

700 options = axis["defaults"].copy() 

701 options.update(user_options) 

702 

703 name, name_formula = self._process_names( 

704 options.get("name"), options.get("name_formula") 

705 ) 

706 

707 data_id = self._get_data_id(name_formula, options.get("data")) 

708 

709 axis = { 

710 "defaults": axis["defaults"], 

711 "name": name, 

712 "formula": name_formula, 

713 "data_id": data_id, 

714 "reverse": options.get("reverse"), 

715 "min": options.get("min"), 

716 "max": options.get("max"), 

717 "minor_unit": options.get("minor_unit"), 

718 "major_unit": options.get("major_unit"), 

719 "minor_unit_type": options.get("minor_unit_type"), 

720 "major_unit_type": options.get("major_unit_type"), 

721 "display_units": options.get("display_units"), 

722 "log_base": options.get("log_base"), 

723 "crossing": options.get("crossing"), 

724 "position_axis": options.get("position_axis"), 

725 "position": options.get("position"), 

726 "label_position": options.get("label_position"), 

727 "label_align": options.get("label_align"), 

728 "num_format": options.get("num_format"), 

729 "num_format_linked": options.get("num_format_linked"), 

730 "interval_unit": options.get("interval_unit"), 

731 "interval_tick": options.get("interval_tick"), 

732 "text_axis": False, 

733 } 

734 

735 axis["visible"] = options.get("visible", True) 

736 

737 # Convert the display units. 

738 axis["display_units"] = self._get_display_units(axis["display_units"]) 

739 axis["display_units_visible"] = options.get("display_units_visible", True) 

740 

741 # Map major_gridlines properties. 

742 if options.get("major_gridlines") and options["major_gridlines"]["visible"]: 

743 axis["major_gridlines"] = self._get_gridline_properties( 

744 options["major_gridlines"] 

745 ) 

746 

747 # Map minor_gridlines properties. 

748 if options.get("minor_gridlines") and options["minor_gridlines"]["visible"]: 

749 axis["minor_gridlines"] = self._get_gridline_properties( 

750 options["minor_gridlines"] 

751 ) 

752 

753 # Only use the first letter of bottom, top, left or right. 

754 if axis.get("position"): 

755 axis["position"] = axis["position"].lower()[0] 

756 

757 # Set the position for a category axis on or between the tick marks. 

758 if axis.get("position_axis"): 

759 if axis["position_axis"] == "on_tick": 

760 axis["position_axis"] = "midCat" 

761 elif axis["position_axis"] == "between": 

762 # Doesn't need to be modified. 

763 pass 

764 else: 

765 # Otherwise use the default value. 

766 axis["position_axis"] = None 

767 

768 # Set the category axis as a date axis. 

769 if options.get("date_axis"): 

770 self.date_category = True 

771 

772 # Set the category axis as a text axis. 

773 if options.get("text_axis"): 

774 self.date_category = False 

775 axis["text_axis"] = True 

776 

777 # Convert datetime args if required. 

778 if axis.get("min") and _supported_datetime(axis["min"]): 

779 axis["min"] = _datetime_to_excel_datetime( 

780 axis["min"], self.date_1904, self.remove_timezone 

781 ) 

782 if axis.get("max") and _supported_datetime(axis["max"]): 

783 axis["max"] = _datetime_to_excel_datetime( 

784 axis["max"], self.date_1904, self.remove_timezone 

785 ) 

786 if axis.get("crossing") and _supported_datetime(axis["crossing"]): 

787 axis["crossing"] = _datetime_to_excel_datetime( 

788 axis["crossing"], self.date_1904, self.remove_timezone 

789 ) 

790 

791 # Set the font properties if present. 

792 axis["num_font"] = self._convert_font_args(options.get("num_font")) 

793 axis["name_font"] = self._convert_font_args(options.get("name_font")) 

794 

795 # Set the axis name layout. 

796 axis["name_layout"] = self._get_layout_properties( 

797 options.get("name_layout"), True 

798 ) 

799 

800 # Set the line properties for the axis. 

801 axis["line"] = Shape._get_line_properties(options.get("line")) 

802 

803 # Set the fill properties for the axis. 

804 axis["fill"] = Shape._get_fill_properties(options.get("fill")) 

805 

806 # Set the pattern fill properties for the series. 

807 axis["pattern"] = Shape._get_pattern_properties(options.get("pattern")) 

808 

809 # Set the gradient fill properties for the series. 

810 axis["gradient"] = Shape._get_gradient_properties(options.get("gradient")) 

811 

812 # Pattern fill overrides solid fill. 

813 if axis.get("pattern"): 

814 axis["fill"] = None 

815 

816 # Gradient fill overrides the solid and pattern fill. 

817 if axis.get("gradient"): 

818 axis["pattern"] = None 

819 axis["fill"] = None 

820 

821 # Set the tick marker types. 

822 axis["minor_tick_mark"] = self._get_tick_type(options.get("minor_tick_mark")) 

823 axis["major_tick_mark"] = self._get_tick_type(options.get("major_tick_mark")) 

824 

825 return axis 

826 

827 def _convert_font_args(self, options): 

828 # Convert user defined font values into private dict values. 

829 if not options: 

830 return {} 

831 

832 font = { 

833 "name": options.get("name"), 

834 "color": options.get("color"), 

835 "size": options.get("size"), 

836 "bold": options.get("bold"), 

837 "italic": options.get("italic"), 

838 "underline": options.get("underline"), 

839 "pitch_family": options.get("pitch_family"), 

840 "charset": options.get("charset"), 

841 "baseline": options.get("baseline", 0), 

842 "rotation": options.get("rotation"), 

843 } 

844 

845 # Convert font size units. 

846 if font["size"]: 

847 font["size"] = int(font["size"] * 100) 

848 

849 # Convert rotation into 60,000ths of a degree. 

850 if font["rotation"]: 

851 font["rotation"] = 60000 * int(font["rotation"]) 

852 

853 if font.get("color"): 

854 font["color"] = Color._from_value(font["color"]) 

855 

856 return font 

857 

858 def _list_to_formula(self, data): 

859 # Convert and list of row col values to a range formula. 

860 

861 # If it isn't an array ref it is probably a formula already. 

862 if not isinstance(data, list): 

863 # Check for unquoted sheetnames. 

864 if data and " " in data and "'" not in data and self.warn_sheetname: 

865 warn( 

866 f"Sheetname in '{data}' contains spaces but isn't quoted. " 

867 f"This may cause an error in Excel." 

868 ) 

869 return data 

870 

871 formula = xl_range_formula(*data) 

872 

873 return formula 

874 

875 def _process_names(self, name, name_formula): 

876 # Switch name and name_formula parameters if required. 

877 

878 if name is not None: 

879 if isinstance(name, list): 

880 # Convert a list of values into a name formula. 

881 cell = xl_rowcol_to_cell(name[1], name[2], True, True) 

882 name_formula = quote_sheetname(name[0]) + "!" + cell 

883 name = "" 

884 elif re.match(r"^=?[^!]+!\$?[A-Z]+\$?\d+", name): 

885 # Name looks like a formula, use it to set name_formula. 

886 name_formula = name 

887 name = "" 

888 

889 return name, name_formula 

890 

891 def _get_data_type(self, data) -> str: 

892 # Find the overall type of the data associated with a series. 

893 

894 # Check for no data in the series. 

895 if data is None or len(data) == 0: 

896 return "none" 

897 

898 if isinstance(data[0], list): 

899 return "multi_str" 

900 

901 # Determine if data is numeric or strings. 

902 for token in data: 

903 if token is None: 

904 continue 

905 

906 # Check for strings that would evaluate to float like 

907 # '1.1_1' of ' 1'. 

908 if isinstance(token, str) and re.search("[_ ]", token): 

909 # Assume entire data series is string data. 

910 return "str" 

911 

912 try: 

913 float(token) 

914 except ValueError: 

915 # Not a number. Assume entire data series is string data. 

916 return "str" 

917 

918 # The series data was all numeric. 

919 return "num" 

920 

921 def _get_data_id(self, formula, data): 

922 # Assign an id to a each unique series formula or title/axis formula. 

923 # Repeated formulas such as for categories get the same id. If the 

924 # series or title has user specified data associated with it then 

925 # that is also stored. This data is used to populate cached Excel 

926 # data when creating a chart. If there is no user defined data then 

927 # it will be populated by the parent Workbook._add_chart_data(). 

928 

929 # Ignore series without a range formula. 

930 if not formula: 

931 return None 

932 

933 # Strip the leading '=' from the formula. 

934 if formula.startswith("="): 

935 formula = formula.lstrip("=") 

936 

937 # Store the data id in a hash keyed by the formula and store the data 

938 # in a separate array with the same id. 

939 if formula not in self.formula_ids: 

940 # Haven't seen this formula before. 

941 formula_id = len(self.formula_data) 

942 

943 self.formula_data.append(data) 

944 self.formula_ids[formula] = formula_id 

945 else: 

946 # Formula already seen. Return existing id. 

947 formula_id = self.formula_ids[formula] 

948 

949 # Store user defined data if it isn't already there. 

950 if self.formula_data[formula_id] is None: 

951 self.formula_data[formula_id] = data 

952 

953 return formula_id 

954 

955 def _get_marker_properties(self, marker): 

956 # Convert user marker properties to the structure required internally. 

957 

958 if not marker: 

959 return None 

960 

961 # Copy the user defined properties since they will be modified. 

962 marker = copy.deepcopy(marker) 

963 

964 types = { 

965 "automatic": "automatic", 

966 "none": "none", 

967 "square": "square", 

968 "diamond": "diamond", 

969 "triangle": "triangle", 

970 "x": "x", 

971 "star": "star", 

972 "dot": "dot", 

973 "short_dash": "dot", 

974 "dash": "dash", 

975 "long_dash": "dash", 

976 "circle": "circle", 

977 "plus": "plus", 

978 "picture": "picture", 

979 } 

980 

981 # Check for valid types. 

982 marker_type = marker.get("type") 

983 

984 if marker_type is not None: 

985 if marker_type in types: 

986 marker["type"] = types[marker_type] 

987 else: 

988 warn(f"Unknown marker type '{marker_type}") 

989 return None 

990 

991 # Set the line properties for the marker. 

992 line = Shape._get_line_properties(marker.get("line")) 

993 

994 # Allow 'border' as a synonym for 'line'. 

995 if "border" in marker: 

996 line = Shape._get_line_properties(marker["border"]) 

997 

998 # Set the fill properties for the marker. 

999 fill = Shape._get_fill_properties(marker.get("fill")) 

1000 

1001 # Set the pattern fill properties for the series. 

1002 pattern = Shape._get_pattern_properties(marker.get("pattern")) 

1003 

1004 # Set the gradient fill properties for the series. 

1005 gradient = Shape._get_gradient_properties(marker.get("gradient")) 

1006 

1007 # Pattern fill overrides solid fill. 

1008 if pattern: 

1009 self.fill = None 

1010 

1011 # Gradient fill overrides the solid and pattern fill. 

1012 if gradient: 

1013 pattern = None 

1014 fill = None 

1015 

1016 marker["line"] = line 

1017 marker["fill"] = fill 

1018 marker["pattern"] = pattern 

1019 marker["gradient"] = gradient 

1020 

1021 return marker 

1022 

1023 def _get_trendline_properties(self, trendline): 

1024 # Convert user trendline properties to structure required internally. 

1025 

1026 if not trendline: 

1027 return None 

1028 

1029 # Copy the user defined properties since they will be modified. 

1030 trendline = copy.deepcopy(trendline) 

1031 

1032 types = { 

1033 "exponential": "exp", 

1034 "linear": "linear", 

1035 "log": "log", 

1036 "moving_average": "movingAvg", 

1037 "polynomial": "poly", 

1038 "power": "power", 

1039 } 

1040 

1041 # Check the trendline type. 

1042 trend_type = trendline.get("type") 

1043 

1044 if trend_type in types: 

1045 trendline["type"] = types[trend_type] 

1046 else: 

1047 warn(f"Unknown trendline type '{trend_type}'") 

1048 return None 

1049 

1050 # Set the line properties for the trendline. 

1051 line = Shape._get_line_properties(trendline.get("line")) 

1052 

1053 # Allow 'border' as a synonym for 'line'. 

1054 if "border" in trendline: 

1055 line = Shape._get_line_properties(trendline["border"]) 

1056 

1057 # Set the fill properties for the trendline. 

1058 fill = Shape._get_fill_properties(trendline.get("fill")) 

1059 

1060 # Set the pattern fill properties for the trendline. 

1061 pattern = Shape._get_pattern_properties(trendline.get("pattern")) 

1062 

1063 # Set the gradient fill properties for the trendline. 

1064 gradient = Shape._get_gradient_properties(trendline.get("gradient")) 

1065 

1066 # Set the format properties for the trendline label. 

1067 label = self._get_trendline_label_properties(trendline.get("label")) 

1068 

1069 # Pattern fill overrides solid fill. 

1070 if pattern: 

1071 self.fill = None 

1072 

1073 # Gradient fill overrides the solid and pattern fill. 

1074 if gradient: 

1075 pattern = None 

1076 fill = None 

1077 

1078 trendline["line"] = line 

1079 trendline["fill"] = fill 

1080 trendline["pattern"] = pattern 

1081 trendline["gradient"] = gradient 

1082 trendline["label"] = label 

1083 

1084 return trendline 

1085 

1086 def _get_trendline_label_properties(self, label): 

1087 # Convert user trendline properties to structure required internally. 

1088 

1089 if not label: 

1090 return {} 

1091 

1092 # Copy the user defined properties since they will be modified. 

1093 label = copy.deepcopy(label) 

1094 

1095 # Set the font properties if present. 

1096 font = self._convert_font_args(label.get("font")) 

1097 

1098 # Set the line properties for the label. 

1099 line = Shape._get_line_properties(label.get("line")) 

1100 

1101 # Allow 'border' as a synonym for 'line'. 

1102 if "border" in label: 

1103 line = Shape._get_line_properties(label["border"]) 

1104 

1105 # Set the fill properties for the label. 

1106 fill = Shape._get_fill_properties(label.get("fill")) 

1107 

1108 # Set the pattern fill properties for the label. 

1109 pattern = Shape._get_pattern_properties(label.get("pattern")) 

1110 

1111 # Set the gradient fill properties for the label. 

1112 gradient = Shape._get_gradient_properties(label.get("gradient")) 

1113 

1114 # Pattern fill overrides solid fill. 

1115 if pattern: 

1116 self.fill = None 

1117 

1118 # Gradient fill overrides the solid and pattern fill. 

1119 if gradient: 

1120 pattern = None 

1121 fill = None 

1122 

1123 label["font"] = font 

1124 label["line"] = line 

1125 label["fill"] = fill 

1126 label["pattern"] = pattern 

1127 label["gradient"] = gradient 

1128 

1129 return label 

1130 

1131 def _get_error_bars_props(self, options): 

1132 # Convert user error bars properties to structure required internally. 

1133 if not options: 

1134 return {} 

1135 

1136 # Default values. 

1137 error_bars = {"type": "fixedVal", "value": 1, "endcap": 1, "direction": "both"} 

1138 

1139 types = { 

1140 "fixed": "fixedVal", 

1141 "percentage": "percentage", 

1142 "standard_deviation": "stdDev", 

1143 "standard_error": "stdErr", 

1144 "custom": "cust", 

1145 } 

1146 

1147 # Check the error bars type. 

1148 error_type = options["type"] 

1149 

1150 if error_type in types: 

1151 error_bars["type"] = types[error_type] 

1152 else: 

1153 warn(f"Unknown error bars type '{error_type}") 

1154 return {} 

1155 

1156 # Set the value for error types that require it. 

1157 if "value" in options: 

1158 error_bars["value"] = options["value"] 

1159 

1160 # Set the end-cap style. 

1161 if "end_style" in options: 

1162 error_bars["endcap"] = options["end_style"] 

1163 

1164 # Set the error bar direction. 

1165 if "direction" in options: 

1166 if options["direction"] == "minus": 

1167 error_bars["direction"] = "minus" 

1168 elif options["direction"] == "plus": 

1169 error_bars["direction"] = "plus" 

1170 else: 

1171 # Default to 'both'. 

1172 pass 

1173 

1174 # Set any custom values. 

1175 error_bars["plus_values"] = options.get("plus_values") 

1176 error_bars["minus_values"] = options.get("minus_values") 

1177 error_bars["plus_data"] = options.get("plus_data") 

1178 error_bars["minus_data"] = options.get("minus_data") 

1179 

1180 # Set the line properties for the error bars. 

1181 error_bars["line"] = Shape._get_line_properties(options.get("line")) 

1182 

1183 return error_bars 

1184 

1185 def _get_gridline_properties(self, options): 

1186 # Convert user gridline properties to structure required internally. 

1187 

1188 # Set the visible property for the gridline. 

1189 gridline = {"visible": options.get("visible")} 

1190 

1191 # Set the line properties for the gridline. 

1192 gridline["line"] = Shape._get_line_properties(options.get("line")) 

1193 

1194 return gridline 

1195 

1196 def _get_labels_properties(self, labels): 

1197 # Convert user labels properties to the structure required internally. 

1198 

1199 if not labels: 

1200 return None 

1201 

1202 # Copy the user defined properties since they will be modified. 

1203 labels = copy.deepcopy(labels) 

1204 

1205 # Map user defined label positions to Excel positions. 

1206 position = labels.get("position") 

1207 

1208 if position: 

1209 if position in self.label_positions: 

1210 if position == self.label_position_default: 

1211 labels["position"] = None 

1212 else: 

1213 labels["position"] = self.label_positions[position] 

1214 else: 

1215 warn(f"Unsupported label position '{position}' for this chart type") 

1216 return None 

1217 

1218 # Map the user defined label separator to the Excel separator. 

1219 separator = labels.get("separator") 

1220 separators = { 

1221 ",": ", ", 

1222 ";": "; ", 

1223 ".": ". ", 

1224 "\n": "\n", 

1225 " ": " ", 

1226 } 

1227 

1228 if separator: 

1229 if separator in separators: 

1230 labels["separator"] = separators[separator] 

1231 else: 

1232 warn("Unsupported label separator") 

1233 return None 

1234 

1235 # Set the font properties if present. 

1236 labels["font"] = self._convert_font_args(labels.get("font")) 

1237 

1238 # Set the line properties for the labels. 

1239 line = Shape._get_line_properties(labels.get("line")) 

1240 

1241 # Allow 'border' as a synonym for 'line'. 

1242 if "border" in labels: 

1243 line = Shape._get_line_properties(labels["border"]) 

1244 

1245 # Set the fill properties for the labels. 

1246 fill = Shape._get_fill_properties(labels.get("fill")) 

1247 

1248 # Set the pattern fill properties for the labels. 

1249 pattern = Shape._get_pattern_properties(labels.get("pattern")) 

1250 

1251 # Set the gradient fill properties for the labels. 

1252 gradient = Shape._get_gradient_properties(labels.get("gradient")) 

1253 

1254 # Pattern fill overrides solid fill. 

1255 if pattern: 

1256 self.fill = None 

1257 

1258 # Gradient fill overrides the solid and pattern fill. 

1259 if gradient: 

1260 pattern = None 

1261 fill = None 

1262 

1263 labels["line"] = line 

1264 labels["fill"] = fill 

1265 labels["pattern"] = pattern 

1266 labels["gradient"] = gradient 

1267 

1268 if labels.get("custom"): 

1269 for label in labels["custom"]: 

1270 if label is None: 

1271 continue 

1272 

1273 value = label.get("value") 

1274 if value and re.match(r"^=?[^!]+!\$?[A-Z]+\$?\d+", str(value)): 

1275 label["formula"] = value 

1276 

1277 formula = label.get("formula") 

1278 if formula and formula.startswith("="): 

1279 label["formula"] = formula.lstrip("=") 

1280 

1281 data_id = self._get_data_id(formula, label.get("data")) 

1282 label["data_id"] = data_id 

1283 

1284 label["font"] = self._convert_font_args(label.get("font")) 

1285 

1286 # Set the line properties for the label. 

1287 line = Shape._get_line_properties(label.get("line")) 

1288 

1289 # Allow 'border' as a synonym for 'line'. 

1290 if "border" in label: 

1291 line = Shape._get_line_properties(label["border"]) 

1292 

1293 # Set the fill properties for the label. 

1294 fill = Shape._get_fill_properties(label.get("fill")) 

1295 

1296 # Set the pattern fill properties for the label. 

1297 pattern = Shape._get_pattern_properties(label.get("pattern")) 

1298 

1299 # Set the gradient fill properties for the label. 

1300 gradient = Shape._get_gradient_properties(label.get("gradient")) 

1301 

1302 # Pattern fill overrides solid fill. 

1303 if pattern: 

1304 self.fill = None 

1305 

1306 # Gradient fill overrides the solid and pattern fill. 

1307 if gradient: 

1308 pattern = None 

1309 fill = None 

1310 

1311 # Map user defined label positions to Excel positions. 

1312 position = label.get("position") 

1313 

1314 if position: 

1315 if position in self.label_positions: 

1316 if position == self.label_position_default: 

1317 label["position"] = None 

1318 else: 

1319 label["position"] = self.label_positions[position] 

1320 else: 

1321 warn(f"Unsupported label position '{position}' for chart type") 

1322 return None 

1323 

1324 label["line"] = line 

1325 label["fill"] = fill 

1326 label["pattern"] = pattern 

1327 label["gradient"] = gradient 

1328 

1329 return labels 

1330 

1331 def _get_area_properties(self, options): 

1332 # Convert user area properties to the structure required internally. 

1333 area = {} 

1334 

1335 # Set the line properties for the chartarea. 

1336 line = Shape._get_line_properties(options.get("line")) 

1337 

1338 # Allow 'border' as a synonym for 'line'. 

1339 if options.get("border"): 

1340 line = Shape._get_line_properties(options["border"]) 

1341 

1342 # Set the fill properties for the chartarea. 

1343 fill = Shape._get_fill_properties(options.get("fill")) 

1344 

1345 # Set the pattern fill properties for the series. 

1346 pattern = Shape._get_pattern_properties(options.get("pattern")) 

1347 

1348 # Set the gradient fill properties for the series. 

1349 gradient = Shape._get_gradient_properties(options.get("gradient")) 

1350 

1351 # Pattern fill overrides solid fill. 

1352 if pattern: 

1353 self.fill = None 

1354 

1355 # Gradient fill overrides the solid and pattern fill. 

1356 if gradient: 

1357 pattern = None 

1358 fill = None 

1359 

1360 # Set the plotarea layout. 

1361 layout = self._get_layout_properties(options.get("layout"), False) 

1362 

1363 area["line"] = line 

1364 area["fill"] = fill 

1365 area["pattern"] = pattern 

1366 area["layout"] = layout 

1367 area["gradient"] = gradient 

1368 

1369 return area 

1370 

1371 def _get_legend_properties(self, options: Optional[Dict[str, Any]] = None): 

1372 # Convert user legend properties to the structure required internally. 

1373 legend = {} 

1374 

1375 if options is None: 

1376 options = {} 

1377 

1378 legend["position"] = options.get("position", "right") 

1379 legend["delete_series"] = options.get("delete_series") 

1380 legend["font"] = self._convert_font_args(options.get("font")) 

1381 legend["layout"] = self._get_layout_properties(options.get("layout"), False) 

1382 

1383 # Turn off the legend. 

1384 if options.get("none"): 

1385 legend["position"] = "none" 

1386 

1387 # Set the line properties for the legend. 

1388 line = Shape._get_line_properties(options.get("line")) 

1389 

1390 # Allow 'border' as a synonym for 'line'. 

1391 if options.get("border"): 

1392 line = Shape._get_line_properties(options["border"]) 

1393 

1394 # Set the fill properties for the legend. 

1395 fill = Shape._get_fill_properties(options.get("fill")) 

1396 

1397 # Set the pattern fill properties for the series. 

1398 pattern = Shape._get_pattern_properties(options.get("pattern")) 

1399 

1400 # Set the gradient fill properties for the series. 

1401 gradient = Shape._get_gradient_properties(options.get("gradient")) 

1402 

1403 # Pattern fill overrides solid fill. 

1404 if pattern: 

1405 self.fill = None 

1406 

1407 # Gradient fill overrides the solid and pattern fill. 

1408 if gradient: 

1409 pattern = None 

1410 fill = None 

1411 

1412 # Set the legend layout. 

1413 layout = self._get_layout_properties(options.get("layout"), False) 

1414 

1415 legend["line"] = line 

1416 legend["fill"] = fill 

1417 legend["pattern"] = pattern 

1418 legend["layout"] = layout 

1419 legend["gradient"] = gradient 

1420 

1421 return legend 

1422 

1423 def _get_layout_properties(self, args, is_text): 

1424 # Convert user defined layout properties to format used internally. 

1425 layout = {} 

1426 

1427 if not args: 

1428 return {} 

1429 

1430 if is_text: 

1431 properties = ("x", "y") 

1432 else: 

1433 properties = ("x", "y", "width", "height") 

1434 

1435 # Check for valid properties. 

1436 for key in args.keys(): 

1437 if key not in properties: 

1438 warn(f"Property '{key}' not supported in layout options") 

1439 return {} 

1440 

1441 # Set the layout properties. 

1442 for prop in properties: 

1443 if prop not in args.keys(): 

1444 warn(f"Property '{prop}' must be specified in layout options") 

1445 return {} 

1446 

1447 value = args[prop] 

1448 

1449 try: 

1450 float(value) 

1451 except ValueError: 

1452 warn(f"Property '{prop}' value '{value}' must be numeric in layout") 

1453 return {} 

1454 

1455 if value < 0 or value > 1: 

1456 warn( 

1457 f"Property '{prop}' value '{value}' must be in range " 

1458 f"0 < x <= 1 in layout options" 

1459 ) 

1460 return {} 

1461 

1462 # Convert to the format used by Excel for easier testing 

1463 layout[prop] = f"{value:.17g}" 

1464 

1465 return layout 

1466 

1467 def _get_points_properties(self, user_points): 

1468 # Convert user points properties to structure required internally. 

1469 points = [] 

1470 

1471 if not user_points: 

1472 return [] 

1473 

1474 for user_point in user_points: 

1475 point = {} 

1476 

1477 if user_point is not None: 

1478 # Set the line properties for the point. 

1479 line = Shape._get_line_properties(user_point.get("line")) 

1480 

1481 # Allow 'border' as a synonym for 'line'. 

1482 if "border" in user_point: 

1483 line = Shape._get_line_properties(user_point["border"]) 

1484 

1485 # Set the fill properties for the chartarea. 

1486 fill = Shape._get_fill_properties(user_point.get("fill")) 

1487 

1488 # Set the pattern fill properties for the series. 

1489 pattern = Shape._get_pattern_properties(user_point.get("pattern")) 

1490 

1491 # Set the gradient fill properties for the series. 

1492 gradient = Shape._get_gradient_properties(user_point.get("gradient")) 

1493 

1494 # Pattern fill overrides solid fill. 

1495 if pattern: 

1496 self.fill = None 

1497 

1498 # Gradient fill overrides the solid and pattern fill. 

1499 if gradient: 

1500 pattern = None 

1501 fill = None 

1502 

1503 point["line"] = line 

1504 point["fill"] = fill 

1505 point["pattern"] = pattern 

1506 point["gradient"] = gradient 

1507 

1508 points.append(point) 

1509 

1510 return points 

1511 

1512 def _has_fill_formatting(self, element): 

1513 # Check if a chart element has line, fill or gradient formatting. 

1514 has_fill = False 

1515 has_line = False 

1516 has_pattern = element.get("pattern") 

1517 has_gradient = element.get("gradient") 

1518 

1519 if element.get("fill") and element["fill"]["defined"]: 

1520 has_fill = True 

1521 

1522 if element.get("line") and element["line"]["defined"]: 

1523 has_line = True 

1524 

1525 return has_fill or has_line or has_pattern or has_gradient 

1526 

1527 def _get_display_units(self, display_units): 

1528 # Convert user defined display units to internal units. 

1529 if not display_units: 

1530 return None 

1531 

1532 types = { 

1533 "hundreds": "hundreds", 

1534 "thousands": "thousands", 

1535 "ten_thousands": "tenThousands", 

1536 "hundred_thousands": "hundredThousands", 

1537 "millions": "millions", 

1538 "ten_millions": "tenMillions", 

1539 "hundred_millions": "hundredMillions", 

1540 "billions": "billions", 

1541 "trillions": "trillions", 

1542 } 

1543 

1544 if display_units in types: 

1545 display_units = types[display_units] 

1546 else: 

1547 warn(f"Unknown display_units type '{display_units}'") 

1548 return None 

1549 

1550 return display_units 

1551 

1552 def _get_tick_type(self, tick_type): 

1553 # Convert user defined display units to internal units. 

1554 if not tick_type: 

1555 return None 

1556 

1557 types = { 

1558 "outside": "out", 

1559 "inside": "in", 

1560 "none": "none", 

1561 "cross": "cross", 

1562 } 

1563 

1564 if tick_type in types: 

1565 tick_type = types[tick_type] 

1566 else: 

1567 warn(f"Unknown tick_type '{tick_type}'") 

1568 return None 

1569 

1570 return tick_type 

1571 

1572 def _get_primary_axes_series(self): 

1573 # Returns series which use the primary axes. 

1574 primary_axes_series = [] 

1575 

1576 for series in self.series: 

1577 if not series["y2_axis"]: 

1578 primary_axes_series.append(series) 

1579 

1580 return primary_axes_series 

1581 

1582 def _get_secondary_axes_series(self): 

1583 # Returns series which use the secondary axes. 

1584 secondary_axes_series = [] 

1585 

1586 for series in self.series: 

1587 if series["y2_axis"]: 

1588 secondary_axes_series.append(series) 

1589 

1590 return secondary_axes_series 

1591 

1592 def _add_axis_ids(self, args) -> None: 

1593 # Add unique ids for primary or secondary axes 

1594 chart_id = 5001 + int(self.id) 

1595 axis_count = 1 + len(self.axis2_ids) + len(self.axis_ids) 

1596 

1597 id1 = f"{chart_id:04d}{axis_count:04d}" 

1598 id2 = f"{chart_id:04d}{axis_count + 1:04d}" 

1599 

1600 if args["primary_axes"]: 

1601 self.axis_ids.append(id1) 

1602 self.axis_ids.append(id2) 

1603 

1604 if not args["primary_axes"]: 

1605 self.axis2_ids.append(id1) 

1606 self.axis2_ids.append(id2) 

1607 

1608 def _set_default_properties(self) -> None: 

1609 # Setup the default properties for a chart. 

1610 

1611 self.x_axis["defaults"] = { 

1612 "num_format": "General", 

1613 "major_gridlines": {"visible": 0}, 

1614 } 

1615 

1616 self.y_axis["defaults"] = { 

1617 "num_format": "General", 

1618 "major_gridlines": {"visible": 1}, 

1619 } 

1620 

1621 self.x2_axis["defaults"] = { 

1622 "num_format": "General", 

1623 "label_position": "none", 

1624 "crossing": "max", 

1625 "visible": 0, 

1626 } 

1627 

1628 self.y2_axis["defaults"] = { 

1629 "num_format": "General", 

1630 "major_gridlines": {"visible": 0}, 

1631 "position": "right", 

1632 "visible": 1, 

1633 } 

1634 

1635 self.set_x_axis({}) 

1636 self.set_y_axis({}) 

1637 

1638 self.set_x2_axis({}) 

1639 self.set_y2_axis({}) 

1640 

1641 ########################################################################### 

1642 # 

1643 # XML methods. 

1644 # 

1645 ########################################################################### 

1646 

1647 def _write_chart_space(self) -> None: 

1648 # Write the <c:chartSpace> element. 

1649 schema = "http://schemas.openxmlformats.org/" 

1650 xmlns_c = schema + "drawingml/2006/chart" 

1651 xmlns_a = schema + "drawingml/2006/main" 

1652 xmlns_r = schema + "officeDocument/2006/relationships" 

1653 

1654 attributes = [ 

1655 ("xmlns:c", xmlns_c), 

1656 ("xmlns:a", xmlns_a), 

1657 ("xmlns:r", xmlns_r), 

1658 ] 

1659 

1660 self._xml_start_tag("c:chartSpace", attributes) 

1661 

1662 def _write_lang(self) -> None: 

1663 # Write the <c:lang> element. 

1664 val = "en-US" 

1665 

1666 attributes = [("val", val)] 

1667 

1668 self._xml_empty_tag("c:lang", attributes) 

1669 

1670 def _write_style(self) -> None: 

1671 # Write the <c:style> element. 

1672 style_id = self.style_id 

1673 

1674 # Don't write an element for the default style, 2. 

1675 if style_id == 2: 

1676 return 

1677 

1678 attributes = [("val", style_id)] 

1679 

1680 self._xml_empty_tag("c:style", attributes) 

1681 

1682 def _write_chart(self) -> None: 

1683 # Write the <c:chart> element. 

1684 self._xml_start_tag("c:chart") 

1685 

1686 if self.title_none: 

1687 # Turn off the title. 

1688 self._write_c_auto_title_deleted() 

1689 else: 

1690 # Write the chart title elements. 

1691 if self.title_formula is not None: 

1692 self._write_title_formula( 

1693 self.title_formula, 

1694 self.title_data_id, 

1695 None, 

1696 self.title_font, 

1697 self.title_layout, 

1698 self.title_overlay, 

1699 ) 

1700 elif self.title_name is not None: 

1701 self._write_title_rich( 

1702 self.title_name, 

1703 None, 

1704 self.title_font, 

1705 self.title_layout, 

1706 self.title_overlay, 

1707 ) 

1708 

1709 # Write the c:plotArea element. 

1710 self._write_plot_area() 

1711 

1712 # Write the c:legend element. 

1713 self._write_legend() 

1714 

1715 # Write the c:plotVisOnly element. 

1716 self._write_plot_vis_only() 

1717 

1718 # Write the c:dispBlanksAs element. 

1719 self._write_disp_blanks_as() 

1720 

1721 # Write the c:extLst element. 

1722 if self.show_na_as_empty: 

1723 self._write_c_ext_lst_display_na() 

1724 

1725 self._xml_end_tag("c:chart") 

1726 

1727 def _write_disp_blanks_as(self) -> None: 

1728 # Write the <c:dispBlanksAs> element. 

1729 val = self.show_blanks 

1730 

1731 # Ignore the default value. 

1732 if val == "gap": 

1733 return 

1734 

1735 attributes = [("val", val)] 

1736 

1737 self._xml_empty_tag("c:dispBlanksAs", attributes) 

1738 

1739 def _write_plot_area(self) -> None: 

1740 # Write the <c:plotArea> element. 

1741 self._xml_start_tag("c:plotArea") 

1742 

1743 # Write the c:layout element. 

1744 self._write_layout(self.plotarea.get("layout"), "plot") 

1745 

1746 # Write subclass chart type elements for primary and secondary axes. 

1747 self._write_chart_type({"primary_axes": True}) 

1748 self._write_chart_type({"primary_axes": False}) 

1749 

1750 # Configure a combined chart if present. 

1751 second_chart = self.combined 

1752 if second_chart: 

1753 # Secondary axis has unique id otherwise use same as primary. 

1754 if second_chart.is_secondary: 

1755 second_chart.id = 1000 + self.id 

1756 else: 

1757 second_chart.id = self.id 

1758 

1759 # Share the same filehandle for writing. 

1760 second_chart.fh = self.fh 

1761 

1762 # Share series index with primary chart. 

1763 second_chart.series_index = self.series_index 

1764 

1765 # Write the subclass chart type elements for combined chart. 

1766 second_chart._write_chart_type({"primary_axes": True}) 

1767 second_chart._write_chart_type({"primary_axes": False}) 

1768 

1769 # Write the category and value elements for the primary axes. 

1770 args = {"x_axis": self.x_axis, "y_axis": self.y_axis, "axis_ids": self.axis_ids} 

1771 

1772 if self.date_category: 

1773 self._write_date_axis(args) 

1774 else: 

1775 self._write_cat_axis(args) 

1776 

1777 self._write_val_axis(args) 

1778 

1779 # Write the category and value elements for the secondary axes. 

1780 args = { 

1781 "x_axis": self.x2_axis, 

1782 "y_axis": self.y2_axis, 

1783 "axis_ids": self.axis2_ids, 

1784 } 

1785 

1786 self._write_val_axis(args) 

1787 

1788 # Write the secondary axis for the secondary chart. 

1789 if second_chart and second_chart.is_secondary: 

1790 args = { 

1791 "x_axis": second_chart.x2_axis, 

1792 "y_axis": second_chart.y2_axis, 

1793 "axis_ids": second_chart.axis2_ids, 

1794 } 

1795 

1796 second_chart._write_val_axis(args) 

1797 

1798 if self.date_category: 

1799 self._write_date_axis(args) 

1800 else: 

1801 self._write_cat_axis(args) 

1802 

1803 # Write the c:dTable element. 

1804 self._write_d_table() 

1805 

1806 # Write the c:spPr element for the plotarea formatting. 

1807 self._write_sp_pr(self.plotarea) 

1808 

1809 self._xml_end_tag("c:plotArea") 

1810 

1811 def _write_layout(self, layout, layout_type) -> None: 

1812 # Write the <c:layout> element. 

1813 

1814 if not layout: 

1815 # Automatic layout. 

1816 self._xml_empty_tag("c:layout") 

1817 else: 

1818 # User defined manual layout. 

1819 self._xml_start_tag("c:layout") 

1820 self._write_manual_layout(layout, layout_type) 

1821 self._xml_end_tag("c:layout") 

1822 

1823 def _write_manual_layout(self, layout, layout_type) -> None: 

1824 # Write the <c:manualLayout> element. 

1825 self._xml_start_tag("c:manualLayout") 

1826 

1827 # Plotarea has a layoutTarget element. 

1828 if layout_type == "plot": 

1829 self._xml_empty_tag("c:layoutTarget", [("val", "inner")]) 

1830 

1831 # Set the x, y positions. 

1832 self._xml_empty_tag("c:xMode", [("val", "edge")]) 

1833 self._xml_empty_tag("c:yMode", [("val", "edge")]) 

1834 self._xml_empty_tag("c:x", [("val", layout["x"])]) 

1835 self._xml_empty_tag("c:y", [("val", layout["y"])]) 

1836 

1837 # For plotarea and legend set the width and height. 

1838 if layout_type != "text": 

1839 self._xml_empty_tag("c:w", [("val", layout["width"])]) 

1840 self._xml_empty_tag("c:h", [("val", layout["height"])]) 

1841 

1842 self._xml_end_tag("c:manualLayout") 

1843 

1844 def _write_chart_type(self, args) -> None: 

1845 # pylint: disable=unused-argument 

1846 # Write the chart type element. This method should be overridden 

1847 # by the subclasses. 

1848 return 

1849 

1850 def _write_grouping(self, val) -> None: 

1851 # Write the <c:grouping> element. 

1852 attributes = [("val", val)] 

1853 

1854 self._xml_empty_tag("c:grouping", attributes) 

1855 

1856 def _write_series(self, series) -> None: 

1857 # Write the series elements. 

1858 self._write_ser(series) 

1859 

1860 def _write_ser(self, series) -> None: 

1861 # Write the <c:ser> element. 

1862 index = self.series_index 

1863 self.series_index += 1 

1864 

1865 self._xml_start_tag("c:ser") 

1866 

1867 # Write the c:idx element. 

1868 self._write_idx(index) 

1869 

1870 # Write the c:order element. 

1871 self._write_order(index) 

1872 

1873 # Write the series name. 

1874 self._write_series_name(series) 

1875 

1876 # Write the c:spPr element. 

1877 self._write_sp_pr(series) 

1878 

1879 # Write the c:marker element. 

1880 self._write_marker(series["marker"]) 

1881 

1882 # Write the c:invertIfNegative element. 

1883 self._write_c_invert_if_negative(series["invert_if_neg"]) 

1884 

1885 # Write the c:dPt element. 

1886 self._write_d_pt(series["points"]) 

1887 

1888 # Write the c:dLbls element. 

1889 self._write_d_lbls(series["labels"]) 

1890 

1891 # Write the c:trendline element. 

1892 self._write_trendline(series["trendline"]) 

1893 

1894 # Write the c:errBars element. 

1895 self._write_error_bars(series["error_bars"]) 

1896 

1897 # Write the c:cat element. 

1898 self._write_cat(series) 

1899 

1900 # Write the c:val element. 

1901 self._write_val(series) 

1902 

1903 # Write the c:smooth element. 

1904 if self.smooth_allowed: 

1905 self._write_c_smooth(series["smooth"]) 

1906 

1907 # Write the c:extLst element. 

1908 if series.get("inverted_color"): 

1909 self._write_c_ext_lst_inverted_color(series["inverted_color"]) 

1910 

1911 self._xml_end_tag("c:ser") 

1912 

1913 def _write_c_ext_lst_inverted_color(self, color: Color) -> None: 

1914 # Write the <c:extLst> element for the inverted fill color. 

1915 

1916 uri = "{6F2FDCE9-48DA-4B69-8628-5D25D57E5C99}" 

1917 xmlns_c_14 = "http://schemas.microsoft.com/office/drawing/2007/8/2/chart" 

1918 

1919 attributes1 = [ 

1920 ("uri", uri), 

1921 ("xmlns:c14", xmlns_c_14), 

1922 ] 

1923 

1924 attributes2 = [("xmlns:c14", xmlns_c_14)] 

1925 

1926 self._xml_start_tag("c:extLst") 

1927 self._xml_start_tag("c:ext", attributes1) 

1928 self._xml_start_tag("c14:invertSolidFillFmt") 

1929 self._xml_start_tag("c14:spPr", attributes2) 

1930 

1931 self._write_a_solid_fill({"color": color}) 

1932 

1933 self._xml_end_tag("c14:spPr") 

1934 self._xml_end_tag("c14:invertSolidFillFmt") 

1935 self._xml_end_tag("c:ext") 

1936 self._xml_end_tag("c:extLst") 

1937 

1938 def _write_c_ext_lst_display_na(self) -> None: 

1939 # Write the <c:extLst> element for the display NA as empty cell option. 

1940 

1941 uri = "{56B9EC1D-385E-4148-901F-78D8002777C0}" 

1942 xmlns_c_16 = "http://schemas.microsoft.com/office/drawing/2017/03/chart" 

1943 

1944 attributes1 = [ 

1945 ("uri", uri), 

1946 ("xmlns:c16r3", xmlns_c_16), 

1947 ] 

1948 

1949 attributes2 = [("val", 1)] 

1950 

1951 self._xml_start_tag("c:extLst") 

1952 self._xml_start_tag("c:ext", attributes1) 

1953 self._xml_start_tag("c16r3:dataDisplayOptions16") 

1954 self._xml_empty_tag("c16r3:dispNaAsBlank", attributes2) 

1955 self._xml_end_tag("c16r3:dataDisplayOptions16") 

1956 self._xml_end_tag("c:ext") 

1957 self._xml_end_tag("c:extLst") 

1958 

1959 def _write_idx(self, val) -> None: 

1960 # Write the <c:idx> element. 

1961 

1962 attributes = [("val", val)] 

1963 

1964 self._xml_empty_tag("c:idx", attributes) 

1965 

1966 def _write_order(self, val) -> None: 

1967 # Write the <c:order> element. 

1968 

1969 attributes = [("val", val)] 

1970 

1971 self._xml_empty_tag("c:order", attributes) 

1972 

1973 def _write_series_name(self, series) -> None: 

1974 # Write the series name. 

1975 

1976 if series["name_formula"] is not None: 

1977 self._write_tx_formula(series["name_formula"], series["name_id"]) 

1978 elif series["name"] is not None: 

1979 self._write_tx_value(series["name"]) 

1980 

1981 def _write_c_smooth(self, smooth) -> None: 

1982 # Write the <c:smooth> element. 

1983 

1984 if smooth: 

1985 self._xml_empty_tag("c:smooth", [("val", "1")]) 

1986 

1987 def _write_cat(self, series) -> None: 

1988 # Write the <c:cat> element. 

1989 formula = series["categories"] 

1990 data_id = series["cat_data_id"] 

1991 data = None 

1992 

1993 if data_id is not None: 

1994 data = self.formula_data[data_id] 

1995 

1996 # Ignore <c:cat> elements for charts without category values. 

1997 if not formula: 

1998 return 

1999 

2000 self._xml_start_tag("c:cat") 

2001 

2002 # Check the type of cached data. 

2003 cat_type = self._get_data_type(data) 

2004 

2005 if cat_type == "str": 

2006 self.cat_has_num_fmt = False 

2007 # Write the c:numRef element. 

2008 self._write_str_ref(formula, data, cat_type) 

2009 

2010 elif cat_type == "multi_str": 

2011 self.cat_has_num_fmt = False 

2012 # Write the c:numRef element. 

2013 self._write_multi_lvl_str_ref(formula, data) 

2014 

2015 else: 

2016 self.cat_has_num_fmt = True 

2017 # Write the c:numRef element. 

2018 self._write_num_ref(formula, data, cat_type) 

2019 

2020 self._xml_end_tag("c:cat") 

2021 

2022 def _write_val(self, series) -> None: 

2023 # Write the <c:val> element. 

2024 formula = series["values"] 

2025 data_id = series["val_data_id"] 

2026 data = self.formula_data[data_id] 

2027 

2028 self._xml_start_tag("c:val") 

2029 

2030 # Unlike Cat axes data should only be numeric. 

2031 # Write the c:numRef element. 

2032 self._write_num_ref(formula, data, "num") 

2033 

2034 self._xml_end_tag("c:val") 

2035 

2036 def _write_num_ref(self, formula, data, ref_type) -> None: 

2037 # Write the <c:numRef> element. 

2038 self._xml_start_tag("c:numRef") 

2039 

2040 # Write the c:f element. 

2041 self._write_series_formula(formula) 

2042 

2043 if ref_type == "num": 

2044 # Write the c:numCache element. 

2045 self._write_num_cache(data) 

2046 elif ref_type == "str": 

2047 # Write the c:strCache element. 

2048 self._write_str_cache(data) 

2049 

2050 self._xml_end_tag("c:numRef") 

2051 

2052 def _write_str_ref(self, formula, data, ref_type) -> None: 

2053 # Write the <c:strRef> element. 

2054 

2055 self._xml_start_tag("c:strRef") 

2056 

2057 # Write the c:f element. 

2058 self._write_series_formula(formula) 

2059 

2060 if ref_type == "num": 

2061 # Write the c:numCache element. 

2062 self._write_num_cache(data) 

2063 elif ref_type == "str": 

2064 # Write the c:strCache element. 

2065 self._write_str_cache(data) 

2066 

2067 self._xml_end_tag("c:strRef") 

2068 

2069 def _write_multi_lvl_str_ref(self, formula, data) -> None: 

2070 # Write the <c:multiLvlStrRef> element. 

2071 

2072 if not data: 

2073 return 

2074 

2075 self._xml_start_tag("c:multiLvlStrRef") 

2076 

2077 # Write the c:f element. 

2078 self._write_series_formula(formula) 

2079 

2080 self._xml_start_tag("c:multiLvlStrCache") 

2081 

2082 # Write the c:ptCount element. 

2083 count = len(data[-1]) 

2084 self._write_pt_count(count) 

2085 

2086 for cat_data in reversed(data): 

2087 self._xml_start_tag("c:lvl") 

2088 

2089 for i, point in enumerate(cat_data): 

2090 # Write the c:pt element. 

2091 self._write_pt(i, point) 

2092 

2093 self._xml_end_tag("c:lvl") 

2094 

2095 self._xml_end_tag("c:multiLvlStrCache") 

2096 self._xml_end_tag("c:multiLvlStrRef") 

2097 

2098 def _write_series_formula(self, formula) -> None: 

2099 # Write the <c:f> element. 

2100 

2101 # Strip the leading '=' from the formula. 

2102 if formula.startswith("="): 

2103 formula = formula.lstrip("=") 

2104 

2105 self._xml_data_element("c:f", formula) 

2106 

2107 def _write_axis_ids(self, args) -> None: 

2108 # Write the <c:axId> elements for the primary or secondary axes. 

2109 

2110 # Generate the axis ids. 

2111 self._add_axis_ids(args) 

2112 

2113 if args["primary_axes"]: 

2114 # Write the axis ids for the primary axes. 

2115 self._write_axis_id(self.axis_ids[0]) 

2116 self._write_axis_id(self.axis_ids[1]) 

2117 else: 

2118 # Write the axis ids for the secondary axes. 

2119 self._write_axis_id(self.axis2_ids[0]) 

2120 self._write_axis_id(self.axis2_ids[1]) 

2121 

2122 def _write_axis_id(self, val) -> None: 

2123 # Write the <c:axId> element. 

2124 

2125 attributes = [("val", val)] 

2126 

2127 self._xml_empty_tag("c:axId", attributes) 

2128 

2129 def _write_cat_axis(self, args) -> None: 

2130 # Write the <c:catAx> element. Usually the X axis. 

2131 x_axis = args["x_axis"] 

2132 y_axis = args["y_axis"] 

2133 axis_ids = args["axis_ids"] 

2134 

2135 # If there are no axis_ids then we don't need to write this element. 

2136 if axis_ids is None or not axis_ids: 

2137 return 

2138 

2139 position = self.cat_axis_position 

2140 is_y_axis = self.horiz_cat_axis 

2141 

2142 # Overwrite the default axis position with a user supplied value. 

2143 if x_axis.get("position"): 

2144 position = x_axis["position"] 

2145 

2146 self._xml_start_tag("c:catAx") 

2147 

2148 self._write_axis_id(axis_ids[0]) 

2149 

2150 # Write the c:scaling element. 

2151 self._write_scaling(x_axis.get("reverse"), None, None, None) 

2152 

2153 if not x_axis.get("visible"): 

2154 self._write_delete(1) 

2155 

2156 # Write the c:axPos element. 

2157 self._write_axis_pos(position, y_axis.get("reverse")) 

2158 

2159 # Write the c:majorGridlines element. 

2160 self._write_major_gridlines(x_axis.get("major_gridlines")) 

2161 

2162 # Write the c:minorGridlines element. 

2163 self._write_minor_gridlines(x_axis.get("minor_gridlines")) 

2164 

2165 # Write the axis title elements. 

2166 if x_axis["formula"] is not None: 

2167 self._write_title_formula( 

2168 x_axis["formula"], 

2169 x_axis["data_id"], 

2170 is_y_axis, 

2171 x_axis["name_font"], 

2172 x_axis["name_layout"], 

2173 ) 

2174 elif x_axis["name"] is not None: 

2175 self._write_title_rich( 

2176 x_axis["name"], is_y_axis, x_axis["name_font"], x_axis["name_layout"] 

2177 ) 

2178 

2179 # Write the c:numFmt element. 

2180 self._write_cat_number_format(x_axis) 

2181 

2182 # Write the c:majorTickMark element. 

2183 self._write_major_tick_mark(x_axis.get("major_tick_mark")) 

2184 

2185 # Write the c:minorTickMark element. 

2186 self._write_minor_tick_mark(x_axis.get("minor_tick_mark")) 

2187 

2188 # Write the c:tickLblPos element. 

2189 self._write_tick_label_pos(x_axis.get("label_position")) 

2190 

2191 # Write the c:spPr element for the axis line. 

2192 self._write_sp_pr(x_axis) 

2193 

2194 # Write the axis font elements. 

2195 self._write_axis_font(x_axis.get("num_font")) 

2196 

2197 # Write the c:crossAx element. 

2198 self._write_cross_axis(axis_ids[1]) 

2199 

2200 if self.show_crosses or x_axis.get("visible"): 

2201 # Note, the category crossing comes from the value axis. 

2202 if ( 

2203 y_axis.get("crossing") is None 

2204 or y_axis.get("crossing") == "max" 

2205 or y_axis["crossing"] == "min" 

2206 ): 

2207 # Write the c:crosses element. 

2208 self._write_crosses(y_axis.get("crossing")) 

2209 else: 

2210 # Write the c:crossesAt element. 

2211 self._write_c_crosses_at(y_axis.get("crossing")) 

2212 

2213 # Write the c:auto element. 

2214 if not x_axis.get("text_axis"): 

2215 self._write_auto(1) 

2216 

2217 # Write the c:labelAlign element. 

2218 self._write_label_align(x_axis.get("label_align")) 

2219 

2220 # Write the c:labelOffset element. 

2221 self._write_label_offset(100) 

2222 

2223 # Write the c:tickLblSkip element. 

2224 self._write_c_tick_lbl_skip(x_axis.get("interval_unit")) 

2225 

2226 # Write the c:tickMarkSkip element. 

2227 self._write_c_tick_mark_skip(x_axis.get("interval_tick")) 

2228 

2229 self._xml_end_tag("c:catAx") 

2230 

2231 def _write_val_axis(self, args) -> None: 

2232 # Write the <c:valAx> element. Usually the Y axis. 

2233 x_axis = args["x_axis"] 

2234 y_axis = args["y_axis"] 

2235 axis_ids = args["axis_ids"] 

2236 position = args.get("position", self.val_axis_position) 

2237 is_y_axis = self.horiz_val_axis 

2238 

2239 # If there are no axis_ids then we don't need to write this element. 

2240 if axis_ids is None or not axis_ids: 

2241 return 

2242 

2243 # Overwrite the default axis position with a user supplied value. 

2244 position = y_axis.get("position") or position 

2245 

2246 self._xml_start_tag("c:valAx") 

2247 

2248 self._write_axis_id(axis_ids[1]) 

2249 

2250 # Write the c:scaling element. 

2251 self._write_scaling( 

2252 y_axis.get("reverse"), 

2253 y_axis.get("min"), 

2254 y_axis.get("max"), 

2255 y_axis.get("log_base"), 

2256 ) 

2257 

2258 if not y_axis.get("visible"): 

2259 self._write_delete(1) 

2260 

2261 # Write the c:axPos element. 

2262 self._write_axis_pos(position, x_axis.get("reverse")) 

2263 

2264 # Write the c:majorGridlines element. 

2265 self._write_major_gridlines(y_axis.get("major_gridlines")) 

2266 

2267 # Write the c:minorGridlines element. 

2268 self._write_minor_gridlines(y_axis.get("minor_gridlines")) 

2269 

2270 # Write the axis title elements. 

2271 if y_axis["formula"] is not None: 

2272 self._write_title_formula( 

2273 y_axis["formula"], 

2274 y_axis["data_id"], 

2275 is_y_axis, 

2276 y_axis["name_font"], 

2277 y_axis["name_layout"], 

2278 ) 

2279 elif y_axis["name"] is not None: 

2280 self._write_title_rich( 

2281 y_axis["name"], 

2282 is_y_axis, 

2283 y_axis.get("name_font"), 

2284 y_axis.get("name_layout"), 

2285 ) 

2286 

2287 # Write the c:numberFormat element. 

2288 self._write_number_format(y_axis) 

2289 

2290 # Write the c:majorTickMark element. 

2291 self._write_major_tick_mark(y_axis.get("major_tick_mark")) 

2292 

2293 # Write the c:minorTickMark element. 

2294 self._write_minor_tick_mark(y_axis.get("minor_tick_mark")) 

2295 

2296 # Write the c:tickLblPos element. 

2297 self._write_tick_label_pos(y_axis.get("label_position")) 

2298 

2299 # Write the c:spPr element for the axis line. 

2300 self._write_sp_pr(y_axis) 

2301 

2302 # Write the axis font elements. 

2303 self._write_axis_font(y_axis.get("num_font")) 

2304 

2305 # Write the c:crossAx element. 

2306 self._write_cross_axis(axis_ids[0]) 

2307 

2308 # Note, the category crossing comes from the value axis. 

2309 if ( 

2310 x_axis.get("crossing") is None 

2311 or x_axis["crossing"] == "max" 

2312 or x_axis["crossing"] == "min" 

2313 ): 

2314 # Write the c:crosses element. 

2315 self._write_crosses(x_axis.get("crossing")) 

2316 else: 

2317 # Write the c:crossesAt element. 

2318 self._write_c_crosses_at(x_axis.get("crossing")) 

2319 

2320 # Write the c:crossBetween element. 

2321 self._write_cross_between(x_axis.get("position_axis")) 

2322 

2323 # Write the c:majorUnit element. 

2324 self._write_c_major_unit(y_axis.get("major_unit")) 

2325 

2326 # Write the c:minorUnit element. 

2327 self._write_c_minor_unit(y_axis.get("minor_unit")) 

2328 

2329 # Write the c:dispUnits element. 

2330 self._write_disp_units( 

2331 y_axis.get("display_units"), y_axis.get("display_units_visible") 

2332 ) 

2333 

2334 self._xml_end_tag("c:valAx") 

2335 

2336 def _write_cat_val_axis(self, args) -> None: 

2337 # Write the <c:valAx> element. This is for the second valAx 

2338 # in scatter plots. Usually the X axis. 

2339 x_axis = args["x_axis"] 

2340 y_axis = args["y_axis"] 

2341 axis_ids = args["axis_ids"] 

2342 position = args["position"] or self.val_axis_position 

2343 is_y_axis = self.horiz_val_axis 

2344 

2345 # If there are no axis_ids then we don't need to write this element. 

2346 if axis_ids is None or not axis_ids: 

2347 return 

2348 

2349 # Overwrite the default axis position with a user supplied value. 

2350 position = x_axis.get("position") or position 

2351 

2352 self._xml_start_tag("c:valAx") 

2353 

2354 self._write_axis_id(axis_ids[0]) 

2355 

2356 # Write the c:scaling element. 

2357 self._write_scaling( 

2358 x_axis.get("reverse"), 

2359 x_axis.get("min"), 

2360 x_axis.get("max"), 

2361 x_axis.get("log_base"), 

2362 ) 

2363 

2364 if not x_axis.get("visible"): 

2365 self._write_delete(1) 

2366 

2367 # Write the c:axPos element. 

2368 self._write_axis_pos(position, y_axis.get("reverse")) 

2369 

2370 # Write the c:majorGridlines element. 

2371 self._write_major_gridlines(x_axis.get("major_gridlines")) 

2372 

2373 # Write the c:minorGridlines element. 

2374 self._write_minor_gridlines(x_axis.get("minor_gridlines")) 

2375 

2376 # Write the axis title elements. 

2377 if x_axis["formula"] is not None: 

2378 self._write_title_formula( 

2379 x_axis["formula"], 

2380 x_axis["data_id"], 

2381 is_y_axis, 

2382 x_axis["name_font"], 

2383 x_axis["name_layout"], 

2384 ) 

2385 elif x_axis["name"] is not None: 

2386 self._write_title_rich( 

2387 x_axis["name"], is_y_axis, x_axis["name_font"], x_axis["name_layout"] 

2388 ) 

2389 

2390 # Write the c:numberFormat element. 

2391 self._write_number_format(x_axis) 

2392 

2393 # Write the c:majorTickMark element. 

2394 self._write_major_tick_mark(x_axis.get("major_tick_mark")) 

2395 

2396 # Write the c:minorTickMark element. 

2397 self._write_minor_tick_mark(x_axis.get("minor_tick_mark")) 

2398 

2399 # Write the c:tickLblPos element. 

2400 self._write_tick_label_pos(x_axis.get("label_position")) 

2401 

2402 # Write the c:spPr element for the axis line. 

2403 self._write_sp_pr(x_axis) 

2404 

2405 # Write the axis font elements. 

2406 self._write_axis_font(x_axis.get("num_font")) 

2407 

2408 # Write the c:crossAx element. 

2409 self._write_cross_axis(axis_ids[1]) 

2410 

2411 # Note, the category crossing comes from the value axis. 

2412 if ( 

2413 y_axis.get("crossing") is None 

2414 or y_axis["crossing"] == "max" 

2415 or y_axis["crossing"] == "min" 

2416 ): 

2417 # Write the c:crosses element. 

2418 self._write_crosses(y_axis.get("crossing")) 

2419 else: 

2420 # Write the c:crossesAt element. 

2421 self._write_c_crosses_at(y_axis.get("crossing")) 

2422 

2423 # Write the c:crossBetween element. 

2424 self._write_cross_between(y_axis.get("position_axis")) 

2425 

2426 # Write the c:majorUnit element. 

2427 self._write_c_major_unit(x_axis.get("major_unit")) 

2428 

2429 # Write the c:minorUnit element. 

2430 self._write_c_minor_unit(x_axis.get("minor_unit")) 

2431 

2432 # Write the c:dispUnits element. 

2433 self._write_disp_units( 

2434 x_axis.get("display_units"), x_axis.get("display_units_visible") 

2435 ) 

2436 

2437 self._xml_end_tag("c:valAx") 

2438 

2439 def _write_date_axis(self, args) -> None: 

2440 # Write the <c:dateAx> element. Usually the X axis. 

2441 x_axis = args["x_axis"] 

2442 y_axis = args["y_axis"] 

2443 axis_ids = args["axis_ids"] 

2444 

2445 # If there are no axis_ids then we don't need to write this element. 

2446 if axis_ids is None or not axis_ids: 

2447 return 

2448 

2449 position = self.cat_axis_position 

2450 

2451 # Overwrite the default axis position with a user supplied value. 

2452 position = x_axis.get("position") or position 

2453 

2454 self._xml_start_tag("c:dateAx") 

2455 

2456 self._write_axis_id(axis_ids[0]) 

2457 

2458 # Write the c:scaling element. 

2459 self._write_scaling( 

2460 x_axis.get("reverse"), 

2461 x_axis.get("min"), 

2462 x_axis.get("max"), 

2463 x_axis.get("log_base"), 

2464 ) 

2465 

2466 if not x_axis.get("visible"): 

2467 self._write_delete(1) 

2468 

2469 # Write the c:axPos element. 

2470 self._write_axis_pos(position, y_axis.get("reverse")) 

2471 

2472 # Write the c:majorGridlines element. 

2473 self._write_major_gridlines(x_axis.get("major_gridlines")) 

2474 

2475 # Write the c:minorGridlines element. 

2476 self._write_minor_gridlines(x_axis.get("minor_gridlines")) 

2477 

2478 # Write the axis title elements. 

2479 if x_axis["formula"] is not None: 

2480 self._write_title_formula( 

2481 x_axis["formula"], 

2482 x_axis["data_id"], 

2483 None, 

2484 x_axis["name_font"], 

2485 x_axis["name_layout"], 

2486 ) 

2487 elif x_axis["name"] is not None: 

2488 self._write_title_rich( 

2489 x_axis["name"], None, x_axis["name_font"], x_axis["name_layout"] 

2490 ) 

2491 

2492 # Write the c:numFmt element. 

2493 self._write_number_format(x_axis) 

2494 

2495 # Write the c:majorTickMark element. 

2496 self._write_major_tick_mark(x_axis.get("major_tick_mark")) 

2497 

2498 # Write the c:minorTickMark element. 

2499 self._write_minor_tick_mark(x_axis.get("minor_tick_mark")) 

2500 

2501 # Write the c:tickLblPos element. 

2502 self._write_tick_label_pos(x_axis.get("label_position")) 

2503 

2504 # Write the c:spPr element for the axis line. 

2505 self._write_sp_pr(x_axis) 

2506 

2507 # Write the axis font elements. 

2508 self._write_axis_font(x_axis.get("num_font")) 

2509 

2510 # Write the c:crossAx element. 

2511 self._write_cross_axis(axis_ids[1]) 

2512 

2513 if self.show_crosses or x_axis.get("visible"): 

2514 # Note, the category crossing comes from the value axis. 

2515 if ( 

2516 y_axis.get("crossing") is None 

2517 or y_axis.get("crossing") == "max" 

2518 or y_axis["crossing"] == "min" 

2519 ): 

2520 # Write the c:crosses element. 

2521 self._write_crosses(y_axis.get("crossing")) 

2522 else: 

2523 # Write the c:crossesAt element. 

2524 self._write_c_crosses_at(y_axis.get("crossing")) 

2525 

2526 # Write the c:auto element. 

2527 self._write_auto(1) 

2528 

2529 # Write the c:labelOffset element. 

2530 self._write_label_offset(100) 

2531 

2532 # Write the c:tickLblSkip element. 

2533 self._write_c_tick_lbl_skip(x_axis.get("interval_unit")) 

2534 

2535 # Write the c:tickMarkSkip element. 

2536 self._write_c_tick_mark_skip(x_axis.get("interval_tick")) 

2537 

2538 # Write the c:majorUnit element. 

2539 self._write_c_major_unit(x_axis.get("major_unit")) 

2540 

2541 # Write the c:majorTimeUnit element. 

2542 if x_axis.get("major_unit"): 

2543 self._write_c_major_time_unit(x_axis["major_unit_type"]) 

2544 

2545 # Write the c:minorUnit element. 

2546 self._write_c_minor_unit(x_axis.get("minor_unit")) 

2547 

2548 # Write the c:minorTimeUnit element. 

2549 if x_axis.get("minor_unit"): 

2550 self._write_c_minor_time_unit(x_axis["minor_unit_type"]) 

2551 

2552 self._xml_end_tag("c:dateAx") 

2553 

2554 def _write_scaling(self, reverse, min_val, max_val, log_base) -> None: 

2555 # Write the <c:scaling> element. 

2556 

2557 self._xml_start_tag("c:scaling") 

2558 

2559 # Write the c:logBase element. 

2560 self._write_c_log_base(log_base) 

2561 

2562 # Write the c:orientation element. 

2563 self._write_orientation(reverse) 

2564 

2565 # Write the c:max element. 

2566 self._write_c_max(max_val) 

2567 

2568 # Write the c:min element. 

2569 self._write_c_min(min_val) 

2570 

2571 self._xml_end_tag("c:scaling") 

2572 

2573 def _write_c_log_base(self, val) -> None: 

2574 # Write the <c:logBase> element. 

2575 

2576 if not val: 

2577 return 

2578 

2579 attributes = [("val", val)] 

2580 

2581 self._xml_empty_tag("c:logBase", attributes) 

2582 

2583 def _write_orientation(self, reverse) -> None: 

2584 # Write the <c:orientation> element. 

2585 val = "minMax" 

2586 

2587 if reverse: 

2588 val = "maxMin" 

2589 

2590 attributes = [("val", val)] 

2591 

2592 self._xml_empty_tag("c:orientation", attributes) 

2593 

2594 def _write_c_max(self, max_val) -> None: 

2595 # Write the <c:max> element. 

2596 

2597 if max_val is None: 

2598 return 

2599 

2600 attributes = [("val", max_val)] 

2601 

2602 self._xml_empty_tag("c:max", attributes) 

2603 

2604 def _write_c_min(self, min_val) -> None: 

2605 # Write the <c:min> element. 

2606 

2607 if min_val is None: 

2608 return 

2609 

2610 attributes = [("val", min_val)] 

2611 

2612 self._xml_empty_tag("c:min", attributes) 

2613 

2614 def _write_axis_pos(self, val, reverse) -> None: 

2615 # Write the <c:axPos> element. 

2616 

2617 if reverse: 

2618 if val == "l": 

2619 val = "r" 

2620 if val == "b": 

2621 val = "t" 

2622 

2623 attributes = [("val", val)] 

2624 

2625 self._xml_empty_tag("c:axPos", attributes) 

2626 

2627 def _write_number_format(self, axis) -> None: 

2628 # Write the <c:numberFormat> element. Note: It is assumed that if 

2629 # a user defined number format is supplied (i.e., non-default) then 

2630 # the sourceLinked attribute is 0. 

2631 # The user can override this if required. 

2632 format_code = axis.get("num_format") 

2633 source_linked = 1 

2634 

2635 # Check if a user defined number format has been set. 

2636 if format_code is not None and format_code != axis["defaults"]["num_format"]: 

2637 source_linked = 0 

2638 

2639 # User override of sourceLinked. 

2640 if axis.get("num_format_linked"): 

2641 source_linked = 1 

2642 

2643 attributes = [ 

2644 ("formatCode", format_code), 

2645 ("sourceLinked", source_linked), 

2646 ] 

2647 

2648 self._xml_empty_tag("c:numFmt", attributes) 

2649 

2650 def _write_cat_number_format(self, axis) -> None: 

2651 # Write the <c:numFmt> element. Special case handler for category 

2652 # axes which don't always have a number format. 

2653 format_code = axis.get("num_format") 

2654 source_linked = 1 

2655 default_format = 1 

2656 

2657 # Check if a user defined number format has been set. 

2658 if format_code is not None and format_code != axis["defaults"]["num_format"]: 

2659 source_linked = 0 

2660 default_format = 0 

2661 

2662 # User override of sourceLinked. 

2663 if axis.get("num_format_linked"): 

2664 source_linked = 1 

2665 

2666 # Skip if cat doesn't have a num format (unless it is non-default). 

2667 if not self.cat_has_num_fmt and default_format: 

2668 return 

2669 

2670 attributes = [ 

2671 ("formatCode", format_code), 

2672 ("sourceLinked", source_linked), 

2673 ] 

2674 

2675 self._xml_empty_tag("c:numFmt", attributes) 

2676 

2677 def _write_data_label_number_format(self, format_code) -> None: 

2678 # Write the <c:numberFormat> element for data labels. 

2679 source_linked = 0 

2680 

2681 attributes = [ 

2682 ("formatCode", format_code), 

2683 ("sourceLinked", source_linked), 

2684 ] 

2685 

2686 self._xml_empty_tag("c:numFmt", attributes) 

2687 

2688 def _write_major_tick_mark(self, val) -> None: 

2689 # Write the <c:majorTickMark> element. 

2690 

2691 if not val: 

2692 return 

2693 

2694 attributes = [("val", val)] 

2695 

2696 self._xml_empty_tag("c:majorTickMark", attributes) 

2697 

2698 def _write_minor_tick_mark(self, val) -> None: 

2699 # Write the <c:minorTickMark> element. 

2700 

2701 if not val: 

2702 return 

2703 

2704 attributes = [("val", val)] 

2705 

2706 self._xml_empty_tag("c:minorTickMark", attributes) 

2707 

2708 def _write_tick_label_pos(self, val=None) -> None: 

2709 # Write the <c:tickLblPos> element. 

2710 if val is None or val == "next_to": 

2711 val = "nextTo" 

2712 

2713 attributes = [("val", val)] 

2714 

2715 self._xml_empty_tag("c:tickLblPos", attributes) 

2716 

2717 def _write_cross_axis(self, val) -> None: 

2718 # Write the <c:crossAx> element. 

2719 

2720 attributes = [("val", val)] 

2721 

2722 self._xml_empty_tag("c:crossAx", attributes) 

2723 

2724 def _write_crosses(self, val=None) -> None: 

2725 # Write the <c:crosses> element. 

2726 if val is None: 

2727 val = "autoZero" 

2728 

2729 attributes = [("val", val)] 

2730 

2731 self._xml_empty_tag("c:crosses", attributes) 

2732 

2733 def _write_c_crosses_at(self, val) -> None: 

2734 # Write the <c:crossesAt> element. 

2735 

2736 attributes = [("val", val)] 

2737 

2738 self._xml_empty_tag("c:crossesAt", attributes) 

2739 

2740 def _write_auto(self, val) -> None: 

2741 # Write the <c:auto> element. 

2742 

2743 attributes = [("val", val)] 

2744 

2745 self._xml_empty_tag("c:auto", attributes) 

2746 

2747 def _write_label_align(self, val=None) -> None: 

2748 # Write the <c:labelAlign> element. 

2749 

2750 if val is None: 

2751 val = "ctr" 

2752 

2753 if val == "right": 

2754 val = "r" 

2755 

2756 if val == "left": 

2757 val = "l" 

2758 

2759 attributes = [("val", val)] 

2760 

2761 self._xml_empty_tag("c:lblAlgn", attributes) 

2762 

2763 def _write_label_offset(self, val) -> None: 

2764 # Write the <c:labelOffset> element. 

2765 

2766 attributes = [("val", val)] 

2767 

2768 self._xml_empty_tag("c:lblOffset", attributes) 

2769 

2770 def _write_c_tick_lbl_skip(self, val) -> None: 

2771 # Write the <c:tickLblSkip> element. 

2772 if val is None: 

2773 return 

2774 

2775 attributes = [("val", val)] 

2776 

2777 self._xml_empty_tag("c:tickLblSkip", attributes) 

2778 

2779 def _write_c_tick_mark_skip(self, val) -> None: 

2780 # Write the <c:tickMarkSkip> element. 

2781 if val is None: 

2782 return 

2783 

2784 attributes = [("val", val)] 

2785 

2786 self._xml_empty_tag("c:tickMarkSkip", attributes) 

2787 

2788 def _write_major_gridlines(self, gridlines) -> None: 

2789 # Write the <c:majorGridlines> element. 

2790 

2791 if not gridlines: 

2792 return 

2793 

2794 if not gridlines["visible"]: 

2795 return 

2796 

2797 if gridlines["line"]["defined"]: 

2798 self._xml_start_tag("c:majorGridlines") 

2799 

2800 # Write the c:spPr element. 

2801 self._write_sp_pr(gridlines) 

2802 

2803 self._xml_end_tag("c:majorGridlines") 

2804 else: 

2805 self._xml_empty_tag("c:majorGridlines") 

2806 

2807 def _write_minor_gridlines(self, gridlines) -> None: 

2808 # Write the <c:minorGridlines> element. 

2809 

2810 if not gridlines: 

2811 return 

2812 

2813 if not gridlines["visible"]: 

2814 return 

2815 

2816 if gridlines["line"]["defined"]: 

2817 self._xml_start_tag("c:minorGridlines") 

2818 

2819 # Write the c:spPr element. 

2820 self._write_sp_pr(gridlines) 

2821 

2822 self._xml_end_tag("c:minorGridlines") 

2823 else: 

2824 self._xml_empty_tag("c:minorGridlines") 

2825 

2826 def _write_cross_between(self, val) -> None: 

2827 # Write the <c:crossBetween> element. 

2828 if val is None: 

2829 val = self.cross_between 

2830 

2831 attributes = [("val", val)] 

2832 

2833 self._xml_empty_tag("c:crossBetween", attributes) 

2834 

2835 def _write_c_major_unit(self, val) -> None: 

2836 # Write the <c:majorUnit> element. 

2837 

2838 if not val: 

2839 return 

2840 

2841 attributes = [("val", val)] 

2842 

2843 self._xml_empty_tag("c:majorUnit", attributes) 

2844 

2845 def _write_c_minor_unit(self, val) -> None: 

2846 # Write the <c:minorUnit> element. 

2847 

2848 if not val: 

2849 return 

2850 

2851 attributes = [("val", val)] 

2852 

2853 self._xml_empty_tag("c:minorUnit", attributes) 

2854 

2855 def _write_c_major_time_unit(self, val=None) -> None: 

2856 # Write the <c:majorTimeUnit> element. 

2857 if val is None: 

2858 val = "days" 

2859 

2860 attributes = [("val", val)] 

2861 

2862 self._xml_empty_tag("c:majorTimeUnit", attributes) 

2863 

2864 def _write_c_minor_time_unit(self, val=None) -> None: 

2865 # Write the <c:minorTimeUnit> element. 

2866 if val is None: 

2867 val = "days" 

2868 

2869 attributes = [("val", val)] 

2870 

2871 self._xml_empty_tag("c:minorTimeUnit", attributes) 

2872 

2873 def _write_legend(self) -> None: 

2874 # Write the <c:legend> element. 

2875 legend = self.legend 

2876 position = legend.get("position", "right") 

2877 font = legend.get("font") 

2878 delete_series = [] 

2879 overlay = 0 

2880 

2881 if legend.get("delete_series") and isinstance(legend["delete_series"], list): 

2882 delete_series = legend["delete_series"] 

2883 

2884 if position.startswith("overlay_"): 

2885 position = position.replace("overlay_", "") 

2886 overlay = 1 

2887 

2888 allowed = { 

2889 "right": "r", 

2890 "left": "l", 

2891 "top": "t", 

2892 "bottom": "b", 

2893 "top_right": "tr", 

2894 } 

2895 

2896 if position == "none": 

2897 return 

2898 

2899 if position not in allowed: 

2900 return 

2901 

2902 position = allowed[position] 

2903 

2904 self._xml_start_tag("c:legend") 

2905 

2906 # Write the c:legendPos element. 

2907 self._write_legend_pos(position) 

2908 

2909 # Remove series labels from the legend. 

2910 for index in delete_series: 

2911 # Write the c:legendEntry element. 

2912 self._write_legend_entry(index) 

2913 

2914 # Write the c:layout element. 

2915 self._write_layout(legend.get("layout"), "legend") 

2916 

2917 # Write the c:overlay element. 

2918 if overlay: 

2919 self._write_overlay() 

2920 

2921 if font: 

2922 self._write_tx_pr(font) 

2923 

2924 # Write the c:spPr element. 

2925 self._write_sp_pr(legend) 

2926 

2927 self._xml_end_tag("c:legend") 

2928 

2929 def _write_legend_pos(self, val) -> None: 

2930 # Write the <c:legendPos> element. 

2931 

2932 attributes = [("val", val)] 

2933 

2934 self._xml_empty_tag("c:legendPos", attributes) 

2935 

2936 def _write_legend_entry(self, index) -> None: 

2937 # Write the <c:legendEntry> element. 

2938 

2939 self._xml_start_tag("c:legendEntry") 

2940 

2941 # Write the c:idx element. 

2942 self._write_idx(index) 

2943 

2944 # Write the c:delete element. 

2945 self._write_delete(1) 

2946 

2947 self._xml_end_tag("c:legendEntry") 

2948 

2949 def _write_overlay(self) -> None: 

2950 # Write the <c:overlay> element. 

2951 val = 1 

2952 

2953 attributes = [("val", val)] 

2954 

2955 self._xml_empty_tag("c:overlay", attributes) 

2956 

2957 def _write_plot_vis_only(self) -> None: 

2958 # Write the <c:plotVisOnly> element. 

2959 val = 1 

2960 

2961 # Ignore this element if we are plotting hidden data. 

2962 if self.show_hidden: 

2963 return 

2964 

2965 attributes = [("val", val)] 

2966 

2967 self._xml_empty_tag("c:plotVisOnly", attributes) 

2968 

2969 def _write_print_settings(self) -> None: 

2970 # Write the <c:printSettings> element. 

2971 self._xml_start_tag("c:printSettings") 

2972 

2973 # Write the c:headerFooter element. 

2974 self._write_header_footer() 

2975 

2976 # Write the c:pageMargins element. 

2977 self._write_page_margins() 

2978 

2979 # Write the c:pageSetup element. 

2980 self._write_page_setup() 

2981 

2982 self._xml_end_tag("c:printSettings") 

2983 

2984 def _write_header_footer(self) -> None: 

2985 # Write the <c:headerFooter> element. 

2986 self._xml_empty_tag("c:headerFooter") 

2987 

2988 def _write_page_margins(self) -> None: 

2989 # Write the <c:pageMargins> element. 

2990 bottom = 0.75 

2991 left = 0.7 

2992 right = 0.7 

2993 top = 0.75 

2994 header = 0.3 

2995 footer = 0.3 

2996 

2997 attributes = [ 

2998 ("b", bottom), 

2999 ("l", left), 

3000 ("r", right), 

3001 ("t", top), 

3002 ("header", header), 

3003 ("footer", footer), 

3004 ] 

3005 

3006 self._xml_empty_tag("c:pageMargins", attributes) 

3007 

3008 def _write_page_setup(self) -> None: 

3009 # Write the <c:pageSetup> element. 

3010 self._xml_empty_tag("c:pageSetup") 

3011 

3012 def _write_c_auto_title_deleted(self) -> None: 

3013 # Write the <c:autoTitleDeleted> element. 

3014 self._xml_empty_tag("c:autoTitleDeleted", [("val", 1)]) 

3015 

3016 def _write_title_rich(self, title, is_y_axis, font, layout, overlay=False) -> None: 

3017 # Write the <c:title> element for a rich string. 

3018 

3019 self._xml_start_tag("c:title") 

3020 

3021 # Write the c:tx element. 

3022 self._write_tx_rich(title, is_y_axis, font) 

3023 

3024 # Write the c:layout element. 

3025 self._write_layout(layout, "text") 

3026 

3027 # Write the c:overlay element. 

3028 if overlay: 

3029 self._write_overlay() 

3030 

3031 self._xml_end_tag("c:title") 

3032 

3033 def _write_title_formula( 

3034 self, title, data_id, is_y_axis, font, layout, overlay=False 

3035 ) -> None: 

3036 # Write the <c:title> element for a rich string. 

3037 

3038 self._xml_start_tag("c:title") 

3039 

3040 # Write the c:tx element. 

3041 self._write_tx_formula(title, data_id) 

3042 

3043 # Write the c:layout element. 

3044 self._write_layout(layout, "text") 

3045 

3046 # Write the c:overlay element. 

3047 if overlay: 

3048 self._write_overlay() 

3049 

3050 # Write the c:txPr element. 

3051 self._write_tx_pr(font, is_y_axis) 

3052 

3053 self._xml_end_tag("c:title") 

3054 

3055 def _write_tx_rich(self, title, is_y_axis, font) -> None: 

3056 # Write the <c:tx> element. 

3057 

3058 self._xml_start_tag("c:tx") 

3059 

3060 # Write the c:rich element. 

3061 self._write_rich(title, font, is_y_axis, ignore_rich_pr=False) 

3062 

3063 self._xml_end_tag("c:tx") 

3064 

3065 def _write_tx_value(self, title) -> None: 

3066 # Write the <c:tx> element with a value such as for series names. 

3067 

3068 self._xml_start_tag("c:tx") 

3069 

3070 # Write the c:v element. 

3071 self._write_v(title) 

3072 

3073 self._xml_end_tag("c:tx") 

3074 

3075 def _write_tx_formula(self, title, data_id) -> None: 

3076 # Write the <c:tx> element. 

3077 data = None 

3078 

3079 if data_id is not None: 

3080 data = self.formula_data[data_id] 

3081 

3082 self._xml_start_tag("c:tx") 

3083 

3084 # Write the c:strRef element. 

3085 self._write_str_ref(title, data, "str") 

3086 

3087 self._xml_end_tag("c:tx") 

3088 

3089 def _write_rich(self, title, font, is_y_axis, ignore_rich_pr) -> None: 

3090 # Write the <c:rich> element. 

3091 

3092 if font and font.get("rotation") is not None: 

3093 rotation = font["rotation"] 

3094 else: 

3095 rotation = None 

3096 

3097 self._xml_start_tag("c:rich") 

3098 

3099 # Write the a:bodyPr element. 

3100 self._write_a_body_pr(rotation, is_y_axis) 

3101 

3102 # Write the a:lstStyle element. 

3103 self._write_a_lst_style() 

3104 

3105 # Write the a:p element. 

3106 self._write_a_p_rich(title, font, ignore_rich_pr) 

3107 

3108 self._xml_end_tag("c:rich") 

3109 

3110 def _write_a_body_pr(self, rotation, is_y_axis) -> None: 

3111 # Write the <a:bodyPr> element. 

3112 attributes = [] 

3113 

3114 if rotation is None and is_y_axis: 

3115 rotation = -5400000 

3116 

3117 if rotation is not None: 

3118 if rotation == 16200000: 

3119 # 270 deg/stacked angle. 

3120 attributes.append(("rot", 0)) 

3121 attributes.append(("vert", "wordArtVert")) 

3122 elif rotation == 16260000: 

3123 # 271 deg/East Asian vertical. 

3124 attributes.append(("rot", 0)) 

3125 attributes.append(("vert", "eaVert")) 

3126 else: 

3127 attributes.append(("rot", rotation)) 

3128 attributes.append(("vert", "horz")) 

3129 

3130 self._xml_empty_tag("a:bodyPr", attributes) 

3131 

3132 def _write_a_lst_style(self) -> None: 

3133 # Write the <a:lstStyle> element. 

3134 self._xml_empty_tag("a:lstStyle") 

3135 

3136 def _write_a_p_rich(self, title, font, ignore_rich_pr) -> None: 

3137 # Write the <a:p> element for rich string titles. 

3138 

3139 self._xml_start_tag("a:p") 

3140 

3141 # Write the a:pPr element. 

3142 if not ignore_rich_pr: 

3143 self._write_a_p_pr_rich(font) 

3144 

3145 # Write the a:r element. 

3146 self._write_a_r(title, font) 

3147 

3148 self._xml_end_tag("a:p") 

3149 

3150 def _write_a_p_formula(self, font) -> None: 

3151 # Write the <a:p> element for formula titles. 

3152 

3153 self._xml_start_tag("a:p") 

3154 

3155 # Write the a:pPr element. 

3156 self._write_a_p_pr_rich(font) 

3157 

3158 # Write the a:endParaRPr element. 

3159 self._write_a_end_para_rpr() 

3160 

3161 self._xml_end_tag("a:p") 

3162 

3163 def _write_a_p_pr_rich(self, font) -> None: 

3164 # Write the <a:pPr> element for rich string titles. 

3165 

3166 self._xml_start_tag("a:pPr") 

3167 

3168 # Write the a:defRPr element. 

3169 self._write_a_def_rpr(font) 

3170 

3171 self._xml_end_tag("a:pPr") 

3172 

3173 def _write_a_def_rpr(self, font) -> None: 

3174 # Write the <a:defRPr> element. 

3175 has_color = False 

3176 

3177 style_attributes = Shape._get_font_style_attributes(font) 

3178 latin_attributes = Shape._get_font_latin_attributes(font) 

3179 

3180 if font and font.get("color"): 

3181 has_color = True 

3182 

3183 if latin_attributes or has_color: 

3184 self._xml_start_tag("a:defRPr", style_attributes) 

3185 

3186 if has_color: 

3187 self._write_a_solid_fill({"color": font["color"]}) 

3188 

3189 if latin_attributes: 

3190 self._write_a_latin(latin_attributes) 

3191 

3192 self._xml_end_tag("a:defRPr") 

3193 else: 

3194 self._xml_empty_tag("a:defRPr", style_attributes) 

3195 

3196 def _write_a_end_para_rpr(self) -> None: 

3197 # Write the <a:endParaRPr> element. 

3198 lang = "en-US" 

3199 

3200 attributes = [("lang", lang)] 

3201 

3202 self._xml_empty_tag("a:endParaRPr", attributes) 

3203 

3204 def _write_a_r(self, title, font) -> None: 

3205 # Write the <a:r> element. 

3206 

3207 self._xml_start_tag("a:r") 

3208 

3209 # Write the a:rPr element. 

3210 self._write_a_r_pr(font) 

3211 

3212 # Write the a:t element. 

3213 self._write_a_t(title) 

3214 

3215 self._xml_end_tag("a:r") 

3216 

3217 def _write_a_r_pr(self, font) -> None: 

3218 # Write the <a:rPr> element. 

3219 has_color = False 

3220 lang = "en-US" 

3221 

3222 style_attributes = Shape._get_font_style_attributes(font) 

3223 latin_attributes = Shape._get_font_latin_attributes(font) 

3224 

3225 if font and font["color"]: 

3226 has_color = True 

3227 

3228 # Add the lang type to the attributes. 

3229 style_attributes.insert(0, ("lang", lang)) 

3230 

3231 if latin_attributes or has_color: 

3232 self._xml_start_tag("a:rPr", style_attributes) 

3233 

3234 if has_color: 

3235 self._write_a_solid_fill({"color": font["color"]}) 

3236 

3237 if latin_attributes: 

3238 self._write_a_latin(latin_attributes) 

3239 

3240 self._xml_end_tag("a:rPr") 

3241 else: 

3242 self._xml_empty_tag("a:rPr", style_attributes) 

3243 

3244 def _write_a_t(self, title) -> None: 

3245 # Write the <a:t> element. 

3246 

3247 self._xml_data_element("a:t", title) 

3248 

3249 def _write_tx_pr(self, font, is_y_axis=False) -> None: 

3250 # Write the <c:txPr> element. 

3251 

3252 if font and font.get("rotation") is not None: 

3253 rotation = font["rotation"] 

3254 else: 

3255 rotation = None 

3256 

3257 self._xml_start_tag("c:txPr") 

3258 

3259 # Write the a:bodyPr element. 

3260 self._write_a_body_pr(rotation, is_y_axis) 

3261 

3262 # Write the a:lstStyle element. 

3263 self._write_a_lst_style() 

3264 

3265 # Write the a:p element. 

3266 self._write_a_p_formula(font) 

3267 

3268 self._xml_end_tag("c:txPr") 

3269 

3270 def _write_marker(self, marker) -> None: 

3271 # Write the <c:marker> element. 

3272 if marker is None: 

3273 marker = self.default_marker 

3274 

3275 if not marker: 

3276 return 

3277 

3278 if marker["type"] == "automatic": 

3279 return 

3280 

3281 self._xml_start_tag("c:marker") 

3282 

3283 # Write the c:symbol element. 

3284 self._write_symbol(marker["type"]) 

3285 

3286 # Write the c:size element. 

3287 if marker.get("size"): 

3288 self._write_marker_size(marker["size"]) 

3289 

3290 # Write the c:spPr element. 

3291 self._write_sp_pr(marker) 

3292 

3293 self._xml_end_tag("c:marker") 

3294 

3295 def _write_marker_size(self, val) -> None: 

3296 # Write the <c:size> element. 

3297 

3298 attributes = [("val", val)] 

3299 

3300 self._xml_empty_tag("c:size", attributes) 

3301 

3302 def _write_symbol(self, val) -> None: 

3303 # Write the <c:symbol> element. 

3304 

3305 attributes = [("val", val)] 

3306 

3307 self._xml_empty_tag("c:symbol", attributes) 

3308 

3309 def _write_sp_pr(self, series) -> None: 

3310 # Write the <c:spPr> element. 

3311 

3312 if not self._has_fill_formatting(series): 

3313 return 

3314 

3315 self._xml_start_tag("c:spPr") 

3316 

3317 # Write the fill elements for solid charts such as pie and bar. 

3318 if series.get("fill") and series["fill"]["defined"]: 

3319 if "none" in series["fill"]: 

3320 # Write the a:noFill element. 

3321 self._write_a_no_fill() 

3322 else: 

3323 # Write the a:solidFill element. 

3324 self._write_a_solid_fill(series["fill"]) 

3325 

3326 if series.get("pattern"): 

3327 # Write the a:gradFill element. 

3328 self._write_a_patt_fill(series["pattern"]) 

3329 

3330 if series.get("gradient"): 

3331 # Write the a:gradFill element. 

3332 self._write_a_grad_fill(series["gradient"]) 

3333 

3334 # Write the a:ln element. 

3335 if series.get("line") and series["line"]["defined"]: 

3336 self._write_a_ln(series["line"]) 

3337 

3338 self._xml_end_tag("c:spPr") 

3339 

3340 def _write_a_ln(self, line) -> None: 

3341 # Write the <a:ln> element. 

3342 attributes = [] 

3343 

3344 # Add the line width as an attribute. 

3345 width = line.get("width") 

3346 

3347 if width is not None: 

3348 # Round width to nearest 0.25, like Excel. 

3349 width = int((width + 0.125) * 4) / 4.0 

3350 

3351 # Convert to internal units. 

3352 width = int(0.5 + (12700 * width)) 

3353 

3354 attributes = [("w", width)] 

3355 

3356 if line.get("none") or line.get("color") or line.get("dash_type"): 

3357 self._xml_start_tag("a:ln", attributes) 

3358 

3359 # Write the line fill. 

3360 if "none" in line: 

3361 # Write the a:noFill element. 

3362 self._write_a_no_fill() 

3363 elif "color" in line: 

3364 # Write the a:solidFill element. 

3365 self._write_a_solid_fill(line) 

3366 

3367 # Write the line/dash type. 

3368 line_type = line.get("dash_type") 

3369 if line_type: 

3370 # Write the a:prstDash element. 

3371 self._write_a_prst_dash(line_type) 

3372 

3373 self._xml_end_tag("a:ln") 

3374 else: 

3375 self._xml_empty_tag("a:ln", attributes) 

3376 

3377 def _write_a_no_fill(self) -> None: 

3378 # Write the <a:noFill> element. 

3379 self._xml_empty_tag("a:noFill") 

3380 

3381 def _write_a_solid_fill(self, fill) -> None: 

3382 # Write the <a:solidFill> element. 

3383 

3384 self._xml_start_tag("a:solidFill") 

3385 

3386 if fill.get("color"): 

3387 self._write_color(fill["color"], fill.get("transparency")) 

3388 

3389 self._xml_end_tag("a:solidFill") 

3390 

3391 def _write_color(self, color: Color, transparency=None) -> None: 

3392 # Write the appropriate chart color element. 

3393 

3394 if not color: 

3395 return 

3396 

3397 if color._is_automatic: 

3398 # Write the a:sysClr element. 

3399 self._write_a_sys_clr() 

3400 elif color._type == ColorTypes.RGB: 

3401 # Write the a:srgbClr element. 

3402 self._write_a_srgb_clr(color, transparency) 

3403 elif color._type == ColorTypes.THEME: 

3404 self._write_a_scheme_clr(color, transparency) 

3405 

3406 def _write_a_sys_clr(self) -> None: 

3407 # Write the <a:sysClr> element. 

3408 

3409 val = "window" 

3410 last_clr = "FFFFFF" 

3411 

3412 attributes = [ 

3413 ("val", val), 

3414 ("lastClr", last_clr), 

3415 ] 

3416 

3417 self._xml_empty_tag("a:sysClr", attributes) 

3418 

3419 def _write_a_srgb_clr(self, color: Color, transparency=None) -> None: 

3420 # Write the <a:srgbClr> element. 

3421 

3422 if not color: 

3423 return 

3424 

3425 attributes = [("val", color._rgb_hex_value())] 

3426 

3427 if transparency: 

3428 self._xml_start_tag("a:srgbClr", attributes) 

3429 

3430 # Write the a:alpha element. 

3431 self._write_a_alpha(transparency) 

3432 

3433 self._xml_end_tag("a:srgbClr") 

3434 else: 

3435 self._xml_empty_tag("a:srgbClr", attributes) 

3436 

3437 def _write_a_scheme_clr(self, color: Color, transparency=None) -> None: 

3438 # Write the <a:schemeClr> element. 

3439 scheme, lum_mod, lum_off = color._chart_scheme() 

3440 attributes = [("val", scheme)] 

3441 

3442 if lum_mod > 0 or lum_off > 0 or transparency: 

3443 self._xml_start_tag("a:schemeClr", attributes) 

3444 

3445 if lum_mod > 0: 

3446 # Write the a:lumMod element. 

3447 self._write_a_lum_mod(lum_mod) 

3448 

3449 if lum_off > 0: 

3450 # Write the a:lumOff element. 

3451 self._write_a_lum_off(lum_off) 

3452 

3453 if transparency: 

3454 # Write the a:alpha element. 

3455 self._write_a_alpha(transparency) 

3456 

3457 self._xml_end_tag("a:schemeClr") 

3458 else: 

3459 self._xml_empty_tag("a:schemeClr", attributes) 

3460 

3461 def _write_a_lum_mod(self, value: int) -> None: 

3462 # Write the <a:lumMod> element. 

3463 attributes = [("val", value)] 

3464 

3465 self._xml_empty_tag("a:lumMod", attributes) 

3466 

3467 def _write_a_lum_off(self, value: int) -> None: 

3468 # Write the <a:lumOff> element. 

3469 attributes = [("val", value)] 

3470 

3471 self._xml_empty_tag("a:lumOff", attributes) 

3472 

3473 def _write_a_alpha(self, val) -> None: 

3474 # Write the <a:alpha> element. 

3475 

3476 val = int((100 - int(val)) * 1000) 

3477 

3478 attributes = [("val", val)] 

3479 

3480 self._xml_empty_tag("a:alpha", attributes) 

3481 

3482 def _write_a_prst_dash(self, val) -> None: 

3483 # Write the <a:prstDash> element. 

3484 

3485 attributes = [("val", val)] 

3486 

3487 self._xml_empty_tag("a:prstDash", attributes) 

3488 

3489 def _write_trendline(self, trendline) -> None: 

3490 # Write the <c:trendline> element. 

3491 

3492 if not trendline: 

3493 return 

3494 

3495 self._xml_start_tag("c:trendline") 

3496 

3497 # Write the c:name element. 

3498 self._write_name(trendline.get("name")) 

3499 

3500 # Write the c:spPr element. 

3501 self._write_sp_pr(trendline) 

3502 

3503 # Write the c:trendlineType element. 

3504 self._write_trendline_type(trendline["type"]) 

3505 

3506 # Write the c:order element for polynomial trendlines. 

3507 if trendline["type"] == "poly": 

3508 self._write_trendline_order(trendline.get("order")) 

3509 

3510 # Write the c:period element for moving average trendlines. 

3511 if trendline["type"] == "movingAvg": 

3512 self._write_period(trendline.get("period")) 

3513 

3514 # Write the c:forward element. 

3515 self._write_forward(trendline.get("forward")) 

3516 

3517 # Write the c:backward element. 

3518 self._write_backward(trendline.get("backward")) 

3519 

3520 if "intercept" in trendline: 

3521 # Write the c:intercept element. 

3522 self._write_c_intercept(trendline["intercept"]) 

3523 

3524 if trendline.get("display_r_squared"): 

3525 # Write the c:dispRSqr element. 

3526 self._write_c_disp_rsqr() 

3527 

3528 if trendline.get("display_equation"): 

3529 # Write the c:dispEq element. 

3530 self._write_c_disp_eq() 

3531 

3532 # Write the c:trendlineLbl element. 

3533 self._write_c_trendline_lbl(trendline) 

3534 

3535 self._xml_end_tag("c:trendline") 

3536 

3537 def _write_trendline_type(self, val) -> None: 

3538 # Write the <c:trendlineType> element. 

3539 

3540 attributes = [("val", val)] 

3541 

3542 self._xml_empty_tag("c:trendlineType", attributes) 

3543 

3544 def _write_name(self, data) -> None: 

3545 # Write the <c:name> element. 

3546 

3547 if data is None: 

3548 return 

3549 

3550 self._xml_data_element("c:name", data) 

3551 

3552 def _write_trendline_order(self, val) -> None: 

3553 # Write the <c:order> element. 

3554 val = max(val, 2) 

3555 

3556 attributes = [("val", val)] 

3557 

3558 self._xml_empty_tag("c:order", attributes) 

3559 

3560 def _write_period(self, val) -> None: 

3561 # Write the <c:period> element. 

3562 val = max(val, 2) 

3563 

3564 attributes = [("val", val)] 

3565 

3566 self._xml_empty_tag("c:period", attributes) 

3567 

3568 def _write_forward(self, val) -> None: 

3569 # Write the <c:forward> element. 

3570 

3571 if not val: 

3572 return 

3573 

3574 attributes = [("val", val)] 

3575 

3576 self._xml_empty_tag("c:forward", attributes) 

3577 

3578 def _write_backward(self, val) -> None: 

3579 # Write the <c:backward> element. 

3580 

3581 if not val: 

3582 return 

3583 

3584 attributes = [("val", val)] 

3585 

3586 self._xml_empty_tag("c:backward", attributes) 

3587 

3588 def _write_c_intercept(self, val) -> None: 

3589 # Write the <c:intercept> element. 

3590 attributes = [("val", val)] 

3591 

3592 self._xml_empty_tag("c:intercept", attributes) 

3593 

3594 def _write_c_disp_eq(self) -> None: 

3595 # Write the <c:dispEq> element. 

3596 attributes = [("val", 1)] 

3597 

3598 self._xml_empty_tag("c:dispEq", attributes) 

3599 

3600 def _write_c_disp_rsqr(self) -> None: 

3601 # Write the <c:dispRSqr> element. 

3602 attributes = [("val", 1)] 

3603 

3604 self._xml_empty_tag("c:dispRSqr", attributes) 

3605 

3606 def _write_c_trendline_lbl(self, trendline) -> None: 

3607 # Write the <c:trendlineLbl> element. 

3608 self._xml_start_tag("c:trendlineLbl") 

3609 

3610 # Write the c:layout element. 

3611 self._write_layout(None, None) 

3612 

3613 # Write the c:numFmt element. 

3614 self._write_trendline_num_fmt() 

3615 

3616 # Write the c:spPr element. 

3617 self._write_sp_pr(trendline["label"]) 

3618 

3619 # Write the data label font elements. 

3620 if trendline["label"]: 

3621 font = trendline["label"].get("font") 

3622 if font: 

3623 self._write_axis_font(font) 

3624 

3625 self._xml_end_tag("c:trendlineLbl") 

3626 

3627 def _write_trendline_num_fmt(self) -> None: 

3628 # Write the <c:numFmt> element. 

3629 attributes = [ 

3630 ("formatCode", "General"), 

3631 ("sourceLinked", 0), 

3632 ] 

3633 

3634 self._xml_empty_tag("c:numFmt", attributes) 

3635 

3636 def _write_hi_low_lines(self) -> None: 

3637 # Write the <c:hiLowLines> element. 

3638 hi_low_lines = self.hi_low_lines 

3639 

3640 if hi_low_lines is None: 

3641 return 

3642 

3643 if "line" in hi_low_lines and hi_low_lines["line"]["defined"]: 

3644 self._xml_start_tag("c:hiLowLines") 

3645 

3646 # Write the c:spPr element. 

3647 self._write_sp_pr(hi_low_lines) 

3648 

3649 self._xml_end_tag("c:hiLowLines") 

3650 else: 

3651 self._xml_empty_tag("c:hiLowLines") 

3652 

3653 def _write_drop_lines(self) -> None: 

3654 # Write the <c:dropLines> element. 

3655 drop_lines = self.drop_lines 

3656 

3657 if drop_lines is None: 

3658 return 

3659 

3660 if drop_lines["line"]["defined"]: 

3661 self._xml_start_tag("c:dropLines") 

3662 

3663 # Write the c:spPr element. 

3664 self._write_sp_pr(drop_lines) 

3665 

3666 self._xml_end_tag("c:dropLines") 

3667 else: 

3668 self._xml_empty_tag("c:dropLines") 

3669 

3670 def _write_overlap(self, val) -> None: 

3671 # Write the <c:overlap> element. 

3672 

3673 if val is None: 

3674 return 

3675 

3676 attributes = [("val", val)] 

3677 

3678 self._xml_empty_tag("c:overlap", attributes) 

3679 

3680 def _write_num_cache(self, data) -> None: 

3681 # Write the <c:numCache> element. 

3682 if data: 

3683 count = len(data) 

3684 else: 

3685 count = 0 

3686 

3687 self._xml_start_tag("c:numCache") 

3688 

3689 # Write the c:formatCode element. 

3690 self._write_format_code("General") 

3691 

3692 # Write the c:ptCount element. 

3693 self._write_pt_count(count) 

3694 

3695 for i in range(count): 

3696 token = data[i] 

3697 

3698 if token is None: 

3699 continue 

3700 

3701 try: 

3702 float(token) 

3703 except ValueError: 

3704 # Write non-numeric data as 0. 

3705 token = 0 

3706 

3707 # Write the c:pt element. 

3708 self._write_pt(i, token) 

3709 

3710 self._xml_end_tag("c:numCache") 

3711 

3712 def _write_str_cache(self, data) -> None: 

3713 # Write the <c:strCache> element. 

3714 count = len(data) 

3715 

3716 self._xml_start_tag("c:strCache") 

3717 

3718 # Write the c:ptCount element. 

3719 self._write_pt_count(count) 

3720 

3721 for i in range(count): 

3722 # Write the c:pt element. 

3723 self._write_pt(i, data[i]) 

3724 

3725 self._xml_end_tag("c:strCache") 

3726 

3727 def _write_format_code(self, data) -> None: 

3728 # Write the <c:formatCode> element. 

3729 

3730 self._xml_data_element("c:formatCode", data) 

3731 

3732 def _write_pt_count(self, val) -> None: 

3733 # Write the <c:ptCount> element. 

3734 

3735 attributes = [("val", val)] 

3736 

3737 self._xml_empty_tag("c:ptCount", attributes) 

3738 

3739 def _write_pt(self, idx, value) -> None: 

3740 # Write the <c:pt> element. 

3741 

3742 if value is None: 

3743 return 

3744 

3745 attributes = [("idx", idx)] 

3746 

3747 self._xml_start_tag("c:pt", attributes) 

3748 

3749 # Write the c:v element. 

3750 self._write_v(value) 

3751 

3752 self._xml_end_tag("c:pt") 

3753 

3754 def _write_v(self, data) -> None: 

3755 # Write the <c:v> element. 

3756 

3757 self._xml_data_element("c:v", data) 

3758 

3759 def _write_protection(self) -> None: 

3760 # Write the <c:protection> element. 

3761 if not self.protection: 

3762 return 

3763 

3764 self._xml_empty_tag("c:protection") 

3765 

3766 def _write_d_pt(self, points) -> None: 

3767 # Write the <c:dPt> elements. 

3768 index = -1 

3769 

3770 if not points: 

3771 return 

3772 

3773 for point in points: 

3774 index += 1 

3775 if not point: 

3776 continue 

3777 

3778 self._write_d_pt_point(index, point) 

3779 

3780 def _write_d_pt_point(self, index, point) -> None: 

3781 # Write an individual <c:dPt> element. 

3782 

3783 self._xml_start_tag("c:dPt") 

3784 

3785 # Write the c:idx element. 

3786 self._write_idx(index) 

3787 

3788 # Write the c:spPr element. 

3789 self._write_sp_pr(point) 

3790 

3791 self._xml_end_tag("c:dPt") 

3792 

3793 def _write_d_lbls(self, labels) -> None: 

3794 # Write the <c:dLbls> element. 

3795 

3796 if not labels: 

3797 return 

3798 

3799 self._xml_start_tag("c:dLbls") 

3800 

3801 # Write the custom c:dLbl elements. 

3802 if labels.get("custom"): 

3803 self._write_custom_labels(labels, labels["custom"]) 

3804 

3805 # Write the c:numFmt element. 

3806 if labels.get("num_format"): 

3807 self._write_data_label_number_format(labels["num_format"]) 

3808 

3809 # Write the c:spPr element for the plotarea formatting. 

3810 self._write_sp_pr(labels) 

3811 

3812 # Write the data label font elements. 

3813 if labels.get("font"): 

3814 self._write_axis_font(labels["font"]) 

3815 

3816 # Write the c:dLblPos element. 

3817 if labels.get("position"): 

3818 self._write_d_lbl_pos(labels["position"]) 

3819 

3820 # Write the c:showLegendKey element. 

3821 if labels.get("legend_key"): 

3822 self._write_show_legend_key() 

3823 

3824 # Write the c:showVal element. 

3825 if labels.get("value"): 

3826 self._write_show_val() 

3827 

3828 # Write the c:showCatName element. 

3829 if labels.get("category"): 

3830 self._write_show_cat_name() 

3831 

3832 # Write the c:showSerName element. 

3833 if labels.get("series_name"): 

3834 self._write_show_ser_name() 

3835 

3836 # Write the c:showPercent element. 

3837 if labels.get("percentage"): 

3838 self._write_show_percent() 

3839 

3840 # Write the c:separator element. 

3841 if labels.get("separator"): 

3842 self._write_separator(labels["separator"]) 

3843 

3844 # Write the c:showLeaderLines element. 

3845 if labels.get("leader_lines"): 

3846 self._write_show_leader_lines() 

3847 

3848 self._xml_end_tag("c:dLbls") 

3849 

3850 def _write_custom_labels(self, parent, labels) -> None: 

3851 # Write the <c:showLegendKey> element. 

3852 index = 0 

3853 

3854 for label in labels: 

3855 index += 1 

3856 

3857 if label is None: 

3858 continue 

3859 

3860 use_custom_formatting = True 

3861 

3862 self._xml_start_tag("c:dLbl") 

3863 

3864 # Write the c:idx element. 

3865 self._write_idx(index - 1) 

3866 

3867 delete_label = label.get("delete") 

3868 

3869 if delete_label: 

3870 self._write_delete(1) 

3871 

3872 elif label.get("formula") or label.get("value") or label.get("position"): 

3873 

3874 # Write the c:layout element. 

3875 self._write_layout(None, None) 

3876 

3877 if label.get("formula"): 

3878 self._write_custom_label_formula(label) 

3879 elif label.get("value"): 

3880 self._write_custom_label_str(label) 

3881 # String values use spPr formatting. 

3882 use_custom_formatting = False 

3883 

3884 if use_custom_formatting: 

3885 self._write_custom_label_format(label) 

3886 

3887 if label.get("position"): 

3888 self._write_d_lbl_pos(label["position"]) 

3889 elif parent.get("position"): 

3890 self._write_d_lbl_pos(parent["position"]) 

3891 

3892 if parent.get("value"): 

3893 self._write_show_val() 

3894 

3895 if parent.get("category"): 

3896 self._write_show_cat_name() 

3897 

3898 if parent.get("series_name"): 

3899 self._write_show_ser_name() 

3900 

3901 else: 

3902 self._write_custom_label_format(label) 

3903 

3904 self._xml_end_tag("c:dLbl") 

3905 

3906 def _write_custom_label_str(self, label) -> None: 

3907 # Write parts of the <c:dLbl> element for strings. 

3908 title = label.get("value") 

3909 font = label.get("font") 

3910 has_formatting = self._has_fill_formatting(label) 

3911 

3912 self._xml_start_tag("c:tx") 

3913 

3914 # Write the c:rich element. 

3915 self._write_rich(title, font, False, not has_formatting) 

3916 

3917 self._xml_end_tag("c:tx") 

3918 

3919 # Write the c:spPr element. 

3920 self._write_sp_pr(label) 

3921 

3922 def _write_custom_label_formula(self, label) -> None: 

3923 # Write parts of the <c:dLbl> element for formulas. 

3924 formula = label.get("formula") 

3925 data_id = label.get("data_id") 

3926 data = None 

3927 

3928 if data_id is not None: 

3929 data = self.formula_data[data_id] 

3930 

3931 self._xml_start_tag("c:tx") 

3932 

3933 # Write the c:strRef element. 

3934 self._write_str_ref(formula, data, "str") 

3935 

3936 self._xml_end_tag("c:tx") 

3937 

3938 def _write_custom_label_format(self, label) -> None: 

3939 # Write the formatting and font elements for the custom labels. 

3940 font = label.get("font") 

3941 has_formatting = self._has_fill_formatting(label) 

3942 

3943 if has_formatting: 

3944 self._write_sp_pr(label) 

3945 self._write_tx_pr(font) 

3946 elif font: 

3947 self._xml_empty_tag("c:spPr") 

3948 self._write_tx_pr(font) 

3949 

3950 def _write_show_legend_key(self) -> None: 

3951 # Write the <c:showLegendKey> element. 

3952 val = "1" 

3953 

3954 attributes = [("val", val)] 

3955 

3956 self._xml_empty_tag("c:showLegendKey", attributes) 

3957 

3958 def _write_show_val(self) -> None: 

3959 # Write the <c:showVal> element. 

3960 val = 1 

3961 

3962 attributes = [("val", val)] 

3963 

3964 self._xml_empty_tag("c:showVal", attributes) 

3965 

3966 def _write_show_cat_name(self) -> None: 

3967 # Write the <c:showCatName> element. 

3968 val = 1 

3969 

3970 attributes = [("val", val)] 

3971 

3972 self._xml_empty_tag("c:showCatName", attributes) 

3973 

3974 def _write_show_ser_name(self) -> None: 

3975 # Write the <c:showSerName> element. 

3976 val = 1 

3977 

3978 attributes = [("val", val)] 

3979 

3980 self._xml_empty_tag("c:showSerName", attributes) 

3981 

3982 def _write_show_percent(self) -> None: 

3983 # Write the <c:showPercent> element. 

3984 val = 1 

3985 

3986 attributes = [("val", val)] 

3987 

3988 self._xml_empty_tag("c:showPercent", attributes) 

3989 

3990 def _write_separator(self, data) -> None: 

3991 # Write the <c:separator> element. 

3992 self._xml_data_element("c:separator", data) 

3993 

3994 def _write_show_leader_lines(self) -> None: 

3995 # Write the <c:showLeaderLines> element. 

3996 # 

3997 # This is different for Pie/Doughnut charts. Other chart types only 

3998 # supported leader lines after Excel 2015 via an extension element. 

3999 # 

4000 uri = "{CE6537A1-D6FC-4f65-9D91-7224C49458BB}" 

4001 xmlns_c_15 = "http://schemas.microsoft.com/office/drawing/2012/chart" 

4002 

4003 attributes = [ 

4004 ("uri", uri), 

4005 ("xmlns:c15", xmlns_c_15), 

4006 ] 

4007 

4008 self._xml_start_tag("c:extLst") 

4009 self._xml_start_tag("c:ext", attributes) 

4010 self._xml_empty_tag("c15:showLeaderLines", [("val", 1)]) 

4011 self._xml_end_tag("c:ext") 

4012 self._xml_end_tag("c:extLst") 

4013 

4014 def _write_d_lbl_pos(self, val) -> None: 

4015 # Write the <c:dLblPos> element. 

4016 

4017 attributes = [("val", val)] 

4018 

4019 self._xml_empty_tag("c:dLblPos", attributes) 

4020 

4021 def _write_delete(self, val) -> None: 

4022 # Write the <c:delete> element. 

4023 

4024 attributes = [("val", val)] 

4025 

4026 self._xml_empty_tag("c:delete", attributes) 

4027 

4028 def _write_c_invert_if_negative(self, invert) -> None: 

4029 # Write the <c:invertIfNegative> element. 

4030 val = 1 

4031 

4032 if not invert: 

4033 return 

4034 

4035 attributes = [("val", val)] 

4036 

4037 self._xml_empty_tag("c:invertIfNegative", attributes) 

4038 

4039 def _write_axis_font(self, font) -> None: 

4040 # Write the axis font elements. 

4041 

4042 if not font: 

4043 return 

4044 

4045 self._xml_start_tag("c:txPr") 

4046 self._write_a_body_pr(font.get("rotation"), None) 

4047 self._write_a_lst_style() 

4048 self._xml_start_tag("a:p") 

4049 

4050 self._write_a_p_pr_rich(font) 

4051 

4052 self._write_a_end_para_rpr() 

4053 self._xml_end_tag("a:p") 

4054 self._xml_end_tag("c:txPr") 

4055 

4056 def _write_a_latin(self, attributes) -> None: 

4057 # Write the <a:latin> element. 

4058 self._xml_empty_tag("a:latin", attributes) 

4059 

4060 def _write_d_table(self) -> None: 

4061 # Write the <c:dTable> element. 

4062 table = self.table 

4063 

4064 if not table: 

4065 return 

4066 

4067 self._xml_start_tag("c:dTable") 

4068 

4069 if table["horizontal"]: 

4070 # Write the c:showHorzBorder element. 

4071 self._write_show_horz_border() 

4072 

4073 if table["vertical"]: 

4074 # Write the c:showVertBorder element. 

4075 self._write_show_vert_border() 

4076 

4077 if table["outline"]: 

4078 # Write the c:showOutline element. 

4079 self._write_show_outline() 

4080 

4081 if table["show_keys"]: 

4082 # Write the c:showKeys element. 

4083 self._write_show_keys() 

4084 

4085 if table["font"]: 

4086 # Write the table font. 

4087 self._write_tx_pr(table["font"]) 

4088 

4089 self._xml_end_tag("c:dTable") 

4090 

4091 def _write_show_horz_border(self) -> None: 

4092 # Write the <c:showHorzBorder> element. 

4093 attributes = [("val", 1)] 

4094 

4095 self._xml_empty_tag("c:showHorzBorder", attributes) 

4096 

4097 def _write_show_vert_border(self) -> None: 

4098 # Write the <c:showVertBorder> element. 

4099 attributes = [("val", 1)] 

4100 

4101 self._xml_empty_tag("c:showVertBorder", attributes) 

4102 

4103 def _write_show_outline(self) -> None: 

4104 # Write the <c:showOutline> element. 

4105 attributes = [("val", 1)] 

4106 

4107 self._xml_empty_tag("c:showOutline", attributes) 

4108 

4109 def _write_show_keys(self) -> None: 

4110 # Write the <c:showKeys> element. 

4111 attributes = [("val", 1)] 

4112 

4113 self._xml_empty_tag("c:showKeys", attributes) 

4114 

4115 def _write_error_bars(self, error_bars) -> None: 

4116 # Write the X and Y error bars. 

4117 

4118 if not error_bars: 

4119 return 

4120 

4121 if error_bars["x_error_bars"]: 

4122 self._write_err_bars("x", error_bars["x_error_bars"]) 

4123 

4124 if error_bars["y_error_bars"]: 

4125 self._write_err_bars("y", error_bars["y_error_bars"]) 

4126 

4127 def _write_err_bars(self, direction, error_bars) -> None: 

4128 # Write the <c:errBars> element. 

4129 

4130 if not error_bars: 

4131 return 

4132 

4133 self._xml_start_tag("c:errBars") 

4134 

4135 # Write the c:errDir element. 

4136 self._write_err_dir(direction) 

4137 

4138 # Write the c:errBarType element. 

4139 self._write_err_bar_type(error_bars["direction"]) 

4140 

4141 # Write the c:errValType element. 

4142 self._write_err_val_type(error_bars["type"]) 

4143 

4144 if not error_bars["endcap"]: 

4145 # Write the c:noEndCap element. 

4146 self._write_no_end_cap() 

4147 

4148 if error_bars["type"] == "stdErr": 

4149 # Don't need to write a c:errValType tag. 

4150 pass 

4151 elif error_bars["type"] == "cust": 

4152 # Write the custom error tags. 

4153 self._write_custom_error(error_bars) 

4154 else: 

4155 # Write the c:val element. 

4156 self._write_error_val(error_bars["value"]) 

4157 

4158 # Write the c:spPr element. 

4159 self._write_sp_pr(error_bars) 

4160 

4161 self._xml_end_tag("c:errBars") 

4162 

4163 def _write_err_dir(self, val) -> None: 

4164 # Write the <c:errDir> element. 

4165 

4166 attributes = [("val", val)] 

4167 

4168 self._xml_empty_tag("c:errDir", attributes) 

4169 

4170 def _write_err_bar_type(self, val) -> None: 

4171 # Write the <c:errBarType> element. 

4172 

4173 attributes = [("val", val)] 

4174 

4175 self._xml_empty_tag("c:errBarType", attributes) 

4176 

4177 def _write_err_val_type(self, val) -> None: 

4178 # Write the <c:errValType> element. 

4179 

4180 attributes = [("val", val)] 

4181 

4182 self._xml_empty_tag("c:errValType", attributes) 

4183 

4184 def _write_no_end_cap(self) -> None: 

4185 # Write the <c:noEndCap> element. 

4186 attributes = [("val", 1)] 

4187 

4188 self._xml_empty_tag("c:noEndCap", attributes) 

4189 

4190 def _write_error_val(self, val) -> None: 

4191 # Write the <c:val> element for error bars. 

4192 

4193 attributes = [("val", val)] 

4194 

4195 self._xml_empty_tag("c:val", attributes) 

4196 

4197 def _write_custom_error(self, error_bars) -> None: 

4198 # Write the custom error bars tags. 

4199 

4200 if error_bars["plus_values"]: 

4201 # Write the c:plus element. 

4202 self._xml_start_tag("c:plus") 

4203 

4204 if isinstance(error_bars["plus_values"], list): 

4205 self._write_num_lit(error_bars["plus_values"]) 

4206 else: 

4207 self._write_num_ref( 

4208 error_bars["plus_values"], error_bars["plus_data"], "num" 

4209 ) 

4210 self._xml_end_tag("c:plus") 

4211 

4212 if error_bars["minus_values"]: 

4213 # Write the c:minus element. 

4214 self._xml_start_tag("c:minus") 

4215 

4216 if isinstance(error_bars["minus_values"], list): 

4217 self._write_num_lit(error_bars["minus_values"]) 

4218 else: 

4219 self._write_num_ref( 

4220 error_bars["minus_values"], error_bars["minus_data"], "num" 

4221 ) 

4222 self._xml_end_tag("c:minus") 

4223 

4224 def _write_num_lit(self, data) -> None: 

4225 # Write the <c:numLit> element for literal number list elements. 

4226 count = len(data) 

4227 

4228 # Write the c:numLit element. 

4229 self._xml_start_tag("c:numLit") 

4230 

4231 # Write the c:formatCode element. 

4232 self._write_format_code("General") 

4233 

4234 # Write the c:ptCount element. 

4235 self._write_pt_count(count) 

4236 

4237 for i in range(count): 

4238 token = data[i] 

4239 

4240 if token is None: 

4241 continue 

4242 

4243 try: 

4244 float(token) 

4245 except ValueError: 

4246 # Write non-numeric data as 0. 

4247 token = 0 

4248 

4249 # Write the c:pt element. 

4250 self._write_pt(i, token) 

4251 

4252 self._xml_end_tag("c:numLit") 

4253 

4254 def _write_up_down_bars(self) -> None: 

4255 # Write the <c:upDownBars> element. 

4256 up_down_bars = self.up_down_bars 

4257 

4258 if up_down_bars is None: 

4259 return 

4260 

4261 self._xml_start_tag("c:upDownBars") 

4262 

4263 # Write the c:gapWidth element. 

4264 self._write_gap_width(150) 

4265 

4266 # Write the c:upBars element. 

4267 self._write_up_bars(up_down_bars.get("up")) 

4268 

4269 # Write the c:downBars element. 

4270 self._write_down_bars(up_down_bars.get("down")) 

4271 

4272 self._xml_end_tag("c:upDownBars") 

4273 

4274 def _write_gap_width(self, val) -> None: 

4275 # Write the <c:gapWidth> element. 

4276 

4277 if val is None: 

4278 return 

4279 

4280 attributes = [("val", val)] 

4281 

4282 self._xml_empty_tag("c:gapWidth", attributes) 

4283 

4284 def _write_up_bars(self, bar_format) -> None: 

4285 # Write the <c:upBars> element. 

4286 

4287 if bar_format["line"] and bar_format["line"]["defined"]: 

4288 self._xml_start_tag("c:upBars") 

4289 

4290 # Write the c:spPr element. 

4291 self._write_sp_pr(bar_format) 

4292 

4293 self._xml_end_tag("c:upBars") 

4294 else: 

4295 self._xml_empty_tag("c:upBars") 

4296 

4297 def _write_down_bars(self, bar_format) -> None: 

4298 # Write the <c:downBars> element. 

4299 

4300 if bar_format["line"] and bar_format["line"]["defined"]: 

4301 self._xml_start_tag("c:downBars") 

4302 

4303 # Write the c:spPr element. 

4304 self._write_sp_pr(bar_format) 

4305 

4306 self._xml_end_tag("c:downBars") 

4307 else: 

4308 self._xml_empty_tag("c:downBars") 

4309 

4310 def _write_disp_units(self, units, display) -> None: 

4311 # Write the <c:dispUnits> element. 

4312 

4313 if not units: 

4314 return 

4315 

4316 attributes = [("val", units)] 

4317 

4318 self._xml_start_tag("c:dispUnits") 

4319 self._xml_empty_tag("c:builtInUnit", attributes) 

4320 

4321 if display: 

4322 self._xml_start_tag("c:dispUnitsLbl") 

4323 self._xml_empty_tag("c:layout") 

4324 self._xml_end_tag("c:dispUnitsLbl") 

4325 

4326 self._xml_end_tag("c:dispUnits") 

4327 

4328 def _write_a_grad_fill(self, gradient) -> None: 

4329 # Write the <a:gradFill> element. 

4330 

4331 attributes = [("flip", "none"), ("rotWithShape", "1")] 

4332 

4333 if gradient["type"] == "linear": 

4334 attributes = [] 

4335 

4336 self._xml_start_tag("a:gradFill", attributes) 

4337 

4338 # Write the a:gsLst element. 

4339 self._write_a_gs_lst(gradient) 

4340 

4341 if gradient["type"] == "linear": 

4342 # Write the a:lin element. 

4343 self._write_a_lin(gradient["angle"]) 

4344 else: 

4345 # Write the a:path element. 

4346 self._write_a_path(gradient["type"]) 

4347 

4348 # Write the a:tileRect element. 

4349 self._write_a_tile_rect(gradient["type"]) 

4350 

4351 self._xml_end_tag("a:gradFill") 

4352 

4353 def _write_a_gs_lst(self, gradient) -> None: 

4354 # Write the <a:gsLst> element. 

4355 positions = gradient["positions"] 

4356 colors = gradient["colors"] 

4357 

4358 self._xml_start_tag("a:gsLst") 

4359 

4360 for i, color in enumerate(colors): 

4361 pos = int(positions[i] * 1000) 

4362 attributes = [("pos", pos)] 

4363 self._xml_start_tag("a:gs", attributes) 

4364 

4365 self._write_color(color) 

4366 

4367 self._xml_end_tag("a:gs") 

4368 

4369 self._xml_end_tag("a:gsLst") 

4370 

4371 def _write_a_lin(self, angle) -> None: 

4372 # Write the <a:lin> element. 

4373 

4374 angle = int(60000 * angle) 

4375 

4376 attributes = [ 

4377 ("ang", angle), 

4378 ("scaled", "0"), 

4379 ] 

4380 

4381 self._xml_empty_tag("a:lin", attributes) 

4382 

4383 def _write_a_path(self, gradient_type) -> None: 

4384 # Write the <a:path> element. 

4385 

4386 attributes = [("path", gradient_type)] 

4387 

4388 self._xml_start_tag("a:path", attributes) 

4389 

4390 # Write the a:fillToRect element. 

4391 self._write_a_fill_to_rect(gradient_type) 

4392 

4393 self._xml_end_tag("a:path") 

4394 

4395 def _write_a_fill_to_rect(self, gradient_type) -> None: 

4396 # Write the <a:fillToRect> element. 

4397 

4398 if gradient_type == "shape": 

4399 attributes = [ 

4400 ("l", "50000"), 

4401 ("t", "50000"), 

4402 ("r", "50000"), 

4403 ("b", "50000"), 

4404 ] 

4405 else: 

4406 attributes = [ 

4407 ("l", "100000"), 

4408 ("t", "100000"), 

4409 ] 

4410 

4411 self._xml_empty_tag("a:fillToRect", attributes) 

4412 

4413 def _write_a_tile_rect(self, gradient_type) -> None: 

4414 # Write the <a:tileRect> element. 

4415 

4416 if gradient_type == "shape": 

4417 attributes = [] 

4418 else: 

4419 attributes = [ 

4420 ("r", "-100000"), 

4421 ("b", "-100000"), 

4422 ] 

4423 

4424 self._xml_empty_tag("a:tileRect", attributes) 

4425 

4426 def _write_a_patt_fill(self, pattern) -> None: 

4427 # Write the <a:pattFill> element. 

4428 

4429 attributes = [("prst", pattern["pattern"])] 

4430 

4431 self._xml_start_tag("a:pattFill", attributes) 

4432 

4433 # Write the a:fgClr element. 

4434 self._write_a_fg_clr(pattern["fg_color"]) 

4435 

4436 # Write the a:bgClr element. 

4437 self._write_a_bg_clr(pattern["bg_color"]) 

4438 

4439 self._xml_end_tag("a:pattFill") 

4440 

4441 def _write_a_fg_clr(self, color: Color) -> None: 

4442 # Write the <a:fgClr> element. 

4443 self._xml_start_tag("a:fgClr") 

4444 self._write_color(color) 

4445 self._xml_end_tag("a:fgClr") 

4446 

4447 def _write_a_bg_clr(self, color: Color) -> None: 

4448 # Write the <a:bgClr> element. 

4449 self._xml_start_tag("a:bgClr") 

4450 self._write_color(color) 

4451 self._xml_end_tag("a:bgClr")