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

1992 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 .chart_title import ChartTitle 

19from .shape import Shape 

20from .utility import ( 

21 _datetime_to_excel_datetime, 

22 _supported_datetime, 

23 quote_sheetname, 

24 xl_range_formula, 

25 xl_rowcol_to_cell, 

26) 

27 

28 

29class Chart(xmlwriter.XMLwriter): 

30 """ 

31 A class for writing the Excel XLSX Chart file. 

32 

33 

34 """ 

35 

36 ########################################################################### 

37 # 

38 # Public API. 

39 # 

40 ########################################################################### 

41 

42 def __init__(self) -> None: 

43 """ 

44 Constructor. 

45 

46 """ 

47 

48 super().__init__() 

49 

50 self.subtype = None 

51 self.sheet_type = 0x0200 

52 self.orientation = 0x0 

53 self.series = [] 

54 self.embedded = 0 

55 self.id = -1 

56 self.series_index = 0 

57 self.style_id = 2 

58 self.axis_ids = [] 

59 self.axis2_ids = [] 

60 self.cat_has_num_fmt = False 

61 self.requires_category = False 

62 self.legend = {} 

63 self.cat_axis_position = "b" 

64 self.val_axis_position = "l" 

65 self.formula_ids = {} 

66 self.formula_data = [] 

67 self.horiz_cat_axis = 0 

68 self.horiz_val_axis = 1 

69 self.protection = 0 

70 self.chartarea = {} 

71 self.plotarea = {} 

72 self.x_axis = {} 

73 self.y_axis = {} 

74 self.y2_axis = {} 

75 self.x2_axis = {} 

76 self.chart_name = "" 

77 self.show_blanks = "gap" 

78 self.show_na_as_empty = False 

79 self.show_hidden = False 

80 self.show_crosses = True 

81 self.width = 480 

82 self.height = 288 

83 self.x_scale = 1 

84 self.y_scale = 1 

85 self.x_offset = 0 

86 self.y_offset = 0 

87 self.table = None 

88 self.cross_between = "between" 

89 self.default_marker = None 

90 self.series_gap_1 = None 

91 self.series_gap_2 = None 

92 self.series_overlap_1 = None 

93 self.series_overlap_2 = None 

94 self.drop_lines = None 

95 self.hi_low_lines = None 

96 self.up_down_bars = None 

97 self.smooth_allowed = False 

98 self.title = ChartTitle() 

99 

100 self.date_category = False 

101 self.date_1904 = False 

102 self.remove_timezone = False 

103 self.label_positions = {} 

104 self.label_position_default = "" 

105 self.already_inserted = False 

106 self.combined = None 

107 self.is_secondary = False 

108 self.warn_sheetname = True 

109 self._set_default_properties() 

110 self.fill = {} 

111 

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

113 """ 

114 Add a data series to a chart. 

115 

116 Args: 

117 options: A dictionary of chart series options. 

118 

119 Returns: 

120 Nothing. 

121 

122 """ 

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

124 if options is None: 

125 options = {} 

126 

127 # Check that the required input has been specified. 

128 if "values" not in options: 

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

130 return 

131 

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

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

134 return 

135 

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

137 warn( 

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

139 "Excel Chart is 255" 

140 ) 

141 return 

142 

143 # Convert list into a formula string. 

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

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

146 

147 # Switch name and name_formula parameters if required. 

148 name, name_formula = self._process_names( 

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

150 ) 

151 

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

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

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

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

156 

157 # Set the line properties for the series. 

158 line = Shape._get_line_properties(options) 

159 

160 # Set the fill properties for the series. 

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

162 

163 # Set the pattern fill properties for the series. 

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

165 

166 # Set the gradient fill properties for the series. 

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

168 

169 # Pattern fill overrides solid fill. 

170 if pattern: 

171 self.fill = None 

172 

173 # Gradient fill overrides the solid and pattern fill. 

174 if gradient: 

175 pattern = None 

176 fill = None 

177 

178 # Set the marker properties for the series. 

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

180 

181 # Set the trendline properties for the series. 

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

183 

184 # Set the line smooth property for the series. 

185 smooth = options.get("smooth") 

186 

187 # Set the error bars properties for the series. 

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

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

190 

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

192 

193 # Set the point properties for the series. 

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

195 

196 # Set the labels properties for the series. 

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

198 

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

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

201 inverted_color = options.get("invert_if_negative_color") 

202 

203 if inverted_color: 

204 inverted_color = Color._from_value(inverted_color) 

205 

206 # Set the secondary axis properties. 

207 x2_axis = options.get("x2_axis") 

208 y2_axis = options.get("y2_axis") 

209 

210 # Store secondary status for combined charts. 

211 if x2_axis or y2_axis: 

212 self.is_secondary = True 

213 

214 # Set the gap for Bar/Column charts. 

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

216 if y2_axis: 

217 self.series_gap_2 = options["gap"] 

218 else: 

219 self.series_gap_1 = options["gap"] 

220 

221 # Set the overlap for Bar/Column charts. 

222 if options.get("overlap"): 

223 if y2_axis: 

224 self.series_overlap_2 = options["overlap"] 

225 else: 

226 self.series_overlap_1 = options["overlap"] 

227 

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

229 series = { 

230 "values": values, 

231 "categories": categories, 

232 "name": name, 

233 "name_formula": name_formula, 

234 "name_id": name_id, 

235 "val_data_id": val_id, 

236 "cat_data_id": cat_id, 

237 "line": line, 

238 "fill": fill, 

239 "pattern": pattern, 

240 "gradient": gradient, 

241 "marker": marker, 

242 "trendline": trendline, 

243 "labels": labels, 

244 "invert_if_neg": invert_if_neg, 

245 "inverted_color": inverted_color, 

246 "x2_axis": x2_axis, 

247 "y2_axis": y2_axis, 

248 "points": points, 

249 "error_bars": error_bars, 

250 "smooth": smooth, 

251 } 

252 

253 self.series.append(series) 

254 

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

256 """ 

257 Set the chart X axis options. 

258 

259 Args: 

260 options: A dictionary of axis options. 

261 

262 Returns: 

263 Nothing. 

264 

265 """ 

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

267 

268 self.x_axis = axis 

269 

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

271 """ 

272 Set the chart Y axis options. 

273 

274 Args: 

275 options: A dictionary of axis options. 

276 

277 Returns: 

278 Nothing. 

279 

280 """ 

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

282 

283 self.y_axis = axis 

284 

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

286 """ 

287 Set the chart secondary X axis options. 

288 

289 Args: 

290 options: A dictionary of axis options. 

291 

292 Returns: 

293 Nothing. 

294 

295 """ 

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

297 

298 self.x2_axis = axis 

299 

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

301 """ 

302 Set the chart secondary Y axis options. 

303 

304 Args: 

305 options: A dictionary of axis options. 

306 

307 Returns: 

308 Nothing. 

309 

310 """ 

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

312 

313 self.y2_axis = axis 

314 

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

316 """ 

317 Set the chart title options. 

318 

319 Args: 

320 options: A dictionary of chart title options. 

321 

322 Returns: 

323 Nothing. 

324 

325 """ 

326 if options is None: 

327 options = {} 

328 

329 name, name_formula = self._process_names( 

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

331 ) 

332 

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

334 

335 # Update the main chart title. 

336 self.title.name = name 

337 self.title.formula = name_formula 

338 self.title.data_id = data_id 

339 

340 # Set the font properties if present. 

341 if options.get("font"): 

342 self.title.font = self._convert_font_args(options.get("font")) 

343 else: 

344 # For backward/axis compatibility. 

345 self.title.font = self._convert_font_args(options.get("name_font")) 

346 

347 # Set the line properties. 

348 self.title.line = Shape._get_line_properties(options) 

349 

350 # Set the fill properties. 

351 self.title.fill = Shape._get_fill_properties(options.get("fill")) 

352 

353 # Set the gradient properties. 

354 self.title.gradient = Shape._get_gradient_properties(options.get("gradient")) 

355 

356 # Set the layout. 

357 self.title.layout = self._get_layout_properties(options.get("layout"), True) 

358 

359 # Set the title overlay option. 

360 self.title.overlay = options.get("overlay") 

361 

362 # Set the automatic title option. 

363 self.title.hidden = options.get("none", False) 

364 

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

366 """ 

367 Set the chart legend options. 

368 

369 Args: 

370 options: A dictionary of chart legend options. 

371 

372 Returns: 

373 Nothing. 

374 """ 

375 # Convert the user defined properties to internal properties. 

376 self.legend = self._get_legend_properties(options) 

377 

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

379 """ 

380 Set the chart plot area options. 

381 

382 Args: 

383 options: A dictionary of chart plot area options. 

384 

385 Returns: 

386 Nothing. 

387 """ 

388 # Convert the user defined properties to internal properties. 

389 self.plotarea = self._get_area_properties(options) 

390 

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

392 """ 

393 Set the chart area options. 

394 

395 Args: 

396 options: A dictionary of chart area options. 

397 

398 Returns: 

399 Nothing. 

400 """ 

401 # Convert the user defined properties to internal properties. 

402 self.chartarea = self._get_area_properties(options) 

403 

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

405 """ 

406 Set the chart style type. 

407 

408 Args: 

409 style_id: An int representing the chart style. 

410 

411 Returns: 

412 Nothing. 

413 """ 

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

415 if style_id is None: 

416 style_id = 2 

417 

418 if style_id < 1 or style_id > 48: 

419 style_id = 2 

420 

421 self.style_id = style_id 

422 

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

424 """ 

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

426 

427 Args: 

428 option: A string representing the display option. 

429 

430 Returns: 

431 Nothing. 

432 """ 

433 if not option: 

434 return 

435 

436 valid_options = { 

437 "gap": 1, 

438 "zero": 1, 

439 "span": 1, 

440 } 

441 

442 if option not in valid_options: 

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

444 return 

445 

446 self.show_blanks = option 

447 

448 def show_na_as_empty_cell(self) -> None: 

449 """ 

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

451 

452 Args: 

453 None. 

454 

455 Returns: 

456 Nothing. 

457 """ 

458 self.show_na_as_empty = True 

459 

460 def show_hidden_data(self) -> None: 

461 """ 

462 Display data on charts from hidden rows or columns. 

463 

464 Args: 

465 None. 

466 

467 Returns: 

468 Nothing. 

469 """ 

470 self.show_hidden = True 

471 

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

473 """ 

474 Set size or scale of the chart. 

475 

476 Args: 

477 options: A dictionary of chart size options. 

478 

479 Returns: 

480 Nothing. 

481 """ 

482 if options is None: 

483 options = {} 

484 

485 # Set dimensions or scale for the chart. 

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

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

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

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

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

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

492 

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

494 """ 

495 Set properties for an axis data table. 

496 

497 Args: 

498 options: A dictionary of axis table options. 

499 

500 Returns: 

501 Nothing. 

502 

503 """ 

504 if options is None: 

505 options = {} 

506 

507 table = {} 

508 

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

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

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

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

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

514 

515 self.table = table 

516 

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

518 """ 

519 Set properties for the chart up-down bars. 

520 

521 Args: 

522 options: A dictionary of options. 

523 

524 Returns: 

525 Nothing. 

526 

527 """ 

528 if options is None: 

529 options = {} 

530 

531 # Defaults. 

532 up_line = None 

533 up_fill = None 

534 down_line = None 

535 down_fill = None 

536 

537 # Set properties for 'up' bar. 

538 if options.get("up"): 

539 up_line = Shape._get_line_properties(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 down_line = Shape._get_line_properties(options["down"]) 

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

546 

547 self.up_down_bars = { 

548 "up": { 

549 "line": up_line, 

550 "fill": up_fill, 

551 }, 

552 "down": { 

553 "line": down_line, 

554 "fill": down_fill, 

555 }, 

556 } 

557 

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

559 """ 

560 Set properties for the chart drop lines. 

561 

562 Args: 

563 options: A dictionary of options. 

564 

565 Returns: 

566 Nothing. 

567 

568 """ 

569 if options is None: 

570 options = {} 

571 

572 line = Shape._get_line_properties(options) 

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

574 

575 # Set the pattern fill properties for the series. 

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

577 

578 # Set the gradient fill properties for the series. 

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

580 

581 # Pattern fill overrides solid fill. 

582 if pattern: 

583 self.fill = None 

584 

585 # Gradient fill overrides the solid and pattern fill. 

586 if gradient: 

587 pattern = None 

588 fill = None 

589 

590 self.drop_lines = { 

591 "line": line, 

592 "fill": fill, 

593 "pattern": pattern, 

594 "gradient": gradient, 

595 } 

596 

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

598 """ 

599 Set properties for the chart high-low lines. 

600 

601 Args: 

602 options: A dictionary of options. 

603 

604 Returns: 

605 Nothing. 

606 

607 """ 

608 if options is None: 

609 options = {} 

610 

611 line = Shape._get_line_properties(options) 

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

613 

614 # Set the pattern fill properties for the series. 

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

616 

617 # Set the gradient fill properties for the series. 

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

619 

620 # Pattern fill overrides solid fill. 

621 if pattern: 

622 self.fill = None 

623 

624 # Gradient fill overrides the solid and pattern fill. 

625 if gradient: 

626 pattern = None 

627 fill = None 

628 

629 self.hi_low_lines = { 

630 "line": line, 

631 "fill": fill, 

632 "pattern": pattern, 

633 "gradient": gradient, 

634 } 

635 

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

637 """ 

638 Create a combination chart with a secondary chart. 

639 

640 Args: 

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

642 

643 Returns: 

644 Nothing. 

645 

646 """ 

647 if chart is None: 

648 return 

649 

650 self.combined = chart 

651 

652 ########################################################################### 

653 # 

654 # Private API. 

655 # 

656 ########################################################################### 

657 

658 def _assemble_xml_file(self) -> None: 

659 # Assemble and write the XML file. 

660 

661 # Write the XML declaration. 

662 self._xml_declaration() 

663 

664 # Write the c:chartSpace element. 

665 self._write_chart_space() 

666 

667 # Write the c:lang element. 

668 self._write_lang() 

669 

670 # Write the c:style element. 

671 self._write_style() 

672 

673 # Write the c:protection element. 

674 self._write_protection() 

675 

676 # Write the c:chart element. 

677 self._write_chart() 

678 

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

680 self._write_sp_pr(self.chartarea) 

681 

682 # Write the c:printSettings element. 

683 if self.embedded: 

684 self._write_print_settings() 

685 

686 # Close the worksheet tag. 

687 self._xml_end_tag("c:chartSpace") 

688 # Close the file. 

689 self._xml_close() 

690 

691 def _convert_axis_args(self, axis, user_options): 

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

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

694 options.update(user_options) 

695 

696 axis = { 

697 "defaults": axis["defaults"], 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

716 "text_axis": False, 

717 "title": ChartTitle(), 

718 } 

719 

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

721 

722 # Convert the display units. 

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

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

725 

726 # Map major_gridlines properties. 

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

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

729 options["major_gridlines"] 

730 ) 

