Repository: napari/napari

Issue:
Adding points with properties / empty properties
## 🐛 Bug

@kevinyamauchi I'm trying to create a basic demo where I load an empty points layer with a properties dict with an empty list - say named "class", which I can then populate as I add points - leveraging a combination of custom keybindings / mouse functions. The overall goal is to create a simple demo of how to annotate a movie for input into a tracking program like DeepLabCut. The reason I'm doing this is to try and learn about whether our Points / keybindings / mouse functions APIs are enabling real world use cases like that. I'm running into a couple problems though.

First is around initializing the empty points layer:

<details>

```python
"""
Displays empty points with properties
"""

import napari
import numpy as np


with napari.gui_qt():
    # create an empty viewer
    viewer = napari.Viewer()
    viewer.add_image(np.random.random((5, 400, 400)))

    # create properties for each point
    properties = {
        'class': np.array([]),
    }

    # define the color cycle for the face_color annotation
    face_color_cycle = ['red', 'blue', 'green', 'cyan', 'magenta', 'yellow']

    points_layer = viewer.add_points(
        properties=properties,
        size=10,
        edge_width=1,
        edge_color='white',
        face_color='class',
        face_color_cycle=face_color_cycle,
        name='points'
    )
```

```python-traceback
(base) czirwc1macos2701:image-demos nsofroniew$ python test-examples/points_properties_empty.py
Traceback (most recent call last):
  File "test-examples/points_properties_empty.py", line 29, in <module>
    name='points'
  File "/Users/nsofroniew/GitHub/napari/napari/components/add_layers_mixin.py", line 366, in add_points
    visible=visible,
  File "/Users/nsofroniew/GitHub/napari/napari/layers/points/points.py", line 336, in __init__
    self._current_face_color = self.face_color[-1]
IndexError: index -1 is out of bounds for axis 0 with size 0
```

The expected behavior is to be able to instantiate the layer.

</details>

The second error is around adding points when using properties

<details>

```python
"""
Displays points with properties
"""

import napari
import numpy as np


with napari.gui_qt():
    # create an empty viewer
    viewer = napari.Viewer()
    viewer.add_image(np.random.random((5, 400, 400)))

    points = np.array([[100, 100], [200, 200], [333, 111]])

    # create properties for each point
    properties = {
        'class': np.array([1, 2, 3]),
    }

    # define the color cycle for the face_color annotation
    face_color_cycle = ['red', 'blue', 'green', 'cyan', 'magenta', 'yellow']

    points_layer = viewer.add_points(
        points,
        properties=properties,
        size=10,
        edge_width=1,
        edge_color='white',
        face_color='class',
        face_color_cycle=face_color_cycle,
        name='points'
    )
```

If you now enter "add" mode and start clicking I then get the following error:

```python-traceback
Traceback (most recent call last):
  File "/Users/nsofroniew/GitHub/vispy/vispy/app/backends/_qt.py", line 447, in mousePressEvent
    modifiers=self._modifiers(ev),
  File "/Users/nsofroniew/GitHub/vispy/vispy/app/base.py", line 181, in _vispy_mouse_press
    ev = self._vispy_canvas.events.mouse_press(**kwargs)
  File "/Users/nsofroniew/GitHub/vispy/vispy/util/event.py", line 455, in __call__
    self._invoke_callback(cb, event)
  File "/Users/nsofroniew/GitHub/vispy/vispy/util/event.py", line 475, in _invoke_callback
    self, cb_event=(cb, event))
  File "/Users/nsofroniew/GitHub/vispy/vispy/util/event.py", line 471, in _invoke_callback
    cb(event)
  File "/Users/nsofroniew/GitHub/napari/napari/_qt/qt_viewer.py", line 338, in on_mouse_press
    self.layer_to_visual[layer].on_mouse_press(event)
  File "/Users/nsofroniew/GitHub/napari/napari/_vispy/vispy_base_layer.py", line 189, in on_mouse_press
    self.layer.on_mouse_press(event)
  File "/Users/nsofroniew/GitHub/napari/napari/layers/points/points.py", line 1513, in on_mouse_press
    self.add(self.coordinates)
  File "/Users/nsofroniew/GitHub/napari/napari/layers/points/points.py", line 1334, in add
    self.data = np.append(self.data, [coord], axis=0)
  File "/Users/nsofroniew/GitHub/napari/napari/layers/points/points.py", line 427, in data
    ][0]
IndexError: invalid index to scalar variable.
```

