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

2006 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 warnings import warn 

13 

14from xlsxwriter.color import Color, ColorTypes 

15 

16from . import xmlwriter 

17from .shape import Shape 

18from .utility import ( 

19 _datetime_to_excel_datetime, 

20 _supported_datetime, 

21 quote_sheetname, 

22 xl_range_formula, 

23 xl_rowcol_to_cell, 

24) 

25 

26 

27class Chart(xmlwriter.XMLwriter): 

28 """ 

29 A class for writing the Excel XLSX Chart file. 

30 

31 

32 """ 

33 

34 ########################################################################### 

35 # 

36 # Public API. 

37 # 

38 ########################################################################### 

39 

40 def __init__(self): 

41 """ 

42 Constructor. 

43 

44 """ 

45 

46 super().__init__() 

47 

48 self.subtype = None 

49 self.sheet_type = 0x0200 

50 self.orientation = 0x0 

51 self.series = [] 

52 self.embedded = 0 

53 self.id = -1 

54 self.series_index = 0 

55 self.style_id = 2 

56 self.axis_ids = [] 

57 self.axis2_ids = [] 

58 self.cat_has_num_fmt = False 

59 self.requires_category = False 

60 self.legend = {} 

61 self.cat_axis_position = "b" 

62 self.val_axis_position = "l" 

63 self.formula_ids = {} 

64 self.formula_data = [] 

65 self.horiz_cat_axis = 0 

66 self.horiz_val_axis = 1 

67 self.protection = 0 

68 self.chartarea = {} 

69 self.plotarea = {} 

70 self.x_axis = {} 

71 self.y_axis = {} 

72 self.y2_axis = {} 

73 self.x2_axis = {} 

74 self.chart_name = "" 

75 self.show_blanks = "gap" 

76 self.show_na_as_empty = False 

77 self.show_hidden = False 

78 self.show_crosses = True 

79 self.width = 480 

80 self.height = 288 

81 self.x_scale = 1 

82 self.y_scale = 1 

83 self.x_offset = 0 

84 self.y_offset = 0 

85 self.table = None 

86 self.cross_between = "between" 

87 self.default_marker = None 

88 self.series_gap_1 = None 

89 self.series_gap_2 = None 

90 self.series_overlap_1 = None 

91 self.series_overlap_2 = None 

92 self.drop_lines = None 

93 self.hi_low_lines = None 

94 self.up_down_bars = None 

95 self.smooth_allowed = False 

96 self.title_font = None 

97 self.title_name = None 

98 self.title_formula = None 

99 self.title_data_id = None 

100 self.title_layout = None 

101 self.title_overlay = None 

102 self.title_none = False 

103 self.date_category = False 

104 self.date_1904 = False 

105 self.remove_timezone = False 

106 self.label_positions = {} 

107 self.label_position_default = "" 

108 self.already_inserted = False 

109 self.combined = None 

110 self.is_secondary = False 

111 self.warn_sheetname = True 

112 self._set_default_properties() 

113 self.fill = {} 

114 

115 def add_series(self, options=None): 

116 """ 

117 Add a data series to a chart. 

118 

119 Args: 

120 options: A dictionary of chart series options. 

121 

122 Returns: 

123 Nothing. 

124 

125 """ 

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

127 if options is None: 

128 options = {} 

129 

130 # Check that the required input has been specified. 

131 if "values" not in options: 

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

133 return 

134 

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

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

137 return 

138 

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

140 warn( 

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

142 "Excel Chart is 255" 

143 ) 

144 return 

145 

146 # Convert list into a formula string. 

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

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

149 

150 # Switch name and name_formula parameters if required. 

151 name, name_formula = self._process_names( 

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

153 ) 

154 

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

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

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

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

159 

160 # Set the line properties for the series. 

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

162 

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

164 if options.get("border"): 

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

166 

167 # Set the fill properties for the series. 

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

169 

170 # Set the pattern fill properties for the series. 

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

172 

173 # Set the gradient fill properties for the series. 

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

175 

176 # Pattern fill overrides solid fill. 

177 if pattern: 

178 self.fill = None 

179 

180 # Gradient fill overrides the solid and pattern fill. 

181 if gradient: 

182 pattern = None 

183 fill = None 

184 

185 # Set the marker properties for the series. 

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

187 

188 # Set the trendline properties for the series. 

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

190 

191 # Set the line smooth property for the series. 

192 smooth = options.get("smooth") 

193 

194 # Set the error bars properties for the series. 

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

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

197 

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

199 

200 # Set the point properties for the series. 

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

202 

203 # Set the labels properties for the series. 

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

205 

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

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

208 inverted_color = options.get("invert_if_negative_color") 

209 

210 if inverted_color: 

211 inverted_color = Color._from_value(inverted_color) 

212 

213 # Set the secondary axis properties. 

214 x2_axis = options.get("x2_axis") 

215 y2_axis = options.get("y2_axis") 

216 

217 # Store secondary status for combined charts. 

218 if x2_axis or y2_axis: 

219 self.is_secondary = True 

220 

221 # Set the gap for Bar/Column charts. 

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

223 if y2_axis: 

224 self.series_gap_2 = options["gap"] 

225 else: 

226 self.series_gap_1 = options["gap"] 

227 

228 # Set the overlap for Bar/Column charts. 

229 if options.get("overlap"): 

230 if y2_axis: 

231 self.series_overlap_2 = options["overlap"] 

232 else: 

233 self.series_overlap_1 = options["overlap"] 

234 

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

236 series = { 

237 "values": values, 

238 "categories": categories, 

239 "name": name, 

240 "name_formula": name_formula, 

241 "name_id": name_id, 

242 "val_data_id": val_id, 

243 "cat_data_id": cat_id, 

244 "line": line, 

245 "fill": fill, 

246 "pattern": pattern, 

247 "gradient": gradient, 

248 "marker": marker, 

249 "trendline": trendline, 

250 "labels": labels, 

251 "invert_if_neg": invert_if_neg, 

252 "inverted_color": inverted_color, 

253 "x2_axis": x2_axis, 

254 "y2_axis": y2_axis, 

255 "points": points, 

256 "error_bars": error_bars, 

257 "smooth": smooth, 

258 } 

259 

260 self.series.append(series) 

261 

262 def set_x_axis(self, options): 

263 """ 

264 Set the chart X axis options. 

265 

266 Args: 

267 options: A dictionary of axis options. 

268 

269 Returns: 

270 Nothing. 

271 

272 """ 

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

274 

275 self.x_axis = axis 

276 

277 def set_y_axis(self, options): 

278 """ 

279 Set the chart Y axis options. 

280 

281 Args: 

282 options: A dictionary of axis options. 

283 

284 Returns: 

285 Nothing. 

286 

287 """ 

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

289 

290 self.y_axis = axis 

291 

292 def set_x2_axis(self, options): 

293 """ 

294 Set the chart secondary X axis options. 

295 

296 Args: 

297 options: A dictionary of axis options. 

298 

299 Returns: 

300 Nothing. 

301 

302 """ 

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

304 

305 self.x2_axis = axis 

306 

307 def set_y2_axis(self, options): 

308 """ 

309 Set the chart secondary Y axis options. 

310 

311 Args: 

312 options: A dictionary of axis options. 

313 

314 Returns: 

315 Nothing. 

316 

317 """ 

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

319 

320 self.y2_axis = axis 

321 

322 def set_title(self, options=None): 

323 """ 

324 Set the chart title options. 

325 

326 Args: 

327 options: A dictionary of chart title options. 

328 

329 Returns: 

330 Nothing. 

331 

332 """ 

333 if options is None: 

334 options = {} 

335 

336 name, name_formula = self._process_names( 

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

338 ) 

339 

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

341 

342 self.title_name = name 

343 self.title_formula = name_formula 

344 self.title_data_id = data_id 

345 

346 # Set the font properties if present. 

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

348 

349 # Set the axis name layout. 

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

351 # Set the title overlay option. 

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

353 

354 # Set the automatic title option. 

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

356 

357 def set_legend(self, options): 

358 """ 

359 Set the chart legend options. 

360 

361 Args: 

362 options: A dictionary of chart legend options. 

363 

364 Returns: 

365 Nothing. 

366 """ 

367 # Convert the user defined properties to internal properties. 

368 self.legend = self._get_legend_properties(options) 

369 

370 def set_plotarea(self, options): 

371 """ 

372 Set the chart plot area options. 

373 

374 Args: 

375 options: A dictionary of chart plot area options. 

376 

377 Returns: 

378 Nothing. 

379 """ 

380 # Convert the user defined properties to internal properties. 

381 self.plotarea = self._get_area_properties(options) 

382 

383 def set_chartarea(self, options): 

384 """ 

385 Set the chart area options. 

386 

387 Args: 

388 options: A dictionary of chart area options. 

389 

390 Returns: 

391 Nothing. 

392 """ 

393 # Convert the user defined properties to internal properties. 

394 self.chartarea = self._get_area_properties(options) 

395 

396 def set_style(self, style_id): 

397 """ 

398 Set the chart style type. 

399 

400 Args: 

401 style_id: An int representing the chart style. 

402 

403 Returns: 

404 Nothing. 

405 """ 

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

407 if style_id is None: 

408 style_id = 2 

409 

410 if style_id < 1 or style_id > 48: 

411 style_id = 2 

412 

413 self.style_id = style_id 

414 

415 def show_blanks_as(self, option): 

416 """ 

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

418 

419 Args: 

420 option: A string representing the display option. 

421 

422 Returns: 

423 Nothing. 

424 """ 

425 if not option: 

426 return 

427 

428 valid_options = { 

429 "gap": 1, 

430 "zero": 1, 

431 "span": 1, 

432 } 

433 

434 if option not in valid_options: 

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

436 return 

437 

438 self.show_blanks = option 

439 

440 def show_na_as_empty_cell(self): 

441 """ 

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

443 

444 Args: 

445 None. 

446 

447 Returns: 

448 Nothing. 

449 """ 

450 self.show_na_as_empty = True 

451 

452 def show_hidden_data(self): 

453 """ 

454 Display data on charts from hidden rows or columns. 

455 

456 Args: 

457 None. 

458 

459 Returns: 

460 Nothing. 

461 """ 

462 self.show_hidden = True 

463 

464 def set_size(self, options=None): 

465 """ 

466 Set size or scale of the chart. 

467 

468 Args: 

469 options: A dictionary of chart size options. 

470 

471 Returns: 

472 Nothing. 

473 """ 

474 if options is None: 

475 options = {} 

476 

477 # Set dimensions or scale for the chart. 

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

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

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

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

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

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

484 

485 def set_table(self, options=None): 

486 """ 

487 Set properties for an axis data table. 

488 

489 Args: 

490 options: A dictionary of axis table options. 

491 

492 Returns: 

493 Nothing. 

494 

495 """ 

496 if options is None: 

497 options = {} 

498 

499 table = {} 

500 

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

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

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

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

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

506 

507 self.table = table 

508 

509 def set_up_down_bars(self, options=None): 

510 """ 

511 Set properties for the chart up-down bars. 

512 

513 Args: 

514 options: A dictionary of options. 

515 

516 Returns: 

517 Nothing. 

518 

519 """ 

520 if options is None: 

521 options = {} 

522 

523 # Defaults. 

524 up_line = None 

525 up_fill = None 

526 down_line = None 

527 down_fill = None 

528 

529 # Set properties for 'up' bar. 

530 if options.get("up"): 

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

532 # Map border to line. 

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

534 

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

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

537 

538 if "fill" in 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 if "border" in options["down"]: 

544 # Map border to line. 

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

546 

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

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

549 

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

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

552 

553 self.up_down_bars = { 

554 "up": { 

555 "line": up_line, 

556 "fill": up_fill, 

557 }, 

558 "down": { 

559 "line": down_line, 

560 "fill": down_fill, 

561 }, 

562 } 

563 

564 def set_drop_lines(self, options=None): 

565 """ 

566 Set properties for the chart drop lines. 

567 

568 Args: 

569 options: A dictionary of options. 

570 

571 Returns: 

572 Nothing. 

573 

574 """ 

575 if options is None: 

576 options = {} 

577 

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

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

580 

581 # Set the pattern fill properties for the series. 

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

583 

584 # Set the gradient fill properties for the series. 

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

586 

587 # Pattern fill overrides solid fill. 

588 if pattern: 

589 self.fill = None 

590 

591 # Gradient fill overrides the solid and pattern fill. 

592 if gradient: 

593 pattern = None 

594 fill = None 

595 

596 self.drop_lines = { 

597 "line": line, 

598 "fill": fill, 

599 "pattern": pattern, 

600 "gradient": gradient, 

601 } 

602 

603 def set_high_low_lines(self, options=None): 

604 """ 

605 Set properties for the chart high-low lines. 

606 

607 Args: 

608 options: A dictionary of options. 

609 

610 Returns: 

611 Nothing. 

612 

613 """ 

614 if options is None: 

615 options = {} 

616 

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

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

619 

620 # Set the pattern fill properties for the series. 

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

622 

623 # Set the gradient fill properties for the series. 

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

625 

626 # Pattern fill overrides solid fill. 

627 if pattern: 

628 self.fill = None 

629 

630 # Gradient fill overrides the solid and pattern fill. 

631 if gradient: 

632 pattern = None 

633 fill = None 

634 

635 self.hi_low_lines = { 

636 "line": line, 

637 "fill": fill, 

638 "pattern": pattern, 

639 "gradient": gradient, 

640 } 

641 

642 def combine(self, chart=None): 

