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
« prev ^ index » next coverage.py v7.6.4, created at 2024-10-29 15:11 +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
211 hfig.clf() # Clear existing axis
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)")
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)
261 # Linestyles (used in case multiple palettes are handed over)
262 linestyles = ["-", "--", "-.", ":"]
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
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)
286 # Plotting HCL spectrum
287 if hcl:
288 count = 0
289 for key,val in coords.items():
290 [H, C, L] = val["HCL"]
292 # Setting limits for left y-axis
293 ymax = max(0, max(C), max(L))
294 ax3.set_ylim(0, ymax * 1.05)
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)
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)
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)
314 count += 1
315 ax33.yaxis.set_major_formatter(ticker.FormatStrFormatter('%0.0f'))
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")
329 if isinstance(title, str):
330 plt.gcf().get_axes()[0].set_title(title, va = "top",
331 fontdict = dict(fontsize = "large", fontweight = "semibold"))
334 # Show figure or return the Axes object (in case `ax` has not been None).
335 if not fig: plt.show() # Show figure
337 return hfig