Coverage for moptipy / utils / plot_defaults.py: 92%

122 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-24 08:49 +0000

1"""Default styles for plots.""" 

2from typing import Final, cast 

3 

4import matplotlib.cm as mplcm # type: ignore 

5import matplotlib.pyplot as plt # type: ignore 

6import numpy as np 

7from matplotlib import colors # type: ignore 

8from pycommons.types import check_int_range, type_error 

9 

10#: The internal color black. 

11COLOR_BLACK: Final[tuple[float, float, float]] = (0.0, 0.0, 0.0) 

12#: The internal color white. 

13COLOR_WHITE: Final[tuple[float, float, float]] = (1.0, 1.0, 1.0) 

14 

15 

16def str_to_palette(palette: str) \ 

17 -> tuple[tuple[float, float, float], ...]: 

18 """ 

19 Obtain a palette from a string. 

20 

21 :param palette: the string with all the color data. 

22 :returns: the palette 

23 """ 

24 if isinstance(colors, str): 

25 raise type_error(colors, "colors", str) 

26 result: list[tuple[float, float, float]] = [] 

27 end: int = -1 

28 length: Final[int] = len(palette) 

29 while end < length: 

30 start: int = end + 1 

31 end = length 

32 for ch in "\n\r\t ,;": 

33 end_new: int = palette.find(ch, start) 

34 if start < end_new < end: 

35 end = end_new 

36 color: str = palette[start:end].strip() 

37 if color.startswith("#"): 

38 color = color[1:].strip() 

39 if len(color) > 0: 

40 if len(color) != 6: 

41 raise ValueError(f"invalid color: {color!r} in {palette!r}") 

42 result.append((int(color[0:2], 16) / 255, 

43 int(color[2:4], 16) / 255, 

44 int(color[4:6], 16) / 255)) 

45 if len(result) <= 0: 

46 raise ValueError(f"empty colors: {palette!r}") 

47 return tuple(result) 

48 

49 

50#: A palette with 11 distinct colors. 

51__PALETTE_11: Final[tuple[tuple[float, float, float], ...]] = str_to_palette( 

52 "ff0000 0000ff 00d900 a32626 ffa500 008833 7788ff cf00ff 770066 cccc00 " 

53 "a0a9a0") 

54 

55#: A palette with 21 distinct colors. 

56__PALETTE_21: Final[tuple[tuple[float, float, float], ...]] = str_to_palette( 

57 "e6194b 3cb44b ffe119 0082c8 f58231 911eb4 46f0f0 f032e6 d2f53c fabebe " 

58 "008080 e6beff aa6e28 cfca08 800000 aaffc3 808000 ffd8b1 000080 505000 " 

59 "90a0a0") 

60 

61#: A palette with 25 distinct colors. 

62__PALETTE_25: Final[tuple[tuple[float, float, float], ...]] = str_to_palette( 

63 "20b2aa f0e68c b8860b ff00ff 00fa9a ee82ee 9acd32 dcdcdc 556b2f db7093 " 

64 "00bfff 1e90ff 8a2be2 ff1493 008000 ffd700 000080 800000 7fff00 8b008b " 

65 "0000ff 483d8b ff0000 696969 ff7f50") 

66 

67#: A palette with 28 distinct colors. 

68__PALETTE_28: Final[tuple[tuple[float, float, float], ...]] = str_to_palette( 

69 "545454 550000 b0b0ff bababa 878787 005500 550055 b000b0 e4e400 00b0b0 " 

70 "00ffff 00ff00 878700 870087 005555 ff0000 0000ff baba00 00b000 ff00ff " 

71 "870000 4949ff 008700 8484ff 008787 e4e4e4 545400 b00000") 

72 

73#: A palette with 30 distinct colors. 

74__PALETTE_30: Final[tuple[tuple[float, float, float], ...]] = str_to_palette( 

75 "00bfff ff1493 7f007f 8a2be2 1e90ff adff2f ffb6c1 fa8072 ffff54 90ee90 " 

76 "ff4500 00ff00 483d8b ffa500 696969 add8e6 ff00ff 7f0000 8fbc8f da70d6 " 

77 "006400 dc143c 00ff7f 008b8b 808000 b03060 00ffff 0000ff 000080 deb887 ") 

78 

79#: A palette with 35 distinct colors. 

80__PALETTE_35: Final[tuple[tuple[float, float, float], ...]] = str_to_palette( 

81 "228b22 808080 9acd32 008080 483d8b 7f0000 d2b48c 00ff00 00008b 808000 " 

82 "00ced1 8fbc8f 556b2f ffc0cb 1e90ff ffff54 800080 00bfff ff00ff 7b68ee " 

83 "ff6347 9400d3 ffa500 7fffd4 b03060 ff0000 f08080 f4a460 00ff7f 98fb98 " 

84 "f0e68c ff1493 0000ff b0e0e6 dda0dd") 

85 

86#: A palette with 40 distinct colors. 

87__PALETTE_40: Final[tuple[tuple[float, float, float], ...]] = str_to_palette( 

88 "ff00ff 7f007f a9a9a9 0000ff daa520 2f4f4f da70d6 ff8c00 ffc0cb afeeee " 

89 "00008b 20b2aa 00ff00 f4a460 8b0000 008000 f08080 a0522d ff6347 808000 " 

90 "bdb76b 98fb98 191970 ffe4c4 ffff00 4682b4 32cd32 1e90ff 3cb371 ff1493 " 

91 "b03060 7fffd4 556b2f dda0dd adff2f 7b68ee 9acd32 ff0000 00bfff 9400d3 ") 

92 

93#: A palette with 45 distinct colors. 

94__PALETTE_45: Final[tuple[tuple[float, float, float], ...]] = str_to_palette( 

95 "ffb60c 00008b 8b4513 e9967a a020f0 ff0000 6a5acd 808080 008b8b 00ff00 " 

96 "90ee90 4682b4 daa520 7f007f ffd700 00bfff 00ced1 556b2f c0c0c0 008000 " 

97 "ff69b4 dda0dd adff2f 3cb371 8b0000 0000ff f5deb3 32cd32 1e90ff b03060 " 

98 "ff7f50 cd5c5c ff00ff 7fffd4 6b8e23 dc143c 9acd32 afeeee 2f4f4f 00fa9a " 

99 "191970 8fbc8f ba55d3 ff8c00 bdb76b") 

100 

101#: A palette with 50 distinct colors. 

102__PALETTE_50: Final[tuple[tuple[float, float, float], ...]] = str_to_palette( 

103 "7fffd4 000080 00ff7f 483d8b 008000 afeeee ffc0cb daa520 ffd700 ff8c00 " 

104 "87cefa ff00ff c0c0c0 cd5c5c 00ff00 adff2f e9967a ffff00 dc143c cd853f " 

105 "9932cc 98fb98 708090 800000 ee82ee 800080 8fbc8f 4682b4 dda0dd 32cd32 " 

106 "a020f0 48d1cc a0522d ff1493 ff69b4 f0e68c 9370db 2f4f4f ff0000 6b8e23 " 

107 "556b2f ff6347 f5deb3 008b8b db7093 0000cd 9acd32 3cb371 0000ff 6495ed") 

108 

109#: A palette with 60 distinct colors. 

110__PALETTE_60: Final[tuple[tuple[float, float, float], ...]] = str_to_palette( 

111 "d2b48c 191970 000080 20b2aa bc8f8f 2f4f4f 8b4513 ff4500 dc143c afeeee " 

112 "db7093 c71585 bdb76b b03060 708090 00ffff 6495ed 808000 f08080 b8860b " 

113 "cd853f 556b2f ffa07a 008080 eee8aa dda0dd ee82ee 7fffd4 9370db 7f007f " 

114 "d8bfd8 9acd32 4682b4 98fb98 00ff00 9932cc 0000cd d2691e 00fa9a ff8c00 " 

115 "663399 a9a9a9 ffff00 32cd32 8b0000 4169e1 8fbc8f 0000ff ff69b4 008000 " 

116 "ffb6c1 00bfff ffd700 ff1493 ff00ff 3cb371 a020f0 87ceeb ff6347 adff2f") 

117 

118#: A palette with 70 distinct colors. 

119__PALETTE_70: Final[tuple[tuple[float, float, float], ...]] = str_to_palette( 

120 "b8860b a52a2a ffe4c4 663399 da70d6 a020f0 eee8aa 9acd32 ba55d3 ff8c00 " 

121 "00008b 2e8b57 66cdaa 2f4f4f 0000cd 32cd32 556b2f adff2f 7fffd4 00ff00 " 

122 "008b8b 4b0082 ffd700 00bfff 6b8e23 48d1cc b03060 778899 ffff54 ff0000 " 

123 "00ff7f d2b48c 3cb371 bc8f8f b0e0e6 ffc0cb dcdcdc ff00ff cd5c5c 6495ed " 

124 "0000ff 9370db dc143c a9a9a9 bdb76b f4a460 c71585 e9967a 4682b4 db7093 " 

125 "483d8b 8b008b 87cefa 7fff00 dda0dd 006400 9932cc 808000 ff1493 191970 " 

126 "4169e1 ff6347 b0c4de ff69b4 d2691e 00ffff 98fb98 ffff00 a0522d 8fbc8f") 

127 

128#: A palette with 80 distinct colors. 

129__PALETTE_80: Final[tuple[tuple[float, float, float], ...]] = str_to_palette( 

130 "eee8aa bdb76b 2e8b57 0000ff 1e90ff cd5c5c ffd700 90ee90 00ffff ba55d3 " 

131 "d3d3d3 40e0d0 ff0000 b0c4de 008000 0000cd 00bfff ffa07a ff00ff 778899 " 

132 "dc143c cd853f 556b2f 9acd32 9400d3 663399 b03060 ffdab9 b22222 ffa500 " 

133 "7fffd4 4169e1 dda0dd 8fbc8f ffff00 ff1493 191970 00008b 9932cc 006400 " 

134 "483d8b 808000 696969 00ff7f d8bfd8 d2691e 4682b4 a9a9a9 c71585 5f9ea0 " 

135 "ffc0cb 7f0000 800080 ee82ee 008080 4b0082 ffff54 9370db 8b4513 ff6347 " 

136 "daa520 87cefa adff2f ff8c00 6b8e23 deb887 00fa9a 66cdaa 7b68ee fa8072 " 

137 "ff69b4 db7093 b0e0e6 00ff00 2f4f4f bc8f8f 32cd32 3cb371 20b2aa 7cfc00") 

138 

139#: A palette with 90 distinct colors. 

140__PALETTE_90: Final[tuple[tuple[float, float, float], ...]] = str_to_palette( 

141 "fa8072 ffff54 0000cd 32cd32 dc143c 40e0d0 ff8c00 9932cc 7b68ee 800000 " 

142 "4b0082 008000 afeeee ee82ee ffd700 6a5acd 2e8b57 eee8aa a9a9a9 ff69b4 " 

143 "b8860b c71585 ff4500 7fff00 2f4f4f 00ff7f ffa500 add8e6 008b8b bdb76b " 

144 "ff00ff cd853f f0e68c 808080 db7093 800080 a0522d 87ceeb 3cb371 9400d3 " 

145 "b0c4de daa520 b03060 dda0dd 66cdaa 6b8e23 d3d3d3 4682b4 9acd32 4169e1 " 

146 "ff7f50 5f9ea0 f08080 00fa9a 00ced1 808000 adff2f 0000ff d2691e ff1493 " 

147 "ff6347 ffdead 20b2aa 006400 7fffd4 483d8b cd5c5c f4a460 8fbc8f 663399 " 

148 "d8bfd8 9370db b22222 d2b48c ffff00 ba55d3 bc8f8f ffb6c1 6495ed e9967a " 

149 "ff0000 000080 00ff00 191970 90ee90 00ffff 778899 1e90ff 00bfff 556b2f") 

