Skip to content

Commit 5527725

Browse files
committed
chart: add chart title formatting options
Add border, fill, gradient and pattern formatting options for chart titles. Request #957
1 parent 65dd8eb commit 5527725

File tree

10 files changed

+268
-25
lines changed

10 files changed

+268
-25
lines changed

dev/docs/source/chart.rst

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -720,9 +720,21 @@ The properties that can be set are:
720720
with a sheetname, row and column such as ``['Sheet1', 0, 0]``. The name
721721
property is optional. The default is to have no chart title.
722722

723-
* ``name_font``: Set the font properties for the chart title. See
723+
* ``font``: Set the font properties for the chart title. See
724724
:ref:`chart_fonts`.
725725

726+
* ``border``: Set the border properties of the legend such as color and
727+
style. See :ref:`chart_formatting_border`.
728+
729+
* ``fill``: Set the solid fill properties of the legend such as color. See
730+
:ref:`chart_formatting_fill`.
731+
732+
* ``pattern``: Set the pattern fill properties of the legend. See
733+
:ref:`chart_formatting_pattern`.
734+
735+
* ``gradient``: Set the gradient fill properties of the legend. See
736+
:ref:`chart_formatting_gradient`.
737+
726738
* ``overlay``: Allow the title to be overlaid on the chart. Generally used
727739
with the layout property below.
728740

xlsxwriter/chart.py

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -338,9 +338,22 @@ def set_title(self, options: Optional[Dict[str, Any]] = None) -> None:
338338
self.title.data_id = data_id
339339

340340
# Set the font properties if present.
341-
self.title.font = self._convert_font_args(options.get("name_font"))
341+
if options.get("font"):
342+
self.title.font = self._convert_font_args(options.get("font"))
343+
else:
344+
# For backward/axis compatibility.
345+
self.title.font = self._convert_font_args(options.get("name_font"))
346+
347+
# Set the line properties.
348+
self.title.line = Shape._get_line_properties(options)
349+
350+
# Set the fill properties.
351+
self.title.fill = Shape._get_fill_properties(options.get("fill"))
342352

343-
# Set the axis name layout.
353+
# Set the gradient properties.
354+
self.title.gradient = Shape._get_gradient_properties(options.get("gradient"))
355+
356+
# Set the layout.
344357
self.title.layout = self._get_layout_properties(options.get("layout"), True)
345358

346359
# Set the title overlay option.
@@ -2901,10 +2914,11 @@ def _write_title(self, title: ChartTitle, is_horizontal: bool = False) -> None:
29012914
self._write_title_rich(title, is_horizontal)
29022915
elif title.has_formula():
29032916
self._write_title_formula(title, is_horizontal)
2917+
elif title.has_formatting():
2918+
self._write_title_format_only(title)
29042919

29052920
def _write_title_rich(self, title: ChartTitle, is_horizontal: bool = False) -> None:
29062921
# Write the <c:title> element for a rich string.
2907-
29082922
self._xml_start_tag("c:title")
29092923

29102924
# Write the c:tx element.
@@ -2917,13 +2931,15 @@ def _write_title_rich(self, title: ChartTitle, is_horizontal: bool = False) -> N
29172931
if title.overlay:
29182932
self._write_overlay()
29192933

2934+
# Write the c:spPr element.
2935+
self._write_sp_pr(title.get_formatting())
2936+
29202937
self._xml_end_tag("c:title")
29212938

29222939
def _write_title_formula(
29232940
self, title: ChartTitle, is_horizontal: bool = False
29242941
) -> None:
29252942
# Write the <c:title> element for a rich string.
2926-
29272943
self._xml_start_tag("c:title")
29282944

29292945
# Write the c:tx element.
@@ -2936,11 +2952,30 @@ def _write_title_formula(
29362952
if title.overlay:
29372953
self._write_overlay()
29382954

2955+
# Write the c:spPr element.
2956+
self._write_sp_pr(title.get_formatting())
2957+
29392958
# Write the c:txPr element.
29402959
self._write_tx_pr(title.font, is_horizontal)
29412960

29422961
self._xml_end_tag("c:title")
29432962

2963+
def _write_title_format_only(self, title: ChartTitle) -> None:
2964+
# Write the <c:title> element title with formatting and default name.
2965+
self._xml_start_tag("c:title")
2966+
2967+
# Write the c:layout element.
2968+
self._write_layout(title.layout, "text")
2969+
2970+
# Write the c:overlay element.
2971+
if title.overlay:
2972+
self._write_overlay()
2973+
2974+
# Write the c:spPr element.
2975+
self._write_sp_pr(title.get_formatting())
2976+
2977+
self._xml_end_tag("c:title")
2978+
29442979
def _write_tx_rich(self, title, is_horizontal, font) -> None:
29452980
# Write the <c:tx> element.
29462981

@@ -3195,34 +3230,33 @@ def _write_symbol(self, val) -> None:
31953230

31963231
self._xml_empty_tag("c:symbol", attributes)
31973232

3198-
def _write_sp_pr(self, series) -> None:
3233+
def _write_sp_pr(self, chart_format: dict) -> None:
31993234
# Write the <c:spPr> element.
3200-
3201-
if not self._has_formatting(series):
3235+
if not self._has_formatting(chart_format):
32023236
return
32033237

32043238
self._xml_start_tag("c:spPr")
32053239

32063240
# Write the fill elements for solid charts such as pie and bar.
3207-
if series.get("fill") and series["fill"]["defined"]:
3208-
if "none" in series["fill"]:
3241+
if chart_format.get("fill") and chart_format["fill"]["defined"]:
3242+
if "none" in chart_format["fill"]:
32093243
# Write the a:noFill element.
32103244
self._write_a_no_fill()
32113245
else:
32123246
# Write the a:solidFill element.
3213-
self._write_a_solid_fill(series["fill"])
3247+
self._write_a_solid_fill(chart_format["fill"])
32143248

3215-
if series.get("pattern"):
3249+
if chart_format.get("pattern"):
32163250
# Write the a:gradFill element.
3217-
self._write_a_patt_fill(series["pattern"])
3251+
self._write_a_patt_fill(chart_format["pattern"])
32183252

3219-
if series.get("gradient"):
3253+
if chart_format.get("gradient"):
32203254
# Write the a:gradFill element.
3221-
self._write_a_grad_fill(series["gradient"])
3255+
self._write_a_grad_fill(chart_format["gradient"])
32223256

32233257
# Write the a:ln element.
3224-
if series.get("line") and series["line"]["defined"]:
3225-
self._write_a_ln(series["line"])
3258+
if chart_format.get("line") and chart_format["line"]["defined"]:
3259+
self._write_a_ln(chart_format["line"])
32263260

32273261
self._xml_end_tag("c:spPr")
32283262

xlsxwriter/chart_title.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def __init__(self) -> None:
2929
self.layout: Optional[Dict[str, Any]] = None
3030
self.overlay: Optional[bool] = None
3131
self.hidden: bool = False
32+
self.line: Optional[Dict[str, Any]] = None
3233
self.fill: Optional[Dict[str, Any]] = None
3334
self.pattern: Optional[Dict[str, Any]] = None
3435
self.gradient: Optional[Dict[str, Any]] = None
@@ -56,14 +57,28 @@ def has_formatting(self) -> bool:
5657
Check if the title has any formatting properties set.
5758
5859
Returns:
59-
True if the title has font, fill, pattern, or gradient formatting.
60+
True if the title has line, fill, pattern, or gradient formatting.
6061
"""
61-
has_font = self.font is not None
62+
has_line = self.line is not None and self.line.get("defined", False)
6263
has_fill = self.fill is not None and self.fill.get("defined", False)
6364
has_pattern = self.pattern is not None
6465
has_gradient = self.gradient is not None
6566

66-
return has_font or has_fill or has_pattern or has_gradient
67+
return has_line or has_fill or has_pattern or has_gradient
68+
69+
def get_formatting(self) -> Dict[str, Any]:
70+
"""
71+
Get a dictionary containing the formatting properties.
72+
73+
Returns:
74+
A dictionary with line, fill, pattern, and gradient properties.
75+
"""
76+
return {
77+
"line": self.line,
78+
"fill": self.fill,
79+
"pattern": self.pattern,
80+
"gradient": self.gradient,
81+
}
6782

6883
def is_hidden(self) -> bool:
6984
"""
@@ -82,9 +97,14 @@ def __repr__(self) -> str:
8297
f"ChartTitle(\n"
8398
f" name = {self.name!r},\n"
8499
f" formula = {self.formula!r},\n"
85-
f" hidden = {self.hidden!r})\n,"
100+
f" hidden = {self.hidden!r},\n"
86101
f" font = {self.font!r},\n"
102+
f" line = {self.line!r},\n"
103+
f" fill = {self.fill!r},\n"
104+
f" pattern = {self.pattern!r},\n"
105+
f" gradient = {self.gradient!r},\n"
87106
f" layout = {self.layout!r},\n"
88107
f" overlay = {self.overlay!r},\n"
89-
f")"
108+
f" has_formatting = {self.has_formatting()!r},\n"
109+
f")\n"
90110
)

