Source code for psy_view.ds_widget
# -*- coding: utf-8 -*-
"""Dataset widget to display the contents of a dataset."""
# SPDX-FileCopyrightText: 2020-2021 Helmholtz-Zentrum Geesthacht
# SPDX-FileCopyrightText: 2021-2024 Helmholtz-Zentrum hereon GmbH
#
# SPDX-License-Identifier: LGPL-3.0-only
from __future__ import annotations
import contextlib
import os
import os.path as osp
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Hashable,
Iterator,
List,
Optional,
Tuple,
Union,
)
import psyplot.data as psyd
import yaml
from matplotlib.animation import FuncAnimation
from psyplot.config.rcsetup import get_configdir
from psyplot_gui.common import DockMixin, PyErrorMessage
from psyplot_gui.common import get_icon as get_psy_icon
from psyplot_gui.content_widget import (
DatasetTree,
DatasetTreeItem,
escape_html,
)
from PyQt5 import QtGui, QtWidgets
from PyQt5.QtCore import Qt # pylint: disable=no-name-in-module
import psy_view.utils as utils
from psy_view.plotmethods import (
LinePlotWidget,
MapPlotWidget,
Plot2DWidget,
PlotMethodWidget,
)
from psy_view.rcsetup import rcParams
if TYPE_CHECKING:
from matplotlib.backend_bases import MouseEvent
from matplotlib.figure import Figure
from psyplot.plotter import Plotter
from psyplot.project import PlotterInterface, Project
from psyplot_gui.main import MainWindow
from xarray import DataArray, Dataset
NOTSET = "__NOVARIABLEAVAILABLE"
[docs]
def get_dims_to_iterate(arr: DataArray) -> List[str]:
"""Get the dimensions of an array to iterate over
This function takes a data array and returns the dimension in the base
dataset that one can interator over.
Parameters
----------
arr: xarray.DataArray
The data array to iterate over
Returns
-------
list of strings
The dimension strings
"""
base_var = next(arr.psy.iter_base_variables)
return [
dim
for dim, size in zip(base_var.dims, base_var.shape)
if size > 1 and arr[dim].ndim == 0
]
TOO_MANY_FIGURES_WARNING = """
Multiple figures are open but you specified only {} filenames: {}.<br>
Saving the figures will cause that not all images are saved! We recommend to
export to a single PDF (that then includes multiple pages), or modify your
filename with strings like
<ul>
<li> <code>%i</code> for a continuous counter of the images</li>
<li><code>%(name)s</code> for variable names</li>
<li>or other netCDF attributes (see the
<a href="https://psyplot.github.io/psyplot/api/psyplot.project.html#psyplot.project.Project.export">
examples of exporting psyplot projects</a>)</li>
</ul>
Shall I continue anyway and save the figures?
"""
[docs]
class DatasetWidget(QtWidgets.QSplitter):
"""A widget to control the visualization of the variables in a dataset"""
#: The title of the widget
title: str = "psy-view Plot Control"
#: Display the dock widget at the right side of the GUI
dock_position = Qt.RightDockWidgetArea
_animating: bool = False
_ani: Optional[FuncAnimation] = None
_init_step: int = 0
#: A :class:`PyQt5.QtWidgets.QGroupBox` that contains the variable buttons
variable_frame: Optional[QtWidgets.QGroupBox] = None
#: Buttons for selecting variables in the :attr:`ds`
variable_buttons: Dict[str, QtWidgets.QPushButton]
_new_plot: bool = False
_preset: Optional[Union[str, Dict]] = None
#: Attributes to use in the dataset tree
ds_attr_columns: List[str] = ["long_name", "dims", "shape"]
def __init__(self, ds: Optional[Dataset] = None, *args, **kwargs) -> None:
"""
Parameters
----------
ds: xarray.Dataset
A dataset to visualize with this widget
"""
super().__init__(*args, **kwargs)
self._ds_nums: Dict[int, Dataset] = {}
self.setChildrenCollapsible(False)
self.ds: Optional[Dataset] = ds
self.setOrientation(Qt.Vertical)
self.error_msg = PyErrorMessage(self)
# first row: dataset name
self.open_box = QtWidgets.QHBoxLayout()
self.lbl_ds = QtWidgets.QLineEdit()
self.open_box.addWidget(self.lbl_ds)
self.btn_open = utils.add_pushbutton(
get_psy_icon("run_arrow.png"),
lambda: self.set_dataset(),
"Select and open a netCDF dataset",
self.open_box,
icon=True,
)
self.open_widget = QtWidgets.QWidget()
self.open_widget.setLayout(self.open_box)
self.addWidget(self.open_widget)
# second row: dataset representation
self.setup_ds_tree()
if ds is not None:
self.add_ds_item()
self.ds_tree.itemExpanded.connect(self.change_ds)
self.ds_tree.itemExpanded.connect(self.load_variable_desc)
self.addWidget(self.ds_tree)
# third row, navigation
self.navigation_box = QtWidgets.QHBoxLayout()
# -- animate backwards button
self.btn_animate_backward = utils.add_pushbutton(
"◀◀",
lambda: self.animate_backward(),
"Animate the time dimension backwards",
self.navigation_box,
)
self.btn_animate_backward.setCheckable(True)
# -- go to previous button
self.btn_prev = utils.add_pushbutton(
"◀",
self.go_to_previous_step,
"Go to previous time step",
self.navigation_box,
)
# -- dimension menu for animation
self.combo_dims = QtWidgets.QComboBox()
self.navigation_box.addWidget(self.combo_dims)
# -- go to next button
self.btn_next = utils.add_pushbutton(
"▶",
self.go_to_next_step,
"Go to next time step",
self.navigation_box,
)
# -- animate forward button
self.btn_animate_forward = utils.add_pushbutton(
"▶▶",
lambda: self.animate_forward(),
"Animate the time dimension",
self.navigation_box,
)
self.btn_animate_forward.setCheckable(True)
# -- interval slider
self.sl_interval = QtWidgets.QSlider(Qt.Horizontal)
self.sl_interval.setMinimum(40) # 24 fps
self.sl_interval.setMaximum(10000)
self.sl_interval.setSingleStep(50)
self.sl_interval.setPageStep(500)
self.sl_interval.setValue(500)
self.sl_interval.valueChanged.connect(self.reset_timer_interval)
self.navigation_box.addWidget(self.sl_interval)
# -- interval label
self.lbl_interval = QtWidgets.QLabel("500 ms")
self.navigation_box.addWidget(self.lbl_interval)
# --- export/import menus
self.export_box = QtWidgets.QHBoxLayout()
# -- Export button
self.btn_export = QtWidgets.QToolButton()
self.btn_export.setText("Export")
self.btn_export.setPopupMode(QtWidgets.QToolButton.InstantPopup)
self.btn_export.setMenu(self.setup_export_menu())
self.btn_export.setEnabled(False)
self.export_box.addWidget(self.btn_export)
# --- Presets button
self.frm_preset = QtWidgets.QFrame()
self.frm_preset.setFrameStyle(QtWidgets.QFrame.StyledPanel)
hbox = QtWidgets.QHBoxLayout(self.frm_preset)
self.btn_preset = QtWidgets.QToolButton()
self.btn_preset.setText("Preset")
self.btn_preset.setPopupMode(QtWidgets.QToolButton.InstantPopup)
self.btn_preset.setMenu(self.setup_preset_menu())
hbox.addWidget(self.btn_preset)
# --- presets label
self.lbl_preset = QtWidgets.QLabel("")
self.lbl_preset.setVisible(False)
hbox.addWidget(self.lbl_preset)
# --- unset preset button
self.btn_unset_preset = utils.add_pushbutton(
get_psy_icon("invalid.png"),
self.unset_preset,
"Unset the current preset",
hbox,
icon=True,
)
self.btn_unset_preset.setVisible(False)
self.export_box.addWidget(self.frm_preset)
self.btn_reload = utils.add_pushbutton(
get_psy_icon("refresh.png"),
self.reload,
"Close all open datasets and recreate the plots",
self.export_box,
icon=True,
)
self.export_box.addStretch(0)
vbox = QtWidgets.QVBoxLayout()
vbox.addLayout(self.navigation_box)
vbox.addLayout(self.export_box)
self.addLayout(vbox)
# fourth row: array selector
self.array_frame = QtWidgets.QGroupBox("Current plot")
hbox = QtWidgets.QHBoxLayout()
self.combo_array = QtWidgets.QComboBox()
self.combo_array.setEditable(False)
self.combo_array.currentIndexChanged.connect(lambda: self.refresh())
self.combo_array.currentIndexChanged.connect(self.show_current_figure)
hbox.addWidget(self.combo_array)
self.btn_add = utils.add_pushbutton(
QtGui.QIcon(get_psy_icon("plus")),
self.new_plot,
"Create a new plot",
hbox,
icon=True,
)
self.btn_add.setEnabled(ds is not None)
self.btn_del = utils.add_pushbutton(
QtGui.QIcon(get_psy_icon("minus")),
self.close_current_plot,
"Remove the current plot",
hbox,
icon=True,
)
self.btn_del.setEnabled(False)
hbox.addWidget(self.btn_add)
hbox.addWidget(self.btn_del)
self.array_frame.setLayout(hbox)
self.addWidget(self.array_frame)
# fifth row: plot interface
self.plot_tabs = QtWidgets.QTabWidget()
self.setup_plot_tabs()
self.plot_tabs.currentChanged.connect(self.switch_tab)
self.addWidget(self.plot_tabs)
# sixth row: variables
self.variable_scroll = QtWidgets.QScrollArea()
self.variable_scroll.setWidgetResizable(True)
self.setup_variable_buttons()
self.addWidget(self.variable_scroll)
# seventh row: dimensions
self.dimension_table = QtWidgets.QTableWidget()
self.addWidget(self.dimension_table)
self.disable_navigation()
if self.ds is not None:
self.refresh()
self.cids: Dict[str, int] = {}
[docs]
def reload(self) -> None:
"""Close the plot and recreate it."""
import psyplot.project as psy
sp = self._sp
if sp is None:
return
fname = sp.dsnames_map[self.ds.psy.num] # type: ignore
project = sp.save_project()
sp.close(True, True, True)
self.ds_tree.clear()
self._ds_nums.clear()
self.refresh()
self._sp = sp = psy.Project.load_project(project)
self._ds_nums = sp.datasets
num = next(num for num, f in sp.dsnames_map.items() if f == fname)
self.ds = self.open_datasets[num]
for ds in self._ds_nums.values():
self._add_ds_item(ds)
sp.show()
self.refresh()
[docs]
def setup_ds_tree(self) -> None:
"""Setup the number of columns and the header of the dataset tree."""
self.ds_tree = tree = QtWidgets.QTreeWidget()
tree.setColumnCount(len(self.ds_attr_columns) + 1)
tree.setHeaderLabels([""] + self.ds_attr_columns)
[docs]
def showEvent(self, event):
ret = super().showEvent(event)
current_size = self.size()
current_sizes = self.sizes()
itree = self.indexOf(self.ds_tree)
itable = self.indexOf(self.dimension_table)
diff = 0
if current_sizes[itree] < 400:
diff += 400 - current_sizes[itree]
current_sizes[itree] = 400
if current_sizes[itable] < 300:
diff += 300 - current_sizes[itable]
current_sizes[itable] = 300
if diff:
self.resize(current_size.width(), current_size.height() + diff)
self.setSizes(current_sizes)
return ret
[docs]
def close_current_plot(self) -> None:
"""Close the figure of the current variable."""
self.variable_buttons[self.variable].click()
[docs]
def excepthook(self, type, value, traceback) -> None:
"""A method to replace the sys.excepthook"""
self.error_msg.excepthook(type, value, traceback)
@property
def arr_name(self) -> Optional[str]:
"""Get the name of the array of the current plot (if there is one)."""
if not self.combo_array.count():
return None
else:
return self.combo_array.currentText().split(":")[0]
[docs]
def change_ds(self, ds_item: DatasetTreeItem) -> None:
"""Change the current dataset to another one.
Parameters
----------
ds_item: psyplot_gui.content_widget.DatasetTreeItem
The item in the tree of the new dataset to use
"""
ds_items = self.ds_items
if ds_item in ds_items:
with self.block_tree():
self.ds = ds_item.ds()
self.expand_ds_item(ds_item)
self.setup_variable_buttons()
self.change_combo_array()
self.refresh(reset_combo=False)
[docs]
def expand_ds_item(self, ds_item: DatasetTreeItem) -> None:
"""Expand an item of a dataset.
Parameters
----------
ds_item: DatasetTreeItem
The item to expand
"""
tree = self.ds_tree
tree.collapseAll()
tree.expandItem(ds_item)
ds = ds_item.ds()
if len(ds) <= 10:
tree.expandItem(ds_item.child(0))
if len(ds.coords) <= 10:
tree.expandItem(ds_item.child(1))
if len(ds.attrs) <= 10:
tree.expandItem(ds_item.attrs)
def _open_dataset(self) -> Optional[Dataset]:
"""Open a dialog to open a new dataset from disk.
Returns
-------
xarray.Dataset or None
The :class:`xarray.Dataset` of the selected file, or None if the
user aborted the dialog.
"""
current = self.lbl_ds.text()
if not current or not osp.exists(current):
current = os.getcwd()
fname, ok = QtWidgets.QFileDialog.getOpenFileName(
self,
"Open dataset",
current,
"NetCDF files (*.nc *.nc4);;"
"Shape files (*.shp);;"
"All files (*)",
)
if not ok:
return None
ds = psyd.open_dataset(fname)
return ds
[docs]
@contextlib.contextmanager
def block_tree(self) -> Iterator[None]:
"""Block all signals of a tree temporarily.
Use this via::
with self.block_tree():
do_something
"""
self.ds_tree.blockSignals(True)
yield
self.ds_tree.blockSignals(False)
[docs]
def set_dataset(self, ds: Optional[Dataset] = None) -> None:
"""Ask for a file name and open the dataset."""
if ds is None:
ds = self._open_dataset()
if ds is None:
return
self.ds = ds
with self.block_tree():
self.add_ds_item()
self.setup_variable_buttons()
self.btn_add.setEnabled(True)
self.btn_del.setEnabled(True)
[docs]
def add_ds_item(self) -> None:
"""Add a new :class:`DatasetTreeItem` for the current :attr:`ds`."""
ds: Dataset = self.ds # type: ignore
self._add_ds_item(ds)
def _add_ds_item(self, ds: Dataset) -> None:
tree = self.ds_tree
ds_item = DatasetTreeItem(ds, self.ds_attr_columns, 0)
fname = psyd.get_filename_ds(ds, False)[0]
if fname is not None:
self.lbl_ds.setText(fname)
fname = osp.basename(fname)
else:
self.lbl_ds.setText("")
fname = ""
ds_item.setText(0, fname)
tree.addTopLevelItem(ds_item)
self.expand_ds_item(ds_item)
tree.resizeColumnToContents(0)
if ds.psy.num not in self.open_datasets:
# make sure we do not loose track of open datasets
self._ds_nums[ds.psy.num] = ds
@property
def open_datasets(self) -> Dict[int, Dataset]:
"""Get a mapping from path to dataset number of the open datasets."""
return self._ds_nums
@property
def ds_items(self) -> List[DatasetTreeItem]:
"""Get the :class:`DatasetTreeItems` for the open datasets."""
tree = self.ds_tree
return list(map(tree.topLevelItem, range(tree.topLevelItemCount())))
@property
def ds_item(self) -> Optional[DatasetTreeItem]:
"""Get the current dataset item (if there is one)."""
ds = self.ds
for item in self.ds_items:
if item.ds() is ds:
return item
return None
[docs]
def expand_current_variable(
self, variable: Optional[Union[Any, Hashable]] = None
) -> None:
"""Expand the item in the dataset tree of variable.
Parameters
----------
variable: str
The name of the variable to expand. If None, the current variable is
used.
"""
tree = self.ds_tree
top: DatasetTreeItem = self.ds_item # type: ignore
tree.expandItem(top)
tree.expandItem(top.child(0))
if variable is None:
variable: str = self.variable # type: ignore
for var_item in map(
top.child(0).child, range(top.child(0).childCount())
):
if var_item.text(0) == variable:
tree.expandItem(var_item)
else:
tree.collapseItem(var_item)
[docs]
def setup_variable_buttons(self, ncols: int = 4) -> None:
"""Setup the variable buttons for the current dataset."""
variable_frame = QtWidgets.QGroupBox("Variables")
self.variable_scroll.setWidget(variable_frame)
self.variable_frame = variable_frame
self.variable_layout = QtWidgets.QGridLayout(self.variable_frame)
self.variable_buttons = {}
ds = self.ds
if ds is not None:
for i, v in enumerate(ds):
btn = utils.add_pushbutton(
v, self._draw_variable(v), f"Visualize variable {v}"
)
btn.setCheckable(True)
self.variable_buttons[v] = btn
self.variable_layout.addWidget(btn, i // ncols, i % ncols)
if len(ds):
rows = len(ds) // ncols
minrows = max(1, min(3, rows))
self.variable_scroll.setMinimumHeight(
(minrows + 2) * btn.sizeHint().height()
)
[docs]
def load_variable_desc(self, item: QtWidgets.QTreeWidgetItem) -> None:
"""Load the description of the variable of a given tree item.
Parameters
----------
item: PyQt5.QtWidget.QTreeWidgetItem
The item of the variable in the :attr:`ds_tree`. If this is not an
item of a variable, nothing is done.
"""
parent = item.parent()
tree = self.ds_tree
if (
parent is tree
or parent is None
or not (
DatasetTree.is_variable(item) or DatasetTree.is_coord(item)
)
):
return
if tree.isColumnHidden(1):
tree.showColumn(1)
tree.resizeColumnToContents(0)
top = item
while top.parent() and top.parent() is not self:
top = top.parent()
ds = top.ds()
if ds is None:
return
desc = escape_html(str(ds.variables[item.text(0)]))
item.setToolTip(0, "<pre>" + desc + "</pre>")
[docs]
def clear_table(self) -> None:
"""Clear the table that shows the available dimensions."""
self.dimension_table.clear()
self.dimension_table.setColumnCount(5)
self.dimension_table.setHorizontalHeaderLabels(
["Type", "First", "Current", "Last", "Units"]
)
self.dimension_table.setRowCount(0)
[docs]
def addLayout(self, layout: QtWidgets.QLayout) -> QtWidgets.QWidget:
"""Add a layout to the splitter.
This convenience function creates a new QWidget that wraps the given
layout and returns it.
Parameters
----------
layout: QtWidget.QLayout
The layout to add
Returns
-------
QtWidgets.QWidget
The widget that wraps the given layout
"""
widget = QtWidgets.QWidget()
widget.setLayout(layout)
self.addWidget(widget)
return widget
[docs]
def go_to_previous_step(self) -> None:
"""Decrease the movie dimension to the previous step."""
dim = self.combo_dims.currentText()
self.increase_dim(dim, -1)()
[docs]
def go_to_next_step(self) -> None:
"""Increase the movie dimension to the next step."""
dim = self.combo_dims.currentText()
self.increase_dim(dim)()
[docs]
def animate_backward(self) -> None:
"""Start the current animation in backward direction, or stop it."""
if self._animating:
self.stop_animation()
self.btn_animate_backward.setText("◀◀")
self.enable_navigation()
else:
self._animate_forward = False
self.btn_animate_backward.setText("■")
self.disable_navigation(self.btn_animate_backward)
self.start_animation()
[docs]
def animate_forward(self, nframes=None):
"""Start the current animation in forward direction, or stop it."""
if self._animating:
self.stop_animation()
self.btn_animate_forward.setText("▶▶")
self.enable_navigation()
else:
self._animate_forward = True
self.btn_animate_forward.setText("■")
self.disable_navigation(self.btn_animate_forward)
self.start_animation(nframes)
[docs]
def setup_plot_tabs(self) -> None:
"""Setup the tabs of the various plot methods."""
self.plot_tabs.addTab(MapPlotWidget(self.get_sp, self.ds), "mapplot")
self.plot_tabs.addTab(Plot2DWidget(self.get_sp, self.ds), "plot2d")
lineplot_widget = LinePlotWidget(self.get_sp, self.ds)
self.plot_tabs.addTab(lineplot_widget, "lineplot")
for w in map(self.plot_tabs.widget, range(self.plot_tabs.count())):
w.replot.connect(self.replot)
w.reset.connect(self.reset)
w.changed.connect(lambda: self.refresh())
[docs]
def replot(self, plotmethod: str) -> None:
"""Regenerate the plot of a given plotmethod, without closing it.
Parameters
----------
plotmethod: str
The name of the plotmethod
See Also
--------
reset: The same method, but closes the plot before genereting a new one.
"""
self.plotmethod = plotmethod
self.make_plot()
self.refresh()
[docs]
def reset(self, plotmethod: str) -> None:
"""Close the plot of the given plotmethod and regenerate it.
The same as :meth:`replot`, but closes the plot.
Parameters
----------
plotmethod: str
The name of the plotmethod
See Also
--------
reset: The same method, but closes the plot before genereting a new one.
"""
self.plotmethod = plotmethod
self.close_sp()
self.make_plot()
self.refresh()
[docs]
def disable_variables(self):
"""Disable all variable selection buttons."""
for btn in self.variable_buttons.values():
btn.setEnabled(False)
[docs]
def enable_variables(self):
"""Enable all variable selection buttons again."""
valid_variables = self.plotmethod_widget.valid_variables(self.ds)
for v, btn in self.variable_buttons.items():
btn.setEnabled(v in valid_variables)
[docs]
def start_animation(self, nframes: Optional[int] = None):
"""Start the animation along the selected dimension.
Parameters
----------
nframes: int or None
If not None, the number of frames to draw
See Also
--------
animation_frames: The iterator to generate the frames
"""
self._animating = True
self._animation_frames = nframes
self._starting_step = 1
self.disable_variables()
self.plot_tabs.setEnabled(False)
if self.sp is not None:
if self.animation is None or self.animation.event_source is None:
self.animation = FuncAnimation(
self.fig,
self.update_dims,
frames=self.animation_frames(),
init_func=self.sp.draw,
interval=self.sl_interval.value(),
repeat=False,
)
# HACK: Make sure that the animation starts although the figure
# is already shown
self.animation._draw_frame(next(self.animation_frames()))
else:
self.animation.event_source.start()
[docs]
def reset_timer_interval(self, value: int) -> None:
"""Change the interval of the timer."""
self.lbl_interval.setText("%i ms" % value)
if self.animation is None or self.animation.event_source is None:
pass
else:
self.animation.event_source.stop()
self.animation._interval = value
self.animation.event_source.interval = value
self.animation.event_source.start()
[docs]
def stop_animation(self) -> None:
"""Stop the current animation."""
self._animating = False
if (
self.animation is not None
and self.animation.event_source is not None
):
self.animation.event_source.stop()
self.plot_tabs.setEnabled(True)
self.enable_variables()
self.refresh()
[docs]
def animation_frames(self) -> Iterator[Dict[str, int]]:
"""Get the animation frames for the :attr:`combo_dims` dimension."""
while (
self._animating
and self._animation_frames is None
or self._animation_frames
):
if self._animation_frames is not None and not self._init_step:
self._animation_frames -= 1
dim = self.combo_dims.currentText()
i = self.data.psy.idims[dim]
imax = self.ds.dims[dim] - 1 # type: ignore
if self._init_step:
self._init_step -= 1
elif self._starting_step:
self._starting_step -= 1
elif self._animate_forward:
i += -i if i == imax else 1
else:
i += imax if i == 0 else -1
yield {dim: i}
[docs]
def update_dims(self, dims: Dict[str, Any]):
if self.sp is not None:
self.sp.update(dims=dims)
def _load_preset(self) -> None:
"""Open a file dialog and load the selected preset."""
fname, ok = QtWidgets.QFileDialog.getOpenFileName(
self,
"Load preset",
osp.join(get_configdir(), "presets"),
"YAML files (*.yml *.yaml);;" "All files (*)",
)
if ok:
self.load_preset(fname)
[docs]
def load_preset(self, preset: Optional[Union[str, Dict[str, Any]]]):
"""Load a given preset from disk.
Parameters
----------
preset: str or dict
The name or path to the preset, or a dictionary
"""
self.preset = preset # type: ignore
if self.sp:
loaded_preset: Dict[str, Any] = self.preset # now that it's loaded
if loaded_preset:
self.sp.load_preset(loaded_preset)
self.refresh()
self.maybe_show_preset()
@property
def preset(self) -> Dict[str, Any]:
"""Get the currently loaded preset."""
if self._preset is None:
return {}
import psyplot.project as psy
preset = self._preset
try:
preset = psy.Project._load_preset(preset)
except yaml.constructor.ConstructorError:
answer = QtWidgets.QMessageBox.question(
self,
"Can I trust this?",
f"Failed to load the preset at <i>{preset}</i> in safe mode. Can we "
"trust this preset and load it in unsafe mode?",
)
if answer == QtWidgets.QMessageBox.Yes:
psyd.rcParams["presets.trusted"].append(
psy.Project._resolve_preset_path(preset)
)
preset = psy.Project._load_preset(preset)
else:
preset = {}
return preset # type: ignore
@preset.setter
def preset(self, value: Optional[Union[str, Dict[str, Any]]]):
self._preset = value
[docs]
def unset_preset(self) -> None:
"""Unset the current preset and do not use it anymore."""
self.preset = None # type: ignore
self.maybe_show_preset()
[docs]
def maybe_show_preset(self) -> None:
"""Show the name of the current preset if one is selected."""
if self._preset is not None and isinstance(self._preset, str):
self.lbl_preset.setText(
"<i>" + osp.basename(osp.splitext(self._preset)[0]) + "</i>"
)
self.lbl_preset.setVisible(True)
self.btn_unset_preset.setVisible(True)
elif self._preset is not None:
self.lbl_preset.setText("<i>custom</i>")
self.lbl_preset.setVisible(True)
self.btn_unset_preset.setVisible(True)
else:
self.lbl_preset.setVisible(False)
self.btn_unset_preset.setVisible(False)
[docs]
def save_current_preset(self) -> None:
"""Save the preset of the current plot to a file."""
if self.sp is not None:
preset_func = self.sp.save_preset
self._save_preset(preset_func)
[docs]
def save_full_preset(self) -> None:
"""Save the preset of all open plots to a file."""
sp = self._sp
if sp is not None:
return self._save_preset(sp.save_preset)
return None
def _save_preset(self, save_func: Callable[[str], Any]) -> None:
"""Save the preset to a file.
Parameters
----------
save_func: function
The function that is called to save the preset. Must accept the
path as an argument
"""
fname, ok = QtWidgets.QFileDialog.getSaveFileName(
self,
"Save preset",
osp.join(get_configdir(), "presets"),
"YAML files (*.yml *.yaml);;" "All files (*)",
)
if not ok:
return None
save_func(fname)
[docs]
def export_image(self) -> None:
"""Ask for a filename and export the current plot to a file."""
if self.sp is not None:
fname, ok = QtWidgets.QFileDialog.getSaveFileName(
self,
"Export image",
os.getcwd(),
"Images (*.png *.pdf *.jpg *.svg)",
)
if ok:
self.sp.export(fname, **rcParams["savefig_kws"])
[docs]
def export_all_images(self) -> None:
"""Ask for a filename and export all plots to one (or more) files."""
fname, ok = QtWidgets.QFileDialog.getSaveFileName(
self,
"Export image",
os.getcwd(),
"Images (*.png *.pdf *.jpg *.svg)",
)
if ok and self._sp:
# test filenames
if not osp.splitext(fname)[-1].lower() == ".pdf":
fnames = [
sp.format_string(fname, False, i)
for i, sp in enumerate(self._sp.figs.values())
]
if len(fnames) != len(set(fnames)):
answer = QtWidgets.QMessageBox.question(
self,
"Too many figures",
TOO_MANY_FIGURES_WARNING.format(
len(set(fnames)), ", ".join(set(fnames))
),
)
if answer == QtWidgets.QMessageBox.No:
return
self._sp.export(fname, **rcParams["savefig_kws"])
[docs]
def export_animation(self) -> None:
"""Ask for a filename and export the animation."""
fname, ok = QtWidgets.QFileDialog.getSaveFileName(
self, "Export animation", os.getcwd(), "Movie (*.mp4 *.mov *.gif)"
)
if ok:
dim = self.combo_dims.currentText()
nframes: int = self.ds.dims[dim] # type: ignore
self._init_step = 1
self.animate_forward(nframes)
if self.animation is not None:
self.animation.save(
fname,
**rcParams["animations.export_kws"],
fps=round(1000.0 / self.sl_interval.value()),
)
self.animate_forward()
self.animation = None
[docs]
def export_project(self) -> None:
"""Ask for a filename and export the psyplot project as .pkl file."""
if self.sp is not None:
fname, ok = QtWidgets.QFileDialog.getSaveFileName(
self, "Export project", os.getcwd(), "Psyplot projects (*.pkl)"
)
if ok:
self.sp.save_project(fname)
[docs]
def export_project_with_data(self) -> None:
"""Ask for a filename export project (incl. data) as .pkl file.
Same as :meth:`export_project`, but adds the data to the pickle dump.
"""
if self.sp is not None:
fname, ok = QtWidgets.QFileDialog.getSaveFileName(
self, "Export project", os.getcwd(), "Psyplot projects (*.pkl)"
)
if ok:
self.sp.save_project(fname, ds_description={"ds"})
[docs]
def export_python(self):
"""Export the project as python file.
This method is not yet implemented as the functionality is missing in
psyplot.
"""
pass
def _draw_variable(self, v):
def func():
"""Visualize variable v"""
btn = self.variable_buttons[v]
if not btn.isChecked():
self.close_sp()
if self.combo_array.count() > 1:
with self.block_widgets(self.combo_array):
current = self.combo_array.currentIndex()
self.combo_array.setCurrentIndex(current - 1)
else:
with self.silence_variable_buttons():
for var, btn in self.variable_buttons.items():
if var != v:
btn.setChecked(False)
self.make_plot()
self.refresh()
return func
[docs]
@contextlib.contextmanager
def silence_variable_buttons(self) -> Iterator[None]:
"""Context manager to disable variable selection buttons."""
yield self.block_widgets(*self.variable_buttons.values()) # type: ignore
@property
def variable(self) -> str:
"""The current variable"""
for v, btn in self.variable_buttons.items():
if btn.isChecked():
return v
return NOTSET
@variable.setter
def variable(self, value: str) -> None:
with self.silence_variable_buttons():
for v, btn in self.variable_buttons.items():
btn.setChecked(v == value)
@property
def available_plotmethods(self) -> List[str]:
"""Get the plotmethods that can visualize the selected variable.
Returns
-------
list of str
A list of plotmethod names that can visualize the current
:attr:`variable`
"""
v = self.variable
if v == NOTSET:
return []
ret = []
ds: Dataset = self.ds # type: ignore
plot = ds.psy.plot
for plotmethod in self.plotmethods:
if plotmethod in plot._plot_methods:
if getattr(plot, plotmethod).check_data(ds, v, {})[0]:
ret.append(plotmethod)
return ret
@property
def plot(self) -> PlotterInterface:
"""Get the plotting function of the currently selected plotmethod."""
if self.ds is not None:
return getattr(self.ds.psy.plot, self.plotmethod)
else:
raise ValueError(
"No dataset has yet been selected, so no plot method!"
)
@property
def plot_options(self) -> Dict[str, Any]:
"""Get further keyword arguments for the :attr:`plot` function."""
if self.ds is not None:
ret: Dict[str, Any] = self.plotmethod_widget.get_fmts( # type: ignore
self.ds.psy[self.variable], True
)
preset = self.preset
if preset:
import psyplot.project as psy
preset = psy.Project.extract_fmts_from_preset(
preset, self.plotmethod
)
ret.update(dict(preset))
return ret
return {}
@property
def plotmethod(self) -> str:
"""Get the name of the current plotmethod."""
return self.plot_tabs.tabText(self.plot_tabs.currentIndex())
@plotmethod.setter
def plotmethod(self, label: str):
i = next(
(
i
for i in range(self.plot_tabs.count())
if self.plot_tabs.tabText(i) == label
),
None,
)
if i is not None:
self.plot_tabs.setCurrentIndex(i)
@property
def plotmethods(self) -> List[str]:
"""Get a list of available plotmethods."""
return list(map(self.plot_tabs.tabText, range(self.plot_tabs.count())))
@property
def plotmethod_widget(self) -> PlotMethodWidget:
"""Get widget of the current plotmethod."""
label = self.plotmethod
i = next(
(
i
for i in range(self.plot_tabs.count())
if self.plot_tabs.tabText(i) == label
),
None,
)
return self.plot_tabs.widget(i)
@property
def plotmethod_widgets(self) -> Dict[str, PlotMethodWidget]:
"""Get a list of available plotmethod widgets."""
return dict(
zip(
self.plotmethods,
map(self.plot_tabs.widget, range(self.plot_tabs.count())),
)
)
_sp = None
[docs]
def get_sp(self) -> Optional[Project]:
sp = self._sp
if sp is None:
return sp
return self.filter_sp(sp)
[docs]
def filter_sp(self, sp: Project, ds_only: bool = False) -> Project:
"""Filter the psyplot project to only include the arrays of :attr:`ds`"""
if self._new_plot:
return None
if self.ds is None:
return sp
num = self.ds.psy.num
ret = sp[:0]
for i in range(len(sp)):
if list(sp[i : i + 1].datasets) == [num]:
ret += sp[i : i + 1]
if ds_only:
return ret
arr_name = self.arr_name
if arr_name is None:
return ret
return ret(arr_name=arr_name)
[docs]
def new_plot(self) -> None:
"""Select a new variable and make a plot.
This method asks for a variable and them makes a new plot with the
selected plotmethod.
See Also
--------
make_plot: plot the currently selected variable without asking
"""
if self.ds is not None:
name, ok = QtWidgets.QInputDialog.getItem(
self,
"New plot",
"Select a variable",
self.plotmethod_widget.valid_variables(self.ds),
)
if not ok:
return
with self.silence_variable_buttons():
for v, btn in self.variable_buttons.items():
btn.setChecked(v == name)
with self.creating_new_plot():
self.make_plot()
self.btn_del.setEnabled(True)
self.refresh()
[docs]
@contextlib.contextmanager
def creating_new_plot(self) -> Iterator[None]:
"""Tell that we are making a new plot."""
self._new_plot = True
yield
self._new_plot = False
@property
def sp(self) -> Optional[Project]:
"""Get the psyplot project of the current plotmethod."""
return self.plotmethod_widget.sp or None
@sp.setter
def sp(self, sp: Optional[Project]):
if sp is None and (
not self._sp or not getattr(self._sp, self.plotmethod)
):
pass
else:
# first remove the current arrays
if self.get_sp() and getattr(self.get_sp(), self.plotmethod):
to_remove = getattr(self.get_sp(), self.plotmethod).arr_names
_sp: Project = self._sp # type: ignore
for i in list(reversed(range(len(_sp)))):
if _sp[i].psy.arr_name in to_remove:
_sp.pop(i)
# then add the new arrays
if sp:
if self._sp:
self._sp.extend(list(sp), new_name=True)
else:
self._sp = sp
@property
def data(self) -> Union[psyd.InteractiveList, DataArray]:
"""Get the raw data of the current plot."""
return self.plotmethod_widget.data
@property
def plotter(self) -> Plotter:
"""Get the psyplot plotter of the current plot."""
return self.plotmethod_widget.plotter
@property
def fig(self) -> Figure:
"""Get the figure of the current plot."""
if self.sp:
return list(self.sp.figs)[0]
_animations: Dict[str, FuncAnimation] = {}
@property
def animation(self) -> Optional[FuncAnimation]:
"""Get the animation of the current plotmethod."""
return self._animations.get(self.plotmethod)
@animation.setter
def animation(self, ani: Optional[FuncAnimation]):
if ani is None:
self._animations.pop(self.plotmethod, None)
else:
self._animations[self.plotmethod] = ani
[docs]
def make_plot(self) -> None:
"""Make or update the plot of the current variable.
See Also
--------
new_plot: A function to select the variable first
"""
plotmethods = self.available_plotmethods
plotmethod = self.plotmethod
if plotmethod not in plotmethods:
if not plotmethods:
QtWidgets.QMessageBox.critical(
self,
"Visualization impossible",
f"Found no plotmethod for variable {self.variable}",
)
return
plotmethod, ok = QtWidgets.QInputDialog.getItem(
self, "Choose a plot method", "Plot method:", plotmethods
)
if not ok:
return
self.plotmethod = plotmethod
new_v = self.variable
fmts: Dict[str, Any] = {}
dims: Dict[Hashable, int] = {}
if self.sp:
ds: Dataset = self.ds # type: ignore
if not set(self.data.dims) <= set(ds[new_v].dims):
self.close_sp()
else:
for dim in set(ds[new_v].dims) - set(self.data.psy.idims):
dims[dim] = 0
for dim in set(self.data.psy.idims) - set(ds[new_v].dims):
del self.data.psy.idims[dim]
if self.sp:
if self.data.psy.plotter is None:
self.data.psy.update(name=self.variable)
self.data.psy.update(dims=dims, **fmts)
self.sp.update(replot=True)
else:
self.data.psy.update(name=self.variable, dims=dims, **fmts)
self.show_fig(self.sp)
else:
self.ani = None
self.sp = sp = self.plot(name=self.variable, **self.plot_options)
self._preset = None
cid = sp.plotters[0].ax.figure.canvas.mpl_connect(
"button_press_event", self.display_line
)
self.cids[self.plotmethod] = cid
self.show_fig(sp)
descr = sp[0].psy._short_info()
with self.block_widgets(self.combo_array):
self.combo_array.addItem(descr)
self.combo_array.setCurrentText(descr)
self.expand_current_variable()
self.enable_navigation()
[docs]
@contextlib.contextmanager
def block_widgets(self, *widgets: QtWidgets.QWidget) -> Iterator[None]:
"""Temporarilly block all signals for the given widgets."""
for w in widgets:
w.blockSignals(True)
yield
for w in widgets:
w.blockSignals(False)
[docs]
def display_line(self, event: MouseEvent) -> None:
"""Display the line when clicked on a 2D-plot."""
if not event.inaxes:
return
else:
sl = None
for widget in map(
self.plot_tabs.widget, range(self.plot_tabs.count())
):
if widget.sp and event.inaxes == widget.plotter.ax:
sl = widget.get_slice(event.xdata, event.ydata)
break
variable = widget.data.name
raw_data = widget.data.psy.base.psy[variable]
if (
sl is None
or widget.plotmethod not in ["mapplot", "plot2d"]
or raw_data.ndim == 2
or widget.plotter.ax.figure.canvas.manager.toolbar.mode != ""
):
return
# check if the mappable contains the event
if not self.plotter.plot.mappable.contains(event)[0] and (
not hasattr(self.plotter.plot, "_wrapped_plot")
or not self.plotter.plot._wrapped_plot.contains(event)[0]
):
return
current_pm = self.plotmethod
self.plotmethod = "lineplot"
linewidget = self.plotmethod_widget
xdim = linewidget.xdim
if xdim is None:
xdim = self.combo_dims.currentText()
if not linewidget.sp or (
linewidget.xdim and linewidget.xdim not in raw_data.dims
):
with self.silence_variable_buttons():
for v, btn in self.variable_buttons.items():
btn.setChecked(v == variable)
self.make_plot()
linewidget.xdim = xdim
else:
linewidget.xdim = xdim
linewidget.add_line(variable, **sl)
self.plotmethod = current_pm
[docs]
def close_sp(self) -> None:
"""Close the current subproject."""
sp = self.sp
self.sp = None
if sp is not None:
sp.close(figs=True, data=True, ds=False)
[docs]
def show_current_figure(self) -> None:
"""Show figure of the current plotmethod."""
if self.sp is not None:
self.show_fig(self.sp)
[docs]
def show_fig(self, sp: Project) -> None:
"""Show the first figure in a psyplot project."""
if len(sp):
try:
fig = sp.plotters[0].ax.figure
fig.canvas.window().show()
fig.canvas.window().raise_()
except AttributeError:
sp.show()
[docs]
def switch_tab(self) -> None:
"""Select a valid variable when switching the plotmethod tabs."""
with self.silence_variable_buttons():
if self.sp:
name = self.data.name
else:
name = NOTSET
for v, btn in self.variable_buttons.items():
btn.setChecked(v == name)
self.refresh()
[docs]
def change_combo_array(self) -> None:
"""Update the iteration dimension depending on the selected variable."""
with self.block_widgets(self.combo_array):
sp = self.filter_sp(self._sp, ds_only=True)
if sp and self.arr_name not in sp.arr_names:
new_arr = sp.arr_names[0]
all_arrays = getattr(self._sp, self.plotmethod)
try:
idx_arr = all_arrays.arr_names.index(new_arr)
except ValueError:
idx_arr = 0
self.combo_array.setCurrentIndex(idx_arr)
try:
vname = self.data.name
except Exception:
vname = self.variable
if vname != NOTSET:
self.expand_current_variable(vname)
self.show_fig(sp[:1])
[docs]
def reset_combo_array(self) -> None:
"""Clear and fill the iteration dimension based on :attr:`ds`."""
curr_arr_name = self.arr_name
with self.block_widgets(self.combo_array):
self.combo_array.clear()
if self._sp:
all_arrays = getattr(self._sp, self.plotmethod)
current_ds = self.ds
if all_arrays:
for arr in all_arrays:
self.combo_array.addItem(arr.psy._short_info())
if curr_arr_name in all_arrays.arr_names:
idx_arr = all_arrays.arr_names.index(curr_arr_name)
self.combo_array.setCurrentIndex(idx_arr)
else:
idx_arr = 0
self.ds = list(
all_arrays[idx_arr : idx_arr + 1].datasets.values()
)[0]
if self.ds is not current_ds:
with self.block_tree():
self.expand_ds_item(self.ds_item)
self.expand_current_variable(self.data.name)
[docs]
def refresh(self, reset_combo: bool = True) -> None:
"""Refresh the state of this widget.
This method refreshes the widget based on the selected project,
variable, etc.
Parameters
----------
reset_combo: bool
If True (default), the :meth:`reset_combo_array` is called
"""
self.clear_table()
if reset_combo:
self.reset_combo_array()
if self.sp:
variable = self.data.name
for action in self._save_preset_actions:
action.setEnabled(True)
self.btn_del.setEnabled(True)
self.btn_export.setEnabled(True)
else:
variable = self.variable
for action in self._save_preset_actions:
action.setEnabled(False)
self.btn_del.setEnabled(False)
self.btn_export.setEnabled(False)
# refresh variable buttons
with self.silence_variable_buttons():
for v, btn in self.variable_buttons.items():
btn.setChecked(v == variable)
# refresh tabs
for i in range(self.plot_tabs.count()):
w = self.plot_tabs.widget(i)
w.refresh(self.ds)
if self.ds is not None and self.variable_buttons:
valid_variables = self.plotmethod_widget.valid_variables(self.ds)
for v, btn in self.variable_buttons.items():
btn.setEnabled(v in valid_variables)
elif self.ds is None or variable == NOTSET or not self.sp:
return
table = self.dimension_table
if self.sp:
data = self.data
ds_data = self.ds.psy[self.variable]
with self.silence_variable_buttons():
self.variable_buttons[self.variable].setChecked(True)
dims: Tuple[Hashable, ...] = ds_data.dims # type: ignore
table.setRowCount(ds_data.ndim)
table.setVerticalHeaderLabels(ds_data.dims)
# set time, z, x, y info
for c in "XYTZ":
cname = ds_data.psy.get_dim(c)
if cname and cname in dims:
table.setItem(
dims.index(cname), 0, QtWidgets.QTableWidgetItem(c)
)
for i, dim in enumerate(dims):
coord = self.ds[dim]
if "units" in coord.attrs:
table.setItem(
i,
4,
QtWidgets.QTableWidgetItem(str(coord.attrs["units"])),
)
try:
coord = list(map("{:1.4f}".format, coord.values)) # type: ignore
except (ValueError, TypeError):
try:
coord = coord.to_pandas().dt.to_pydatetime() # type: ignore
except AttributeError:
coord = list(map(str, coord.values)) # type: ignore
else:
coord = [t.isoformat() for t in coord] # type: ignore
first = coord[0]
last = coord[-1]
table.setItem(i, 1, QtWidgets.QTableWidgetItem(first))
table.setItem(i, 3, QtWidgets.QTableWidgetItem(last))
current = data.psy.idims.get(dim)
if current is not None and isinstance(current, int):
table.setCellWidget(
i, 2, self.new_dimension_button(dim, coord[current])
)
# update animation checkbox
dims_to_animate = get_dims_to_iterate(data)
current_dims_to_animate = list(
map(self.combo_dims.itemText, range(self.combo_dims.count()))
)
if dims_to_animate != current_dims_to_animate:
self.combo_dims.clear()
self.combo_dims.addItems(dims_to_animate)
table.resizeColumnsToContents()
# update plots items
for ds_item in self.ds_items:
for item in map(ds_item.child, range(ds_item.childCount())):
for child in map(item.child, range(item.childCount())):
if DatasetTree.is_variable(child):
plots_item = ds_item.get_plots_item(child)
ds_item.refresh_plots_item(
plots_item, child.text(0), self._sp, self.sp
)
[docs]
def new_dimension_button(
self, dim: Hashable, label: Any
) -> utils.QRightPushButton:
"""Generate a new button to increase of decrease a dimension."""
btn = utils.QRightPushButton(label)
btn.clicked.connect(self.increase_dim(str(dim)))
btn.rightclicked.connect(self.increase_dim(str(dim), -1))
btn.setToolTip(
f"Increase dimension {dim} with left-click, and "
"decrease with right-click."
)
return btn
[docs]
def update_project(self, *args, **kwargs) -> None:
"""Update the correct project :attr:`sp` and refresh the widget."""
sp = self.sp
if sp is not None:
sp.update(*args, **kwargs)
self.refresh()
[docs]
def increase_dim(self, dim: str, increase: int = 1) -> Callable[[], None]:
"""Get a function to update a specific dimension.
Parameters
----------
dim: str
The dimension name
increase: int
The number of steps to increase (or decrease) the given `dim`.
"""
def update():
i = self.data.psy.idims[dim]
self.data.psy.update(
dims={dim: (i + increase) % self.ds.dims[dim]}
)
if self.data.psy.plotter is None:
self.sp.update(replot=True)
self.refresh()
return update
[docs]
class DatasetWidgetStandAlone(DatasetWidget):
"""A :class:`DatasetWidget` that is supposed to work as a standalone GUI"""
[docs]
def closeEvent(self, event: Any):
"""Reimplemented close event to stop the running ``QApplication``."""
ret = super().closeEvent(event)
QtWidgets.QApplication.instance().quit()
return ret
[docs]
class DatasetWidgetPlugin(DatasetWidget, DockMixin):
"""A :class:`DatasetWidget` plugin for the psyplot GUI.
This widget can be embeded in the psyplot GUI. Different from the standalone
:class:`DatasetWidget` class, this one here uses the current psyplot project
(:func:`psyplot.project.gcp`)
"""
#: The title of the widget
title = "psy-view Dataset viewer"
#: Display the dock widget at the right side of the GUI
dock_position = Qt.RightDockWidgetArea
def __init__(self, *args, **kwargs):
import psyplot.project as psy
super().__init__(*args, **kwargs)
psy.Project.oncpchange.connect(self.oncpchange)
@property # type: ignore
def _sp(self) -> Project: # type: ignore
import psyplot.project as psy
return psy.gcp(True)
@_sp.setter
def _sp(self, value):
pass
@property
def sp(self) -> Optional[Project]:
"""Get the psyplot project of the current plotmethod."""
return self.plotmethod_widget.sp or None
@sp.setter
def sp(self, sp: Optional[Project]):
current = self.get_sp()
if sp is None or not current:
return
elif getattr(current, self.plotmethod, []):
if len(current) == 1 and len(sp) == 1:
pass
# first remove the current arrays
if current and getattr(current, self.plotmethod):
to_remove = getattr(self.get_sp(), self.plotmethod).arr_names
for i in list(reversed(range(len(current)))):
if current[i].psy.arr_name in to_remove:
current.pop(i)
# then add the new arrays
if sp:
if current:
current.extend(list(sp), new_name=True)
else:
current = sp
current.oncpchange.emit(current)
@property
def open_datasets(self) -> Dict[int, Dataset]:
"""Get a mapping from path to dataset number of the open datasets."""
ret = self._sp.datasets
ret.update(self._ds_nums)
return ret
[docs]
def close_sp(self) -> None:
ds = self.ds
super().close_sp()
if ds is not None:
if ds.psy.num not in self._sp.datasets:
self.set_dataset(ds)
[docs]
def reload(self) -> None:
"""Close the plot and recreate it."""
import psyplot.project as psy
if not all(self._sp.dsnames_map.values()):
# we have datasets that only exist in memory, so better ask
answer = QtWidgets.QMessageBox.question(
self,
"Shall I close this?",
"Reloading the data closes all open plots. Any data in the memory "
"is lost and open files are reloaded from disk! "
"Shall I really continue?",
)
if answer != QtWidgets.QMessageBox.Yes:
return
sp = self._sp
fname = sp.dsnames_map[self.ds.psy.num] # type: ignore
project = sp.save_project()
sp.close(True, True, True)
self.ds_tree.clear()
self._ds_nums.clear()
self.refresh()
self._sp = sp = psy.Project.load_project(project)
num = next(num for num, f in sp.dsnames_map.items() if f == fname)
self.ds = self.open_datasets[num]
sp.show()
self.refresh()
[docs]
def oncpchange(self, sp: Optional[Project]) -> None:
"""Update this widget from the current psyplot main (or sub) project."""
self.reset_combo_array()
if self.ds is not None and self.ds.psy.num not in self._sp.datasets:
self.ds = None
self.disable_navigation()
self.setup_variable_buttons()
self.btn_add.setEnabled(False)
self.btn_del.setEnabled(False)
elif self.ds is None and self._sp:
self.set_dataset(next(iter(self._sp.datasets.values())))
elif self.ds is not None and self._sp:
self.enable_navigation()
[docs]
def show_fig(self, sp: Optional[Project]) -> None:
"""Show the figure of the the current subproject."""
from psyplot_gui.main import mainwindow
super().show_fig(sp)
if mainwindow.figures and sp:
try:
dock = sp.plotters[0].ax.figure.canvas.manager.window
dock.widget().show_plugin()
dock.raise_()
except AttributeError:
pass
[docs]
def setup_ds_tree(self) -> None:
"""Setup the number of columns and the header of the dataset tree.
Reimplemented to use the :class:`psyplot_gui.content_widget.DatasetTree`
"""
self.ds_tree = tree = DatasetTree()
tree.setColumnCount(len(self.ds_attr_columns) + 1)
tree.setHeaderLabels([""] + self.ds_attr_columns)
[docs]
def position_dock(self, main: MainWindow, *args, **kwargs) -> None:
height = main.help_explorer.dock.size().height()
main.splitDockWidget(main.help_explorer.dock, self.dock, Qt.Vertical)
if hasattr(main, "resizeDocks"): # qt >= 5.6
main.resizeDocks(
[main.help_explorer.dock, self.dock],
[height // 2, height // 2],
Qt.Vertical,
)