150 

151#: A palette with 100 distinct colors. 

152__PALETTE_100: Final[tuple[tuple[float, float, float], ...]] = str_to_palette( 

153 "bdb76b 8a2be2 bc8f8f eee8aa 8fbc8f a020f0 ff4500 2f4f4f 000080 ff6347 " 

154 "6b8e23 808080 663399 afeeee dcdcdc ff8c00 3cb371 191970 4682b4 7cfc00 " 

155 "ee82ee 32cd32 9370db ff1493 00ff00 6a5acd adff2f 006400 00fa9a ffd700 " 

156 "d2691e c0c0c0 00ff7f ba55d3 add8e6 ffff00 6495ed 8b008b 48d1cc 87cefa " 

157 "fa8072 deb887 b22222 7b68ee b8860b 90ee90 8b4513 9acd32 cd853f f08080 " 

158 "ff00ff da70d6 9400d3 ff69b4 ffe4b5 556b2f 8b0000 1e90ff e9967a 696969 " 

159 "c71585 ffa07a f0e68c a9a9a9 20b2aa a0522d 87ceeb 0000ff 008000 daa520 " 

160 "00ffff b03060 483d8b ffb6c1 dda0dd ff0000 9932cc 708090 cd5c5c 4b0082 " 

161 "ffa500 0000cd dc143c a52a2a 008080 7fffd4 5f9ea0 b0c4de d8bfd8 808000 " 

162 "db7093 f4a460 2e8b57 ffff54 40e0d0 66cdaa ffe4c4 ff7f50 4169e1 00bfff") 

163 

164#: A palette with 110 distinct colors. 

165__PALETTE_110: Final[tuple[tuple[float, float, float], ...]] = str_to_palette( 

166 "0000ff 556b2f ff4500 000080 f5deb3 ff1493 00008b 0000cd ff8c00 ffc0cb " 

167 "fa8072 696969 da70d6 8b4513 d2691e 00fa9a 008080 c71585 bdb76b 40e0d0 " 

168 "6495ed daa520 5f9ea0 ffe4c4 f4a460 ba55d3 228b22 00ced1 00ffff 66cdaa " 

169 "b03060 9400d3 f08080 dc143c adff2f 00ff00 9932cc ee82ee 8a2be2 32cd32 " 

170 "afeeee 191970 808080 9acd32 90ee90 b22222 a0522d ff00ff 006400 c0c0c0 " 

171 "2f4f4f d3d3d3 cd853f 98fb98 9370db 4169e1 4b0082 00ff7f ff7f50 8fbc8f " 

172 "8b0000 eee8aa dda0dd 008000 ffa500 ff6347 ff69b4 d8bfd8 ffa07a 4682b4 " 

173 "b0c4de 2e8b57 ff0000 00bfff f0e68c 7f0000 b8860b d2b48c ffff54 1e90ff " 

174 "e9967a 3cb371 87cefa 87ceeb 6a5acd ffd700 708090 bc8f8f 7b68ee 6b8e23 " 

175 "a52a2a 7fffd4 ffdead ffdab9 808000 483d8b 20b2aa a020f0 48d1cc db7093 " 

176 "a9a9a9 663399 b0e0e6 deb887 7f007f 7fff00 8b008b ffff00 cd5c5c add8e6") 

177 

178 

179#: A set of predefined uniquely-looking colors. 

180__FIXED_COLORS: Final[tuple[tuple[tuple[float, float, float], ...], ...]] = ( 

181 __PALETTE_11, __PALETTE_21, __PALETTE_25, __PALETTE_28, __PALETTE_30, 

182 __PALETTE_35, __PALETTE_40, __PALETTE_45, __PALETTE_50, __PALETTE_60, 

183 __PALETTE_70, __PALETTE_80, __PALETTE_90, __PALETTE_100, __PALETTE_110) 

