diff --git a/docs/changelog.md b/docs/changelog.md index 4ca30298..2677b3e1 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -50,6 +50,9 @@ behavior they verify rather than listed separately. Escape, and outside clicks release filter focus so global shortcuts resume. - Display metadata columns containing multiple values in the Cluster and Similarity Views instead of leaving their cells blank. +- Sort text columns holding numbers, such as the channel column `ch`, by + numeric value in the Cluster and Similarity Views. Channel 2 no longer + appears after channel 10. ### Changed diff --git a/phy/gui/tests/test_widgets.py b/phy/gui/tests/test_widgets.py index 533528b1..7e2c054b 100644 --- a/phy/gui/tests/test_widgets.py +++ b/phy/gui/tests/test_widgets.py @@ -566,6 +566,36 @@ def test_table_change_and_sort_2(qtbot, table): _assert(table.get_ids, [9, 8, 7, 6, 4, 3, 2, 1, 0, 5]) +def test_table_sort_numeric_strings(qtbot): + # The `ch` column of the cluster view holds channel labels, which are strings. + # A plain string comparison sorts '10' before '2', so numeric strings have to be + # compared as numbers. + data = [{'id': i, 'ch': ch} for i, ch in enumerate(['2', '10', '1', '21', '3'])] + table = Table(columns=['id', 'ch'], value_names=['id', 'ch'], data=data) + _wait_until_table_ready(qtbot, table) + + table.sort_by('ch', 'asc') + _assert(table.get_ids, [2, 0, 4, 1, 3]) + + table.sort_by('ch', 'desc') + _assert(table.get_ids, [3, 1, 4, 0, 2]) + + table.close() + + +def test_table_sort_mixed_strings(qtbot): + # In a column that mixes numbers and free text, the numbers come first in numeric + # order and the remaining entries keep their string ordering. + data = [{'id': i, 'label': label} for i, label in enumerate(['mua', '10', 'good', '2'])] + table = Table(columns=['id', 'label'], value_names=['id', 'label'], data=data) + _wait_until_table_ready(qtbot, table) + + table.sort_by('label', 'asc') + _assert(table.get_ids, [3, 1, 2, 0]) + + table.close() + + def test_table_change_metadata_preserves_sort(qtbot): data = [ {'id': 0, 'count': 30, 'group': 'noise'}, diff --git a/phy/gui/widgets.py b/phy/gui/widgets.py index 209c79de..5a17c5e1 100644 --- a/phy/gui/widgets.py +++ b/phy/gui/widgets.py @@ -9,6 +9,7 @@ import inspect import json import logging +import math import re import sys from contextlib import contextmanager @@ -330,6 +331,25 @@ def predicate(row): return predicate, True +def _text_sort_key(value): + """Return a sort key that orders numeric text by value rather than character by character. + + Columns such as `ch` hold channel labels, which are strings, so a plain string + comparison places '10' before '2'. Numeric entries sort first, in numeric order, and + the remaining entries keep their string ordering. + + """ + text = value if isinstance(value, str) else str(value) + try: + number = float(text) + except ValueError: + return (1, 0.0, text) + # NaN has no consistent ordering, so keep it with the non-numeric entries. + if math.isnan(number): + return (1, 0.0, text) + return (0, number, '') + + class _TableModel(QAbstractTableModel): """Model backing the native Qt table.""" @@ -438,6 +458,8 @@ def lessThan(self, left, right): return False if right_value is None: return True + if isinstance(left_value, str) or isinstance(right_value, str): + return _text_sort_key(left_value) < _text_sort_key(right_value) try: return bool(left_value < right_value) except TypeError: