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

1991 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 import xmlwriter 

16from xlsxwriter.chart_title import ChartTitle 

17from xlsxwriter.color import Color, ColorTypes 

18from xlsxwriter.shape import Shape 

19from xlsxwriter.utility import ( 

20 _datetime_to_excel_datetime, 

21 _supported_datetime, 

22 quote_sheetname, 

23 xl_range_formula, 

24 xl_rowcol_to_cell, 

25) 

26 

27 

28class Chart(xmlwriter.XMLwriter): 

29 """ 

30 A class for writing the Excel XLSX Chart file. 

31 

32 

33 """ 

34 

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

36 # 

37 # Public API. 

38 # 

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

40 

41 def __init__(self) -> None: 

42 """ 

43 Constructor. 

44 

45 """ 

46 

47 super().__init__() 

48 

49 self.subtype = None 

50 self.orientation = 0x0 

51 self.series = [] 

52 self.embedded = 0 

53 self.id = -1 

54 self.series_index = 0 

55 self.style_id = 2 

56 self.axis_ids = [] 

57 self.axis2_ids = [] 

58 self.cat_has_num_fmt = False 

59 self.requires_category = False 

60 self.legend = {} 

61 self.cat_axis_position = "b" 

62 self.val_axis_position = "l" 

63 self.formula_ids = {} 

64 self.formula_data = [] 

65 self.horiz_cat_axis = 0 

66 self.horiz_val_axis = 1 

67 self.protection = 0 

68 self.chartarea = {} 

69 self.plotarea = {} 

70 self.x_axis = {} 

71 self.y_axis = {} 

72 self.y2_axis = {} 

73 self.x2_axis = {} 

74 self.chart_name = "" 

75 self.show_blanks = "gap" 

76 self.show_na_as_empty = False 

77 self.show_hidden = False 

78 self.show_crosses = True 

79 self.width = 480 

80 self.height = 288 

81 self.x_scale = 1 

82 self.y_scale = 1 

83 self.x_offset = 0 

84 self.y_offset = 0 

85 self.table = None 

86 self.cross_between = "between" 

87 self.default_marker = None 

88 self.series_gap_1 = None 

89 self.series_gap_2 = None 

90 self.series_overlap_1 = None 

91 self.series_overlap_2 = None 

92 self.drop_lines = None 

93 self.hi_low_lines = None 

94 self.up_down_bars = None 

95 self.smooth_allowed = False 

96 self.title = ChartTitle() 

97 

98 self.date_category = False 

99 self.date_1904 = False 

100 self.remove_timezone = False 

101 self.label_positions = {} 

102 self.label_position_default = "" 

103 self.already_inserted = False 

104 self.combined = None 

105 self.is_secondary = False 

106 self.warn_sheetname = True 

107 self._set_default_properties() 

108 self.fill = {} 

109 

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

111 """ 

112 Add a data series to a chart. 

113 

114 Args: 

115 options: A dictionary of chart series options. 

116 

117 Returns: 

118 Nothing. 

119 

120 """ 

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

122 if options is None: 

123 options = {} 

124 

125 # Check that the required input has been specified. 

126 if "values" not in options: 

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

128 return 

129 

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

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

132 return 

133 

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

135 warn( 

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

137 "Excel Chart is 255" 

138 ) 

139 return 

140 

141 # Convert list into a formula string. 

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

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

144 

145 # Switch name and name_formula parameters if required. 

146 name, name_formula = self._process_names( 

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

148 ) 

149 

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

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

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

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

154 

155 # Set the line properties for the series. 

156 line = Shape._get_line_properties(options) 

157 

158 # Set the fill properties for the series. 

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

160 

161 # Set the pattern fill properties for the series. 

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

163 

164 # Set the gradient fill properties for the series. 

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

166 

167 # Pattern fill overrides solid fill. 

168 if pattern: 

169 self.fill = None 

170 

171 # Gradient fill overrides the solid and pattern fill. 

172 if gradient: 

173 pattern = None 

174 fill = None 

175 

176 # Set the marker properties for the series. 

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

178 

179 # Set the trendline properties for the series. 

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

181 

182 # Set the line smooth property for the series. 

183 smooth = options.get("smooth") 

184 

185 # Set the error bars properties for the series. 

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

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

188 

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

190 

191 # Set the point properties for the series. 

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

193 

194 # Set the labels properties for the series. 

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

196 

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

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

199 inverted_color = options.get("invert_if_negative_color") 

200 

201 if inverted_color: 

202 inverted_color = Color._from_value(inverted_color) 

203 

204 # Set the secondary axis properties. 

205 x2_axis = options.get("x2_axis") 

206 y2_axis = options.get("y2_axis") 

207 

208 # Store secondary status for combined charts. 

209 if x2_axis or y2_axis: 

210 self.is_secondary = True 

211 

212 # Set the gap for Bar/Column charts. 

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

214 if y2_axis: 

215 self.series_gap_2 = options["gap"] 

216 else: 

217 self.series_gap_1 = options["gap"] 

218 

219 # Set the overlap for Bar/Column charts. 

220 if options.get("overlap"): 

221 if y2_axis: 

222 self.series_overlap_2 = options["overlap"] 

223 else: 

224 self.series_overlap_1 = options["overlap"] 

225 

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

227 series = { 

228 "values": values, 

229 "categories": categories, 

230 "name": name, 

231 "name_formula": name_formula, 

232 "name_id": name_id, 

233 "val_data_id": val_id, 

234 "cat_data_id": cat_id, 

235 "line": line, 

236 "fill": fill, 

237 "pattern": pattern, 

238 "gradient": gradient, 

239 "marker": marker, 

240 "trendline": trendline, 

241 "labels": labels, 

242 "invert_if_neg": invert_if_neg, 

243 "inverted_color": inverted_color, 

244 "x2_axis": x2_axis, 

245 "y2_axis": y2_axis, 

246 "points": points, 

247 "error_bars": error_bars, 

248 "smooth": smooth, 

249 } 

250 

251 self.series.append(series) 

252 

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

254 """ 

255 Set the chart X axis options. 

256 

257 Args: 

258 options: A dictionary of axis options. 

259 

260 Returns: 

261 Nothing. 

262 

263 """ 

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

265 

266 self.x_axis = axis 

267 

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

269 """ 

270 Set the chart Y axis options. 

271 

272 Args: 

273 options: A dictionary of axis options. 

274 

275 Returns: 

276 Nothing. 

277 

278 """ 

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

280 

281 self.y_axis = axis 

282 

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

284 """ 

285 Set the chart secondary X axis options. 

286 

287 Args: 

288 options: A dictionary of axis options. 

289 

290 Returns: 

291 Nothing. 

292 

293 """ 

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

295 

296 self.x2_axis = axis 

297 

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

299 """ 

300 Set the chart secondary Y axis options. 

301 

302 Args: 

303 options: A dictionary of axis options. 

304 

305 Returns: 

306 Nothing. 

307 

308 """ 

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

310 

311 self.y2_axis = axis 

312 

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

314 """ 

315 Set the chart title options. 

316 

317 Args: 

318 options: A dictionary of chart title options. 

319 

320 Returns: 

321 Nothing. 

322 

323 """ 

324 if options is None: 

325 options = {} 

326 

327 name, name_formula = self._process_names( 

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

329 ) 

330 

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

332 

333 # Update the main chart title. 

334 self.title.name = name 

335 self.title.formula = name_formula 

336 self.title.data_id = data_id 

337 

338 # Set the font properties if present. 

339 if options.get("font"): 

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

341 else: 

342 # For backward/axis compatibility. 

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

344 

345 # Set the line properties. 

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

347 

348 # Set the fill properties. 

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

350 

351 # Set the gradient properties. 

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

353 

354 # Set the layout. 

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

356 

357 # Set the title overlay option. 

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

359 

360 # Set the automatic title option. 

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

362 

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

364 """ 

365 Set the chart legend options. 

366 

367 Args: 

368 options: A dictionary of chart legend options. 

369 

370 Returns: 

371 Nothing. 

372 """ 

373 # Convert the user defined properties to internal properties. 

374 self.legend = self._get_legend_properties(options) 

375 

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

377 """ 

378 Set the chart plot area options. 

379 

380 Args: 

381 options: A dictionary of chart plot area options. 

382 

383 Returns: 

384 Nothing. 

385 """ 

386 # Convert the user defined properties to internal properties. 

387 self.plotarea = self._get_area_properties(options) 

388 

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

390 """ 

391 Set the chart area options. 

392 

393 Args: 

394 options: A dictionary of chart area options. 

395 

396 Returns: 

397 Nothing. 

398 """ 

399 # Convert the user defined properties to internal properties. 

400 self.chartarea = self._get_area_properties(options) 

401 

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

403 """ 

404 Set the chart style type. 

405 

406 Args: 

407 style_id: An int representing the chart style. 

408 

409 Returns: 

410 Nothing. 

411 """ 

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

413 if style_id is None: 

414 style_id = 2 

415 

416 if style_id < 1 or style_id > 48: 

417 style_id = 2 

418 

419 self.style_id = style_id 

420 

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

422 """ 

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

424 

425 Args: 

426 option: A string representing the display option. 

427 

428 Returns: 

429 Nothing. 

430 """ 

431 if not option: 

432 return 

433 

434 valid_options = { 

435 "gap": 1, 

436 "zero": 1, 

437 "span": 1, 

438 } 

439 

440 if option not in valid_options: 

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

442 return 

443 

444 self.show_blanks = option 

445 

446 def show_na_as_empty_cell(self) -> None: 

447 """ 

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

449 

450 Args: 

451 None. 

452 

453 Returns: 

454 Nothing. 

455 """ 

456 self.show_na_as_empty = True 

457 

458 def show_hidden_data(self) -> None: 

459 """ 

460 Display data on charts from hidden rows or columns. 

461 

462 Args: 

463 None. 

464 

465 Returns: 

466 Nothing. 

467 """ 

468 self.show_hidden = True 

469 

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

471 """ 

472 Set size or scale of the chart. 

473 

474 Args: 

475 options: A dictionary of chart size options. 

476 

477 Returns: 

478 Nothing. 

479 """ 

480 if options is None: 

481 options = {} 

482 

483 # Set dimensions or scale for the chart. 

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

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

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

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

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

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

490 

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

492 """ 

493 Set properties for an axis data table. 

494 

495 Args: 

496 options: A dictionary of axis table options. 

497 

498 Returns: 

499 Nothing. 

500 

501 """ 

502 if options is None: 

503 options = {} 

504 

505 table = {} 

506 

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

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

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

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

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

512 

513 self.table = table 

514 

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

516 """ 

517 Set properties for the chart up-down bars. 

518 

519 Args: 

520 options: A dictionary of options. 

521 

522 Returns: 

523 Nothing. 

524 

525 """ 

526 if options is None: 

527 options = {} 

528 

529 # Defaults. 

530 up_line = None 

531 up_fill = None 

532 down_line = None 

533 down_fill = None 

534 

535 # Set properties for 'up' bar. 

536 if options.get("up"): 

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

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

539 

540 # Set properties for 'down' bar. 

541 if options.get("down"): 

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

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

544 

545 self.up_down_bars = { 

546 "up": { 

547 "line": up_line, 

548 "fill": up_fill, 

549 }, 

550 "down": { 

551 "line": down_line, 

552 "fill": down_fill, 

553 }, 

554 } 

555 

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

557 """ 

558 Set properties for the chart drop lines. 

559 

560 Args: 

561 options: A dictionary of options. 

562 

563 Returns: 

564 Nothing. 

565 

566 """ 

567 if options is None: 

568 options = {} 

569 

570 line = Shape._get_line_properties(options) 

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

572 

573 # Set the pattern fill properties for the series. 

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

575 

576 # Set the gradient fill properties for the series. 

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

578 

579 # Pattern fill overrides solid fill. 

580 if pattern: 

581 self.fill = None 

582 

583 # Gradient fill overrides the solid and pattern fill. 

584 if gradient: 

585 pattern = None 

586 fill = None 

587 

588 self.drop_lines = { 

589 "line": line, 

590 "fill": fill, 

591 "pattern": pattern, 

592 "gradient": gradient, 

593 } 

594 

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

596 """ 

597 Set properties for the chart high-low lines. 

598 

599 Args: 

600 options: A dictionary of options. 

601 

602 Returns: 

603 Nothing. 

604 

605 """ 

606 if options is None: 

607 options = {} 

608 

609 line = Shape._get_line_properties(options) 

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

611 

612 # Set the pattern fill properties for the series. 

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

614 

615 # Set the gradient fill properties for the series. 

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

617 

618 # Pattern fill overrides solid fill. 

619 if pattern: 

620 self.fill = None 

621 

622 # Gradient fill overrides the solid and pattern fill. 

623 if gradient: 

624 pattern = None 

625 fill = None 

626 

627 self.hi_low_lines = { 

628 "line": line, 

629 "fill": fill, 

630 "pattern": pattern, 

631 "gradient": gradient, 

632 } 

633 

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

635 """ 

636 Create a combination chart with a secondary chart. 

637 

638 Args: 

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

640 

641 Returns: 

642 Nothing. 

643 

644 """ 

645 if chart is None: 

646 return 

647 

648 self.combined = chart 

649 

650 ########################################################################### 

651 # 

652 # Private API. 

653 # 

654 ########################################################################### 

655 

656 def _assemble_xml_file(self) -> None: 

657 # Assemble and write the XML file. 

658 

659 # Write the XML declaration. 

660 self._xml_declaration() 

661 

662 # Write the c:chartSpace element. 

663 self._write_chart_space() 

664 

665 # Write the c:lang element. 

666 self._write_lang() 

667 

668 # Write the c:style element. 

669 self._write_style() 

670 

671 # Write the c:protection element. 

672 self._write_protection() 

673 

674 # Write the c:chart element. 

675 self._write_chart() 

676 

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

678 self._write_sp_pr(self.chartarea) 

679 

680 # Write the c:printSettings element. 

681 if self.embedded: 

682 self._write_print_settings() 

683 

684 # Close the worksheet tag. 

685 self._xml_end_tag("c:chartSpace") 

686 # Close the file. 

687 self._xml_close() 

688 

689 def _convert_axis_args(self, axis, user_options): 

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

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

692 options.update(user_options) 

693 

694 axis = { 

695 "defaults": axis["defaults"], 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

714 "text_axis": False, 

715 "title": ChartTitle(), 

716 } 

717 

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

719 

720 # Convert the display units. 

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

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

723 

724 # Map major_gridlines properties. 

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

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

727 options["major_gridlines"] 

728 ) 

729 

730 # Map minor_gridlines properties. 

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

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

733 options["minor_gridlines"] 

734 ) 

735 

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

737 if axis.get("position"): 

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

739 

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

741 if axis.get("position_axis"): 

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

743 axis["position_axis"] = "midCat" 

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

745 # Doesn't need to be modified. 

746 pass 

747 else: 

748 # Otherwise use the default value. 

749 axis["position_axis"] = None 

750 

751 # Set the category axis as a date axis. 

752 if options.get("date_axis"): 

753 self.date_category = True 

754 

755 # Set the category axis as a text axis. 

756 if options.get("text_axis"): 

757 self.date_category = False 

758 axis["text_axis"] = True 

759 

760 # Convert datetime args if required. 

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

762 axis["min"] = _datetime_to_excel_datetime( 

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

764 ) 

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

766 axis["max"] = _datetime_to_excel_datetime( 

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

768 ) 

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

770 axis["crossing"] = _datetime_to_excel_datetime( 

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

772 ) 

773 

774 # Set the font properties if present. 

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

776 

777 # Set the line properties for the axis. 

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

779 

780 # Set the fill properties for the axis. 

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

782 

783 # Set the pattern fill properties for the series. 

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

785 

786 # Set the gradient fill properties for the series. 

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

788 

789 # Pattern fill overrides solid fill. 

790 if axis.get("pattern"): 

791 axis["fill"] = None 

792 

793 # Gradient fill overrides the solid and pattern fill. 

794 if axis.get("gradient"): 

795 axis["pattern"] = None 

796 axis["fill"] = None 

797 

798 # Set the tick marker types. 

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

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

801 

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

803 name, name_formula = self._process_names( 

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

805 ) 

806 

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

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

809 

810 # Set the title properties. 

811 axis["title"].name = name 

812 axis["title"].formula = name_formula 

813 axis["title"].data_id = data_id 

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

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

816 options.get("name_layout"), True 

817 ) 

818 

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

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

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

822 

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

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

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

826 options.get("name_pattern") 

827 ) 

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

829 options.get("name_gradient") 

830 ) 

831 

832 return axis 

833 

834 def _convert_font_args(self, options): 

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

836 if not options: 

837 return {} 

838 

839 font = { 

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

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

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

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

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

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

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

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

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

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

850 } 

851 

852 # Convert font size units. 

853 if font["size"]: 

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

855 

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

857 if font["rotation"]: 

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

859 

860 if font.get("color"): 

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

862 

863 return font 

864 

865 def _list_to_formula(self, data): 

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

867 

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

869 if not isinstance(data, list): 

870 # Check for unquoted sheetnames. 

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

872 warn( 

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

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

875 ) 

876 return data 

877 

878 formula = xl_range_formula(*data) 

879 

880 return formula 

881 

882 def _process_names(self, name, name_formula): 

883 # Switch name and name_formula parameters if required. 

884 

885 if name is not None: 

886 if isinstance(name, list): 

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

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

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

890 name = "" 

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

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

893 name_formula = name 

894 name = "" 

895 

896 return name, name_formula 

897 

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

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

900 

901 # Check for no data in the series. 

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

903 return "none" 

904 

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

906 return "multi_str" 

907 

908 # Determine if data is numeric or strings. 

909 for token in data: 

910 if token is None: 

911 continue 

912 

913 # Check for strings that would evaluate to float like 

914 # '1.1_1' of ' 1'. 

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

916 # Assume entire data series is string data. 

917 return "str" 

918 

919 try: 

920 float(token) 

921 except ValueError: 

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

923 return "str" 

924 

925 # The series data was all numeric. 

926 return "num" 

927 

928 def _get_data_id(self, formula, data): 

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

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

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

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

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

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

935 

936 # Ignore series without a range formula. 

937 if not formula: 

938 return None 

939 

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

941 if formula.startswith("="): 

942 formula = formula.lstrip("=") 

943 

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

945 # in a separate array with the same id. 

946 if formula not in self.formula_ids: 

947 # Haven't seen this formula before. 

948 formula_id = len(self.formula_data) 

949 

950 self.formula_data.append(data) 

951 self.formula_ids[formula] = formula_id 

952 else: 

953 # Formula already seen. Return existing id. 

954 formula_id = self.formula_ids[formula] 

955 

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

957 if self.formula_data[formula_id] is None: 

958 self.formula_data[formula_id] = data 

959 

960 return formula_id 

961 

962 def _get_marker_properties(self, marker): 

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

964 

965 if not marker: 

966 return None 

967 

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

969 marker = copy.deepcopy(marker) 

970 

971 types = { 

972 "automatic": "automatic", 

973 "none": "none", 

974 "square": "square", 

975 "diamond": "diamond", 

976 "triangle": "triangle", 

977 "x": "x", 

978 "star": "star", 

979 "dot": "dot", 

980 "short_dash": "dot", 

981 "dash": "dash", 

982 "long_dash": "dash", 

983 "circle": "circle", 

984 "plus": "plus", 

985 "picture": "picture", 

986 } 

987 

988 # Check for valid types. 

989 marker_type = marker.get("type") 

990 

991 if marker_type is not None: 

992 if marker_type in types: 

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

994 else: 

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

996 return None 

997 

998 # Set the line properties for the marker. 

999 line = Shape._get_line_properties(marker) 

1000 

1001 # Set the fill properties for the marker. 

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

1003 

1004 # Set the pattern fill properties for the series. 

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

1006 

1007 # Set the gradient fill properties for the series. 

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

1009 

1010 # Pattern fill overrides solid fill. 

1011 if pattern: 

1012 self.fill = None 

1013 

1014 # Gradient fill overrides the solid and pattern fill. 

1015 if gradient: 

1016 pattern = None 

1017 fill = None 

1018 

1019 marker["line"] = line 

1020 marker["fill"] = fill 

1021 marker["pattern"] = pattern 

1022 marker["gradient"] = gradient 

1023 

1024 return marker 

1025 

1026 def _get_trendline_properties(self, trendline): 

1027 # Convert user trendline properties to structure required internally. 

1028 

1029 if not trendline: 

1030 return None 

1031 

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

1033 trendline = copy.deepcopy(trendline) 

1034 

1035 types = { 

1036 "exponential": "exp", 

1037 "linear": "linear", 

1038 "log": "log", 

1039 "moving_average": "movingAvg", 

1040 "polynomial": "poly", 

1041 "power": "power", 

1042 } 

1043 

1044 # Check the trendline type. 

1045 trend_type = trendline.get("type") 

1046 

1047 if trend_type in types: 

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

1049 else: 

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

1051 return None 

1052 

1053 # Set the line properties for the trendline. 

1054 line = Shape._get_line_properties(trendline) 

1055 

1056 # Set the fill properties for the trendline. 

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

1058 

1059 # Set the pattern fill properties for the trendline. 

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

1061 

1062 # Set the gradient fill properties for the trendline. 

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

1064 

1065 # Set the format properties for the trendline label. 

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

1067 

1068 # Pattern fill overrides solid fill. 

1069 if pattern: 

1070 self.fill = None 

1071 

1072 # Gradient fill overrides the solid and pattern fill. 

1073 if gradient: 

1074 pattern = None 

1075 fill = None 

1076 

1077 trendline["line"] = line 

1078 trendline["fill"] = fill 

1079 trendline["pattern"] = pattern 

1080 trendline["gradient"] = gradient 

1081 trendline["label"] = label 

1082 

1083 return trendline 

1084 

1085 def _get_trendline_label_properties(self, label): 

1086 # Convert user trendline properties to structure required internally. 

1087 

1088 if not label: 

1089 return {} 

1090 

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

1092 label = copy.deepcopy(label) 

1093 

1094 # Set the font properties if present. 

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

1096 

1097 # Set the line properties for the label. 

1098 line = Shape._get_line_properties(label) 

1099 

1100 # Set the fill properties for the label. 

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

1102 

1103 # Set the pattern fill properties for the label. 

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

1105 

1106 # Set the gradient fill properties for the label. 

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

1108 

1109 # Pattern fill overrides solid fill. 

1110 if pattern: 

1111 self.fill = None 

1112 

1113 # Gradient fill overrides the solid and pattern fill. 

1114 if gradient: 

1115 pattern = None 

1116 fill = None 

1117 

1118 label["font"] = font 

1119 label["line"] = line 

1120 label["fill"] = fill 

1121 label["pattern"] = pattern 

1122 label["gradient"] = gradient 

1123 

1124 return label 

1125 

1126 def _get_error_bars_props(self, options): 

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

1128 if not options: 

1129 return {} 

1130 

1131 # Default values. 

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

1133 

1134 types = { 

1135 "fixed": "fixedVal", 

1136 "percentage": "percentage", 

1137 "standard_deviation": "stdDev", 

1138 "standard_error": "stdErr", 

1139 "custom": "cust", 

1140 } 

1141 

1142 # Check the error bars type. 

1143 error_type = options["type"] 

1144 

1145 if error_type in types: 

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

1147 else: 

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

1149 return {} 

1150 

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

1152 if "value" in options: 

1153 error_bars["value"] = options["value"] 

1154 

1155 # Set the end-cap style. 

1156 if "end_style" in options: 

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

1158 

1159 # Set the error bar direction. 

1160 if "direction" in options: 

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

1162 error_bars["direction"] = "minus" 

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

1164 error_bars["direction"] = "plus" 

1165 else: 

1166 # Default to 'both'. 

1167 pass 

1168 

1169 # Set any custom values. 

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

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

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

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

1174 

1175 # Set the line properties for the error bars. 

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

1177 

1178 return error_bars 

1179 

1180 def _get_gridline_properties(self, options): 

1181 # Convert user gridline properties to structure required internally. 

1182 

1183 # Set the visible property for the gridline. 

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

1185 

1186 # Set the line properties for the gridline. 

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

1188 

1189 return gridline 

1190 

1191 def _get_labels_properties(self, labels): 

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

1193 

1194 if not labels: 

1195 return None 

1196 

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

1198 labels = copy.deepcopy(labels) 

1199 

1200 # Map user defined label positions to Excel positions. 

1201 position = labels.get("position") 

1202 

1203 if position: 

1204 if position in self.label_positions: 

1205 if position == self.label_position_default: 

1206 labels["position"] = None 

1207 else: 

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

1209 else: 

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

1211 return None 

1212 

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

1214 separator = labels.get("separator") 

1215 separators = { 

1216 ",": ", ", 

1217 ";": "; ", 

1218 ".": ". ", 

1219 "\n": "\n", 

1220 " ": " ", 

1221 } 

1222 

1223 if separator: 

1224 if separator in separators: 

1225 labels["separator"] = separators[separator] 

1226 else: 

1227 warn("Unsupported label separator") 

1228 return None 

1229 

1230 # Set the font properties if present. 

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

1232 

1233 # Set the line properties for the labels. 

1234 line = Shape._get_line_properties(labels) 

1235 

1236 # Set the fill properties for the labels. 

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

1238 

1239 # Set the pattern fill properties for the labels. 

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

1241 

1242 # Set the gradient fill properties for the labels. 

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

1244 

1245 # Pattern fill overrides solid fill. 

1246 if pattern: 

1247 self.fill = None 

1248 

1249 # Gradient fill overrides the solid and pattern fill. 

1250 if gradient: 

1251 pattern = None 

1252 fill = None 

1253 

1254 labels["line"] = line 

1255 labels["fill"] = fill 

1256 labels["pattern"] = pattern 

1257 labels["gradient"] = gradient 

1258 

1259 if labels.get("custom"): 

1260 for label in labels["custom"]: 

1261 if label is None: 

1262 continue 

1263 

1264 value = label.get("value") 

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

1266 label["formula"] = value 

1267 

1268 formula = label.get("formula") 

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

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

1271 

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

1273 label["data_id"] = data_id 

1274 

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

1276 

1277 # Set the line properties for the label. 

1278 line = Shape._get_line_properties(label) 

1279 

1280 # Set the fill properties for the label. 

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

1282 

1283 # Set the pattern fill properties for the label. 

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

1285 

1286 # Set the gradient fill properties for the label. 

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

1288 

1289 # Pattern fill overrides solid fill. 

1290 if pattern: 

1291 self.fill = None 

1292 

1293 # Gradient fill overrides the solid and pattern fill. 

1294 if gradient: 

1295 pattern = None 

1296 fill = None 

1297 

1298 # Map user defined label positions to Excel positions. 

1299 position = label.get("position") 

1300 

1301 if position: 

1302 if position in self.label_positions: 

1303 if position == self.label_position_default: 

1304 label["position"] = None 

1305 else: 

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

1307 else: 

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

1309 return None 

1310 

1311 label["line"] = line 

1312 label["fill"] = fill 

1313 label["pattern"] = pattern 

1314 label["gradient"] = gradient 

1315 

1316 return labels 

1317 

1318 def _get_area_properties(self, options): 

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

1320 area = {} 

1321 

1322 # Set the line properties for the chartarea. 

1323 line = Shape._get_line_properties(options) 

1324 

1325 # Set the fill properties for the chartarea. 

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

1327 

1328 # Set the pattern fill properties for the series. 

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

1330 

1331 # Set the gradient fill properties for the series. 

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

1333 

1334 # Pattern fill overrides solid fill. 

1335 if pattern: 

1336 self.fill = None 

1337 

1338 # Gradient fill overrides the solid and pattern fill. 

1339 if gradient: 

1340 pattern = None 

1341 fill = None 

1342 

1343 # Set the plotarea layout. 

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

1345 

1346 area["line"] = line 

1347 area["fill"] = fill 

1348 area["pattern"] = pattern 

1349 area["layout"] = layout 

1350 area["gradient"] = gradient 

1351 

1352 return area 

1353 

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

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

1356 legend = {} 

1357 

1358 if options is None: 

1359 options = {} 

1360 

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

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

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

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

1365 

1366 # Turn off the legend. 

1367 if options.get("none"): 

1368 legend["position"] = "none" 

1369 

1370 # Set the line properties for the legend. 

1371 line = Shape._get_line_properties(options) 

1372 

1373 # Set the fill properties for the legend. 

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

1375 

1376 # Set the pattern fill properties for the series. 

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

1378 

1379 # Set the gradient fill properties for the series. 

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

1381 

1382 # Pattern fill overrides solid fill. 

1383 if pattern: 

1384 self.fill = None 

1385 

1386 # Gradient fill overrides the solid and pattern fill. 

1387 if gradient: 

1388 pattern = None 

1389 fill = None 

1390 

1391 # Set the legend layout. 

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

1393 

1394 legend["line"] = line 

1395 legend["fill"] = fill 

1396 legend["pattern"] = pattern 

1397 legend["layout"] = layout 

1398 legend["gradient"] = gradient 

1399 

1400 return legend 

1401 

1402 def _get_layout_properties(self, args, is_text): 

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

1404 layout = {} 

1405 

1406 if not args: 

1407 return {} 

1408 

1409 if is_text: 

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

1411 else: 

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

1413 

1414 # Check for valid properties. 

1415 for key in args.keys(): 

1416 if key not in properties: 

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

1418 return {} 

1419 

1420 # Set the layout properties. 

1421 for prop in properties: 

1422 if prop not in args.keys(): 

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

1424 return {} 

1425 

1426 value = args[prop] 

1427 

1428 try: 

1429 float(value) 

1430 except ValueError: 

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

1432 return {} 

1433 

1434 if value < 0 or value > 1: 

1435 warn( 

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

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

1438 ) 

1439 return {} 

1440 

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

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

1443 

1444 return layout 

1445 

1446 def _get_points_properties(self, user_points): 

1447 # Convert user points properties to structure required internally. 

1448 points = [] 

1449 

1450 if not user_points: 

1451 return [] 

1452 

1453 for user_point in user_points: 

1454 point = {} 

1455 

1456 if user_point is not None: 

1457 # Set the line properties for the point. 

1458 line = Shape._get_line_properties(user_point) 

1459 

1460 # Set the fill properties for the chartarea. 

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

1462 

1463 # Set the pattern fill properties for the series. 

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

1465 

1466 # Set the gradient fill properties for the series. 

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

1468 

1469 # Pattern fill overrides solid fill. 

1470 if pattern: 

1471 self.fill = None 

1472 

1473 # Gradient fill overrides the solid and pattern fill. 

1474 if gradient: 

1475 pattern = None 

1476 fill = None 

1477 

1478 point["line"] = line 

1479 point["fill"] = fill 

1480 point["pattern"] = pattern 

1481 point["gradient"] = gradient 

1482 

1483 points.append(point) 

1484 

1485 return points 

1486 

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

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

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

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

1491 has_pattern = element.get("pattern") 

1492 has_gradient = element.get("gradient") 

1493 

1494 return has_fill or has_line or has_pattern or has_gradient 

1495 

1496 def _get_display_units(self, display_units): 

1497 # Convert user defined display units to internal units. 

1498 if not display_units: 

1499 return None 

1500 

1501 types = { 

1502 "hundreds": "hundreds", 

1503 "thousands": "thousands", 

1504 "ten_thousands": "tenThousands", 

1505 "hundred_thousands": "hundredThousands", 

1506 "millions": "millions", 

1507 "ten_millions": "tenMillions", 

1508 "hundred_millions": "hundredMillions", 

1509 "billions": "billions", 

1510 "trillions": "trillions", 

1511 } 

1512 

1513 if display_units in types: 

1514 display_units = types[display_units] 

1515 else: 

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

1517 return None 

1518 

1519 return display_units 

1520 

1521 def _get_tick_type(self, tick_type): 

1522 # Convert user defined display units to internal units. 

1523 if not tick_type: 

1524 return None 

1525 

1526 types = { 

1527 "outside": "out", 

1528 "inside": "in", 

1529 "none": "none", 

1530 "cross": "cross", 

1531 } 

1532 

1533 if tick_type in types: 

1534 tick_type = types[tick_type] 

1535 else: 

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

1537 return None 

1538 

1539 return tick_type 

1540 

1541 def _get_primary_axes_series(self): 

1542 # Returns series which use the primary axes. 

1543 primary_axes_series = [] 

1544 

1545 for series in self.series: 

1546 if not series["y2_axis"]: 

1547 primary_axes_series.append(series) 

1548 

1549 return primary_axes_series 

1550 

1551 def _get_secondary_axes_series(self): 

1552 # Returns series which use the secondary axes. 

1553 secondary_axes_series = [] 

1554 

1555 for series in self.series: 

1556 if series["y2_axis"]: 

1557 secondary_axes_series.append(series) 

1558 

1559 return secondary_axes_series 

1560 

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

1562 # Add unique ids for primary or secondary axes 

1563 chart_id = 5001 + int(self.id) 

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

1565 

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

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

1568 

1569 if args["primary_axes"]: 

1570 self.axis_ids.append(id1) 

1571 self.axis_ids.append(id2) 

1572 

1573 if not args["primary_axes"]: 

1574 self.axis2_ids.append(id1) 

1575 self.axis2_ids.append(id2) 

1576 

1577 def _set_default_properties(self) -> None: 

1578 # Setup the default properties for a chart. 

1579 

1580 self.x_axis["defaults"] = { 

1581 "num_format": "General", 

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

1583 } 

1584 

1585 self.y_axis["defaults"] = { 

1586 "num_format": "General", 

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

1588 } 

1589 

1590 self.x2_axis["defaults"] = { 

1591 "num_format": "General", 

1592 "label_position": "none", 

1593 "crossing": "max", 

1594 "visible": 0, 

1595 } 

1596 

1597 self.y2_axis["defaults"] = { 

1598 "num_format": "General", 

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

1600 "position": "right", 

1601 "visible": 1, 

1602 } 

1603 

1604 self.set_x_axis({}) 

1605 self.set_y_axis({}) 

1606 

1607 self.set_x2_axis({}) 

1608 self.set_y2_axis({}) 

1609 

1610 ########################################################################### 

1611 # 

1612 # XML methods. 

1613 # 

1614 ########################################################################### 

1615 

1616 def _write_chart_space(self) -> None: 

1617 # Write the <c:chartSpace> element. 

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

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

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

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

1622 

1623 attributes = [ 

1624 ("xmlns:c", xmlns_c), 

1625 ("xmlns:a", xmlns_a), 

1626 ("xmlns:r", xmlns_r), 

1627 ] 

1628 

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

1630 

1631 def _write_lang(self) -> None: 

1632 # Write the <c:lang> element. 

1633 val = "en-US" 

1634 

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

1636 

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

1638 

1639 def _write_style(self) -> None: 

1640 # Write the <c:style> element. 

1641 style_id = self.style_id 

1642 

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

1644 if style_id == 2: 

1645 return 

1646 

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

1648 

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

1650 

1651 def _write_chart(self) -> None: 

1652 # Write the <c:chart> element. 

1653 self._xml_start_tag("c:chart") 

1654 

1655 if self.title.is_hidden(): 

1656 # Turn off the title. 

1657 self._write_c_auto_title_deleted() 

1658 else: 

1659 # Write the chart title elements. 

1660 self._write_title(self.title) 

1661 

1662 # Write the c:plotArea element. 

1663 self._write_plot_area() 

1664 

1665 # Write the c:legend element. 

1666 self._write_legend() 

1667 

1668 # Write the c:plotVisOnly element. 

1669 self._write_plot_vis_only() 

1670 

1671 # Write the c:dispBlanksAs element. 

1672 self._write_disp_blanks_as() 

1673 

1674 # Write the c:extLst element. 

1675 if self.show_na_as_empty: 

1676 self._write_c_ext_lst_display_na() 

1677 

1678 self._xml_end_tag("c:chart") 

1679 

1680 def _write_disp_blanks_as(self) -> None: 

1681 # Write the <c:dispBlanksAs> element. 

1682 val = self.show_blanks 

1683 

1684 # Ignore the default value. 

1685 if val == "gap": 

1686 return 

1687 

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

1689 

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

1691 

1692 def _write_plot_area(self) -> None: 

1693 # Write the <c:plotArea> element. 

1694 self._xml_start_tag("c:plotArea") 

1695 

1696 # Write the c:layout element. 

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

1698 

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

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

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

1702 

1703 # Configure a combined chart if present. 

1704 second_chart = self.combined 

1705 if second_chart: 

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

1707 if second_chart.is_secondary: 

1708 second_chart.id = 1000 + self.id 

1709 else: 

1710 second_chart.id = self.id 

1711 

1712 # Share the same filehandle for writing. 

1713 second_chart.fh = self.fh 

1714 

1715 # Share series index with primary chart. 

1716 second_chart.series_index = self.series_index 

1717 

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

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

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

1721 

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

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

1724 

1725 if self.date_category: 

1726 self._write_date_axis(args) 

1727 else: 

1728 self._write_cat_axis(args) 

1729 

1730 self._write_val_axis(args) 

1731 

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

1733 args = { 

1734 "x_axis": self.x2_axis, 

1735 "y_axis": self.y2_axis, 

1736 "axis_ids": self.axis2_ids, 

1737 } 

1738 

1739 self._write_val_axis(args) 

1740 

1741 # Write the secondary axis for the secondary chart. 

1742 if second_chart and second_chart.is_secondary: 

1743 args = { 

1744 "x_axis": second_chart.x2_axis, 

1745 "y_axis": second_chart.y2_axis, 

1746 "axis_ids": second_chart.axis2_ids, 

1747 } 

1748 

1749 second_chart._write_val_axis(args) 

1750 

1751 if self.date_category: 

1752 self._write_date_axis(args) 

1753 else: 

1754 self._write_cat_axis(args) 

1755 

1756 # Write the c:dTable element. 

1757 self._write_d_table() 

1758 

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

1760 self._write_sp_pr(self.plotarea) 

1761 

1762 self._xml_end_tag("c:plotArea") 

1763 

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

1765 # Write the <c:layout> element. 

1766 

1767 if not layout: 

1768 # Automatic layout. 

1769 self._xml_empty_tag("c:layout") 

1770 else: 

1771 # User defined manual layout. 

1772 self._xml_start_tag("c:layout") 

1773 self._write_manual_layout(layout, layout_type) 

1774 self._xml_end_tag("c:layout") 

1775 

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

1777 # Write the <c:manualLayout> element. 

1778 self._xml_start_tag("c:manualLayout") 

1779 

1780 # Plotarea has a layoutTarget element. 

1781 if layout_type == "plot": 

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

1783 

1784 # Set the x, y positions. 

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

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

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

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

1789 

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

1791 if layout_type != "text": 

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

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

1794 

1795 self._xml_end_tag("c:manualLayout") 

1796 

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

1798 # pylint: disable=unused-argument 

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

1800 # by the subclasses. 

1801 return 

1802 

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

1804 # Write the <c:grouping> element. 

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

1806 

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

1808 

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

1810 # Write the series elements. 

1811 self._write_ser(series) 

1812 

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

1814 # Write the <c:ser> element. 

1815 index = self.series_index 

1816 self.series_index += 1 

1817 

1818 self._xml_start_tag("c:ser") 

1819 

1820 # Write the c:idx element. 

1821 self._write_idx(index) 

1822 

1823 # Write the c:order element. 

1824 self._write_order(index) 

1825 

1826 # Write the series name. 

1827 self._write_series_name(series) 

1828 

1829 # Write the c:spPr element. 

1830 self._write_sp_pr(series) 

1831 

1832 # Write the c:marker element. 

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

1834 

1835 # Write the c:invertIfNegative element. 

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

1837 

1838 # Write the c:dPt element. 

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

1840 

1841 # Write the c:dLbls element. 

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

1843 

1844 # Write the c:trendline element. 

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

1846 

1847 # Write the c:errBars element. 

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

1849 

1850 # Write the c:cat element. 

1851 self._write_cat(series) 

1852 

1853 # Write the c:val element. 

1854 self._write_val(series) 

1855 

1856 # Write the c:smooth element. 

1857 if self.smooth_allowed: 

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

1859 

1860 # Write the c:extLst element. 

1861 if series.get("inverted_color"): 

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

1863 

1864 self._xml_end_tag("c:ser") 

1865 

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

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

1868 

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

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

1871 

1872 attributes1 = [ 

1873 ("uri", uri), 

1874 ("xmlns:c14", xmlns_c_14), 

1875 ] 

1876 

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

1878 

1879 self._xml_start_tag("c:extLst") 

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

1881 self._xml_start_tag("c14:invertSolidFillFmt") 

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

1883 

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

1885 

1886 self._xml_end_tag("c14:spPr") 

1887 self._xml_end_tag("c14:invertSolidFillFmt") 

1888 self._xml_end_tag("c:ext") 

1889 self._xml_end_tag("c:extLst") 

1890 

1891 def _write_c_ext_lst_display_na(self) -> None: 

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

1893 

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

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

1896 

1897 attributes1 = [ 

1898 ("uri", uri), 

1899 ("xmlns:c16r3", xmlns_c_16), 

1900 ] 

1901 

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

1903 

1904 self._xml_start_tag("c:extLst") 

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

1906 self._xml_start_tag("c16r3:dataDisplayOptions16") 

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

1908 self._xml_end_tag("c16r3:dataDisplayOptions16") 

1909 self._xml_end_tag("c:ext") 

1910 self._xml_end_tag("c:extLst") 

1911 

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

1913 # Write the <c:idx> element. 

1914 

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

1916 

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

1918 

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

1920 # Write the <c:order> element. 

1921 

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

1923 

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

1925 

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

1927 # Write the series name. 

1928 

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

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

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

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

1933 

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

1935 # Write the <c:smooth> element. 

1936 

1937 if smooth: 

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

1939 

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

1941 # Write the <c:cat> element. 

1942 formula = series["categories"] 

1943 data_id = series["cat_data_id"] 

1944 data = None 

1945 

1946 if data_id is not None: 

1947 data = self.formula_data[data_id] 

1948 

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

1950 if not formula: 

1951 return 

1952 

1953 self._xml_start_tag("c:cat") 

1954 

1955 # Check the type of cached data. 

1956 cat_type = self._get_data_type(data) 

1957 

1958 if cat_type == "str": 

1959 self.cat_has_num_fmt = False 

1960 # Write the c:numRef element. 

1961 self._write_str_ref(formula, data, cat_type) 

1962 

1963 elif cat_type == "multi_str": 

1964 self.cat_has_num_fmt = False 

1965 # Write the c:numRef element. 

1966 self._write_multi_lvl_str_ref(formula, data) 

1967 

1968 else: 

1969 self.cat_has_num_fmt = True 

1970 # Write the c:numRef element. 

1971 self._write_num_ref(formula, data, cat_type) 

1972 

1973 self._xml_end_tag("c:cat") 

1974 

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

1976 # Write the <c:val> element. 

1977 formula = series["values"] 

1978 data_id = series["val_data_id"] 

1979 data = self.formula_data[data_id] 

1980 

1981 self._xml_start_tag("c:val") 

1982 

1983 # Unlike Cat axes data should only be numeric. 

1984 # Write the c:numRef element. 

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

1986 

1987 self._xml_end_tag("c:val") 

1988 

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

1990 # Write the <c:numRef> element. 

1991 self._xml_start_tag("c:numRef") 

1992 

1993 # Write the c:f element. 

1994 self._write_series_formula(formula) 

1995 

1996 if ref_type == "num": 

1997 # Write the c:numCache element. 

1998 self._write_num_cache(data) 

1999 elif ref_type == "str": 

2000 # Write the c:strCache element. 

2001 self._write_str_cache(data) 

2002 

2003 self._xml_end_tag("c:numRef") 

2004 

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

2006 # Write the <c:strRef> element. 

2007 

2008 self._xml_start_tag("c:strRef") 

2009 

2010 # Write the c:f element. 

2011 self._write_series_formula(formula) 

2012 

2013 if ref_type == "num": 

2014 # Write the c:numCache element. 

2015 self._write_num_cache(data) 

2016 elif ref_type == "str": 

2017 # Write the c:strCache element. 

2018 self._write_str_cache(data) 

2019 

2020 self._xml_end_tag("c:strRef") 

2021 

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

2023 # Write the <c:multiLvlStrRef> element. 

2024 

2025 if not data: 

2026 return 

2027 

2028 self._xml_start_tag("c:multiLvlStrRef") 

2029 

2030 # Write the c:f element. 

2031 self._write_series_formula(formula) 

2032 

2033 self._xml_start_tag("c:multiLvlStrCache") 

2034 

2035 # Write the c:ptCount element. 

2036 count = len(data[-1]) 

2037 self._write_pt_count(count) 

2038 

2039 for cat_data in reversed(data): 

2040 self._xml_start_tag("c:lvl") 

2041 

2042 for i, point in enumerate(cat_data): 

2043 # Write the c:pt element. 

2044 self._write_pt(i, point) 

2045 

2046 self._xml_end_tag("c:lvl") 

2047 

2048 self._xml_end_tag("c:multiLvlStrCache") 

2049 self._xml_end_tag("c:multiLvlStrRef") 

2050 

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

2052 # Write the <c:f> element. 

2053 

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

2055 if formula.startswith("="): 

2056 formula = formula.lstrip("=") 

2057 

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

2059 

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

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

2062 

2063 # Generate the axis ids. 

2064 self._add_axis_ids(args) 

2065 

2066 if args["primary_axes"]: 

2067 # Write the axis ids for the primary axes. 

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

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

2070 else: 

2071 # Write the axis ids for the secondary axes. 

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

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

2074 

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

2076 # Write the <c:axId> element. 

2077 

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

2079 

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

2081 

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

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

2084 x_axis = args["x_axis"] 

2085 y_axis = args["y_axis"] 

2086 axis_ids = args["axis_ids"] 

2087 

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

2089 if axis_ids is None or not axis_ids: 

2090 return 

2091 

2092 position = self.cat_axis_position 

2093 is_horizontal = self.horiz_cat_axis 

2094 

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

2096 if x_axis.get("position"): 

2097 position = x_axis["position"] 

2098 

2099 self._xml_start_tag("c:catAx") 

2100 

2101 self._write_axis_id(axis_ids[0]) 

2102 

2103 # Write the c:scaling element. 

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

2105 

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

2107 self._write_delete(1) 

2108 

2109 # Write the c:axPos element. 

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

2111 

2112 # Write the c:majorGridlines element. 

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

2114 

2115 # Write the c:minorGridlines element. 

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

2117 

2118 # Write the axis title elements. 

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

2120 

2121 # Write the c:numFmt element. 

2122 self._write_cat_number_format(x_axis) 

2123 

2124 # Write the c:majorTickMark element. 

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

2126 

2127 # Write the c:minorTickMark element. 

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

2129 

2130 # Write the c:tickLblPos element. 

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

2132 

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

2134 self._write_sp_pr(x_axis) 

2135 

2136 # Write the axis font elements. 

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

2138 

2139 # Write the c:crossAx element. 

2140 self._write_cross_axis(axis_ids[1]) 

2141 

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

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

2144 if ( 

2145 y_axis.get("crossing") is None 

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

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

2148 ): 

2149 # Write the c:crosses element. 

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

2151 else: 

2152 # Write the c:crossesAt element. 

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

2154 

2155 # Write the c:auto element. 

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

2157 self._write_auto(1) 

2158 

2159 # Write the c:labelAlign element. 

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

2161 

2162 # Write the c:labelOffset element. 

2163 self._write_label_offset(100) 

2164 

2165 # Write the c:tickLblSkip element. 

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

2167 

2168 # Write the c:tickMarkSkip element. 

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

2170 

2171 self._xml_end_tag("c:catAx") 

2172 

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

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

2175 x_axis = args["x_axis"] 

2176 y_axis = args["y_axis"] 

2177 axis_ids = args["axis_ids"] 

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

2179 is_horizontal = self.horiz_val_axis 

2180 

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

2182 if axis_ids is None or not axis_ids: 

2183 return 

2184 

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

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

2187 

2188 self._xml_start_tag("c:valAx") 

2189 

2190 self._write_axis_id(axis_ids[1]) 

2191 

2192 # Write the c:scaling element. 

2193 self._write_scaling( 

2194 y_axis.get("reverse"), 

2195 y_axis.get("min"), 

2196 y_axis.get("max"), 

2197 y_axis.get("log_base"), 

2198 ) 

2199 

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

2201 self._write_delete(1) 

2202 

2203 # Write the c:axPos element. 

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

2205 

2206 # Write the c:majorGridlines element. 

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

2208 

2209 # Write the c:minorGridlines element. 

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

2211 

2212 # Write the axis title elements. 

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

2214 

2215 # Write the c:numberFormat element. 

2216 self._write_number_format(y_axis) 

2217 

2218 # Write the c:majorTickMark element. 

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

2220 

2221 # Write the c:minorTickMark element. 

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

2223 

2224 # Write the c:tickLblPos element. 

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

2226 

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

2228 self._write_sp_pr(y_axis) 

2229 

2230 # Write the axis font elements. 

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

2232 

2233 # Write the c:crossAx element. 

2234 self._write_cross_axis(axis_ids[0]) 

2235 

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

2237 if ( 

2238 x_axis.get("crossing") is None 

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

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

2241 ): 

2242 # Write the c:crosses element. 

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

2244 else: 

2245 # Write the c:crossesAt element. 

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

2247 

2248 # Write the c:crossBetween element. 

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

2250 

2251 # Write the c:majorUnit element. 

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

2253 

2254 # Write the c:minorUnit element. 

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

2256 

2257 # Write the c:dispUnits element. 

2258 self._write_disp_units( 

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

2260 ) 

2261 

2262 self._xml_end_tag("c:valAx") 

2263 

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

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

2266 # in scatter plots. Usually the X axis. 

2267 x_axis = args["x_axis"] 

2268 y_axis = args["y_axis"] 

2269 axis_ids = args["axis_ids"] 

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

2271 is_horizontal = self.horiz_val_axis 

2272 

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

2274 if axis_ids is None or not axis_ids: 

2275 return 

2276 

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

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

2279 

2280 self._xml_start_tag("c:valAx") 

2281 

2282 self._write_axis_id(axis_ids[0]) 

2283 

2284 # Write the c:scaling element. 

2285 self._write_scaling( 

2286 x_axis.get("reverse"), 

2287 x_axis.get("min"), 

2288 x_axis.get("max"), 

2289 x_axis.get("log_base"), 

2290 ) 

2291 

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

2293 self._write_delete(1) 

2294 

2295 # Write the c:axPos element. 

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

2297 

2298 # Write the c:majorGridlines element. 

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

2300 

2301 # Write the c:minorGridlines element. 

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

2303 

2304 # Write the axis title elements. 

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

2306 

2307 # Write the c:numberFormat element. 

2308 self._write_number_format(x_axis) 

2309 

2310 # Write the c:majorTickMark element. 

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

2312 

2313 # Write the c:minorTickMark element. 

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

2315 

2316 # Write the c:tickLblPos element. 

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

2318 

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

2320 self._write_sp_pr(x_axis) 

2321 

2322 # Write the axis font elements. 

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

2324 

2325 # Write the c:crossAx element. 

2326 self._write_cross_axis(axis_ids[1]) 

2327 

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

2329 if ( 

2330 y_axis.get("crossing") is None 

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

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

2333 ): 

2334 # Write the c:crosses element. 

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

2336 else: 

2337 # Write the c:crossesAt element. 

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

2339 

2340 # Write the c:crossBetween element. 

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

2342 

2343 # Write the c:majorUnit element. 

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

2345 

2346 # Write the c:minorUnit element. 

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

2348 

2349 # Write the c:dispUnits element. 

2350 self._write_disp_units( 

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

2352 ) 

2353 

2354 self._xml_end_tag("c:valAx") 

2355 

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

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

2358 x_axis = args["x_axis"] 

2359 y_axis = args["y_axis"] 

2360 axis_ids = args["axis_ids"] 

2361 

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

2363 if axis_ids is None or not axis_ids: 

2364 return 

2365 

2366 position = self.cat_axis_position 

2367 

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

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

2370 

2371 self._xml_start_tag("c:dateAx") 

2372 

2373 self._write_axis_id(axis_ids[0]) 

2374 

2375 # Write the c:scaling element. 

2376 self._write_scaling( 

2377 x_axis.get("reverse"), 

2378 x_axis.get("min"), 

2379 x_axis.get("max"), 

2380 x_axis.get("log_base"), 

2381 ) 

2382 

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

2384 self._write_delete(1) 

2385 

2386 # Write the c:axPos element. 

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

2388 

2389 # Write the c:majorGridlines element. 

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

2391 

2392 # Write the c:minorGridlines element. 

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

2394 

2395 # Write the axis title elements. 

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

2397 

2398 # Write the c:numFmt element. 

2399 self._write_number_format(x_axis) 

2400 

2401 # Write the c:majorTickMark element. 

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

2403 

2404 # Write the c:minorTickMark element. 

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

2406 

2407 # Write the c:tickLblPos element. 

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

2409 

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

2411 self._write_sp_pr(x_axis) 

2412 

2413 # Write the axis font elements. 

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

2415 

2416 # Write the c:crossAx element. 

2417 self._write_cross_axis(axis_ids[1]) 

2418 

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

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

2421 if ( 

2422 y_axis.get("crossing") is None 

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

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

2425 ): 

2426 # Write the c:crosses element. 

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

2428 else: 

2429 # Write the c:crossesAt element. 

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

2431 

2432 # Write the c:auto element. 

2433 self._write_auto(1) 

2434 

2435 # Write the c:labelOffset element. 

2436 self._write_label_offset(100) 

2437 

2438 # Write the c:tickLblSkip element. 

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

2440 

2441 # Write the c:tickMarkSkip element. 

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

2443 

2444 # Write the c:majorUnit element. 

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

2446 

2447 # Write the c:majorTimeUnit element. 

2448 if x_axis.get("major_unit"): 

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

2450 

2451 # Write the c:minorUnit element. 

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

2453 

2454 # Write the c:minorTimeUnit element. 

2455 if x_axis.get("minor_unit"): 

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

2457 

2458 self._xml_end_tag("c:dateAx") 

2459 

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

2461 # Write the <c:scaling> element. 

2462 

2463 self._xml_start_tag("c:scaling") 

2464 

2465 # Write the c:logBase element. 

2466 self._write_c_log_base(log_base) 

2467 

2468 # Write the c:orientation element. 

2469 self._write_orientation(reverse) 

2470 

2471 # Write the c:max element. 

2472 self._write_c_max(max_val) 

2473 

2474 # Write the c:min element. 

2475 self._write_c_min(min_val) 

2476 

2477 self._xml_end_tag("c:scaling") 

2478 

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

2480 # Write the <c:logBase> element. 

2481 

2482 if not val: 

2483 return 

2484 

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

2486 

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

2488 

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

2490 # Write the <c:orientation> element. 

2491 val = "minMax" 

2492 

2493 if reverse: 

2494 val = "maxMin" 

2495 

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

2497 

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

2499 

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

2501 # Write the <c:max> element. 

2502 

2503 if max_val is None: 

2504 return 

2505 

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

2507 

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

2509 

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

2511 # Write the <c:min> element. 

2512 

2513 if min_val is None: 

2514 return 

2515 

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

2517 

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

2519 

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

2521 # Write the <c:axPos> element. 

2522 

2523 if reverse: 

2524 if val == "l": 

2525 val = "r" 

2526 if val == "b": 

2527 val = "t" 

2528 

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

2530 

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

2532 

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

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

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

2536 # the sourceLinked attribute is 0. 

2537 # The user can override this if required. 

2538 format_code = axis.get("num_format") 

2539 source_linked = 1 

2540 

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

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

2543 source_linked = 0 

2544 

2545 # User override of sourceLinked. 

2546 if axis.get("num_format_linked"): 

2547 source_linked = 1 

2548 

2549 attributes = [ 

2550 ("formatCode", format_code), 

2551 ("sourceLinked", source_linked), 

2552 ] 

2553 

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

2555 

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

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

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

2559 format_code = axis.get("num_format") 

2560 source_linked = 1 

2561 default_format = 1 

2562 

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

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

2565 source_linked = 0 

2566 default_format = 0 

2567 

2568 # User override of sourceLinked. 

2569 if axis.get("num_format_linked"): 

2570 source_linked = 1 

2571 

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

2573 if not self.cat_has_num_fmt and default_format: 

2574 return 

2575 

2576 attributes = [ 

2577 ("formatCode", format_code), 

2578 ("sourceLinked", source_linked), 

2579 ] 

2580 

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

2582 

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

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

2585 source_linked = 0 

2586 

2587 attributes = [ 

2588 ("formatCode", format_code), 

2589 ("sourceLinked", source_linked), 

2590 ] 

2591 

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

2593 

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

2595 # Write the <c:majorTickMark> element. 

2596 

2597 if not val: 

2598 return 

2599 

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

2601 

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

2603 

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

2605 # Write the <c:minorTickMark> element. 

2606 

2607 if not val: 

2608 return 

2609 

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

2611 

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

2613 

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

2615 # Write the <c:tickLblPos> element. 

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

2617 val = "nextTo" 

2618 

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

2620 

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

2622 

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

2624 # Write the <c:crossAx> element. 

2625 

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

2627 

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

2629 

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

2631 # Write the <c:crosses> element. 

2632 if val is None: 

2633 val = "autoZero" 

2634 

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

2636 

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

2638 

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

2640 # Write the <c:crossesAt> element. 

2641 

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

2643 

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

2645 

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

2647 # Write the <c:auto> element. 

2648 

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

2650 

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

2652 

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

2654 # Write the <c:labelAlign> element. 

2655 

2656 if val is None: 

2657 val = "ctr" 

2658 

2659 if val == "right": 

2660 val = "r" 

2661 

2662 if val == "left": 

2663 val = "l" 

2664 

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

2666 

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

2668 

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

2670 # Write the <c:labelOffset> element. 

2671 

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

2673 

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

2675 

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

2677 # Write the <c:tickLblSkip> element. 

2678 if val is None: 

2679 return 

2680 

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

2682 

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

2684 

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

2686 # Write the <c:tickMarkSkip> element. 

2687 if val is None: 

2688 return 

2689 

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

2691 

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

2693 

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

2695 # Write the <c:majorGridlines> element. 

2696 

2697 if not gridlines: 

2698 return 

2699 

2700 if not gridlines["visible"]: 

2701 return 

2702 

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

2704 self._xml_start_tag("c:majorGridlines") 

2705 

2706 # Write the c:spPr element. 

2707 self._write_sp_pr(gridlines) 

2708 

2709 self._xml_end_tag("c:majorGridlines") 

2710 else: 

2711 self._xml_empty_tag("c:majorGridlines") 

2712 

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

2714 # Write the <c:minorGridlines> element. 

2715 

2716 if not gridlines: 

2717 return 

2718 

2719 if not gridlines["visible"]: 

2720 return 

2721 

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

2723 self._xml_start_tag("c:minorGridlines") 

2724 

2725 # Write the c:spPr element. 

2726 self._write_sp_pr(gridlines) 

2727 

2728 self._xml_end_tag("c:minorGridlines") 

2729 else: 

2730 self._xml_empty_tag("c:minorGridlines") 

2731 

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

2733 # Write the <c:crossBetween> element. 

2734 if val is None: 

2735 val = self.cross_between 

2736 

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

2738 

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

2740 

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

2742 # Write the <c:majorUnit> element. 

2743 

2744 if not val: 

2745 return 

2746 

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

2748 

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

2750 

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

2752 # Write the <c:minorUnit> element. 

2753 

2754 if not val: 

2755 return 

2756 

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

2758 

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

2760 

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

2762 # Write the <c:majorTimeUnit> element. 

2763 if val is None: 

2764 val = "days" 

2765 

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

2767 

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

2769 

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

2771 # Write the <c:minorTimeUnit> element. 

2772 if val is None: 

2773 val = "days" 

2774 

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

2776 

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

2778 

2779 def _write_legend(self) -> None: 

2780 # Write the <c:legend> element. 

2781 legend = self.legend 

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

2783 font = legend.get("font") 

2784 delete_series = [] 

2785 overlay = 0 

2786 

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

2788 delete_series = legend["delete_series"] 

2789 

2790 if position.startswith("overlay_"): 

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

2792 overlay = 1 

2793 

2794 allowed = { 

2795 "right": "r", 

2796 "left": "l", 

2797 "top": "t", 

2798 "bottom": "b", 

2799 "top_right": "tr", 

2800 } 

2801 

2802 if position == "none": 

2803 return 

2804 

2805 if position not in allowed: 

2806 return 

2807 

2808 position = allowed[position] 

2809 

2810 self._xml_start_tag("c:legend") 

2811 

2812 # Write the c:legendPos element. 

2813 self._write_legend_pos(position) 

2814 

2815 # Remove series labels from the legend. 

2816 for index in delete_series: 

2817 # Write the c:legendEntry element. 

2818 self._write_legend_entry(index) 

2819 

2820 # Write the c:layout element. 

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

2822 

2823 # Write the c:overlay element. 

2824 if overlay: 

2825 self._write_overlay() 

2826 

2827 if font: 

2828 self._write_tx_pr(font) 

2829 

2830 # Write the c:spPr element. 

2831 self._write_sp_pr(legend) 

2832 

2833 self._xml_end_tag("c:legend") 

2834 

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

2836 # Write the <c:legendPos> element. 

2837 

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

2839 

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

2841 

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

2843 # Write the <c:legendEntry> element. 

2844 

2845 self._xml_start_tag("c:legendEntry") 

2846 

2847 # Write the c:idx element. 

2848 self._write_idx(index) 

2849 

2850 # Write the c:delete element. 

2851 self._write_delete(1) 

2852 

2853 self._xml_end_tag("c:legendEntry") 

2854 

2855 def _write_overlay(self) -> None: 

2856 # Write the <c:overlay> element. 

2857 val = 1 

2858 

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

2860 

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

2862 

2863 def _write_plot_vis_only(self) -> None: 

2864 # Write the <c:plotVisOnly> element. 

2865 val = 1 

2866 

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

2868 if self.show_hidden: 

2869 return 

2870 

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

2872 

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

2874 

2875 def _write_print_settings(self) -> None: 

2876 # Write the <c:printSettings> element. 

2877 self._xml_start_tag("c:printSettings") 

2878 

2879 # Write the c:headerFooter element. 

2880 self._write_header_footer() 

2881 

2882 # Write the c:pageMargins element. 

2883 self._write_page_margins() 

2884 

2885 # Write the c:pageSetup element. 

2886 self._write_page_setup() 

2887 

2888 self._xml_end_tag("c:printSettings") 

2889 

2890 def _write_header_footer(self) -> None: 

2891 # Write the <c:headerFooter> element. 

2892 self._xml_empty_tag("c:headerFooter") 

2893 

2894 def _write_page_margins(self) -> None: 

2895 # Write the <c:pageMargins> element. 

2896 bottom = 0.75 

2897 left = 0.7 

2898 right = 0.7 

2899 top = 0.75 

2900 header = 0.3 

2901 footer = 0.3 

2902 

2903 attributes = [ 

2904 ("b", bottom), 

2905 ("l", left), 

2906 ("r", right), 

2907 ("t", top), 

2908 ("header", header), 

2909 ("footer", footer), 

2910 ] 