184 

185 

186def distinct_colors(n: int) -> tuple[tuple[float, float, float], ...]: 

187 """ 

188 Obtain a set of `n` distinct colors. 

189 

190 :param n: the number of colors required 

191 :return: a tuple of colors 

192 """ 

193 check_int_range(n, "n", 1, 1000) 

194 

195 # First, let us see if we can cover the range with hand-picked colors. 

196 for k in __FIXED_COLORS: 

197 lk = len(k) 

198 if lk >= n: 

199 if lk == n: 

200 return k 

201 return tuple(k[0:n]) 

202 

203 # Second, let's see whether the method from 

204 # https://stackoverflow.com/questions/8389636 

205 # works. 

206 # This method does not seem to make good use of the available color space. 

207 # Since we use it only for cases with more than 110 colors, that's OK. 

208 cm = plt.get_cmap("gist_rainbow") 

209 c_norm = colors.Normalize(vmin=0, vmax=n - 1) 

210 scalar_map = mplcm.ScalarMappable(norm=c_norm, cmap=cm) 

211 qq = cast("list[tuple[float, float, float]]", 

212 [tuple(scalar_map.to_rgba(cast("np.ndarray", i))[0:3]) 

213 for i in np.arange(n)]) 

214 ss = set(qq) 

215 if len(ss) == n: 

216 return tuple(qq) 

217 

218 raise ValueError(f"Could not obtain {n} distinct colors.") 

219 

220 

221#: The solid line dash 

222LINE_DASH_SOLID: Final[str] = "solid" 

223 

224#: An internal array of fixed line styles. 

225__FIXED_LINE_DASHES: \ 

226 Final[tuple[str | tuple[float, tuple[float, ...]], ...]] = \ 

227 (LINE_DASH_SOLID, 

228 "dashed", 

229 "dashdot", 

230 "dotted", 

231 (0.0, (3.0, 5.0, 1.0, 5.0, 1.0, 5.0)), # dashdotdotted 

232 (0.0, (3.0, 1.0, 1.0, 1.0)), # densely dashdotted 

233 (0.0, (5.0, 1.0)), # densely dashed 

234 (0.0, (1.0, 1.0)), # densely dotted 

235 (0.0, (3.0, 1.0, 1.0, 1.0, 1.0, 1.0)), # densely dashdotdotted 

236 (0.0, (1.0, 10.0)), # loosely dotted 

237 (0.0, (5.0, 10.0)), # loosely dashed 

238 (0.0, (3.0, 10.0, 1.0, 10.0)), # loosely dashdotted 

239 (0.0, (3.0, 10.0, 1.0, 10.0, 1.0, 10.0))) # loosely dashdotdotted 

240 

241 

242def distinct_line_dashes(n: int) -> \ 

243 tuple[str | tuple[float, tuple[float, ...]], ...]: 

244 """ 

245 Create a sequence of distinct line dashes. 

246 

247 :param n: the number of styles 

248 :return: the styles 

249 """ 

250 check_int_range(n, "n", 1, len(__FIXED_LINE_DASHES) - 1) 

251 if n == __FIXED_LINE_DASHES: 

252 return __FIXED_LINE_DASHES 

253 return tuple(__FIXED_LINE_DASHES[0:n]) 

254 

255 

256#: The fixed predefined distinct markers 

257__FIXED_MARKERS: tuple[str, ...] = ("o", "^", "s", "P", "X", "D", "*", "p") 

258 

259 

260def distinct_markers(n: int) -> tuple[str, ...]: 

261 """ 

262 Create a sequence of distinct markers. 

263 

264 :param n: the number of markers 

265 :return: the markers 

266 """ 

267 lfm: Final[int] = len(__FIXED_MARKERS) 

268 check_int_range(n, "n", 1, lfm) 

269 if n == lfm: 

270 return __FIXED_MARKERS 

271 return tuple(__FIXED_MARKERS[0:n]) 

272 

273 

