Repository: holoviz/holoviews

Issue:
Datetime axis problems in nested Layouts
```
import holoviews as hv
import numpy as np
import pandas as pd
import xarray as xr
hv.extension('bokeh')
dates = pd.date_range('2017-1-1', '2018-1-1', periods=242)
# dates = np.arange(242)

rr = xr.DataArray(np.random.rand(242,3),
                  coords=dict(Date=dates, Float=range(3)), 
                  dims=['Date', 'Float'],
                  name='r')

i = pd.DataFrame(dict(ice=range(242), Date=dates))
i2 = pd.DataFrame(dict(sic=np.linspace(0,300,242), Date=dates))

a = hv.Dataset(rr, kdims=['Date', 'Float'], vdims=hv.Dimension('r', range=(-.01, .05))
              ).to(hv.Scatter).overlay()
b = hv.Curve(i, 'Date', hv.Dimension('ice', range=(0,100)))
bb = hv.Scatter(i2, 'Date', 'sic')
l = a+b*bb
print(l)
l
```

gives a `TypeError`. The Layout is:
```
:Layout
   .NdOverlay.I :NdOverlay   [Float]
      :Scatter   [Date]   (r)
   .Overlay.I   :Overlay
      .Curve.I   :Curve   [Date]   (ice)
      .Scatter.I :Scatter   [Date]   (sic)
```

Replacing the `dates` coordinate by something numeric it works, as do the individual combinations `a+bb`, `a+bb`, `b*bb`.


Discussion:
So the problem stems from the fact that in this case (when Overlay.I consist of a Curve and a Scatter Element), `max_extents` gets slightly different arguments. And then it turns out that
```
np.max([np.datetime64('2018-01-01T00:00:00.000000000'), pd.Timestamp('2018-01-01 00:00:00')])
np.max([pd.Timestamp('2018-01-01 00:00:00'), np.datetime64('2018-01-01T00:00:00.000000000')])
```

are not at all equivalent... the first throws a `TypeError: Cannot compare type 'Timestamp' with type 'int'`, the second one returns the actual `Timestamp(...)`. Go figure...

I am unsure why there are Timestamps popping up here in the first place, shouldn't they have been converted to datetime64 earlier?

Resolving patch:
diff --git a/holoviews/core/util.py b/holoviews/core/util.py
--- a/holoviews/core/util.py
+++ b/holoviews/core/util.py
@@ -928,21 +928,42 @@ def find_range(values, soft_range=[]):
 def max_range(ranges, combined=True):
     """
     Computes the maximal lower and upper bounds from a list bounds.
+
+    Args:
+       ranges (list of tuples): A list of range tuples
+       combined (boolean, optional): Whether to combine bounds
+          Whether range should be computed on lower and upper bound
+          independently or both at once
+
+    Returns:
+       The maximum range as a single tuple
     """
     try:
         with warnings.catch_warnings():
             warnings.filterwarnings('ignore', r'All-NaN (slice|axis) encountered')
             values = [tuple(np.NaN if v is None else v for v in r) for r in ranges]
-            if pd and all(isinstance(v, pd.Timestamp) for r in values for v in r):
-                values = [(v1.to_datetime64(), v2.to_datetime64()) for v1, v2 in values]
+            if pd and any(isinstance(v, datetime_types) and not isinstance(v, cftime_types)
+                          for r in values for v in r):
+                converted = []
+                for l, h in values:
+                    if isinstance(l, datetime_types) and isinstance(h, datetime_types):
+                        l, h = (pd.Timestamp(l).to_datetime64(),
+                                pd.Timestamp(h).to_datetime64())
+                    converted.append((l, h))
+                values = converted
+
             arr = np.array(values)
             if not len(arr):
                 return np.NaN, np.NaN
             elif arr.dtype.kind in 'OSU':
-                arr = list(python2sort([v for r in values for v in r if not is_nan(v) and v is not None]))
+                arr = list(python2sort([
+                    v for r in values for v in r
+                    if not is_nan(v) and v is not None]))
                 return arr[0], arr[-1]
             elif arr.dtype.kind in 'M':
-                return (arr.min(), arr.max()) if combined else (arr[:, 0].min(), arr[:, 1].min())
+                return ((arr.min(), arr.max()) if combined else
+                        (arr[:, 0].min(), arr[:, 1].min()))
+
             if combined:
                 return (np.nanmin(arr), np.nanmax(arr))
             else:
@@ -1085,12 +1106,28 @@ def unique_iterator(seq):
 
 def unique_array(arr):
     """
-    Returns an array of unique values in the input order
+    Returns an array of unique values in the input order.
+
+    Args:
+       arr (np.ndarray or list): The array to compute unique values on
+
+    Returns:
+       A new array of unique values
     """
     if not len(arr):
-        return arr
+        return np.asarray(arr)
     elif pd:
-        return pd.unique(arr)
+        if isinstance(arr, np.ndarray) and arr.dtype.kind not in 'MO':
+            # Avoid expensive unpacking if not potentially datetime
+            return pd.unique(arr)
+
+        values = []
+        for v in arr:
+            if (isinstance(v, datetime_types) and
+                not isinstance(v, cftime_types)):
+                v = pd.Timestamp(v).to_datetime64()
+            values.append(v)
+        return pd.unique(values)
     else:
         arr = np.asarray(arr)
         _, uniq_inds = np.unique(arr, return_index=True)


Test patch: