Repository: pydata/xarray

Issue:
Including last coordinate values when displaying coordinates
I first posted this to https://groups.google.com/forum/#!topic/xarray/wjPtXMr91sg .

I'm afraid I'm not set up to submit a PR, but I think something like the following should work once one has created a ``last_n_items()`` similar to the existing ``first_n_items()``.

Also, I wonder if it would make sense to change the minimum number of items displayed from 1 to min(2, items_ndarray.size).

```
def format_array_flat(items_ndarray, max_width):
    """Return a formatted string for as many items in the flattened version of
    items_ndarray that will fit within max_width characters
    """
    # every item will take up at least two characters, but we always want to
    # print at least one item
    max_possibly_relevant = max(int(np.ceil(max_width / 2.0)), 1)
    relevant_items = sum(([x, y] for (x, y) in
                          zip(first_n_items(items_ndarray, (max_possibly_relevant + 1) // 2),
                              reversed(last_n_items(items_ndarray, max_possibly_relevant // 2)))),
                         [])
    pprint_items = format_items(relevant_items)

    cum_len = np.cumsum([len(s) + 1 for s in pprint_items]) - 1
    if (max_possibly_relevant < items_ndarray.size or
            (cum_len > max_width).any()):
        padding = u' ... '
        count = max(np.argmax((cum_len + len(padding)) > max_width), 1)
    else:
        count = items_ndarray.size
        padding = u'' if (count <= 1) else u' '

    pprint_str = u' '.join(np.take(pprint_items, range(0, count, 2))) + padding + \
                 u' '.join(np.take(pprint_items, range(count - (count % 2) - 1, 0, -2)))
    return pprint_str
```



Discussion:
seth-p: Sample output:

