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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1992 statements  

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

2# 

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

4# 

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

6# 

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

8# 

9 

10import copy 

11import re 

12from typing import Any, Dict, Optional 

13from warnings import warn 

14 

15from xlsxwriter 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.sheet_type = 0x0200 

51 self.orientation = 0x0 

52 self.series = [] 

53 self.embedded = 0 

54 self.id = -1 

55 self.series_index = 0 

56 self.style_id = 2 

57 self.axis_ids = [] 

58 self.axis2_ids = [] 

59 self.cat_has_num_fmt = False 

60 self.requires_category = False 

61 self.legend = {} 

62 self.cat_axis_position = "b" 

63 self.val_axis_position = "l" 

64 self.formula_ids = {} 

65 self.formula_data = [] 

66 self.horiz_cat_axis = 0 

67 self.horiz_val_axis = 1 

68 self.protection = 0 

69 self.chartarea = {} 

70 self.plotarea = {} 

71 self.x_axis = {} 

72 self.y_axis = {} 

73 self.y2_axis = {} 

74 self.x2_axis = {} 

75 self.chart_name = "" 

76 self.show_blanks = "gap" 

77 self.show_na_as_empty = False 

78 self.show_hidden = False 

79 self.show_crosses = True 

80 self.width = 480 

81 self.height = 288 

82 self.x_scale = 1 

83 self.y_scale = 1 

84 self.x_offset = 0 

85 self.y_offset = 0 

86 self.table = None 

87 self.cross_between = "between" 

88 self.default_marker = None 

89 self.series_gap_1 = None 

90 self.series_gap_2 = None 

91 self.series_overlap_1 = None 

92 self.series_overlap_2 = None 

93 self.drop_lines = None 

94 self.hi_low_lines = None 

95 self.up_down_bars = None 

96 self.smooth_allowed = False 

97 self.title = ChartTitle() 

98 

99 self.date_category = False 

100 self.date_1904 = False 

101 self.remove_timezone = False 

102 self.label_positions = {} 

103 self.label_position_default = "" 

104 self.already_inserted = False 

105 self.combined = None 

106 self.is_secondary = False 

107 self.warn_sheetname = True 

108 self._set_default_properties() 

109 self.fill = {} 

110 

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

112 """ 

113 Add a data series to a chart. 

114 

115 Args: 

116 options: A dictionary of chart series options. 

117 

118 Returns: 

119 Nothing. 

120 

121 """ 

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

123 if options is None: 

124 options = {} 

125 

126 # Check that the required input has been specified. 

127 if "values" not in options: 

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

129 return 

130 

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

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

133 return 

134 

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

136 warn( 

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

138 "Excel Chart is 255" 

139 ) 

140 return 

141 

142 # Convert list into a formula string. 

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

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

145 

146 # Switch name and name_formula parameters if required. 

147 name, name_formula = self._process_names( 

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

149 ) 

150 

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

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

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

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

155 

156 # Set the line properties for the series. 

157 line = Shape._get_line_properties(options) 

158 

159 # Set the fill properties for the series. 

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

161 

162 # Set the pattern fill properties for the series. 

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

164 

165 # Set the gradient fill properties for the series. 

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

167 

168 # Pattern fill overrides solid fill. 

169 if pattern: 

170 self.fill = None 

171 

172 # Gradient fill overrides the solid and pattern fill. 

173 if gradient: 

174 pattern = None 

175 fill = None 

176 

177 # Set the marker properties for the series. 

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

179 

180 # Set the trendline properties for the series. 

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

182 

183 # Set the line smooth property for the series. 

184 smooth = options.get("smooth") 

185 

186 # Set the error bars properties for the series. 

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

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

189 

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

191 

192 # Set the point properties for the series. 

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

194 

195 # Set the labels properties for the series. 

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

197 

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

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

200 inverted_color = options.get("invert_if_negative_color") 

201 

202 if inverted_color: 

203 inverted_color = Color._from_value(inverted_color) 

204 

205 # Set the secondary axis properties. 

206 x2_axis = options.get("x2_axis") 

207 y2_axis = options.get("y2_axis") 

208 

209 # Store secondary status for combined charts. 

210 if x2_axis or y2_axis: 

211 self.is_secondary = True 

212 

213 # Set the gap for Bar/Column charts. 

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

215 if y2_axis: 

216 self.series_gap_2 = options["gap"] 

217 else: 

218 self.series_gap_1 = options["gap"] 

219 

220 # Set the overlap for Bar/Column charts. 

221 if options.get("overlap"): 

222 if y2_axis: 

223 self.series_overlap_2 = options["overlap"] 

224 else: 

225 self.series_overlap_1 = options["overlap"] 

226 

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

228 series = { 

229 "values": values, 

230 "categories": categories, 

231 "name": name, 

232 "name_formula": name_formula, 

233 "name_id": name_id, 

234 "val_data_id": val_id, 

235 "cat_data_id": cat_id, 

236 "line": line, 

237 "fill": fill, 

238 "pattern": pattern, 

239 "gradient": gradient, 

240 "marker": marker, 

241 "trendline": trendline, 

242 "labels": labels, 

243 "invert_if_neg": invert_if_neg, 

244 "inverted_color": inverted_color, 

245 "x2_axis": x2_axis, 

246 "y2_axis": y2_axis, 

247 "points": points, 

248 "error_bars": error_bars, 

249 "smooth": smooth, 

250 } 

251 

252 self.series.append(series) 

253 

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

255 """ 

256 Set the chart X axis options. 

257 

258 Args: 

259 options: A dictionary of axis options. 

260 

261 Returns: 

262 Nothing. 

263 

264 """ 

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

266 

267 self.x_axis = axis 

268 

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

270 """ 

271 Set the chart Y axis options. 

272 

273 Args: 

274 options: A dictionary of axis options. 

275 

276 Returns: 

277 Nothing. 

278 

279 """ 

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

281 

282 self.y_axis = axis 

283 

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

285 """ 

286 Set the chart secondary X axis options. 

287 

288 Args: 

289 options: A dictionary of axis options. 

290 

291 Returns: 

292 Nothing. 

293 

294 """ 

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

296 

297 self.x2_axis = axis 

298 

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

300 """ 

301 Set the chart secondary Y axis options. 

302 

303 Args: 

304 options: A dictionary of axis options. 

305 

306 Returns: 

307 Nothing. 

308 

309 """ 

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

311 

312 self.y2_axis = axis 

313 

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

315 """ 

316 Set the chart title options. 

317 

318 Args: 

319 options: A dictionary of chart title options. 

320 

321 Returns: 

322 Nothing. 

323 

324 """ 

325 if options is None: 

326 options = {} 

327 

328 name, name_formula = self._process_names( 

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

330 ) 

331 

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

333 

334 # Update the main chart title. 

335 self.title.name = name 

336 self.title.formula = name_formula 

337 self.title.data_id = data_id 

338 

339 # Set the font properties if present. 

340 if options.get("font"): 

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

342 else: 

343 # For backward/axis compatibility. 

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

345 

346 # Set the line properties. 

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

348 

349 # Set the fill properties. 

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

351 

352 # Set the gradient properties. 

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

354 

355 # Set the layout. 

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

357 

358 # Set the title overlay option. 

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

360 

361 # Set the automatic title option. 

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

363 

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

365 """ 

366 Set the chart legend options. 

367 

368 Args: 

369 options: A dictionary of chart legend options. 

370 

371 Returns: 

372 Nothing. 

373 """ 

374 # Convert the user defined properties to internal properties. 

375 self.legend = self._get_legend_properties(options) 

376 

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

378 """ 

379 Set the chart plot area options. 

380 

381 Args: 

382 options: A dictionary of chart plot area options. 

383 

384 Returns: 

385 Nothing. 

386 """ 

387 # Convert the user defined properties to internal properties. 

388 self.plotarea = self._get_area_properties(options) 

389 

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

391 """ 

392 Set the chart area options. 

393 

394 Args: 

395 options: A dictionary of chart area options. 

396 

397 Returns: 

398 Nothing. 

399 """ 

400 # Convert the user defined properties to internal properties. 

401 self.chartarea = self._get_area_properties(options) 

402 

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

404 """ 

405 Set the chart style type. 

406 

407 Args: 

408 style_id: An int representing the chart style. 

409 

410 Returns: 

411 Nothing. 

412 """ 

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

414 if style_id is None: 

415 style_id = 2 

416 

417 if style_id < 1 or style_id > 48: 

418 style_id = 2 

419 

420 self.style_id = style_id 

421 

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

423 """ 

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

425 

426 Args: 

427 option: A string representing the display option. 

428 

429 Returns: 

430 Nothing. 

431 """ 

432 if not option: 

433 return 

434 

435 valid_options = { 

436 "gap": 1, 

437 "zero": 1, 

438 "span": 1, 

439 } 

440 

441 if option not in valid_options: 

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

443 return 

444 

445 self.show_blanks = option 

446 

447 def show_na_as_empty_cell(self) -> None: 

448 """ 

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

450 

451 Args: 

452 None. 

453 

454 Returns: 

455 Nothing. 

456 """ 

457 self.show_na_as_empty = True 

458 

459 def show_hidden_data(self) -> None: 

460 """ 

461 Display data on charts from hidden rows or columns. 

462 

463 Args: 

464 None. 

465 

466 Returns: 

467 Nothing. 

468 """ 

469 self.show_hidden = True 

470 

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

472 """ 

473 Set size or scale of the chart. 

474 

475 Args: 

476 options: A dictionary of chart size options. 

477 

478 Returns: 

479 Nothing. 

480 """ 

481 if options is None: 

482 options = {} 

483 

484 # Set dimensions or scale for the chart. 

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

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

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

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

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

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

491 

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

493 """ 

494 Set properties for an axis data table. 

495 

496 Args: 

497 options: A dictionary of axis table options. 

498 

499 Returns: 

500 Nothing. 

501 

502 """ 

503 if options is None: 

504 options = {} 

505 

506 table = {} 

507 

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

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

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

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

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

513 

514 self.table = table 

515 

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

517 """ 

518 Set properties for the chart up-down bars. 

519 

520 Args: 

521 options: A dictionary of options. 

522 

523 Returns: 

524 Nothing. 

525 

526 """ 

527 if options is None: 

528 options = {} 

529 

530 # Defaults. 

531 up_line = None 

532 up_fill = None 

533 down_line = None 

534 down_fill = None 

535 

536 # Set properties for 'up' bar. 

537 if options.get("up"): 

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

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

540 

541 # Set properties for 'down' bar. 

542 if options.get("down"): 

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

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

545 

546 self.up_down_bars = { 

547 "up": { 

548 "line": up_line, 

549 "fill": up_fill, 

550 }, 

551 "down": { 

552 "line": down_line, 

553 "fill": down_fill, 

554 }, 

555 } 

556 

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

558 """ 

559 Set properties for the chart drop lines. 

560 

561 Args: 

562 options: A dictionary of options. 

563 

564 Returns: 

565 Nothing. 

566 

567 """ 

568 if options is None: 

569 options = {} 

570 

571 line = Shape._get_line_properties(options) 

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

573 

574 # Set the pattern fill properties for the series. 

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

576 

577 # Set the gradient fill properties for the series. 

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

579 

580 # Pattern fill overrides solid fill. 

581 if pattern: 

582 self.fill = None 

583 

584 # Gradient fill overrides the solid and pattern fill. 

585 if gradient: 

586 pattern = None 

587 fill = None 

588 

589 self.drop_lines = { 

590 "line": line, 

591 "fill": fill, 

592 "pattern": pattern, 

593 "gradient": gradient, 

594 } 

595 

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

597 """ 

598 Set properties for the chart high-low lines. 

599 

600 Args: 

601 options: A dictionary of options. 

602 

603 Returns: 

604 Nothing. 

605 

606 """ 

607 if options is None: 

608 options = {} 

609 

610 line = Shape._get_line_properties(options) 

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

612 

613 # Set the pattern fill properties for the series. 

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

615 

616 # Set the gradient fill properties for the series. 

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

618 

619 # Pattern fill overrides solid fill. 

620 if pattern: 

621 self.fill = None 

622 

623 # Gradient fill overrides the solid and pattern fill. 

624 if gradient: 

625 pattern = None 

626 fill = None 

627 

628 self.hi_low_lines = { 

629 "line": line, 

630 "fill": fill, 

631 "pattern": pattern, 

632 "gradient": gradient, 

633 } 

634 

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

636 """ 

637 Create a combination chart with a secondary chart. 

638 

639 Args: 

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

641 

642 Returns: 

643 Nothing. 

644 

645 """ 

646 if chart is None: 

647 return 

648 

649 self.combined = chart 

650 

651 ########################################################################### 

652 # 

653 # Private API. 

654 # 

655 ########################################################################### 

656 