274def importance_to_line_width(importance: int) -> float: 

275 """ 

276 Transform an importance value to a line width. 

277 

278 Basically, an importance of `0` indicates a normal line in a normal 

279 plot that does not need to be emphasized. 

280 A positive importance means that the line should be emphasized. 

281 A negative importance means that the line should be de-emphasized. 

282 

283 :param importance: a value between -9 and 9 

284 :return: the line width 

285 """ 

286 check_int_range(importance, "importance", -9, 9) 

287 if importance >= 0: 

288 return 2.0 * (0.5 + importance) 

289 if importance == -1: 

290 return 2.0 / 3.0 

291 if importance == -2: 

292 return 0.5 

293 return 0.7 ** (-importance) 

294 

295 

296def importance_to_alpha(importance: int) -> float: 

297 """ 

298 Transform an importance value to an alpha value. 

299 

300 Basically, an importance of `0` indicates a normal line in a normal 

301 plot that does not need to be emphasized. 

302 A positive importance means that the line should be emphasized. 

303 A negative importance means that the line should be de-emphasized. 

304 

305 :param importance: a value between -9 and 9 

306 :return: the alpha 

307 """ 

308 check_int_range(importance, "importance", -9, 9) 

309 if importance >= 0: 

310 return 1.0 

311 if importance == -1: 

312 return 2.0 / 3.0 

313 if importance == -2: 

314 return 0.5 

315 return 1.0 / 3.0 

316 

317 

318#: The internal default basic style 

319__BASE_LINE_STYLE: Final[dict[str, object]] = { 

320 "alpha": 1.0, 

321 "antialiased": True, 

322 "color": COLOR_BLACK, 

323 "dash_capstyle": "butt", 

324 "dash_joinstyle": "round", 

325 "linestyle": LINE_DASH_SOLID, 

326 "linewidth": 1.0, 

327 "solid_capstyle": "round", 

328 "solid_joinstyle": "round", 

329} 

330 

331 

332def create_line_style(**kwargs) -> dict[str, object]: 

333 """ 

334 Obtain the basic style for lines in diagrams. 

335 

336 :param kwargs: any additional overrides 

337 :return: a dictionary with the style elements 

338 """ 

339 res = dict(__BASE_LINE_STYLE) 

340 res.update(kwargs) 

341 return res 

342 

343 

344def importance_to_font_size(importance: float) -> float: 

345 """ 

346 Transform an importance value to a font size. 

347 

348 :param importance: the importance value 

349 :return: the font size 

350 """ 

351 check_int_range(importance, "importance", -9, 9) 

352 if importance < 0: 

353 return 7.5 

354 if importance <= 0: 

355 return 8.0 

356 if importance == 1: 

357 return 8.5 

358 if importance == 2: 

359 return 9.0 

360 if importance == 3: 

361 return 10.0 

362 return 11.0 

363 

364 

365#: The default grid color 

366GRID_COLOR: Final[tuple[float, float, float]] = \ 

367 (7.0 / 11.0, 7.0 / 11.0, 7.0 / 11.0) 

368 

369 

370def rgb_to_gray(r: float, g: float, b: float) -> float: 

371 """ 

372 Convert RGB values to gray scale. 

373 

374 :param r: the red value 

375 :param g: the green value 

376 :param b: the blue value 

377 :return: the gray value 

378 """ 

379 return (0.2989 * r) + (0.5870 * g) + (0.1140 * b) 

380 

381 

382def text_color_for_background(background: tuple[float, float, float]) \ 

383 -> tuple[float, float, float]: 

384 """ 

385 Get a reasonable text color for a given background color. 

386 

387 :param background: the background color 

388 :return: the text color 

389 """ 

390 br: Final[float] = background[0] 

391 bg: Final[float] = background[1] 

392 bb: Final[float] = background[2] 

393 bgg: Final[float] = rgb_to_gray(br, bg, bb) 

394 

395 return COLOR_WHITE if bgg < 0.3 else COLOR_BLACK