Coverage for src/colorspace/specplot.py: 97%

155 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-08-23 19:54 +0000

1 

2 

3def specplot(x, y = None, hcl = True, palette = True, fix = True, rgb = False, \ 

4 title = None, fig = None, **figargs): 

5 """Color Spectrum Plot 

6 

7 Visualization of color palettes (given as hex codes) in HCL and/or 

8 RGB coordinates. 

9 

10 As the hues for low-chroma colors are not (or poorly) identified, by 

11 default a smoothing is applied to the hues (`fix = TRUE`). Also, to avoid 

12 jumps from `0` to `360` or vice versa, the hue coordinates are shifted 

13 suitably. 

14 

15 If argument `x` is a `maplotlib.colors.LinearSegmentedColormap` or 

16 `matplotlib.colors.ListedColormap`, `256` distinct 

17 colors across the color map are drawn and visualized. 

18 

19 Args: 

20 x (list, LinearSegmentedColormap, ListedColormap): list of str (hex colors or 

21 standard-names of colors) or a `matplotlib.colors.LinearSegmentedColormap`. 

22 y (None, list, LinearSegmentedColormap): if set it must be a list of 

23 str (see `x`) with the very same length as the object provided on 

24 argument `x` or a `maplotlib.colors.LinearSegmentedColormap`. 

25 Allows to draw two sets of colors for comparison, defaults to `None`. 

26 hcl (bool): Whether or not to plot the HCL color spectrum. 

27 palette (bool): Whether or not to plot the colors as a color map (color swatch). 

28 fix (bool): Should the hues be fixed to be on a smooth(er) curve? 

29 Details in the functions description. 

30 rgb (bool): Whether or not to plot the RGB color spectrum, defaults to `False`. 

31 title (None or str): title of the figure. Defaults to `None` (no title). 

32 fig (None, matplotlib.figure.Figure): If `None`, a new 

33 `matplotlib.figure.Figure` is created.  

34 **figargs: forwarded to `matplotlib.pyplot.subplot`. Only has an effect 

35 if `fig = None`. 

36 

37 Example: 

38 

39 >>> from colorspace import rainbow_hcl, diverging_hcl 

40 >>> from colorspace import specplot 

41 >>> pal = rainbow_hcl() 

42 >>> specplot(pal.colors(21)); 

43 >>> #: Show spectrum in standard RGB space 

44 >>> specplot(pal.colors(21), rgb = True); 

45 >>> #: Reduced number of colors. 

46 >>> # Show sRGB spectrum, hide HCL spectrum 

47 >>> # and color palette swatch. 

48 >>> specplot(pal.colors(), rgb = True, hcl = False, 

49 >>> palette = False, figsize = (8, 3)); 

50 >>> #: Comparing full diverging_hcl() color spectrum to 

51 >>> # a LinearSegmentedColormap (cmap) with only 5 colors 

52 >>> # (an extreme example) 

53 >>> specplot(diverging_hcl("Green-Orange").colors(101), 

54 >>> diverging_hcl("Green-Orange").cmap(5), 

55 >>> rgb = True, figsize = (8, 3)); 

56 >>> #: Same as above using .cmap() default with N = 256 colors 

57 >>> specplot(diverging_hcl("Green-Orange").colors(101), 

58 >>> diverging_hcl("Green-Orange").cmap(), 

59 >>> rgb = True, figsize = (8, 3)); 

60 

61 Raises: 

62 ImportError: If `matplotlib` is not installed. 

63 TypeError: If `x` is not list or `matplotlib.colors.LinearSegmentedColormap`. 

64 TypeError: If `y` is neither a list nor `None`. 

65 ValueError: If `x` contains str which can not be converted to hex colors. 

66 ValueError: If `y` contains str which can not be converted to hex colors. 

67 ValueError: If `y` is not the same length as `y`. Only checked if `y` is not `None`. 

68 TypeError: If either `rgb`, `hcl`, or `palette` is not bool. 

69 ValueError: If all, `rgb`, `hcl` and `palette` are set to `False` as this would 

70 result in an empty plot. 

71 TypeError: If 'title' is neither `None` nor `str`. 

72 """ 

