diff --git a/CHANGELOG b/CHANGELOG
index 114019e..ffc26d2 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,7 +1,11 @@
-2.0.0b1
+2.0.0a25
- feat: implement data export
+ - feat: implement simple computation of basic statistics
- fix: pipeline modified plain dataset filter
- fix: allow to remove all datasets or filters
+ - fix: failed to load some datasets b/c of zero-valued meta data
+ - fix: toggle buttons in Block Matrix not functional
+ - enh: update font sizes
- docs: minor update
2.0.0a24
- implemented polygon filters
diff --git a/docs/sec_interface.rst b/docs/sec_interface.rst
index 4b394af..b868639 100644
--- a/docs/sec_interface.rst
+++ b/docs/sec_interface.rst
@@ -42,14 +42,14 @@ your data analysis from Shape-Out 1 to Shape-Out 2:
could be possible to convert sessions (including the corresponding
.tdms files), but the effort in doing so would probably exceed the
effort required to just rebuild a clean analysis session in Shape-Out 2.
-- Shape-Out 2 currently does not provide a linear mixed effects models
- (LMM) analysis. The reason behind that is quite pragmatic: LMM analysis
- in Shape-Out 1 is done using
+- Shape-Out 2 does not provide a linear mixed effects models
+ (LMM) analysis. LMM analysis in Shape-Out 1 is done using
`R/lme4 `_ and thus
- requires a full R distribution shipped with Shape-Out 1. While this
- blows up the download and installation size, it is also not clear
- whether it would work just like that on macOS. However, if many users
- need this feature, then we can think of a workaround.
+ requires a full R distribution shipped with Shape-Out 1. This
+ blows up the installation size and makes it more difficult to deploy.
+ Furthermore (and we are not saying that LMM Analysis is "bad") we are
+ also looking into other methods for determining statistical significance
+ which might be more intuitive to understand.
Basic usage
diff --git a/docs/sec_qg_mixed_effects.rst b/docs/sec_qg_mixed_effects.rst
deleted file mode 100644
index cebf166..0000000
--- a/docs/sec_qg_mixed_effects.rst
+++ /dev/null
@@ -1,79 +0,0 @@
-.. _sec_qg_mixed_effects:
-
-===========================
-Comparing datasets with LMM
-===========================
-
-.. warning:: This section is outdated.
-
-Consider the following datasets. A treatment is applied three times at different
-time points. For each treatment, a control measurement is performed.
-For each measurement day, a reservoir measurement is performed additionally
-for treatment and control.
-
-- Day1:
-
- - one sample, called "Treatment I", measured at flow rates of 0.04,
- 0.08 and 0.12 µl/s and one measurement in the reservoir
- - one control, called "Control I", measured at flow rates 0.04,
- 0.08 and 0.12 µl/s and one measurement in the reservoir
-
-- Day2:
-
- - two samples, called "Treatment II" and "Treatment III", measured
- at flow rates 0.04, 0.08 and 0.12 µl/s and one measurement in the reservoir
- - two controls, called "Control II" and "Control III", measured at
- flow rates 0.04, 0.08 and 0.12 µl/s and one measurement in the reservoir
-
-Linear mixed models (LMM) allow to assign a significance to a treatment (fixed effect)
-while considering the systematic bias in-between the measurement repetitions
-(random effect).
-
-We will assume that the datasets are loaded into Shape-Out and that
-invalid events have been filtered (see e.g. :ref:`sec_qg_filtering`).
-The *Analyze* configuration tab enables the comparison of an experiment
-(control and treatment) and repetitions of the experiment using
-LMM :cite:`Herbig2017`, :cite:`Herbig2018`.
-
-- **Basic analysis:**
-
- Assign which measurement is a control and which is a treatment by choosing
- the option in the dropdown lists under Interpretation. Group the pairs of
- control and treatment done in one experiment, by choosing an index number,
- called Repetition. Here, Treatment I and Control I are one experiment –
- called Repetition 1, Treatment II and Control II are a repetition of the
- experiment – called Repetition 2, Treatment III and Control III are another
- repetition of the experiment – called Repetition 3.
-
- Press Apply to start the calculations. A text file will open to show the results.
-
- The most important numbers are:
-
- - **Fixed effects:**
-
- (Intercept)-Estimate
- The mean of the parameter chosen for all controls.
-
- treatment-Estimate
- The effect size of the parameter chosen between the mean
- of all controls and the mean of all treatments.
-
- - **Full coefficient table:**
- Shows the effect size of the parameter chosen between control and
- treatment for every single experiment.
-
- - **Model-Pr(>Chisq):**
- Shows the p-value and the significance of the test.
-
-
-- **Differential feature analysis:**
-
- The LMM analysis is only applicable if the respective measurements
- show little difference in the reservoir for the feature chosen.
- For instance, if a treatment results in non-spherical cells in the reservoir,
- then the deformation recorded for the treatment might be biased towards
- higher values.
- In this case, the information of the reservoir measurement has to be
- included by means of the differential deformation :cite:`Herbig2018`.
- This can be achieved by selecting the respective reservoir measurements
- in the dropdown menu.
diff --git a/shapeout2/gui/analysis/ana_meta.py b/shapeout2/gui/analysis/ana_meta.py
index 8082167..c2c034d 100644
--- a/shapeout2/gui/analysis/ana_meta.py
+++ b/shapeout2/gui/analysis/ana_meta.py
@@ -109,11 +109,14 @@ def format_config_key_value(section, key, value):
tip = ""
# Value formatting
if dctype == float: # pretty-print floats
- # determine number of decimals
- dec = int(np.ceil(np.log10(1/np.abs(value))))
- if dec < 0:
- dec = 0
- string = ("{:." + "{}".format(dec + 2) + "f}").format(value)
+ if value == 0:
+ string = "0.0"
+ else:
+ # determine number of decimals
+ dec = int(np.ceil(np.log10(1/np.abs(value))))
+ if dec < 0:
+ dec = 0
+ string = ("{:." + "{}".format(dec + 2) + "f}").format(value)
else:
string = str(value)
diff --git a/shapeout2/gui/compute/__init__.py b/shapeout2/gui/compute/__init__.py
new file mode 100644
index 0000000..9e42a4a
--- /dev/null
+++ b/shapeout2/gui/compute/__init__.py
@@ -0,0 +1 @@
+from .comp_stats import ComputeStatistics # noqa: F401
diff --git a/shapeout2/gui/compute/comp_stats.py b/shapeout2/gui/compute/comp_stats.py
new file mode 100644
index 0000000..b046826
--- /dev/null
+++ b/shapeout2/gui/compute/comp_stats.py
@@ -0,0 +1,176 @@
+import codecs
+import numbers
+import pathlib
+import pkg_resources
+import time
+
+import dclab
+from PyQt5 import uic, QtWidgets
+
+from ...pipeline import Pipeline
+from ..._version import version
+
+STAT_METHODS = sorted(dclab.statistics.Statistics.available_methods.keys())
+STAT_METHODS.remove("%-gated") # This does not make sense with Pipeline
+
+
+class ComputeStatistics(QtWidgets.QDialog):
+ def __init__(self, parent, pipeline, *args, **kwargs):
+ QtWidgets.QWidget.__init__(self, parent, *args, **kwargs)
+ path_ui = pkg_resources.resource_filename(
+ "shapeout2.gui.compute", "comp_stats.ui")
+ uic.loadUi(path_ui, self)
+ # for external statistics
+ self.path = None
+ # set pipeline
+ self.pipeline = pipeline
+ # Signals
+ self.pushButton_path.clicked.connect(self.on_browse)
+ self.comboBox.currentIndexChanged.connect(self.on_combobox)
+ # Populate statistics methods
+ self.listWidget_stats.clear()
+ for meth in STAT_METHODS:
+ wid = QtWidgets.QListWidgetItem(meth)
+ wid.setCheckState(2)
+ self.listWidget_stats.addItem(wid)
+ # initialize rest
+ if len(self.pipeline.slots) == 0:
+ self.comboBox.setCurrentIndex(1)
+ else:
+ self.comboBox.setCurrentIndex(0)
+ self.on_combobox() # computes self.features
+
+ def done(self, r):
+ if r:
+ success = self.export_statistics()
+ else:
+ success = True
+ if success:
+ super(ComputeStatistics, self).done(r)
+
+ def export_statistics(self):
+ """Export statistics to .tsv"""
+ # get features
+ features = []
+ for ii in range(self.listWidget_features.count()):
+ if self.listWidget_features.item(ii).checkState() == 2:
+ features.append(self.features[ii])
+ # get methods
+ methods = []
+ for ii in range(self.listWidget_stats.count()):
+ if self.listWidget_stats.item(ii).checkState() == 2:
+ methods.append(STAT_METHODS[ii])
+
+ prog = QtWidgets.QProgressDialog("Computing statistics...", "Abort", 1,
+ 1, self)
+ prog.setMinimumDuration(0)
+ time.sleep(0.01)
+ prog.setValue(0)
+ # compute statistics
+ values = []
+ if self.comboBox.currentIndex() == 0:
+ # from pipeline
+ datasets = self.pipeline.get_datasets()
+ prog.setMaximum(len(datasets))
+ for ii, ds in enumerate(datasets):
+ h, v = dclab.statistics.get_statistics(ds,
+ methods=methods,
+ features=features)
+ h = ["Path", "Slot", "Name"] + h
+ v = ["{}".format(ds.path), ii, ds.title] + v
+ values.append(v)
+ if prog.wasCanceled():
+ break
+ prog.setValue(ii + 1)
+ QtWidgets.QApplication.processEvents()
+ else:
+ # from path
+ path = pathlib.Path(self.path)
+ files = sorted(path.rglob("*.rtdc"))
+ prog.setMaximum(len(files))
+ for ii, pp in enumerate(files):
+ ds = dclab.new_dataset(pp)
+ h, v = dclab.statistics.get_statistics(ds,
+ methods=methods,
+ features=features)
+ h = ["Path", "Name"] + h
+ v = ["{}".format(ds.path), ds.title] + v
+ values.append(v)
+ prog.setValue(ii + 1)
+ QtWidgets.QApplication.processEvents()
+ path, _ = QtWidgets.QFileDialog.getSaveFileName(
+ self, 'Save statistics', '', 'tab-separated values (*.tsv)')
+ if not path:
+ # Abort export
+ return False
+ elif not path.endswith(".tsv"):
+ path += ".tsv"
+
+ # Header
+ header = ["Statistics Output",
+ "Shape-Out {}".format(version),
+ "",
+ "\t".join(h),
+ ]
+ # Data
+ data = []
+ for v in values:
+ line = []
+ for vi in v:
+ if (isinstance(vi, numbers.Real)
+ and not isinstance(vi, numbers.Integral)):
+ line.append("{:.5e}".format(vi))
+ else:
+ line.append("{}".format(vi))
+ data.append("\t".join(line))
+ # Write BOM
+ with codecs.open(path, "wb") as fd:
+ fd.write(codecs.BOM_UTF8)
+ # Write rest
+ with codecs.open(path, "a", encoding="utf-8") as fd:
+ for line in header:
+ fd.write("# " + line + "\r\n")
+ for line in data:
+ fd.write(line + "\r\n")
+ return True # True means success
+
+ def on_browse(self):
+ out = QtWidgets.QFileDialog.getExistingDirectory(self,
+ 'Export directory')
+ if out:
+ self.path = out
+ self.lineEdit_path.setText(self.path)
+ self.comboBox.setCurrentIndex(1)
+ else:
+ self.path = None
+ self.comboBox.setCurrentIndex(0)
+
+ def on_combobox(self):
+ if self.comboBox.currentIndex() == 1:
+ self.widget_path.show()
+ if self.path is None:
+ self.on_browse()
+ if self.path:
+ self.update_feature_list(use_pipeline=False)
+ # else, on_combobox is triggered again
+ else:
+ self.widget_path.hide()
+ self.update_feature_list(use_pipeline=True)
+
+ def update_feature_list(self, use_pipeline=True):
+ if use_pipeline:
+ self.features = self.pipeline.get_features(scalar=True,
+ union=True,
+ label_sort=True)
+ else:
+ # This is just a cheap way of getting a label-sorted list
+ # of all scalar features.
+ empty_pipeline = Pipeline()
+ self.features = empty_pipeline.get_features(scalar=True,
+ label_sort=True)
+
+ self.listWidget_features.clear()
+ for feat in self.features:
+ wid = QtWidgets.QListWidgetItem(dclab.dfn.feature_name2label[feat])
+ wid.setCheckState(0)
+ self.listWidget_features.addItem(wid)
diff --git a/shapeout2/gui/compute/comp_stats.ui b/shapeout2/gui/compute/comp_stats.ui
new file mode 100644
index 0000000..37be45b
--- /dev/null
+++ b/shapeout2/gui/compute/comp_stats.ui
@@ -0,0 +1,127 @@
+
+
+ Dialog
+
+
+
+ 0
+ 0
+ 400
+ 300
+
+
+
+ Dialog
+
+
+ -
+
+
+ Compute statistics for multiple datasets.
+
+
+
+ -
+
+
-
+
+ All datasets of the current session
+
+
+ -
+
+ All datasets in a directory
+
+
+
+
+ -
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ PATH
+
+
+
+ -
+
+
+ Browse
+
+
+
+
+
+
+ -
+
+
-
+
+
+ -
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QDialogButtonBox::Cancel|QDialogButtonBox::Ok
+
+
+
+
+
+
+
+
+ buttonBox
+ accepted()
+ Dialog
+ accept()
+
+
+ 248
+ 254
+
+
+ 157
+ 274
+
+
+
+
+ buttonBox
+ rejected()
+ Dialog
+ reject()
+
+
+ 316
+ 260
+
+
+ 286
+ 274
+
+
+
+
+
diff --git a/shapeout2/gui/main.py b/shapeout2/gui/main.py
index 12a2aab..16a068b 100644
--- a/shapeout2/gui/main.py
+++ b/shapeout2/gui/main.py
@@ -15,8 +15,9 @@
import pyqtgraph as pg
from . import analysis
-from .matrix import BlockMatrix
+from . import compute
from . import export
+from .matrix import BlockMatrix
from . import pipeline_plot
from . import quick_view
@@ -56,6 +57,9 @@ def __init__(self):
self.actionAbout.triggered.connect(self.on_action_about)
# Export menu
self.actionExportData.triggered.connect(self.on_action_export_data)
+ # Comput menu
+ self.actionComputeStatistics.triggered.connect(
+ self.on_action_compute_statistics)
# Initially hide buttons
self.pushButton_preset_load.hide()
self.pushButton_preset_save.hide()
@@ -319,6 +323,10 @@ def on_action_about(self):
"Shape-Out {}".format(__version__),
about_text)
+ def on_action_compute_statistics(self):
+ dlg = compute.ComputeStatistics(self, pipeline=self.pipeline)
+ dlg.exec()
+
def on_action_docs(self):
webbrowser.open("https://shapeout2.readthedocs.io")
diff --git a/shapeout2/gui/matrix/data_matrix.py b/shapeout2/gui/matrix/data_matrix.py
index 3cc98db..cfe77a5 100644
--- a/shapeout2/gui/matrix/data_matrix.py
+++ b/shapeout2/gui/matrix/data_matrix.py
@@ -285,8 +285,6 @@ def enable_quickview(self, b=True):
def clear(self):
"""Reset layout"""
self._reset_layout()
- self.semi_states_dataset = {}
- self.semi_states_filter = {}
def dragEnterEvent(self, event):
print("drag enter event on data matrix")
@@ -469,38 +467,38 @@ def toggle_dataset_active(self):
which is defined by the signal sender :class:`MatrixDataset`.
Cyclic toggling order: semi -> all -> none
"""
- self.semi_states_filter = {}
+ self.semi_states_filter = {} # sic
sender = self.sender()
slot_id = sender.identifier
- state = self.get_slot_widget_state(slot_id)
- num_actives = sum([s["active"] for s in state.values()])
+ state = self.__getstate__()["elements"][slot_id]
+ num_actives = sum([s for s in state.values()])
# update state according to the scheme in the docstring
if num_actives == 0:
if slot_id in self.semi_states_dataset:
# use semi state
oldstate = self.semi_states_dataset[slot_id]
- for key in oldstate:
- if key in state:
- state[key] = oldstate[key]
+ for filt_id in oldstate:
+ if filt_id in state:
+ state[filt_id] = oldstate[filt_id]
else:
# toggle all to active
- for key in state:
- state[key]["active"] = True
+ for filt_id in state:
+ state[filt_id] = True
elif num_actives == len(state):
# toggle all to inactive
- for key in state:
- state[key]["active"] = False
+ for filt_id in state:
+ state[filt_id] = False
else:
# save semi state
self.semi_states_dataset[slot_id] = copy.deepcopy(state)
# toggle all to active
- for key in state:
- state[key]["active"] = True
+ for filt_id in state:
+ state[filt_id] = True
for filt_id in state:
me = self.get_matrix_element(slot_id, filt_id)
- me.__setstate__(state[filt_id])
+ me.set_active(state[filt_id])
self.publish_matrix()
@QtCore.pyqtSlot(bool)
@@ -527,43 +525,43 @@ def toggle_filter_active(self):
which is defined by the signal sender :class:`MatrixFilter`.
Cyclic toggling order: semi -> all -> none
"""
- self.semi_states_dataset = {}
+ self.semi_states_dataset = {} # sic
sender = self.sender()
filt_id = sender.identifier
states = self.__getstate__()["elements"]
state = {}
- for key in states:
- state[key] = states[key][filt_id]
+ for slot_id in states:
+ state[slot_id] = states[slot_id][filt_id]
- num_actives = sum([s["active"] for s in state.values()])
+ num_actives = sum(list(state.values()))
# update state according to the scheme in the docstring
if num_actives == 0:
if filt_id in self.semi_states_filter:
# use semi state
oldstate = self.semi_states_filter[filt_id]
- for key in oldstate:
- if key in state:
- state[key] = oldstate[key]
+ for slot_id in oldstate:
+ if slot_id in state:
+ state[slot_id] = oldstate[slot_id]
else:
# toggle all to active
- for key in state:
- state[key]["active"] = True
+ for slot_id in state:
+ state[slot_id] = True
elif num_actives == len(state):
# toggle all to inactive
- for key in state:
- state[key]["active"] = False
+ for slot_id in state:
+ state[slot_id] = False
else:
# save semi state
self.semi_states_filter[filt_id] = copy.deepcopy(state)
# toggle all to active
- for key in state:
- state[key]["active"] = True
+ for slot_id in state:
+ state[slot_id] = True
for slot_id in state:
me = self.get_matrix_element(slot_id, filt_id)
- me.__setstate__(state[slot_id])
+ me.set_active(state[slot_id])
self.publish_matrix()
@QtCore.pyqtSlot(bool)
diff --git a/shapeout2/gui/matrix/dm_dataset.py b/shapeout2/gui/matrix/dm_dataset.py
index 3d86b3f..bdb7325 100644
--- a/shapeout2/gui/matrix/dm_dataset.py
+++ b/shapeout2/gui/matrix/dm_dataset.py
@@ -79,6 +79,6 @@ def update_content(self):
title = meta_tool.get_repr(self.path, append_path=True)
self.setToolTip(title)
self.label.setToolTip(title)
- if len(title) > 8:
- title = title[:5] + "..."
+ if len(title) > 12:
+ title = title[:9] + "..."
self.label.setText(title)
diff --git a/shapeout2/gui/matrix/dm_dataset.ui b/shapeout2/gui/matrix/dm_dataset.ui
index 9e3ed84..59daed7 100644
--- a/shapeout2/gui/matrix/dm_dataset.ui
+++ b/shapeout2/gui/matrix/dm_dataset.ui
@@ -103,6 +103,11 @@
0
+
+
+ 9
+
+
Name
diff --git a/shapeout2/gui/matrix/dm_element.py b/shapeout2/gui/matrix/dm_element.py
index 7c8f523..f503824 100644
--- a/shapeout2/gui/matrix/dm_element.py
+++ b/shapeout2/gui/matrix/dm_element.py
@@ -52,6 +52,11 @@ def mousePressEvent(self, event):
self.update_content(quickview)
event.accept()
+ def set_active(self, b=True):
+ state = self.__getstate__()
+ state["active"] = b
+ self.__setstate__(state)
+
def update_content(self, quickview=False):
if self.invalid:
color = "#DCDCDC" # gray
diff --git a/shapeout2/gui/matrix/dm_filter.py b/shapeout2/gui/matrix/dm_filter.py
index f0fdb64..f8513d0 100644
--- a/shapeout2/gui/matrix/dm_filter.py
+++ b/shapeout2/gui/matrix/dm_filter.py
@@ -94,8 +94,8 @@ def on_modify(self):
def update_content(self):
"""Reset tool tips and title"""
self.label.setToolTip(self.name)
- if len(self.name) > 8:
- title = self.name[:5]+"..."
+ if len(self.name) > 12:
+ title = self.name[:9]+"..."
else:
title = self.name
self.checkBox.blockSignals(True)
diff --git a/shapeout2/gui/matrix/dm_filter.ui b/shapeout2/gui/matrix/dm_filter.ui
index 5edcbe3..a11bf75 100644
--- a/shapeout2/gui/matrix/dm_filter.ui
+++ b/shapeout2/gui/matrix/dm_filter.ui
@@ -103,6 +103,11 @@
0
+
+
+ 9
+
+
Name
diff --git a/shapeout2/gui/matrix/plot_matrix.py b/shapeout2/gui/matrix/plot_matrix.py
index 3d6987b..eb41429 100644
--- a/shapeout2/gui/matrix/plot_matrix.py
+++ b/shapeout2/gui/matrix/plot_matrix.py
@@ -276,43 +276,42 @@ def toggle_plot_active(self):
which is defined by the signal sender :class:`MatrixPlot`.
Cyclic toggling order: semi -> all -> none
"""
- self.semi_states_dataset = {}
sender = self.sender()
- sid = sender.identifier
+ plot_id = sender.identifier
states = self.__getstate__()["elements"]
state = {}
- for key in states:
- state[key] = states[key][sid]
+ for slot_id in states:
+ state[slot_id] = states[slot_id][plot_id]
- num_actives = sum([s["active"] for s in state.values()])
+ num_actives = sum(list(state.values()))
# update state according to the scheme in the docstring
if num_actives == 0:
- if sid in self.semi_states_plot:
+ if plot_id in self.semi_states_plot:
# use semi state
- oldstate = self.semi_states_plot[sid]
- for key in oldstate:
- if key in state:
- state[key] = oldstate[key]
+ oldstate = self.semi_states_plot[plot_id]
+ for slot_id in oldstate:
+ if slot_id in state:
+ state[slot_id] = oldstate[slot_id]
else:
# toggle all to active
- for key in state:
- state[key]["active"] = True
+ for slot_id in state:
+ state[slot_id] = True
elif num_actives == len(state):
# toggle all to inactive
- for key in state:
- state[key]["active"] = False
+ for slot_id in state:
+ state[slot_id] = False
else:
# save semi state
- self.semi_states_plot[sid] = copy.deepcopy(state)
+ self.semi_states_plot[plot_id] = copy.deepcopy(state)
# toggle all to active
- for key in state:
- state[key]["active"] = True
+ for slot_id in state:
+ state[slot_id] = True
- for dsid in state:
- me = self.get_matrix_element(dsid, sid)
- me.__setstate__(state[dsid])
+ for slot_id in state:
+ me = self.get_matrix_element(slot_id, plot_id)
+ me.set_active(state[slot_id])
self.publish_matrix()
def update_content(self):
diff --git a/shapeout2/gui/matrix/pm_plot.py b/shapeout2/gui/matrix/pm_plot.py
index c02f438..f18f5b8 100644
--- a/shapeout2/gui/matrix/pm_plot.py
+++ b/shapeout2/gui/matrix/pm_plot.py
@@ -69,8 +69,8 @@ def on_modify(self):
def update_content(self):
"""Reset tool tips and title"""
self.label.setToolTip(self.name)
- if len(self.name) > 8:
- title = self.name[:5]+"..."
+ if len(self.name) > 12:
+ title = self.name[:9]+"..."
else:
title = self.name
self.label.setText(title)
diff --git a/shapeout2/gui/matrix/pm_plot.ui b/shapeout2/gui/matrix/pm_plot.ui
index 9146f23..ff64380 100644
--- a/shapeout2/gui/matrix/pm_plot.ui
+++ b/shapeout2/gui/matrix/pm_plot.ui
@@ -103,6 +103,11 @@
0
+
+
+ 9
+
+
Name
diff --git a/shapeout2/gui/pipeline_plot.py b/shapeout2/gui/pipeline_plot.py
index d5aff32..efdb1ad 100644
--- a/shapeout2/gui/pipeline_plot.py
+++ b/shapeout2/gui/pipeline_plot.py
@@ -4,7 +4,7 @@
import dclab
from dclab import kde_contours
import numpy as np
-from PyQt5 import uic, QtCore, QtWidgets
+from PyQt5 import uic, QtCore, QtGui, QtWidgets
import pyqtgraph as pg
from ..pipeline import Plot
@@ -13,6 +13,7 @@
class PipelinePlot(QtWidgets.QWidget):
+ """Implements the plotting pipeline using pyqtgraph"""
def __init__(self, parent, pipeline, plot_id, *args, **kwargs):
QtWidgets.QWidget.__init__(self, parent=parent, *args, **kwargs)
path_ui = pkg_resources.resource_filename(
@@ -46,7 +47,9 @@ def update_content(self):
labelx, labely = get_axes_labels(plot_state, slot_states)
- self.plot_layout.addLabel(lay["name"], colspan=2)
+ self.plot_layout.addLabel(lay["name"], colspan=2,
+ # default size + 2
+ size="{}pt".format(QtGui.QFont().pointSize() + 2))
self.plot_layout.nextRow()
self.plot_layout.addLabel(labely, angle=-90)
@@ -178,7 +181,9 @@ def redraw(self, dslist, slot_states, plot_state):
self.setTitle(title)
if plot_state["scatter"]["show event count"]:
# set event count
- chtml = "{} events"
+ chtml = "".format(
+ # default font size - 1
+ QtGui.QFont().pointSize() - 1) + "{} events"
label = QtWidgets.QGraphicsTextItem(
"",
# This is kind of hackish: set the parent to the right
diff --git a/shapeout2/gui/rangecontrol.py b/shapeout2/gui/rangecontrol.py
index 8ea4218..ae04268 100644
--- a/shapeout2/gui/rangecontrol.py
+++ b/shapeout2/gui/rangecontrol.py
@@ -195,10 +195,13 @@ def setSpinLimits(self, vmin, vmax):
# decimals
if not self.is_integer:
- # two significant digits
- dec = -int(np.ceil(np.log10(vmax-vmin))) + 4
- if dec <= 0:
+ if vmax == vmin:
dec = 1
+ else:
+ # two significant digits
+ dec = int(np.ceil(np.log10(1/np.abs(vmax-vmin)))) + 5
+ if dec <= 0:
+ dec = 1
self.doubleSpinBox_min.setDecimals(dec)
self.doubleSpinBox_max.setDecimals(dec)
self.doubleSpinBox_min.setSingleStep(10**-dec)
diff --git a/shapeout2/pipeline/core.py b/shapeout2/pipeline/core.py
index f8e397c..a0ea8b3 100644
--- a/shapeout2/pipeline/core.py
+++ b/shapeout2/pipeline/core.py
@@ -312,6 +312,10 @@ def get_dataset(self, slot_index, filt_index=-1, apply_filter=True):
dsend.apply_filter()
return dsend
+ def get_datasets(self):
+ """Return all datasets with filters applied"""
+ return [self.get_dataset(ii) for ii in range(len(self.slots))]
+
def get_features(self, scalar=False, label_sort=False, union=False,
plot_id=None):
"""Return a list of features in the pipeline
diff --git a/shapeout2/pipeline/dataslot.py b/shapeout2/pipeline/dataslot.py
index 9d90765..67d6b76 100644
--- a/shapeout2/pipeline/dataslot.py
+++ b/shapeout2/pipeline/dataslot.py
@@ -1,6 +1,8 @@
import dclab
import numpy as np
+from .. import meta_tool
+
class Dataslot(object):
"""Handles datasets in a pipeline"""
@@ -18,7 +20,7 @@ def __init__(self, path, identifier=None, name=None):
identifier = "Dataslot_{}".format(Dataslot._instance_counter)
if name is None:
- name = identifier
+ name = meta_tool.get_rtdc_config(path)["experiment"]["sample"]
#: unique identifier of the filter
self.identifier = identifier
#: user-defined name of the filter