Skip to content

Commit

Permalink
Merge pull request #241 from wpfff/feature/histogram-node
Browse files Browse the repository at this point in the history
Add new analysis node for creating histogram of data
  • Loading branch information
astafan8 committed Dec 20, 2021
2 parents 3e088ae + d88af9f commit a31b3d4
Show file tree
Hide file tree
Showing 18 changed files with 979 additions and 120 deletions.
105 changes: 105 additions & 0 deletions doc/examples/node_with_dimension_selector_widget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""A simple script that illustrates how to use the :class:`.MultiDimensionSelector` widget
in a node to select axes in a dataset.
This example does the following:
* create a flowchart with one node, that has a node widget.
* selected axes in the node widget will be deleted from the data when the
selection is changed, and the remaining data is printed to stdout.
"""

from typing import List, Optional
from pprint import pprint

import numpy as np

from plottr import QtCore, QtWidgets, Signal, Slot
from plottr.data import DataDict
from plottr.node.node import Node, NodeWidget, updateOption, updateGuiQuietly
from plottr.node.tools import linearFlowchart
from plottr.gui.widgets import MultiDimensionSelector
from plottr.gui.tools import widgetDialog
from plottr.utils import testdata


class DummyNodeWidget(NodeWidget):
"""Node widget for this dummy node"""

def __init__(self, node: Node):

super().__init__(embedWidgetClass=MultiDimensionSelector)
assert isinstance(self.widget, MultiDimensionSelector) # this is for mypy

# allow selection of axis dimensions. See :class:`.MultiDimensionSelector`.
self.widget.dimensionType = 'axes'

# specify the functions that link node property to GUI elements
self.optSetters = {
'selectedAxes': self.setSelected,
}
self.optGetters = {
'selectedAxes': self.getSelected,
}

# make sure the widget is populated with the right dimensions
self.widget.connectNode(node)

# when the user selects an option, notify the node
self.widget.dimensionSelectionMade.connect(lambda x: self.signalOption('selectedAxes'))

@updateGuiQuietly
def setSelected(self, selected: List[str]) -> None:
self.widget.setSelected(selected)

def getSelected(self) -> List[str]:
return self.widget.getSelected()


class DummyNode(Node):
useUi = True
uiClass = DummyNodeWidget

def __init__(self, name: str):
super().__init__(name)
self._selectedAxes: List[str] = []

@property
def selectedAxes(self):
return self._selectedAxes

@selectedAxes.setter
@updateOption('selectedAxes')
def selectedAxes(self, value: List[str]):
self._selectedAxes = value

def process(self, dataIn = None) -> Dict[str, Optional[DataDict]]:
if super().process(dataIn) is None:
return None
data = dataIn.copy()
for k, v in data.items():
for s in self.selectedAxes:
if s in v.get('axes', []):
idx = v['axes'].index(s)
v['axes'].pop(idx)

for a in self.selectedAxes:
if a in data:
del data[a]

pprint(data)
return dict(dataOut=data)


def main():
fc = linearFlowchart(('dummy', DummyNode))
node = fc.nodes()['dummy']
dialog = widgetDialog(node.ui, title='dummy node')
data = testdata.get_2d_scalar_cos_data(2, 2, 1)
fc.setInput(dataIn=data)
return dialog, fc


if __name__ == '__main__':
app = QtWidgets.QApplication([])
dialog, fc = main()
dialog.show()
app.exec_()
4 changes: 4 additions & 0 deletions plottr/apps/autoplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from ..node.grid import DataGridder, GridOption
from ..node.tools import linearFlowchart
from ..node.node import Node
from ..node.histogram import Histogrammer
from ..plot import PlotNode, makeFlowchartWithPlot, PlotWidget
from ..plot.pyqtgraph.autoplot import AutoPlot as PGAutoPlot
from ..utils.misc import unwrap_optional
Expand Down Expand Up @@ -345,13 +346,16 @@ def autoplotDDH5(filepath: str = '', groupname: str = 'data') \
('Data loader', DDH5Loader),
('Data selection', DataSelector),
('Grid', DataGridder),
('Histogram', Histogrammer),
('Dimension assignment', XYSelector),
('plot', PlotNode)
)

widgetOptions = {
"Data selection": dict(visible=True,
dockArea=QtCore.Qt.TopDockWidgetArea),
"Histogram": dict(visible=False,
dockArea=QtCore.Qt.TopDockWidgetArea),
"Dimension assignment": dict(visible=True,
dockArea=QtCore.Qt.TopDockWidgetArea),
}
Expand Down
5 changes: 3 additions & 2 deletions plottr/gui/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ def dpiScalingFactor(widget: QtWidgets.QWidget) -> float:
return scaling


def widgetDialog(widget: QtWidgets.QWidget, title: str = '',
def widgetDialog(*widget: QtWidgets.QWidget, title: str = '',
show: bool = True) -> QtWidgets.QDialog:
win = QtWidgets.QDialog()
win.setWindowTitle('plottr ' + title)
layout = QtWidgets.QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
layout.addWidget(widget)
for w in widget:
layout.addWidget(w)
win.setLayout(layout)
if show:
win.show()
Expand Down
183 changes: 182 additions & 1 deletion plottr/gui/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

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

Expand Down Expand Up @@ -305,3 +306,183 @@ def _onButton(self) -> None:
else:
self.widget.setVisible(False)
self.btn.setText(self.collapsedTitle)


class DimensionCombo(QtWidgets.QComboBox):
"""A Combo Box that allows selection of a single data dimension.
This widget is designed to be used in a node widget.
Which type of dimensions are available for selection is set through the
``dimensionType`` option when creating the instance.
The widget can be linked to a node using the :meth:`.connectNode` method.
After linking, the available options will be populated whenever the data in
the node changes.
"""

#: Signal(str)
#: emitted when the user selects a dimension.
dimensionSelected = Signal(str)

def __init__(self, parent: Optional[QtWidgets.QWidget] = None,
dimensionType: str = 'axes') -> None:
"""Constructor.
:param parent: parent widget
:param dimensionType: one of `axes`, `dependents` or `all`.
"""
super().__init__(parent)

self.node: Optional[Node] = None
self.dimensionType = dimensionType

self.clear()
self.entries = ['None']
for e in self.entries:
self.addItem(e)

self.currentTextChanged.connect(self.signalDimensionSelection)

def connectNode(self, node: Optional[Node] = None) -> None:
"""Connect a node. will result in populating the combo box options
based on dimensions available in the node data.
:param node: instance of :class:`.Node`
"""
if node is None:
raise RuntimeError
self.node = node
if self.dimensionType == 'axes':
self.node.dataAxesChanged.connect(self.setDimensions)
elif self.dimensionType == 'dependents':
self.node.dataDependentsChanged.connect(self.setDimensions)
else:
self.node.dataFieldsChanged.connect(self.setDimensions)

@updateGuiQuietly
def setDimensions(self, dims: Sequence[str]) -> None:
"""Set the dimensions that are available for selection.
:param dims: list of dimensions, as strings.
:return: ``None``
"""
self.clear()
allDims = self.entries + list(dims)
for d in allDims:
self.addItem(d)

@Slot(str)
@emitGuiUpdate('dimensionSelected')
def signalDimensionSelection(self, val: str) -> str:
return val


class DimensionSelector(FormLayoutWrapper):
"""A widget that allows the user to select a dimension from a dataset
via a combobox.
Contains a label and a :class:`.DimensionCombo`."""

def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
super().__init__(
parent=parent,
elements=[('Dimension', DimensionCombo(dimensionType='all'))],
)
self.combo = self.elements['Dimension']


class DependentSelector(FormLayoutWrapper):
"""A widget that allows the user to select a dependent dimension from a dataset
via a combobox.
Contains a label and a :class:`.DimensionCombo`."""

def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
super().__init__(
parent=parent,
elements=[('Dependent', DimensionCombo(dimensionType='dependents'))],
)
self.combo = self.elements['Dependent']


class AxisSelector(FormLayoutWrapper):
"""A widget that allows the user to select an axis dimension from a dataset
via a combobox.
Contains a label and a :class:`.DimensionCombo`."""

def __init__(self, parent: Optional[QtWidgets.QWidget] = None):
super().__init__(
parent=parent,
elements=[('Axis', DimensionCombo(dimensionType='axes'))],
)
self.combo = self.elements['Axis']


class MultiDimensionSelector(QtWidgets.QListWidget):
"""A simple list widget that allows selection of multiple data dimensions."""

#: signal (List[str]) that is emitted when the selection is modified.
dimensionSelectionMade = Signal(list)

def __init__(self, parent: Optional[QtWidgets.QWidget] = None,
dimensionType: str = 'all') -> None:
"""Constructor.
:param parent: parent widget.
:param dimensionType: one of ``all``, ``axes``, or ``dependents``.
"""
super().__init__(parent)

self.node: Optional[Node] = None
self.dimensionType = dimensionType

self.setSelectionMode(self.MultiSelection)
self.itemSelectionChanged.connect(self.emitSelection)

def setDimensions(self, dimensions: List[str]) -> None:
"""set the available dimensions.
:param dimensions: list of dimension names.
"""
self.clear()
self.addItems(dimensions)

def getSelected(self) -> List[str]:
"""Get selected dimensions.
:return: List of dimensions (as strings).
"""
selectedItems = self.selectedItems()
return [s.text() for s in selectedItems]

def setSelected(self, selected: List[str]) -> None:
"""Set dimension selection.
:param selected: List of dimensions to be selected.
"""
for i in range(self.count()):
item = self.item(i)
if item.text() in selected:
item.setSelected(True)
else:
item.setSelected(False)

def emitSelection(self) -> None:
self.dimensionSelectionMade.emit(self.getSelected())

def connectNode(self, node: Optional[Node] = None) -> None:
"""Connect a node. Will result in populating the available options
based on dimensions available in the node data.
:param node: instance of :class:`.Node`
"""
if node is None:
raise RuntimeError
self.node = node
if self.dimensionType == 'axes':
self.node.dataAxesChanged.connect(self.setDimensions)
elif self.dimensionType == 'dependents':
self.node.dataDependentsChanged.connect(self.setDimensions)
else:
self.node.dataFieldsChanged.connect(self.setDimensions)
8 changes: 6 additions & 2 deletions plottr/node/dim_reducer.py
Original file line number Diff line number Diff line change
Expand Up @@ -556,10 +556,14 @@ def validateOptions(self, data: DataDictBase) -> bool:
the second element is taken as the arg-list.
The function can be of type :class:`.ReductionMethod`.
"""

delete = []
for ax, reduction in self._reductions.items():

if ax not in data.axes():
self.logger().warning(f"{ax} is not a known dimension. Removing.")
delete.append(ax)
continue

if reduction is None:
if isinstance(data, MeshgridDataDict):
self.logger().warning(f'Reduction for axis {ax} is None. '
Expand Down Expand Up @@ -599,6 +603,7 @@ def validateOptions(self, data: DataDictBase) -> bool:
self.logger().info(f'Reduction set for axis {ax} is only suited for '
f'grid data. Removing.')
delete.append(ax)
continue

# set the reduction in the correct format.
self._reductions[ax] = (fun, arg, kw)
Expand Down Expand Up @@ -817,7 +822,6 @@ def validateOptions(self, data: DataDictBase) -> bool:
self.optionChangeNotification.emit(
{'dimensionRoles': self.dimensionRoles}
)

return True

def process(
Expand Down

0 comments on commit a31b3d4

Please sign in to comment.