657 def _assemble_xml_file(self) -> None: 

658 # Assemble and write the XML file. 

659 

660 # Write the XML declaration. 

661 self._xml_declaration() 

662 

663 # Write the c:chartSpace element. 

664 self._write_chart_space() 

665 

666 # Write the c:lang element. 

667 self._write_lang() 

668 

669 # Write the c:style element. 

670 self._write_style() 

671 

672 # Write the c:protection element. 

673 self._write_protection() 

674 

675 # Write the c:chart element. 

676 self._write_chart() 

677 

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

679 self._write_sp_pr(self.chartarea) 

680 

681 # Write the c:printSettings element. 

682 if self.embedded: 

683 self._write_print_settings() 

684 

685 # Close the worksheet tag. 

686 self._xml_end_tag("c:chartSpace") 

687 # Close the file. 

688 self._xml_close() 

689 

690 def _convert_axis_args(self, axis, user_options): 

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

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

693 options.update(user_options) 

694 

695 axis = { 

696 "defaults": axis["defaults"], 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

715 "text_axis": False, 

716 "title": ChartTitle(), 

717 } 

718 

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

720 

721 # Convert the display units. 

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

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

724 

725 # Map major_gridlines properties. 

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

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

728 options["major_gridlines"] 

729 ) 

730 

731 # Map minor_gridlines properties. 

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

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

734 options["minor_gridlines"] 

735 ) 

736 

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

738 if axis.get("position"): 

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

740 

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

742 if axis.get("position_axis"): 

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

744 axis["position_axis"] = "midCat" 

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

746 # Doesn't need to be modified. 

747 pass 

748 else: 

749 # Otherwise use the default value. 

750 axis["position_axis"] = None 

751 

752 # Set the category axis as a date axis. 

753 if options.get("date_axis"): 

754 self.date_category = True 

755 

756 # Set the category axis as a text axis. 

757 if options.get("text_axis"): 

758 self.date_category = False 

759 axis["text_axis"] = True 

760 

761 # Convert datetime args if required. 

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

763 axis["min"] = _datetime_to_excel_datetime( 

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

765 ) 

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

767 axis["max"] = _datetime_to_excel_datetime( 

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

769 ) 

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

771 axis["crossing"] = _datetime_to_excel_datetime( 

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

773 ) 

774 

775 # Set the font properties if present. 

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

777 

778 # Set the line properties for the axis. 

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

780 

781 # Set the fill properties for the axis. 

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

783 

784 # Set the pattern fill properties for the series. 

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

786 

787 # Set the gradient fill properties for the series. 

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

789 

790 # Pattern fill overrides solid fill. 

791 if axis.get("pattern"): 

792 axis["fill"] = None 

793 

794 # Gradient fill overrides the solid and pattern fill. 

795 if axis.get("gradient"): 

796 axis["pattern"] = None 

797 axis["fill"] = None 

798 

799 # Set the tick marker types. 

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

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

802 

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

804 name, name_formula = self._process_names( 

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

806 ) 

807 

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

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

810 

811 # Set the title properties. 

812 axis["title"].name = name 

813 axis["title"].formula = name_formula 

814 axis["title"].data_id = data_id 

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

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

817 options.get("name_layout"), True 

818 ) 

819 

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

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

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

823 

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

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

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

827 options.get("name_pattern") 

828 ) 

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

830 options.get("name_gradient") 

831 ) 

832 

833 return axis 

834 

835 def _convert_font_args(self, options): 

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

837 if not options: 

838 return {} 

839 

840 font = { 

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

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

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

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

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

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

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

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

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

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

851 } 

852 

853 # Convert font size units. 

854 if font["size"]: 

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

856 

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

858 if font["rotation"]: 

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

860 

861 if font.get("color"): 

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

863 

864 return font 

865 

866 def _list_to_formula(self, data): 

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

868 

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

870 if not isinstance(data, list): 

871 # Check for unquoted sheetnames. 

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

873 warn( 

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

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

876 ) 

877 return data 

878 

879 formula = xl_range_formula(*data) 

880 

881 return formula 

882 

883 def _process_names(self, name, name_formula): 

884 # Switch name and name_formula parameters if required. 

885 

886 if name is not None: 

887 if isinstance(name, list): 

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

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

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

891 name = "" 

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

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

894 name_formula = name 

895 name = "" 

896 

897 return name, name_formula 

898 

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

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

901 

902 # Check for no data in the series. 

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

904 return "none" 

905 

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

907 return "multi_str" 

908 

909 # Determine if data is numeric or strings. 

910 for token in data: 

911 if token is None: 

912 continue 

913 

914 # Check for strings that would evaluate to float like 

915 # '1.1_1' of ' 1'. 

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

917 # Assume entire data series is string data. 

918 return "str" 

919 

920 try: 

921 float(token) 

922 except ValueError: 

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

924 return "str" 

925 

926 # The series data was all numeric. 

927 return "num" 

928 

929 def _get_data_id(self, formula, data): 

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

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

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

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

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

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

936 

937 # Ignore series without a range formula. 

938 if not formula: 

939 return None 

940 

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

942 if formula.startswith("="): 

943 formula = formula.lstrip("=") 

944 

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

946 # in a separate array with the same id. 

947 if formula not in self.formula_ids: 

948 # Haven't seen this formula before. 

949 formula_id = len(self.formula_data) 

950 

951 self.formula_data.append(data) 

952 self.formula_ids[formula] = formula_id 

953 else: 

954 # Formula already seen. Return existing id. 

955 formula_id = self.formula_ids[formula] 

956 

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

958 if self.formula_data[formula_id] is None: 

959 self.formula_data[formula_id] = data 

960 

961 return formula_id 

962 

963 def _get_marker_properties(self, marker): 

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

965 

966 if not marker: 

967 return None 

968 

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

970 marker = copy.deepcopy(marker) 

971 

972 types = { 

973 "automatic": "automatic", 

974 "none": "none", 

975 "square": "square", 

976 "diamond": "diamond", 

977 "triangle": "triangle", 

978 "x": "x", 

979 "star": "star", 

980 "dot": "dot", 

981 "short_dash": "dot", 

982 "dash": "dash", 

983 "long_dash": "dash", 

984 "circle": "circle", 

985 "plus": "plus", 

986 "picture": "picture", 

987 } 

988 

989 # Check for valid types. 

990 marker_type = marker.get("type") 

991 

992 if marker_type is not None: 

993 if marker_type in types: 

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

995 else: 

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

997 return None 

998 

999 # Set the line properties for the marker. 

1000 line = Shape._get_line_properties(marker) 

1001 

1002 # Set the fill properties for the marker. 

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

1004 

1005 # Set the pattern fill properties for the series. 

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

1007 

1008 # Set the gradient fill properties for the series. 

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

1010 

1011 # Pattern fill overrides solid fill. 

1012 if pattern: 

1013 self.fill = None 

1014 

1015 # Gradient fill overrides the solid and pattern fill. 

1016 if gradient: 

1017 pattern = None 

1018 fill = None 

1019 

1020 marker["line"] = line 

1021 marker["fill"] = fill 

1022 marker["pattern"] = pattern 

1023 marker["gradient"] = gradient 

1024 

1025 return marker 

1026 

1027 def _get_trendline_properties(self, trendline): 

1028 # Convert user trendline properties to structure required internally. 

1029 

1030 if not trendline: 

1031 return None 

1032 

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

1034 trendline = copy.deepcopy(trendline) 

1035 

1036 types = { 

1037 "exponential": "exp", 

1038 "linear": "linear", 

1039 "log": "log", 

1040 "moving_average": "movingAvg", 

1041 "polynomial": "poly", 

1042 "power": "power", 

1043 } 

1044 

1045 # Check the trendline type. 

1046 trend_type = trendline.get("type") 

1047 

1048 if trend_type in types: 

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

1050 else: 

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

1052 return None 

1053 

1054 # Set the line properties for the trendline. 

1055 line = Shape._get_line_properties(trendline) 

1056 

1057 # Set the fill properties for the trendline. 

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

1059 

1060 # Set the pattern fill properties for the trendline. 

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

1062 

1063 # Set the gradient fill properties for the trendline. 

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

1065 

1066 # Set the format properties for the trendline label. 

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

1068 

1069 # Pattern fill overrides solid fill. 

1070 if pattern: 

1071 self.fill = None 

1072 

1073 # Gradient fill overrides the solid and pattern fill. 

1074 if gradient: 

1075 pattern = None 

1076 fill = None 

1077 

1078 trendline["line"] = line 

1079 trendline["fill"] = fill 

1080 trendline["pattern"] = pattern 

1081 trendline["gradient"] = gradient 

1082 trendline["label"] = label 

1083 

1084 return trendline 

1085 

1086 def _get_trendline_label_properties(self, label): 

1087 # Convert user trendline properties to structure required internally. 

1088 

1089 if not label: 

1090 return {} 

1091 

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

1093 label = copy.deepcopy(label) 

1094 

1095 # Set the font properties if present. 

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

1097 

1098 # Set the line properties for the label. 

1099 line = Shape._get_line_properties(label) 

1100 

1101 # Set the fill properties for the label. 

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

1103 

1104 # Set the pattern fill properties for the label. 

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

1106 

1107 # Set the gradient fill properties for the label. 

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

1109 

1110 # Pattern fill overrides solid fill. 

1111 if pattern: 

1112 self.fill = None 

1113 

1114 # Gradient fill overrides the solid and pattern fill. 

1115 if gradient: 

1116 pattern = None 

1117 fill = None 

1118 

1119 label["font"] = font 

1120 label["line"] = line 

1121 label["fill"] = fill 

1122 label["pattern"] = pattern 

1123 label["gradient"] = gradient 

1124 

1125 return label 

1126 

1127 def _get_error_bars_props(self, options): 

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

1129 if not options: 

1130 return {} 

1131 

1132 # Default values. 

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

1134 

1135 types = { 

1136 "fixed": "fixedVal", 

1137 "percentage": "percentage", 

1138 "standard_deviation": "stdDev", 

1139 "standard_error": "stdErr", 

1140 "custom": "cust", 

1141 } 

1142 

1143 # Check the error bars type. 

1144 error_type = options["type"] 

1145 

1146 if error_type in types: 

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

1148 else: 

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

1150 return {} 

1151 

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

1153 if "value" in options: 

1154 error_bars["value"] = options["value"] 

1155 

1156 # Set the end-cap style. 

1157 if "end_style" in options: 

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

1159 

1160 # Set the error bar direction. 

1161 if "direction" in options: 

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

1163 error_bars["direction"] = "minus" 

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

1165 error_bars["direction"] = "plus" 

1166 else: 

1167 # Default to 'both'. 

1168 pass 

1169 

1170 # Set any custom values. 

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

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

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

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

1175 

1176 # Set the line properties for the error bars. 

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

1178 

1179 return error_bars 

1180 

1181 def _get_gridline_properties(self, options): 

1182 # Convert user gridline properties to structure required internally. 

1183 

1184 # Set the visible property for the gridline. 

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

1186 

1187 # Set the line properties for the gridline. 

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

1189 

1190 return gridline 

1191 

1192 def _get_labels_properties(self, labels): 

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

1194 

1195 if not labels: 

1196 return None 

1197 

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

1199 labels = copy.deepcopy(labels) 

1200 

1201 # Map user defined label positions to Excel positions. 

1202 position = labels.get("position") 

1203 

1204 if position: 

1205 if position in self.label_positions: 

1206 if position == self.label_position_default: 

1207 labels["position"] = None 

1208 else: 

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

1210 else: 

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

1212 return None 

1213 

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

1215 separator = labels.get("separator") 

1216 separators = { 

1217 ",": ", ", 

1218 ";": "; ", 

1219 ".": ". ", 

1220 "\n": "\n", 

1221 " ": " ", 

1222 } 

1223 

1224 if separator: 

1225 if separator in separators: 

1226 labels["separator"] = separators[separator] 

1227 else: 

1228 warn("Unsupported label separator") 

1229 return None 

1230 

1231 # Set the font properties if present. 

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

1233 

1234 # Set the line properties for the labels. 

1235 line = Shape._get_line_properties(labels) 

1236 

1237 # Set the fill properties for the labels. 

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

1239 

1240 # Set the pattern fill properties for the labels. 

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

1242 

1243 # Set the gradient fill properties for the labels. 

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

1245 

1246 # Pattern fill overrides solid fill. 

1247 if pattern: 

1248 self.fill = None 

1249 

1250 # Gradient fill overrides the solid and pattern fill. 

1251 if gradient: 

1252 pattern = None 

1253 fill = None 

1254 

1255 labels["line"] = line 

1256 labels["fill"] = fill 

1257 labels["pattern"] = pattern 

1258 labels["gradient"] = gradient 

1259 

1260 if labels.get("custom"): 

