"""Dialogs for manipulating formatoptions."""
# 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
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Type, Union
import yaml
from matplotlib.backends.backend_qt5agg import (
FigureCanvasQTAgg as FigureCanvas,
)
from matplotlib.figure import Figure
from psyplot.plotter import Formatoption, Plotter
from PyQt5 import QtGui, QtWidgets
if TYPE_CHECKING:
from psyplot.project import Project
from PyQt5.QtCore import QEvent # pylint: disable=no-name-in-module
#: TODO: Find a more appropriate description here
Color = Any
#: TODO: Find a more appropriate description here
LSM_T = Dict[str, Any]
[docs]
class BasemapDialog(QtWidgets.QDialog):
"""A dialog to modify the basemap settings."""
xgrid_value: Optional[Union[str, Tuple[Any, Any]]]
ygrid_value: Optional[Union[str, Tuple[Any, Any]]]
def __init__(self, plotter: Plotter, *args, **kwargs) -> None:
"""
Parameters
----------
plotter: psy_maps.plotters.MapPlotter
The psyplot plotter to configure
"""
import pandas as pd
import psy_simple.widgets.colors as pswc
super().__init__(*args, **kwargs)
vbox = QtWidgets.QVBoxLayout(self)
#: colors that affect the map background
self.colors = ["background", "land", "ocean", "coast"]
#: QGridLayout to display the various colors
grid = QtWidgets.QGridLayout()
defaults = self.default_colors
#: :class:`pandas.DataFrame` of widgets to modifiy the :attr:`colors`
self.widgets = widgets = pd.DataFrame(
index=["enable", "color"], columns=self.colors, dtype=object
)
for i, col in enumerate(self.colors):
widgets.iloc[0, i] = cb = QtWidgets.QCheckBox()
cb.setChecked(False)
widgets.iloc[1, i] = lbl = pswc.ColorLabel(defaults[col])
lbl.setEnabled(False)
cb.stateChanged.connect(lbl.setEnabled)
grid.addWidget(QtWidgets.QLabel(col), 0, i)
grid.addWidget(cb, 1, i)
grid.addWidget(lbl, 2, i)
vbox.addLayout(grid)
#: Button box to cancel the operator or update the plotter
self.button_box = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel,
self,
)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
proj_box = QtWidgets.QGroupBox("Projection settings")
layout = QtWidgets.QFormLayout(proj_box)
#: text box for the central longitude (clon formatoption)
self.txt_clon = QtWidgets.QLineEdit()
self.txt_clon.setPlaceholderText("auto")
self.txt_clon.setToolTip("Central longitude in degrees East")
self.txt_clon.setValidator(QtGui.QDoubleValidator(-360, 360, 7))
layout.addRow("Central longitude: ", self.txt_clon)
#: text box for the central latitude (clat formatoption)
self.txt_clat = QtWidgets.QLineEdit()
self.txt_clat.setPlaceholderText("auto")
self.txt_clat.setToolTip("Central latitude in degrees North")
self.txt_clat.setValidator(QtGui.QDoubleValidator(-90, 90, 7))
layout.addRow("Central latitude: ", self.txt_clat)
vbox.addWidget(proj_box)
#: group box for modifying the resolution of the land-sea-mask, see
#: :attr:`opt_110m`, :attr:`opt_50m`, :attr:`opt_10m`
self.lsm_box = QtWidgets.QGroupBox("Coastlines")
self.lsm_box.setCheckable(True)
hbox = QtWidgets.QHBoxLayout(self.lsm_box)
hbox.addWidget(QtWidgets.QLabel("Resolution:"))
#: Radiobutton for 110m resolution of lsm
self.opt_110m = QtWidgets.QRadioButton("110m")
#: Radiobutton for 50m resolution of lsm
self.opt_50m = QtWidgets.QRadioButton("50m")
#: Radiobutton for 10m resolution of lsm
self.opt_10m = QtWidgets.QRadioButton("10m")
hbox.addWidget(self.opt_110m)
hbox.addWidget(self.opt_50m)
hbox.addWidget(self.opt_10m)
vbox.addWidget(self.lsm_box)
#: group box drawing grid lines and labels
self.grid_labels_box = QtWidgets.QGroupBox("Labels")
self.grid_labels_box.setToolTip(
"Draw labels of meridionals and " "parallels"
)
self.grid_labels_box.setCheckable(True)
#: text box for the fontsize of grid labels
self.txt_grid_fontsize = QtWidgets.QLineEdit()
form = QtWidgets.QFormLayout(self.grid_labels_box)
form.addRow("Font size:", self.txt_grid_fontsize)
vbox.addWidget(self.grid_labels_box)
#: Group box for options specific to meridionals (see
#: :attr:`opt_meri_auto`, :attr:`opt_meri_at` and
#: :attr:`opt_meri_every`, :attr:`opt_meri_num`)
self.meridionals_box = QtWidgets.QGroupBox("Meridionals")
self.meridionals_box.setCheckable(True)
#: Radiobutton for automatic drawing of meridionals
self.opt_meri_auto = QtWidgets.QRadioButton("auto")
#: Radiobutton for giving the exact position of meridionals (see
#: :attr:`txt_meri_at`)
self.opt_meri_at = QtWidgets.QRadioButton("At:")
#: Text field to enter the location of the meridionals on the map (see
#: :attr:`opt_meri_at`)
self.txt_meri_at = QtWidgets.QLineEdit()
self.txt_meri_at.setPlaceholderText("30, 60, 90, 120, ... °E")
# TODO: Add validator
#: Radiobutton for equal-width spaced meridionals (see
#: :attr:`txt_meri_every`)
self.opt_meri_every = QtWidgets.QRadioButton("Every:")
#: Text box to specify the distance between two meridionals (see
#: :attr:`opt_meri_every`)
self.txt_meri_every = QtWidgets.QLineEdit()
self.txt_meri_every.setPlaceholderText("30 °E")
self.txt_meri_every.setValidator(QtGui.QDoubleValidator(0, 360, 7))
#: Radiobutton to draw a specific number of meridionals with
#: equal-distance (see also :attr:`txt_meri_num`)
self.opt_meri_num = QtWidgets.QRadioButton("Number:")
#: Text box to set the number of meridionals to be shown (see
#: :attr:`opt_meri_num`)
self.txt_meri_num = QtWidgets.QLineEdit()
self.txt_meri_num.setPlaceholderText("5")
self.txt_meri_num.setValidator(QtGui.QIntValidator(1, 720))
form = QtWidgets.QFormLayout(self.meridionals_box)
form.addRow(self.opt_meri_auto)
form.addRow(self.opt_meri_at, self.txt_meri_at)
form.addRow(self.opt_meri_every, self.txt_meri_every)
form.addRow(self.opt_meri_num, self.txt_meri_num)
vbox.addWidget(self.meridionals_box)
#: Group box for options specific to parallels (see
#: :attr:`opt_para_auto`, :attr:`opt_para_at` and
#: :attr:`opt_para_every`, :attr:`opt_para_num`)
self.parallels_box = QtWidgets.QGroupBox("Parallels")
self.parallels_box.setCheckable(True)
#: Radiobutton for automatic drawing of parallels
self.opt_para_auto = QtWidgets.QRadioButton("auto")
#: Radiobutton for giving the exact position of parallels (see
#: :attr:`txt_para_at`)
self.opt_para_at = QtWidgets.QRadioButton("At:")
#: Text field to enter the location of the parallels on the map (see
#: :attr:`opt_para_at`)
self.txt_para_at = QtWidgets.QLineEdit()
self.txt_para_at.setPlaceholderText("-60, -30, 0, 30, ... °N")
# TODO: Add validator
#: Radiobutton for equal-width spaced parallels (see
#: :attr:`txt_para_every`)
self.opt_para_every = QtWidgets.QRadioButton("Every:")
#: Text box to specify the distance between two parallels (see
#: :attr:`opt_para_every`)
self.txt_para_every = QtWidgets.QLineEdit()
self.txt_para_every.setPlaceholderText("30 °N")
self.txt_para_every.setValidator(QtGui.QDoubleValidator(0, 90, 7))
#: Radiobutton to draw a specific number of parallels with
#: equal-distance (see also :attr:`txt_para_num`)
self.opt_para_num = QtWidgets.QRadioButton("Number:")
#: Text box to set the number of parallels to be shown (see
#: :attr:`opt_para_num`)
self.txt_para_num = QtWidgets.QLineEdit()
self.txt_para_num.setPlaceholderText("5")
self.txt_para_num.setValidator(QtGui.QIntValidator(1, 360))
form = QtWidgets.QFormLayout(self.parallels_box)
form.addRow(self.opt_para_auto)
form.addRow(self.opt_para_at, self.txt_para_at)
form.addRow(self.opt_para_every, self.txt_para_every)
form.addRow(self.opt_para_num, self.txt_para_num)
vbox.addWidget(self.parallels_box)
#: A box for selecting the background image
self.background_img_box = QtWidgets.QGroupBox("Background image")
self.background_img_box.setCheckable(True)
#: Radio button for using the ``stock_image`` formatoption
self.opt_stock_img = QtWidgets.QRadioButton("Stock image")
#: Radio button for using the ``google_map_detail`` formatoption
self.opt_google_image = QtWidgets.QRadioButton("Google maps")
#: Test box for specifying the detail
self.sb_google_image = QtWidgets.QSpinBox()
self.sb_google_image.setMinimum(0)
self.sb_google_image.setValue(4)
self.sb_google_image.setToolTip(
"The detail level for the Google image. The higher this "
"number, the more details you'll see (and the slower it is)"
)
form = QtWidgets.QFormLayout(self.background_img_box)
form.addRow(self.opt_stock_img)
form.addRow(self.opt_google_image, self.sb_google_image)
vbox.addWidget(self.background_img_box)
vbox.addWidget(self.button_box)
self.fill_from_plotter(plotter)
for button in [
self.opt_meri_at,
self.opt_meri_auto,
self.opt_meri_num,
self.opt_meri_every,
self.opt_para_at,
self.opt_para_auto,
self.opt_para_num,
self.opt_para_every,
self.opt_stock_img,
self.opt_google_image,
]:
button.clicked.connect(self.update_forms)
@property
def default_colors(self) -> Dict[str, Color]:
"""Get default colors for the color labels in :attr:`widgets`."""
import cartopy.feature as cf
import matplotlib as mpl
return {
"background": mpl.rcParams["axes.facecolor"],
"land": cf.LAND._kwargs["facecolor"],
"ocean": cf.OCEAN._kwargs["facecolor"],
"coast": "k",
}
[docs]
def get_colors(self, plotter: Plotter) -> Dict[str, Color]:
"""Get the colors for :attr:`widgets` from the plotter formatoptions.
Parameters
----------
plotter: psy_maps.plotters.MapPlotter
The plotter with the formatoptions
Returns
-------
dict
A mapping from formatoptions in :attr:`colors` to the corresponding
color in the `plotter`.
"""
ret = {}
if plotter.background.value != "rc":
ret["background"] = plotter.background.value
lsm = plotter.lsm.value
for part in ["land", "ocean", "coast"]:
if part in lsm:
ret[part] = lsm[part]
return ret
[docs]
def fill_from_plotter(self, plotter: Plotter) -> None:
"""Fill the dialog from a given plotter.
Parameters
----------
plotter: psy_maps.plotters.MapPlotter
The plotter to get the formatoptions from.
"""
chosen_colors = self.get_colors(plotter)
for i, col in enumerate(self.colors):
enable = col in chosen_colors
cb = self.widgets.iloc[0, i]
lbl = self.widgets.iloc[1, i]
cb.setChecked(enable)
if enable:
lbl._set_color(chosen_colors[col])
if plotter.clon.value is not None:
self.txt_clon.setText(str(plotter.clon.value))
if plotter.clat.value is not None:
self.txt_clat.setText(str(plotter.clat.value))
lsm = plotter.lsm.value
if not lsm:
self.lsm_box.setChecked(False)
else:
res = lsm["res"]
getattr(self, "opt_" + res).setChecked(True)
grid_labels = plotter.grid_labels.value
if grid_labels is None:
grid_labels = True
self.grid_labels_box.setChecked(grid_labels)
self.txt_grid_fontsize.setText(str(plotter.grid_labelsize.value))
self.xgrid_value = None
value = plotter.xgrid.value
if not value:
self.meridionals_box.setChecked(False)
elif value is True:
self.opt_meri_auto.setChecked(True)
elif isinstance(value[0], str):
self.xgrid_value = value[0]
self.opt_meri_num.setChecked(True)
self.txt_meri_num.setText(str(value[1]))
elif isinstance(value, tuple):
self.xgrid_value: Tuple[Any, Any] = value[:2] # type: ignore
self.opt_meri_num.setChecked(True)
steps = 11 if len(value) == 2 else value[3]
self.txt_meri_num.setText(str(steps))
else:
self.opt_meri_at.setChecked(True)
self.txt_meri_at.setText(", ".join(map(str, value)))
self.ygrid_value = None
value = plotter.ygrid.value
if not value:
self.parallels_box.setChecked(False)
elif value is True:
self.opt_para_auto.setChecked(True)
elif isinstance(value[0], str):
self.opt_para_num.setChecked(True)
self.txt_para_num.setText(str(value[1]))
self.ygrid_value = value[0]
elif isinstance(value, tuple):
self.ygrid_value: Tuple[Any, Any] = value[:2] # type: ignore
self.opt_para_num.setChecked(True)
steps = 11 if len(value) == 2 else value[3]
self.txt_para_num.setText(str(steps))
else:
self.opt_para_at.setChecked(True)
self.txt_para_at.setText(", ".join(map(str, value)))
stock_img = plotter.stock_img.value
google_img_detail = plotter.google_map_detail.value
if not stock_img and google_img_detail is None:
self.background_img_box.setChecked(False)
else:
self.background_img_box.setChecked(True)
if stock_img:
self.opt_stock_img.setChecked(True)
elif google_img_detail is not None:
self.opt_google_image.setChecked(True)
self.sb_google_image.setValue(google_img_detail)
@property
def value(self) -> Dict[str, Any]:
"""Get the formatoptions of this dialog to update a plotter."""
import numpy as np
ret: Dict[str, Any] = {}
ret["clon"] = (
None
if not self.txt_clon.text().strip()
else float(self.txt_clon.text().strip())
)
ret["clat"] = (
None
if not self.txt_clat.text().strip()
else float(self.txt_clat.text().strip())
)
lsm: LSM_T = {}
for col in ["land", "ocean", "coast"]:
lbl = self.widgets.loc["color", col]
if lbl.isEnabled():
lsm[col] = list(lbl.color.getRgbF())
if lsm or self.lsm_box.isChecked():
if self.opt_110m.isChecked():
lsm["res"] = "110m"
elif self.opt_50m.isChecked():
lsm["res"] = "50m"
elif self.opt_10m.isChecked():
lsm["res"] = "10m"
else:
lsm["res"] = "110m"
else:
lsm["res"] = False
if lsm:
ret["lsm"] = lsm
bc_lbl = self.widgets.loc["color", "background"]
if bc_lbl.isEnabled():
ret["background"] = list(bc_lbl.color.getRgbF())
ret["grid_labels"] = self.grid_labels_box.isChecked()
if ret["grid_labels"]:
ret["grid_labels"] = None
labelsize = self.txt_grid_fontsize.text().strip()
if labelsize:
try:
labelsize = float(labelsize)
except TypeError:
pass
ret["grid_labelsize"] = labelsize
if not self.meridionals_box.isChecked():
ret["xgrid"] = False
elif self.opt_meri_auto.isChecked():
ret["xgrid"] = True
elif self.opt_meri_every.isChecked():
ret["xgrid"] = np.arange(
-180, 180, float(self.txt_meri_every.text().strip() or 30)
)
elif self.opt_meri_at.isChecked():
ret["xgrid"] = (
list(map(float, self.txt_meri_at.text().split(","))) or False
)
elif self.opt_meri_num.isChecked():
if self.xgrid_value is None:
ret["xgrid"] = ["rounded", int(self.txt_meri_num.text() or 5)]
elif isinstance(self.xgrid_value, str):
ret["xgrid"] = [
self.xgrid_value,
int(self.txt_meri_num.text() or 5),
]
else:
ret["xgrid"] = tuple(self.xgrid_value) + (
int(self.txt_meri_num.text() or 5),
)
if not self.parallels_box.isChecked():
ret["ygrid"] = False
elif self.opt_para_auto.isChecked():
ret["ygrid"] = True
elif self.opt_para_every.isChecked():
ret["ygrid"] = np.arange(
-180, 180, float(self.txt_para_every.text().strip() or 30)
)
elif self.opt_para_at.isChecked():
ret["ygrid"] = (
list(map(float, self.txt_para_at.text().split(","))) or False
)
elif self.opt_para_num.isChecked():
if self.ygrid_value is None:
ret["ygrid"] = ["rounded", int(self.txt_para_num.text() or 5)]
elif isinstance(self.ygrid_value, str):
ret["ygrid"] = [
self.ygrid_value,
int(self.txt_para_num.text() or 5),
]
else:
ret["ygrid"] = tuple(self.ygrid_value) + (
int(self.txt_para_num.text() or 5),
)
if self.background_img_box.isChecked():
if self.opt_stock_img.isChecked():
ret["stock_img"] = True
ret["google_map_detail"] = None
elif self.opt_google_image.isChecked():
ret["stock_img"] = False
ret["google_map_detail"] = self.sb_google_image.value() or 0
else:
ret["stock_img"] = False
ret["google_map_detail"] = None
return ret
[docs]
@classmethod
def update_plotter(cls, plotter: Plotter) -> None:
"""Open a :class:`BasemapDialog` to update a plotter.
Parameters
----------
plotter: psy_maps.plotters.MapPlotter
The plotter to update.
"""
dialog = cls(plotter)
dialog.show()
dialog.raise_()
dialog.activateWindow()
dialog.exec_()
if dialog.result() == QtWidgets.QDialog.Accepted:
plotter.update(**dialog.value)
[docs]
class CmapDialog(QtWidgets.QDialog):
"""A dialog to modify color bounds and colormaps."""
def __init__(self, project: Project, *args, **kwargs) -> None:
"""
Parameters
----------
project: psyplot.project.Project
The psyplot project to update. Note that we will only use the
very first plotter in this project
"""
import psy_simple.widgets.colors as pswc
super().__init__(*args, **kwargs)
#: Button box to accept or cancel this dialog
self.button_box = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel,
self,
)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
#: Mapping from formatoption key to :class:`LabelWidgetLine` widgets to
#: controlling the formatoption
self.fmt_widgets = {}
plotter = project(fmts=["cmap", "bounds"]).plotters[0]
#: Widget for manipulating the color map
self.cmap_widget = self.fmt_widgets["cmap"] = LabelWidgetLine(
plotter.cmap,
project,
pswc.CMapFmtWidget,
widget_kws=dict(properties=False),
)
self.cmap_widget.editor.setVisible(False)
self.cmap_widget.editor.line_edit.textChanged.connect(
self.update_preview
)
#: tabs for switching between bounds (:attr:`bounds_widget`) and
#: colorbar ticks (:attr:`cticks_widget`)
self.tabs = QtWidgets.QTabWidget()
#: :class:`LabelWidgetLine` to controll the colorbar bounds
self.bounds_widget = self.fmt_widgets["bounds"] = LabelWidgetLine(
plotter.bounds,
project,
pswc.BoundsFmtWidget,
widget_kws=dict(properties=False),
)
self.bounds_widget.editor.line_edit.textChanged.connect(
self.update_preview
)
self.tabs.addTab(self.bounds_widget, "Colormap boundaries")
#: :class:`LabelWidgetLine` to controll the ctick positions
self.cticks_widget = self.fmt_widgets["cticks"] = LabelWidgetLine(
plotter.cticks,
project,
pswc.CTicksFmtWidget,
widget_kws=dict(properties=False),
)
self.cticks_widget.editor.line_edit.textChanged.connect(
self.update_preview
)
self.tabs.addTab(self.cticks_widget, "Colorbar ticks")
#: :class:`ColorbarPreview` to show a preview of the colorbar with
#: the selected formatoption in :attr:`fmt_widgets`
self.cbar_preview = ColorbarPreview(plotter)
self.cbar_preview.setMaximumHeight(self.tabs.sizeHint().height() // 3)
vbox = QtWidgets.QVBoxLayout(self)
vbox.addWidget(self.cmap_widget)
vbox.addWidget(self.tabs)
vbox.addWidget(self.cbar_preview)
vbox.addWidget(self.button_box)
@property
def plotter(self) -> Plotter:
"""Get the plotter with the formatoptions we use to fill this dialog."""
return self.bounds_widget.editor.fmto.plotter
[docs]
def update_preview(self) -> None:
"""Update the :attr:`cbar_preview` from the various :attr:`fmt_widgets`."""
try:
bounds = self.bounds_widget.editor.value
except Exception:
bounds = self.plotter.bounds.value
try:
cticks = self.cticks_widget.editor.value
except Exception:
cticks = self.plotter.cticks.value
try:
cmap = self.cmap_widget.editor.value
except Exception:
cmap = self.plotter.cmap.value
self.cbar_preview.update_colorbar(
bounds=bounds, cticks=cticks, cmap=cmap
)
@property
def fmts(self) -> Dict[str, Any]:
"""Map from formatoption in :attr:`fmt_widgets` to values."""
ret = {}
for fmt, widget in self.fmt_widgets.items():
if widget.editor.changed:
try:
value = widget.editor.value
except Exception:
raise IOError(
f"{fmt}-value {widget.editor.text} could "
"not be parsed to python!"
)
else:
ret[fmt] = value
return ret
[docs]
@classmethod
def update_project(cls, project: Project) -> None:
"""Create a :class:`CmapDialog` to update a `project`
This classmethod creates a new :class:`CmapDialog` instance, fills it
with the formatoptions of the first plotter in `project`, enters the
main event loop, and updates the `project` upon acceptance.
Parameters
----------
project: psyplot.project.Project
The psyplot project to update
"""
dialog = cls(project)
dialog.show()
dialog.raise_()
dialog.activateWindow()
dialog.exec_()
if dialog.result() == QtWidgets.QDialog.Accepted:
project.update(**dialog.fmts)
class _DummyFormatOption(Formatoption):
"""Dummy formatoption for static type checking.
This is just a workaround for the static type checker to be able to tell
what the :class:`FakePlotter` formatoptions are, used in
:attr:`ColorbarPreview.fake_plotter`
"""
def update(
self,
):
pass
[docs]
class FakePlotter(Plotter):
"""A dummy plotter for the colorbar preview."""
bounds: Formatoption = _DummyFormatOption("bounds")
cmap: Formatoption = _DummyFormatOption("cmap")
cticks: Formatoption = _DummyFormatOption("cticks")
cbar: Formatoption = _DummyFormatOption("cbar")
[docs]
class ColorbarPreview(FigureCanvas):
"""A preview widget of a colorbar.
This matplotlib figure contains one single axes to display the colorbar
filled by the formatoptions of a given `plotter`."""
def __init__(
self,
plotter: Plotter,
parent: Optional[QtWidgets.QWidget] = None,
*args,
**kwargs,
) -> None:
"""
Parameters
----------
plotter: psy_simple.plotters.Base2D
The plotter to use to draw the colorbar from
parent: QtWidget.QWidget
The parent widget
"""
fig = Figure(*args, **kwargs)
FigureCanvas.__init__(self, fig)
self.setParent(parent)
FigureCanvas.setSizePolicy(
self,
QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding,
)
FigureCanvas.updateGeometry(self)
self.axes_counter = 0
#: The plotter to use for displaying the colorbar
self.plotter = plotter
self.init_colorbar(plotter)
[docs]
def resizeEvent(self, event: QEvent) -> Any:
"""Reimplemented to make sure we cannot get smaller than 0."""
h = event.size().height()
if h <= 0:
return
return super().resizeEvent(event)
[docs]
def init_colorbar(self, plotter: Plotter) -> None:
"""Initialize the colorbar.
This method extracts the formatoptions of the given `plotter` and draws
the colorbar.
"""
from matplotlib.cm import ScalarMappable
norm = plotter.bounds.norm
cmap = plotter.cmap.get_cmap(self.plotter.plot.array)
self.mappable = sm = ScalarMappable(cmap=cmap, norm=norm)
sm.set_array([])
self.cax = self.figure.add_axes(
[0.1, 0.5, 0.8, 0.5], label=self.axes_counter
)
self.cbar = self.figure.colorbar(
sm, norm=norm, cmap=cmap, cax=self.cax, orientation="horizontal"
)
@property
def fake_plotter(self) -> FakePlotter:
"""Create a plotter with the formatoptions of the real :attr:`plotter`.
We can update this plotter without impacting the origin :attr:`plotter`
"""
class _FakePlotter(FakePlotter):
bounds = self.plotter.bounds.__class__("bounds")
cmap = self.plotter.cmap.__class__("cmap")
cticks = self.plotter.cticks.__class__("cticks")
cbar = self.plotter.cbar.__class__("cbar")
_rcparams_string = self.plotter._get_rc_strings()
ref = self.plotter
fig = Figure()
ax = fig.add_subplot()
plotter = _FakePlotter(
ref.data.copy(),
make_plot=False,
bounds=ref["bounds"],
cmap=ref["cmap"],
cticks=ref["cticks"],
cbar="",
ax=ax,
)
plotter.cticks._colorbar = self.cbar
plotter.plot_data = ref.plot_data
return plotter
[docs]
def update_colorbar(self, **kwargs) -> None:
"""Update the colorbar with new formatoptions.
This method takes the :attr:`fake_plotter`, updates it from the given
`kwargs`, updates the colorbar preview.
Parameters
----------
``**kwargs``
`bounds`, `cmap`, `cticks` or `cbar` formatoption keyword-value
pairs
"""
# create a dummy plotter
plotter = self.fake_plotter
# update from the given kwargs
try:
for key, val in kwargs.items():
plotter[key] = val
except (ValueError, TypeError):
return
plotter.initialize_plot(ax=plotter.ax)
current_norm = self.mappable.norm
current_cmap = self.mappable.get_cmap()
current_locator = self.cbar.locator
# update the preview with the norm of the plotter
try:
try:
plotter.bounds.norm._check_vmin_vmax()
except (AttributeError, TypeError):
pass
try:
plotter.bounds.norm.autoscale_None(plotter.bounds.array)
except AttributeError:
pass
self.mappable.set_norm(plotter.bounds.norm)
self.mappable.set_cmap(
plotter.cmap.get_cmap(self.plotter.plot.array)
)
plotter.cticks.colorbar = self.cbar
plotter.cticks.default_locator = (
self.plotter.cticks.default_locator
)
plotter.cticks.update_axis(plotter.cticks.value)
self.draw()
except Exception:
self.mappable.set_norm(current_norm)
self.mappable.set_cmap(current_cmap)
self.cbar.locator = current_locator
self.cbar.update_ticks()
[docs]
class LabelDialog(QtWidgets.QDialog):
"""A dialog to change labels.
This class contains one :class:`LabelWidgetLine` per text formatoption."""
def __init__(self, project: Project, *fmts: str) -> None:
"""
Parameters
----------
project: psyplot.project.Project
The psyplot project to update. Note that we will only use the
very first plotter in this project
``*fmts``
The formatoption keys to display. Each formatoption should be a
subclass of :class:`psy_simple.base.TextBase`
"""
from psy_simple.widgets.texts import LabelWidget
super().__init__()
self.project = project
layout = QtWidgets.QVBoxLayout()
plotter = project.plotters[0]
self.fmt_widgets = {}
for fmt in fmts:
fmto = getattr(plotter, fmt)
fmt_widget = LabelWidgetLine(
fmto, project, LabelWidget, widget_kws=dict(properties=False)
)
self.fmt_widgets[fmt] = fmt_widget
layout.addWidget(fmt_widget)
self.button_box = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel,
self,
)
self.button_box.accepted.connect(self.accept)
self.button_box.rejected.connect(self.reject)
layout.addWidget(self.button_box)
self.setLayout(layout)
@property
def fmts(self) -> Dict[str, Any]:
"""Mapping from formatoption key to value in this dialog."""
ret = {}
for fmt, widget in self.fmt_widgets.items():
if widget.editor.changed:
try:
value = widget.editor.value
except Exception:
raise IOError(
f"{fmt}-value {widget.editor.text} could "
"not be parsed to python!"
)
else:
ret[fmt] = value
return ret
[docs]
@classmethod
def update_project(cls, project: Project, *fmts: str) -> None:
"""Create a :class:`LabelDialog` to update the labels in a `project`.
This classmethod creates a new :class:`LabelDialog` instance, fills it
with the formatoptions of the first plotter in `project`, enters the
main event loop, and updates the `project` upon acceptance.
Parameters
----------
project: psyplot.project.Project
The psyplot project to update. Note that we will only use the
very first plotter in this project
``*fmts``
The formatoption keys to display. Each formatoption should be a
subclass of :class:`psy_simple.base.TextBase`
"""
dialog = cls(project, *fmts)
dialog.show()
dialog.raise_()
dialog.activateWindow()
dialog.exec_()
if dialog.result() == QtWidgets.QDialog.Accepted:
project.update(**dialog.fmts)