Repository: pydata/xarray

Issue:
# Ordering of Coords in Repr

### What is your issue?



## Problem
This preferentially placing dimension coordinates first in the order. Which makes a lot of sense. But has a negative side effect for the visualization non-dimension coords that share a dimension wiht a dim coord. e..g here word coord and the other coords sharing the word dim are disconnected:


<img width="704" height="270" alt="Image" src="https://github.com/user-attachments/assets/4977b203-b0d0-494e-aec5-dfacde5d3add" />


Or a simple example:

```python
import numpy as np

ds = xr.Dataset(
    {"data": (("a", "b", "c"), np.random.rand(3, 3, 3))},
    coords={"a": [0, 1, 2], "b": [0, 1, 2], "c": [3, 2, 1]},
)

ds = ds.assign_coords({"a_1":('a', [5,6,7])})
ds = ds.assign_coords({"b_1":('b', [5,6,7])})
ds = ds.assign_coords({"c_1":('c', [5,6,7])})
print(ds)
```

```
<xarray.Dataset> Size: 360B
Dimensions:  (a: 3, b: 3, c: 3)
Coordinates:
  * a        (a) int64 24B 0 1 2
  * b        (b) int64 24B 0 1 2
  * c        (c) int64 24B 3 2 1
    a_1      (a) int64 24B 5 6 7
    b_1      (b) int64 24B 5 6 7
    c_1      (c) int64 24B 5 6 7
Data variables:
    data     (a, b, c) float64 216B 0.2621 0.7528 0.7416 ... 0.3515 0.1 0.2662
```

## Solution

The order of coords in the repr is currently defined here:

https://github.com/pydata/xarray/blob/c3747542521b438a94d495b3e85f1658df6a69f7/xarray/core/formatting.py#L450-L452

We could (possibly optionally) change the sort key to something like this:

```python
def _coord_sort_key(name, var, dims):
    """Sort key for coordinate ordering.

        Orders by:
        1. Primary: index of the first matching dimension in dataset dims
        2. Secondary: dimension coordinates (name == dim) come before non-dimension coordinates

        This groups non-dimension coordinates right after their associated dimension
    coordinate.
    """
    # Dimension coordinates sort by their position in dims, come first (0)
    if name in dims:
        return (dims.index(name), 0)

    # Non-dimension coordinates sort by their first dim, come second (1)
    for d in var.dims:
        if d in dims:
            return (dims.index(d), 1)

    # Scalar coords or coords with dims not in dataset dims go at end
    return (len(dims), 1)
```

which gives 
```
<xarray.Dataset> Size: 360B
Dimensions:  (a: 3, b: 3, c: 3)
Coordinates:
  * a        (a) int64 24B 0 1 2
    a_1      (a) int64 24B 5 6 7
  * b        (b) int64 24B 0 1 2
    b_1      (b) int64 24B 5 6 7
  * c        (c) int64 24B 3 2 1
    c_1      (c) int64 24B 5 6 7
Data variables:
    data     (a, b, c) float64 216B 0.0534 0.7124 0.705 ... 0.5102 0.563 0.1605
```


or potentially going even farther and indenting the non-dimension coords 

Implementation patch:
diff --git a/xarray/core/common.py b/xarray/core/common.py
index 123d74dec72..80cbae822bc 100644
--- a/xarray/core/common.py
+++ b/xarray/core/common.py
@@ -619,9 +619,9 @@ def assign_coords(
         <xarray.Dataset> Size: 360B
         Dimensions:         (x: 2, y: 2, time: 4)
         Coordinates:
-          * time            (time) datetime64[ns] 32B 2014-09-06 ... 2014-09-09
             lon             (x, y) float64 32B 260.2 260.7 260.2 260.8
             lat             (x, y) float64 32B 42.25 42.21 42.63 42.59
+          * time            (time) datetime64[ns] 32B 2014-09-06 ... 2014-09-09
             reference_time  datetime64[ns] 8B 2014-09-05
         Dimensions without coordinates: x, y
         Data variables:
@@ -633,9 +633,9 @@ def assign_coords(
         <xarray.Dataset> Size: 360B
         Dimensions:         (x: 2, y: 2, time: 4)
         Coordinates:
-          * time            (time) datetime64[ns] 32B 2014-09-06 ... 2014-09-09
             lon             (x, y) float64 32B -99.83 -99.32 -99.79 -99.23
             lat             (x, y) float64 32B 42.25 42.21 42.63 42.59
+          * time            (time) datetime64[ns] 32B 2014-09-06 ... 2014-09-09
             reference_time  datetime64[ns] 8B 2014-09-05
         Dimensions without coordinates: x, y
         Data variables:
diff --git a/xarray/core/dataarray.py b/xarray/core/dataarray.py
index 706c35e5459..a6a7f1f80ae 100644
--- a/xarray/core/dataarray.py
+++ b/xarray/core/dataarray.py
@@ -369,9 +369,9 @@ class DataArray(
            [[22.60070734, 13.78914233, 14.17424919],
             [18.28478802, 16.15234857, 26.63418806]]])
     Coordinates:
-      * time            (time) datetime64[ns] 24B 2014-09-06 2014-09-07 2014-09-08
         lon             (x, y) float64 32B -99.83 -99.32 -99.79 -99.23
         lat             (x, y) float64 32B 42.25 42.21 42.63 42.59
+      * time            (time) datetime64[ns] 24B 2014-09-06 2014-09-07 2014-09-08
         reference_time  datetime64[ns] 8B 2014-09-05
     Dimensions without coordinates: x, y
     Attributes:
@@ -2807,8 +2807,8 @@ def set_index(
                [1., 1., 1.]])
         Coordinates:
           * x        (x) int64 16B 0 1
-          * y        (y) int64 24B 0 1 2
             a        (x) int64 16B 3 4
+          * y        (y) int64 24B 0 1 2
         >>> arr.set_index(x="a")
         <xarray.DataArray (x: 2, y: 3)> Size: 48B
         array([[1., 1., 1.],
@@ -5964,8 +5964,8 @@ def pad(
                [nan, nan, nan, nan]])
         Coordinates:
           * x        (x) float64 32B nan 0.0 1.0 nan
-          * y        (y) int64 32B 10 20 30 40
             z        (x) float64 32B nan 100.0 200.0 nan
+          * y        (y) int64 32B 10 20 30 40
 
         Careful, ``constant_values`` are coerced to the data type of the array which may
         lead to a loss of precision:
@@ -5978,8 +5978,8 @@ def pad(
                [ 1,  1,  1,  1]])
         Coordinates:
           * x        (x) float64 32B nan 0.0 1.0 nan
-          * y        (y) int64 32B 10 20 30 40
             z        (x) float64 32B nan 100.0 200.0 nan
+          * y        (y) int64 32B 10 20 30 40
         """
         ds = self._to_temp_dataset().pad(
             pad_width=pad_width,
diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py
index 8513f69c445..fc82e89d021 100644
--- a/xarray/core/dataset.py
+++ b/xarray/core/dataset.py
@@ -322,10 +322,10 @@ class Dataset(
     <xarray.Dataset> Size: 552B
     Dimensions:         (loc: 2, instrument: 3, time: 4)
     Coordinates:
-      * instrument      (instrument) <U8 96B 'manufac1' 'manufac2' 'manufac3'
-      * time            (time) datetime64[ns] 32B 2014-09-06 ... 2014-09-09
         lon             (loc) float64 16B -99.83 -99.32
         lat             (loc) float64 16B 42.25 42.21
+      * instrument      (instrument) <U8 96B 'manufac1' 'manufac2' 'manufac3'
+      * time            (time) datetime64[ns] 32B 2014-09-06 ... 2014-09-09
         reference_time  datetime64[ns] 8B 2014-09-05
     Dimensions without coordinates: loc
     Data variables:
@@ -4733,8 +4733,8 @@ def set_index(
         Dimensions:  (x: 2, y: 3)
         Coordinates:
           * x        (x) int64 16B 0 1
-          * y        (y) int64 24B 0 1 2
             a        (x) int64 16B 3 4
+          * y        (y) int64 24B 0 1 2
         Data variables:
             v        (x, y) float64 48B 1.0 1.0 1.0 1.0 1.0 1.0
         >>> ds.set_index(x="a")
@@ -8713,9 +8713,9 @@ def filter_by_attrs(self, **kwargs) -> Self:
         <xarray.Dataset> Size: 192B
         Dimensions:         (x: 2, y: 2, time: 3)
         Coordinates:
-          * time            (time) datetime64[ns] 24B 2014-09-06 2014-09-07 2014-09-08
             lon             (x, y) float64 32B -99.83 -99.32 -99.79 -99.23
             lat             (x, y) float64 32B 42.25 42.21 42.63 42.59
+          * time            (time) datetime64[ns] 24B 2014-09-06 2014-09-07 2014-09-08
             reference_time  datetime64[ns] 8B 2014-09-05
         Dimensions without coordinates: x, y
         Data variables:
@@ -8728,9 +8728,9 @@ def filter_by_attrs(self, **kwargs) -> Self:
         <xarray.Dataset> Size: 288B
         Dimensions:         (x: 2, y: 2, time: 3)
         Coordinates:
-          * time            (time) datetime64[ns] 24B 2014-09-06 2014-09-07 2014-09-08
             lon             (x, y) float64 32B -99.83 -99.32 -99.79 -99.23
             lat             (x, y) float64 32B 42.25 42.21 42.63 42.59
+          * time            (time) datetime64[ns] 24B 2014-09-06 2014-09-07 2014-09-08
             reference_time  datetime64[ns] 8B 2014-09-05
         Dimensions without coordinates: x, y
         Data variables:
diff --git a/xarray/core/formatting.py b/xarray/core/formatting.py
index 25437be0990..b6a6bd2c4b4 100644
--- a/xarray/core/formatting.py
+++ b/xarray/core/formatting.py
@@ -443,12 +443,38 @@ def _mapping_repr(
 )
 
 
+def _coord_sort_key(coord, dims):
+    """Sort key for coordinate ordering.
+
+        Orders by:
+        1. Primary: index of the matching dimension in dataset dims.
+        2. Secondary: dimension coordinates (name == dim) come before non-dimension coordinates
+
+        This groups non-dimension coordinates right after their associated dimension
+    coordinate.
+    """
+    name, var = coord
+
+    # Dimension coordinates come first within their dim section
+    if name in dims:
+        return (dims.index(name), 0)
+
+    # Non-dimension coordinates come second within their dim section
+    # Check the var.dims list in backwards order to put (x, y) after (x) and (y)
+    for d in var.dims[::-1]:
+        if d in dims:
+            return (dims.index(d), 1)
+
+    # Scalar coords or coords with dims not in dataset dims go at the end
+    return (len(dims), 1)
+
+
 def coords_repr(coords: AbstractCoordinates, col_width=None, max_rows=None):
     if col_width is None:
         col_width = _calculate_col_width(coords)
     dims = tuple(coords._data.dims)
     dim_ordered_coords = sorted(
-        coords.items(), key=lambda x: dims.index(x[0]) if x[0] in dims else len(dims)
+        coords.items(), key=functools.partial(_coord_sort_key, dims=dims)
     )
     return _mapping_repr(
         dict(dim_ordered_coords),
diff --git a/xarray/core/formatting_html.py b/xarray/core/formatting_html.py
index 3f7dbc3d4b9..cfe5753904b 100644
--- a/xarray/core/formatting_html.py
+++ b/xarray/core/formatting_html.py
@@ -11,6 +11,7 @@
 from typing import TYPE_CHECKING
 
 from xarray.core.formatting import (
+    _coord_sort_key,
     filter_nondefault_indexes,
     inherited_vars,
     inline_index_repr,
@@ -120,7 +121,7 @@ def summarize_coords(variables) -> str:
     li_items = []
     dims = tuple(variables._data.dims)
     dim_ordered_coords = sorted(
-        variables.items(), key=lambda x: dims.index(x[0]) if x[0] in dims else len(dims)
+        variables.items(), key=partial(_coord_sort_key, dims=dims)
     )
     for k, v in dim_ordered_coords:
         li_content = summarize_variable(k, v, is_index=k in variables.xindexes)
diff --git a/xarray/structure/concat.py b/xarray/structure/concat.py
index 69b05880e3d..4ed4d3e3641 100644
--- a/xarray/structure/concat.py
+++ b/xarray/structure/concat.py
@@ -239,8 +239,8 @@ def concat(
     array([[0, 1, 2],
            [3, 4, 5]])
     Coordinates:
-      * y        (y) int64 24B 10 20 30
         x        (new_dim) <U1 8B 'a' 'b'
+      * y        (y) int64 24B 10 20 30
     Dimensions without coordinates: new_dim
 
     >>> xr.concat(
@@ -253,8 +253,8 @@ def concat(
            [3, 4, 5]])
     Coordinates:
       * new_dim  (new_dim) int64 16B -90 -100
-      * y        (y) int64 24B 10 20 30
         x        (new_dim) <U1 8B 'a' 'b'
+      * y        (y) int64 24B 10 20 30
 
     # Concatenate a scalar variable along a new dimension of the same name with and without creating a new index
 


Test patch:
diff --git a/xarray/tests/test_dataset.py b/xarray/tests/test_dataset.py
index 656ce9575de..2848ae5a7be 100644
--- a/xarray/tests/test_dataset.py
+++ b/xarray/tests/test_dataset.py
@@ -294,8 +294,8 @@ def test_repr(self) -> None:
             Coordinates:
               * dim2     (dim2) float64 72B 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0
               * dim3     (dim3) {data["dim3"].dtype} 40B 'a' 'b' 'c' 'd' 'e' 'f' 'g' 'h' 'i' 'j'
-              * time     (time) datetime64[ns] 160B 2000-01-01 2000-01-02 ... 2000-01-20
                 numbers  (dim3) int64 80B 0 1 2 0 0 1 1 2 2 3
+              * time     (time) datetime64[ns] 160B 2000-01-01 2000-01-02 ... 2000-01-20
             Dimensions without coordinates: dim1
             Data variables:
                 var1     (dim1, dim2) float64 576B -0.9891 -0.3678 1.288 ... -0.2116 0.364
@@ -875,8 +875,8 @@ def test_coords_properties(self) -> None:
             """\
         Coordinates:
           * x        (x) int64 16B -1 -2
-          * y        (y) int64 24B 0 1 2
             a        (x) int64 16B 4 5
+          * y        (y) int64 24B 0 1 2
             b        int64 8B -10"""
         )
         actual = repr(coords)
diff --git a/xarray/tests/test_datatree.py b/xarray/tests/test_datatree.py
index cb9e81da97a..9384270a0a6 100644
--- a/xarray/tests/test_datatree.py
+++ b/xarray/tests/test_datatree.py
@@ -644,8 +644,8 @@ def test_properties(self) -> None:
             """\
         Coordinates:
           * x        (x) int64 16B -1 -2
-          * y        (y) int64 24B 0 1 2
             a        (x) int64 16B 4 5
+          * y        (y) int64 24B 0 1 2
             b        int64 8B -10"""
         )
         actual = repr(coords)