Repository: holoviz/holoviews

Issue:
hv.Image(group="") no longer works
Until HoloViews 1.13.5 it was legal to use a group name of "" for an Image, but starting with v1.14.0 that gives an error:

```python
import numpy as np, holoviews as hv
hv.extension()

ls = np.linspace(0, 10, 200)
xx, yy = np.meshgrid(ls, ls)
hv.Image(np.sin(xx)*np.cos(yy), group="").opts(cmap="greys")
```
![image](https://user-images.githubusercontent.com/1695496/141535863-a5df0cb1-796f-4c84-8e4f-e37c8f763809.png)

In a brief investigation I couldn't see what code had changed, but apparently something has. In any case, searching for "capitalize" in https://github.com/holoviz/holoviews/blob/master/holoviews/core/util.py reveals various code paths that implicitly depend on the length of a string being nonzero, which doesn't seem like a safe assumption in general and is problematic in this case. 

Here, obviously group could simply be omitted, but this came up for existing code in colorcet that accepted a user argument for the group, and when the user wants a blank group they now have to set the group to the very non-intuitive value "Image", whereas the much more reasonable group="" was sufficient to suppress printing the group name before.


Discussion:
A `git bisect` shows that this was introduced in #4567
Thanks; that helps! Alas, #4567 is a squash of 98 commits, with changes to 51 files, so we'll probably have to `git bisect` within that PR.
Done. Commit 969aa172c9badd2d3ce0d17022eb908c817b8fcf seem to be the culprit.

You can get the desired plot by adding `nodata=True` in `opts`.
``` python
import numpy as np
import holoviews as hv
import panel as pn

hv.extension()

ls = np.linspace(0, 10, 200)
xx, yy = np.meshgrid(ls, ls)
image = hv.Image(np.sin(xx) * np.cos(yy)).opts(cmap="greys", nodata=True)
```

Resolving patch:
diff --git a/holoviews/core/util.py b/holoviews/core/util.py
--- a/holoviews/core/util.py
+++ b/holoviews/core/util.py
@@ -346,6 +346,8 @@ def tree_attribute(identifier):
     These custom attributes start with a capitalized character when
     applicable (not applicable to underscore or certain unicode characters)
     """
+    if identifier == '':
+        return True
     if identifier[0].upper().isupper() is False and identifier[0] != '_':
         return True
     else:
@@ -1750,7 +1752,10 @@ def capitalize(string):
     """
     Capitalizes the first letter of a string.
     """
-    return string[0].upper() + string[1:]
+    if string:
+        return string[0].upper() + string[1:]
+    else:
+        return string
 
 
 def get_path(item):


Test patch:
diff --git a/holoviews/tests/core/test_options.py b/holoviews/tests/core/test_options.py
--- a/holoviews/tests/core/test_options.py
+++ b/holoviews/tests/core/test_options.py
@@ -6,6 +6,7 @@
 import numpy as np
 import pytest
 
+from holoviews import util
 from holoviews import Store, Histogram, Image, Curve, Points, DynamicMap, opts
 from holoviews.core.options import (
     OptionError, Cycle, Options, OptionTree, StoreOptions, options_policy
@@ -313,6 +314,12 @@ def initialize_option_tree(self):
         options.Image = Options('style', cmap='hot', interpolation='nearest')
         return options
 
+    def test_empty_group(self):
+        "Test to prevent regression of issue fixed in #5131"
+        ls = np.linspace(0, 10, 200)
+        xx, yy = np.meshgrid(ls, ls)
+        util.render(Image(np.sin(xx)*np.cos(yy), group="").opts(cmap="greys"))
+
     def test_merge_keywords(self):
         options = self.initialize_option_tree()
         options.Image = Options('style', clims=(0, 0.5))