643 """ 

644 Create a combination chart with a secondary chart. 

645 

646 Args: 

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

648 

649 Returns: 

650 Nothing. 

651 

652 """ 

653 if chart is None: 

654 return 

655 

656 self.combined = chart 

657 

658 ########################################################################### 

659 # 

660 # Private API. 

661 # 

662 ########################################################################### 

663 

664 def _assemble_xml_file(self): 

665 # Assemble and write the XML file. 

666 

667 # Write the XML declaration. 

668 self._xml_declaration() 

669 

670 # Write the c:chartSpace element. 

671 self._write_chart_space() 

672 

673 # Write the c:lang element. 

674 self._write_lang() 

675 

676 # Write the c:style element. 

677 self._write_style() 

678 

679 # Write the c:protection element. 

680 self._write_protection() 

681 

682 # Write the c:chart element. 

683 self._write_chart() 

684 

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

686 self._write_sp_pr(self.chartarea) 

687 

688 # Write the c:printSettings element. 

689 if self.embedded: 

690 self._write_print_settings() 

691 

692 # Close the worksheet tag. 

693 self._xml_end_tag("c:chartSpace") 

694 # Close the file. 

695 self._xml_close() 

696 

697 def _convert_axis_args(self, axis, user_options): 

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

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

700 options.update(user_options) 

701 

702 name, name_formula = self._process_names( 

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

704 ) 

705 

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

707 

708 axis = { 

709 "defaults": axis["defaults"], 

710 "name": name, 

711 "formula": name_formula, 

712 "data_id": data_id, 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

731 "text_axis": False, 

732 } 

733 

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

735 

736 # Convert the display units. 

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

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

739 

740 # Map major_gridlines properties. 

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

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

743 options["major_gridlines"] 

744 ) 

745 

746 # Map minor_gridlines properties. 

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

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

749 options["minor_gridlines"] 

750 ) 

751 

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

753 if axis.get("position"): 

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

755 

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

757 if axis.get("position_axis"): 

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

759 axis["position_axis"] = "midCat" 

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

761 # Doesn't need to be modified. 

762 pass 

763 else: 

764 # Otherwise use the default value. 

765 axis["position_axis"] = None 

766 

767 # Set the category axis as a date axis. 

768 if options.get("date_axis"): 

769 self.date_category = True 

770 

771 # Set the category axis as a text axis. 

772 if options.get("text_axis"): 

773 self.date_category = False 

774 axis["text_axis"] = True 

775 

776 # Convert datetime args if required. 

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

778 axis["min"] = _datetime_to_excel_datetime( 

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

780 ) 

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

782 axis["max"] = _datetime_to_excel_datetime( 

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

784 ) 

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

786 axis["crossing"] = _datetime_to_excel_datetime( 

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

788 ) 

789 

790 # Set the font properties if present. 

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

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

793 

794 # Set the axis name layout. 

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

796 options.get("name_layout"), True 

797 ) 

798 

799 # Set the line properties for the axis. 

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

801 

802 # Set the fill properties for the axis. 

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

804 

805 # Set the pattern fill properties for the series. 

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

807 

808 # Set the gradient fill properties for the series. 

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

810 

811 # Pattern fill overrides solid fill. 

812 if axis.get("pattern"): 

813 axis["fill"] = None 

814 

815 # Gradient fill overrides the solid and pattern fill. 

816 if axis.get("gradient"): 

817 axis["pattern"] = None 

818 axis["fill"] = None 

819 

820 # Set the tick marker types. 

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

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

823 

824 return axis 

825 

826 def _convert_font_args(self, options): 

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

828 if not options: 

829 return {} 

830 

831 font = { 

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

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

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

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

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

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

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

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

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

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

842 } 

843 

844 # Convert font size units. 

845 if font["size"]: 

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

847 

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

849 if font["rotation"]: 

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

851 

852 if font.get("color"): 

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

854 

855 return font 

856 

857 def _list_to_formula(self, data): 

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

859 

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

861 if not isinstance(data, list): 

862 # Check for unquoted sheetnames. 

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

864 warn( 

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

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

867 ) 

868 return data 

869 

870 formula = xl_range_formula(*data) 

871 

872 return formula 

873 

874 def _process_names(self, name, name_formula): 

875 # Switch name and name_formula parameters if required. 

876 

877 if name is not None: 

878 if isinstance(name, list): 

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

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

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

882 name = "" 

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

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

885 name_formula = name 

886 name = "" 

887 

888 return name, name_formula 

889 

890 def _get_data_type(self, data): 

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

892 

893 # Check for no data in the series. 

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

895 return "none" 

896 

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

898 return "multi_str" 

899 

900 # Determine if data is numeric or strings. 

901 for token in data: 

902 if token is None: 

903 continue 

904 

905 # Check for strings that would evaluate to float like 

906 # '1.1_1' of ' 1'. 

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

908 # Assume entire data series is string data. 

909 return "str" 

910 

911 try: 

912 float(token) 

913 except ValueError: 

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

915 return "str" 

916 

917 # The series data was all numeric. 

918 return "num" 

919 

920 def _get_data_id(self, formula, data): 

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

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

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

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

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

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

927 

928 # Ignore series without a range formula. 

929 if not formula: 

930 return None 

931 

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

933 if formula.startswith("="): 

934 formula = formula.lstrip("=") 

935 

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

937 # in a separate array with the same id. 

938 if formula not in self.formula_ids: 

939 # Haven't seen this formula before. 

940 formula_id = len(self.formula_data) 

941 

942 self.formula_data.append(data) 

943 self.formula_ids[formula] = formula_id 

944 else: 

945 # Formula already seen. Return existing id. 

946 formula_id = self.formula_ids[formula] 

947 

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

949 if self.formula_data[formula_id] is None: 

950 self.formula_data[formula_id] = data 

951 

952 return formula_id 

953 

954 def _get_marker_properties(self, marker): 

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

956 

957 if not marker: 

958 return None 

959 

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

961 marker = copy.deepcopy(marker) 

962 

963 types = { 

964 "automatic": "automatic", 

965 "none": "none", 

966 "square": "square", 

967 "diamond": "diamond", 

968 "triangle": "triangle", 

969 "x": "x", 

970 "star": "star", 

971 "dot": "dot", 

972 "short_dash": "dot", 

973 "dash": "dash", 

974 "long_dash": "dash", 

975 "circle": "circle", 

976 "plus": "plus", 

977 "picture": "picture", 

978 } 

979 

980 # Check for valid types. 

981 marker_type = marker.get("type") 

982 

983 if marker_type is not None: 

984 if marker_type in types: 

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

986 else: 

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

988 return None 

989 

990 # Set the line properties for the marker. 

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

992 

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

994 if "border" in marker: 

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

996 

997 # Set the fill properties for the marker. 

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

999 

1000 # Set the pattern fill properties for the series. 

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

1002 

1003 # Set the gradient fill properties for the series. 

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

1005 

1006 # Pattern fill overrides solid fill. 

1007 if pattern: 

1008 self.fill = None 

1009 

1010 # Gradient fill overrides the solid and pattern fill. 

1011 if gradient: 

1012 pattern = None 

1013 fill = None 

1014 

1015 marker["line"] = line 

1016 marker["fill"] = fill 

1017 marker["pattern"] = pattern 

1018 marker["gradient"] = gradient 

1019 

1020 return marker 

1021 

1022 def _get_trendline_properties(self, trendline): 

1023 # Convert user trendline properties to structure required internally. 

1024 

1025 if not trendline: 

1026 return None 

1027 

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

1029 trendline = copy.deepcopy(trendline) 

1030 

1031 types = { 

1032 "exponential": "exp", 

1033 "linear": "linear", 

1034 "log": "log", 

1035 "moving_average": "movingAvg", 

1036 "polynomial": "poly", 

1037 "power": "power", 

1038 } 

1039 

1040 # Check the trendline type. 

1041 trend_type = trendline.get("type") 

1042 

1043 if trend_type in types: 

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

1045 else: 

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

1047 return None 

1048 

1049 # Set the line properties for the trendline. 

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

1051 

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

1053 if "border" in trendline: 

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

1055 

1056 # Set the fill properties for the trendline. 

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

1058 

1059 # Set the pattern fill properties for the trendline. 

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

1061 

1062 # Set the gradient fill properties for the trendline. 

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

1064 

1065 # Set the format properties for the trendline label. 

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

1067 

1068 # Pattern fill overrides solid fill. 

1069 if pattern: 

1070 self.fill = None 

1071 

1072 # Gradient fill overrides the solid and pattern fill. 

1073 if gradient: 

1074 pattern = None 

1075 fill = None 

1076 

1077 trendline["line"] = line 

1078 trendline["fill"] = fill 

1079 trendline["pattern"] = pattern 

1080 trendline["gradient"] = gradient 

1081 trendline["label"] = label 

1082 

1083 return trendline 

1084 

1085 def _get_trendline_label_properties(self, label): 

1086 # Convert user trendline properties to structure required internally. 

1087 

1088 if not label: 

1089 return {} 

1090 

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

1092 label = copy.deepcopy(label) 

1093 

1094 # Set the font properties if present. 

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

1096 

1097 # Set the line properties for the label. 

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

1099 

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

1101 if "border" in label: 

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

1103 

1104 # Set the fill properties for the label. 

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

1106 

1107 # Set the pattern fill properties for the label. 

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

1109 

1110 # Set the gradient fill properties for the label. 

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

1112 

1113 # Pattern fill overrides solid fill. 

1114 if pattern: 

1115 self.fill = None 

1116 

1117 # Gradient fill overrides the solid and pattern fill. 

1118 if gradient: 

1119 pattern = None 

1120 fill = None 

1121 

1122 label["font"] = font 

1123 label["line"] = line 

1124 label["fill"] = fill 

1125 label["pattern"] = pattern 

1126 label["gradient"] = gradient 

1127 

1128 return label 

1129 

1130 def _get_error_bars_props(self, options): 

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

1132 if not options: 

1133 return {} 

1134 

1135 # Default values. 

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

1137 

1138 types = { 

1139 "fixed": "fixedVal", 

1140 "percentage": "percentage", 

1141 "standard_deviation": "stdDev", 

1142 "standard_error": "stdErr", 

1143 "custom": "cust", 

1144 } 

1145 

1146 # Check the error bars type. 

1147 error_type = options["type"] 

1148 

1149 if error_type in types: 

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

1151 else: 

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

1153 return {} 

1154 

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

1156 if "value" in options: 

1157 error_bars["value"] = options["value"] 

1158 

1159 # Set the end-cap style. 

1160 if "end_style" in options: 

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

1162 

1163 # Set the error bar direction. 

1164 if "direction" in options: 

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

1166 error_bars["direction"] = "minus" 

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

1168 error_bars["direction"] = "plus" 

1169 else: 

1170 # Default to 'both'. 

1171 pass 

1172 

1173 # Set any custom values. 

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

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

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

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

1178 

1179 # Set the line properties for the error bars. 

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

1181 

1182 return error_bars 

1183 

1184 def _get_gridline_properties(self, options): 

1185 # Convert user gridline properties to structure required internally. 

1186 

1187 # Set the visible property for the gridline. 

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

1189 

1190 # Set the line properties for the gridline. 

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

1192 

1193 return gridline 

1194 

1195 def _get_labels_properties(self, labels): 

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

1197 

1198 if not labels: 

1199 return None 

1200 

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

1202 labels = copy.deepcopy(labels) 

1203 

1204 # Map user defined label positions to Excel positions. 

1205 position = labels.get("position") 

1206 

1207 if position: 

1208 if position in self.label_positions: 

1209 if position == self.label_position_default: 

1210 labels["position"] = None 

1211 else: 

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

1213 else: 

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

1215 return None 

1216 

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

1218 separator = labels.get("separator") 

1219 separators = { 

1220 ",": ", ", 

1221 ";": "; ", 

1222 ".": ". ", 

1223 "\n": "\n", 

1224 " ": " ", 

1225 } 

1226 

1227 if separator: 

1228 if separator in separators: 

1229 labels["separator"] = separators[separator] 

1230 else: 

1231 warn("Unsupported label separator") 

1232 return None 

1233 

1234 # Set the font properties if present. 

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

1236 

1237 # Set the line properties for the labels. 

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

1239 

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

1241 if "border" in labels: 

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

1243 

1244 # Set the fill properties for the labels. 

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

1246 

1247 # Set the pattern fill properties for the labels. 

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

1249 

1250 # Set the gradient fill properties for the labels. 

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

1252 

1253 # Pattern fill overrides solid fill. 

1254 if pattern: 

1255 self.fill = None 

1256 

1257 # Gradient fill overrides the solid and pattern fill. 

1258 if gradient: 

1259 pattern = None 

1260 fill = None 

1261 

1262 labels["line"] = line 

1263 labels["fill"] = fill 

1264 labels["pattern"] = pattern 

1265 labels["gradient"] = gradient 

1266 

1267 if labels.get("custom"): 

1268 for label in labels["custom"]: 

1269 if label is None: 

1270 continue 

1271 

1272 value = label.get("value") 

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

1274 label["formula"] = value 

1275 

1276 formula = label.get("formula") 

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

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

1279 

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

1281 label["data_id"] = data_id 

1282 

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

1284 

1285 # Set the line properties for the label. 

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

1287 

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

1289 if "border" in label: 

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

1291 

1292 # Set the fill properties for the label. 

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

1294 

1295 # Set the pattern fill properties for the label. 

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

1297 

1298 # Set the gradient fill properties for the label. 

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

1300 

1301 # Pattern fill overrides solid fill. 

1302 if pattern: 

1303 self.fill = None 

1304 