1261 for label in labels["custom"]: 

1262 if label is None: 

1263 continue 

1264 

1265 value = label.get("value") 

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

1267 label["formula"] = value 

1268 

1269 formula = label.get("formula") 

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

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

1272 

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

1274 label["data_id"] = data_id 

1275 

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

1277 

1278 # Set the line properties for the label. 

1279 line = Shape._get_line_properties(label) 

1280 

1281 # Set the fill properties for the label. 

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

1283 

1284 # Set the pattern fill properties for the label. 

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

1286 

1287 # Set the gradient fill properties for the label. 

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

1289 

1290 # Pattern fill overrides solid fill. 

1291 if pattern: 

1292 self.fill = None 

1293 

1294 # Gradient fill overrides the solid and pattern fill. 

1295 if gradient: 

1296 pattern = None 

1297 fill = None 

1298 

1299 # Map user defined label positions to Excel positions. 

1300 position = label.get("position") 

1301 

1302 if position: 

1303 if position in self.label_positions: 

1304 if position == self.label_position_default: 

1305 label["position"] = None 

1306 else: 

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

1308 else: 

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

1310 return None 

1311 

1312 label["line"] = line 

1313 label["fill"] = fill 

1314 label["pattern"] = pattern 

1315 label["gradient"] = gradient 

1316 

1317 return labels 

1318 

1319 def _get_area_properties(self, options): 

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

1321 area = {} 

1322 

1323 # Set the line properties for the chartarea. 

1324 line = Shape._get_line_properties(options) 

1325 

1326 # Set the fill properties for the chartarea. 

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

1328 

1329 # Set the pattern fill properties for the series. 

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

1331 

1332 # Set the gradient fill properties for the series. 

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

1334 

1335 # Pattern fill overrides solid fill. 

1336 if pattern: 

1337 self.fill = None 

1338 

1339 # Gradient fill overrides the solid and pattern fill. 

1340 if gradient: 

1341 pattern = None 

1342 fill = None 

1343 

1344 # Set the plotarea layout. 

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

1346 

1347 area["line"] = line 

1348 area["fill"] = fill 

1349 area["pattern"] = pattern 

1350 area["layout"] = layout 

1351 area["gradient"] = gradient 

1352 

1353 return area 

1354 

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

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

1357 legend = {} 

1358 

1359 if options is None: 

1360 options = {} 

1361 

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

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

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

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

1366 

1367 # Turn off the legend. 

1368 if options.get("none"): 

1369 legend["position"] = "none" 

1370 

1371 # Set the line properties for the legend. 

1372 line = Shape._get_line_properties(options) 

1373 

1374 # Set the fill properties for the legend. 

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

1376 

1377 # Set the pattern fill properties for the series. 

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

1379 

1380 # Set the gradient fill properties for the series. 

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

1382 

1383 # Pattern fill overrides solid fill. 

1384 if pattern: 

1385 self.fill = None 

1386 

1387 # Gradient fill overrides the solid and pattern fill. 

1388 if gradient: 

1389 pattern = None 

1390 fill = None 

1391 

1392 # Set the legend layout. 

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

1394 

1395 legend["line"] = line 

1396 legend["fill"] = fill 

1397 legend["pattern"] = pattern 

1398 legend["layout"] = layout 

1399 legend["gradient"] = gradient 

1400 

1401 return legend 

1402 

1403 def _get_layout_properties(self, args, is_text): 

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

1405 layout = {} 

1406 

1407 if not args: 

1408 return {} 

1409 

1410 if is_text: 

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

1412 else: 

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

1414 

1415 # Check for valid properties. 

1416 for key in args.keys(): 

1417 if key not in properties: 

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

1419 return {} 

1420 

1421 # Set the layout properties. 

1422 for prop in properties: 

1423 if prop not in args.keys(): 

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

1425 return {} 

1426 

1427 value = args[prop] 

1428 

1429 try: 

1430 float(value) 

1431 except ValueError: 

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

1433 return {} 

1434 

1435 if value < 0 or value > 1: 

1436 warn( 

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

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

1439 ) 

1440 return {} 

1441 

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

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

1444 

1445 return layout 

1446 

1447 def _get_points_properties(self, user_points): 

1448 # Convert user points properties to structure required internally. 

1449 points = [] 

1450 

1451 if not user_points: 

1452 return [] 

1453 

1454 for user_point in user_points: 

1455 point = {} 

1456 

1457 if user_point is not None: 

1458 # Set the line properties for the point. 

1459 line = Shape._get_line_properties(user_point) 

1460 

1461 # Set the fill properties for the chartarea. 

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

1463 

1464 # Set the pattern fill properties for the series. 

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

1466 

1467 # Set the gradient fill properties for the series. 

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

1469 

1470 # Pattern fill overrides solid fill. 

1471 if pattern: 

1472 self.fill = None 

1473 

1474 # Gradient fill overrides the solid and pattern fill. 

1475 if gradient: 

1476 pattern = None 

1477 fill = None 

1478 

1479 point["line"] = line 

1480 point["fill"] = fill 

1481 point["pattern"] = pattern 

1482 point["gradient"] = gradient 

1483 

1484 points.append(point) 

1485 

1486 return points 

1487 

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

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

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

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

1492 has_pattern = element.get("pattern") 

1493 has_gradient = element.get("gradient") 

1494 

1495 return has_fill or has_line or has_pattern or has_gradient 

1496 

1497 def _get_display_units(self, display_units): 

1498 # Convert user defined display units to internal units. 

1499 if not display_units: 

1500 return None 

1501 

1502 types = { 

1503 "hundreds": "hundreds", 

1504 "thousands": "thousands", 

1505 "ten_thousands": "tenThousands", 

1506 "hundred_thousands": "hundredThousands", 

1507 "millions": "millions", 

1508 "ten_millions": "tenMillions", 

1509 "hundred_millions": "hundredMillions", 

1510 "billions": "billions", 

1511 "trillions": "trillions", 

1512 } 

1513 

1514 if display_units in types: 

1515 display_units = types[display_units] 

1516 else: 

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

1518 return None 

1519 

1520 return display_units 

1521 

1522 def _get_tick_type(self, tick_type): 

1523 # Convert user defined display units to internal units. 

1524 if not tick_type: 

1525 return None 

1526 

1527 types = { 

1528 "outside": "out", 

1529 "inside": "in", 

1530 "none": "none", 

1531 "cross": "cross", 

1532 } 

1533 

1534 if tick_type in types: 

1535 tick_type = types[tick_type] 

1536 else: 

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

1538 return None 

1539 

1540 return tick_type 

1541 

1542 def _get_primary_axes_series(self): 

1543 # Returns series which use the primary axes. 

1544 primary_axes_series = [] 

1545 

1546 for series in self.series: 

1547 if not series["y2_axis"]: 

1548 primary_axes_series.append(series) 

1549 

1550 return primary_axes_series 

1551 

1552 def _get_secondary_axes_series(self): 

1553 # Returns series which use the secondary axes. 

1554 secondary_axes_series = [] 

1555 

1556 for series in self.series: 

1557 if series["y2_axis"]: 

1558 secondary_axes_series.append(series) 

1559 

1560 return secondary_axes_series 

1561 

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

1563 # Add unique ids for primary or secondary axes 

1564 chart_id = 5001 + int(self.id) 

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

1566 

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

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

1569 

1570 if args["primary_axes"]: 

1571 self.axis_ids.append(id1) 

1572 self.axis_ids.append(id2) 

1573 

1574 if not args["primary_axes"]: 

1575 self.axis2_ids.append(id1) 

1576 self.axis2_ids.append(id2) 

1577 

1578 def _set_default_properties(self) -> None: 

1579 # Setup the default properties for a chart. 

1580 

1581 self.x_axis["defaults"] = { 

1582 "num_format": "General", 

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

1584 } 

1585 

1586 self.y_axis["defaults"] = { 

1587 "num_format": "General", 

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

1589 } 

1590 

1591 self.x2_axis["defaults"] = { 

1592 "num_format": "General", 

1593 "label_position": "none", 

1594 "crossing": "max", 

1595 "visible": 0, 

1596 } 

1597 

1598 self.y2_axis["defaults"] = { 

1599 "num_format": "General", 

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

1601 "position": "right", 

1602 "visible": 1, 

1603 } 

1604 

1605 self.set_x_axis({}) 

1606 self.set_y_axis({}) 

1607 

1608 self.set_x2_axis({}) 

1609 self.set_y2_axis({}) 

1610 

1611 ########################################################################### 

1612 # 

1613 # XML methods. 

1614 # 

1615 ########################################################################### 

1616 

1617 def _write_chart_space(self) -> None: 

1618 # Write the <c:chartSpace> element. 

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

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

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

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

1623 

1624 attributes = [ 

1625 ("xmlns:c", xmlns_c), 

1626 ("xmlns:a", xmlns_a), 

1627 ("xmlns:r", xmlns_r), 

1628 ] 

1629 

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

1631 

1632 def _write_lang(self) -> None: 

1633 # Write the <c:lang> element. 

1634 val = "en-US" 

1635 

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

1637 

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

1639 

1640 def _write_style(self) -> None: 

1641 # Write the <c:style> element. 

1642 style_id = self.style_id 

1643 

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

1645 if style_id == 2: 

1646 return 

1647 

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

1649 

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

1651 

1652 def _write_chart(self) -> None: 

1653 # Write the <c:chart> element. 

1654 self._xml_start_tag("c:chart") 

1655 

1656 if self.title.is_hidden(): 

1657 # Turn off the title. 

1658 self._write_c_auto_title_deleted() 

1659 else: 

1660 # Write the chart title elements. 

1661 self._write_title(self.title) 

1662 

1663 # Write the c:plotArea element. 

1664 self._write_plot_area() 

1665 

1666 # Write the c:legend element. 

1667 self._write_legend() 

1668 

1669 # Write the c:plotVisOnly element. 

1670 self._write_plot_vis_only() 

1671 

1672 # Write the c:dispBlanksAs element. 

1673 self._write_disp_blanks_as() 

1674 

1675 # Write the c:extLst element. 

1676 if self.show_na_as_empty: 

1677 self._write_c_ext_lst_display_na() 

1678 

1679 self._xml_end_tag("c:chart") 

1680 

1681 def _write_disp_blanks_as(self) -> None: 

1682 # Write the <c:dispBlanksAs> element. 

1683 val = self.show_blanks 

1684 

1685 # Ignore the default value. 

1686 if val == "gap": 

1687 return 

1688 

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

1690 

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

1692 

1693 def _write_plot_area(self) -> None: 

1694 # Write the <c:plotArea> element. 

1695 self._xml_start_tag("c:plotArea") 

1696 

1697 # Write the c:layout element. 

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

1699 

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

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

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

1703 

1704 # Configure a combined chart if present. 

1705 second_chart = self.combined 

1706 if second_chart: 

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

1708 if second_chart.is_secondary: 

1709 second_chart.id = 1000 + self.id 

1710 else: 

1711 second_chart.id = self.id 

1712 

1713 # Share the same filehandle for writing. 

1714 second_chart.fh = self.fh 

1715 

1716 # Share series index with primary chart. 

1717 second_chart.series_index = self.series_index 

1718 

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

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

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

1722 

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

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

1725 

1726 if self.date_category: 

1727 self._write_date_axis(args) 

1728 else: 

1729 self._write_cat_axis(args) 

1730 

1731 self._write_val_axis(args) 

1732 

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

1734 args = { 

1735 "x_axis": self.x2_axis, 

1736 "y_axis": self.y2_axis, 

1737 "axis_ids": self.axis2_ids, 

1738 } 

1739 

1740 self._write_val_axis(args) 

1741 

1742 # Write the secondary axis for the secondary chart. 

1743 if second_chart and second_chart.is_secondary: 

1744 args = { 

1745 "x_axis": second_chart.x2_axis, 

1746 "y_axis": second_chart.y2_axis, 

1747 "axis_ids": second_chart.axis2_ids, 

1748 } 

1749 

1750 second_chart._write_val_axis(args) 

1751 

1752 if self.date_category: 

1753 self._write_date_axis(args) 

1754 else: 

1755 self._write_cat_axis(args) 

1756 

1757 # Write the c:dTable element. 

1758 self._write_d_table() 

1759 

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

1761 self._write_sp_pr(self.plotarea) 

1762 

1763 self._xml_end_tag("c:plotArea") 

1764 

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

1766 # Write the <c:layout> element. 

1767 

1768 if not layout: 

1769 # Automatic layout. 

1770 self._xml_empty_tag("c:layout") 

1771 else: 

1772 # User defined manual layout. 

1773 self._xml_start_tag("c:layout") 

1774 self._write_manual_layout(layout, layout_type) 

1775 self._xml_end_tag("c:layout") 

1776 

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

1778 # Write the <c:manualLayout> element. 

