Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/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

1977 statements  

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

2# 

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

4# 

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

6# Copyright 2013-2024, John McNamara, jmcnamara@cpan.org 

7# 

8import re 

9import copy 

10from warnings import warn 

11 

12from .shape import Shape 

13from . import xmlwriter 

14from .utility import get_rgb_color 

15from .utility import xl_rowcol_to_cell 

16from .utility import xl_range_formula 

17from .utility import supported_datetime 

18from .utility import datetime_to_excel_datetime 

19from .utility import quote_sheetname 

20 

21 

22class Chart(xmlwriter.XMLwriter): 

23 """ 

24 A class for writing the Excel XLSX Chart file. 

25 

26 

27 """ 

28 

29 ########################################################################### 

30 # 

31 # Public API. 

32 # 

33 ########################################################################### 

34 

35 def __init__(self, options=None): 

36 """ 

37 Constructor. 

38 

39 """ 

40 

41 super(Chart, self).__init__() 

42 

43 self.subtype = None 

44 self.sheet_type = 0x0200 

45 self.orientation = 0x0 

46 self.series = [] 

47 self.embedded = 0 

48 self.id = -1 

49 self.series_index = 0 

50 self.style_id = 2 

51 self.axis_ids = [] 

52 self.axis2_ids = [] 

53 self.cat_has_num_fmt = 0 

54 self.requires_category = False 

55 self.legend = {} 

56 self.cat_axis_position = "b" 

57 self.val_axis_position = "l" 

58 self.formula_ids = {} 

59 self.formula_data = [] 

60 self.horiz_cat_axis = 0 

61 self.horiz_val_axis = 1 

62 self.protection = 0 

63 self.chartarea = {} 

64 self.plotarea = {} 

65 self.x_axis = {} 

66 self.y_axis = {} 

67 self.y2_axis = {} 

68 self.x2_axis = {} 

69 self.chart_name = "" 

70 self.show_blanks = "gap" 

71 self.show_na_as_empty = False 

72 self.show_hidden = False 

73 self.show_crosses = True 

74 self.width = 480 

75 self.height = 288 

76 self.x_scale = 1 

77 self.y_scale = 1 

78 self.x_offset = 0 

79 self.y_offset = 0 

80 self.table = None 

81 self.cross_between = "between" 

82 self.default_marker = None 

83 self.series_gap_1 = None 

84 self.series_gap_2 = None 

85 self.series_overlap_1 = None 

86 self.series_overlap_2 = None 

87 self.drop_lines = None 

88 self.hi_low_lines = None 

89 self.up_down_bars = None 

90 self.smooth_allowed = False 

91 self.title_font = None 

92 self.title_name = None 

93 self.title_formula = None 

94 self.title_data_id = None 

95 self.title_layout = None 

96 self.title_overlay = None 

97 self.title_none = False 

98 self.date_category = False 

99 self.date_1904 = False 

100 self.remove_timezone = False 

101 self.label_positions = {} 

102 self.label_position_default = "" 

103 self.already_inserted = False 

104 self.combined = None 

105 self.is_secondary = False 

106 self.warn_sheetname = True 

107 self._set_default_properties() 

108 

109 def add_series(self, options=None): 

110 """ 

111 Add a data series to a chart. 

112 

113 Args: 

114 options: A dictionary of chart series options. 

115 

116 Returns: 

117 Nothing. 

118 

119 """ 

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

121 if options is None: 

122 options = {} 

123 

124 # Check that the required input has been specified. 

125 if "values" not in options: 

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

127 return 

128 

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

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

131 return 

132 

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

134 warn( 

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

136 "Excel Chart is 255" 

137 ) 

138 return 

139 

140 # Convert list into a formula string. 

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

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

143 

144 # Switch name and name_formula parameters if required. 

145 name, name_formula = self._process_names( 

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

147 ) 

148 

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

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

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

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

153 

154 # Set the line properties for the series. 

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

156 

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

158 if options.get("border"): 

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

160 

161 # Set the fill properties for the series. 

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

163 

164 # Set the pattern fill properties for the series. 

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

166 

167 # Set the gradient fill properties for the series. 

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

169 

170 # Pattern fill overrides solid fill. 

171 if pattern: 

172 self.fill = None 

173 

174 # Gradient fill overrides the solid and pattern fill. 

175 if gradient: 

176 pattern = None 

177 fill = None 

178 

179 # Set the marker properties for the series. 

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

181 

182 # Set the trendline properties for the series. 

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

184 

185 # Set the line smooth property for the series. 

186 smooth = options.get("smooth") 

187 

188 # Set the error bars properties for the series. 

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

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

191 

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

193 

194 # Set the point properties for the series. 

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

196 

197 # Set the labels properties for the series. 

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

199 

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

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

202 inverted_color = options.get("invert_if_negative_color", False) 

203 

204 # Set the secondary axis properties. 

205 x2_axis = options.get("x2_axis") 

206 y2_axis = options.get("y2_axis") 

207 

208 # Store secondary status for combined charts. 

209 if x2_axis or y2_axis: 

210 self.is_secondary = True 

211 

212 # Set the gap for Bar/Column charts. 

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

214 if y2_axis: 

215 self.series_gap_2 = options["gap"] 

216 else: 

217 self.series_gap_1 = options["gap"] 

218 

219 # Set the overlap for Bar/Column charts. 

220 if options.get("overlap"): 

221 if y2_axis: 

222 self.series_overlap_2 = options["overlap"] 

223 else: 

224 self.series_overlap_1 = options["overlap"] 

225 

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

227 series = { 

228 "values": values, 

229 "categories": categories, 

230 "name": name, 

231 "name_formula": name_formula, 

232 "name_id": name_id, 

233 "val_data_id": val_id, 

234 "cat_data_id": cat_id, 

235 "line": line, 

236 "fill": fill, 

237 "pattern": pattern, 

238 "gradient": gradient, 

239 "marker": marker, 

240 "trendline": trendline, 

241 "labels": labels, 

242 "invert_if_neg": invert_if_neg, 

243 "inverted_color": inverted_color, 

244 "x2_axis": x2_axis, 

245 "y2_axis": y2_axis, 

246 "points": points, 

247 "error_bars": error_bars, 

248 "smooth": smooth, 

249 } 

250 

251 self.series.append(series) 

252 

253 def set_x_axis(self, options): 

254 """ 

255 Set the chart X axis options. 

256 

257 Args: 

258 options: A dictionary of axis options. 

259 

260 Returns: 

261 Nothing. 

262 

263 """ 

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

265 

266 self.x_axis = axis 

267 

268 def set_y_axis(self, options): 

269 """ 

270 Set the chart Y axis options. 

271 

272 Args: 

273 options: A dictionary of axis options. 

274 

275 Returns: 

276 Nothing. 

277 

278 """ 

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

280 

281 self.y_axis = axis 

282 

283 def set_x2_axis(self, options): 

284 """ 

285 Set the chart secondary X axis options. 

286 

287 Args: 

288 options: A dictionary of axis options. 

289 

290 Returns: 

291 Nothing. 

292 

293 """ 

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

295 

296 self.x2_axis = axis 

297 

298 def set_y2_axis(self, options): 

299 """ 

300 Set the chart secondary Y axis options. 

301 

302 Args: 

303 options: A dictionary of axis options. 

304 

305 Returns: 

306 Nothing. 

307 

308 """ 

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

310 

311 self.y2_axis = axis 

312 

313 def set_title(self, options=None): 

314 """ 

315 Set the chart title options. 

316 

317 Args: 

318 options: A dictionary of chart title options. 

319 

320 Returns: 

321 Nothing. 

322 

323 """ 

324 if options is None: 

325 options = {} 

326 

327 name, name_formula = self._process_names( 

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

329 ) 

330 

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

332 

333 self.title_name = name 

334 self.title_formula = name_formula 

335 self.title_data_id = data_id 

336 

337 # Set the font properties if present. 

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

339 

340 # Set the axis name layout. 

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

342 # Set the title overlay option. 

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

344 

345 # Set the automatic title option. 

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

347 

348 def set_legend(self, options): 

349 """ 

350 Set the chart legend options. 

351 

352 Args: 

353 options: A dictionary of chart legend options. 

354 

355 Returns: 

356 Nothing. 

357 """ 

358 # Convert the user defined properties to internal properties. 

359 self.legend = self._get_legend_properties(options) 

360 

361 def set_plotarea(self, options): 

362 """ 

363 Set the chart plot area options. 

364 

365 Args: 

366 options: A dictionary of chart plot area options. 

367 

368 Returns: 

369 Nothing. 

370 """ 

371 # Convert the user defined properties to internal properties. 

372 self.plotarea = self._get_area_properties(options) 

373 

374 def set_chartarea(self, options): 

375 """ 

376 Set the chart area options. 

377 

378 Args: 

379 options: A dictionary of chart area options. 

380 

381 Returns: 

382 Nothing. 

383 """ 

384 # Convert the user defined properties to internal properties. 

385 self.chartarea = self._get_area_properties(options) 

386 

387 def set_style(self, style_id): 

388 """ 

389 Set the chart style type. 

390 

391 Args: 

392 style_id: An int representing the chart style. 

393 

394 Returns: 

395 Nothing. 

396 """ 

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

398 if style_id is None: 

399 style_id = 2 

400 

401 if style_id < 1 or style_id > 48: 

402 style_id = 2 

403 

404 self.style_id = style_id 

405 

406 def show_blanks_as(self, option): 

407 """ 

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

409 

410 Args: 

411 option: A string representing the display option. 

412 

413 Returns: 

414 Nothing. 

415 """ 

416 if not option: 

417 return 

418 

419 valid_options = { 

420 "gap": 1, 

421 "zero": 1, 

422 "span": 1, 

423 } 

424 

425 if option not in valid_options: 

426 warn("Unknown show_blanks_as() option '%s'" % option) 

427 return 

428 

429 self.show_blanks = option 

430 

431 def show_na_as_empty_cell(self): 

432 """ 

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

434 

435 Args: 

436 None. 

437 

438 Returns: 

439 Nothing. 

440 """ 

441 self.show_na_as_empty = True 

442 

443 def show_hidden_data(self): 

444 """ 

445 Display data on charts from hidden rows or columns. 

446 

447 Args: 

448 None. 

449 

450 Returns: 

451 Nothing. 

452 """ 

453 self.show_hidden = True 

454 

455 def set_size(self, options=None): 

456 """ 

457 Set size or scale of the chart. 

458 

459 Args: 

460 options: A dictionary of chart size options. 

461 

462 Returns: 

463 Nothing. 

464 """ 

465 if options is None: 

466 options = {} 

467 

468 # Set dimensions or scale for the chart. 

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

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

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

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

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

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

475 

476 def set_table(self, options=None): 

477 """ 

478 Set properties for an axis data table. 

479 

480 Args: 

481 options: A dictionary of axis table options. 

482 

483 Returns: 

484 Nothing. 

485 

486 """ 

487 if options is None: 

488 options = {} 

489 

490 table = {} 

491 

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

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

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

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

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

497 

498 self.table = table 

499 

500 def set_up_down_bars(self, options=None): 

501 """ 

502 Set properties for the chart up-down bars. 

503 

504 Args: 

505 options: A dictionary of options. 

506 

507 Returns: 

508 Nothing. 

509 

510 """ 

511 if options is None: 

512 options = {} 

513 

514 # Defaults. 

515 up_line = None 

516 up_fill = None 

517 down_line = None 

518 down_fill = None 

519 

520 # Set properties for 'up' bar. 

521 if options.get("up"): 

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

523 # Map border to line. 

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

525 

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

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

528 

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

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

531 

532 # Set properties for 'down' bar. 

533 if options.get("down"): 

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

535 # Map border to line. 

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

537 

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

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

540 

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

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

543 

544 self.up_down_bars = { 

545 "up": { 

546 "line": up_line, 

547 "fill": up_fill, 

548 }, 

549 "down": { 

550 "line": down_line, 

551 "fill": down_fill, 

552 }, 

553 } 

554 

555 def set_drop_lines(self, options=None): 

556 """ 

557 Set properties for the chart drop lines. 

558 

559 Args: 

560 options: A dictionary of options. 

561 

562 Returns: 

563 Nothing. 

564 

565 """ 

566 if options is None: 

567 options = {} 

568 

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

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

571 

572 # Set the pattern fill properties for the series. 

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

574 

575 # Set the gradient fill properties for the series. 

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

577 

578 # Pattern fill overrides solid fill. 

579 if pattern: 

580 self.fill = None 

581 

582 # Gradient fill overrides the solid and pattern fill. 

583 if gradient: 

584 pattern = None 

585 fill = None 

586 

587 self.drop_lines = { 

588 "line": line, 

589 "fill": fill, 

590 "pattern": pattern, 

591 "gradient": gradient, 

592 } 

593 

594 def set_high_low_lines(self, options=None): 

595 """ 

596 Set properties for the chart high-low lines. 

597 

598 Args: 

599 options: A dictionary of options. 

600 

601 Returns: 

602 Nothing. 

603 

604 """ 

605 if options is None: 

606 options = {} 

607 

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

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

610 

611 # Set the pattern fill properties for the series. 

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