73 

74 # Requires matpotlib for plotting. If not available, throw ImportError 

75 try: 

76 import matplotlib.pyplot as plt 

77 except ImportError as e: 

78 raise ImportError("problems importing matplotlib.pyplt (not installed?)") 

79 

80 from .utils import check_hex_colors 

81 from matplotlib.colors import LinearSegmentedColormap, ListedColormap 

82 

83 # Support function to draw the color map (the color strip) 

84 def cmap(ax, hex_, ylo = 0): 

85 """Plotting cmap-based palettes 

86 

87 Args: 

88 ax (matplotlib.Axis): The axis object on which the color map should be drawn 

89 hex_ (list): List of hex colors. 

90 ylo (float): Lower limit where the rectangles are plotted. Height is always 

91 1, if multiple palettes have to be plotted xlo has to be set to 0, 1, ... 

92 """ 

93 

94 from numpy import linspace 

95 from matplotlib.patches import Rectangle 

96 

97 n = len(hex_) 

98 w = 1. / float(n - 1) 

99 x = linspace(-w / 2., 1. + w / 2, n + 1) 

100 for i in range(0,n): 

101 rect = Rectangle((x[i], 0. + ylo), w, 1. + ylo, 

102 color = "#FFFFFF" if hex_[i] is None else hex_[i]) 

103 ax.add_patch(rect) 

104 if ylo > 0: 

105 ax.plot([0, 1], [ylo] * 2, ls = "-", c = "0") 

106 

107 # Checking `x` 

108 if isinstance(x, (ListedColormap, LinearSegmentedColormap)): 

109 from colorspace.cmap import cmap_to_sRGB 

110 x = cmap_to_sRGB(x).colors() 

111 elif not isinstance(x, list): 

112 raise TypeError("argument `x` must be list or matplotlib colormap") 

113 

114 # Checks if all entries are valid 

115 x = check_hex_colors(x) 

116 

117 # Checking `y` 

118 if isinstance(y, (LinearSegmentedColormap, ListedColormap)): 

119 # [!] Do not import as 'palette' (we have a variable called 'palette') 

120 from colorspace import palette as cp 

121 y = cp(y, n = len(x)).colors() 

122 if not isinstance(y, (type(None), list)): 

123 raise TypeError("argument `y` must be None or list") 

124 if not isinstance(y, type(None)): 

125 y = check_hex_colors(y) # Checks if all entries are valid 

126 if not len(x) == len(y): 

127 raise ValueError("if argument `y` is provided it must be of the same length as `x`") 

128 

129 # Sanity check for input arguemnts to control the different parts 

130 # of the spectogram plot. Namely rgb spectrum, hcl spectrum, and the palette. 

131 if not isinstance(rgb, bool): raise TypeError("argument `rgb` must be bool") 

132 if not isinstance(hcl, bool): raise TypeError("argument `hcl` must be bool") 

133 if not isinstance(palette, bool): raise TypeError("argument `palette` must be bool") 

134 if not rgb and not hcl and not palette: 

135 raise ValueError("disabling rgb, hcl, and palette all at the same time is not possible ") 

136 

137 if not isinstance(title, (type(None), str)): 

138 raise TypeError("argument `title` must be either None or str") 

139 

140 # Import hexcolors: convert colors to hexcolors for the plot if needed. 

141 from .colorlib import hexcols 

142 

143 # If input parameter "fix = True": fixing 

144 # the hue coordinates to avoid jumping which 

145 # can occur due to the HCL->RGB transformation. 

146 def fixcoords(x): 

147 

148 [H, C, L] = x 

149 n = len(H) # Number of colors 

150 # Fixing spikes 

151 import numpy as np 

152 for i in range(1,n): 

153 d = H[i] - H[i - 1] 

154 if np.abs(d) > 320.: H[i] = H[i] - np.sign(d) * 360. 

155 if np.abs(H[i]) > 360: H[0:i+1] = H[0:i+1] - np.sign(H[i]) * 360 

156 

157 # Smoothing the hue values in batches where chroma is very low 