```
(base) C:\Users\Seth\github\xarray>ipython
Python 3.6.5 | packaged by conda-forge | (default, Apr  6 2018, 16:13:55) [MSC v.1900 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 6.4.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import xarray as xr

In [2]: words = "This is the time for all good men to come to the aid of their country".split(' ')

In [3]: for i in range(0, len(words) + 1):
   ...:     print("-------------------------------------------------------------------------------")
   ...:     print(xr.DataArray(words[:i], dims=('foo',), coords={'foo': words[:i]}))
   ...:
-------------------------------------------------------------------------------
<xarray.DataArray (foo: 0)>
array([], dtype=float64)
Coordinates:
  * foo      (foo) float64
-------------------------------------------------------------------------------
<xarray.DataArray (foo: 1)>
array(['This'], dtype='<U4')
Coordinates:
  * foo      (foo) <U4 'This'
-------------------------------------------------------------------------------
<xarray.DataArray (foo: 2)>
array(['This', 'is'], dtype='<U4')
Coordinates:
  * foo      (foo) <U4 'This' 'is'
-------------------------------------------------------------------------------
<xarray.DataArray (foo: 3)>
array(['This', 'is', 'the'], dtype='<U4')
Coordinates:
  * foo      (foo) <U4 'This' 'is' 'the'
-------------------------------------------------------------------------------
<xarray.DataArray (foo: 4)>
array(['This', 'is', 'the', 'time'], dtype='<U4')
Coordinates:
  * foo      (foo) <U4 'This' 'is' 'the' 'time'
-------------------------------------------------------------------------------
<xarray.DataArray (foo: 5)>
array(['This', 'is', 'the', 'time', 'for'], dtype='<U4')
Coordinates:
  * foo      (foo) <U4 'This' 'is' 'the' 'time' 'for'
-------------------------------------------------------------------------------
<xarray.DataArray (foo: 6)>
array(['This', 'is', 'the', 'time', 'for', 'all'], dtype='<U4')
Coordinates:
  * foo      (foo) <U4 'This' 'is' 'the' 'time' 'for' 'all'
-------------------------------------------------------------------------------
<xarray.DataArray (foo: 7)>
array(['This', 'is', 'the', 'time', 'for', 'all', 'good'], dtype='<U4')
Coordinates:
  * foo      (foo) <U4 'This' 'is' 'the' 'time' 'for' 'all' 'good'
-------------------------------------------------------------------------------
<xarray.DataArray (foo: 8)>
array(['This', 'is', 'the', 'time', 'for', 'all', 'good', 'men'], dtype='<U4')
Coordinates:
  * foo      (foo) <U4 'This' 'is' 'the' 'time' 'for' 'all' 'good' 'men'
-------------------------------------------------------------------------------
<xarray.DataArray (foo: 9)>
array(['This', 'is', 'the', 'time', 'for', 'all', 'good', 'men', 'to'],
      dtype='<U4')
Coordinates:
  * foo      (foo) <U4 'This' 'is' 'the' 'time' 'for' 'all' 'good' 'men' 'to'
-------------------------------------------------------------------------------
<xarray.DataArray (foo: 10)>
array(['This', 'is', 'the', 'time', 'for', 'all', 'good', 'men', 'to', 'come'],
      dtype='<U4')
Coordinates:
  * foo      (foo) <U4 'This' 'is' 'the' 'time' ... 'good' 'men' 'to' 'come'
-------------------------------------------------------------------------------
<xarray.DataArray (foo: 11)>
array(['This', 'is', 'the', 'time', 'for', 'all', 'good', 'men', 'to', 'come',
       'to'], dtype='<U4')
Coordinates:
  * foo      (foo) <U4 'This' 'is' 'the' 'time' 'for' ... 'men' 'to' 'come' 'to'
-------------------------------------------------------------------------------
<xarray.DataArray (foo: 12)>
array(['This', 'is', 'the', 'time', 'for', 'all', 'good', 'men', 'to', 'come',
       'to', 'the'], dtype='<U4')
Coordinates:
  * foo      (foo) <U4 'This' 'is' 'the' 'time' 'for' ... 'to' 'come' 'to' 'the'
-------------------------------------------------------------------------------
<xarray.DataArray (foo: 13)>
array(['This', 'is', 'the', 'time', 'for', 'all', 'good', 'men', 'to', 'come',
       'to', 'the', 'aid'], dtype='<U4')
Coordinates:
  * foo      (foo) <U4 'This' 'is' 'the' 'time' ... 'come' 'to' 'the' 'aid'
-------------------------------------------------------------------------------
<xarray.DataArray (foo: 14)>
array(['This', 'is', 'the', 'time', 'for', 'all', 'good', 'men', 'to', 'come',
       'to', 'the', 'aid', 'of'], dtype='<U4')
Coordinates:
  * foo      (foo) <U4 'This' 'is' 'the' 'time' 'for' ... 'to' 'the' 'aid' 'of'
-------------------------------------------------------------------------------
<xarray.DataArray (foo: 15)>
array(['This', 'is', 'the', 'time', 'for', 'all', 'good', 'men', 'to', 'come',
       'to', 'the', 'aid', 'of', 'their'], dtype='<U5')
Coordinates:
  * foo      (foo) <U5 'This' 'is' 'the' 'time' ... 'the' 'aid' 'of' 'their'
-------------------------------------------------------------------------------
<xarray.DataArray (foo: 16)>
array(['This', 'is', 'the', 'time', 'for', 'all', 'good', 'men', 'to', 'come',
       'to', 'the', 'aid', 'of', 'their', 'country'], dtype='<U7')
Coordinates:
  * foo      (foo) <U7 'This' 'is' 'the' 'time' ... 'aid' 'of' 'their' 'country'
```

seth-p: @shoyer, I think I've implemented all your suggestions. Let me know what you think. (I haven't yet updated whats-new.rst.)
seth-p: One slight oddity is that ``formatting.format_array_flat(np.arange(4), 0)`` returns ``0 ... 3`` even though ``0 1 2 3`` would take up the same number of characters.
shoyer: > One slight oddity is that formatting.format_array_flat(np.arange(4), 0) returns 0 ... 3 even though 0 1 2 3 would take up the same number of characters. I'm not inclined to add a special case for this, but let me know if you think I should.

Yeah, I don't think we'll be passing in a max length of 0 very often :)
shoyer: OK looks good to me. Can you kindly add a brief release note, since this is user visible?
seth-p: I added a note to whats-new.rst.

Patch:
diff --git a/doc/whats-new.rst b/doc/whats-new.rst
index e392e3ce..af485015 100644
--- a/doc/whats-new.rst
+++ b/doc/whats-new.rst
@@ -36,6 +36,12 @@ Documentation
 Enhancements
 ~~~~~~~~~~~~
 
+- DataArray coordinates and Dataset coordinates and data variables are
+  now displayed as `a b ... y z` rather than `a b c d ...`.
+  (:issue:`1186`)
+  By `Seth P <https://github.com/seth-p>`_.
+
+
 Bug fixes
 ~~~~~~~~~
 
diff --git a/xarray/core/formatting.py b/xarray/core/formatting.py
index 2009df3b..65f3c91c 100644
--- a/xarray/core/formatting.py
+++ b/xarray/core/formatting.py
@@ -14,7 +14,9 @@ import numpy as np
 import pandas as pd
 
 from .options import OPTIONS
-from .pycompat import PY2, bytes_type, dask_array_type, unicode_type
+from .pycompat import (
+    PY2, bytes_type, dask_array_type, unicode_type, zip_longest,
+)
 
 try:
     from pandas.errors import OutOfBoundsDatetime
@@ -64,13 +66,13 @@ class ReprMixin(object):
         return ensure_valid_repr(self.__unicode__())
 
 
-def _get_indexer_at_least_n_items(shape, n_desired):
+def _get_indexer_at_least_n_items(shape, n_desired, from_end):
     assert 0 < n_desired <= np.prod(shape)
     cum_items = np.cumprod(shape[::-1])
     n_steps = np.argmax(cum_items >= n_desired)
     stop = int(np.ceil(float(n_desired) / np.r_[1, cum_items][n_steps]))
-    indexer = ((0,) * (len(shape) - 1 - n_steps) +
-               (slice(stop),) +
+    indexer = (((-1 if from_end else 0),) * (len(shape) - 1 - n_steps) +
+               ((slice(-stop, None) if from_end else slice(stop)),) +
                (slice(None),) * n_steps)
     return indexer
 
@@ -89,11 +91,28 @@ def first_n_items(array, n_desired):
         return []
 
     if n_desired < array.size:
-        indexer = _get_indexer_at_least_n_items(array.shape, n_desired)
+        indexer = _get_indexer_at_least_n_items(array.shape, n_desired,
+                                                from_end=False)
         array = array[indexer]
     return np.asarray(array).flat[:n_desired]
 
 
+def last_n_items(array, n_desired):
+    """Returns the last n_desired items of an array"""
+    # Unfortunately, we can't just do array.flat[-n_desired:] here because it
+    # might not be a numpy.ndarray. Moreover, access to elements of the array
+    # could be very expensive (e.g. if it's only available over DAP), so go out
+    # of our way to get them in a single call to __getitem__ using only slices.
+    if (n_desired == 0) or (array.size == 0):
+        return []
+
+    if n_desired < array.size:
+        indexer = _get_indexer_at_least_n_items(array.shape, n_desired,
+                                                from_end=True)
+        array = array[indexer]
+    return np.asarray(array).flat[-n_desired:]
+
+
 def last_item(array):
     """Returns the last item of an array in a list or an empty list."""
     if array.size == 0:
@@ -180,20 +199,36 @@ def format_array_flat(array, max_width):
     array that will fit within max_width characters.
     """
     # every item will take up at least two characters, but we always want to
-    # print at least one item
-    max_possibly_relevant = max(int(np.ceil(max_width / 2.0)), 1)
-    relevant_items = first_n_items(array, max_possibly_relevant)
-    pprint_items = format_items(relevant_items)
-
-    cum_len = np.cumsum([len(s) + 1 for s in pprint_items]) - 1
-    if (max_possibly_relevant < array.size or (cum_len > max_width).any()):
-        end_padding = u' ...'
-        count = max(np.argmax((cum_len + len(end_padding)) > max_width), 1)
-        pprint_items = pprint_items[:count]
+    # print at least first and last items
+    max_possibly_relevant = min(max(array.size, 1),
+                                max(int(np.ceil(max_width / 2.)), 2))
+    relevant_front_items = format_items(
+        first_n_items(array, (max_possibly_relevant + 1) // 2))
+    relevant_back_items = format_items(
+        last_n_items(array, max_possibly_relevant // 2))
+    # interleave relevant front and back items:
+    #     [a, b, c] and [y, z] -> [a, z, b, y, c]
+    relevant_items = sum(zip_longest(relevant_front_items,
+                                     reversed(relevant_back_items)),
+                         ())[:max_possibly_relevant]
+
+    cum_len = np.cumsum([len(s) + 1 for s in relevant_items]) - 1
+    if (array.size > 2) and ((max_possibly_relevant < array.size) or
+                             (cum_len > max_width).any()):
+        padding = u' ... '
+        count = min(array.size,
+                    max(np.argmax(cum_len + len(padding) - 1 > max_width), 2))
     else:
-        end_padding = u''
-
-    pprint_str = u' '.join(pprint_items) + end_padding
+        count = array.size
+        padding = u'' if (count <= 1) else u' '
+
+    num_front = (count + 1) // 2
+    num_back = count - num_front
+    # note that num_back is 0 <--> array.size is 0 or 1
+    #                         <--> relevant_back_items is []
+    pprint_str = (u' '.join(relevant_front_items[:num_front]) +
+                  padding +
+                  u' '.join(relevant_back_items[-num_back:]))
     return pprint_str
 
 
diff --git a/xarray/core/pycompat.py b/xarray/core/pycompat.py
index df7781ca..78c26f1e 100644
--- a/xarray/core/pycompat.py
+++ b/xarray/core/pycompat.py
@@ -23,6 +23,7 @@ if PY3:  # pragma: no cover
 
     range = range
     zip = zip
+    from itertools import zip_longest
     from functools import reduce
     import builtins
     from urllib.request import urlretrieve
@@ -41,7 +42,9 @@ else:  # pragma: no cover
         return d.itervalues()
 
     range = xrange
-    from itertools import izip as zip, imap as map
+    from itertools import (
+        izip as zip, imap as map, izip_longest as zip_longest,
+    )
     reduce = reduce
     import __builtin__ as builtins
     from urllib import urlretrieve


Test patch:
diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py
index 4aa99b8e..5c9131d0 100644
--- a/xarray/tests/test_dataset.py
+++ b/xarray/tests/test_dataset.py
@@ -93,15 +93,15 @@ class TestDataset(TestCase):
         <xarray.Dataset>
         Dimensions:  (dim1: 8, dim2: 9, dim3: 10, time: 20)
         Coordinates:
-          * time     (time) datetime64[ns] 2000-01-01 2000-01-02 2000-01-03 ...
+          * time     (time) datetime64[ns] 2000-01-01 2000-01-02 ... 2000-01-20
           * dim2     (dim2) float64 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0
           * dim3     (dim3) %s 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j'
             numbers  (dim3) int64 0 1 2 0 0 1 1 2 2 3
         Dimensions without coordinates: dim1
         Data variables:
-            var1     (dim1, dim2) float64 -1.086 0.9973 0.283 -1.506 -0.5786 1.651 ...
-            var2     (dim1, dim2) float64 1.162 -1.097 -2.123 1.04 -0.4034 -0.126 ...
-            var3     (dim3, dim1) float64 0.5565 -0.2121 0.4563 1.545 -0.2397 0.1433 ...
+            var1     (dim1, dim2) float64 -1.086 0.9973 0.283 ... 0.1995 0.4684 -0.8312
+            var2     (dim1, dim2) float64 1.162 -1.097 -2.123 ... 0.1302 1.267 0.3328
+            var3     (dim3, dim1) float64 0.5565 -0.2121 0.4563 ... -0.2452 -0.3616
         Attributes:
             foo:      bar""") % data['dim3'].dtype  # noqa: E501
         actual = '\n'.join(x.rstrip() for x in repr(data).split('\n'))
diff --git a/xarray/tests/test_formatting.py b/xarray/tests/test_formatting.py
index 34552891..8a1003f1 100644
--- a/xarray/tests/test_formatting.py
+++ b/xarray/tests/test_formatting.py
@@ -14,20 +14,31 @@ class TestFormatting(TestCase):
 
     def test_get_indexer_at_least_n_items(self):
         cases = [
-            ((20,), (slice(10),)),
-            ((3, 20,), (0, slice(10))),
-            ((2, 10,), (0, slice(10))),
-            ((2, 5,), (slice(2), slice(None))),
-            ((1, 2, 5,), (0, slice(2), slice(None))),
-            ((2, 3, 5,), (0, slice(2), slice(None))),
-            ((1, 10, 1,), (0, slice(10), slice(None))),
-            ((2, 5, 1,), (slice(2), slice(None), slice(None))),
-            ((2, 5, 3,), (0, slice(4), slice(None))),
-            ((2, 3, 3,), (slice(2), slice(None), slice(None))),
+            ((20,), (slice(10),), (slice(-10, None),)),
+            ((3, 20,), (0, slice(10)), (-1, slice(-10, None))),
+            ((2, 10,), (0, slice(10)), (-1, slice(-10, None))),
+            ((2, 5,), (slice(2), slice(None)),
+             (slice(-2, None), slice(None))),
+            ((1, 2, 5,), (0, slice(2), slice(None)),
+             (-1, slice(-2, None), slice(None))),
+            ((2, 3, 5,), (0, slice(2), slice(None)),
+             (-1, slice(-2, None), slice(None))),
+            ((1, 10, 1,), (0, slice(10), slice(None)),
+             (-1, slice(-10, None), slice(None))),
+            ((2, 5, 1,), (slice(2), slice(None), slice(None)),
+             (slice(-2, None), slice(None), slice(None))),
+            ((2, 5, 3,), (0, slice(4), slice(None)),
+             (-1, slice(-4, None), slice(None))),
+            ((2, 3, 3,), (slice(2), slice(None), slice(None)),
+             (slice(-2, None), slice(None), slice(None))),
         ]