613 

614 # Set the gradient fill properties for the series. 

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

616 

617 # Pattern fill overrides solid fill. 

618 if pattern: 

619 self.fill = None 

620 

621 # Gradient fill overrides the solid and pattern fill. 

622 if gradient: 

623 pattern = None 

624 fill = None 

625 

626 self.hi_low_lines = { 

627 "line": line, 

628 "fill": fill, 

629 "pattern": pattern, 

630 "gradient": gradient, 

631 } 

632 

633 def combine(self, chart=None): 

634 """ 

635 Create a combination chart with a secondary chart. 

636 

637 Args: 

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

639 

640 Returns: 

641 Nothing. 

642 

643 """ 

644 if chart is None: 

645 return 

646 

647 self.combined = chart 

648 

649 ########################################################################### 

650 # 

651 # Private API. 

652 # 

653 ########################################################################### 

654 

655 def _assemble_xml_file(self): 

656 # Assemble and write the XML file. 

657 

658 # Write the XML declaration. 

659 self._xml_declaration() 

660 

661 # Write the c:chartSpace element. 

662 self._write_chart_space() 

663 

664 # Write the c:lang element. 

665 self._write_lang() 

666 

667 # Write the c:style element. 

668 self._write_style() 

669 

670 # Write the c:protection element. 

671 self._write_protection() 

672 

673 # Write the c:chart element. 

674 self._write_chart() 

675 

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

677 self._write_sp_pr(self.chartarea) 

678 

679 # Write the c:printSettings element. 

680 if self.embedded: 

681 self._write_print_settings() 

682 

683 # Close the worksheet tag. 

684 self._xml_end_tag("c:chartSpace") 

685 # Close the file. 

686 self._xml_close() 

687 

688 def _convert_axis_args(self, axis, user_options): 

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

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

691 options.update(user_options) 

692 

693 name, name_formula = self._process_names( 

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

695 ) 

696 

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

698 

699 axis = { 

700 "defaults": axis["defaults"], 

701 "name": name, 

702 "formula": name_formula, 

703 "data_id": data_id, 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

722 "text_axis": False, 

723 } 

724 

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

726 

727 # Convert the display units. 

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

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

730 

731 # Map major_gridlines properties. 

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

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

734 options["major_gridlines"] 

735 ) 

736 

737 # Map minor_gridlines properties. 

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

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

740 options["minor_gridlines"] 

741 ) 

742 

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

744 if axis.get("position"): 

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

746 

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

748 if axis.get("position_axis"): 

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

750 axis["position_axis"] = "midCat" 

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

752 # Doesn't need to be modified. 

753 pass 

754 else: 

755 # Otherwise use the default value. 

756 axis["position_axis"] = None 

757 

758 # Set the category axis as a date axis. 

759 if options.get("date_axis"): 

760 self.date_category = True 

761 

762 # Set the category axis as a text axis. 

763 if options.get("text_axis"): 

764 self.date_category = False 

765 axis["text_axis"] = True 

766 

767 # Convert datetime args if required. 

768 if axis.get("min") and supported_datetime(axis["min"]): 

769 axis["min"] = datetime_to_excel_datetime( 

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

771 ) 

772 if axis.get("max") and supported_datetime(axis["max"]): 

773 axis["max"] = datetime_to_excel_datetime( 

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

775 ) 

776 if axis.get("crossing") and supported_datetime(axis["crossing"]): 

777 axis["crossing"] = datetime_to_excel_datetime( 

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

779 ) 

780 

781 # Set the font properties if present. 

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

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

784 

785 # Set the axis name layout. 

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

787 options.get("name_layout"), True 

788 ) 

789 

790 # Set the line properties for the axis. 

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

792 

793 # Set the fill properties for the axis. 

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

795 

796 # Set the pattern fill properties for the series. 

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

798 

799 # Set the gradient fill properties for the series. 

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

801 

802 # Pattern fill overrides solid fill. 

803 if axis.get("pattern"): 

804 axis["fill"] = None 

805 

806 # Gradient fill overrides the solid and pattern fill. 

807 if axis.get("gradient"): 

808 axis["pattern"] = None 

809 axis["fill"] = None 

810 

811 # Set the tick marker types. 

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

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

814 

815 return axis 

816 

817 def _convert_font_args(self, options): 

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

819 if not options: 

820 return 

821 

822 font = { 

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

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

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

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

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

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

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

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

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

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

833 } 

834 

835 # Convert font size units. 

836 if font["size"]: 

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

838 

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

840 if font["rotation"]: 

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

842 

843 return font 

844 

845 def _list_to_formula(self, data): 

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

847 

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

849 if not isinstance(data, list): 

850 # Check for unquoted sheetnames. 

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

852 warn( 

853 "Sheetname in '%s' contains spaces but isn't quoted. " 

854 "This may cause errors in Excel." % data 

855 ) 

856 return data 

857 

858 formula = xl_range_formula(*data) 

859 

860 return formula 

861 

862 def _process_names(self, name, name_formula): 

863 # Switch name and name_formula parameters if required. 

864 

865 if name is not None: 

866 if isinstance(name, list): 

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

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

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

870 name = "" 

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

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

873 name_formula = name 

874 name = "" 

875 

876 return name, name_formula 

877 

878 def _get_data_type(self, data): 

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

880 

881 # Check for no data in the series. 

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

883 return "none" 

884 

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

886 return "multi_str" 

887 

888 # Determine if data is numeric or strings. 

889 for token in data: 

890 if token is None: 

891 continue 

892 

893 # Check for strings that would evaluate to float like 

894 # '1.1_1' of ' 1'. 

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

896 # Assume entire data series is string data. 

897 return "str" 

898 

899 try: 

900 float(token) 

901 except ValueError: 

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

903 return "str" 

904 

905 # The series data was all numeric. 

906 return "num" 

907 

908 def _get_data_id(self, formula, data): 

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

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

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

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

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

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

915 

916 # Ignore series without a range formula. 

917 if not formula: 

918 return 

919 

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

921 if formula.startswith("="): 

922 formula = formula.lstrip("=") 

923 

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

925 # in a separate array with the same id. 

926 if formula not in self.formula_ids: 

927 # Haven't seen this formula before. 

928 formula_id = len(self.formula_data) 

929 

930 self.formula_data.append(data) 

931 self.formula_ids[formula] = formula_id 

932 else: 

933 # Formula already seen. Return existing id. 

934 formula_id = self.formula_ids[formula] 

935 

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

937 if self.formula_data[formula_id] is None: 

938 self.formula_data[formula_id] = data 

939 

940 return formula_id 

941 

942 def _get_marker_properties(self, marker): 

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

944 

945 if not marker: 

946 return 

947 

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

949 marker = copy.deepcopy(marker) 

950 

951 types = { 

952 "automatic": "automatic", 

953 "none": "none", 

954 "square": "square", 

955 "diamond": "diamond", 

956 "triangle": "triangle", 

957 "x": "x", 

958 "star": "star", 

959 "dot": "dot", 

960 "short_dash": "dot", 

961 "dash": "dash", 

962 "long_dash": "dash", 

963 "circle": "circle", 

964 "plus": "plus", 

965 "picture": "picture", 

966 } 

967 

968 # Check for valid types. 

969 marker_type = marker.get("type") 

970 

971 if marker_type is not None: 

972 if marker_type in types: 

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

974 else: 

975 warn("Unknown marker type '%s" % marker_type) 

976 return 

977 

978 # Set the line properties for the marker. 

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

980 

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

982 if "border" in marker: 

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

984 

985 # Set the fill properties for the marker. 

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

987 

988 # Set the pattern fill properties for the series. 

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

990 

991 # Set the gradient fill properties for the series. 

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

993 

994 # Pattern fill overrides solid fill. 

995 if pattern: 

996 self.fill = None 

997 

998 # Gradient fill overrides the solid and pattern fill. 

999 if gradient: 

1000 pattern = None 

1001 fill = None 

1002 

1003 marker["line"] = line 

1004 marker["fill"] = fill 

1005 marker["pattern"] = pattern 

1006 marker["gradient"] = gradient 

1007 

1008 return marker 

1009 

1010 def _get_trendline_properties(self, trendline): 

1011 # Convert user trendline properties to structure required internally. 

1012 

1013 if not trendline: 

1014 return 

1015 

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

1017 trendline = copy.deepcopy(trendline) 

1018 

1019 types = { 

1020 "exponential": "exp", 

1021 "linear": "linear", 

1022 "log": "log", 

1023 "moving_average": "movingAvg", 

1024 "polynomial": "poly", 

1025 "power": "power", 

1026 } 

1027 

1028 # Check the trendline type. 

1029 trend_type = trendline.get("type") 

1030 

1031 if trend_type in types: 

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

1033 else: 

1034 warn("Unknown trendline type '%s'" % trend_type) 

1035 return 

1036 

1037 # Set the line properties for the trendline. 

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

1039 

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

1041 if "border" in trendline: 

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

1043 

1044 # Set the fill properties for the trendline. 

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

1046 

1047 # Set the pattern fill properties for the trendline. 

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

1049 

1050 # Set the gradient fill properties for the trendline. 

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

1052 

1053 # Set the format properties for the trendline label. 

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

1055 

1056 # Pattern fill overrides solid fill. 

1057 if pattern: 

1058 self.fill = None 

1059 

1060 # Gradient fill overrides the solid and pattern fill. 

1061 if gradient: 

1062 pattern = None 

1063 fill = None 

1064 

1065 trendline["line"] = line 

1066 trendline["fill"] = fill 

1067 trendline["pattern"] = pattern 

1068 trendline["gradient"] = gradient 

1069 trendline["label"] = label 

1070 

1071 return trendline 

1072 

1073 def _get_trendline_label_properties(self, label): 

1074 # Convert user trendline properties to structure required internally. 

1075 

1076 if not label: 

1077 return {} 

1078 

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

1080 label = copy.deepcopy(label) 

1081 

1082 # Set the font properties if present. 

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

1084 

1085 # Set the line properties for the label. 

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

1087 

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

1089 if "border" in label: 

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

1091 

1092 # Set the fill properties for the label. 

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

1094 

1095 # Set the pattern fill properties for the label. 

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

1097 

1098 # Set the gradient fill properties for the label. 

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

1100 

1101 # Pattern fill overrides solid fill. 

1102 if pattern: 

1103 self.fill = None 

1104 

1105 # Gradient fill overrides the solid and pattern fill. 

1106 if gradient: 

1107 pattern = None 

1108 fill = None 

1109 

1110 label["font"] = font 

1111 label["line"] = line 

1112 label["fill"] = fill 

1113 label["pattern"] = pattern 

1114 label["gradient"] = gradient 

1115 

1116 return label 

1117 

1118 def _get_error_bars_props(self, options): 

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

1120 if not options: 

1121 return 

1122 

1123 # Default values. 

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

1125 

1126 types = { 

1127 "fixed": "fixedVal", 

1128 "percentage": "percentage", 

1129 "standard_deviation": "stdDev", 

1130 "standard_error": "stdErr", 

1131 "custom": "cust", 

1132 } 

1133 

1134 # Check the error bars type. 

1135 error_type = options["type"] 

1136 

1137 if error_type in types: 

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

1139 else: 

1140 warn("Unknown error bars type '%s" % error_type) 

1141 return 

1142 

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

1144 if "value" in options: 

1145 error_bars["value"] = options["value"] 

1146 

1147 # Set the end-cap style. 

1148 if "end_style" in options: 

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

1150 

1151 # Set the error bar direction. 

1152 if "direction" in options: 

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

1154 error_bars["direction"] = "minus" 

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

1156 error_bars["direction"] = "plus" 

1157 else: 

1158 # Default to 'both'. 

1159 pass 

1160 

1161 # Set any custom values. 

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

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

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

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

1166 

1167 # Set the line properties for the error bars. 

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

1169 

1170 return error_bars 

1171 

1172 def _get_gridline_properties(self, options): 

1173 # Convert user gridline properties to structure required internally. 

1174 

1175 # Set the visible property for the gridline. 

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

1177 

1178 # Set the line properties for the gridline. 

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

1180 

1181 return gridline 

1182 

1183 def _get_labels_properties(self, labels): 

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

1185 

1186 if not labels: 

1187 return None 

1188 

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

1190 labels = copy.deepcopy(labels) 

1191 

1192 # Map user defined label positions to Excel positions. 

1193 position = labels.get("position") 

1194 

1195 if position: 

1196 if position in self.label_positions: 

1197 if position == self.label_position_default: 

1198 labels["position"] = None 

1199 else: 

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

1201 else: 

1202 warn("Unsupported label position '%s' for this chart type" % position) 

1203 return 

1204 

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

1206 separator = labels.get("separator") 

1207 separators = { 

1208 ",": ", ", 

1209 ";": "; ", 

1210 ".": ". ", 

1211 "\n": "\n", 

1212 " ": " ", 

1213 } 

1214 

1215 if separator: 

1216 if separator in separators: 

1217 labels["separator"] = separators[separator] 

1218 else: 

1219 warn("Unsupported label separator") 

1220 return 

1221 

1222 # Set the font properties if present. 

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

1224 