2911 

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

2913 

2914 def _write_page_setup(self) -> None: 

2915 # Write the <c:pageSetup> element. 

2916 self._xml_empty_tag("c:pageSetup") 

2917 

2918 def _write_c_auto_title_deleted(self) -> None: 

2919 # Write the <c:autoTitleDeleted> element. 

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

2921 

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

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

2924 if title.has_name(): 

2925 self._write_title_rich(title, is_horizontal) 

2926 elif title.has_formula(): 

2927 self._write_title_formula(title, is_horizontal) 

2928 elif title.has_formatting(): 

2929 self._write_title_format_only(title) 

2930 

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

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

2933 self._xml_start_tag("c:title") 

2934 

2935 # Write the c:tx element. 

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

2937 

2938 # Write the c:layout element. 

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

2940 

2941 # Write the c:overlay element. 

2942 if title.overlay: 

2943 self._write_overlay() 

2944 

2945 # Write the c:spPr element. 

2946 self._write_sp_pr(title.get_formatting()) 

2947 

2948 self._xml_end_tag("c:title") 

2949 

2950 def _write_title_formula( 

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

2952 ) -> None: 

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

2954 self._xml_start_tag("c:title") 

2955 

2956 # Write the c:tx element. 

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

2958 

2959 # Write the c:layout element. 

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

2961 

2962 # Write the c:overlay element. 

2963 if title.overlay: 

2964 self._write_overlay() 

2965 

2966 # Write the c:spPr element. 

2967 self._write_sp_pr(title.get_formatting()) 

2968 

2969 # Write the c:txPr element. 

2970 self._write_tx_pr(title.font, is_horizontal) 

2971 

2972 self._xml_end_tag("c:title") 

2973 

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

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

2976 self._xml_start_tag("c:title") 

2977 

2978 # Write the c:layout element. 

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

2980 

2981 # Write the c:overlay element. 

2982 if title.overlay: 

2983 self._write_overlay() 

2984 

2985 # Write the c:spPr element. 

2986 self._write_sp_pr(title.get_formatting()) 

2987 

2988 self._xml_end_tag("c:title") 

2989 

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

2991 # Write the <c:tx> element. 

2992 

2993 self._xml_start_tag("c:tx") 

2994 

2995 # Write the c:rich element. 

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

2997 

2998 self._xml_end_tag("c:tx") 

2999 

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

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

3002 

3003 self._xml_start_tag("c:tx") 

3004 

3005 # Write the c:v element. 

3006 self._write_v(title) 

3007 

3008 self._xml_end_tag("c:tx") 

3009 

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

3011 # Write the <c:tx> element. 

3012 data = None 

3013 

3014 if data_id is not None: 

3015 data = self.formula_data[data_id] 

3016 

3017 self._xml_start_tag("c:tx") 

3018 

3019 # Write the c:strRef element. 

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

3021 

3022 self._xml_end_tag("c:tx") 

3023 

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

3025 # Write the <c:rich> element. 

3026 

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

3028 rotation = font["rotation"] 

3029 else: 

3030 rotation = None 

3031 

3032 self._xml_start_tag("c:rich") 

3033 

3034 # Write the a:bodyPr element. 

3035 self._write_a_body_pr(rotation, is_horizontal) 

3036 

3037 # Write the a:lstStyle element. 

3038 self._write_a_lst_style() 

3039 

3040 # Write the a:p element. 

3041 self._write_a_p_rich(title, font, ignore_rich_pr) 

3042 

3043 self._xml_end_tag("c:rich") 

3044 

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

3046 # Write the <a:bodyPr> element. 

3047 attributes = [] 

3048 

3049 if rotation is None and is_horizontal: 

3050 rotation = -5400000 

3051 

3052 if rotation is not None: 

3053 if rotation == 16200000: 

3054 # 270 deg/stacked angle. 

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

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

3057 elif rotation == 16260000: 

3058 # 271 deg/East Asian vertical. 

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

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

3061 else: 

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

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

3064 

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

3066 

3067 def _write_a_lst_style(self) -> None: 

3068 # Write the <a:lstStyle> element. 

3069 self._xml_empty_tag("a:lstStyle") 

3070 

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

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

3073 

3074 self._xml_start_tag("a:p") 

3075 

3076 # Write the a:pPr element. 

3077 if not ignore_rich_pr: 

3078 self._write_a_p_pr_rich(font) 

3079 

3080 # Write the a:r element. 

3081 self._write_a_r(title, font) 

3082 

3083 self._xml_end_tag("a:p") 

3084 

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

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

3087 

3088 self._xml_start_tag("a:p") 

3089 

3090 # Write the a:pPr element. 

3091 self._write_a_p_pr_rich(font) 

3092 

3093 # Write the a:endParaRPr element. 

3094 self._write_a_end_para_rpr() 

3095 

3096 self._xml_end_tag("a:p") 

3097 

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

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

3100 

3101 self._xml_start_tag("a:pPr") 

3102 

3103 # Write the a:defRPr element. 

3104 self._write_a_def_rpr(font) 

3105 

3106 self._xml_end_tag("a:pPr") 

3107 

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

3109 # Write the <a:defRPr> element. 

3110 has_color = False 

3111 

3112 style_attributes = Shape._get_font_style_attributes(font) 

3113 latin_attributes = Shape._get_font_latin_attributes(font) 

3114 

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

3116 has_color = True 

3117 

3118 if latin_attributes or has_color: 

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

3120 

3121 if has_color: 

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

3123 

3124 if latin_attributes: 

3125 self._write_a_latin(latin_attributes) 

3126 

3127 self._xml_end_tag("a:defRPr") 

3128 else: 

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

3130 

3131 def _write_a_end_para_rpr(self) -> None: 

3132 # Write the <a:endParaRPr> element. 

3133 lang = "en-US" 

3134 

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

3136 

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

3138 

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

3140 # Write the <a:r> element. 

3141 

3142 self._xml_start_tag("a:r") 

3143 

3144 # Write the a:rPr element. 

3145 self._write_a_r_pr(font) 

3146 

3147 # Write the a:t element. 

3148 self._write_a_t(title) 

3149 

3150 self._xml_end_tag("a:r") 

3151 

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

3153 # Write the <a:rPr> element. 

3154 has_color = False 

3155 lang = "en-US" 

3156 

3157 style_attributes = Shape._get_font_style_attributes(font) 

3158 latin_attributes = Shape._get_font_latin_attributes(font) 

3159 

3160 if font and font["color"]: 

3161 has_color = True 

3162 

3163 # Add the lang type to the attributes. 

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

3165 

3166 if latin_attributes or has_color: 

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

3168 

3169 if has_color: 

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

3171 

3172 if latin_attributes: 

3173 self._write_a_latin(latin_attributes) 

3174 

3175 self._xml_end_tag("a:rPr") 

3176 else: 

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

3178 

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

3180 # Write the <a:t> element. 

3181 

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

3183 

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

3185 # Write the <c:txPr> element. 

3186 

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

3188 rotation = font["rotation"] 

3189 else: 

3190 rotation = None 

3191 

3192 self._xml_start_tag("c:txPr") 

3193 

3194 # Write the a:bodyPr element. 

3195 self._write_a_body_pr(rotation, is_horizontal) 

3196 

3197 # Write the a:lstStyle element. 

3198 self._write_a_lst_style() 

3199 

3200 # Write the a:p element. 

3201 self._write_a_p_formula(font) 

3202 

3203 self._xml_end_tag("c:txPr") 

3204 

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

3206 # Write the <c:marker> element. 

3207 if marker is None: 

3208 marker = self.default_marker 

3209 

3210 if not marker: 

3211 return 

3212 

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

3214 return 

3215 

3216 self._xml_start_tag("c:marker") 

3217 

3218 # Write the c:symbol element. 

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

3220 

3221 # Write the c:size element. 

3222 if marker.get("size"): 

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

3224 

3225 # Write the c:spPr element. 

3226 self._write_sp_pr(marker) 

3227 

3228 self._xml_end_tag("c:marker") 

3229 

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

3231 # Write the <c:size> element. 

3232 

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

3234 

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

3236 

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

3238 # Write the <c:symbol> element. 

3239 

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

3241 

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

3243 

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

3245 # Write the <c:spPr> element. 

3246 if not self._has_formatting(chart_format): 

3247 return 

3248 

3249 self._xml_start_tag("c:spPr") 

3250 

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

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

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

3254 # Write the a:noFill element. 

3255 self._write_a_no_fill() 

3256 else: 

3257 # Write the a:solidFill element. 

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

3259 

3260 if chart_format.get("pattern"): 

3261 # Write the a:gradFill element. 

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

3263 

3264 if chart_format.get("gradient"): 

3265 # Write the a:gradFill element. 

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

3267 

3268 # Write the a:ln element. 

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

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

3271 

3272 self._xml_end_tag("c:spPr") 

3273 

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

3275 # Write the <a:ln> element. 

3276 attributes = [] 

3277 

3278 # Add the line width as an attribute. 

3279 width = line.get("width") 

3280 

3281 if width is not None: 

3282 # Round width to nearest 0.25, like Excel. 

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

3284 

3285 # Convert to internal units. 

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

3287 

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

3289 

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

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

3292 

3293 # Write the line fill. 

3294 if "none" in line: 

3295 # Write the a:noFill element. 

3296 self._write_a_no_fill() 

3297 elif "color" in line: 

3298 # Write the a:solidFill element. 

3299 self._write_a_solid_fill(line) 

3300 

3301 # Write the line/dash type. 

3302 line_type = line.get("dash_type") 

3303 if line_type: 

3304 # Write the a:prstDash element. 

3305 self._write_a_prst_dash(line_type) 

3306 

3307 self._xml_end_tag("a:ln") 

3308 else: 

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

3310 

3311 def _write_a_no_fill(self) -> None: 

3312 # Write the <a:noFill> element. 

3313 self._xml_empty_tag("a:noFill") 

3314 

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

3316 # Write the <a:solidFill> element. 

3317 

3318 self._xml_start_tag("a:solidFill") 

3319 

3320 if fill.get("color"): 

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

3322 

3323 self._xml_end_tag("a:solidFill") 

3324 

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

3326 # Write the appropriate chart color element. 

3327 

3328 if not color: 

3329 return 

3330 

3331 if color._is_automatic: 

3332 # Write the a:sysClr element. 

3333 self._write_a_sys_clr() 

3334 elif color._type == ColorTypes.RGB: 

3335 # Write the a:srgbClr element. 

3336 self._write_a_srgb_clr(color, transparency) 

3337 elif color._type == ColorTypes.THEME: 

3338 self._write_a_scheme_clr(color, transparency) 

3339 

3340 def _write_a_sys_clr(self) -> None: 

3341 # Write the <a:sysClr> element. 

3342 

3343 val = "window" 

3344 last_clr = "FFFFFF" 

3345 

3346 attributes = [ 

3347 ("val", val), 

3348 ("lastClr", last_clr), 

3349 ] 

3350 

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

3352 

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

3354 # Write the <a:srgbClr> element. 

3355 

3356 if not color: 

3357 return 

3358 

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

3360 

3361 if transparency: 

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

3363 

3364 # Write the a:alpha element. 

3365 self._write_a_alpha(transparency) 

3366 

3367 self._xml_end_tag("a:srgbClr") 

3368 else: 

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

3370 

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

3372 # Write the <a:schemeClr> element. 

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

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

3375 

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

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

3378 

3379 if lum_mod > 0: 

3380 # Write the a:lumMod element. 

3381 self._write_a_lum_mod(lum_mod) 

3382 

3383 if lum_off > 0: 

3384 # Write the a:lumOff element. 

3385 self._write_a_lum_off(lum_off) 

3386 

3387 if transparency: 

3388 # Write the a:alpha element. 

3389 self._write_a_alpha(transparency) 

3390 

3391 self._xml_end_tag("a:schemeClr") 

3392 else: 

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

3394 

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

3396 # Write the <a:lumMod> element. 

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

3398 

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

3400 

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

3402 # Write the <a:lumOff> element. 

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

3404 

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

3406 

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

3408 # Write the <a:alpha> element. 

3409 

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

3411 

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

3413 

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

3415 

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

3417 # Write the <a:prstDash> element. 

3418 

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

3420 

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

3422 

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

3424 # Write the <c:trendline> element. 

3425 

3426 if not trendline: 

3427 return 

3428 

3429 self._xml_start_tag("c:trendline") 

3430 

3431 # Write the c:name element. 

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

3433 

3434 # Write the c:spPr element. 

3435 self._write_sp_pr(trendline) 

3436 

3437 # Write the c:trendlineType element. 

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

3439 

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

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

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

3443 

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

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

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

3447 

3448 # Write the c:forward element. 

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

3450 

3451 # Write the c:backward element. 

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

3453 

3454 if "intercept" in trendline: 