1305 # Gradient fill overrides the solid and pattern fill. 

1306 if gradient: 

1307 pattern = None 

1308 fill = None 

1309 

1310 label["line"] = line 

1311 label["fill"] = fill 

1312 label["pattern"] = pattern 

1313 label["gradient"] = gradient 

1314 

1315 return labels 

1316 

1317 def _get_area_properties(self, options): 

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

1319 area = {} 

1320 

1321 # Set the line properties for the chartarea. 

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

1323 

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

1325 if options.get("border"): 

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

1327 

1328 # Set the fill properties for the chartarea. 

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

1330 

1331 # Set the pattern fill properties for the series. 

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

1333 

1334 # Set the gradient fill properties for the series. 

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

1336 

1337 # Pattern fill overrides solid fill. 

1338 if pattern: 

1339 self.fill = None 

1340 

1341 # Gradient fill overrides the solid and pattern fill. 

1342 if gradient: 

1343 pattern = None 

1344 fill = None 

1345 

1346 # Set the plotarea layout. 

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

1348 

1349 area["line"] = line 

1350 area["fill"] = fill 

1351 area["pattern"] = pattern 

1352 area["layout"] = layout 

1353 area["gradient"] = gradient 

1354 

1355 return area 

1356 

1357 def _get_legend_properties(self, options=None): 

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

1359 legend = {} 

1360 

1361 if options is None: 

1362 options = {} 

1363 

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

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

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

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

1368 

1369 # Turn off the legend. 

1370 if options.get("none"): 

1371 legend["position"] = "none" 

1372 

1373 # Set the line properties for the legend. 

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

1375 

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

1377 if options.get("border"): 

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

1379 

1380 # Set the fill properties for the legend. 

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

1382 

1383 # Set the pattern fill properties for the series. 

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

1385 

1386 # Set the gradient fill properties for the series. 

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

1388 

1389 # Pattern fill overrides solid fill. 

1390 if pattern: 

1391 self.fill = None 

1392 

1393 # Gradient fill overrides the solid and pattern fill. 

1394 if gradient: 

1395 pattern = None 

1396 fill = None 

1397 

1398 # Set the legend layout. 

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

1400 

1401 legend["line"] = line 

1402 legend["fill"] = fill 

1403 legend["pattern"] = pattern 

1404 legend["layout"] = layout 

1405 legend["gradient"] = gradient 

1406 

1407 return legend 

1408 

1409 def _get_layout_properties(self, args, is_text): 

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

1411 layout = {} 

1412 

1413 if not args: 

1414 return {} 

1415 

1416 if is_text: 

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

1418 else: 

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

1420 

1421 # Check for valid properties. 

1422 for key in args.keys(): 

1423 if key not in properties: 

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

1425 return {} 

1426 

1427 # Set the layout properties. 

1428 for prop in properties: 

1429 if prop not in args.keys(): 

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

1431 return {} 

1432 

1433 value = args[prop] 

1434 

1435 try: 

1436 float(value) 

1437 except ValueError: 

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

1439 return {} 

1440 

1441 if value < 0 or value > 1: 

1442 warn( 

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

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

1445 ) 

1446 return {} 

1447 

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

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

1450 

1451 return layout 

1452 

1453 def _get_points_properties(self, user_points): 

1454 # Convert user points properties to structure required internally. 

1455 points = [] 

1456 

1457 if not user_points: 

1458 return [] 

1459 

1460 for user_point in user_points: 

1461 point = {} 

1462 

1463 if user_point is not None: 

1464 # Set the line properties for the point. 

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

1466 

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

1468 if "border" in user_point: 

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

1470 

1471 # Set the fill properties for the chartarea. 

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

1473 

1474 # Set the pattern fill properties for the series. 

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

1476 

1477 # Set the gradient fill properties for the series. 

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

1479 

1480 # Pattern fill overrides solid fill. 

1481 if pattern: 

1482 self.fill = None 

1483 

1484 # Gradient fill overrides the solid and pattern fill. 

1485 if gradient: 

1486 pattern = None 

1487 fill = None 

1488 

1489 point["line"] = line 

1490 point["fill"] = fill 

1491 point["pattern"] = pattern 

1492 point["gradient"] = gradient 

1493 

1494 points.append(point) 

1495 

1496 return points 

1497 

1498 def _has_fill_formatting(self, element): 

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

1500 has_fill = False 

1501 has_line = False 

1502 has_pattern = element.get("pattern") 

1503 has_gradient = element.get("gradient") 

1504 

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

1506 has_fill = True 

1507 

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

1509 has_line = True 

1510 

1511 return has_fill or has_line or has_pattern or has_gradient 

1512 

1513 def _get_display_units(self, display_units): 

1514 # Convert user defined display units to internal units. 

1515 if not display_units: 

1516 return None 

1517 

1518 types = { 

1519 "hundreds": "hundreds", 

1520 "thousands": "thousands", 

1521 "ten_thousands": "tenThousands", 

1522 "hundred_thousands": "hundredThousands", 

1523 "millions": "millions", 

1524 "ten_millions": "tenMillions", 

1525 "hundred_millions": "hundredMillions", 

1526 "billions": "billions", 

1527 "trillions": "trillions", 

1528 } 

1529 

1530 if display_units in types: 

1531 display_units = types[display_units] 

1532 else: 

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

1534 return None 

1535 

1536 return display_units 

1537 

1538 def _get_tick_type(self, tick_type): 

1539 # Convert user defined display units to internal units. 

1540 if not tick_type: 

1541 return None 

1542 

1543 types = { 

1544 "outside": "out", 

1545 "inside": "in", 

1546 "none": "none", 

1547 "cross": "cross", 

1548 } 

1549 

1550 if tick_type in types: 

1551 tick_type = types[tick_type] 

1552 else: 

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

1554 return None 

1555 

1556 return tick_type 

1557 

1558 def _get_primary_axes_series(self): 

1559 # Returns series which use the primary axes. 

1560 primary_axes_series = [] 

1561 

1562 for series in self.series: 

1563 if not series["y2_axis"]: 

1564 primary_axes_series.append(series) 

1565 

1566 return primary_axes_series 

1567 

1568 def _get_secondary_axes_series(self): 

1569 # Returns series which use the secondary axes. 

1570 secondary_axes_series = [] 

1571 

1572 for series in self.series: 

1573 if series["y2_axis"]: 

1574 secondary_axes_series.append(series) 

1575 

1576 return secondary_axes_series 

1577 

1578 def _add_axis_ids(self, args): 

1579 # Add unique ids for primary or secondary axes 

1580 chart_id = 5001 + int(self.id) 

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

1582 

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

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

1585 

1586 if args["primary_axes"]: 

1587 self.axis_ids.append(id1) 

1588 self.axis_ids.append(id2) 

1589 

1590 if not args["primary_axes"]: 

1591 self.axis2_ids.append(id1) 

1592 self.axis2_ids.append(id2) 

1593 

1594 def _set_default_properties(self): 

1595 # Setup the default properties for a chart. 

1596 

1597 self.x_axis["defaults"] = { 

1598 "num_format": "General", 

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

1600 } 

1601 

1602 self.y_axis["defaults"] = { 

1603 "num_format": "General", 

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

1605 } 

1606 

1607 self.x2_axis["defaults"] = { 

1608 "num_format": "General", 

1609 "label_position": "none", 

1610 "crossing": "max", 

1611 "visible": 0, 

1612 } 

1613 

1614 self.y2_axis["defaults"] = { 

1615 "num_format": "General", 

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

1617 "position": "right", 

1618 "visible": 1, 

1619 } 

1620 

1621 self.set_x_axis({}) 

1622 self.set_y_axis({}) 

1623 

1624 self.set_x2_axis({}) 

1625 self.set_y2_axis({}) 

1626 

1627 ########################################################################### 

1628 # 

1629 # XML methods. 

1630 # 

1631 ########################################################################### 

1632 

1633 def _write_chart_space(self): 

1634 # Write the <c:chartSpace> element. 

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

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

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

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

1639 

1640 attributes = [ 

1641 ("xmlns:c", xmlns_c), 

1642 ("xmlns:a", xmlns_a), 

1643 ("xmlns:r", xmlns_r), 

1644 ] 

1645 

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

1647 

1648 def _write_lang(self): 

1649 # Write the <c:lang> element. 

1650 val = "en-US" 

1651 

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

1653 

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

1655 

1656 def _write_style(self): 

1657 # Write the <c:style> element. 

1658 style_id = self.style_id 

1659 

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

1661 if style_id == 2: 

1662 return 

1663 

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

1665 

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

1667 

1668 def _write_chart(self): 

1669 # Write the <c:chart> element. 

1670 self._xml_start_tag("c:chart") 

1671 

1672 if self.title_none: 

1673 # Turn off the title. 

1674 self._write_c_auto_title_deleted() 

1675 else: 

1676 # Write the chart title elements. 

1677 if self.title_formula is not None: 

1678 self._write_title_formula( 

1679 self.title_formula, 

1680 self.title_data_id, 

1681 None, 

1682 self.title_font, 

1683 self.title_layout, 

1684 self.title_overlay, 

1685 ) 

1686 elif self.title_name is not None: 

1687 self._write_title_rich( 

1688 self.title_name, 

1689 None, 

1690 self.title_font, 

1691 self.title_layout, 

1692 self.title_overlay, 

1693 ) 

1694 

1695 # Write the c:plotArea element. 

1696 self._write_plot_area() 

1697 

1698 # Write the c:legend element. 

1699 self._write_legend() 

1700 

1701 # Write the c:plotVisOnly element. 

1702 self._write_plot_vis_only() 

1703 

1704 # Write the c:dispBlanksAs element. 

1705 self._write_disp_blanks_as() 

1706 

1707 # Write the c:extLst element. 

1708 if self.show_na_as_empty: 

1709 self._write_c_ext_lst_display_na() 

1710 

1711 self._xml_end_tag("c:chart") 

1712 

1713 def _write_disp_blanks_as(self): 

1714 # Write the <c:dispBlanksAs> element. 

1715 val = self.show_blanks 

1716 

1717 # Ignore the default value. 

1718 if val == "gap": 

1719 return 

1720 

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

1722 

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

1724 

1725 def _write_plot_area(self): 

1726 # Write the <c:plotArea> element. 

1727 self._xml_start_tag("c:plotArea") 

1728 

1729 # Write the c:layout element. 

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

1731 

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

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

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

1735 

1736 # Configure a combined chart if present. 

1737 second_chart = self.combined 

1738 if second_chart: 

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

1740 if second_chart.is_secondary: 

1741 second_chart.id = 1000 + self.id 

1742 else: 

1743 second_chart.id = self.id 

1744 

1745 # Share the same filehandle for writing. 

1746 second_chart.fh = self.fh 

1747 

1748 # Share series index with primary chart. 

1749 second_chart.series_index = self.series_index 

1750 

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

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

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

1754 

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

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

1757 

1758 if self.date_category: 

1759 self._write_date_axis(args) 

1760 else: 

1761 self._write_cat_axis(args) 

1762 

1763 self._write_val_axis(args) 

1764 

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

1766 args = { 

1767 "x_axis": self.x2_axis, 

1768 "y_axis": self.y2_axis, 

1769 "axis_ids": self.axis2_ids, 

1770 } 

1771 

1772 self._write_val_axis(args) 

1773 

1774 # Write the secondary axis for the secondary chart. 

1775 if second_chart and second_chart.is_secondary: 

1776 args = { 

1777 "x_axis": second_chart.x2_axis, 

1778 "y_axis": second_chart.y2_axis, 

1779 "axis_ids": second_chart.axis2_ids, 

1780 } 

1781 

1782 second_chart._write_val_axis(args) 

1783 

1784 if self.date_category: 

1785 self._write_date_axis(args) 

1786 else: 

1787 self._write_cat_axis(args) 

1788 

1789 # Write the c:dTable element. 

1790 self._write_d_table() 

1791 

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

1793 self._write_sp_pr(self.plotarea) 

1794 

1795 self._xml_end_tag("c:plotArea") 

1796 

1797 def _write_layout(self, layout, layout_type): 

1798 # Write the <c:layout> element. 

1799 

1800 if not layout: 

1801 # Automatic layout. 

1802 self._xml_empty_tag("c:layout") 

1803 else: 

1804 # User defined manual layout. 

1805 self._xml_start_tag("c:layout") 

1806 self._write_manual_layout(layout, layout_type) 

1807 self._xml_end_tag("c:layout") 

1808 

1809 def _write_manual_layout(self, layout, layout_type): 

1810 # Write the <c:manualLayout> element. 

1811 self._xml_start_tag("c:manualLayout") 

1812 

1813 # Plotarea has a layoutTarget element. 

1814 if layout_type == "plot": 

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

1816 

1817 # Set the x, y positions. 

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

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

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

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

1822 

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

1824 if layout_type != "text": 

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

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

1827 

1828 self._xml_end_tag("c:manualLayout") 

1829 

1830 def _write_chart_type(self, args): 

1831 # pylint: disable=unused-argument 

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

1833 # by the subclasses. 

1834 return 

1835 

1836 def _write_grouping(self, val): 

1837 # Write the <c:grouping> element. 

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

1839 

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

1841 

1842 def _write_series(self, series): 

1843 # Write the series elements. 

1844 self._write_ser(series) 

1845 

1846 def _write_ser(self, series): 

1847 # Write the <c:ser> element. 

1848 index = self.series_index 

1849 self.series_index += 1 

1850 

1851 self._xml_start_tag("c:ser") 

1852 

1853 # Write the c:idx element. 

