Repository: napari/napari

Issue:
the "order" layer-creation parameter is not parsed as expected
## 🐛 Bug

`napari.view_image` does not parse the `order` parameter how I expecte.  `order=(2,1,0)` will return a viewer where `viewer.dims.order` is `(0,2,1)`.

Using an array of shape `(100,100,10)` and `order=(2,1,0)`, I get the YZ plane:
![image](https://user-images.githubusercontent.com/671841/132202492-e608c7aa-d247-4687-9719-373a4044f457.png)

Setting the order with `viewer.dims.order = (2,1,0)` works:

![image](https://user-images.githubusercontent.com/671841/132201037-3002611f-81ab-47af-a1b2-4852afad3046.png)


## To Reproduce

Steps to reproduce the behavior:

```
import napari
import numpy as np

data = np.full(shape=(100,100,10), dtype=np.uint8, fill_value=255)
viewer = napari.view_image(data, order=(2,1,0))
print(viewer.dims.order)       # output (0, 2, 1) ???
#viewer.dims.order = (2,1,0)   # Uncomment to correctly draw the 100x100 layer
napari.run()
```

<!-- If you have a code sample, error messages, stack traces, please provide it here as well -->

## Expected behavior

<!-- A clear and concise description of what you expected to happen. -->
Passing in `order` as a layer-creation parameters should behave identically to setting the `dims.order` property.


## Environment

 - Please copy and paste the information at napari info option in help menubar here:

napari: 0.4.11
Platform: macOS-11.5.2-x86_64-i386-64bit
System: MacOS 11.5.2
Python: 3.9.6 (default, Jun 29 2021, 05:25:02) [Clang 12.0.5 (clang-1205.0.22.9)]
Qt: 5.15.2
PyQt5: 5.15.4
NumPy: 1.21.0
SciPy: 1.7.0
Dask: 2021.06.2
VisPy: 0.8.1

OpenGL:
- GL version: 2.1 ATI-4.6.20
- MAX_TEXTURE_SIZE: 16384

Screens:
- screen 1: resolution 1792x1120, scale 2.0

Plugins:
- console: 0.0.4
- scikit-image: 0.4.11
- svg: 0.1.5

 - Any other relevant information:

## Additional context

<!-- Add any other context about the problem here. -->



Discussion:
hmm something sounds funny here. maybe @DragaDoncila could look into reproducing and make a recommendation. not sure if @jni has thoughts here too?
I've looked a bit into this and it's happening because the default `ndim` for the `Viewer` on creation is 2, and [this block of code](https://github.com/napari/napari/blob/79ab3589eb398ce56d2d0a16025cdb7f435ffaa5/napari/components/dims.py#L112) rearranges the passed order of `(2, 1, 0)` to just be `(1, 0)` - which is still technically correct.

But then when we add a new layer, the `dims.ndim` gets updated to 3 and we rebuild the `order` incorrectly, because [line 115 here](https://github.com/napari/napari/blob/79ab3589eb398ce56d2d0a16025cdb7f435ffaa5/napari/components/dims.py#L115) assumes that the first dimensions will be first in the order.

I'm not really sure what we should do here - maybe if we get passed an order on `__init__` we should set `ndim` to that? Or at the very least we should warn the user when we reduce order? @sofroniewn you are probably more familiar with this
eek! good detective work. ok i'm not sure what we should do either. maybe once we have better axes labels figured out we can improve this. @andy-sweet might have ideas too
Thanks @DragaDoncila for finding the cause!

I would have a slight preference for having ndim updated rather than a warning. `napari.view_image(...)` is touted in many examples as _the_ method to view data, and I doubt I'm the only one who is trying to load data with a warped dimension order.  (Why did I not transpose the array? Well, the actual data I was passing in was not a true numpy array...)

In any case, I definitely agree with the extremely low severity label. 😄 

Happy to give some thoughts, but I don't think there going to help fix these in the short term!

I've made a little progress on axes labels (#906) and ran into a similar (though not as confusing) issue/cause, as `axis_labels` are also trimmed/padded to have a length that corresponds to the default value of `ndim=2`. So you get behavior like the following.

```python
viewer_model = ViewerModel(axis_labels=('channel', 'time', 'z', 'y', 'x'))
viewer_model.dims.axis_labels
# ('y', 'x')
viewer_model.dims.ndim = 3
viewer_model.dims.axis_labels
# ('0', 'y', 'x')
```

As #906 is about giving each layer its own `axis_labels`, I am trying to understand if `ViewerModel` needs to have its own `axis_labels` because the set of axis labels can be derived from the union of all the layers' labels. One way that `ViewerModel.axis_labels` could be useful is to specify an ordering over that set of labels, so maybe that's one way we could specify the desired view order. Something like the following.

```python
viewer_model = ViewerModel()
viewer_model.add_image(..., axis_labels=('time', 'y', 'x'))
viewer_model.axis_labels
# ('time', 'y', 'x')
viewer_model.add_image(..., axis_labels=('x', 'y', 'z')
viewer_model.axis_labels
# ('z', 'time', 'y', 'x') - 'z' is new, so prepend it
viewer_model.axis_labels = ('time', 'z', 'y', 'x')
# user doesn't like that ordering so specifies a new one
```

Of course, this works nicely if axis labels were specified, but by default they probably won't be, so we should also accept something like `viewer_model.axis_labels = (2, 3, 1, 0)`. If `axis_labels` are given that are not compatible with the current layers, then I think we should error.

In the long term, I'm thinking that `axis_labels`, `order`, and maybe other things are not parameters that you should pass to `ViewerModel` at construction time when there are no layers, data, or dimensions yet, but only once you have added some data.
> In the long term, I'm thinking that axis_labels, order, and maybe other things are not parameters that you should pass to ViewerModel at construction time when there are no layers, data, or dimensions yet, but only once you have added some data.

Yes, agreed - maybe answer here is to not allow these to be passed to `Viewer` or a `view_*` method directly. Will take a bit of time to get there, but I like that direction.

Thoughts on all this @jni? 
I agree with @andy-sweet that there are some attributes on dims that only make sense once you have added some data - `order` being one of them.

> Yes, agreed - maybe answer here is to not allow these to be passed to Viewer or a view_* method directly. Will take a bit of time to get there, but I like that direction.

I kind of don't agree with this simply because in the particular case of the `view_*` methods, we *are* getting data, and so any parameters which may not make sense on `Viewer` creation would definitely make sense by the end of the function call because we've added a layer.

We could therefore (in the short term) move some of these `Viewer` arguments passed to `view_*` methods to be set on the viewer after we've added the layer  - assuming we have a concrete list of which arguments need this - maybe anything on dims?

Resolving patch:
diff --git a/napari/view_layers.py b/napari/view_layers.py
--- a/napari/view_layers.py
+++ b/napari/view_layers.py
@@ -16,6 +16,8 @@ def view_<layer_type>(*args, **kwargs):
 
 from numpydoc.docscrape import NumpyDocString as _NumpyDocString
 
+from napari.components.dims import Dims
+
 from .viewer import Viewer
 
 __all__ = [
@@ -107,16 +109,23 @@ def _merge_layer_viewer_sigs_docs(func):
 
 
 _viewer_params = inspect.signature(Viewer).parameters
+_dims_params = inspect.signature(Dims).parameters
 
 
 def _make_viewer_then(add_method: str, args, kwargs) -> Viewer:
     """Utility function that creates a viewer, adds a layer, returns viewer."""
     vkwargs = {k: kwargs.pop(k) for k in list(kwargs) if k in _viewer_params}
+    # separate dims kwargs because we want to set those after adding data
+    dims_kwargs = {
+        k: vkwargs.pop(k) for k in list(vkwargs) if k in _dims_params
+    }
     viewer = Viewer(**vkwargs)
     if 'kwargs' in kwargs:
         kwargs.update(kwargs.pop("kwargs"))
     method = getattr(viewer, add_method)
     method(*args, **kwargs)
+    for arg_name, arg_val in dims_kwargs.items():
+        setattr(viewer.dims, arg_name, arg_val)
     return viewer
 
 


Test patch:
diff --git a/napari/_tests/test_view_layers.py b/napari/_tests/test_view_layers.py
--- a/napari/_tests/test_view_layers.py
+++ b/napari/_tests/test_view_layers.py
@@ -159,6 +159,6 @@ def test_kwargs_passed(monkeypatch):
         scale=(1, 2, 3),
     )
     assert viewer_mock.mock_calls == [
-        call(title='my viewer', ndisplay=3),
+        call(title='my viewer'),
         call().open(path='some/path', name='img name', scale=(1, 2, 3)),
     ]