1225 # Set the line properties for the labels. 

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

1227 

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

1229 if "border" in labels: 

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

1231 

1232 # Set the fill properties for the labels. 

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

1234 

1235 # Set the pattern fill properties for the labels. 

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

1237 

1238 # Set the gradient fill properties for the labels. 

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

1240 

1241 # Pattern fill overrides solid fill. 

1242 if pattern: 

1243 self.fill = None 

1244 

1245 # Gradient fill overrides the solid and pattern fill. 

1246 if gradient: 

1247 pattern = None 

1248 fill = None 

1249 

1250 labels["line"] = line 

1251 labels["fill"] = fill 

1252 labels["pattern"] = pattern 

1253 labels["gradient"] = gradient 

1254 

1255 if labels.get("custom"): 

1256 for label in labels["custom"]: 

1257 if label is None: 

1258 continue 

1259 

1260 value = label.get("value") 

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

1262 label["formula"] = value 

1263 

1264 formula = label.get("formula") 

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

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

1267 

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

1269 label["data_id"] = data_id 

1270 

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

1272 

1273 # Set the line properties for the label. 

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

1275 

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

1277 if "border" in label: 

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

1279 

1280 # Set the fill properties for the label. 

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

1282 

1283 # Set the pattern fill properties for the label. 

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

1285 

1286 # Set the gradient fill properties for the label. 

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

1288 

1289 # Pattern fill overrides solid fill. 

1290 if pattern: 

1291 self.fill = None 

1292 

1293 # Gradient fill overrides the solid and pattern fill. 

1294 if gradient: 

1295 pattern = None 

1296 fill = None 

1297 

1298 label["line"] = line 

1299 label["fill"] = fill 

1300 label["pattern"] = pattern 

1301 label["gradient"] = gradient 

1302 

1303 return labels 

1304 

1305 def _get_area_properties(self, options): 

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

1307 area = {} 

1308 

1309 # Set the line properties for the chartarea. 

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

1311 

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

1313 if options.get("border"): 

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

1315 

1316 # Set the fill properties for the chartarea. 

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

1318 

1319 # Set the pattern fill properties for the series. 

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

1321 

1322 # Set the gradient fill properties for the series. 

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

1324 

1325 # Pattern fill overrides solid fill. 

1326 if pattern: 

1327 self.fill = None 

1328 

1329 # Gradient fill overrides the solid and pattern fill. 

1330 if gradient: 

1331 pattern = None 

1332 fill = None 

1333 

1334 # Set the plotarea layout. 

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

1336 

1337 area["line"] = line 

1338 area["fill"] = fill 

1339 area["pattern"] = pattern 

1340 area["layout"] = layout 

1341 area["gradient"] = gradient 

1342 

1343 return area 

1344 

1345 def _get_legend_properties(self, options=None): 

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

1347 legend = {} 

1348 

1349 if options is None: 

1350 options = {} 

1351 

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

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

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

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

1356 

1357 # Turn off the legend. 

1358 if options.get("none"): 

1359 legend["position"] = "none" 

1360 

1361 # Set the line properties for the legend. 

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

1363 

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

1365 if options.get("border"): 

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

1367 

1368 # Set the fill properties for the legend. 

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

1370 

1371 # Set the pattern fill properties for the series. 

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

1373 

1374 # Set the gradient fill properties for the series. 

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

1376 

1377 # Pattern fill overrides solid fill. 

1378 if pattern: 

1379 self.fill = None 

1380 

1381 # Gradient fill overrides the solid and pattern fill. 

1382 if gradient: 

1383 pattern = None 

1384 fill = None 

1385 

1386 # Set the legend layout. 

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

1388 

1389 legend["line"] = line 

1390 legend["fill"] = fill 

1391 legend["pattern"] = pattern 

1392 legend["layout"] = layout 

1393 legend["gradient"] = gradient 

1394 

1395 return legend 

1396 

1397 def _get_layout_properties(self, args, is_text): 

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

1399 layout = {} 

1400 

1401 if not args: 

1402 return 

1403 

1404 if is_text: 

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

1406 else: 

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

1408 

1409 # Check for valid properties. 

1410 for key in args.keys(): 

1411 if key not in properties: 

1412 warn("Property '%s' allowed not in layout options" % key) 

1413 return 

1414 

1415 # Set the layout properties. 

1416 for prop in properties: 

1417 if prop not in args.keys(): 

1418 warn("Property '%s' must be specified in layout options" % prop) 

1419 return 

1420 

1421 value = args[prop] 

1422 

1423 try: 

1424 float(value) 

1425 except ValueError: 

1426 warn( 

1427 "Property '%s' value '%s' must be numeric in layout" % (prop, value) 

1428 ) 

1429 return 

1430 

1431 if value < 0 or value > 1: 

1432 warn( 

1433 "Property '%s' value '%s' must be in range " 

1434 "0 < x <= 1 in layout options" % (prop, value) 

1435 ) 

1436 return 

1437 

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

1439 layout[prop] = "%.17g" % value 

1440 

1441 return layout 

1442 

1443 def _get_points_properties(self, user_points): 

1444 # Convert user points properties to structure required internally. 

1445 points = [] 

1446 

1447 if not user_points: 

1448 return 

1449 

1450 for user_point in user_points: 

1451 point = {} 

1452 

1453 if user_point is not None: 

1454 # Set the line properties for the point. 

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

1456 

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

1458 if "border" in user_point: 

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

1460 

1461 # Set the fill properties for the chartarea. 

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

1463 

1464 # Set the pattern fill properties for the series. 

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

1466 

1467 # Set the gradient fill properties for the series. 

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

1469 

1470 # Pattern fill overrides solid fill. 

1471 if pattern: 

1472 self.fill = None 

1473 

1474 # Gradient fill overrides the solid and pattern fill. 

1475 if gradient: 

1476 pattern = None 

1477 fill = None 

1478 

1479 point["line"] = line 

1480 point["fill"] = fill 

1481 point["pattern"] = pattern 

1482 point["gradient"] = gradient 

1483 

1484 points.append(point) 

1485 

1486 return points 

1487 

1488 def _has_fill_formatting(self, element): 

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

1490 has_fill = False 

1491 has_line = False 

1492 has_pattern = element.get("pattern") 

1493 has_gradient = element.get("gradient") 

1494 

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

1496 has_fill = True 

1497 

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

1499 has_line = True 

1500 

1501 if not has_fill and not has_line and not has_pattern and not has_gradient: 

1502 return False 

1503 else: 

1504 return True 

1505 

1506 def _get_display_units(self, display_units): 

1507 # Convert user defined display units to internal units. 

1508 if not display_units: 

1509 return 

1510 

1511 types = { 

1512 "hundreds": "hundreds", 

1513 "thousands": "thousands", 

1514 "ten_thousands": "tenThousands", 

1515 "hundred_thousands": "hundredThousands", 

1516 "millions": "millions", 

1517 "ten_millions": "tenMillions", 

1518 "hundred_millions": "hundredMillions", 

1519 "billions": "billions", 

1520 "trillions": "trillions", 

1521 } 

1522 

1523 if display_units in types: 

1524 display_units = types[display_units] 

1525 else: 

1526 warn("Unknown display_units type '%s'" % display_units) 

1527 return 

1528 

1529 return display_units 

1530 

1531 def _get_tick_type(self, tick_type): 

1532 # Convert user defined display units to internal units. 

1533 if not tick_type: 

1534 return 

1535 

1536 types = { 

1537 "outside": "out", 

1538 "inside": "in", 

1539 "none": "none", 

1540 "cross": "cross", 

1541 } 

1542 

1543 if tick_type in types: 

1544 tick_type = types[tick_type] 

1545 else: 

1546 warn("Unknown tick_type '%s'" % tick_type) 

1547 return 

1548 

1549 return tick_type 

1550 

1551 def _get_primary_axes_series(self): 

1552 # Returns series which use the primary axes. 

1553 primary_axes_series = [] 

1554 

1555 for series in self.series: 

1556 if not series["y2_axis"]: 

1557 primary_axes_series.append(series) 

1558 

1559 return primary_axes_series 

1560 

1561 def _get_secondary_axes_series(self): 

1562 # Returns series which use the secondary axes. 

1563 secondary_axes_series = [] 

1564 

1565 for series in self.series: 

1566 if series["y2_axis"]: 

1567 secondary_axes_series.append(series) 

1568 

1569 return secondary_axes_series 

1570 

1571 def _add_axis_ids(self, args): 

1572 # Add unique ids for primary or secondary axes 

1573 chart_id = 5001 + int(self.id) 

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

1575 

1576 id1 = "%04d%04d" % (chart_id, axis_count) 

1577 id2 = "%04d%04d" % (chart_id, axis_count + 1) 

1578 

1579 if args["primary_axes"]: 

1580 self.axis_ids.append(id1) 

1581 self.axis_ids.append(id2) 

1582 

1583 if not args["primary_axes"]: 

1584 self.axis2_ids.append(id1) 

1585 self.axis2_ids.append(id2) 

1586 

1587 def _set_default_properties(self): 

1588 # Setup the default properties for a chart. 

1589 

1590 self.x_axis["defaults"] = { 

1591 "num_format": "General", 

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

1593 } 

1594 

1595 self.y_axis["defaults"] = { 

1596 "num_format": "General", 

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

1598 } 

1599 

1600 self.x2_axis["defaults"] = { 

1601 "num_format": "General", 

1602 "label_position": "none", 

1603 "crossing": "max", 

1604 "visible": 0, 

1605 } 

1606 

1607 self.y2_axis["defaults"] = { 

1608 "num_format": "General", 

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

1610 "position": "right", 

1611 "visible": 1, 

1612 } 

1613 

1614 self.set_x_axis({}) 

1615 self.set_y_axis({}) 

1616 

1617 self.set_x2_axis({}) 

1618 self.set_y2_axis({}) 

1619 

1620 ########################################################################### 

1621 # 

1622 # XML methods. 

1623 # 

1624 ########################################################################### 

1625 

1626 def _write_chart_space(self): 

1627 # Write the <c:chartSpace> element. 

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

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

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

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

1632 

1633 attributes = [ 

1634 ("xmlns:c", xmlns_c), 

1635 ("xmlns:a", xmlns_a), 

1636 ("xmlns:r", xmlns_r), 

1637 ] 

1638 

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

1640 

1641 def _write_lang(self): 

1642 # Write the <c:lang> element. 

1643 val = "en-US" 

1644 

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

1646 

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

1648 

1649 def _write_style(self): 

1650 # Write the <c:style> element. 

1651 style_id = self.style_id 

1652 

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

1654 if style_id == 2: 

1655 return 

1656 

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

1658 

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

1660 

1661 def _write_chart(self): 

1662 # Write the <c:chart> element. 

1663 self._xml_start_tag("c:chart") 

1664 

1665 if self.title_none: 

1666 # Turn off the title. 

1667 self._write_c_auto_title_deleted() 

1668 else: 

1669 # Write the chart title elements. 

1670 if self.title_formula is not None: 

1671 self._write_title_formula( 

1672 self.title_formula, 

1673 self.title_data_id, 

1674 None, 

1675 self.title_font, 

1676 self.title_layout, 

1677 self.title_overlay, 

1678 ) 

1679 elif self.title_name is not None: 

1680 self._write_title_rich( 

1681 self.title_name, 

1682 None, 

1683 self.title_font, 

1684 self.title_layout, 

1685 self.title_overlay, 

1686 ) 

1687 

1688 # Write the c:plotArea element. 

1689 self._write_plot_area() 

1690 

1691 # Write the c:legend element. 

1692 self._write_legend() 

1693 

1694 # Write the c:plotVisOnly element. 

1695 self._write_plot_vis_only() 

1696 

1697 # Write the c:dispBlanksAs element. 

1698 self._write_disp_blanks_as() 

1699 

1700 # Write the c:extLst element. 

1701 if self.show_na_as_empty: 

1702 self._write_c_ext_lst_display_na() 

1703 

1704 self._xml_end_tag("c:chart") 

1705 

1706 def _write_disp_blanks_as(self): 

1707 # Write the <c:dispBlanksAs> element. 

1708 val = self.show_blanks 

1709 

1710 # Ignore the default value. 

1711 if val == "gap": 

1712 return 

1713 

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

1715 

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

1717 

1718 def _write_plot_area(self): 

1719 # Write the <c:plotArea> element. 

1720 self._xml_start_tag("c:plotArea") 

1721 

1722 # Write the c:layout element. 

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

1724 

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

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

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

1728 

1729 # Configure a combined chart if present. 

1730 second_chart = self.combined 

1731 if second_chart: 

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

1733 if second_chart.is_secondary: 

1734 second_chart.id = 1000 + self.id 

1735 else: 

1736 second_chart.id = self.id 

1737 

1738 # Share the same filehandle for writing. 

1739 second_chart.fh = self.fh 

1740 

1741 # Share series index with primary chart. 

1742 second_chart.series_index = self.series_index 

1743 

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

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

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

1747 

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

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

1750 

1751 if self.date_category: 

1752 self._write_date_axis(args) 

1753 else: 

1754 self._write_cat_axis(args) 

1755 