1854 self._write_idx(index) 

1855 

1856 # Write the c:order element. 

1857 self._write_order(index) 

1858 

1859 # Write the series name. 

1860 self._write_series_name(series) 

1861 

1862 # Write the c:spPr element. 

1863 self._write_sp_pr(series) 

1864 

1865 # Write the c:marker element. 

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

1867 

1868 # Write the c:invertIfNegative element. 

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

1870 

1871 # Write the c:dPt element. 

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

1873 

1874 # Write the c:dLbls element. 

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

1876 

1877 # Write the c:trendline element. 

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

1879 

1880 # Write the c:errBars element. 

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

1882 

1883 # Write the c:cat element. 

1884 self._write_cat(series) 

1885 

1886 # Write the c:val element. 

1887 self._write_val(series) 

1888 

1889 # Write the c:smooth element. 

1890 if self.smooth_allowed: 

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

1892 

1893 # Write the c:extLst element. 

1894 if series.get("inverted_color"): 

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

1896 

1897 self._xml_end_tag("c:ser") 

1898 

1899 def _write_c_ext_lst_inverted_color(self, color: Color): 

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

1901 

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

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

1904 

1905 attributes1 = [ 

1906 ("uri", uri), 

1907 ("xmlns:c14", xmlns_c_14), 

1908 ] 

1909 

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

1911 

1912 self._xml_start_tag("c:extLst") 

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

1914 self._xml_start_tag("c14:invertSolidFillFmt") 

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

1916 

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

1918 

1919 self._xml_end_tag("c14:spPr") 

1920 self._xml_end_tag("c14:invertSolidFillFmt") 

1921 self._xml_end_tag("c:ext") 

1922 self._xml_end_tag("c:extLst") 

1923 

1924 def _write_c_ext_lst_display_na(self): 

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

1926 

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

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

1929 

1930 attributes1 = [ 

1931 ("uri", uri), 

1932 ("xmlns:c16r3", xmlns_c_16), 

1933 ] 

1934 

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

1936 

1937 self._xml_start_tag("c:extLst") 

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

1939 self._xml_start_tag("c16r3:dataDisplayOptions16") 

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

1941 self._xml_end_tag("c16r3:dataDisplayOptions16") 

1942 self._xml_end_tag("c:ext") 

1943 self._xml_end_tag("c:extLst") 

1944 

1945 def _write_idx(self, val): 

1946 # Write the <c:idx> element. 

1947 

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

1949 

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

1951 

1952 def _write_order(self, val): 

1953 # Write the <c:order> element. 

1954 

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

1956 

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

1958 

1959 def _write_series_name(self, series): 

1960 # Write the series name. 

1961 

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

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

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

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

1966 

1967 def _write_c_smooth(self, smooth): 

1968 # Write the <c:smooth> element. 

1969 

1970 if smooth: 

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

1972 

1973 def _write_cat(self, series): 

1974 # Write the <c:cat> element. 

1975 formula = series["categories"] 

1976 data_id = series["cat_data_id"] 

1977 data = None 

1978 

1979 if data_id is not None: 

1980 data = self.formula_data[data_id] 

1981 

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

1983 if not formula: 

1984 return 

1985 

1986 self._xml_start_tag("c:cat") 

1987 

1988 # Check the type of cached data. 

1989 cat_type = self._get_data_type(data) 

1990 

1991 if cat_type == "str": 

1992 self.cat_has_num_fmt = False 

1993 # Write the c:numRef element. 

1994 self._write_str_ref(formula, data, cat_type) 

1995 

1996 elif cat_type == "multi_str": 

1997 self.cat_has_num_fmt = False 

1998 # Write the c:numRef element. 

1999 self._write_multi_lvl_str_ref(formula, data) 

2000 

2001 else: 

2002 self.cat_has_num_fmt = True 

2003 # Write the c:numRef element. 

2004 self._write_num_ref(formula, data, cat_type) 

2005 

2006 self._xml_end_tag("c:cat") 

2007 

2008 def _write_val(self, series): 

2009 # Write the <c:val> element. 

2010 formula = series["values"] 

2011 data_id = series["val_data_id"] 

2012 data = self.formula_data[data_id] 

2013 

2014 self._xml_start_tag("c:val") 

2015 

2016 # Unlike Cat axes data should only be numeric. 

2017 # Write the c:numRef element. 

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

2019 

2020 self._xml_end_tag("c:val") 

2021 

2022 def _write_num_ref(self, formula, data, ref_type): 

2023 # Write the <c:numRef> element. 

2024 self._xml_start_tag("c:numRef") 

2025 

2026 # Write the c:f element. 

2027 self._write_series_formula(formula) 

2028 

2029 if ref_type == "num": 

2030 # Write the c:numCache element. 

2031 self._write_num_cache(data) 

2032 elif ref_type == "str": 

2033 # Write the c:strCache element. 

2034 self._write_str_cache(data) 

2035 

2036 self._xml_end_tag("c:numRef") 

2037 

2038 def _write_str_ref(self, formula, data, ref_type): 

2039 # Write the <c:strRef> element. 

2040 

2041 self._xml_start_tag("c:strRef") 

2042 

2043 # Write the c:f element. 

2044 self._write_series_formula(formula) 

2045 

2046 if ref_type == "num": 

2047 # Write the c:numCache element. 

2048 self._write_num_cache(data) 

2049 elif ref_type == "str": 

2050 # Write the c:strCache element. 

2051 self._write_str_cache(data) 

2052 

2053 self._xml_end_tag("c:strRef") 

2054 

2055 def _write_multi_lvl_str_ref(self, formula, data): 

2056 # Write the <c:multiLvlStrRef> element. 

2057 

2058 if not data: 

2059 return 

2060 

2061 self._xml_start_tag("c:multiLvlStrRef") 

2062 

2063 # Write the c:f element. 

2064 self._write_series_formula(formula) 

2065 

2066 self._xml_start_tag("c:multiLvlStrCache") 

2067 

2068 # Write the c:ptCount element. 

2069 count = len(data[-1]) 

2070 self._write_pt_count(count) 

2071 

2072 for cat_data in reversed(data): 

2073 self._xml_start_tag("c:lvl") 

2074 

2075 for i, point in enumerate(cat_data): 

2076 # Write the c:pt element. 

2077 self._write_pt(i, point) 

2078 

2079 self._xml_end_tag("c:lvl") 

2080 

2081 self._xml_end_tag("c:multiLvlStrCache") 

2082 self._xml_end_tag("c:multiLvlStrRef") 

2083 

2084 def _write_series_formula(self, formula): 

2085 # Write the <c:f> element. 

2086 

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

2088 if formula.startswith("="): 

2089 formula = formula.lstrip("=") 

2090 

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

2092 

2093 def _write_axis_ids(self, args): 

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

2095 

2096 # Generate the axis ids. 

2097 self._add_axis_ids(args) 

2098 

2099 if args["primary_axes"]: 

2100 # Write the axis ids for the primary axes. 

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

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

2103 else: 

2104 # Write the axis ids for the secondary axes. 

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

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

2107 

2108 def _write_axis_id(self, val): 

2109 # Write the <c:axId> element. 

2110 

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

2112 

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

2114 

2115 def _write_cat_axis(self, args): 

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

2117 x_axis = args["x_axis"] 

2118 y_axis = args["y_axis"] 

2119 axis_ids = args["axis_ids"] 

2120 

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

2122 if axis_ids is None or not axis_ids: 

2123 return 

2124 

2125 position = self.cat_axis_position 

2126 is_y_axis = self.horiz_cat_axis 

2127 

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

2129 if x_axis.get("position"): 

2130 position = x_axis["position"] 

2131 

2132 self._xml_start_tag("c:catAx") 

2133 

2134 self._write_axis_id(axis_ids[0]) 

2135 

2136 # Write the c:scaling element. 

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

2138 

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

2140 self._write_delete(1) 

2141 

2142 # Write the c:axPos element. 

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

2144 

2145 # Write the c:majorGridlines element. 

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

2147 

2148 # Write the c:minorGridlines element. 

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

2150 

2151 # Write the axis title elements. 

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

2153 self._write_title_formula( 

2154 x_axis["formula"], 

2155 x_axis["data_id"], 

2156 is_y_axis, 

2157 x_axis["name_font"], 

2158 x_axis["name_layout"], 

2159 ) 

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

2161 self._write_title_rich( 

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

2163 ) 

2164 

2165 # Write the c:numFmt element. 

2166 self._write_cat_number_format(x_axis) 

2167 

2168 # Write the c:majorTickMark element. 

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

2170 

2171 # Write the c:minorTickMark element. 

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

2173 

2174 # Write the c:tickLblPos element. 

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

2176 

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

2178 self._write_sp_pr(x_axis) 

2179 

2180 # Write the axis font elements. 

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

2182 

2183 # Write the c:crossAx element. 

2184 self._write_cross_axis(axis_ids[1]) 

2185 

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

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

2188 if ( 

2189 y_axis.get("crossing") is None 

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

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

2192 ): 

2193 # Write the c:crosses element. 

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

2195 else: 

2196 # Write the c:crossesAt element. 

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

2198 

2199 # Write the c:auto element. 

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

2201 self._write_auto(1) 

2202 

2203 # Write the c:labelAlign element. 

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

2205 

2206 # Write the c:labelOffset element. 

2207 self._write_label_offset(100) 

2208 

2209 # Write the c:tickLblSkip element. 

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

2211 

2212 # Write the c:tickMarkSkip element. 

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

2214 

2215 self._xml_end_tag("c:catAx") 

2216 

2217 def _write_val_axis(self, args): 

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

2219 x_axis = args["x_axis"] 

2220 y_axis = args["y_axis"] 

2221 axis_ids = args["axis_ids"] 

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

2223 is_y_axis = self.horiz_val_axis 

2224 

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

2226 if axis_ids is None or not axis_ids: 

2227 return 

2228 

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

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

2231 

2232 self._xml_start_tag("c:valAx") 

2233 

2234 self._write_axis_id(axis_ids[1]) 

2235 

2236 # Write the c:scaling element. 

2237 self._write_scaling( 

2238 y_axis.get("reverse"), 

2239 y_axis.get("min"), 

2240 y_axis.get("max"), 

2241 y_axis.get("log_base"), 

2242 ) 

2243 

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

2245 self._write_delete(1) 

2246 

2247 # Write the c:axPos element. 

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

2249 

2250 # Write the c:majorGridlines element. 

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

2252 

2253 # Write the c:minorGridlines element. 

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

2255 

2256 # Write the axis title elements. 

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

2258 self._write_title_formula( 

2259 y_axis["formula"], 

2260 y_axis["data_id"], 

2261 is_y_axis, 

2262 y_axis["name_font"], 

2263 y_axis["name_layout"], 

2264 ) 

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

2266 self._write_title_rich( 

2267 y_axis["name"], 

2268 is_y_axis, 

2269 y_axis.get("name_font"), 

2270 y_axis.get("name_layout"), 

2271 ) 

2272 

2273 # Write the c:numberFormat element. 

2274 self._write_number_format(y_axis) 

2275 

2276 # Write the c:majorTickMark element. 

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

2278 

2279 # Write the c:minorTickMark element. 

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

2281 

2282 # Write the c:tickLblPos element. 

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

2284 

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

2286 self._write_sp_pr(y_axis) 

2287 

2288 # Write the axis font elements. 

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

2290 

2291 # Write the c:crossAx element. 

2292 self._write_cross_axis(axis_ids[0]) 

2293 

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

2295 if ( 

2296 x_axis.get("crossing") is None 

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

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

2299 ): 

2300 # Write the c:crosses element. 

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

2302 else: 

2303 # Write the c:crossesAt element. 

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

2305 

2306 # Write the c:crossBetween element. 

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

2308 

2309 # Write the c:majorUnit element. 

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

2311 

2312 # Write the c:minorUnit element. 

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

2314 

2315 # Write the c:dispUnits element. 

2316 self._write_disp_units( 

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

2318 ) 

2319 

2320 self._xml_end_tag("c:valAx") 

2321 

2322 def _write_cat_val_axis(self, args): 

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

2324 # in scatter plots. Usually the X axis. 

2325 x_axis = args["x_axis"] 

2326 y_axis = args["y_axis"] 

2327 axis_ids = args["axis_ids"] 

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

2329 is_y_axis = self.horiz_val_axis 

2330 

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

2332 if axis_ids is None or not axis_ids: 

2333 return 

2334 

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

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

2337 

2338 self._xml_start_tag("c:valAx") 

2339 

2340 self._write_axis_id(axis_ids[0]) 

2341 

2342 # Write the c:scaling element. 

2343 self._write_scaling( 

2344 x_axis.get("reverse"), 

2345 x_axis.get("min"), 

2346 x_axis.get("max"), 

2347 x_axis.get("log_base"), 

2348 ) 

2349 

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

2351 self._write_delete(1) 

2352 

2353 # Write the c:axPos element. 

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

2355 

2356 # Write the c:majorGridlines element. 

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

2358 

2359 # Write the c:minorGridlines element. 

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

2361 

2362 # Write the axis title elements. 

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

2364 self._write_title_formula( 

2365 x_axis["formula"], 

2366 x_axis["data_id"], 

2367 is_y_axis, 

2368 x_axis["name_font"], 

2369 x_axis["name_layout"], 

2370 ) 

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

2372 self._write_title_rich( 

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

2374 ) 

2375 

2376 # Write the c:numberFormat element. 

2377 self._write_number_format(x_axis) 

2378 

2379 # Write the c:majorTickMark element. 

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

2381 

2382 # Write the c:minorTickMark element. 

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