Expected behaviour - I should be able to add points, and the properties should just extend with default values (say last one, or `None`). I havn't tried removing points but that should also work

</details>

## Environment


```
napari: 0.2.11
Platform: Darwin-19.3.0-x86_64-i386-64bit
Python: 3.7.4 (default, Aug 13 2019, 15:17:50)  [Clang 4.0.1 (tags/RELEASE_401/final)]
Qt: 5.13.2
PySide2: 5.13.2
VisPy: 0.6.5.dev2+g9e0822db
NumPy: 1.18.1
SciPy: 1.3.1
scikit-image: 0.16.2
Dask: 2.5.2

GL version:  2.1 INTEL-14.4.23
MAX_TEXTURE_SIZE: 16384
```


Discussion:
Looks like the problematic line is `colors = self.face_color[self._indices_view]`, this maybe much easier to do after the new `_view` approach to properties. Note the issue is that we don't want all the face colors, we only want at most 1024 corresponding to the points that we randomly selected
Dang it! Thanks for flagging these, @sofroniewn . 

I hadn't made a test for the use case where the properties is initiated with keys and empty arrays (as oppose to an empty dict), but it makes sense. For an empty points layer with properties, what do you think default `Points.current_properties` value be? Maybe `''`?

For the point adding bug, I've pushed a fix it and added a test (#933 ). In short, there was a bug in how I was initializing `Points.current_properties`, so adding points worked when selecting a point first (to update `Points.current_properties`), but did not work when just adding points straight away without selecting one first. I added a test to cover that case.
Thanks for jumping on fixes so quickly. I'll do some thorough testing.

> default `Points.current_properties' value

I'm not sure - maybe the empty string, or None, or np.NaN? Curious what @jni thinks
>  I'm trying to create a basic demo where I load an empty points layer with a properties dict with an empty list - say named "class", which I can then populate as I add points - leveraging a combination of custom keybindings / mouse functions. The overall goal is to create a simple demo of how to annotate a movie for input into a tracking program like DeepLabCut.

Separate to the bug fixes, I'd love it if we could make a nice gist with this working well. Do you think you can take a stab at that too? Could be a nice "applications" tutorial.
> Separate to the bug fixes, I'd love it if we could make a nice gist with this working well. Do you think you can take a stab at that too? Could be a nice "applications" tutorial.

No problem! I will be traveling over the next week, so I will have less time, but I can work on that when I get back!
> I totally agree. My plan was to get all of the features into the model and then figure out the best way to interact with them via the GUI. I was thinking along the same lines as you and I am definitely open to suggestions!

@kevinyamauchi we can also wait till this is done, as by then it might become trivial. Sounds like we are on the same page :-)
@sofroniewn , I've started working on this (finally) and I am a little stuck on what should happen when the user attempts to add a point to a layer initialized without any data where a display property (e.g., `edge_color`) is set as a function of one of the properties.

That is, what should happen in this case:
```python
points_layer = viewer.add_points(
    properties={'label': np.empty(0)},
    edge_color='label',
)