1779 self._xml_start_tag("c:manualLayout") 

1780 

1781 # Plotarea has a layoutTarget element. 

1782 if layout_type == "plot": 

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

1784 

1785 # Set the x, y positions. 

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

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

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

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

1790 

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

1792 if layout_type != "text": 

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

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

1795 

1796 self._xml_end_tag("c:manualLayout") 

1797 

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

1799 # pylint: disable=unused-argument 

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

1801 # by the subclasses. 

1802 return 

1803 

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

1805 # Write the <c:grouping> element. 

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

1807 

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

1809 

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

1811 # Write the series elements. 

1812 self._write_ser(series) 

1813 

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

1815 # Write the <c:ser> element. 

1816 index = self.series_index 

1817 self.series_index += 1 

1818 

1819 self._xml_start_tag("c:ser") 

1820 

1821 # Write the c:idx element. 

1822 self._write_idx(index) 

1823 

1824 # Write the c:order element. 

1825 self._write_order(index) 

1826 

1827 # Write the series name. 

1828 self._write_series_name(series) 

1829 

1830 # Write the c:spPr element. 

1831 self._write_sp_pr(series) 

1832 

1833 # Write the c:marker element. 

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

1835 

1836 # Write the c:invertIfNegative element. 

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

1838 

1839 # Write the c:dPt element. 

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

1841 

1842 # Write the c:dLbls element. 

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

1844 

1845 # Write the c:trendline element. 

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

1847 

1848 # Write the c:errBars element. 

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

1850 

1851 # Write the c:cat element. 

1852 self._write_cat(series) 

1853 

1854 # Write the c:val element. 

1855 self._write_val(series) 

1856 

1857 # Write the c:smooth element. 

1858 if self.smooth_allowed: 

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

1860 

1861 # Write the c:extLst element. 

1862 if series.get("inverted_color"): 

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

1864 

1865 self._xml_end_tag("c:ser") 

1866 

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

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

1869 

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

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

1872 

1873 attributes1 = [ 

1874 ("uri", uri), 

1875 ("xmlns:c14", xmlns_c_14), 

1876 ] 

1877 

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

1879 

1880 self._xml_start_tag("c:extLst") 

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

1882 self._xml_start_tag("c14:invertSolidFillFmt") 

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

1884 

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

1886 

1887 self._xml_end_tag("c14:spPr") 

1888 self._xml_end_tag("c14:invertSolidFillFmt") 

1889 self._xml_end_tag("c:ext") 

1890 self._xml_end_tag("c:extLst") 

1891 

1892 def _write_c_ext_lst_display_na(self) -> None: 

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

1894 

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

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

1897 

1898 attributes1 = [ 

1899 ("uri", uri), 

1900 ("xmlns:c16r3", xmlns_c_16), 

1901 ] 

1902 

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

1904 

1905 self._xml_start_tag("c:extLst") 

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

1907 self._xml_start_tag("c16r3:dataDisplayOptions16") 

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

1909 self._xml_end_tag("c16r3:dataDisplayOptions16") 

1910 self._xml_end_tag("c:ext") 

1911 self._xml_end_tag("c:extLst") 

1912 

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

1914 # Write the <c:idx> element. 

1915 

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

1917 

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

1919 

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

1921 # Write the <c:order> element. 

1922 

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

1924 

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

1926 

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

1928 # Write the series name. 

1929 

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

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

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

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

1934 

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

1936 # Write the <c:smooth> element. 

1937 

1938 if smooth: 

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

1940 

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

1942 # Write the <c:cat> element. 

1943 formula = series["categories"] 

1944 data_id = series["cat_data_id"] 

1945 data = None 

1946 

1947 if data_id is not None: 

1948 data = self.formula_data[data_id] 

1949 

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

1951 if not formula: 

1952 return 

1953 

1954 self._xml_start_tag("c:cat") 

1955 

1956 # Check the type of cached data. 

1957 cat_type = self._get_data_type(data) 

1958 

1959 if cat_type == "str": 

1960 self.cat_has_num_fmt = False 

1961 # Write the c:numRef element. 

1962 self._write_str_ref(formula, data, cat_type) 

1963 

1964 elif cat_type == "multi_str": 

1965 self.cat_has_num_fmt = False 

1966 # Write the c:numRef element. 

1967 self._write_multi_lvl_str_ref(formula, data) 

1968 

1969 else: 

1970 self.cat_has_num_fmt = True 

1971 # Write the c:numRef element. 

1972 self._write_num_ref(formula, data, cat_type) 

1973 

1974 self._xml_end_tag("c:cat") 

1975 

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

1977 # Write the <c:val> element. 

1978 formula = series["values"] 

1979 data_id = series["val_data_id"] 

1980 data = self.formula_data[data_id] 

1981 

1982 self._xml_start_tag("c:val") 

1983 

1984 # Unlike Cat axes data should only be numeric. 

1985 # Write the c:numRef element. 

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

1987 

1988 self._xml_end_tag("c:val") 

1989 

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

1991 # Write the <c:numRef> element. 

1992 self._xml_start_tag("c:numRef") 

1993 

1994 # Write the c:f element. 

1995 self._write_series_formula(formula) 

1996 

1997 if ref_type == "num": 

1998 # Write the c:numCache element. 

1999 self._write_num_cache(data) 

2000 elif ref_type == "str": 

2001 # Write the c:strCache element. 

2002 self._write_str_cache(data) 

2003 

2004 self._xml_end_tag("c:numRef") 

2005 

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

2007 # Write the <c:strRef> element. 

2008 

2009 self._xml_start_tag("c:strRef") 

2010 

2011 # Write the c:f element. 

2012 self._write_series_formula(formula) 

2013 

2014 if ref_type == "num": 

2015 # Write the c:numCache element. 

2016 self._write_num_cache(data) 

2017 elif ref_type == "str": 

2018 # Write the c:strCache element. 

2019 self._write_str_cache(data) 

2020 

2021 self._xml_end_tag("c:strRef") 

2022 

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

2024 # Write the <c:multiLvlStrRef> element. 

2025 

2026 if not data: 

2027 return 

2028 

2029 self._xml_start_tag("c:multiLvlStrRef") 

2030 

2031 # Write the c:f element. 

2032 self._write_series_formula(formula) 

2033 

2034 self._xml_start_tag("c:multiLvlStrCache") 

2035 

2036 # Write the c:ptCount element. 

2037 count = len(data[-1]) 

2038 self._write_pt_count(count) 

2039 

2040 for cat_data in reversed(data): 

2041 self._xml_start_tag("c:lvl") 

2042 

2043 for i, point in enumerate(cat_data): 

2044 # Write the c:pt element. 

2045 self._write_pt(i, point) 

2046 

2047 self._xml_end_tag("c:lvl") 

2048 

2049 self._xml_end_tag("c:multiLvlStrCache") 

2050 self._xml_end_tag("c:multiLvlStrRef") 

2051 

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

2053 # Write the <c:f> element. 

2054 

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

2056 if formula.startswith("="): 

2057 formula = formula.lstrip("=") 

2058 

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

2060 

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

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

2063 

2064 # Generate the axis ids. 

2065 self._add_axis_ids(args) 

2066 

2067 if args["primary_axes"]: 

2068 # Write the axis ids for the primary axes. 

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

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

2071 else: 

2072 # Write the axis ids for the secondary axes. 

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

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

2075 

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

2077 # Write the <c:axId> element. 

2078 

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

2080 

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

2082 

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

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

2085 x_axis = args["x_axis"] 

2086 y_axis = args["y_axis"] 

2087 axis_ids = args["axis_ids"] 

2088 

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

2090 if axis_ids is None or not axis_ids: 

2091 return 

2092 

2093 position = self.cat_axis_position 

2094 is_horizontal = self.horiz_cat_axis 

2095 

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

2097 if x_axis.get("position"): 

2098 position = x_axis["position"] 

2099 

2100 self._xml_start_tag("c:catAx") 

2101 

2102 self._write_axis_id(axis_ids[0]) 

2103 

2104 # Write the c:scaling element. 

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

2106 

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

2108 self._write_delete(1) 

2109 

2110 # Write the c:axPos element. 

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

2112 

2113 # Write the c:majorGridlines element. 

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

2115 

2116 # Write the c:minorGridlines element. 

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

2118 

2119 # Write the axis title elements. 

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

2121 

2122 # Write the c:numFmt element. 

2123 self._write_cat_number_format(x_axis) 

2124 

2125 # Write the c:majorTickMark element. 

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

2127 

2128 # Write the c:minorTickMark element. 

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

2130 

2131 # Write the c:tickLblPos element. 

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

2133 

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

2135 self._write_sp_pr(x_axis) 

2136 

2137 # Write the axis font elements. 

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

2139 

2140 # Write the c:crossAx element. 

2141 self._write_cross_axis(axis_ids[1]) 

2142 

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

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

2145 if ( 

2146 y_axis.get("crossing") is None 

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

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

2149 ): 

2150 # Write the c:crosses element. 

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

2152 else: 

2153 # Write the c:crossesAt element. 

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

2155 

2156 # Write the c:auto element. 

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

2158 self._write_auto(1) 

2159 

2160 # Write the c:labelAlign element. 

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

2162 

2163 # Write the c:labelOffset element. 

2164 self._write_label_offset(100) 

2165 

2166 # Write the c:tickLblSkip element. 

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

2168 

2169 # Write the c:tickMarkSkip element. 

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

2171 

2172 self._xml_end_tag("c:catAx") 

2173 

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

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

2176 x_axis = args["x_axis"] 

2177 y_axis = args["y_axis"] 

2178 axis_ids = args["axis_ids"] 

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

2180 is_horizontal = self.horiz_val_axis 

2181 

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

2183 if axis_ids is None or not axis_ids: 

2184 return 

2185 

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

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

2188 

2189 self._xml_start_tag("c:valAx") 

2190 

2191 self._write_axis_id(axis_ids[1]) 

2192 

2193 # Write the c:scaling element. 

2194 self._write_scaling( 

2195 y_axis.get("reverse"), 

2196 y_axis.get("min"), 

2197 y_axis.get("max"), 

2198 y_axis.get("log_base"), 

2199 ) 

2200 

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

2202 self._write_delete(1) 

2203 

2204 # Write the c:axPos element. 

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

2206 

2207 # Write the c:majorGridlines element. 

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

2209 

2210 # Write the c:minorGridlines element. 

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

2212 

2213 # Write the axis title elements. 

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

2215 

2216 # Write the c:numberFormat element. 

2217 self._write_number_format(y_axis) 

2218 

2219 # Write the c:majorTickMark element. 

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

2221 

2222 # Write the c:minorTickMark element. 

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

2224 

2225 # Write the c:tickLblPos element. 

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

2227 

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

2229 self._write_sp_pr(y_axis) 

2230 

2231 # Write the axis font elements. 

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

2233 

2234 # Write the c:crossAx element. 

2235 self._write_cross_axis(axis_ids[0]) 

2236 

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

2238 if ( 

2239 x_axis.get("crossing") is None 

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

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

2242 ): 

2243 # Write the c:crosses element. 

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

2245 else: 

2246 # Write the c:crossesAt element. 

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

2248 

2249 # Write the c:crossBetween element. 

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

2251 

2252 # Write the c:majorUnit element. 

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

2254 

2255 # Write the c:minorUnit element. 

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

2257 

2258 # Write the c:dispUnits element. 

2259 self._write_disp_units( 

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

2261 ) 

2262 

2263 self._xml_end_tag("c:valAx") 

2264 

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

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

2267 # in scatter plots. Usually the X axis. 

2268 x_axis = args["x_axis"] 

2269 y_axis = args["y_axis"] 

2270 axis_ids = args["axis_ids"] 

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

2272 is_horizontal = self.horiz_val_axis 

2273 

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

2275 if axis_ids is None or not axis_ids: 

2276 return 

2277 

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

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

2280 

2281 self._xml_start_tag("c:valAx") 

2282 

2283 self._write_axis_id(axis_ids[0]) 

2284 

2285 # Write the c:scaling element. 

2286 self._write_scaling( 

2287 x_axis.get("reverse"), 

2288 x_axis.get("min"), 

2289 x_axis.get("max"), 

2290 x_axis.get("log_base"), 

2291 ) 

2292 

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

2294 self._write_delete(1) 

2295 

2296 # Write the c:axPos element. 

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

2298 

2299 # Write the c:majorGridlines element. 

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

2301 

2302 # Write the c:minorGridlines element. 

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

2304 

2305 # Write the axis title elements. 

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

2307 

2308 # Write the c:numberFormat element. 

2309 self._write_number_format(x_axis) 

2310 

2311 # Write the c:majorTickMark element. 

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

2313 

2314 # Write the c:minorTickMark element. 

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

2316 