1756 self._write_val_axis(args) 

1757 

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

1759 args = { 

1760 "x_axis": self.x2_axis, 

1761 "y_axis": self.y2_axis, 

1762 "axis_ids": self.axis2_ids, 

1763 } 

1764 

1765 self._write_val_axis(args) 

1766 

1767 # Write the secondary axis for the secondary chart. 

1768 if second_chart and second_chart.is_secondary: 

1769 args = { 

1770 "x_axis": second_chart.x2_axis, 

1771 "y_axis": second_chart.y2_axis, 

1772 "axis_ids": second_chart.axis2_ids, 

1773 } 

1774 

1775 second_chart._write_val_axis(args) 

1776 

1777 if self.date_category: 

1778 self._write_date_axis(args) 

1779 else: 

1780 self._write_cat_axis(args) 

1781 

1782 # Write the c:dTable element. 

1783 self._write_d_table() 

1784 

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

1786 self._write_sp_pr(self.plotarea) 

1787 

1788 self._xml_end_tag("c:plotArea") 

1789 

1790 def _write_layout(self, layout, layout_type): 

1791 # Write the <c:layout> element. 

1792 

1793 if not layout: 

1794 # Automatic layout. 

1795 self._xml_empty_tag("c:layout") 

1796 else: 

1797 # User defined manual layout. 

1798 self._xml_start_tag("c:layout") 

1799 self._write_manual_layout(layout, layout_type) 

1800 self._xml_end_tag("c:layout") 

1801 

1802 def _write_manual_layout(self, layout, layout_type): 

1803 # Write the <c:manualLayout> element. 

1804 self._xml_start_tag("c:manualLayout") 

1805 

1806 # Plotarea has a layoutTarget element. 

1807 if layout_type == "plot": 

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

1809 

1810 # Set the x, y positions. 

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

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

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

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

1815 

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

1817 if layout_type != "text": 

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

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

1820 

1821 self._xml_end_tag("c:manualLayout") 

1822 

1823 def _write_chart_type(self, options): 

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

1825 # by the subclasses. 

1826 return 

1827 

1828 def _write_grouping(self, val): 

1829 # Write the <c:grouping> element. 

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

1831 

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

1833 

1834 def _write_series(self, series): 

1835 # Write the series elements. 

1836 self._write_ser(series) 

1837 

1838 def _write_ser(self, series): 

1839 # Write the <c:ser> element. 

1840 index = self.series_index 

1841 self.series_index += 1 

1842 

1843 self._xml_start_tag("c:ser") 

1844 

1845 # Write the c:idx element. 

1846 self._write_idx(index) 

1847 

1848 # Write the c:order element. 

1849 self._write_order(index) 

1850 

1851 # Write the series name. 

1852 self._write_series_name(series) 

1853 

1854 # Write the c:spPr element. 

1855 self._write_sp_pr(series) 

1856 

1857 # Write the c:marker element. 

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

1859 

1860 # Write the c:invertIfNegative element. 

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

1862 

1863 # Write the c:dPt element. 

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

1865 

1866 # Write the c:dLbls element. 

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

1868 

1869 # Write the c:trendline element. 

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

1871 

1872 # Write the c:errBars element. 

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

1874 

1875 # Write the c:cat element. 

1876 self._write_cat(series) 

1877 

1878 # Write the c:val element. 

1879 self._write_val(series) 

1880 

1881 # Write the c:smooth element. 

1882 if self.smooth_allowed: 

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

1884 

1885 # Write the c:extLst element. 

1886 if series.get("inverted_color"): 

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

1888 

1889 self._xml_end_tag("c:ser") 

1890 

1891 def _write_c_ext_lst_inverted_color(self, color): 

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

1893 

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

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

1896 

1897 attributes1 = [ 

1898 ("uri", uri), 

1899 ("xmlns:c14", xmlns_c_14), 

1900 ] 

1901 

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

1903 

1904 self._xml_start_tag("c:extLst") 

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

1906 self._xml_start_tag("c14:invertSolidFillFmt") 

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

1908 

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

1910 

1911 self._xml_end_tag("c14:spPr") 

1912 self._xml_end_tag("c14:invertSolidFillFmt") 

1913 self._xml_end_tag("c:ext") 

1914 self._xml_end_tag("c:extLst") 

1915 

1916 def _write_c_ext_lst_display_na(self): 

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

1918 

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

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

1921 

1922 attributes1 = [ 

1923 ("uri", uri), 

1924 ("xmlns:c16r3", xmlns_c_16), 

1925 ] 

1926 

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

1928 

1929 self._xml_start_tag("c:extLst") 

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

1931 self._xml_start_tag("c16r3:dataDisplayOptions16") 

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

1933 self._xml_end_tag("c16r3:dataDisplayOptions16") 

1934 self._xml_end_tag("c:ext") 

1935 self._xml_end_tag("c:extLst") 

1936 

1937 def _write_idx(self, val): 

1938 # Write the <c:idx> element. 

1939 

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

1941 

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

1943 

1944 def _write_order(self, val): 

1945 # Write the <c:order> element. 

1946 

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

1948 

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

1950 

1951 def _write_series_name(self, series): 

1952 # Write the series name. 

1953 

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

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

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

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

1958 

1959 def _write_c_smooth(self, smooth): 

1960 # Write the <c:smooth> element. 

1961 

1962 if smooth: 

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

1964 

1965 def _write_cat(self, series): 

1966 # Write the <c:cat> element. 

1967 formula = series["categories"] 

1968 data_id = series["cat_data_id"] 

1969 data = None 

1970 

1971 if data_id is not None: 

1972 data = self.formula_data[data_id] 

1973 

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

1975 if not formula: 

1976 return 

1977 

1978 self._xml_start_tag("c:cat") 

1979 

1980 # Check the type of cached data. 

1981 cat_type = self._get_data_type(data) 

1982 

1983 if cat_type == "str": 

1984 self.cat_has_num_fmt = 0 

1985 # Write the c:numRef element. 

1986 self._write_str_ref(formula, data, cat_type) 

1987 

1988 elif cat_type == "multi_str": 

1989 self.cat_has_num_fmt = 0 

1990 # Write the c:numRef element. 

1991 self._write_multi_lvl_str_ref(formula, data) 

1992 

1993 else: 

1994 self.cat_has_num_fmt = 1 

1995 # Write the c:numRef element. 

1996 self._write_num_ref(formula, data, cat_type) 

1997 

1998 self._xml_end_tag("c:cat") 

1999 

2000 def _write_val(self, series): 

2001 # Write the <c:val> element. 

2002 formula = series["values"] 

2003 data_id = series["val_data_id"] 

2004 data = self.formula_data[data_id] 

2005 

2006 self._xml_start_tag("c:val") 

2007 

2008 # Unlike Cat axes data should only be numeric. 

2009 # Write the c:numRef element. 

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

2011 

2012 self._xml_end_tag("c:val") 

2013 

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

2015 # Write the <c:numRef> element. 

2016 self._xml_start_tag("c:numRef") 

2017 

2018 # Write the c:f element. 

2019 self._write_series_formula(formula) 

2020 

2021 if ref_type == "num": 

2022 # Write the c:numCache element. 

2023 self._write_num_cache(data) 

2024 elif ref_type == "str": 

2025 # Write the c:strCache element. 

2026 self._write_str_cache(data) 

2027 

2028 self._xml_end_tag("c:numRef") 

2029 

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

2031 # Write the <c:strRef> element. 

2032 

2033 self._xml_start_tag("c:strRef") 

2034 

2035 # Write the c:f element. 

2036 self._write_series_formula(formula) 

2037 

2038 if ref_type == "num": 

2039 # Write the c:numCache element. 

2040 self._write_num_cache(data) 

2041 elif ref_type == "str": 

2042 # Write the c:strCache element. 

2043 self._write_str_cache(data) 

2044 

2045 self._xml_end_tag("c:strRef") 

2046 

2047 def _write_multi_lvl_str_ref(self, formula, data): 

2048 # Write the <c:multiLvlStrRef> element. 

2049 

2050 if not data: 

2051 return 

2052 

2053 self._xml_start_tag("c:multiLvlStrRef") 

2054 

2055 # Write the c:f element. 

2056 self._write_series_formula(formula) 

2057 

2058 self._xml_start_tag("c:multiLvlStrCache") 

2059 

2060 # Write the c:ptCount element. 

2061 count = len(data[-1]) 

2062 self._write_pt_count(count) 

2063 

2064 for cat_data in reversed(data): 

2065 self._xml_start_tag("c:lvl") 

2066 

2067 for i, point in enumerate(cat_data): 

2068 # Write the c:pt element. 

2069 self._write_pt(i, cat_data[i]) 

2070 

2071 self._xml_end_tag("c:lvl") 

2072 

2073 self._xml_end_tag("c:multiLvlStrCache") 

2074 self._xml_end_tag("c:multiLvlStrRef") 

2075 

2076 def _write_series_formula(self, formula): 

2077 # Write the <c:f> element. 

2078 

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

2080 if formula.startswith("="): 

2081 formula = formula.lstrip("=") 

2082 

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

2084 

2085 def _write_axis_ids(self, args): 

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

2087 

2088 # Generate the axis ids. 

2089 self._add_axis_ids(args) 

2090 

2091 if args["primary_axes"]: 

2092 # Write the axis ids for the primary axes. 

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

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

2095 else: 

2096 # Write the axis ids for the secondary axes. 

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

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

2099 

2100 def _write_axis_id(self, val): 

2101 # Write the <c:axId> element. 

2102 

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

2104 

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

2106 

2107 def _write_cat_axis(self, args): 

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

2109 x_axis = args["x_axis"] 

2110 y_axis = args["y_axis"] 

2111 axis_ids = args["axis_ids"] 

2112 

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

2114 if axis_ids is None or not len(axis_ids): 

2115 return 

2116 

2117 position = self.cat_axis_position 

2118 is_y_axis = self.horiz_cat_axis 

2119 

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

2121 if x_axis.get("position"): 

2122 position = x_axis["position"] 

2123 

2124 self._xml_start_tag("c:catAx") 

2125 

2126 self._write_axis_id(axis_ids[0]) 

2127 

2128 # Write the c:scaling element. 

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

2130 

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

2132 self._write_delete(1) 

2133 

2134 # Write the c:axPos element. 

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

2136 

2137 # Write the c:majorGridlines element. 

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

2139 

2140 # Write the c:minorGridlines element. 

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

2142 

2143 # Write the axis title elements. 

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

2145 self._write_title_formula( 

2146 x_axis["formula"], 

2147 x_axis["data_id"], 

2148 is_y_axis, 

2149 x_axis["name_font"], 

2150 x_axis["name_layout"], 

2151 ) 

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

2153 self._write_title_rich( 

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

2155 ) 

2156 

2157 # Write the c:numFmt element. 

2158 self._write_cat_number_format(x_axis) 

2159 

2160 # Write the c:majorTickMark element. 

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

2162 

2163 # Write the c:minorTickMark element. 

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

2165 

2166 # Write the c:tickLblPos element. 

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

2168 

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

2170 self._write_sp_pr(x_axis) 

2171 

2172 # Write the axis font elements. 

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

2174 

2175 # Write the c:crossAx element. 

2176 self._write_cross_axis(axis_ids[1]) 

2177 

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

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

2180 if ( 

2181 y_axis.get("crossing") is None 

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

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

2184 ): 

2185 # Write the c:crosses element. 

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

2187 else: 

2188 # Write the c:crossesAt element. 

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

2190 

2191 # Write the c:auto element. 

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

2193 self._write_auto(1) 

2194 

2195 # Write the c:labelAlign element. 

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

2197 

2198 # Write the c:labelOffset element. 

2199 self._write_label_offset(100) 

2200 

2201 # Write the c:tickLblSkip element. 

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

2203 

2204 # Write the c:tickMarkSkip element. 

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

2206 

2207 self._xml_end_tag("c:catAx") 

2208 

2209 def _write_val_axis(self, args): 

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

2211 x_axis = args["x_axis"] 

2212 y_axis = args["y_axis"] 

2213 axis_ids = args["axis_ids"] 

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

2215 is_y_axis = self.horiz_val_axis 

2216 

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

2218 if axis_ids is None or not len(axis_ids): 

2219 return 

2220 

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

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

2223 

2224 self._xml_start_tag("c:valAx") 

2225 

2226 self._write_axis_id(axis_ids[1]) 

2227 

2228 # Write the c:scaling element. 

2229 self._write_scaling( 

2230 y_axis.get("reverse"), 

2231 y_axis.get("min"), 

2232 y_axis.get("max"), 

2233 y_axis.get("log_base"), 

2234 ) 

2235 

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

2237 self._write_delete(1) 

2238 

2239 # Write the c:axPos element. 

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

2241 

2242 # Write the c:majorGridlines element. 

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

2244 

2245 # Write the c:minorGridlines element. 

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

2247 

2248 # Write the axis title elements. 

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

2250 self._write_title_formula( 

2251 y_axis["formula"], 

2252 y_axis["data_id"], 

2253 is_y_axis, 

2254 y_axis["name_font"], 

2255 y_axis["name_layout"], 

2256 ) 

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