points_layer.add([10, 10])
```
Should to `edge_color_mode` switch to direct and then set the edge color as the default? Should it not add a point and issue a warning? What are your thoughts?


Hmm, I think you definitely want to be able to add points, but it's unclear what a default value should be - that's the problem right? Can you make in None? What will that do? I guess right now we don't have a nice way for people to change these properties from the GUI. Switching to direct mode seems less desirable as people will be using properties to avoid that. What are your thoughts?
I agree that the ideal thing is to be able to add points. However, when the edge color is mapped to the `label` property, I am not sure how to assign the color. I think it's tricky because the model doesn't know what the valid `label` values are, so I don't know what a sensible default would be.

Perhaps we could somehow attach the valid values to the empty property array (I think pandas might do this with their categorical datatype). Alternatively, we could accept a default property values as an input argument. What do you think?
> Perhaps we could somehow attach the valid values to the empty property array (I think pandas might do this with their categorical datatype). Alternatively, we could accept a default property values as an input argument. What do you think?

If possible this would be nice
> I guess right now we don't have a nice way for people to change these properties from the GUI.

Do you have plans / ideas for this? It could help influence how we think about this. I think now that we have properties it might make sense to have some GUI for this, could be autogenerated. Maybe @tlambert03 has ideas?
> Perhaps we could somehow attach the valid values to the empty property array (I think pandas might do this with their categorical datatype). Alternatively, we could accept a default property values as an input argument. What do you think?
>> If possible this would be nice

I think accepting a default property as an input argument is straightforward, but it does add to the increasingly large number API. That being said, both matplotlib and seaborn seem to have gone the route of lots of kwargs with sensible defaults, so maybe it's the best way to balance customizability with usability.

I will look into the pandas categorical dtype. Even if it's possible, we would have to take on pandas as a dependency and I'm not sure this is sufficient rationale. Also, I'm not sure if it would be compatible with other dataframes that would be nice to support in the future (e.g., dask DataFrame, cuDF).

> Do you have plans / ideas for this? It could help influence how we think about this. I think now that we have properties it might make sense to have some GUI for this, could be autogenerated. Maybe @tlambert03 has ideas?

My thought when adding the properties was that it would be nice to have a selector for face color and edge color that sets their respective color modes. We could then change the UI elements of the edge color and face color UI elements depending on the color mode:

- `ColorMode.DIRECT` would have the color drop down menu we currently have (eventually a color picker)
- `ColorMode.CYCLE` would have a dropdown to select the property and a dropdown to select the value from the existing values in the property. I thought it would be too much to include a way to add or modify an existing value.
- `ColorMode.MAP` would have a dropdown to select the property.
> I think accepting a default property as an input argument is straightforward, but it does add to the increasingly large number API.

I prefer this option. I am less excited about "the pandas categorical dtype" for reasons you mention above.

> My thought when adding the properties was that it would be nice to have a selector for face color and edge color that sets their respective color modes.

Agreed

> I thought it would be too much to include a way to add or modify an existing value.

If there was a separate way to provide / edit a list of default values that makes even more sense. Maybe figuring that bit out is top priority

Based on our conversation at the sync last night, I will be implementing the following:
- Introduce a property to store user-provided property values  (`Points.default_property_values` or `Points.preset_property_values` or something else?). The user provided values can serve as default values and also provide a list of valid values for each property. The property will store a dictionary where the keys match the keys in `Points.properties` and the values are numpy arrays with the user-provided values.
- Initially, we will use the property to have fall back values if a user adds a point to an empty layer without setting `layer.current_properties`.
- Eventually, we can use the property to look up the current properties and values to add/edit them via the GUI. I think the total valid list should be the union of the values in `Points.properties` and `Points.default_property_values` (or whatever we call it) rather than using `Points.default_property_values` as a registry. I think synchronizing `Points.properties` and `Points.default_property_values` would be a bit thorny (e.g., what happens to the elements in `Points.properties` if a particular in-use value is deleted from `Points.default_property_values`?).

Does this seem right, @sofroniewn ?

Resolving patch:
diff --git a/napari/layers/points/_points_utils.py b/napari/layers/points/_points_utils.py
--- a/napari/layers/points/_points_utils.py
+++ b/napari/layers/points/_points_utils.py
@@ -103,7 +103,10 @@ def dataframe_to_properties(dataframe) -> Dict[str, np.ndarray]:
 def guess_continuous(property: np.ndarray) -> bool:
     """Guess if the property is continuous (return True) or categorical (return False)"""
     # if the property is a floating type, guess continuous
-    if issubclass(property.dtype.type, np.floating) and len(property < 16):
+    if (
+        issubclass(property.dtype.type, np.floating)
+        or len(np.unique(property)) > 16
+    ):
         return True
     else:
         return False
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
@@ -185,6 +185,9 @@ class Points(Layer):
 
     Extended Summary
     ----------
+    _property_choices : dict {str: array (N,)}
+        Possible values for the properties in Points.properties.
+        If properties is not provided, it will be {} (empty dictionary).
     _view_data : array (M, 2)
         2D coordinates of points in the currently viewed slice.
     _view_size : array (M, )
@@ -257,6 +260,7 @@ def __init__(
             current_face_color=Event,
             edge_color=Event,
             current_edge_color=Event,
+            current_properties=Event,
             symbol=Event,
             n_dimensional=Event,
             highlight=Event,
@@ -273,10 +277,23 @@ def __init__(
 
         # Save the properties
         if properties is None:
-            properties = {}
-        elif not isinstance(properties, dict):
+            self._properties = {}
+            self._property_choices = {}
+        elif len(data) > 0:
             properties = dataframe_to_properties(properties)
-        self._properties = self._validate_properties(properties)
+            self._properties = self._validate_properties(properties)
+            self._property_choices = {
+                k: np.unique(v) for k, v in properties.items()
+            }
+        elif len(data) == 0:
+            self._property_choices = {
+                k: np.asarray(v) for k, v in properties.items()
+            }
+            empty_properties = {
+                k: np.empty(0, dtype=v.dtype)
+                for k, v in self._property_choices.items()
+            }
+            self._properties = empty_properties
 
         # Save the point style params
         self.symbol = symbol
@@ -322,7 +339,7 @@ def __init__(
             self.edge_color_property = ''
             self.edge_color = edge_color
             if edge_color_cycle is None:
-                edge_color_cycle = DEFAULT_COLOR_CYCLE
+                edge_color_cycle = deepcopy(DEFAULT_COLOR_CYCLE)
             self.edge_color_cycle = edge_color_cycle
             self.edge_color_cycle_map = {}
             self.edge_colormap = edge_colormap
@@ -331,7 +348,7 @@ def __init__(
             self._face_color_property = ''
             self.face_color = face_color
             if face_color_cycle is None:
-                face_color_cycle = DEFAULT_COLOR_CYCLE
+                face_color_cycle = deepcopy(DEFAULT_COLOR_CYCLE)
             self.face_color_cycle = face_color_cycle
             self.face_color_cycle_map = {}
             self.face_colormap = face_colormap
@@ -339,13 +356,72 @@ def __init__(
 
         self.refresh_colors()
 
-        # set the current_* properties
-        self._current_edge_color = self.edge_color[-1]
-        self._current_face_color = self.face_color[-1]
         self.size = size
-        self.current_properties = {
-            k: np.asarray([v[-1]]) for k, v in self.properties.items()
-        }
+        # set the current_* properties
+        if len(data) > 0:
+            self._current_edge_color = self.edge_color[-1]
+            self._current_face_color = self.face_color[-1]
+            self.current_properties = {
+                k: np.asarray([v[-1]]) for k, v in self.properties.items()
+            }
+        elif len(data) == 0 and self.properties:
+            self.current_properties = {
+                k: np.asarray([v[0]])
+                for k, v in self._property_choices.items()
+            }
+            if self._edge_color_mode == ColorMode.DIRECT:
+                self._current_edge_color = transform_color_with_defaults(
+                    num_entries=1,
+                    colors=edge_color,
+                    elem_name="edge_color",
+                    default="white",
+                )
+            elif self._edge_color_mode == ColorMode.CYCLE:
+                curr_edge_color = transform_color(next(self.edge_color_cycle))
+                prop_value = self._property_choices[self._edge_color_property][
+                    0
+                ]
+                self.edge_color_cycle_map[prop_value] = curr_edge_color
+                self._current_edge_color = curr_edge_color
+            elif self._edge_color_mode == ColorMode.COLORMAP:
+                prop_value = self._property_choices[self._edge_color_property][
+                    0
+                ]
+                curr_edge_color, _ = map_property(
+                    prop=prop_value,
+                    colormap=self.edge_colormap[1],
+                    contrast_limits=self._edge_contrast_limits,
+                )
+                self._current_edge_color = curr_edge_color
+
+            if self._face_color_mode == ColorMode.DIRECT:
+                self._current_face_color = transform_color_with_defaults(
+                    num_entries=1,
+                    colors=face_color,
+                    elem_name="face_color",
+                    default="white",
+                )
+            elif self._face_color_mode == ColorMode.CYCLE:
+                curr_face_color = transform_color(next(self.face_color_cycle))
+                prop_value = self._property_choices[self._face_color_property][
+                    0
+                ]
+                self.face_color_cycle_map[prop_value] = curr_face_color
+                self._current_face_color = curr_face_color
+            elif self._face_color_mode == ColorMode.COLORMAP:
+                prop_value = self._property_choices[self._face_color_property][
+                    0
+                ]
+                curr_face_color, _ = map_property(
+                    prop=prop_value,
+                    colormap=self.face_colormap[1],
+                    contrast_limits=self._face_contrast_limits,
+                )
+                self._current_face_color = curr_face_color
+        else:
+            self._current_edge_color = self.edge_color[-1]
+            self._current_face_color = self.face_color[-1]
+            self.current_properties = {}
 
         # Trigger generation of view slice and thumbnail
         self._update_dims()
@@ -405,6 +481,15 @@ def data(self, data: np.ndarray):
                     edge_color_property = self.current_properties[
                         self._edge_color_property
                     ][0]
+
+                    # check if the new edge color property is in the cycle map
+                    # and add it if it is not
+                    edge_color_cycle_keys = [*self.edge_color_cycle_map]
+                    if edge_color_property not in edge_color_cycle_keys:
+                        self.edge_color_cycle_map[edge_color_property] = next(
+                            self.edge_color_cycle
+                        )
+
                     new_edge_colors = np.tile(
                         self.edge_color_cycle_map[edge_color_property],
                         (adding, 1),
@@ -430,11 +515,20 @@ def data(self, data: np.ndarray):
                         self._current_face_color, (adding, 1)
                     )
                 elif self._face_color_mode == ColorMode.CYCLE:
-                    face_color_property_value = self.current_properties[
+                    face_color_property = self.current_properties[
                         self._face_color_property
                     ][0]
+
+                    # check if the new edge color property is in the cycle map
+                    # and add it if it is not
+                    face_color_cycle_keys = [*self.face_color_cycle_map]
+                    if face_color_property not in face_color_cycle_keys:
+                        self.face_color_cycle_map[face_color_property] = next(
+                            self.face_color_cycle
+                        )
+
                     new_face_colors = np.tile(
-                        self.face_color_cycle_map[face_color_property_value],
+                        self.face_color_cycle_map[face_color_property],
                         (adding, 1),
                     )
                 elif self._face_color_mode == ColorMode.COLORMAP:
@@ -460,7 +554,7 @@ def data(self, data: np.ndarray):
 
     @property
     def properties(self):
-        """dict {str: array (N,)}, DataFrame: Annotations for each point"""
+        """dict {str: np.ndarray (N,)}, DataFrame: Annotations for each point"""
         return self._properties
 
     @properties.setter
@@ -474,13 +568,44 @@ def properties(self, properties: Dict[str, np.ndarray]):
             self._face_color_property = ''
             warnings.warn('property used for face_color dropped')
 
+        if self._edge_color_property and (
+            self._edge_color_property not in self._properties
+        ):
+            self._edge_color_property = ''
+            warnings.warn('property used for edge_color dropped')
+
+    @property
+    def current_properties(self):
+        """dict{str: np.ndarray(1,)}: properties for the next added point."""
+        return self._current_properties
+
+    @current_properties.setter
+    def current_properties(self, current_properties):
+        self._current_properties = current_properties
+
+        if (
+            self._update_properties
+            and len(self.selected_data) > 0
+            and self._mode != Mode.ADD
+        ):
+            props = self.properties
+            for k in props:
+                props[k][list(self.selected_data)] = current_properties[k]
+            self.properties = props
+
+            self.refresh_colors()
+        self.events.current_properties()
+
     def _validate_properties(self, properties: Dict[str, np.ndarray]):
         """Validates the type and size of the properties"""
-        for v in properties.values():
+        for k, v in properties.items():
             if len(v) != len(self.data):
                 raise ValueError(
                     'the number of properties must equal the number of points'
                 )
+            # ensure the property values are a numpy array
+            if type(v) != np.ndarray:
+                properties[k] = np.asarray(v)
 
         return properties
 
@@ -880,7 +1005,7 @@ def refresh_colors(self, update_color_mapping: bool = False):
             are adding/modifying points and want them to be colored with the same
             mapping as the other points (i.e., the new points shouldn't affect
             the color cycle map or colormap), set update_color_mapping=False.
-            Default value is True.
+            Default value is False.
         """
         if self._update_properties:
             if self._face_color_mode == ColorMode.CYCLE:
@@ -917,6 +1042,8 @@ def refresh_colors(self, update_color_mapping: bool = False):
                         for x in face_color_properties
                     ]
                 )
+                if len(face_colors) == 0:
+                    face_colors = np.empty((0, 4))
                 self._face_color = face_colors
 
                 self.events.face_color()
@@ -924,18 +1051,24 @@ def refresh_colors(self, update_color_mapping: bool = False):
                 face_color_properties = self.properties[
                     self._face_color_property
                 ]
-                if update_color_mapping or self.face_contrast_limits is None:
-                    face_colors, contrast_limits = map_property(
-                        prop=face_color_properties,
-                        colormap=self.face_colormap[1],
-                    )
-                    self.face_contrast_limits = contrast_limits
+                if len(face_color_properties) > 0:
+                    if (
+                        update_color_mapping
+                        or self.face_contrast_limits is None
+                    ):
+                        face_colors, contrast_limits = map_property(
+                            prop=face_color_properties,
+                            colormap=self.face_colormap[1],
+                        )
+                        self.face_contrast_limits = contrast_limits
+                    else:
+                        face_colors, _ = map_property(
+                            prop=face_color_properties,
+                            colormap=self.face_colormap[1],
+                            contrast_limits=self.face_contrast_limits,
+                        )
                 else:
-                    face_colors, _ = map_property(
-                        prop=face_color_properties,
-                        colormap=self.face_colormap[1],
-                        contrast_limits=self.face_contrast_limits,
-                    )
+                    face_colors = np.empty((0, 4))
                 self._face_color = face_colors
 
             if self._edge_color_mode == ColorMode.CYCLE:
@@ -971,24 +1104,31 @@ def refresh_colors(self, update_color_mapping: bool = False):
                         for x in edge_color_properties
                     ]
                 )
+                if len(edge_colors) == 0:
+                    edge_colors = np.empty((0, 4))
                 self._edge_color = edge_colors
             elif self._edge_color_mode == ColorMode.COLORMAP:
                 edge_color_properties = self.properties[
                     self._edge_color_property
                 ]
-                if update_color_mapping or self.edge_contrast_limits is None:
-                    edge_colors, contrast_limits = map_property(
-                        prop=edge_color_properties,
-                        colormap=self.edge_colormap[1],
-                    )
-                    self.edge_contrast_limits = contrast_limits
+                if len(edge_color_properties) > 0:
+                    if (
+                        update_color_mapping
+                        or self.edge_contrast_limits is None
+                    ):
+                        edge_colors, contrast_limits = map_property(
+                            prop=edge_color_properties,
+                            colormap=self.edge_colormap[1],
+                        )
+                        self.edge_contrast_limits = contrast_limits
+                    else:
+                        edge_colors, _ = map_property(
+                            prop=edge_color_properties,
+                            colormap=self.edge_colormap[1],
+                            contrast_limits=self.edge_contrast_limits,
+                        )
                 else:
-                    edge_colors, _ = map_property(
-                        prop=edge_color_properties,
-                        colormap=self.edge_colormap[1],
-                        contrast_limits=self.edge_contrast_limits,
-                    )
-
+                    edge_colors = np.empty((0, 4))
                 self._edge_color = edge_colors
             self.events.face_color()
             self.events.edge_color()
@@ -1081,7 +1221,8 @@ def selected_data(self, selected_data):
         }
         n_unique_properties = np.array([len(v) for v in properties.values()])
         if np.all(n_unique_properties == 1):
-            self.current_properties = properties
+            with self.block_update_properties():
+                self.current_properties = properties
         self._set_highlight()
 
     def interaction_box(self, index):


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
@@ -15,6 +15,92 @@ def test_empty_points():
     assert pts.data.shape == (0, 2)
 
 
+def test_empty_points_with_properties():
+    """ Test instantiating an empty Points layer with properties
+
+    See: https://github.com/napari/napari/pull/1069
+    """
+    properties = {
+        'label': np.array(['label1', 'label2']),
+        'cont_prop': np.array([0], dtype=np.float),
+    }
+    pts = Points(properties=properties)
+    current_props = {k: v[0] for k, v in properties.items()}
+    np.testing.assert_equal(pts.current_properties, current_props)
+
+    # verify the property datatype is correct
+    assert pts.properties['cont_prop'].dtype == np.float
+
+    # add two points and verify the default property was applied
+    pts.add([10, 10])
+    pts.add([20, 20])
+    props = {
+        'label': np.array(['label1', 'label1']),
+        'cont_prop': np.array([0, 0], dtype=np.float),
+    }
+    np.testing.assert_equal(pts.properties, props)
+
+
+def test_empty_points_with_properties_list():
+    """ Test instantiating an empty Points layer with properties
+    stored in a list
+
+    See: https://github.com/napari/napari/pull/1069
+    """
+    properties = {
+        'label': ['label1', 'label2'],
+        'cont_prop': [0],
+    }
+    pts = Points(properties=properties)
+    current_props = {k: np.asarray(v[0]) for k, v in properties.items()}
+    np.testing.assert_equal(pts.current_properties, current_props)
+
+    # add two points and verify the default property was applied
+    pts.add([10, 10])
+    pts.add([20, 20])
+    props = {
+        'label': np.array(['label1', 'label1']),
+        'cont_prop': np.array([0, 0], dtype=np.float),
+    }
+    np.testing.assert_equal(pts.properties, props)
+
+
+def test_empty_layer_with_face_colorap():
+    """ Test creating an empty layer where the face color is a colormap
+    See: https://github.com/napari/napari/pull/1069
+    """
+    default_properties = {'point_type': np.array([1.5], dtype=np.float)}
+    layer = Points(
+        properties=default_properties,
+        face_color='point_type',
+        face_colormap='grays',
+    )
+
+    assert layer.face_color_mode == 'colormap'
+
+    # verify the current_face_color is correct
+    face_color = np.array([1, 1, 1, 1])
+    assert np.all(layer._current_face_color == face_color)
+
+
+def test_empty_layer_with_edge_colormap():
+    """ Test creating an empty layer where the face color is a colormap
+    See: https://github.com/napari/napari/pull/1069
+    """
+    default_properties = {'point_type': np.array([1.5], dtype=np.float)}
+    layer = Points(
+        properties=default_properties,
+        edge_color='point_type',
+        edge_colormap='grays',
+    )
+
+    assert layer.edge_color_mode == 'colormap'
+
+    # verify the current_face_color is correct
+    edge_color = np.array([1, 1, 1, 1])
+    assert np.all(layer._current_edge_color == edge_color)
+
+
 def test_random_points():
     """Test instantiating Points layer with random 2D data."""
     shape = (10, 2)
@@ -371,6 +457,19 @@ def test_properties_dataframe():
     np.testing.assert_equal(layer.properties, properties)
 
 
+def test_properties_list():
+    """test if properties can be provided as a dict of lists"""
+    shape = (10, 2)
+    np.random.seed(0)
+    data = 20 * np.random.random(shape)
+    properties = {'point_type': ['A', 'B'] * int(shape[0] / 2)}
+    layer = Points(data, properties=properties)
+    np.testing.assert_equal(layer.properties, properties)
+
+    # verify the lists were converted to numpy arrays
+    assert type(layer.properties['point_type']) == np.ndarray
+
+
 def test_adding_annotations():
     shape = (10, 2)
     np.random.seed(0)
@@ -405,7 +504,38 @@ def test_add_points_with_properties():
     np.testing.assert_equal(layer.properties, new_prop)
 
 
-def test_annotations_errors():
+def test_add_points_with_properties_as_list():
+    # test adding points initialized with properties as list
+    shape = (10, 2)
+    np.random.seed(0)
+    data = 20 * np.random.random(shape)
+    properties = {'point_type': ['A', 'B'] * int((shape[0] / 2))}
+    layer = Points(data, properties=copy(properties))
+
+    coord = [18, 18]
+    layer.add(coord)
+    new_prop = {'point_type': np.append(properties['point_type'], 'B')}
+    np.testing.assert_equal(layer.properties, new_prop)
+
+
+def test_updating_points_properties():
+    # test adding points initialized with properties
+    shape = (10, 2)
+    np.random.seed(0)
+    data = 20 * np.random.random(shape)
+    properties = {'point_type': np.array(['A', 'B'] * int((shape[0] / 2)))}
+    layer = Points(data, properties=copy(properties))
+
+    layer.mode = 'select'
+    layer.selected_data = [len(data) - 1]
+    layer.current_properties = {'point_type': np.array(['A'])}
+
+    updated_properties = properties
+    updated_properties['point_type'][-1] = 'A'
+    np.testing.assert_equal(layer.properties, updated_properties)
+
+
+def test_points_errors():
     shape = (3, 2)
     np.random.seed(0)
     data = 20 * np.random.random(shape)
@@ -598,6 +728,41 @@ def test_edge_color_cycle():
     )
 
 
+def test_add_edge_color_cycle_to_empty_layer():
+    """ Test adding a point to an empty layer when edge color is a color cycle
+
+    See: https://github.com/napari/napari/pull/1069
+    """
+    default_properties = {'point_type': np.array(['A'])}
+    color_cycle = ['red', 'blue']
+    layer = Points(
+        properties=default_properties,
+        edge_color='point_type',
+        edge_color_cycle=color_cycle,
+    )
+
+    # verify the current_edge_color is correct
+    edge_color = transform_color(color_cycle[0])
+    assert np.all(layer._current_edge_color == edge_color)
+
+    # add a point
+    layer.add([10, 10])
+    props = {'point_type': np.array(['A'])}
+    edge_color = np.array([[1, 0, 0, 1]])
+    assert layer.properties == props
+    np.testing.assert_allclose(layer.edge_color, edge_color)
+
+    # add a point with a new property
+    layer.selected_data = []
+    layer.current_properties = {'point_type': np.array(['B'])}
+    layer.add([12, 12])
+    new_color = np.array([0, 0, 1, 1])
+    edge_color = np.vstack((edge_color, new_color))
+    new_properties = {'point_type': np.array(['A', 'B'])}
+    np.testing.assert_allclose(layer.edge_color, edge_color)
+    np.testing.assert_equal(layer.properties, new_properties)
+
+
 def test_adding_value_edge_color_cycle():
     """ Test that adding values to properties used to set an edge color cycle
     and then calling Points.refresh_colors() performs the update and adds the
@@ -660,11 +825,6 @@ def test_edge_color_colormap():
         np.vstack((edge_color_array, transform_color('black'))),
     )
 
-    # change the colormap
-    new_colormap = 'viridis'
-    layer.edge_colormap = new_colormap
-    assert layer.edge_colormap[1] == get_colormap(new_colormap)
-
     # Check removing data adjusts colors correctly
     layer.selected_data = {0, 2}
     layer.remove_selected()
@@ -681,6 +841,16 @@ def test_edge_color_colormap():
         ),
     )
 
+    # adjust the clims
+    layer.edge_contrast_limits = (0, 3)
+    layer.refresh_colors(update_color_mapping=False)
+    np.testing.assert_allclose(layer.edge_color[-2], [0.5, 0.5, 0.5, 1])
+
+    # change the colormap
+    new_colormap = 'viridis'
+    layer.edge_colormap = new_colormap
+    assert layer.edge_colormap[1] == get_colormap(new_colormap)
+
 
 def test_face_color_direct():
     """Test setting face color."""
@@ -799,6 +969,40 @@ def test_face_color_cycle():
     )
 
 
+def test_add_face_color_cycle_to_empty_layer():
+    """ Test adding a point to an empty layer when face color is a color cycle
+    See: https://github.com/napari/napari/pull/1069
+    """
+    default_properties = {'point_type': np.array(['A'])}
+    color_cycle = ['red', 'blue']
+    layer = Points(
+        properties=default_properties,
+        face_color='point_type',
+        face_color_cycle=color_cycle,
+    )
+
+    # verify the current_face_color is correct
+    face_color = transform_color(color_cycle[0])
+    assert np.all(layer._current_face_color == face_color)
+
+    # add a point
+    layer.add([10, 10])
+    props = {'point_type': np.array(['A'])}
+    face_color = np.array([[1, 0, 0, 1]])
+    assert layer.properties == props
+    np.testing.assert_allclose(layer.face_color, face_color)
+
+    # add a point with a new property
+    layer.selected_data = []
+    layer.current_properties = {'point_type': np.array(['B'])}
+    layer.add([12, 12])
+    new_color = np.array([0, 0, 1, 1])
+    face_color = np.vstack((face_color, new_color))
+    new_properties = {'point_type': np.array(['A', 'B'])}
+    np.testing.assert_allclose(layer.face_color, face_color)
+    np.testing.assert_equal(layer.properties, new_properties)
+
+
 def test_adding_value_face_color_cycle():
     """ Test that adding values to properties used to set an face color cycle
     and then calling Points.refresh_colors() performs the update and adds the
@@ -861,11 +1065,6 @@ def test_face_color_colormap():
         np.vstack((face_color_array, transform_color('black'))),
     )
 
-    # change the colormap
-    new_colormap = 'viridis'
-    layer.face_colormap = new_colormap
-    assert layer.face_colormap[1] == get_colormap(new_colormap)
-
     # Check removing data adjusts colors correctly
     layer.selected_data = {0, 2}
     layer.remove_selected()
@@ -882,6 +1081,16 @@ def test_face_color_colormap():
         ),
     )
 
+    # adjust the clims
+    layer.face_contrast_limits = (0, 3)
+    layer.refresh_colors(update_color_mapping=False)
+    np.testing.assert_allclose(layer.face_color[-2], [0.5, 0.5, 0.5, 1])
+
+    # change the colormap
+    new_colormap = 'viridis'
+    layer.face_colormap = new_colormap
+    assert layer.face_colormap[1] == get_colormap(new_colormap)
+
 
 def test_size():
     """Test setting size with scalar."""