From 4e6a206e2ac46e75b291edab26ddd419bb18d0a8 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Fri, 3 Jul 2020 16:17:42 +0100 Subject: [PATCH 1/7] Make variable name clearer --- lib/matplotlib/colors.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index b1e76e27175b..f32748204c43 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1473,18 +1473,18 @@ def __init__(self, boundaries, ncolors, clip=False, *, extend='neither'): self.Ncmap = ncolors self.extend = extend - self._N = self.N - 1 # number of colors needed + self._n_regions = self.N - 1 # number of colors needed self._offset = 0 if extend in ('min', 'both'): - self._N += 1 + self._n_regions += 1 self._offset = 1 if extend in ('max', 'both'): - self._N += 1 - if self._N > self.Ncmap: - raise ValueError(f"There are {self._N} color bins including " - f"extensions, but ncolors = {ncolors}; " - "ncolors must equal or exceed the number of " - "bins") + self._n_regions += 1 + if self._n_regions > self.Ncmap: + raise ValueError(f"There are {self._n_regions} color bins " + "including extensions, but ncolors = " + f"{ncolors}; ncolors must equal or exceed the " + "number of bins") def __call__(self, value, clip=None): if clip is None: @@ -1499,8 +1499,8 @@ def __call__(self, value, clip=None): else: max_col = self.Ncmap iret = np.digitize(xx, self.boundaries) - 1 + self._offset - if self.Ncmap > self._N: - scalefac = (self.Ncmap - 1) / (self._N - 1) + if self.Ncmap > self._n_regions: + scalefac = (self.Ncmap - 1) / (self._n_regions - 1) iret = (iret * scalefac).astype(np.int16) iret[xx < self.vmin] = -1 iret[xx >= self.vmax] = max_col From 590f4019c158a4caa3d9bd5e1e31d3769a1ec42e Mon Sep 17 00:00:00 2001 From: David Stansby Date: Fri, 3 Jul 2020 16:46:26 +0100 Subject: [PATCH 2/7] Fix BoundaryNorm for multiple colors and one region --- lib/matplotlib/colors.py | 10 +++++++--- lib/matplotlib/tests/test_colors.py | 5 +++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index f32748204c43..23ed6373f200 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1429,9 +1429,9 @@ def __init__(self, boundaries, ncolors, clip=False, *, extend='neither'): Parameters ---------- boundaries : array-like - Monotonically increasing sequence of boundaries + Monotonically increasing sequence of boundaries. ncolors : int - Number of colors in the colormap to be used + Number of colors in the colormap to be used. clip : bool, optional If clip is ``True``, out of range values are mapped to 0 if they are below ``boundaries[0]`` or mapped to ``ncolors - 1`` if they @@ -1492,6 +1492,7 @@ def __call__(self, value, clip=None): xx, is_scalar = self.process_value(value) mask = np.ma.getmaskarray(xx) + # Fill masked values a value above the upper boundary xx = np.atleast_1d(xx.filled(self.vmax + 1)) if clip: np.clip(xx, self.vmin, self.vmax, out=xx) @@ -1500,7 +1501,10 @@ def __call__(self, value, clip=None): max_col = self.Ncmap iret = np.digitize(xx, self.boundaries) - 1 + self._offset if self.Ncmap > self._n_regions: - scalefac = (self.Ncmap - 1) / (self._n_regions - 1) + if self._n_regions == 1: + scalefac = 1 + else: + scalefac = (self.Ncmap - 1) / (self._n_regions - 1) iret = (iret * scalefac).astype(np.int16) iret[xx < self.vmin] = -1 iret[xx >= self.vmax] = max_col diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index ba1192d16ac3..5ca954fed320 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -207,6 +207,11 @@ def test_BoundaryNorm(): bn = mcolors.BoundaryNorm(boundaries, ncolors) assert_array_equal(bn(vals), expected) + # with a single region and interpolation + expected = [-1, 0, 0, 0, 3, 3] + bn = mcolors.BoundaryNorm([0, 2.2], ncolors) + assert_array_equal(bn(vals), expected) + # more boundaries for a third color boundaries = [0, 1, 2, 3] vals = [-1, 0.1, 1.1, 2.2, 4] From f8017d4a646a45d2e3027bf148d451cbf5d42ae7 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 3 Jul 2020 14:28:43 -0400 Subject: [PATCH 3/7] DOC: add inline explanation of the re-scaling logic in BoundrayNorm --- lib/matplotlib/colors.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 23ed6373f200..dd8718e31b7d 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1499,12 +1499,27 @@ def __call__(self, value, clip=None): max_col = self.Ncmap - 1 else: max_col = self.Ncmap + # this gives us the bins in the lookup table in the range + # [0, _n_regions - 1] (the offset is baked in in the init) iret = np.digitize(xx, self.boundaries) - 1 + self._offset + # if we have more colors than regions, stretch the region index + # computed above to full range of the color bins. This will + # make use skip some of the colors in the middle of the + # colormap but will use the full range if self.Ncmap > self._n_regions: - if self._n_regions == 1: + # the maximum possible value in iret + divsor = self._n_regions - 1 + if divsor == 0: + # special case the 1 region case. What we should do here + # is a bit undefined (as _any_ color in the colormap is + # justifiable) we go with not adjusting the value (which will + # either be 0 or 1 depending on if we are extending down) scalefac = 1 else: - scalefac = (self.Ncmap - 1) / (self._n_regions - 1) + # otherwise linearly remap the values from the region index + # to the color index spaces + scalefac = (self.Ncmap - 1) / divsor + # do the scaling and re-cast to integers iret = (iret * scalefac).astype(np.int16) iret[xx < self.vmin] = -1 iret[xx >= self.vmax] = max_col From 20ef92732e68e89de0cfed663488f4bd36fa20a9 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 3 Jul 2020 18:31:35 -0400 Subject: [PATCH 4/7] DOC: re-word comments --- lib/matplotlib/colors.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index dd8718e31b7d..7600d2f29bc4 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1502,18 +1502,16 @@ def __call__(self, value, clip=None): # this gives us the bins in the lookup table in the range # [0, _n_regions - 1] (the offset is baked in in the init) iret = np.digitize(xx, self.boundaries) - 1 + self._offset - # if we have more colors than regions, stretch the region index - # computed above to full range of the color bins. This will - # make use skip some of the colors in the middle of the - # colormap but will use the full range + # if we have more colors than regions, stretch the region + # index computed above to full range of the color bins. This + # will make use of the full range (but skip some of the colors + # in the middle) such that the first region is mapped to the + # first color and the last region is mapped to the last color. if self.Ncmap > self._n_regions: # the maximum possible value in iret divsor = self._n_regions - 1 if divsor == 0: - # special case the 1 region case. What we should do here - # is a bit undefined (as _any_ color in the colormap is - # justifiable) we go with not adjusting the value (which will - # either be 0 or 1 depending on if we are extending down) + # special case the 1 region case, don't scale anything scalefac = 1 else: # otherwise linearly remap the values from the region index From 64fff76d3157a7cb070cf570dca8cddb870ac517 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 3 Jul 2020 18:32:20 -0400 Subject: [PATCH 5/7] API: add a check that we have at least one region --- lib/matplotlib/colors.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 7600d2f29bc4..91afddbe116d 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1470,6 +1470,9 @@ def __init__(self, boundaries, ncolors, clip=False, *, extend='neither'): self.vmax = boundaries[-1] self.boundaries = np.asarray(boundaries) self.N = len(self.boundaries) + if self.N < 2: + raise ValueError("You must provide at least 2 boundaries " + f"(1 region) but you passed in {boundaries!r}") self.Ncmap = ncolors self.extend = extend From a5f531791c0d4218c6cb5ebe782c2ab685eb922a Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 4 Jul 2020 18:14:39 -0400 Subject: [PATCH 6/7] ENH: pick the middle color only 1 region with BoundaryNorm This never worked before so is not an API change. --- lib/matplotlib/colors.py | 14 ++++++-------- lib/matplotlib/tests/test_colors.py | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 91afddbe116d..e9e411d72206 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1511,17 +1511,15 @@ def __call__(self, value, clip=None): # in the middle) such that the first region is mapped to the # first color and the last region is mapped to the last color. if self.Ncmap > self._n_regions: - # the maximum possible value in iret - divsor = self._n_regions - 1 - if divsor == 0: - # special case the 1 region case, don't scale anything - scalefac = 1 + if self._n_regions == 1: + # special case the 1 region case, pick the middle color + iret[iret == 0] = (self.Ncmap - 1) // 2 else: # otherwise linearly remap the values from the region index # to the color index spaces - scalefac = (self.Ncmap - 1) / divsor - # do the scaling and re-cast to integers - iret = (iret * scalefac).astype(np.int16) + iret = (self.Ncmap - 1) / (self._n_regions - 1) * iret + # cast to 16bit integers in all cases + iret = iret.astype(np.int16) iret[xx < self.vmin] = -1 iret[xx >= self.vmax] = max_col ret = np.ma.array(iret, mask=mask) diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 5ca954fed320..ff9e985f78fb 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -208,7 +208,7 @@ def test_BoundaryNorm(): assert_array_equal(bn(vals), expected) # with a single region and interpolation - expected = [-1, 0, 0, 0, 3, 3] + expected = [-1, 1, 1, 1, 3, 3] bn = mcolors.BoundaryNorm([0, 2.2], ncolors) assert_array_equal(bn(vals), expected) From 39b5dfcb34e3551873671d389b21590bb678c53b Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 5 Jul 2020 15:08:45 -0400 Subject: [PATCH 7/7] DOC: make doc string slightly more precise --- 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 e9e411d72206..f89017b87a82 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1429,7 +1429,7 @@ def __init__(self, boundaries, ncolors, clip=False, *, extend='neither'): Parameters ---------- boundaries : array-like - Monotonically increasing sequence of boundaries. + Monotonically increasing sequence of at least 2 boundaries. ncolors : int Number of colors in the colormap to be used. clip : bool, optional