-        for shape, expected in cases:
-            actual = formatting._get_indexer_at_least_n_items(shape, 10)
-            assert expected == actual
+        for shape, start_expected, end_expected in cases:
+            actual = formatting._get_indexer_at_least_n_items(shape, 10,
+                                                              from_end=False)
+            assert start_expected == actual
+            actual = formatting._get_indexer_at_least_n_items(shape, 10,
+                                                              from_end=True)
+            assert end_expected == actual
 
     def test_first_n_items(self):
         array = np.arange(100).reshape(10, 5, 2)
@@ -39,6 +50,16 @@ class TestFormatting(TestCase):
         with raises_regex(ValueError, 'at least one item'):
             formatting.first_n_items(array, 0)
 
+    def test_last_n_items(self):
+        array = np.arange(100).reshape(10, 5, 2)
+        for n in [3, 10, 13, 100, 200]:
+            actual = formatting.last_n_items(array, n)
+            expected = array.flat[-n:]
+            self.assertItemsEqual(expected, actual)
+
+        with raises_regex(ValueError, 'at least one item'):
+            formatting.first_n_items(array, 0)
+
     def test_last_item(self):
         array = np.arange(100)
 
@@ -87,16 +108,32 @@ class TestFormatting(TestCase):
             assert expected == actual
 
     def test_format_array_flat(self):
+        actual = formatting.format_array_flat(np.arange(100), 2)
+        expected = '0 ... 99'
+        assert expected == actual
+
+        actual = formatting.format_array_flat(np.arange(100), 9)
+        expected = '0 ... 99'
+        assert expected == actual
+
+        actual = formatting.format_array_flat(np.arange(100), 10)
+        expected = '0 1 ... 99'
+        assert expected == actual
+
         actual = formatting.format_array_flat(np.arange(100), 13)
-        expected = '0 1 2 3 4 ...'
+        expected = '0 1 ... 98 99'
+        assert expected == actual
+
+        actual = formatting.format_array_flat(np.arange(100), 15)
+        expected = '0 1 2 ... 98 99'
         assert expected == actual
 
         actual = formatting.format_array_flat(np.arange(100.0), 11)
-        expected = '0.0 1.0 ...'
+        expected = '0.0 ... 99.0'
         assert expected == actual
 
         actual = formatting.format_array_flat(np.arange(100.0), 1)
-        expected = '0.0 ...'
+        expected = '0.0 ... 99.0'
         assert expected == actual
 
         actual = formatting.format_array_flat(np.arange(3), 5)
@@ -104,11 +141,23 @@ class TestFormatting(TestCase):
         assert expected == actual
 
         actual = formatting.format_array_flat(np.arange(4.0), 11)
-        expected = '0.0 1.0 ...'
+        expected = '0.0 ... 3.0'
+        assert expected == actual
+
+        actual = formatting.format_array_flat(np.arange(0), 0)
+        expected = ''
+        assert expected == actual
+
+        actual = formatting.format_array_flat(np.arange(1), 0)
+        expected = '0'
+        assert expected == actual
+
+        actual = formatting.format_array_flat(np.arange(2), 0)
+        expected = '0 1'
         assert expected == actual
 
         actual = formatting.format_array_flat(np.arange(4), 0)
-        expected = '0 ...'
+        expected = '0 ... 3'
         assert expected == actual
 
     def test_pretty_print(self):