158 idx = np.where(C < 8.)[0] 

159 if len(idx) == n: 

160 H = np.repeat(np.mean(H), n) 

161 else: 

162 # pre-smoothing the hue 

163 # Running mean 

164 if n > 49: 

165 H = 1./3. * (H + np.append(H[0],H[0:-1]) + np.append(H[1:],H[n-1])) 

166 

167 # TODO(enhancement): Spline smoother not yet implemented 

168 

169 return [H, C, L] 

170 

171 

172 # Calculate coordinates 

173 colors = {"x": x, "y": y} 

174 coords = {} 

175 for key, vals in colors.items(): 

176 # This happens if 'y' is set to 'None' (default) 

177 if vals is None: continue 

178 

179 # Else get HCL and RGB coordinates for all colors 

180 cols = hexcols(vals) 

181 cols.to("sRGB") 

182 coords[key] = {"hex":vals} 

183 if rgb: 

184 coords[key]["sRGB"] = [cols.get("R"), cols.get("G"), cols.get("B")] 

185 if hcl: 

186 cols.to("HCL") 

187 coords[key]["HCL"] = [cols.get("H"), cols.get("C"), cols.get("L")] 

188 if fix: coords[key]["HCL"] = fixcoords(coords[key]["HCL"]) 

189 

190 

191 from .colorlib import sRGB 

192 from .palettes import rainbow_hcl 

193 

194 # Specify the colors for the spectrum plots 

195 rgbcols = sRGB([0.8, 0, 0], [0, 0.8, 0], [0, 0, 0.8]) 

196 hclcols = rainbow_hcl()(4) 

197 

198 # Create figure 

199 from numpy import linspace, arange 

200 import matplotlib.ticker as ticker 

201 import matplotlib.pyplot as plt 

202 

203 # Create plot 

204 import numpy as np 

205 

206 # Open new figure. 

207 if not fig: 

208 hfig = plt.figure(**figargs) 

209 else: 

210 hfig = fig 

211 

212 # All three 

213 if rgb and hcl and palette: 

214 ax1 = plt.subplot2grid((7, 1), (0, 0), rowspan = 3) 

215 ax2 = plt.subplot2grid((7, 1), (3, 0)) 

216 ax3 = plt.subplot2grid((7, 1), (4, 0), rowspan = 3) 

217 # Only rgb and hcl spectra 

218 elif rgb and hcl: 

219 ax1 = plt.subplot2grid((2, 1), (0, 0)) 

220 ax3 = plt.subplot2grid((2, 1), (1, 0)) 

221 # Only rgb and palette 

222 elif rgb and palette: 

223 ax1 = plt.subplot2grid((4, 1), (0, 0), rowspan = 3) 

224 ax2 = plt.subplot2grid((4, 1), (3, 0)) 

225 # Only hcl and palette 

226 elif hcl and palette: 

227 ax2 = plt.subplot2grid((4, 1), (0, 0)) 

228 ax3 = plt.subplot2grid((4, 1), (1, 0), rowspan = 3) 

229 # Only rgb spectrum 

230 elif rgb: 

231 ax1 = plt.subplot2grid((1, 1), (0, 0)) 

232 # Only hcl spectrum 

233 elif hcl: 

234 ax3 = plt.subplot2grid((1, 1), (0, 0)) 

235 # Only palette 

236 elif palette: 

237 # Adjusting outer margins 

238 ax2 = plt.subplot2grid((1, 1), (0, 0)) 

239 hfig.subplots_adjust(left = 0., bottom = 0., right = 1., 

240 top = 1., wspace = 0., hspace = 0.) 

241 else: 

242 raise Exception("unexpected condition (ups, sorry)") 

243 

244 # Setting axis properties 

245 # ax1: RGB 

246 if rgb: 

247 ax1.set_xlim(0, 1); ax1.set_ylim(0, 1); 

248 ax1.get_xaxis().set_visible(False) 

249 # ax2: color map 

250 if palette: 

251 ax2.set_xlim(0, 1); ax2.set_ylim(len(coords), 0); 

252 ax2.get_xaxis().set_visible(False) 

