diff --git a/docs/source/_static/js/sortable-numeric.js b/docs/source/_static/js/sortable-numeric.js new file mode 100644 index 000000000..c648087bc --- /dev/null +++ b/docs/source/_static/js/sortable-numeric.js @@ -0,0 +1,64 @@ +/* + * sortable-numeric.js + * + * DataTables custom column type: "num-varies". + * + * The paradigm summary tables on dataset_summary.html are made sortable by a + * single shared `$('.sortable').DataTable()` call with no per-column config, so + * DataTables has to auto-detect each column's type. Its numeric detector is + * all-or-nothing: one non-numeric cell downgrades the whole column to string + * (lexicographic) sorting. Several numeric columns ("Total_trials", + * "#Trials / class", "#Runs") carry the sentinel "varies" for datasets whose + * trial count is not fixed, which silently broke numeric sorting for them + * (e.g. Total_trials ordered 11000, 1114, 11496, 1200, ...). + * + * This plugin registers a column type that treats a column as numeric when + * every cell is EITHER a number OR a known sentinel, ordering the sentinel + * rows as -Infinity so they sink to the bottom of a largest-first sort while + * still DISPLAYING their original text ("varies") to the reader. + */ +(function () { + "use strict"; + + var DataTable = jQuery.fn.dataTable; + + // Tokens that stand in for "no single numeric value". Matched case-folded and + // trimmed; the empty string counts too, so one blank cell can't silently + // re-trigger the string-sort bug. + var SENTINELS = ["varies", "n/a", "na", "-", "—", ""]; + + // Strip any HTML DataTables may hand us and normalise for comparison. + function clean(value) { + return String(value).replace(/<[^>]*>/g, "").trim(); + } + + function isSentinel(text) { + return SENTINELS.indexOf(text.toLowerCase()) !== -1; + } + + function asNumber(text) { + return parseFloat(text.replace(/,/g, "")); + } + + DataTable.type("num-varies", { + // Claim a column only if EVERY cell is a number or a known sentinel. + // A named custom type is always checked before the built-in "string" + // fallback, so a mixed number/"varies" column lands here rather than being + // sorted lexicographically. + detect: function (value) { + var text = clean(value); + return isSentinel(text) || !isNaN(asNumber(text)); + }, + // Ordering key: real numbers sort normally; sentinels pin to the bottom. + order: { + pre: function (value) { + var text = clean(value); + if (isSentinel(text)) { + return -Infinity; + } + var n = asNumber(text); + return isNaN(n) ? -Infinity : n; + }, + }, + }); +})(); diff --git a/docs/source/conf.py b/docs/source/conf.py index c7cf0b98d..846ba919e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -381,6 +381,9 @@ def linkcode_resolve(domain, info): # noqa: C901 "https://cdn.datatables.net/searchpanes/2.3.5/js/searchPanes.dataTables.js", # FixedHeader (sticky header on scroll) "https://cdn.datatables.net/fixedheader/4.0.6/js/dataTables.fixedHeader.js", + # Custom "num-varies" column type: numeric sort for columns that mix numbers + # with the "varies" sentinel (used by the .sortable paradigm tables). + "js/sortable-numeric.js", "js/section-nav-hierarchy.js", "js/macro-table.js", ] diff --git a/docs/source/whats_new.rst b/docs/source/whats_new.rst index 08418d41e..81358531c 100644 --- a/docs/source/whats_new.rst +++ b/docs/source/whats_new.rst @@ -74,6 +74,8 @@ Bugs - Fix missing electrode positions (NaN xyz) in six motor-imagery datasets so topographic maps, interpolation, and spatial methods work: :class:`moabb.datasets.Forenzo2023` and :class:`moabb.datasets.GuttmannFlury2025_MI`/``_ME`` normalize Neuroscan ALL_CAPS labels and apply ``standard_1005`` (CB1/CB2 kept as ``misc``); :class:`moabb.datasets.Dreyer2023` falls back to ``standard_1005`` when the BIDS archive ships no ``electrodes.tsv``; :class:`moabb.datasets.BNCI2003_004` maps its 26 legacy Berlin channel labels to their modern 10-5 equivalents for exact positions; :class:`moabb.datasets.BNCI2014_002` applies an approximate 3x5 grid for its unlabeled small-Laplacian channels; and :class:`moabb.datasets.Zhang2017` applies the ``GSN-HydroCel-32`` montage in EGI sensor order. Adds the shared :func:`moabb.datasets.utils.set_neuroscan_montage` helper (:gh:`1089` by `Bruno Aristimunha`_). - Fix ``BaseEvaluation._aggregate_fold_results`` aborting the whole evaluation with ``TypeError: agg function failed [how->mean,dtype->object]`` when a single fold contributes a non-numeric ``score`` (e.g. an error fold). The numeric aggregation columns are now coerced with ``pandas.to_numeric(errors="coerce")`` before ``groupby.agg``, so a bad fold becomes ``NaN`` and is skipped instead of taking down every subject/pipeline (:gh:`1095` by `Bruno Aristimunha`_). - Fix :class:`moabb.evaluations.splitters.WithinSessionSplitter` and :class:`moabb.evaluations.splitters.WithinSubjectSplitter` overwriting an explicit ``n_splits`` passed through ``cv_kwargs`` with the ``n_folds`` default; the caller-provided ``n_splits`` now takes precedence, so a single holdout split can be requested directly via ``cv_class=StratifiedShuffleSplit, n_splits=1``. :class:`moabb.evaluations.WithinSessionEvaluation` and :class:`moabb.evaluations.WithinSubjectEvaluation` now honour the ``n_splits`` argument instead of always running 5 folds, and :class:`moabb.evaluations.splitters.WithinSubjectSplitter` now yields reproducible per-subject folds for a fixed ``random_state`` (:gh:`1106` by `Bruno Aristimunha`_). +- Fix numeric sorting in the dataset summary tables (:doc:`dataset_summary`): columns containing the ``varies`` sentinel (e.g. ``Total_trials``) were auto-detected as strings by DataTables and sorted lexicographically (``11000 < 1114 < 11496``). A custom ``num-varies`` column type now treats such columns as numeric, sorting sentinel rows last while keeping their displayed text unchanged (:gh:`1118` by `Bhargav Kowshik`_). + Code health ~~~~~~~~~~~ - Install CPU-only PyTorch wheels in CI by setting ``UV_TORCH_BACKEND=cpu`` in the test, braindecode, and docs workflows, so runners no longer download multi-GB CUDA builds of ``torch`` (pulled transitively via the ``deeplearning`` extra / braindecode) (:gh:`1083` by `Bhargav Kowshik`_).