-
-
Notifications
You must be signed in to change notification settings - Fork 7.8k
2D Normalization & Colormapping for ScalerMappables #8738
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
lib/matplotlib/colors.py
Outdated
@@ -1346,6 +1346,60 @@ def __call__(self, value, clip=None): | |||
def inverse(self, value): | |||
return value | |||
|
|||
class Norm2d: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"BivariateNorm" just so that it can follow the mpl convention of "Xnorm"?
lib/matplotlib/colors.py
Outdated
""" | ||
Normalize a list of two values corresponding to two 1D normalizers | ||
""" | ||
def __init__(self, norm_instances=None): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there are a reason norm_instances = none and is not:
def __init__(self, norm1 = None, norm2 = None):
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No reason. I think what you are proposing is more convenient from user's perspective.
lib/matplotlib/colors.py
Outdated
norm_instances : | ||
A list of length two having instances of 1D normalizers | ||
""" | ||
if norm_instances is None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do you handle the use case [BoundaryNorm(), None]
or [None, BoundaryNorm]
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes this should be handled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is being handled now.
How would this relate to actual plotting functions? |
@anntzer Currently, functions like imshow, pcolormesh, scalar mappable etc expects 1D normalizer. To extend their support for bivariate colormaps there is a need for bivariate norm also. |
I understand, but right now all I see this PR implements is some trivial way of combining to normalizers. If there's no bigger example usage, it's hard to judge whether that's sufficient, or too simple, or too complex. |
This should stay in its own PR for reviewing ease, but what about postponing the decision on whether to merge this until the whole set of 2d coloring PRs come in? The issue with an N-color normalizer (I like it in theory/kinda agree on creating it and then sub classing to this) is that past 3 variables it's not really implementable. |
I agree with keeping the PR open for now. Not sure why you cannot have more three dimensions, at the end we're just writing a mapping from R^n -> {colors}. |
Yes, you can technically have N dimensions, but: |
Well a color triangle really is only two dimensional (the third variable is determined by the first two). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Further PRs on nonscalar normalizers need to come in before we can judge whether this design makes sense.
1a3d16b
to
8e93d20
Compare
13627d3
to
2f8b5bf
Compare
lib/matplotlib/axes/_axes.py
Outdated
|
||
if len(args) == 1: | ||
C = np.asanyarray(args[0]) | ||
isBivari = (isinstance(norm, mcolors.BivariateNorm) or |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
try to stick to pep8 (is_bivar
)
also this can br written as isinstance(norm, (mcolors.BivariateNorm, mcolors.BivariateColormap))
(similarly below)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The previous change was reverted because two different variables norm
and cmap
are being checked for their respective instances.
xlabel='', | ||
ylabel='', | ||
): | ||
#: The axes that this colorbar lives in. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unless there is a strong reason to expose them, attributes should be private (start with an underscore) by default. Otherwise, they all become part of the public API, which is very difficult to change while maintaining back-compatibility.
(Same comment throughout the PR.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any specific attribute you are pointing to? I tried to keep it similar to ColorbarBase class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ax
is probably fine to stay public (it is public on every Artist
), but the xvalues, xboundaries, etc can probably be made private.
lib/matplotlib/axes/_axes.py
Outdated
isNorm = isinstance(norm, (mcolors.Normalize, mcolors.BivariateNorm)) | ||
if norm is not None and not isNorm: | ||
msg = "'norm' must be an instance of 'mcolors.Normalize' " \ | ||
"or 'mcolors.BivariateNorm'" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
shouldn't BivariateNorm
subclass Normalize? 'specially since now it's normalizing down to a 1d space?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BivariateNorm does not inherit anything from Normalize so I thought it should not subclass it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the new design, BivariateNorm should havel all the same classes as Normalize...and I think subclassing it so it's registered generically as a Norm is preferable to having to treat it as a special case when it doesn't need to be.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be done with an ABC
https://docs.python.org/3/library/abc.html#abc.ABCMeta.register which both Normalize and BivariateNorm register with.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, but I don't see a reason why BivariateNorm shouldn't be subclassing Norm as it has the same architecture as any other norm...take data (scaler or vector)->map to 1D lut value ->get color, and color ->lut->scaler/vector.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the code paths are completely different, why force the sub-class? In general, sub-classing is only worth it when you can share significant amounts of implementation details, otherwise duck-typing is better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My point is mostly that the code paths shouldn't end up being all that different and more to the point in mpl land there's probably a ton of code that checks if things are norms and if this code works in those instances it doesn't make sense to change all that code to cover both cases when this is still fundamentally a norm. But I think I'm on the same page as you on the solution maybe being a "Norm" metaclass.
lib/matplotlib/colors.py
Outdated
temp[0] = temp[0] * (256) | ||
temp[1] = temp[1] * (256) | ||
temp = temp.astype(int) | ||
return temp[0] + temp[1] * 256 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you guarantee that this scheme always yields uniqueness?
(I'm confused about what's going on here and sort of have been thinking about a more general mapping between nd arrays and numbers wherein you get a sorted unique list of the nd tuples in the dataset so that the lookup/map is just the index of the value in the sorted list.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since after normalization every variable ends up between 0 to 1. So here I am changing it to index flattened 1d lut of BivariateColormap by mapping 0-1 to 1 to 256*256. Here I am currently hardcoding 256 as number of rows and columns in lut of 2d colormap. But this should be changed somehow to use N
attribute of BivariateColormap.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is column major indexing (row * num_cols + col
is c / numpy style, col * num_rows + row
).
I am more concerned that the descritization is happening here rather than in the color map (which is done https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/colors.py#L483) which prevents the size of the colormap from having to be known by the norm.
Why can't this return a Nd array? I think the most important places the norm and color map are called is https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/cm.py#L209 and
https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/image.py#L365 . In the first case it can be blindly passed through and in the second that resampling needs to happen over each plane.
In the case of the color bar, that already needs to be heavily special cased.
20:11 $ git grep '\.norm[ (]' | grep -v linalg
lib/matplotlib/cm.py: self.norm = norm
lib/matplotlib/cm.py: x = self.norm(x)
lib/matplotlib/cm.py: self.norm = norm
lib/matplotlib/collections.py: self.norm = other.norm
lib/matplotlib/colorbar.py: y = self.norm(self._boundaries.copy())
lib/matplotlib/colorbar.py: b = self.norm(self._boundaries, clip=False).filled()
lib/matplotlib/colorbar.py: xn = self.norm(x, clip=False).filled()
lib/matplotlib/colorbar.py: self.norm = mappable.norm
lib/matplotlib/colorbar.py: facecolor=self.cmap(self.norm(val)),
lib/matplotlib/image.py: A = self.norm(A)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am still a bit concerned that the descritization is being done here rather than in the color map. Won't this break for any colormap which is not 256 by 256?
lib/matplotlib/axes/_axes.py
Outdated
isNorm = isinstance(norm, (mcolors.Normalize, mcolors.BivariateNorm)) | ||
if norm is not None and not isNorm: | ||
msg = "'norm' must be an instance of 'mcolors.Normalize' " \ | ||
"or 'mcolors.BivariateNorm'" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be done with an ABC
https://docs.python.org/3/library/abc.html#abc.ABCMeta.register which both Normalize and BivariateNorm register with.
lib/matplotlib/axes/_axes.py
Outdated
temp = np.asarray(X) | ||
if temp.ndim == 3 and isinstance(norm, mcolors.BivariateNorm): | ||
temp = norm(temp) | ||
X = cmap(temp, alpha=self.get_alpha(), bytes=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doing this here means you can not change anything about the color map or norm later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed this.
lib/matplotlib/axes/_axes.py
Outdated
|
||
if len(args) == 1: | ||
C = np.asanyarray(args[0]) | ||
isBivari = isinstance(norm, (mcolors.BivariateNorm, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would the norm ever be a colormap?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for pointing this out. Fixed now.
lib/matplotlib/axes/_axes.py
Outdated
cmap = mcolors.BivariateColormap() | ||
if norm is None: | ||
norm = mcolors.BivariateNorm() | ||
C = norm(C) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do this up-front? defering all of this to _make_image
means the colormap and norm and be updated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check now.
lib/matplotlib/colorbar.py
Outdated
linthresh=norm.linthresh, | ||
base=10) | ||
else: | ||
if mpl.rcParams['_internal.classic_mode']: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't need any of the classic shims as there is no back-compatibility to maintain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed.
lib/matplotlib/colorbar.py
Outdated
or len(self._x) >= self.n_rasterize): | ||
self.solids.set_rasterized(True) | ||
|
||
def _ticker(self, norm): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably better to factor this as def _make_ticker(norm, values, boundaries, locator, formatter):
as a local function in update_ticks
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You mean local/private function of class or nested function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nested function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
lib/matplotlib/colors.py
Outdated
temp[0] = temp[0] * (256) | ||
temp[1] = temp[1] * (256) | ||
temp = temp.astype(int) | ||
return temp[0] + temp[1] * 256 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is column major indexing (row * num_cols + col
is c / numpy style, col * num_rows + row
).
I am more concerned that the descritization is happening here rather than in the color map (which is done https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/colors.py#L483) which prevents the size of the colormap from having to be known by the norm.
Why can't this return a Nd array? I think the most important places the norm and color map are called is https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/cm.py#L209 and
https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/image.py#L365 . In the first case it can be blindly passed through and in the second that resampling needs to happen over each plane.
In the case of the color bar, that already needs to be heavily special cased.
20:11 $ git grep '\.norm[ (]' | grep -v linalg
lib/matplotlib/cm.py: self.norm = norm
lib/matplotlib/cm.py: x = self.norm(x)
lib/matplotlib/cm.py: self.norm = norm
lib/matplotlib/collections.py: self.norm = other.norm
lib/matplotlib/colorbar.py: y = self.norm(self._boundaries.copy())
lib/matplotlib/colorbar.py: b = self.norm(self._boundaries, clip=False).filled()
lib/matplotlib/colorbar.py: xn = self.norm(x, clip=False).filled()
lib/matplotlib/colorbar.py: self.norm = mappable.norm
lib/matplotlib/colorbar.py: facecolor=self.cmap(self.norm(val)),
lib/matplotlib/image.py: A = self.norm(A)
self.ax.set_xlabel(self._xlabel, **self._labelkw) | ||
self.stale = True | ||
|
||
def set_label(self, xlabel, ylabel, **kw): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The different signature from the ColorBar
base class is a bit problematic, but not sure what to do about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
perhaps set_labels(self, xylabel) (where xylabel=(xlabel, ylabel)); set_xlabel; set_ylabel?
let set_label raise an error
this likely would play better with traitlets
@anntzer can you have a look at this PR again? Some things have changed since your last review. |
lib/matplotlib/axes/_axes.py
Outdated
C = C.ravel() | ||
# convert to one dimensional arrays if univariate | ||
if isinstance(norm, mcolors.BivariateNorm): | ||
C = np.asarray([C[0].ravel(), C[1].ravel()]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may be better done as np.asarray([c.ravel() for c in C])
to be a bit future proof.
lib/matplotlib/axes/_axes.py
Outdated
msg = "'norm' must be an instance of 'mcolors.Normalize'" | ||
|
||
if norm is not None and not isinstance(norm, mcolors.Norms): | ||
msg = "'norm' must be an instance of 'mcolors.Normalize' " \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have a style preference for using ()
for continuation rather than \
so
msg = ('...' +
'...')
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The + is not needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
lib/matplotlib/axes/_axes.py
Outdated
|
||
if len(args) == 1: | ||
C = np.asanyarray(args[0]) | ||
numRows, numCols = C.shape | ||
isBivari = (isinstance(norm, mcolors.BivariateNorm) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use_underscores_notCapsForVariables
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed isBivari
to is_bivari
.
@@ -730,7 +730,8 @@ def update_scalarmappable(self): | |||
""" | |||
if self._A is None: | |||
return | |||
if self._A.ndim > 1: | |||
if (self._A.ndim > 1 and | |||
not isinstance(self.norm, mcolors.BivariateNorm)): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should check ndim == 2 in this case?
""" | ||
Label the axes of the colorbar | ||
""" | ||
self._xlabel = '%s' % (xlabel, ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just self._xlabel = xlabel should be enough (unless we explicitly want to support any object via conversion to string, and even then str(xlabel) is clearer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just imitating how it is done in ColorbarBase.set_label
.
lib/matplotlib/colors.py
Outdated
""" | ||
Abstract Base Class to group `Normalize` and `BivariateNorm` | ||
""" | ||
__metaclass__ = ABCMeta |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
definitely wrong on Py3
use six
should have been caught by pycodestyle perhaps?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed now.
Appveyor seems to be having issues with connectivity the last day or so. It is failing on almost all PRs. The circle / docs failures do need to be fixed though. |
# Bivariate plotting demo | ||
# ----------------------- | ||
|
||
air_temp = np.load(get_sample_data('air_temperature.npy')) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How big are these data files? I have a slight preference for 'generated' data for examples (to keep the repository size small).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
84.2 kB each.
Couldn't come up with generated data so used these.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That isn't too bad, suspect that is smaller than the test images.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One argument in favour of this can be that the user seeing the example might relate to use case of bivariate plotting better with a real world example than with some mathematical function.
lib/matplotlib/axes/_axes.py
Outdated
to colors based on the `norm` (mapping scalar to scalar) | ||
and the `cmap` (mapping the normed scalar to a color). | ||
|
||
cmap : `~matplotlib.colors.Colormap`, optional, default: None | ||
cmap : `~matplotlib.colors.Colormap`, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the one exception to the ()
over \
rule so that numpydoc correctly grabs this as a single lin.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These might be the source of the docs failures?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added \
but still failing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is probably fastest to build the docs locally. That also lets you get access to the full traceback.
pip install -r doc-requirements.txt
cd docs
python make.py html
should work.
This is looking good 👍 |
Can you build the docs locally? |
@tacaswell doc build is passing now. |
@patniharshit I broke your branch via #8966 this needs to be rebased (sorry). I think it is ok to either just update the shim around the resampling code or to move it all into a loop (I think clearer now) to resample each plane. |
I don't have access to my laptop for two days. Will resolve the conflicts by Wednesday. |
- MxNx3 -- RGB (float or uint8) | ||
- MxNx4 -- RGBA (float or uint8) | ||
- 2xMxN -- bivariate values to be mapped (float or int) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens when the input is shape (2, 3, 4)
?
This sort of ambiguity should be avoided (and indicates that this API is probably not the right design choice).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR came up in #14168.
I'm not sure the status of this, but in case someone comes back to it...
This looks technically well done. However, I'm not a fan of this API (see @shoyer comment as well). Its very complex from a documentation point of view. There is no way from the dosctrings that someone who didn't know what this PR did would understand what is meant by "2xMxN -- bivariate values to be mapped (float or int)". Further propagating support for two norms across all the 2-D plotting functions is going to be a maintenance nightmare. I'd far prefer this just got its own methods, i.e.: ax.bivariate_pcolormesh(x, y, Z1, Z1, norm1, norm2)
etc.
I'm also very concerned about adding a whole new colorbar implementation. Maybe its unavoidable, but given that its so different, I wonder if it should also get its own method...
I agree with putting this into a separate API. In shaping this API, I think it would be helping to identify clear use-cases / critical user journeys and make sure they are well supported and documented. For example, one good use case is visualizing complex-valued functions with luminosity and (perceptually uniform?) hue. My concern is that in general this isn't a terribly effective visualization technique. That's not to say matplotlib shouldn't have this functionality, but let's try to limit its impact on the rest of the API. |
My use case is shading a quantity based on gradient strength... If you'll forgive the sacrilege of linking to mathworks. The API for this was just: shadedpcolor(x,z,U,(dU),[-1 1]/4,[-1 1]/300,0.7,cmp,0);
axis ij;
shadedcolorbar('v',[-1 1]/4,0.7,cmp); https://www.mathworks.com/matlabcentral/fileexchange/14157-shaded-pseudo-color |
Since this Pull Request has not been updated in 60 days, it has been marked "inactive." This does not mean that it will be closed, though it may be moved to a "Draft" state. This helps maintainers prioritize their reviewing efforts. You can pick the PR back up anytime - please ping us if you need a review or guidance to move the PR forward! If you do not plan on continuing the work, please let us know so that we can either find someone to take the PR over, or close it. |
PR Summary
This PR is in referance to GSoC project '2D colormaps'.
(Project proposal is here)
The
BivariateColormap
class subclassesColormap
class. After generation it is flattened to 1d look up table.The implementation of
BivariateColormap
is not perfect yet and just something has been generated to show proof of concept.The bivariate data is identified by ndim == 3 and if an instance of either Bivariate Colormap or Norm has been passed in. This data is normalized and mapped to univariate integer index so as to directly index flattened BivariateColormap.
Plotting some air temperature and surface pressure data:

Feel free to review.
@story645 @tacaswell
PR Checklist