2258 self._write_title_rich( 

2259 y_axis["name"], 

2260 is_y_axis, 

2261 y_axis.get("name_font"), 

2262 y_axis.get("name_layout"), 

2263 ) 

2264 

2265 # Write the c:numberFormat element. 

2266 self._write_number_format(y_axis) 

2267 

2268 # Write the c:majorTickMark element. 

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

2270 

2271 # Write the c:minorTickMark element. 

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

2273 

2274 # Write the c:tickLblPos element. 

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

2276 

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

2278 self._write_sp_pr(y_axis) 

2279 

2280 # Write the axis font elements. 

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

2282 

2283 # Write the c:crossAx element. 

2284 self._write_cross_axis(axis_ids[0]) 

2285 

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

2287 if ( 

2288 x_axis.get("crossing") is None 

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

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

2291 ): 

2292 # Write the c:crosses element. 

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

2294 else: 

2295 # Write the c:crossesAt element. 

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

2297 

2298 # Write the c:crossBetween element. 

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

2300 

2301 # Write the c:majorUnit element. 

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

2303 

2304 # Write the c:minorUnit element. 

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

2306 

2307 # Write the c:dispUnits element. 

2308 self._write_disp_units( 

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

2310 ) 

2311 

2312 self._xml_end_tag("c:valAx") 

2313 

2314 def _write_cat_val_axis(self, args): 

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

2316 # in scatter plots. Usually the X axis. 

2317 x_axis = args["x_axis"] 

2318 y_axis = args["y_axis"] 

2319 axis_ids = args["axis_ids"] 

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

2321 is_y_axis = self.horiz_val_axis 

2322 

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

2324 if axis_ids is None or not len(axis_ids): 

2325 return 

2326 

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

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

2329 

2330 self._xml_start_tag("c:valAx") 

2331 

2332 self._write_axis_id(axis_ids[0]) 

2333 

2334 # Write the c:scaling element. 

2335 self._write_scaling( 

2336 x_axis.get("reverse"), 

2337 x_axis.get("min"), 

2338 x_axis.get("max"), 

2339 x_axis.get("log_base"), 

2340 ) 

2341 

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

2343 self._write_delete(1) 

2344 

2345 # Write the c:axPos element. 

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

2347 

2348 # Write the c:majorGridlines element. 

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

2350 

2351 # Write the c:minorGridlines element. 

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

2353 

2354 # Write the axis title elements. 

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

2356 self._write_title_formula( 

2357 x_axis["formula"], 

2358 x_axis["data_id"], 

2359 is_y_axis, 

2360 x_axis["name_font"], 

2361 x_axis["name_layout"], 

2362 ) 

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

2364 self._write_title_rich( 

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

2366 ) 

2367 

2368 # Write the c:numberFormat element. 

2369 self._write_number_format(x_axis) 

2370 

2371 # Write the c:majorTickMark element. 

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

2373 

2374 # Write the c:minorTickMark element. 

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

2376 

2377 # Write the c:tickLblPos element. 

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

2379 

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

2381 self._write_sp_pr(x_axis) 

2382 

2383 # Write the axis font elements. 

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

2385 

2386 # Write the c:crossAx element. 

2387 self._write_cross_axis(axis_ids[1]) 

2388 

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

2390 if ( 

2391 y_axis.get("crossing") is None 

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

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

2394 ): 

2395 # Write the c:crosses element. 

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

2397 else: 

2398 # Write the c:crossesAt element. 

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

2400 

2401 # Write the c:crossBetween element. 

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

2403 

2404 # Write the c:majorUnit element. 

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

2406 

2407 # Write the c:minorUnit element. 

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

2409 

2410 # Write the c:dispUnits element. 

2411 self._write_disp_units( 

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

2413 ) 

2414 

2415 self._xml_end_tag("c:valAx") 

2416 

2417 def _write_date_axis(self, args): 

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

2419 x_axis = args["x_axis"] 

2420 y_axis = args["y_axis"] 

2421 axis_ids = args["axis_ids"] 

2422 

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

2424 if axis_ids is None or not len(axis_ids): 

2425 return 

2426 

2427 position = self.cat_axis_position 

2428 

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

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

2431 

2432 self._xml_start_tag("c:dateAx") 

2433 

2434 self._write_axis_id(axis_ids[0]) 

2435 

2436 # Write the c:scaling element. 

2437 self._write_scaling( 

2438 x_axis.get("reverse"), 

2439 x_axis.get("min"), 

2440 x_axis.get("max"), 

2441 x_axis.get("log_base"), 

2442 ) 

2443 

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

2445 self._write_delete(1) 

2446 

2447 # Write the c:axPos element. 

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

2449 

2450 # Write the c:majorGridlines element. 

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

2452 

2453 # Write the c:minorGridlines element. 

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

2455 

2456 # Write the axis title elements. 

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

2458 self._write_title_formula( 

2459 x_axis["formula"], 

2460 x_axis["data_id"], 

2461 None, 

2462 x_axis["name_font"], 

2463 x_axis["name_layout"], 

2464 ) 

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

2466 self._write_title_rich( 

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

2468 ) 

2469 

2470 # Write the c:numFmt element. 

2471 self._write_number_format(x_axis) 

2472 

2473 # Write the c:majorTickMark element. 

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

2475 

2476 # Write the c:minorTickMark element. 

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

2478 

2479 # Write the c:tickLblPos element. 

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

2481 

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

2483 self._write_sp_pr(x_axis) 

2484 

2485 # Write the axis font elements. 

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

2487 

2488 # Write the c:crossAx element. 

2489 self._write_cross_axis(axis_ids[1]) 

2490 

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

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

2493 if ( 

2494 y_axis.get("crossing") is None 

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

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

2497 ): 

2498 # Write the c:crosses element. 

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

2500 else: 

2501 # Write the c:crossesAt element. 

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

2503 

2504 # Write the c:auto element. 

2505 self._write_auto(1) 

2506 

2507 # Write the c:labelOffset element. 

2508 self._write_label_offset(100) 

2509 

2510 # Write the c:tickLblSkip element. 

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

2512 

2513 # Write the c:tickMarkSkip element. 

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

2515 

2516 # Write the c:majorUnit element. 

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

2518 

2519 # Write the c:majorTimeUnit element. 

2520 if x_axis.get("major_unit"): 

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

2522 

2523 # Write the c:minorUnit element. 

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

2525 

2526 # Write the c:minorTimeUnit element. 

2527 if x_axis.get("minor_unit"): 

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

2529 

2530 self._xml_end_tag("c:dateAx") 

2531 

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

2533 # Write the <c:scaling> element. 

2534 

2535 self._xml_start_tag("c:scaling") 

2536 

2537 # Write the c:logBase element. 

2538 self._write_c_log_base(log_base) 

2539 

2540 # Write the c:orientation element. 

2541 self._write_orientation(reverse) 

2542 

2543 # Write the c:max element. 

2544 self._write_c_max(max_val) 

2545 

2546 # Write the c:min element. 

2547 self._write_c_min(min_val) 

2548 

2549 self._xml_end_tag("c:scaling") 

2550 

2551 def _write_c_log_base(self, val): 

2552 # Write the <c:logBase> element. 

2553 

2554 if not val: 

2555 return 

2556 

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

2558 

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

2560 

2561 def _write_orientation(self, reverse): 

2562 # Write the <c:orientation> element. 

2563 val = "minMax" 

2564 

2565 if reverse: 

2566 val = "maxMin" 

2567 

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

2569 

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

2571 

2572 def _write_c_max(self, max_val): 

2573 # Write the <c:max> element. 

2574 

2575 if max_val is None: 

2576 return 

2577 

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

2579 

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

2581 

2582 def _write_c_min(self, min_val): 

2583 # Write the <c:min> element. 

2584 

2585 if min_val is None: 

2586 return 

2587 

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

2589 

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

2591 

2592 def _write_axis_pos(self, val, reverse): 

2593 # Write the <c:axPos> element. 

2594 

2595 if reverse: 

2596 if val == "l": 

2597 val = "r" 

2598 if val == "b": 

2599 val = "t" 

2600 

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

2602 

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

2604 

2605 def _write_number_format(self, axis): 

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

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

2608 # the sourceLinked attribute is 0. 

2609 # The user can override this if required. 

2610 format_code = axis.get("num_format") 

2611 source_linked = 1 

2612 

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

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

2615 source_linked = 0 

2616 

2617 # User override of sourceLinked. 

2618 if axis.get("num_format_linked"): 

2619 source_linked = 1 

2620 

2621 attributes = [ 

2622 ("formatCode", format_code), 

2623 ("sourceLinked", source_linked), 

2624 ] 

2625 

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

2627 

2628 def _write_cat_number_format(self, axis): 

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

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

2631 format_code = axis.get("num_format") 

2632 source_linked = 1 

2633 default_format = 1 

2634 

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

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

2637 source_linked = 0 

2638 default_format = 0 

2639 

2640 # User override of sourceLinked. 

2641 if axis.get("num_format_linked"): 

2642 source_linked = 1 

2643 

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

2645 if not self.cat_has_num_fmt and default_format: 

2646 return 

2647 

2648 attributes = [ 

2649 ("formatCode", format_code), 

2650 ("sourceLinked", source_linked), 

2651 ] 

2652 

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

2654 

2655 def _write_data_label_number_format(self, format_code): 

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

2657 source_linked = 0 

2658 

2659 attributes = [ 

2660 ("formatCode", format_code), 

2661 ("sourceLinked", source_linked), 

2662 ] 

2663 

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

2665 

2666 def _write_major_tick_mark(self, val): 

2667 # Write the <c:majorTickMark> element. 

2668 

2669 if not val: 

2670 return 

2671 

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

2673 

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

2675 

2676 def _write_minor_tick_mark(self, val): 

2677 # Write the <c:minorTickMark> element. 

2678 

2679 if not val: 

2680 return 

2681 

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

2683 

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

2685 

2686 def _write_tick_label_pos(self, val=None): 

2687 # Write the <c:tickLblPos> element. 

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

2689 val = "nextTo" 

2690 

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

2692 

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

2694 

2695 def _write_cross_axis(self, val): 

2696 # Write the <c:crossAx> element. 

2697 

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

2699 

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

2701 

2702 def _write_crosses(self, val=None): 

2703 # Write the <c:crosses> element. 

2704 if val is None: 

2705 val = "autoZero" 

2706 

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

2708 

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

2710 

2711 def _write_c_crosses_at(self, val): 

2712 # Write the <c:crossesAt> element. 

2713 

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

2715 

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

2717 

2718 def _write_auto(self, val): 

2719 # Write the <c:auto> element. 

2720 

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

2722 

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

2724 

2725 def _write_label_align(self, val=None): 

2726 # Write the <c:labelAlign> element. 

2727 

2728 if val is None: 

2729 val = "ctr" 

2730 

2731 if val == "right": 

2732 val = "r" 

2733 

2734 if val == "left": 

2735 val = "l" 

2736 

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

2738 

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

2740 

2741 def _write_label_offset(self, val): 

2742 # Write the <c:labelOffset> element. 

2743 

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

2745 

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

2747 

2748 def _write_c_tick_lbl_skip(self, val): 

2749 # Write the <c:tickLblSkip> element. 

2750 if val is None: 

2751 return 

2752 

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

2754 

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

2756 

2757 def _write_c_tick_mark_skip(self, val): 

2758 # Write the <c:tickMarkSkip> element. 

2759 if val is None: 

2760 return 

2761 

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

2763 

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

2765 

2766 def _write_major_gridlines(self, gridlines): 

2767 # Write the <c:majorGridlines> element. 

2768 

2769 if not gridlines: 

2770 return 

2771 

2772 if not gridlines["visible"]: 

2773 return 

2774 

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

2776 self._xml_start_tag("c:majorGridlines") 

2777 

2778 # Write the c:spPr element. 

2779 self._write_sp_pr(gridlines) 

2780 

2781 self._xml_end_tag("c:majorGridlines") 

2782 else: 

2783 self._xml_empty_tag("c:majorGridlines") 

2784 

2785 def _write_minor_gridlines(self, gridlines): 

2786 # Write the <c:minorGridlines> element. 

2787 

2788 if not gridlines: 

2789 return 

2790 

2791 if not gridlines["visible"]: 

2792 return 

2793 

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

2795 self._xml_start_tag("c:minorGridlines") 

2796 

2797 # Write the c:spPr element. 

2798 self._write_sp_pr(gridlines) 

2799 

2800 self._xml_end_tag("c:minorGridlines") 

2801 else: 

2802 self._xml_empty_tag("c:minorGridlines") 

2803 

2804 def _write_cross_between(self, val): 

2805 # Write the <c:crossBetween> element. 

2806 if val is None: 

2807 val = self.cross_between 

2808 

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

2810 

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

2812 

2813 def _write_c_major_unit(self, val): 

2814 # Write the <c:majorUnit> element. 

2815 

2816 if not val: 

2817 return 

2818 

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

2820 

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

2822 

2823 def _write_c_minor_unit(self, val): 

2824 # Write the <c:minorUnit> element. 

2825 

2826 if not val: 

2827 return 

2828 

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

