Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/source/user_guide/gui_tutorials.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ The console workflow is recommended because it shows ``LASTCOM`` and ``ALLCOM``
after each menu action. See :ref:`gui_console_session` for the shared-session
model and implementation details.

.. note::

Plotting commands such as ``Plot > Channel spectra and maps`` and
``Plot > Channel ERPs > With scalp maps`` open a figure window on any
interactive backend, whether you trigger them from a Plot menu or call the
matching ``pop_*`` function in ``eegprep-console``. On non-interactive
backends (for example ``Agg`` in headless runs) the figure is built and
returned without opening a window.

When scripting, pass ``plot='off'`` to any plotting ``pop_*`` function to
build and return the figure without popping a window, for example
``fig = pop_spectopo(EEG, 1, [], freqs=[10], plot='off')['figure']`` to save
or embed it. The default ``plot='on'`` displays it.

Load, Inspect, and Save a Dataset
=================================

Expand Down
9 changes: 8 additions & 1 deletion src/eegprep/functions/adminfunc/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
from collections.abc import Callable, Iterator, Mapping
from typing import Any

import matplotlib.pyplot as plt

import eegprep
from eegprep.functions.adminfunc.eegh import eegh, eegh_find
from eegprep.extension_runtime import ExtensionRuntime
Expand Down Expand Up @@ -653,7 +655,12 @@ def __call__(self) -> None:
_make_shell_prompt_dynamic(self.shell)
restore_logging = _install_prompt_safe_logging()
restore_progress_logging = _install_console_progress_logging()
self.shell.enable_gui("qt")
# qtagg so figures are driven by the Qt loop; a bare enable_gui("qt")
# leaves the default backend, which the loop can't drive.
self.shell.enable_matplotlib("qt")
# Interactive mode off: pop_* functions display via show_figures(), so
# plot='off' opens no window and does not steal GUI focus.
plt.ioff()

def post_run_cell(result: Any) -> None:
raw_cell = getattr(getattr(result, "info", None), "raw_cell", "")
Expand Down
44 changes: 44 additions & 0 deletions src/eegprep/functions/popfunc/plot_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,39 @@

from typing import Any

import matplotlib.pyplot as plt
import numpy as np

from eegprep.functions.miscfunc.misc import finite_matmul
from eegprep.functions.popfunc._chanutils import chanlocs_as_list
from eegprep.functions.popfunc._pop_utils import is_empty_value, parse_numeric_sequence

# matplotlib's file-output backends cannot open a figure window.
_NONINTERACTIVE_BACKENDS = frozenset({"agg", "cairo", "pdf", "pgf", "ps", "svg", "template"})


def backend_can_display() -> bool:
"""Return True when the active matplotlib backend can show a figure window."""
return plt.get_backend().lower() not in _NONINTERACTIVE_BACKENDS


def show_figures(figures: Any, *, plot: str | bool = "on") -> None:
"""Display figures (a single figure, a list, or ``None``) on an interactive backend.

No-op on file-output backends (Agg, PDF, ...) so headless runs stay silent.
``plot`` accepts ``'on'/'off'`` or ``True/False``. With it off, the figures are
closed (unregistered from pyplot) so an interactive session cannot auto-display
them; the caller keeps the returned Figure, which stays usable for ``savefig``.
"""
if not _plot_enabled(plot):
for figure in _as_figure_list(figures):
plt.close(figure)
return
if not backend_can_display():
return
for figure in _as_figure_list(figures):
figure.show()


def as_eeg_list(value: Any) -> list[dict[str, Any]]:
"""Return one or more EEG dictionaries as a list."""
Expand Down Expand Up @@ -236,6 +263,23 @@ def history_command(function_name: str, *args: Any, eeg_name: str = "EEG", **kwa
return f"{function_name}({', '.join(pieces)})"


def _plot_enabled(plot: str | bool) -> bool:
"""Whether the ``plot`` flag requests a window: ``'on'/'off'`` or ``True/False``."""
if isinstance(plot, bool):
return plot
if isinstance(plot, str) and plot.strip().lower() in {"on", "off"}:
return plot.strip().lower() == "on"
raise ValueError(f"plot must be 'on'/'off' or True/False, got {plot!r}")


def _as_figure_list(figures: Any) -> list[Any]:
if figures is None:
return []
if isinstance(figures, (list, tuple)):
return [figure for figure in figures if figure is not None]
return [figures]


def _is_empty_sequence(value: Any) -> bool:
return is_empty_value(value)

Expand Down
8 changes: 7 additions & 1 deletion src/eegprep/functions/popfunc/pop_comperp.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
eeg_times_ms,
history_command,
numeric_vector,
show_figures,
)
from eegprep.functions.popfunc._pop_utils import is_on

