From 90088494a18672cdd8785739e52593c99a87fe07 Mon Sep 17 00:00:00 2001 From: Pavel Popov Date: Mon, 20 Apr 2026 18:02:55 -0400 Subject: [PATCH 01/11] FIX: correct label orientation for 0-90 deg nodes in plot_connectivity_circle --- mne/viz/circle.py | 2 +- mne/viz/tests/test_connectivity_circle.py | 60 +++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 mne/viz/tests/test_connectivity_circle.py diff --git a/mne/viz/circle.py b/mne/viz/circle.py index 67a47c0d5fd..a0563fd3767 100644 --- a/mne/viz/circle.py +++ b/mne/viz/circle.py @@ -338,7 +338,7 @@ def _plot_connectivity_circle( # Draw node labels angles_deg = 180 * node_angles / np.pi for name, angle_rad, angle_deg in zip(node_names, node_angles, angles_deg): - if angle_deg >= 270: + if angle_deg >= 270 or angle_deg < 90: # [0, 90] and [270, 360] cover right half ha = "left" else: # Flip the label, so text is always upright diff --git a/mne/viz/tests/test_connectivity_circle.py b/mne/viz/tests/test_connectivity_circle.py new file mode 100644 index 00000000000..15786f407fa --- /dev/null +++ b/mne/viz/tests/test_connectivity_circle.py @@ -0,0 +1,60 @@ +# Authors: The MNE-Python contributors. +# License: BSD-3-Clause +# Copyright the MNE-Python contributors. + +import matplotlib +import matplotlib.pyplot as plt +import numpy as np +import pytest + +from mne.viz.circle import _plot_connectivity_circle + + +def test_plot_connectivity_circle_label_orientation(): + """Labels in the 0-90 deg polar range (12-3 o'clock) must not be flipped. + + Regression test: previously the condition ``angle_deg >= 270`` missed the + [0, 90) range, incorrectly adding 180 degrees to those labels and setting + ha='right', which caused them to point inward instead of outward. + """ + # 8 nodes → uniform angles: 0, 45, 90, 135, 180, 225, 270, 315 degrees. + # This guarantees coverage of all four quadrants, including the previously + # broken 0–90 range (nodes n0 at 0° and n1 at 45°). + n_nodes = 9 + rng = np.random.default_rng(0) + con = rng.uniform(0, 1, size=(n_nodes, n_nodes)) + np.fill_diagonal(con, 0) + node_names = [f"n{i}" for i in range(n_nodes)] + + fig, ax = _plot_connectivity_circle(con, node_names, show=False) + + texts = [c for c in ax.get_children() if isinstance(c, matplotlib.text.Text)] + label_texts = {t.get_text(): t for t in texts if t.get_text() in node_names} + + # node_angles defaults to np.linspace(0, 2*pi, n_nodes, endpoint=False) + angles_deg = np.linspace(0, 360, n_nodes, endpoint=False) + + assert len(label_texts) == n_nodes, ( + f"Expected {n_nodes} label texts, found {len(label_texts)}" + ) + + for i, name in enumerate(node_names): + angle = angles_deg[i] + t = label_texts[name] + ha = t.get_ha() + + if angle >= 270 or angle < 90: + # Right half of circle: text must extend outward to the right. + # ha='left' anchors the left edge at the node, text goes rightward. + assert ha == "left", ( + f"Node '{name}' at {angle:.1f}° (right half) should have " + f"ha='left', got '{ha}'" + ) + else: + # Left half: text is flipped 180° so it stays upright; ha='right' + # anchors the right edge at the node, text extends leftward/outward. + assert ha == "right", ( + f"Node '{name}' at {angle:.1f}° (left half) should have " + f"ha='right', got '{ha}'" + ) + plt.close(fig) From bb860b8875d9f987ef82c9758e4a9f3d987006ed Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 22:32:14 +0000 Subject: [PATCH 02/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mne/viz/circle.py | 4 +++- mne/viz/tests/test_connectivity_circle.py | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mne/viz/circle.py b/mne/viz/circle.py index a0563fd3767..21d8b2b2696 100644 --- a/mne/viz/circle.py +++ b/mne/viz/circle.py @@ -338,7 +338,9 @@ def _plot_connectivity_circle( # Draw node labels angles_deg = 180 * node_angles / np.pi for name, angle_rad, angle_deg in zip(node_names, node_angles, angles_deg): - if angle_deg >= 270 or angle_deg < 90: # [0, 90] and [270, 360] cover right half + if ( + angle_deg >= 270 or angle_deg < 90 + ): # [0, 90] and [270, 360] cover right half ha = "left" else: # Flip the label, so text is always upright diff --git a/mne/viz/tests/test_connectivity_circle.py b/mne/viz/tests/test_connectivity_circle.py index 15786f407fa..482fa860a87 100644 --- a/mne/viz/tests/test_connectivity_circle.py +++ b/mne/viz/tests/test_connectivity_circle.py @@ -5,7 +5,6 @@ import matplotlib import matplotlib.pyplot as plt import numpy as np -import pytest from mne.viz.circle import _plot_connectivity_circle From 7d5c9931e220c52483ad205245a7df3b9022be57 Mon Sep 17 00:00:00 2001 From: Pavel Popov Date: Mon, 20 Apr 2026 18:47:38 -0400 Subject: [PATCH 03/11] add changelog entry for PR #13855 --- doc/changes/dev/13855.bugfix.rst | 3 +++ doc/changes/names.inc | 1 + 2 files changed, 4 insertions(+) create mode 100644 doc/changes/dev/13855.bugfix.rst diff --git a/doc/changes/dev/13855.bugfix.rst b/doc/changes/dev/13855.bugfix.rst new file mode 100644 index 00000000000..b39b72f4ae8 --- /dev/null +++ b/doc/changes/dev/13855.bugfix.rst @@ -0,0 +1,3 @@ +Fixed incorrect label orientation for nodes in the 0–90° polar range (the +12–3 o'clock quadrant) of :func:`mne.viz.circle._plot_connectivity_circle`, by +:newcontrib:`Pavel Popov`. diff --git a/doc/changes/names.inc b/doc/changes/names.inc index d3dbb9c8995..2c7157a96fc 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -370,3 +370,4 @@ .. _Zhi Zhang: https://github.com/tczhangzhi/ .. _Ziyi ZENG: https://github.com/ZiyiTsang .. _Zvi Baratz: https://github.com/ZviBaratz +.. _Pavel Popov: https://github.com/paavalipopov From 58ab11a819fcfcfa7b76171ad015340be8658650 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 22:50:09 +0000 Subject: [PATCH 04/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- doc/changes/names.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 2c7157a96fc..54b131eaf87 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -257,6 +257,7 @@ .. _Paul Pasler: https://github.com/ppasler .. _Paul Roujansky: https://github.com/paulroujansky .. _Pavel Navratil: https://github.com/navrpa13 +.. _Pavel Popov: https://github.com/paavalipopov .. _Peter Molfese: https://github.com/pmolfese .. _Phillip Alday: https://palday.bitbucket.io .. _Pierre Ablin: https://pierreablin.com @@ -370,4 +371,3 @@ .. _Zhi Zhang: https://github.com/tczhangzhi/ .. _Ziyi ZENG: https://github.com/ZiyiTsang .. _Zvi Baratz: https://github.com/ZviBaratz -.. _Pavel Popov: https://github.com/paavalipopov From cdf406051a5358783f6cad5c9dd6bdb15f706cf3 Mon Sep 17 00:00:00 2001 From: Pavel Popov Date: Mon, 20 Apr 2026 18:53:35 -0400 Subject: [PATCH 05/11] fixed test description --- mne/viz/tests/test_connectivity_circle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/tests/test_connectivity_circle.py b/mne/viz/tests/test_connectivity_circle.py index 482fa860a87..21cfdb2d4a7 100644 --- a/mne/viz/tests/test_connectivity_circle.py +++ b/mne/viz/tests/test_connectivity_circle.py @@ -16,7 +16,7 @@ def test_plot_connectivity_circle_label_orientation(): [0, 90) range, incorrectly adding 180 degrees to those labels and setting ha='right', which caused them to point inward instead of outward. """ - # 8 nodes → uniform angles: 0, 45, 90, 135, 180, 225, 270, 315 degrees. + # 9 nodes → uniform angles: 0, 40, 80, 120, 160, 200, 240, 280, 320 degrees. # This guarantees coverage of all four quadrants, including the previously # broken 0–90 range (nodes n0 at 0° and n1 at 45°). n_nodes = 9 From 7424a3d7491b4e457845ce576bbb31ad21e86465 Mon Sep 17 00:00:00 2001 From: Pavel Popov Date: Mon, 20 Apr 2026 19:05:49 -0400 Subject: [PATCH 06/11] DOC: update changelog entry to remove private function reference --- doc/changes/dev/13855.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/dev/13855.bugfix.rst b/doc/changes/dev/13855.bugfix.rst index b39b72f4ae8..422409d9ec1 100644 --- a/doc/changes/dev/13855.bugfix.rst +++ b/doc/changes/dev/13855.bugfix.rst @@ -1,3 +1,3 @@ Fixed incorrect label orientation for nodes in the 0–90° polar range (the -12–3 o'clock quadrant) of :func:`mne.viz.circle._plot_connectivity_circle`, by +12–3 o'clock quadrant) of the connectivity circle plot, by :newcontrib:`Pavel Popov`. From f96ce9febe1142ab88eec86f4e667e5ee7db98fd Mon Sep 17 00:00:00 2001 From: Pavel Popov Date: Mon, 20 Apr 2026 19:07:03 -0400 Subject: [PATCH 07/11] add _plot_connectivity_circle label orientation test --- mne/viz/tests/test_circle.py | 53 ++++++++++++++++++++ mne/viz/tests/test_connectivity_circle.py | 59 ----------------------- 2 files changed, 53 insertions(+), 59 deletions(-) delete mode 100644 mne/viz/tests/test_connectivity_circle.py diff --git a/mne/viz/tests/test_circle.py b/mne/viz/tests/test_circle.py index c5f3719746b..a3debfc7f5a 100644 --- a/mne/viz/tests/test_circle.py +++ b/mne/viz/tests/test_circle.py @@ -4,9 +4,12 @@ import matplotlib +import matplotlib.pyplot as plt +import numpy as np import pytest from mne.viz import plot_channel_labels_circle +from mne.viz.circle import _plot_connectivity_circle @pytest.mark.filterwarnings( @@ -33,3 +36,53 @@ def test_plot_channel_labels_circle(): plot_channel_labels_circle( dict(brain=["big", "great", "smart"]), colors=dict(big="r", great="y") ) + + +def test_plot_connectivity_circle_label_orientation(): + """Labels in the 0-90 deg polar range (12-3 o'clock) must not be flipped. + + Regression test: previously the condition ``angle_deg >= 270`` missed the + [0, 90) range, incorrectly adding 180 degrees to those labels and setting + ha='right', which caused them to point inward instead of outward. + """ + # 9 nodes → uniform angles: 0, 40, 80, 120, 160, 200, 240, 280, 320 degrees. + # This guarantees coverage of all four quadrants, including the previously + # broken 0–90 range (nodes n0 at 0° and n1 at 40°). + n_nodes = 9 + rng = np.random.default_rng(0) + con = rng.uniform(0, 1, size=(n_nodes, n_nodes)) + np.fill_diagonal(con, 0) + node_names = [f"n{i}" for i in range(n_nodes)] + + fig, ax = _plot_connectivity_circle(con, node_names, show=False) + + texts = [c for c in ax.get_children() if isinstance(c, matplotlib.text.Text)] + label_texts = {t.get_text(): t for t in texts if t.get_text() in node_names} + + # node_angles defaults to np.linspace(0, 2*pi, n_nodes, endpoint=False) + angles_deg = np.linspace(0, 360, n_nodes, endpoint=False) + + assert len(label_texts) == n_nodes, ( + f"Expected {n_nodes} label texts, found {len(label_texts)}" + ) + + for i, name in enumerate(node_names): + angle = angles_deg[i] + t = label_texts[name] + ha = t.get_ha() + + if angle >= 270 or angle < 90: + # Right half of circle: text must extend outward to the right. + # ha='left' anchors the left edge at the node, text goes rightward. + assert ha == "left", ( + f"Node '{name}' at {angle:.1f}° (right half) should have " + f"ha='left', got '{ha}'" + ) + else: + # Left half: text is flipped 180° so it stays upright; ha='right' + # anchors the right edge at the node, text extends leftward/outward. + assert ha == "right", ( + f"Node '{name}' at {angle:.1f}° (left half) should have " + f"ha='right', got '{ha}'" + ) + plt.close(fig) diff --git a/mne/viz/tests/test_connectivity_circle.py b/mne/viz/tests/test_connectivity_circle.py deleted file mode 100644 index 21cfdb2d4a7..00000000000 --- a/mne/viz/tests/test_connectivity_circle.py +++ /dev/null @@ -1,59 +0,0 @@ -# Authors: The MNE-Python contributors. -# License: BSD-3-Clause -# Copyright the MNE-Python contributors. - -import matplotlib -import matplotlib.pyplot as plt -import numpy as np - -from mne.viz.circle import _plot_connectivity_circle - - -def test_plot_connectivity_circle_label_orientation(): - """Labels in the 0-90 deg polar range (12-3 o'clock) must not be flipped. - - Regression test: previously the condition ``angle_deg >= 270`` missed the - [0, 90) range, incorrectly adding 180 degrees to those labels and setting - ha='right', which caused them to point inward instead of outward. - """ - # 9 nodes → uniform angles: 0, 40, 80, 120, 160, 200, 240, 280, 320 degrees. - # This guarantees coverage of all four quadrants, including the previously - # broken 0–90 range (nodes n0 at 0° and n1 at 45°). - n_nodes = 9 - rng = np.random.default_rng(0) - con = rng.uniform(0, 1, size=(n_nodes, n_nodes)) - np.fill_diagonal(con, 0) - node_names = [f"n{i}" for i in range(n_nodes)] - - fig, ax = _plot_connectivity_circle(con, node_names, show=False) - - texts = [c for c in ax.get_children() if isinstance(c, matplotlib.text.Text)] - label_texts = {t.get_text(): t for t in texts if t.get_text() in node_names} - - # node_angles defaults to np.linspace(0, 2*pi, n_nodes, endpoint=False) - angles_deg = np.linspace(0, 360, n_nodes, endpoint=False) - - assert len(label_texts) == n_nodes, ( - f"Expected {n_nodes} label texts, found {len(label_texts)}" - ) - - for i, name in enumerate(node_names): - angle = angles_deg[i] - t = label_texts[name] - ha = t.get_ha() - - if angle >= 270 or angle < 90: - # Right half of circle: text must extend outward to the right. - # ha='left' anchors the left edge at the node, text goes rightward. - assert ha == "left", ( - f"Node '{name}' at {angle:.1f}° (right half) should have " - f"ha='left', got '{ha}'" - ) - else: - # Left half: text is flipped 180° so it stays upright; ha='right' - # anchors the right edge at the node, text extends leftward/outward. - assert ha == "right", ( - f"Node '{name}' at {angle:.1f}° (left half) should have " - f"ha='right', got '{ha}'" - ) - plt.close(fig) From bc84754581f43502333bb4b4af8ff144376cdda3 Mon Sep 17 00:00:00 2001 From: Pavel Popov Date: Tue, 21 Apr 2026 16:37:10 -0400 Subject: [PATCH 08/11] fix to test description --- mne/viz/tests/test_circle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/tests/test_circle.py b/mne/viz/tests/test_circle.py index a3debfc7f5a..eec63757a27 100644 --- a/mne/viz/tests/test_circle.py +++ b/mne/viz/tests/test_circle.py @@ -47,7 +47,7 @@ def test_plot_connectivity_circle_label_orientation(): """ # 9 nodes → uniform angles: 0, 40, 80, 120, 160, 200, 240, 280, 320 degrees. # This guarantees coverage of all four quadrants, including the previously - # broken 0–90 range (nodes n0 at 0° and n1 at 40°). + # broken 0–90 range (nodes n0 at 0°, n1 at 40°, and n2 at 80°). n_nodes = 9 rng = np.random.default_rng(0) con = rng.uniform(0, 1, size=(n_nodes, n_nodes)) From 852eea84c67e4a48a528a8c2f77ea661b7c07371 Mon Sep 17 00:00:00 2001 From: Daniel McCloy Date: Thu, 23 Apr 2026 09:26:43 -0500 Subject: [PATCH 09/11] Apply suggestions from code review Co-authored-by: Daniel McCloy --- mne/viz/tests/test_circle.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/mne/viz/tests/test_circle.py b/mne/viz/tests/test_circle.py index eec63757a27..a38d072801b 100644 --- a/mne/viz/tests/test_circle.py +++ b/mne/viz/tests/test_circle.py @@ -49,8 +49,7 @@ def test_plot_connectivity_circle_label_orientation(): # This guarantees coverage of all four quadrants, including the previously # broken 0–90 range (nodes n0 at 0°, n1 at 40°, and n2 at 80°). n_nodes = 9 - rng = np.random.default_rng(0) - con = rng.uniform(0, 1, size=(n_nodes, n_nodes)) + con = np.ones((n_nodes, n_nodes)) np.fill_diagonal(con, 0) node_names = [f"n{i}" for i in range(n_nodes)] @@ -66,8 +65,7 @@ def test_plot_connectivity_circle_label_orientation(): f"Expected {n_nodes} label texts, found {len(label_texts)}" ) - for i, name in enumerate(node_names): - angle = angles_deg[i] + for angle, name in zip(angles_deg, node_names, strict=True): t = label_texts[name] ha = t.get_ha() @@ -85,4 +83,3 @@ def test_plot_connectivity_circle_label_orientation(): f"Node '{name}' at {angle:.1f}° (left half) should have " f"ha='right', got '{ha}'" ) - plt.close(fig) From ec6d61648be651a597e83a611dea272facb52bed Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 23 Apr 2026 14:27:02 +0000 Subject: [PATCH 10/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- mne/viz/tests/test_circle.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mne/viz/tests/test_circle.py b/mne/viz/tests/test_circle.py index a38d072801b..a64cc39046f 100644 --- a/mne/viz/tests/test_circle.py +++ b/mne/viz/tests/test_circle.py @@ -4,7 +4,6 @@ import matplotlib -import matplotlib.pyplot as plt import numpy as np import pytest From 91e82bb210eb3e21fc40aef80914a176ae050f0e Mon Sep 17 00:00:00 2001 From: Pavel Popov Date: Fri, 24 Apr 2026 13:40:38 -0400 Subject: [PATCH 11/11] add variability to test connectivity --- mne/viz/tests/test_circle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/tests/test_circle.py b/mne/viz/tests/test_circle.py index a64cc39046f..092a9cdd481 100644 --- a/mne/viz/tests/test_circle.py +++ b/mne/viz/tests/test_circle.py @@ -49,7 +49,7 @@ def test_plot_connectivity_circle_label_orientation(): # broken 0–90 range (nodes n0 at 0°, n1 at 40°, and n2 at 80°). n_nodes = 9 con = np.ones((n_nodes, n_nodes)) - np.fill_diagonal(con, 0) + con[::2, ::2] = 0 # add some zeros to avoid 0-div in normalization node_names = [f"n{i}" for i in range(n_nodes)] fig, ax = _plot_connectivity_circle(con, node_names, show=False)