2317 # Write the c:tickLblPos element. 

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

2319 

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

2321 self._write_sp_pr(x_axis) 

2322 

2323 # Write the axis font elements. 

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

2325 

2326 # Write the c:crossAx element. 

2327 self._write_cross_axis(axis_ids[1]) 

2328 

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

2330 if ( 

2331 y_axis.get("crossing") is None 

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

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

2334 ): 

2335 # Write the c:crosses element. 

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

2337 else: 

2338 # Write the c:crossesAt element. 

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

2340 

2341 # Write the c:crossBetween element. 

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

2343 

2344 # Write the c:majorUnit element. 

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

2346 

2347 # Write the c:minorUnit element. 

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

2349 

2350 # Write the c:dispUnits element. 

2351 self._write_disp_units( 

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

2353 ) 

2354 

2355 self._xml_end_tag("c:valAx") 

2356 

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

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

2359 x_axis = args["x_axis"] 

2360 y_axis = args["y_axis"] 

2361 axis_ids = args["axis_ids"] 

2362 

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

2364 if axis_ids is None or not axis_ids: 

2365 return 

2366 

2367 position = self.cat_axis_position 

2368 

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

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

2371 

2372 self._xml_start_tag("c:dateAx") 

2373 

2374 self._write_axis_id(axis_ids[0]) 

2375 

2376 # Write the c:scaling element. 

2377 self._write_scaling( 

2378 x_axis.get("reverse"), 

2379 x_axis.get("min"), 

2380 x_axis.get("max"), 

2381 x_axis.get("log_base"), 

2382 ) 

2383 

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

2385 self._write_delete(1) 

2386 

2387 # Write the c:axPos element. 

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

2389 

2390 # Write the c:majorGridlines element. 

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

2392 

2393 # Write the c:minorGridlines element. 

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

2395 

2396 # Write the axis title elements. 

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

2398 

2399 # Write the c:numFmt element. 

2400 self._write_number_format(x_axis) 

2401 

2402 # Write the c:majorTickMark element. 

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

2404 

2405 # Write the c:minorTickMark element. 

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

2407 

2408 # Write the c:tickLblPos element. 

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

2410 

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

2412 self._write_sp_pr(x_axis) 

2413 

2414 # Write the axis font elements. 

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

2416 

2417 # Write the c:crossAx element. 

2418 self._write_cross_axis(axis_ids[1]) 

2419 

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

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

2422 if ( 

2423 y_axis.get("crossing") is None 

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

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

2426 ): 

2427 # Write the c:crosses element. 

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

2429 else: 

2430 # Write the c:crossesAt element. 

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

2432 

2433 # Write the c:auto element. 

2434 self._write_auto(1) 

2435 

2436 # Write the c:labelOffset element. 

2437 self._write_label_offset(100) 

2438 

2439 # Write the c:tickLblSkip element. 

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

2441 

2442 # Write the c:tickMarkSkip element. 

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

2444 

2445 # Write the c:majorUnit element. 

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

2447 

2448 # Write the c:majorTimeUnit element. 

2449 if x_axis.get("major_unit"): 

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

2451 

2452 # Write the c:minorUnit element. 

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

2454 

2455 # Write the c:minorTimeUnit element. 

2456 if x_axis.get("minor_unit"): 

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

2458 

2459 self._xml_end_tag("c:dateAx") 

2460 

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

2462 # Write the <c:scaling> element. 

2463 

2464 self._xml_start_tag("c:scaling") 

2465 

2466 # Write the c:logBase element. 

2467 self._write_c_log_base(log_base) 

2468 

2469 # Write the c:orientation element. 

2470 self._write_orientation(reverse) 

2471 

2472 # Write the c:max element. 

2473 self._write_c_max(max_val) 

2474 

2475 # Write the c:min element. 

2476 self._write_c_min(min_val) 

2477 

2478 self._xml_end_tag("c:scaling") 

2479 

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

2481 # Write the <c:logBase> element. 

2482 

2483 if not val: 

2484 return 

2485 

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

2487 

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

2489 

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

2491 # Write the <c:orientation> element. 

2492 val = "minMax" 

2493 

2494 if reverse: 

2495 val = "maxMin" 

2496 

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

2498 

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

2500 

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

2502 # Write the <c:max> element. 

2503 

2504 if max_val is None: 

2505 return 

2506 

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

2508 

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

2510 

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

2512 # Write the <c:min> element. 

2513 

2514 if min_val is None: 

2515 return 

2516 

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

2518 

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

2520 

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

2522 # Write the <c:axPos> element. 

2523 

2524 if reverse: 

2525 if val == "l": 

2526 val = "r" 

2527 if val == "b": 

2528 val = "t" 

2529 

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

2531 

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

2533 

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

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

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

2537 # the sourceLinked attribute is 0. 

2538 # The user can override this if required. 

2539 format_code = axis.get("num_format") 

2540 source_linked = 1 

2541 

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

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

2544 source_linked = 0 

2545 

2546 # User override of sourceLinked. 

2547 if axis.get("num_format_linked"): 

2548 source_linked = 1 

2549 

2550 attributes = [ 

2551 ("formatCode", format_code), 

2552 ("sourceLinked", source_linked), 

2553 ] 

2554 

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

2556 

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

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

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

2560 format_code = axis.get("num_format") 

2561 source_linked = 1 

2562 default_format = 1 

2563 

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

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

2566 source_linked = 0 

2567 default_format = 0 

2568 

2569 # User override of sourceLinked. 

2570 if axis.get("num_format_linked"): 

2571 source_linked = 1 

2572 

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

2574 if not self.cat_has_num_fmt and default_format: 

2575 return 

2576 

2577 attributes = [ 

2578 ("formatCode", format_code), 

2579 ("sourceLinked", source_linked), 

2580 ] 

2581 

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

2583 

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

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

2586 source_linked = 0 

2587 

2588 attributes = [ 

2589 ("formatCode", format_code), 

2590 ("sourceLinked", source_linked), 

2591 ] 

2592 

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

2594 

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

2596 # Write the <c:majorTickMark> element. 

2597 

2598 if not val: 

2599 return 

2600 

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

2602 

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

2604 

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

2606 # Write the <c:minorTickMark> element. 

2607 

2608 if not val: 

2609 return 

2610 

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

2612 

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

2614 

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

2616 # Write the <c:tickLblPos> element. 

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

2618 val = "nextTo" 

2619 

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

2621 

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

2623 

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

2625 # Write the <c:crossAx> element. 

2626 

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

2628 

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

2630 

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

2632 # Write the <c:crosses> element. 

2633 if val is None: 

2634 val = "autoZero" 

2635 

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

2637 

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

2639 

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

2641 # Write the <c:crossesAt> element. 

2642 

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

2644 

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

2646 

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

2648 # Write the <c:auto> element. 

2649 

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

2651 

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

2653 

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

2655 # Write the <c:labelAlign> element. 

2656 

2657 if val is None: 

2658 val = "ctr" 

2659 

2660 if val == "right": 

2661 val = "r" 

2662 

2663 if val == "left": 

2664 val = "l" 

2665 

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

2667 

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

2669 

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

2671 # Write the <c:labelOffset> element. 

2672 

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

2674 

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

2676 

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

2678 # Write the <c:tickLblSkip> element. 

2679 if val is None: 

2680 return 

2681 

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

2683 

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

2685 

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

2687 # Write the <c:tickMarkSkip> element. 

2688 if val is None: 

2689 return 

2690 

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

2692 

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

2694 

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

2696 # Write the <c:majorGridlines> element. 

2697 

2698 if not gridlines: 

2699 return 

2700 

2701 if not gridlines["visible"]: 

2702 return 

2703 

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

2705 self._xml_start_tag("c:majorGridlines") 

2706 

2707 # Write the c:spPr element. 

2708 self._write_sp_pr(gridlines) 

2709 

2710 self._xml_end_tag("c:majorGridlines") 

2711 else: 

2712 self._xml_empty_tag("c:majorGridlines") 

2713 

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

2715 # Write the <c:minorGridlines> element. 

2716 

2717 if not gridlines: 

2718 return 

2719 

2720 if not gridlines["visible"]: 

2721 return 

2722 

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

2724 self._xml_start_tag("c:minorGridlines") 

2725 

2726 # Write the c:spPr element. 

2727 self._write_sp_pr(gridlines) 

2728 

2729 self._xml_end_tag("c:minorGridlines") 

2730 else: 

2731 self._xml_empty_tag("c:minorGridlines") 

2732 

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

2734 # Write the <c:crossBetween> element. 

2735 if val is None: 

2736 val = self.cross_between 

2737 

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

2739 

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

2741 

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

2743 # Write the <c:majorUnit> element. 

2744 

2745 if not val: 

2746 return 

2747 

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

2749 

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

2751 

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

2753 # Write the <c:minorUnit> element. 

2754 

2755 if not val: 

2756 return 

2757 

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

2759 

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

2761 

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

2763 # Write the <c:majorTimeUnit> element. 

2764 if val is None: 

2765 val = "days" 

2766 

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

2768 

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

2770 

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

2772 # Write the <c:minorTimeUnit> element. 

2773 if val is None: 

2774 val = "days" 

2775 

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

2777 

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

2779 

2780 def _write_legend(self) -> None: 

2781 # Write the <c:legend> element. 

2782 legend = self.legend 

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

2784 font = legend.get("font") 

2785 delete_series = [] 

2786 overlay = 0 

2787 

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

2789 delete_series = legend["delete_series"] 

2790 

2791 if position.startswith("overlay_"): 

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

2793 overlay = 1 

2794 

2795 allowed = { 

2796 "right": "r", 

2797 "left": "l", 

2798 "top": "t", 

2799 "bottom": "b", 

2800 "top_right": "tr", 

2801 } 

2802 

2803 if position == "none": 

2804 return 

2805 

2806 if position not in allowed: 

2807 return 

2808 

2809 position = allowed[position] 

2810 

2811 self._xml_start_tag("c:legend") 

2812 

2813 # Write the c:legendPos element. 

2814 self._write_legend_pos(position) 

2815 

2816 # Remove series labels from the legend. 

2817 for index in delete_series: 

2818 # Write the c:legendEntry element. 

2819 self._write_legend_entry(index) 

2820 

2821 # Write the c:layout element. 

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

2823 

2824 # Write the c:overlay element. 

2825 if overlay: 

2826 self._write_overlay() 

2827 

2828 if font: 

2829 self._write_tx_pr(font) 

2830 

2831 # Write the c:spPr element. 

2832 self._write_sp_pr(legend) 

2833 

2834 self._xml_end_tag("c:legend") 

2835 

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

2837 # Write the <c:legendPos> element. 

2838 

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

2840 

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

2842 

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

2844 # Write the <c:legendEntry> element. 

2845 

2846 self._xml_start_tag("c:legendEntry") 

2847 

2848 # Write the c:idx element. 

2849 self._write_idx(index) 

2850 

2851 # Write the c:delete element. 

2852 self._write_delete(1) 

2853 

2854 self._xml_end_tag("c:legendEntry") 

2855 

2856 def _write_overlay(self) -> None: 

2857 # Write the <c:overlay> element. 

2858 val = 1 

2859 

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

2861 

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

2863 

2864 def _write_plot_vis_only(self) -> None: 

2865 # Write the <c:plotVisOnly> element. 

2866 val = 1 

2867 

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

2869 if self.show_hidden: 

2870 return 

2871 

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

2873 

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

2875 

2876 def _write_print_settings(self) -> None: 

2877 # Write the <c:printSettings> element. 

2878 self._xml_start_tag("c:printSettings") 

2879 

2880 # Write the c:headerFooter element. 

2881 self._write_header_footer() 

2882 

2883 # Write the c:pageMargins element. 

2884 self._write_page_margins() 

2885 

2886 # Write the c:pageSetup element. 

2887 self._write_page_setup() 

2888 

2889 self._xml_end_tag("c:printSettings") 

2890 

2891 def _write_header_footer(self) -> None: 

2892 # Write the <c:headerFooter> element. 

2893 self._xml_empty_tag("c:headerFooter") 

2894 

2895 def _write_page_margins(self) -> None: 

2896 # Write the <c:pageMargins> element. 

2897 bottom = 0.75 

2898 left = 0.7 

2899 right = 0.7 

2900 top = 0.75 

2901 header = 0.3 

2902 footer = 0.3 

2903 

2904 attributes = [ 

2905 ("b", bottom), 

2906 ("l", left), 

2907 ("r", right), 

2908 ("t", top), 

2909 ("header", header), 

2910 ("footer", footer), 

2911 ] 

2912 

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

2914 

2915 def _write_page_setup(self) -> None: 

2916 # Write the <c:pageSetup> element. 

2917 self._xml_empty_tag("c:pageSetup") 

2918 

2919 def _write_c_auto_title_deleted(self) -> None: 

