Skip to content

Commit

Permalink
Merge pull request #215 from wpfff/feature/pg-backend
Browse files Browse the repository at this point in the history
added pyqtgraph backend
  • Loading branch information
wpfff committed Sep 10, 2021
2 parents fa81487 + c476897 commit e168c8a
Show file tree
Hide file tree
Showing 23 changed files with 1,172 additions and 47 deletions.
34 changes: 33 additions & 1 deletion plottr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,5 +111,37 @@ def config(names: Optional[List[str]] = None) -> \
return config


def config_entry(*path: str, default: Optional[Any] = None,
names: Optional[List[str]] = None) -> Any:
"""Get a specific config value.
..Example: If the config is:: python
config = {
'foo' : {
'bar' : 'spam',
},
}
.. then we can get an entry like this:: python
>>> config_entry('foo', 'bar', default=None)
'spam'
>>> config_entry('foo', 'bacon')
None
>>> config_entry('foo', 'bar', 'bacon')
None
:param path: strings denoting the nested keys to the desired value
:param names: see :func:`.config`.
:param default: what to return when key isn't found in the config.
:returns: desired value
"""


cfg: Any = config(names)
for k in path:
if isinstance(cfg, dict) and k in cfg:
cfg = cfg.get(k)
else:
return default
return cfg
29 changes: 21 additions & 8 deletions plottr/apps/autoplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import os
import time
import argparse
from typing import Union, Tuple, Optional, Type, List, Any
from typing import Union, Tuple, Optional, Type, List, Any, Type
from packaging import version

from .. import QtCore, Flowchart, Signal, Slot, QtWidgets, QtGui
Expand All @@ -23,7 +23,8 @@
from ..node.grid import DataGridder, GridOption
from ..node.tools import linearFlowchart
from ..node.node import Node
from ..plot import PlotNode, makeFlowchartWithPlot
from ..plot import PlotNode, makeFlowchartWithPlot, PlotWidget
from ..plot.pyqtgraph.autoplot import AutoPlot as PGAutoPlot
from ..utils.misc import unwrap_optional

__author__ = 'Wolfgang Pfaff'
Expand All @@ -39,7 +40,8 @@ def logger() -> logging.Logger:
return logger


def autoplot(inputData: Union[None, DataDictBase] = None) \
def autoplot(inputData: Union[None, DataDictBase] = None,
plotWidgetClass: Optional[Type[PlotWidget]] = None) \
-> Tuple[Flowchart, 'AutoPlotMainWindow']:
"""
Sets up a simple flowchart consisting of a data selector, gridder,
Expand All @@ -63,7 +65,8 @@ def autoplot(inputData: Union[None, DataDictBase] = None) \
}

fc = makeFlowchartWithPlot(nodes)
win = AutoPlotMainWindow(fc, widgetOptions=widgetOptions)
win = AutoPlotMainWindow(fc, widgetOptions=widgetOptions,
plotWidgetClass=plotWidgetClass)
win.show()

if inputData is not None:
Expand Down Expand Up @@ -130,9 +133,11 @@ def __init__(self, fc: Flowchart,
monitor: bool = False,
monitorInterval: Union[float, None] = None,
loaderName: Optional[str] = None,
plotWidgetClass: Optional[Type[PlotWidget]] = None,
**kwargs: Any):

super().__init__(parent, fc=fc, **kwargs)
super().__init__(parent, fc=fc, plotWidgetClass=plotWidgetClass,
**kwargs)

self.fc = fc
if loaderName is not None:
Expand Down Expand Up @@ -236,7 +241,7 @@ def setDefaults(self, data: DataDictBase) -> None:
self.fc.nodes()['Data selection'].selectedData = selected
self.fc.nodes()['Grid'].grid = GridOption.guessShape, {}
self.fc.nodes()['Dimension assignment'].dimensionRoles = drs
unwrap_optional(self.plotWidget).plot.draw()
unwrap_optional(self.plotWidget).update()


class QCAutoPlotMainWindow(AutoPlotMainWindow):
Expand Down Expand Up @@ -324,11 +329,19 @@ def autoplotDDH5(filepath: str = '', groupname: str = 'data') \
('Data selection', DataSelector),
('Grid', DataGridder),
('Dimension assignment', XYSelector),
# ('Subtract average', SubtractAverage),
('plot', PlotNode)
)

win = AutoPlotMainWindow(fc, loaderName='Data loader', monitor=True,
widgetOptions = {
"Data selection": dict(visible=True,
dockArea=QtCore.Qt.TopDockWidgetArea),
"Dimension assignment": dict(visible=True,
dockArea=QtCore.Qt.TopDockWidgetArea),
}

win = AutoPlotMainWindow(fc, loaderName='Data loader',
widgetOptions=widgetOptions,
monitor=True,
monitorInterval=2.0)
win.show()

Expand Down
4 changes: 2 additions & 2 deletions plottr/apps/inspectr.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,14 +293,14 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None,
self._plotWindows: Dict[int, WindowDict] = {}

self.filepath = dbPath
self.dbdf = None
self.dbdf: Optional[pandas.DataFrame] = None
self.monitor = QtCore.QTimer()

# flag for determining what has been loaded so far.
# * None: nothing opened yet.
# * -1: empty DS open.
# * any value > 0: run ID from the most recent loading.
self.latestRunId = None
self.latestRunId: Optional[int] = None

self.setWindowTitle('Plottr | QCoDeS dataset inspectr')

Expand Down
14 changes: 14 additions & 0 deletions plottr/config/plottrcfg_main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
from matplotlib import cycler
from plottr.plot.pyqtgraph.autoplot import AutoPlot as PGAutoPlot
from plottr.plot.mpl.autoplot import AutoPlot as MPLAutoPlot

config = {

'default-plotwidget': MPLAutoPlot,

'matplotlibrc': {
'axes.grid': True,
'axes.prop_cycle': cycler('color', ['1f77b4', 'ff7f0e', '2ca02c', 'd62728', '9467bd', '8c564b',
Expand All @@ -26,4 +31,13 @@
'savefig.transparent': False,
},

'pyqtgraph': {
'background': 'w',
'foreground': 'k',
'line_colors': ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd',
'#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'],
'line_symbols': ['o', ],
'line_symbol_size': 7,
'minimum_plot_size': (400, 400),
}
}
1 change: 0 additions & 1 deletion plottr/data/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
from .datadict import DataDict, MeshgridDataDict
from .datadict_storage import DDH5Writer, datadict_from_hdf5
5 changes: 5 additions & 0 deletions plottr/data/datadict.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ def __init__(self, **kw: Any):

def __eq__(self, other: object) -> bool:
"""Check for content equality of two datadicts."""

# TODO: require a version that ignores metadata.
# FIXME: proper comparison of arrays for metadata.
# FIXME: arrays can be equal even if dtypes are not

if not isinstance(other, DataDictBase):
return NotImplemented

Expand Down
8 changes: 5 additions & 3 deletions plottr/data/datadict_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
DATAFILEXT = '.ddh5'
TIMESTRFORMAT = "%Y-%m-%d %H:%M:%S"

# FIXME: need correct handling of dtypes and list/array conversion


class AppendMode(Enum):
"""How/Whether to append data to existing data."""
Expand All @@ -51,7 +53,7 @@ class AppendMode(Enum):

def h5ify(obj: Any) -> Any:
"""
Convert an object into something that we can assing to an HDF5 attribute.
Convert an object into something that we can assign to an HDF5 attribute.
Performs the following conversions:
- list/array of strings -> numpy chararray of unicode type
Expand All @@ -69,7 +71,7 @@ def h5ify(obj: Any) -> Any:
obj = np.array(obj)

if type(obj) == np.ndarray and obj.dtype.kind == 'U':
return np.chararray.encode(obj, encoding='utf8')
return np.char.encode(obj, encoding='utf8')

return obj

Expand All @@ -80,7 +82,7 @@ def deh5ify(obj: Any) -> Any:
return obj.decode()

if type(obj) == np.ndarray and obj.dtype.kind == 'S':
return np.chararray.decode(obj)
return np.char.decode(obj)

return obj

Expand Down
2 changes: 2 additions & 0 deletions plottr/gui/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ def widgetDialog(widget: QtWidgets.QWidget, title: str = '',
win = QtWidgets.QDialog()
win.setWindowTitle('plottr ' + title)
layout = QtWidgets.QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(widget)
win.setLayout(layout)
if show:
Expand Down
92 changes: 84 additions & 8 deletions plottr/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
Common GUI widgets that are re-used across plottr.
"""
from numpy import rint
from typing import Union, List, Tuple, Optional, Type, Sequence, Dict, Any
from typing import Union, List, Tuple, Optional, Type, Sequence, Dict, Any, Type

from .tools import dictToTreeWidgetItems, dpiScalingFactor
from plottr import QtGui, QtCore, Flowchart, QtWidgets, Signal, Slot
from plottr.node import Node, linearFlowchart
from ..plot import PlotNode, PlotWidgetContainer, PlotWidget
from .. import config_entry as getcfg

__author__ = 'Wolfgang Pfaff'
__license__ = 'MIT'
Expand Down Expand Up @@ -68,24 +69,35 @@ def spinValueChanged(self, val: float) -> None:

class PlotWindow(QtWidgets.QMainWindow):
"""
Simple MainWindow class for embedding flowcharts and plots.
All keyword arguments supplied will be propagated to
:meth:`addNodeWidgetFromFlowchart`.
Simple MainWindow class for embedding flowcharts and plots, based on
``QtWidgets.QMainWindow``.
"""

#: Signal() -- emitted when the window is closed
windowClosed = Signal()

def __init__(self, parent: Optional[QtWidgets.QMainWindow] = None,
fc: Optional[Flowchart] = None,
plotWidgetClass: Optional[Any] = None,
plotWidgetClass: Optional[Type[PlotWidget]] = None,
**kw: Any):
"""
Constructor for :class:`.PlotWindow`.
:param parent: parent widget
:param fc: flowchart with nodes. if given, we will generate node widgets
in this window.
:param plotWidgetClass: class of the plot widget to use.
defaults to :class:`plottr.plot.mpl.AutoPlot`.
:param kw: any keywords will be propagated to
:meth:`addNodeWidgetFromFlowchart`.
"""
super().__init__(parent)

if plotWidgetClass is None:
from ..plot.mpl import AutoPlot
plotWidgetClass = AutoPlot
plotWidgetClass = getcfg('main', 'default-plotwidget')

if plotWidgetClass is None:
raise RuntimeError("No PlotWidget has been specified.")

self.plotWidgetClass = plotWidgetClass
self.plot = PlotWidgetContainer(parent=self)
Expand Down Expand Up @@ -229,3 +241,67 @@ def loadSnapshot(self, snapshotDict : Optional[dict]) -> None:
for i in range(2):
self.resizeColumnToContents(i)


def setHExpanding(w: QtWidgets.QWidget) -> None:
"""Set the size policy of a widget such that is expands horizontally."""
p = w.sizePolicy()
p.setHorizontalPolicy(QtWidgets.QSizePolicy.MinimumExpanding)
p.setHorizontalStretch(1)
w.setSizePolicy(p)


def setVExpanding(w: QtWidgets.QWidget) -> None:
"""Set the size policy of a widget such that is expands vertically."""
p = w.sizePolicy()
p.setVerticalPolicy(QtWidgets.QSizePolicy.MinimumExpanding)
p.setVerticalStretch(1)
w.setSizePolicy(p)


class Collapsible(QtWidgets.QWidget):
"""A wrapper that allow collapsing a widget."""

def __init__(self, widget: QtWidgets.QWidget, title: str = '',
parent: Optional[QtWidgets.QWidget] = None,
expanding: bool = True) -> None:
"""Constructor.
:param widget: the widget we'd like to collapse.
:param title: title of the widget. will appear on the toolbutton that
we use to trigger collapse/expansion.
:param parent: parent widget.
"""
super().__init__(parent=parent)

self.widget = widget
self.widget.setParent(self)
if expanding:
setVExpanding(self.widget)

self.expandedTitle = "[-] " + title
self.collapsedTitle = "[+] " + title

self.btn = QtWidgets.QPushButton(self.expandedTitle, parent=self)
self.btn.setStyleSheet("""background: white;
color: black;
border: 2px solid white;
text-align: left;""")
self.btn.setFlat(True)
self.btn.setCheckable(True)
self.btn.setChecked(True)
setHExpanding(self.btn)
self.btn.clicked.connect(self._onButton)

layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(2)
layout.addWidget(self.btn)
layout.addWidget(self.widget)

def _onButton(self) -> None:
if self.btn.isChecked():
self.widget.setVisible(True)
self.btn.setText(self.expandedTitle)
else:
self.widget.setVisible(False)
self.btn.setText(self.collapsedTitle)

0 comments on commit e168c8a

Please sign in to comment.