2830 

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

2832 

2833 def _write_c_major_time_unit(self, val=None): 

2834 # Write the <c:majorTimeUnit> element. 

2835 if val is None: 

2836 val = "days" 

2837 

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

2839 

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

2841 

2842 def _write_c_minor_time_unit(self, val=None): 

2843 # Write the <c:minorTimeUnit> element. 

2844 if val is None: 

2845 val = "days" 

2846 

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

2848 

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

2850 

2851 def _write_legend(self): 

2852 # Write the <c:legend> element. 

2853 legend = self.legend 

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

2855 font = legend.get("font") 

2856 delete_series = [] 

2857 overlay = 0 

2858 

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

2860 delete_series = legend["delete_series"] 

2861 

2862 if position.startswith("overlay_"): 

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

2864 overlay = 1 

2865 

2866 allowed = { 

2867 "right": "r", 

2868 "left": "l", 

2869 "top": "t", 

2870 "bottom": "b", 

2871 "top_right": "tr", 

2872 } 

2873 

2874 if position == "none": 

2875 return 

2876 

2877 if position not in allowed: 

2878 return 

2879 

2880 position = allowed[position] 

2881 

2882 self._xml_start_tag("c:legend") 

2883 

2884 # Write the c:legendPos element. 

2885 self._write_legend_pos(position) 

2886 

2887 # Remove series labels from the legend. 

2888 for index in delete_series: 

2889 # Write the c:legendEntry element. 

2890 self._write_legend_entry(index) 

2891 

2892 # Write the c:layout element. 

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

2894 

2895 # Write the c:overlay element. 

2896 if overlay: 

2897 self._write_overlay() 

2898 

2899 if font: 

2900 self._write_tx_pr(font) 

2901 

2902 # Write the c:spPr element. 

2903 self._write_sp_pr(legend) 

2904 

2905 self._xml_end_tag("c:legend") 

2906 

2907 def _write_legend_pos(self, val): 

2908 # Write the <c:legendPos> element. 

2909 

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

2911 

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

2913 

2914 def _write_legend_entry(self, index): 

2915 # Write the <c:legendEntry> element. 

2916 

2917 self._xml_start_tag("c:legendEntry") 

2918 

2919 # Write the c:idx element. 

2920 self._write_idx(index) 

2921 

2922 # Write the c:delete element. 

2923 self._write_delete(1) 

2924 

2925 self._xml_end_tag("c:legendEntry") 

2926 

2927 def _write_overlay(self): 

2928 # Write the <c:overlay> element. 

2929 val = 1 

2930 

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

2932 

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

2934 

2935 def _write_plot_vis_only(self): 

2936 # Write the <c:plotVisOnly> element. 

2937 val = 1 

2938 

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

2940 if self.show_hidden: 

2941 return 

2942 

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

2944 

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

2946 

2947 def _write_print_settings(self): 

2948 # Write the <c:printSettings> element. 

2949 self._xml_start_tag("c:printSettings") 

2950 

2951 # Write the c:headerFooter element. 

2952 self._write_header_footer() 

2953 

2954 # Write the c:pageMargins element. 

2955 self._write_page_margins() 

2956 

2957 # Write the c:pageSetup element. 

2958 self._write_page_setup() 

2959 

2960 self._xml_end_tag("c:printSettings") 

2961 

2962 def _write_header_footer(self): 

2963 # Write the <c:headerFooter> element. 

2964 self._xml_empty_tag("c:headerFooter") 

2965 

2966 def _write_page_margins(self): 

2967 # Write the <c:pageMargins> element. 

2968 bottom = 0.75 

2969 left = 0.7 

2970 right = 0.7 

2971 top = 0.75 

2972 header = 0.3 

2973 footer = 0.3 

2974 

2975 attributes = [ 

2976 ("b", bottom), 

2977 ("l", left), 

2978 ("r", right), 

2979 ("t", top), 

2980 ("header", header), 

2981 ("footer", footer), 

2982 ] 

2983 

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

2985 

2986 def _write_page_setup(self): 

2987 # Write the <c:pageSetup> element. 

2988 self._xml_empty_tag("c:pageSetup") 

2989 

2990 def _write_c_auto_title_deleted(self): 

2991 # Write the <c:autoTitleDeleted> element. 

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

2993 

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

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

2996 

2997 self._xml_start_tag("c:title") 

2998 

2999 # Write the c:tx element. 

3000 self._write_tx_rich(title, is_y_axis, font) 

3001 

3002 # Write the c:layout element. 

3003 self._write_layout(layout, "text") 

3004 

3005 # Write the c:overlay element. 

3006 if overlay: 

3007 self._write_overlay() 

3008 

3009 self._xml_end_tag("c:title") 

3010 

3011 def _write_title_formula( 

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

3013 ): 

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

3015 

3016 self._xml_start_tag("c:title") 

3017 

3018 # Write the c:tx element. 

3019 self._write_tx_formula(title, data_id) 

3020 

3021 # Write the c:layout element. 

3022 self._write_layout(layout, "text") 

3023 

3024 # Write the c:overlay element. 

3025 if overlay: 

3026 self._write_overlay() 

3027 

3028 # Write the c:txPr element. 

3029 self._write_tx_pr(font, is_y_axis) 

3030 

3031 self._xml_end_tag("c:title") 

3032 

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

3034 # Write the <c:tx> element. 

3035 

3036 self._xml_start_tag("c:tx") 

3037 

3038 # Write the c:rich element. 

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

3040 

3041 self._xml_end_tag("c:tx") 

3042 

3043 def _write_tx_value(self, title): 

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

3045 

3046 self._xml_start_tag("c:tx") 

3047 

3048 # Write the c:v element. 

3049 self._write_v(title) 

3050 

3051 self._xml_end_tag("c:tx") 

3052 

3053 def _write_tx_formula(self, title, data_id): 

3054 # Write the <c:tx> element. 

3055 data = None 

3056 

3057 if data_id is not None: 

3058 data = self.formula_data[data_id] 

3059 

3060 self._xml_start_tag("c:tx") 

3061 

3062 # Write the c:strRef element. 

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

3064 

3065 self._xml_end_tag("c:tx") 

3066 

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

3068 # Write the <c:rich> element. 

3069 

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

3071 rotation = font["rotation"] 

3072 else: 

3073 rotation = None 

3074 

3075 self._xml_start_tag("c:rich") 

3076 

3077 # Write the a:bodyPr element. 

3078 self._write_a_body_pr(rotation, is_y_axis) 

3079 

3080 # Write the a:lstStyle element. 

3081 self._write_a_lst_style() 

3082 

3083 # Write the a:p element. 

3084 self._write_a_p_rich(title, font, ignore_rich_pr) 

3085 

3086 self._xml_end_tag("c:rich") 

3087 

3088 def _write_a_body_pr(self, rotation, is_y_axis): 

3089 # Write the <a:bodyPr> element. 

3090 attributes = [] 

3091 

3092 if rotation is None and is_y_axis: 

3093 rotation = -5400000 

3094 

3095 if rotation is not None: 

3096 if rotation == 16200000: 

3097 # 270 deg/stacked angle. 

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

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

3100 elif rotation == 16260000: 

3101 # 271 deg/East Asian vertical. 

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

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

3104 else: 

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

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

3107 

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

3109 

3110 def _write_a_lst_style(self): 

3111 # Write the <a:lstStyle> element. 

3112 self._xml_empty_tag("a:lstStyle") 

3113 

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

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

3116 

3117 self._xml_start_tag("a:p") 

3118 

3119 # Write the a:pPr element. 

3120 if not ignore_rich_pr: 

3121 self._write_a_p_pr_rich(font) 

3122 

3123 # Write the a:r element. 

3124 self._write_a_r(title, font) 

3125 

3126 self._xml_end_tag("a:p") 

3127 

3128 def _write_a_p_formula(self, font): 

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

3130 

3131 self._xml_start_tag("a:p") 

3132 

3133 # Write the a:pPr element. 

3134 self._write_a_p_pr_rich(font) 

3135 

3136 # Write the a:endParaRPr element. 

3137 self._write_a_end_para_rpr() 

3138 

3139 self._xml_end_tag("a:p") 

3140 

3141 def _write_a_p_pr_rich(self, font): 

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

3143 

3144 self._xml_start_tag("a:pPr") 

3145 

3146 # Write the a:defRPr element. 

3147 self._write_a_def_rpr(font) 

3148 

3149 self._xml_end_tag("a:pPr") 

3150 

3151 def _write_a_def_rpr(self, font): 

3152 # Write the <a:defRPr> element. 

3153 has_color = 0 

3154 

3155 style_attributes = Shape._get_font_style_attributes(font) 

3156 latin_attributes = Shape._get_font_latin_attributes(font) 

3157 

3158 if font and font.get("color") is not None: 

3159 has_color = 1 

3160 

3161 if latin_attributes or has_color: 

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

3163 

3164 if has_color: 

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

3166 

3167 if latin_attributes: 

3168 self._write_a_latin(latin_attributes) 

3169 

3170 self._xml_end_tag("a:defRPr") 

3171 else: 

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

3173 

3174 def _write_a_end_para_rpr(self): 

3175 # Write the <a:endParaRPr> element. 

3176 lang = "en-US" 

3177 

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

3179 

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

3181 

3182 def _write_a_r(self, title, font): 

3183 # Write the <a:r> element. 

3184 

3185 self._xml_start_tag("a:r") 

3186 

3187 # Write the a:rPr element. 

3188 self._write_a_r_pr(font) 

3189 

3190 # Write the a:t element. 

3191 self._write_a_t(title) 

3192 

3193 self._xml_end_tag("a:r") 

3194 

3195 def _write_a_r_pr(self, font): 

3196 # Write the <a:rPr> element. 

3197 has_color = 0 

3198 lang = "en-US" 

3199 

3200 style_attributes = Shape._get_font_style_attributes(font) 

3201 latin_attributes = Shape._get_font_latin_attributes(font) 

3202 

3203 if font and font["color"] is not None: 

3204 has_color = 1 

3205 

3206 # Add the lang type to the attributes. 

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

3208 

3209 if latin_attributes or has_color: 

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

3211 

3212 if has_color: 

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

3214 

3215 if latin_attributes: 

3216 self._write_a_latin(latin_attributes) 

3217 

3218 self._xml_end_tag("a:rPr") 

3219 else: 

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

3221 

3222 def _write_a_t(self, title): 

3223 # Write the <a:t> element. 

3224 

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

3226 

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

3228 # Write the <c:txPr> element. 

3229 

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

3231 rotation = font["rotation"] 

3232 else: 

3233 rotation = None 

3234 

3235 self._xml_start_tag("c:txPr") 

3236 

3237 # Write the a:bodyPr element. 

3238 self._write_a_body_pr(rotation, is_y_axis) 

3239 

3240 # Write the a:lstStyle element. 

3241 self._write_a_lst_style() 

3242 

3243 # Write the a:p element. 

3244 self._write_a_p_formula(font) 

3245 

3246 self._xml_end_tag("c:txPr") 

3247 

3248 def _write_marker(self, marker): 

3249 # Write the <c:marker> element. 

3250 if marker is None: 

3251 marker = self.default_marker 

3252 

3253 if not marker: 

3254 return 

3255 

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

3257 return 

3258 

3259 self._xml_start_tag("c:marker") 

3260 

3261 # Write the c:symbol element. 

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

3263 

3264 # Write the c:size element. 

3265 if marker.get("size"): 

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

3267 

3268 # Write the c:spPr element. 

3269 self._write_sp_pr(marker) 

3270 

3271 self._xml_end_tag("c:marker") 

3272 

3273 def _write_marker_size(self, val): 

3274 # Write the <c:size> element. 

3275 

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

3277 

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

3279 

3280 def _write_symbol(self, val): 

3281 # Write the <c:symbol> element. 

3282 

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

3284 

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

3286 

3287 def _write_sp_pr(self, series): 

3288 # Write the <c:spPr> element. 

3289 

3290 if not self._has_fill_formatting(series): 

3291 return 

3292 

3293 self._xml_start_tag("c:spPr") 

3294 

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

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

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

3298 # Write the a:noFill element. 

3299 self._write_a_no_fill() 

3300 else: 

3301 # Write the a:solidFill element. 

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

3303 

3304 if series.get("pattern"): 

3305 # Write the a:gradFill element. 

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

3307 

3308 if series.get("gradient"): 

3309 # Write the a:gradFill element. 

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

3311 

3312 # Write the a:ln element. 

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

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

3315 

3316 self._xml_end_tag("c:spPr") 

3317 

3318 def _write_a_ln(self, line): 

3319 # Write the <a:ln> element. 

3320 attributes = [] 

3321 

3322 # Add the line width as an attribute. 

3323 width = line.get("width") 

3324 

3325 if width is not None: 

3326 # Round width to nearest 0.25, like Excel. 

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

3328 

3329 # Convert to internal units. 

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

3331 

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

3333 

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

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

3336 

3337 # Write the line fill. 

3338 if "none" in line: 

3339 # Write the a:noFill element. 

3340 self._write_a_no_fill() 

3341 elif "color" in line: 

3342 # Write the a:solidFill element. 

