from collections.abc import Callable
from functools import partial
import mne
import numpy as np
from matplotlib import pyplot as plt
from mne._fiff.pick import pick_info
from mne.utils.check import _check_option, _validate_type
from mne.utils.numerics import _time_mask
from mne.viz.circle import _plot_connectivity_circle
from mne.viz.utils import plt_show
from ..utils import fill_doc
from .helpers import (
_add_comps_as_connections,
_butterfly_on_button_press,
_butterfly_onpick,
_check_data_is_real,
_check_info,
_combine_connections,
_get_con_info,
_get_node_names_and_indices,
_handle_data_and_indices,
_handle_picks,
)
[docs]
@fill_doc
def plot_spectral_connectivity(
con,
*,
info=None,
picks=None,
selection="both",
exclude="bads",
combine=None,
ci="sd",
fmin=None,
fmax=None,
node_aliases=None,
colors="auto",
cmap="turbo",
highlight=None,
interactive=True,
show=True,
):
"""Plot spectral connectivity as line plots, with circle plot overviews.
Parameters
----------
con : ~mne_connectivity.SpectralConnectivity
The spectral connectivity object to plot.
%(viz_info)s
%(viz_picks)s
%(viz_selection_line)s
%(viz_exclude)s
%(viz_combine_line_spectral)s
%(viz_ci)s
%(viz_fmin_fmax)s
%(viz_node_aliases)s
%(viz_colors_line)s
%(viz_cmap_line)s
%(viz_highlight)s
%(viz_interactive)s
%(viz_show)s
Returns
-------
%(viz_figures)s
Notes
-----
%(viz_circle_line_note)s
%(viz_components_note)s
"""
from mne_connectivity import SpectralConnectivity
_validate_type(con, SpectralConnectivity, "con", "SpectralConnectivity")
return _plot_line_connectivity(
con=con,
info=info,
picks=picks,
selection=selection,
exclude=exclude,
combine=combine,
ci=ci,
xlim=(fmin, fmax),
node_aliases=node_aliases,
colors=colors,
cmap=cmap,
highlight=highlight,
interactive=interactive,
show=show,
xvar=con.freqs,
xlabel="Frequency (Hz)",
)
[docs]
@fill_doc
def plot_temporal_connectivity(
con,
*,
info=None,
picks=None,
selection="both",
exclude="bads",
combine=None,
ci="sd",
tmin=None,
tmax=None,
node_aliases=None,
colors="auto",
cmap="turbo",
highlight=None,
interactive=True,
show=True,
):
"""Plot temporal connectivity as line plots, with circle plot overviews.
Parameters
----------
con : ~mne_connectivity.TemporalConnectivity
The temporal connectivity object to plot.
%(viz_info)s
%(viz_picks)s
%(viz_selection_line)s
%(viz_exclude)s
%(viz_combine_line_temporal)s
%(viz_ci)s
%(viz_tmin_tmax)s
%(viz_node_aliases)s
%(viz_colors_line)s
%(viz_cmap_line)s
%(viz_highlight)s
%(viz_interactive)s
%(viz_show)s
Returns
-------
%(viz_figures)s
Notes
-----
%(viz_circle_line_note)s
%(viz_components_note)s
"""
from mne_connectivity import TemporalConnectivity
_validate_type(con, TemporalConnectivity, "con", "TemporalConnectivity")
return _plot_line_connectivity(
con=con,
info=info,
picks=picks,
selection=selection,
exclude=exclude,
combine=combine,
ci=ci,
xlim=(tmin, tmax),
node_aliases=node_aliases,
colors=colors,
cmap=cmap,
highlight=highlight,
interactive=interactive,
show=show,
xvar=con.times,
xlabel="Time (s)",
)
def _plot_line_connectivity(
con,
info,
picks,
selection,
exclude,
combine,
ci,
xlim,
node_aliases,
colors,
cmap,
highlight,
interactive,
show,
xvar,
xlabel,
):
"""Plot connectivity as line plots with circle plot overviews.
Connectivity has dims [connections, frequencies | times].
"""
_check_data_is_real(con.get_data())
_check_option("con.shape", len(con.shape), [2, 3], " length")
_check_option("selection", selection, ["both", "seeds", "targets"])
_validate_type(info, (mne.Info, None), "`info`", "mne.Info or None")
_validate_type(combine, (str, Callable, None), "`combine`")
if isinstance(combine, str):
_check_option("combine", combine, ["mean"], " as a string")
_validate_type(ci, (str, int, float, None), "`ci`")
if isinstance(ci, str):
_check_option("ci", ci, ["sd", "range"], " as a string")
elif isinstance(ci, int | float):
if not 0 < ci <= 100:
raise ValueError("If `ci` is a float, it must be > 0 and <= 100.")
_validate_type(node_aliases, (dict, None), "`node_aliases`", "dict or None")
_check_option("colors", colors, ["auto", "global", "relative"])
_validate_type(highlight, ("array-like", None), "`highlight`", "array-like or None")
if highlight is not None:
_check_option("highlight", np.ndim(highlight), [1, 2], " number of dimensions")
if np.shape(highlight)[-1] != 2:
raise ValueError("`highlight` must have shape (2,) or (n, 2).")
highlight = np.atleast_2d(highlight) # so a single period can be iterated over
_validate_type(interactive, bool, "`interactive`", "bool")
_validate_type(show, bool, "`show`", "bool")
ch_names = con.names
con_method = con.method if con.method is not None else "connectivity"
ch_info = _check_info(info, ch_names)
data, indices, is_multivar = _handle_data_and_indices(con, ch_info)
# Get info about nodes and connections
node_names, node_indices = _get_node_names_and_indices(
ch_names, node_aliases, indices, is_multivar
)
con_info = _get_con_info(ch_info, node_names, indices, node_indices, is_multivar)
# Get requested connections
picks = _handle_picks(picks, exclude, ch_info, indices, is_multivar, selection)
data = data[picks]
indices = (indices[0][picks], indices[1][picks])
node_indices = (node_indices[0][picks], node_indices[1][picks])
con_info = pick_info(con_info, picks)
con_info["temp"]["con_types"] = con_info["temp"]["con_types"][picks]
# Add multivariate components as additional connections
n_comps = 1
if data.ndim == 3:
data, con_info, node_indices, n_comps = _add_comps_as_connections(
data, con_info, node_indices, comps_axis=1
)
# Mask data to relevant x values
xvar = np.asarray(xvar)
xvar_mask = np.nonzero(
_time_mask(
times=xvar, tmin=xlim[0], tmax=xlim[1], sfreq=None, include_tmax=True
)
)[0]
data = data[..., xvar_mask]
xvar = xvar[xvar_mask]
con_types = con_info["temp"]["con_types"]
figs = []
axes = []
for con_type in np.unique(con_types):
# Prepare connectivity info for plotting
type_mask = con_types == con_type
type_data = data[type_mask]
type_con_names = np.array(con_info["ch_names"])[type_mask]
type_node_names = node_names.copy()
type_node_indices = tuple(idcs[type_mask] for idcs in node_indices)
# Combine connectivity across connections
type_ci = None
if combine is not None:
(
type_data,
type_ci,
type_con_names,
type_node_names,
type_node_indices,
) = _combine_connections(type_data, combine, ci, n_comps)
# Create figure and axes
fig = plt.figure(figsize=(15, 5), facecolor="w", layout="constrained")
plot_circle = True
line_subplot_idx = (1, 2)
if len(type_node_indices[0]) == 1:
plot_circle = False # don't plot circle for a single connection
line_subplot_idx = (1, 3)
line_ax = fig.add_subplot(1, 3, line_subplot_idx)
circle_ax = None
if plot_circle:
# Prepare circle plot values
circle_names, circle_indices, is_all_to_all = _get_circle_names_and_indices(
type_node_names, type_node_indices
)
n_circle_nodes = len(circle_names)
node_is_selectable = _get_node_selectability(circle_indices, selection)
# If:
# - plot is interactive
# - connectivity data is (lower/upper-triangular) all-to-all
# - nodes as both seeds and targets in connections can be selected
# then colouring works best if connections are duplicated such that all
# nodes are seeds and targets
duplicate_cons = is_all_to_all and interactive and selection == "both"
if duplicate_cons:
circle_indices = (
np.concatenate([circle_indices[0], circle_indices[1]]),
np.concatenate([circle_indices[1], circle_indices[0]]),
)
if colors == "auto":
type_connection_colors = (
"relative" if is_all_to_all and interactive else "global"
)
else:
type_connection_colors = colors
circle_con, circle_con_order = _get_circle_con(
circle_indices, n_circle_nodes, type_connection_colors, selection
)
# avoid a zero colour range (e.g. for a single pair of nodes), which
# would make MNE's circle plot divide by zero
circle_vmin, circle_vmax = circle_con.min(), circle_con.max()
if circle_vmin == circle_vmax:
circle_vmax = circle_vmin + 1
circle_ax = fig.add_subplot(1, 3, 3, polar=True)
# Plot connectivity as circle
fig, circle_ax = _plot_connectivity_circle(
con=circle_con,
node_names=circle_names,
indices=circle_indices,
node_width=None,
node_height=1.0,
node_colors=["black"], # expects list
node_edgecolor="white",
node_linewidth=2.0,
facecolor="white",
textcolor="black",
colormap=cmap,
vmin=circle_vmin,
vmax=circle_vmax,
colorbar=False,
linewidth=1.5,
fontsize_names=8,
padding=6.0,
ax=circle_ax,
interactive=False, # use our modified callback
title=(
"Node selection\n"
f"({selection.replace('both', 'seeds and targets')})"
if interactive
else "Nodes"
),
show=show,
)
_set_node_alpha(circle_ax, node_is_selectable)
con_colors = _get_con_colors(circle_ax, circle_con_order)
else:
con_colors = "k"
duplicate_cons = False
# Plot connectivity as lines
fig, line_ax = _plot_connectivity_lines(
data=type_data,
ci=type_ci,
con_colors=con_colors,
con_names=type_con_names,
duplicate_cons=duplicate_cons,
fig=fig,
ax=line_ax,
xvar=xvar,
xlabel=xlabel,
title=f"{con_type} | {con_method}",
interactive=interactive,
line_alpha=0.75,
ci_alpha=0.3,
linewidth=2.0,
highlight=highlight,
)
# Add connectivity selection callback
if plot_circle and interactive:
callback = partial(
_plot_connectivity_circle_onpick,
fig=fig,
circle_ax=circle_ax,
line_ax=line_ax,
indices=circle_indices,
node_angles=np.linspace(0, 2 * np.pi, n_circle_nodes, endpoint=False),
duplicate_cons=duplicate_cons,
circle_con_order=circle_con_order,
selection=selection,
node_selectability=node_is_selectable,
has_ci=type_ci is not None,
)
fig.canvas.mpl_connect("button_press_event", callback)
# Hide duplicate connections initially
if plot_circle and duplicate_cons:
_hide_duplicate_cons(
fig,
circle_ax,
line_ax,
len(type_data),
circle_con_order,
has_ci=type_ci is not None,
)
figs.append(fig)
axes.append((line_ax, circle_ax))
plt_show(show)
if len(figs) == 1:
return figs[0], axes[0]
return figs, axes
def _get_circle_names_and_indices(node_names, node_indices):
"""Get names of nodes and indices of connections between them for circle plot."""
unique_nodes = np.unique(np.r_[node_indices[0], node_indices[1]])
circle_names = [node_names[idx] for idx in unique_nodes]
circle_indices = [np.searchsorted(unique_nodes, ind) for ind in node_indices]
is_all_to_all = [] # check if all-to-all connectivity
for ind, all_to_all_ind in zip(
circle_indices, np.tril_indices(len(circle_names), -1)
):
if len(ind) != len(all_to_all_ind):
is_all_to_all.append(False)
break
if not (np.all(ind == all_to_all_ind) or np.all(ind == all_to_all_ind.T)):
is_all_to_all.append(False)
break
is_all_to_all.append(True)
is_all_to_all = all(is_all_to_all)
return circle_names, circle_indices, is_all_to_all
def _get_node_selectability(circle_indices, selection):
"""Get selectability of nodes in circle plot based on node selection type."""
n_unique_nodes = len(np.unique(np.r_[circle_indices[0], circle_indices[1]]))
if selection == "both":
node_selectability = [True] * n_unique_nodes
else:
if selection == "seeds":
relevant_indices = circle_indices[0]
else: # selection == "targets"
relevant_indices = circle_indices[1]
node_selectability = [idx in relevant_indices for idx in range(n_unique_nodes)]
return node_selectability
def _get_circle_con(circle_indices, n_nodes, connection_colors, selection):
"""Get connectivity values for circle plot (determines colour)."""
if connection_colors == "relative": # values span colourbar per node
node_angles = np.linspace(0, 2 * np.pi, n_nodes, endpoint=False)
circle_con = np.zeros(len(circle_indices[0]))
for con_idx, (seed, target) in enumerate(zip(*circle_indices)):
node_diff = node_angles[seed] - node_angles[target]
if node_diff > 0:
node_diff -= 2 * np.pi
circle_con[con_idx] = np.abs(node_diff)
# Normalise values for different number of connections per node
if selection != "both":
consider_indices = (
circle_indices[0] if selection == "seeds" else circle_indices[1]
)
for node_idx in range(n_nodes):
node_mask = consider_indices == node_idx
if np.any(node_mask):
circle_con[node_mask] -= circle_con[node_mask].min()
if circle_con[node_mask].size > 1: # avoid division by zero
circle_con[node_mask] /= circle_con[node_mask].max()
else: # values span colourbar over all connections
circle_con = circle_indices[0] + circle_indices[1]
# mne.viz.circle._plot_connectivity_circle default behaviour is to sort connections
# by strength (valid as of MNE v1.11)
circle_con_order = np.argsort(circle_con) # to map cons in circle plot to indices
return circle_con, circle_con_order
def _set_node_alpha(circle_ax, node_is_selectable):
"""Set alpha of nodes in circle plot based on selectability."""
for node_idx, node_selectable in enumerate(node_is_selectable):
node_patch = circle_ax.containers[0][node_idx]
if not node_selectable:
node_patch.set_alpha(0.3)
def _get_con_colors(circle_ax, circle_con_order):
"""Get colors of connections from circle plot."""
con_colors = [None] * len(circle_con_order)
for patch_idx, con_idx in enumerate(circle_con_order):
patch = circle_ax.patches[patch_idx]
con_colors[con_idx] = patch.get_edgecolor()
return con_colors
def _plot_connectivity_circle_onpick(
event,
fig,
circle_ax,
line_ax,
indices,
node_angles,
duplicate_cons,
circle_con_order,
selection,
node_selectability,
has_ci,
ylim=(9, 10),
):
"""Isolate connections for a single node and reflect this in the line plot.
On left click, shows only connections related to the clicked node.
On right click, resets all connections.
`y_lim` radius is default in circle plot (valid in MNE v1.11).
"""
if event.inaxes != circle_ax:
return
patches = circle_ax.patches
lines = line_ax.lines
collections = line_ax.collections
if event.button == 1: # left click
if not ylim[0] <= event.ydata <= ylim[1]:
return # ignore click if not near nodes
# all angles in range [0, 2*pi]
node_angles = node_angles % (np.pi * 2)
node = np.argmin(np.abs(event.xdata - node_angles))
if not node_selectability[node]:
return # ignore click if node not selectable
for text in line_ax.texts:
text.set_alpha(0) # hide any connection labels
for circle_idx, line_idx in enumerate(circle_con_order):
seed, target = indices[0][line_idx], indices[1][line_idx]
if selection == "both":
viable_nodes = [seed, target] if not duplicate_cons else [seed]
elif selection == "seeds":
viable_nodes = [seed]
else: # selection == "targets"
viable_nodes = [target]
visible = node in viable_nodes
patches[circle_idx].set_visible(visible)
lines[line_idx].set_visible(visible)
lines[line_idx].set_picker(0 if not visible else True)
if has_ci:
collections[line_idx].set_visible(visible)
fig.canvas.draw()
elif event.button == 3: # right click
n_cons = len(indices[0]) if not duplicate_cons else len(indices[0]) // 2
for circle_idx, line_idx in enumerate(circle_con_order):
# Make original connections visible and hide duplicated connections
visible = line_idx < n_cons
patches[circle_idx].set_visible(visible)
lines[line_idx].set_visible(visible)
lines[line_idx].set_picker(0 if not visible else True)
if has_ci:
collections[line_idx].set_visible(visible)
for text in line_ax.texts:
text.set_alpha(0) # hide any connection labels
fig.canvas.draw()
def _hide_duplicate_cons(fig, circle_ax, line_ax, n_cons, circle_con_order, has_ci):
"""Hide duplicated connections in circle and line plots."""
for circle_idx, line_idx in enumerate(circle_con_order):
if line_idx >= n_cons:
circle_ax.patches[circle_idx].set_visible(False)
line_ax.lines[line_idx].set_visible(False)
line_ax.lines[line_idx].set_picker(False)
if has_ci:
line_ax.collections[line_idx].set_visible(False)
fig.canvas.draw()
def _plot_connectivity_lines(
data,
ci,
con_colors,
con_names,
duplicate_cons,
fig,
ax,
xvar,
xlabel,
title,
interactive,
line_alpha,
ci_alpha,
linewidth,
highlight,
):
"""Plot data as butterfly plot."""
texts = list()
n_cons = data.shape[0]
idxs = np.arange(n_cons)
if duplicate_cons:
idxs = np.concatenate([idxs, idxs + n_cons])
lines = list()
if interactive:
# Parameters for butterfly interactive plots
if duplicate_cons:
con_names = np.concatenate([con_names, con_names])
params = dict(
axes=[ax],
texts=texts,
lines=[lines],
ch_names=con_names,
idxs=[idxs],
need_draw=False,
path_effects=None,
)
fig.canvas.mpl_connect("pick_event", partial(_butterfly_onpick, params=params))
fig.canvas.mpl_connect(
"button_press_event", partial(_butterfly_on_button_press, params=params)
)
# Map cons with least activity behind the more active ones
z_ord = data.std(axis=1).argsort()[::-1]
# plot connections
for con_idx, z in enumerate(z_ord):
if ci is not None:
ax.fill_between(
xvar,
ci[con_idx, :, 0],
ci[con_idx, :, 1],
zorder=z + 1,
color=con_colors[con_idx],
edgecolor=None,
alpha=ci_alpha,
)
lines.append(
ax.plot(
xvar,
data[con_idx],
picker=interactive,
zorder=z + 1,
color=con_colors[con_idx],
alpha=line_alpha,
linewidth=linewidth,
)[0]
)
lines[-1].set_pickradius(3.0)
if duplicate_cons:
for con_idx, z in enumerate(z_ord):
if ci is not None:
ax.fill_between(
xvar,
ci[con_idx],
zorder=z + 1,
color=con_colors[con_idx],
alpha=ci_alpha,
)
lines.append(
ax.plot(
xvar,
data[con_idx],
picker=interactive,
zorder=z + 1,
color=con_colors[con_idx + n_cons],
alpha=line_alpha,
linewidth=linewidth,
)[0]
)
lines[-1].set_pickradius(3.0)
ax.set_xlim(xvar[0], xvar[-1])
ax.set_xlabel(xlabel)
ax.set_ylabel("Connectivity (A.U.)")
texts.append(
ax.text(
0,
0,
"",
zorder=3,
verticalalignment="baseline",
horizontalalignment="left",
fontweight="bold",
alpha=0,
clip_on=True,
)
)
ax.set_title(title)
# Plot highlights
if highlight is not None:
this_ylim = ax.get_ylim()
for this_highlight in highlight:
ax.fill_betweenx(
this_ylim,
this_highlight[0],
this_highlight[1],
facecolor="orange",
alpha=0.15,
zorder=99,
)
# Put back the y limits as fill_betweenx messes them up
ax.set_ylim(this_ylim)
return fig, ax