Repository: napari/napari

Issue:
Initializing a points layer with 3D empty data creates a 2D layer
### 🐛 Bug Report

See the title and the failing test.

### 💡 Steps to Reproduce

The following test fails.

```python
def test_empty_3d_points():
    pts = Points(np.empty((0, 3)))
    assert pts.ndim == 3
```

### 💡 Expected Behavior

I would expect to get a 3D layer when passing a numpy array of shape (0, 3).

### 🌎 Environment

napari: 0.5.0a2.dev495+gd507dd64e
Platform: macOS-10.16-x86_64-i386-64bit
System: MacOS 14.2.1
Python: 3.11.5 (main, Sep 11 2023, 08:19:27) [Clang 14.0.6 ]
Qt: 6.6.1
PyQt6: 
NumPy: 1.26.2
SciPy: 1.11.4
Dask: 2023.12.1
VisPy: 0.14.1
magicgui: 0.8.1
superqt: 0.6.1
in-n-out: 0.1.9
app-model: 0.2.4
npe2: 0.7.3

OpenGL:
  - GL version:  2.1 INTEL-22.1.29
  - MAX_TEXTURE_SIZE: 16384

Screens:
  - screen 1: resolution 3440x1440, scale 1.0
  - screen 2: resolution 1440x900, scale 2.0

Settings path:
  - /Users/asweet/Library/Application Support/napari/napari-dev_94a2c6ecd2bb4e54de7c17f279d1ba0ad46bb93d/settings.yaml
Plugins:
  - napari: 0.5.0a2.dev495+gd507dd64e (77 contributions)
  - napari-console: 0.0.9 (0 contributions)
  - napari-svg: 0.1.10 (2 contributions)

### 💡 Additional Context

Not dissimilar to #2273, but also not the same.


Discussion:
I think the bug is here:

https://github.com/napari/napari/blob/4a1b0b547f2dc6ae804f66af77a5d7154820463b/napari/layers/points/_points_utils.py#L236

It should be easy to solve. If someone do it fast, it may land in 0.4.19 
I don't think this is particularly critical and it's also easy to workaround (by specifying `ndim=3`), so need to hold up v0.4.19 for a fix. But if someone does fix it and it's easy to cherry-pick, that's also good.
Please excuse the stupid question, but why would shape (0, 3) be 3D? isn't that still just 2 dimensions?
Because in general, points data has shape `(n, dim)` where `n` is a number of points. 
Hi, just in case, maybe adding a validation around https://github.com/napari/napari/blob/4a1b0b547f2dc6ae804f66af77a5d7154820463b/napari/layers/points/points.py#L381-L384 before `fix_data_points` is called to initialize `ndim`, so something like:

```python
        if ndim is None:
            if scale is not None:
                ndim = len(scale)
            elif data is not None and len(data.shape) == 2:
                ndim = data.shape[1]
``` 

Also, is there any other shape that the data could have when provided that would also need for `ndim` to be initialized over the layer constructor when the kwarg `ndim` is not passed?

Resolving patch:
diff --git a/napari/layers/points/points.py b/napari/layers/points/points.py
--- a/napari/layers/points/points.py
+++ b/napari/layers/points/points.py
@@ -378,8 +378,15 @@ def __init__(
         shown=True,
         projection_mode='none',
     ) -> None:
-        if ndim is None and scale is not None:
-            ndim = len(scale)
+        if ndim is None:
+            if scale is not None:
+                ndim = len(scale)
+            elif (
+                data is not None
+                and hasattr(data, 'shape')
+                and len(data.shape) == 2
+            ):
+                ndim = data.shape[1]
 
         data, ndim = fix_data_points(data, ndim)
 


Test patch:
diff --git a/napari/layers/points/_tests/test_points.py b/napari/layers/points/_tests/test_points.py
--- a/napari/layers/points/_tests/test_points.py
+++ b/napari/layers/points/_tests/test_points.py
@@ -49,6 +49,12 @@ def _make_cycled_properties(values, length):
 def test_empty_points():
     pts = Points()
     assert pts.data.shape == (0, 2)
+    assert pts.ndim == 2
+
+
+def test_3d_empty_points():
+    pts = Points(np.empty((0, 3)))
+    assert pts.ndim == 3
 
 
 def test_empty_points_with_features():