3343 self._write_a_solid_fill(line) 

3344 

3345 # Write the line/dash type. 

3346 line_type = line.get("dash_type") 

3347 if line_type: 

3348 # Write the a:prstDash element. 

3349 self._write_a_prst_dash(line_type) 

3350 

3351 self._xml_end_tag("a:ln") 

3352 else: 

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

3354 

3355 def _write_a_no_fill(self): 

3356 # Write the <a:noFill> element. 

3357 self._xml_empty_tag("a:noFill") 

3358 

3359 def _write_a_solid_fill(self, fill): 

3360 # Write the <a:solidFill> element. 

3361 

3362 self._xml_start_tag("a:solidFill") 

3363 

3364 if "color" in fill: 

3365 color = get_rgb_color(fill["color"]) 

3366 transparency = fill.get("transparency") 

3367 # Write the a:srgbClr element. 

3368 self._write_a_srgb_clr(color, transparency) 

3369 

3370 self._xml_end_tag("a:solidFill") 

3371 

3372 def _write_a_srgb_clr(self, val, transparency=None): 

3373 # Write the <a:srgbClr> element. 

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

3375 

3376 if transparency: 

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

3378 

3379 # Write the a:alpha element. 

3380 self._write_a_alpha(transparency) 

3381 

3382 self._xml_end_tag("a:srgbClr") 

3383 else: 

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

3385 

3386 def _write_a_alpha(self, val): 

3387 # Write the <a:alpha> element. 

3388 

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

3390 

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

3392 

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

3394 

3395 def _write_a_prst_dash(self, val): 

3396 # Write the <a:prstDash> element. 

3397 

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

3399 

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

3401 

3402 def _write_trendline(self, trendline): 

3403 # Write the <c:trendline> element. 

3404 

3405 if not trendline: 

3406 return 

3407 

3408 self._xml_start_tag("c:trendline") 

3409 

3410 # Write the c:name element. 

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

3412 

3413 # Write the c:spPr element. 

3414 self._write_sp_pr(trendline) 

3415 

3416 # Write the c:trendlineType element. 

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

3418 

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

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

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

3422 

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

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

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

3426 

3427 # Write the c:forward element. 

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

3429 

3430 # Write the c:backward element. 

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

3432 

3433 if "intercept" in trendline: 

3434 # Write the c:intercept element. 

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

3436 

3437 if trendline.get("display_r_squared"): 

3438 # Write the c:dispRSqr element. 

3439 self._write_c_disp_rsqr() 

3440 

3441 if trendline.get("display_equation"): 

3442 # Write the c:dispEq element. 

3443 self._write_c_disp_eq() 

3444 

3445 # Write the c:trendlineLbl element. 

3446 self._write_c_trendline_lbl(trendline) 

3447 

3448 self._xml_end_tag("c:trendline") 

3449 

3450 def _write_trendline_type(self, val): 

3451 # Write the <c:trendlineType> element. 

3452 

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

3454 

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

3456 

3457 def _write_name(self, data): 

3458 # Write the <c:name> element. 

3459 

3460 if data is None: 

3461 return 

3462 

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

3464 

3465 def _write_trendline_order(self, val): 

3466 # Write the <c:order> element. 

3467 if val < 2: 

3468 val = 2 

3469 

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

3471 

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

3473 

3474 def _write_period(self, val): 

3475 # Write the <c:period> element. 

3476 if val < 2: 

3477 val = 2 

3478 

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

3480 

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

3482 

3483 def _write_forward(self, val): 

3484 # Write the <c:forward> element. 

3485 

3486 if not val: 

3487 return 

3488 

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

3490 

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

3492 

3493 def _write_backward(self, val): 

3494 # Write the <c:backward> element. 

3495 

3496 if not val: 

3497 return 

3498 

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

3500 

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

3502 

3503 def _write_c_intercept(self, val): 

3504 # Write the <c:intercept> element. 

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

3506 

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

3508 

3509 def _write_c_disp_eq(self): 

3510 # Write the <c:dispEq> element. 

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

3512 

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

3514 

3515 def _write_c_disp_rsqr(self): 

3516 # Write the <c:dispRSqr> element. 

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

3518 

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

3520 

3521 def _write_c_trendline_lbl(self, trendline): 

3522 # Write the <c:trendlineLbl> element. 

3523 self._xml_start_tag("c:trendlineLbl") 

3524 

3525 # Write the c:layout element. 

3526 self._write_layout(None, None) 

3527 

3528 # Write the c:numFmt element. 

3529 self._write_trendline_num_fmt() 

3530 

3531 # Write the c:spPr element. 

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

3533 

3534 # Write the data label font elements. 

3535 if trendline["label"]: 

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

3537 if font: 

3538 self._write_axis_font(font) 

3539 

3540 self._xml_end_tag("c:trendlineLbl") 

3541 

3542 def _write_trendline_num_fmt(self): 

3543 # Write the <c:numFmt> element. 

3544 attributes = [ 

3545 ("formatCode", "General"), 

3546 ("sourceLinked", 0), 

3547 ] 

3548 

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

3550 

3551 def _write_hi_low_lines(self): 

3552 # Write the <c:hiLowLines> element. 

3553 hi_low_lines = self.hi_low_lines 

3554 

3555 if hi_low_lines is None: 

3556 return 

3557 

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

3559 self._xml_start_tag("c:hiLowLines") 

3560 

3561 # Write the c:spPr element. 

3562 self._write_sp_pr(hi_low_lines) 

3563 

3564 self._xml_end_tag("c:hiLowLines") 

3565 else: 

3566 self._xml_empty_tag("c:hiLowLines") 

3567 

3568 def _write_drop_lines(self): 

3569 # Write the <c:dropLines> element. 

3570 drop_lines = self.drop_lines 

3571 

3572 if drop_lines is None: 

3573 return 

3574 

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

3576 self._xml_start_tag("c:dropLines") 

3577 

3578 # Write the c:spPr element. 

3579 self._write_sp_pr(drop_lines) 

3580 

3581 self._xml_end_tag("c:dropLines") 

3582 else: 

3583 self._xml_empty_tag("c:dropLines") 

3584 

3585 def _write_overlap(self, val): 

3586 # Write the <c:overlap> element. 

3587 

3588 if val is None: 

3589 return 

3590 

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

3592 

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

3594 

3595 def _write_num_cache(self, data): 

3596 # Write the <c:numCache> element. 

3597 if data: 

3598 count = len(data) 

3599 else: 

3600 count = 0 

3601 

3602 self._xml_start_tag("c:numCache") 

3603 

3604 # Write the c:formatCode element. 

3605 self._write_format_code("General") 

3606 

3607 # Write the c:ptCount element. 

3608 self._write_pt_count(count) 

3609 

3610 for i in range(count): 

3611 token = data[i] 

3612 

3613 if token is None: 

3614 continue 

3615 

3616 try: 

3617 float(token) 

3618 except ValueError: 

3619 # Write non-numeric data as 0. 

3620 token = 0 

3621 

3622 # Write the c:pt element. 

3623 self._write_pt(i, token) 

3624 

3625 self._xml_end_tag("c:numCache") 

3626 

3627 def _write_str_cache(self, data): 

3628 # Write the <c:strCache> element. 

3629 count = len(data) 

3630 

3631 self._xml_start_tag("c:strCache") 

3632 

3633 # Write the c:ptCount element. 

3634 self._write_pt_count(count) 

3635 

3636 for i in range(count): 

3637 # Write the c:pt element. 

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

3639 

3640 self._xml_end_tag("c:strCache") 

3641 

3642 def _write_format_code(self, data): 

3643 # Write the <c:formatCode> element. 

3644 

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

3646 

3647 def _write_pt_count(self, val): 

3648 # Write the <c:ptCount> element. 

3649 

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

3651 

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

3653 

3654 def _write_pt(self, idx, value): 

3655 # Write the <c:pt> element. 

3656 

3657 if value is None: 

3658 return 

3659 

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

3661 

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

3663 

3664 # Write the c:v element. 

3665 self._write_v(value) 

3666 

3667 self._xml_end_tag("c:pt") 

3668 

3669 def _write_v(self, data): 

3670 # Write the <c:v> element. 

3671 

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

3673 

3674 def _write_protection(self): 

3675 # Write the <c:protection> element. 

3676 if not self.protection: 

3677 return 

3678 

3679 self._xml_empty_tag("c:protection") 

3680 

3681 def _write_d_pt(self, points): 

3682 # Write the <c:dPt> elements. 

3683 index = -1 

3684 

3685 if not points: 

3686 return 

3687 

3688 for point in points: 

3689 index += 1 

3690 if not point: 

3691 continue 

3692 

3693 self._write_d_pt_point(index, point) 

3694 

3695 def _write_d_pt_point(self, index, point): 

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

3697 

3698 self._xml_start_tag("c:dPt") 

3699 

3700 # Write the c:idx element. 

3701 self._write_idx(index) 

3702 

3703 # Write the c:spPr element. 

3704 self._write_sp_pr(point) 

3705 

3706 self._xml_end_tag("c:dPt") 

3707 

3708 def _write_d_lbls(self, labels): 

3709 # Write the <c:dLbls> element. 

3710 

3711 if not labels: 

3712 return 

3713 

3714 self._xml_start_tag("c:dLbls") 

3715 

3716 # Write the custom c:dLbl elements. 

3717 if labels.get("custom"): 

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

3719 

3720 # Write the c:numFmt element. 

3721 if labels.get("num_format"): 

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

3723 

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

3725 self._write_sp_pr(labels) 

3726 

3727 # Write the data label font elements. 

3728 if labels.get("font"): 

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

3730 

3731 # Write the c:dLblPos element. 

3732 if labels.get("position"): 

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

3734 

3735 # Write the c:showLegendKey element. 

3736 if labels.get("legend_key"): 

3737 self._write_show_legend_key() 

3738 

3739 # Write the c:showVal element. 

3740 if labels.get("value"): 

3741 self._write_show_val() 

3742 

3743 # Write the c:showCatName element. 

3744 if labels.get("category"): 

3745 self._write_show_cat_name() 

3746 

3747 # Write the c:showSerName element. 

3748 if labels.get("series_name"): 

3749 self._write_show_ser_name() 

3750 

3751 # Write the c:showPercent element. 

3752 if labels.get("percentage"): 

3753 self._write_show_percent() 

3754 

3755 # Write the c:separator element. 

3756 if labels.get("separator"): 

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

3758 

3759 # Write the c:showLeaderLines element. 

3760 if labels.get("leader_lines"): 

3761 self._write_show_leader_lines() 

3762 

3763 self._xml_end_tag("c:dLbls") 

3764 

3765 def _write_custom_labels(self, parent, labels): 

3766 # Write the <c:showLegendKey> element. 

3767 index = 0 

3768 

3769 for label in labels: 

3770 index += 1 

3771 

3772 if label is None: 

3773 continue 

3774 

3775 self._xml_start_tag("c:dLbl") 

3776 

3777 # Write the c:idx element. 

3778 self._write_idx(index - 1) 

3779 

3780 delete_label = label.get("delete") 

3781 

3782 if delete_label: 

3783 self._write_delete(1) 

3784 

3785 elif label.get("formula"): 

3786 self._write_custom_label_formula(label) 

3787 

3788 if parent.get("position"): 

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

3790 

3791 if parent.get("value"): 

3792 self._write_show_val() 

3793 if parent.get("category"): 

3794 self._write_show_cat_name() 

3795 if parent.get("series_name"): 

3796 self._write_show_ser_name() 

3797 

3798 elif label.get("value"): 

3799 self._write_custom_label_str(label) 

3800 

3801 if parent.get("position"): 

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

3803 

3804 if parent.get("value"): 

3805 self._write_show_val() 

3806 if parent.get("category"): 

3807 self._write_show_cat_name() 

3808 if parent.get("series_name"): 

3809 self._write_show_ser_name() 

3810 else: 

3811 self._write_custom_label_format_only(label) 

3812 

3813 self._xml_end_tag("c:dLbl") 

3814 

3815 def _write_custom_label_str(self, label): 

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

3817 title = label.get("value") 

3818 font = label.get("font") 

3819 has_formatting = self._has_fill_formatting(label) 

3820 

3821 # Write the c:layout element. 

3822 self._write_layout(None, None) 

3823 

3824 self._xml_start_tag("c:tx") 

3825 

3826 # Write the c:rich element. 

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

3828 

3829 self._xml_end_tag("c:tx") 

3830 

3831 # Write the c:spPr element. 

3832 self._write_sp_pr(label) 

3833 

3834 def _write_custom_label_formula(self, label): 

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

3836 formula = label.get("formula") 

3837 data_id = label.get("data_id") 

3838 data = None 

3839 

3840 if data_id is not None: 

3841 data = self.formula_data[data_id] 

3842 

3843 # Write the c:layout element. 

3844 self._write_layout(None, None) 

3845 

3846 self._xml_start_tag("c:tx") 

3847 

3848 # Write the c:strRef element. 

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

3850 

3851 self._xml_end_tag("c:tx") 

3852 

3853 # Write the data label formatting, if any. 

3854 self._write_custom_label_format_only(label) 