731 

732 # Map minor_gridlines properties. 

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

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

735 options["minor_gridlines"] 

736 ) 

737 

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

739 if axis.get("position"): 

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

741 

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

743 if axis.get("position_axis"): 

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

745 axis["position_axis"] = "midCat" 

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

747 # Doesn't need to be modified. 

748 pass 

749 else: 

750 # Otherwise use the default value. 

751 axis["position_axis"] = None 

752 

753 # Set the category axis as a date axis. 

754 if options.get("date_axis"): 

755 self.date_category = True 

756 

757 # Set the category axis as a text axis. 

758 if options.get("text_axis"): 

759 self.date_category = False 

760 axis["text_axis"] = True 

761 

762 # Convert datetime args if required. 

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

764 axis["min"] = _datetime_to_excel_datetime( 

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

766 ) 

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

768 axis["max"] = _datetime_to_excel_datetime( 

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

770 ) 

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

772 axis["crossing"] = _datetime_to_excel_datetime( 

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

774 ) 

775 

776 # Set the font properties if present. 

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

778 

779 # Set the line properties for the axis. 

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

781 

782 # Set the fill properties for the axis. 

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

784 

785 # Set the pattern fill properties for the series. 

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

787 

788 # Set the gradient fill properties for the series. 

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

790 

791 # Pattern fill overrides solid fill. 

792 if axis.get("pattern"): 

793 axis["fill"] = None 

794 

795 # Gradient fill overrides the solid and pattern fill. 

796 if axis.get("gradient"): 

797 axis["pattern"] = None 

798 axis["fill"] = None 

799 

800 # Set the tick marker types. 

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

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

803 

804 # Check if the axis title is simple text or a formula. 

805 name, name_formula = self._process_names( 

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

807 ) 

808 

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

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

811 

812 # Set the title properties. 

813 axis["title"].name = name 

814 axis["title"].formula = name_formula 

815 axis["title"].data_id = data_id 

816 axis["title"].font = self._convert_font_args(options.get("name_font")) 

817 axis["title"].layout = self._get_layout_properties( 

818 options.get("name_layout"), True 

819 ) 

820 

821 # Map the line and border properties for the axis title. 

822 options["line"] = options.get("name_line") 

823 options["border"] = options.get("name_border") 

824 

825 axis["title"].line = Shape._get_line_properties(options) 

826 axis["title"].fill = Shape._get_fill_properties(options.get("name_fill")) 

827 axis["title"].pattern = Shape._get_pattern_properties( 

828 options.get("name_pattern") 

829 ) 

830 axis["title"].gradient = Shape._get_gradient_properties( 

831 options.get("name_gradient") 

832 ) 

833 

834 return axis 

835 

836 def _convert_font_args(self, options): 

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

838 if not options: 

839 return {} 

840 

841 font = { 

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

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

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

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

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

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

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

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

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

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

852 } 

853 

854 # Convert font size units. 

855 if font["size"]: 

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

857 

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

859 if font["rotation"]: 

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

861 

862 if font.get("color"): 

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

864 

865 return font 

866 

867 def _list_to_formula(self, data): 

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

869 

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

871 if not isinstance(data, list): 

872 # Check for unquoted sheetnames. 

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

874 warn( 

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

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

877 ) 

878 return data 

879 

880 formula = xl_range_formula(*data) 

881 

882 return formula 

883 

884 def _process_names(self, name, name_formula): 

885 # Switch name and name_formula parameters if required. 

886 

887 if name is not None: 

888 if isinstance(name, list): 

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

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

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

892 name = "" 

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

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

895 name_formula = name 

896 name = "" 

897 

898 return name, name_formula 

899 

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

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

902 

903 # Check for no data in the series. 

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

905 return "none" 

906 

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

908 return "multi_str" 

909 

910 # Determine if data is numeric or strings. 

911 for token in data: 

912 if token is None: 

913 continue 

914 

915 # Check for strings that would evaluate to float like 

916 # '1.1_1' of ' 1'. 

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

918 # Assume entire data series is string data. 

919 return "str" 

920 

921 try: 

922 float(token) 

923 except ValueError: 

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

925 return "str" 

926 

927 # The series data was all numeric. 

928 return "num" 

929 

930 def _get_data_id(self, formula, data): 

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

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

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

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

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

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

937 

938 # Ignore series without a range formula. 

939 if not formula: 

940 return None 

941 

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

943 if formula.startswith("="): 

944 formula = formula.lstrip("=") 

945 

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

947 # in a separate array with the same id. 

948 if formula not in self.formula_ids: 

949 # Haven't seen this formula before. 

950 formula_id = len(self.formula_data) 

951 

952 self.formula_data.append(data) 

953 self.formula_ids[formula] = formula_id 

954 else: 

955 # Formula already seen. Return existing id. 

956 formula_id = self.formula_ids[formula] 

957 

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

959 if self.formula_data[formula_id] is None: 

960 self.formula_data[formula_id] = data 

961 

962 return formula_id 

963 

964 def _get_marker_properties(self, marker): 

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

966 

967 if not marker: 

968 return None 

969 

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

971 marker = copy.deepcopy(marker) 

972 

973 types = { 

974 "automatic": "automatic", 

975 "none": "none", 

976 "square": "square", 

977 "diamond": "diamond", 

978 "triangle": "triangle", 

979 "x": "x", 

980 "star": "star", 

981 "dot": "dot", 

982 "short_dash": "dot", 

983 "dash": "dash", 

984 "long_dash": "dash", 

985 "circle": "circle", 

986 "plus": "plus", 

987 "picture": "picture", 

988 } 

989 

990 # Check for valid types. 

991 marker_type = marker.get("type") 

992 

993 if marker_type is not None: 

994 if marker_type in types: 

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

996 else: 

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

998 return None 

999 

1000 # Set the line properties for the marker. 

1001 line = Shape._get_line_properties(marker) 

1002 

1003 # Set the fill properties for the marker. 

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

1005 

1006 # Set the pattern fill properties for the series. 

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

1008 

1009 # Set the gradient fill properties for the series. 

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

1011 

1012 # Pattern fill overrides solid fill. 

1013 if pattern: 

1014 self.fill = None 

1015 

1016 # Gradient fill overrides the solid and pattern fill. 

1017 if gradient: 

1018 pattern = None 

1019 fill = None 

1020 

1021 marker["line"] = line 

1022 marker["fill"] = fill 

1023 marker["pattern"] = pattern 

1024 marker["gradient"] = gradient 

1025 

1026 return marker 

1027 

1028 def _get_trendline_properties(self, trendline): 

1029 # Convert user trendline properties to structure required internally. 

1030 

1031 if not trendline: 

1032 return None 

1033 

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

1035 trendline = copy.deepcopy(trendline) 

1036 

1037 types = { 

1038 "exponential": "exp", 

1039 "linear": "linear", 

1040 "log": "log", 

1041 "moving_average": "movingAvg", 

1042 "polynomial": "poly", 

1043 "power": "power", 

1044 } 

1045 

1046 # Check the trendline type. 

1047 trend_type = trendline.get("type") 

1048 

1049 if trend_type in types: 

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

1051 else: 

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

1053 return None 

1054 

1055 # Set the line properties for the trendline. 

1056 line = Shape._get_line_properties(trendline) 

1057 

1058 # Set the fill properties for the trendline. 

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

1060 

1061 # Set the pattern fill properties for the trendline. 

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

1063 

1064 # Set the gradient fill properties for the trendline. 

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

1066 

1067 # Set the format properties for the trendline label. 

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

1069 

1070 # Pattern fill overrides solid fill. 

1071 if pattern: 

1072 self.fill = None 

1073 

1074 # Gradient fill overrides the solid and pattern fill. 

1075 if gradient: 

1076 pattern = None 

1077 fill = None 

1078 

1079 trendline["line"] = line 

1080 trendline["fill"] = fill 

1081 trendline["pattern"] = pattern 

1082 trendline["gradient"] = gradient 

1083 trendline["label"] = label 

1084 

1085 return trendline 

1086 

1087 def _get_trendline_label_properties(self, label): 

1088 # Convert user trendline properties to structure required internally. 

1089 

1090 if not label: 

1091 return {} 

1092 

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

1094 label = copy.deepcopy(label) 

1095 

1096 # Set the font properties if present. 

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

1098 

1099 # Set the line properties for the label. 

1100 line = Shape._get_line_properties(label) 

1101 

1102 # Set the fill properties for the label. 

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

1104 

1105 # Set the pattern fill properties for the label. 

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

1107 

1108 # Set the gradient fill properties for the label. 

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

1110 

1111 # Pattern fill overrides solid fill. 

1112 if pattern: 

1113 self.fill = None 

1114 

1115 # Gradient fill overrides the solid and pattern fill. 

1116 if gradient: 

1117 pattern = None 

1118 fill = None 

1119 

1120 label["font"] = font 

1121 label["line"] = line 

1122 label["fill"] = fill 

1123 label["pattern"] = pattern 

1124 label["gradient"] = gradient 

1125 

1126 return label 

1127 

1128 def _get_error_bars_props(self, options): 

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

1130 if not options: 

1131 return {} 

1132 

1133 # Default values. 

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

1135 

1136 types = { 

1137 "fixed": "fixedVal", 

1138 "percentage": "percentage", 

1139 "standard_deviation": "stdDev", 

1140 "standard_error": "stdErr", 

1141 "custom": "cust", 

1142 } 

1143 

1144 # Check the error bars type. 

1145 error_type = options["type"] 

1146 

1147 if error_type in types: 

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

1149 else: 

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

1151 return {} 

1152 

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

1154 if "value" in options: 

1155 error_bars["value"] = options["value"] 

1156 

1157 # Set the end-cap style. 

1158 if "end_style" in options: 

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

1160 

1161 # Set the error bar direction. 

1162 if "direction" in options: 

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

1164 error_bars["direction"] = "minus" 

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

1166 error_bars["direction"] = "plus" 

1167 else: 

1168 # Default to 'both'. 

1169 pass 

1170 

1171 # Set any custom values. 

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

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

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

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

1176 

1177 # Set the line properties for the error bars. 

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

1179 

1180 return error_bars 

1181 

1182 def _get_gridline_properties(self, options): 

1183 # Convert user gridline properties to structure required internally. 

1184 

1185 # Set the visible property for the gridline. 

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

1187 

1188 # Set the line properties for the gridline. 

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

1190 

1191 return gridline 

1192 

1193 def _get_labels_properties(self, labels): 

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

1195 

1196 if not labels: 

1197 return None 

1198 

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

1200 labels = copy.deepcopy(labels) 

1201 

1202 # Map user defined label positions to Excel positions. 

1203 position = labels.get("position") 

1204 

1205 if position: 

1206 if position in self.label_positions: 

1207 if position == self.label_position_default: 

1208 labels["position"] = None 

1209 else: 

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

1211 else: 

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

1213 return None 

1214 

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

1216 separator = labels.get("separator") 

1217 separators = { 

1218 ",": ", ", 

1219 ";": "; ", 

1220 ".": ". ", 

1221 "\n": "\n", 

1222 " ": " ", 

1223 } 

1224 

1225 if separator: 

1226 if separator in separators: 

1227 labels["separator"] = separators[separator] 

1228 else: 

1229 warn("Unsupported label separator") 

1230 return None 

1231 

1232 # Set the font properties if present. 

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

1234 

1235 # Set the line properties for the labels. 

1236 line = Shape._get_line_properties(labels) 

1237 

1238 # Set the fill properties for the labels. 

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

1240 

1241 # Set the pattern fill properties for the labels. 

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

1243 

1244 # Set the gradient fill properties for the labels. 

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

1246 

1247 # Pattern fill overrides solid fill. 