2384 

2385 # Write the c:tickLblPos element. 

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

2387 

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

2389 self._write_sp_pr(x_axis) 

2390 

2391 # Write the axis font elements. 

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

2393 

2394 # Write the c:crossAx element. 

2395 self._write_cross_axis(axis_ids[1]) 

2396 

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

2398 if ( 

2399 y_axis.get("crossing") is None 

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

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

2402 ): 

2403 # Write the c:crosses element. 

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

2405 else: 

2406 # Write the c:crossesAt element. 

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

2408 

2409 # Write the c:crossBetween element. 

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

2411 

2412 # Write the c:majorUnit element. 

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

2414 

2415 # Write the c:minorUnit element. 

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

2417 

2418 # Write the c:dispUnits element. 

2419 self._write_disp_units( 

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

2421 ) 

2422 

2423 self._xml_end_tag("c:valAx") 

2424 

2425 def _write_date_axis(self, args): 

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

2427 x_axis = args["x_axis"] 

2428 y_axis = args["y_axis"] 

2429 axis_ids = args["axis_ids"] 

2430 

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

2432 if axis_ids is None or not axis_ids: 

2433 return 

2434 

2435 position = self.cat_axis_position 

2436 

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

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

2439 

2440 self._xml_start_tag("c:dateAx") 

2441 

2442 self._write_axis_id(axis_ids[0]) 

2443 

2444 # Write the c:scaling element. 

2445 self._write_scaling( 

2446 x_axis.get("reverse"), 

2447 x_axis.get("min"), 

2448 x_axis.get("max"), 

2449 x_axis.get("log_base"), 

2450 ) 

2451 

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

2453 self._write_delete(1) 

2454 

2455 # Write the c:axPos element. 

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

2457 

2458 # Write the c:majorGridlines element. 

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

2460 

2461 # Write the c:minorGridlines element. 

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

2463 

2464 # Write the axis title elements. 

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

2466 self._write_title_formula( 

2467 x_axis["formula"], 

2468 x_axis["data_id"], 

2469 None, 

2470 x_axis["name_font"], 

2471 x_axis["name_layout"], 

2472 ) 

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

2474 self._write_title_rich( 

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

2476 ) 

2477 

2478 # Write the c:numFmt element. 

2479 self._write_number_format(x_axis) 

2480 

2481 # Write the c:majorTickMark element. 

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

2483 

2484 # Write the c:minorTickMark element. 

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

2486 

2487 # Write the c:tickLblPos element. 

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

2489 

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

2491 self._write_sp_pr(x_axis) 

2492 

2493 # Write the axis font elements. 

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

2495 

2496 # Write the c:crossAx element. 

2497 self._write_cross_axis(axis_ids[1]) 

2498 

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

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

2501 if ( 

2502 y_axis.get("crossing") is None 

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

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

2505 ): 

2506 # Write the c:crosses element. 

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

2508 else: 

2509 # Write the c:crossesAt element. 

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

2511 

2512 # Write the c:auto element. 

2513 self._write_auto(1) 

2514 

2515 # Write the c:labelOffset element. 

2516 self._write_label_offset(100) 

2517 

2518 # Write the c:tickLblSkip element. 

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

2520 

2521 # Write the c:tickMarkSkip element. 

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

2523 

2524 # Write the c:majorUnit element. 

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

2526 

2527 # Write the c:majorTimeUnit element. 

2528 if x_axis.get("major_unit"): 

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

2530 

2531 # Write the c:minorUnit element. 

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

2533 

2534 # Write the c:minorTimeUnit element. 

2535 if x_axis.get("minor_unit"): 

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

2537 

2538 self._xml_end_tag("c:dateAx") 

2539 

2540 def _write_scaling(self, reverse, min_val, max_val, log_base): 

2541 # Write the <c:scaling> element. 

2542 

2543 self._xml_start_tag("c:scaling") 

2544 

2545 # Write the c:logBase element. 

2546 self._write_c_log_base(log_base) 

2547 

2548 # Write the c:orientation element. 

2549 self._write_orientation(reverse) 

2550 

2551 # Write the c:max element. 

2552 self._write_c_max(max_val) 

2553 

2554 # Write the c:min element. 

2555 self._write_c_min(min_val) 

2556 

2557 self._xml_end_tag("c:scaling") 

2558 

2559 def _write_c_log_base(self, val): 

2560 # Write the <c:logBase> element. 

2561 

2562 if not val: 

2563 return 

2564 

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

2566 

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

2568 

2569 def _write_orientation(self, reverse): 

2570 # Write the <c:orientation> element. 

2571 val = "minMax" 

2572 

2573 if reverse: 

2574 val = "maxMin" 

2575 

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

2577 

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

2579 

2580 def _write_c_max(self, max_val): 

2581 # Write the <c:max> element. 

2582 

2583 if max_val is None: 

2584 return 

2585 

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

2587 

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

2589 

2590 def _write_c_min(self, min_val): 

2591 # Write the <c:min> element. 

2592 

2593 if min_val is None: 

2594 return 

2595 

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

2597 

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

2599 

2600 def _write_axis_pos(self, val, reverse): 

2601 # Write the <c:axPos> element. 

2602 

2603 if reverse: 

2604 if val == "l": 

2605 val = "r" 

2606 if val == "b": 

2607 val = "t" 

2608 

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

2610 

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

2612 

2613 def _write_number_format(self, axis): 

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

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

2616 # the sourceLinked attribute is 0. 

2617 # The user can override this if required. 

2618 format_code = axis.get("num_format") 

2619 source_linked = 1 

2620 

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

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

2623 source_linked = 0 

2624 

2625 # User override of sourceLinked. 

2626 if axis.get("num_format_linked"): 

2627 source_linked = 1 

2628 

2629 attributes = [ 

2630 ("formatCode", format_code), 

2631 ("sourceLinked", source_linked), 

2632 ] 

2633 

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

2635 

2636 def _write_cat_number_format(self, axis): 

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

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

2639 format_code = axis.get("num_format") 

2640 source_linked = 1 

2641 default_format = 1 

2642 

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

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

2645 source_linked = 0 

2646 default_format = 0 

2647 

2648 # User override of sourceLinked. 

2649 if axis.get("num_format_linked"): 

2650 source_linked = 1 

2651 

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

2653 if not self.cat_has_num_fmt and default_format: 

2654 return 

2655 

2656 attributes = [ 

2657 ("formatCode", format_code), 

2658 ("sourceLinked", source_linked), 

2659 ] 

2660 

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

2662 

2663 def _write_data_label_number_format(self, format_code): 

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

2665 source_linked = 0 

2666 

2667 attributes = [ 

2668 ("formatCode", format_code), 

2669 ("sourceLinked", source_linked), 

2670 ] 

2671 

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

2673 

2674 def _write_major_tick_mark(self, val): 

2675 # Write the <c:majorTickMark> element. 

2676 

2677 if not val: 

2678 return 

2679 

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

2681 

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

2683 

2684 def _write_minor_tick_mark(self, val): 

2685 # Write the <c:minorTickMark> element. 

2686 

2687 if not val: 

2688 return 

2689 

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

2691 

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

2693 

2694 def _write_tick_label_pos(self, val=None): 

2695 # Write the <c:tickLblPos> element. 

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

2697 val = "nextTo" 

2698 

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

2700 

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

2702 

2703 def _write_cross_axis(self, val): 

2704 # Write the <c:crossAx> element. 

2705 

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

2707 

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

2709 

2710 def _write_crosses(self, val=None): 

2711 # Write the <c:crosses> element. 

2712 if val is None: 

2713 val = "autoZero" 

2714 

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

2716 

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

2718 

2719 def _write_c_crosses_at(self, val): 

2720 # Write the <c:crossesAt> element. 

2721 

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

2723 

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

2725 

2726 def _write_auto(self, val): 

2727 # Write the <c:auto> element. 

2728 

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

2730 

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

2732 

2733 def _write_label_align(self, val=None): 

2734 # Write the <c:labelAlign> element. 

2735 

2736 if val is None: 

2737 val = "ctr" 

2738 

2739 if val == "right": 

2740 val = "r" 

2741 

2742 if val == "left": 

2743 val = "l" 

2744 

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

2746 

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

2748 

2749 def _write_label_offset(self, val): 

2750 # Write the <c:labelOffset> element. 

2751 

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

2753 

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

2755 

2756 def _write_c_tick_lbl_skip(self, val): 

2757 # Write the <c:tickLblSkip> element. 

2758 if val is None: 

2759 return 

2760 

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

2762 

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

2764 

2765 def _write_c_tick_mark_skip(self, val): 

2766 # Write the <c:tickMarkSkip> element. 

2767 if val is None: 

2768 return 

2769 

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

2771 

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

2773 

2774 def _write_major_gridlines(self, gridlines): 

2775 # Write the <c:majorGridlines> element. 

2776 

2777 if not gridlines: 

2778 return 

2779 

2780 if not gridlines["visible"]: 

2781 return 

2782 

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

2784 self._xml_start_tag("c:majorGridlines") 

2785 

2786 # Write the c:spPr element. 

2787 self._write_sp_pr(gridlines) 

2788 

2789 self._xml_end_tag("c:majorGridlines") 

2790 else: 

2791 self._xml_empty_tag("c:majorGridlines") 

2792 

2793 def _write_minor_gridlines(self, gridlines): 

2794 # Write the <c:minorGridlines> element. 

2795 

2796 if not gridlines: 

2797 return 

2798 

2799 if not gridlines["visible"]: 

2800 return 

2801 

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

2803 self._xml_start_tag("c:minorGridlines") 

2804 

2805 # Write the c:spPr element. 

2806 self._write_sp_pr(gridlines) 

2807 

2808 self._xml_end_tag("c:minorGridlines") 

2809 else: 

2810 self._xml_empty_tag("c:minorGridlines") 

2811 

2812 def _write_cross_between(self, val): 

2813 # Write the <c:crossBetween> element. 

2814 if val is None: 

2815 val = self.cross_between 

2816 

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

2818 

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

2820 

2821 def _write_c_major_unit(self, val): 

2822 # Write the <c:majorUnit> element. 

2823 

2824 if not val: 

2825 return 

2826 

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

2828 

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

2830 

2831 def _write_c_minor_unit(self, val): 

2832 # Write the <c:minorUnit> element. 

2833 

2834 if not val: 

2835 return 

2836 

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

2838 

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

2840 

2841 def _write_c_major_time_unit(self, val=None): 

2842 # Write the <c:majorTimeUnit> element. 

2843 if val is None: 

2844 val = "days" 

2845 

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

2847 

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

2849 

2850 def _write_c_minor_time_unit(self, val=None): 

2851 # Write the <c:minorTimeUnit> element. 

2852 if val is None: 

2853 val = "days" 

2854 

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

2856 

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

2858 

2859 def _write_legend(self): 

2860 # Write the <c:legend> element. 

2861 legend = self.legend 

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

2863 font = legend.get("font") 

2864 delete_series = [] 

2865 overlay = 0 

2866 

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

2868 delete_series = legend["delete_series"] 

2869 

2870 if position.startswith("overlay_"): 

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

2872 overlay = 1 

2873 

2874 allowed = { 

2875 "right": "r", 

2876 "left": "l", 

2877 "top": "t", 

2878 "bottom": "b", 

2879 "top_right": "tr", 

2880 } 

2881 

2882 if position == "none": 

2883 return 

2884 

2885 if position not in allowed: 

2886 return 

2887 

2888 position = allowed[position] 

2889 

2890 self._xml_start_tag("c:legend") 

2891 

2892 # Write the c:legendPos element. 

2893 self._write_legend_pos(position) 

2894 

2895 # Remove series labels from the legend. 

2896 for index in delete_series: 

2897 # Write the c:legendEntry element. 

2898 self._write_legend_entry(index) 

2899 

2900 # Write the c:layout element. 

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

2902 

2903 # Write the c:overlay element. 

2904 if overlay: 

2905 self._write_overlay() 

2906 

2907 if font: 

2908 self._write_tx_pr(font) 

2909 

2910 # Write the c:spPr element. 

2911 self._write_sp_pr(legend) 

2912 

2913 self._xml_end_tag("c:legend") 

2914 

2915 def _write_legend_pos(self, val): 

2916 # Write the <c:legendPos> element. 

2917 

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

2919 

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

2921 

2922 def _write_legend_entry(self, index): 

2923 # Write the <c:legendEntry> element. 

2924 

2925 self._xml_start_tag("c:legendEntry") 

2926 

2927 # Write the c:idx element. 

2928 self._write_idx(index) 

2929 

2930 # Write the c:delete element. 

2931 self._write_delete(1) 

2932 

2933 self._xml_end_tag("c:legendEntry") 

2934 

2935 def _write_overlay(self): 

2936 # Write the <c:overlay> element. 

2937 val = 1 

2938 

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

2940 

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

2942 

2943 def _write_plot_vis_only(self): 

2944 # Write the <c:plotVisOnly> element. 

2945 val = 1 

2946 

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

2948 if self.show_hidden: 

2949 return 

2950 

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

2952 

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

2954 

2955 def _write_print_settings(self): 

2956 # Write the <c:printSettings> element. 

2957 self._xml_start_tag("c:printSettings") 

2958 

2959 # Write the c:headerFooter element. 

2960 self._write_header_footer() 

2961 

2962 # Write the c:pageMargins element. 

2963 self._write_page_margins() 

2964 

2965 # Write the c:pageSetup element. 

2966 self._write_page_setup() 

2967 

2968 self._xml_end_tag("c:printSettings") 

2969 

