From 2d90c5aa40f3702d26fd908848f3711c065daa22 Mon Sep 17 00:00:00 2001 From: alvarosg Date: Mon, 17 Oct 2016 18:01:46 +0100 Subject: [PATCH 01/33] Added ArbitaryNorm and RootNorm classes in colors, as well as examples of use. --- .../colormap_normalizations_arbitrarynorm.py | 62 +++ .../colormap_normalizations_rootnorm.py | 54 +++ lib/matplotlib/colors.py | 424 ++++++++++++++++++ 3 files changed, 540 insertions(+) create mode 100644 doc/users/plotting/examples/colormap_normalizations_arbitrarynorm.py create mode 100644 doc/users/plotting/examples/colormap_normalizations_rootnorm.py diff --git a/doc/users/plotting/examples/colormap_normalizations_arbitrarynorm.py b/doc/users/plotting/examples/colormap_normalizations_arbitrarynorm.py new file mode 100644 index 000000000000..ee6fa2740334 --- /dev/null +++ b/doc/users/plotting/examples/colormap_normalizations_arbitrarynorm.py @@ -0,0 +1,62 @@ +""" +Demonstration of using the ArbitraryNorm classes for normalization. +""" + +import numpy as np +import matplotlib.colors as colors +import matplotlib.pyplot as plt +import matplotlib.cm as cm + +x=np.linspace(0,16*np.pi,1024) +y=np.linspace(-1,1,512) +X,Y=np.meshgrid(x,y) + +data=np.zeros(X.shape) + +data[Y>0]=np.cos(X[Y>0])*Y[Y>0]**2 + +for i,val in enumerate(np.arange(-1,1.1,0.2)): + if val<0:data[(X>(i*(50./11)))*(Y<0)]=val + if val>0:data[(X>(i*(50./11)))*(Y<0)]=val*2 + +figsize=(16,10) +cmap=cm.gist_rainbow + +plt.figure(figsize=figsize) +plt.pcolormesh(x,y,data,cmap=cmap) +plt.title('Linear Scale') +plt.colorbar(format='%.3g') +plt.xlim(0,16*np.pi) +plt.ylim(-1,1) + +plt.figure(figsize=figsize) +norm=colors.ArbitraryNorm(fpos=(lambda x: x**0.2), + fposinv=(lambda x: x**5), + fneg=(lambda x: x**0.5), + fneginv=(lambda x: x**2), + center=0.4) +plt.pcolormesh(x,y,data,cmap=cmap,norm=norm) +plt.title('Arbitrary norm') +plt.colorbar(ticks=norm.ticks(),format='%.3g') +plt.xlim(0,16*np.pi) +plt.ylim(-1,1) + +plt.figure(figsize=figsize) +norm=colors.PositiveArbitraryNorm(vmin=0, + fpos=(lambda x: x**0.5), + fposinv=(lambda x: x**2)) +plt.pcolormesh(x,y,data,cmap=cmap,norm=norm) +plt.title('Positive arbitrary norm') +plt.colorbar(ticks=norm.ticks(),format='%.3g') +plt.xlim(0,16*np.pi) +plt.ylim(-1,1) + +plt.figure(figsize=figsize) +norm=colors.NegativeArbitraryNorm(vmax=0, + fneg=(lambda x: x**0.5), + fneginv=(lambda x: x**2)) +plt.pcolormesh(x,y,data,cmap=cmap,norm=norm) +plt.title('Negative arbitrary norm') +plt.colorbar(ticks=norm.ticks(),format='%.3g') +plt.xlim(0,16*np.pi) +plt.ylim(-1,1) \ No newline at end of file diff --git a/doc/users/plotting/examples/colormap_normalizations_rootnorm.py b/doc/users/plotting/examples/colormap_normalizations_rootnorm.py new file mode 100644 index 000000000000..41fd221037e5 --- /dev/null +++ b/doc/users/plotting/examples/colormap_normalizations_rootnorm.py @@ -0,0 +1,54 @@ +""" +Demonstration of using the RootNorm classes for normalization. +""" + +import numpy as np +import matplotlib.colors as colors +import matplotlib.pyplot as plt +import matplotlib.cm as cm + +x=np.linspace(0,16*np.pi,1024) +y=np.linspace(-1,1,512) +X,Y=np.meshgrid(x,y) + +data=np.zeros(X.shape) + +data[Y>0]=np.cos(X[Y>0])*Y[Y>0]**2 + +for i,val in enumerate(np.arange(-1,1.1,0.2)): + if val<0:data[(X>(i*(50./11)))*(Y<0)]=val + if val>0:data[(X>(i*(50./11)))*(Y<0)]=val*2 + +figsize=(16,10) +cmap=cm.gist_rainbow + +plt.figure(figsize=figsize) +plt.pcolormesh(x,y,data,cmap=cmap) +plt.title('Linear Scale') +plt.colorbar(format='%.3g') +plt.xlim(0,16*np.pi) +plt.ylim(-1,1) + +plt.figure(figsize=figsize) +norm=colors.SymRootNorm(orderpos=7,orderneg=2,center=0.3) +plt.pcolormesh(x,y,data,cmap=cmap,norm=norm) +plt.title('Symmetric root norm') +plt.colorbar(ticks=norm.ticks(),format='%.3g') +plt.xlim(0,16*np.pi) +plt.ylim(-1,1) + +plt.figure(figsize=figsize) +norm=colors.PositiveRootNorm(vmin=0,orderpos=5) +plt.pcolormesh(x,y,data,cmap=cmap,norm=norm) +plt.title('Positive root norm') +plt.colorbar(ticks=norm.ticks(),format='%.3g') +plt.xlim(0,16*np.pi) +plt.ylim(-1,1) + +plt.figure(figsize=figsize) +norm=colors.NegativeRootNorm(vmax=0,orderneg=5) +plt.pcolormesh(x,y,data,cmap=cmap,norm=norm) +plt.title('Negative root norm') +plt.colorbar(ticks=norm.ticks(),format='%.3g') +plt.xlim(0,16*np.pi) +plt.ylim(-1,1) \ No newline at end of file diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 8a7e99faa988..313dddc48eaf 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -958,6 +958,430 @@ def scaled(self): return (self.vmin is not None and self.vmax is not None) +class ArbitraryNorm(Normalize): + """ + Normalization allowing the definition of any arbitrary non linear + function for the colorbar, for both, the positive, and the negative + directions. + >>> norm=ArbitraryNorm(fpos=(lambda x: x**0.5), + fposinv=(lambda x: x**2), + fneg=(lambda x: x**0.25), + fneginv=(lambda x: x**4)) + """ + + def __init__(self, fpos=(lambda x: x**.5), + fposinv=(lambda x: x**2), + fneg=None, fneginv=None, + center=.5, + vmin=None, vmax=None, clip=False): + """ + *fpos*: + Non-linear function used to normalize the positive range. Must be + able to operate take floating point numbers and numpy arrays. + *fposinv*: + Inverse function of *fpos*. + *fneg*: + Non-linear function used to normalize the negative range. It + not need to take in to account the negative sign. Must be + able to operate take floating point numbers and numpy arrays. + *fneginv*: + Inverse function of *fneg*. + *center*: Value between 0. and 1. indicating the color in the colorbar + that will be assigned to the zero value. + """ + + if fneg is None: + fneg = fpos + if fneginv is None: + fneginv = fposinv + if vmin is not None and vmax is not None: + if vmin > vmax: + raise ValueError("vmin must be less than vmax") + self.fneg = fneg + self.fpos = fpos + self.fneginv = fneginv + self.fposinv = fposinv + self.center = center + Normalize.__init__(self, vmin, vmax, clip) + + def __call__(self, value, clip=None): + if clip is None: + clip = self.clip + + result, is_scalar = self.process_value(value) + + vmin = self.vmin + vmax = self.vmax + + widthpos = 1 - self.center + widthneg = self.center + + result[result > vmax] = vmax + result[result < vmin] = vmin + + maskpositive = result > 0 + masknegative = result < 0 + if vmax > 0 and vmin < 0: + result[masknegative] = - \ + self.fneg(result[masknegative] / vmin) * widthneg + result[maskpositive] = self.fpos( + result[maskpositive] / vmax) * widthpos + + elif vmax > 0 and vmin >= 0: + result[maskpositive] = self.fpos( + (result[maskpositive] - vmin) / (vmax - vmin)) * widthpos + + elif vmax <= 0 and vmin < 0: + result[masknegative] = - \ + self.fneg((result[maskpositive] - vmax) / + (vmin - vmax)) * widthneg + + result = result + widthneg + + self.autoscale_None(result) + return result + + def inverse(self, value): + + vmin = self.vmin + vmax = self.vmax + widthpos = 1 - self.center + widthneg = self.center + + value = value - widthneg + + if cbook.iterable(value): + + maskpositive = value > 0 + masknegative = value < 0 + + if vmax > 0 and vmin < 0: + value[masknegative] = \ + self.fneginv(-value[masknegative] / widthneg) * vmin + value[maskpositive] = self.fposinv( + value[maskpositive] / widthpos) * vmax + + elif vmax > 0 and vmin >= 0: + value[maskpositive] = self.fposinv( + value[maskpositive] / widthpos) * (vmax - vmin) + vmin + value[masknegative] = -self.fposinv( + value[masknegative] / widthneg) * (vmax - vmin) + vmin + elif vmax <= 0 and vmin < 0: + value[masknegative] = self.fneginv( + -value[masknegative] / widthneg) * (vmin - vmax) + vmax + + else: + + if vmax > 0 and vmin < 0: + if value < 0: + value = self.fneginv(-value / widthneg) * vmin + else: + value = self.fposinv(value / widthpos) * vmax + + elif vmax > 0 and vmin >= 0: + if value > 0: + value = self.fposinv(value / widthpos) * \ + (vmax - vmin) + vmin + + elif vmax <= 0 and vmin < 0: + if value < 0: + value = self.fneginv(-value / widthneg) * \ + (vmin - vmax) + vmax + return value + + def ticks(self, N=13): + return self.inverse(np.linspace(0, 1, N)) + + def autoscale(self, A): + """ + vmin = self.vmin + vmax = self.vmax + + if vmax==0 or np.ma.max(A)==0: + self.vmin = np.ma.min(A) + self.vmax = -self.vmin + elif vmin==0 or np.ma.min(A)==0: + self.vmax = np.ma.max(A) + self.vmin = -self.vmax + else: + self.vmin = np.ma.min(A) + self.vmax = np.ma.max(A) + """ + self.vmin = np.ma.min(A) + self.vmax = np.ma.max(A) + + def autoscale_None(self, A): + if self.vmin is not None and self.vmax is not None: + return + if self.vmin is None: + self.vmin = np.ma.min(A) + if self.vmax is None: + self.vmax = np.ma.max(A) + + +class PositiveArbitraryNorm(Normalize): + """ + Normalization allowing the definition of any arbitrary non linear + function for the colorbar for positive data. + >>> norm=PositiveArbitraryNorm(fpos=(lambda x: x**0.5), + fposinv=(lambda x: x**2)) + """ + + def __init__(self, fpos=(lambda x: x**0.5), + fposinv=(lambda x: x**2), + vmin=None, vmax=None, clip=False): + """ + *fpos*: + Non-linear function used to normalize the positive range. Must be + able to operate take floating point numbers and numpy arrays. + *fposinv*: + Inverse function of *fpos*. + """ + + if vmin is not None and vmax is not None: + if vmin > vmax: + raise ValueError("vmin must be less than vmax") + self.fpos = fpos + self.fposinv = fposinv + Normalize.__init__(self, vmin, vmax, clip) + + def __call__(self, value, clip=None): + if clip is None: + clip = self.clip + + result, is_scalar = self.process_value(value) + + vmin = self.vmin + vmax = self.vmax + + result[result > vmax] = vmax + result[result < vmin] = vmin + + result = self.fpos((result - vmin) / (vmax - vmin)) + + self.autoscale_None(result) + return result + + def inverse(self, value): + + vmin = self.vmin + vmax = self.vmax + + if cbook.iterable(value): + value = self.fposinv(value) * (vmax - vmin) + vmin + else: + value = self.fposinv(value) * (vmax - vmin) + vmin + return value + + def ticks(self, N=11): + return self.inverse(np.linspace(0, 1, N)) + + def autoscale(self, A): + self.vmin = np.ma.min(A) + self.vmax = np.ma.max(A) + + def autoscale_None(self, A): + if self.vmin is not None and self.vmax is not None: + return + if self.vmin is None: + self.vmin = np.ma.min(A) + if self.vmax is None: + self.vmax = np.ma.max(A) + + +class NegativeArbitraryNorm(Normalize): + """ + Normalization allowing the definition of any arbitrary non linear + function for the colorbar for negative data. + >>> norm=NegativeArbitraryNorm(fneg=(lambda x: x**0.5), + fneginv=(lambda x: x**2)) + """ + + def __init__(self, fneg=(lambda x: x**0.5), fneginv=(lambda x: x**2), + vmin=None, vmax=None, clip=False): + """ + *fneg*: + Non-linear function used to normalize the negative range. It + not need to take in to account the negative sign. Must be + able to operate take floating point numbers and numpy arrays. + *fneginv*: + Inverse function of *fneg*. + """ + + if vmin is not None and vmax is not None: + if vmin > vmax: + raise ValueError("vmin must be less than vmax") + self.fneg = fneg + self.fneginv = fneginv + Normalize.__init__(self, vmin, vmax, clip) + + def __call__(self, value, clip=None): + if clip is None: + clip = self.clip + + result, is_scalar = self.process_value(value) + + vmin = self.vmin + vmax = self.vmax + + result[result > vmax] = vmax + result[result < vmin] = vmin + + result = -self.fneg((result - vmax) / (vmin - vmax)) + result = result + 1 + + self.autoscale_None(result) + return result + + def inverse(self, value): + + vmin = self.vmin + vmax = self.vmax + + value = value - 1 + + if cbook.iterable(value): + value = self.fneginv(-value) * (vmin - vmax) + vmax + else: + value = self.fneginv(value) * (vmin - vmax) + vmax + + return value + + def ticks(self, N=11): + return self.inverse(np.linspace(0, 1, N)) + + def autoscale(self, A): + self.vmin = np.ma.min(A) + self.vmax = np.ma.max(A) + + def autoscale_None(self, A): + if self.vmin is not None and self.vmax is not None: + return + if self.vmin is None: + self.vmin = np.ma.min(A) + if self.vmax is None: + self.vmax = np.ma.max(A) + + +class SymRootNorm(ArbitraryNorm): + """ + Root normalization for positive and negative data. + >>> norm=PositiveRootNorm(orderneg=3,orderpos=7) + """ + def __init__(self, orderpos=2, orderneg=None, + vmin=None, vmax=None, clip=False, center=0.5): + """ + *orderpos*: + Degree of the root used to normalize the data for the positive + direction. + *orderneg*: + Degree of the root used to normalize the data for the negative + direction. By default equal to *orderpos*. + """ + + if orderneg is None: + orderneg = orderpos + ArbitraryNorm.__init__(self, + fneg=(lambda x: x**(1. / orderneg)), + fneginv=(lambda x: x**(orderneg)), + fpos=(lambda x: x**(1. / orderpos)), + fposinv=(lambda x: x**(orderpos)), + center=center, + vmin=vmin, vmax=vmax, clip=clip) + + +class PositiveRootNorm(PositiveArbitraryNorm): + """ + Root normalization for positive data. + >>> norm=PositiveRootNorm(vmin=0,orderpos=7) + """ + def __init__(self, orderpos=2, vmin=None, vmax=None, clip=False): + """ + *orderpos*: + Degree of the root used to normalize the data for the positive + direction. + """ + PositiveArbitraryNorm.__init__(self, + fpos=(lambda x: x**(1. / orderpos)), + fposinv=(lambda x: x**(orderpos)), + vmin=vmin, vmax=vmax, clip=clip) + + +class NegativeRootNorm(NegativeArbitraryNorm): + """ + Root normalization for negative data. + >>> norm=NegativeRootNorm(vmax=0,orderneg=2) + """ + def __init__(self, orderneg=2, vmin=None, vmax=None, clip=False): + """ + *orderneg*: + Degree of the root used to normalize the data for the negative + direction. + """ + NegativeArbitraryNorm.__init__(self, + fneg=(lambda x: x**(1. / orderneg)), + fneginv=(lambda x: x**(orderneg)), + vmin=vmin, vmax=vmax, clip=clip) + + +class SymRootNorm(ArbitraryNorm): + """ + Root normalization for positive and negative data. + """ + def __init__(self, orderpos=2, orderneg=None, + vmin=None, vmax=None, clip=False, center=0.5): + """ + *orderpos*: + Degree of the root used to normalize the data for the positive + direction. + *orderneg*: + Degree of the root used to normalize the data for the negative + direction. By default equal to *orderpos*. + """ + + if orderneg is None: + orderneg = orderpos + ArbitraryNorm.__init__(self, + fneg=(lambda x: x**(1. / orderneg)), + fneginv=(lambda x: x**(orderneg)), + fpos=(lambda x: x**(1. / orderpos)), + fposinv=(lambda x: x**(orderpos)), + center=center, + vmin=vmin, vmax=vmax, clip=clip) + + +class PositiveRootNorm(PositiveArbitraryNorm): + """ + Root normalization for positive data. + """ + def __init__(self, orderpos=2, vmin=None, vmax=None, clip=False): + """ + *orderpos*: + Degree of the root used to normalize the data for the positive + direction. + """ + PositiveArbitraryNorm.__init__(self, + fpos=(lambda x: x**(1. / orderpos)), + fposinv=(lambda x: x**(orderpos)), + vmin=vmin, vmax=vmax, clip=clip) + + +class NegativeRootNorm(NegativeArbitraryNorm): + """ + Root normalization for negative data. + """ + def __init__(self, orderneg=2, vmin=None, vmax=None, clip=False): + """ + *orderneg*: + Degree of the root used to normalize the data for the negative + direction. + """ + NegativeArbitraryNorm.__init__(self, + fneg=(lambda x: x**(1. / orderneg)), + fneginv=(lambda x: x**(orderneg)), + vmin=vmin, vmax=vmax, clip=clip) + + class LogNorm(Normalize): """ Normalize a given value to the 0-1 range on a log scale From 57aad3dfdafc04b7708a8d5489840c52ca2bdd95 Mon Sep 17 00:00:00 2001 From: alvarosg Date: Tue, 18 Oct 2016 16:01:17 +0100 Subject: [PATCH 02/33] PEP8 formatting on examples, plotting using the object oriented approach, test matrix data improved --- .../colormap_normalizations_arbitrarynorm.py | 114 ++++++++++-------- .../colormap_normalizations_rootnorm.py | 97 ++++++++------- 2 files changed, 113 insertions(+), 98 deletions(-) diff --git a/doc/users/plotting/examples/colormap_normalizations_arbitrarynorm.py b/doc/users/plotting/examples/colormap_normalizations_arbitrarynorm.py index ee6fa2740334..664881ecd393 100644 --- a/doc/users/plotting/examples/colormap_normalizations_arbitrarynorm.py +++ b/doc/users/plotting/examples/colormap_normalizations_arbitrarynorm.py @@ -7,56 +7,64 @@ import matplotlib.pyplot as plt import matplotlib.cm as cm -x=np.linspace(0,16*np.pi,1024) -y=np.linspace(-1,1,512) -X,Y=np.meshgrid(x,y) - -data=np.zeros(X.shape) - -data[Y>0]=np.cos(X[Y>0])*Y[Y>0]**2 - -for i,val in enumerate(np.arange(-1,1.1,0.2)): - if val<0:data[(X>(i*(50./11)))*(Y<0)]=val - if val>0:data[(X>(i*(50./11)))*(Y<0)]=val*2 - -figsize=(16,10) -cmap=cm.gist_rainbow - -plt.figure(figsize=figsize) -plt.pcolormesh(x,y,data,cmap=cmap) -plt.title('Linear Scale') -plt.colorbar(format='%.3g') -plt.xlim(0,16*np.pi) -plt.ylim(-1,1) - -plt.figure(figsize=figsize) -norm=colors.ArbitraryNorm(fpos=(lambda x: x**0.2), - fposinv=(lambda x: x**5), - fneg=(lambda x: x**0.5), - fneginv=(lambda x: x**2), - center=0.4) -plt.pcolormesh(x,y,data,cmap=cmap,norm=norm) -plt.title('Arbitrary norm') -plt.colorbar(ticks=norm.ticks(),format='%.3g') -plt.xlim(0,16*np.pi) -plt.ylim(-1,1) - -plt.figure(figsize=figsize) -norm=colors.PositiveArbitraryNorm(vmin=0, - fpos=(lambda x: x**0.5), - fposinv=(lambda x: x**2)) -plt.pcolormesh(x,y,data,cmap=cmap,norm=norm) -plt.title('Positive arbitrary norm') -plt.colorbar(ticks=norm.ticks(),format='%.3g') -plt.xlim(0,16*np.pi) -plt.ylim(-1,1) - -plt.figure(figsize=figsize) -norm=colors.NegativeArbitraryNorm(vmax=0, - fneg=(lambda x: x**0.5), - fneginv=(lambda x: x**2)) -plt.pcolormesh(x,y,data,cmap=cmap,norm=norm) -plt.title('Negative arbitrary norm') -plt.colorbar(ticks=norm.ticks(),format='%.3g') -plt.xlim(0,16*np.pi) -plt.ylim(-1,1) \ No newline at end of file +xmax = 16 * np.pi +x = np.linspace(0, xmax, 1024) +y = np.linspace(-2, 1, 512) +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 = 61 +for i in range(N): + data = data + gauss2d(X, Y, 2. * i / N, i * + (xmax / N), -0.25, xmax / (3 * N), 0.07) + data = data - gauss2d(X, Y, 1. * i / N, i * + (xmax / N), -0.75, xmax / (3 * N), 0.07) + +data[Y > 0] = np.cos(X[Y > 0]) * Y[Y > 0]**2 + +N = 61 +for i, val in enumerate(np.linspace(-1, 1, N)): + if val < 0: + aux = val + if val > 0: + aux = val * 2 + data[(X > (i * (xmax / N))) * (Y < -1)] = aux + + +cmap = cm.gist_rainbow + +norms = [('Linear Scale', None), + ('Arbitrary norm', + colors.ArbitraryNorm(fpos=(lambda x: x**0.2), + fposinv=(lambda x: x**5), + fneg=(lambda x: x**0.5), + fneginv=(lambda x: x**2), + center=0.4)), + ('Positive arbitrary norm', + colors.PositiveArbitraryNorm(vmin=0, + fpos=(lambda x: x**0.5), + fposinv=(lambda x: x**2))), + ('Negative arbitrary norm', + colors.NegativeArbitraryNorm(vmax=0, + fneg=(lambda x: x**0.5), + fneginv=(lambda x: x**2)))] + + +for label, norm in norms: + fig, ax = plt.subplots() + cax = ax.pcolormesh(x, y, data, cmap=cmap, norm=norm) + ax.set_title(label) + ax.set_xlim(0, xmax) + ax.set_ylim(-2, 1) + if norm: + ticks = norm.ticks() + else: + ticks = None + cbar = fig.colorbar(cax, format='%.3g', ticks=ticks) + +plt.show() diff --git a/doc/users/plotting/examples/colormap_normalizations_rootnorm.py b/doc/users/plotting/examples/colormap_normalizations_rootnorm.py index 41fd221037e5..8eb644d67619 100644 --- a/doc/users/plotting/examples/colormap_normalizations_rootnorm.py +++ b/doc/users/plotting/examples/colormap_normalizations_rootnorm.py @@ -7,48 +7,55 @@ import matplotlib.pyplot as plt import matplotlib.cm as cm -x=np.linspace(0,16*np.pi,1024) -y=np.linspace(-1,1,512) -X,Y=np.meshgrid(x,y) - -data=np.zeros(X.shape) - -data[Y>0]=np.cos(X[Y>0])*Y[Y>0]**2 - -for i,val in enumerate(np.arange(-1,1.1,0.2)): - if val<0:data[(X>(i*(50./11)))*(Y<0)]=val - if val>0:data[(X>(i*(50./11)))*(Y<0)]=val*2 - -figsize=(16,10) -cmap=cm.gist_rainbow - -plt.figure(figsize=figsize) -plt.pcolormesh(x,y,data,cmap=cmap) -plt.title('Linear Scale') -plt.colorbar(format='%.3g') -plt.xlim(0,16*np.pi) -plt.ylim(-1,1) - -plt.figure(figsize=figsize) -norm=colors.SymRootNorm(orderpos=7,orderneg=2,center=0.3) -plt.pcolormesh(x,y,data,cmap=cmap,norm=norm) -plt.title('Symmetric root norm') -plt.colorbar(ticks=norm.ticks(),format='%.3g') -plt.xlim(0,16*np.pi) -plt.ylim(-1,1) - -plt.figure(figsize=figsize) -norm=colors.PositiveRootNorm(vmin=0,orderpos=5) -plt.pcolormesh(x,y,data,cmap=cmap,norm=norm) -plt.title('Positive root norm') -plt.colorbar(ticks=norm.ticks(),format='%.3g') -plt.xlim(0,16*np.pi) -plt.ylim(-1,1) - -plt.figure(figsize=figsize) -norm=colors.NegativeRootNorm(vmax=0,orderneg=5) -plt.pcolormesh(x,y,data,cmap=cmap,norm=norm) -plt.title('Negative root norm') -plt.colorbar(ticks=norm.ticks(),format='%.3g') -plt.xlim(0,16*np.pi) -plt.ylim(-1,1) \ No newline at end of file +xmax = 16 * np.pi +x = np.linspace(0, xmax, 1024) +y = np.linspace(-2, 1, 512) +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 = 61 +for i in range(N): + data = data + gauss2d(X, Y, 2. * i / N, i * + (xmax / N), -0.25, xmax / (3 * N), 0.07) + data = data - gauss2d(X, Y, 1. * i / N, i * + (xmax / N), -0.75, xmax / (3 * N), 0.07) + +data[Y > 0] = np.cos(X[Y > 0]) * Y[Y > 0]**2 + +N = 61 +for i, val in enumerate(np.linspace(-1, 1, N)): + if val < 0: + aux = val + if val > 0: + aux = val * 2 + data[(X > (i * (xmax / N))) * (Y < -1)] = aux + +cmap = cm.gist_rainbow + +norms = [('Linear Scale', None), + ('Symmetric root norm', + colors.SymRootNorm(orderpos=7, orderneg=2, center=0.3)), + ('Positive root norm', + colors.PositiveRootNorm(vmin=0, orderpos=5)), + ('Negative root norm', + colors.NegativeRootNorm(vmax=0, orderneg=5))] + + +for label, norm in norms: + fig, ax = plt.subplots() + cax = ax.pcolormesh(x, y, data, cmap=cmap, norm=norm) + ax.set_title(label) + ax.set_xlim(0, xmax) + ax.set_ylim(-2, 1) + if norm: + ticks = norm.ticks() + else: + ticks = None + cbar = fig.colorbar(cax, format='%.3g', ticks=ticks) + +plt.show() From f818affbd636e75ac39b16ff8eb5a9b7bdbe5eef Mon Sep 17 00:00:00 2001 From: alvarosg Date: Tue, 18 Oct 2016 16:11:06 +0100 Subject: [PATCH 03/33] Added title/description to the examples --- .../examples/colormap_normalizations_arbitrarynorm.py | 9 ++++++++- .../examples/colormap_normalizations_rootnorm.py | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/doc/users/plotting/examples/colormap_normalizations_arbitrarynorm.py b/doc/users/plotting/examples/colormap_normalizations_arbitrarynorm.py index 664881ecd393..9c6918339371 100644 --- a/doc/users/plotting/examples/colormap_normalizations_arbitrarynorm.py +++ b/doc/users/plotting/examples/colormap_normalizations_arbitrarynorm.py @@ -1,5 +1,12 @@ """ -Demonstration of using the ArbitraryNorm classes for normalization. +============================================ +Examples of arbitrary colormap normalization +============================================ + +Here I plot an image array with data spanning for a large dynamic range, +using different normalizations. Look at how each of them enhances +different features. + """ import numpy as np diff --git a/doc/users/plotting/examples/colormap_normalizations_rootnorm.py b/doc/users/plotting/examples/colormap_normalizations_rootnorm.py index 8eb644d67619..a5c35b438686 100644 --- a/doc/users/plotting/examples/colormap_normalizations_rootnorm.py +++ b/doc/users/plotting/examples/colormap_normalizations_rootnorm.py @@ -1,5 +1,12 @@ """ -Demonstration of using the RootNorm classes for normalization. +============================================ +Examples of colormap root normalization +============================================ + +Here I plot an image array with data spanning for a large dynamic range, +using different normalizations. Look at how each of them enhances +different features. + """ import numpy as np From ffe1b9dafa4e85e16e5dbed5e318e5ade2a278ba Mon Sep 17 00:00:00 2001 From: alvarosg Date: Tue, 18 Oct 2016 17:05:31 +0100 Subject: [PATCH 04/33] Class attributes are now hidden --- lib/matplotlib/colors.py | 122 ++++++++++----------------------------- 1 file changed, 32 insertions(+), 90 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 313dddc48eaf..ea11d20b42db 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -997,11 +997,11 @@ def __init__(self, fpos=(lambda x: x**.5), if vmin is not None and vmax is not None: if vmin > vmax: raise ValueError("vmin must be less than vmax") - self.fneg = fneg - self.fpos = fpos - self.fneginv = fneginv - self.fposinv = fposinv - self.center = center + self._fneg = fneg + self._fpos = fpos + self._fneginv = fneginv + self._fposinv = fposinv + self._center = center Normalize.__init__(self, vmin, vmax, clip) def __call__(self, value, clip=None): @@ -1013,8 +1013,8 @@ def __call__(self, value, clip=None): vmin = self.vmin vmax = self.vmax - widthpos = 1 - self.center - widthneg = self.center + widthpos = 1 - self._center + widthneg = self._center result[result > vmax] = vmax result[result < vmin] = vmin @@ -1023,17 +1023,17 @@ def __call__(self, value, clip=None): masknegative = result < 0 if vmax > 0 and vmin < 0: result[masknegative] = - \ - self.fneg(result[masknegative] / vmin) * widthneg - result[maskpositive] = self.fpos( + self._fneg(result[masknegative] / vmin) * widthneg + result[maskpositive] = self._fpos( result[maskpositive] / vmax) * widthpos elif vmax > 0 and vmin >= 0: - result[maskpositive] = self.fpos( + result[maskpositive] = self._fpos( (result[maskpositive] - vmin) / (vmax - vmin)) * widthpos elif vmax <= 0 and vmin < 0: result[masknegative] = - \ - self.fneg((result[maskpositive] - vmax) / + self._fneg((result[maskpositive] - vmax) / (vmin - vmax)) * widthneg result = result + widthneg @@ -1045,8 +1045,8 @@ def inverse(self, value): vmin = self.vmin vmax = self.vmax - widthpos = 1 - self.center - widthneg = self.center + widthpos = 1 - self._center + widthneg = self._center value = value - widthneg @@ -1057,35 +1057,35 @@ def inverse(self, value): if vmax > 0 and vmin < 0: value[masknegative] = \ - self.fneginv(-value[masknegative] / widthneg) * vmin - value[maskpositive] = self.fposinv( + self._fneginv(-value[masknegative] / widthneg) * vmin + value[maskpositive] = self._fposinv( value[maskpositive] / widthpos) * vmax elif vmax > 0 and vmin >= 0: - value[maskpositive] = self.fposinv( + value[maskpositive] = self._fposinv( value[maskpositive] / widthpos) * (vmax - vmin) + vmin - value[masknegative] = -self.fposinv( + value[masknegative] = -self._fposinv( value[masknegative] / widthneg) * (vmax - vmin) + vmin elif vmax <= 0 and vmin < 0: - value[masknegative] = self.fneginv( + value[masknegative] = self._fneginv( -value[masknegative] / widthneg) * (vmin - vmax) + vmax else: if vmax > 0 and vmin < 0: if value < 0: - value = self.fneginv(-value / widthneg) * vmin + value = self._fneginv(-value / widthneg) * vmin else: - value = self.fposinv(value / widthpos) * vmax + value = self._fposinv(value / widthpos) * vmax elif vmax > 0 and vmin >= 0: if value > 0: - value = self.fposinv(value / widthpos) * \ + value = self._fposinv(value / widthpos) * \ (vmax - vmin) + vmin elif vmax <= 0 and vmin < 0: if value < 0: - value = self.fneginv(-value / widthneg) * \ + value = self._fneginv(-value / widthneg) * \ (vmin - vmax) + vmax return value @@ -1141,8 +1141,8 @@ def __init__(self, fpos=(lambda x: x**0.5), if vmin is not None and vmax is not None: if vmin > vmax: raise ValueError("vmin must be less than vmax") - self.fpos = fpos - self.fposinv = fposinv + self._fpos = fpos + self._fposinv = fposinv Normalize.__init__(self, vmin, vmax, clip) def __call__(self, value, clip=None): @@ -1157,7 +1157,7 @@ def __call__(self, value, clip=None): result[result > vmax] = vmax result[result < vmin] = vmin - result = self.fpos((result - vmin) / (vmax - vmin)) + result = self._fpos((result - vmin) / (vmax - vmin)) self.autoscale_None(result) return result @@ -1168,9 +1168,9 @@ def inverse(self, value): vmax = self.vmax if cbook.iterable(value): - value = self.fposinv(value) * (vmax - vmin) + vmin + value = self._fposinv(value) * (vmax - vmin) + vmin else: - value = self.fposinv(value) * (vmax - vmin) + vmin + value = self._fposinv(value) * (vmax - vmin) + vmin return value def ticks(self, N=11): @@ -1211,8 +1211,8 @@ def __init__(self, fneg=(lambda x: x**0.5), fneginv=(lambda x: x**2), if vmin is not None and vmax is not None: if vmin > vmax: raise ValueError("vmin must be less than vmax") - self.fneg = fneg - self.fneginv = fneginv + self._fneg = fneg + self._fneginv = fneginv Normalize.__init__(self, vmin, vmax, clip) def __call__(self, value, clip=None): @@ -1227,7 +1227,7 @@ def __call__(self, value, clip=None): result[result > vmax] = vmax result[result < vmin] = vmin - result = -self.fneg((result - vmax) / (vmin - vmax)) + result = -self._fneg((result - vmax) / (vmin - vmax)) result = result + 1 self.autoscale_None(result) @@ -1241,9 +1241,9 @@ def inverse(self, value): value = value - 1 if cbook.iterable(value): - value = self.fneginv(-value) * (vmin - vmax) + vmax + value = self._fneginv(-value) * (vmin - vmax) + vmax else: - value = self.fneginv(value) * (vmin - vmax) + vmax + value = self._fneginv(value) * (vmin - vmax) + vmax return value @@ -1324,64 +1324,6 @@ def __init__(self, orderneg=2, vmin=None, vmax=None, clip=False): vmin=vmin, vmax=vmax, clip=clip) -class SymRootNorm(ArbitraryNorm): - """ - Root normalization for positive and negative data. - """ - def __init__(self, orderpos=2, orderneg=None, - vmin=None, vmax=None, clip=False, center=0.5): - """ - *orderpos*: - Degree of the root used to normalize the data for the positive - direction. - *orderneg*: - Degree of the root used to normalize the data for the negative - direction. By default equal to *orderpos*. - """ - - if orderneg is None: - orderneg = orderpos - ArbitraryNorm.__init__(self, - fneg=(lambda x: x**(1. / orderneg)), - fneginv=(lambda x: x**(orderneg)), - fpos=(lambda x: x**(1. / orderpos)), - fposinv=(lambda x: x**(orderpos)), - center=center, - vmin=vmin, vmax=vmax, clip=clip) - - -class PositiveRootNorm(PositiveArbitraryNorm): - """ - Root normalization for positive data. - """ - def __init__(self, orderpos=2, vmin=None, vmax=None, clip=False): - """ - *orderpos*: - Degree of the root used to normalize the data for the positive - direction. - """ - PositiveArbitraryNorm.__init__(self, - fpos=(lambda x: x**(1. / orderpos)), - fposinv=(lambda x: x**(orderpos)), - vmin=vmin, vmax=vmax, clip=clip) - - -class NegativeRootNorm(NegativeArbitraryNorm): - """ - Root normalization for negative data. - """ - def __init__(self, orderneg=2, vmin=None, vmax=None, clip=False): - """ - *orderneg*: - Degree of the root used to normalize the data for the negative - direction. - """ - NegativeArbitraryNorm.__init__(self, - fneg=(lambda x: x**(1. / orderneg)), - fneginv=(lambda x: x**(orderneg)), - vmin=vmin, vmax=vmax, clip=clip) - - class LogNorm(Normalize): """ Normalize a given value to the 0-1 range on a log scale From 1d22b90be9ad99bafd1715f0fd83968e78bd93a1 Mon Sep 17 00:00:00 2001 From: alvarosg Date: Wed, 19 Oct 2016 15:50:23 +0100 Subject: [PATCH 05/33] Major update: complete refactorization of code. A much more powerful version of ArbitraryNorm introduced. All the other classes now derive from this one. Included support for certain named functions as strings. Added automatic normalization of the provided functions. A smarter automatic way of adding ticks to the colorbar implemented. Examples changed accordingly. --- .../colormap_normalizations_arbitrarynorm.py | 45 +- .../colormap_normalizations_rootnorm.py | 22 +- lib/matplotlib/colors.py | 466 +++++++++--------- 3 files changed, 262 insertions(+), 271 deletions(-) diff --git a/doc/users/plotting/examples/colormap_normalizations_arbitrarynorm.py b/doc/users/plotting/examples/colormap_normalizations_arbitrarynorm.py index 9c6918339371..ab08ba4feae2 100644 --- a/doc/users/plotting/examples/colormap_normalizations_arbitrarynorm.py +++ b/doc/users/plotting/examples/colormap_normalizations_arbitrarynorm.py @@ -45,24 +45,8 @@ def gauss2d(x, y, a0, x0, y0, wx, wy): cmap = cm.gist_rainbow -norms = [('Linear Scale', None), - ('Arbitrary norm', - colors.ArbitraryNorm(fpos=(lambda x: x**0.2), - fposinv=(lambda x: x**5), - fneg=(lambda x: x**0.5), - fneginv=(lambda x: x**2), - center=0.4)), - ('Positive arbitrary norm', - colors.PositiveArbitraryNorm(vmin=0, - fpos=(lambda x: x**0.5), - fposinv=(lambda x: x**2))), - ('Negative arbitrary norm', - colors.NegativeArbitraryNorm(vmax=0, - fneg=(lambda x: x**0.5), - fneginv=(lambda x: x**2)))] - - -for label, norm in norms: + +def makePlot(norm, label): fig, ax = plt.subplots() cax = ax.pcolormesh(x, y, data, cmap=cmap, norm=norm) ax.set_title(label) @@ -74,4 +58,29 @@ def gauss2d(x, y, a0, x0, y0, wx, wy): ticks = None cbar = fig.colorbar(cax, format='%.3g', ticks=ticks) + +makePlot(None, 'Regular linear scale') + + +norm = colors.SingleArbitraryNorm(vmin=0, f='sqrt') +makePlot(norm, 'Single arbitrary norm') + +norm = colors.SingleArbitraryNorm(vmin=0, + f=(lambda x: x**0.25), + finv=(lambda x: x**4)) +makePlot(norm, 'Simple arbitrary norm, custom function') + + +norm = colors.MirrorArbitraryNorm(fpos='crt', + fneg='linear', + center_cm=0.3, + center_data=0.0) +makePlot(norm, 'Mirror arbitrary norm') + + +norm = colors.ArbitraryNorm(flist=['linear', 'quadratic', 'cubic'], + refpoints_cm=[0.5, 0.75], + refpoints_data=[0., 1.]) +makePlot(norm, 'Arbitrary norm') + plt.show() diff --git a/doc/users/plotting/examples/colormap_normalizations_rootnorm.py b/doc/users/plotting/examples/colormap_normalizations_rootnorm.py index a5c35b438686..771f0afdfce5 100644 --- a/doc/users/plotting/examples/colormap_normalizations_rootnorm.py +++ b/doc/users/plotting/examples/colormap_normalizations_rootnorm.py @@ -3,7 +3,7 @@ Examples of colormap root normalization ============================================ -Here I plot an image array with data spanning for a large dynamic range, +Here I plot an image array with data spanning for a large dynamic range, using different normalizations. Look at how each of them enhances different features. @@ -44,16 +44,8 @@ def gauss2d(x, y, a0, x0, y0, wx, wy): cmap = cm.gist_rainbow -norms = [('Linear Scale', None), - ('Symmetric root norm', - colors.SymRootNorm(orderpos=7, orderneg=2, center=0.3)), - ('Positive root norm', - colors.PositiveRootNorm(vmin=0, orderpos=5)), - ('Negative root norm', - colors.NegativeRootNorm(vmax=0, orderneg=5))] - -for label, norm in norms: +def makePlot(norm, label): fig, ax = plt.subplots() cax = ax.pcolormesh(x, y, data, cmap=cmap, norm=norm) ax.set_title(label) @@ -65,4 +57,14 @@ def gauss2d(x, y, a0, x0, y0, wx, wy): ticks = None cbar = fig.colorbar(cax, format='%.3g', ticks=ticks) + +makePlot(None, 'Regular linear scale') + +norm = colors.RootNorm(vmin=0, orderpos=5) +makePlot(norm, 'Root norm') + +norm = colors.MirrorRootNorm( + orderpos=7, orderneg=2, center_cm=0.3, center_data=0.) +makePlot(norm, 'Mirror root norm') + plt.show() diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index ea11d20b42db..718e9ed5a325 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -960,19 +960,17 @@ def scaled(self): class ArbitraryNorm(Normalize): """ - Normalization allowing the definition of any arbitrary non linear - function for the colorbar, for both, the positive, and the negative - directions. + Normalization allowing the definition of any non linear + function for different ranges of the colorbar. >>> norm=ArbitraryNorm(fpos=(lambda x: x**0.5), - fposinv=(lambda x: x**2), - fneg=(lambda x: x**0.25), - fneginv=(lambda x: x**4)) + >>> fposinv=(lambda x: x**2), + >>> fneg=(lambda x: x**0.25), + >>> fneginv=(lambda x: x**4)) """ - def __init__(self, fpos=(lambda x: x**.5), - fposinv=(lambda x: x**2), - fneg=None, fneginv=None, - center=.5, + def __init__(self, flist, + finvlist=None, + refpoints_cm=[None], refpoints_data=[None], vmin=None, vmax=None, clip=False): """ *fpos*: @@ -990,123 +988,173 @@ def __init__(self, fpos=(lambda x: x**.5), that will be assigned to the zero value. """ - if fneg is None: - fneg = fpos - if fneginv is None: - fneginv = fposinv if vmin is not None and vmax is not None: - if vmin > vmax: + if vmin >= vmax: raise ValueError("vmin must be less than vmax") - self._fneg = fneg - self._fpos = fpos - self._fneginv = fneginv - self._fposinv = fposinv - self._center = center - Normalize.__init__(self, vmin, vmax, clip) - def __call__(self, value, clip=None): - if clip is None: - clip = self.clip + if finvlist == None: + finvlist = [None] * len(flist) - result, is_scalar = self.process_value(value) + if len(flist) != len(finvlist): + raise ValueError("len(flist) must be equal than len(finvlist)") - vmin = self.vmin - vmax = self.vmax + if len(refpoints_cm) != len(flist) - 1: + raise ValueError( + "len(refpoints_cm) must be equal than len(flist) -1") - widthpos = 1 - self._center - widthneg = self._center + if len(refpoints_data) != len(refpoints_cm): + raise ValueError( + "len(refpoints_data) must be equal than len(refpoints_cm)") - result[result > vmax] = vmax - result[result < vmin] = vmin + self._refpoints_cm = np.concatenate( + ([0.0], np.array(refpoints_cm), [1.0])) + if any(np.diff(self._refpoints_cm) <= 0): + raise ValueError( + "refpoints_cm values must be monotonically increasing and within the (0.0,1.0) interval") - maskpositive = result > 0 - masknegative = result < 0 - if vmax > 0 and vmin < 0: - result[masknegative] = - \ - self._fneg(result[masknegative] / vmin) * widthneg - result[maskpositive] = self._fpos( - result[maskpositive] / vmax) * widthpos + self._refpoints_data = np.concatenate( + ([None], np.array(refpoints_data), [None])) - elif vmax > 0 and vmin >= 0: - result[maskpositive] = self._fpos( - (result[maskpositive] - vmin) / (vmax - vmin)) * widthpos + if len(self._refpoints_data[1:-1]) > 2 and any(np.diff(self._refpoints_data[1:-1]) <= 0): + raise ValueError( + "refpoints_data values must be monotonically increasing") - elif vmax <= 0 and vmin < 0: - result[masknegative] = - \ - self._fneg((result[maskpositive] - vmax) / - (vmin - vmax)) * widthneg + # Parsing the function strings if any: + self._flist = [] + self._finvlist = [] + for i in range(len(flist)): + funs = ArbitraryNorm._fun_parser((flist[i], finvlist[i])) + if funs[0] is None or funs[1] is None: + raise ValueError( + "Inverse function not provided for %i range" % i) - result = result + widthneg + self._flist.append(ArbitraryNorm._fun_normalizer(funs[0])) + self._finvlist.append(ArbitraryNorm._fun_normalizer(funs[1])) - self.autoscale_None(result) - return result + super(ArbitraryNorm, self).__init__(vmin, vmax, clip) - def inverse(self, value): + def __call__(self, value, clip=None): + if clip is None: + clip = self.clip + + result, is_scalar = self.process_value(value) vmin = self.vmin vmax = self.vmax - widthpos = 1 - self._center - widthneg = self._center + 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( + "data reference points must be within the (vmin,vmax) interval") + + widths_cm = np.diff(rp_cm) + widths_d = np.diff(rp_d) + resultnorm = result.copy() * 0 + + result[result >= vmax] = vmax + result[result <= vmin] = vmin + + for i in range(len(widths_cm)): + width_cm = widths_cm[i] + width_d = widths_d[i] + mask = (result >= rp_d[i]) * (result <= rp_d[i + 1]) + resultnorm[mask] = self._flist[i]( + (result[mask] - rp_d[i]) / width_d) * width_cm + rp_cm[i] + self.autoscale_None(resultnorm) + return resultnorm + + def inverse(self, value): - value = value - widthneg + 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) if cbook.iterable(value): + value_aux = value.copy() * 0 + for i in range(len(widths_cm)): + width_cm = widths_cm[i] + width_d = widths_d[i] + mask = (value >= rp_cm[i]) * (value <= rp_cm[i + 1]) + value_aux[mask] = self._finvlist[i]( + (value[mask] - rp_cm[i]) / width_cm) * width_d + rp_d[i] + value = value_aux + else: + for i in range(len(widths_cm)): + width_cm = widths_cm[i] + width_d = widths_d[i] + if (value >= rp_cm[i]) and (value <= rp_cm[i + 1]): + value = self._finvlist[i]( + (result[mask] - rp_cm[i]) / width_cm) * width_d + rp_d[i] + return value - maskpositive = value > 0 - masknegative = value < 0 + @staticmethod + def _fun_parser(funsin): + flog = 2000 + funs = [('linear', (lambda x: x), (lambda x: x)), + ('quadratic', (lambda x: x**2), (lambda x: x**(1. / 2))), + ('cubic', (lambda x: x**3), (lambda x: x**(1. / 3))), + ('sqrt', (lambda x: x**(1. / 2)), (lambda x: x**2)), + ('crt', (lambda x: x**(1. / 3)), (lambda x: x**3)), + ('log', (lambda x: np.log10(x * flog + 1) / np.log10(flog + 1)), + (lambda x: (10**(np.log10(flog + 1) * x) - 1) / flog))] + + if isinstance(funsin[0], basestring): + funstrs = [] + for fstr, fun, inv in funs: + funstrs.append(fstr) + if funsin[0] == fstr: + return fun, inv + raise ValueError( + "the only strings recognized as functions are %s" % funstrs) + else: + return funsin - if vmax > 0 and vmin < 0: - value[masknegative] = \ - self._fneginv(-value[masknegative] / widthneg) * vmin - value[maskpositive] = self._fposinv( - value[maskpositive] / widthpos) * vmax + @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.))) - elif vmax > 0 and vmin >= 0: - value[maskpositive] = self._fposinv( - value[maskpositive] / widthpos) * (vmax - vmin) + vmin - value[masknegative] = -self._fposinv( - value[masknegative] / widthneg) * (vmax - vmin) + vmin - elif vmax <= 0 and vmin < 0: - value[masknegative] = self._fneginv( - -value[masknegative] / widthneg) * (vmin - vmax) + vmax + def ticks(self, N=None): - else: + rp_cm = self._refpoints_cm + widths_cm = np.diff(rp_cm) - if vmax > 0 and vmin < 0: - if value < 0: - value = self._fneginv(-value / widthneg) * vmin - else: - value = self._fposinv(value / widthpos) * vmax + if N is None: + N = max([13, len(rp_cm)]) - elif vmax > 0 and vmin >= 0: - if value > 0: - value = self._fposinv(value / widthpos) * \ - (vmax - vmin) + vmin + if N < len(rp_cm): + ValueError( + "the number of ticks must me larger that the number or intervals +1") - elif vmax <= 0 and vmin < 0: - if value < 0: - value = self._fneginv(-value / widthneg) * \ - (vmin - vmax) + vmax - return value + ticks = rp_cm.copy() - def ticks(self, N=13): - return self.inverse(np.linspace(0, 1, N)) + available_ticks = N - len(-rp_cm) + distribution = widths_cm * (available_ticks) / widths_cm.sum() + nticks = np.floor(distribution) - def autoscale(self, A): - """ - vmin = self.vmin - vmax = self.vmax + while(nticks.sum() < available_ticks): + ind = np.argmax((distribution - nticks)) + nticks[ind] += 1 - if vmax==0 or np.ma.max(A)==0: - self.vmin = np.ma.min(A) - self.vmax = -self.vmin - elif vmin==0 or np.ma.min(A)==0: - self.vmax = np.ma.max(A) - self.vmin = -self.vmax - else: - self.vmin = np.ma.min(A) - self.vmax = np.ma.max(A) - """ + for i in range(len(nticks)): + if nticks[i] > 0: + N = nticks[i] + auxticks = np.linspace(rp_cm[i], rp_cm[i + 1], N + 2) + ticks = np.concatenate([ticks, auxticks[1:-1]]) + return self.inverse(np.sort(ticks)) + + def autoscale(self, A): self.vmin = np.ma.min(A) self.vmax = np.ma.max(A) @@ -1119,157 +1167,106 @@ def autoscale_None(self, A): self.vmax = np.ma.max(A) -class PositiveArbitraryNorm(Normalize): +class MirrorArbitraryNorm(ArbitraryNorm): """ Normalization allowing the definition of any arbitrary non linear - function for the colorbar for positive data. - >>> norm=PositiveArbitraryNorm(fpos=(lambda x: x**0.5), - fposinv=(lambda x: x**2)) + function for the colorbar, for both, the positive, and the negative + directions. + >>> norm=ArbitraryNorm(fpos=(lambda x: x**0.5), + >>> fposinv=(lambda x: x**2), + >>> fneg=(lambda x: x**0.25), + >>> fneginv=(lambda x: x**4)) """ - def __init__(self, fpos=(lambda x: x**0.5), - fposinv=(lambda x: x**2), + def __init__(self, + fpos, fposinv=None, + fneg=None, fneginv=None, + center_cm=.5, center_data=0.0, vmin=None, vmax=None, clip=False): - """ - *fpos*: - Non-linear function used to normalize the positive range. Must be - able to operate take floating point numbers and numpy arrays. - *fposinv*: - Inverse function of *fpos*. - """ - if vmin is not None and vmax is not None: - if vmin > vmax: - raise ValueError("vmin must be less than vmax") - self._fpos = fpos - self._fposinv = fposinv - Normalize.__init__(self, vmin, vmax, clip) - - def __call__(self, value, clip=None): - if clip is None: - clip = self.clip + if fneg is None and fneginv is not None: + raise ValueError("fneginv not expected without fneg") - result, is_scalar = self.process_value(value) - - vmin = self.vmin - vmax = self.vmax - - result[result > vmax] = vmax - result[result < vmin] = vmin - - result = self._fpos((result - vmin) / (vmax - vmin)) + if fneg is None: + fneg = fpos + fneginv = fposinv - self.autoscale_None(result) - return result + fpos, fposinv = ArbitraryNorm._fun_parser([fpos, fposinv]) + fneg, fneginv = ArbitraryNorm._fun_parser([fneg, fneginv]) - def inverse(self, value): + 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") - vmin = self.vmin - vmax = self.vmax + if vmin is not None and vmax is not None: + if vmin >= vmax: + raise ValueError("vmin must be less than vmax") - if cbook.iterable(value): - value = self._fposinv(value) * (vmax - vmin) + vmin - else: - value = self._fposinv(value) * (vmax - vmin) + vmin - return value + if center_cm <= 0.0 or center_cm >= 1.0: + raise ValueError("center must be within the (0.0,1.0) interval") - def ticks(self, N=11): - return self.inverse(np.linspace(0, 1, N)) + refpoints_cm = np.array([center_cm]) + refpoints_data = np.array([center_data]) - def autoscale(self, A): - self.vmin = np.ma.min(A) - self.vmax = np.ma.max(A) + flist = [lambda (x):(-fneg(-x + 1) + 1), fpos] + finvlist = [lambda (x):(-fneginv(-x + 1) + 1), fposinv] - def autoscale_None(self, A): - if self.vmin is not None and self.vmax is not None: - return - if self.vmin is None: - self.vmin = np.ma.min(A) - if self.vmax is None: - self.vmax = np.ma.max(A) + super(MirrorArbitraryNorm, self).__init__(flist=flist, + finvlist=finvlist, + refpoints_cm=refpoints_cm, + refpoints_data=refpoints_data, + vmin=vmin, vmax=vmax, clip=clip) -class NegativeArbitraryNorm(Normalize): +class SingleArbitraryNorm(ArbitraryNorm): """ Normalization allowing the definition of any arbitrary non linear - function for the colorbar for negative data. - >>> norm=NegativeArbitraryNorm(fneg=(lambda x: x**0.5), - fneginv=(lambda x: x**2)) + function for the colorbar, for both, the positive, and the negative + directions. + >>> norm=ArbitraryNorm(fpos=(lambda x: x**0.5), + >>> fposinv=(lambda x: x**2), + >>> fneg=(lambda x: x**0.25), + >>> fneginv=(lambda x: x**4)) """ - def __init__(self, fneg=(lambda x: x**0.5), fneginv=(lambda x: x**2), + def __init__(self, f, finv=None, vmin=None, vmax=None, clip=False): - """ - *fneg*: - Non-linear function used to normalize the negative range. It - not need to take in to account the negative sign. Must be - able to operate take floating point numbers and numpy arrays. - *fneginv*: - Inverse function of *fneg*. - """ - - if vmin is not None and vmax is not None: - if vmin > vmax: - raise ValueError("vmin must be less than vmax") - self._fneg = fneg - self._fneginv = fneginv - Normalize.__init__(self, vmin, vmax, clip) - - def __call__(self, value, clip=None): - if clip is None: - clip = self.clip - result, is_scalar = self.process_value(value) + fp, finv = ArbitraryNorm._fun_parser([f, finv]) - vmin = self.vmin - vmax = self.vmax + if finv is None: + raise ValueError("Inverse function not provided") - result[result > vmax] = vmax - result[result < vmin] = vmin - - result = -self._fneg((result - vmax) / (vmin - vmax)) - result = result + 1 - - self.autoscale_None(result) - return result - - def inverse(self, value): - - vmin = self.vmin - vmax = self.vmax - - value = value - 1 + if vmin is not None and vmax is not None: + if vmin >= vmax: + raise ValueError("vmin must be less than vmax") - if cbook.iterable(value): - value = self._fneginv(-value) * (vmin - vmax) + vmax - else: - value = self._fneginv(value) * (vmin - vmax) + vmax + refpoints_cm = np.array([]) + refpoints_data = np.array([]) - return value + flist = [f] + finvlist = [finv] - def ticks(self, N=11): - return self.inverse(np.linspace(0, 1, N)) + super(SingleArbitraryNorm, self).__init__(flist=flist, + finvlist=finvlist, + refpoints_cm=refpoints_cm, + refpoints_data=refpoints_data, + vmin=vmin, vmax=vmax, clip=clip) - def autoscale(self, A): - self.vmin = np.ma.min(A) - self.vmax = np.ma.max(A) - def autoscale_None(self, A): - if self.vmin is not None and self.vmax is not None: - return - if self.vmin is None: - self.vmin = np.ma.min(A) - if self.vmax is None: - self.vmax = np.ma.max(A) - - -class SymRootNorm(ArbitraryNorm): +class MirrorRootNorm(MirrorArbitraryNorm): """ Root normalization for positive and negative data. >>> norm=PositiveRootNorm(orderneg=3,orderpos=7) """ + def __init__(self, orderpos=2, orderneg=None, - vmin=None, vmax=None, clip=False, center=0.5): + center_cm=0.5, center_data=0.0, + vmin=None, vmax=None, clip=False, + ): """ *orderpos*: Degree of the root used to normalize the data for the positive @@ -1281,46 +1278,29 @@ def __init__(self, orderpos=2, orderneg=None, if orderneg is None: orderneg = orderpos - ArbitraryNorm.__init__(self, - fneg=(lambda x: x**(1. / orderneg)), - fneginv=(lambda x: x**(orderneg)), - fpos=(lambda x: x**(1. / orderpos)), - fposinv=(lambda x: x**(orderpos)), - center=center, - vmin=vmin, vmax=vmax, clip=clip) + 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, + vmin=vmin, vmax=vmax, clip=clip) -class PositiveRootNorm(PositiveArbitraryNorm): +class RootNorm(SingleArbitraryNorm): """ Root normalization for positive data. >>> norm=PositiveRootNorm(vmin=0,orderpos=7) """ + def __init__(self, orderpos=2, vmin=None, vmax=None, clip=False): """ *orderpos*: Degree of the root used to normalize the data for the positive direction. """ - PositiveArbitraryNorm.__init__(self, - fpos=(lambda x: x**(1. / orderpos)), - fposinv=(lambda x: x**(orderpos)), - vmin=vmin, vmax=vmax, clip=clip) - - -class NegativeRootNorm(NegativeArbitraryNorm): - """ - Root normalization for negative data. - >>> norm=NegativeRootNorm(vmax=0,orderneg=2) - """ - def __init__(self, orderneg=2, vmin=None, vmax=None, clip=False): - """ - *orderneg*: - Degree of the root used to normalize the data for the negative - direction. - """ - NegativeArbitraryNorm.__init__(self, - fneg=(lambda x: x**(1. / orderneg)), - fneginv=(lambda x: x**(orderneg)), + super(RootNorm, self).__init__(f=(lambda x: x**(1. / orderpos)), + finv=(lambda x: x**(orderpos)), vmin=vmin, vmax=vmax, clip=clip) From b5801ea45d9e4bb9f53c49b522323efa82a05d1b Mon Sep 17 00:00:00 2001 From: alvarosg Date: Wed, 19 Oct 2016 16:33:42 +0100 Subject: [PATCH 06/33] Corrected lambda function syntax that was not compatible with python 3. Change name of argument orderpos by simple order in RootNorm --- .../examples/colormap_normalizations_rootnorm.py | 2 +- lib/matplotlib/colors.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/users/plotting/examples/colormap_normalizations_rootnorm.py b/doc/users/plotting/examples/colormap_normalizations_rootnorm.py index 771f0afdfce5..04f940e75849 100644 --- a/doc/users/plotting/examples/colormap_normalizations_rootnorm.py +++ b/doc/users/plotting/examples/colormap_normalizations_rootnorm.py @@ -60,7 +60,7 @@ def makePlot(norm, label): makePlot(None, 'Regular linear scale') -norm = colors.RootNorm(vmin=0, orderpos=5) +norm = colors.RootNorm(vmin=0, order=5) makePlot(norm, 'Root norm') norm = colors.MirrorRootNorm( diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 718e9ed5a325..78241511661c 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1211,8 +1211,8 @@ def __init__(self, refpoints_cm = np.array([center_cm]) refpoints_data = np.array([center_data]) - flist = [lambda (x):(-fneg(-x + 1) + 1), fpos] - finvlist = [lambda (x):(-fneginv(-x + 1) + 1), fposinv] + flist = [(lambda x:(-fneg(-x + 1) + 1)), fpos] + finvlist = [(lambda x:(-fneginv(-x + 1) + 1)), fposinv] super(MirrorArbitraryNorm, self).__init__(flist=flist, finvlist=finvlist, @@ -1293,14 +1293,14 @@ class RootNorm(SingleArbitraryNorm): >>> norm=PositiveRootNorm(vmin=0,orderpos=7) """ - def __init__(self, orderpos=2, vmin=None, vmax=None, clip=False): + def __init__(self, order=2, vmin=None, vmax=None, clip=False): """ - *orderpos*: + *order*: Degree of the root used to normalize the data for the positive direction. """ - super(RootNorm, self).__init__(f=(lambda x: x**(1. / orderpos)), - finv=(lambda x: x**(orderpos)), + super(RootNorm, self).__init__(f=(lambda x: x**(1. / order)), + finv=(lambda x: x**(order)), vmin=vmin, vmax=vmax, clip=clip) From e93d82dac91e64e33644d66f2cda90fc72932088 Mon Sep 17 00:00:00 2001 From: alvarosg Date: Thu, 20 Oct 2016 17:32:05 +0100 Subject: [PATCH 07/33] Added FuncNorm: now everything inherits from this. Changed the name of ArbitryNorm to PiecewiseNorm. PiecewiseNorm now internally uses np.piecewise. Also improved the examples. --- .../colormap_normalizations_arbitrarynorm.py | 86 ----- .../colormap_normalizations_piecewisenorm.py | 131 +++++++ .../colormap_normalizations_rootnorm.py | 70 ---- lib/matplotlib/colors.py | 344 ++++++++++-------- 4 files changed, 325 insertions(+), 306 deletions(-) delete mode 100644 doc/users/plotting/examples/colormap_normalizations_arbitrarynorm.py create mode 100644 doc/users/plotting/examples/colormap_normalizations_piecewisenorm.py delete mode 100644 doc/users/plotting/examples/colormap_normalizations_rootnorm.py diff --git a/doc/users/plotting/examples/colormap_normalizations_arbitrarynorm.py b/doc/users/plotting/examples/colormap_normalizations_arbitrarynorm.py deleted file mode 100644 index ab08ba4feae2..000000000000 --- a/doc/users/plotting/examples/colormap_normalizations_arbitrarynorm.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -============================================ -Examples of arbitrary colormap normalization -============================================ - -Here I plot an image array with data spanning for a large dynamic range, -using different normalizations. Look at how each of them enhances -different features. - -""" - -import numpy as np -import matplotlib.colors as colors -import matplotlib.pyplot as plt -import matplotlib.cm as cm - -xmax = 16 * np.pi -x = np.linspace(0, xmax, 1024) -y = np.linspace(-2, 1, 512) -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 = 61 -for i in range(N): - data = data + gauss2d(X, Y, 2. * i / N, i * - (xmax / N), -0.25, xmax / (3 * N), 0.07) - data = data - gauss2d(X, Y, 1. * i / N, i * - (xmax / N), -0.75, xmax / (3 * N), 0.07) - -data[Y > 0] = np.cos(X[Y > 0]) * Y[Y > 0]**2 - -N = 61 -for i, val in enumerate(np.linspace(-1, 1, N)): - if val < 0: - aux = val - if val > 0: - aux = val * 2 - data[(X > (i * (xmax / N))) * (Y < -1)] = aux - - -cmap = cm.gist_rainbow - - -def makePlot(norm, label): - fig, ax = plt.subplots() - cax = ax.pcolormesh(x, y, data, cmap=cmap, norm=norm) - ax.set_title(label) - ax.set_xlim(0, xmax) - ax.set_ylim(-2, 1) - if norm: - ticks = norm.ticks() - else: - ticks = None - cbar = fig.colorbar(cax, format='%.3g', ticks=ticks) - - -makePlot(None, 'Regular linear scale') - - -norm = colors.SingleArbitraryNorm(vmin=0, f='sqrt') -makePlot(norm, 'Single arbitrary norm') - -norm = colors.SingleArbitraryNorm(vmin=0, - f=(lambda x: x**0.25), - finv=(lambda x: x**4)) -makePlot(norm, 'Simple arbitrary norm, custom function') - - -norm = colors.MirrorArbitraryNorm(fpos='crt', - fneg='linear', - center_cm=0.3, - center_data=0.0) -makePlot(norm, 'Mirror arbitrary norm') - - -norm = colors.ArbitraryNorm(flist=['linear', 'quadratic', 'cubic'], - refpoints_cm=[0.5, 0.75], - refpoints_data=[0., 1.]) -makePlot(norm, 'Arbitrary norm') - -plt.show() diff --git a/doc/users/plotting/examples/colormap_normalizations_piecewisenorm.py b/doc/users/plotting/examples/colormap_normalizations_piecewisenorm.py new file mode 100644 index 000000000000..151f6ce11755 --- /dev/null +++ b/doc/users/plotting/examples/colormap_normalizations_piecewisenorm.py @@ -0,0 +1,131 @@ +""" +============================================ +Examples of arbitrary colormap normalization +============================================ + +Here I plot an image array with data spanning for a large dynamic range, +using different normalizations. Look at how each of them enhances +different features. + +""" + +import ArbitraryNorm as colors + +import numpy as np +# import matplotlib.colors as colors +import matplotlib.pyplot as plt +import matplotlib.cm as cm + +# Creating some toy data +xmax = 16 * np.pi +x = np.linspace(0, xmax, 1024) +y = np.linspace(-2, 2, 512) +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) + +maskY = (Y > -1) * (Y <= 0) +N = 31 +for i in range(N): + maskX = (X > (i * (xmax / N))) * (X <= ((i + 1) * (xmax / N))) + mask = maskX * maskY + data[mask] += gauss2d(X[mask], Y[mask], 2. * i / (N - 1), (i + 0.5) * + (xmax / N), -0.25, xmax / (3 * N), 0.07) + data[mask] -= gauss2d(X[mask], Y[mask], 1. * i / (N - 1), (i + 0.5) * + (xmax / N), -0.75, xmax / (3 * N), 0.07) + +maskY = (Y > 0) * (Y <= 1) +data[maskY] = np.cos(X[maskY]) * Y[maskY]**2 + +N = 61 +maskY = (Y > 1) * (Y <= 2.) +for i, val in enumerate(np.linspace(-1, 1, N)): + if val < 0: + aux = val + if val > 0: + aux = val * 2 + + maskX = (X > (i * (xmax / N))) * (X <= ((i + 1) * (xmax / N))) + data[maskX * maskY] = aux + +N = 11 +maskY = (Y <= -1) +for i, val in enumerate(np.linspace(-1, 1, N)): + if val < 0: + factor = 1 + if val >= 0: + factor = 2 + maskX = (X > (i * (xmax / N))) * (X <= ((i + 1) * (xmax / N))) + mask = maskX * maskY + data[mask] = val * factor + + if i != N - 1: + data[mask] += gauss2d(X[mask], Y[mask], 0.03 * factor, (i + 0.5) * + (xmax / N), -1.25, xmax / (3 * N), 0.07) + if i != 0: + data[mask] -= gauss2d(X[mask], Y[mask], 0.1 * factor, (i + 0.5) * + (xmax / N), -1.75, xmax / (3 * N), 0.07) + + +cmap = cm.spectral + + +def makePlot(norm, label=''): + fig, ax = plt.subplots() + cax = ax.pcolormesh(x, y, data, cmap=cmap, norm=norm) + ax.set_title(label) + ax.set_xlim(0, xmax) + ax.set_ylim(-2, 2) + if norm: + ticks = norm.ticks() + else: + ticks = None + cbar = fig.colorbar(cax, format='%.3g', ticks=ticks) + + +makePlot(None, 'Regular linear scale') + +# Example of logarithm normalization using FuncNorm +norm = colors.FuncNorm(f=lambda x: np.log10(x), + finv=lambda x: 10.**(x), vmin=0.01, vmax=2) +makePlot(norm, "Log normalization using FuncNorm") +# The same can be achived with +# norm = colors.FuncNorm(f='log',vmin=0.01,vmax=2) + +# Example of root normalization using FuncNorm +norm = colors.FuncNorm(f='sqrt', vmin=0.0, vmax=2) +makePlot(norm, "Root normalization using FuncNorm") + +# Performing a symmetric amplification of the features around 0 +norm = colors.MirrorPiecewiseNorm(fpos='crt') +makePlot(norm, "Amplified features symetrically around \n" + "0 with MirrorPiecewiseNorm") + + +# Amplifying features near 0.6 with MirrorPiecewiseNorm +norm = colors.MirrorPiecewiseNorm(fpos='crt', fneg='crt', + center_cm=0.35, + center_data=0.6) +makePlot(norm, "Amplifying positive and negative features\n" + "standing on 0.6 with MirrorPiecewiseNorm") + +# Amplifying features near both -0.4 and near 1.2 with PiecewiseNorm +norm = colors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], + refpoints_cm=[0.25, 0.5, 0.75], + refpoints_data=[-0.4, 1, 1.2]) +makePlot(norm, "Amplifying positive and negative features standing\n" + " on -0.4 and on 1.2 with PiecewiseNorm") + +# Amplifying features near both -0.4 and near 1.2 with PiecewiseNorm +norm = colors.PiecewiseNorm(flist=['linear', 'crt', 'crt'], + refpoints_cm=[0.2, 0.6], + refpoints_data=[-0.6, 1.2]) +makePlot(norm, "Amplifying only positive features standing on -0.6\n" + " and on 1.2 with PiecewiseNorm") + + +plt.show() diff --git a/doc/users/plotting/examples/colormap_normalizations_rootnorm.py b/doc/users/plotting/examples/colormap_normalizations_rootnorm.py deleted file mode 100644 index 04f940e75849..000000000000 --- a/doc/users/plotting/examples/colormap_normalizations_rootnorm.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -============================================ -Examples of colormap root normalization -============================================ - -Here I plot an image array with data spanning for a large dynamic range, -using different normalizations. Look at how each of them enhances -different features. - -""" - -import numpy as np -import matplotlib.colors as colors -import matplotlib.pyplot as plt -import matplotlib.cm as cm - -xmax = 16 * np.pi -x = np.linspace(0, xmax, 1024) -y = np.linspace(-2, 1, 512) -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 = 61 -for i in range(N): - data = data + gauss2d(X, Y, 2. * i / N, i * - (xmax / N), -0.25, xmax / (3 * N), 0.07) - data = data - gauss2d(X, Y, 1. * i / N, i * - (xmax / N), -0.75, xmax / (3 * N), 0.07) - -data[Y > 0] = np.cos(X[Y > 0]) * Y[Y > 0]**2 - -N = 61 -for i, val in enumerate(np.linspace(-1, 1, N)): - if val < 0: - aux = val - if val > 0: - aux = val * 2 - data[(X > (i * (xmax / N))) * (Y < -1)] = aux - -cmap = cm.gist_rainbow - - -def makePlot(norm, label): - fig, ax = plt.subplots() - cax = ax.pcolormesh(x, y, data, cmap=cmap, norm=norm) - ax.set_title(label) - ax.set_xlim(0, xmax) - ax.set_ylim(-2, 1) - if norm: - ticks = norm.ticks() - else: - ticks = None - cbar = fig.colorbar(cax, format='%.3g', ticks=ticks) - - -makePlot(None, 'Regular linear scale') - -norm = colors.RootNorm(vmin=0, order=5) -makePlot(norm, 'Root norm') - -norm = colors.MirrorRootNorm( - orderpos=7, orderneg=2, center_cm=0.3, center_data=0.) -makePlot(norm, 'Mirror root norm') - -plt.show() diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 78241511661c..cd01badc9b72 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -958,11 +958,111 @@ def scaled(self): return (self.vmin is not None and self.vmax is not None) -class ArbitraryNorm(Normalize): +class FuncNorm(Normalize): + + def __init__(self, f, finv=None, + vmin=None, vmax=None, clip=False): + + f, finv = FuncNorm._fun_parser([f, finv]) + + if finv is None: + raise ValueError("Inverse function not provided") + + if vmin is not None and vmax is not None: + if vmin >= vmax: + raise ValueError("vmin must be less than vmax") + + self._f = f + self._finv = finv + + super(FuncNorm, self).__init__(vmin, vmax, clip) + + def _update_f(self, vmin, vmax): + return + + def __call__(self, value, clip=None): + if clip is None: + clip = self.clip + + result, is_scalar = self.process_value(value) + + vmin = self.vmin + vmax = self.vmax + + self._update_f(vmin, vmax) + + result[result >= vmax] = vmax + result[result <= vmin] = vmin + resultnorm = (self._f(result) - self._f(vmin)) / \ + (self._f(vmax) - self._f(vmin)) + + self.autoscale_None(resultnorm) + return np.ma.array(resultnorm) + + def inverse(self, value): + 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_parser(funsin): + flog = 2000 + funs = [('linear', (lambda x: x), (lambda x: x)), + ('quadratic', (lambda x: x**2), (lambda x: x**(1. / 2))), + ('cubic', (lambda x: x**3), (lambda x: x**(1. / 3))), + ('sqrt', (lambda x: x**(1. / 2)), (lambda x: x**2)), + ('crt', (lambda x: x**(1. / 3)), (lambda x: x**3)), + ('log(x+1)', + (lambda x: np.log10(x * flog + 1) / np.log10(flog + 1)), + (lambda x: (10**(np.log10(flog + 1) * x) - 1) / flog)), + ('log', + (lambda x: np.log10(x)), + (lambda x: (10**(x))))] + + if isinstance(funsin[0], basestring): + funstrs = [] + for fstr, fun, inv in funs: + funstrs.append(fstr) + if funsin[0] == fstr: + return fun, inv + raise ValueError( + "the only strings recognized as functions are %s" % funstrs) + else: + return funsin + + @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, N=13): + return self.inverse(np.linspace(0, 1, N)) + + def autoscale(self, A): + self.vmin = np.ma.min(A) + self.vmax = np.ma.max(A) + + def autoscale_None(self, A): + if self.vmin is not None and self.vmax is not None: + return + if self.vmin is None: + self.vmin = np.ma.min(A) + if self.vmax is None: + self.vmax = np.ma.max(A) + + +class PiecewiseNorm(FuncNorm): """ Normalization allowing the definition of any non linear function for different ranges of the colorbar. - >>> norm=ArbitraryNorm(fpos=(lambda x: x**0.5), + >>> norm=PiecewiseNorm(fpos=(lambda x: x**0.5), >>> fposinv=(lambda x: x**2), >>> fneg=(lambda x: x**0.25), >>> fneginv=(lambda x: x**4)) @@ -992,7 +1092,7 @@ def __init__(self, flist, if vmin >= vmax: raise ValueError("vmin must be less than vmax") - if finvlist == None: + if finvlist is None: finvlist = [None] * len(flist) if len(flist) != len(finvlist): @@ -1010,12 +1110,14 @@ def __init__(self, flist, ([0.0], np.array(refpoints_cm), [1.0])) if any(np.diff(self._refpoints_cm) <= 0): raise ValueError( - "refpoints_cm values must be monotonically increasing and within the (0.0,1.0) interval") + "refpoints_cm values 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): + if (len(self._refpoints_data[1:-1]) > 2 and + any(np.diff(self._refpoints_data[1:-1]) <= 0)): raise ValueError( "refpoints_data values must be monotonically increasing") @@ -1023,50 +1125,55 @@ def __init__(self, flist, self._flist = [] self._finvlist = [] for i in range(len(flist)): - funs = ArbitraryNorm._fun_parser((flist[i], finvlist[i])) + funs = FuncNorm._fun_parser((flist[i], finvlist[i])) if funs[0] is None or funs[1] is None: raise ValueError( "Inverse function not provided for %i range" % i) - self._flist.append(ArbitraryNorm._fun_normalizer(funs[0])) - self._finvlist.append(ArbitraryNorm._fun_normalizer(funs[1])) + self._flist.append(FuncNorm._fun_normalizer(funs[0])) + self._finvlist.append(FuncNorm._fun_normalizer(funs[1])) - super(ArbitraryNorm, self).__init__(vmin, vmax, clip) - - def __call__(self, value, clip=None): - if clip is None: - clip = self.clip - - result, is_scalar = self.process_value(value) + # 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, vmin, vmax, clip) + 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): + if (len(rp_d[1:-1]) > 0 and + (any(rp_d[1:-1]) <= vmin or any(rp_d[1:-1]) >= vmax)): raise ValueError( - "data reference points must be within the (vmin,vmax) interval") + "data reference points must be " + "within the (vmin,vmax) interval") widths_cm = np.diff(rp_cm) widths_d = np.diff(rp_d) - resultnorm = result.copy() * 0 - result[result >= vmax] = vmax - result[result <= vmin] = vmin + masks = [] + funcs = [] for i in range(len(widths_cm)): - width_cm = widths_cm[i] - width_d = widths_d[i] - mask = (result >= rp_d[i]) * (result <= rp_d[i + 1]) - resultnorm[mask] = self._flist[i]( - (result[mask] - rp_d[i]) / width_d) * width_cm + rp_cm[i] - self.autoscale_None(resultnorm) - return resultnorm - - def inverse(self, value): - + 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) == True]) if np.isscalar( + x) else mi(x) == True 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 @@ -1076,54 +1183,36 @@ def inverse(self, value): widths_cm = np.diff(rp_cm) widths_d = np.diff(rp_d) - if cbook.iterable(value): - value_aux = value.copy() * 0 - for i in range(len(widths_cm)): - width_cm = widths_cm[i] - width_d = widths_d[i] - mask = (value >= rp_cm[i]) * (value <= rp_cm[i + 1]) - value_aux[mask] = self._finvlist[i]( - (value[mask] - rp_cm[i]) / width_cm) * width_d + rp_d[i] - value = value_aux - else: - for i in range(len(widths_cm)): - width_cm = widths_cm[i] - width_d = widths_d[i] - if (value >= rp_cm[i]) and (value <= rp_cm[i + 1]): - value = self._finvlist[i]( - (result[mask] - rp_cm[i]) / width_cm) * width_d + rp_d[i] - return value + masks = [] + funcs = [] - @staticmethod - def _fun_parser(funsin): - flog = 2000 - funs = [('linear', (lambda x: x), (lambda x: x)), - ('quadratic', (lambda x: x**2), (lambda x: x**(1. / 2))), - ('cubic', (lambda x: x**3), (lambda x: x**(1. / 3))), - ('sqrt', (lambda x: x**(1. / 2)), (lambda x: x**2)), - ('crt', (lambda x: x**(1. / 3)), (lambda x: x**3)), - ('log', (lambda x: np.log10(x * flog + 1) / np.log10(flog + 1)), - (lambda x: (10**(np.log10(flog + 1) * x) - 1) / flog))] - - if isinstance(funsin[0], basestring): - funstrs = [] - for fstr, fun, inv in funs: - funstrs.append(fstr) - if funsin[0] == fstr: - return fun, inv - raise ValueError( - "the only strings recognized as functions are %s" % funstrs) - else: - return funsin - - @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.))) + 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) == True]) if np.isscalar( + x) else mi(x) == True for mi in masks]) + x = np.array([0.5, 1]) + 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() + #import matplotlib.pyplot as plt + # plt.figure() + # x=np.linspace(-1,2,100) + # plt.plot(x,self._f(x)) + # y=np.linspace(0,1,100) + # plt.plot(self._finv(y),y) + # plt.show() + + return def ticks(self, N=None): @@ -1135,7 +1224,8 @@ def ticks(self, N=None): if N < len(rp_cm): ValueError( - "the number of ticks must me larger that the number or intervals +1") + "the number of ticks must me larger " + "that the number or intervals +1") ticks = rp_cm.copy() @@ -1154,25 +1244,12 @@ def ticks(self, N=None): ticks = np.concatenate([ticks, auxticks[1:-1]]) return self.inverse(np.sort(ticks)) - def autoscale(self, A): - self.vmin = np.ma.min(A) - self.vmax = np.ma.max(A) - - def autoscale_None(self, A): - if self.vmin is not None and self.vmax is not None: - return - if self.vmin is None: - self.vmin = np.ma.min(A) - if self.vmax is None: - self.vmax = np.ma.max(A) - -class MirrorArbitraryNorm(ArbitraryNorm): +class MirrorPiecewiseNorm(PiecewiseNorm): """ - Normalization allowing the definition of any arbitrary non linear - function for the colorbar, for both, the positive, and the negative - directions. - >>> norm=ArbitraryNorm(fpos=(lambda x: x**0.5), + Normalization allowing the definition of data ranges and colormap ranges, + with a non linear map between each. + >>> norm=PiecewiseNorm(fpos=(lambda x: x**0.5), >>> fposinv=(lambda x: x**2), >>> fneg=(lambda x: x**0.25), >>> fneginv=(lambda x: x**4)) @@ -1191,8 +1268,8 @@ def __init__(self, fneg = fpos fneginv = fposinv - fpos, fposinv = ArbitraryNorm._fun_parser([fpos, fposinv]) - fneg, fneginv = ArbitraryNorm._fun_parser([fneg, fneginv]) + fpos, fposinv = PiecewiseNorm._fun_parser([fpos, fposinv]) + fneg, fneginv = PiecewiseNorm._fun_parser([fneg, fneginv]) if fposinv is None: raise ValueError( @@ -1214,50 +1291,15 @@ def __init__(self, flist = [(lambda x:(-fneg(-x + 1) + 1)), fpos] finvlist = [(lambda x:(-fneginv(-x + 1) + 1)), fposinv] - super(MirrorArbitraryNorm, self).__init__(flist=flist, - finvlist=finvlist, - refpoints_cm=refpoints_cm, - refpoints_data=refpoints_data, - vmin=vmin, vmax=vmax, clip=clip) - - -class SingleArbitraryNorm(ArbitraryNorm): - """ - Normalization allowing the definition of any arbitrary non linear - function for the colorbar, for both, the positive, and the negative - directions. - >>> norm=ArbitraryNorm(fpos=(lambda x: x**0.5), - >>> fposinv=(lambda x: x**2), - >>> fneg=(lambda x: x**0.25), - >>> fneginv=(lambda x: x**4)) - """ - - def __init__(self, f, finv=None, - vmin=None, vmax=None, clip=False): - - fp, finv = ArbitraryNorm._fun_parser([f, finv]) - - if finv is None: - raise ValueError("Inverse function not provided") - - if vmin is not None and vmax is not None: - if vmin >= vmax: - raise ValueError("vmin must be less than vmax") - - refpoints_cm = np.array([]) - refpoints_data = np.array([]) - - flist = [f] - finvlist = [finv] - - super(SingleArbitraryNorm, self).__init__(flist=flist, - finvlist=finvlist, - refpoints_cm=refpoints_cm, - refpoints_data=refpoints_data, - vmin=vmin, vmax=vmax, clip=clip) + (super(MirrorPiecewiseNorm, self) + .__init__(flist=flist, + finvlist=finvlist, + refpoints_cm=refpoints_cm, + refpoints_data=refpoints_data, + vmin=vmin, vmax=vmax, clip=clip)) -class MirrorRootNorm(MirrorArbitraryNorm): +class MirrorRootNorm(MirrorPiecewiseNorm): """ Root normalization for positive and negative data. >>> norm=PositiveRootNorm(orderneg=3,orderpos=7) @@ -1278,16 +1320,17 @@ def __init__(self, orderpos=2, orderneg=None, 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, - vmin=vmin, vmax=vmax, clip=clip) + (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, + vmin=vmin, vmax=vmax, clip=clip)) -class RootNorm(SingleArbitraryNorm): +class RootNorm(FuncNorm): """ Root normalization for positive data. >>> norm=PositiveRootNorm(vmin=0,orderpos=7) @@ -1299,9 +1342,10 @@ def __init__(self, order=2, vmin=None, vmax=None, clip=False): Degree of the root used to normalize the data for the positive direction. """ - super(RootNorm, self).__init__(f=(lambda x: x**(1. / order)), - finv=(lambda x: x**(order)), - vmin=vmin, vmax=vmax, clip=clip) + (super(FuncNorm, self) + .__init__(f=(lambda x: x**(1. / order)), + finv=(lambda x: x**(order)), + vmin=vmin, vmax=vmax, clip=clip)) class LogNorm(Normalize): From 3749b0af38868c3c9e54fcd7ab7a164faa660fea Mon Sep 17 00:00:00 2001 From: alvarosg Date: Thu, 20 Oct 2016 17:46:05 +0100 Subject: [PATCH 08/33] Forgot to uncomment an import --- .../examples/colormap_normalizations_piecewisenorm.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/doc/users/plotting/examples/colormap_normalizations_piecewisenorm.py b/doc/users/plotting/examples/colormap_normalizations_piecewisenorm.py index 151f6ce11755..447311d5e1a5 100644 --- a/doc/users/plotting/examples/colormap_normalizations_piecewisenorm.py +++ b/doc/users/plotting/examples/colormap_normalizations_piecewisenorm.py @@ -9,10 +9,8 @@ """ -import ArbitraryNorm as colors - import numpy as np -# import matplotlib.colors as colors +import matplotlib.colors as colors import matplotlib.pyplot as plt import matplotlib.cm as cm From de624916e556a017342519239e11cf656da92a55 Mon Sep 17 00:00:00 2001 From: alvarosg Date: Thu, 20 Oct 2016 18:45:32 +0100 Subject: [PATCH 09/33] Improved the auto-tick feature, and corrected some pep8 issues --- lib/matplotlib/colors.py | 43 +++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index cd01badc9b72..09cd40684963 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1043,7 +1043,13 @@ def _fun_normalizer(fun): return (lambda x: (fun(x) - fun(0.)) / (fun(1.) - fun(0.))) def ticks(self, N=13): - return self.inverse(np.linspace(0, 1, N)) + ticks = self.inverse(np.linspace(0, 1, N)) + finalticks = np.zeros(ticks.shape, dtype=np.bool) + finalticks[0] = True + finalticks[0] = True + ticks = FuncNorm._round_ticks(ticks, finalticks) + + return ticks def autoscale(self, A): self.vmin = np.ma.min(A) @@ -1057,6 +1063,19 @@ def autoscale_None(self, A): if self.vmax is None: self.vmax = np.ma.max(A) + @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): """ @@ -1168,8 +1187,8 @@ def _build_f(self): (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) == True]) if np.isscalar( - x) else mi(x) == True for mi in masks]) + 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 @@ -1195,8 +1214,8 @@ def _build_finv(self): 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) == True]) if np.isscalar( - x) else mi(x) == True for mi in masks]) + maskmaker = (lambda x: [np.array([mi(x)]) if np.isscalar( + x) else mi(x) for mi in masks]) x = np.array([0.5, 1]) finv = (lambda x: np.piecewise(x, maskmaker(x), funcs)) return finv @@ -1242,12 +1261,22 @@ def ticks(self, N=None): N = nticks[i] auxticks = np.linspace(rp_cm[i], rp_cm[i + 1], N + 2) ticks = np.concatenate([ticks, auxticks[1:-1]]) - return self.inverse(np.sort(ticks)) + + 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 the definition of data ranges and colormap ranges, + Normalization allowing the definition of data ranges and colormap ranges, with a non linear map between each. >>> norm=PiecewiseNorm(fpos=(lambda x: x**0.5), >>> fposinv=(lambda x: x**2), From d148756ff5c9585dae818e85280a91faa0c511ea Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Sat, 22 Oct 2016 15:06:26 +0100 Subject: [PATCH 10/33] Improved examples, created a new file for generating sample data.' --- .../colormap_normalizations_piecewisenorm.py | 129 ------------------ .../colormap_normalizations_piecewisenorm.py | 98 +++++++++++++ examples/colormap_normalization/sampledata.py | 90 ++++++++++++ 3 files changed, 188 insertions(+), 129 deletions(-) delete mode 100644 doc/users/plotting/examples/colormap_normalizations_piecewisenorm.py create mode 100644 examples/colormap_normalization/colormap_normalizations_piecewisenorm.py create mode 100644 examples/colormap_normalization/sampledata.py diff --git a/doc/users/plotting/examples/colormap_normalizations_piecewisenorm.py b/doc/users/plotting/examples/colormap_normalizations_piecewisenorm.py deleted file mode 100644 index 447311d5e1a5..000000000000 --- a/doc/users/plotting/examples/colormap_normalizations_piecewisenorm.py +++ /dev/null @@ -1,129 +0,0 @@ -""" -============================================ -Examples of arbitrary colormap normalization -============================================ - -Here I plot an image array with data spanning for a large dynamic range, -using different normalizations. Look at how each of them enhances -different features. - -""" - -import numpy as np -import matplotlib.colors as colors -import matplotlib.pyplot as plt -import matplotlib.cm as cm - -# Creating some toy data -xmax = 16 * np.pi -x = np.linspace(0, xmax, 1024) -y = np.linspace(-2, 2, 512) -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) - -maskY = (Y > -1) * (Y <= 0) -N = 31 -for i in range(N): - maskX = (X > (i * (xmax / N))) * (X <= ((i + 1) * (xmax / N))) - mask = maskX * maskY - data[mask] += gauss2d(X[mask], Y[mask], 2. * i / (N - 1), (i + 0.5) * - (xmax / N), -0.25, xmax / (3 * N), 0.07) - data[mask] -= gauss2d(X[mask], Y[mask], 1. * i / (N - 1), (i + 0.5) * - (xmax / N), -0.75, xmax / (3 * N), 0.07) - -maskY = (Y > 0) * (Y <= 1) -data[maskY] = np.cos(X[maskY]) * Y[maskY]**2 - -N = 61 -maskY = (Y > 1) * (Y <= 2.) -for i, val in enumerate(np.linspace(-1, 1, N)): - if val < 0: - aux = val - if val > 0: - aux = val * 2 - - maskX = (X > (i * (xmax / N))) * (X <= ((i + 1) * (xmax / N))) - data[maskX * maskY] = aux - -N = 11 -maskY = (Y <= -1) -for i, val in enumerate(np.linspace(-1, 1, N)): - if val < 0: - factor = 1 - if val >= 0: - factor = 2 - maskX = (X > (i * (xmax / N))) * (X <= ((i + 1) * (xmax / N))) - mask = maskX * maskY - data[mask] = val * factor - - if i != N - 1: - data[mask] += gauss2d(X[mask], Y[mask], 0.03 * factor, (i + 0.5) * - (xmax / N), -1.25, xmax / (3 * N), 0.07) - if i != 0: - data[mask] -= gauss2d(X[mask], Y[mask], 0.1 * factor, (i + 0.5) * - (xmax / N), -1.75, xmax / (3 * N), 0.07) - - -cmap = cm.spectral - - -def makePlot(norm, label=''): - fig, ax = plt.subplots() - cax = ax.pcolormesh(x, y, data, cmap=cmap, norm=norm) - ax.set_title(label) - ax.set_xlim(0, xmax) - ax.set_ylim(-2, 2) - if norm: - ticks = norm.ticks() - else: - ticks = None - cbar = fig.colorbar(cax, format='%.3g', ticks=ticks) - - -makePlot(None, 'Regular linear scale') - -# Example of logarithm normalization using FuncNorm -norm = colors.FuncNorm(f=lambda x: np.log10(x), - finv=lambda x: 10.**(x), vmin=0.01, vmax=2) -makePlot(norm, "Log normalization using FuncNorm") -# The same can be achived with -# norm = colors.FuncNorm(f='log',vmin=0.01,vmax=2) - -# Example of root normalization using FuncNorm -norm = colors.FuncNorm(f='sqrt', vmin=0.0, vmax=2) -makePlot(norm, "Root normalization using FuncNorm") - -# Performing a symmetric amplification of the features around 0 -norm = colors.MirrorPiecewiseNorm(fpos='crt') -makePlot(norm, "Amplified features symetrically around \n" - "0 with MirrorPiecewiseNorm") - - -# Amplifying features near 0.6 with MirrorPiecewiseNorm -norm = colors.MirrorPiecewiseNorm(fpos='crt', fneg='crt', - center_cm=0.35, - center_data=0.6) -makePlot(norm, "Amplifying positive and negative features\n" - "standing on 0.6 with MirrorPiecewiseNorm") - -# Amplifying features near both -0.4 and near 1.2 with PiecewiseNorm -norm = colors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], - refpoints_cm=[0.25, 0.5, 0.75], - refpoints_data=[-0.4, 1, 1.2]) -makePlot(norm, "Amplifying positive and negative features standing\n" - " on -0.4 and on 1.2 with PiecewiseNorm") - -# Amplifying features near both -0.4 and near 1.2 with PiecewiseNorm -norm = colors.PiecewiseNorm(flist=['linear', 'crt', 'crt'], - refpoints_cm=[0.2, 0.6], - refpoints_data=[-0.6, 1.2]) -makePlot(norm, "Amplifying only positive features standing on -0.6\n" - " and on 1.2 with PiecewiseNorm") - - -plt.show() diff --git a/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py b/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py new file mode 100644 index 000000000000..61f8fcbb2a60 --- /dev/null +++ b/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py @@ -0,0 +1,98 @@ +""" +============================================ +Examples of arbitrary colormap normalization +============================================ + +Here I plot an image array with data spanning for a large dynamic range, +using different normalizations. Look at how each of them enhances +different features. + +""" + +from mpl_toolkits.mplot3d import Axes3D +import matplotlib.colors as colors +import matplotlib.pyplot as plt +import matplotlib.cm as cm +import numpy as np +from sampledata import PiecewiseNormData + +X, Y, data = PiecewiseNormData() +cmap = cm.spectral + +# Creating functions for plotting + + +def makePlot(norm, label=''): + fig, (ax1, ax2) = plt.subplots(1, 2, gridspec_kw={ + 'width_ratios': [1, 2]}, figsize=[9, 4.5]) + fig.subplots_adjust(top=0.87, left=0.07, right=0.96) + fig.suptitle(label) + + cax = ax2.pcolormesh(X, Y, data, cmap=cmap, norm=norm) + ticks = cax.norm.ticks() if norm else None + cbar = fig.colorbar(cax, format='%.3g', ticks=ticks) + ax2.set_xlim(X.min(), X.max()) + ax2.set_ylim(Y.min(), Y.max()) + + data_values = np.linspace(cax.norm.vmin, cax.norm.vmax, 100) + cm_values = cax.norm(data_values) + ax1.plot(data_values, cm_values) + ax1.set_xlabel('Data values') + ax1.set_ylabel('Colormap values') + + +def make3DPlot(label=''): + fig = plt.figure() + fig.suptitle(label) + ax = fig.gca(projection='3d') + cax = ax.plot_surface(X, Y, data, rstride=1, cstride=1, + cmap=cmap, linewidth=0, antialiased=False) + ax.set_zlim(data.min(), data.max()) + fig.colorbar(cax, shrink=0.5, aspect=5) + ax.view_init(20, 225) + + +# Showing how the data looks in linear scale +make3DPlot('Regular linear scale') +makePlot(None, 'Regular linear scale') + +# Example of logarithm normalization using FuncNorm +norm = colors.FuncNorm(f=lambda x: np.log10(x), + finv=lambda x: 10.**(x), vmin=0.01, vmax=2) +makePlot(norm, "Log normalization using FuncNorm") +# The same can be achived with +# norm = colors.FuncNorm(f='log',vmin=0.01,vmax=2) + +# Example of root normalization using FuncNorm +norm = colors.FuncNorm(f='sqrt', vmin=0.0, vmax=2) +makePlot(norm, "Root normalization using FuncNorm") + +# Performing a symmetric amplification of the features around 0 +norm = colors.MirrorPiecewiseNorm(fpos='crt') +makePlot(norm, "Amplified features symetrically around \n" + "0 with MirrorPiecewiseNorm") + + +# Amplifying features near 0.6 with MirrorPiecewiseNorm +norm = colors.MirrorPiecewiseNorm(fpos='crt', fneg='crt', + center_cm=0.35, + center_data=0.6) +makePlot(norm, "Amplifying positive and negative features\n" + "standing on 0.6 with MirrorPiecewiseNorm") + +# Amplifying features near both -0.4 and near 1.2 with PiecewiseNorm +norm = colors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], + refpoints_cm=[0.25, 0.5, 0.75], + refpoints_data=[-0.4, 1, 1.2]) +makePlot(norm, "Amplifying positive and negative features standing\n" + " on -0.4 and 1.2 with PiecewiseNorm") + +# Amplifying features near both -1, -0.2 and near 1.2 with PiecewiseNorm +norm = colors.PiecewiseNorm(flist=['crt', 'crt', 'crt'], + refpoints_cm=[0.4, 0.7], + refpoints_data=[-0.2, 1.2]) +makePlot(norm, "Amplifying only positive features standing on -1, -0.2\n" + " and 1.2 with PiecewiseNorm") + + +plt.show() diff --git a/examples/colormap_normalization/sampledata.py b/examples/colormap_normalization/sampledata.py new file mode 100644 index 000000000000..9b25b72fd915 --- /dev/null +++ b/examples/colormap_normalization/sampledata.py @@ -0,0 +1,90 @@ +""" +================================================================ +Creating sample data for the different examples on normalization +================================================================ + +Data with special features tailored to the need of the different examples on +colormal normalization is created. + +""" + +import numpy as np + + +def PiecewiseNormData(NX=512, NY=256): + """Sample data for the PiecewiseNorm class. + + Returns a 2d array with sample data, along with the X and Y values for the + array. + + Parameters + ---------- + NX : int + Number of samples for the data accross the horizontal dimension. + Default is 512. + NY : int + Number of samples for the data accross the vertical dimension. + Default is 256. + + Returns + ------- + X, Y, data : ndarray of shape (NX,NY) + Values for the `X` coordinates, the `Y` coordinates, and the `data`. + + Examples + -------- + >>> X,Y,Z=PiecewiseNormData() + """ + + xmax = 16 * np.pi + x = np.linspace(0, xmax, NX) + y = np.linspace(-2, 2, NY) + 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) + + maskY = (Y > -1) * (Y <= 0) + N = 31 + for i in range(N): + maskX = (X > (i * (xmax / N))) * (X <= ((i + 1) * (xmax / N))) + mask = maskX * maskY + data[mask] += gauss2d(X[mask], Y[mask], 2. * i / (N - 1), (i + 0.5) * + (xmax / N), -0.25, xmax / (3 * N), 0.07) + data[mask] -= gauss2d(X[mask], Y[mask], 1. * i / (N - 1), (i + 0.5) * + (xmax / N), -0.75, xmax / (3 * N), 0.07) + + maskY = (Y > 0) * (Y <= 1) + data[maskY] = np.cos(X[maskY]) * Y[maskY]**2 + + N = 61 + maskY = (Y > 1) * (Y <= 2.) + for i, val in enumerate(np.linspace(-1, 1, N)): + if val < 0: + aux = val + if val > 0: + aux = val * 2 + + maskX = (X >= (i * (xmax / N))) * (X <= ((i + 1) * (xmax / N))) + data[maskX * maskY] = aux + + N = 11 + maskY = (Y <= -1) + for i, val in enumerate(np.linspace(-1, 1, N)): + if val < 0: + factor = 1 + if val >= 0: + factor = 2 + maskX = (X >= (i * (xmax / N))) * (X <= ((i + 1) * (xmax / N))) + mask = maskX * maskY + data[mask] = val * factor + + if i != N - 1: + data[mask] += gauss2d(X[mask], Y[mask], 0.05 * factor, (i + 0.5) * + (xmax / N), -1.25, xmax / (3 * N), 0.07) + if i != 0: + data[mask] -= gauss2d(X[mask], Y[mask], 0.05 * factor, (i + 0.5) * + (xmax / N), -1.75, xmax / (3 * N), 0.07) + return X, Y, data From 5373a981cbbfe80d8d311d352e3d162cdfeaeb01 Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Sat, 22 Oct 2016 15:12:38 +0100 Subject: [PATCH 11/33] Corrected a double line, and removed a comment --- lib/matplotlib/colors.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 09cd40684963..2aa3c0edb393 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1046,7 +1046,6 @@ def ticks(self, N=13): ticks = self.inverse(np.linspace(0, 1, N)) finalticks = np.zeros(ticks.shape, dtype=np.bool) finalticks[0] = True - finalticks[0] = True ticks = FuncNorm._round_ticks(ticks, finalticks) return ticks @@ -1223,14 +1222,6 @@ def _build_finv(self): def _update_f(self, vmin, vmax): self._f = self._build_f() self._finv = self._build_finv() - #import matplotlib.pyplot as plt - # plt.figure() - # x=np.linspace(-1,2,100) - # plt.plot(x,self._f(x)) - # y=np.linspace(0,1,100) - # plt.plot(self._finv(y),y) - # plt.show() - return def ticks(self, N=None): From 13edeabea7979c7bf619272c0ce3f83e9b718e69 Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Sat, 22 Oct 2016 15:40:19 +0100 Subject: [PATCH 12/33] Tests for FuncNorm added, and bug corrected in FuncNorm --- lib/matplotlib/colors.py | 3 +-- lib/matplotlib/tests/test_colors.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 2aa3c0edb393..7f834aea7de6 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -964,7 +964,6 @@ def __init__(self, f, finv=None, vmin=None, vmax=None, clip=False): f, finv = FuncNorm._fun_parser([f, finv]) - if finv is None: raise ValueError("Inverse function not provided") @@ -985,6 +984,7 @@ def __call__(self, value, clip=None): clip = self.clip result, is_scalar = self.process_value(value) + self.autoscale_None(result) vmin = self.vmin vmax = self.vmax @@ -996,7 +996,6 @@ def __call__(self, value, clip=None): resultnorm = (self._f(result) - self._f(vmin)) / \ (self._f(vmax) - self._f(vmin)) - self.autoscale_None(resultnorm) return np.ma.array(resultnorm) def inverse(self, value): diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index ebfc41aa53e3..c4cf5dd37740 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -155,6 +155,24 @@ def test_LogNorm(): ln = mcolors.LogNorm(clip=True, vmax=5) assert_array_equal(ln([1, 6]), [0, 1.0]) +def test_FuncNorm(): + # Testing limits using a string + norm = mcolors.FuncNorm(f='log', vmin=0.01, vmax=2) + assert_array_equal(norm([0.01, 2]), [0, 1.0]) + + # Testing limits using a string + 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]) + + # Testing limits without vmin, vmax + norm = mcolors.FuncNorm(f='log') + assert_array_equal(norm([0.01, 2]), [0, 1.0]) + + # Testing intermediate values + norm = mcolors.FuncNorm(f='log') + assert_array_almost_equal(norm([0.01, 0.5, 2]), [0, 0.73835195870437, 1.0]) + def test_PowerNorm(): a = np.array([0, 0.5, 1, 1.5], dtype=float) From 21d5cd0a980ee6ae5264721baa92c508eef5c3d0 Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Sat, 22 Oct 2016 16:21:00 +0100 Subject: [PATCH 13/33] Added compatibility for python 3 string check, added tests for PiecewiseNorm --- examples/colormap_normalization/sampledata.py | 4 +- lib/matplotlib/colors.py | 6 +- lib/matplotlib/tests/test_colors.py | 58 +++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) diff --git a/examples/colormap_normalization/sampledata.py b/examples/colormap_normalization/sampledata.py index 9b25b72fd915..8d9ff63de537 100644 --- a/examples/colormap_normalization/sampledata.py +++ b/examples/colormap_normalization/sampledata.py @@ -3,7 +3,7 @@ Creating sample data for the different examples on normalization ================================================================ -Data with special features tailored to the need of the different examples on +Data with special features tailored to the need of the different examples on colormal normalization is created. """ @@ -14,7 +14,7 @@ def PiecewiseNormData(NX=512, NY=256): """Sample data for the PiecewiseNorm class. - Returns a 2d array with sample data, along with the X and Y values for the + Returns a 2d array with sample data, along with the X and Y values for the array. Parameters diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 7f834aea7de6..b0b618eb1db5 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1021,7 +1021,9 @@ def _fun_parser(funsin): (lambda x: np.log10(x)), (lambda x: (10**(x))))] - if isinstance(funsin[0], basestring): + if isinstance(funsin[0], ("".__class__, + u"".__class__, + str("").__class__)): funstrs = [] for fstr, fun, inv in funs: funstrs.append(fstr) @@ -1162,7 +1164,7 @@ def _build_f(self): 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)): + (any(rp_d[1:-1] <= vmin) or any(rp_d[1:-1] >= vmax))): raise ValueError( "data reference points must be " "within the (vmin,vmax) interval") diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index c4cf5dd37740..768e8aaa0e7b 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -155,6 +155,7 @@ def test_LogNorm(): ln = mcolors.LogNorm(clip=True, vmax=5) assert_array_equal(ln([1, 6]), [0, 1.0]) + def test_FuncNorm(): # Testing limits using a string norm = mcolors.FuncNorm(f='log', vmin=0.01, vmax=2) @@ -169,11 +170,68 @@ def test_FuncNorm(): norm = mcolors.FuncNorm(f='log') assert_array_equal(norm([0.01, 2]), [0, 1.0]) + # Testing limits without vmin + norm = mcolors.FuncNorm(f='log', vmax=2) + assert_array_equal(norm([0.01, 2]), [0, 1.0]) + + # Testing limits without vmin + norm = mcolors.FuncNorm(f='log', vmin=0.01) + assert_array_equal(norm([0.01, 2]), [0, 1.0]) + # Testing intermediate values norm = mcolors.FuncNorm(f='log') assert_array_almost_equal(norm([0.01, 0.5, 2]), [0, 0.73835195870437, 1.0]) +def test_PieceWiseNorm(): + # Testing using both strings and functions + norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', lambda x:x**3, 'crt'], + 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]) + + # Testing using only strings + norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], + 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]) + + # Testing with limits + norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], + 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]) + + # Testing with vmin + norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], + 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]) + + # Testing with vmin + norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], + 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]) + + # Testing intermediate values + norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], + 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_PowerNorm(): a = np.array([0, 0.5, 1, 1.5], dtype=float) pnorm = mcolors.PowerNorm(1) From d359a4edc2b99e132c766e649274dc1763cc8666 Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Sat, 22 Oct 2016 17:00:41 +0100 Subject: [PATCH 14/33] Added tests on all classes, including all public methods --- lib/matplotlib/tests/test_colors.py | 115 +++++++++++++++++++++++++++- 1 file changed, 113 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 768e8aaa0e7b..539c12f18f58 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -182,8 +182,20 @@ def test_FuncNorm(): norm = mcolors.FuncNorm(f='log') assert_array_almost_equal(norm([0.01, 0.5, 2]), [0, 0.73835195870437, 1.0]) + # Checking inverse + norm = mcolors.FuncNorm(f='log', vmin=0.01, vmax=2) + x = np.linspace(0.01, 2, 10) + assert_array_almost_equal(x, norm.inverse(norm(x))) + + # Checking ticks + norm = mcolors.FuncNorm(f='log', 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) -def test_PieceWiseNorm(): + +def test_PiecewiseNorm(): # Testing using both strings and functions norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', lambda x:x**3, 'crt'], finvlist=[None, None, @@ -212,7 +224,7 @@ def test_PieceWiseNorm(): vmin=-2) assert_array_equal(norm([-2, -1, 1, 3, 4]), [0., 0.2, 0.5, 0.7, 1.0]) - # Testing with vmin + # Testing with vmax norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], refpoints_cm=[0.2, 0.5, 0.7], refpoints_data=[-1, 1, 3], @@ -231,6 +243,105 @@ def test_PieceWiseNorm(): 0.93811016] assert_array_almost_equal(norm(np.linspace(-0.5, 3.5, 5)), expected) + # Checking inverse + norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], + 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))) + + # Checking ticks + norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], + 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) + + +def test_MirrorPiecewiseNorm(): + # Not necessary to test vmin,vmax, as they are just passed to the + # base class + + # Single argument positive range, defaults + norm = mcolors.MirrorPiecewiseNorm(fpos='crt') + assert_array_equal(norm([-2, 0, 1]), [0., 0.5, 1.0]) + + # Single argument positive, and negative range, with ref point + norm = mcolors.MirrorPiecewiseNorm(fpos='crt', fneg='sqrt', + center_cm=0.35, + center_data=0.6) + assert_array_equal(norm([-2, 0.6, 1]), [0., 0.35, 1.0]) + + # Single argument positive, and negative range, with ref point + norm = mcolors.MirrorPiecewiseNorm(fpos='crt', fneg='sqrt', + center_data=0.6) + assert_array_equal(norm([-2, 0.6, 1]), [0., 0.5, 1.0]) + + # Providing lambda function + 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]) + + # Providing lambda function and negative function string + norm = mcolors.MirrorPiecewiseNorm(fpos=lambda x: x**2, + fposinv=lambda x: x**0.5, + fneg='crt') + assert_array_equal(norm([-2, 0., 1]), [0., 0.5, 1.0]) + + # Testing intermediate values + norm = mcolors.MirrorPiecewiseNorm(fpos=lambda x: x**2, + fposinv=lambda x: x**0.5, + fneg='crt') + expected = [0., + 0.1606978, + 0.52295918, + 0.68431122, + 1.] + assert_array_almost_equal(norm(np.linspace(-2, 3.5, 5)), expected) + + +def test_MirrorRootNorm(): + # All parameters except the order are just passed to the base class + norm = mcolors.MirrorRootNorm(orderpos=2) + assert_array_equal(norm([-2, 0., 1]), [0., 0.5, 1.0]) + + # Checking that the order is copied by default + 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)) + + # Checking intermediate values + 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) + + +def test_RootNorm(): + # All parameters except the order are just passed to the base class + + 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) From 46228295e93acff90369fe8f479eccf7b5b8a835 Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Sat, 22 Oct 2016 17:10:45 +0100 Subject: [PATCH 15/33] Change type of arrays in tests from int to float --- lib/matplotlib/tests/test_colors.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 539c12f18f58..5d1c77e87035 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -202,34 +202,34 @@ def test_PiecewiseNorm(): 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]) + assert_array_equal(norm([-2., -1, 1, 3, 4]), [0., 0.2, 0.5, 0.7, 1.0]) # Testing using only strings norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], 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]) + assert_array_equal(norm([-2., -1, 1, 3, 4]), [0., 0.2, 0.5, 0.7, 1.0]) # Testing with limits norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], 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]) + assert_array_equal(norm([-2., -1, 1, 3, 4]), [0., 0.2, 0.5, 0.7, 1.0]) # Testing with vmin norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], 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]) + assert_array_equal(norm([-2., -1, 1, 3, 4]), [0., 0.2, 0.5, 0.7, 1.0]) # Testing with vmax norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], 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]) + assert_array_equal(norm([-2., -1, 1, 3, 4]), [0., 0.2, 0.5, 0.7, 1.0]) # Testing intermediate values norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], @@ -268,7 +268,7 @@ def test_MirrorPiecewiseNorm(): # Single argument positive range, defaults norm = mcolors.MirrorPiecewiseNorm(fpos='crt') - assert_array_equal(norm([-2, 0, 1]), [0., 0.5, 1.0]) + assert_array_equal(norm([-2, 0., 1]), [0., 0.5, 1.0]) # Single argument positive, and negative range, with ref point norm = mcolors.MirrorPiecewiseNorm(fpos='crt', fneg='sqrt', From 30ff404abb71d185f83b5937df357a55eee55ee7 Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Sat, 22 Oct 2016 17:14:13 +0100 Subject: [PATCH 16/33] Corrected wrong `super()` for RootNorm --- lib/matplotlib/colors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index b0b618eb1db5..f1fcee6f70a5 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1363,7 +1363,7 @@ def __init__(self, order=2, vmin=None, vmax=None, clip=False): Degree of the root used to normalize the data for the positive direction. """ - (super(FuncNorm, self) + (super(RootNorm, self) .__init__(f=(lambda x: x**(1. / order)), finv=(lambda x: x**(order)), vmin=vmin, vmax=vmax, clip=clip)) From df835cb38e077c656d2c8ed270869846abbfcc66 Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Sat, 22 Oct 2016 19:11:39 +0100 Subject: [PATCH 17/33] Solve problem with implicit int to float casting that was not working for numpy 1.6 --- lib/matplotlib/colors.py | 8 ++++++-- lib/matplotlib/tests/test_colors.py | 22 +++++++++++----------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index f1fcee6f70a5..2aaeaa7f8fbb 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -973,6 +973,10 @@ def __init__(self, f, finv=None, self._f = f self._finv = finv + if vmin is not None: + vmin = float(vmin) + if vmax is not None: + vmax = float(vmax) super(FuncNorm, self).__init__(vmin, vmax, clip) @@ -1059,9 +1063,9 @@ def autoscale_None(self, A): if self.vmin is not None and self.vmax is not None: return if self.vmin is None: - self.vmin = np.ma.min(A) + self.vmin = float(np.ma.min(A)) if self.vmax is None: - self.vmax = np.ma.max(A) + self.vmax = float(np.ma.max(A)) @staticmethod def _round_ticks(ticks, permanenttick): diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 5d1c77e87035..5bbc84fea2bb 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -158,12 +158,12 @@ def test_LogNorm(): def test_FuncNorm(): # Testing limits using a string - norm = mcolors.FuncNorm(f='log', vmin=0.01, vmax=2) + norm = mcolors.FuncNorm(f='log', vmin=0.01, vmax=2.) assert_array_equal(norm([0.01, 2]), [0, 1.0]) # Testing limits using a string norm = mcolors.FuncNorm(f=lambda x: np.log10(x), - finv=lambda x: 10.**(x), vmin=0.01, vmax=2) + finv=lambda x: 10.**(x), vmin=0.01, vmax=2.) assert_array_equal(norm([0.01, 2]), [0, 1.0]) # Testing limits without vmin, vmax @@ -171,7 +171,7 @@ def test_FuncNorm(): assert_array_equal(norm([0.01, 2]), [0, 1.0]) # Testing limits without vmin - norm = mcolors.FuncNorm(f='log', vmax=2) + norm = mcolors.FuncNorm(f='log', vmax=2.) assert_array_equal(norm([0.01, 2]), [0, 1.0]) # Testing limits without vmin @@ -183,12 +183,12 @@ def test_FuncNorm(): assert_array_almost_equal(norm([0.01, 0.5, 2]), [0, 0.73835195870437, 1.0]) # Checking inverse - norm = mcolors.FuncNorm(f='log', vmin=0.01, vmax=2) + norm = mcolors.FuncNorm(f='log', vmin=0.01, vmax=2.) x = np.linspace(0.01, 2, 10) assert_array_almost_equal(x, norm.inverse(norm(x))) # Checking ticks - norm = mcolors.FuncNorm(f='log', vmin=0.01, vmax=2) + norm = mcolors.FuncNorm(f='log', 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.] @@ -214,28 +214,28 @@ def test_PiecewiseNorm(): norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], refpoints_cm=[0.2, 0.5, 0.7], refpoints_data=[-1, 1, 3], - vmin=-2, vmax=4) + vmin=-2., vmax=4.) assert_array_equal(norm([-2., -1, 1, 3, 4]), [0., 0.2, 0.5, 0.7, 1.0]) # Testing with vmin norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], refpoints_cm=[0.2, 0.5, 0.7], refpoints_data=[-1, 1, 3], - vmin=-2) + vmin=-2.) assert_array_equal(norm([-2., -1, 1, 3, 4]), [0., 0.2, 0.5, 0.7, 1.0]) # Testing with vmax norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], refpoints_cm=[0.2, 0.5, 0.7], refpoints_data=[-1, 1, 3], - vmax=4) + vmax=4.) assert_array_equal(norm([-2., -1, 1, 3, 4]), [0., 0.2, 0.5, 0.7, 1.0]) # Testing intermediate values norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], refpoints_cm=[0.2, 0.5, 0.7], refpoints_data=[-1, 1, 3], - vmin=-2, vmax=4) + vmin=-2., vmax=4.) expected = [0.38898816, 0.47256809, 0.503125, @@ -247,7 +247,7 @@ def test_PiecewiseNorm(): norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], refpoints_cm=[0.2, 0.5, 0.7], refpoints_data=[-1, 1, 3], - vmin=-2, vmax=4) + vmin=-2., vmax=4.) x = np.linspace(-2, 4, 10) assert_array_almost_equal(x, norm.inverse(norm(x))) @@ -255,7 +255,7 @@ def test_PiecewiseNorm(): norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], refpoints_cm=[0.2, 0.5, 0.7], refpoints_data=[-1, 1, 3], - vmin=-2, vmax=4) + 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.] From dfaa0f8df44aafdeb1220c3cc33d326c1209e9ec Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Sun, 23 Oct 2016 11:22:13 +0100 Subject: [PATCH 18/33] Added documentation in the numpydoc format --- lib/matplotlib/colors.py | 323 ++++++++++++++++++++++++++++++--------- 1 file changed, 248 insertions(+), 75 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 2aaeaa7f8fbb..75ac09e2a044 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -959,39 +959,73 @@ def scaled(self): class FuncNorm(Normalize): + """ + Creates a normalizer using a custom function - def __init__(self, f, finv=None, - vmin=None, vmax=None, clip=False): + 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','crt','log'] can be used. + 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 passed to `matplotlib.colors.Normalize`. + + """ f, finv = FuncNorm._fun_parser([f, finv]) if finv is None: raise ValueError("Inverse function not provided") - if vmin is not None and vmax is not None: - if vmin >= vmax: - raise ValueError("vmin must be less than vmax") - self._f = f self._finv = finv - if vmin is not None: - vmin = float(vmin) - if vmax is not None: - vmax = float(vmax) - super(FuncNorm, self).__init__(vmin, vmax, clip) + 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 in the `[0.0, 1.0]` interval. + + """ if clip is None: clip = self.clip result, is_scalar = self.process_value(value) self.autoscale_None(result) - vmin = self.vmin - vmax = self.vmax + vmin = float(self.vmin) + vmax = float(self.vmax) self._update_f(vmin, vmax) @@ -1003,6 +1037,21 @@ def __call__(self, value, clip=None): 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) @@ -1048,24 +1097,60 @@ def _fun_normalizer(fun): return (lambda x: (fun(x) - fun(0.)) / (fun(1.) - fun(0.))) def ticks(self, N=13): + """ + Returns an automatic list of `N` points in the data space to be used + as ticks in the colorbar. + + Parameters + ---------- + N : integer, optional + Number of ticks to be returned. Default 13. + + Returns + ------- + ticks : ndarray + 1d array of length `N` with the proposed tick locations. + + """ ticks = self.inverse(np.linspace(0, 1, N)) finalticks = np.zeros(ticks.shape, dtype=np.bool) finalticks[0] = True ticks = FuncNorm._round_ticks(ticks, finalticks) - return ticks def autoscale(self, A): - self.vmin = np.ma.min(A) - self.vmax = np.ma.max(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): - if self.vmin is not None and self.vmax is not None: - return + """ + 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): @@ -1083,37 +1168,56 @@ def _round_ticks(ticks, permanenttick): class PiecewiseNorm(FuncNorm): """ - Normalization allowing the definition of any non linear + Normalization allowing the definition of any linear or non-linear function for different ranges of the colorbar. - >>> norm=PiecewiseNorm(fpos=(lambda x: x**0.5), - >>> fposinv=(lambda x: x**2), - >>> fneg=(lambda x: x**0.25), - >>> fneginv=(lambda x: x**4)) + """ def __init__(self, flist, finvlist=None, - refpoints_cm=[None], refpoints_data=[None], - vmin=None, vmax=None, clip=False): + 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 they 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 a different + data range [d0, d1] into colormap ranges [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 + Reference points for the colorbar ranges which will go as + `[0., refpoints_cm[0]]`,... , `[refpoints_cm[i], + refpoints_cm[i+1]]`, `[refpoints_cm[-1], 0.]`, and for the data + ranges which will go as `[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 passed to `matplotlib.colors.Normalize`. + """ - *fpos*: - Non-linear function used to normalize the positive range. Must be - able to operate take floating point numbers and numpy arrays. - *fposinv*: - Inverse function of *fpos*. - *fneg*: - Non-linear function used to normalize the negative range. It - not need to take in to account the negative sign. Must be - able to operate take floating point numbers and numpy arrays. - *fneginv*: - Inverse function of *fneg*. - *center*: Value between 0. and 1. indicating the color in the colorbar - that will be assigned to the zero value. - """ - - if vmin is not None and vmax is not None: - if vmin >= vmax: - raise ValueError("vmin must be less than vmax") if finvlist is None: finvlist = [None] * len(flist) @@ -1158,7 +1262,9 @@ def __init__(self, flist, # 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, vmin, vmax, clip) + 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 @@ -1220,7 +1326,6 @@ def _build_finv(self): maskmaker = (lambda x: [np.array([mi(x)]) if np.isscalar( x) else mi(x) for mi in masks]) - x = np.array([0.5, 1]) finv = (lambda x: np.piecewise(x, maskmaker(x), funcs)) return finv @@ -1230,7 +1335,21 @@ def _update_f(self, vmin, vmax): return def ticks(self, N=None): + """ + Returns an automatic list of *N* points in the data space to be used + as ticks in the colorbar. + + Parameters + ---------- + N : integer, optional + Number of ticks to be returned. Default 13. + Returns + ------- + ticks : ndarray + 1d array of length *N* with the proposed tick locations. + + """ rp_cm = self._refpoints_cm widths_cm = np.diff(rp_cm) @@ -1272,20 +1391,51 @@ def ticks(self, N=None): class MirrorPiecewiseNorm(PiecewiseNorm): """ - Normalization allowing the definition of data ranges and colormap ranges, - with a non linear map between each. - >>> norm=PiecewiseNorm(fpos=(lambda x: x**0.5), - >>> fposinv=(lambda x: x**2), - >>> fneg=(lambda x: x**0.25), - >>> fneginv=(lambda x: x**4)) + Normalization alowing a dual `PiecewiseNorm` simmetrically around a point. + + Data above `center_data` will be normalized independently that data below + it. If only one function is give, the normalization will be symmetric + around that point. """ def __init__(self, fpos, fposinv=None, fneg=None, fneginv=None, - center_cm=.5, center_data=0.0, - vmin=None, vmax=None, clip=False): + center_data=0.0, center_cm=.5, + **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 they 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 a different + data range [d0, d1] into colormap ranges [cm0, cm1] as + ftrans=fnorm((x-d0)/(d1-d0))*(cm1-cm0)+cm0. + 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 in the data that will separate the use of `fneg` and `fpos`. + Must be in the (vmin, vmax) range. Default 0.0. + center_cm : float, optional + Normalized value that will correspond to `center_data`. + Must be in the (0.0, 1.0) range. + Default 0.5. + normalize_kw : dict, optional + Dict with keywords passed to `matplotlib.colors.Normalize`. + + """ if fneg is None and fneginv is not None: raise ValueError("fneginv not expected without fneg") @@ -1303,10 +1453,6 @@ def __init__(self, raise ValueError( "Inverse function must be provided for the negative interval") - if vmin is not None and vmax is not None: - if vmin >= vmax: - raise ValueError("vmin must be less than vmax") - if center_cm <= 0.0 or center_cm >= 1.0: raise ValueError("center must be within the (0.0,1.0) interval") @@ -1321,26 +1467,42 @@ def __init__(self, finvlist=finvlist, refpoints_cm=refpoints_cm, refpoints_data=refpoints_data, - vmin=vmin, vmax=vmax, clip=clip)) + **normalize_kw)) class MirrorRootNorm(MirrorPiecewiseNorm): """ Root normalization for positive and negative data. - >>> norm=PositiveRootNorm(orderneg=3,orderpos=7) + + Data above `center_data` will be normalized with a root of the order + `orderpos` and data below it with a symmetric root of order `orderg neg`. + If only `orderpos` function is given, the normalization will be completely + mirrored. """ def __init__(self, orderpos=2, orderneg=None, center_cm=0.5, center_data=0.0, - vmin=None, vmax=None, clip=False, - ): + **normalize_kw): """ - *orderpos*: - Degree of the root used to normalize the data for the positive - direction. - *orderneg*: - Degree of the root used to normalize the data for the negative - direction. By default equal to *orderpos*. + 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 in the data that will separate range. Must be + in the (vmin, vmax) range. + Default 0.0. + center_cm : float, optional + Normalized value that will correspond do `center_data`. Must be + in the (0.0, 1.0) range. + Default 0.5. + normalize_kw : dict, optional + Dict with keywords passed to `matplotlib.colors.Normalize`. """ if orderneg is None: @@ -1352,25 +1514,36 @@ def __init__(self, orderpos=2, orderneg=None, fposinv=(lambda x: x**(orderpos)), center_cm=center_cm, center_data=center_data, - vmin=vmin, vmax=vmax, clip=clip)) + **normalize_kw)) class RootNorm(FuncNorm): """ - Root normalization for positive data. - >>> norm=PositiveRootNorm(vmin=0,orderpos=7) + Simple root normalization using 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`) + """ - def __init__(self, order=2, vmin=None, vmax=None, clip=False): + def __init__(self, order=2, **normalize_kw): """ - *order*: - Degree of the root used to normalize the data for the positive - direction. + Parameters + ---------- + order : float or int, optional + Degree of the root to be used for normalization. Default 2. + normalize_kw : dict, optional + Dict with keywords passed to `matplotlib.colors.Normalize`. + + Notes + ----- + Only valid for arrays with possitive values, or setting `vmin >= 0`. """ (super(RootNorm, self) .__init__(f=(lambda x: x**(1. / order)), finv=(lambda x: x**(order)), - vmin=vmin, vmax=vmax, clip=clip)) + **normalize_kw)) class LogNorm(Normalize): From a386395f05aefe25563a953c9b7eb8b104fe40ee Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Sun, 23 Oct 2016 12:37:13 +0100 Subject: [PATCH 19/33] Improve style in the examples. Corrected intending problem in the docstrings of `PiecewiseNorm` --- .../colormap_normalizations_piecewisenorm.py | 40 ++++++++++--------- lib/matplotlib/colors.py | 29 ++++++++------ 2 files changed, 38 insertions(+), 31 deletions(-) diff --git a/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py b/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py index 61f8fcbb2a60..69e94efdbcba 100644 --- a/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py +++ b/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py @@ -9,11 +9,15 @@ """ -from mpl_toolkits.mplot3d import Axes3D + +import matplotlib.cm as cm import matplotlib.colors as colors import matplotlib.pyplot as plt -import matplotlib.cm as cm + +from mpl_toolkits.mplot3d import Axes3D + import numpy as np + from sampledata import PiecewiseNormData X, Y, data = PiecewiseNormData() @@ -22,15 +26,15 @@ # Creating functions for plotting -def makePlot(norm, label=''): +def make_plot(norm, label=''): fig, (ax1, ax2) = plt.subplots(1, 2, gridspec_kw={ - 'width_ratios': [1, 2]}, figsize=[9, 4.5]) + 'width_ratios': [1, 2]}, figsize=plt.figaspect(0.5)) fig.subplots_adjust(top=0.87, left=0.07, right=0.96) fig.suptitle(label) cax = ax2.pcolormesh(X, Y, data, cmap=cmap, norm=norm) ticks = cax.norm.ticks() if norm else None - cbar = fig.colorbar(cax, format='%.3g', ticks=ticks) + fig.colorbar(cax, format='%.3g', ticks=ticks) ax2.set_xlim(X.min(), X.max()) ax2.set_ylim(Y.min(), Y.max()) @@ -41,7 +45,7 @@ def makePlot(norm, label=''): ax1.set_ylabel('Colormap values') -def make3DPlot(label=''): +def make_3dplot(label=''): fig = plt.figure() fig.suptitle(label) ax = fig.gca(projection='3d') @@ -53,46 +57,46 @@ def make3DPlot(label=''): # Showing how the data looks in linear scale -make3DPlot('Regular linear scale') -makePlot(None, 'Regular linear scale') +make_3dplot('Regular linear scale') +make_plot(None, 'Regular linear scale') # Example of logarithm normalization using FuncNorm norm = colors.FuncNorm(f=lambda x: np.log10(x), finv=lambda x: 10.**(x), vmin=0.01, vmax=2) -makePlot(norm, "Log normalization using FuncNorm") +make_plot(norm, "Log normalization using FuncNorm") # The same can be achived with # norm = colors.FuncNorm(f='log',vmin=0.01,vmax=2) # Example of root normalization using FuncNorm norm = colors.FuncNorm(f='sqrt', vmin=0.0, vmax=2) -makePlot(norm, "Root normalization using FuncNorm") +make_plot(norm, "Root normalization using FuncNorm") # Performing a symmetric amplification of the features around 0 norm = colors.MirrorPiecewiseNorm(fpos='crt') -makePlot(norm, "Amplified features symetrically around \n" - "0 with MirrorPiecewiseNorm") +make_plot(norm, "Amplified features symetrically around \n" + "0 with MirrorPiecewiseNorm") # Amplifying features near 0.6 with MirrorPiecewiseNorm norm = colors.MirrorPiecewiseNorm(fpos='crt', fneg='crt', center_cm=0.35, center_data=0.6) -makePlot(norm, "Amplifying positive and negative features\n" - "standing on 0.6 with MirrorPiecewiseNorm") +make_plot(norm, "Amplifying positive and negative features\n" + "standing on 0.6 with MirrorPiecewiseNorm") # Amplifying features near both -0.4 and near 1.2 with PiecewiseNorm norm = colors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], refpoints_cm=[0.25, 0.5, 0.75], refpoints_data=[-0.4, 1, 1.2]) -makePlot(norm, "Amplifying positive and negative features standing\n" - " on -0.4 and 1.2 with PiecewiseNorm") +make_plot(norm, "Amplifying positive and negative features standing\n" + " on -0.4 and 1.2 with PiecewiseNorm") # Amplifying features near both -1, -0.2 and near 1.2 with PiecewiseNorm norm = colors.PiecewiseNorm(flist=['crt', 'crt', 'crt'], refpoints_cm=[0.4, 0.7], refpoints_data=[-0.2, 1.2]) -makePlot(norm, "Amplifying only positive features standing on -1, -0.2\n" - " and 1.2 with PiecewiseNorm") +make_plot(norm, "Amplifying only positive features standing on -1, -0.2\n" + " and 1.2 with PiecewiseNorm") plt.show() diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 75ac09e2a044..637f778d7912 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1168,7 +1168,9 @@ def _round_ticks(ticks, permanenttick): class PiecewiseNorm(FuncNorm): """ - Normalization allowing the definition of any linear or non-linear + Normalization defined as a piecewise function + + It allows the definition of any linear or non-linear function for different ranges of the colorbar. """ @@ -1193,7 +1195,7 @@ def __init__(self, flist, 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 + 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 @@ -1201,17 +1203,18 @@ def __init__(self, flist, `flist`. It must satisfy `len(flist)==len(finvlist)`. refpoints_cm, refpoints_data : list or array of scalars Reference points for the colorbar ranges which will go as - `[0., refpoints_cm[0]]`,... , `[refpoints_cm[i], - refpoints_cm[i+1]]`, `[refpoints_cm[-1], 0.]`, and for the data - ranges which will go as `[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. + `[0., refpoints_cm[0]]`,... , + `[refpoints_cm[i], refpoints_cm[i+1]]`, + `[refpoints_cm[-1], 0.]`, and for the data + ranges which will go as `[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 From b9dafb0c53ec339163365e92a0ab8f7564030542 Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Mon, 24 Oct 2016 00:48:44 +0100 Subject: [PATCH 20/33] Added example in `FuncNorm` docstring --- .../colormap_normalizations_piecewisenorm.py | 2 +- lib/matplotlib/colors.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py b/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py index 69e94efdbcba..f8a5b3e51007 100644 --- a/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py +++ b/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py @@ -65,7 +65,7 @@ def make_3dplot(label=''): finv=lambda x: 10.**(x), vmin=0.01, vmax=2) make_plot(norm, "Log normalization using FuncNorm") # The same can be achived with -# norm = colors.FuncNorm(f='log',vmin=0.01,vmax=2) +# norm = colors.FuncNorm(f='log', vmin=0.01, vmax=2) # Example of root normalization using FuncNorm norm = colors.FuncNorm(f='sqrt', vmin=0.0, vmax=2) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 637f778d7912..51338b2db935 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -985,6 +985,20 @@ def __init__(self, f, finv=None, **normalize_kw): normalize_kw : dict, optional Dict with keywords passed to `matplotlib.colors.Normalize`. + Examples + -------- + Creating a logarithmic normalization using the predefined strings: + + >>> import matplotlib.colors as colors + >>> norm = colors.FuncNorm(f='log', 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) + """ f, finv = FuncNorm._fun_parser([f, finv]) From d10be73f56bfc20a93f9b0dbf706134cf0085415 Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Mon, 24 Oct 2016 22:08:24 +0100 Subject: [PATCH 21/33] Finished with the examples in the docstrings --- .../colormap_normalizations_piecewisenorm.py | 3 +- lib/matplotlib/colors.py | 63 +++++++++++++++++-- 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py b/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py index f8a5b3e51007..a2d3f1e190b5 100644 --- a/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py +++ b/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py @@ -91,7 +91,8 @@ def make_3dplot(label=''): make_plot(norm, "Amplifying positive and negative features standing\n" " on -0.4 and 1.2 with PiecewiseNorm") -# Amplifying features near both -1, -0.2 and near 1.2 with PiecewiseNorm +# Amplifying positive features near -1, -0.2 and 1.2 simultaneously with +# PiecewiseNorm norm = colors.PiecewiseNorm(flist=['crt', 'crt', 'crt'], refpoints_cm=[0.4, 0.7], refpoints_data=[-0.2, 1.2]) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 51338b2db935..fdfda73bad22 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -983,7 +983,8 @@ def __init__(self, f, finv=None, **normalize_kw): 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 passed to `matplotlib.colors.Normalize`. + Dict with keywords (`vmin`,`vmax`,`clip`) passed + to `matplotlib.colors.Normalize`. Examples -------- @@ -1232,7 +1233,18 @@ def __init__(self, flist, The final normalization will meet: `norm(refpoints_data[i])==refpoints_cm[i]`. normalize_kw : dict, optional - Dict with keywords passed to `matplotlib.colors.Normalize`. + 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', 'crt', 'cubic', 'crt'], + >>> refpoints_cm=[0.25, 0.5, 0.75], + >>> refpoints_data=[-0.4, 1, 1.2]) """ @@ -1450,7 +1462,22 @@ def __init__(self, Must be in the (0.0, 1.0) range. Default 0.5. normalize_kw : dict, optional - Dict with keywords passed to `matplotlib.colors.Normalize`. + 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='crt'): + + Obtaining an asymmetric amplification of the features around 0.6: + + >>> import matplotlib.colors as colors + >>> norm = colors.MirrorPiecewiseNorm(fpos='sqrt', fneg='crt', + >>> center_cm=0.35, + >>> center_data=0.6) """ if fneg is None and fneginv is not None: @@ -1519,7 +1546,24 @@ def __init__(self, orderpos=2, orderneg=None, in the (0.0, 1.0) range. Default 0.5. normalize_kw : dict, optional - Dict with keywords passed to `matplotlib.colors.Normalize`. + 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: @@ -1551,11 +1595,20 @@ def __init__(self, order=2, **normalize_kw): order : float or int, optional Degree of the root to be used for normalization. Default 2. normalize_kw : dict, optional - Dict with keywords passed to `matplotlib.colors.Normalize`. + 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)), From c85a14ce923d426f418db23ec930615e7874f821 Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Wed, 26 Oct 2016 22:26:38 +0100 Subject: [PATCH 22/33] Implemented clipping behavoir. Refactored _func_parser --- lib/matplotlib/colors.py | 79 +++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index fdfda73bad22..67cfdcaa2d9a 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1002,7 +1002,7 @@ def __init__(self, f, finv=None, **normalize_kw): """ - f, finv = FuncNorm._fun_parser([f, finv]) + f, finv = FuncNorm._func_parser([f, finv]) if finv is None: raise ValueError("Inverse function not provided") @@ -1044,10 +1044,23 @@ def __call__(self, value, clip=None): self._update_f(vmin, vmax) - result[result >= vmax] = vmax - result[result <= vmin] = vmin - resultnorm = (self._f(result) - self._f(vmin)) / \ - (self._f(vmax) - self._f(vmin)) + if clip: + result[result >= vmax] = vmax + result[result <= vmin] = vmin + 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) @@ -1075,32 +1088,32 @@ def inverse(self, value): return value @staticmethod - def _fun_parser(funsin): + def _func_parser(funcsin): + if hasattr(funcsin[0], '__call__'): + return funcsin + flog = 2000 - funs = [('linear', (lambda x: x), (lambda x: x)), - ('quadratic', (lambda x: x**2), (lambda x: x**(1. / 2))), - ('cubic', (lambda x: x**3), (lambda x: x**(1. / 3))), - ('sqrt', (lambda x: x**(1. / 2)), (lambda x: x**2)), - ('crt', (lambda x: x**(1. / 3)), (lambda x: x**3)), - ('log(x+1)', - (lambda x: np.log10(x * flog + 1) / np.log10(flog + 1)), - (lambda x: (10**(np.log10(flog + 1) * x) - 1) / flog)), - ('log', - (lambda x: np.log10(x)), - (lambda x: (10**(x))))] - - if isinstance(funsin[0], ("".__class__, - u"".__class__, - str("").__class__)): - funstrs = [] - for fstr, fun, inv in funs: - funstrs.append(fstr) - if funsin[0] == fstr: - return fun, inv - raise ValueError( - "the only strings recognized as functions are %s" % funstrs) - else: - return funsin + funcs = {'linear': (lambda x: x, lambda x: x), + 'quadratic': (lambda x: x**2, lambda x: x**(1. / 2)), + 'cubic': (lambda x: x**3, lambda x: x**(1. / 3)), + 'sqrt': (lambda x: x**(1. / 2), lambda x: x**2), + 'crt': (lambda x: x**(1. / 3), lambda x: x**3), + 'log(x+1)': (lambda x: (np.log10(x * flog + 1) / + np.log10(flog + 1), + lambda x: ((10**(np.log10(flog + 1) * x) - 1) / + flog))), + 'log': (lambda x: np.log10(x), + lambda x: (10**(x)))} + try: + return funcs[six.text_type(funcsin[0])] + except KeyError: + raise ValueError("%s: invalid function. The only strings " + "recognized as functions are %s." % + (funcsin[0], funcs.keys())) + except: + raise ValueError("Invalid function. The only strings recognized " + "as functions are %s." % + (funcs.keys())) @staticmethod def _fun_normalizer(fun): @@ -1281,7 +1294,7 @@ def __init__(self, flist, self._flist = [] self._finvlist = [] for i in range(len(flist)): - funs = FuncNorm._fun_parser((flist[i], finvlist[i])) + funs = FuncNorm._func_parser((flist[i], finvlist[i])) if funs[0] is None or funs[1] is None: raise ValueError( "Inverse function not provided for %i range" % i) @@ -1487,8 +1500,8 @@ def __init__(self, fneg = fpos fneginv = fposinv - fpos, fposinv = PiecewiseNorm._fun_parser([fpos, fposinv]) - fneg, fneginv = PiecewiseNorm._fun_parser([fneg, fneginv]) + fpos, fposinv = PiecewiseNorm._func_parser([fpos, fposinv]) + fneg, fneginv = PiecewiseNorm._func_parser([fneg, fneginv]) if fposinv is None: raise ValueError( From 7597ddd878ffe479048010c98eb17f27bb1e53c5 Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Fri, 28 Oct 2016 22:17:51 +0100 Subject: [PATCH 23/33] It now allows some string functions with parameters. Added a test for the func_parser, corrected a docstring, and now it uses np.clip to clip the data when necessary. --- .../colormap_normalizations_piecewisenorm.py | 2 +- lib/matplotlib/colors.py | 4 ++- lib/matplotlib/tests/test_colors.py | 34 ++++++++++++------- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py b/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py index a2d3f1e190b5..7d0cc1fe249c 100644 --- a/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py +++ b/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py @@ -65,7 +65,7 @@ def make_3dplot(label=''): finv=lambda x: 10.**(x), vmin=0.01, vmax=2) make_plot(norm, "Log normalization using FuncNorm") # The same can be achived with -# norm = colors.FuncNorm(f='log', vmin=0.01, vmax=2) +# norm = colors.FuncNorm(f='log10', vmin=0.01, vmax=2) # Example of root normalization using FuncNorm norm = colors.FuncNorm(f='sqrt', vmin=0.0, vmax=2) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 67cfdcaa2d9a..4cdb9889b12b 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1030,7 +1030,9 @@ def __call__(self, value, clip=None): Returns ------- result : masked array of floats - Normalized data in the `[0.0, 1.0]` interval. + Normalized data to the `[0.0, 1.0]` interval. If clip == False, + the values original below vmin or above vmax will be assigned to + -0.1 or 1.1, respectively """ if clip is None: diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 5bbc84fea2bb..29991fe0b834 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -158,7 +158,7 @@ def test_LogNorm(): def test_FuncNorm(): # Testing limits using a string - norm = mcolors.FuncNorm(f='log', vmin=0.01, vmax=2.) + norm = mcolors.FuncNorm(f='log10', vmin=0.01, vmax=2.) assert_array_equal(norm([0.01, 2]), [0, 1.0]) # Testing limits using a string @@ -167,33 +167,43 @@ def test_FuncNorm(): assert_array_equal(norm([0.01, 2]), [0, 1.0]) # Testing limits without vmin, vmax - norm = mcolors.FuncNorm(f='log') + norm = mcolors.FuncNorm(f='log10') assert_array_equal(norm([0.01, 2]), [0, 1.0]) # Testing limits without vmin - norm = mcolors.FuncNorm(f='log', vmax=2.) + norm = mcolors.FuncNorm(f='log10', vmax=2.) assert_array_equal(norm([0.01, 2]), [0, 1.0]) # Testing limits without vmin - norm = mcolors.FuncNorm(f='log', vmin=0.01) + norm = mcolors.FuncNorm(f='log10', vmin=0.01) assert_array_equal(norm([0.01, 2]), [0, 1.0]) # Testing intermediate values - norm = mcolors.FuncNorm(f='log') + norm = mcolors.FuncNorm(f='log10') assert_array_almost_equal(norm([0.01, 0.5, 2]), [0, 0.73835195870437, 1.0]) # Checking inverse - norm = mcolors.FuncNorm(f='log', vmin=0.01, vmax=2.) + 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))) # Checking ticks - norm = mcolors.FuncNorm(f='log', 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.] + 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) +def test_FuncNorm_func_parser(): + validstrings = ['linear', 'square', 'cubic', 'sqrt', 'crt', + 'log', 'log10', 'power{1.5}', 'root{2.5}', + 'log(x+{0.5})', 'log10(x+{0.1})'] + + x = np.linspace(0.01, 0.5, 1) + for string in validstrings: + funcs = mcolors.FuncNorm._func_parser(validstrings) + assert_array_almost_equal(funcs[1](funcs[0](x)), x) + def test_PiecewiseNorm(): # Testing using both strings and functions @@ -257,8 +267,8 @@ def test_PiecewiseNorm(): 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.] + -0.4, 1., 2.4, 2.7, 3., + 3.04, 3.3, 4.] assert_array_almost_equal(norm.ticks(), expected) From 7fce503b1cc1594d9d4b8315336a84f0293aff51 Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Fri, 28 Oct 2016 22:47:03 +0100 Subject: [PATCH 24/33] Forgot to add a file... --- lib/matplotlib/colors.py | 121 +++++++++++++++++++++++++-------------- 1 file changed, 78 insertions(+), 43 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 4cdb9889b12b..bc007b9edf92 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -977,8 +977,10 @@ def __init__(self, f, finv=None, **normalize_kw): 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','crt','log'] can be used. + Alternatively a string from the list ['linear', 'quadratic', + 'cubic', 'sqrt', 'crt','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. @@ -991,7 +993,7 @@ def __init__(self, f, finv=None, **normalize_kw): Creating a logarithmic normalization using the predefined strings: >>> import matplotlib.colors as colors - >>> norm = colors.FuncNorm(f='log', vmin=0.01, vmax=2) + >>> norm = colors.FuncNorm(f='log10', vmin=0.01, vmax=2) Or doing it manually: @@ -1032,7 +1034,7 @@ def __call__(self, value, clip=None): result : masked array of floats Normalized data to the `[0.0, 1.0]` interval. If clip == False, the values original below vmin or above vmax will be assigned to - -0.1 or 1.1, respectively + -0.1 or 1.1, respectively. """ if clip is None: @@ -1047,8 +1049,7 @@ def __call__(self, value, clip=None): self._update_f(vmin, vmax) if clip: - result[result >= vmax] = vmax - result[result <= vmin] = vmin + result = np.clip(result, vmin, vmax) resultnorm = (self._f(result) - self._f(vmin)) / \ (self._f(vmax) - self._f(vmin)) else: @@ -1090,24 +1091,58 @@ def inverse(self, value): return value @staticmethod - def _func_parser(funcsin): + def _func_parser(funcsin, onlybounded=False): if hasattr(funcsin[0], '__call__'): return funcsin + # Each element has the direct, the inverse, and and interval indicating + # wether 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), + 'crt': (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)} + + # Checking if it comes with a parameter + param = None + regex = '\{(.*?)\}' + search = re.search(regex, funcsin[0]) + 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 different than 0, " + "e.g. 'log10(x+{0.1})'.") + if param == 0: + raise ValueError("'a' in parametric function strings must be " + "replaced by a number different than 0.") + funcsin[0] = re.sub(regex, '{a}', funcsin[0]) - flog = 2000 - funcs = {'linear': (lambda x: x, lambda x: x), - 'quadratic': (lambda x: x**2, lambda x: x**(1. / 2)), - 'cubic': (lambda x: x**3, lambda x: x**(1. / 3)), - 'sqrt': (lambda x: x**(1. / 2), lambda x: x**2), - 'crt': (lambda x: x**(1. / 3), lambda x: x**3), - 'log(x+1)': (lambda x: (np.log10(x * flog + 1) / - np.log10(flog + 1), - lambda x: ((10**(np.log10(flog + 1) * x) - 1) / - flog))), - 'log': (lambda x: np.log10(x), - lambda x: (10**(x)))} try: - return funcs[six.text_type(funcsin[0])] + output = funcs[six.text_type(funcsin[0])] + if onlybounded and not output[2]: + raise ValueError("Only functions bounded in the (0, 1)" + "domain are allowed: %s" % + [key for key in funcs.keys() if funcs[key][2]] + ) + + if param is not None: + output = (lambda x, output=output: output[0](x, param), + lambda x, output=output: output[1](x, param), + output[2]) + return output[0:2] except KeyError: raise ValueError("%s: invalid function. The only strings " "recognized as functions are %s." % @@ -1126,23 +1161,23 @@ def _fun_normalizer(fun): else: return (lambda x: (fun(x) - fun(0.)) / (fun(1.) - fun(0.))) - def ticks(self, N=13): + def ticks(self, nticks=13): """ - Returns an automatic list of `N` points in the data space to be used - as ticks in the colorbar. + Returns an automatic list of `nticks` points in the data space + to be used as ticks in the colorbar. Parameters ---------- - N : integer, optional + nticks : integer, optional Number of ticks to be returned. Default 13. Returns ------- ticks : ndarray - 1d array of length `N` with the proposed tick locations. + 1d array of length `nticks` with the proposed tick locations. """ - ticks = self.inverse(np.linspace(0, 1, N)) + 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) @@ -1378,47 +1413,47 @@ def _update_f(self, vmin, vmax): self._finv = self._build_finv() return - def ticks(self, N=None): + def ticks(self, nticks=None): """ - Returns an automatic list of *N* points in the data space to be used - as ticks in the colorbar. + Returns an automatic list of `nticks` points in the data space + to be used as ticks in the colorbar. Parameters ---------- - N : integer, optional + nticks : integer, optional Number of ticks to be returned. Default 13. Returns ------- ticks : ndarray - 1d array of length *N* with the proposed tick locations. + 1d array of length `nticks` with the proposed tick locations. """ rp_cm = self._refpoints_cm widths_cm = np.diff(rp_cm) - if N is None: - N = max([13, len(rp_cm)]) + if nticks is None: + nticks = max([13, len(rp_cm)]) - if N < 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 = N - len(-rp_cm) + available_ticks = nticks - len(-rp_cm) distribution = widths_cm * (available_ticks) / widths_cm.sum() - nticks = np.floor(distribution) + nticks_each = np.floor(distribution) - while(nticks.sum() < available_ticks): - ind = np.argmax((distribution - nticks)) - nticks[ind] += 1 + while(nticks_each.sum() < available_ticks): + ind = np.argmax((distribution - nticks_each)) + nticks_each[ind] += 1 - for i in range(len(nticks)): - if nticks[i] > 0: - N = nticks[i] - auxticks = np.linspace(rp_cm[i], rp_cm[i + 1], N + 2) + 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) From bcd7dd0889dc8b7db9d3910717bb2db30d65e680 Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Fri, 28 Oct 2016 23:30:03 +0100 Subject: [PATCH 25/33] Forgot to add another file... --- lib/matplotlib/tests/test_colors.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 29991fe0b834..f6f7a43c95b1 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -194,6 +194,7 @@ def test_FuncNorm(): 0.8, 1.3, 2.] assert_array_almost_equal(norm.ticks(), expected) + def test_FuncNorm_func_parser(): validstrings = ['linear', 'square', 'cubic', 'sqrt', 'crt', 'log', 'log10', 'power{1.5}', 'root{2.5}', From a71e1e99e1a2b2207c68529bef005f7d7e991637 Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Mon, 31 Oct 2016 22:55:18 +0000 Subject: [PATCH 26/33] Improved tests, documentation, and exceptions --- lib/matplotlib/colors.py | 55 ++++- lib/matplotlib/tests/test_colors.py | 359 ++++++++++++++-------------- 2 files changed, 223 insertions(+), 191 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index bc007b9edf92..1a60db198e46 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1006,7 +1006,7 @@ def __init__(self, f, finv=None, **normalize_kw): f, finv = FuncNorm._func_parser([f, finv]) if finv is None: - raise ValueError("Inverse function not provided") + raise ValueError("Inverse function finv not provided") self._f = f self._finv = finv @@ -1133,7 +1133,7 @@ def _func_parser(funcsin, onlybounded=False): try: output = funcs[six.text_type(funcsin[0])] if onlybounded and not output[2]: - raise ValueError("Only functions bounded in the (0, 1)" + raise ValueError("Only functions bounded in the [0, 1]" "domain are allowed: %s" % [key for key in funcs.keys() if funcs[key][2]] ) @@ -1302,21 +1302,28 @@ def __init__(self, flist, finvlist = [None] * len(flist) if len(flist) != len(finvlist): - raise ValueError("len(flist) must be equal than 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( - "len(refpoints_cm) must be equal than len(flist) -1") + "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( - "len(refpoints_data) must be equal than len(refpoints_cm)") + "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( - "refpoints_cm values must be monotonically increasing " + "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( @@ -1325,7 +1332,8 @@ def __init__(self, flist, if (len(self._refpoints_data[1:-1]) > 2 and any(np.diff(self._refpoints_data[1:-1]) <= 0)): raise ValueError( - "refpoints_data values must be monotonically increasing") + "The values for the reference points for the data " + "`refpoints_data` must be monotonically increasing") # Parsing the function strings if any: self._flist = [] @@ -1355,8 +1363,9 @@ def _build_f(self): if (len(rp_d[1:-1]) > 0 and (any(rp_d[1:-1] <= vmin) or any(rp_d[1:-1] >= vmax))): raise ValueError( - "data reference points must be " - "within the (vmin,vmax) interval") + "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) @@ -1531,7 +1540,9 @@ def __init__(self, """ if fneg is None and fneginv is not None: - raise ValueError("fneginv not expected without fneg") + raise ValueError("Inverse function for the negative range " + "`fneginv`not expected without function for " + "the negative range `fneg`") if fneg is None: fneg = fpos @@ -1548,7 +1559,8 @@ def __init__(self, "Inverse function must be provided for the negative interval") if center_cm <= 0.0 or center_cm >= 1.0: - raise ValueError("center must be within the (0.0,1.0) interval") + 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]) @@ -1566,12 +1578,23 @@ def __init__(self, class MirrorRootNorm(MirrorPiecewiseNorm): """ - Root normalization for positive and negative data. + Root normalization for positive and negative data using + `MirrorPiecewiseNorm`. Data above `center_data` will be normalized with a root of the order `orderpos` and data below it with a symmetric root of order `orderg neg`. If only `orderpos` function is given, the normalization will be completely mirrored. + + `mcolors.MirrorRootNorm(orderpos=2)` is equivalent + to `colors.MirrorPiecewiseNorm(fpos='sqrt')` + + `mcolors.MirrorRootNorm(orderpos=2, orderneg=3)` is equivalent + to `colors.MirrorPiecewiseNorm(fpos='sqrt',fneg='crt')` + + `mcolors.MirrorRootNorm(orderpos=N1, orderneg=N2)` is equivalent + to `colors.MirrorPiecewiseNorm(fpos=root{N1}',fneg=root{N2}')` + """ def __init__(self, orderpos=2, orderneg=None, @@ -1630,12 +1653,18 @@ def __init__(self, orderpos=2, orderneg=None, class RootNorm(FuncNorm): """ - Simple root normalization using FuncNorm. + Simple root normalization using `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`) + `mcolors.RootNorm(order=2)` is equivalent to `colors.FuncNorm(f='sqrt')` + + `mcolors.RootNorm(order=3)` is equivalent to `colors.FuncNorm(f='crt')` + + `mcolors.RootNorm(order=N)` is equivalent to `colors.FuncNorm(f='root{N}')` + """ def __init__(self, order=2, **normalize_kw): diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index f6f7a43c95b1..a072577ec5de 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -14,6 +14,7 @@ except ImportError: assert_sequence_equal = None +import pytest import numpy as np from numpy.testing.utils import assert_array_equal, assert_array_almost_equal from nose.plugins.skip import SkipTest @@ -156,202 +157,204 @@ def test_LogNorm(): assert_array_equal(ln([1, 6]), [0, 1.0]) -def test_FuncNorm(): - # Testing limits using a string - norm = mcolors.FuncNorm(f='log10', vmin=0.01, vmax=2.) - assert_array_equal(norm([0.01, 2]), [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]) - # Testing limits using a string - 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_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]) - # Testing limits without vmin, vmax - norm = mcolors.FuncNorm(f='log10') - 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]) - # Testing limits without vmin - norm = mcolors.FuncNorm(f='log10', vmax=2.) - 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]) - # Testing limits without vmin - norm = mcolors.FuncNorm(f='log10', vmin=0.01) - 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]) - # Testing intermediate values - norm = mcolors.FuncNorm(f='log10') - assert_array_almost_equal(norm([0.01, 0.5, 2]), [0, 0.73835195870437, 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]) - # Checking inverse - 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_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))) - # Checking ticks - 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) + 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) - -def test_FuncNorm_func_parser(): - validstrings = ['linear', 'square', 'cubic', 'sqrt', 'crt', + validstrings = ['linear', 'quadratic', 'cubic', 'sqrt', 'crt', 'log', 'log10', 'power{1.5}', 'root{2.5}', 'log(x+{0.5})', 'log10(x+{0.1})'] - x = np.linspace(0.01, 0.5, 1) - for string in validstrings: - funcs = mcolors.FuncNorm._func_parser(validstrings) + @pytest.mark.parametrize("string", validstrings, ids=validstrings) + def test_func_parser(self, string): + x = np.linspace(0.01, 0.5, 1) + funcs = mcolors.FuncNorm._func_parser([string, None]) assert_array_almost_equal(funcs[1](funcs[0](x)), x) -def test_PiecewiseNorm(): - # Testing using both strings and functions - norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', lambda x:x**3, 'crt'], - 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]) - - # Testing using only strings - norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], - 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]) - - # Testing with limits - norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], - 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]) - - # Testing with vmin - norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], - 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]) - - # Testing with vmax - norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], - 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]) - - # Testing intermediate values - norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], - 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) - - # Checking inverse - norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], - 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))) - - # Checking ticks - norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], - 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) - - -def test_MirrorPiecewiseNorm(): +class TestPiecewiseNorm(object): + def test_strings_and_funcs(self): + norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', + lambda x:x**3, 'crt'], + 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', 'crt', 'cubic', 'crt'], + 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', 'crt', 'cubic', 'crt'], + 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', 'crt', 'cubic', 'crt'], + 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', 'crt', 'cubic', 'crt'], + 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', 'crt', 'cubic', 'crt'], + 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', 'crt', 'cubic', 'crt'], + 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', 'crt', 'cubic', 'crt'], + 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 - - # Single argument positive range, defaults - norm = mcolors.MirrorPiecewiseNorm(fpos='crt') - assert_array_equal(norm([-2, 0., 1]), [0., 0.5, 1.0]) - - # Single argument positive, and negative range, with ref point - norm = mcolors.MirrorPiecewiseNorm(fpos='crt', fneg='sqrt', - center_cm=0.35, - center_data=0.6) - assert_array_equal(norm([-2, 0.6, 1]), [0., 0.35, 1.0]) - - # Single argument positive, and negative range, with ref point - norm = mcolors.MirrorPiecewiseNorm(fpos='crt', fneg='sqrt', - center_data=0.6) - assert_array_equal(norm([-2, 0.6, 1]), [0., 0.5, 1.0]) - - # Providing lambda function - 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]) - - # Providing lambda function and negative function string - norm = mcolors.MirrorPiecewiseNorm(fpos=lambda x: x**2, - fposinv=lambda x: x**0.5, - fneg='crt') - assert_array_equal(norm([-2, 0., 1]), [0., 0.5, 1.0]) - - # Testing intermediate values - norm = mcolors.MirrorPiecewiseNorm(fpos=lambda x: x**2, - fposinv=lambda x: x**0.5, - fneg='crt') - expected = [0., - 0.1606978, - 0.52295918, - 0.68431122, - 1.] - assert_array_almost_equal(norm(np.linspace(-2, 3.5, 5)), expected) - - -def test_MirrorRootNorm(): + def test_defaults_only_fpos(self): + norm = mcolors.MirrorPiecewiseNorm(fpos='crt') + assert_array_equal(norm([-2, 0., 1]), [0., 0.5, 1.0]) + + def test_fposfneg_refpoint_data_cm(self): + norm = mcolors.MirrorPiecewiseNorm(fpos='crt', 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='crt', 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='crt') + 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='crt') + 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 - norm = mcolors.MirrorRootNorm(orderpos=2) - assert_array_equal(norm([-2, 0., 1]), [0., 0.5, 1.0]) - - # Checking that the order is copied by default - 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)) - - # Checking intermediate values - 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) - - -def test_RootNorm(): + 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 - - 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_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(): From 33f57d1094fe7f9130a2b7c84e82ae796027c7f0 Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Mon, 31 Oct 2016 23:24:11 +0000 Subject: [PATCH 27/33] Removed test_colors.py from __init__.py after including parametrize --- lib/matplotlib/__init__.py | 1 - lib/matplotlib/colors.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index c2939ff18a04..8b2643c47e3e 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1484,7 +1484,6 @@ def _jupyter_nbextension_paths(): 'matplotlib.tests.test_coding_standards', 'matplotlib.tests.test_collections', 'matplotlib.tests.test_colorbar', - 'matplotlib.tests.test_colors', 'matplotlib.tests.test_compare_images', 'matplotlib.tests.test_container', 'matplotlib.tests.test_contour', diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 1a60db198e46..f9f582ab3473 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1590,10 +1590,10 @@ class MirrorRootNorm(MirrorPiecewiseNorm): to `colors.MirrorPiecewiseNorm(fpos='sqrt')` `mcolors.MirrorRootNorm(orderpos=2, orderneg=3)` is equivalent - to `colors.MirrorPiecewiseNorm(fpos='sqrt',fneg='crt')` + to `colors.MirrorPiecewiseNorm(fpos='sqrt', fneg='crt')` `mcolors.MirrorRootNorm(orderpos=N1, orderneg=N2)` is equivalent - to `colors.MirrorPiecewiseNorm(fpos=root{N1}',fneg=root{N2}')` + to `colors.MirrorPiecewiseNorm(fpos=root{N1}', fneg=root{N2}')` """ From 96871733250e10df87766bb9d8a496b4dc62ef5c Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Tue, 1 Nov 2016 23:05:14 +0000 Subject: [PATCH 28/33] Moved the string function parser to its own class in cbook. Added tests. Since now parametrized is not used in test_colors.py anymore, the file has been readded in __init__.py for testing with nose --- lib/matplotlib/__init__.py | 1 + lib/matplotlib/cbook.py | 77 ++++++++++++++ lib/matplotlib/colors.py | 150 +++++++++++----------------- lib/matplotlib/tests/test_cbook.py | 33 ++++++ lib/matplotlib/tests/test_colors.py | 11 -- 5 files changed, 172 insertions(+), 100 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 8b2643c47e3e..c2939ff18a04 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1484,6 +1484,7 @@ def _jupyter_nbextension_paths(): 'matplotlib.tests.test_coding_standards', 'matplotlib.tests.test_collections', 'matplotlib.tests.test_colorbar', + 'matplotlib.tests.test_colors', 'matplotlib.tests.test_compare_images', 'matplotlib.tests.test_container', 'matplotlib.tests.test_contour', diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 80e1b3146216..ff6cf2e996ce 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2696,3 +2696,80 @@ 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), + 'crt': (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 different than 0, " + "e.g. 'log10(x+{0.1})'.") + if param == 0: + raise ValueError("'a' in parametric function strings must be " + "replaced by a number different than 0.") + 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 f9f582ab3473..d48292407f55 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -968,7 +968,7 @@ class FuncNorm(Normalize): def __init__(self, f, finv=None, **normalize_kw): """ - Specify the function to be used (and its inverse), as well as other + 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)). @@ -976,7 +976,7 @@ def __init__(self, f, finv=None, **normalize_kw): ---------- f : callable or string Function to be used for the normalization receiving a single - parameter, compatible with scalar values, and ndarrays. + parameter, compatible with scalar values and ndarrays. Alternatively a string from the list ['linear', 'quadratic', 'cubic', 'sqrt', 'crt','log', 'log10', 'power{a}', 'root{a}', 'log(x+{a})', 'log10(x+{a})'] can be used, replacing 'a' by a @@ -1004,7 +1004,11 @@ def __init__(self, f, finv=None, **normalize_kw): """ - f, finv = FuncNorm._func_parser([f, finv]) + 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") @@ -1033,8 +1037,8 @@ def __call__(self, value, clip=None): ------- result : masked array of floats Normalized data to the `[0.0, 1.0]` interval. If clip == False, - the values original below vmin or above vmax will be assigned to - -0.1 or 1.1, respectively. + the values original below vmin and above vmax will be assigned to + -0.1 and 1.1, respectively. """ if clip is None: @@ -1090,68 +1094,6 @@ def inverse(self, value): value * (self._f(vmax) - self._f(vmin)) + self._f(vmin)) return value - @staticmethod - def _func_parser(funcsin, onlybounded=False): - if hasattr(funcsin[0], '__call__'): - return funcsin - # Each element has the direct, the inverse, and and interval indicating - # wether 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), - 'crt': (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)} - - # Checking if it comes with a parameter - param = None - regex = '\{(.*?)\}' - search = re.search(regex, funcsin[0]) - 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 different than 0, " - "e.g. 'log10(x+{0.1})'.") - if param == 0: - raise ValueError("'a' in parametric function strings must be " - "replaced by a number different than 0.") - funcsin[0] = re.sub(regex, '{a}', funcsin[0]) - - try: - output = funcs[six.text_type(funcsin[0])] - if onlybounded and not output[2]: - raise ValueError("Only functions bounded in the [0, 1]" - "domain are allowed: %s" % - [key for key in funcs.keys() if funcs[key][2]] - ) - - if param is not None: - output = (lambda x, output=output: output[0](x, param), - lambda x, output=output: output[1](x, param), - output[2]) - return output[0:2] - except KeyError: - raise ValueError("%s: invalid function. The only strings " - "recognized as functions are %s." % - (funcsin[0], funcs.keys())) - except: - raise ValueError("Invalid function. The only strings recognized " - "as functions are %s." % - (funcs.keys())) - @staticmethod def _fun_normalizer(fun): if fun(0.) == 0. and fun(1.) == 1.: @@ -1246,10 +1188,10 @@ def __init__(self, flist, refpoints_cm=[None], **normalize_kw): """ - Specify a series of functions, as well as intervals to map the data + 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 they will be normalized as - fnorm(x)=(f(x)-f(0))/(f(1)-f(0)), to guarantee that fnorm(0)=0 and + 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 a different data range [d0, d1] into colormap ranges [cm0, cm1] as ftrans=fnorm((x-d0)/(d1-d0))*(cm1-cm0)+cm0. @@ -1270,8 +1212,9 @@ def __init__(self, flist, Reference points for the colorbar ranges which will go as `[0., refpoints_cm[0]]`,... , `[refpoints_cm[i], refpoints_cm[i+1]]`, - `[refpoints_cm[-1], 0.]`, and for the data - ranges which will go as `[self.vmin, refpoints_data[0]]`,... , + `[refpoints_cm[-1], 0.]`, + and for the data ranges which will go as + `[self.vmin, refpoints_data[0]]`,... , `[refpoints_data[i], refpoints_data[i+1]]`, `[refpoints_cm[-1], self.vmax]` It must satisfy @@ -1335,17 +1278,30 @@ def __init__(self, flist, "The values for the reference points for the data " "`refpoints_data` must be monotonically increasing") - # Parsing the function strings if any: self._flist = [] self._finvlist = [] for i in range(len(flist)): - funs = FuncNorm._func_parser((flist[i], finvlist[i])) - if funs[0] is None or funs[1] is None: + 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(funs[0])) - self._finvlist.append(FuncNorm._fun_normalizer(funs[1])) + 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 @@ -1479,10 +1435,10 @@ def ticks(self, nticks=None): class MirrorPiecewiseNorm(PiecewiseNorm): """ - Normalization alowing a dual `PiecewiseNorm` simmetrically around a point. + Normalization allowing a dual `PiecewiseNorm` symmetrically around a point. Data above `center_data` will be normalized independently that data below - it. If only one function is give, the normalization will be symmetric + it. If only one function is given, the normalization will be symmetric around that point. """ @@ -1492,13 +1448,8 @@ def __init__(self, center_data=0.0, center_cm=.5, **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 they 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 a different - data range [d0, d1] into colormap ranges [cm0, cm1] as - ftrans=fnorm((x-d0)/(d1-d0))*(cm1-cm0)+cm0. + Specify two functions to normalize the data above and below a + provided reference point. Parameters ---------- @@ -1548,8 +1499,22 @@ def __init__(self, fneg = fpos fneginv = fposinv - fpos, fposinv = PiecewiseNorm._func_parser([fpos, fposinv]) - fneg, fneginv = PiecewiseNorm._func_parser([fneg, fneginv]) + 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( @@ -1565,6 +1530,13 @@ def __init__(self, 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] @@ -1582,7 +1554,7 @@ class MirrorRootNorm(MirrorPiecewiseNorm): `MirrorPiecewiseNorm`. Data above `center_data` will be normalized with a root of the order - `orderpos` and data below it with a symmetric root of order `orderg neg`. + `orderpos` and data below it with a symmetric root of order `orderneg`. If only `orderpos` function is given, the normalization will be completely mirrored. @@ -1611,7 +1583,7 @@ def __init__(self, orderpos=2, orderneg=None, below `center_data` using `MirrorPiecewiseNorm`. Default `orderpos`. center_data : float, optional - Value in the data that will separate range. Must be + Value in the data that will separate the ranges. Must be in the (vmin, vmax) range. Default 0.0. center_cm : float, optional diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index a8017a98f1dd..1ff028066d99 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', 'crt', + '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 a072577ec5de..6af3391e6a41 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -14,7 +14,6 @@ except ImportError: assert_sequence_equal = None -import pytest import numpy as np from numpy.testing.utils import assert_array_equal, assert_array_almost_equal from nose.plugins.skip import SkipTest @@ -197,16 +196,6 @@ def test_ticks(self): 0.8, 1.3, 2.] assert_array_almost_equal(norm.ticks(), expected) - validstrings = ['linear', 'quadratic', 'cubic', 'sqrt', 'crt', - 'log', 'log10', 'power{1.5}', 'root{2.5}', - 'log(x+{0.5})', 'log10(x+{0.1})'] - - @pytest.mark.parametrize("string", validstrings, ids=validstrings) - def test_func_parser(self, string): - x = np.linspace(0.01, 0.5, 1) - funcs = mcolors.FuncNorm._func_parser([string, None]) - assert_array_almost_equal(funcs[1](funcs[0](x)), x) - class TestPiecewiseNorm(object): def test_strings_and_funcs(self): From 46395aabbaff456e39b601067d21658c64ac3ac0 Mon Sep 17 00:00:00 2001 From: alvarosg Date: Wed, 2 Nov 2016 17:48:34 +0000 Subject: [PATCH 29/33] Improved documentation --- lib/matplotlib/cbook.py | 7 +-- lib/matplotlib/colors.py | 95 ++++++++++++++++++++++++---------------- 2 files changed, 61 insertions(+), 41 deletions(-) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index ff6cf2e996ce..bbb264a47a8d 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2752,11 +2752,12 @@ def _get_element(self, ind): param = float(parstring) except: raise ValueError("'a' in parametric function strings must be " - "replaced by a number different than 0, " - "e.g. 'log10(x+{0.1})'.") + "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 different than 0.") + "replaced by a number that is not " + "zero.") str_func = re.sub(regex, '{a}', str_func) try: diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index d48292407f55..0cd25f84a2a0 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1010,7 +1010,7 @@ def __init__(self, f, finv=None, **normalize_kw): finv = func_parser.get_invfunc() if finv is None: - raise ValueError("Inverse function finv not provided") + raise ValueError("Inverse function `finv` not provided") self._f = f self._finv = finv @@ -1037,8 +1037,8 @@ def __call__(self, value, clip=None): ------- result : masked array of floats Normalized data to the `[0.0, 1.0]` interval. If clip == False, - the values original below vmin and above vmax will be assigned to - -0.1 and 1.1, respectively. + values smaller than vmin or greater than vmax will be clipped to + -0.1 and 1.1 respectively. """ if clip is None: @@ -1177,8 +1177,8 @@ class PiecewiseNorm(FuncNorm): """ Normalization defined as a piecewise function - It allows the definition of any linear or non-linear - function for different ranges of the colorbar. + It allows the definition of different linear or non-linear + functions for different ranges of the colorbar. """ @@ -1190,11 +1190,11 @@ def __init__(self, flist, """ 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 they will be normalized as + [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 a different - data range [d0, d1] into colormap ranges [cm0, cm1] as - ftrans=fnorm((x-d0)/(d1-d0))*(cm1-cm0)+cm0. + 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 ---------- @@ -1209,11 +1209,12 @@ def __init__(self, flist, 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 - Reference points for the colorbar ranges which will go as + 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 for the data ranges which will go as + and the data ranges will be: `[self.vmin, refpoints_data[0]]`,... , `[refpoints_data[i], refpoints_data[i+1]]`, `[refpoints_cm[-1], self.vmax]` @@ -1435,11 +1436,29 @@ def ticks(self, nticks=None): class MirrorPiecewiseNorm(PiecewiseNorm): """ - Normalization allowing a dual `PiecewiseNorm` symmetrically around a point. + 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. - Data above `center_data` will be normalized independently that data below - it. If only one function is given, the normalization will be symmetric - around that point. """ def __init__(self, @@ -1448,9 +1467,6 @@ def __init__(self, center_data=0.0, center_cm=.5, **normalize_kw): """ - Specify two functions to normalize the data above and below a - provided reference point. - Parameters ---------- fpos, fposinv : callable or string @@ -1465,11 +1481,11 @@ def __init__(self, i.e. the actual normalization passed to `PiecewiseNorm` will be (-fneg(-x + 1) + 1)). Default `fneg`=`fpos`. center_data : float, optional - Value in the data that will separate the use of `fneg` and `fpos`. + 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 to `center_data`. - Must be in the (0.0, 1.0) range. + 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 @@ -1550,21 +1566,25 @@ def __init__(self, class MirrorRootNorm(MirrorPiecewiseNorm): """ - Root normalization for positive and negative data using - `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 data below it with a symmetric root of order `orderneg`. - If only `orderpos` function is given, the normalization will be completely - mirrored. + `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). - `mcolors.MirrorRootNorm(orderpos=2)` is equivalent + `colors.MirrorRootNorm(orderpos=2)` is equivalent to `colors.MirrorPiecewiseNorm(fpos='sqrt')` - `mcolors.MirrorRootNorm(orderpos=2, orderneg=3)` is equivalent + `colors.MirrorRootNorm(orderpos=2, orderneg=3)` is equivalent to `colors.MirrorPiecewiseNorm(fpos='sqrt', fneg='crt')` - `mcolors.MirrorRootNorm(orderpos=N1, orderneg=N2)` is equivalent + `colors.MirrorRootNorm(orderpos=N1, orderneg=N2)` is equivalent to `colors.MirrorPiecewiseNorm(fpos=root{N1}', fneg=root{N2}')` """ @@ -1583,12 +1603,11 @@ def __init__(self, orderpos=2, orderneg=None, below `center_data` using `MirrorPiecewiseNorm`. Default `orderpos`. center_data : float, optional - Value in the data that will separate the ranges. Must be - in the (vmin, vmax) range. - Default 0.0. + 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`. Must be - in the (0.0, 1.0) range. + 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 @@ -1625,17 +1644,17 @@ def __init__(self, orderpos=2, orderneg=None, class RootNorm(FuncNorm): """ - Simple root normalization using `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`) - `mcolors.RootNorm(order=2)` is equivalent to `colors.FuncNorm(f='sqrt')` + `colors.RootNorm(order=2)` is equivalent to `colors.FuncNorm(f='sqrt')` - `mcolors.RootNorm(order=3)` is equivalent to `colors.FuncNorm(f='crt')` + `colors.RootNorm(order=3)` is equivalent to `colors.FuncNorm(f='crt')` - `mcolors.RootNorm(order=N)` is equivalent to `colors.FuncNorm(f='root{N}')` + `colors.RootNorm(order=N)` is equivalent to `colors.FuncNorm(f='root{N}')` """ From 63dab61f6a0a4398c8eeaef7960feb96baf9e8ea Mon Sep 17 00:00:00 2001 From: alvarosg Date: Wed, 2 Nov 2016 18:47:36 +0000 Subject: [PATCH 30/33] Added new example --- .../colormap_normalizations_funcnorm.py | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 examples/colormap_normalization/colormap_normalizations_funcnorm.py diff --git a/examples/colormap_normalization/colormap_normalizations_funcnorm.py b/examples/colormap_normalization/colormap_normalizations_funcnorm.py new file mode 100644 index 000000000000..317ee9b4c1eb --- /dev/null +++ b/examples/colormap_normalization/colormap_normalizations_funcnorm.py @@ -0,0 +1,86 @@ +""" +===================================================================== +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, 2]}, figsize=plt.figaspect(0.6)) + + cax = make_plot(None, 'Regular linear scale', fig, ax11, ax12) + fig.colorbar(cax, format='%.3g', ax=ax12) + + # Example of logarithm normalization using FuncNorm + norm = colors.FuncNorm(f=np.log10, + finv=lambda x: 10.**(x), vmin=0.01) + cax = make_plot(norm, 'Log normalization using FuncNorm', 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='log10', vmin=0.01) + + # Example of root normalization using FuncNorm + norm = colors.FuncNorm(f=lambda x: x**0.5, + finv=lambda x: x**2, vmin=0.0) + cax = make_plot(norm, 'Root normalization using FuncNorm', 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='sqrt', vmin=0.0) + # or with + # norm = colors.FuncNorm(f='root{2}', vmin=0.) + + fig.subplots_adjust(hspace=0.4, wspace=0.15) + plt.show() + + +def make_plot(norm, label, fig, ax1, ax2): + X, Y, data = get_data() + cax = ax2.imshow(data, cmap=cm.gray, 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() From 8abf2c2bb193e311f0e693b4df4508a2ef359a8e Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Thu, 3 Nov 2016 22:15:41 +0000 Subject: [PATCH 31/33] Added example for PiecewiseNorm, and MirrorPiecewiseNorm. String in the func_parser changed from crt to cbrt, to match common nomenclature --- .../colormap_normalizations_funcnorm.py | 11 +- ...rmap_normalizations_mirrorpiecewisenorm.py | 95 +++++++++++ .../colormap_normalizations_piecewisenorm.py | 158 ++++++++---------- lib/matplotlib/cbook.py | 2 +- lib/matplotlib/colors.py | 12 +- lib/matplotlib/tests/test_cbook.py | 2 +- lib/matplotlib/tests/test_colors.py | 28 ++-- 7 files changed, 197 insertions(+), 111 deletions(-) create mode 100644 examples/colormap_normalization/colormap_normalizations_mirrorpiecewisenorm.py diff --git a/examples/colormap_normalization/colormap_normalizations_funcnorm.py b/examples/colormap_normalization/colormap_normalizations_funcnorm.py index 317ee9b4c1eb..84af3106b8e4 100644 --- a/examples/colormap_normalization/colormap_normalizations_funcnorm.py +++ b/examples/colormap_normalization/colormap_normalizations_funcnorm.py @@ -20,7 +20,7 @@ def main(): fig, ((ax11, ax12), (ax21, ax22), (ax31, ax32)) = plt.subplots(3, 2, gridspec_kw={ - 'width_ratios': [1, 2]}, figsize=plt.figaspect(0.6)) + '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) @@ -28,7 +28,7 @@ def main(): # Example of logarithm normalization using FuncNorm norm = colors.FuncNorm(f=np.log10, finv=lambda x: 10.**(x), vmin=0.01) - cax = make_plot(norm, 'Log normalization using FuncNorm', fig, ax21, ax22) + 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='log10', vmin=0.01) @@ -36,7 +36,7 @@ def main(): # Example of root normalization using FuncNorm norm = colors.FuncNorm(f=lambda x: x**0.5, finv=lambda x: x**2, vmin=0.0) - cax = make_plot(norm, 'Root normalization using FuncNorm', fig, ax31, ax32) + 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='sqrt', vmin=0.0) @@ -44,12 +44,13 @@ def main(): # norm = colors.FuncNorm(f='root{2}', vmin=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.gray, norm=norm) + 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) @@ -75,7 +76,7 @@ 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 += gauss2d(X, Y, x, x, 0, 0.25 / N, 0.25) data = data - data.min() data = data / data.max() diff --git a/examples/colormap_normalization/colormap_normalizations_mirrorpiecewisenorm.py b/examples/colormap_normalization/colormap_normalizations_mirrorpiecewisenorm.py new file mode 100644 index 000000000000..32c31804a3cf --- /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) + + # Example of symmetric root normalization using MirrorPiecewiseNorm + norm = colors.MirrorPiecewiseNorm(fpos=lambda x: x**(1 / 3.), + fposinv=lambda x: x**3) + 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='cbrt') + # or with + # norm = colors.MirrorPiecewiseNorm(fpos='root{3}') + + # Example of asymmetric root normalization using MirrorPiecewiseNorm + norm = colors.MirrorPiecewiseNorm(fpos=lambda x: x**(1 / 3.), + fposinv=lambda x: x**3, + fneg=lambda x: x, + fneginv=lambda x: x) + 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='cbrt', fneg='linear') + # or with + # norm = colors.MirrorPiecewiseNorm(fpos='root{3}', fneg='linear') + + 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_piecewisenorm.py b/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py index 7d0cc1fe249c..65dfaaf01834 100644 --- a/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py +++ b/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py @@ -1,103 +1,93 @@ """ -============================================ -Examples of arbitrary colormap normalization -============================================ +========================================================================= +Examples of normalization using :class:`~matplotlib.colors.PiecewiseNorm` +========================================================================= -Here I plot an image array with data spanning for a large dynamic range, -using different normalizations. Look at how each of them enhances -different features. +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 -from mpl_toolkits.mplot3d import Axes3D - import numpy as np -from sampledata import PiecewiseNormData -X, Y, data = PiecewiseNormData() -cmap = cm.spectral +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) + + # 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 -# Creating functions for plotting +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) -def make_plot(norm, label=''): - fig, (ax1, ax2) = plt.subplots(1, 2, gridspec_kw={ - 'width_ratios': [1, 2]}, figsize=plt.figaspect(0.5)) - fig.subplots_adjust(top=0.87, left=0.07, right=0.96) - fig.suptitle(label) + data = np.zeros(X.shape) - cax = ax2.pcolormesh(X, Y, data, cmap=cmap, norm=norm) - ticks = cax.norm.ticks() if norm else None - fig.colorbar(cax, format='%.3g', ticks=ticks) - ax2.set_xlim(X.min(), X.max()) - ax2.set_ylim(Y.min(), Y.max()) + 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_values = np.linspace(cax.norm.vmin, cax.norm.vmax, 100) - cm_values = cax.norm(data_values) - ax1.plot(data_values, cm_values) - ax1.set_xlabel('Data values') - ax1.set_ylabel('Colormap values') + 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] -def make_3dplot(label=''): - fig = plt.figure() - fig.suptitle(label) - ax = fig.gca(projection='3d') - cax = ax.plot_surface(X, Y, data, rstride=1, cstride=1, - cmap=cmap, linewidth=0, antialiased=False) - ax.set_zlim(data.min(), data.max()) - fig.colorbar(cax, shrink=0.5, aspect=5) - ax.view_init(20, 225) - - -# Showing how the data looks in linear scale -make_3dplot('Regular linear scale') -make_plot(None, 'Regular linear scale') - -# Example of logarithm normalization using FuncNorm -norm = colors.FuncNorm(f=lambda x: np.log10(x), - finv=lambda x: 10.**(x), vmin=0.01, vmax=2) -make_plot(norm, "Log normalization using FuncNorm") -# The same can be achived with -# norm = colors.FuncNorm(f='log10', vmin=0.01, vmax=2) - -# Example of root normalization using FuncNorm -norm = colors.FuncNorm(f='sqrt', vmin=0.0, vmax=2) -make_plot(norm, "Root normalization using FuncNorm") - -# Performing a symmetric amplification of the features around 0 -norm = colors.MirrorPiecewiseNorm(fpos='crt') -make_plot(norm, "Amplified features symetrically around \n" - "0 with MirrorPiecewiseNorm") - - -# Amplifying features near 0.6 with MirrorPiecewiseNorm -norm = colors.MirrorPiecewiseNorm(fpos='crt', fneg='crt', - center_cm=0.35, - center_data=0.6) -make_plot(norm, "Amplifying positive and negative features\n" - "standing on 0.6 with MirrorPiecewiseNorm") - -# Amplifying features near both -0.4 and near 1.2 with PiecewiseNorm -norm = colors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], - refpoints_cm=[0.25, 0.5, 0.75], - refpoints_data=[-0.4, 1, 1.2]) -make_plot(norm, "Amplifying positive and negative features standing\n" - " on -0.4 and 1.2 with PiecewiseNorm") - -# Amplifying positive features near -1, -0.2 and 1.2 simultaneously with -# PiecewiseNorm -norm = colors.PiecewiseNorm(flist=['crt', 'crt', 'crt'], - refpoints_cm=[0.4, 0.7], - refpoints_data=[-0.2, 1.2]) -make_plot(norm, "Amplifying only positive features standing on -1, -0.2\n" - " and 1.2 with PiecewiseNorm") - - -plt.show() +main() diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index bbb264a47a8d..d50b5aff7fec 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2709,7 +2709,7 @@ class _StringFuncParser(object): '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), - 'crt': (lambda x: x**(1. / 3), lambda x: x**3, 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, diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 0cd25f84a2a0..809d99a2f451 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -978,7 +978,7 @@ def __init__(self, f, finv=None, **normalize_kw): 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', 'crt','log', 'log10', 'power{a}', 'root{a}', + '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 @@ -1236,7 +1236,7 @@ def __init__(self, flist, 1.2 using four intervals: >>> import matplotlib.colors as colors - >>> norm = colors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], + >>> norm = colors.PiecewiseNorm(flist=['cubic', 'cbrt', 'cubic', 'cbrt'], >>> refpoints_cm=[0.25, 0.5, 0.75], >>> refpoints_data=[-0.4, 1, 1.2]) @@ -1496,12 +1496,12 @@ def __init__(self, Obtaining a symmetric amplification of the features around 0: >>> import matplotlib.colors as colors - >>> norm = colors.MirrorPiecewiseNorm(fpos='crt'): + >>> 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='crt', + >>> norm = colors.MirrorPiecewiseNorm(fpos='sqrt', fneg='cbrt', >>> center_cm=0.35, >>> center_data=0.6) @@ -1582,7 +1582,7 @@ class MirrorRootNorm(MirrorPiecewiseNorm): to `colors.MirrorPiecewiseNorm(fpos='sqrt')` `colors.MirrorRootNorm(orderpos=2, orderneg=3)` is equivalent - to `colors.MirrorPiecewiseNorm(fpos='sqrt', fneg='crt')` + to `colors.MirrorPiecewiseNorm(fpos='sqrt', fneg='cbrt')` `colors.MirrorRootNorm(orderpos=N1, orderneg=N2)` is equivalent to `colors.MirrorPiecewiseNorm(fpos=root{N1}', fneg=root{N2}')` @@ -1652,7 +1652,7 @@ class RootNorm(FuncNorm): `colors.RootNorm(order=2)` is equivalent to `colors.FuncNorm(f='sqrt')` - `colors.RootNorm(order=3)` is equivalent to `colors.FuncNorm(f='crt')` + `colors.RootNorm(order=3)` is equivalent to `colors.FuncNorm(f='cbrt')` `colors.RootNorm(order=N)` is equivalent to `colors.FuncNorm(f='root{N}')` diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index 1ff028066d99..ffa44a3b4b1c 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -525,7 +525,7 @@ def test_flatiter(): class TestFuncParser(object): x_test = np.linspace(0.01, 0.5, 3) - validstrings = ['linear', 'quadratic', 'cubic', 'sqrt', 'crt', + 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), diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 6af3391e6a41..784bb822044c 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -199,8 +199,8 @@ def test_ticks(self): class TestPiecewiseNorm(object): def test_strings_and_funcs(self): - norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', - lambda x:x**3, 'crt'], + 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], @@ -208,34 +208,34 @@ def test_strings_and_funcs(self): 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', 'crt', 'cubic', 'crt'], + 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', 'crt', 'cubic', 'crt'], + 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', 'crt', 'cubic', 'crt'], + 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', 'crt', 'cubic', 'crt'], + 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', 'crt', 'cubic', 'crt'], + 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.) @@ -247,7 +247,7 @@ def test_intermediate_values(self): assert_array_almost_equal(norm(np.linspace(-0.5, 3.5, 5)), expected) def test_inverse(self): - norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], + 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.) @@ -255,7 +255,7 @@ def test_inverse(self): assert_array_almost_equal(x, norm.inverse(norm(x))) def test_ticks(self): - norm = mcolors.PiecewiseNorm(flist=['cubic', 'crt', 'cubic', 'crt'], + 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.) @@ -269,17 +269,17 @@ 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='crt') + 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='crt', fneg='sqrt', + 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='crt', fneg='sqrt', + norm = mcolors.MirrorPiecewiseNorm(fpos='cbrt', fneg='sqrt', center_data=0.6) assert_array_equal(norm([-2, 0.6, 1]), [0., 0.5, 1.0]) @@ -291,13 +291,13 @@ def test_fpos_lambdafunc(self): def test_fpos_lambdafunc_fneg_string(self): norm = mcolors.MirrorPiecewiseNorm(fpos=lambda x: x**2, fposinv=lambda x: x**0.5, - fneg='crt') + 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='crt') + fneg='cbrt') expected = [0., 0.1606978, 0.52295918, From b4ecdb2a0f568bdb09bcddf0c6f02216814e7c66 Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Thu, 3 Nov 2016 22:24:28 +0000 Subject: [PATCH 32/33] Removed sampledata.py no longer necessary, and changed examples in docstrings according to numpydoc --- examples/colormap_normalization/sampledata.py | 90 ------------------- lib/matplotlib/colors.py | 21 ++--- 2 files changed, 11 insertions(+), 100 deletions(-) delete mode 100644 examples/colormap_normalization/sampledata.py diff --git a/examples/colormap_normalization/sampledata.py b/examples/colormap_normalization/sampledata.py deleted file mode 100644 index 8d9ff63de537..000000000000 --- a/examples/colormap_normalization/sampledata.py +++ /dev/null @@ -1,90 +0,0 @@ -""" -================================================================ -Creating sample data for the different examples on normalization -================================================================ - -Data with special features tailored to the need of the different examples on -colormal normalization is created. - -""" - -import numpy as np - - -def PiecewiseNormData(NX=512, NY=256): - """Sample data for the PiecewiseNorm class. - - Returns a 2d array with sample data, along with the X and Y values for the - array. - - Parameters - ---------- - NX : int - Number of samples for the data accross the horizontal dimension. - Default is 512. - NY : int - Number of samples for the data accross the vertical dimension. - Default is 256. - - Returns - ------- - X, Y, data : ndarray of shape (NX,NY) - Values for the `X` coordinates, the `Y` coordinates, and the `data`. - - Examples - -------- - >>> X,Y,Z=PiecewiseNormData() - """ - - xmax = 16 * np.pi - x = np.linspace(0, xmax, NX) - y = np.linspace(-2, 2, NY) - 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) - - maskY = (Y > -1) * (Y <= 0) - N = 31 - for i in range(N): - maskX = (X > (i * (xmax / N))) * (X <= ((i + 1) * (xmax / N))) - mask = maskX * maskY - data[mask] += gauss2d(X[mask], Y[mask], 2. * i / (N - 1), (i + 0.5) * - (xmax / N), -0.25, xmax / (3 * N), 0.07) - data[mask] -= gauss2d(X[mask], Y[mask], 1. * i / (N - 1), (i + 0.5) * - (xmax / N), -0.75, xmax / (3 * N), 0.07) - - maskY = (Y > 0) * (Y <= 1) - data[maskY] = np.cos(X[maskY]) * Y[maskY]**2 - - N = 61 - maskY = (Y > 1) * (Y <= 2.) - for i, val in enumerate(np.linspace(-1, 1, N)): - if val < 0: - aux = val - if val > 0: - aux = val * 2 - - maskX = (X >= (i * (xmax / N))) * (X <= ((i + 1) * (xmax / N))) - data[maskX * maskY] = aux - - N = 11 - maskY = (Y <= -1) - for i, val in enumerate(np.linspace(-1, 1, N)): - if val < 0: - factor = 1 - if val >= 0: - factor = 2 - maskX = (X >= (i * (xmax / N))) * (X <= ((i + 1) * (xmax / N))) - mask = maskX * maskY - data[mask] = val * factor - - if i != N - 1: - data[mask] += gauss2d(X[mask], Y[mask], 0.05 * factor, (i + 0.5) * - (xmax / N), -1.25, xmax / (3 * N), 0.07) - if i != 0: - data[mask] -= gauss2d(X[mask], Y[mask], 0.05 * factor, (i + 0.5) * - (xmax / N), -1.75, xmax / (3 * N), 0.07) - return X, Y, data diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 809d99a2f451..451b18f33df6 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -999,8 +999,8 @@ def __init__(self, f, finv=None, **normalize_kw): >>> import matplotlib.colors as colors >>> norm = colors.FuncNorm(f=lambda x: np.log10(x), - >>> finv=lambda x: 10.**(x), - >>> vmin=0.01, vmax=2) + ... finv=lambda x: 10.**(x), + ... vmin=0.01, vmax=2) """ @@ -1236,9 +1236,10 @@ def __init__(self, flist, 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]) + >>> norm = colors.PiecewiseNorm(flist=['cubic', 'cbrt', + ... 'cubic', 'cbrt'], + ... refpoints_cm=[0.25, 0.5, 0.75], + ... refpoints_data=[-0.4, 1, 1.2]) """ @@ -1502,8 +1503,8 @@ def __init__(self, >>> import matplotlib.colors as colors >>> norm = colors.MirrorPiecewiseNorm(fpos='sqrt', fneg='cbrt', - >>> center_cm=0.35, - >>> center_data=0.6) + ... center_cm=0.35, + ... center_data=0.6) """ if fneg is None and fneginv is not None: @@ -1624,9 +1625,9 @@ def __init__(self, orderpos=2, orderneg=None, >>> import matplotlib.colors as colors >>> norm = mcolors.MirrorRootNorm(orderpos=3, - >>> orderneg=4, - >>> center_data=0.6, - >>> center_cm=0.3) + ... orderneg=4, + ... center_data=0.6, + ... center_cm=0.3) """ From 42007eebbfbbe50af60a9d000855b34929f7bdfb Mon Sep 17 00:00:00 2001 From: Alvaro Sanchez Date: Thu, 3 Nov 2016 23:17:44 +0000 Subject: [PATCH 33/33] Added examples for MirrorRootNorm and RootNorm --- .../colormap_normalizations_funcnorm.py | 16 ++-- ...rmap_normalizations_mirrorpiecewisenorm.py | 22 ++--- .../colormap_normalizations_mirrorrootnorm.py | 83 +++++++++++++++++++ .../colormap_normalizations_piecewisenorm.py | 2 +- .../colormap_normalizations_rootnorm.py | 79 ++++++++++++++++++ 5 files changed, 182 insertions(+), 20 deletions(-) create mode 100644 examples/colormap_normalization/colormap_normalizations_mirrorrootnorm.py create mode 100644 examples/colormap_normalization/colormap_normalizations_rootnorm.py diff --git a/examples/colormap_normalization/colormap_normalizations_funcnorm.py b/examples/colormap_normalization/colormap_normalizations_funcnorm.py index 84af3106b8e4..288abafb8a73 100644 --- a/examples/colormap_normalization/colormap_normalizations_funcnorm.py +++ b/examples/colormap_normalization/colormap_normalizations_funcnorm.py @@ -23,25 +23,25 @@ def main(): '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) + fig.colorbar(cax, format='%.3g', ax=ax12, ticks=np.linspace(0, 1, 6)) # Example of logarithm normalization using FuncNorm - norm = colors.FuncNorm(f=np.log10, - finv=lambda x: 10.**(x), vmin=0.01) + 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='log10', vmin=0.01) + # norm = colors.FuncNorm(f=np.log10, + # finv=lambda x: 10.**(x), vmin=0.01) # Example of root normalization using FuncNorm - norm = colors.FuncNorm(f=lambda x: x**0.5, - finv=lambda x: x**2, vmin=0.0) + 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='sqrt', vmin=0.0) - # or 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') diff --git a/examples/colormap_normalization/colormap_normalizations_mirrorpiecewisenorm.py b/examples/colormap_normalization/colormap_normalizations_mirrorpiecewisenorm.py index 32c31804a3cf..646bb6d8f641 100644 --- a/examples/colormap_normalization/colormap_normalizations_mirrorpiecewisenorm.py +++ b/examples/colormap_normalization/colormap_normalizations_mirrorpiecewisenorm.py @@ -23,32 +23,32 @@ def main(): '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) + fig.colorbar(cax, format='%.3g', ax=ax12, ticks=np.linspace(-1, 1, 5)) # Example of symmetric root normalization using MirrorPiecewiseNorm - norm = colors.MirrorPiecewiseNorm(fpos=lambda x: x**(1 / 3.), - fposinv=lambda x: x**3) + 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='cbrt') - # or 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=lambda x: x**(1 / 3.), - fposinv=lambda x: x**3, - fneg=lambda x: x, - fneginv=lambda x: x) + 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='cbrt', fneg='linear') - # or 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') 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 index 65dfaaf01834..1da0f262de18 100644 --- a/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py +++ b/examples/colormap_normalization/colormap_normalizations_piecewisenorm.py @@ -21,7 +21,7 @@ def main(): '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) + 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', 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()