Expand All @@ -30,10 +31,14 @@ def pop_comperp(
*args: Any,
gui: bool | None = None,
renderer: Any | None = None,
plot: str | bool = "on",
return_com: bool = False,
**kwargs: Any,
):
"""Compute and plot grand-average ERPs across loaded datasets."""
"""Compute and plot grand-average ERPs across loaded datasets.

Pass ``plot='off'`` to build and return the figure without opening a window.
"""
datasets = as_eeg_list(ALLEEG)
if gui is None:
gui = datadd is None
Expand Down Expand Up @@ -90,6 +95,7 @@ def pop_comperp(
options=options,
)
result = {"erp1": erp1, "erp2": erp2, "erpsub": erpsub, "times": times, "pvalues": pvalues, "figure": figure}
show_figures(figure, plot=plot)
command = history_command(
"pop_comperp", int(flag), (add_indices + 1).tolist(), (sub_indices + 1).tolist(), eeg_name="ALLEEG", **kwargs
)
Expand Down
8 changes: 7 additions & 1 deletion src/eegprep/functions/popfunc/pop_envtopo.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
history_command,
numeric_vector,
parse_plot_options_text,
show_figures,
)
from eegprep.functions.sigprocfunc.envtopo import envtopo

Expand All @@ -27,10 +28,14 @@ def pop_envtopo(
*args: Any,
gui: bool | None = None,
renderer: Any | None = None,
plot: str | bool = "on",
return_com: bool = False,
**kwargs: Any,
):
"""Plot largest component ERP envelopes and component maps."""
"""Plot largest component ERP envelopes and component maps.

Pass ``plot='off'`` to build and return the figure without opening a window.
"""
if EEG is None:
return (None, "") if return_com else None
if isinstance(EEG, list):
Expand Down Expand Up @@ -81,6 +86,7 @@ def pop_envtopo(
title=title,
)
command = history_command("pop_envtopo", timerange, **command_kwargs)
show_figures(figure, plot=plot)
return (figure, command) if return_com else figure


Expand Down
8 changes: 7 additions & 1 deletion src/eegprep/functions/popfunc/pop_erpimage.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
history_command,
numeric_vector,
parse_plot_options_text,
show_figures,
)
from eegprep.functions.popfunc._pop_utils import is_on
from eegprep.functions.sigprocfunc.erpimage import erpimage
Expand All @@ -29,10 +30,14 @@ def pop_erpimage(
*args: Any,
gui: bool | None = None,
renderer: Any | None = None,
plot: str | bool = "on",
return_com: bool = False,
**kwargs: Any,
):
"""Plot an ERP image for one channel or component."""
"""Plot an ERP image for one channel or component.

Pass ``plot='off'`` to build and return the figure without opening a window.
"""
if EEG is None:
return (None, "") if return_com else None
typeplot = int(typeplot)
Expand Down Expand Up @@ -84,6 +89,7 @@ def pop_erpimage(
vert=kwargs.pop("vert", None),
)
command = history_command("pop_erpimage", typeplot, int(index), **command_kwargs)
show_figures(figure, plot=plot)
return ({"figure": figure, "image": image}, command) if return_com else {"figure": figure, "image": image}


Expand Down
9 changes: 7 additions & 2 deletions src/eegprep/functions/popfunc/pop_eventstat.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from eegprep.functions.guifunc.inputgui import inputgui
from eegprep.functions.guifunc.spec import ControlSpec, DialogSpec
from eegprep.functions.popfunc.plot_utils import history_command, numeric_vector
from eegprep.functions.popfunc.plot_utils import history_command, numeric_vector, show_figures
from eegprep.functions.popfunc._pop_utils import is_empty_value as _is_empty
from eegprep.functions.popfunc.eeg_point2lat import eeg_point2lat
from eegprep.functions.sigprocfunc.signalstat import signalstat
Expand All @@ -23,9 +23,13 @@ def pop_eventstat(
*,
gui: bool | None = None,
renderer: Any | None = None,
plot: str | bool = "on",
return_com: bool = False,
):
"""Compute and plot statistics for numeric EEG event fields."""
"""Compute and plot statistics for numeric EEG event fields.

Pass ``plot='off'`` to build and return the figure without opening a window.
"""
if EEG is None:
return (None, "") if return_com else None
if gui is None:
Expand All @@ -50,6 +54,7 @@ def pop_eventstat(
)
result = signalstat(values, 1, label, float(percent), title)
command = history_command("pop_eventstat", eventfield, type, latrange, percent)
show_figures(result.figure, plot=plot)
return (result, command) if return_com else result


Expand Down
5 changes: 5 additions & 0 deletions src/eegprep/functions/popfunc/pop_headplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
history_command,
numeric_vector,
parse_plot_options_text,
show_figures,
)
from eegprep.functions.popfunc._pop_utils import is_empty_value as _is_empty
from eegprep.functions.popfunc._pop_utils import is_on as _is_on
Expand All @@ -45,6 +46,7 @@ def pop_headplot(
*args: Any,
gui: bool | None = None,
renderer: Any | None = None,
plot: str | bool = "on",
return_com: bool = False,
**kwargs: Any,
):
Expand All @@ -53,6 +55,8 @@ def pop_headplot(
Like EEGLAB, ``pop_headplot`` requires a ``.spl`` spline setup file. Use
``setup={...}`` to create one, ``load=...`` to reuse one, or launch the GUI
to select/create it interactively.

Pass ``plot='off'`` to build and return the figures without opening a window.
"""
if EEG is None:
return ([], "") if return_com else []
Expand Down Expand Up @@ -110,6 +114,7 @@ def pop_headplot(
command = _history_command(
typeplot, items_array, topotitle, [rows, columns], colorbar, setup, load, command_options
)
show_figures(figures, plot=plot)
return (figures, command) if return_com else figures


Expand Down
8 changes: 7 additions & 1 deletion src/eegprep/functions/popfunc/pop_newcrossf.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
history_command,
numeric_vector,
parse_plot_options_text,
show_figures,
)
from eegprep.functions.popfunc._pop_utils import parse_key_value_args
from eegprep.functions.timefreqfunc.newcrossf import newcrossf
Expand All @@ -29,10 +30,14 @@ def pop_newcrossf(
*args: Any,
gui: bool | None = None,
renderer: Any | None = None,
plot: str | bool = "on",
return_com: bool = False,
**kwargs: Any,
):
"""Plot event-related channel/component cross-coherence."""
"""Plot event-related channel/component cross-coherence.

Pass ``plot='off'`` to build and return the figure without opening a window.
"""
if EEG is None:
return (None, "") if return_com else None
typeproc = int(typeproc)
Expand Down Expand Up @@ -63,6 +68,7 @@ def pop_newcrossf(
command = history_command(
"pop_newcrossf", typeproc, _first_index(num1), _first_index(num2), tlimits, cycles, **options
)
show_figures(result.figure, plot=plot)
return (result, command) if return_com else result


Expand Down
8 changes: 7 additions & 1 deletion src/eegprep/functions/popfunc/pop_newtimef.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
history_command,
numeric_vector,
parse_plot_options_text,
show_figures,
)
from eegprep.functions.popfunc._pop_utils import parse_key_value_args
from eegprep.functions.timefreqfunc.newtimef import newtimef
Expand All @@ -29,10 +30,14 @@ def pop_newtimef(
*args: Any,
gui: bool | None = None,
renderer: Any | None = None,
plot: str | bool = "on",
return_com: bool = False,
**kwargs: Any,
):
"""Plot a channel or component ERSP/ITC decomposition."""
"""Plot a channel or component ERSP/ITC decomposition.

Pass ``plot='off'`` to build and return the figure without opening a window.
"""
if EEG is None:
return (None, "") if return_com else None
typeproc = int(typeproc)
Expand All @@ -56,6 +61,7 @@ def pop_newtimef(
data, times = _selected_signal(EEG, typeproc, num, tlimits)
result = newtimef(data, data.shape[0], [times[0], times[-1]], float(EEG.get("srate", 1) or 1), cycles, **options)
command = history_command("pop_newtimef", typeproc, _first_index(num), tlimits, cycles, **options)
show_figures(result.figure, plot=plot)
return (result, command) if return_com else result


Expand Down
15 changes: 13 additions & 2 deletions src/eegprep/functions/popfunc/pop_plotdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@

from eegprep.functions.guifunc.inputgui import inputgui
from eegprep.functions.guifunc.spec import ControlSpec, DialogSpec
from eegprep.functions.popfunc.plot_utils import component_activations, eeg_times_ms, history_command, numeric_vector
from eegprep.functions.popfunc.plot_utils import (
component_activations,
eeg_times_ms,
history_command,
numeric_vector,
show_figures,
)
from eegprep.functions.sigprocfunc.plottopo import plottopo


Expand All @@ -18,10 +24,14 @@ def pop_plotdata(
*args: Any,
gui: bool | None = None,
renderer: Any | None = None,
plot: str | bool = "on",
return_com: bool = False,
**kwargs: Any,
):
"""Plot component ERP activations in a rectangular array."""
"""Plot component ERP activations in a rectangular array.

Pass ``plot='off'`` to build and return the figure without opening a window.
"""
if EEG is None:
return (None, "") if return_com else None
if gui is None:
Expand All @@ -45,6 +55,7 @@ def pop_plotdata(
ylimits=ylimits,
)
command = history_command("pop_plotdata", components, **command_kwargs)
show_figures(figure, plot=plot)
return (figure, command) if return_com else figure


Expand Down
Loading
Loading