Repository: napari/napari

Issue:
Adding 0 and 1 coords if missed in colormap
# Description
<!-- What does this pull request (PR) do? Why is it necessary? -->
<!-- Tell us about your new feature, improvement, or fix! -->
<!-- If your change includes user interface changes, please add an image, or an animation "An image is worth a thousando words!" -->
<!-- You can use https://www.cockos.com/licecap/ or similar to create animations -->
Fix #2400

## Type of change
<!-- Please delete options that are not relevant. -->


# References
<!-- What resources, documentation, and guides were used in the creation of this PR? -->
<!-- If this is a bug-fix or otherwise resolves an issue, reference it here with "closes #(issue)" -->

# How has this been tested?
<!-- Please describe the tests that you ran to verify your changes. -->
- [ ] example: the test suite for my feature covers cases x, y, and z
- [ ] example: all tests pass with my change

## Final checklist:
- [ ] My PR is the minimum possible work for the desired functionality
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] I have added tests that prove my fix is effective or that my feature works

Adding 0 and 1 coords if missed in colormap
# Description
<!-- What does this pull request (PR) do? Why is it necessary? -->
<!-- Tell us about your new feature, improvement, or fix! -->
<!-- If your change includes user interface changes, please add an image, or an animation "An image is worth a thousando words!" -->
<!-- You can use https://www.cockos.com/licecap/ or similar to create animations -->
Fix #2400

## Type of change
<!-- Please delete options that are not relevant. -->


# References
<!-- What resources, documentation, and guides were used in the creation of this PR? -->
<!-- If this is a bug-fix or otherwise resolves an issue, reference it here with "closes #(issue)" -->

# How has this been tested?
<!-- Please describe the tests that you ran to verify your changes. -->
- [ ] example: the test suite for my feature covers cases x, y, and z
- [ ] example: all tests pass with my change

## Final checklist:
- [ ] My PR is the minimum possible work for the desired functionality
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] I have added tests that prove my fix is effective or that my feature works



Discussion:
I don't like adding the `vispy_controls` attribute. I'd rather leave our colormap object simpler, and more independent of vispy. 

The more I look at it the more I find it magically to be guessing that a user wanted us to add `0` and `1` to the controls, I think instead we should raise an error if controls don't being with `0`, end with `1`, and be strictly increasing. We could check for all three properties in the validator here https://github.com/napari/napari/blob/1dbb8bfbd07e9aae1fbb0e0ac864abf88e0d8af4/napari/utils/colormaps/colormap.py#L60

We should then add a line to the documentation saying controls must start with `0` and `1` and be strictly increasing.

Maybe we can ask @jni for a third opinion if you disagree, but to me that is the cleanest and most robust solution.
> I don't like adding the `vispy_controls` attribute. I'd rather leave our colormap object simpler, and more independent of vispy.

How it introduces a dependency on vispy? Its only transforms inner representation to this one which is accepted by vispy. Maybe this method should be private?