xlsxwriter/shape.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -378,13 +378,13 @@ def _get_font_latin_attributes(font):
378378
if not font:
379379
return attributes
380380

381-
if font["name"] is not None:
381+
if font.get("name") is not None:
382382
attributes.append(("typeface", font["name"]))
383383

384-
if font["pitch_family"] is not None:
384+
if font.get("pitch_family") is not None:
385385
attributes.append(("pitchFamily", font["pitch_family"]))
386386

387-
if font["charset"] is not None:
387+
if font.get("charset") is not None:
388388
attributes.append(("charset", font["charset"]))
389389

390390
return attributes
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
###############################################################################
2+
#
3+
# Tests for XlsxWriter.
4+
#
5+
# SPDX-License-Identifier: BSD-2-Clause
6+
#
7+
# Copyright (c), 2013-2025, John McNamara, jmcnamara@cpan.org
8+
#
9+
10+
from ...workbook import Workbook
11+
from ..excel_comparison_test import ExcelComparisonTest
12+
13+
14+
class TestCompareXLSXFiles(ExcelComparisonTest):
15+
"""
16+
Test file created by XlsxWriter against a file created by Excel.
17+
18+
"""
19+
20+
def setUp(self):
21+
22+
self.set_filename("bootstrap62.xlsx")
23+
24+
def test_create_file(self):
25+
"""Test the creation of an XlsxWriter file with default title."""
26+
27+
workbook = Workbook(self.got_filename)
28+
29+
worksheet = workbook.add_worksheet()
30+
chart = workbook.add_chart({"type": "column"})
31+
32+
chart.axis_ids = [67991424, 68001152]
33+
34+
data = [
35+
[1, 2, 3, 4, 5],
36+
[2, 4, 6, 8, 10],
37+
[3, 6, 9, 12, 15],
38+
]
39+
worksheet.write_column("A1", data[0])
40+
worksheet.write_column("B1", data[1])
41+
worksheet.write_column("C1", data[2])
42+
43+
chart.add_series({"values": "=Sheet1!$A$1:$A$5"})
44+
chart.add_series({"values": "=Sheet1!$B$1:$B$5"})
45+
chart.add_series({"values": "=Sheet1!$C$1:$C$5"})
46+
47+
chart.set_title(
48+
{
49+
"border": {"color": "yellow"},
50+
"fill": {"color": "red"},
51+
}
52+
)
53+
54+
worksheet.insert_chart("E9", chart)
55+
56+
workbook.close()
57+
58+
self.assertExcelEqual()
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
###############################################################################
2+
#
3+
# Tests for XlsxWriter.
4+
#
5+
# SPDX-License-Identifier: BSD-2-Clause
6+
#
7+
# Copyright (c), 2013-2025, John McNamara, jmcnamara@cpan.org
8+
#
9+
10+
from ...workbook import Workbook
11+
from ..excel_comparison_test import ExcelComparisonTest
12+
13+
14+
class TestCompareXLSXFiles(ExcelComparisonTest):
15+
"""
16+
Test file created by XlsxWriter against a file created by Excel.
17+
18+
"""
19+
20+
def setUp(self):
21+
22+
self.set_filename("bootstrap63.xlsx")
23+
24+
def test_create_file(self):
25+
"""Test the creation of an XlsxWriter file with default title."""
26+
27+
workbook = Workbook(self.got_filename)
28+
29+
worksheet = workbook.add_worksheet()
30+
chart = workbook.add_chart({"type": "column"})
31+
32+
chart.axis_ids = [67991424, 68001152]
33+
34+
data = [
35+
[1, 2, 3, 4, 5],
36+
[2, 4, 6, 8, 10],
37+
[3, 6, 9, 12, 15],
38+
]
39+
worksheet.write_column("A1", data[0])
40+
worksheet.write_column("B1", data[1])
41+
worksheet.write_column("C1", data[2])
42+
43+
chart.add_series({"values": "=Sheet1!$A$1:$A$5"})
44+
chart.add_series({"values": "=Sheet1!$B$1:$B$5"})
45+
chart.add_series({"values": "=Sheet1!$C$1:$C$5"})
46+
47+
chart.set_title(
48+
{
49+
"name": "Title",
50+
"border": {"color": "yellow"},
51+
"fill": {"color": "red"},
52+
}
53+
)
54+
55+
worksheet.insert_chart("E9", chart)
56+
57+
workbook.close()
58+
59+
self.assertExcelEqual()

0 commit comments

Comments
 (0)