File: DataFilterWidget.py

package info (click to toggle)
python-pyqtgraph 0.13.7-5
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 8,068 kB
  • sloc: python: 54,043; makefile: 129; ansic: 40; sh: 2
file content (212 lines) | stat: -rw-r--r-- 7,669 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
from collections import OrderedDict

import numpy as np

from .. import functions as fn
from .. import parametertree as ptree
from ..Qt import QtCore

__all__ = ['DataFilterWidget']


class DataFilterWidget(ptree.ParameterTree):
    """
    This class allows the user to filter multi-column data sets by specifying
    multiple criteria
    
    Wraps methods from DataFilterParameter: setFields, generateMask,
    filterData, and describe.
    """
    
    sigFilterChanged = QtCore.Signal(object)
    
    def __init__(self):
        ptree.ParameterTree.__init__(self, showHeader=False)
        self.params = DataFilterParameter()
        
        self.setParameters(self.params)
        self.params.sigFilterChanged.connect(self.sigFilterChanged)
        
        self.setFields = self.params.setFields
        self.generateMask = self.params.generateMask
        self.filterData = self.params.filterData
        self.describe = self.params.describe
        
    def parameters(self):
        return self.params

    def addFilter(self, name):
        """Add a new filter and return the created parameter item.
        """
        return self.params.addNew(name)

        
class DataFilterParameter(ptree.types.GroupParameter):
    """A parameter group that specifies a set of filters to apply to tabular data.
    """
    sigFilterChanged = QtCore.Signal(object)
    
    def __init__(self):
        self.fields = {}
        ptree.types.GroupParameter.__init__(self, name='Data Filter', addText='Add filter..', addList=[])
        self.sigTreeStateChanged.connect(self.filterChanged)
    
    def filterChanged(self):
        self.sigFilterChanged.emit(self)
        
    def addNew(self, name):
        mode = self.fields[name].get('mode', 'range')
        if mode == 'range':
            child = self.addChild(RangeFilterItem(name, self.fields[name]))
        elif mode == 'enum':
            child = self.addChild(EnumFilterItem(name, self.fields[name]))
        else:
            raise ValueError("field mode must be 'range' or 'enum'")
        return child
            
    def fieldNames(self):
        return self.fields.keys()
    
    def setFields(self, fields):
        """Set the list of fields that are available to be filtered.

        *fields* must be a dict or list of tuples that maps field names
        to a specification describing the field. Each specification is
        itself a dict with either ``'mode':'range'`` or ``'mode':'enum'``::

            filter.setFields([
                ('field1', {'mode': 'range'}),
                ('field2', {'mode': 'enum', 'values': ['val1', 'val2', 'val3']}),
                ('field3', {'mode': 'enum', 'values': {'val1':True, 'val2':False, 'val3':True}}),
            ])
        """
        with fn.SignalBlock(self.sigTreeStateChanged, self.filterChanged):
            self.fields = OrderedDict(fields)
            names = self.fieldNames()
            self.setAddList(names)

            # update any existing filters
            for ch in self.children():
                name = ch.fieldName
                if name in fields:
                    ch.updateFilter(fields[name])
        self.sigFilterChanged.emit(self)
    
    def filterData(self, data):
        if len(data) == 0:
            return data
        return data[self.generateMask(data)]
    
    def generateMask(self, data):
        """Return a boolean mask indicating whether each item in *data* passes
        the filter critera.
        """
        mask = np.ones(len(data), dtype=bool)
        if len(data) == 0:
            return mask
        for fp in self:
            if fp.value() is False:
                continue
            mask &= fp.generateMask(data, mask.copy())
            #key, mn, mx = fp.fieldName, fp['Min'], fp['Max']
            
            #vals = data[key]
            #mask &= (vals >= mn)
            #mask &= (vals < mx)  ## Use inclusive minimum and non-inclusive maximum. This makes it easier to create non-overlapping selections
        return mask
    
    def describe(self):
        """Return a list of strings describing the currently enabled filters."""
        desc = []
        for fp in self:
            if fp.value() is False:
                continue
            desc.append(fp.describe())
        return desc


class RangeFilterItem(ptree.types.SimpleParameter):
    def __init__(self, name, opts):
        self.fieldName = name
        units = opts.get('units', '')
        self.units = units
        ptree.types.SimpleParameter.__init__(self, 
            name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True, 
            children=[
                #dict(name="Field", type='list', value=name, limits=fields),
                dict(name='Min', type='float', value=0.0, suffix=units, siPrefix=True),
                dict(name='Max', type='float', value=1.0, suffix=units, siPrefix=True),
            ])
            
    def generateMask(self, data, mask):
        vals = data[self.fieldName][mask]
        mask[mask] = (vals >= self['Min']) & (vals < self['Max'])  ## Use inclusive minimum and non-inclusive maximum. This makes it easier to create non-overlapping selections
        return mask
    
    def describe(self):
        return "%s < %s < %s" % (fn.siFormat(self['Min'], suffix=self.units), self.fieldName, fn.siFormat(self['Max'], suffix=self.units))

    def updateFilter(self, opts):
        pass
    

class EnumFilterItem(ptree.types.SimpleParameter):
    def __init__(self, name, opts):
        self.fieldName = name
        ptree.types.SimpleParameter.__init__(self, 
            name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True)
        self.setEnumVals(opts)            
    
    def generateMask(self, data, startMask):
        vals = data[self.fieldName][startMask]
        mask = np.ones(len(vals), dtype=bool)
        otherMask = np.ones(len(vals), dtype=bool)
        for c in self:
            key = c.maskValue
            if key == '__other__':
                m = ~otherMask
            else:
                m = vals != key
                otherMask &= m
            if c.value() is False:
                mask &= m
        startMask[startMask] = mask
        return startMask

    def describe(self):
        vals = [ch.name() for ch in self if ch.value() is True]
        return "%s: %s" % (self.fieldName, ', '.join(vals))

    def updateFilter(self, opts):
        self.setEnumVals(opts)

    def setEnumVals(self, opts):
        vals = opts.get('values', {})

        prevState = {}
        for ch in self.children():
            prevState[ch.name()] = ch.value()
            self.removeChild(ch)

        if not isinstance(vals, dict):
            vals = OrderedDict([(v,(str(v), True)) for v in vals])
        
        # Each filterable value can come with either (1) a string name, (2) a bool
        # indicating whether the value is enabled by default, or (3) a tuple providing
        # both.
        for val,valopts in vals.items():
            if isinstance(valopts, bool):
                enabled = valopts
                vname = str(val)
            elif isinstance(valopts, str):
                enabled = True
                vname = valopts
            elif isinstance(valopts, tuple):
                vname, enabled = valopts

            ch = ptree.Parameter.create(name=vname, type='bool', value=prevState.get(vname, enabled))
            ch.maskValue = val
            self.addChild(ch)
        ch = ptree.Parameter.create(name='(other)', type='bool', value=prevState.get('(other)', True))
        ch.maskValue = '__other__'
        self.addChild(ch)