3455 # Write the c:intercept element. 

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

3457 

3458 if trendline.get("display_r_squared"): 

3459 # Write the c:dispRSqr element. 

3460 self._write_c_disp_rsqr() 

3461 

3462 if trendline.get("display_equation"): 

3463 # Write the c:dispEq element. 

3464 self._write_c_disp_eq() 

3465 

3466 # Write the c:trendlineLbl element. 

3467 self._write_c_trendline_lbl(trendline) 

3468 

3469 self._xml_end_tag("c:trendline") 

3470 

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

3472 # Write the <c:trendlineType> element. 

3473 

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

3475 

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

3477 

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

3479 # Write the <c:name> element. 

3480 

3481 if data is None: 

3482 return 

3483 

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

3485 

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

3487 # Write the <c:order> element. 

3488 val = max(val, 2) 

3489 

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

3491 

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

3493 

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

3495 # Write the <c:period> element. 

3496 val = max(val, 2) 

3497 

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

3499 

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

3501 

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

3503 # Write the <c:forward> element. 

3504 

3505 if not val: 

3506 return 

3507 

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

3509 

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

3511 

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

3513 # Write the <c:backward> element. 

3514 

3515 if not val: 

3516 return 

3517 

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

3519 

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

3521 

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

3523 # Write the <c:intercept> element. 

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

3525 

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

3527 

3528 def _write_c_disp_eq(self) -> None: 

3529 # Write the <c:dispEq> element. 

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

3531 

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

3533 

3534 def _write_c_disp_rsqr(self) -> None: 

3535 # Write the <c:dispRSqr> element. 

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

3537 

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

3539 

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

3541 # Write the <c:trendlineLbl> element. 

3542 self._xml_start_tag("c:trendlineLbl") 

3543 

3544 # Write the c:layout element. 

3545 self._write_layout(None, None) 

3546 

3547 # Write the c:numFmt element. 

3548 self._write_trendline_num_fmt() 

3549 

3550 # Write the c:spPr element. 

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

3552 

3553 # Write the data label font elements. 

3554 if trendline["label"]: 

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

3556 if font: 

3557 self._write_axis_font(font) 

3558 

3559 self._xml_end_tag("c:trendlineLbl") 

3560 

3561 def _write_trendline_num_fmt(self) -> None: 

3562 # Write the <c:numFmt> element. 

3563 attributes = [ 

3564 ("formatCode", "General"), 

3565 ("sourceLinked", 0), 

3566 ] 

3567 

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

3569 

3570 def _write_hi_low_lines(self) -> None: 

3571 # Write the <c:hiLowLines> element. 

3572 hi_low_lines = self.hi_low_lines 

3573 

3574 if hi_low_lines is None: 

3575 return 

3576 

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

3578 self._xml_start_tag("c:hiLowLines") 

3579 

3580 # Write the c:spPr element. 

3581 self._write_sp_pr(hi_low_lines) 

3582 

3583 self._xml_end_tag("c:hiLowLines") 

3584 else: 

3585 self._xml_empty_tag("c:hiLowLines") 

3586 

3587 def _write_drop_lines(self) -> None: 

3588 # Write the <c:dropLines> element. 

3589 drop_lines = self.drop_lines 

3590 

3591 if drop_lines is None: 

3592 return 

3593 

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

3595 self._xml_start_tag("c:dropLines") 

3596 

3597 # Write the c:spPr element. 

3598 self._write_sp_pr(drop_lines) 

3599 

3600 self._xml_end_tag("c:dropLines") 

3601 else: 

3602 self._xml_empty_tag("c:dropLines") 

3603 

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

3605 # Write the <c:overlap> element. 

3606 

3607 if val is None: 

3608 return 

3609 

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

3611 

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

3613 

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

3615 # Write the <c:numCache> element. 

3616 if data: 

3617 count = len(data) 

3618 else: 

3619 count = 0 

3620 

3621 self._xml_start_tag("c:numCache") 

3622 

3623 # Write the c:formatCode element. 

3624 self._write_format_code("General") 

3625 

3626 # Write the c:ptCount element. 

3627 self._write_pt_count(count) 

3628 

3629 for i in range(count): 

3630 token = data[i] 

3631 

3632 if token is None: 

3633 continue 

3634 

3635 try: 

3636 float(token) 

3637 except ValueError: 

3638 # Write non-numeric data as 0. 

3639 token = 0 

3640 

3641 # Write the c:pt element. 

3642 self._write_pt(i, token) 

3643 

3644 self._xml_end_tag("c:numCache") 

3645 

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

3647 # Write the <c:strCache> element. 

3648 count = len(data) 

3649 

3650 self._xml_start_tag("c:strCache") 

3651 

3652 # Write the c:ptCount element. 

3653 self._write_pt_count(count) 

3654 

3655 for i in range(count): 

3656 # Write the c:pt element. 

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

3658 

3659 self._xml_end_tag("c:strCache") 

3660 

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

3662 # Write the <c:formatCode> element. 

3663 

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

3665 

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

3667 # Write the <c:ptCount> element. 

3668 

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

3670 

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

3672 

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

3674 # Write the <c:pt> element. 

3675 

3676 if value is None: 

3677 return 

3678 

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

3680 

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

3682 

3683 # Write the c:v element. 

3684 self._write_v(value) 

3685 

3686 self._xml_end_tag("c:pt") 

3687 

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

3689 # Write the <c:v> element. 

3690 

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

3692 

3693 def _write_protection(self) -> None: 

3694 # Write the <c:protection> element. 

3695 if not self.protection: 

3696 return 

3697 

3698 self._xml_empty_tag("c:protection") 

3699 

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

3701 # Write the <c:dPt> elements. 

3702 index = -1 

3703 

3704 if not points: 

3705 return 

3706 

3707 for point in points: 

3708 index += 1 

3709 if not point: 

3710 continue 

3711 

3712 self._write_d_pt_point(index, point) 

3713 

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

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

3716 

3717 self._xml_start_tag("c:dPt") 

3718 

3719 # Write the c:idx element. 

3720 self._write_idx(index) 

3721 

3722 # Write the c:spPr element. 

3723 self._write_sp_pr(point) 

3724 

3725 self._xml_end_tag("c:dPt") 

3726 

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

3728 # Write the <c:dLbls> element. 

3729 

3730 if not labels: 

3731 return 

3732 

3733 self._xml_start_tag("c:dLbls") 

3734 

3735 # Write the custom c:dLbl elements. 

3736 if labels.get("custom"): 

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

3738 

3739 # Write the c:numFmt element. 

3740 if labels.get("num_format"): 

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

3742 

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

3744 self._write_sp_pr(labels) 

3745 

3746 # Write the data label font elements. 

3747 if labels.get("font"): 

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

3749 

3750 # Write the c:dLblPos element. 

3751 if labels.get("position"): 

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

3753 

3754 # Write the c:showLegendKey element. 

3755 if labels.get("legend_key"): 

3756 self._write_show_legend_key() 

3757 

3758 # Write the c:showVal element. 

3759 if labels.get("value"): 

3760 self._write_show_val() 

3761 

3762 # Write the c:showCatName element. 

3763 if labels.get("category"): 

3764 self._write_show_cat_name() 

3765 

3766 # Write the c:showSerName element. 

3767 if labels.get("series_name"): 

3768 self._write_show_ser_name() 

3769 

3770 # Write the c:showPercent element. 

3771 if labels.get("percentage"): 

3772 self._write_show_percent() 

3773 

3774 # Write the c:separator element. 

3775 if labels.get("separator"): 

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

3777 

3778 # Write the c:showLeaderLines element. 

3779 if labels.get("leader_lines"): 

3780 self._write_show_leader_lines() 

3781 

3782 self._xml_end_tag("c:dLbls") 

3783 

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

3785 # Write the <c:showLegendKey> element. 

3786 index = 0 

3787 

3788 for label in labels: 

3789 index += 1 

3790 

3791 if label is None: 

3792 continue 

3793 

3794 use_custom_formatting = True 

3795 

3796 self._xml_start_tag("c:dLbl") 

3797 

3798 # Write the c:idx element. 

3799 self._write_idx(index - 1) 

3800 

3801 delete_label = label.get("delete") 

3802 

3803 if delete_label: 

3804 self._write_delete(1) 

3805 

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

3807 

3808 # Write the c:layout element. 

3809 self._write_layout(None, None) 

3810 

3811 if label.get("formula"): 

3812 self._write_custom_label_formula(label) 

3813 elif label.get("value"): 

3814 self._write_custom_label_str(label) 

3815 # String values use spPr formatting. 

3816 use_custom_formatting = False 

3817 

3818 if use_custom_formatting: 

3819 self._write_custom_label_format(label) 

3820 

3821 if label.get("position"): 

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

3823 elif parent.get("position"): 

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

3825 

3826 if parent.get("value"): 

3827 self._write_show_val() 

3828 

3829 if parent.get("category"): 

3830 self._write_show_cat_name() 

3831 

3832 if parent.get("series_name"): 

3833 self._write_show_ser_name() 

3834 

3835 else: 

3836 self._write_custom_label_format(label) 

3837 

3838 self._xml_end_tag("c:dLbl") 

3839 

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

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

3842 title = label.get("value") 

3843 font = label.get("font") 

3844 has_formatting = self._has_formatting(label) 

3845 

3846 self._xml_start_tag("c:tx") 

3847 

3848 # Write the c:rich element. 

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

3850 

3851 self._xml_end_tag("c:tx") 

3852 

3853 # Write the c:spPr element. 

3854 self._write_sp_pr(label) 

3855 

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

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

3858 formula = label.get("formula") 

3859 data_id = label.get("data_id") 

3860 data = None 

3861 

3862 if data_id is not None: 

3863 data = self.formula_data[data_id] 

3864 

3865 self._xml_start_tag("c:tx") 

3866 

3867 # Write the c:strRef element. 

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

3869 

3870 self._xml_end_tag("c:tx") 

3871 

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

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

3874 font = label.get("font") 

3875 has_formatting = self._has_formatting(label) 

3876 

3877 if has_formatting: 

3878 self._write_sp_pr(label) 

3879 self._write_tx_pr(font) 

3880 elif font: 

3881 self._xml_empty_tag("c:spPr") 

3882 self._write_tx_pr(font) 

3883 

3884 def _write_show_legend_key(self) -> None: 

3885 # Write the <c:showLegendKey> element. 

3886 val = "1" 

3887 

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

3889 

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

3891 

3892 def _write_show_val(self) -> None: 

3893 # Write the <c:showVal> element. 

3894 val = 1 

3895 

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

3897 

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

3899 

3900 def _write_show_cat_name(self) -> None: 

3901 # Write the <c:showCatName> element. 

3902 val = 1 

3903 

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

3905 

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

3907 

3908 def _write_show_ser_name(self) -> None: 

3909 # Write the <c:showSerName> element. 

3910 val = 1 

3911 

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

3913 

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

3915 

3916 def _write_show_percent(self) -> None: 

3917 # Write the <c:showPercent> element. 

3918 val = 1 

3919 

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

3921 

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

3923 

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

3925 # Write the <c:separator> element. 

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

3927 

3928 def _write_show_leader_lines(self) -> None: 

3929 # Write the <c:showLeaderLines> element. 

3930 # 

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

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

3933 # 

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

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

3936 

3937 attributes = [ 

3938 ("uri", uri), 

3939 ("xmlns:c15", xmlns_c_15), 

3940 ] 

3941 

3942 self._xml_start_tag("c:extLst") 

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

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

3945 self._xml_end_tag("c:ext") 

3946 self._xml_end_tag("c:extLst") 

3947 

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

3949 # Write the <c:dLblPos> element. 

3950 

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

3952 

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

3954 

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

3956 # Write the <c:delete> element. 

3957 

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

3959 

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

3961 

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

3963 # Write the <c:invertIfNegative> element. 

3964 val = 1 

3965 

3966 if not invert: 

3967 return 

3968 

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

3970 

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

3972 

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

3974 # Write the axis font elements. 

3975 

3976 if not font: 

3977 return 

3978 

3979 self._xml_start_tag("c:txPr") 

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

3981 self._write_a_lst_style() 

3982 self._xml_start_tag("a:p") 

3983 

3984 self._write_a_p_pr_rich(font) 

3985 

3986 self._write_a_end_para_rpr() 

3987 self._xml_end_tag("a:p") 

3988 self._xml_end_tag("c:txPr") 

3989 

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

3991 # Write the <a:latin> element. 

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

3993 

3994 def _write_d_table(self) -> None: 

3995 # Write the <c:dTable> element. 

3996 table = self.table 

3997 

3998 if not table: 

3999 return 

4000 

4001 self._xml_start_tag("c:dTable") 

4002 

4003 if table["horizontal"]: 

4004 # Write the c:showHorzBorder element. 

4005 self._write_show_horz_border() 