253 ax2.get_yaxis().set_visible(False) 

254 # ax3 and ax33: HCL 

255 if hcl: 

256 ax3.set_xlim(0,1); ax3.set_ylim(0,100); ax3.get_xaxis().set_visible(False) 

257 ax33 = ax3.twinx() 

258 ax33.set_ylim(-360,360) 

259 

260 # Linestyles (used in case multiple palettes are handed over) 

261 linestyles = ["-", "--", "-.", ":"] 

262 

263 # Plotting RGB spectrum 

264 if rgb: 

265 count = 0 

266 for key,val in coords.items(): 

267 [R, G, B] = val["sRGB"] 

268 x = linspace(0., 1., len(R)) 

269 linestyle = linestyles[count % len(linestyles)] 

270 LR, = ax1.plot(x, R, color = rgbcols.colors()[0], 

271 linestyle = linestyle, label = "R" if key == "x" else None) 

272 LG, = ax1.plot(x, G, color = rgbcols.colors()[1], 

273 linestyle = linestyle, label = "G" if key == "x" else None) 

274 LB, = ax1.plot(x, B, color = rgbcols.colors()[2], 

275 linestyle = linestyle, label = "B" if key == "x" else None) 

276 ax1.legend(loc = "upper left", ncols = 3, frameon = False, 

277 handlelength = 1, borderpad = 0) 

278 count += 1 

279 

280 # Plotting the color map 

281 if palette: 

282 for i in range(len(coords)): 

283 cmap(ax2, coords[list(coords.keys())[i]]["hex"], ylo = i) 

284 

285 # Plotting HCL spectrum 

286 if hcl: 

287 count = 0 

288 for key,val in coords.items(): 

289 [H, C, L] = val["HCL"] 

290 

291 # Setting limits for left y-axis 

292 ymax = max(0, max(C), max(L)) 

293 ax3.set_ylim(0, ymax * 1.05) 

294 

295 x = linspace(0., 1., len(H)) 

296 linestyle = linestyles[count % len(linestyles)] 

297 ax3.plot(x, C, color = hclcols[1], 

298 linestyle = linestyle, label = "C" if key == "x" else None) 

299 ax3.plot(x, L, color = hclcols[2], 

300 linestyle = linestyle, label = "L" if key == "x" else None) 

301 ax33.plot(x, H, color = hclcols[0], 

302 linestyle = linestyle, label = "H" if key == "x" else None) 

303 

304 ax3.legend(loc = "upper left", ncols = 3, frameon = False, 

305 handlelength = 1, borderpad = 0) 

306 ax33.legend(loc = "upper right", ncols = 3, frameon = False, 

307 handlelength = 1, borderpad = 0) 

308 

309 # If the minimum of H does not go below 0, set axis to 0, 360 

310 ax33.set_yticks(arange(-360, 361, 120)) 

311 ax33.set_ylim(-360 if min(H) < 0 else 0, 360) 

312 

313 count += 1 

314 ax33.yaxis.set_major_formatter(ticker.FormatStrFormatter('%0.0f')) 

315 

316 # Labels and annotations 

317 if rgb: 

318 ax1.set_ylabel("Red/Green/Blue") 

319 ax1.xaxis.set_label_position("top") 

320 ax1.text(0.5, 1.05, "RGB Spectrum", horizontalalignment = "center", 

321 verticalalignment = "bottom", fontsize = 10, fontweight = "bold") 

322 if hcl: 

323 ax3.set_ylabel("Chroma/Luminance") 

324 ax33.set_ylabel("Hue") 

325 ax3.text(0.5, -10, "HCL Spectrum", horizontalalignment = "center", 

326 verticalalignment = "top", fontsize = 10, fontweight = "bold") 

327 

328 if isinstance(title, str): 

329 plt.gcf().get_axes()[0].set_title(title, va = "top", 

330 fontdict = dict(fontsize = "large", fontweight = "semibold")) 

331 

332 

333 # Show figure or return the Axes object (in case `ax` has not been None). 

334 if not fig: plt.show() # Show figure 

335 

336 return hfig 

337 

338