3855 

3856 def _write_custom_label_format_only(self, label): 

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

3858 font = label.get("font") 

3859 has_formatting = self._has_fill_formatting(label) 

3860 

3861 if has_formatting: 

3862 self._write_sp_pr(label) 

3863 self._write_tx_pr(font) 

3864 elif font: 

3865 self._xml_empty_tag("c:spPr") 

3866 self._write_tx_pr(font) 

3867 

3868 def _write_show_legend_key(self): 

3869 # Write the <c:showLegendKey> element. 

3870 val = "1" 

3871 

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

3873 

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

3875 

3876 def _write_show_val(self): 

3877 # Write the <c:showVal> element. 

3878 val = 1 

3879 

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

3881 

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

3883 

3884 def _write_show_cat_name(self): 

3885 # Write the <c:showCatName> element. 

3886 val = 1 

3887 

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

3889 

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

3891 

3892 def _write_show_ser_name(self): 

3893 # Write the <c:showSerName> element. 

3894 val = 1 

3895 

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

3897 

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

3899 

3900 def _write_show_percent(self): 

3901 # Write the <c:showPercent> element. 

3902 val = 1 

3903 

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

3905 

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

3907 

3908 def _write_separator(self, data): 

3909 # Write the <c:separator> element. 

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

3911 

3912 def _write_show_leader_lines(self): 

3913 # Write the <c:showLeaderLines> element. 

3914 # 

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

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

3917 # 

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

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

3920 

3921 attributes = [ 

3922 ("uri", uri), 

3923 ("xmlns:c15", xmlns_c_15), 

3924 ] 

3925 

3926 self._xml_start_tag("c:extLst") 

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

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

3929 self._xml_end_tag("c:ext") 

3930 self._xml_end_tag("c:extLst") 

3931 

3932 def _write_d_lbl_pos(self, val): 

3933 # Write the <c:dLblPos> element. 

3934 

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

3936 

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

3938 

3939 def _write_delete(self, val): 

3940 # Write the <c:delete> element. 

3941 

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

3943 

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

3945 

3946 def _write_c_invert_if_negative(self, invert): 

3947 # Write the <c:invertIfNegative> element. 

3948 val = 1 

3949 

3950 if not invert: 

3951 return 

3952 

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

3954 

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

3956 

3957 def _write_axis_font(self, font): 

3958 # Write the axis font elements. 

3959 

3960 if not font: 

3961 return 

3962 

3963 self._xml_start_tag("c:txPr") 

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

3965 self._write_a_lst_style() 

3966 self._xml_start_tag("a:p") 

3967 

3968 self._write_a_p_pr_rich(font) 

3969 

3970 self._write_a_end_para_rpr() 

3971 self._xml_end_tag("a:p") 

3972 self._xml_end_tag("c:txPr") 

3973 

3974 def _write_a_latin(self, attributes): 

3975 # Write the <a:latin> element. 

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

3977 

3978 def _write_d_table(self): 

3979 # Write the <c:dTable> element. 

3980 table = self.table 

3981 

3982 if not table: 

3983 return 

3984 

3985 self._xml_start_tag("c:dTable") 

3986 

3987 if table["horizontal"]: 

3988 # Write the c:showHorzBorder element. 

3989 self._write_show_horz_border() 

3990 

3991 if table["vertical"]: 

3992 # Write the c:showVertBorder element. 

3993 self._write_show_vert_border() 

3994 

3995 if table["outline"]: 

3996 # Write the c:showOutline element. 

3997 self._write_show_outline() 

3998 

3999 if table["show_keys"]: 

4000 # Write the c:showKeys element. 

4001 self._write_show_keys() 

4002 

4003 if table["font"]: 

4004 # Write the table font. 

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

4006 

4007 self._xml_end_tag("c:dTable") 

4008 

4009 def _write_show_horz_border(self): 

4010 # Write the <c:showHorzBorder> element. 

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

4012 

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

4014 

4015 def _write_show_vert_border(self): 

4016 # Write the <c:showVertBorder> element. 

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

4018 

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

4020 

4021 def _write_show_outline(self): 

4022 # Write the <c:showOutline> element. 

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

4024 

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

4026 

4027 def _write_show_keys(self): 

4028 # Write the <c:showKeys> element. 

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

4030 

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

4032 

4033 def _write_error_bars(self, error_bars): 

4034 # Write the X and Y error bars. 

4035 

4036 if not error_bars: 

4037 return 

4038 

4039 if error_bars["x_error_bars"]: 

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

4041 

4042 if error_bars["y_error_bars"]: 

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

4044 

4045 def _write_err_bars(self, direction, error_bars): 

4046 # Write the <c:errBars> element. 

4047 

4048 if not error_bars: 

4049 return 

4050 

4051 self._xml_start_tag("c:errBars") 

4052 

4053 # Write the c:errDir element. 

4054 self._write_err_dir(direction) 

4055 

4056 # Write the c:errBarType element. 

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

4058 

4059 # Write the c:errValType element. 

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

4061 

4062 if not error_bars["endcap"]: 

4063 # Write the c:noEndCap element. 

4064 self._write_no_end_cap() 

4065 

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

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

4068 pass 

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

4070 # Write the custom error tags. 

4071 self._write_custom_error(error_bars) 

4072 else: 

4073 # Write the c:val element. 

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

4075 

4076 # Write the c:spPr element. 

4077 self._write_sp_pr(error_bars) 

4078 

4079 self._xml_end_tag("c:errBars") 

4080 

4081 def _write_err_dir(self, val): 

4082 # Write the <c:errDir> element. 

4083 

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

4085 

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

4087 

4088 def _write_err_bar_type(self, val): 

4089 # Write the <c:errBarType> element. 

4090 

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

4092 

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

4094 

4095 def _write_err_val_type(self, val): 

4096 # Write the <c:errValType> element. 

4097 

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

4099 

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

4101 

4102 def _write_no_end_cap(self): 

4103 # Write the <c:noEndCap> element. 

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

4105 

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

4107 

4108 def _write_error_val(self, val): 

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

4110 

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

4112 

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

4114 

4115 def _write_custom_error(self, error_bars): 

4116 # Write the custom error bars tags. 

4117 

4118 if error_bars["plus_values"]: 

4119 # Write the c:plus element. 

4120 self._xml_start_tag("c:plus") 

4121 

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

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

4124 else: 

4125 self._write_num_ref( 

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

4127 ) 

4128 self._xml_end_tag("c:plus") 

4129 

4130 if error_bars["minus_values"]: 

4131 # Write the c:minus element. 

4132 self._xml_start_tag("c:minus") 

4133 

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

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

4136 else: 

4137 self._write_num_ref( 

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

4139 ) 

4140 self._xml_end_tag("c:minus") 

4141 

4142 def _write_num_lit(self, data): 

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

4144 count = len(data) 

4145 

4146 # Write the c:numLit element. 

4147 self._xml_start_tag("c:numLit") 

4148 

4149 # Write the c:formatCode element. 

4150 self._write_format_code("General") 

4151 

4152 # Write the c:ptCount element. 

4153 self._write_pt_count(count) 

4154 

4155 for i in range(count): 

4156 token = data[i] 

4157 

4158 if token is None: 

4159 continue 

4160 

4161 try: 

4162 float(token) 

4163 except ValueError: 

4164 # Write non-numeric data as 0. 

4165 token = 0 

4166 

4167 # Write the c:pt element. 

4168 self._write_pt(i, token) 

4169 

4170 self._xml_end_tag("c:numLit") 

4171 

4172 def _write_up_down_bars(self): 

4173 # Write the <c:upDownBars> element. 

4174 up_down_bars = self.up_down_bars 

4175 

4176 if up_down_bars is None: 

4177 return 

4178 

4179 self._xml_start_tag("c:upDownBars") 

4180 

4181 # Write the c:gapWidth element. 

4182 self._write_gap_width(150) 

4183 

4184 # Write the c:upBars element. 

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

4186 

4187 # Write the c:downBars element. 

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

4189 

4190 self._xml_end_tag("c:upDownBars") 

4191 

4192 def _write_gap_width(self, val): 

4193 # Write the <c:gapWidth> element. 

4194 

4195 if val is None: 

4196 return 

4197 

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

4199 

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

4201 

4202 def _write_up_bars(self, bar_format): 

4203 # Write the <c:upBars> element. 

4204 

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

4206 self._xml_start_tag("c:upBars") 

4207 

4208 # Write the c:spPr element. 

4209 self._write_sp_pr(bar_format) 

4210 

4211 self._xml_end_tag("c:upBars") 

4212 else: 

4213 self._xml_empty_tag("c:upBars") 

4214 

4215 def _write_down_bars(self, bar_format): 

4216 # Write the <c:downBars> element. 

4217 

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

4219 self._xml_start_tag("c:downBars") 

4220 

4221 # Write the c:spPr element. 

4222 self._write_sp_pr(bar_format) 

4223 

4224 self._xml_end_tag("c:downBars") 

4225 else: 

4226 self._xml_empty_tag("c:downBars") 

4227 

4228 def _write_disp_units(self, units, display): 

4229 # Write the <c:dispUnits> element. 

4230 

4231 if not units: 

4232 return 

4233 

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

4235 

4236 self._xml_start_tag("c:dispUnits") 

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

4238 

4239 if display: 

4240 self._xml_start_tag("c:dispUnitsLbl") 

4241 self._xml_empty_tag("c:layout") 

4242 self._xml_end_tag("c:dispUnitsLbl") 

4243 

4244 self._xml_end_tag("c:dispUnits") 

4245 

4246 def _write_a_grad_fill(self, gradient): 

4247 # Write the <a:gradFill> element. 

4248 

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

4250 

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

4252 attributes = [] 

4253 

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

4255 

4256 # Write the a:gsLst element. 

4257 self._write_a_gs_lst(gradient) 

4258 

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

4260 # Write the a:lin element. 

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

4262 else: 

4263 # Write the a:path element. 

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

4265 

4266 # Write the a:tileRect element. 

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

4268 

4269 self._xml_end_tag("a:gradFill") 

4270 

4271 def _write_a_gs_lst(self, gradient): 

4272 # Write the <a:gsLst> element. 

4273 positions = gradient["positions"] 

4274 colors = gradient["colors"] 

4275 

4276 self._xml_start_tag("a:gsLst") 

4277 

4278 for i in range(len(colors)): 

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

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

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

4282 

4283 # Write the a:srgbClr element. 

4284 color = get_rgb_color(colors[i]) 

4285 self._write_a_srgb_clr(color) 

4286 

4287 self._xml_end_tag("a:gs") 

4288 

4289 self._xml_end_tag("a:gsLst") 

4290 

4291 def _write_a_lin(self, angle): 

4292 # Write the <a:lin> element. 

4293 

4294 angle = int(60000 * angle) 

4295 

4296 attributes = [ 

4297 ("ang", angle), 

4298 ("scaled", "0"), 

4299 ] 

4300 

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

4302 

4303 def _write_a_path(self, gradient_type): 

4304 # Write the <a:path> element. 

4305 

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

4307 

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

4309 

4310 # Write the a:fillToRect element. 

4311 self._write_a_fill_to_rect(gradient_type) 

4312 

4313 self._xml_end_tag("a:path") 

4314 

4315 def _write_a_fill_to_rect(self, gradient_type): 

4316 # Write the <a:fillToRect> element. 

4317 

4318 if gradient_type == "shape": 

4319 attributes = [ 

4320 ("l", "50000"), 

4321 ("t", "50000"), 

4322 ("r", "50000"), 

4323 ("b", "50000"), 

4324 ] 

4325 else: 

4326 attributes = [ 

4327 ("l", "100000"), 

4328 ("t", "100000"), 

4329 ] 

4330 

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

4332 

4333 def _write_a_tile_rect(self, gradient_type): 

4334 # Write the <a:tileRect> element. 

4335 

4336 if gradient_type == "shape": 

4337 attributes = [] 

4338 else: 

4339 attributes = [ 

4340 ("r", "-100000"), 

4341 ("b", "-100000"), 

4342 ] 

4343 

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

4345 

4346 def _write_a_patt_fill(self, pattern): 

4347 # Write the <a:pattFill> element. 

4348 

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

4350 

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

4352 

4353 # Write the a:fgClr element. 

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

4355 

4356 # Write the a:bgClr element. 

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

4358 

4359 self._xml_end_tag("a:pattFill") 

4360 

4361 def _write_a_fg_clr(self, color): 

4362 # Write the <a:fgClr> element. 

4363 

4364 color = get_rgb_color(color) 

4365 

4366 self._xml_start_tag("a:fgClr") 

4367 

4368 # Write the a:srgbClr element. 

4369 self._write_a_srgb_clr(color) 

4370 

4371 self._xml_end_tag("a:fgClr") 

4372 

4373 def _write_a_bg_clr(self, color): 

4374 # Write the <a:bgClr> element. 

4375 

4376 color = get_rgb_color(color) 

4377 

4378 self._xml_start_tag("a:bgClr") 

4379 

4380 # Write the a:srgbClr element. 

4381 self._write_a_srgb_clr(color) 

4382 

4383 self._xml_end_tag("a:bgClr")