2920 # Write the <c:autoTitleDeleted> element. 

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

2922 

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

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

2925 if title.has_name(): 

2926 self._write_title_rich(title, is_horizontal) 

2927 elif title.has_formula(): 

2928 self._write_title_formula(title, is_horizontal) 

2929 elif title.has_formatting(): 

2930 self._write_title_format_only(title) 

2931 

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

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

2934 self._xml_start_tag("c:title") 

2935 

2936 # Write the c:tx element. 

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

2938 

2939 # Write the c:layout element. 

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

2941 

2942 # Write the c:overlay element. 

2943 if title.overlay: 

2944 self._write_overlay() 

2945 

2946 # Write the c:spPr element. 

2947 self._write_sp_pr(title.get_formatting()) 

2948 

2949 self._xml_end_tag("c:title") 

2950 

2951 def _write_title_formula( 

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

2953 ) -> None: 

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

2955 self._xml_start_tag("c:title") 

2956 

2957 # Write the c:tx element. 

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

2959 

2960 # Write the c:layout element. 

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

2962 

2963 # Write the c:overlay element. 

2964 if title.overlay: 

2965 self._write_overlay() 

2966 

2967 # Write the c:spPr element. 

2968 self._write_sp_pr(title.get_formatting()) 

2969 

2970 # Write the c:txPr element. 

2971 self._write_tx_pr(title.font, is_horizontal) 

2972 

2973 self._xml_end_tag("c:title") 

2974 

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

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

2977 self._xml_start_tag("c:title") 

2978 

2979 # Write the c:layout element. 

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

2981 

2982 # Write the c:overlay element. 

2983 if title.overlay: 

2984 self._write_overlay() 

2985 

2986 # Write the c:spPr element. 

2987 self._write_sp_pr(title.get_formatting()) 

2988 

2989 self._xml_end_tag("c:title") 

2990 

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

2992 # Write the <c:tx> element. 

2993 

2994 self._xml_start_tag("c:tx") 

2995 

2996 # Write the c:rich element. 

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

2998 

2999 self._xml_end_tag("c:tx") 

3000 

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

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

3003 

3004 self._xml_start_tag("c:tx") 

3005 

3006 # Write the c:v element. 

3007 self._write_v(title) 

3008 

3009 self._xml_end_tag("c:tx") 

3010 

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

3012 # Write the <c:tx> element. 

3013 data = None 

3014 

3015 if data_id is not None: 

3016 data = self.formula_data[data_id] 

3017 

3018 self._xml_start_tag("c:tx") 

3019 

3020 # Write the c:strRef element. 

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

3022 

3023 self._xml_end_tag("c:tx") 

3024 

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

3026 # Write the <c:rich> element. 

3027 

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

3029 rotation = font["rotation"] 

3030 else: 

3031 rotation = None 

3032 

3033 self._xml_start_tag("c:rich") 

3034 

3035 # Write the a:bodyPr element. 

3036 self._write_a_body_pr(rotation, is_horizontal) 

3037 

3038 # Write the a:lstStyle element. 

3039 self._write_a_lst_style() 

3040 

3041 # Write the a:p element. 

3042 self._write_a_p_rich(title, font, ignore_rich_pr) 

3043 

3044 self._xml_end_tag("c:rich") 

3045 

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

3047 # Write the <a:bodyPr> element. 

3048 attributes = [] 

3049 

3050 if rotation is None and is_horizontal: 

3051 rotation = -5400000 

3052 

3053 if rotation is not None: 

3054 if rotation == 16200000: 

3055 # 270 deg/stacked angle. 

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

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

3058 elif rotation == 16260000: 

3059 # 271 deg/East Asian vertical. 

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

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

3062 else: 

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

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

3065 

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

3067 

3068 def _write_a_lst_style(self) -> None: 

3069 # Write the <a:lstStyle> element. 

3070 self._xml_empty_tag("a:lstStyle") 

3071 

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

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

3074 

3075 self._xml_start_tag("a:p") 

3076 

3077 # Write the a:pPr element. 

3078 if not ignore_rich_pr: 

3079 self._write_a_p_pr_rich(font) 

3080 

3081 # Write the a:r element. 

3082 self._write_a_r(title, font) 

3083 

3084 self._xml_end_tag("a:p") 

3085 

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

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

3088 

3089 self._xml_start_tag("a:p") 

3090 

3091 # Write the a:pPr element. 

3092 self._write_a_p_pr_rich(font) 

3093 

3094 # Write the a:endParaRPr element. 

3095 self._write_a_end_para_rpr() 

3096 

3097 self._xml_end_tag("a:p") 

3098 

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

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

3101 

3102 self._xml_start_tag("a:pPr") 

3103 

3104 # Write the a:defRPr element. 

3105 self._write_a_def_rpr(font) 

3106 

3107 self._xml_end_tag("a:pPr") 

3108 

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

3110 # Write the <a:defRPr> element. 

3111 has_color = False 

3112 

3113 style_attributes = Shape._get_font_style_attributes(font) 

3114 latin_attributes = Shape._get_font_latin_attributes(font) 

3115 

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

3117 has_color = True 

3118 

3119 if latin_attributes or has_color: 

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

3121 

3122 if has_color: 

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

3124 

3125 if latin_attributes: 

3126 self._write_a_latin(latin_attributes) 

3127 

3128 self._xml_end_tag("a:defRPr") 

3129 else: 

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

3131 

3132 def _write_a_end_para_rpr(self) -> None: 

3133 # Write the <a:endParaRPr> element. 

3134 lang = "en-US" 

3135 

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

3137 

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

3139 

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

3141 # Write the <a:r> element. 

3142 

3143 self._xml_start_tag("a:r") 

3144 

3145 # Write the a:rPr element. 

3146 self._write_a_r_pr(font) 

3147 

3148 # Write the a:t element. 

3149 self._write_a_t(title) 

3150 

3151 self._xml_end_tag("a:r") 

3152 

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

3154 # Write the <a:rPr> element. 

3155 has_color = False 

3156 lang = "en-US" 

3157 

3158 style_attributes = Shape._get_font_style_attributes(font) 

3159 latin_attributes = Shape._get_font_latin_attributes(font) 

3160 

3161 if font and font["color"]: 

3162 has_color = True 

3163 

3164 # Add the lang type to the attributes. 

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

3166 

3167 if latin_attributes or has_color: 

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

3169 

3170 if has_color: 

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

3172 

3173 if latin_attributes: 

3174 self._write_a_latin(latin_attributes) 

3175 

3176 self._xml_end_tag("a:rPr") 

3177 else: 

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

3179 

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

3181 # Write the <a:t> element. 

3182 

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

3184 

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

3186 # Write the <c:txPr> element. 

3187 

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

3189 rotation = font["rotation"] 

3190 else: 

3191 rotation = None 

3192 

3193 self._xml_start_tag("c:txPr") 

3194 

3195 # Write the a:bodyPr element. 

3196 self._write_a_body_pr(rotation, is_horizontal) 

3197 

3198 # Write the a:lstStyle element. 

3199 self._write_a_lst_style() 

3200 

3201 # Write the a:p element. 

3202 self._write_a_p_formula(font) 

3203 

3204 self._xml_end_tag("c:txPr") 

3205 

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

3207 # Write the <c:marker> element. 

3208 if marker is None: 

3209 marker = self.default_marker 

3210 

3211 if not marker: 

3212 return 

3213 

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

3215 return 

3216 

3217 self._xml_start_tag("c:marker") 

3218 

3219 # Write the c:symbol element. 

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

3221 

3222 # Write the c:size element. 

3223 if marker.get("size"): 

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

3225 

3226 # Write the c:spPr element. 

3227 self._write_sp_pr(marker) 

3228 

3229 self._xml_end_tag("c:marker") 

3230 

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

3232 # Write the <c:size> element. 

3233 

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

3235 

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

3237 

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

3239 # Write the <c:symbol> element. 

3240 

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

3242 

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

3244 

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

3246 # Write the <c:spPr> element. 

3247 if not self._has_formatting(chart_format): 

3248 return 

3249 

3250 self._xml_start_tag("c:spPr") 

3251 

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

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

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

3255 # Write the a:noFill element. 

3256 self._write_a_no_fill() 

3257 else: 

3258 # Write the a:solidFill element. 

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

3260 

3261 if chart_format.get("pattern"): 

3262 # Write the a:gradFill element. 

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

3264 

3265 if chart_format.get("gradient"): 

3266 # Write the a:gradFill element. 

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

3268 

3269 # Write the a:ln element. 

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

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

3272 

3273 self._xml_end_tag("c:spPr") 

3274 

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

3276 # Write the <a:ln> element. 

3277 attributes = [] 

3278 

3279 # Add the line width as an attribute. 

3280 width = line.get("width") 

3281 

3282 if width is not None: 

3283 # Round width to nearest 0.25, like Excel. 

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

3285 

3286 # Convert to internal units. 

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

3288 

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

3290 

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

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

3293 

3294 # Write the line fill. 

3295 if "none" in line: 

3296 # Write the a:noFill element. 

3297 self._write_a_no_fill() 

3298 elif "color" in line: 

3299 # Write the a:solidFill element. 

3300 self._write_a_solid_fill(line) 

3301 

3302 # Write the line/dash type. 

3303 line_type = line.get("dash_type") 

3304 if line_type: 

3305 # Write the a:prstDash element. 

3306 self._write_a_prst_dash(line_type) 

3307 

3308 self._xml_end_tag("a:ln") 

3309 else: 

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

3311 

3312 def _write_a_no_fill(self) -> None: 

3313 # Write the <a:noFill> element. 

3314 self._xml_empty_tag("a:noFill") 

3315 

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

3317 # Write the <a:solidFill> element. 

3318 

3319 self._xml_start_tag("a:solidFill") 

3320 

3321 if fill.get("color"): 

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

3323 

3324 self._xml_end_tag("a:solidFill") 

3325 

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

3327 # Write the appropriate chart color element. 

3328 

3329 if not color: 

3330 return 

3331 

3332 if color._is_automatic: 

3333 # Write the a:sysClr element. 

3334 self._write_a_sys_clr() 

3335 elif color._type == ColorTypes.RGB: 

3336 # Write the a:srgbClr element. 

3337 self._write_a_srgb_clr(color, transparency) 

3338 elif color._type == ColorTypes.THEME: 

3339 self._write_a_scheme_clr(color, transparency) 

3340 

3341 def _write_a_sys_clr(self) -> None: 

3342 # Write the <a:sysClr> element. 

3343 

3344 val = "window" 

3345 last_clr = "FFFFFF" 

3346 

3347 attributes = [ 

3348 ("val", val), 

3349 ("lastClr", last_clr), 

3350 ] 

3351 

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

3353 

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

3355 # Write the <a:srgbClr> element. 

3356 

3357 if not color: 

3358 return 

3359 

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

3361 

3362 if transparency: 

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

3364 

3365 # Write the a:alpha element. 

3366 self._write_a_alpha(transparency) 

3367 

3368 self._xml_end_tag("a:srgbClr") 

3369 else: 

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

3371 

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

3373 # Write the <a:schemeClr> element. 

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

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

3376 

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

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

3379 

3380 if lum_mod > 0: 

3381 # Write the a:lumMod element. 

3382 self._write_a_lum_mod(lum_mod) 

3383 

3384 if lum_off > 0: 

3385 # Write the a:lumOff element. 

3386 self._write_a_lum_off(lum_off) 

3387 

3388 if transparency: 

3389 # Write the a:alpha element. 

3390 self._write_a_alpha(transparency) 

3391 

3392 self._xml_end_tag("a:schemeClr") 

3393 else: 

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

3395 

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

3397 # Write the <a:lumMod> element. 

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

3399 

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

3401 

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

3403 # Write the <a:lumOff> element. 

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

3405 

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

3407 

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

3409 # Write the <a:alpha> element. 

3410 

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

3412 

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

3414 

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

3416 

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

3418 # Write the <a:prstDash> element. 

3419 

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

3421 

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

3423 

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

3425 # Write the <c:trendline> element. 

3426 

3427 if not trendline: 

3428 return 

3429 

3430 self._xml_start_tag("c:trendline") 

3431 

3432 # Write the c:name element. 

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

3434 

3435 # Write the c:spPr element. 

3436 self._write_sp_pr(trendline) 

3437 

3438 # Write the c:trendlineType element. 

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

3440 

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

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

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

3444 

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

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

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

3448 

3449 # Write the c:forward element. 

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

3451 

3452 # Write the c:backward element. 

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

3454 

3455 if "intercept" in trendline: 

3456 # Write the c:intercept element. 

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

3458 

3459 if trendline.get("display_r_squared"): 

3460 # Write the c:dispRSqr element. 

3461 self._write_c_disp_rsqr() 

3462 

