{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n.. redirect-from:: /tutorials/colors/colormapnorms\n\n\n# Colormap normalization\n\nObjects that use colormaps by default linearly map the colors in the\ncolormap from data values *vmin* to *vmax*. For example::\n\n pcm = ax.pcolormesh(x, y, Z, vmin=-1., vmax=1., cmap='RdBu_r')\n\nwill map the data in *Z* linearly from -1 to +1, so *Z=0* will\ngive a color at the center of the colormap *RdBu_r* (white in this\ncase).\n\nMatplotlib does this mapping in two steps, with a normalization from\nthe input data to [0, 1] occurring first, and then mapping onto the\nindices in the colormap. Normalizations are classes defined in the\n:func:`matplotlib.colors` module. The default, linear normalization\nis :func:`matplotlib.colors.Normalize`.\n\nArtists that map data to color pass the arguments *vmin* and *vmax* to\nconstruct a :func:`matplotlib.colors.Normalize` instance, then call it:\n\n```pycon\n>>> import matplotlib as mpl\n>>> norm = mpl.colors.Normalize(vmin=-1, vmax=1)\n>>> norm(0)\n0.5\n```\nHowever, there are sometimes cases where it is useful to map data to\ncolormaps in a non-linear fashion.\n\n## Logarithmic\n\nOne of the most common transformations is to plot data by taking its logarithm\n(to the base-10). This transformation is useful to display changes across\ndisparate scales. Using `.colors.LogNorm` normalizes the data via\n$log_{10}$. In the example below, there are two bumps, one much smaller\nthan the other. Using `.colors.LogNorm`, the shape and location of each bump\ncan clearly be seen:\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import matplotlib.pyplot as plt\nimport numpy as np\n\nfrom matplotlib import cm\nimport matplotlib.cbook as cbook\nimport matplotlib.colors as colors\n\nN = 100\nX, Y = np.mgrid[-3:3:complex(0, N), -2:2:complex(0, N)]\n\n# A low hump with a spike coming out of the top right. Needs to have\n# z/colour axis on a log scale, so we see both hump and spike. A linear\n# scale only shows the spike.\nZ1 = np.exp(-X**2 - Y**2)\nZ2 = np.exp(-(X * 10)**2 - (Y * 10)**2)\nZ = Z1 + 50 * Z2\n\nfig, ax = plt.subplots(2, 1)\n\npcm = ax[0].pcolor(X, Y, Z,\n norm=colors.LogNorm(vmin=Z.min(), vmax=Z.max()),\n cmap='PuBu_r', shading='auto')\nfig.colorbar(pcm, ax=ax[0], extend='max')\n\npcm = ax[1].pcolor(X, Y, Z, cmap='PuBu_r', shading='auto')\nfig.colorbar(pcm, ax=ax[1], extend='max')\nplt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Centered\n\nIn many cases, data is symmetrical around a center, for example, positive and\nnegative anomalies around a center 0. In this case, we would like the center\nto be mapped to 0.5 and the datapoint with the largest deviation from the\ncenter to be mapped to 1.0, if its value is greater than the center, or 0.0\notherwise. The norm `.colors.CenteredNorm` creates such a mapping\nautomatically. It is well suited to be combined with a divergent colormap\nwhich uses different colors edges that meet in the center at an unsaturated\ncolor.\n\nIf the center of symmetry is different from 0, it can be set with the\n*vcenter* argument. For logarithmic scaling on both sides of the center, see\n`.colors.SymLogNorm` below; to apply a different mapping above and below the\ncenter, use `.colors.TwoSlopeNorm` below.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "delta = 0.1\nx = np.arange(-3.0, 4.001, delta)\ny = np.arange(-4.0, 3.001, delta)\nX, Y = np.meshgrid(x, y)\nZ1 = np.exp(-X**2 - Y**2)\nZ2 = np.exp(-(X - 1)**2 - (Y - 1)**2)\nZ = (0.9*Z1 - 0.5*Z2) * 2\n\n# select a divergent colormap\ncmap = cm.coolwarm\n\nfig, (ax1, ax2) = plt.subplots(ncols=2)\npc = ax1.pcolormesh(Z, cmap=cmap)\nfig.colorbar(pc, ax=ax1)\nax1.set_title('Normalize()')\n\npc = ax2.pcolormesh(Z, norm=colors.CenteredNorm(), cmap=cmap)\nfig.colorbar(pc, ax=ax2)\nax2.set_title('CenteredNorm()')\n\nplt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Symmetric logarithmic\n\nSimilarly, it sometimes happens that there is data that is positive\nand negative, but we would still like a logarithmic scaling applied to\nboth. In this case, the negative numbers are also scaled\nlogarithmically, and mapped to smaller numbers; e.g., if ``vmin=-vmax``,\nthen the negative numbers are mapped from 0 to 0.5 and the\npositive from 0.5 to 1.\n\nSince the logarithm of values close to zero tends toward infinity, a\nsmall range around zero needs to be mapped linearly. The parameter\n*linthresh* allows the user to specify the size of this range\n(-*linthresh*, *linthresh*). The size of this range in the colormap is\nset by *linscale*. When *linscale* == 1.0 (the default), the space used\nfor the positive and negative halves of the linear range will be equal\nto one decade in the logarithmic range.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "N = 100\nX, Y = np.mgrid[-3:3:complex(0, N), -2:2:complex(0, N)]\nZ1 = np.exp(-X**2 - Y**2)\nZ2 = np.exp(-(X - 1)**2 - (Y - 1)**2)\nZ = (Z1 - Z2) * 2\n\nfig, ax = plt.subplots(2, 1)\n\npcm = ax[0].pcolormesh(X, Y, Z,\n norm=colors.SymLogNorm(linthresh=0.03, linscale=0.03,\n vmin=-1.0, vmax=1.0, base=10),\n cmap='RdBu_r', shading='auto')\nfig.colorbar(pcm, ax=ax[0], extend='both')\n\npcm = ax[1].pcolormesh(X, Y, Z, cmap='RdBu_r', vmin=-np.max(Z), shading='auto')\nfig.colorbar(pcm, ax=ax[1], extend='both')\nplt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Power-law\n\nSometimes it is useful to remap the colors onto a power-law\nrelationship (i.e. $y=x^{\\gamma}$, where $\\gamma$ is the\npower). For this we use the `.colors.PowerNorm`. It takes as an\nargument *gamma* (*gamma* == 1.0 will just yield the default linear\nnormalization):\n\n

Note

There should probably be a good reason for plotting the data using\n this type of transformation. Technical viewers are used to linear\n and logarithmic axes and data transformations. Power laws are less\n common, and viewers should explicitly be made aware that they have\n been used.

\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "N = 100\nX, Y = np.mgrid[0:3:complex(0, N), 0:2:complex(0, N)]\nZ1 = (1 + np.sin(Y * 10.)) * X**2\n\nfig, ax = plt.subplots(2, 1, layout='constrained')\n\npcm = ax[0].pcolormesh(X, Y, Z1, norm=colors.PowerNorm(gamma=0.5),\n cmap='PuBu_r', shading='auto')\nfig.colorbar(pcm, ax=ax[0], extend='max')\nax[0].set_title('PowerNorm()')\n\npcm = ax[1].pcolormesh(X, Y, Z1, cmap='PuBu_r', shading='auto')\nfig.colorbar(pcm, ax=ax[1], extend='max')\nax[1].set_title('Normalize()')\nplt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Discrete bounds\n\nAnother normalization that comes with Matplotlib is `.colors.BoundaryNorm`.\nIn addition to *vmin* and *vmax*, this takes as arguments boundaries between\nwhich data is to be mapped. The colors are then linearly distributed between\nthese \"bounds\". It can also take an *extend* argument to add upper and/or\nlower out-of-bounds values to the range over which the colors are\ndistributed. For instance:\n\n```pycon\n>>> import matplotlib.colors as colors\n>>> bounds = np.array([-0.25, -0.125, 0, 0.5, 1])\n>>> norm = colors.BoundaryNorm(boundaries=bounds, ncolors=4)\n>>> print(norm([-0.2, -0.15, -0.02, 0.3, 0.8, 0.99]))\n[0 0 1 2 3 3]\n```\nNote: Unlike the other norms, this norm returns values from 0 to *ncolors*-1.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "N = 100\nX, Y = np.meshgrid(np.linspace(-3, 3, N), np.linspace(-2, 2, N))\nZ1 = np.exp(-X**2 - Y**2)\nZ2 = np.exp(-(X - 1)**2 - (Y - 1)**2)\nZ = ((Z1 - Z2) * 2)[:-1, :-1]\n\nfig, ax = plt.subplots(2, 2, figsize=(8, 6), layout='constrained')\nax = ax.flatten()\n\n# Default norm:\npcm = ax[0].pcolormesh(X, Y, Z, cmap='RdBu_r')\nfig.colorbar(pcm, ax=ax[0], orientation='vertical')\nax[0].set_title('Default norm')\n\n# Even bounds give a contour-like effect:\nbounds = np.linspace(-1.5, 1.5, 7)\nnorm = colors.BoundaryNorm(boundaries=bounds, ncolors=256)\npcm = ax[1].pcolormesh(X, Y, Z, norm=norm, cmap='RdBu_r')\nfig.colorbar(pcm, ax=ax[1], extend='both', orientation='vertical')\nax[1].set_title('BoundaryNorm: 7 boundaries')\n\n# Bounds may be unevenly spaced:\nbounds = np.array([-0.2, -0.1, 0, 0.5, 1])\nnorm = colors.BoundaryNorm(boundaries=bounds, ncolors=256)\npcm = ax[2].pcolormesh(X, Y, Z, norm=norm, cmap='RdBu_r')\nfig.colorbar(pcm, ax=ax[2], extend='both', orientation='vertical')\nax[2].set_title('BoundaryNorm: nonuniform')\n\n# With out-of-bounds colors:\nbounds = np.linspace(-1.5, 1.5, 7)\nnorm = colors.BoundaryNorm(boundaries=bounds, ncolors=256, extend='both')\npcm = ax[3].pcolormesh(X, Y, Z, norm=norm, cmap='RdBu_r')\n# The colorbar inherits the \"extend\" argument from BoundaryNorm.\nfig.colorbar(pcm, ax=ax[3], orientation='vertical')\nax[3].set_title('BoundaryNorm: extend=\"both\"')\nplt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## TwoSlopeNorm: Different mapping on either side of a center\n\nSometimes we want to have a different colormap on either side of a\nconceptual center point, and we want those two colormaps to have\ndifferent linear scales. An example is a topographic map where the land\nand ocean have a center at zero, but land typically has a greater\nelevation range than the water has depth range, and they are often\nrepresented by a different colormap.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "dem = cbook.get_sample_data('topobathy.npz')\ntopo = dem['topo']\nlongitude = dem['longitude']\nlatitude = dem['latitude']\n\nfig, ax = plt.subplots()\n# make a colormap that has land and ocean clearly delineated and of the\n# same length (256 + 256)\ncolors_undersea = plt.cm.terrain(np.linspace(0, 0.17, 256))\ncolors_land = plt.cm.terrain(np.linspace(0.25, 1, 256))\nall_colors = np.vstack((colors_undersea, colors_land))\nterrain_map = colors.LinearSegmentedColormap.from_list(\n 'terrain_map', all_colors)\n\n# make the norm: Note the center is offset so that the land has more\n# dynamic range:\ndivnorm = colors.TwoSlopeNorm(vmin=-500., vcenter=0, vmax=4000)\n\npcm = ax.pcolormesh(longitude, latitude, topo, rasterized=True, norm=divnorm,\n cmap=terrain_map, shading='auto')\n# Simple geographic plot, set aspect ratio because distance between lines of\n# longitude depends on latitude.\nax.set_aspect(1 / np.cos(np.deg2rad(49)))\nax.set_title('TwoSlopeNorm(x)')\ncb = fig.colorbar(pcm, shrink=0.6)\ncb.set_ticks([-500, 0, 1000, 2000, 3000, 4000])\nplt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## FuncNorm: Arbitrary function normalization\n\nIf the above norms do not provide the normalization you want, you can use\n`~.colors.FuncNorm` to define your own. Note that this example is the same\nas `~.colors.PowerNorm` with a power of 0.5:\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "def _forward(x):\n return np.sqrt(x)\n\n\ndef _inverse(x):\n return x**2\n\nN = 100\nX, Y = np.mgrid[0:3:complex(0, N), 0:2:complex(0, N)]\nZ1 = (1 + np.sin(Y * 10.)) * X**2\nfig, ax = plt.subplots()\n\nnorm = colors.FuncNorm((_forward, _inverse), vmin=0, vmax=20)\npcm = ax.pcolormesh(X, Y, Z1, norm=norm, cmap='PuBu_r', shading='auto')\nax.set_title('FuncNorm(x)')\nfig.colorbar(pcm, shrink=0.6)\nplt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Custom normalization: Manually implement two linear ranges\n\nThe `.TwoSlopeNorm` described above makes a useful example for\ndefining your own norm. Note for the colorbar to work, you must\ndefine an inverse for your norm:\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "class MidpointNormalize(colors.Normalize):\n def __init__(self, vmin=None, vmax=None, vcenter=None, clip=False):\n self.vcenter = vcenter\n super().__init__(vmin, vmax, clip)\n\n def __call__(self, value, clip=None):\n # I'm ignoring masked values and all kinds of edge cases to make a\n # simple example...\n # Note also that we must extrapolate beyond vmin/vmax\n x, y = [self.vmin, self.vcenter, self.vmax], [0, 0.5, 1.]\n return np.ma.masked_array(np.interp(value, x, y,\n left=-np.inf, right=np.inf))\n\n def inverse(self, value):\n y, x = [self.vmin, self.vcenter, self.vmax], [0, 0.5, 1]\n return np.interp(value, x, y, left=-np.inf, right=np.inf)\n\n\nfig, ax = plt.subplots()\nmidnorm = MidpointNormalize(vmin=-500., vcenter=0, vmax=4000)\n\npcm = ax.pcolormesh(longitude, latitude, topo, rasterized=True, norm=midnorm,\n cmap=terrain_map, shading='auto')\nax.set_aspect(1 / np.cos(np.deg2rad(49)))\nax.set_title('Custom norm')\ncb = fig.colorbar(pcm, shrink=0.6, extend='both')\ncb.set_ticks([-500, 0, 1000, 2000, 3000, 4000])\n\nplt.show()" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.13.5" } }, "nbformat": 4, "nbformat_minor": 0 }