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
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-23 19:54 +0000
3def specplot(x, y = None, hcl = True, palette = True, fix = True, rgb = False, \
4 title = None, fig = None, **figargs):
5 """Color Spectrum Plot
7 Visualization of color palettes (given as hex codes) in HCL and/or
8 RGB coordinates.
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.
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.
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`.
37 Example:
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));
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 """
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?)")
80 from .utils import check_hex_colors
81 from matplotlib.colors import LinearSegmentedColormap, ListedColormap
83 # Support function to draw the color map (the color strip)
84 def cmap(ax, hex_, ylo = 0):
85 """Plotting cmap-based palettes
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 """
94 from numpy import linspace
95 from matplotlib.patches import Rectangle
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")
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")
114 # Checks if all entries are valid
115 x = check_hex_colors(x)
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`")
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 ")
137 if not isinstance(title, (type(None), str)):
138 raise TypeError("argument `title` must be either None or str")
140 # Import hexcolors: convert colors to hexcolors for the plot if needed.
141 from .colorlib import hexcols
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):
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
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]))
167 # TODO(enhancement): Spline smoother not yet implemented
169 return [H, C, L]
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
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"])
191 from .colorlib import sRGB
192 from .palettes import rainbow_hcl
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)
198 # Create figure
199 from numpy import linspace, arange
200 import matplotlib.ticker as ticker
201 import matplotlib.pyplot as plt
203 # Create plot
204 import numpy as np
206 # Open new figure.
207 if not fig:
208 hfig = plt.figure(**figargs)
209 else:
210 hfig = fig
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)")
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)
260 # Linestyles (used in case multiple palettes are handed over)
261 linestyles = ["-", "--", "-.", ":"]
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
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)
285 # Plotting HCL spectrum
286 if hcl:
287 count = 0
288 for key,val in coords.items():
289 [H, C, L] = val["HCL"]
291 # Setting limits for left y-axis
292 ymax = max(0, max(C), max(L))
293 ax3.set_ylim(0, ymax * 1.05)
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)
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)
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)
313 count += 1
314 ax33.yaxis.set_major_formatter(ticker.FormatStrFormatter('%0.0f'))
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")
328 if isinstance(title, str):
329 plt.gcf().get_axes()[0].set_title(title, va = "top",
330 fontdict = dict(fontsize = "large", fontweight = "semibold"))
333 # Show figure or return the Axes object (in case `ax` has not been None).
334 if not fig: plt.show() # Show figure
336 return hfig