1248 if pattern: 

1249 self.fill = None 

1250 

1251 # Gradient fill overrides the solid and pattern fill. 

1252 if gradient: 

1253 pattern = None 

1254 fill = None 

1255 

1256 labels["line"] = line 

1257 labels["fill"] = fill 

1258 labels["pattern"] = pattern 

1259 labels["gradient"] = gradient 

1260 

1261 if labels.get("custom"): 

1262 for label in labels["custom"]: 

1263 if label is None: 

1264 continue 

1265 

1266 value = label.get("value") 

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

1268 label["formula"] = value 

1269 

1270 formula = label.get("formula") 

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

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

1273 

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

1275 label["data_id"] = data_id 

1276 

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

1278 

1279 # Set the line properties for the label. 

1280 line = Shape._get_line_properties(label) 

1281 

1282 # Set the fill properties for the label. 

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

1284 

1285 # Set the pattern fill properties for the label. 

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

1287 

1288 # Set the gradient fill properties for the label. 

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

1290 

1291 # Pattern fill overrides solid fill. 

1292 if pattern: 

1293 self.fill = None 

1294 

1295 # Gradient fill overrides the solid and pattern fill. 

1296 if gradient: 

1297 pattern = None 

1298 fill = None 

1299 

1300 # Map user defined label positions to Excel positions. 

1301 position = label.get("position") 

1302 

1303 if position: 

1304 if position in self.label_positions: 

1305 if position == self.label_position_default: 

1306 label["position"] = None 

1307 else: 

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

1309 else: 

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

1311 return None 

1312 

1313 label["line"] = line 

1314 label["fill"] = fill 

1315 label["pattern"] = pattern 

1316 label["gradient"] = gradient 

1317 

1318 return labels 

1319 

1320 def _get_area_properties(self, options): 

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

1322 area = {} 

1323 

1324 # Set the line properties for the chartarea. 

1325 line = Shape._get_line_properties(options) 

1326 

1327 # Set the fill properties for the chartarea. 

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

1329 

1330 # Set the pattern fill properties for the series. 

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

1332 

1333 # Set the gradient fill properties for the series. 

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

1335 

1336 # Pattern fill overrides solid fill. 

1337 if pattern: 

1338 self.fill = None 

1339 

1340 # Gradient fill overrides the solid and pattern fill. 

1341 if gradient: 

1342 pattern = None 

1343 fill = None 

1344 

1345 # Set the plotarea layout. 

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

1347 

1348 area["line"] = line 

1349 area["fill"] = fill 

1350 area["pattern"] = pattern 

1351 area["layout"] = layout 

1352 area["gradient"] = gradient 

1353 

1354 return area 

1355 

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

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

1358 legend = {} 

1359 

1360 if options is None: 

1361 options = {} 

1362 

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

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

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

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

1367 

1368 # Turn off the legend. 

1369 if options.get("none"): 

1370 legend["position"] = "none" 

1371 

1372 # Set the line properties for the legend. 

1373 line = Shape._get_line_properties(options) 

1374 

1375 # Set the fill properties for the legend. 

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

1377 

1378 # Set the pattern fill properties for the series. 

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

1380 

1381 # Set the gradient fill properties for the series. 

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

1383 

1384 # Pattern fill overrides solid fill. 

1385 if pattern: 

1386 self.fill = None 

1387 

1388 # Gradient fill overrides the solid and pattern fill. 

1389 if gradient: 

1390 pattern = None 

1391 fill = None 

1392 

1393 # Set the legend layout. 

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

1395 

1396 legend["line"] = line 

1397 legend["fill"] = fill 

1398 legend["pattern"] = pattern 

1399 legend["layout"] = layout 

1400 legend["gradient"] = gradient 

1401 

1402 return legend 

1403 

1404 def _get_layout_properties(self, args, is_text): 

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

1406 layout = {} 

1407 

1408 if not args: 

1409 return {} 

1410 

1411 if is_text: 

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

1413 else: 

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

1415 

1416 # Check for valid properties. 

1417 for key in args.keys(): 

1418 if key not in properties: 

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

1420 return {} 

1421 

1422 # Set the layout properties. 

1423 for prop in properties: 

1424 if prop not in args.keys(): 

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

1426 return {} 

1427 

1428 value = args[prop] 

1429 

1430 try: 

1431 float(value) 

1432 except ValueError: 

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

1434 return {} 

1435 

1436 if value < 0 or value > 1: 

1437 warn( 

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

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

1440 ) 

1441 return {} 

1442 

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

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

1445 

1446 return layout 

1447 

1448 def _get_points_properties(self, user_points): 

1449 # Convert user points properties to structure required internally. 

1450 points = [] 

1451 

1452 if not user_points: 

1453 return [] 

1454 

1455 for user_point in user_points: 

1456 point = {} 

1457 

1458 if user_point is not None: 

1459 # Set the line properties for the point. 

1460 line = Shape._get_line_properties(user_point) 

1461 

1462 # Set the fill properties for the chartarea. 

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

1464 

1465 # Set the pattern fill properties for the series. 

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

1467 

1468 # Set the gradient fill properties for the series. 

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

1470 

1471 # Pattern fill overrides solid fill. 

1472 if pattern: 

1473 self.fill = None 

1474 

1475 # Gradient fill overrides the solid and pattern fill. 

1476 if gradient: 

1477 pattern = None 

1478 fill = None 

1479 

1480 point["line"] = line 

1481 point["fill"] = fill 

1482 point["pattern"] = pattern 

1483 point["gradient"] = gradient 

1484 

1485 points.append(point) 

1486 

1487 return points 

1488 

1489 def _has_formatting(self, element: dict) -> bool: 

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

1491 has_fill = element.get("fill") and element["fill"]["defined"] 

1492 has_line = element.get("line") and element["line"]["defined"] 

1493 has_pattern = element.get("pattern") 

1494 has_gradient = element.get("gradient") 

1495 

1496 return has_fill or has_line or has_pattern or has_gradient 

1497 

1498 def _get_display_units(self, display_units): 

1499 # Convert user defined display units to internal units. 

1500 if not display_units: 

1501 return None 

1502 

1503 types = { 

1504 "hundreds": "hundreds", 

1505 "thousands": "thousands", 

1506 "ten_thousands": "tenThousands", 

1507 "hundred_thousands": "hundredThousands", 

1508 "millions": "millions", 

1509 "ten_millions": "tenMillions", 

1510 "hundred_millions": "hundredMillions", 

1511 "billions": "billions", 

1512 "trillions": "trillions", 

1513 } 

1514 

1515 if display_units in types: 

1516 display_units = types[display_units] 

1517 else: 

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

1519 return None 

1520 

1521 return display_units 

1522 

1523 def _get_tick_type(self, tick_type): 

1524 # Convert user defined display units to internal units. 

1525 if not tick_type: 

1526 return None 

1527 

1528 types = { 

1529 "outside": "out", 

1530 "inside": "in", 

1531 "none": "none", 

1532 "cross": "cross", 

1533 } 

1534 

1535 if tick_type in types: 

1536 tick_type = types[tick_type] 

1537 else: 

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

1539 return None 

1540 

1541 return tick_type 

1542 

1543 def _get_primary_axes_series(self): 

1544 # Returns series which use the primary axes. 

1545 primary_axes_series = [] 

1546 

1547 for series in self.series: 

1548 if not series["y2_axis"]: 

1549 primary_axes_series.append(series) 

1550 

1551 return primary_axes_series 

1552 

1553 def _get_secondary_axes_series(self): 

1554 # Returns series which use the secondary axes. 

1555 secondary_axes_series = [] 

1556 

1557 for series in self.series: 

1558 if series["y2_axis"]: 

1559 secondary_axes_series.append(series) 

1560 

1561 return secondary_axes_series 

1562 

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

1564 # Add unique ids for primary or secondary axes 

1565 chart_id = 5001 + int(self.id) 

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

1567 

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

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

1570 

1571 if args["primary_axes"]: 

1572 self.axis_ids.append(id1) 

1573 self.axis_ids.append(id2) 

1574 

1575 if not args["primary_axes"]: 

1576 self.axis2_ids.append(id1) 

1577 self.axis2_ids.append(id2) 

1578 

1579 def _set_default_properties(self) -> None: 

1580 # Setup the default properties for a chart. 

1581 

1582 self.x_axis["defaults"] = { 

1583 "num_format": "General", 

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

1585 } 

1586 

1587 self.y_axis["defaults"] = { 

1588 "num_format": "General", 

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

1590 } 

1591 

1592 self.x2_axis["defaults"] = { 

1593 "num_format": "General", 

1594 "label_position": "none", 

1595 "crossing": "max", 

1596 "visible": 0, 

1597 } 

1598 

1599 self.y2_axis["defaults"] = { 

1600 "num_format": "General", 

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

1602 "position": "right", 

1603 "visible": 1, 

1604 } 

1605 

1606 self.set_x_axis({}) 

1607 self.set_y_axis({}) 

1608 

1609 self.set_x2_axis({}) 

1610 self.set_y2_axis({}) 

1611 

1612 ########################################################################### 

1613 # 

1614 # XML methods. 

1615 # 

1616 ########################################################################### 

1617 

1618 def _write_chart_space(self) -> None: 

1619 # Write the <c:chartSpace> element. 

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

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

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

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

1624 

1625 attributes = [ 

1626 ("xmlns:c", xmlns_c), 

1627 ("xmlns:a", xmlns_a), 

1628 ("xmlns:r", xmlns_r), 

1629 ] 

1630 

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

1632 

1633 def _write_lang(self) -> None: 

1634 # Write the <c:lang> element. 

1635 val = "en-US" 

1636 

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

1638 

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

1640 

1641 def _write_style(self) -> None: 

1642 # Write the <c:style> element. 

1643 style_id = self.style_id 

1644 

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

1646 if style_id == 2: 

1647 return 

1648 

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

1650 

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

1652 

1653 def _write_chart(self) -> None: 

1654 # Write the <c:chart> element. 

1655 self._xml_start_tag("c:chart") 

1656 

1657 if self.title.is_hidden(): 

1658 # Turn off the title. 

1659 self._write_c_auto_title_deleted() 

1660 else: 

1661 # Write the chart title elements. 

1662 self._write_title(self.title) 

1663 

1664 # Write the c:plotArea element. 

1665 self._write_plot_area() 

1666 

1667 # Write the c:legend element. 

1668 self._write_legend() 

1669 

1670 # Write the c:plotVisOnly element. 

1671 self._write_plot_vis_only() 

1672 

1673 # Write the c:dispBlanksAs element. 

1674 self._write_disp_blanks_as() 

1675 

1676 # Write the c:extLst element. 

1677 if self.show_na_as_empty: 

1678 self._write_c_ext_lst_display_na() 

1679 

1680 self._xml_end_tag("c:chart") 

1681 

1682 def _write_disp_blanks_as(self) -> None: 

1683 # Write the <c:dispBlanksAs> element. 

1684 val = self.show_blanks 

1685 

1686 # Ignore the default value. 

1687 if val == "gap": 

1688 return 

1689 

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

1691 

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

1693 

1694 def _write_plot_area(self) -> None: 

1695 # Write the <c:plotArea> element. 

1696 self._xml_start_tag("c:plotArea") 

1697 

1698 # Write the c:layout element. 

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

1700 

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

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

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

1704 

1705 # Configure a combined chart if present. 

1706 second_chart = self.combined 

1707 if second_chart: 

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

1709 if second_chart.is_secondary: 

1710 second_chart.id = 1000 + self.id 

1711 else: 

1712 second_chart.id = self.id 

1713 

1714 # Share the same filehandle for writing. 

1715 second_chart.fh = self.fh 

1716 

1717 # Share series index with primary chart. 

1718 second_chart.series_index = self.series_index 

1719 

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

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

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

1723 

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

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

1726 

1727 if self.date_category: 

1728 self._write_date_axis(args) 

1729 else: 

1730 self._write_cat_axis(args) 

1731 

1732 self._write_val_axis(args) 

1733 

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

1735 args = { 

1736 "x_axis": self.x2_axis, 

1737 "y_axis": self.y2_axis, 

1738 "axis_ids": self.axis2_ids, 

1739 } 

1740 

1741 self._write_val_axis(args) 

1742 

1743 # Write the secondary axis for the secondary chart. 

1744 if second_chart and second_chart.is_secondary: 

1745 args = { 

1746 "x_axis": second_chart.x2_axis, 

1747 "y_axis": second_chart.y2_axis, 

1748 "axis_ids": second_chart.axis2_ids, 

1749 } 

1750 

1751 second_chart._write_val_axis(args) 

1752 

1753 if self.date_category: 

1754 self._write_date_axis(args) 

1755 else: 

1756 self._write_cat_axis(args) 

1757 

1758 # Write the c:dTable element. 

1759 self._write_d_table() 

1760 

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

1762 self._write_sp_pr(self.plotarea) 

1763 

1764 self._xml_end_tag("c:plotArea") 

1765 

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

1767 # Write the <c:layout> element. 

1768 

1769 if not layout: 

1770 # Automatic layout. 

1771 self._xml_empty_tag("c:layout") 

1772 else: 

1773 # User defined manual layout. 

1774 self._xml_start_tag("c:layout") 

1775 self._write_manual_layout(layout, layout_type) 

1776 self._xml_end_tag("c:layout") 

1777 

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