3463 if trendline.get("display_equation"): 

3464 # Write the c:dispEq element. 

3465 self._write_c_disp_eq() 

3466 

3467 # Write the c:trendlineLbl element. 

3468 self._write_c_trendline_lbl(trendline) 

3469 

3470 self._xml_end_tag("c:trendline") 

3471 

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

3473 # Write the <c:trendlineType> element. 

3474 

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

3476 

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

3478 

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

3480 # Write the <c:name> element. 

3481 

3482 if data is None: 

3483 return 

3484 

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

3486 

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

3488 # Write the <c:order> element. 

3489 val = max(val, 2) 

3490 

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

3492 

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

3494 

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

3496 # Write the <c:period> element. 

3497 val = max(val, 2) 

3498 

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

3500 

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

3502 

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

3504 # Write the <c:forward> element. 

3505 

3506 if not val: 

3507 return 

3508 

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

3510 

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

3512 

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

3514 # Write the <c:backward> element. 

3515 

3516 if not val: 

3517 return 

3518 

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

3520 

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

3522 

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

3524 # Write the <c:intercept> element. 

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

3526 

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

3528 

3529 def _write_c_disp_eq(self) -> None: 

3530 # Write the <c:dispEq> element. 

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

3532 

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

3534 

3535 def _write_c_disp_rsqr(self) -> None: 

3536 # Write the <c:dispRSqr> element. 

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

3538 

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

3540 

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

3542 # Write the <c:trendlineLbl> element. 

3543 self._xml_start_tag("c:trendlineLbl") 

3544 

3545 # Write the c:layout element. 

3546 self._write_layout(None, None) 

3547 

3548 # Write the c:numFmt element. 

3549 self._write_trendline_num_fmt() 

3550 

3551 # Write the c:spPr element. 

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

3553 

3554 # Write the data label font elements. 

3555 if trendline["label"]: 

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

3557 if font: 

3558 self._write_axis_font(font) 

3559 

3560 self._xml_end_tag("c:trendlineLbl") 

3561 

3562 def _write_trendline_num_fmt(self) -> None: 

3563 # Write the <c:numFmt> element. 

3564 attributes = [ 

3565 ("formatCode", "General"), 

3566 ("sourceLinked", 0), 

3567 ] 

3568 

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

3570 

3571 def _write_hi_low_lines(self) -> None: 

3572 # Write the <c:hiLowLines> element. 

3573 hi_low_lines = self.hi_low_lines 

3574 

3575 if hi_low_lines is None: 

3576 return 

3577 

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

3579 self._xml_start_tag("c:hiLowLines") 

3580 

3581 # Write the c:spPr element. 

3582 self._write_sp_pr(hi_low_lines) 

3583 

3584 self._xml_end_tag("c:hiLowLines") 

3585 else: 

3586 self._xml_empty_tag("c:hiLowLines") 

3587 

3588 def _write_drop_lines(self) -> None: 

3589 # Write the <c:dropLines> element. 

3590 drop_lines = self.drop_lines 

3591 

3592 if drop_lines is None: 

3593 return 

3594 

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

3596 self._xml_start_tag("c:dropLines") 

3597 

3598 # Write the c:spPr element. 

3599 self._write_sp_pr(drop_lines) 

3600 

3601 self._xml_end_tag("c:dropLines") 

3602 else: 

3603 self._xml_empty_tag("c:dropLines") 

3604 

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

3606 # Write the <c:overlap> element. 

3607 

3608 if val is None: 

3609 return 

3610 

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

3612 

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

3614 

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

3616 # Write the <c:numCache> element. 

3617 if data: 

3618 count = len(data) 

3619 else: 

3620 count = 0 

3621 

3622 self._xml_start_tag("c:numCache") 

3623 

3624 # Write the c:formatCode element. 

3625 self._write_format_code("General") 

3626 

3627 # Write the c:ptCount element. 

3628 self._write_pt_count(count) 

3629 

3630 for i in range(count): 

3631 token = data[i] 

3632 

3633 if token is None: 

3634 continue 

3635 

3636 try: 

3637 float(token) 

3638 except ValueError: 

3639 # Write non-numeric data as 0. 

3640 token = 0 

3641 

3642 # Write the c:pt element. 

3643 self._write_pt(i, token) 

3644 

3645 self._xml_end_tag("c:numCache") 

3646 

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

3648 # Write the <c:strCache> element. 

3649 count = len(data) 

3650 

3651 self._xml_start_tag("c:strCache") 

3652 

3653 # Write the c:ptCount element. 

3654 self._write_pt_count(count) 

3655 

3656 for i in range(count): 

3657 # Write the c:pt element. 

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

3659 

3660 self._xml_end_tag("c:strCache") 

3661 

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

3663 # Write the <c:formatCode> element. 

3664 

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

3666 

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

3668 # Write the <c:ptCount> element. 

3669 

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

3671 

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

3673 

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

3675 # Write the <c:pt> element. 

3676 

3677 if value is None: 

3678 return 

3679 

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

3681 

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

3683 

3684 # Write the c:v element. 

3685 self._write_v(value) 

3686 

3687 self._xml_end_tag("c:pt") 

3688 

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

3690 # Write the <c:v> element. 

3691 

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

3693 

3694 def _write_protection(self) -> None: 

3695 # Write the <c:protection> element. 

3696 if not self.protection: 

3697 return 

3698 

3699 self._xml_empty_tag("c:protection") 

3700 

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

3702 # Write the <c:dPt> elements. 

3703 index = -1 

3704 

3705 if not points: 

3706 return 

3707 

3708 for point in points: 

3709 index += 1 

3710 if not point: 

3711 continue 

3712 

3713 self._write_d_pt_point(index, point) 

3714 

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

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

3717 

3718 self._xml_start_tag("c:dPt") 

3719 

3720 # Write the c:idx element. 

3721 self._write_idx(index) 

3722 

3723 # Write the c:spPr element. 

3724 self._write_sp_pr(point) 

3725 

3726 self._xml_end_tag("c:dPt") 

3727 

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

3729 # Write the <c:dLbls> element. 

3730 

3731 if not labels: 

3732 return 

3733 

3734 self._xml_start_tag("c:dLbls") 

3735 

3736 # Write the custom c:dLbl elements. 

3737 if labels.get("custom"): 

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

3739 

3740 # Write the c:numFmt element. 

3741 if labels.get("num_format"): 

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

3743 

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

3745 self._write_sp_pr(labels) 

3746 

3747 # Write the data label font elements. 

3748 if labels.get("font"): 

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

3750 

3751 # Write the c:dLblPos element. 

3752 if labels.get("position"): 

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

3754 

3755 # Write the c:showLegendKey element. 

3756 if labels.get("legend_key"): 

3757 self._write_show_legend_key() 

3758 

3759 # Write the c:showVal element. 

3760 if labels.get("value"): 

3761 self._write_show_val() 

3762 

3763 # Write the c:showCatName element. 

3764 if labels.get("category"): 

3765 self._write_show_cat_name() 

3766 

3767 # Write the c:showSerName element. 

3768 if labels.get("series_name"): 

3769 self._write_show_ser_name() 

3770 

3771 # Write the c:showPercent element. 

3772 if labels.get("percentage"): 

3773 self._write_show_percent() 

3774 

3775 # Write the c:separator element. 

3776 if labels.get("separator"): 

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

3778 

3779 # Write the c:showLeaderLines element. 

3780 if labels.get("leader_lines"): 

3781 self._write_show_leader_lines() 

3782 

3783 self._xml_end_tag("c:dLbls") 

3784 

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

3786 # Write the <c:showLegendKey> element. 

3787 index = 0 

3788 

3789 for label in labels: 

3790 index += 1 

3791 

3792 if label is None: 

3793 continue 

3794 

3795 use_custom_formatting = True 

3796 

3797 self._xml_start_tag("c:dLbl") 

3798 

3799 # Write the c:idx element. 

3800 self._write_idx(index - 1) 

3801 

3802 delete_label = label.get("delete") 

3803 

3804 if delete_label: 

3805 self._write_delete(1) 

3806 

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

3808 

3809 # Write the c:layout element. 

3810 self._write_layout(None, None) 

3811 

3812 if label.get("formula"): 

3813 self._write_custom_label_formula(label) 

3814 elif label.get("value"): 

3815 self._write_custom_label_str(label) 

3816 # String values use spPr formatting. 

3817 use_custom_formatting = False 

3818 

3819 if use_custom_formatting: 

3820 self._write_custom_label_format(label) 

3821 

3822 if label.get("position"): 

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

3824 elif parent.get("position"): 

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

3826 

3827 if parent.get("value"): 

3828 self._write_show_val() 

3829 

3830 if parent.get("category"): 

3831 self._write_show_cat_name() 

3832 

3833 if parent.get("series_name"): 

3834 self._write_show_ser_name() 

3835 

3836 else: 

3837 self._write_custom_label_format(label) 

3838 

3839 self._xml_end_tag("c:dLbl") 

3840 

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

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

3843 title = label.get("value") 

3844 font = label.get("font") 

3845 has_formatting = self._has_formatting(label) 

3846 

3847 self._xml_start_tag("c:tx") 

3848 

3849 # Write the c:rich element. 

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

3851 

3852 self._xml_end_tag("c:tx") 

3853 

3854 # Write the c:spPr element. 

3855 self._write_sp_pr(label) 

3856 

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

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

3859 formula = label.get("formula") 

3860 data_id = label.get("data_id") 

3861 data = None 

3862 

3863 if data_id is not None: 

3864 data = self.formula_data[data_id] 

3865 

3866 self._xml_start_tag("c:tx") 

3867 

3868 # Write the c:strRef element. 

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

3870 

3871 self._xml_end_tag("c:tx") 

3872 

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

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

3875 font = label.get("font") 

3876 has_formatting = self._has_formatting(label) 

3877 

3878 if has_formatting: 

3879 self._write_sp_pr(label) 

3880 self._write_tx_pr(font) 

3881 elif font: 

3882 self._xml_empty_tag("c:spPr") 

3883 self._write_tx_pr(font) 

3884 

3885 def _write_show_legend_key(self) -> None: 

3886 # Write the <c:showLegendKey> element. 

3887 val = "1" 

3888 

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

3890 

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

3892 

3893 def _write_show_val(self) -> None: 

3894 # Write the <c:showVal> element. 

3895 val = 1 

3896 

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

3898 

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

3900 

3901 def _write_show_cat_name(self) -> None: 

3902 # Write the <c:showCatName> element. 

3903 val = 1 

3904 

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

3906 

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

3908 

3909 def _write_show_ser_name(self) -> None: 

3910 # Write the <c:showSerName> element. 

3911 val = 1 

3912 

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

3914 

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

3916 

3917 def _write_show_percent(self) -> None: 

3918 # Write the <c:showPercent> element. 

3919 val = 1 

3920 

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

3922 

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

3924 

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

3926 # Write the <c:separator> element. 

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

3928 

3929 def _write_show_leader_lines(self) -> None: 

3930 # Write the <c:showLeaderLines> element. 

3931 # 

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

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

3934 # 

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

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

3937 

3938 attributes = [ 

3939 ("uri", uri), 

3940 ("xmlns:c15", xmlns_c_15), 

3941 ] 

3942 

3943 self._xml_start_tag("c:extLst") 

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

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

3946 self._xml_end_tag("c:ext") 

3947 self._xml_end_tag("c:extLst") 

3948 

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

3950 # Write the <c:dLblPos> element. 

3951 

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

3953 

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

3955 

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

3957 # Write the <c:delete> element. 

3958 

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

3960 

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

3962 

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

3964 # Write the <c:invertIfNegative> element. 

3965 val = 1 

3966 

3967 if not invert: 

3968 return 

3969 

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

3971 

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

3973 

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

3975 # Write the axis font elements. 

3976 

3977 if not font: 

3978 return 

3979 

3980 self._xml_start_tag("c:txPr") 

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

3982 self._write_a_lst_style() 

3983 self._xml_start_tag("a:p") 

3984 

3985 self._write_a_p_pr_rich(font) 

3986 

3987 self._write_a_end_para_rpr() 

3988 self._xml_end_tag("a:p") 

3989 self._xml_end_tag("c:txPr") 

3990 

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

3992 # Write the <a:latin> element. 

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

3994 

3995 def _write_d_table(self) -> None: 

3996 # Write the <c:dTable> element. 

3997 table = self.table 

3998 

3999 if not table: 

4000 return 

4001 

4002 self._xml_start_tag("c:dTable") 

4003 

4004 if table["horizontal"]: 

4005 # Write the c:showHorzBorder element. 

4006 self._write_show_horz_border() 

4007 

4008 if table["vertical"]: 

4009 # Write the c:showVertBorder element. 

4010 self._write_show_vert_border() 

4011 

4012 if table["outline"]: 

