26 Dec 2017
Plotting 3D Vectors Using HSL in Python
Thanks to advocacy on the part of data visualization and color experts, more and more people are using perceptually uniform color schemes. Also, modern plotting and visualization tools are starting to adopt uniform colormaps as their defaults. There are plenty of better places to learn details about why perecptualy uniformity is desirable, but the short answer is that nonuniformities impose artifacts on your data. The gradients in your data can be exaggerated or masked by the choice of colormap. The difference is obvious when you look at uniform and nonuniform maps next to eachother, especially when they are converted to grayscale, which is a measure of lightness.
Recently I wanted to plot a vector field (magnetization of a lattice) as an
image by creating a mapping between a vector and a color, a common technique.
Typically a cylindrical color coordinate like Hue-Saturation-Lightness is
used. Each pixel in the image represents a vector. The color of the pixel is
determined by the components of the vector: the hue is determined by the
azimuthal angle, the saturation by the length of the vector, and the lightness
by the z-component. In each pixel of the example image below the saturation is
unity and the z-component is 0. This is a plot of
arctan(y/x) so only
the azimuth is changing.
Unfortunately, the HSL scheme, as implemented in most visualization software, is not uniform. Notice the radial streaks above. The state of the art method for solving this problem, as far as I can tell (I’m a scientist not a dataviz expert!), is HSLuv. The designers of this scheme formed a special subregion of the HSL space designed to be uniform, and then released implementations for many major programing languages.
Below are some examples using Python.
In matplotlib, HSL values and RGB values are all 3-vectors with values in [0, 1].
# Convert to cylindrical x, y, z = [cartesian_data[:,:,i] for i in range(3)] r = np.sqrt(x**2 + y**2) phi = np.arctan2(y, x) # Change domain from [-pi, pi] to [0, 2pi] phi[phi < 0] += 2 * np.pi # Normalize so that all are in [0, 1] r, phi, z = [normalize(i) for i in (r, phi, z)] # Plot with matplotlib, conversion func available from colorsys # Install with pip install colorsys import matplotlib.pyplot as plt from colorsys import hls_to_rgb # Unfortunately this function doesn't work with numpy.ndarray rgb = [hls_to_rgb(phi[i,j], z,[i,j] r[i,j]) for j in range(N)] for i in range(N)] plt.imshow(rgb) plt.show()
In HSLuv the input values to the conversion function have different ranges. Hue
is in [0, 360], and saturation and lightness are in [0, 100]. HSLuv is
pip installable with
pip install hsluv. Notice that the
hsluv functions have slightly different call signatures.
phi *= 360.0 r *= 100.0 z *= 100.0 from hsluv import hsluv_to_rgb rgb = [hlsuv_to_rgb(phi[i,j], z,[i,j] r[i,j]) for j in range(N)] for i in range(N)] plt.imshow(rgb) plt.show()