2970 def _write_header_footer(self): 

2971 # Write the <c:headerFooter> element. 

2972 self._xml_empty_tag("c:headerFooter") 

2973 

2974 def _write_page_margins(self): 

2975 # Write the <c:pageMargins> element. 

2976 bottom = 0.75 

2977 left = 0.7 

2978 right = 0.7 

2979 top = 0.75 

2980 header = 0.3 

2981 footer = 0.3 

2982 

2983 attributes = [ 

2984 ("b", bottom), 

2985 ("l", left), 

2986 ("r", right), 

2987 ("t", top), 

2988 ("header", header), 

2989 ("footer", footer), 

2990 ] 

2991 

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

2993 

2994 def _write_page_setup(self): 

2995 # Write the <c:pageSetup> element. 

2996 self._xml_empty_tag("c:pageSetup") 

2997 

2998 def _write_c_auto_title_deleted(self): 

2999 # Write the <c:autoTitleDeleted> element. 

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

3001 

3002 def _write_title_rich(self, title, is_y_axis, font, layout, overlay=False): 

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

3004 

3005 self._xml_start_tag("c:title") 

3006 

3007 # Write the c:tx element. 

3008 self._write_tx_rich(title, is_y_axis, font) 

3009 

3010 # Write the c:layout element. 

3011 self._write_layout(layout, "text") 

3012 

3013 # Write the c:overlay element. 

3014 if overlay: 

3015 self._write_overlay() 

3016 

3017 self._xml_end_tag("c:title") 

3018 

3019 def _write_title_formula( 

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

3021 ): 

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

3023 

3024 self._xml_start_tag("c:title") 

3025 

3026 # Write the c:tx element. 

3027 self._write_tx_formula(title, data_id) 

3028 

3029 # Write the c:layout element. 

3030 self._write_layout(layout, "text") 

3031 

3032 # Write the c:overlay element. 

3033 if overlay: 

3034 self._write_overlay() 

3035 

3036 # Write the c:txPr element. 

3037 self._write_tx_pr(font, is_y_axis) 

3038 

3039 self._xml_end_tag("c:title") 

3040 

3041 def _write_tx_rich(self, title, is_y_axis, font): 

3042 # Write the <c:tx> element. 

3043 

3044 self._xml_start_tag("c:tx") 

3045 

3046 # Write the c:rich element. 

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

3048 

3049 self._xml_end_tag("c:tx") 

3050 

3051 def _write_tx_value(self, title): 

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

3053 

3054 self._xml_start_tag("c:tx") 

3055 

3056 # Write the c:v element. 

3057 self._write_v(title) 

3058 

3059 self._xml_end_tag("c:tx") 

3060 

3061 def _write_tx_formula(self, title, data_id): 

3062 # Write the <c:tx> element. 

3063 data = None 

3064 

3065 if data_id is not None: 

3066 data = self.formula_data[data_id] 

3067 

3068 self._xml_start_tag("c:tx") 

3069 

3070 # Write the c:strRef element. 

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

3072 

3073 self._xml_end_tag("c:tx") 

3074 

3075 def _write_rich(self, title, font, is_y_axis, ignore_rich_pr): 

3076 # Write the <c:rich> element. 

3077 

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

3079 rotation = font["rotation"] 

3080 else: 

3081 rotation = None 

3082 

3083 self._xml_start_tag("c:rich") 

3084 

3085 # Write the a:bodyPr element. 

3086 self._write_a_body_pr(rotation, is_y_axis) 

3087 

3088 # Write the a:lstStyle element. 

3089 self._write_a_lst_style() 

3090 

3091 # Write the a:p element. 

3092 self._write_a_p_rich(title, font, ignore_rich_pr) 

3093 

3094 self._xml_end_tag("c:rich") 

3095 

3096 def _write_a_body_pr(self, rotation, is_y_axis): 

3097 # Write the <a:bodyPr> element. 

3098 attributes = [] 

3099 

3100 if rotation is None and is_y_axis: 

3101 rotation = -5400000 

3102 

3103 if rotation is not None: 

3104 if rotation == 16200000: 

3105 # 270 deg/stacked angle. 

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

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

3108 elif rotation == 16260000: 

3109 # 271 deg/East Asian vertical. 

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

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

3112 else: 

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

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

3115 

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

3117 

3118 def _write_a_lst_style(self): 

3119 # Write the <a:lstStyle> element. 

3120 self._xml_empty_tag("a:lstStyle") 

3121 

3122 def _write_a_p_rich(self, title, font, ignore_rich_pr): 

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

3124 

3125 self._xml_start_tag("a:p") 

3126 

3127 # Write the a:pPr element. 

3128 if not ignore_rich_pr: 

3129 self._write_a_p_pr_rich(font) 

3130 

3131 # Write the a:r element. 

3132 self._write_a_r(title, font) 

3133 

3134 self._xml_end_tag("a:p") 

3135 

3136 def _write_a_p_formula(self, font): 

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

3138 

3139 self._xml_start_tag("a:p") 

3140 

3141 # Write the a:pPr element. 

3142 self._write_a_p_pr_rich(font) 

3143 

3144 # Write the a:endParaRPr element. 

3145 self._write_a_end_para_rpr() 

3146 

3147 self._xml_end_tag("a:p") 

3148 

3149 def _write_a_p_pr_rich(self, font): 

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

3151 

3152 self._xml_start_tag("a:pPr") 

3153 

3154 # Write the a:defRPr element. 

3155 self._write_a_def_rpr(font) 

3156 

3157 self._xml_end_tag("a:pPr") 

3158 

3159 def _write_a_def_rpr(self, font): 

3160 # Write the <a:defRPr> element. 

3161 has_color = False 

3162 

3163 style_attributes = Shape._get_font_style_attributes(font) 

3164 latin_attributes = Shape._get_font_latin_attributes(font) 

3165 

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

3167 has_color = True 

3168 

3169 if latin_attributes or has_color: 

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

3171 

3172 if has_color: 

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

3174 

3175 if latin_attributes: 

3176 self._write_a_latin(latin_attributes) 

3177 

3178 self._xml_end_tag("a:defRPr") 

3179 else: 

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

3181 

3182 def _write_a_end_para_rpr(self): 

3183 # Write the <a:endParaRPr> element. 

3184 lang = "en-US" 

3185 

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

3187 

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

3189 

3190 def _write_a_r(self, title, font): 

3191 # Write the <a:r> element. 

3192 

3193 self._xml_start_tag("a:r") 

3194 

3195 # Write the a:rPr element. 

3196 self._write_a_r_pr(font) 

3197 

3198 # Write the a:t element. 

3199 self._write_a_t(title) 

3200 

3201 self._xml_end_tag("a:r") 

3202 

3203 def _write_a_r_pr(self, font): 

3204 # Write the <a:rPr> element. 

3205 has_color = False 

3206 lang = "en-US" 

3207 

3208 style_attributes = Shape._get_font_style_attributes(font) 

3209 latin_attributes = Shape._get_font_latin_attributes(font) 

3210 

3211 if font and font["color"]: 

3212 has_color = True 

3213 

3214 # Add the lang type to the attributes. 

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

3216 

3217 if latin_attributes or has_color: 

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

3219 

3220 if has_color: 

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

3222 

3223 if latin_attributes: 

3224 self._write_a_latin(latin_attributes) 

3225 

3226 self._xml_end_tag("a:rPr") 

3227 else: 

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

3229 

3230 def _write_a_t(self, title): 

3231 # Write the <a:t> element. 

3232 

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

3234 

3235 def _write_tx_pr(self, font, is_y_axis=False): 

3236 # Write the <c:txPr> element. 

3237 

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

3239 rotation = font["rotation"] 

3240 else: 

3241 rotation = None 

3242 

3243 self._xml_start_tag("c:txPr") 

3244 

3245 # Write the a:bodyPr element. 

3246 self._write_a_body_pr(rotation, is_y_axis) 

3247 

3248 # Write the a:lstStyle element. 

3249 self._write_a_lst_style() 

3250 

3251 # Write the a:p element. 

3252 self._write_a_p_formula(font) 

3253 

3254 self._xml_end_tag("c:txPr") 

3255 

3256 def _write_marker(self, marker): 

3257 # Write the <c:marker> element. 

3258 if marker is None: 

3259 marker = self.default_marker 

3260 

3261 if not marker: 

3262 return 

3263 

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

3265 return 

3266 

3267 self._xml_start_tag("c:marker") 

3268 

3269 # Write the c:symbol element. 

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

3271 

3272 # Write the c:size element. 

3273 if marker.get("size"): 

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

3275 

3276 # Write the c:spPr element. 

3277 self._write_sp_pr(marker) 

3278 

3279 self._xml_end_tag("c:marker") 

3280 

3281 def _write_marker_size(self, val): 

3282 # Write the <c:size> element. 

3283 

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

3285 

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

3287 

3288 def _write_symbol(self, val): 

3289 # Write the <c:symbol> element. 

3290 

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

3292 

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

3294 

3295 def _write_sp_pr(self, series): 

3296 # Write the <c:spPr> element. 

3297 

3298 if not self._has_fill_formatting(series): 

3299 return 

3300 

3301 self._xml_start_tag("c:spPr") 

3302 

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

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

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

3306 # Write the a:noFill element. 

3307 self._write_a_no_fill() 

3308 else: 

3309 # Write the a:solidFill element. 

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

3311 

3312 if series.get("pattern"): 

3313 # Write the a:gradFill element. 

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

3315 

3316 if series.get("gradient"): 

3317 # Write the a:gradFill element. 

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

3319 

3320 # Write the a:ln element. 

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

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

3323 

3324 self._xml_end_tag("c:spPr") 

3325 

3326 def _write_a_ln(self, line): 

3327 # Write the <a:ln> element. 

3328 attributes = [] 

3329 

3330 # Add the line width as an attribute. 

3331 width = line.get("width") 

3332 

3333 if width is not None: 

3334 # Round width to nearest 0.25, like Excel. 

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

3336 

3337 # Convert to internal units. 

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

3339 

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

3341 

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

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

3344 

3345 # Write the line fill. 

3346 if "none" in line: 

3347 # Write the a:noFill element. 

3348 self._write_a_no_fill() 

3349 elif "color" in line: 

3350 # Write the a:solidFill element. 

3351 self._write_a_solid_fill(line) 

3352 

3353 # Write the line/dash type. 

3354 line_type = line.get("dash_type") 

3355 if line_type: 

3356 # Write the a:prstDash element. 

3357 self._write_a_prst_dash(line_type) 

3358 

3359 self._xml_end_tag("a:ln") 

3360 else: 

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

3362 

3363 def _write_a_no_fill(self): 

3364 # Write the <a:noFill> element. 

3365 self._xml_empty_tag("a:noFill") 

3366 

3367 def _write_a_solid_fill(self, fill): 

3368 # Write the <a:solidFill> element. 

3369 

3370 self._xml_start_tag("a:solidFill") 

3371 

3372 if fill.get("color"): 

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

3374 

3375 self._xml_end_tag("a:solidFill") 

3376 

3377 def _write_color(self, color: Color, transparency=None): 

3378 # Write the appropriate chart color element. 

3379 

3380 if not color: 

3381 return 

3382 

3383 if color._is_automatic: 

3384 # Write the a:sysClr element. 

3385 self._write_a_sys_clr() 

3386 elif color._type == ColorTypes.RGB: 

3387 # Write the a:srgbClr element. 

3388 self._write_a_srgb_clr(color, transparency) 

3389 elif color._type == ColorTypes.THEME: 

3390 self._write_a_scheme_clr(color, transparency) 

3391 

3392 def _write_a_sys_clr(self): 

3393 # Write the <a:sysClr> element. 

3394 

3395 val = "window" 

3396 last_clr = "FFFFFF" 

3397 

3398 attributes = [ 

3399 ("val", val), 

3400 ("lastClr", last_clr), 

3401 ] 

3402 

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

3404 

3405 def _write_a_srgb_clr(self, color: Color, transparency=None): 

3406 # Write the <a:srgbClr> element. 

3407 

3408 if not color: 

3409 return 

3410 

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

3412 

3413 if transparency: 

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

3415 

3416 # Write the a:alpha element. 

3417 self._write_a_alpha(transparency) 

3418 

3419 self._xml_end_tag("a:srgbClr") 

3420 else: 

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

3422 

3423 def _write_a_scheme_clr(self, color: Color, transparency=None): 

3424 # Write the <a:schemeClr> element. 

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

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

3427 

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

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

3430 

3431 if lum_mod > 0: 

3432 # Write the a:lumMod element. 

3433 self._write_a_lum_mod(lum_mod) 

3434 

3435 if lum_off > 0: 

3436 # Write the a:lumOff element. 

3437 self._write_a_lum_off(lum_off) 

3438 

3439 if transparency: 

3440 # Write the a:alpha element. 

3441 self._write_a_alpha(transparency) 

3442 

3443 self._xml_end_tag("a:schemeClr") 

3444 else: 

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

3446 

3447 def _write_a_lum_mod(self, value: int): 

3448 # Write the <a:lumMod> element. 

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

3450 

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

3452 

3453 def _write_a_lum_off(self, value: int): 

3454 # Write the <a:lumOff> element. 

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

3456 

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

3458 

3459 def _write_a_alpha(self, val): 

3460 # Write the <a:alpha> element. 

3461 

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

3463 

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

3465 

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

3467 

3468 def _write_a_prst_dash(self, val): 

3469 # Write the <a:prstDash> element. 

3470 

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

3472 

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

3474 

3475 def _write_trendline(self, trendline): 

