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

156 statements  

« prev     ^ index     » next       coverage.py v7.6.4, created at 2024-10-29 15:11 +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 hfig.clf() # Clear existing axis 

212 

213 # All three 

214 if rgb and hcl and palette: 

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

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

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

218 # Only rgb and hcl spectra 

219 elif rgb and hcl: 

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

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

222 # Only rgb and palette 

223 elif rgb and palette: 

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

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

226 # Only hcl and palette 

227 elif hcl and palette: 

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

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

230 # Only rgb spectrum 

231 elif rgb: 

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

233 # Only hcl spectrum 

234 elif hcl: 

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

236 # Only palette 

237 elif palette: 

238 # Adjusting outer margins 

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

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

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

242 else: 

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

244 

245 # Setting axis properties 

246 # ax1: RGB 

247 if rgb: 

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

249 ax1.get_xaxis().set_visible(False) 

250 # ax2: color map 

251 if palette: 

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

253 ax2.get_xaxis().set_visible(False) 

254 ax2.get_yaxis().set_visible(False) 

255 # ax3 and ax33: HCL 

256 if hcl: 

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

258 ax33 = ax3.twinx() 

259 ax33.set_ylim(-360,360) 

260 

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

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

263 

264 # Plotting RGB spectrum 

265 if rgb: 

266 count = 0 

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

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

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

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

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

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

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

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

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

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

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

278 handlelength = 1, borderpad = 0) 

279 count += 1 

280 

281 # Plotting the color map 

282 if palette: 

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

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

285 

286 # Plotting HCL spectrum 

287 if hcl: 

288 count = 0 

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

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

291 

292 # Setting limits for left y-axis 

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

294 ax3.set_ylim(0, ymax * 1.05) 

295 

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

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

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

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

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

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

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

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

304 

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

306 handlelength = 1, borderpad = 0) 

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

308 handlelength = 1, borderpad = 0) 

309 

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

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

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

313 

314 count += 1 

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

316 

317 # Labels and annotations 

318 if rgb: 

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

320 ax1.xaxis.set_label_position("top") 

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

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

323 if hcl: 

324 ax3.set_ylabel("Chroma/Luminance") 

325 ax33.set_ylabel("Hue") 

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

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

328 

329 if isinstance(title, str): 

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

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

332 

333 

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

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

336 

337 return hfig 

338 

339