4013 # Write the c:showOutline element. 

4014 self._write_show_outline() 

4015 

4016 if table["show_keys"]: 

4017 # Write the c:showKeys element. 

4018 self._write_show_keys() 

4019 

4020 if table["font"]: 

4021 # Write the table font. 

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

4023 

4024 self._xml_end_tag("c:dTable") 

4025 

4026 def _write_show_horz_border(self) -> None: 

4027 # Write the <c:showHorzBorder> element. 

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

4029 

4030 self._xml_empty_tag("c:showHorzBorder", attributes) 

4031 

4032 def _write_show_vert_border(self) -> None: 

4033 # Write the <c:showVertBorder> element. 

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

4035 

4036 self._xml_empty_tag("c:showVertBorder", attributes) 

4037 

4038 def _write_show_outline(self) -> None: 

4039 # Write the <c:showOutline> element. 

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

4041 

4042 self._xml_empty_tag("c:showOutline", attributes) 

4043 

4044 def _write_show_keys(self) -> None: 

4045 # Write the <c:showKeys> element. 

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

4047 

4048 self._xml_empty_tag("c:showKeys", attributes) 

4049 

4050 def _write_error_bars(self, error_bars) -> None: 

4051 # Write the X and Y error bars. 

4052 

4053 if not error_bars: 

4054 return 

4055 

4056 if error_bars["x_error_bars"]: 

4057 self._write_err_bars("x", error_bars["x_error_bars"]) 

4058 

4059 if error_bars["y_error_bars"]: 

4060 self._write_err_bars("y", error_bars["y_error_bars"]) 

4061 

4062 def _write_err_bars(self, direction, error_bars) -> None: 

4063 # Write the <c:errBars> element. 

4064 

4065 if not error_bars: 

4066 return 

4067 

4068 self._xml_start_tag("c:errBars") 

4069 

4070 # Write the c:errDir element. 

4071 self._write_err_dir(direction) 

4072 

4073 # Write the c:errBarType element. 

4074 self._write_err_bar_type(error_bars["direction"]) 

4075 

4076 # Write the c:errValType element. 

4077 self._write_err_val_type(error_bars["type"]) 

4078 

4079 if not error_bars["endcap"]: 

4080 # Write the c:noEndCap element. 

4081 self._write_no_end_cap() 

4082 

4083 if error_bars["type"] == "stdErr": 

4084 # Don't need to write a c:errValType tag. 

4085 pass 

4086 elif error_bars["type"] == "cust": 

4087 # Write the custom error tags. 

4088 self._write_custom_error(error_bars) 

4089 else: 

4090 # Write the c:val element. 

4091 self._write_error_val(error_bars["value"]) 

4092 

4093 # Write the c:spPr element. 

4094 self._write_sp_pr(error_bars) 

4095 

4096 self._xml_end_tag("c:errBars") 

4097 

4098 def _write_err_dir(self, val) -> None: 

4099 # Write the <c:errDir> element. 

4100 

4101 attributes = [("val", val)] 

4102 

4103 self._xml_empty_tag("c:errDir", attributes) 

4104 

4105 def _write_err_bar_type(self, val) -> None: 

4106 # Write the <c:errBarType> element. 

4107 

4108 attributes = [("val", val)] 

4109 

4110 self._xml_empty_tag("c:errBarType", attributes) 

4111 

4112 def _write_err_val_type(self, val) -> None: 

4113 # Write the <c:errValType> element. 

4114 

4115 attributes = [("val", val)] 

4116 

4117 self._xml_empty_tag("c:errValType", attributes) 

4118 

4119 def _write_no_end_cap(self) -> None: 

4120 # Write the <c:noEndCap> element. 

4121 attributes = [("val", 1)] 

4122 

4123 self._xml_empty_tag("c:noEndCap", attributes) 

4124 

4125 def _write_error_val(self, val) -> None: 

4126 # Write the <c:val> element for error bars. 

4127 

4128 attributes = [("val", val)] 

4129 

4130 self._xml_empty_tag("c:val", attributes) 

4131 

4132 def _write_custom_error(self, error_bars) -> None: 

4133 # Write the custom error bars tags. 

4134 

4135 if error_bars["plus_values"]: 

4136 # Write the c:plus element. 

4137 self._xml_start_tag("c:plus") 

4138 

4139 if isinstance(error_bars["plus_values"], list): 

4140 self._write_num_lit(error_bars["plus_values"]) 

4141 else: 

4142 self._write_num_ref( 

4143 error_bars["plus_values"], error_bars["plus_data"], "num" 

4144 ) 

4145 self._xml_end_tag("c:plus") 

4146 

4147 if error_bars["minus_values"]: 

4148 # Write the c:minus element. 

4149 self._xml_start_tag("c:minus") 

4150 

4151 if isinstance(error_bars["minus_values"], list): 

4152 self._write_num_lit(error_bars["minus_values"]) 

4153 else: 

4154 self._write_num_ref( 

4155 error_bars["minus_values"], error_bars["minus_data"], "num" 

4156 ) 

4157 self._xml_end_tag("c:minus") 

4158 

4159 def _write_num_lit(self, data) -> None: 

4160 # Write the <c:numLit> element for literal number list elements. 

4161 count = len(data) 

4162 

4163 # Write the c:numLit element. 

4164 self._xml_start_tag("c:numLit") 

4165 

4166 # Write the c:formatCode element. 

4167 self._write_format_code("General") 

4168 

4169 # Write the c:ptCount element. 

4170 self._write_pt_count(count) 

4171 

4172 for i in range(count): 

4173 token = data[i] 

4174 

4175 if token is None: 

4176 continue 

4177 

4178 try: 

4179 float(token) 

4180 except ValueError: 

4181 # Write non-numeric data as 0. 

4182 token = 0 

4183 

4184 # Write the c:pt element. 

4185 self._write_pt(i, token) 

4186 

4187 self._xml_end_tag("c:numLit") 

4188 

4189 def _write_up_down_bars(self) -> None: 

4190 # Write the <c:upDownBars> element. 

4191 up_down_bars = self.up_down_bars 

4192 

4193 if up_down_bars is None: 

4194 return 

4195 

4196 self._xml_start_tag("c:upDownBars") 

4197 

4198 # Write the c:gapWidth element. 

4199 self._write_gap_width(150) 

4200 

4201 # Write the c:upBars element. 

4202 self._write_up_bars(up_down_bars.get("up")) 

4203 

4204 # Write the c:downBars element. 

4205 self._write_down_bars(up_down_bars.get("down")) 

4206 

4207 self._xml_end_tag("c:upDownBars") 

4208 

4209 def _write_gap_width(self, val) -> None: 

4210 # Write the <c:gapWidth> element. 

4211 

4212 if val is None: 

4213 return 

4214 

4215 attributes = [("val", val)] 

4216 

4217 self._xml_empty_tag("c:gapWidth", attributes) 

4218 

4219 def _write_up_bars(self, bar_format) -> None: 

4220 # Write the <c:upBars> element. 

4221 

4222 if bar_format["line"] and bar_format["line"]["defined"]: 

4223 self._xml_start_tag("c:upBars") 

4224 

4225 # Write the c:spPr element. 

4226 self._write_sp_pr(bar_format) 

4227 

4228 self._xml_end_tag("c:upBars") 

4229 else: 

4230 self._xml_empty_tag("c:upBars") 

4231 

4232 def _write_down_bars(self, bar_format) -> None: 

4233 # Write the <c:downBars> element. 

4234 

4235 if bar_format["line"] and bar_format["line"]["defined"]: 

4236 self._xml_start_tag("c:downBars") 

4237 

4238 # Write the c:spPr element. 

4239 self._write_sp_pr(bar_format) 

4240 

4241 self._xml_end_tag("c:downBars") 

4242 else: 

4243 self._xml_empty_tag("c:downBars") 

4244 

4245 def _write_disp_units(self, units, display) -> None: 

4246 # Write the <c:dispUnits> element. 

4247 

4248 if not units: 

4249 return 

4250 

4251 attributes = [("val", units)] 

4252 

4253 self._xml_start_tag("c:dispUnits") 

4254 self._xml_empty_tag("c:builtInUnit", attributes) 

4255 

4256 if display: 

4257 self._xml_start_tag("c:dispUnitsLbl") 

4258 self._xml_empty_tag("c:layout") 

4259 self._xml_end_tag("c:dispUnitsLbl") 

4260 

4261 self._xml_end_tag("c:dispUnits") 

4262 

4263 def _write_a_grad_fill(self, gradient) -> None: 

4264 # Write the <a:gradFill> element. 

4265 

4266 attributes = [("flip", "none"), ("rotWithShape", "1")] 

4267 

4268 if gradient["type"] == "linear": 

4269 attributes = [] 

4270 

4271 self._xml_start_tag("a:gradFill", attributes) 

4272 

4273 # Write the a:gsLst element. 

4274 self._write_a_gs_lst(gradient) 

4275 

4276 if gradient["type"] == "linear": 

4277 # Write the a:lin element. 

4278 self._write_a_lin(gradient["angle"]) 

4279 else: 

4280 # Write the a:path element. 

4281 self._write_a_path(gradient["type"]) 

4282 

4283 # Write the a:tileRect element. 

4284 self._write_a_tile_rect(gradient["type"]) 

4285 

4286 self._xml_end_tag("a:gradFill") 

4287 

4288 def _write_a_gs_lst(self, gradient) -> None: 

4289 # Write the <a:gsLst> element. 

4290 positions = gradient["positions"] 

4291 colors = gradient["colors"] 

4292 

4293 self._xml_start_tag("a:gsLst") 

4294 

4295 for i, color in enumerate(colors): 

4296 pos = int(positions[i] * 1000) 

4297 attributes = [("pos", pos)] 

4298 self._xml_start_tag("a:gs", attributes) 

4299 

4300 self._write_color(color) 

4301 

4302 self._xml_end_tag("a:gs") 

4303 

4304 self._xml_end_tag("a:gsLst") 

4305 

4306 def _write_a_lin(self, angle) -> None: 

4307 # Write the <a:lin> element. 

4308 

4309 angle = int(60000 * angle) 

4310 

4311 attributes = [ 

4312 ("ang", angle), 

4313 ("scaled", "0"), 

4314 ] 

4315 

4316 self._xml_empty_tag("a:lin", attributes) 

4317 

4318 def _write_a_path(self, gradient_type) -> None: 

4319 # Write the <a:path> element. 

4320 

4321 attributes = [("path", gradient_type)] 

4322 

4323 self._xml_start_tag("a:path", attributes) 

4324 

4325 # Write the a:fillToRect element. 

4326 self._write_a_fill_to_rect(gradient_type) 

4327 

4328 self._xml_end_tag("a:path") 

4329 

4330 def _write_a_fill_to_rect(self, gradient_type) -> None: 

4331 # Write the <a:fillToRect> element. 

4332 

4333 if gradient_type == "shape": 

4334 attributes = [ 

4335 ("l", "50000"), 

4336 ("t", "50000"), 

4337 ("r", "50000"), 

4338 ("b", "50000"), 

4339 ] 

4340 else: 

4341 attributes = [ 

4342 ("l", "100000"), 

4343 ("t", "100000"), 

4344 ] 

4345 

4346 self._xml_empty_tag("a:fillToRect", attributes) 

4347 

4348 def _write_a_tile_rect(self, gradient_type) -> None: 

4349 # Write the <a:tileRect> element. 

4350 

4351 if gradient_type == "shape": 

4352 attributes = [] 

4353 else: 

4354 attributes = [ 

4355 ("r", "-100000"), 

4356 ("b", "-100000"), 

4357 ] 

4358 

4359 self._xml_empty_tag("a:tileRect", attributes) 

4360 

4361 def _write_a_patt_fill(self, pattern) -> None: 

4362 # Write the <a:pattFill> element. 

4363 

4364 attributes = [("prst", pattern["pattern"])] 

4365 

4366 self._xml_start_tag("a:pattFill", attributes) 

4367 

4368 # Write the a:fgClr element. 

4369 self._write_a_fg_clr(pattern["fg_color"]) 

4370 

4371 # Write the a:bgClr element. 

4372 self._write_a_bg_clr(pattern["bg_color"]) 

4373 

4374 self._xml_end_tag("a:pattFill") 

4375 

4376 def _write_a_fg_clr(self, color: Color) -> None: 

4377 # Write the <a:fgClr> element. 

4378 self._xml_start_tag("a:fgClr") 

4379 self._write_color(color) 

4380 self._xml_end_tag("a:fgClr") 

4381 

4382 def _write_a_bg_clr(self, color: Color) -> None: 

4383 # Write the <a:bgClr> element. 

4384 self._xml_start_tag("a:bgClr") 

4385 self._write_color(color) 

4386 self._xml_end_tag("a:bgClr")