3476 # Write the <c:trendline> element. 

3477 

3478 if not trendline: 

3479 return 

3480 

3481 self._xml_start_tag("c:trendline") 

3482 

3483 # Write the c:name element. 

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

3485 

3486 # Write the c:spPr element. 

3487 self._write_sp_pr(trendline) 

3488 

3489 # Write the c:trendlineType element. 

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

3491 

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

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

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

3495 

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

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

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

3499 

3500 # Write the c:forward element. 

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

3502 

3503 # Write the c:backward element. 

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

3505 

3506 if "intercept" in trendline: 

3507 # Write the c:intercept element. 

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

3509 

3510 if trendline.get("display_r_squared"): 

3511 # Write the c:dispRSqr element. 

3512 self._write_c_disp_rsqr() 

3513 

3514 if trendline.get("display_equation"): 

3515 # Write the c:dispEq element. 

3516 self._write_c_disp_eq() 

3517 

3518 # Write the c:trendlineLbl element. 

3519 self._write_c_trendline_lbl(trendline) 

3520 

3521 self._xml_end_tag("c:trendline") 

3522 

3523 def _write_trendline_type(self, val): 

3524 # Write the <c:trendlineType> element. 

3525 

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

3527 

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

3529 

3530 def _write_name(self, data): 

3531 # Write the <c:name> element. 

3532 

3533 if data is None: 

3534 return 

3535 

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

3537 

3538 def _write_trendline_order(self, val): 

3539 # Write the <c:order> element. 

3540 val = max(val, 2) 

3541 

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

3543 

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

3545 

3546 def _write_period(self, val): 

3547 # Write the <c:period> element. 

3548 val = max(val, 2) 

3549 

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

3551 

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

3553 

3554 def _write_forward(self, val): 

3555 # Write the <c:forward> element. 

3556 

3557 if not val: 

3558 return 

3559 

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

3561 

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

3563 

3564 def _write_backward(self, val): 

3565 # Write the <c:backward> element. 

3566 

3567 if not val: 

3568 return 

3569 

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

3571 

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

3573 

3574 def _write_c_intercept(self, val): 

3575 # Write the <c:intercept> element. 

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

3577 

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

3579 

3580 def _write_c_disp_eq(self): 

3581 # Write the <c:dispEq> element. 

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

3583 

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

3585 

3586 def _write_c_disp_rsqr(self): 

3587 # Write the <c:dispRSqr> element. 

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

3589 

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

3591 

3592 def _write_c_trendline_lbl(self, trendline): 

3593 # Write the <c:trendlineLbl> element. 

3594 self._xml_start_tag("c:trendlineLbl") 

3595 

3596 # Write the c:layout element. 

3597 self._write_layout(None, None) 

3598 

3599 # Write the c:numFmt element. 

3600 self._write_trendline_num_fmt() 

3601 

3602 # Write the c:spPr element. 

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

3604 

3605 # Write the data label font elements. 

3606 if trendline["label"]: 

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

3608 if font: 

3609 self._write_axis_font(font) 

3610 

3611 self._xml_end_tag("c:trendlineLbl") 

3612 

3613 def _write_trendline_num_fmt(self): 

3614 # Write the <c:numFmt> element. 

3615 attributes = [ 

3616 ("formatCode", "General"), 

3617 ("sourceLinked", 0), 

3618 ] 

3619 

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

3621 

3622 def _write_hi_low_lines(self): 

3623 # Write the <c:hiLowLines> element. 

3624 hi_low_lines = self.hi_low_lines 

3625 

3626 if hi_low_lines is None: 

3627 return 

3628 

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

3630 self._xml_start_tag("c:hiLowLines") 

3631 

3632 # Write the c:spPr element. 

3633 self._write_sp_pr(hi_low_lines) 

3634 

3635 self._xml_end_tag("c:hiLowLines") 

3636 else: 

3637 self._xml_empty_tag("c:hiLowLines") 

3638 

3639 def _write_drop_lines(self): 

3640 # Write the <c:dropLines> element. 

3641 drop_lines = self.drop_lines 

3642 

3643 if drop_lines is None: 

3644 return 

3645 

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

3647 self._xml_start_tag("c:dropLines") 

3648 

3649 # Write the c:spPr element. 

3650 self._write_sp_pr(drop_lines) 

3651 

3652 self._xml_end_tag("c:dropLines") 

3653 else: 

3654 self._xml_empty_tag("c:dropLines") 

3655 

3656 def _write_overlap(self, val): 

3657 # Write the <c:overlap> element. 

3658 

3659 if val is None: 

3660 return 

3661 

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

3663 

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

3665 

3666 def _write_num_cache(self, data): 

3667 # Write the <c:numCache> element. 

3668 if data: 

3669 count = len(data) 

3670 else: 

3671 count = 0 

3672 

3673 self._xml_start_tag("c:numCache") 

3674 

3675 # Write the c:formatCode element. 

3676 self._write_format_code("General") 

3677 

3678 # Write the c:ptCount element. 

3679 self._write_pt_count(count) 

3680 

3681 for i in range(count): 

3682 token = data[i] 

3683 

3684 if token is None: 

3685 continue 

3686 

3687 try: 

3688 float(token) 

3689 except ValueError: 

3690 # Write non-numeric data as 0. 

3691 token = 0 

3692 

3693 # Write the c:pt element. 

3694 self._write_pt(i, token) 

3695 

3696 self._xml_end_tag("c:numCache") 

3697 

3698 def _write_str_cache(self, data): 

3699 # Write the <c:strCache> element. 

3700 count = len(data) 

3701 

3702 self._xml_start_tag("c:strCache") 

3703 

3704 # Write the c:ptCount element. 

3705 self._write_pt_count(count) 

3706 

3707 for i in range(count): 

3708 # Write the c:pt element. 

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

3710 

3711 self._xml_end_tag("c:strCache") 

3712 

3713 def _write_format_code(self, data): 

3714 # Write the <c:formatCode> element. 

3715 

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

3717 

3718 def _write_pt_count(self, val): 

3719 # Write the <c:ptCount> element. 

3720 

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

3722 

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

3724 

3725 def _write_pt(self, idx, value): 

3726 # Write the <c:pt> element. 

3727 

3728 if value is None: 

3729 return 

3730 

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

3732 

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

3734 

3735 # Write the c:v element. 

3736 self._write_v(value) 

3737 

3738 self._xml_end_tag("c:pt") 

3739 

3740 def _write_v(self, data): 

3741 # Write the <c:v> element. 

3742 

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

3744 

3745 def _write_protection(self): 

3746 # Write the <c:protection> element. 

3747 if not self.protection: 

3748 return 

3749 

3750 self._xml_empty_tag("c:protection") 

3751 

3752 def _write_d_pt(self, points): 

3753 # Write the <c:dPt> elements. 

3754 index = -1 

3755 

3756 if not points: 

3757 return 

3758 

3759 for point in points: 

3760 index += 1 

3761 if not point: 

3762 continue 

3763 

3764 self._write_d_pt_point(index, point) 

3765 

3766 def _write_d_pt_point(self, index, point): 

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

3768 

3769 self._xml_start_tag("c:dPt") 

3770 

3771 # Write the c:idx element. 

3772 self._write_idx(index) 

3773 

3774 # Write the c:spPr element. 

3775 self._write_sp_pr(point) 

3776 

3777 self._xml_end_tag("c:dPt") 

3778 

3779 def _write_d_lbls(self, labels): 

3780 # Write the <c:dLbls> element. 

3781 

3782 if not labels: 

3783 return 

3784 

3785 self._xml_start_tag("c:dLbls") 

3786 

3787 # Write the custom c:dLbl elements. 

3788 if labels.get("custom"): 

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

3790 

3791 # Write the c:numFmt element. 

3792 if labels.get("num_format"): 

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

3794 

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

3796 self._write_sp_pr(labels) 

3797 

3798 # Write the data label font elements. 

3799 if labels.get("font"): 

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

3801 

3802 # Write the c:dLblPos element. 

3803 if labels.get("position"): 

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

3805 

3806 # Write the c:showLegendKey element. 

3807 if labels.get("legend_key"): 

3808 self._write_show_legend_key() 

3809 

3810 # Write the c:showVal element. 

3811 if labels.get("value"): 

3812 self._write_show_val() 

3813 

3814 # Write the c:showCatName element. 

3815 if labels.get("category"): 

3816 self._write_show_cat_name() 

3817 

3818 # Write the c:showSerName element. 

3819 if labels.get("series_name"): 

3820 self._write_show_ser_name() 

3821 

3822 # Write the c:showPercent element. 

3823 if labels.get("percentage"): 

3824 self._write_show_percent() 

3825 

3826 # Write the c:separator element. 

3827 if labels.get("separator"): 

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

3829 

3830 # Write the c:showLeaderLines element. 

3831 if labels.get("leader_lines"): 

3832 self._write_show_leader_lines() 

3833 

3834 self._xml_end_tag("c:dLbls") 

3835 

3836 def _write_custom_labels(self, parent, labels): 

3837 # Write the <c:showLegendKey> element. 

3838 index = 0 

3839 

3840 for label in labels: 

3841 index += 1 

3842 

3843 if label is None: 

3844 continue 

3845 

3846 self._xml_start_tag("c:dLbl") 

3847 

3848 # Write the c:idx element. 

3849 self._write_idx(index - 1) 

3850 

3851 delete_label = label.get("delete") 

3852 

3853 if delete_label: 

3854 self._write_delete(1) 

3855 

3856 elif label.get("formula"): 

3857 self._write_custom_label_formula(label) 

3858 

3859 if parent.get("position"): 

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

3861 

3862 if parent.get("value"): 

3863 self._write_show_val() 

3864 if parent.get("category"): 

3865 self._write_show_cat_name() 

3866 if parent.get("series_name"): 

3867 self._write_show_ser_name() 

3868 

3869 elif label.get("value"): 

3870 self._write_custom_label_str(label) 

3871 

3872 if parent.get("position"): 

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

3874 

3875 if parent.get("value"): 

3876 self._write_show_val() 

3877 if parent.get("category"): 

3878 self._write_show_cat_name() 

3879 if parent.get("series_name"): 

3880 self._write_show_ser_name() 

3881 else: 

3882 self._write_custom_label_format_only(label) 

3883 

3884 self._xml_end_tag("c:dLbl") 

3885 

3886 def _write_custom_label_str(self, label): 

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

3888 title = label.get("value") 

3889 font = label.get("font") 

3890 has_formatting = self._has_fill_formatting(label) 

3891 

3892 # Write the c:layout element. 

3893 self._write_layout(None, None) 

3894 

3895 self._xml_start_tag("c:tx") 

3896 

3897 # Write the c:rich element. 

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

3899 

3900 self._xml_end_tag("c:tx") 

3901 

3902 # Write the c:spPr element. 

3903 self._write_sp_pr(label) 

3904 

3905 def _write_custom_label_formula(self, label): 

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

3907 formula = label.get("formula") 

3908 data_id = label.get("data_id") 

3909 data = None 

3910 

3911 if data_id is not None: 

3912 data = self.formula_data[data_id] 

3913 

3914 # Write the c:layout element. 

3915 self._write_layout(None, None) 

3916 

3917 self._xml_start_tag("c:tx") 

3918 

3919 # Write the c:strRef element. 

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

3921 

3922 self._xml_end_tag("c:tx") 

3923 

3924 # Write the data label formatting, if any. 

3925 self._write_custom_label_format_only(label) 

3926 

3927 def _write_custom_label_format_only(self, label): 

3928 # Write parts of the <c:dLbl> labels with changed formatting. 

3929 font = label.get("font") 

3930 has_formatting = self._has_fill_formatting(label) 

3931 

3932 if has_formatting: 

3933 self._write_sp_pr(label) 

3934 self._write_tx_pr(font) 

3935 elif font: 

3936 self._xml_empty_tag("c:spPr") 

3937 self._write_tx_pr(font) 

3938 

3939 def _write_show_legend_key(self): 

3940 # Write the <c:showLegendKey> element. 

3941 val = "1" 

3942 

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

3944 

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

3946 

3947 def _write_show_val(self): 

3948 # Write the <c:showVal> element. 

3949 val = 1 

3950 

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

3952 

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

3954 

3955 def _write_show_cat_name(self): 

3956 # Write the <c:showCatName> element. 

3957 val = 1 

3958 

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

3960 

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

3962 

3963 def _write_show_ser_name(self): 

3964 # Write the <c:showSerName> element. 

3965 val = 1 

3966 

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

3968 

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

3970 

3971 def _write_show_percent(self): 

3972 # Write the <c:showPercent> element. 

3973 val = 1 

3974 

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

3976 

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

3978 

3979 def _write_separator(self, data): 

3980 # Write the <c:separator> element. 

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

3982 

3983 def _write_show_leader_lines(self): 

3984 # Write the <c:showLeaderLines> element. 

3985 # 

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

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

3988 # 

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

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

3991 

3992 attributes = [ 

3993 ("uri", uri), 

3994 ("xmlns:c15", xmlns_c_15), 

3995 ] 

3996 

3997 self._xml_start_tag("c:extLst") 

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

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

4000 self._xml_end_tag("c:ext") 

4001 self._xml_end_tag("c:extLst") 

4002 

4003 def _write_d_lbl_pos(self, val): 

4004 # Write the <c:dLblPos> element. 

4005 

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

4007 

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

4009 

4010 def _write_delete(self, val): 

4011 # Write the <c:delete> element. 

4012 

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

4014 

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

4016 

4017 def _write_c_invert_if_negative(self, invert): 