1779 # Write the <c:manualLayout> element. 

1780 self._xml_start_tag("c:manualLayout") 

1781 

1782 # Plotarea has a layoutTarget element. 

1783 if layout_type == "plot": 

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

1785 

1786 # Set the x, y positions. 

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

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

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

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

1791 

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

1793 if layout_type != "text": 

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

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

1796 

1797 self._xml_end_tag("c:manualLayout") 

1798 

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

1800 # pylint: disable=unused-argument 

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

1802 # by the subclasses. 

1803 return 

1804 

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

1806 # Write the <c:grouping> element. 

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

1808 

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

1810 

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

1812 # Write the series elements. 

1813 self._write_ser(series) 

1814 

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

1816 # Write the <c:ser> element. 

1817 index = self.series_index 

1818 self.series_index += 1 

1819 

1820 self._xml_start_tag("c:ser") 

1821 

1822 # Write the c:idx element. 

1823 self._write_idx(index) 

1824 

1825 # Write the c:order element. 

1826 self._write_order(index) 

1827 

1828 # Write the series name. 

1829 self._write_series_name(series) 

1830 

1831 # Write the c:spPr element. 

1832 self._write_sp_pr(series) 

1833 

1834 # Write the c:marker element. 

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

1836 

1837 # Write the c:invertIfNegative element. 

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

1839 

1840 # Write the c:dPt element. 

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

1842 

1843 # Write the c:dLbls element. 

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

1845 

1846 # Write the c:trendline element. 

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

1848 

1849 # Write the c:errBars element. 

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

1851 

1852 # Write the c:cat element. 

1853 self._write_cat(series) 

1854 

1855 # Write the c:val element. 

1856 self._write_val(series) 

1857 

1858 # Write the c:smooth element. 

1859 if self.smooth_allowed: 

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

1861 

1862 # Write the c:extLst element. 

1863 if series.get("inverted_color"): 

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

1865 

1866 self._xml_end_tag("c:ser") 

1867 

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

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

1870 

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

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

1873 

1874 attributes1 = [ 

1875 ("uri", uri), 

1876 ("xmlns:c14", xmlns_c_14), 

1877 ] 

1878 

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

1880 

1881 self._xml_start_tag("c:extLst") 

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

1883 self._xml_start_tag("c14:invertSolidFillFmt") 

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

1885 

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

1887 

1888 self._xml_end_tag("c14:spPr") 

1889 self._xml_end_tag("c14:invertSolidFillFmt") 

1890 self._xml_end_tag("c:ext") 

1891 self._xml_end_tag("c:extLst") 

1892 

1893 def _write_c_ext_lst_display_na(self) -> None: 

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

1895 

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

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

1898 

1899 attributes1 = [ 

1900 ("uri", uri), 

1901 ("xmlns:c16r3", xmlns_c_16), 

1902 ] 

1903 

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

1905 

1906 self._xml_start_tag("c:extLst") 

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

1908 self._xml_start_tag("c16r3:dataDisplayOptions16") 

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

1910 self._xml_end_tag("c16r3:dataDisplayOptions16") 

1911 self._xml_end_tag("c:ext") 

1912 self._xml_end_tag("c:extLst") 

1913 

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

1915 # Write the <c:idx> element. 

1916 

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

1918 

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

1920 

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

1922 # Write the <c:order> element. 

1923 

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

1925 

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

1927 

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

1929 # Write the series name. 

1930 

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

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

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

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

1935 

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

1937 # Write the <c:smooth> element. 

1938 

1939 if smooth: 

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

1941 

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

1943 # Write the <c:cat> element. 

1944 formula = series["categories"] 

1945 data_id = series["cat_data_id"] 

1946 data = None 

1947 

1948 if data_id is not None: 

1949 data = self.formula_data[data_id] 

1950 

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

1952 if not formula: 

1953 return 

1954 

1955 self._xml_start_tag("c:cat") 

1956 

1957 # Check the type of cached data. 

1958 cat_type = self._get_data_type(data) 

1959 

1960 if cat_type == "str": 

1961 self.cat_has_num_fmt = False 

1962 # Write the c:numRef element. 

1963 self._write_str_ref(formula, data, cat_type) 

1964 

1965 elif cat_type == "multi_str": 

1966 self.cat_has_num_fmt = False 

1967 # Write the c:numRef element. 

1968 self._write_multi_lvl_str_ref(formula, data) 

1969 

1970 else: 

1971 self.cat_has_num_fmt = True 

1972 # Write the c:numRef element. 

1973 self._write_num_ref(formula, data, cat_type) 

1974 

1975 self._xml_end_tag("c:cat") 

1976 

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

1978 # Write the <c:val> element. 

1979 formula = series["values"] 

1980 data_id = series["val_data_id"] 

1981 data = self.formula_data[data_id] 

1982 

1983 self._xml_start_tag("c:val") 

1984 

1985 # Unlike Cat axes data should only be numeric. 

1986 # Write the c:numRef element. 

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

1988 

1989 self._xml_end_tag("c:val") 

1990 

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

1992 # Write the <c:numRef> element. 

1993 self._xml_start_tag("c:numRef") 

1994 

1995 # Write the c:f element. 

1996 self._write_series_formula(formula) 

1997 

1998 if ref_type == "num": 

1999 # Write the c:numCache element. 

2000 self._write_num_cache(data) 

2001 elif ref_type == "str": 

2002 # Write the c:strCache element. 

2003 self._write_str_cache(data) 

2004 

2005 self._xml_end_tag("c:numRef") 

2006 

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

2008 # Write the <c:strRef> element. 

2009 

2010 self._xml_start_tag("c:strRef") 

2011 

2012 # Write the c:f element. 

2013 self._write_series_formula(formula) 

2014 

2015 if ref_type == "num": 

2016 # Write the c:numCache element. 

2017 self._write_num_cache(data) 

2018 elif ref_type == "str": 

2019 # Write the c:strCache element. 

2020 self._write_str_cache(data) 

2021 

2022 self._xml_end_tag("c:strRef") 

2023 

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

2025 # Write the <c:multiLvlStrRef> element. 

2026 

2027 if not data: 

2028 return 

2029 

2030 self._xml_start_tag("c:multiLvlStrRef") 

2031 

2032 # Write the c:f element. 

2033 self._write_series_formula(formula) 

2034 

2035 self._xml_start_tag("c:multiLvlStrCache") 

2036 

2037 # Write the c:ptCount element. 

2038 count = len(data[-1]) 

2039 self._write_pt_count(count) 

2040 

2041 for cat_data in reversed(data): 

2042 self._xml_start_tag("c:lvl") 

2043 

2044 for i, point in enumerate(cat_data): 

2045 # Write the c:pt element. 

2046 self._write_pt(i, point) 

2047 

2048 self._xml_end_tag("c:lvl") 

2049 

2050 self._xml_end_tag("c:multiLvlStrCache") 

2051 self._xml_end_tag("c:multiLvlStrRef") 

2052 

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

2054 # Write the <c:f> element. 

2055 

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

2057 if formula.startswith("="): 

2058 formula = formula.lstrip("=") 

2059 

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

2061 

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

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

2064 

2065 # Generate the axis ids. 

2066 self._add_axis_ids(args) 

2067 

2068 if args["primary_axes"]: 

2069 # Write the axis ids for the primary axes. 

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

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

2072 else: 

2073 # Write the axis ids for the secondary axes. 

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

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

2076 

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

2078 # Write the <c:axId> element. 

2079 

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

2081 

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

2083 

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

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

2086 x_axis = args["x_axis"] 

2087 y_axis = args["y_axis"] 

2088 axis_ids = args["axis_ids"] 

2089 

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

2091 if axis_ids is None or not axis_ids: 

2092 return 

2093 

2094 position = self.cat_axis_position 

2095 is_horizontal = self.horiz_cat_axis 

2096 

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

2098 if x_axis.get("position"): 

2099 position = x_axis["position"] 

2100 

2101 self._xml_start_tag("c:catAx") 

2102 

2103 self._write_axis_id(axis_ids[0]) 

2104 

2105 # Write the c:scaling element. 

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

2107 

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

2109 self._write_delete(1) 

2110 

2111 # Write the c:axPos element. 

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

2113 

2114 # Write the c:majorGridlines element. 

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

2116 

2117 # Write the c:minorGridlines element. 

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

2119 

2120 # Write the axis title elements. 

2121 self._write_title(x_axis["title"], is_horizontal) 

2122 

2123 # Write the c:numFmt element. 

2124 self._write_cat_number_format(x_axis) 

2125 

2126 # Write the c:majorTickMark element. 

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

2128 

2129 # Write the c:minorTickMark element. 

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

2131 

2132 # Write the c:tickLblPos element. 

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

2134 

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

2136 self._write_sp_pr(x_axis) 

2137 

2138 # Write the axis font elements. 

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

2140 

2141 # Write the c:crossAx element. 

2142 self._write_cross_axis(axis_ids[1]) 

2143 

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

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

2146 if ( 

2147 y_axis.get("crossing") is None 

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

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

2150 ): 

2151 # Write the c:crosses element. 

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

2153 else: 

2154 # Write the c:crossesAt element. 

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

2156 

2157 # Write the c:auto element. 

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

2159 self._write_auto(1) 

2160 

2161 # Write the c:labelAlign element. 

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

2163 

2164 # Write the c:labelOffset element. 

2165 self._write_label_offset(100) 

2166 

2167 # Write the c:tickLblSkip element. 

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

2169 

2170 # Write the c:tickMarkSkip element. 

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

2172 

2173 self._xml_end_tag("c:catAx") 

2174 

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

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

2177 x_axis = args["x_axis"] 

2178 y_axis = args["y_axis"] 

2179 axis_ids = args["axis_ids"] 

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

2181 is_horizontal = self.horiz_val_axis 

2182 

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

2184 if axis_ids is None or not axis_ids: 

2185 return 

2186 

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

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

2189 

2190 self._xml_start_tag("c:valAx") 

2191 

2192 self._write_axis_id(axis_ids[1]) 

2193 

2194 # Write the c:scaling element. 

2195 self._write_scaling( 

2196 y_axis.get("reverse"), 

2197 y_axis.get("min"), 

2198 y_axis.get("max"), 

2199 y_axis.get("log_base"), 

2200 ) 

2201 

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

2203 self._write_delete(1) 

2204 

2205 # Write the c:axPos element. 

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

2207 

2208 # Write the c:majorGridlines element. 

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

2210 

2211 # Write the c:minorGridlines element. 

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

2213 

2214 # Write the axis title elements. 

2215 self._write_title(y_axis["title"], is_horizontal) 

2216 

2217 # Write the c:numberFormat element. 

2218 self._write_number_format(y_axis) 

2219 

2220 # Write the c:majorTickMark element. 

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

2222 

2223 # Write the c:minorTickMark element. 

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

2225 

2226 # Write the c:tickLblPos element. 

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

2228 

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

2230 self._write_sp_pr(y_axis) 

2231 

2232 # Write the axis font elements. 

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

2234 

2235 # Write the c:crossAx element. 

2236 self._write_cross_axis(axis_ids[0]) 

2237 

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

2239 if ( 

2240 x_axis.get("crossing") is None 

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

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

2243 ): 

2244 # Write the c:crosses element. 

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

2246 else: 

2247 # Write the c:crossesAt element. 

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

2249 

2250 # Write the c:crossBetween element. 

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

2252 

2253 # Write the c:majorUnit element. 

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

2255 

2256 # Write the c:minorUnit element. 

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

2258 

2259 # Write the c:dispUnits element. 

2260 self._write_disp_units( 

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

2262 ) 

2263 

2264 self._xml_end_tag("c:valAx") 

2265 

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

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

2268 # in scatter plots. Usually the X axis. 

2269 x_axis = args["x_axis"] 

2270 y_axis = args["y_axis"] 

2271 axis_ids = args["axis_ids"] 

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

2273 is_horizontal = self.horiz_val_axis 

2274 

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

2276 if axis_ids is None or not axis_ids: 

2277 return 

2278 

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

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

2281 

2282 self._xml_start_tag("c:valAx") 

2283 

2284 self._write_axis_id(axis_ids[0]) 

2285 

2286 # Write the c:scaling element. 

2287 self._write_scaling( 

2288 x_axis.get("reverse"), 

2289 x_axis.get("min"), 

2290 x_axis.get("max"), 

2291 x_axis.get("log_base"), 

2292 ) 

2293 

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

2295 self._write_delete(1) 

2296 

2297 # Write the c:axPos element. 

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

2299 

2300 # Write the c:majorGridlines element. 

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

2302 

2303 # Write the c:minorGridlines element. 

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

2305 

2306 # Write the axis title elements. 

2307 self._write_title(x_axis["title"], is_horizontal) 

2308 

2309 # Write the c:numberFormat element. 

2310 self._write_number_format(x_axis) 

2311 

2312 # Write the c:majorTickMark element. 

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

2314 

2315 # Write the c:minorTickMark element. 

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

2317 

2318 # Write the c:tickLblPos element. 

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

2320 

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

2322 self._write_sp_pr(x_axis) 

2323 

2324 # Write the axis font elements. 

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

2326 

2327 # Write the c:crossAx element. 

2328 self._write_cross_axis(axis_ids[1]) 

2329 

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

2331 if ( 

2332 y_axis.get("crossing") is None 

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

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

2335 ): 

