From 99a4eadbe777937a1523802e3265c227121a5582 Mon Sep 17 00:00:00 2001 From: Robert Kern Date: Wed, 4 Jun 2014 23:05:02 +0100 Subject: [PATCH 1/8] ENH: Add new perceptual colormaps. --- chaco/default_colormaps.py | 118 +++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/chaco/default_colormaps.py b/chaco/default_colormaps.py index 9c80c1c42..2f10fe275 100644 --- a/chaco/default_colormaps.py +++ b/chaco/default_colormaps.py @@ -6654,6 +6654,120 @@ def gist_yarg(range, **traits): return ColorMapper.from_segment_map(_data, range=range, **traits) +def CubicYF(range, **traits): + """ Generator of the 'CubicYF' colormap from Matteo Niccoli. + + Lab-based rainbow scheme with cubic-law luminance. + + http://mycarta.wordpress.com/color-palettes/ + """ + palette = np.array([ + [0.5151, 0.0482, 0.6697], + [0.5199, 0.1762, 0.8083], + [0.4884, 0.2912, 0.9234], + [0.4297, 0.3855, 0.9921], + [0.3893, 0.4792, 0.9775], + [0.3337, 0.5650, 0.9056], + [0.2795, 0.6419, 0.8287], + [0.2210, 0.7123, 0.7258], + [0.2468, 0.7612, 0.6248], + [0.2833, 0.8125, 0.5069], + [0.3198, 0.8492, 0.3956], + [0.3602, 0.8896, 0.2919], + [0.4568, 0.9136, 0.3018], + [0.6033, 0.9255, 0.3295], + [0.7066, 0.9255, 0.3414], + [0.8000, 0.9255, 0.3529], + ]) + return ColorMapper.from_palette_array(palette, range=range, **traits) + + +def CubicL(range, **traits): + """ Generator of the 'CubicL' colormap from Matteo Niccoli. + + Lab-based rainbow scheme with cubic-law luminance, like `CubicYF` + but with red at the high end, a modest deviation from being + completely perceptual. + + http://mycarta.wordpress.com/color-palettes/ + """ + palette = np.array([ + [0.4706, 0.0000, 0.5216], + [0.5137, 0.0527, 0.7096], + [0.4942, 0.2507, 0.8781], + [0.4296, 0.3858, 0.9922], + [0.3691, 0.5172, 0.9495], + [0.2963, 0.6191, 0.8515], + [0.2199, 0.7134, 0.7225], + [0.2643, 0.7836, 0.5756], + [0.3094, 0.8388, 0.4248], + [0.3623, 0.8917, 0.2858], + [0.5200, 0.9210, 0.3137], + [0.6800, 0.9255, 0.3386], + [0.8000, 0.9255, 0.3529], + [0.8706, 0.8549, 0.3608], + [0.9514, 0.7466, 0.3686], + [0.9765, 0.5887, 0.3569], + ]) + return ColorMapper.from_palette_array(palette, range=range, **traits) + + +def LinearL(range, **traits): + """ Generator of the 'LinearL' colormap from Matteo Niccoli. + + Lab-based linear lightness rainbow. + + http://mycarta.wordpress.com/color-palettes/ + """ + palette = np.array([ + [0.0143, 0.0143, 0.0143], + [0.1413, 0.0555, 0.1256], + [0.1761, 0.0911, 0.2782], + [0.1710, 0.1314, 0.4540], + [0.1074, 0.2234, 0.4984], + [0.0686, 0.3044, 0.5068], + [0.0008, 0.3927, 0.4267], + [0.0000, 0.4763, 0.3464], + [0.0000, 0.5565, 0.2469], + [0.0000, 0.6381, 0.1638], + [0.2167, 0.6966, 0.0000], + [0.3898, 0.7563, 0.0000], + [0.6912, 0.7795, 0.0000], + [0.8548, 0.8041, 0.4555], + [0.9712, 0.8429, 0.7287], + [0.9692, 0.9273, 0.8961], + ]) + return ColorMapper.from_palette_array(palette, range=range, **traits) + + +def LinearLHot(range, **traits): + """ Generator of the 'LinearLHot' colormap from Matteo Niccoli. + + Linear lightness modification of the `hot` colormap. + + http://mycarta.wordpress.com/color-palettes/ + """ + palette = np.array([ + [0.0225, 0.0121, 0.0121], + [0.1927, 0.0225, 0.0311], + [0.3243, 0.0106, 0.0000], + [0.4463, 0.0000, 0.0091], + [0.5706, 0.0000, 0.0737], + [0.6969, 0.0000, 0.1337], + [0.8213, 0.0000, 0.1792], + [0.8636, 0.0000, 0.0565], + [0.8821, 0.2555, 0.0000], + [0.8720, 0.4182, 0.0000], + [0.8424, 0.5552, 0.0000], + [0.8031, 0.6776, 0.0000], + [0.7659, 0.7870, 0.0000], + [0.8170, 0.8296, 0.0000], + [0.8853, 0.8896, 0.4113], + [0.9481, 0.9486, 0.7165], + ]) + return ColorMapper.from_palette_array(palette, range=range, **traits) + + # Make the convenient list of all the function names as well as a dictionary # of name->function mappings. These are useful for UI editors. @@ -6712,6 +6826,10 @@ def gist_yarg(range, **traits): gist_rainbow, gist_stern, gist_yarg, + CubicYF, + CubicL, + LinearL, + LinearLHot, ] color_map_dict = {} From 48c136d69ee8ec8ea061d39e52712b93f4419920 Mon Sep 17 00:00:00 2001 From: Robert Kern Date: Wed, 4 Jun 2014 23:06:07 +0100 Subject: [PATCH 2/8] ENH: CH-CH-CHANGES.txt --- CHANGES.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index db43f2ead..321aa85ad 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -10,7 +10,9 @@ Change summary since 4.4.1 New features - * Addeded asynchronous_updates.py demo that shows a pattern for generating + * Added perceptual colormaps by Matteo Niccoli. + + * Added asynchronous_updates.py demo that shows a pattern for generating expensive plots while keeping the interface responsive (PR#170). Fixes From 3442db58abd502d243bbb9fd640c1a904cceb9e9 Mon Sep 17 00:00:00 2001 From: Robert Kern Date: Fri, 13 Jun 2014 20:03:25 +0200 Subject: [PATCH 3/8] ENH: Add Kenneth Moreland's CoolWarm colormap and generator function for general diverging colormaps. --- chaco/color_spaces.py | 625 ++++++++++++++++++++++++++++++++++++ chaco/default_colormaps.py | 26 +- chaco/diverging_colormap.py | 85 +++++ 3 files changed, 731 insertions(+), 5 deletions(-) create mode 100644 chaco/color_spaces.py create mode 100644 chaco/diverging_colormap.py diff --git a/chaco/color_spaces.py b/chaco/color_spaces.py new file mode 100644 index 000000000..566b37423 --- /dev/null +++ b/chaco/color_spaces.py @@ -0,0 +1,625 @@ +""" Conversion functions between various color spaces. + +The implementations and data are mostly taken from the old +scipy.sandbox.image package. + +The CIE XYZ tristimulus colorspace with a standard D65 whitepoint is the +default interchange color space for the implementations here. This is +a useful whitepoint for viewing on computer monitors. However, it should +be noted that the dimmer D50 whitepoint is often used in print +applications. Notably, ICC profiles use the XYZ space with a D50 +whitepoint as one of its standard interchange color spaces. +""" + +import numpy as np +from numpy.linalg import inv, solve + + +#### Utilities ################################################################ + +def convert(matrix, TTT, axis=-1): + """ Apply linear matrix transformation to an array of color triples. + + Parameters + ---------- + matrix : float array (3, 3) + The transformation to apply. + TTT : float array + The set of colors to transform. + axis : int, optional + The axis of `TTT` along which the color triples extend. + + Returns + ------- + OUT : float array + The transformed colors. + """ + TTT = np.asarray(TTT) + if (axis != 0): + TTT = np.swapaxes(TTT, 0, axis) + oldshape = TTT.shape + TTT = np.reshape(TTT, (3, -1)) + OUT = np.dot(matrix, TTT) + OUT.shape = oldshape + if (axis != 0): + OUT = np.swapaxes(OUT, axis, 0) + return OUT + + +def makeslices(n): + """ Return a list of `n` slice objects. + + Each slice object corresponds to [:] without arguments. + """ + slices = [slice(None)] * n + return slices + + +def separate_colors(xyz, axis=-1): + """ Separate an array of color triples into three arrays, one for + each color axis. + + Parameters + ---------- + xyz : float array + axis : int, optional + The axis along which the color triples extend. + + Returns + ------- + x : float array + y : float array + z : float array + The separate color arrays. + axis : int + The axis along which they need to be reassembled. + """ + n = len(xyz.shape) + if axis < 0: + axis = n + axis + slices = makeslices(n) + slices[axis] = 0 + x = xyz[slices] + slices[axis] = 1 + y = xyz[slices] + slices[axis] = 2 + z = xyz[slices] + + return x, y, z, axis + + +def join_colors(c1, c2, c3, axis): + """ Rejoin the separated colors into a single array. + """ + c1 = np.asarray(c1) + c2 = np.asarray(c2) + c3 = np.asarray(c3) + newshape = c1.shape[:axis] + (1,) + c1.shape[axis:] + c1.shape = c2.shape = c3.shape = newshape + return np.concatenate((c1, c2, c3), axis=axis) + + +def triwhite(x, y): + """ Convert x,y chromaticity coordinates to XYZ tristimulus values. + """ + X = x / y + Y = 1.0 + Z = (1-x-y)/y + return [X, Y, Z] + + +def adapt_whitepoint(src, dst): + """ Compute the adaptation matrix for converting XYZ tristimulus + values from a one standard illuminant to another using the Bradford + transform. + + This implementation follows the presentation on + http://www.color.org/chadtag.html + + The results are cached. + + Parameters + ---------- + src : str + dst : str + The names of the standard illuminants to convert from and to, + respectively. Valid values are the keys of `whitepoints` like + 'D65' or 'CIE A'. + + Returns + ------- + adapt : float array (3, 3) + Matrix-multiply this matrix against XYZ values with the `src` + whitepoint to get XYZ values with the `dst` whitepoint. + """ + # Check the cache first. + key = (src, dst) + if key not in adapt_whitepoint.cache: + bradford = np.array([[+0.8951, 0.2664, -0.1614], + [-0.7502, 1.7135, 0.0367], + [+0.0389, -0.0685, 1.0296]]) + + src_whitepoint = whitepoints[src][-1] + src_rgb = np.dot(bradford, src_whitepoint) + dst_whitepoint = whitepoints[dst][-1] + dst_rgb = np.dot(bradford, dst_whitepoint) + + scale = dst_rgb / src_rgb + adapt_whitepoint.cache[key] = solve( + bradford, + scale[:, np.newaxis] * bradford, + ) + + adapt = adapt_whitepoint.cache[key] + return adapt + +# Create the cache. +adapt_whitepoint.cache = {} + +#### Data ##################################################################### + +# From the sRGB specification. +xyz_from_rgb = np.array([[0.412453, 0.357580, 0.180423], + [0.212671, 0.715160, 0.072169], + [0.019334, 0.119193, 0.950227]]) +rgb_from_xyz = inv(xyz_from_rgb) + +# This transformation to LMS space keeps the peaks of colormatching curves +# normalized to 1. +lms_from_xyz = np.array([ + [+0.2434974736455316, 0.8523911562030849, -0.0515994646411065], + [-0.3958579552426224, 1.1655483851630273, 0.0837969419671409], + [+0.0, 0.0, 0.6185822095756526], +]) +xyz_from_lms = inv(lms_from_xyz) + +# The transformation from XYZ to the ATD opponent colorspace. These are +# "official" values directly from Guth 1980. +atd_from_xyz = np.array([[+0., 0.9341, 0.], + [+0.7401, -0.6801, -0.1567], + [-0.0061, -0.0212, 0.0314]]) +xyz_from_atd = inv(atd_from_xyz) + +# Now we need to compute the intermediate transformations between LMS and ATD. +# We derive these directly from the other two instead of specifying potentially +# truncated values. +atd_from_lms = solve(lms_from_xyz.T, atd_from_xyz.T).T +lms_from_atd = solve(atd_from_xyz.T, lms_from_xyz.T).T + + +# XYZ white-point coordinates +# from http://www.aim-dtp.net/aim/technology/cie_xyz/cie_xyz.htm +whitepoints = { + 'CIE A': ['Normal incandescent', triwhite(0.4476, 0.4074)], + 'CIE B': ['Direct sunlight', triwhite(0.3457, 0.3585)], + 'CIE C': ['Average sunlight', triwhite(0.3101, 0.3162)], + 'CIE E': ['Normalized reference', triwhite(1.0/3, 1.0/3)], + 'D50': ['Bright tungsten', triwhite(0.3457, 0.3585)], + 'D55': ['Cloudy daylight', triwhite(0.3324, 0.3474)], + 'D65': ['Daylight', triwhite(0.312713, 0.329016)], + 'D75': ['?', triwhite(0.299, 0.3149)], + 'D93': ['low-quality old CRT', triwhite(0.2848, 0.2932)], +} + + +#### Conversion routines ###################################################### + +def xyz2lab(xyz, axis=-1, wp=whitepoints['D65'][-1]): + """ Convert XYZ tristimulus values to CIE L*a*b*. + + Parameters + ---------- + xyz : float array + XYZ values. + axis : int, optional + The axis of the XYZ values. + wp : list of 3 floats, optional + The XYZ tristimulus values of the whitepoint. + + Returns + ------- + lab : float array + The L*a*b* colors. + """ + x, y, z, axis = separate_colors(xyz, axis) + xn, yn, zn = x/wp[0], y/wp[1], z/wp[2] + + def f(t): + eps = 216/24389. + kap = 24389/27. + return np.where(t > eps, + np.power(t, 1.0/3), + (kap*t + 16.0)/116) + + fx, fy, fz = f(xn), f(yn), f(zn) + L = 116*fy - 16 + a = 500*(fx - fy) + b = 200*(fy - fz) + + return join_colors(L, a, b, axis) + + +def lab2xyz(lab, axis=-1, wp=whitepoints['D65'][-1]): + """ Convert CIE L*a*b* colors to XYZ tristimulus values. + + Parameters + ---------- + lab : float array + L*a*b* values. + axis : int, optional + The axis of the XYZ values. + wp : list of 3 floats, optional + The XYZ tristimulus values of the whitepoint. + + Returns + ------- + xyz : float array + The XYZ colors. + """ + lab = np.asarray(lab) + L, a, b, axis = separate_colors(lab, axis) + fy = (L+16)/116.0 + fz = fy - b / 200. + fx = a/500.0 + fy + + def finv(y): + eps3 = (216/24389.)**3 + kap = 24389/27. + return np.where(y > eps3, + np.power(y, 3), + (116*y - 16)/kap) + xr, yr, zr = finv(fx), finv(fy), finv(fz) + + return join_colors(xr*wp[0], yr*wp[1], zr*wp[2], axis) + + +def _uv(x, y, z): + """ The u, v formulae for CIE 1976 L*u*v* computations. + """ + denominator = (x + 15*y + 3*z) + zeros = (denominator == 0.0) + denominator = np.where(zeros, 1.0, denominator) + u_numerator = np.where(zeros, 4.0, 4 * x) + v_numerator = np.where(zeros, 9.0, 9 * y) + + return u_numerator/denominator, v_numerator/denominator + + +def xyz2luv(xyz, axis=-1, wp=whitepoints['D65'][-1]): + """ Convert XYZ tristimulus values to CIE L*u*v*. + + Parameters + ---------- + xyz : float array + XYZ values. + axis : int, optional + The axis of the XYZ values. + wp : list of 3 floats, optional + The XYZ tristimulus values of the whitepoint. + + Returns + ------- + luv : float array + The L*u*v* colors. + """ + x, y, z, axis = separate_colors(xyz, axis) + xn, yn, zn = x/wp[0], y/wp[1], z/wp[2] + Ls = 116.0 * np.power(yn, 1./3) - 16.0 + small_mask = (y <= 0.008856*wp[1]) + Ls[small_mask] = 903.0 * y[small_mask] / wp[1] + unp, vnp = _uv(*wp) + up, vp = _uv(x, y, z) + us = 13 * Ls * (up - unp) + vs = 13 * Ls * (vp - vnp) + + return join_colors(Ls, us, vs, axis) + + +def luv2xyz(luv, axis=-1, wp=whitepoints['D65'][-1]): + """ Convert CIE L*u*v* colors to XYZ tristimulus values. + + Parameters + ---------- + luv : float array + L*u*v* values. + axis : int, optional + The axis of the XYZ values. + wp : list of 3 floats, optional + The XYZ tristimulus values of the whitepoint. + + Returns + ------- + xyz : float array + The XYZ colors. + """ + Ls, us, vs, axis = separate_colors(luv, axis) + unp, vnp = _uv(*wp) + small_mask = (Ls <= 903.3 * 0.008856) + y = wp[1] * ((Ls + 16.0) / 116.0) ** 3 + y[small_mask] = Ls[small_mask] * wp[1] / 903.0 + # Where L==0, X=Y=Z=0. Avoid the nans and infs in the meantime. + black = (Ls == 0) + Ls[black] = 1.0 + + up = us / (13*Ls) + unp + vp = vs / (13*Ls) + vnp + x = 9.0 * y * up / (4.0 * vp) + z = -x / 3.0 - 5.0 * y + 3.0 * y/vp + + x[black] = 0.0 + y[black] = 0.0 + z[black] = 0.0 + + return join_colors(x, y, z, axis) + + +def xyz2hcl(xyz, axis=-1, wp=whitepoints['D65'][-1], luvlab='luv'): + """ Convert XYZ tristimulus values to HCL. + + HCL (Hue - Chroma - Luminance) is a cylindrical-coordinate form of + Luv or Lab. + + Parameters + ---------- + xyz : float array + XYZ values. + axis : int, optional + The axis of the XYZ values. + wp : list of 3 floats, optional + The XYZ tristimulus values of the whitepoint. + luvlab : 'luv' or 'lab', optional + Whether to use the L*u*v* or L*a*b*. + + Returns + ------- + hcl : float array + The HCL colors. + """ + if luvlab == 'luv': + xyz2lxx = xyz2luv + else: + xyz2lxx = xyz2lab + + Ls, x1, x2, axis = separate_colors(xyz2lxx(xyz, axis=axis, wp=wp), axis) + Cs = np.hypot(x1, x2) + Hs = (180. / np.pi) * np.arctan2(x2, x1) + + return join_colors(Hs, Cs, Ls, axis) + + +def hcl2xyz(hcl, axis=-1, wp=whitepoints['D65'][-1], luvlab='luv'): + """ Convert HCL values to XYZ tristimulus values. + + Parameters + ---------- + hcl : float array + Te HCL colors. + axis : int, optional + The axis of the XYZ values. + wp : list of 3 floats, optional + The XYZ tristimulus values of the whitepoint. + luvlab : 'luv' or 'lab', optional + Whether to use the L*u*v* or L*a*b*. + + Returns + ------- + xyz : float array + XYZ values. + """ + if luvlab == 'luv': + lxx2xyz = luv2xyz + else: + lxx2xyz = lab2xyz + + Hs, Cs, Ls, axis = separate_colors(hcl, axis) + theta = Hs * (np.pi / 180) + x1 = Cs * np.cos(theta) + x2 = Cs * np.sin(theta) + + return lxx2xyz(join_colors(Ls, x1, x2, axis), axis=axis, wp=wp) + + +# RGB values that will be displayed on a screen are always nonlinear +# R'G'B' values. To get the XYZ value of the color that will be +# displayed you need a calibrated monitor with a profile. +# But, for quick-and-dirty calculation you can often assume the standard +# sR'G'B' coordinate system for your computer, and so the rgbp2rgb will +# put you in the linear coordinate system (assuming normalized to [0,1] +# sR'G'B' coordinates) +# + +# sRGB <-> sR'G'B' equations from +# http://www.w3.org/Graphics/Color/sRGB +# http://www.srgb.com/basicsofsrgb.htm + +# Macintosh displays are usually gamma = 1.8 + +def rgb2rgbp(rgb, gamma=None): + """ Convert linear RGB coordinates to nonlinear R'G'B' coordinates. + + Parameters + ---------- + rgb : float array + gamma : float, optional + If provided, then this value of gamma will be used to correct the + colors. If not provided, then the standard sR'G'B' space will be + assumed. It is almost, but not quite equivalent to a gamma of 2.2. + + Returns + ------- + rgbp : float array + """ + rgb = np.asarray(rgb) + if gamma is None: + eps = 0.0031308 + mask = rgb < eps + rgbp = np.empty_like(rgb) + rgbp[mask] = 12.92 * rgb[mask] + rgbp[~mask] = 1.055*rgb[~mask]**(1.0/2.4) - 0.055 + return rgbp + else: + return rgb**(1.0/gamma) + + +def rgbp2rgb(rgbp, gamma=None): + """ Convert nonlinear R'G'B' coordinates to linear RGB coordinates. + + Parameters + ---------- + rgbp : float array + gamma : float, optional + If provided, then this value of gamma will be used to correct the + colors. If not provided, then the standard sR'G'B' space will be + assumed. It is almost, but not quite equivalent to a gamma of 2.2. + + Returns + ------- + rgb : float array + """ + rgbp = np.asarray(rgbp) + if gamma is None: + eps = 0.04045 + mask = rgbp <= eps + rgb = np.empty_like(rgbp) + rgb[mask] = rgbp[mask] / 12.92 + rgb[~mask] = ((rgbp[~mask] + 0.055) / 1.055) ** 2.4 + return rgb + else: + return rgbp**gamma + + +def xyz2rgb(xyz, axis=-1): + """ Convert XYZ tristimulus values to linear RGB coordinates. + + Parameters + ---------- + xyz : float array + XYZ values. + axis : int, optional + The axis of the XYZ values. + + Returns + ------- + rgb : float array + The RGB colors. + """ + return convert(rgb_from_xyz, xyz, axis) + + +def rgb2xyz(rgb, axis=-1): + """ Convert linear RGB coordinates to XYZ tristimulus values. + + Parameters + ---------- + rgb : float array + RGB values. + axis : int, optional + The axis of the XYZ values. + + Returns + ------- + xyz : float array + The XYZ colors. + """ + return convert(xyz_from_rgb, rgb, axis) + + +def srgb2xyz(srgb, axis=-1): + """ Convert sR'G'B' colors to XYZ. + + Parameters + ---------- + srgb : float array + sR'G'B' values. + axis : int, optional + The axis of the XYZ values. + + Returns + ------- + xyz : float array + The XYZ colors. + """ + return rgb2xyz(rgbp2rgb(srgb), axis=axis) + + +def xyz2srgb(xyz, axis=-1): + """ Convert XYZ colors to sR'G'B'. + + Parameters + ---------- + xyz : float array + XYZ values. + axis : int, optional + The axis of the XYZ values. + + Returns + ------- + srgb : float array + The sR'G'B' colors. + """ + return rgb2rgbp(xyz2rgb(xyz, axis=axis)) + + +def xyz2xyz(xyz): + """ Identity mapping. + """ + return xyz + + +def xyz2msh(xyz, axis=-1, wp=whitepoints['D65'][-1]): + """ Convert XYZ tristimulus values to Msh. + + Msh is a hemispherical coordinate system derived from L*a*b*. The + origin remains the same. M is the distance from the origin. s is an + inclination angle from the vertical corresponding to saturation. + h is the azimuthal angle corresponding to hue. + + Moreland, Kenneth. Diverging Color Maps for Scientific Visualization + (Expanded). + http://www.sandia.gov/~kmorel/documents/ColorMaps/ColorMapsExpanded.pdf + + Parameters + ---------- + xyz : float array + XYZ values. + axis : int, optional + The axis of the XYZ values. + wp : list of 3 floats, optional + The XYZ tristimulus values of the whitepoint. + + Returns + ------- + msh : float array + The Msh colors. + """ + L, a, b, axis = separate_colors(xyz2lab(xyz, axis=axis, wp=wp), axis) + M = np.sqrt(L*L + a*a + b*b) + s = np.arccos(L / M) + h = np.arctan2(b, a) + + return join_colors(M, s, h, axis) + + +def msh2xyz(msh, axis=-1, wp=whitepoints['D65'][-1]): + """ Convert Msh values to XYZ tristimulus values. + + Parameters + ---------- + msh : float array + The Msh colors. + axis : int, optional + The axis of the XYZ values. + wp : list of 3 floats, optional + The XYZ tristimulus values of the whitepoint. + + Returns + ------- + xyz : float array + XYZ values. + """ + M, s, h, axis = separate_colors(msh, axis) + L = M * np.cos(s) + a = M * np.sin(s) * np.cos(h) + b = M * np.sin(s) * np.sin(h) + + return lab2xyz(join_colors(L, a, b, axis), axis=axis, wp=wp) diff --git a/chaco/default_colormaps.py b/chaco/default_colormaps.py index 2f10fe275..16dbda800 100644 --- a/chaco/default_colormaps.py +++ b/chaco/default_colormaps.py @@ -24,7 +24,8 @@ from numpy import array # Local imports. -from color_mapper import ColorMapper +from .color_mapper import ColorMapper +from .diverging_colormaps import generate_diverging_palette # The colormaps will be added to this at the end of the file. __all__ = ['reverse', 'center', 'color_map_functions', 'color_map_dict', @@ -6661,7 +6662,7 @@ def CubicYF(range, **traits): http://mycarta.wordpress.com/color-palettes/ """ - palette = np.array([ + palette = array([ [0.5151, 0.0482, 0.6697], [0.5199, 0.1762, 0.8083], [0.4884, 0.2912, 0.9234], @@ -6691,7 +6692,7 @@ def CubicL(range, **traits): http://mycarta.wordpress.com/color-palettes/ """ - palette = np.array([ + palette = array([ [0.4706, 0.0000, 0.5216], [0.5137, 0.0527, 0.7096], [0.4942, 0.2507, 0.8781], @@ -6719,7 +6720,7 @@ def LinearL(range, **traits): http://mycarta.wordpress.com/color-palettes/ """ - palette = np.array([ + palette = array([ [0.0143, 0.0143, 0.0143], [0.1413, 0.0555, 0.1256], [0.1761, 0.0911, 0.2782], @@ -6747,7 +6748,7 @@ def LinearLHot(range, **traits): http://mycarta.wordpress.com/color-palettes/ """ - palette = np.array([ + palette = array([ [0.0225, 0.0121, 0.0121], [0.1927, 0.0225, 0.0311], [0.3243, 0.0106, 0.0000], @@ -6768,6 +6769,20 @@ def LinearLHot(range, **traits): return ColorMapper.from_palette_array(palette, range=range, **traits) +def CoolWarm(range, **traits): + """ Generator of Kenneth Moreland's CoolWarm colormap. + + Blue-White-Red with smooth lightness transitions. Good for applying to 3D + surfaces or otherwise have extra shading applied. + + http://www.sandia.gov/~kmorel/documents/ColorMaps/ColorMapsExpanded.pdf + """ + cool = array([59, 76, 192]) / 255.0 + warm = array([180, 4, 38]) / 255.0 + palette = generate_diverging_palette(cool, warm, 33) + return ColorMapper.from_palette_array(palette, range=range, **traits) + + # Make the convenient list of all the function names as well as a dictionary # of name->function mappings. These are useful for UI editors. @@ -6830,6 +6845,7 @@ def LinearLHot(range, **traits): CubicL, LinearL, LinearLHot, + CoolWarm, ] color_map_dict = {} diff --git a/chaco/diverging_colormap.py b/chaco/diverging_colormap.py new file mode 100644 index 000000000..1def0c67c --- /dev/null +++ b/chaco/diverging_colormap.py @@ -0,0 +1,85 @@ +""" Generate perceptual diverging colormaps. + +Moreland, Kenneth. Diverging Color Maps for Scientific Visualization +(Expanded). +http://www.sandia.gov/~kmorel/documents/ColorMaps/ColorMapsExpanded.pdf +""" + +import numpy as np + +from .color_spaces import msh2xyz, srgb2xyz, xyz2msh, xyz2srgb + + +def adjust_hue(msh_sat, m_unsat): + """ Adjust the hue when interpolating to an unsaturated color. + + Parameters + ---------- + msh_sat : float array (3,) + Saturated Msh color at an endpoint of the colormap. + m_unsat : float + The magnitude of the target unsaturated color. + + Returns + ------- + h_adjusted : float + The adjusted target hue value. + """ + m_sat, s_sat, h_sat = msh_sat + if m_sat >= m_unsat: + return h_sat + else: + spin = s_sat * np.sqrt(m_unsat*m_unsat - m_sat*m_sat) / (m_sat * np.sin(s_sat)) + if h_sat > -np.pi / 3: + return h_sat + spin + else: + return h_sat - spin + + +def generate_diverging_palette(srgb1, srgb2, n_colors=256): + """ Generate a diverging color palette with two endpoint colors. + + Parameters + ---------- + srgb1, srgb2 : float array (3,) + RGB colors for the endpoints. An unsaturated white/grey color will be + in the middle. + n_colors : int, optional + The number of colors to generate in the palette. + + Returns + ------- + srgb_palette : float array (n_colors, 3) + RGB color palette. + """ + m1, s1, h1 = np.squeeze(xyz2msh(srgb2xyz([srgb1]))) + m2, s2, h2 = np.squeeze(xyz2msh(srgb2xyz([srgb2]))) + mmid = max(m1, m2, 88.0) + hmid1 = 0.0 + hmid2 = 0.0 + + x = np.linspace(0.0, 1.0, n_colors) + # srgb1 -> white + half1 = 2 * x[x <= 0.5] + # white -> srgb2 + half2 = 2 * x[x > 0.5] - 1.0 + if s1 > 0.05: + hmid1 = adjust_hue((m1, s1, h1), mmid) + if s2 > 0.05: + hmid2 = adjust_hue((m2, s2, h2), mmid) + + m_palette = np.hstack([ + half1 * mmid + (1 - half1) * m1, + half2 * m2 + (1 - half2) * mmid, + ]) + s_palette = np.hstack([ + (1 - half1) * s1, + half2 * s2, + ]) + h_palette = np.hstack([ + half1 * hmid1 + (1 - half1) * h1, + half2 * h2 + (1 - half2) * hmid2, + ]) + msh_palette = np.column_stack([m_palette, s_palette, h_palette]) + srgb_palette = xyz2srgb(msh2xyz(msh_palette)).clip(0.0, 1.0) + return srgb_palette From 2506da58ef6a8b15e6dd622782cffe9b41ce46e7 Mon Sep 17 00:00:00 2001 From: Robert Kern Date: Tue, 17 Jun 2014 13:05:24 +0100 Subject: [PATCH 4/8] ENH: Add CubeHelix colormap from Dave Green. --- ...ing_colormap.py => colormap_generators.py} | 57 +++++++++++++++++-- chaco/default_colormaps.py | 18 +++++- 2 files changed, 69 insertions(+), 6 deletions(-) rename chaco/{diverging_colormap.py => colormap_generators.py} (50%) diff --git a/chaco/diverging_colormap.py b/chaco/colormap_generators.py similarity index 50% rename from chaco/diverging_colormap.py rename to chaco/colormap_generators.py index 1def0c67c..e8f5a9362 100644 --- a/chaco/diverging_colormap.py +++ b/chaco/colormap_generators.py @@ -1,8 +1,20 @@ -""" Generate perceptual diverging colormaps. +""" Generate parameteric colormaps. -Moreland, Kenneth. Diverging Color Maps for Scientific Visualization -(Expanded). -http://www.sandia.gov/~kmorel/documents/ColorMaps/ColorMapsExpanded.pdf +Diverging colormaps can be generated via Kenneth Moreland's procedure using +``generate_diverging_palette()``. + + Moreland, Kenneth. Diverging Color Maps for Scientific Visualization + (Expanded). + http://www.sandia.gov/~kmorel/documents/ColorMaps/ColorMapsExpanded.pdf + +Dave Green's cubehelix family of colormaps can be generated using +``generate_cubehelix_palette()``. + + Green, D. A., 2011, A colour scheme for the display of astronomical + intensity images. Bulletin of the Astronomical Society of India, 39, 289. + (2011BASI...39..289G at ADS.) + http://adsabs.harvard.edu/abs/2011arXiv1108.5083G + https://www.mrao.cam.ac.uk/~dag/CUBEHELIX/ """ import numpy as np @@ -83,3 +95,40 @@ def generate_diverging_palette(srgb1, srgb2, n_colors=256): msh_palette = np.column_stack([m_palette, s_palette, h_palette]) srgb_palette = xyz2srgb(msh2xyz(msh_palette)).clip(0.0, 1.0) return srgb_palette + + +def generate_cubehelix_palette(start=0.5, rot=-1.5, saturation=1.2, + lightness_range=(0.0, 1.0), gamma=1.0, + n_colors=256): + """ Generate a sequential color palette from black to white spiraling + through intermediate colors. + + Parameters + ---------- + start : float between 0.0 and 3.0, optional + The starting hue. 0 is blue, 1 is red, 2 is green. + rot : float, optional + How many rotations to go in hue space. + saturation : float, optional + The saturation intensity factor. + lightness_range : (float, float), optional + The range of lightness values to interpolate between. + gamma : float, optional + The gamma exponent adjustment to apply to the lightness values. + n_colors : int, optional + The number of colors to generate in the palette. + + Returns + ------- + srgb_palette : float array (n_colors, 3) + RGB color palette. + """ + x = np.linspace(lightness_range[0], lightness_range[1], n_colors) + theta = 2.0 * np.pi * (start / 3.0 + rot * x + 1.) + x **= gamma + amplitude = saturation * x * (1 - x) / 2.0 + red = x + amplitude * (-0.14861*np.cos(theta) + 1.78277*np.sin(theta)) + green = x + amplitude * (-0.29227*np.cos(theta) - 0.90649*np.sin(theta)) + blue = x + amplitude * (1.97294*np.cos(theta)) + srgb_palette = np.column_stack([red, green, blue]).clip(0.0, 1.0) + return srgb_palette diff --git a/chaco/default_colormaps.py b/chaco/default_colormaps.py index 16dbda800..13a8c40db 100644 --- a/chaco/default_colormaps.py +++ b/chaco/default_colormaps.py @@ -25,7 +25,8 @@ # Local imports. from .color_mapper import ColorMapper -from .diverging_colormaps import generate_diverging_palette +from .colormap_generators import generate_cubehelix_palette, \ + generate_diverging_palette # The colormaps will be added to this at the end of the file. __all__ = ['reverse', 'center', 'color_map_functions', 'color_map_dict', @@ -6779,7 +6780,19 @@ def CoolWarm(range, **traits): """ cool = array([59, 76, 192]) / 255.0 warm = array([180, 4, 38]) / 255.0 - palette = generate_diverging_palette(cool, warm, 33) + palette = generate_diverging_palette(cool, warm, 256) + return ColorMapper.from_palette_array(palette, range=range, **traits) + + +def CubeHelix(range, **traits): + """ Generator of Dave Green's CubeHelix colormap. + + Sequential colormap with a linear lightness increasing from black to white + deviating away from gray in a tapered helix. + + https://www.mrao.cam.ac.uk/~dag/CUBEHELIX/ + """ + palette = generate_cubehelix_palette() return ColorMapper.from_palette_array(palette, range=range, **traits) @@ -6846,6 +6859,7 @@ def CoolWarm(range, **traits): LinearL, LinearLHot, CoolWarm, + CubeHelix, ] color_map_dict = {} From 81e89495179a6732df8ca9715cfaaee23539eab0 Mon Sep 17 00:00:00 2001 From: Robert Kern Date: Wed, 18 Jun 2014 09:53:40 +0200 Subject: [PATCH 5/8] ENH: More CHANGES --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index 321aa85ad..e73e6a636 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -10,7 +10,7 @@ Change summary since 4.4.1 New features - * Added perceptual colormaps by Matteo Niccoli. + * Added perceptual colormaps by Matteo Niccoli and Dave Green. * Added asynchronous_updates.py demo that shows a pattern for generating expensive plots while keeping the interface responsive (PR#170). From 192d810da51c32184be0535f5b37bd969c6b7175 Mon Sep 17 00:00:00 2001 From: Robert Kern Date: Wed, 18 Jun 2014 09:56:32 +0200 Subject: [PATCH 6/8] ENH: More credits. --- CHANGES.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.txt b/CHANGES.txt index e73e6a636..8bd8083af 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -10,7 +10,7 @@ Change summary since 4.4.1 New features - * Added perceptual colormaps by Matteo Niccoli and Dave Green. + * Added perceptual colormaps by Matteo Niccoli, Dave Green and Kenneth Moreland. * Added asynchronous_updates.py demo that shows a pattern for generating expensive plots while keeping the interface responsive (PR#170). From e22f7a71c5461f6f64c82eac402d18891fa0aa93 Mon Sep 17 00:00:00 2001 From: Robert Kern Date: Wed, 30 Jul 2014 15:19:56 +0100 Subject: [PATCH 7/8] ENH: Update whitepoint values and reference. --- chaco/color_spaces.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/chaco/color_spaces.py b/chaco/color_spaces.py index 566b37423..fe2ad5e07 100644 --- a/chaco/color_spaces.py +++ b/chaco/color_spaces.py @@ -188,17 +188,16 @@ def adapt_whitepoint(src, dst): # XYZ white-point coordinates -# from http://www.aim-dtp.net/aim/technology/cie_xyz/cie_xyz.htm +# from http://en.wikipedia.org/wiki/Standard_illuminant whitepoints = { - 'CIE A': ['Normal incandescent', triwhite(0.4476, 0.4074)], - 'CIE B': ['Direct sunlight', triwhite(0.3457, 0.3585)], - 'CIE C': ['Average sunlight', triwhite(0.3101, 0.3162)], + 'CIE A': ['Normal incandescent', triwhite(0.44757, 0.40745)], + 'CIE B': ['Direct sunlight', triwhite(0.34842, 0.35161)], + 'CIE C': ['Average sunlight', triwhite(0.31006, 0.31616)], 'CIE E': ['Normalized reference', triwhite(1.0/3, 1.0/3)], - 'D50': ['Bright tungsten', triwhite(0.3457, 0.3585)], - 'D55': ['Cloudy daylight', triwhite(0.3324, 0.3474)], - 'D65': ['Daylight', triwhite(0.312713, 0.329016)], - 'D75': ['?', triwhite(0.299, 0.3149)], - 'D93': ['low-quality old CRT', triwhite(0.2848, 0.2932)], + 'D50': ['Bright tungsten', triwhite(0.34567, 0.35850)], + 'D55': ['Cloudy daylight', triwhite(0.33242, 0.34743)], + 'D65': ['Daylight', triwhite(0.31271, 0.32902)], + 'D75': ['?', triwhite(0.29902, 0.31485)], } From 4f7d3f42c098646830a5616a1a62a1609489169f Mon Sep 17 00:00:00 2001 From: Robert Kern Date: Mon, 4 Aug 2014 12:10:21 +0100 Subject: [PATCH 8/8] ENH: Smoke test for colormaps. Remove unused color transformations. --- chaco/color_spaces.py | 217 --------------------- chaco/default_colormaps.py | 17 +- chaco/tests/default_colormaps_test_case.py | 49 +++++ 3 files changed, 62 insertions(+), 221 deletions(-) create mode 100644 chaco/tests/default_colormaps_test_case.py diff --git a/chaco/color_spaces.py b/chaco/color_spaces.py index fe2ad5e07..b5434f8d4 100644 --- a/chaco/color_spaces.py +++ b/chaco/color_spaces.py @@ -107,55 +107,6 @@ def triwhite(x, y): Z = (1-x-y)/y return [X, Y, Z] - -def adapt_whitepoint(src, dst): - """ Compute the adaptation matrix for converting XYZ tristimulus - values from a one standard illuminant to another using the Bradford - transform. - - This implementation follows the presentation on - http://www.color.org/chadtag.html - - The results are cached. - - Parameters - ---------- - src : str - dst : str - The names of the standard illuminants to convert from and to, - respectively. Valid values are the keys of `whitepoints` like - 'D65' or 'CIE A'. - - Returns - ------- - adapt : float array (3, 3) - Matrix-multiply this matrix against XYZ values with the `src` - whitepoint to get XYZ values with the `dst` whitepoint. - """ - # Check the cache first. - key = (src, dst) - if key not in adapt_whitepoint.cache: - bradford = np.array([[+0.8951, 0.2664, -0.1614], - [-0.7502, 1.7135, 0.0367], - [+0.0389, -0.0685, 1.0296]]) - - src_whitepoint = whitepoints[src][-1] - src_rgb = np.dot(bradford, src_whitepoint) - dst_whitepoint = whitepoints[dst][-1] - dst_rgb = np.dot(bradford, dst_whitepoint) - - scale = dst_rgb / src_rgb - adapt_whitepoint.cache[key] = solve( - bradford, - scale[:, np.newaxis] * bradford, - ) - - adapt = adapt_whitepoint.cache[key] - return adapt - -# Create the cache. -adapt_whitepoint.cache = {} - #### Data ##################################################################### # From the sRGB specification. @@ -164,28 +115,6 @@ def adapt_whitepoint(src, dst): [0.019334, 0.119193, 0.950227]]) rgb_from_xyz = inv(xyz_from_rgb) -# This transformation to LMS space keeps the peaks of colormatching curves -# normalized to 1. -lms_from_xyz = np.array([ - [+0.2434974736455316, 0.8523911562030849, -0.0515994646411065], - [-0.3958579552426224, 1.1655483851630273, 0.0837969419671409], - [+0.0, 0.0, 0.6185822095756526], -]) -xyz_from_lms = inv(lms_from_xyz) - -# The transformation from XYZ to the ATD opponent colorspace. These are -# "official" values directly from Guth 1980. -atd_from_xyz = np.array([[+0., 0.9341, 0.], - [+0.7401, -0.6801, -0.1567], - [-0.0061, -0.0212, 0.0314]]) -xyz_from_atd = inv(atd_from_xyz) - -# Now we need to compute the intermediate transformations between LMS and ATD. -# We derive these directly from the other two instead of specifying potentially -# truncated values. -atd_from_lms = solve(lms_from_xyz.T, atd_from_xyz.T).T -lms_from_atd = solve(atd_from_xyz.T, lms_from_xyz.T).T - # XYZ white-point coordinates # from http://en.wikipedia.org/wiki/Standard_illuminant @@ -272,152 +201,6 @@ def finv(y): return join_colors(xr*wp[0], yr*wp[1], zr*wp[2], axis) -def _uv(x, y, z): - """ The u, v formulae for CIE 1976 L*u*v* computations. - """ - denominator = (x + 15*y + 3*z) - zeros = (denominator == 0.0) - denominator = np.where(zeros, 1.0, denominator) - u_numerator = np.where(zeros, 4.0, 4 * x) - v_numerator = np.where(zeros, 9.0, 9 * y) - - return u_numerator/denominator, v_numerator/denominator - - -def xyz2luv(xyz, axis=-1, wp=whitepoints['D65'][-1]): - """ Convert XYZ tristimulus values to CIE L*u*v*. - - Parameters - ---------- - xyz : float array - XYZ values. - axis : int, optional - The axis of the XYZ values. - wp : list of 3 floats, optional - The XYZ tristimulus values of the whitepoint. - - Returns - ------- - luv : float array - The L*u*v* colors. - """ - x, y, z, axis = separate_colors(xyz, axis) - xn, yn, zn = x/wp[0], y/wp[1], z/wp[2] - Ls = 116.0 * np.power(yn, 1./3) - 16.0 - small_mask = (y <= 0.008856*wp[1]) - Ls[small_mask] = 903.0 * y[small_mask] / wp[1] - unp, vnp = _uv(*wp) - up, vp = _uv(x, y, z) - us = 13 * Ls * (up - unp) - vs = 13 * Ls * (vp - vnp) - - return join_colors(Ls, us, vs, axis) - - -def luv2xyz(luv, axis=-1, wp=whitepoints['D65'][-1]): - """ Convert CIE L*u*v* colors to XYZ tristimulus values. - - Parameters - ---------- - luv : float array - L*u*v* values. - axis : int, optional - The axis of the XYZ values. - wp : list of 3 floats, optional - The XYZ tristimulus values of the whitepoint. - - Returns - ------- - xyz : float array - The XYZ colors. - """ - Ls, us, vs, axis = separate_colors(luv, axis) - unp, vnp = _uv(*wp) - small_mask = (Ls <= 903.3 * 0.008856) - y = wp[1] * ((Ls + 16.0) / 116.0) ** 3 - y[small_mask] = Ls[small_mask] * wp[1] / 903.0 - # Where L==0, X=Y=Z=0. Avoid the nans and infs in the meantime. - black = (Ls == 0) - Ls[black] = 1.0 - - up = us / (13*Ls) + unp - vp = vs / (13*Ls) + vnp - x = 9.0 * y * up / (4.0 * vp) - z = -x / 3.0 - 5.0 * y + 3.0 * y/vp - - x[black] = 0.0 - y[black] = 0.0 - z[black] = 0.0 - - return join_colors(x, y, z, axis) - - -def xyz2hcl(xyz, axis=-1, wp=whitepoints['D65'][-1], luvlab='luv'): - """ Convert XYZ tristimulus values to HCL. - - HCL (Hue - Chroma - Luminance) is a cylindrical-coordinate form of - Luv or Lab. - - Parameters - ---------- - xyz : float array - XYZ values. - axis : int, optional - The axis of the XYZ values. - wp : list of 3 floats, optional - The XYZ tristimulus values of the whitepoint. - luvlab : 'luv' or 'lab', optional - Whether to use the L*u*v* or L*a*b*. - - Returns - ------- - hcl : float array - The HCL colors. - """ - if luvlab == 'luv': - xyz2lxx = xyz2luv - else: - xyz2lxx = xyz2lab - - Ls, x1, x2, axis = separate_colors(xyz2lxx(xyz, axis=axis, wp=wp), axis) - Cs = np.hypot(x1, x2) - Hs = (180. / np.pi) * np.arctan2(x2, x1) - - return join_colors(Hs, Cs, Ls, axis) - - -def hcl2xyz(hcl, axis=-1, wp=whitepoints['D65'][-1], luvlab='luv'): - """ Convert HCL values to XYZ tristimulus values. - - Parameters - ---------- - hcl : float array - Te HCL colors. - axis : int, optional - The axis of the XYZ values. - wp : list of 3 floats, optional - The XYZ tristimulus values of the whitepoint. - luvlab : 'luv' or 'lab', optional - Whether to use the L*u*v* or L*a*b*. - - Returns - ------- - xyz : float array - XYZ values. - """ - if luvlab == 'luv': - lxx2xyz = luv2xyz - else: - lxx2xyz = lab2xyz - - Hs, Cs, Ls, axis = separate_colors(hcl, axis) - theta = Hs * (np.pi / 180) - x1 = Cs * np.cos(theta) - x2 = Cs * np.sin(theta) - - return lxx2xyz(join_colors(Ls, x1, x2, axis), axis=axis, wp=wp) - - # RGB values that will be displayed on a screen are always nonlinear # R'G'B' values. To get the XYZ value of the color that will be # displayed you need a calibrated monitor with a profile. diff --git a/chaco/default_colormaps.py b/chaco/default_colormaps.py index 13a8c40db..70f680cab 100644 --- a/chaco/default_colormaps.py +++ b/chaco/default_colormaps.py @@ -1,5 +1,5 @@ #------------------------------------------------------------------------------ -# Copyright (c) 2005, Enthought, Inc. +# Copyright (c) 2005-2014, Enthought, Inc. # All rights reserved. # # This software is provided without warranty under the terms of the BSD @@ -56,7 +56,10 @@ def cmap(range, **traits): # Look a little like the wrapped function. cmap.__name__ = 'reversed_' + func.__name__ - cmap.__doc__ = 'Reversed: ' + func.__doc__ + if func.__doc__ is not None: + cmap.__doc__ = 'Reversed: ' + func.__doc__ + else: + cmap.__doc__ = 'Reversed: ' + func.__name__ return cmap def center(func, center=0.0): @@ -90,7 +93,10 @@ def cmap(range, **traits): # Look a little like the wrapped function. cmap.__name__ = 'centered_' + func.__name__ - cmap.__doc__ = 'Centered: ' + func.__doc__ + if func.__doc__ is not None: + cmap.__doc__ = 'Centered: ' + func.__doc__ + else: + cmap.__doc__ = 'Centered: ' + func.__name__ return cmap def fix(func, range): @@ -125,7 +131,10 @@ def cmap(dummy_range, **traits): # Look a little like the wrapped function. cmap.__name__ = 'fixed_' + func.__name__ - cmap.__doc__ = 'Fixed: ' + func.__doc__ + if func.__doc__ is not None: + cmap.__doc__ = 'Fixed: ' + func.__doc__ + else: + cmap.__doc__ = 'Fixed: ' + func.__name__ return cmap diff --git a/chaco/tests/default_colormaps_test_case.py b/chaco/tests/default_colormaps_test_case.py new file mode 100644 index 000000000..cbe7513f5 --- /dev/null +++ b/chaco/tests/default_colormaps_test_case.py @@ -0,0 +1,49 @@ +#------------------------------------------------------------------------------ +# Copyright (c) 2014, Enthought, Inc. +# All rights reserved. +# +# This software is provided without warranty under the terms of the BSD +# license included in LICENSE.txt and may be redistributed only +# under the conditions described in the aforementioned license. The license +# is also available online at http://www.enthought.com/licenses/BSD.txt +# Thanks for using Enthought open source! +# +#------------------------------------------------------------------------------ + +import unittest + +import numpy as np +from numpy.testing import assert_array_equal + +from chaco.api import DataRange1D +from .. import default_colormaps + + +class DefaultColormapsTestCase(unittest.TestCase): + + def test_default_colormaps_smoke(self): + # Runs some data through each of the default colormaps and do basic + # sanity checks. + x = np.linspace(-1.5, 2.0, 8) + datarange = DataRange1D(low_setting=-1.0, high_setting=1.5) + for cmap_func in default_colormaps.color_map_functions: + cmapper = cmap_func(datarange) + rgba = cmapper.map_screen(x) + self.assertEqual(rgba.shape, (8, 4)) + self.assertTrue(np.isfinite(rgba).all()) + self.assertTrue((rgba >= 0.0).all()) + self.assertTrue((rgba <= 1.0).all()) + # No transparency for any of the defaults. + assert_array_equal(rgba[:, -1], np.ones(8)) + assert_array_equal(rgba[0], rgba[1]) + assert_array_equal(rgba[-2], rgba[-1]) + r_cmapper = default_colormaps.reverse(cmap_func)(datarange) + r_rgba = r_cmapper.map_screen(x) + assert_array_equal(r_rgba, rgba[::-1]) + c_cmapper = default_colormaps.center(cmap_func)(datarange) + self.assertEqual(c_cmapper.range.low, -1.5) + self.assertEqual(c_cmapper.range.high, 1.5) + f_cmapper = default_colormaps.fix(cmap_func, + (0.0, 1.0))(datarange) + self.assertEqual(f_cmapper.range.low, 0.0) + self.assertEqual(f_cmapper.range.high, 1.0)