diff --git a/examples/colormap_normalization/colormap_normalizations_funcnorm.py b/examples/colormap_normalization/colormap_normalizations_funcnorm.py new file mode 100644 index 000000000000..288abafb8a73 --- /dev/null +++ b/examples/colormap_normalization/colormap_normalizations_funcnorm.py @@ -0,0 +1,87 @@ +""" +===================================================================== +Examples of normalization using :class:`~matplotlib.colors.FuncNorm` +===================================================================== + +This is an example on how to perform a normalization using an arbitrary +function with :class:`~matplotlib.colors.FuncNorm`. A logarithm normalization +and a square root normalization will be use as examples. + +""" + +import matplotlib.cm as cm +import matplotlib.colors as colors +import matplotlib.pyplot as plt + +import numpy as np + + +def main(): + fig, ((ax11, ax12), + (ax21, ax22), + (ax31, ax32)) = plt.subplots(3, 2, gridspec_kw={ + 'width_ratios': [1, 3.5]}, figsize=plt.figaspect(0.6)) + + cax = make_plot(None, 'Regular linear scale', fig, ax11, ax12) + fig.colorbar(cax, format='%.3g', ax=ax12, ticks=np.linspace(0, 1, 6)) + + # Example of logarithm normalization using FuncNorm + norm = colors.FuncNorm(f='log10', vmin=0.01) + cax = make_plot(norm, 'Log normalization', fig, ax21, ax22) + fig.colorbar(cax, format='%.3g', ticks=cax.norm.ticks(5), ax=ax22) + # The same can be achieved with + # norm = colors.FuncNorm(f=np.log10, + # finv=lambda x: 10.**(x), vmin=0.01) + + # Example of root normalization using FuncNorm + norm = colors.FuncNorm(f='sqrt', vmin=0.0) + cax = make_plot(norm, 'Root normalization', fig, ax31, ax32) + fig.colorbar(cax, format='%.3g', ticks=cax.norm.ticks(5), ax=ax32) + # The same can be achieved with + # norm = colors.FuncNorm(f='root{2}', vmin=0.) + # or with + # norm = colors.FuncNorm(f=lambda x: x**0.5, + # finv=lambda x: x**2, vmin=0.0) + + fig.subplots_adjust(hspace=0.4, wspace=0.15) + fig.suptitle('Normalization with FuncNorm') + plt.show() + + +def make_plot(norm, label, fig, ax1, ax2): + X, Y, data = get_data() + cax = ax2.imshow(data, cmap=cm.afmhot, norm=norm) + + d_values = np.linspace(cax.norm.vmin, cax.norm.vmax, 100) + cm_values = cax.norm(d_values) + ax1.plot(d_values, cm_values) + ax1.set_xlabel('Data values') + ax1.set_ylabel('Colormap values') + ax2.set_title(label) + ax2.axes.get_xaxis().set_ticks([]) + ax2.axes.get_yaxis().set_ticks([]) + return cax + + +def get_data(_cache=[]): + if len(_cache) > 0: + return _cache[0] + x = np.linspace(0, 1, 300) + y = np.linspace(-1, 1, 90) + X, Y = np.meshgrid(x, y) + + data = np.zeros(X.shape) + + def gauss2d(x, y, a0, x0, y0, wx, wy): + return a0 * np.exp(-(x - x0)**2 / wx**2 - (y - y0)**2 / wy**2) + N = 15 + for x in np.linspace(0., 1, N): + data += gauss2d(X, Y, x, x, 0, 0.25 / N, 0.25) + + data = data - data.min() + data = data / data.max() + _cache.append((X, Y, data)) + + return _cache[0] + +main() diff --git a/examples/colormap_normalization/colormap_normalizations_mirrorpiecewisenorm.py b/examples/colormap_normalization/colormap_normalizations_mirrorpiecewisenorm.py new file mode 100644 index 000000000000..646bb6d8f641 --- /dev/null +++ b/examples/colormap_normalization/colormap_normalizations_mirrorpiecewisenorm.py @@ -0,0 +1,95 @@ +""" +================================================================================ +Examples of normalization using :class:`~matplotlib.colors.MirrorPiecewiseNorm` +================================================================================ + +This is an example on how to perform a normalization for positive +and negative data around zero independently using +class:`~matplotlib.colors.MirrorPiecewiseNorm`. + +""" + +import matplotlib.cm as cm +import matplotlib.colors as colors +import matplotlib.pyplot as plt + +import numpy as np + + +def main(): + fig, ((ax11, ax12), + (ax21, ax22), + (ax31, ax32)) = plt.subplots(3, 2, gridspec_kw={ + 'width_ratios': [1, 3.5]}, figsize=plt.figaspect(0.6)) + + cax = make_plot(None, 'Regular linear scale', fig, ax11, ax12) + fig.colorbar(cax, format='%.3g', ax=ax12, ticks=np.linspace(-1, 1, 5)) + + # Example of symmetric root normalization using MirrorPiecewiseNorm + norm = colors.MirrorPiecewiseNorm(fpos='cbrt') + cax = make_plot(norm, 'Symmetric cubic root normalization around zero', + fig, ax21, ax22) + fig.colorbar(cax, format='%.3g', ticks=cax.norm.ticks(5), ax=ax22) + # The same can be achieved with + # norm = colors.MirrorPiecewiseNorm(fpos='root{3}') + # or with + # norm = colors.MirrorPiecewiseNorm(fpos=lambda x: x**(1 / 3.), + # fposinv=lambda x: x**3) + + # Example of asymmetric root normalization using MirrorPiecewiseNorm + norm = colors.MirrorPiecewiseNorm(fpos='cbrt', fneg='linear') + cax = make_plot(norm, 'Cubic root normalization above zero\n' + 'and linear below zero', + fig, ax31, ax32) + fig.colorbar(cax, format='%.3g', ticks=cax.norm.ticks(5), ax=ax32) + # The same can be achieved with + # norm = colors.MirrorPiecewiseNorm(fpos='root{3}', fneg='linear') + # or with + # norm = colors.MirrorPiecewiseNorm(fpos=lambda x: x**(1 / 3.), + # fposinv=lambda x: x**3, + # fneg=lambda x: x, + # fneginv=lambda x: x) + + fig.subplots_adjust(hspace=0.4, wspace=0.15) + fig.suptitle('Normalization with MirrorPiecewiseNorm') + plt.show() + + +def make_plot(norm, label, fig, ax1, ax2): + X, Y, data = get_data() + cax = ax2.imshow(data, cmap=cm.seismic, norm=norm) + + d_values = np.linspace(cax.norm.vmin, cax.norm.vmax, 100) + cm_values = cax.norm(d_values) + ax1.plot(d_values, cm_values) + ax1.set_xlabel('Data values') + ax1.set_ylabel('Colormap values') + ax2.set_title(label) + ax2.axes.get_xaxis().set_ticks([]) + ax2.axes.get_yaxis().set_ticks([]) + return cax + + +def get_data(_cache=[]): + if len(_cache) > 0: + return _cache[0] + x = np.linspace(0, 1, 300) + y = np.linspace(-1, 1, 90) + X, Y = np.meshgrid(x, y) + + data = np.zeros(X.shape) + + def gauss2d(x, y, a0, x0, y0, wx, wy): + return a0 * np.exp(-(x - x0)**2 / wx**2 - (y - y0)**2 / wy**2) + N = 15 + for x in np.linspace(0., 1, N): + data += gauss2d(X, Y, x, x, -0.5, 0.25 / N, 0.15) + data -= gauss2d(X, Y, x, x, 0.5, 0.25 / N, 0.15) + + data[data > 0] = data[data > 0] / data.max() + data[data < 0] = data[data < 0] / -data.min() + _cache.append((X, Y, data)) + + return _cache[0] + +main() diff --git a/examples/colormap_normalization/colormap_normalizations_mirrorrootnorm.py b/examples/colormap_normalization/colormap_normalizations_mirrorrootnorm.py new file mode 100644 index 000000000000..6a173bb89a4c --- /dev/null +++ b/examples/colormap_normalization/colormap_normalizations_mirrorrootnorm.py @@ -0,0 +1,83 @@ +""" +================================================================================ +Examples of normalization using :class:`~matplotlib.colors.MirrorRootNorm` +================================================================================ + +This is an example on how to perform a root normalization for positive +and negative data around zero independently using +class:`~matplotlib.colors.MirrorRootNorm`. + +""" + +import matplotlib.cm as cm +import matplotlib.colors as colors +import matplotlib.pyplot as plt + +import numpy as np + + +def main(): + fig, ((ax11, ax12), + (ax21, ax22), + (ax31, ax32)) = plt.subplots(3, 2, gridspec_kw={ + 'width_ratios': [1, 3.5]}, figsize=plt.figaspect(0.6)) + + cax = make_plot(None, 'Regular linear scale', fig, ax11, ax12) + fig.colorbar(cax, format='%.3g', ax=ax12, ticks=np.linspace(-1, 1, 5)) + + # Example of symmetric root normalization using MirrorRootNorm + norm = colors.MirrorRootNorm(orderpos=2) + cax = make_plot(norm, 'Symmetric cubic root normalization around zero', + fig, ax21, ax22) + fig.colorbar(cax, format='%.3g', ticks=cax.norm.ticks(5), ax=ax22) + + # Example of asymmetric root normalization using MirrorRootNorm + norm = colors.MirrorRootNorm(orderpos=2, orderneg=4) + cax = make_plot(norm, 'Square root normalization above zero\n' + 'and quartic root below zero', + fig, ax31, ax32) + fig.colorbar(cax, format='%.3g', ticks=cax.norm.ticks(5), ax=ax32) + + fig.subplots_adjust(hspace=0.4, wspace=0.15) + fig.suptitle('Normalization with MirrorRootNorm') + plt.show() + + +def make_plot(norm, label, fig, ax1, ax2): + X, Y, data = get_data() + cax = ax2.imshow(data, cmap=cm.seismic, norm=norm) + + d_values = np.linspace(cax.norm.vmin, cax.norm.vmax, 100) + cm_values = cax.norm(d_values) + ax1.plot(d_values, cm_values) + ax1.set_xlabel('Data values') + ax1.set_ylabel('Colormap values') + ax2.set_title(label) + ax2.axes.get_xaxis().set_ticks([]) + ax2.axes.get_yaxis().set_ticks([]) + return cax + + +def get_data(_cache=[]): + if len(_cache) > 0: + return _cache[0] + x = np.linspace(0, 1, 300) + y = np.linspace(-1, 1, 90) + X, Y = np.meshgrid(x, y) + + data = np.zeros(X.shape) + + def gauss2d(x, y, a0, x0, y0, wx, wy): + return a0 * np.exp(-(x - x0)**2 / wx**2 - (y - y0)**2 / wy**2) + N = 15 + for x in np.linspace(0., 1, N): + data += gauss2d(X, Y, x, x, -0.5, 0.25 / N, 0.15) + data -= gauss2d(X, Y, x, x, 0.5, 0.25 / N, 0.15) + + data[data > 0] = data[data > 0] / data.max() + data[data < 0] = data[data < 0] / -data.min() + _cache.append((X, Y, data)) + + return _cache[0] + +main() diff --git a/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py b/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py new file mode 100644 index 000000000000..1da0f262de18 --- /dev/null +++ b/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py @@ -0,0 +1,93 @@ +""" +========================================================================= +Examples of normalization using :class:`~matplotlib.colors.PiecewiseNorm` +========================================================================= + +This is an example on how to perform a normalization defined by intervals +using class:`~matplotlib.colors.PiecewiseNorm`. + +""" + +import matplotlib.cm as cm +import matplotlib.colors as colors +import matplotlib.pyplot as plt + +import numpy as np + + +def main(): + fig, ((ax11, ax12), + (ax21, ax22)) = plt.subplots(2, 2, gridspec_kw={ + 'width_ratios': [1, 3]}, figsize=plt.figaspect(0.6)) + + cax = make_plot(None, 'Regular linear scale', fig, ax11, ax12) + fig.colorbar(cax, format='%.3g', ax=ax12, ticks=np.linspace(0, 1, 6)) + + # Example of amplification of features above 0.2 and 0.6 + norm = colors.PiecewiseNorm(flist=['linear', 'root{4}', 'linear', + 'root{4}', 'linear'], + refpoints_cm=[0.2, 0.4, 0.6, 0.8], + refpoints_data=[0.2, 0.4, 0.6, 0.8]) + cax = make_plot(norm, 'Amplification of features above 0.2 and 0.6', + fig, ax21, ax22) + fig.colorbar(cax, format='%.3g', ticks=cax.norm.ticks(11), ax=ax22) + # The same can be achieved with + # norm = colors.PiecewiseNorm(flist=[lambda x: x, + # lambda x: x**(1. / 4), + # lambda x: x, + # lambda x: x**(1. / 4), + # lambda x: x], + # finvlist=[lambda x: x, + # lambda x: x**4, + # lambda x: x, + # lambda x: x**4, + # lambda x: x], + # refpoints_cm=[0.2, 0.4, 0.6, 0.8], + # refpoints_data=[0.2, 0.4, 0.6, 0.8]) + + fig.subplots_adjust(hspace=0.4, wspace=0.15) + fig.suptitle('Normalization with PiecewiseNorm') + plt.show() + + +def make_plot(norm, label, fig, ax1, ax2): + X, Y, data = get_data() + cax = ax2.imshow(data, cmap=cm.gist_heat, norm=norm) + + d_values = np.linspace(cax.norm.vmin, cax.norm.vmax, 300) + cm_values = cax.norm(d_values) + ax1.plot(d_values, cm_values) + ax1.set_xlabel('Data values') + ax1.set_ylabel('Colormap values') + ax2.set_title(label) + ax2.axes.get_xaxis().set_ticks([]) + ax2.axes.get_yaxis().set_ticks([]) + return cax + + +def get_data(_cache=[]): + if len(_cache) > 0: + return _cache[0] + x = np.linspace(0, 1, 301)[:-1] + y = np.linspace(-1, 1, 120) + X, Y = np.meshgrid(x, y) + + data = np.zeros(X.shape) + + def supergauss2d(o, x, y, a0, x0, y0, wx, wy): + x_ax = ((x - x0) / wx)**2 + y_ax = ((y - y0) / wy)**2 + return a0 * np.exp(-(x_ax + y_ax)**o) + N = 6 + + data += np.floor(X * (N)) / (N - 1) + + for x in np.linspace(0., 1, N + 1)[0:-1]: + data += supergauss2d(3, X, Y, 0.05, x + 0.5 / N, -0.5, 0.25 / N, 0.15) + data -= supergauss2d(3, X, Y, 0.05, x + 0.5 / N, 0.5, 0.25 / N, 0.15) + + data = np.clip(data, 0, 1) + _cache.append((X, Y, data)) + return _cache[0] + +main() diff --git a/examples/colormap_normalization/colormap_normalizations_rootnorm.py b/examples/colormap_normalization/colormap_normalizations_rootnorm.py new file mode 100644 index 000000000000..f4197eed0db3 --- /dev/null +++ b/examples/colormap_normalization/colormap_normalizations_rootnorm.py @@ -0,0 +1,79 @@ +""" +===================================================================== +Examples of normalization using :class:`~matplotlib.colors.RootNorm` +===================================================================== + +This is an example on how to perform a root normalization using +:class:`~matplotlib.colors.RootNorm`. Normalizations of order 2 +and order 3 ar compared. + +""" + +import matplotlib.cm as cm +import matplotlib.colors as colors +import matplotlib.pyplot as plt + +import numpy as np + + +def main(): + fig, ((ax11, ax12), + (ax21, ax22), + (ax31, ax32)) = plt.subplots(3, 2, gridspec_kw={ + 'width_ratios': [1, 3.5]}, figsize=plt.figaspect(0.6)) + + cax = make_plot(None, 'Regular linear scale', fig, ax11, ax12) + fig.colorbar(cax, format='%.3g', ax=ax12, ticks=np.linspace(0, 1, 6)) + + # Root normalization of order 2 + norm = colors.RootNorm(order=2, vmin=0.01) + cax = make_plot(norm, 'Root normalization or order 2', fig, ax21, ax22) + fig.colorbar(cax, format='%.3g', ticks=cax.norm.ticks(5), ax=ax22) + + # Root normalization of order 3 + norm = colors.RootNorm(order=3, vmin=0.0) + cax = make_plot(norm, 'Root normalization of order 3', fig, ax31, ax32) + fig.colorbar(cax, format='%.3g', ticks=cax.norm.ticks(5), ax=ax32) + + fig.subplots_adjust(hspace=0.4, wspace=0.15) + fig.suptitle('Normalization with RootNorm') + plt.show() + + +def make_plot(norm, label, fig, ax1, ax2): + X, Y, data = get_data() + cax = ax2.imshow(data, cmap=cm.afmhot, norm=norm) + + d_values = np.linspace(cax.norm.vmin, cax.norm.vmax, 100) + cm_values = cax.norm(d_values) + ax1.plot(d_values, cm_values) + ax1.set_xlabel('Data values') + ax1.set_ylabel('Colormap values') + ax2.set_title(label) + ax2.axes.get_xaxis().set_ticks([]) + ax2.axes.get_yaxis().set_ticks([]) + return cax + + +def get_data(_cache=[]): + if len(_cache) > 0: + return _cache[0] + x = np.linspace(0, 1, 300) + y = np.linspace(-1, 1, 90) + X, Y = np.meshgrid(x, y) + + data = np.zeros(X.shape) + + def gauss2d(x, y, a0, x0, y0, wx, wy): + return a0 * np.exp(-(x - x0)**2 / wx**2 - (y - y0)**2 / wy**2) + N = 15 + for x in np.linspace(0., 1, N): + data += gauss2d(X, Y, x, x, 0, 0.25 / N, 0.25) + + data = data - data.min() + data = data / data.max() + _cache.append((X, Y, data)) + + return _cache[0] + +main() diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 80e1b3146216..d50b5aff7fec 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2696,3 +2696,81 @@ def __exit__(self, exc_type, exc_value, traceback): os.rmdir(path) except OSError: pass + + +class _StringFuncParser(object): + # Each element has: + # -The direct function, + # -The inverse function, + # -A boolean indicating whether the function + # is bounded in the interval 0-1 + + funcs = {'linear': (lambda x: x, lambda x: x, True), + 'quadratic': (lambda x: x**2, lambda x: x**(1. / 2), True), + 'cubic': (lambda x: x**3, lambda x: x**(1. / 3), True), + 'sqrt': (lambda x: x**(1. / 2), lambda x: x**2, True), + 'cbrt': (lambda x: x**(1. / 3), lambda x: x**3, True), + 'log10': (lambda x: np.log10(x), lambda x: (10**(x)), False), + 'log': (lambda x: np.log(x), lambda x: (np.exp(x)), False), + 'power{a}': (lambda x, a: x**a, + lambda x, a: x**(1. / a), True), + 'root{a}': (lambda x, a: x**(1. / a), + lambda x, a: x**a, True), + 'log10(x+{a})': (lambda x, a: np.log10(x + a), + lambda x, a: 10**x - a, True), + 'log(x+{a})': (lambda x, a: np.log(x + a), + lambda x, a: np.exp(x) - a, True)} + + def __init__(self, str_func): + self.str_func = str_func + + def is_string(self): + return not hasattr(self.str_func, '__call__') + + def get_func(self): + return self._get_element(0) + + def get_invfunc(self): + return self._get_element(1) + + def is_bounded_0_1(self): + return self._get_element(2) + + def _get_element(self, ind): + if not self.is_string(): + raise ValueError("The argument passed is not a string.") + + str_func = six.text_type(self.str_func) + # Checking if it comes with a parameter + param = None + regex = '\{(.*?)\}' + search = re.search(regex, str_func) + if search is not None: + parstring = search.group(1) + + try: + param = float(parstring) + except: + raise ValueError("'a' in parametric function strings must be " + "replaced by a number that is not " + "zero, e.g. 'log10(x+{0.1})'.") + if param == 0: + raise ValueError("'a' in parametric function strings must be " + "replaced by a number that is not " + "zero.") + str_func = re.sub(regex, '{a}', str_func) + + try: + output = self.funcs[str_func][ind] + if param is not None: + output = (lambda x, output=output: output(x, param)) + + return output + except KeyError: + raise ValueError("%s: invalid function. The only strings " + "recognized as functions are %s." % + (str_func, self.funcs.keys())) + except: + raise ValueError("Invalid function. The only strings recognized " + "as functions are %s." % + (self.funcs.keys())) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 8a7e99faa988..451b18f33df6 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -958,6 +958,735 @@ def scaled(self): return (self.vmin is not None and self.vmax is not None) +class FuncNorm(Normalize): + """ + Creates a normalizer using a custom function + + The normalizer will be a function mapping the data values into colormap + values in the [0,1] range. + """ + + def __init__(self, f, finv=None, **normalize_kw): + """ + Specify the function to be used, and its inverse, as well as other + parameters to be passed to `Normalize`. The normalization will be + calculated as (f(x)-f(vmin))/(f(max)-f(vmin)). + + Parameters + ---------- + f : callable or string + Function to be used for the normalization receiving a single + parameter, compatible with scalar values and ndarrays. + Alternatively a string from the list ['linear', 'quadratic', + 'cubic', 'sqrt', 'cbrt','log', 'log10', 'power{a}', 'root{a}', + 'log(x+{a})', 'log10(x+{a})'] can be used, replacing 'a' by a + number different than 0 when necessary. + finv : callable, optional + Inverse function of `f` that satisfies finv(f(x))==x. It is + optional in cases where f is provided as a string. + normalize_kw : dict, optional + Dict with keywords (`vmin`,`vmax`,`clip`) passed + to `matplotlib.colors.Normalize`. + + Examples + -------- + Creating a logarithmic normalization using the predefined strings: + + >>> import matplotlib.colors as colors + >>> norm = colors.FuncNorm(f='log10', vmin=0.01, vmax=2) + + Or doing it manually: + + >>> import matplotlib.colors as colors + >>> norm = colors.FuncNorm(f=lambda x: np.log10(x), + ... finv=lambda x: 10.**(x), + ... vmin=0.01, vmax=2) + + """ + + func_parser = cbook._StringFuncParser(f) + if func_parser.is_string(): + f = func_parser.get_func() + finv = func_parser.get_invfunc() + + if finv is None: + raise ValueError("Inverse function `finv` not provided") + + self._f = f + self._finv = finv + + super(FuncNorm, self).__init__(**normalize_kw) + + def _update_f(self, vmin, vmax): + return + + def __call__(self, value, clip=None): + """ + Normalizes `value` data in the `[vmin, vmax]` interval into + the `[0.0, 1.0]` interval and returns it. + + Parameters + ---------- + value : float or ndarray of floats + Data to be normalized. + clip : boolean, optional + Whether to clip the data outside the `[vmin, vmax]` limits. + Default `self.clip` from `Normalize` (which defaults to `False`). + + Returns + ------- + result : masked array of floats + Normalized data to the `[0.0, 1.0]` interval. If clip == False, + values smaller than vmin or greater than vmax will be clipped to + -0.1 and 1.1 respectively. + + """ + if clip is None: + clip = self.clip + + result, is_scalar = self.process_value(value) + self.autoscale_None(result) + + vmin = float(self.vmin) + vmax = float(self.vmax) + + self._update_f(vmin, vmax) + + if clip: + result = np.clip(result, vmin, vmax) + resultnorm = (self._f(result) - self._f(vmin)) / \ + (self._f(vmax) - self._f(vmin)) + else: + resultnorm = result.copy() + mask_over = result > vmax + mask_under = result < vmin + mask = (result >= vmin) * (result <= vmax) + # Since the non linear function is arbitrary and may not be + # defined outside the boundaries, we just set obvious under + # and over values + resultnorm[mask_over] = 1.1 + resultnorm[mask_under] = -0.1 + resultnorm[mask] = (self._f(result[mask]) - self._f(vmin)) / \ + (self._f(vmax) - self._f(vmin)) + + return np.ma.array(resultnorm) + + def inverse(self, value): + """ + Performs the inverse normalization from the `[0.0, 1.0]` into the + `[vmin, vmax]` interval and returns it. + + Parameters + ---------- + value : float or ndarray of floats + Data in the `[0.0, 1.0]` interval. + + Returns + ------- + result : float or ndarray of floats + Data before normalization. + + """ + vmin = self.vmin + vmax = self.vmax + self._update_f(vmin, vmax) + value = self._finv( + value * (self._f(vmax) - self._f(vmin)) + self._f(vmin)) + return value + + @staticmethod + def _fun_normalizer(fun): + if fun(0.) == 0. and fun(1.) == 1.: + return fun + elif fun(0.) == 0.: + return (lambda x: fun(x) / fun(1.)) + else: + return (lambda x: (fun(x) - fun(0.)) / (fun(1.) - fun(0.))) + + def ticks(self, nticks=13): + """ + Returns an automatic list of `nticks` points in the data space + to be used as ticks in the colorbar. + + Parameters + ---------- + nticks : integer, optional + Number of ticks to be returned. Default 13. + + Returns + ------- + ticks : ndarray + 1d array of length `nticks` with the proposed tick locations. + + """ + ticks = self.inverse(np.linspace(0, 1, nticks)) + finalticks = np.zeros(ticks.shape, dtype=np.bool) + finalticks[0] = True + ticks = FuncNorm._round_ticks(ticks, finalticks) + return ticks + + def autoscale(self, A): + """ + Autoscales the normalization based on the maximum and minimum values + of `A`. + + Parameters + ---------- + A : ndarray or maskedarray + Array used to calculate the maximum and minimum values. + + """ + self.vmin = float(np.ma.min(A)) + self.vmax = float(np.ma.max(A)) + + def autoscale_None(self, A): + """ + Autoscales the normalization based on the maximum and minimum values + of `A`, only if the limits were not already set. + + Parameters + ---------- + A : ndarray or maskedarray + Array used to calculate the maximum and minimum values. + + """ + if self.vmin is None: + self.vmin = float(np.ma.min(A)) + if self.vmax is None: + self.vmax = float(np.ma.max(A)) + self.vmin = float(self.vmin) + self.vmax = float(self.vmax) + if self.vmin > self.vmax: + raise ValueError("vmin must be smaller than vmax") + + @staticmethod + def _round_ticks(ticks, permanenttick): + ticks = ticks.copy() + for i in range(len(ticks)): + if i == 0 or i == len(ticks) - 1 or permanenttick[i]: + continue + d1 = ticks[i] - ticks[i - 1] + d2 = ticks[i + 1] - ticks[i] + d = min([d1, d2]) + order = -np.floor(np.log10(d)) + ticks[i] = float(np.round(ticks[i] * 10**order)) / 10**order + return ticks + + +class PiecewiseNorm(FuncNorm): + """ + Normalization defined as a piecewise function + + It allows the definition of different linear or non-linear + functions for different ranges of the colorbar. + + """ + + def __init__(self, flist, + finvlist=None, + refpoints_data=[None], + refpoints_cm=[None], + **normalize_kw): + """ + Specify a series of functions, as well as intervals, to map the data + space into `[0,1]`. Each individual function may not diverge in the + [0,1] interval, as it will be normalized as + fnorm(x)=(f(x)-f(0))/(f(1)-f(0)) to guarantee that fnorm(0)=0 and + fnorm(1)=1. Then each function will be transformed to map each + different data range [d0, d1] into its respective colormap range + [cm0, cm1] as ftrans=fnorm((x-d0)/(d1-d0))*(cm1-cm0)+cm0. + + Parameters + ---------- + flist : list of callable or strings + List of functions to be used for each of the intervals. + Each of the elements must meet the same requirements as the + parameter `f` from `FuncNorm`. + finvlist : list of callable or strings, optional + List of the inverse functions corresponding to each function in + `flist`. Each of the elements must meet the same requirements as + the parameter `finv` from `FuncNorm`. None may be provided as + inverse for the functions that were specified as a string in + `flist`. It must satisfy `len(flist)==len(finvlist)`. + refpoints_cm, refpoints_data : list or array of scalars + Depending on the reference points, + the colorbar ranges will be: + `[0., refpoints_cm[0]]`,... , + `[refpoints_cm[i], refpoints_cm[i+1]]`, + `[refpoints_cm[-1], 0.]`, + and the data ranges will be: + `[self.vmin, refpoints_data[0]]`,... , + `[refpoints_data[i], refpoints_data[i+1]]`, + `[refpoints_cm[-1], self.vmax]` + It must satisfy + `len(flist)==len(refpoints_cm)+1==len(refpoints_data)+1`. + `refpoints_data` must consist of increasing values + in the (vmin, vmax) range. + `refpoints_cm` must consist of increasing values be in + the (0.0, 1.0) range. + The final normalization will meet: + `norm(refpoints_data[i])==refpoints_cm[i]`. + normalize_kw : dict, optional + Dict with keywords (`vmin`,`vmax`,`clip`) passed + to `matplotlib.colors.Normalize`. + + Examples + -------- + Obtaining a normalization to amplify features near both -0.4 and + 1.2 using four intervals: + + >>> import matplotlib.colors as colors + >>> norm = colors.PiecewiseNorm(flist=['cubic', 'cbrt', + ... 'cubic', 'cbrt'], + ... refpoints_cm=[0.25, 0.5, 0.75], + ... refpoints_data=[-0.4, 1, 1.2]) + + """ + + if finvlist is None: + finvlist = [None] * len(flist) + + if len(flist) != len(finvlist): + raise ValueError("The number of provided inverse functions" + " `len(finvlist)` must be equal to the number" + " of provided functions `len(flist)`") + + if len(refpoints_cm) != len(flist) - 1: + raise ValueError( + "The number of reference points for the colorbar " + "`len(refpoints_cm)` must be equal to the number of " + "provided functions `len(flist)` minus 1") + + if len(refpoints_data) != len(refpoints_cm): + raise ValueError( + "The number of reference points for the colorbar " + "`len(refpoints_cm)` must be equal to the number of " + "reference points for the data `len(refpoints_data)`") + + self._refpoints_cm = np.concatenate( + ([0.0], np.array(refpoints_cm), [1.0])) + if any(np.diff(self._refpoints_cm) <= 0): + raise ValueError( + "The values for the reference points for the colorbar " + "`refpoints_cm` must be monotonically increasing " + "and within the (0.0,1.0) interval") + + self._refpoints_data = np.concatenate( + ([None], np.array(refpoints_data), [None])) + + if (len(self._refpoints_data[1:-1]) > 2 and + any(np.diff(self._refpoints_data[1:-1]) <= 0)): + raise ValueError( + "The values for the reference points for the data " + "`refpoints_data` must be monotonically increasing") + + self._flist = [] + self._finvlist = [] + for i in range(len(flist)): + func_parser = cbook._StringFuncParser(flist[i]) + if func_parser.is_string(): + if not func_parser.is_bounded_0_1(): + raise ValueError("Only functions bounded in the " + "[0, 1] domain are allowed.") + + f = func_parser.get_func() + finv = func_parser.get_invfunc() + else: + f = flist[i] + finv = finvlist[i] + if f is None: + raise ValueError( + "Function not provided for %i range" % i) + + if finv is None: + raise ValueError( + "Inverse function not provided for %i range" % i) + + self._flist.append(FuncNorm._fun_normalizer(f)) + self._finvlist.append(FuncNorm._fun_normalizer(finv)) + + # We just say linear, becuase we cannot really make the function unless + # We now vmin, and vmax, and that does happen till the object is called + super(PiecewiseNorm, self).__init__('linear', None, **normalize_kw) + if self.vmin is not None and self.vmax is not None: + self._update_f(self.vmin, self.vmax) + + def _build_f(self): + vmin = self.vmin + vmax = self.vmax + self._refpoints_data[0] = vmin + self._refpoints_data[-1] = vmax + rp_d = self._refpoints_data + rp_cm = self._refpoints_cm + if (len(rp_d[1:-1]) > 0 and + (any(rp_d[1:-1] <= vmin) or any(rp_d[1:-1] >= vmax))): + raise ValueError( + "The values for the reference points for the data " + "`refpoints_data` must be contained within " + "the minimum and maximum values (vmin,vmax) interval") + + widths_cm = np.diff(rp_cm) + widths_d = np.diff(rp_d) + + masks = [] + funcs = [] + + for i in range(len(widths_cm)): + if i == 0: + mask = (lambda x, i=i: (x >= float( + rp_d[i])) * (x <= float(rp_d[i + 1]))) + else: + mask = (lambda x, i=i: ( + x > float(rp_d[i])) * (x <= float(rp_d[i + 1]))) + + func = (lambda x, i=i: self._flist[i]( + (x - rp_d[i]) / widths_d[i]) * widths_cm[i] + rp_cm[i]) + masks.append(mask) + funcs.append(func) + maskmaker = (lambda x: [np.array([mi(x)]) if np.isscalar( + x) else mi(x) for mi in masks]) + f = (lambda x: np.piecewise(x, maskmaker(x), funcs)) + return f + + def _build_finv(self): + vmin = self.vmin + vmax = self.vmax + self._refpoints_data[0] = vmin + self._refpoints_data[-1] = vmax + rp_d = self._refpoints_data + rp_cm = self._refpoints_cm + widths_cm = np.diff(rp_cm) + widths_d = np.diff(rp_d) + + masks = [] + funcs = [] + + for i in range(len(widths_cm)): + if i == 0: + mask = (lambda x, i=i: (x >= rp_cm[i]) * (x <= rp_cm[i + 1])) + else: + mask = (lambda x, i=i: (x > rp_cm[i]) * (x <= rp_cm[i + 1])) + masks.append(mask) + funcs.append(lambda x, i=i: self._finvlist[i]( + (x - rp_cm[i]) / widths_cm[i]) * widths_d[i] + rp_d[i]) + + maskmaker = (lambda x: [np.array([mi(x)]) if np.isscalar( + x) else mi(x) for mi in masks]) + finv = (lambda x: np.piecewise(x, maskmaker(x), funcs)) + return finv + + def _update_f(self, vmin, vmax): + self._f = self._build_f() + self._finv = self._build_finv() + return + + def ticks(self, nticks=None): + """ + Returns an automatic list of `nticks` points in the data space + to be used as ticks in the colorbar. + + Parameters + ---------- + nticks : integer, optional + Number of ticks to be returned. Default 13. + + Returns + ------- + ticks : ndarray + 1d array of length `nticks` with the proposed tick locations. + + """ + rp_cm = self._refpoints_cm + widths_cm = np.diff(rp_cm) + + if nticks is None: + nticks = max([13, len(rp_cm)]) + + if nticks < len(rp_cm): + ValueError( + "the number of ticks must me larger " + "that the number or intervals +1") + + ticks = rp_cm.copy() + + available_ticks = nticks - len(-rp_cm) + distribution = widths_cm * (available_ticks) / widths_cm.sum() + nticks_each = np.floor(distribution) + + while(nticks_each.sum() < available_ticks): + ind = np.argmax((distribution - nticks_each)) + nticks_each[ind] += 1 + + for i in range(len(nticks_each)): + if nticks_each[i] > 0: + nticks_this = nticks_each[i] + auxticks = np.linspace(rp_cm[i], rp_cm[i + 1], nticks_this + 2) + ticks = np.concatenate([ticks, auxticks[1:-1]]) + + finalticks = np.zeros(ticks.shape, dtype=np.bool) + finalticks[0:len(rp_cm)] = True + + inds = np.argsort(ticks) + ticks = ticks[inds] + finalticks = finalticks[inds] + + ticks = PiecewiseNorm._round_ticks(self.inverse(ticks), finalticks) + + return ticks + + +class MirrorPiecewiseNorm(PiecewiseNorm): + """ + Normalization allowing a dual :class:`~matplotlib.colors.PiecewiseNorm` + symmetrically around a point. + + Data above `center_data` will be normalized with the `fpos` function and + mapped into the inverval [`center_cm`,1] of the colorbar. + + Data below `center_data` will be process in a similar way after a mirror + transformation: + + * The interval [`vmin`, `center_data`] is mirrored around center_data, + so the upper side of the interval becomes the lower, and viceversa. + * The function `fneg` will be applied to the inverted interval to give an + interval in the colorbar. + * The obtained interval is mirrored again and mapped into the + [0, center_cm] interval of the colorbar. + + In practice this is effectively the same as applying a transformed + function: `lambda x:(-fneg(-x + 1) + 1))` + + If `fneg` is set to be equal to `fpos`, `center_cm` is set to 0.5 and + `center_data` is set to be the exact middle point between `vmin` and + `vmax`, then the normalization will be perfectly symmetric. + + """ + + def __init__(self, + fpos, fposinv=None, + fneg=None, fneginv=None, + center_data=0.0, center_cm=.5, + **normalize_kw): + """ + Parameters + ---------- + fpos, fposinv : callable or string + Functions to be used for normalization for values + above `center_data` using `PiecewiseNorm`. They must meet the + same requirements as the parameters `f` and `finv` from `FuncNorm`. + fneg, fneginv : callable or string, optional + Functions to be used for normalization for values + below `center_data` using `PiecewiseNorm`. They must meet the + same requirements as the parameters `f` and `finv` from `FuncNorm`. + The transformation will be applied mirrored around center_data, + i.e. the actual normalization passed to `PiecewiseNorm` will be + (-fneg(-x + 1) + 1)). Default `fneg`=`fpos`. + center_data : float, optional + Value at which the normalization will be mirrored. + Must be in the (vmin, vmax) range. Default 0.0. + center_cm : float, optional + Normalized value that will correspond do `center_data` in the + colorbar. Must be in the (0.0, 1.0) range. + Default 0.5. + normalize_kw : dict, optional + Dict with keywords (`vmin`,`vmax`,`clip`) passed + to `matplotlib.colors.Normalize`. + + Examples + -------- + Obtaining a symmetric amplification of the features around 0: + + >>> import matplotlib.colors as colors + >>> norm = colors.MirrorPiecewiseNorm(fpos='cbrt'): + + Obtaining an asymmetric amplification of the features around 0.6: + + >>> import matplotlib.colors as colors + >>> norm = colors.MirrorPiecewiseNorm(fpos='sqrt', fneg='cbrt', + ... center_cm=0.35, + ... center_data=0.6) + + """ + if fneg is None and fneginv is not None: + raise ValueError("Inverse function for the negative range " + "`fneginv`not expected without function for " + "the negative range `fneg`") + + if fneg is None: + fneg = fpos + fneginv = fposinv + + error_bounded = ("Only functions bounded in the " + "[0, 1] domain are allowed.") + + func_parser = cbook._StringFuncParser(fpos) + if func_parser.is_string(): + if not func_parser.is_bounded_0_1(): + raise ValueError(error_bounded) + fpos = func_parser.get_func() + fposinv = func_parser.get_invfunc() + + func_parser = cbook._StringFuncParser(fneg) + if func_parser.is_string(): + if not func_parser.is_bounded_0_1(): + raise ValueError(error_bounded) + fneg = func_parser.get_func() + fneginv = func_parser.get_invfunc() + + if fposinv is None: + raise ValueError( + "Inverse function must be provided for the positive interval") + if fneginv is None: + raise ValueError( + "Inverse function must be provided for the negative interval") + + if center_cm <= 0.0 or center_cm >= 1.0: + raise ValueError("The center point for the colorbar `center_cm` " + "must be within the (0.0,1.0) interval") + + refpoints_cm = np.array([center_cm]) + refpoints_data = np.array([center_data]) + + # It is important to normalize the functions before + # applying the -fneg(-x + 1) + 1) transformation + fneg = FuncNorm._fun_normalizer(fneg) + fpos = FuncNorm._fun_normalizer(fpos) + fposinv = FuncNorm._fun_normalizer(fposinv) + fneginv = FuncNorm._fun_normalizer(fneginv) + + flist = [(lambda x:(-fneg(-x + 1) + 1)), fpos] + finvlist = [(lambda x:(-fneginv(-x + 1) + 1)), fposinv] + + (super(MirrorPiecewiseNorm, self) + .__init__(flist=flist, + finvlist=finvlist, + refpoints_cm=refpoints_cm, + refpoints_data=refpoints_data, + **normalize_kw)) + + +class MirrorRootNorm(MirrorPiecewiseNorm): + """ + Root normalization using :class:`~matplotlib.colors.MirrorPiecewiseNorm`. + + :class:`~matplotlib.backend_bases.LocationEvent` + + Data above `center_data` will be normalized with a root of the order + `orderpos` and mapped into the inverval [`center_cm`,1] of the colorbar. + + Data below `center_data` will be normalized with a root of the order + `orderneg` and mapped into the inverval [0, `center_cm`] under a mirror + transformation (see :class:`~matplotlib.colors.MirrorPiecewiseNorm` for + more details). + + `colors.MirrorRootNorm(orderpos=2)` is equivalent + to `colors.MirrorPiecewiseNorm(fpos='sqrt')` + + `colors.MirrorRootNorm(orderpos=2, orderneg=3)` is equivalent + to `colors.MirrorPiecewiseNorm(fpos='sqrt', fneg='cbrt')` + + `colors.MirrorRootNorm(orderpos=N1, orderneg=N2)` is equivalent + to `colors.MirrorPiecewiseNorm(fpos=root{N1}', fneg=root{N2}')` + + """ + + def __init__(self, orderpos=2, orderneg=None, + center_cm=0.5, center_data=0.0, + **normalize_kw): + """ + Parameters + ---------- + orderpos : float or int, optional + Degree of the root to be used for normalization of values + above `center_data` using `MirrorPiecewiseNorm`. Default 2. + fneg, fneginv : callable or string, optional + Degree of the root to be used for normalization of values + below `center_data` using `MirrorPiecewiseNorm`. Default + `orderpos`. + center_data : float, optional + Value at which the normalization will be mirrored. + Must be in the (vmin, vmax) range. Default 0.0. + center_cm : float, optional + Normalized value that will correspond do `center_data` in the + colorbar. Must be in the (0.0, 1.0) range. + Default 0.5. + normalize_kw : dict, optional + Dict with keywords (`vmin`,`vmax`,`clip`) passed + to `matplotlib.colors.Normalize`. + + Examples + -------- + Obtaining a symmetric amplification of the features around 0: + + >>> import matplotlib.colors as colors + >>> norm = mcolors.MirrorRootNorm(orderpos=2) + + Obtaining an asymmetric amplification of the features around 0.6: + + >>> import matplotlib.colors as colors + >>> norm = mcolors.MirrorRootNorm(orderpos=3, + ... orderneg=4, + ... center_data=0.6, + ... center_cm=0.3) + + """ + + if orderneg is None: + orderneg = orderpos + (super(MirrorRootNorm, self) + .__init__(fneg=(lambda x: x**(1. / orderneg)), + fneginv=(lambda x: x**(orderneg)), + fpos=(lambda x: x ** (1. / orderpos)), + fposinv=(lambda x: x**(orderpos)), + center_cm=center_cm, + center_data=center_data, + **normalize_kw)) + + +class RootNorm(FuncNorm): + """ + Simple root normalization using :class:`~matplotlib.colors.FuncNorm`. + + It defines the root normalization as function of the order of the root. + Data will be normalized as (f(x)-f(`vmin`))/(f(`vmax`)-f(`vmin`)), where + f(x)=x**(1./`order`) + + `colors.RootNorm(order=2)` is equivalent to `colors.FuncNorm(f='sqrt')` + + `colors.RootNorm(order=3)` is equivalent to `colors.FuncNorm(f='cbrt')` + + `colors.RootNorm(order=N)` is equivalent to `colors.FuncNorm(f='root{N}')` + + """ + + def __init__(self, order=2, **normalize_kw): + """ + Parameters + ---------- + order : float or int, optional + Degree of the root to be used for normalization. Default 2. + normalize_kw : dict, optional + Dict with keywords (`vmin`,`vmax`,`clip`) passed + to `matplotlib.colors.Normalize`. + + Notes + ----- + Only valid for arrays with possitive values, or setting `vmin >= 0`. + + Examples + -------- + Obtaining a root normalization of order 3: + + >>> import matplotlib.colors as colors + >>> norm = mcolors.RootNorm(order=3, vmin=0) + + """ + (super(RootNorm, self) + .__init__(f=(lambda x: x**(1. / order)), + finv=(lambda x: x**(order)), + **normalize_kw)) + + class LogNorm(Normalize): """ Normalize a given value to the 0-1 range on a log scale diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index a8017a98f1dd..ffa44a3b4b1c 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -521,3 +521,36 @@ def test_flatiter(): assert 0 == next(it) assert 1 == next(it) + + +class TestFuncParser(object): + x_test = np.linspace(0.01, 0.5, 3) + validstrings = ['linear', 'quadratic', 'cubic', 'sqrt', 'cbrt', + 'log', 'log10', 'power{1.5}', 'root{2.5}', + 'log(x+{0.5})', 'log10(x+{0.1})'] + results = [(lambda x: x), + (lambda x: x**2), + (lambda x: x**3), + (lambda x: x**(1. / 2)), + (lambda x: x**(1. / 3)), + (lambda x: np.log(x)), + (lambda x: np.log10(x)), + (lambda x: x**1.5), + (lambda x: x**(1 / 2.5)), + (lambda x: np.log(x + 0.5)), + (lambda x: np.log10(x + 0.1))] + + @pytest.mark.parametrize("string", validstrings, ids=validstrings) + def test_inverse(self, string): + func_parser = cbook._StringFuncParser(string) + f = func_parser.get_func() + finv = func_parser.get_invfunc() + assert_array_almost_equal(finv(f(self.x_test)), self.x_test) + + @pytest.mark.parametrize("string, func", + zip(validstrings, results), + ids=validstrings) + def test_values(self, string, func): + func_parser = cbook._StringFuncParser(string) + f = func_parser.get_func() + assert_array_almost_equal(f(self.x_test), func(self.x_test)) diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index ebfc41aa53e3..784bb822044c 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -156,6 +156,196 @@ def test_LogNorm(): assert_array_equal(ln([1, 6]), [0, 1.0]) +class TestFuncNorm(object): + def test_limits_with_string(self): + norm = mcolors.FuncNorm(f='log10', vmin=0.01, vmax=2.) + assert_array_equal(norm([0.01, 2]), [0, 1.0]) + + def test_limits_with_lambda(self): + norm = mcolors.FuncNorm(f=lambda x: np.log10(x), + finv=lambda x: 10.**(x), + vmin=0.01, vmax=2.) + assert_array_equal(norm([0.01, 2]), [0, 1.0]) + + def test_limits_without_vmin_vmax(self): + norm = mcolors.FuncNorm(f='log10') + assert_array_equal(norm([0.01, 2]), [0, 1.0]) + + def test_limits_without_vmin(self): + norm = mcolors.FuncNorm(f='log10', vmax=2.) + assert_array_equal(norm([0.01, 2]), [0, 1.0]) + + def test_limits_without_vmax(self): + norm = mcolors.FuncNorm(f='log10', vmin=0.01) + assert_array_equal(norm([0.01, 2]), [0, 1.0]) + + def test_intermediate_values(self): + norm = mcolors.FuncNorm(f='log10') + assert_array_almost_equal(norm([0.01, 0.5, 2]), + [0, 0.73835195870437, 1.0]) + + def test_inverse(self): + norm = mcolors.FuncNorm(f='log10', vmin=0.01, vmax=2.) + x = np.linspace(0.01, 2, 10) + assert_array_almost_equal(x, norm.inverse(norm(x))) + + def test_ticks(self): + norm = mcolors.FuncNorm(f='log10', vmin=0.01, vmax=2.) + expected = [0.01, 0.016, 0.024, 0.04, 0.06, + 0.09, 0.14, 0.22, 0.3, 0.5, + 0.8, 1.3, 2.] + assert_array_almost_equal(norm.ticks(), expected) + + +class TestPiecewiseNorm(object): + def test_strings_and_funcs(self): + norm = mcolors.PiecewiseNorm(flist=['cubic', 'cbrt', + lambda x:x**3, 'cbrt'], + finvlist=[None, None, + lambda x:x**(1. / 3), None], + refpoints_cm=[0.2, 0.5, 0.7], + refpoints_data=[-1, 1, 3]) + assert_array_equal(norm([-2., -1, 1, 3, 4]), [0., 0.2, 0.5, 0.7, 1.0]) + + def test_only_strings(self): + norm = mcolors.PiecewiseNorm(flist=['cubic', 'cbrt', 'cubic', 'cbrt'], + refpoints_cm=[0.2, 0.5, 0.7], + refpoints_data=[-1, 1, 3]) + assert_array_equal(norm([-2., -1, 1, 3, 4]), [0., 0.2, 0.5, 0.7, 1.0]) + + def test_with_vminvmax(self): + norm = mcolors.PiecewiseNorm(flist=['cubic', 'cbrt', 'cubic', 'cbrt'], + refpoints_cm=[0.2, 0.5, 0.7], + refpoints_data=[-1, 1, 3], + vmin=-2., vmax=4.) + assert_array_equal(norm([-2., -1, 1, 3, 4]), [0., 0.2, 0.5, 0.7, 1.0]) + + def test_with_vmin(self): + norm = mcolors.PiecewiseNorm(flist=['cubic', 'cbrt', 'cubic', 'cbrt'], + refpoints_cm=[0.2, 0.5, 0.7], + refpoints_data=[-1, 1, 3], + vmin=-2.) + assert_array_equal(norm([-2., -1, 1, 3, 4]), [0., 0.2, 0.5, 0.7, 1.0]) + + def test_with_vmax(self): + norm = mcolors.PiecewiseNorm(flist=['cubic', 'cbrt', 'cubic', 'cbrt'], + refpoints_cm=[0.2, 0.5, 0.7], + refpoints_data=[-1, 1, 3], + vmax=4.) + assert_array_equal(norm([-2., -1, 1, 3, 4]), [0., 0.2, 0.5, 0.7, 1.0]) + + def test_intermediate_values(self): + norm = mcolors.PiecewiseNorm(flist=['cubic', 'cbrt', 'cubic', 'cbrt'], + refpoints_cm=[0.2, 0.5, 0.7], + refpoints_data=[-1, 1, 3], + vmin=-2., vmax=4.) + expected = [0.38898816, + 0.47256809, + 0.503125, + 0.584375, + 0.93811016] + assert_array_almost_equal(norm(np.linspace(-0.5, 3.5, 5)), expected) + + def test_inverse(self): + norm = mcolors.PiecewiseNorm(flist=['cubic', 'cbrt', 'cubic', 'cbrt'], + refpoints_cm=[0.2, 0.5, 0.7], + refpoints_data=[-1, 1, 3], + vmin=-2., vmax=4.) + x = np.linspace(-2, 4, 10) + assert_array_almost_equal(x, norm.inverse(norm(x))) + + def test_ticks(self): + norm = mcolors.PiecewiseNorm(flist=['cubic', 'cbrt', 'cubic', 'cbrt'], + refpoints_cm=[0.2, 0.5, 0.7], + refpoints_data=[-1, 1, 3], + vmin=-2., vmax=4.) + expected = [-2., -1.3, -1.1, -1., -0.93, + -0.4, 1., 2.4, 2.7, 3., + 3.04, 3.3, 4.] + assert_array_almost_equal(norm.ticks(), expected) + + +class TestMirrorPiecewiseNorm(object): + # Not necessary to test vmin,vmax, as they are just passed to the + # base class + def test_defaults_only_fpos(self): + norm = mcolors.MirrorPiecewiseNorm(fpos='cbrt') + assert_array_equal(norm([-2, 0., 1]), [0., 0.5, 1.0]) + + def test_fposfneg_refpoint_data_cm(self): + norm = mcolors.MirrorPiecewiseNorm(fpos='cbrt', fneg='sqrt', + center_cm=0.35, + center_data=0.6) + assert_array_equal(norm([-2, 0.6, 1]), [0., 0.35, 1.0]) + + def test_fposfneg_refpoint_data(self): + norm = mcolors.MirrorPiecewiseNorm(fpos='cbrt', fneg='sqrt', + center_data=0.6) + assert_array_equal(norm([-2, 0.6, 1]), [0., 0.5, 1.0]) + + def test_fpos_lambdafunc(self): + norm = mcolors.MirrorPiecewiseNorm(fpos=lambda x: x**2, + fposinv=lambda x: x**0.5) + assert_array_equal(norm([-2, 0., 1]), [0., 0.5, 1.0]) + + def test_fpos_lambdafunc_fneg_string(self): + norm = mcolors.MirrorPiecewiseNorm(fpos=lambda x: x**2, + fposinv=lambda x: x**0.5, + fneg='cbrt') + assert_array_equal(norm([-2, 0., 1]), [0., 0.5, 1.0]) + + def test_intermediate_values(self): + norm = mcolors.MirrorPiecewiseNorm(fpos=lambda x: x**2, + fposinv=lambda x: x**0.5, + fneg='cbrt') + expected = [0., + 0.1606978, + 0.52295918, + 0.68431122, + 1.] + assert_array_almost_equal(norm(np.linspace(-2, 3.5, 5)), expected) + + +class TestMirrorRootNorm(object): + # All parameters except the order are just passed to the base class + def test_orderpos_only(self): + norm = mcolors.MirrorRootNorm(orderpos=2) + assert_array_equal(norm([-2, 0., 1]), [0., 0.5, 1.0]) + + def test_symmetric_default(self): + norm1 = mcolors.MirrorRootNorm(orderpos=3, + orderneg=3) + norm2 = mcolors.MirrorRootNorm(orderpos=3) + x = np.linspace(-2, 1, 10) + assert_array_equal(norm1(x), norm2(x)) + + def test_intermediate_values(self): + norm = mcolors.MirrorRootNorm(orderpos=3, + orderneg=4) + expected = [0., + 0.0710536, + 0.23135752, + 0.79920424, + 0.89044833, + 0.95186372, + 1.] + assert_array_almost_equal(norm(np.linspace(-2, 3.5, 7)), expected) + + +class TestRootNorm(object): + # All parameters except the order are just passed to the base class + def test_intermediate_values(self): + norm = mcolors.RootNorm(order=3) + expected = [0., + 0.55032121, + 0.69336127, + 0.79370053, + 0.87358046, + 0.94103603, + 1.] + assert_array_almost_equal(norm(np.linspace(0, 10, 7)), expected) + + def test_PowerNorm(): a = np.array([0, 0.5, 1, 1.5], dtype=float) pnorm = mcolors.PowerNorm(1)