2336 # Write the c:crosses element. 

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

2338 else: 

2339 # Write the c:crossesAt element. 

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

2341 

2342 # Write the c:crossBetween element. 

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

2344 

2345 # Write the c:majorUnit element. 

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

2347 

2348 # Write the c:minorUnit element. 

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

2350 

2351 # Write the c:dispUnits element. 

2352 self._write_disp_units( 

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

2354 ) 

2355 

2356 self._xml_end_tag("c:valAx") 

2357 

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

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

2360 x_axis = args["x_axis"] 

2361 y_axis = args["y_axis"] 

2362 axis_ids = args["axis_ids"] 

2363 

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

2365 if axis_ids is None or not axis_ids: 

2366 return 

2367 

2368 position = self.cat_axis_position 

2369 

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

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

2372 

2373 self._xml_start_tag("c:dateAx") 

2374 

2375 self._write_axis_id(axis_ids[0]) 

2376 

2377 # Write the c:scaling element. 

2378 self._write_scaling( 

2379 x_axis.get("reverse"), 

2380 x_axis.get("min"), 

2381 x_axis.get("max"), 

2382 x_axis.get("log_base"), 

2383 ) 

2384 

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

2386 self._write_delete(1) 

2387 

2388 # Write the c:axPos element. 

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

2390 

2391 # Write the c:majorGridlines element. 

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

2393 

2394 # Write the c:minorGridlines element. 

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

2396 

2397 # Write the axis title elements. 

2398 self._write_title(x_axis["title"]) 

2399 

2400 # Write the c:numFmt element. 

2401 self._write_number_format(x_axis) 

2402 

2403 # Write the c:majorTickMark element. 

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

2405 

2406 # Write the c:minorTickMark element. 

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

2408 

2409 # Write the c:tickLblPos element. 

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

2411 

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

2413 self._write_sp_pr(x_axis) 

2414 

2415 # Write the axis font elements. 

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

2417 

2418 # Write the c:crossAx element. 

2419 self._write_cross_axis(axis_ids[1]) 

2420 

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

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

2423 if ( 

2424 y_axis.get("crossing") is None 

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

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

2427 ): 

2428 # Write the c:crosses element. 

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

2430 else: 

2431 # Write the c:crossesAt element. 

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

2433 

2434 # Write the c:auto element. 

2435 self._write_auto(1) 

2436 

2437 # Write the c:labelOffset element. 

2438 self._write_label_offset(100) 

2439 

2440 # Write the c:tickLblSkip element. 

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

2442 

2443 # Write the c:tickMarkSkip element. 

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

2445 

2446 # Write the c:majorUnit element. 

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

2448 

2449 # Write the c:majorTimeUnit element. 

2450 if x_axis.get("major_unit"): 

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

2452 

2453 # Write the c:minorUnit element. 

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

2455 

2456 # Write the c:minorTimeUnit element. 

2457 if x_axis.get("minor_unit"): 

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

2459 

2460 self._xml_end_tag("c:dateAx") 

2461 

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

2463 # Write the <c:scaling> element. 

2464 

2465 self._xml_start_tag("c:scaling") 

2466 

2467 # Write the c:logBase element. 

2468 self._write_c_log_base(log_base) 

2469 

2470 # Write the c:orientation element. 

2471 self._write_orientation(reverse) 

2472 

2473 # Write the c:max element. 

2474 self._write_c_max(max_val) 

2475 

2476 # Write the c:min element. 

2477 self._write_c_min(min_val) 

2478 

2479 self._xml_end_tag("c:scaling") 

2480 

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

2482 # Write the <c:logBase> element. 

2483 

2484 if not val: 

2485 return 

2486 

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

2488 

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

2490 

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

2492 # Write the <c:orientation> element. 

2493 val = "minMax" 

2494 

2495 if reverse: 

2496 val = "maxMin" 

2497 

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

2499 

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

2501 

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

2503 # Write the <c:max> element. 

2504 

2505 if max_val is None: 

2506 return 

2507 

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

2509 

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

2511 

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

2513 # Write the <c:min> element. 

2514 

2515 if min_val is None: 

2516 return 

2517 

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

2519 

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

2521 

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

2523 # Write the <c:axPos> element. 

2524 

2525 if reverse: 

2526 if val == "l": 

2527 val = "r" 

2528 if val == "b": 

2529 val = "t" 

2530 

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

2532 

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

2534 

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

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

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

2538 # the sourceLinked attribute is 0. 

2539 # The user can override this if required. 

2540 format_code = axis.get("num_format") 

2541 source_linked = 1 

2542 

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

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

2545 source_linked = 0 

2546 

2547 # User override of sourceLinked. 

2548 if axis.get("num_format_linked"): 

2549 source_linked = 1 

2550 

2551 attributes = [ 

2552 ("formatCode", format_code), 

2553 ("sourceLinked", source_linked), 

2554 ] 

2555 

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

2557 

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

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

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

2561 format_code = axis.get("num_format") 

2562 source_linked = 1 

2563 default_format = 1 

2564 

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

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

2567 source_linked = 0 

2568 default_format = 0 

2569 

2570 # User override of sourceLinked. 

2571 if axis.get("num_format_linked"): 

2572 source_linked = 1 

2573 

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

2575 if not self.cat_has_num_fmt and default_format: 

2576 return 

2577 

2578 attributes = [ 

2579 ("formatCode", format_code), 

2580 ("sourceLinked", source_linked), 

2581 ] 

2582 

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

2584 

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

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

2587 source_linked = 0 

2588 

2589 attributes = [ 

2590 ("formatCode", format_code), 

2591 ("sourceLinked", source_linked), 

2592 ] 

2593 

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

2595 

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

2597 # Write the <c:majorTickMark> element. 

2598 

2599 if not val: 

2600 return 

2601 

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

2603 

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

2605 

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

2607 # Write the <c:minorTickMark> element. 

2608 

2609 if not val: 

2610 return 

2611 

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

2613 

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

2615 

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

2617 # Write the <c:tickLblPos> element. 

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

2619 val = "nextTo" 

2620 

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

2622 

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

2624 

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

2626 # Write the <c:crossAx> element. 

2627 

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

2629 

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

2631 

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

2633 # Write the <c:crosses> element. 

2634 if val is None: 

2635 val = "autoZero" 

2636 

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

2638 

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

2640 

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

2642 # Write the <c:crossesAt> element. 

2643 

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

2645 

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

2647 

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

2649 # Write the <c:auto> element. 

2650 

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

2652 

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

2654 

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

2656 # Write the <c:labelAlign> element. 

2657 

2658 if val is None: 

2659 val = "ctr" 

2660 

2661 if val == "right": 

2662 val = "r" 

2663 

2664 if val == "left": 

2665 val = "l" 

2666 

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

2668 

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

2670 

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

2672 # Write the <c:labelOffset> element. 

2673 

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

2675 

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

2677 

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

2679 # Write the <c:tickLblSkip> element. 

2680 if val is None: 

2681 return 

2682 

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

2684 

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

2686 

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

2688 # Write the <c:tickMarkSkip> element. 

2689 if val is None: 

2690 return 

2691 

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

2693 

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

2695 

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

2697 # Write the <c:majorGridlines> element. 

2698 

2699 if not gridlines: 

2700 return 

2701 

2702 if not gridlines["visible"]: 

2703 return 

2704 

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

2706 self._xml_start_tag("c:majorGridlines") 

2707 

2708 # Write the c:spPr element. 

2709 self._write_sp_pr(gridlines) 

2710 

2711 self._xml_end_tag("c:majorGridlines") 

2712 else: 

2713 self._xml_empty_tag("c:majorGridlines") 

2714 

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

2716 # Write the <c:minorGridlines> element. 

2717 

2718 if not gridlines: 

2719 return 

2720 

2721 if not gridlines["visible"]: 

2722 return 

2723 

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

2725 self._xml_start_tag("c:minorGridlines") 

2726 

2727 # Write the c:spPr element. 

2728 self._write_sp_pr(gridlines) 

2729 

2730 self._xml_end_tag("c:minorGridlines") 

2731 else: 

2732 self._xml_empty_tag("c:minorGridlines") 

2733 

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

2735 # Write the <c:crossBetween> element. 

2736 if val is None: 

2737 val = self.cross_between 

2738 

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

2740 

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

2742 

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

2744 # Write the <c:majorUnit> element. 

2745 

2746 if not val: 

2747 return 

2748 

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

2750 

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

2752 

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

2754 # Write the <c:minorUnit> element. 

2755 

2756 if not val: 

2757 return 

2758 

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

2760 

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

2762 

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

2764 # Write the <c:majorTimeUnit> element. 

2765 if val is None: 

2766 val = "days" 

2767 

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

2769 

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

2771 

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

2773 # Write the <c:minorTimeUnit> element. 

2774 if val is None: 

2775 val = "days" 

2776 

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

2778 

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

2780 

2781 def _write_legend(self) -> None: 

2782 # Write the <c:legend> element. 

2783 legend = self.legend 

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

2785 font = legend.get("font") 

2786 delete_series = [] 

2787 overlay = 0 

2788 

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

2790 delete_series = legend["delete_series"] 

2791 

2792 if position.startswith("overlay_"): 

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

2794 overlay = 1 

2795 

2796 allowed = { 

2797 "right": "r", 

2798 "left": "l", 

2799 "top": "t", 

2800 "bottom": "b", 

2801 "top_right": "tr", 

2802 } 

2803 

2804 if position == "none": 

2805 return 

2806 

2807 if position not in allowed: 

2808 return 

2809 

2810 position = allowed[position] 

2811 

2812 self._xml_start_tag("c:legend") 

2813 

2814 # Write the c:legendPos element. 

2815 self._write_legend_pos(position) 

2816 

2817 # Remove series labels from the legend. 

2818 for index in delete_series: 

2819 # Write the c:legendEntry element. 

2820 self._write_legend_entry(index) 

2821 

2822 # Write the c:layout element. 

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

2824 

2825 # Write the c:overlay element. 

2826 if overlay: 

2827 self._write_overlay() 

2828 

2829 if font: 

2830 self._write_tx_pr(font) 

2831 

2832 # Write the c:spPr element. 

2833 self._write_sp_pr(legend) 

2834 

2835 self._xml_end_tag("c:legend") 

2836 

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

2838 # Write the <c:legendPos> element. 

2839 

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

2841 

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

2843 

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

2845 # Write the <c:legendEntry> element. 

2846 

2847 self._xml_start_tag("c:legendEntry") 

2848 

2849 # Write the c:idx element. 

2850 self._write_idx(index) 

2851 

2852 # Write the c:delete element. 

2853 self._write_delete(1) 

2854 

2855 self._xml_end_tag("c:legendEntry") 

2856 

2857 def _write_overlay(self) -> None: 

2858 # Write the <c:overlay> element. 

2859 val = 1 

2860 

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

2862 

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

2864 

2865 def _write_plot_vis_only(self) -> None: 

2866 # Write the <c:plotVisOnly> element. 

2867 val = 1 

2868 

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

2870 if self.show_hidden: 

2871 return 

2872 

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

2874 

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

2876 

2877 def _write_print_settings(self) -> None: 

2878 # Write the <c:printSettings> element. 

2879 self._xml_start_tag("c:printSettings") 

2880 

2881 # Write the c:headerFooter element. 

2882 self._write_header_footer() 

2883 

2884 # Write the c:pageMargins element. 

2885 self._write_page_margins() 

2886 

2887 # Write the c:pageSetup element. 

2888 self._write_page_setup() 

2889 

2890 self._xml_end_tag("c:printSettings") 

2891 

2892 def _write_header_footer(self) -> None: 

2893 # Write the <c:headerFooter> element. 

2894 self._xml_empty_tag("c:headerFooter") 

2895 

2896 def _write_page_margins(self) -> None: 

2897 # Write the <c:pageMargins> element. 

2898 bottom = 0.75 

2899 left = 0.7 

2900 right = 0.7 

2901 top = 0.75 

2902 header = 0.3 

2903 footer = 0.3 

2904 

2905 attributes = [ 

2906 ("b", bottom), 

2907 ("l", left), 

2908 ("r", right), 

2909 ("t", top), 

2910 ("header", header), 

2911 ("footer", footer), 

2912 ] 

2913 

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

2915 

2916 def _write_page_setup(self) -> None: 

2917 # Write the <c:pageSetup> element. 

2918 self._xml_empty_tag("c:pageSetup") 

2919 

2920 def _write_c_auto_title_deleted(self) -> None: 

2921 # Write the <c:autoTitleDeleted> element. 

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

2923 

2924 def _write_title(self, title: ChartTitle, is_horizontal: bool = False) -> None: 

2925 # Write the <c:title> element for different title types. 

2926 if title.has_name(): 

2927 self._write_title_rich(title, is_horizontal) 

2928 elif title.has_formula(): 

2929 self._write_title_formula(title, is_horizontal) 

2930 elif title.has_formatting(): 

2931 self._write_title_format_only(title) 

2932 

2933 def _write_title_rich(self, title: ChartTitle, is_horizontal: bool = False) -> None: 

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

2935 self._xml_start_tag("c:title") 

2936 

2937 # Write the c:tx element. 

2938 self._write_tx_rich(title.name, is_horizontal, title.font) 

2939 

2940 # Write the c:layout element. 

2941 self._write_layout(title.layout, "text") 

2942 

2943 # Write the c:overlay element. 