# [Codecov](https://codecov.io/gh/napari/napari/pull/2422?src=pr&el=h1) Report
> Merging [#2422](https://codecov.io/gh/napari/napari/pull/2422?src=pr&el=desc) (19a2884) into [master](https://codecov.io/gh/napari/napari/commit/447d7bfd1f604e75656a201259b0348e2e33fb00?el=desc) (447d7bf) will **decrease** coverage by `0.33%`.
> The diff coverage is `98.14%`.

[![Impacted file tree graph](https://codecov.io/gh/napari/napari/pull/2422/graphs/tree.svg?width=650&height=150&src=pr&token=1LfdDeezHt)](https://codecov.io/gh/napari/napari/pull/2422?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #2422      +/-   ##
==========================================
- Coverage   81.31%   80.98%   -0.34%     
==========================================
  Files         450      452       +2     
  Lines       37373    37844     +471     
==========================================
+ Hits        30390    30647     +257     
- Misses       6983     7197     +214     
```


| [Impacted Files](https://codecov.io/gh/napari/napari/pull/2422?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [napari/utils/colormaps/colormap.py](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree#diff-bmFwYXJpL3V0aWxzL2NvbG9ybWFwcy9jb2xvcm1hcC5weQ==) | `96.82% <95.65%> (-0.74%)` | :arrow_down: |
| [napari/utils/colormaps/\_tests/test\_colormap.py](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree#diff-bmFwYXJpL3V0aWxzL2NvbG9ybWFwcy9fdGVzdHMvdGVzdF9jb2xvcm1hcC5weQ==) | `100.00% <100.00%> (ø)` | |
| [napari/utils/colormaps/\_tests/test\_colormaps.py](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree#diff-bmFwYXJpL3V0aWxzL2NvbG9ybWFwcy9fdGVzdHMvdGVzdF9jb2xvcm1hcHMucHk=) | `100.00% <100.00%> (ø)` | |
| [napari/utils/colormaps/colormap\_utils.py](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree#diff-bmFwYXJpL3V0aWxzL2NvbG9ybWFwcy9jb2xvcm1hcF91dGlscy5weQ==) | `92.07% <100.00%> (+0.09%)` | :arrow_up: |
| [napari/layers/image/experimental/\_chunk\_set.py](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree#diff-bmFwYXJpL2xheWVycy9pbWFnZS9leHBlcmltZW50YWwvX2NodW5rX3NldC5weQ==) | `0.00% <0.00%> (-50.00%)` | :arrow_down: |
| [napari/layers/image/experimental/octree\_image.py](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree#diff-bmFwYXJpL2xheWVycy9pbWFnZS9leHBlcmltZW50YWwvb2N0cmVlX2ltYWdlLnB5) | `0.00% <0.00%> (-35.22%)` | :arrow_down: |
| [napari/layers/image/experimental/\_octree\_slice.py](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree#diff-bmFwYXJpL2xheWVycy9pbWFnZS9leHBlcmltZW50YWwvX29jdHJlZV9zbGljZS5weQ==) | `0.00% <0.00%> (-35.22%)` | :arrow_down: |
| [...i/layers/image/experimental/octree\_tile\_builder.py](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree#diff-bmFwYXJpL2xheWVycy9pbWFnZS9leHBlcmltZW50YWwvb2N0cmVlX3RpbGVfYnVpbGRlci5weQ==) | `0.00% <0.00%> (-33.34%)` | :arrow_down: |
| [napari/layers/image/experimental/octree.py](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree#diff-bmFwYXJpL2xheWVycy9pbWFnZS9leHBlcmltZW50YWwvb2N0cmVlLnB5) | `0.00% <0.00%> (-22.45%)` | :arrow_down: |
| [napari/layers/image/experimental/\_octree\_loader.py](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree#diff-bmFwYXJpL2xheWVycy9pbWFnZS9leHBlcmltZW50YWwvX29jdHJlZV9sb2FkZXIucHk=) | `0.00% <0.00%> (-20.00%)` | :arrow_down: |
| ... and [13 more](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/napari/napari/pull/2422?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/napari/napari/pull/2422?src=pr&el=footer). Last update [447d7bf...19a2884](https://codecov.io/gh/napari/napari/pull/2422?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

> How it introduces a dependency on vispy? Its only transforms inner representation to this one which is accepted by vispy. Maybe this method should be private?

It's not a dependency, it's just a coupling that adds complexity. My preference is still to just check the input and raise an error if the input does not conform to what it needs to be
For me forcing 0, 1 will force to add additional complexity for colormap create widget. But it is doable. 
I think I see both points, the behaviour is magical but convenient. In the past I have often regretted magical behaviour, as it can lead to difficult to debug edge cases. But I'm ok including it here for now. At any rate, I see @sofroniewn's concern that these changes would make our colormaps behave differently in the context of vispy vs outside of it, so I support his idea of adding the 0-1 bounds for all colormaps, not just for vispy, and doing this in the validator method.
Why behave differently? This implementation mimics current color bar behavior.
@Czaki because self.map will use self.controls, not self.vispy_controls. Using the validator methods to pad the control points will achieve what you want without doing anything related to vispy, won't it?
But `vispy_controls` is the only minimum set of points to restore colormap on the range `[0,1]`. 
sorry, I don't think we are understanding each other, but I might be missing the problem... I'm saying, yes, vispy_controls is what you need to get [0, 1]. Let's make *controls* do the same thing and then we don't need vispy_controls: controls should provide the colormap in [0, 1].
> sorry, I don't think we are understanding each other, but I might be missing the problem... I'm saying, yes, vispy_controls is what you need to get [0, 1]. Let's make _controls_ do the same thing and then we don't need vispy_controls: controls should provide the colormap in [0, 1].

You think about autofix or raise an exception? 
> You think about autofix or raise an exception?

I think @jni is proposing to do the autofix. Let's just do the autofix inside the validator, and adjust the doc string as such. You might as well also check that the control points are strictly increasing after the fix, and raise an error if not.
> I think @jni is proposing to do the autofix. Let's just do the autofix inside the validator, and adjust the doc string as such. You might as well also check that the control points are strictly increasing after the fix, and raise an error if not.

Could I manipulate one variable when validate second one? (when add controls the need to add colors)
> Could I manipulate one variable when validate second one? (when add controls the need to add colors)

Yes, you can use a [root validator](https://pydantic-docs.helpmanual.io/usage/validators/#root-validators) if needed. You can also use that to check the length of the colors and the control points remain in sync
Did I should add a warning message?
> Did I should add a warning message?

Yes, let's add warnings. Can you then test for them in your test too
It looks like I do not understand all usage of Colormaps.
There is a failure of `test_empty_layer_with_edge_colormap` which uses a single point `Colormap`.

Could any of you check why using single-point colormap is expected and how should be handled? 
I think there might be something funky happening here with the `grays` colormap. If you change it to `gray` does the test now pass? I've noticed weird things with that one in the past too. It could be that vispy has a different default way to magically use a single point colormap. Maybe a reason to go back to just raising an error (which could then exclude the bad `grays` map). I'm not sure though, I can try looking in more when I get the chance
This is not from vispy, but from napari and I think that it should be handled on napari side:

https://github.com/napari/napari/blob/f801f184d2f70ddf90e20c5b13fff9f32f9fb18f/napari/utils/colormaps/colormap_utils.py#L85-L90

I push my suggestion of solve to this PR. 
I don't like adding the `vispy_controls` attribute. I'd rather leave our colormap object simpler, and more independent of vispy. 

The more I look at it the more I find it magically to be guessing that a user wanted us to add `0` and `1` to the controls, I think instead we should raise an error if controls don't being with `0`, end with `1`, and be strictly increasing. We could check for all three properties in the validator here https://github.com/napari/napari/blob/1dbb8bfbd07e9aae1fbb0e0ac864abf88e0d8af4/napari/utils/colormaps/colormap.py#L60

We should then add a line to the documentation saying controls must start with `0` and `1` and be strictly increasing.

Maybe we can ask @jni for a third opinion if you disagree, but to me that is the cleanest and most robust solution.
> I don't like adding the `vispy_controls` attribute. I'd rather leave our colormap object simpler, and more independent of vispy.

How it introduces a dependency on vispy? Its only transforms inner representation to this one which is accepted by vispy. Maybe this method should be private?


# [Codecov](https://codecov.io/gh/napari/napari/pull/2422?src=pr&el=h1) Report
> Merging [#2422](https://codecov.io/gh/napari/napari/pull/2422?src=pr&el=desc) (19a2884) into [master](https://codecov.io/gh/napari/napari/commit/447d7bfd1f604e75656a201259b0348e2e33fb00?el=desc) (447d7bf) will **decrease** coverage by `0.33%`.
> The diff coverage is `98.14%`.

[![Impacted file tree graph](https://codecov.io/gh/napari/napari/pull/2422/graphs/tree.svg?width=650&height=150&src=pr&token=1LfdDeezHt)](https://codecov.io/gh/napari/napari/pull/2422?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #2422      +/-   ##
==========================================
- Coverage   81.31%   80.98%   -0.34%     
==========================================
  Files         450      452       +2     
  Lines       37373    37844     +471     
==========================================
+ Hits        30390    30647     +257     
- Misses       6983     7197     +214     
```


| [Impacted Files](https://codecov.io/gh/napari/napari/pull/2422?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [napari/utils/colormaps/colormap.py](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree#diff-bmFwYXJpL3V0aWxzL2NvbG9ybWFwcy9jb2xvcm1hcC5weQ==) | `96.82% <95.65%> (-0.74%)` | :arrow_down: |
| [napari/utils/colormaps/\_tests/test\_colormap.py](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree#diff-bmFwYXJpL3V0aWxzL2NvbG9ybWFwcy9fdGVzdHMvdGVzdF9jb2xvcm1hcC5weQ==) | `100.00% <100.00%> (ø)` | |
| [napari/utils/colormaps/\_tests/test\_colormaps.py](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree#diff-bmFwYXJpL3V0aWxzL2NvbG9ybWFwcy9fdGVzdHMvdGVzdF9jb2xvcm1hcHMucHk=) | `100.00% <100.00%> (ø)` | |
| [napari/utils/colormaps/colormap\_utils.py](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree#diff-bmFwYXJpL3V0aWxzL2NvbG9ybWFwcy9jb2xvcm1hcF91dGlscy5weQ==) | `92.07% <100.00%> (+0.09%)` | :arrow_up: |
| [napari/layers/image/experimental/\_chunk\_set.py](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree#diff-bmFwYXJpL2xheWVycy9pbWFnZS9leHBlcmltZW50YWwvX2NodW5rX3NldC5weQ==) | `0.00% <0.00%> (-50.00%)` | :arrow_down: |
| [napari/layers/image/experimental/octree\_image.py](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree#diff-bmFwYXJpL2xheWVycy9pbWFnZS9leHBlcmltZW50YWwvb2N0cmVlX2ltYWdlLnB5) | `0.00% <0.00%> (-35.22%)` | :arrow_down: |
| [napari/layers/image/experimental/\_octree\_slice.py](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree#diff-bmFwYXJpL2xheWVycy9pbWFnZS9leHBlcmltZW50YWwvX29jdHJlZV9zbGljZS5weQ==) | `0.00% <0.00%> (-35.22%)` | :arrow_down: |
| [...i/layers/image/experimental/octree\_tile\_builder.py](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree#diff-bmFwYXJpL2xheWVycy9pbWFnZS9leHBlcmltZW50YWwvb2N0cmVlX3RpbGVfYnVpbGRlci5weQ==) | `0.00% <0.00%> (-33.34%)` | :arrow_down: |
| [napari/layers/image/experimental/octree.py](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree#diff-bmFwYXJpL2xheWVycy9pbWFnZS9leHBlcmltZW50YWwvb2N0cmVlLnB5) | `0.00% <0.00%> (-22.45%)` | :arrow_down: |
| [napari/layers/image/experimental/\_octree\_loader.py](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree#diff-bmFwYXJpL2xheWVycy9pbWFnZS9leHBlcmltZW50YWwvX29jdHJlZV9sb2FkZXIucHk=) | `0.00% <0.00%> (-20.00%)` | :arrow_down: |
| ... and [13 more](https://codecov.io/gh/napari/napari/pull/2422/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/napari/napari/pull/2422?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/napari/napari/pull/2422?src=pr&el=footer). Last update [447d7bf...19a2884](https://codecov.io/gh/napari/napari/pull/2422?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

> How it introduces a dependency on vispy? Its only transforms inner representation to this one which is accepted by vispy. Maybe this method should be private?

It's not a dependency, it's just a coupling that adds complexity. My preference is still to just check the input and raise an error if the input does not conform to what it needs to be
For me forcing 0, 1 will force to add additional complexity for colormap create widget. But it is doable. 
I think I see both points, the behaviour is magical but convenient. In the past I have often regretted magical behaviour, as it can lead to difficult to debug edge cases. But I'm ok including it here for now. At any rate, I see @sofroniewn's concern that these changes would make our colormaps behave differently in the context of vispy vs outside of it, so I support his idea of adding the 0-1 bounds for all colormaps, not just for vispy, and doing this in the validator method.
Why behave differently? This implementation mimics current color bar behavior.
@Czaki because self.map will use self.controls, not self.vispy_controls. Using the validator methods to pad the control points will achieve what you want without doing anything related to vispy, won't it?
But `vispy_controls` is the only minimum set of points to restore colormap on the range `[0,1]`. 
sorry, I don't think we are understanding each other, but I might be missing the problem... I'm saying, yes, vispy_controls is what you need to get [0, 1]. Let's make *controls* do the same thing and then we don't need vispy_controls: controls should provide the colormap in [0, 1].
> sorry, I don't think we are understanding each other, but I might be missing the problem... I'm saying, yes, vispy_controls is what you need to get [0, 1]. Let's make _controls_ do the same thing and then we don't need vispy_controls: controls should provide the colormap in [0, 1].

You think about autofix or raise an exception? 
> You think about autofix or raise an exception?

I think @jni is proposing to do the autofix. Let's just do the autofix inside the validator, and adjust the doc string as such. You might as well also check that the control points are strictly increasing after the fix, and raise an error if not.
> I think @jni is proposing to do the autofix. Let's just do the autofix inside the validator, and adjust the doc string as such. You might as well also check that the control points are strictly increasing after the fix, and raise an error if not.

Could I manipulate one variable when validate second one? (when add controls the need to add colors)
> Could I manipulate one variable when validate second one? (when add controls the need to add colors)

Yes, you can use a [root validator](https://pydantic-docs.helpmanual.io/usage/validators/#root-validators) if needed. You can also use that to check the length of the colors and the control points remain in sync
Did I should add a warning message?
> Did I should add a warning message?

Yes, let's add warnings. Can you then test for them in your test too
It looks like I do not understand all usage of Colormaps.
There is a failure of `test_empty_layer_with_edge_colormap` which uses a single point `Colormap`.

Could any of you check why using single-point colormap is expected and how should be handled? 
I think there might be something funky happening here with the `grays` colormap. If you change it to `gray` does the test now pass? I've noticed weird things with that one in the past too. It could be that vispy has a different default way to magically use a single point colormap. Maybe a reason to go back to just raising an error (which could then exclude the bad `grays` map). I'm not sure though, I can try looking in more when I get the chance
This is not from vispy, but from napari and I think that it should be handled on napari side:

https://github.com/napari/napari/blob/f801f184d2f70ddf90e20c5b13fff9f32f9fb18f/napari/utils/colormaps/colormap_utils.py#L85-L90

I push my suggestion of solve to this PR. 

Resolving patch:
diff --git a/napari/utils/colormaps/colormap.py b/napari/utils/colormaps/colormap.py
--- a/napari/utils/colormaps/colormap.py
+++ b/napari/utils/colormaps/colormap.py
@@ -67,11 +67,44 @@ def _ensure_color_array(cls, v):
     # controls validator must be called even if None for correct initialization
     @validator('controls', pre=True, always=True)
     def _check_controls(cls, v, values):
+        # If no control points provided generate defaults
         if v is None or len(v) == 0:
             n_controls = len(values['colors']) + int(
                 values['interpolation'] == ColormapInterpolationMode.ZERO
             )
             return np.linspace(0, 1, n_controls)
+
+        # Check control end points are correct
+        if not (v[0] == 0 and v[-1] == 1):
+            raise ValueError(
+                trans._(
+                    f'Control points must start with 0.0 and end with 1.0. Got {v[0]} and {v[-1]}',
+                    deferred=True,
+                )
+            )
+
+        # Check control points are sorted correctly
+        if not np.array_equal(v, sorted(v)):
+            raise ValueError(
+                trans._(
+                    'Control points need to be sorted in ascending order',
+                    deferred=True,
+                )
+            )
+
+        # Check number of control points is correct
+        n_controls_target = len(values['colors']) + int(
+            values['interpolation'] == ColormapInterpolationMode.ZERO
+        )
+        n_controls = len(v)
+        if not n_controls == n_controls_target:
+            raise ValueError(
+                trans._(
+                    f'Wrong number of control points provided. Expected {n_controls_target}, got {n_controls}',
+                    deferred=True,
+                )
+            )
+
         return v
 
     def __iter__(self):


Test patch:
diff --git a/napari/utils/colormaps/_tests/test_colormap.py b/napari/utils/colormaps/_tests/test_colormap.py
--- a/napari/utils/colormaps/_tests/test_colormap.py
+++ b/napari/utils/colormaps/_tests/test_colormap.py
@@ -1,4 +1,5 @@
 import numpy as np
+import pytest
 
 from napari.utils.colormaps import Colormap
 
@@ -27,6 +28,38 @@ def test_linear_colormap_with_control_points():
     np.testing.assert_almost_equal(cmap.map([0.75]), [[0, 1, 0, 1]])
 
 
+def test_non_ascending_control_points():
+    """Test non ascending control points raises an error."""
+    colors = np.array(
+        [[0, 0, 0, 1], [0, 0.5, 0, 1], [0, 1, 0, 1], [0, 0, 1, 1]]
+    )
+    with pytest.raises(ValueError):
+        Colormap(colors, name='testing', controls=[0, 0.75, 0.25, 1])
+
+
+def test_wrong_number_control_points():
+    """Test wrong number of control points raises an error."""
+    colors = np.array(
+        [[0, 0, 0, 1], [0, 0.5, 0, 1], [0, 1, 0, 1], [0, 0, 1, 1]]
+    )
+    with pytest.raises(ValueError):
+        Colormap(colors, name='testing', controls=[0, 0.75, 1])
+
+
+def test_wrong_start_control_point():
+    """Test wrong start of control points raises an error."""
+    colors = np.array([[0, 0, 0, 1], [0, 1, 0, 1], [0, 0, 1, 1]])
+    with pytest.raises(ValueError):
+        Colormap(colors, name='testing', controls=[0.1, 0.75, 1])
+
+
+def test_wrong_end_control_point():
+    """Test wrong end of control points raises an error."""
+    colors = np.array([[0, 0, 0, 1], [0, 1, 0, 1], [0, 0, 1, 1]])
+    with pytest.raises(ValueError):
+        Colormap(colors, name='testing', controls=[0, 0.75, 0.9])
+
+
 def test_binned_colormap():
     """Test a binned colormap."""
     colors = np.array([[0, 0, 0, 1], [0, 1, 0, 1], [0, 0, 1, 1]])
@@ -60,6 +93,6 @@ def test_colormap_equality():
     colors = np.array([[0, 0, 0, 1], [0, 1, 0, 1], [0, 0, 1, 1]])
     cmap_1 = Colormap(colors, name='testing', controls=[0, 0.75, 1])
     cmap_2 = Colormap(colors, name='testing', controls=[0, 0.75, 1])
-    cmap_3 = Colormap(colors, name='testing', controls=[0, 1])
+    cmap_3 = Colormap(colors, name='testing', controls=[0, 0.25, 1])
     assert cmap_1 == cmap_2
     assert cmap_1 != cmap_3