Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new analysis node for creating histogram of data #241

Merged
merged 33 commits into from
Dec 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
cd4147b
started developing central dimension selection widgets.
wpfff Sep 10, 2021
b9a7bc9
Merge branch 'master' into feature/histogram-node
wpfff Sep 11, 2021
5bd67fe
Merge branch 'master' into feature/histogram-node
wpfff Sep 16, 2021
fba3aa2
allow multiple widgets in the dialog.
wpfff Sep 17, 2021
31705ba
added a signal for changed dependents.
wpfff Sep 17, 2021
f89f9b1
generic widgets for dimension selection, for use in node widgets.
wpfff Sep 17, 2021
51065f2
new testdata functions that produce complex data that looks like a ty…
wpfff Oct 15, 2021
0e50878
completed list widget that allows selection of data fields.
wpfff Oct 17, 2021
4509507
better organization of testdata files.
wpfff Oct 17, 2021
3470e88
allow running as standalone script.
wpfff Oct 17, 2021
eb1bd6c
example script illustrating how to use MultiDimensionSelector.
wpfff Oct 17, 2021
3a76a8e
Create histogram.py
wpfff Nov 18, 2021
1ee636a
Merge branch 'master' into feature/histogram-node
wpfff Nov 20, 2021
1a1dcae
bugfix: prevent old reductions from lingering (they lead to crashes).
wpfff Nov 29, 2021
a884fea
fix: should not add ax for deletion multiple times.
wpfff Dec 3, 2021
77b1649
fix: logic in determining whether data has changed was incomplete.
wpfff Dec 4, 2021
8e3fac9
added missing requirements
wpfff Dec 4, 2021
0b35a04
added missing requirements
wpfff Dec 4, 2021
ccc6742
added a note for potential todo.
wpfff Dec 8, 2021
245adf9
finished v1 of the histogramming node.
wpfff Dec 8, 2021
19551c3
added testscript for histogramming node.
wpfff Dec 8, 2021
e9f646a
added additional test data
wpfff Dec 8, 2021
1510431
added histogram by default to ddh5 plot
wpfff Dec 8, 2021
21df34c
fixed mypy issue.
wpfff Dec 8, 2021
781612f
unsupported data will simply result in error rather than flawed checks.
wpfff Dec 8, 2021
13aef80
fixed test to use only supported data.
wpfff Dec 8, 2021
005e90a
bugfix: wrong computation of the histogram axes in the complex case.
wpfff Dec 8, 2021
b839702
Merge branch 'master' into feature/histogram-node
wpfff Dec 9, 2021
4f16b52
fix docs.
wpfff Dec 15, 2021
97d8dfd
Merge branch 'master' into feature/histogram-node
wpfff Dec 15, 2021
863fbd0
added pytest for histogrammer node.
wpfff Dec 17, 2021
136cfc2
Merge remote-tracking branch 'origin/feature/histogram-node' into fea…
wpfff Dec 17, 2021
d88af9f
Merge branch 'master' into feature/histogram-node
wpfff Dec 17, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this actually change self.entries and call addItem on elements of that? or is self.entries actually unused?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self.entries contains currently only one element, None, to give the user the option to not select any item.
And that entry should be stored somewhere other than the actual item list, because it would be cleared when we set a new list of dimensions.
So there will in any case always at least be the None in the dropdown. It looks maybe a bit weird, but i wanted to have that information not in the function but rather in the constructor, and was thinking of cases where more than just the None could be useful to fall under that category.

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
Loading