From 973401773a42e64d90dad83cb3d7b8ea290fa838 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Wed, 5 Aug 2026 05:42:48 -0700 Subject: [PATCH] Sort numeric text columns by value in native tables The cluster and similarity views store channel labels as strings, so the table proxy model compared them character by character and placed '10' before '2'. Compare numeric text by numeric value instead, and keep non-numeric entries in their string order. Fixes #1405 --- docs/changelog.md | 3 +++ phy/gui/tests/test_widgets.py | 30 ++++++++++++++++++++++++++++++ phy/gui/widgets.py | 22 ++++++++++++++++++++++ 3 files changed, 55 insertions(+) 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: