Skip to content

Commit 48415d3

Browse files
Merge branch 'dev' into fr/plotly-annotations
2 parents 71995e2 + e52beb1 commit 48415d3

10 files changed

Lines changed: 301 additions & 73 deletions

release-please-config.json

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/afcharts/af_colours.py

Lines changed: 103 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,27 @@
44
# Py-af-colours source: https://github.com/best-practice-and-impact/py-af-colours
55

66
from pathlib import Path
7+
from typing import List, Literal
78

89
import yaml
910

11+
ColourFormat = Literal["hex", "rgb"]
12+
PaletteName = Literal["duo", "focus", "categorical", "sequential"]
1013

11-
def get_af_colours(palette: str, colour_format="hex", number_of_colours=6, config_path=None):
14+
15+
def get_af_colours(
16+
palette: PaletteName,
17+
colour_format: ColourFormat = "hex",
18+
number_of_colours: int | None = None,
19+
config_path: str | Path | None = None,
20+
include_grey: bool = False,
21+
) -> List:
1222
"""
1323
get_af_colours() is the top level function in af_colours. This returns
1424
the chosen Analysis Function colour palette in hex or rgb format.
1525
For the categorical palette, this can be a chosen number of colours
16-
up to 6.
26+
up to 6. For the sequential palette, this can be a chosen number of colours of 3,
27+
4, or 5.
1728
1829
Parameters
1930
----------
@@ -25,12 +36,17 @@ def get_af_colours(palette: str, colour_format="hex", number_of_colours=6, confi
2536
Colour format required, with accepted values of "hex" or "rgb".
2637
2738
number_of_colours : int, optional
28-
Number of colours required (categorical palette only). Takes
29-
values between 2 and 6. Returns 2 colours by default. If a
30-
palette other than categorical is chosen, any value passed
31-
is ignored.
39+
Number of colours required. For the sequential palette, takes
40+
values 3, 4, or 5 (applying guidance-informed subsets). For categorical
41+
palette, takes values between 2 and 6. The default is None, which uses the
42+
default value for the chosen colour palette.
43+
If palette is another type, this argument is ignored.
44+
45+
include_grey : bool, optional
46+
Whether to include the grey colour in the sequential palette. Can be used to show
47+
null values in charts. The default is False, which excludes the grey colour.
3248
33-
config_path : NoneType, optional
49+
config_path : str | Path | None, optional
3450
Takes the default value None, inside the function this is
3551
mapped to the relative path independent of operating system.
3652
Should not require changing.
@@ -54,35 +70,44 @@ def get_af_colours(palette: str, colour_format="hex", number_of_colours=6, confi
5470
with open(config_path) as file:
5571
config = yaml.load(file, Loader=yaml.BaseLoader)
5672

57-
categorical_hex_list = config["categorical_hex_list"]
58-
duo_hex_list = config["duo_hex_list"]
59-
sequential_hex_list = config["sequential_hex_list"]
60-
focus_hex_list = config["focus_hex_list"]
73+
categorical_hex_list: List[str] = config["categorical_hex_list"]
74+
duo_hex_list: List[str] = config["duo_hex_list"]
75+
sequential_hex_list: List[str] = config["sequential_hex_list"]
76+
focus_hex_list: List[str] = config["focus_hex_list"]
6177

6278
if palette not in ["categorical", "duo", "sequential", "focus"]:
6379
raise ValueError("palette must be one of 'categorical', 'duo', 'sequential' " + f"or 'focus', not {palette}.")
80+
6481
if colour_format not in ["hex", "rgb"]:
6582
raise ValueError(f"colour_format must be 'hex' or 'rgb', not {colour_format}.")
6683

67-
if number_of_colours < 1:
84+
if number_of_colours is not None and number_of_colours < 1:
6885
raise ValueError("number_of_colours must be greater than 0.")
6986

70-
elif palette == "sequential":
71-
chosen_colours_list = sequential_colours(sequential_hex_list, colour_format)
87+
if palette == "sequential":
88+
return sequential_colours(
89+
sequential_hex_list,
90+
colour_format=colour_format,
91+
number_of_colours=number_of_colours or 5,
92+
include_grey=include_grey,
93+
)
7294

73-
elif palette == "focus":
74-
chosen_colours_list = focus_colours(focus_hex_list, colour_format)
95+
if palette == "focus":
96+
return focus_colours(focus_hex_list, colour_format)
7597

76-
elif palette == "duo":
77-
chosen_colours_list = duo_colours(duo_hex_list, colour_format)
98+
if palette == "duo":
99+
return duo_colours(duo_hex_list, colour_format)
78100

79-
elif palette == "categorical":
80-
chosen_colours_list = categorical_colours(categorical_hex_list, duo_hex_list, colour_format, number_of_colours)
101+
# palette == "categorical"
102+
return categorical_colours(categorical_hex_list, duo_hex_list, colour_format, number_of_colours)
81103

82-
return chosen_colours_list
83104

84-
85-
def categorical_colours(categorical_hex_list, duo_hex_list, colour_format="hex", number_of_colours=2):
105+
def categorical_colours(
106+
categorical_hex_list: List[str],
107+
duo_hex_list: List[str],
108+
colour_format: ColourFormat = "hex",
109+
number_of_colours: int | None = 6,
110+
) -> List:
86111
"""
87112
Return the Analysis Function categorical colour palette as a list
88113
in hex or rgb format for up to 6 colours. If number_of_colours is
@@ -102,7 +127,7 @@ def categorical_colours(categorical_hex_list, duo_hex_list, colour_format="hex",
102127
103128
number_of_colours : int
104129
Number of colours required, with accepted values between 2 and 6
105-
inclusive. Returns 2 colours if no value given.
130+
inclusive. Returns 6 colours if no value given.
106131
107132
Raises
108133
------
@@ -115,15 +140,17 @@ def categorical_colours(categorical_hex_list, duo_hex_list, colour_format="hex",
115140
categorical_colours_list
116141
117142
"""
143+
n = 6 if number_of_colours is None else number_of_colours
118144

119-
if number_of_colours > 6:
145+
if n > 6:
120146
raise ValueError("number_of_colours must not be more than 6 for the categorical palette.")
147+
if n < 2:
148+
raise ValueError("number_of_colours must be at least 2 for the categorical palette.")
121149

122-
if number_of_colours == 2:
123-
categorical_colours_list = duo_colours(duo_hex_list, colour_format)
124-
return categorical_colours_list
150+
if n == 2:
151+
return duo_colours(duo_hex_list, colour_format)
125152

126-
elif colour_format == "hex":
153+
if colour_format == "hex":
127154
full_categorical_colours_list = categorical_hex_list
128155

129156
elif colour_format == "rgb":
@@ -132,16 +159,13 @@ def categorical_colours(categorical_hex_list, duo_hex_list, colour_format="hex",
132159
else:
133160
raise ValueError(f"colour_format must be 'hex' or 'rgb', not {colour_format}.")
134161

135-
categorical_colours_list = full_categorical_colours_list[0:number_of_colours]
162+
return full_categorical_colours_list[0:n]
136163

137-
return categorical_colours_list
138164

139-
140-
def duo_colours(duo_hex_list, colour_format="hex"):
165+
def duo_colours(duo_hex_list: List[str], colour_format: ColourFormat = "hex") -> List:
141166
"""
142167
Return the Analysis Function duo colour palette as a list of 2
143-
colours in hex or rgb format. This function is also called by
144-
sequential_colours() if number_of_colours is equal to 2.
168+
colours in hex or rgb format.
145169
146170
Parameters
147171
----------
@@ -158,21 +182,24 @@ def duo_colours(duo_hex_list, colour_format="hex"):
158182
duo_colours_list
159183
160184
"""
161-
162185
if colour_format == "hex":
163-
duo_colours_list = duo_hex_list
186+
return duo_hex_list
164187
elif colour_format == "rgb":
165-
duo_colours_list = hex_to_rgb(duo_hex_list)
188+
return hex_to_rgb(duo_hex_list)
166189
else:
167190
raise ValueError(f"colour_format must be 'hex' or 'rgb', not {colour_format}.")
168191

169-
return duo_colours_list
170-
171192

172-
def sequential_colours(sequential_hex_list, colour_format="hex"):
193+
def sequential_colours(
194+
sequential_hex_list: List[str],
195+
colour_format: ColourFormat = "hex",
196+
number_of_colours: int = 5,
197+
include_grey: bool = False,
198+
) -> List:
173199
"""
174200
Return the Analysis Function sequential colour palette as a list
175-
of 3 colours in hex or rgb format.
201+
in hex or rgb format. Supports combinations of 3, 4, or 5 colours
202+
based on Analysis Function guidance.
176203
177204
Parameters
178205
----------
@@ -182,24 +209,43 @@ def sequential_colours(sequential_hex_list, colour_format="hex"):
182209
colour_format : string
183210
Colour format required, with accepted values of "hex" or "rgb".
184211
212+
number_of_colours: int
213+
Number of sequential colours required, with accepted values of 3,
214+
4, or 5. Defaults to 5.
215+
216+
include_grey : bool, optional
217+
Whether to include the grey colour in the palette. Can be used to show
218+
null values in charts. The default is False, which excludes the grey colour.
219+
185220
Returns
186221
-------
187222
list
188223
sequential_colours_list
189224
190225
"""
226+
SEQUENTIAL_COMBOS = {
227+
3: [sequential_hex_list[1], sequential_hex_list[2], sequential_hex_list[3]],
228+
4: [sequential_hex_list[0], sequential_hex_list[1], sequential_hex_list[2], sequential_hex_list[3]],
229+
5: sequential_hex_list[:5],
230+
}
231+
232+
if number_of_colours not in [3, 4, 5]:
233+
raise ValueError("number_of_colours must be 3, 4, or 5 for the sequential palette.")
234+
235+
if include_grey:
236+
colours = SEQUENTIAL_COMBOS[number_of_colours] + [sequential_hex_list[-1]]
237+
else:
238+
colours = SEQUENTIAL_COMBOS[number_of_colours]
191239

192240
if colour_format == "hex":
193-
sequential_colours_list = sequential_hex_list
241+
return colours
194242
elif colour_format == "rgb":
195-
sequential_colours_list = hex_to_rgb(sequential_hex_list)
243+
return hex_to_rgb(colours)
196244
else:
197245
raise ValueError(f"colour_format must be 'hex' or 'rgb', not {colour_format}.")
198246

199-
return sequential_colours_list
200-
201247

202-
def focus_colours(focus_hex_list, colour_format="hex"):
248+
def focus_colours(focus_hex_list: List[str], colour_format: ColourFormat = "hex") -> List:
203249
"""
204250
Return the Analysis Function focus colour palette as a list of 2
205251
colours in hex or rgb format.
@@ -220,16 +266,14 @@ def focus_colours(focus_hex_list, colour_format="hex"):
220266
"""
221267

222268
if colour_format == "hex":
223-
focus_colours_list = focus_hex_list
269+
return focus_hex_list
224270
elif colour_format == "rgb":
225-
focus_colours_list = hex_to_rgb(focus_hex_list)
271+
return hex_to_rgb(focus_hex_list)
226272
else:
227273
raise ValueError(f"colour_format must be 'hex' or 'rgb', not {colour_format}.")
228274

229-
return focus_colours_list
230275

231-
232-
def hex_to_rgb(hex_colours):
276+
def hex_to_rgb(hex_colours: List[str]) -> List:
233277
"""
234278
Convert a list of hex codes to a list of rgb colours.
235279
@@ -247,13 +291,17 @@ def hex_to_rgb(hex_colours):
247291
Returns
248292
-------
249293
list
250-
converted_list
294+
rgb_list
251295
252296
"""
253297
if type(hex_colours) is not list:
254298
raise TypeError("hex_colours must be a list.")
255299

300+
for value in hex_colours:
301+
if not isinstance(value, str):
302+
raise TypeError("hex_colours must be a list of hex strings.")
303+
256304
hex_colours_new = [i.lstrip("#") for i in hex_colours]
257305

258-
converted_list = [(tuple(int(value[i : i + 2], 16) for i in (0, 2, 4))) for value in hex_colours_new]
259-
return converted_list
306+
rgb_list = [(tuple(int(value[i : i + 2], 16) for i in (0, 2, 4))) for value in hex_colours_new]
307+
return rgb_list

src/afcharts/afcharts.mplstyle

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,3 @@ ytick.direction : out
3838
# LEGEND
3939
legend.frameon : False
4040

41-
# FIGURE OUTPUT
42-
figure.figsize : 6.4, 4.8
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
categorical_hex_list: ["#12436D","#28A197","#801650", "#F46A25","#3D3D3D","#A285D1"]
22
duo_hex_list: ["#12436D","#F46A25"]
3-
sequential_hex_list: ["#12436D", "#2073BC", "#6BACE6"]
3+
sequential_hex_list: ["#092135", "#12436D", "#2073BC", "#6BACE6", "#ADD1F1", "#F2F2F2"]
44
focus_hex_list: ["#12436D","#BFBFBF"]

src/afcharts/cookbook/01-matplotlib-usage.qmd

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,6 +748,12 @@ fig
748748
This line chart uses the afcharts theme. There are pale grey grid lines extending from the y axis, and there is a thicker dark blue line representing the data. A dotted horizontal line has been added at 70 years of age, with an annotation to label it.
749749
</div>
750750

751+
### Saving figures
752+
753+
The afcharts style uses a larger base font size (14pt vs Matplotlib's default 10pt) for accessibility. The `figure.figsize` parameter is intentionally not set in the style sheet — the default canvas remains 6.4 × 4.8 inches.
754+
755+
By default, `savefig` saves at exactly `figure.figsize`. If you pass `bbox_inches='tight'`, the output is cropped to the bounding box of all content, so dimensions will vary between charts. Avoid this if you need a consistent, fixed output size.
756+
751757
### Wrapping text
752758

753759
If text is too long, it may be cut off or distort the dimensions of the chart. To avoid this, text can be wrapped to multiple lines using the `textwrap` module. The width argument controls how many characters are allowed on each line before wrapping. See the figure title below for an example.

src/afcharts/cookbook/04-colour-palettes.qmd

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ get_af_colours("duo")
2424

2525
### Number of colours
2626

27-
By default, `get_af_colours()` will return all colours in the given palette. For the categorical palette, the number of colours returned can be set with the `number_of_colours` argument.
27+
By default, `get_af_colours()` will return all core colours in the given palette with grey as an optional addition. For the categorical and sequential palettes, the number of colours returned can be set with the `number_of_colours` argument.
2828

2929
For example, to return four colours from the categorical palette as hex codes:
3030
```{python}
@@ -40,7 +40,7 @@ For example, to return the sequential colour palette as a list of rgb code tuple
4040
```{python}
4141
#| eval: false
4242
get_af_colours("sequential", colour_format="rgb")
43-
# [(18, 67, 109), (32, 115, 188), (107, 172, 230)]
43+
# [(9, 33, 53), (18, 67, 109), (32, 115, 188), (107, 172, 230), (173, 209, 241)]
4444
```
4545

4646

@@ -57,7 +57,7 @@ from afcharts.af_colours import get_af_colours
5757
5858
cat = get_af_colours("categorical")
5959
duo = get_af_colours("duo")
60-
sequential = get_af_colours("sequential")
60+
sequential = get_af_colours("sequential", include_grey=True)
6161
focus = get_af_colours("focus")
6262
```
6363

@@ -103,6 +103,8 @@ display(HTML(df.to_html(escape=False, index=False)))
103103
### Sequential palette
104104
The `sequential` colour palette should be used for data where the order has some meaning, such as age groups.
105105

106+
The pale grey colour should be used for cases when, for example, you have no data or have suppressed the value. By default, the sequential palette returned by `get_af_colours()` does not include the pale grey. To include it, set `include_grey=True`.
107+
106108
```{python}
107109
#| echo: false
108110

tests/af_colours_list_length_test.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@
1818
@pytest.mark.parametrize(
1919
"palette, colour_format, number_of_colours",
2020
[
21-
# Test categorical palette for length boundaries (1–6 supported values)
22-
("categorical", "hex", 1),
21+
# Test categorical palette for length boundaries (2–6 supported values)
2322
("categorical", "hex", 2),
2423
("categorical", "hex", 3),
2524
("categorical", "hex", 4),
@@ -38,3 +37,11 @@ def test_categorical_list_length(palette, colour_format, number_of_colours):
3837
assert len(result) == number_of_colours, (
3938
f"Expected {number_of_colours} colours but got {len(result)} for palette='{palette}', format='{colour_format}'"
4039
)
40+
41+
42+
def test_categorical_minimum_colours_value():
43+
"""
44+
Verify requesting 1 colour from the categorical palette triggers a ValueError.
45+
"""
46+
with pytest.raises(ValueError):
47+
get_af_colours("categorical", "hex", 1)

0 commit comments

Comments
 (0)