4006 

4007 if table["vertical"]: 

4008 # Write the c:showVertBorder element. 

4009 self._write_show_vert_border() 

4010 

4011 if table["outline"]: 

4012 # Write the c:showOutline element. 

4013 self._write_show_outline() 

4014 

4015 if table["show_keys"]: 

4016 # Write the c:showKeys element. 

4017 self._write_show_keys() 

4018 

4019 if table["font"]: 

4020 # Write the table font. 

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

4022 

4023 self._xml_end_tag("c:dTable") 

4024 

4025 def _write_show_horz_border(self) -> None: 

4026 # Write the <c:showHorzBorder> element. 

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

4028 

4029 self._xml_empty_tag("c:showHorzBorder", attributes) 

4030 

4031 def _write_show_vert_border(self) -> None: 

4032 # Write the <c:showVertBorder> element. 

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

4034 

4035 self._xml_empty_tag("c:showVertBorder", attributes) 

4036 

4037 def _write_show_outline(self) -> None: 

4038 # Write the <c:showOutline> element. 

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

4040 

4041 self._xml_empty_tag("c:showOutline", attributes) 

4042 

4043 def _write_show_keys(self) -> None: 

4044 # Write the <c:showKeys> element. 

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

4046 

4047 self._xml_empty_tag("c:showKeys", attributes) 

4048 

4049 def _write_error_bars(self, error_bars) -> None: 

4050 # Write the X and Y error bars. 

4051 

4052 if not error_bars: 

4053 return 

4054 

4055 if error_bars["x_error_bars"]: 

4056 self._write_err_bars("x", error_bars["x_error_bars"]) 

4057 

4058 if error_bars["y_error_bars"]: 

4059 self._write_err_bars("y", error_bars["y_error_bars"]) 

4060 

4061 def _write_err_bars(self, direction, error_bars) -> None: 

4062 # Write the <c:errBars> element. 

4063 

4064 if not error_bars: 

4065 return 

4066 

4067 self._xml_start_tag("c:errBars") 

4068 

4069 # Write the c:errDir element. 

4070 self._write_err_dir(direction) 

4071 

4072 # Write the c:errBarType element. 

4073 self._write_err_bar_type(error_bars["direction"]) 

4074 

4075 # Write the c:errValType element. 

4076 self._write_err_val_type(error_bars["type"]) 

4077 

4078 if not error_bars["endcap"]: 

4079 # Write the c:noEndCap element. 

4080 self._write_no_end_cap() 

4081 

4082 if error_bars["type"] == "stdErr": 

4083 # Don't need to write a c:errValType tag. 

4084 pass 

4085 elif error_bars["type"] == "cust": 

4086 # Write the custom error tags. 

4087 self._write_custom_error(error_bars) 

4088 else: 

4089 # Write the c:val element. 

4090 self._write_error_val(error_bars["value"]) 

4091 

4092 # Write the c:spPr element. 

4093 self._write_sp_pr(error_bars) 

4094 

4095 self._xml_end_tag("c:errBars") 

4096 

4097 def _write_err_dir(self, val) -> None: 

4098 # Write the <c:errDir> element. 

4099 

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

4101 

4102 self._xml_empty_tag("c:errDir", attributes) 

4103 

4104 def _write_err_bar_type(self, val) -> None: 

4105 # Write the <c:errBarType> element. 

4106 

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

4108 

4109 self._xml_empty_tag("c:errBarType", attributes) 

4110 

4111 def _write_err_val_type(self, val) -> None: 

4112 # Write the <c:errValType> element. 

4113 

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

4115 

4116 self._xml_empty_tag("c:errValType", attributes) 

4117 

4118 def _write_no_end_cap(self) -> None: 

4119 # Write the <c:noEndCap> element. 

4120 attributes = [("val", 1)] 

4121 

4122 self._xml_empty_tag("c:noEndCap", attributes) 

4123 

4124 def _write_error_val(self, val) -> None: 

4125 # Write the <c:val> element for error bars. 

4126 

4127 attributes = [("val", val)] 

4128 

4129 self._xml_empty_tag("c:val", attributes) 

4130 

4131 def _write_custom_error(self, error_bars) -> None: 

4132 # Write the custom error bars tags. 

4133 

4134 if error_bars["plus_values"]: 

4135 # Write the c:plus element. 

4136 self._xml_start_tag("c:plus") 

4137 

4138 if isinstance(error_bars["plus_values"], list): 

4139 self._write_num_lit(error_bars["plus_values"]) 

4140 else: 

4141 self._write_num_ref( 

4142 error_bars["plus_values"], error_bars["plus_data"], "num" 

4143 ) 

4144 self._xml_end_tag("c:plus") 

4145 

4146 if error_bars["minus_values"]: 

4147 # Write the c:minus element. 

4148 self._xml_start_tag("c:minus") 

4149 

4150 if isinstance(error_bars["minus_values"], list): 

4151 self._write_num_lit(error_bars["minus_values"]) 

4152 else: 

4153 self._write_num_ref( 

4154 error_bars["minus_values"], error_bars["minus_data"], "num" 

4155 ) 

4156 self._xml_end_tag("c:minus") 

4157 

4158 def _write_num_lit(self, data) -> None: 

4159 # Write the <c:numLit> element for literal number list elements. 

4160 count = len(data) 

4161 

4162 # Write the c:numLit element. 

4163 self._xml_start_tag("c:numLit") 

4164 

4165 # Write the c:formatCode element. 

4166 self._write_format_code("General") 

4167 

4168 # Write the c:ptCount element. 

4169 self._write_pt_count(count) 

4170 

4171 for i in range(count): 

4172 token = data[i] 

4173 

4174 if token is None: 

4175 continue 

4176 

4177 try: 

4178 float(token) 

4179 except ValueError: 

4180 # Write non-numeric data as 0. 

4181 token = 0 

4182 

4183 # Write the c:pt element. 

4184 self._write_pt(i, token) 

4185 

4186 self._xml_end_tag("c:numLit") 

4187 

4188 def _write_up_down_bars(self) -> None: 

4189 # Write the <c:upDownBars> element. 

4190 up_down_bars = self.up_down_bars 

4191 

4192 if up_down_bars is None: 

4193 return 

4194 

4195 self._xml_start_tag("c:upDownBars") 

4196 

4197 # Write the c:gapWidth element. 

4198 self._write_gap_width(150) 

4199 

4200 # Write the c:upBars element. 

4201 self._write_up_bars(up_down_bars.get("up")) 

4202 

4203 # Write the c:downBars element. 

4204 self._write_down_bars(up_down_bars.get("down")) 

4205 

4206 self._xml_end_tag("c:upDownBars") 

4207 

4208 def _write_gap_width(self, val) -> None: 

4209 # Write the <c:gapWidth> element. 

4210 

4211 if val is None: 

4212 return 

4213 

4214 attributes = [("val", val)] 

4215 

4216 self._xml_empty_tag("c:gapWidth", attributes) 

4217 

4218 def _write_up_bars(self, bar_format) -> None: 

4219 # Write the <c:upBars> element. 

4220 

4221 if bar_format["line"] and bar_format["line"]["defined"]: 

4222 self._xml_start_tag("c:upBars") 

4223 

4224 # Write the c:spPr element. 

4225 self._write_sp_pr(bar_format) 

4226 

4227 self._xml_end_tag("c:upBars") 

4228 else: 

4229 self._xml_empty_tag("c:upBars") 

4230 

4231 def _write_down_bars(self, bar_format) -> None: 

4232 # Write the <c:downBars> element. 

4233 

4234 if bar_format["line"] and bar_format["line"]["defined"]: 

4235 self._xml_start_tag("c:downBars") 

4236 

4237 # Write the c:spPr element. 

4238 self._write_sp_pr(bar_format) 

4239 

4240 self._xml_end_tag("c:downBars") 

4241 else: 

4242 self._xml_empty_tag("c:downBars") 

4243 

4244 def _write_disp_units(self, units, display) -> None: 

4245 # Write the <c:dispUnits> element. 

4246 

4247 if not units: 

4248 return 

4249 

4250 attributes = [("val", units)] 

4251 

4252 self._xml_start_tag("c:dispUnits") 

4253 self._xml_empty_tag("c:builtInUnit", attributes) 

4254 

4255 if display: 

4256 self._xml_start_tag("c:dispUnitsLbl") 

4257 self._xml_empty_tag("c:layout") 

4258 self._xml_end_tag("c:dispUnitsLbl") 

4259 

4260 self._xml_end_tag("c:dispUnits") 

4261 

4262 def _write_a_grad_fill(self, gradient) -> None: 

4263 # Write the <a:gradFill> element. 

4264 

4265 attributes = [("flip", "none"), ("rotWithShape", "1")] 

4266 

4267 if gradient["type"] == "linear": 

4268 attributes = [] 

4269 

4270 self._xml_start_tag("a:gradFill", attributes) 

4271 

4272 # Write the a:gsLst element. 

4273 self._write_a_gs_lst(gradient) 

4274 

4275 if gradient["type"] == "linear": 

4276 # Write the a:lin element. 

4277 self._write_a_lin(gradient["angle"]) 

4278 else: 

4279 # Write the a:path element. 

4280 self._write_a_path(gradient["type"]) 

4281 

4282 # Write the a:tileRect element. 

4283 self._write_a_tile_rect(gradient["type"]) 

4284 

4285 self._xml_end_tag("a:gradFill") 

4286 

4287 def _write_a_gs_lst(self, gradient) -> None: 

4288 # Write the <a:gsLst> element. 

4289 positions = gradient["positions"] 

4290 colors = gradient["colors"] 

4291 

4292 self._xml_start_tag("a:gsLst") 

4293 

4294 for i, color in enumerate(colors): 

4295 pos = int(positions[i] * 1000) 

4296 attributes = [("pos", pos)] 

4297 self._xml_start_tag("a:gs", attributes) 

4298 

4299 self._write_color(color) 

4300 

4301 self._xml_end_tag("a:gs") 

4302 

4303 self._xml_end_tag("a:gsLst") 

4304 

4305 def _write_a_lin(self, angle) -> None: 

4306 # Write the <a:lin> element. 

4307 

4308 angle = int(60000 * angle) 

4309 

4310 attributes = [ 

4311 ("ang", angle), 

4312 ("scaled", "0"), 

4313 ] 

4314 

4315 self._xml_empty_tag("a:lin", attributes) 

4316 

4317 def _write_a_path(self, gradient_type) -> None: 

4318 # Write the <a:path> element. 

4319 

4320 attributes = [("path", gradient_type)] 

4321 

4322 self._xml_start_tag("a:path", attributes) 

4323 

4324 # Write the a:fillToRect element. 

4325 self._write_a_fill_to_rect(gradient_type) 

4326 

4327 self._xml_end_tag("a:path") 

4328 

4329 def _write_a_fill_to_rect(self, gradient_type) -> None: 

4330 # Write the <a:fillToRect> element. 

4331 

4332 if gradient_type == "shape": 

4333 attributes = [ 

4334 ("l", "50000"), 

4335 ("t", "50000"), 

4336 ("r", "50000"), 

4337 ("b", "50000"), 

4338 ] 

4339 else: 

4340 attributes = [ 

4341 ("l", "100000"), 

4342 ("t", "100000"), 

4343 ] 

4344 

4345 self._xml_empty_tag("a:fillToRect", attributes) 

4346 

4347 def _write_a_tile_rect(self, gradient_type) -> None: 

4348 # Write the <a:tileRect> element. 

4349 

4350 if gradient_type == "shape": 

4351 attributes = [] 

4352 else: 

4353 attributes = [ 

4354 ("r", "-100000"), 

4355 ("b", "-100000"), 

4356 ] 

4357 

4358 self._xml_empty_tag("a:tileRect", attributes) 

4359 

4360 def _write_a_patt_fill(self, pattern) -> None: 

4361 # Write the <a:pattFill> element. 

4362 

4363 attributes = [("prst", pattern["pattern"])] 

4364 

4365 self._xml_start_tag("a:pattFill", attributes) 

4366 

4367 # Write the a:fgClr element. 

4368 self._write_a_fg_clr(pattern["fg_color"]) 

4369 

4370 # Write the a:bgClr element. 

4371 self._write_a_bg_clr(pattern["bg_color"]) 

4372 

4373 self._xml_end_tag("a:pattFill") 

4374 

4375 def _write_a_fg_clr(self, color: Color) -> None: 

4376 # Write the <a:fgClr> element. 

4377 self._xml_start_tag("a:fgClr") 

4378 self._write_color(color) 

4379 self._xml_end_tag("a:fgClr") 

4380 

4381 def _write_a_bg_clr(self, color: Color) -> None: 

4382 # Write the <a:bgClr> element. 

4383 self._xml_start_tag("a:bgClr") 

4384 self._write_color(color) 

4385 self._xml_end_tag("a:bgClr")