Skip to content

Commit 0f02104

Browse files
cbrnrsappelhoff
andauthored
Preserve order of elec_names (#12)
* Preserve order of elec_names * Remove default scatter color * Fix tests * Add changelog entry * Extend plotting examples * Some style fixes * Shorten lines to avoid scrollbars * Change thumbnail Co-authored-by: Stefan Appelhoff <[email protected]> Co-authored-by: Stefan Appelhoff <[email protected]>
1 parent 4bcdcb8 commit 0f02104

File tree

5 files changed

+89
-24
lines changed

5 files changed

+89
-24
lines changed

docs/changes.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Changelog
1010
2.1.0 (unreleased)
1111
------------------
1212
- Add a ``show_axis`` parameter to :func:`eeg_positions.plot_coords`, by `Clemens Brunner`_ (:github:`#7`)
13+
- Add a ``sort`` parameter to :func:`eeg_positions.get_coords`, by `Clemens Brunner`_ (:github:`#12`)
14+
- Allow passing a list of colors to :func:`eeg_positions.plot_coords`, by `Clemens Brunner`_ (:github:`#12`)
1315

1416
2.0.0 (2021-02-13)
1517
------------------

eeg_positions/compute.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ def get_elec_coords(
152152
dim="2d",
153153
as_mne_montage=False,
154154
equator="Nz-T10-Iz-T9",
155+
sort=False,
155156
):
156157
"""Get standard EEG electrode coordinates.
157158
@@ -213,6 +214,9 @@ def get_elec_coords(
213214
as the equator, several electrodes may be drawn outside a circular
214215
head shape when projecting to 2D.
215216
Defaults to ``"Nz-T10-Iz-T9"``.
217+
sort : bool
218+
Whether to sort the returned coordinates alphabetically. If ``False`` (default),
219+
preserve the order of ``elec_names`` if available.
216220
217221
Returns
218222
-------
@@ -423,9 +427,14 @@ def get_elec_coords(
423427
# --------------------
424428
if len(elec_names) > 0:
425429
selection = df.label.isin(elec_names)
430+
df_selection = df.loc[selection, :].copy()
431+
if not sort:
432+
df_selection = (
433+
df_selection.set_index("label").reindex(elec_names).reset_index()
434+
)
426435
else:
427436
selection = df.label.isin(system + LANDMARKS)
428-
df_selection = df.loc[selection, :].copy()
437+
df_selection = df.loc[selection, :].copy()
429438

430439
# add special elec positions
431440
pos_to_add = {}
@@ -516,8 +525,9 @@ def get_elec_coords(
516525
df_selection.loc[:, "y"] = ys
517526

518527
df_selection = df_selection.drop_duplicates(subset=["label"])
519-
coords = df_selection.sort_values(by="label")
520-
coords = coords.reset_index(drop=True)
528+
if sort:
529+
df_selection = df_selection.sort_values(by="label")
530+
coords = df_selection.reset_index(drop=True)
521531
return coords
522532

523533

@@ -555,6 +565,7 @@ def _produce_files_and_do_x(x="save"):
555565
dim=dim.lower(),
556566
as_mne_montage=False,
557567
equator=equator,
568+
sort=True,
558569
)
559570

560571
fname = fname_template.format(equator, system, dim)

eeg_positions/tests/test_compute.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,16 @@
1919
("2d", "3d"),
2020
(True, False),
2121
("Nz-T10-Iz-T9", "Fpz-T8-Oz-T7"),
22+
(True, False),
2223
)
2324

2425

2526
@pytest.mark.parametrize(
26-
"system, elec_names, drop_landmarks, dim, as_mne_montage, equator", valid_inputs
27+
"system, elec_names, drop_landmarks, dim, as_mne_montage, equator, sort",
28+
valid_inputs,
2729
)
2830
def test_get_elec_coords(
29-
system, elec_names, drop_landmarks, dim, as_mne_montage, equator
31+
system, elec_names, drop_landmarks, dim, as_mne_montage, equator, sort
3032
):
3133
"""Smoke test the get_elec_coords function."""
3234
get_elec_coords(
@@ -36,6 +38,7 @@ def test_get_elec_coords(
3638
dim=dim,
3739
as_mne_montage=as_mne_montage,
3840
equator=equator,
41+
sort=sort,
3942
)
4043

4144

@@ -73,6 +76,15 @@ def test_get_elec_coords_io():
7376
with pytest.raises(ValueError, match=match):
7477
get_elec_coords(elec_names=["M1", "TP9"])
7578

79+
# check coords order
80+
elec_names = ["Fp1", "AFz"]
81+
coords = get_elec_coords(elec_names=elec_names) # default sort=False
82+
assert coords.label.to_list() == elec_names
83+
coords = get_elec_coords(elec_names=elec_names, sort=False)
84+
assert coords.label.to_list() == elec_names
85+
coords = get_elec_coords(elec_names=elec_names, sort=True)
86+
assert coords.label.to_list() == sorted(elec_names)
87+
7688
# Mock a too-low version of mne
7789
mock_mne = mock.MagicMock()
7890
mock_mne.__version__ = "0.19.0"

eeg_positions/viz.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,16 +178,15 @@ def plot_coords(coords, scatter_kwargs={}, text_kwargs={}):
178178
dim = "3d" if "z" in coords.columns else "2d"
179179

180180
# update kwargs
181-
scatter_settings = dict(color="r")
181+
scatter_settings = dict()
182182
scatter_settings.update(scatter_kwargs)
183183
text_settings = dict(fontsize=6)
184184
text_settings.update(text_kwargs)
185185

186186
if dim == "2d":
187187
fig, ax = _plot_2d_head(RADIUS_INNER_CONTOUR)
188-
188+
ax.scatter(coords["x"], coords["y"], zorder=2.5, **scatter_settings)
189189
for _, row in coords.iterrows():
190-
ax.scatter(row["x"], row["y"], zorder=2.5, **scatter_settings)
191190
ax.text(row["x"], row["y"], row["label"], **text_settings)
192191

193192
else:

examples/plot_positions.py

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,63 +12,104 @@
1212
from eeg_positions import get_elec_coords, plot_coords
1313

1414
# %%
15-
# Get the electrode positions!
16-
# Let's start with the basic 10-20 system in 2 dimensions (``"x"`` and ``"y"``).
15+
# Let's start with the basic 10-20 system in two dimensions:
1716

1817
coords = get_elec_coords(
1918
system="1020",
2019
dim="2d",
2120
)
2221

23-
# `coords` is a pandas.DataFrame object
22+
# %%
23+
# This function returns a ``pandas.DataFrame`` object:
24+
2425
coords.head()
2526

2627
# %%
2728
# Now let's plot these coordinates.
2829
# We can supply some style arguments to :func:`eeg_positions.plot_coords` to control
29-
# the color of the scatter dots, and the text annotations.
30+
# the color of the electrodes and the text annotations.
3031

3132
fig, ax = plot_coords(
32-
coords, scatter_kwargs=dict(color="green"), text_kwargs=dict(fontsize=10)
33+
coords, scatter_kwargs={"color": "g"}, text_kwargs={"fontsize": 10}
3334
)
3435

35-
ax.axis("off")
3636
fig
3737

3838
# %%
39-
# Notice in the above plot, that the "landmarks" are there: ``NAS``, ``LPA``,
40-
# and ``RPA``. We can drop these by passing the ``drop_landmarks=True`` to
41-
# :func:`get_elec_coords`.
39+
# Notice that the "landmarks" ``NAS``, ``LPA``, and ``RPA`` are included. We can drop
40+
# these by passing ``drop_landmarks=True`` to :func:`get_elec_coords`:
4241

4342
coords = get_elec_coords(
4443
system="1020",
4544
drop_landmarks=True,
4645
dim="2d",
4746
)
4847

49-
5048
fig, ax = plot_coords(
51-
coords, scatter_kwargs=dict(color="green"), text_kwargs=dict(fontsize=10)
49+
coords, scatter_kwargs={"color": "g"}, text_kwargs={"fontsize": 10}
5250
)
5351

54-
ax.axis("off")
5552
fig
5653

5754
# %%
58-
# We can also plot in 3D. Let's pick a system with more electrodes now.
55+
# Often, we might have a list of electrode names that we would like to plot. For
56+
# example, let's assume we have the following 64 channel labels (based on the 10-05
57+
# system):
58+
59+
chans = """Fp1 AF7 AF3 F1 F3 F5 F7 Fp2 AF8 AF4 F2 F4 F6 F8 FT7 FC5 FC3
60+
FC1 C1 C3 C5 T7 TP7 CP5 CP3 CP1 FT8 FC6 FC4 FC2 C2 C4 C6 T8 TP8 CP6 CP4
61+
CP2 P1 P3 P5 P7 P9 PO7 PO3 O1 P2 P4 P6 P8 P10 PO8 PO4 O2 Iz Oz POz Pz
62+
Fz AFz Fpz CPz Cz FCz""".split()
63+
64+
# %%
65+
# Many experiments aggregate electrodes into regions of interest (ROIs), which we could
66+
# visualize with different colors. Let's get their coordinates first:
67+
68+
coords = get_elec_coords(elec_names=chans)
69+
70+
# %%
71+
# Now we specifiy individual colors using the ``scatter_kwargs``` argument. We create a
72+
# list of 64 colors corresponding to our 64 coordinates (in the original order as
73+
# provided by ``chans``):
74+
75+
colors = (
76+
["salmon"] * 14
77+
+ ["skyblue"] * 24
78+
+ ["violet"] * 16
79+
+ ["lightgreen"] * 7
80+
+ ["khaki"] * 3
81+
)
82+
83+
# sphinx_gallery_thumbnail_number = 3
84+
fig, ax = plot_coords(
85+
coords,
86+
scatter_kwargs={
87+
"s": 150, # electrode size
88+
"color": colors,
89+
"edgecolors": "black", # black electrode outline
90+
"linewidths": 0.5, # thin outline
91+
},
92+
text_kwargs={
93+
"ha": "center", # center electrode label horizontally
94+
"va": "center", # center electrode label vertically
95+
"fontsize": 5, # smaller font size
96+
},
97+
)
98+
99+
# %%
100+
# We can also plot in 3D. Let's pick a system with more electrodes now:
59101

60102
coords = get_elec_coords(
61103
system="1010",
62104
drop_landmarks=True,
63105
dim="3d",
64106
)
65107

66-
67108
fig, ax = plot_coords(coords, text_kwargs=dict(fontsize=7))
68109

69110
fig
70111

71112
# %%
72113
# When using these commands from an interactive Python session, try to set
73-
# the Ipython magic `%matplotlib qt`, which will allow you to freely view the 3d
74-
# plot and rotate the camera.
114+
# the IPython magic ``%matplotlib`` or ``%matplotlib qt``, which will allow you to
115+
# freely view the 3D plot and rotate the camera.

0 commit comments

Comments
 (0)