2944 if title.overlay: 

2945 self._write_overlay() 

2946 

2947 # Write the c:spPr element. 

2948 self._write_sp_pr(title.get_formatting()) 

2949 

2950 self._xml_end_tag("c:title") 

2951 

2952 def _write_title_formula( 

2953 self, title: ChartTitle, is_horizontal: bool = False 

2954 ) -> None: 

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

2956 self._xml_start_tag("c:title") 

2957 

2958 # Write the c:tx element. 

2959 self._write_tx_formula(title.formula, title.data_id) 

2960 

2961 # Write the c:layout element. 

2962 self._write_layout(title.layout, "text") 

2963 

2964 # Write the c:overlay element. 

2965 if title.overlay: 

2966 self._write_overlay() 

2967 

2968 # Write the c:spPr element. 

2969 self._write_sp_pr(title.get_formatting()) 

2970 

2971 # Write the c:txPr element. 

2972 self._write_tx_pr(title.font, is_horizontal) 

2973 

2974 self._xml_end_tag("c:title") 

2975 

2976 def _write_title_format_only(self, title: ChartTitle) -> None: 

2977 # Write the <c:title> element title with formatting and default name. 

2978 self._xml_start_tag("c:title") 

2979 

2980 # Write the c:layout element. 

2981 self._write_layout(title.layout, "text") 

2982 

2983 # Write the c:overlay element. 

2984 if title.overlay: 

2985 self._write_overlay() 

2986 

2987 # Write the c:spPr element. 

2988 self._write_sp_pr(title.get_formatting()) 

2989 

2990 self._xml_end_tag("c:title") 

2991 

2992 def _write_tx_rich(self, title, is_horizontal, font) -> None: 

2993 # Write the <c:tx> element. 

2994 

2995 self._xml_start_tag("c:tx") 

2996 

2997 # Write the c:rich element. 

2998 self._write_rich(title, font, is_horizontal, ignore_rich_pr=False) 

2999 

3000 self._xml_end_tag("c:tx") 

3001 

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

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

3004 

3005 self._xml_start_tag("c:tx") 

3006 

3007 # Write the c:v element. 

3008 self._write_v(title) 

3009 

3010 self._xml_end_tag("c:tx") 

3011 

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

3013 # Write the <c:tx> element. 

3014 data = None 

3015 

3016 if data_id is not None: 

3017 data = self.formula_data[data_id] 

3018 

3019 self._xml_start_tag("c:tx") 

3020 

3021 # Write the c:strRef element. 

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

3023 

3024 self._xml_end_tag("c:tx") 

3025 

3026 def _write_rich(self, title, font, is_horizontal, ignore_rich_pr) -> None: 

3027 # Write the <c:rich> element. 

3028 

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

3030 rotation = font["rotation"] 

3031 else: 

3032 rotation = None 

3033 

3034 self._xml_start_tag("c:rich") 

3035 

3036 # Write the a:bodyPr element. 

3037 self._write_a_body_pr(rotation, is_horizontal) 

3038 

3039 # Write the a:lstStyle element. 

3040 self._write_a_lst_style() 

3041 

3042 # Write the a:p element. 

3043 self._write_a_p_rich(title, font, ignore_rich_pr) 

3044 

3045 self._xml_end_tag("c:rich") 

3046 

3047 def _write_a_body_pr(self, rotation, is_horizontal) -> None: 

3048 # Write the <a:bodyPr> element. 

3049 attributes = [] 

3050 

3051 if rotation is None and is_horizontal: 

3052 rotation = -5400000 

3053 

3054 if rotation is not None: 

3055 if rotation == 16200000: 

3056 # 270 deg/stacked angle. 

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

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

3059 elif rotation == 16260000: 

3060 # 271 deg/East Asian vertical. 

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

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

3063 else: 

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

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

3066 

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

3068 

3069 def _write_a_lst_style(self) -> None: 

3070 # Write the <a:lstStyle> element. 

3071 self._xml_empty_tag("a:lstStyle") 

3072 

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

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

3075 

3076 self._xml_start_tag("a:p") 

3077 

3078 # Write the a:pPr element. 

3079 if not ignore_rich_pr: 

3080 self._write_a_p_pr_rich(font) 

3081 

3082 # Write the a:r element. 

3083 self._write_a_r(title, font) 

3084 

3085 self._xml_end_tag("a:p") 

3086 

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

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

3089 

3090 self._xml_start_tag("a:p") 

3091 

3092 # Write the a:pPr element. 

3093 self._write_a_p_pr_rich(font) 

3094 

3095 # Write the a:endParaRPr element. 

3096 self._write_a_end_para_rpr() 

3097 

3098 self._xml_end_tag("a:p") 

3099 

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

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

3102 

3103 self._xml_start_tag("a:pPr") 

3104 

3105 # Write the a:defRPr element. 

3106 self._write_a_def_rpr(font) 

3107 

3108 self._xml_end_tag("a:pPr") 

3109 

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

3111 # Write the <a:defRPr> element. 

3112 has_color = False 

3113 

3114 style_attributes = Shape._get_font_style_attributes(font) 

3115 latin_attributes = Shape._get_font_latin_attributes(font) 

3116 

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

3118 has_color = True 

3119 

3120 if latin_attributes or has_color: 

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

3122 

3123 if has_color: 

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

3125 

3126 if latin_attributes: 

3127 self._write_a_latin(latin_attributes) 

3128 

3129 self._xml_end_tag("a:defRPr") 

3130 else: 

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

3132 

3133 def _write_a_end_para_rpr(self) -> None: 

3134 # Write the <a:endParaRPr> element. 

3135 lang = "en-US" 

3136 

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

3138 

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

3140 

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

3142 # Write the <a:r> element. 

3143 

3144 self._xml_start_tag("a:r") 

3145 

3146 # Write the a:rPr element. 

3147 self._write_a_r_pr(font) 

3148 

3149 # Write the a:t element. 

3150 self._write_a_t(title) 

3151 

3152 self._xml_end_tag("a:r") 

3153 

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

3155 # Write the <a:rPr> element. 

3156 has_color = False 

3157 lang = "en-US" 

3158 

3159 style_attributes = Shape._get_font_style_attributes(font) 

3160 latin_attributes = Shape._get_font_latin_attributes(font) 

3161 

3162 if font and font["color"]: 

3163 has_color = True 

3164 

3165 # Add the lang type to the attributes. 

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

3167 

3168 if latin_attributes or has_color: 

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

3170 

3171 if has_color: 

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

3173 

3174 if latin_attributes: 

3175 self._write_a_latin(latin_attributes) 

3176 

3177 self._xml_end_tag("a:rPr") 

3178 else: 

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

3180 

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

3182 # Write the <a:t> element. 

3183 

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

3185 

3186 def _write_tx_pr(self, font, is_horizontal=False) -> None: 

3187 # Write the <c:txPr> element. 

3188 

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

3190 rotation = font["rotation"] 

3191 else: 

3192 rotation = None 

3193 

3194 self._xml_start_tag("c:txPr") 

3195 

3196 # Write the a:bodyPr element. 

3197 self._write_a_body_pr(rotation, is_horizontal) 

3198 

3199 # Write the a:lstStyle element. 

3200 self._write_a_lst_style() 

3201 

3202 # Write the a:p element. 

3203 self._write_a_p_formula(font) 

3204 

3205 self._xml_end_tag("c:txPr") 

3206 

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

3208 # Write the <c:marker> element. 

3209 if marker is None: 

3210 marker = self.default_marker 

3211 

3212 if not marker: 

3213 return 

3214 

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

3216 return 

3217 

3218 self._xml_start_tag("c:marker") 

3219 

3220 # Write the c:symbol element. 

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

3222 

3223 # Write the c:size element. 

3224 if marker.get("size"): 

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

3226 

3227 # Write the c:spPr element. 

3228 self._write_sp_pr(marker) 

3229 

3230 self._xml_end_tag("c:marker") 

3231 

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

3233 # Write the <c:size> element. 

3234 

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

3236 

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

3238 

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

3240 # Write the <c:symbol> element. 

3241 

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

3243 

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

3245 

3246 def _write_sp_pr(self, chart_format: dict) -> None: 

3247 # Write the <c:spPr> element. 

3248 if not self._has_formatting(chart_format): 

3249 return 

3250 

3251 self._xml_start_tag("c:spPr") 

3252 

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

3254 if chart_format.get("fill") and chart_format["fill"]["defined"]: 

3255 if "none" in chart_format["fill"]: 

3256 # Write the a:noFill element. 

3257 self._write_a_no_fill() 

3258 else: 

3259 # Write the a:solidFill element. 

3260 self._write_a_solid_fill(chart_format["fill"]) 

3261 

3262 if chart_format.get("pattern"): 

3263 # Write the a:gradFill element. 

3264 self._write_a_patt_fill(chart_format["pattern"]) 

3265 

3266 if chart_format.get("gradient"): 

3267 # Write the a:gradFill element. 

3268 self._write_a_grad_fill(chart_format["gradient"]) 

3269 

3270 # Write the a:ln element. 

3271 if chart_format.get("line") and chart_format["line"]["defined"]: 

3272 self._write_a_ln(chart_format["line"]) 

3273 

3274 self._xml_end_tag("c:spPr") 

3275 

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

3277 # Write the <a:ln> element. 

3278 attributes = [] 

3279 

3280 # Add the line width as an attribute. 

3281 width = line.get("width") 

3282 

3283 if width is not None: 

3284 # Round width to nearest 0.25, like Excel. 

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

3286 

3287 # Convert to internal units. 

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

3289 

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

3291 

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

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

3294 

3295 # Write the line fill. 

3296 if "none" in line: 

3297 # Write the a:noFill element. 

3298 self._write_a_no_fill() 

3299 elif "color" in line: 

3300 # Write the a:solidFill element. 

3301 self._write_a_solid_fill(line) 

3302 

3303 # Write the line/dash type. 

3304 line_type = line.get("dash_type") 

3305 if line_type: 

3306 # Write the a:prstDash element. 

3307 self._write_a_prst_dash(line_type) 

3308 

3309 self._xml_end_tag("a:ln") 

3310 else: 

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

3312 

3313 def _write_a_no_fill(self) -> None: 

3314 # Write the <a:noFill> element. 

3315 self._xml_empty_tag("a:noFill") 

3316 

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

3318 # Write the <a:solidFill> element. 

3319 

3320 self._xml_start_tag("a:solidFill") 

3321 

3322 if fill.get("color"): 

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

3324 

3325 self._xml_end_tag("a:solidFill") 

3326 

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

3328 # Write the appropriate chart color element. 

3329 

3330 if not color: 

3331 return 

3332 

3333 if color._is_automatic: 

3334 # Write the a:sysClr element. 

3335 self._write_a_sys_clr() 

3336 elif color._type == ColorTypes.RGB: 

3337 # Write the a:srgbClr element. 

3338 self._write_a_srgb_clr(color, transparency) 

3339 elif color._type == ColorTypes.THEME: 

3340 self._write_a_scheme_clr(color, transparency) 

3341 

3342 def _write_a_sys_clr(self) -> None: 

3343 # Write the <a:sysClr> element. 

3344 

3345 val = "window" 

3346 last_clr = "FFFFFF" 

3347 

3348 attributes = [ 

3349 ("val", val), 

3350 ("lastClr", last_clr), 

3351 ] 

3352 

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

3354 

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

3356 # Write the <a:srgbClr> element. 

3357 

3358 if not color: 

3359 return 

3360 

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

3362 

3363 if transparency: 

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

3365 

3366 # Write the a:alpha element. 

3367 self._write_a_alpha(transparency) 

3368 

3369 self._xml_end_tag("a:srgbClr") 

3370 else: 

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

3372 

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

3374 # Write the <a:schemeClr> element. 

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

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

3377 

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

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

3380 

3381 if lum_mod > 0: 

3382 # Write the a:lumMod element. 

3383 self._write_a_lum_mod(lum_mod) 

3384 

3385 if lum_off > 0: 

3386 # Write the a:lumOff element. 

3387 self._write_a_lum_off(lum_off) 

3388 

3389 if transparency: 

3390 # Write the a:alpha element. 

3391 self._write_a_alpha(transparency) 

3392 

3393 self._xml_end_tag("a:schemeClr") 

3394 else: 

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

3396 

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

3398 # Write the <a:lumMod> element. 

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

3400 

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

3402 

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

3404 # Write the <a:lumOff> element. 

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

3406 

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

3408 

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

3410 # Write the <a:alpha> element. 

3411 

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

3413 

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

3415 

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

3417 

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

3419 # Write the <a:prstDash> element. 

3420 

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

3422 

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

3424 

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

3426 # Write the <c:trendline> element. 

3427 

3428 if not trendline: 

3429 return 

3430 

3431 self._xml_start_tag("c:trendline") 

3432 

3433 # Write the c:name element. 

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

3435 

3436 # Write the c:spPr element. 

3437 self._write_sp_pr(trendline) 

3438 

3439 # Write the c:trendlineType element. 

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

3441 

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

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

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

3445 

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

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

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

3449 

3450 # Write the c:forward element. 

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

3452 

3453 # Write the c:backward element. 

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

3455 

3456 if "intercept" in trendline: 

3457 # Write the c:intercept element. 

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

3459 

3460 if trendline.get("display_r_squared"): 

3461 # Write the c:dispRSqr element. 

3462 self._write_c_disp_rsqr() 

3463 

3464 if trendline.get("display_equation"): 

3465 # Write the c:dispEq element. 

3466 self._write_c_disp_eq() 

3467 

3468 # Write the c:trendlineLbl element. 

3469 self._write_c_trendline_lbl(trendline) 

3470 

3471 self._xml_end_tag("c:trendline") 

3472 

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

3474 # Write the <c:trendlineType> element. 

3475 

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

3477 

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

3479 

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

3481 # Write the <c:name> element. 

3482 

3483 if data is None: 

3484 return 

3485 

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

3487 

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

3489 # Write the <c:order> element. 

3490 val = max(val, 2) 

3491 

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

3493 

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

3495 

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

3497 # Write the <c:period> element. 

3498 val = max(val, 2) 

3499 

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

3501 

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

3503 

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

3505 # Write the <c:forward> element. 

3506 

3507 if not val: 

3508 return 

3509 

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

3511 

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

3513 

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

3515 # Write the <c:backward> element. 

3516 

3517 if not val: 

3518 return 

3519 

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

3521 

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

3523 

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

3525 # Write the <c:intercept> element. 

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

3527 

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

3529 

3530 def _write_c_disp_eq(self) -> None: 

3531 # Write the <c:dispEq> element. 

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

3533 

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

3535 

3536 def _write_c_disp_rsqr(self) -> None: 

3537 # Write the <c:dispRSqr> element. 

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

3539 

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

3541 

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

3543 # Write the <c:trendlineLbl> element. 

3544 self._xml_start_tag("c:trendlineLbl") 

3545 

3546 # Write the c:layout element. 

3547 self._write_layout(None, None) 

3548 

3549 # Write the c:numFmt element. 

3550 self._write_trendline_num_fmt() 

3551 

3552 # Write the c:spPr element. 

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

3554 

3555 # Write the data label font elements. 

3556 if trendline["label"]: 

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

3558 if font: 

3559 self._write_axis_font(font) 

3560 

3561 self._xml_end_tag("c:trendlineLbl") 

3562 

3563 def _write_trendline_num_fmt(self) -> None: 

3564 # Write the <c:numFmt> element. 

3565 attributes = [ 

3566 ("formatCode", "General"), 

3567 ("sourceLinked", 0), 

3568 ] 

3569 

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

3571 

3572 def _write_hi_low_lines(self) -> None: 

3573 # Write the <c:hiLowLines> element. 

3574 hi_low_lines = self.hi_low_lines 

3575 

3576 if hi_low_lines is None: 

3577 return 

3578 

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

3580 self._xml_start_tag("c:hiLowLines") 

3581 

3582 # Write the c:spPr element. 

3583 self._write_sp_pr(hi_low_lines) 

3584 

3585 self._xml_end_tag("c:hiLowLines") 

3586 else: 

3587 self._xml_empty_tag("c:hiLowLines") 

3588 

3589 def _write_drop_lines(self) -> None: 

3590 # Write the <c:dropLines> element. 

3591 drop_lines = self.drop_lines 

3592 

3593 if drop_lines is None: 

3594 return 

3595 

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

3597 self._xml_start_tag("c:dropLines") 

3598 

3599 # Write the c:spPr element. 

3600 self._write_sp_pr(drop_lines) 

3601 

3602 self._xml_end_tag("c:dropLines") 

3603 else: 

3604 self._xml_empty_tag("c:dropLines") 

3605 

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

3607 # Write the <c:overlap> element. 

3608 

3609 if val is None: 

3610 return 

3611 

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

3613 

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

3615 

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

3617 # Write the <c:numCache> element. 

3618 if data: 

3619 count = len(data) 

3620 else: 

3621 count = 0 

3622 

3623 self._xml_start_tag("c:numCache") 

3624 

3625 # Write the c:formatCode element. 

3626 self._write_format_code("General") 

3627 

3628 # Write the c:ptCount element. 

3629 self._write_pt_count(count) 

3630 

3631 for i in range(count): 

3632 token = data[i] 

3633 

3634 if token is None: 

3635 continue 

3636 

3637 try: 

3638 float(token) 

3639 except ValueError: 

3640 # Write non-numeric data as 0. 

3641 token = 0 

3642 

3643 # Write the c:pt element. 

3644 self._write_pt(i, token) 

3645 

3646 self._xml_end_tag("c:numCache") 

3647 

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

3649 # Write the <c:strCache> element. 

3650 count = len(data) 

3651 

3652 self._xml_start_tag("c:strCache") 

3653 

3654 # Write the c:ptCount element. 

3655 self._write_pt_count(count) 

3656 

3657 for i in range(count): 

3658 # Write the c:pt element. 

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

3660 

3661 self._xml_end_tag("c:strCache") 

3662 

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

3664 # Write the <c:formatCode> element. 

3665 

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

3667 

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

3669 # Write the <c:ptCount> element. 

3670 

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

3672 

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

3674 

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

3676 # Write the <c:pt> element. 

3677 

3678 if value is None: 

3679 return 

3680 

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

3682 

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

3684 

3685 # Write the c:v element. 

3686 self._write_v(value) 

3687 

3688 self._xml_end_tag("c:pt") 

3689 

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

3691 # Write the <c:v> element. 

3692 

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

3694 

3695 def _write_protection(self) -> None: 

3696 # Write the <c:protection> element. 

3697 if not self.protection: 

3698 return 

3699 

3700 self._xml_empty_tag("c:protection") 

3701 

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

3703 # Write the <c:dPt> elements. 

3704 index = -1 

3705 

3706 if not points: 

3707 return 

3708 

3709 for point in points: 

3710 index += 1 

3711 if not point: 

3712 continue 

3713 

3714 self._write_d_pt_point(index, point) 

3715 

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

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

3718 

3719 self._xml_start_tag("c:dPt") 

3720 

3721 # Write the c:idx element. 

3722 self._write_idx(index) 

3723 

3724 # Write the c:spPr element. 

3725 self._write_sp_pr(point) 

3726 

3727 self._xml_end_tag("c:dPt") 

3728 

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

3730 # Write the <c:dLbls> element. 

3731 

3732 if not labels: 

3733 return 

3734 

3735 self._xml_start_tag("c:dLbls") 

3736 

3737 # Write the custom c:dLbl elements. 

3738 if labels.get("custom"): 

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

3740 

3741 # Write the c:numFmt element. 

3742 if labels.get("num_format"): 

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

3744 

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

3746 self._write_sp_pr(labels) 

3747 

3748 # Write the data label font elements. 

3749 if labels.get("font"): 

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

3751 

3752 # Write the c:dLblPos element. 

3753 if labels.get("position"): 

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

3755 

3756 # Write the c:showLegendKey element. 

3757 if labels.get("legend_key"): 

3758 self._write_show_legend_key() 

3759 

3760 # Write the c:showVal element. 

3761 if labels.get("value"): 

3762 self._write_show_val() 

3763 

3764 # Write the c:showCatName element. 

3765 if labels.get("category"): 

3766 self._write_show_cat_name() 

3767 

3768 # Write the c:showSerName element. 

3769 if labels.get("series_name"): 

3770 self._write_show_ser_name() 

3771 

3772 # Write the c:showPercent element. 

3773 if labels.get("percentage"): 

3774 self._write_show_percent() 

3775 

3776 # Write the c:separator element. 

3777 if labels.get("separator"): 

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

3779 

3780 # Write the c:showLeaderLines element. 

3781 if labels.get("leader_lines"): 

3782 self._write_show_leader_lines() 

3783 

3784 self._xml_end_tag("c:dLbls") 

3785 

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

3787 # Write the <c:showLegendKey> element. 

3788 index = 0 

3789 

3790 for label in labels: 

3791 index += 1 

3792 

3793 if label is None: 

3794 continue 

3795 

3796 use_custom_formatting = True 

3797 

3798 self._xml_start_tag("c:dLbl") 

3799 

3800 # Write the c:idx element. 

3801 self._write_idx(index - 1) 

3802 

3803 delete_label = label.get("delete") 

3804 

3805 if delete_label: 

3806 self._write_delete(1) 

3807 

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

3809 

3810 # Write the c:layout element. 

3811 self._write_layout(None, None) 

3812 

3813 if label.get("formula"): 

3814 self._write_custom_label_formula(label) 

3815 elif label.get("value"): 

3816 self._write_custom_label_str(label) 

3817 # String values use spPr formatting. 

3818 use_custom_formatting = False 

3819 

3820 if use_custom_formatting: 

3821 self._write_custom_label_format(label) 

3822 

3823 if label.get("position"): 

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

3825 elif parent.get("position"): 

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

3827 

3828 if parent.get("value"): 

3829 self._write_show_val() 

3830 

3831 if parent.get("category"): 

3832 self._write_show_cat_name() 

3833 

3834 if parent.get("series_name"): 

3835 self._write_show_ser_name() 

3836 

3837 else: 

3838 self._write_custom_label_format(label) 

3839 

3840 self._xml_end_tag("c:dLbl") 

3841 

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

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

3844 title = label.get("value") 

3845 font = label.get("font") 

3846 has_formatting = self._has_formatting(label) 

3847 

3848 self._xml_start_tag("c:tx") 

3849 

3850 # Write the c:rich element. 

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

3852 

3853 self._xml_end_tag("c:tx") 

3854 

3855 # Write the c:spPr element. 

3856 self._write_sp_pr(label) 

3857 

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

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

3860 formula = label.get("formula") 

3861 data_id = label.get("data_id") 

3862 data = None 

3863 

3864 if data_id is not None: 

3865 data = self.formula_data[data_id] 

3866 

3867 self._xml_start_tag("c:tx") 

3868 

3869 # Write the c:strRef element. 

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

3871 

3872 self._xml_end_tag("c:tx") 

3873 

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

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

3876 font = label.get("font") 

3877 has_formatting = self._has_formatting(label) 

3878 

3879 if has_formatting: 

3880 self._write_sp_pr(label) 

3881 self._write_tx_pr(font) 

3882 elif font: 

3883 self._xml_empty_tag("c:spPr") 

3884 self._write_tx_pr(font) 

3885 

3886 def _write_show_legend_key(self) -> None: 

3887 # Write the <c:showLegendKey> element. 

3888 val = "1" 

3889 

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

3891 

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

3893 

3894 def _write_show_val(self) -> None: 

3895 # Write the <c:showVal> element. 

3896 val = 1 

3897 

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

3899 

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

3901 

3902 def _write_show_cat_name(self) -> None: 

3903 # Write the <c:showCatName> element. 

3904 val = 1 

3905 

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

3907 

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

3909 

3910 def _write_show_ser_name(self) -> None: 

3911 # Write the <c:showSerName> element. 

3912 val = 1 

3913 

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

3915 

3916 self._xml_empty_tag("c:showSerName", attributes) 

3917 

3918 def _write_show_percent(self) -> None: 

3919 # Write the <c:showPercent> element. 

3920 val = 1 

3921 

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

3923 

3924 self._xml_empty_tag("c:showPercent", attributes) 

3925 

3926 def _write_separator(self, data) -> None: 

3927 # Write the <c:separator> element. 

3928 self._xml_data_element("c:separator", data) 

3929 

3930 def _write_show_leader_lines(self) -> None: 

3931 # Write the <c:showLeaderLines> element. 

3932 # 

3933 # This is different for Pie/Doughnut charts. Other chart types only 

3934 # supported leader lines after Excel 2015 via an extension element. 

3935 # 

3936 uri = "{CE6537A1-D6FC-4f65-9D91-7224C49458BB}" 

3937 xmlns_c_15 = "http://schemas.microsoft.com/office/drawing/2012/chart" 

3938 

3939 attributes = [ 

3940 ("uri", uri), 

3941 ("xmlns:c15", xmlns_c_15), 

3942 ] 

3943 

3944 self._xml_start_tag("c:extLst") 

3945 self._xml_start_tag("c:ext", attributes) 

3946 self._xml_empty_tag("c15:showLeaderLines", [("val", 1)]) 

3947 self._xml_end_tag("c:ext") 

3948 self._xml_end_tag("c:extLst") 

3949 

3950 def _write_d_lbl_pos(self, val) -> None: 

3951 # Write the <c:dLblPos> element. 

3952 

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

3954 

3955 self._xml_empty_tag("c:dLblPos", attributes) 

3956 

3957 def _write_delete(self, val) -> None: 

3958 # Write the <c:delete> element. 

3959 

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

3961 

3962 self._xml_empty_tag("c:delete", attributes) 

3963 

3964 def _write_c_invert_if_negative(self, invert) -> None: 

3965 # Write the <c:invertIfNegative> element. 

3966 val = 1 

3967 

3968 if not invert: 

3969 return 

3970 

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

3972 

3973 self._xml_empty_tag("c:invertIfNegative", attributes) 

3974 

3975 def _write_axis_font(self, font) -> None: 

3976 # Write the axis font elements. 

3977 

3978 if not font: 

3979 return 

3980 

3981 self._xml_start_tag("c:txPr") 

3982 self._write_a_body_pr(font.get("rotation"), None) 

3983 self._write_a_lst_style() 

3984 self._xml_start_tag("a:p") 

3985 

3986 self._write_a_p_pr_rich(font) 

3987 

3988 self._write_a_end_para_rpr() 

3989 self._xml_end_tag("a:p") 

3990 self._xml_end_tag("c:txPr") 

3991 

3992 def _write_a_latin(self, attributes) -> None: 

3993 # Write the <a:latin> element. 

3994 self._xml_empty_tag("a:latin", attributes) 

3995 

3996 def _write_d_table(self) -> None: 

3997 # Write the <c:dTable> element. 

3998 table = self.table 

3999 

4000 if not table: 

4001 return 

4002 

4003 self._xml_start_tag("c:dTable") 

4004 

4005 if table["horizontal"]: 

4006 # Write the c:showHorzBorder element. 

4007 self._write_show_horz_border() 

4008 

4009 if table["vertical"]: 

4010 # Write the c:showVertBorder element. 

4011 self._write_show_vert_border() 

4012 

4013 if table["outline"]: 

4014 # Write the c:showOutline element. 

4015 self._write_show_outline() 

4016 

4017 if table["show_keys"]: 

4018 # Write the c:showKeys element. 

4019 self._write_show_keys() 

4020 

4021 if table["font"]: 

4022 # Write the table font. 

4023 self._write_tx_pr(table["font"]) 

4024 

4025 self._xml_end_tag("c:dTable") 

4026 

4027 def _write_show_horz_border(self) -> None: 

4028 # Write the <c:showHorzBorder> element. 

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

4030 

4031 self._xml_empty_tag("c:showHorzBorder", attributes) 

4032 

4033 def _write_show_vert_border(self) -> None: 

4034 # Write the <c:showVertBorder> element. 

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

4036 

4037 self._xml_empty_tag("c:showVertBorder", attributes) 

4038 

4039 def _write_show_outline(self) -> None: 

4040 # Write the <c:showOutline> element. 

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

4042 

4043 self._xml_empty_tag("c:showOutline", attributes) 

4044 

4045 def _write_show_keys(self) -> None: 

4046 # Write the <c:showKeys> element. 

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

4048 

4049 self._xml_empty_tag("c:showKeys", attributes) 

4050 

4051 def _write_error_bars(self, error_bars) -> None: 

4052 # Write the X and Y error bars. 

4053 

4054 if not error_bars: 

4055 return 

4056 

4057 if error_bars["x_error_bars"]: 

4058 self._write_err_bars("x", error_bars["x_error_bars"]) 

4059 

4060 if error_bars["y_error_bars"]: 

4061 self._write_err_bars("y", error_bars["y_error_bars"]) 

4062 

4063 def _write_err_bars(self, direction, error_bars) -> None: 

4064 # Write the <c:errBars> element. 

4065 

4066 if not error_bars: 

4067 return 

4068 

4069 self._xml_start_tag("c:errBars") 

4070 

4071 # Write the c:errDir element. 

4072 self._write_err_dir(direction) 

4073 

4074 # Write the c:errBarType element. 

4075 self._write_err_bar_type(error_bars["direction"]) 

4076 

4077 # Write the c:errValType element. 

4078 self._write_err_val_type(error_bars["type"]) 

4079 

4080 if not error_bars["endcap"]: 

4081 # Write the c:noEndCap element. 

4082 self._write_no_end_cap() 

4083 

4084 if error_bars["type"] == "stdErr": 

4085 # Don't need to write a c:errValType tag. 

4086 pass 

4087 elif error_bars["type"] == "cust": 

4088 # Write the custom error tags. 

4089 self._write_custom_error(error_bars) 

4090 else: 

4091 # Write the c:val element. 

4092 self._write_error_val(error_bars["value"]) 

4093 

4094 # Write the c:spPr element. 

4095 self._write_sp_pr(error_bars) 

4096 

4097 self._xml_end_tag("c:errBars") 

4098 

4099 def _write_err_dir(self, val) -> None: 

4100 # Write the <c:errDir> element. 

4101 

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

4103 

4104 self._xml_empty_tag("c:errDir", attributes) 

4105 

4106 def _write_err_bar_type(self, val) -> None: 

4107 # Write the <c:errBarType> element. 

4108 

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

4110 

4111 self._xml_empty_tag("c:errBarType", attributes) 

4112 

4113 def _write_err_val_type(self, val) -> None: 

4114 # Write the <c:errValType> element. 

4115 

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

4117 

4118 self._xml_empty_tag("c:errValType", attributes) 

4119 

4120 def _write_no_end_cap(self) -> None: 

4121 # Write the <c:noEndCap> element. 

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

4123 

4124 self._xml_empty_tag("c:noEndCap", attributes) 

4125 

4126 def _write_error_val(self, val) -> None: 

4127 # Write the <c:val> element for error bars. 

4128 

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

4130 

4131 self._xml_empty_tag("c:val", attributes) 

4132 

4133 def _write_custom_error(self, error_bars) -> None: 

4134 # Write the custom error bars tags. 

4135 

4136 if error_bars["plus_values"]: 

4137 # Write the c:plus element. 

4138 self._xml_start_tag("c:plus") 

4139 

4140 if isinstance(error_bars["plus_values"], list): 

4141 self._write_num_lit(error_bars["plus_values"]) 

4142 else: 

4143 self._write_num_ref( 

4144 error_bars["plus_values"], error_bars["plus_data"], "num" 

4145 ) 

4146 self._xml_end_tag("c:plus") 

4147 

4148 if error_bars["minus_values"]: 

4149 # Write the c:minus element. 

4150 self._xml_start_tag("c:minus") 

4151 

4152 if isinstance(error_bars["minus_values"], list): 

4153 self._write_num_lit(error_bars["minus_values"]) 

4154 else: 

4155 self._write_num_ref( 

4156 error_bars["minus_values"], error_bars["minus_data"], "num" 

4157 ) 

4158 self._xml_end_tag("c:minus") 

4159 

4160 def _write_num_lit(self, data) -> None: 

4161 # Write the <c:numLit> element for literal number list elements. 

4162 count = len(data) 

4163 

4164 # Write the c:numLit element. 

4165 self._xml_start_tag("c:numLit") 

4166 

4167 # Write the c:formatCode element. 

4168 self._write_format_code("General") 

4169 

4170 # Write the c:ptCount element. 

4171 self._write_pt_count(count) 

4172 

4173 for i in range(count): 

4174 token = data[i] 

4175 

4176 if token is None: 

4177 continue 

4178 

4179 try: 

4180 float(token) 

4181 except ValueError: 

4182 # Write non-numeric data as 0. 

4183 token = 0 

4184 

4185 # Write the c:pt element. 

4186 self._write_pt(i, token) 

4187 

4188 self._xml_end_tag("c:numLit") 

4189 

4190 def _write_up_down_bars(self) -> None: 

4191 # Write the <c:upDownBars> element. 

4192 up_down_bars = self.up_down_bars 

4193 

4194 if up_down_bars is None: 

4195 return 

4196 

4197 self._xml_start_tag("c:upDownBars") 

4198 

4199 # Write the c:gapWidth element. 

4200 self._write_gap_width(150) 

4201 

4202 # Write the c:upBars element. 

4203 self._write_up_bars(up_down_bars.get("up")) 

4204 

4205 # Write the c:downBars element. 

4206 self._write_down_bars(up_down_bars.get("down")) 

4207 

4208 self._xml_end_tag("c:upDownBars") 

4209 

4210 def _write_gap_width(self, val) -> None: 

4211 # Write the <c:gapWidth> element. 

4212 

4213 if val is None: 

4214 return 

4215 

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

4217 

4218 self._xml_empty_tag("c:gapWidth", attributes) 

4219 

4220 def _write_up_bars(self, bar_format) -> None: 

4221 # Write the <c:upBars> element. 

4222 

4223 if bar_format["line"] and bar_format["line"]["defined"]: 

4224 self._xml_start_tag("c:upBars") 

4225 

4226 # Write the c:spPr element. 

4227 self._write_sp_pr(bar_format) 

4228 

4229 self._xml_end_tag("c:upBars") 

4230 else: 

4231 self._xml_empty_tag("c:upBars") 

4232 

4233 def _write_down_bars(self, bar_format) -> None: 

4234 # Write the <c:downBars> element. 

4235 

4236 if bar_format["line"] and bar_format["line"]["defined"]: 

4237 self._xml_start_tag("c:downBars") 

4238 

4239 # Write the c:spPr element. 

4240 self._write_sp_pr(bar_format) 

4241 

4242 self._xml_end_tag("c:downBars") 

4243 else: 

4244 self._xml_empty_tag("c:downBars") 

4245 

4246 def _write_disp_units(self, units, display) -> None: 

4247 # Write the <c:dispUnits> element. 

4248 

4249 if not units: 

4250 return 

4251 

4252 attributes = [("val", units)] 

4253 

4254 self._xml_start_tag("c:dispUnits") 

4255 self._xml_empty_tag("c:builtInUnit", attributes) 

4256 

4257 if display: 

4258 self._xml_start_tag("c:dispUnitsLbl") 

4259 self._xml_empty_tag("c:layout") 

4260 self._xml_end_tag("c:dispUnitsLbl") 

4261 

4262 self._xml_end_tag("c:dispUnits") 

4263 

4264 def _write_a_grad_fill(self, gradient) -> None: 

4265 # Write the <a:gradFill> element. 

4266 

4267 attributes = [("flip", "none"), ("rotWithShape", "1")] 

4268 

4269 if gradient["type"] == "linear": 

4270 attributes = [] 

4271 

4272 self._xml_start_tag("a:gradFill", attributes) 

4273 

4274 # Write the a:gsLst element. 

4275 self._write_a_gs_lst(gradient) 

4276 

4277 if gradient["type"] == "linear": 

4278 # Write the a:lin element. 

4279 self._write_a_lin(gradient["angle"]) 

4280 else: 

4281 # Write the a:path element. 

4282 self._write_a_path(gradient["type"]) 

4283 

4284 # Write the a:tileRect element. 

4285 self._write_a_tile_rect(gradient["type"]) 

4286 

4287 self._xml_end_tag("a:gradFill") 

4288 

4289 def _write_a_gs_lst(self, gradient) -> None: 

4290 # Write the <a:gsLst> element. 

4291 positions = gradient["positions"] 

4292 colors = gradient["colors"] 

4293 

4294 self._xml_start_tag("a:gsLst") 

4295 

4296 for i, color in enumerate(colors): 

4297 pos = int(positions[i] * 1000) 

4298 attributes = [("pos", pos)] 

4299 self._xml_start_tag("a:gs", attributes) 

4300 

4301 self._write_color(color) 

4302 

4303 self._xml_end_tag("a:gs") 

4304 

4305 self._xml_end_tag("a:gsLst") 

4306 

4307 def _write_a_lin(self, angle) -> None: 

4308 # Write the <a:lin> element. 

4309 

4310 angle = int(60000 * angle) 

4311 

4312 attributes = [ 

4313 ("ang", angle), 

4314 ("scaled", "0"), 

4315 ] 

4316 

4317 self._xml_empty_tag("a:lin", attributes) 

4318 

4319 def _write_a_path(self, gradient_type) -> None: 

4320 # Write the <a:path> element. 

4321 

4322 attributes = [("path", gradient_type)] 

4323 

4324 self._xml_start_tag("a:path", attributes) 

4325 

4326 # Write the a:fillToRect element. 

4327 self._write_a_fill_to_rect(gradient_type) 

4328 

4329 self._xml_end_tag("a:path") 

4330 

4331 def _write_a_fill_to_rect(self, gradient_type) -> None: 

4332 # Write the <a:fillToRect> element. 

4333 

4334 if gradient_type == "shape": 

4335 attributes = [ 

4336 ("l", "50000"), 

4337 ("t", "50000"), 

4338 ("r", "50000"), 

4339 ("b", "50000"), 

4340 ] 

4341 else: 

4342 attributes = [ 

4343 ("l", "100000"), 

4344 ("t", "100000"), 

4345 ] 

4346 

4347 self._xml_empty_tag("a:fillToRect", attributes) 

4348 

4349 def _write_a_tile_rect(self, gradient_type) -> None: 

4350 # Write the <a:tileRect> element. 

4351 

4352 if gradient_type == "shape": 

4353 attributes = [] 

4354 else: 

4355 attributes = [ 

4356 ("r", "-100000"), 

4357 ("b", "-100000"), 

4358 ] 

4359 

4360 self._xml_empty_tag("a:tileRect", attributes) 

4361 

4362 def _write_a_patt_fill(self, pattern) -> None: 

4363 # Write the <a:pattFill> element. 

4364 

4365 attributes = [("prst", pattern["pattern"])] 

4366 

4367 self._xml_start_tag("a:pattFill", attributes) 

4368 

4369 # Write the a:fgClr element. 

4370 self._write_a_fg_clr(pattern["fg_color"]) 

4371 

4372 # Write the a:bgClr element. 

4373 self._write_a_bg_clr(pattern["bg_color"]) 

4374 

4375 self._xml_end_tag("a:pattFill") 

4376 

4377 def _write_a_fg_clr(self, color: Color) -> None: 

4378 # Write the <a:fgClr> element. 

4379 self._xml_start_tag("a:fgClr") 

4380 self._write_color(color) 

4381 self._xml_end_tag("a:fgClr") 

4382 

4383 def _write_a_bg_clr(self, color: Color) -> None: 

4384 # Write the <a:bgClr> element. 

4385 self._xml_start_tag("a:bgClr") 

4386 self._write_color(color) 

4387 self._xml_end_tag("a:bgClr")