proplot-dev / proplot

🎨 A succinct matplotlib wrapper for making beautiful, publication-quality graphics
https://proplot.readthedocs.io
MIT License
1.07k stars 96 forks source link

Inconsistent color mappings #414

Closed AWSisco closed 1 year ago

AWSisco commented 1 year ago

Description

Is the method used to map data values to a color inconsistent between proplot and xarray's matplotlib wrapper? Even putting xarray aside, I feel that proplot's method easily leads to confusion when working with binary data (i.e., 0s and 1s both end up the same color unless explicit care is taken to prevent it).

Steps to reproduce

import proplot as pplt
import numpy as np
import matplotlib.colors as mcolors
import xarray as xr
import matplotlib.pyplot as plt

arr = np.arange(0,4).reshape((2,2))

fig, axs = pplt.subplots(ncols=3, share=3)

axs[0].pcolormesh(arr, cmap='viridis',levels=arr.flatten(),extend='max',colorbar=True)
xr.DataArray(arr).plot(ax=axs[1],extend='max',levels=arr.flatten())

norm=mcolors.BoundaryNorm(boundaries=arr.flatten(),ncolors=len(arr.flatten()),extend='max')
cmap= pplt.Colormap('viridis',N=4)
axs[2].pcolormesh(arr,norm=norm,cmap=cmap,colorbar=True, extend='max')

axs.format(grid=False,xlabel='',ylabel='',title=('proplot','xarray','proplot w/ user norm'),xlocator=0.5,ylocator=0.5)

for i in range(0,3):
    for j in range(0,2):
        for k in range(0,2):
            axs[i].text(x=j,y=k,s=arr.T[j,k],c='w',fontsize=18,ha='center',va='center')

image

Expected behavior: For four different integers, I expect to see four different colors as I see with xarray (middle). That mapping appears to show [0,1), [1,2), [2,3),[3,...).

Actual behavior: Proplot (left) appears to map with right-closed intervals [0,1], (1,2], (2,3], (3,...). Here, it seems like the default should be to map 0s and 1s to different colors.

Proplot version

matplotlib 3.4.3 proplot 0.9.5.post356

lukelbd commented 1 year ago

Hmm, thanks for the detailed report, but I think the proplot/xarray differences here are pretty subtle/accidental (maybe not expected to be robust between xarray/matplotlib versions), so maybe not feasible to address. Instead, for an easier way to assign one unique color to each unique data value, I recommend specifying color level centers by passing e.g. values=arr.flatten() instead of the level edges with levels=arr.flatten(). Here's an example:

import proplot as pplt
import numpy as np
import matplotlib.colors as mcolors
import xarray as xr
import matplotlib.pyplot as plt

arr = np.arange(0,4).reshape((2,2))

fig, axs = pplt.subplots(ncols=2, share=3)

axs[0].pcolormesh(arr, cmap='viridis',values=arr.flatten(),colorbar=True)
xr.DataArray(arr).plot(ax=axs[1],extend='max',levels=arr.flatten())

iTerm2 Ku0aEG tmpcux2vufi

In your example, since you explicitly set levels to the data values, you are saying that your data values should fall on the border between two different colors in the resulting discrete normalizer (see: your colorbar). So, the color ultimately selected for those values depends on a floating point comparison between the level edge and the data (this comparison is the non-robust part). It just so happens that in xarray, it assigns the color from the level above, while in proplot, it selects the color from the level below -- so switching to extend='min' in your proplot example produces the desired behavior:

import proplot as pplt
import numpy as np
import matplotlib.colors as mcolors
import xarray as xr
import matplotlib.pyplot as plt

arr = np.arange(0,4).reshape((2,2))

fig, axs = pplt.subplots(ncols=3, share=3)

axs[0].pcolormesh(arr, cmap='viridis',levels=arr.flatten(),extend='min',colorbar=True, locator=1)
xr.DataArray(arr).plot(ax=axs[1],extend='max',levels=arr.flatten())

norm=mcolors.BoundaryNorm(boundaries=arr.flatten(),ncolors=len(arr.flatten()),extend='max')
cmap= pplt.Colormap('viridis',N=4)
axs[2].pcolormesh(arr,norm=norm,cmap=cmap,colorbar=True, extend='max')

axs.format(grid=False,xlabel='',ylabel='',title=('proplot','xarray','proplot w/ user norm'),xlocator=0.5,ylocator=
0.5)

for i in range(0,3):
    for j in range(0,2):
        for k in range(0,2):
            axs[i].text(x=j,y=k,s=arr.T[j,k],c='w',fontsize=18,ha='center',va='center')

iTerm2 QbJAp9 tmp5zgt3wzt

Will close for now, but would consider re-opening if it turns out the xarray/matplotlib behavior is not an accident (i.e., there is explicit documentation of the select-topmost-color level-edge behavior on the xarray/matplotlib websites).

AWSisco commented 1 year ago

Thanks so much for those options. Hadn’t thought to try them.

Perhaps the left-closed/assign-above behavior is a personal preference developed over years of using NCL and xarray plot interfaces. But I can’t think of reason it has to be that way.