4018 # Write the <c:invertIfNegative> element. 

4019 val = 1 

4020 

4021 if not invert: 

4022 return 

4023 

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

4025 

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

4027 

4028 def _write_axis_font(self, font): 

4029 # Write the axis font elements. 

4030 

4031 if not font: 

4032 return 

4033 

4034 self._xml_start_tag("c:txPr") 

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

4036 self._write_a_lst_style() 

4037 self._xml_start_tag("a:p") 

4038 

4039 self._write_a_p_pr_rich(font) 

4040 

4041 self._write_a_end_para_rpr() 

4042 self._xml_end_tag("a:p") 

4043 self._xml_end_tag("c:txPr") 

4044 

4045 def _write_a_latin(self, attributes): 

4046 # Write the <a:latin> element. 

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

4048 

4049 def _write_d_table(self): 

4050 # Write the <c:dTable> element. 

4051 table = self.table 

4052 

4053 if not table: 

4054 return 

4055 

4056 self._xml_start_tag("c:dTable") 

4057 

4058 if table["horizontal"]: 

4059 # Write the c:showHorzBorder element. 

4060 self._write_show_horz_border() 

4061 

4062 if table["vertical"]: 

4063 # Write the c:showVertBorder element. 

4064 self._write_show_vert_border() 

4065 

4066 if table["outline"]: 

4067 # Write the c:showOutline element. 

4068 self._write_show_outline() 

4069 

4070 if table["show_keys"]: 

4071 # Write the c:showKeys element. 

4072 self._write_show_keys() 

4073 

4074 if table["font"]: 

4075 # Write the table font. 

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

4077 

4078 self._xml_end_tag("c:dTable") 

4079 

4080 def _write_show_horz_border(self): 

4081 # Write the <c:showHorzBorder> element. 

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

4083 

4084 self._xml_empty_tag("c:showHorzBorder", attributes) 

4085 

4086 def _write_show_vert_border(self): 

4087 # Write the <c:showVertBorder> element. 

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

4089 

4090 self._xml_empty_tag("c:showVertBorder", attributes) 

4091 

4092 def _write_show_outline(self): 

4093 # Write the <c:showOutline> element. 

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

4095 

4096 self._xml_empty_tag("c:showOutline", attributes) 

4097 

4098 def _write_show_keys(self): 

4099 # Write the <c:showKeys> element. 

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

4101 

4102 self._xml_empty_tag("c:showKeys", attributes) 

4103 

4104 def _write_error_bars(self, error_bars): 

4105 # Write the X and Y error bars. 

4106 

4107 if not error_bars: 

4108 return 

4109 

4110 if error_bars["x_error_bars"]: 

4111 self._write_err_bars("x", error_bars["x_error_bars"]) 

4112 

4113 if error_bars["y_error_bars"]: 

4114 self._write_err_bars("y", error_bars["y_error_bars"]) 

4115 

4116 def _write_err_bars(self, direction, error_bars): 

4117 # Write the <c:errBars> element. 

4118 

4119 if not error_bars: 

4120 return 

4121 

4122 self._xml_start_tag("c:errBars") 

4123 

4124 # Write the c:errDir element. 

4125 self._write_err_dir(direction) 

4126 

4127 # Write the c:errBarType element. 

4128 self._write_err_bar_type(error_bars["direction"]) 

4129 

4130 # Write the c:errValType element. 

4131 self._write_err_val_type(error_bars["type"]) 

4132 

4133 if not error_bars["endcap"]: 

4134 # Write the c:noEndCap element. 

4135 self._write_no_end_cap() 

4136 

4137 if error_bars["type"] == "stdErr": 

4138 # Don't need to write a c:errValType tag. 

4139 pass 

4140 elif error_bars["type"] == "cust": 

4141 # Write the custom error tags. 

4142 self._write_custom_error(error_bars) 

4143 else: 

4144 # Write the c:val element. 

4145 self._write_error_val(error_bars["value"]) 

4146 

4147 # Write the c:spPr element. 

4148 self._write_sp_pr(error_bars) 

4149 

4150 self._xml_end_tag("c:errBars") 

4151 

4152 def _write_err_dir(self, val): 

4153 # Write the <c:errDir> element. 

4154 

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

4156 

4157 self._xml_empty_tag("c:errDir", attributes) 

4158 

4159 def _write_err_bar_type(self, val): 

4160 # Write the <c:errBarType> element. 

4161 

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

4163 

4164 self._xml_empty_tag("c:errBarType", attributes) 

4165 

4166 def _write_err_val_type(self, val): 

4167 # Write the <c:errValType> element. 

4168 

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

4170 

4171 self._xml_empty_tag("c:errValType", attributes) 

4172 

4173 def _write_no_end_cap(self): 

4174 # Write the <c:noEndCap> element. 

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

4176 

4177 self._xml_empty_tag("c:noEndCap", attributes) 

4178 

4179 def _write_error_val(self, val): 

4180 # Write the <c:val> element for error bars. 

4181 

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

4183 

4184 self._xml_empty_tag("c:val", attributes) 

4185 

4186 def _write_custom_error(self, error_bars): 

4187 # Write the custom error bars tags. 

4188 

4189 if error_bars["plus_values"]: 

4190 # Write the c:plus element. 

4191 self._xml_start_tag("c:plus") 

4192 

4193 if isinstance(error_bars["plus_values"], list): 

4194 self._write_num_lit(error_bars["plus_values"]) 

4195 else: 

4196 self._write_num_ref( 

4197 error_bars["plus_values"], error_bars["plus_data"], "num" 

4198 ) 

4199 self._xml_end_tag("c:plus") 

4200 

4201 if error_bars["minus_values"]: 

4202 # Write the c:minus element. 

4203 self._xml_start_tag("c:minus") 

4204 

4205 if isinstance(error_bars["minus_values"], list): 

4206 self._write_num_lit(error_bars["minus_values"]) 

4207 else: 

4208 self._write_num_ref( 

4209 error_bars["minus_values"], error_bars["minus_data"], "num" 

4210 ) 

4211 self._xml_end_tag("c:minus") 

4212 

4213 def _write_num_lit(self, data): 

4214 # Write the <c:numLit> element for literal number list elements. 

4215 count = len(data) 

4216 

4217 # Write the c:numLit element. 

4218 self._xml_start_tag("c:numLit") 

4219 

4220 # Write the c:formatCode element. 

4221 self._write_format_code("General") 

4222 

4223 # Write the c:ptCount element. 

4224 self._write_pt_count(count) 

4225 

4226 for i in range(count): 

4227 token = data[i] 

4228 

4229 if token is None: 

4230 continue 

4231 

4232 try: 

4233 float(token) 

4234 except ValueError: 

4235 # Write non-numeric data as 0. 

4236 token = 0 

4237 

4238 # Write the c:pt element. 

4239 self._write_pt(i, token) 

4240 

4241 self._xml_end_tag("c:numLit") 

4242 

4243 def _write_up_down_bars(self): 

4244 # Write the <c:upDownBars> element. 

4245 up_down_bars = self.up_down_bars 

4246 

4247 if up_down_bars is None: 

4248 return 

4249 

4250 self._xml_start_tag("c:upDownBars") 

4251 

4252 # Write the c:gapWidth element. 

4253 self._write_gap_width(150) 

4254 

4255 # Write the c:upBars element. 

4256 self._write_up_bars(up_down_bars.get("up")) 

4257 

4258 # Write the c:downBars element. 

4259 self._write_down_bars(up_down_bars.get("down")) 

4260 

4261 self._xml_end_tag("c:upDownBars") 

4262 

4263 def _write_gap_width(self, val): 

4264 # Write the <c:gapWidth> element. 

4265 

4266 if val is None: 

4267 return 

4268 

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

4270 

4271 self._xml_empty_tag("c:gapWidth", attributes) 

4272 

4273 def _write_up_bars(self, bar_format): 

4274 # Write the <c:upBars> element. 

4275 

4276 if bar_format["line"] and bar_format["line"]["defined"]: 

4277 self._xml_start_tag("c:upBars") 

4278 

4279 # Write the c:spPr element. 

4280 self._write_sp_pr(bar_format) 

4281 

4282 self._xml_end_tag("c:upBars") 

4283 else: 

4284 self._xml_empty_tag("c:upBars") 

4285 

4286 def _write_down_bars(self, bar_format): 

4287 # Write the <c:downBars> element. 

4288 

4289 if bar_format["line"] and bar_format["line"]["defined"]: 

4290 self._xml_start_tag("c:downBars") 

4291 

4292 # Write the c:spPr element. 

4293 self._write_sp_pr(bar_format) 

4294 

4295 self._xml_end_tag("c:downBars") 

4296 else: 

4297 self._xml_empty_tag("c:downBars") 

4298 

4299 def _write_disp_units(self, units, display): 

4300 # Write the <c:dispUnits> element. 

4301 

4302 if not units: 

4303 return 

4304 

4305 attributes = [("val", units)] 

4306 

4307 self._xml_start_tag("c:dispUnits") 

4308 self._xml_empty_tag("c:builtInUnit", attributes) 

4309 

4310 if display: 

4311 self._xml_start_tag("c:dispUnitsLbl") 

4312 self._xml_empty_tag("c:layout") 

4313 self._xml_end_tag("c:dispUnitsLbl") 

4314 

4315 self._xml_end_tag("c:dispUnits") 

4316 

4317 def _write_a_grad_fill(self, gradient): 

4318 # Write the <a:gradFill> element. 

4319 

4320 attributes = [("flip", "none"), ("rotWithShape", "1")] 

4321 

4322 if gradient["type"] == "linear": 

4323 attributes = [] 

4324 

4325 self._xml_start_tag("a:gradFill", attributes) 

4326 

4327 # Write the a:gsLst element. 

4328 self._write_a_gs_lst(gradient) 

4329 

4330 if gradient["type"] == "linear": 

4331 # Write the a:lin element. 

4332 self._write_a_lin(gradient["angle"]) 

4333 else: 

4334 # Write the a:path element. 

4335 self._write_a_path(gradient["type"]) 

4336 

4337 # Write the a:tileRect element. 

4338 self._write_a_tile_rect(gradient["type"]) 

4339 

4340 self._xml_end_tag("a:gradFill") 

4341 

4342 def _write_a_gs_lst(self, gradient): 

4343 # Write the <a:gsLst> element. 

4344 positions = gradient["positions"] 

4345 colors = gradient["colors"] 

4346 

4347 self._xml_start_tag("a:gsLst") 

4348 

4349 for i, color in enumerate(colors): 

4350 pos = int(positions[i] * 1000) 

4351 attributes = [("pos", pos)] 

4352 self._xml_start_tag("a:gs", attributes) 

4353 

4354 self._write_color(color) 

4355 

4356 self._xml_end_tag("a:gs") 

4357 

4358 self._xml_end_tag("a:gsLst") 

4359 

4360 def _write_a_lin(self, angle): 

4361 # Write the <a:lin> element. 

4362 

4363 angle = int(60000 * angle) 

4364 

4365 attributes = [ 

4366 ("ang", angle), 

4367 ("scaled", "0"), 

4368 ] 

4369 

4370 self._xml_empty_tag("a:lin", attributes) 

4371 

4372 def _write_a_path(self, gradient_type): 

4373 # Write the <a:path> element. 

4374 

4375 attributes = [("path", gradient_type)] 

4376 

4377 self._xml_start_tag("a:path", attributes) 

4378 

4379 # Write the a:fillToRect element. 

4380 self._write_a_fill_to_rect(gradient_type) 

4381 

4382 self._xml_end_tag("a:path") 

4383 

4384 def _write_a_fill_to_rect(self, gradient_type): 

4385 # Write the <a:fillToRect> element. 

4386 

4387 if gradient_type == "shape": 

4388 attributes = [ 

4389 ("l", "50000"), 

4390 ("t", "50000"), 

4391 ("r", "50000"), 

4392 ("b", "50000"), 

4393 ] 

4394 else: 

4395 attributes = [ 

4396 ("l", "100000"), 

4397 ("t", "100000"), 

4398 ] 

4399 

4400 self._xml_empty_tag("a:fillToRect", attributes) 

4401 

4402 def _write_a_tile_rect(self, gradient_type): 

4403 # Write the <a:tileRect> element. 

4404 

4405 if gradient_type == "shape": 

4406 attributes = [] 

4407 else: 

4408 attributes = [ 

4409 ("r", "-100000"), 

4410 ("b", "-100000"), 

4411 ] 

4412 

4413 self._xml_empty_tag("a:tileRect", attributes) 

4414 

4415 def _write_a_patt_fill(self, pattern): 

4416 # Write the <a:pattFill> element. 

4417 

4418 attributes = [("prst", pattern["pattern"])] 

4419 

4420 self._xml_start_tag("a:pattFill", attributes) 

4421 

4422 # Write the a:fgClr element. 

4423 self._write_a_fg_clr(pattern["fg_color"]) 

4424 

4425 # Write the a:bgClr element. 

4426 self._write_a_bg_clr(pattern["bg_color"]) 

4427 

4428 self._xml_end_tag("a:pattFill") 

4429 

4430 def _write_a_fg_clr(self, color: Color): 

4431 # Write the <a:fgClr> element. 

4432 self._xml_start_tag("a:fgClr") 

4433 self._write_color(color) 

4434 self._xml_end_tag("a:fgClr") 

4435 

4436 def _write_a_bg_clr(self, color: Color): 

4437 # Write the <a:bgClr> element. 

4438 self._xml_start_tag("a:bgClr") 

4439 self._write_color(color) 

4440 self._xml_end_tag("a:bgClr")