class OWSelectRows(widget.OWWidget): name = "选择行" id = "Orange.widgets.data.file" description = "根据变量值从数据中选择行。" icon = "icons/SelectRows.svg" priority = 100 category = "Data" keywords = ["filter"] class Inputs: data = Input("Data", Table) class Outputs: matching_data = Output("Matching Data", Table, default=True) unmatched_data = Output("Unmatched Data", Table) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) want_main_area = False settingsHandler = SelectRowsContextHandler() conditions = ContextSetting([]) update_on_change = Setting(True) purge_attributes = Setting(False, schema_only=True) purge_classes = Setting(False, schema_only=True) auto_commit = Setting(True) Operators = { ContinuousVariable: [ (FilterContinuous.Equal, "equals"), (FilterContinuous.NotEqual, "is not"), (FilterContinuous.Less, "is below"), (FilterContinuous.LessEqual, "is at most"), (FilterContinuous.Greater, "is greater than"), (FilterContinuous.GreaterEqual, "is at least"), (FilterContinuous.Between, "is between"), (FilterContinuous.Outside, "is outside"), (FilterContinuous.IsDefined, "is defined"), ], DiscreteVariable: [(FilterDiscreteType.Equal, "is"), (FilterDiscreteType.NotEqual, "is not"), (FilterDiscreteType.In, "is one of"), (FilterDiscreteType.IsDefined, "is defined")], StringVariable: [ (FilterString.Equal, "equals"), (FilterString.NotEqual, "is not"), (FilterString.Less, "is before"), (FilterString.LessEqual, "is equal or before"), (FilterString.Greater, "is after"), (FilterString.GreaterEqual, "is equal or after"), (FilterString.Between, "is between"), (FilterString.Outside, "is outside"), (FilterString.Contains, "contains"), (FilterString.StartsWith, "begins with"), (FilterString.EndsWith, "ends with"), (FilterString.IsDefined, "is defined"), ] } Operators[TimeVariable] = Operators[ContinuousVariable] operator_names = { vtype: [name for _, name in filters] for vtype, filters in Operators.items() } class Error(widget.OWWidget.Error): parsing_error = Msg("{}") def __init__(self): super().__init__() self.old_purge_classes = True self.conditions = [] self.last_output_conditions = None self.data = None self.data_desc = self.match_desc = self.nonmatch_desc = None box = gui.vBox(self.controlArea, '条件', stretch=100) self.cond_list = QTableWidget(box, showGrid=False, selectionMode=QTableWidget.NoSelection) box.layout().addWidget(self.cond_list) self.cond_list.setColumnCount(4) self.cond_list.setRowCount(0) self.cond_list.verticalHeader().hide() self.cond_list.horizontalHeader().hide() for i in range(3): self.cond_list.horizontalHeader().setSectionResizeMode( i, QHeaderView.Stretch) self.cond_list.horizontalHeader().resizeSection(3, 30) self.cond_list.viewport().setBackgroundRole(QPalette.Window) box2 = gui.hBox(box) gui.rubber(box2) self.add_button = gui.button(box2, self, "添加条件", callback=self.add_row) self.add_all_button = gui.button(box2, self, "添加所有变量", callback=self.add_all) self.remove_all_button = gui.button(box2, self, "全部删除", callback=self.remove_all) gui.rubber(box2) boxes = gui.widgetBox(self.controlArea, orientation=QGridLayout()) layout = boxes.layout() layout.setColumnStretch(0, 1) layout.setColumnStretch(1, 1) box_data = gui.vBox(boxes, '数据', addToLayout=False) self.data_in_variables = gui.widgetLabel(box_data, " ") self.data_out_rows = gui.widgetLabel(box_data, " ") layout.addWidget(box_data, 0, 0) box_setting = gui.vBox(boxes, '删除', addToLayout=False) self.cb_pa = gui.checkBox(box_setting, self, "purge_attributes", "删除未使用的特征", callback=self.conditions_changed) gui.separator(box_setting, height=1) self.cb_pc = gui.checkBox(box_setting, self, "purge_classes", "删除未使用的类", callback=self.conditions_changed) layout.addWidget(box_setting, 0, 1) self.report_button.setFixedWidth(120) gui.rubber(self.buttonsArea.layout()) layout.addWidget(self.buttonsArea, 1, 0) acbox = gui.auto_commit(None, self, "auto_commit", label="发送", orientation=Qt.Horizontal, checkbox_label="自动发送") layout.addWidget(acbox, 1, 1) self.set_data(None) self.resize(600, 400) def add_row(self, attr=None, condition_type=None, condition_value=None): model = self.cond_list.model() row = model.rowCount() model.insertRow(row) attr_combo = gui.OrangeComboBox( minimumContentsLength=12, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon) attr_combo.row = row for var in self._visible_variables(self.data.domain): attr_combo.addItem(*gui.attributeItem(var)) attr_combo.setCurrentIndex(attr or 0) self.cond_list.setCellWidget(row, 0, attr_combo) index = QPersistentModelIndex(model.index(row, 3)) temp_button = QPushButton( '×', self, flat=True, styleSheet='* {font-size: 16pt; color: silver}' '*:hover {color: black}') temp_button.clicked.connect(lambda: self.remove_one(index.row())) self.cond_list.setCellWidget(row, 3, temp_button) self.remove_all_button.setDisabled(False) self.set_new_operators(attr_combo, attr is not None, condition_type, condition_value) attr_combo.currentIndexChanged.connect( lambda _: self.set_new_operators(attr_combo, False)) self.cond_list.resizeRowToContents(row) @staticmethod def _visible_variables(domain): """Generate variables in order they should be presented in in combos.""" return filter_visible( chain(domain.class_vars, domain.metas, domain.attributes)) def add_all(self): if self.cond_list.rowCount(): Mb = QMessageBox if Mb.question( self, "Remove existing filters", "This will replace the existing filters with " "filters for all variables.", Mb.Ok | Mb.Cancel) != Mb.Ok: return self.remove_all() domain = self.data.domain for i in range(len(domain.variables) + len(domain.metas)): self.add_row(i) def remove_one(self, rownum): self.remove_one_row(rownum) self.conditions_changed() def remove_all(self): self.remove_all_rows() self.conditions_changed() def remove_one_row(self, rownum): self.cond_list.removeRow(rownum) if self.cond_list.model().rowCount() == 0: self.remove_all_button.setDisabled(True) def remove_all_rows(self): self.cond_list.clear() self.cond_list.setRowCount(0) self.remove_all_button.setDisabled(True) def set_new_operators(self, attr_combo, adding_all, selected_index=None, selected_values=None): oper_combo = QComboBox() oper_combo.row = attr_combo.row oper_combo.attr_combo = attr_combo var = self.data.domain[attr_combo.currentText()] oper_combo.addItems(self.operator_names[type(var)]) oper_combo.setCurrentIndex(selected_index or 0) self.cond_list.setCellWidget(oper_combo.row, 1, oper_combo) self.set_new_values(oper_combo, adding_all, selected_values) oper_combo.currentIndexChanged.connect( lambda _: self.set_new_values(oper_combo, False)) @staticmethod def _get_lineedit_contents(box): return [ child.text() for child in getattr(box, "controls", [box]) if isinstance(child, QLineEdit) ] @staticmethod def _get_value_contents(box): cont = [] names = [] for child in getattr(box, "controls", [box]): if isinstance(child, QLineEdit): cont.append(child.text()) elif isinstance(child, QComboBox): cont.append(child.currentIndex()) elif isinstance(child, QToolButton): if child.popup is not None: model = child.popup.list_view.model() for row in range(model.rowCount()): item = model.item(row) if item.checkState(): cont.append(row + 1) names.append(item.text()) child.desc_text = ', '.join(names) child.set_text() elif isinstance(child, QLabel) or child is None: pass else: raise TypeError('Type %s not supported.' % type(child)) return tuple(cont) class QDoubleValidatorEmpty(QDoubleValidator): def validate(self, input_, pos): if not input_: return QDoubleValidator.Acceptable, input_, pos if self.locale().groupSeparator() in input_: return QDoubleValidator.Invalid, input_, pos return super().validate(input_, pos) def set_new_values(self, oper_combo, adding_all, selected_values=None): # def remove_children(): # for child in box.children()[1:]: # box.layout().removeWidget(child) # child.setParent(None) def add_textual(contents): le = gui.lineEdit(box, self, None, sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) if contents: le.setText(contents) le.setAlignment(Qt.AlignRight) le.editingFinished.connect(self.conditions_changed) return le def add_numeric(contents): le = add_textual(contents) le.setValidator(OWSelectRows.QDoubleValidatorEmpty()) return le def add_datetime(contents): le = add_textual(contents) le.setValidator(QRegExpValidator(QRegExp(TimeVariable.REGEX))) return le var = self.data.domain[oper_combo.attr_combo.currentText()] box = self.cond_list.cellWidget(oper_combo.row, 2) if selected_values is not None: lc = list(selected_values) + ["", ""] lc = [str(x) for x in lc[:2]] else: lc = ["", ""] if box and vartype(var) == box.var_type: lc = self._get_lineedit_contents(box) + lc oper = oper_combo.currentIndex() if oper_combo.currentText() == "is defined": label = QLabel() label.var_type = vartype(var) self.cond_list.setCellWidget(oper_combo.row, 2, label) elif var.is_discrete: if oper_combo.currentText() == "is one of": if selected_values: lc = [x for x in list(selected_values)] button = DropDownToolButton(self, var, lc) button.var_type = vartype(var) self.cond_list.setCellWidget(oper_combo.row, 2, button) else: combo = QComboBox() combo.addItems([""] + var.values) if lc[0]: combo.setCurrentIndex(int(lc[0])) else: combo.setCurrentIndex(0) combo.var_type = vartype(var) self.cond_list.setCellWidget(oper_combo.row, 2, combo) combo.currentIndexChanged.connect(self.conditions_changed) else: box = gui.hBox(self, addToLayout=False) box.var_type = vartype(var) self.cond_list.setCellWidget(oper_combo.row, 2, box) if var.is_continuous: validator = add_datetime if isinstance( var, TimeVariable) else add_numeric box.controls = [validator(lc[0])] if oper > 5: gui.widgetLabel(box, " and ") box.controls.append(validator(lc[1])) elif var.is_string: box.controls = [add_textual(lc[0])] if oper in [6, 7]: gui.widgetLabel(box, " and ") box.controls.append(add_textual(lc[1])) else: box.controls = [] if not adding_all: self.conditions_changed() @Inputs.data def set_data(self, data): self.closeContext() self.data = data self.cb_pa.setEnabled(not isinstance(data, SqlTable)) self.cb_pc.setEnabled(not isinstance(data, SqlTable)) self.remove_all_rows() self.add_button.setDisabled(data is None) self.add_all_button.setDisabled( data is None or len(data.domain.variables) + len(data.domain.metas) > 100) if not data: self.data_desc = None self.commit() return self.data_desc = report.describe_data_brief(data) self.conditions = [] try: self.openContext(data) except Exception: pass variables = list(self._visible_variables(self.data.domain)) varnames = [v.name for v in variables] if self.conditions: for attr, cond_type, cond_value in self.conditions: if attr in varnames: self.add_row(varnames.index(attr), cond_type, cond_value) elif variables: self.add_row() self.update_info(data, self.data_in_variables, "In: ") self.unconditional_commit() def conditions_changed(self): try: self.conditions = [] self.conditions = [ (self.cond_list.cellWidget(row, 0).currentText(), self.cond_list.cellWidget(row, 1).currentIndex(), self._get_value_contents(self.cond_list.cellWidget(row, 2))) for row in range(self.cond_list.rowCount()) ] if self.update_on_change and ( self.last_output_conditions is None or self.last_output_conditions != self.conditions): self.commit() except AttributeError: # Attribute error appears if the signal is triggered when the # controls are being constructed pass def _values_to_floats(self, attr, values): if not len(values): return values if not all(values): return None if isinstance(attr, TimeVariable): parse = lambda x: (attr.parse(x), True) else: parse = QLocale().toDouble try: floats, ok = zip(*[parse(v) for v in values]) if not all(ok): raise ValueError('Some values could not be parsed as floats' 'in the current locale: {}'.format(values)) except TypeError: floats = values # values already floats assert all(isinstance(v, float) for v in floats) return floats def commit(self): matching_output = self.data non_matching_output = None annotated_output = None self.Error.clear() if self.data: domain = self.data.domain conditions = [] for attr_name, oper_idx, values in self.conditions: attr_index = domain.index(attr_name) attr = domain[attr_index] operators = self.Operators[type(attr)] opertype, _ = operators[oper_idx] if attr.is_continuous: try: floats = self._values_to_floats(attr, values) except ValueError as e: self.Error.parsing_error(e.args[0]) return if floats is None: continue filter = data_filter.FilterContinuous( attr_index, opertype, *floats) elif attr.is_string: filter = data_filter.FilterString( attr_index, opertype, *[str(v) for v in values]) else: if opertype == FilterDiscreteType.IsDefined: f_values = None else: if not values or not values[0]: continue values = [attr.values[i - 1] for i in values] if opertype == FilterDiscreteType.Equal: f_values = {values[0]} elif opertype == FilterDiscreteType.NotEqual: f_values = set(attr.values) f_values.remove(values[0]) elif opertype == FilterDiscreteType.In: f_values = set(values) else: raise ValueError("invalid operand") filter = data_filter.FilterDiscrete(attr_index, f_values) conditions.append(filter) if conditions: self.filters = data_filter.Values(conditions) matching_output = self.filters(self.data) self.filters.negate = True non_matching_output = self.filters(self.data) row_sel = np.in1d(self.data.ids, matching_output.ids) annotated_output = create_annotated_table(self.data, row_sel) # if hasattr(self.data, "name"): # matching_output.name = self.data.name # non_matching_output.name = self.data.name purge_attrs = self.purge_attributes purge_classes = self.purge_classes if (purge_attrs or purge_classes) and \ not isinstance(self.data, SqlTable): attr_flags = sum([ Remove.RemoveConstant * purge_attrs, Remove.RemoveUnusedValues * purge_attrs ]) class_flags = sum([ Remove.RemoveConstant * purge_classes, Remove.RemoveUnusedValues * purge_classes ]) # same settings used for attributes and meta features remover = Remove(attr_flags, class_flags, attr_flags) matching_output = remover(matching_output) non_matching_output = remover(non_matching_output) annotated_output = remover(annotated_output) if matching_output is not None and not len(matching_output): matching_output = None if non_matching_output is not None and not len(non_matching_output): non_matching_output = None if annotated_output is not None and not len(annotated_output): annotated_output = None self.Outputs.matching_data.send(matching_output) self.Outputs.unmatched_data.send(non_matching_output) self.Outputs.annotated_data.send(annotated_output) self.match_desc = report.describe_data_brief(matching_output) self.nonmatch_desc = report.describe_data_brief(non_matching_output) self.update_info(matching_output, self.data_out_rows, "Out: ") def update_info(self, data, lab1, label): def sp(s, capitalize=True): return s and s or ("No" if capitalize else "no"), "s" * (s != 1) if data is None: lab1.setText("") else: lab1.setText( label + "~%s row%s, %s variable%s" % (sp(data.approx_len()) + sp(len(data.domain.variables) + len(data.domain.metas)))) def send_report(self): if not self.data: self.report_paragraph("No data.") return pdesc = None describe_domain = False for d in (self.data_desc, self.match_desc, self.nonmatch_desc): if not d or not d["Data instances"]: continue ndesc = d.copy() del ndesc["Data instances"] if pdesc is not None and pdesc != ndesc: describe_domain = True pdesc = ndesc conditions = [] domain = self.data.domain for attr_name, oper, values in self.conditions: attr_index = domain.index(attr_name) attr = domain[attr_index] names = self.operator_names[type(attr)] name = names[oper] if oper == len(names) - 1: conditions.append("{} {}".format(attr, name)) elif attr.is_discrete: if name == "is one of": if len(values) == 1: conditions.append("{} is {}".format( attr, attr.values[values[0] - 1])) elif len(values) > 1: conditions.append("{} is {} or {}".format( attr, ", ".join(attr.values[v - 1] for v in values[:-1]), attr.values[values[-1] - 1])) else: if not (values and values[0]): continue value = values[0] - 1 conditions.append("{} {} {}".format( attr, name, attr.values[value])) else: if len(values) == 1: conditions.append("{} {} {}".format(attr, name, *values)) else: conditions.append("{} {} {} and {}".format( attr, name, *values)) items = OrderedDict() if describe_domain: items.update(self.data_desc) else: items["Instances"] = self.data_desc["Data instances"] items["Condition"] = " AND ".join(conditions) or "no conditions" self.report_items("Data", items) if describe_domain: self.report_items("Matching data", self.match_desc) self.report_items("Non-matching data", self.nonmatch_desc) else: match_inst = \ bool(self.match_desc) and \ self.match_desc["Data instances"] nonmatch_inst = \ bool(self.nonmatch_desc) and \ self.nonmatch_desc["Data instances"] self.report_items( "Output", (("Matching data", "{} instances".format(match_inst) if match_inst else "None"), ("Non-matching data", nonmatch_inst > 0 and "{} instances".format(nonmatch_inst))))
class OWSelectRows(widget.OWWidget): name = "Select Rows" id = "Orange.widgets.data.file" description = "Select rows from the data based on values of variables." icon = "icons/SelectRows.svg" priority = 100 category = "Data" keywords = ["filter"] class Inputs: data = Input("Data", Table) class Outputs: matching_data = Output("Matching Data", Table, default=True) unmatched_data = Output("Unmatched Data", Table) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) want_main_area = False settingsHandler = SelectRowsContextHandler() conditions = ContextSetting([]) update_on_change = Setting(True) purge_attributes = Setting(False, schema_only=True) purge_classes = Setting(False, schema_only=True) auto_commit = Setting(True) settings_version = 2 Operators = { ContinuousVariable: [ (FilterContinuous.Equal, "equals"), (FilterContinuous.NotEqual, "is not"), (FilterContinuous.Less, "is below"), (FilterContinuous.LessEqual, "is at most"), (FilterContinuous.Greater, "is greater than"), (FilterContinuous.GreaterEqual, "is at least"), (FilterContinuous.Between, "is between"), (FilterContinuous.Outside, "is outside"), (FilterContinuous.IsDefined, "is defined"), ], DiscreteVariable: [(FilterDiscreteType.Equal, "is"), (FilterDiscreteType.NotEqual, "is not"), (FilterDiscreteType.In, "is one of"), (FilterDiscreteType.IsDefined, "is defined")], StringVariable: [ (FilterString.Equal, "equals"), (FilterString.NotEqual, "is not"), (FilterString.Less, "is before"), (FilterString.LessEqual, "is equal or before"), (FilterString.Greater, "is after"), (FilterString.GreaterEqual, "is equal or after"), (FilterString.Between, "is between"), (FilterString.Outside, "is outside"), (FilterString.Contains, "contains"), (FilterString.StartsWith, "begins with"), (FilterString.EndsWith, "ends with"), (FilterString.IsDefined, "is defined"), ] } Operators[TimeVariable] = Operators[ContinuousVariable] AllTypes = {} for _all_name, _all_type, _all_ops in (("All variables", 0, [ (None, "are defined") ]), ("All numeric variables", 2, [ (v, _plural(t)) for v, t in Operators[ContinuousVariable] ]), ("All string variables", 3, [(v, _plural(t)) for v, t in Operators[StringVariable]])): Operators[_all_name] = _all_ops AllTypes[_all_name] = _all_type operator_names = { vtype: [name for _, name in filters] for vtype, filters in Operators.items() } class Error(widget.OWWidget.Error): parsing_error = Msg("{}") def __init__(self): super().__init__() self.old_purge_classes = True self.conditions = [] self.last_output_conditions = None self.data = None self.data_desc = self.match_desc = self.nonmatch_desc = None self.variable_model = DomainModel([ list(self.AllTypes), DomainModel.Separator, DomainModel.CLASSES, DomainModel.ATTRIBUTES, DomainModel.METAS ]) box = gui.vBox(self.controlArea, 'Conditions', stretch=100) self.cond_list = QTableWidget(box, showGrid=False, selectionMode=QTableWidget.NoSelection) box.layout().addWidget(self.cond_list) self.cond_list.setColumnCount(4) self.cond_list.setRowCount(0) self.cond_list.verticalHeader().hide() self.cond_list.horizontalHeader().hide() for i in range(3): self.cond_list.horizontalHeader().setSectionResizeMode( i, QHeaderView.Stretch) self.cond_list.horizontalHeader().resizeSection(3, 30) self.cond_list.viewport().setBackgroundRole(QPalette.Window) box2 = gui.hBox(box) gui.rubber(box2) self.add_button = gui.button(box2, self, "Add Condition", callback=self.add_row) self.add_all_button = gui.button(box2, self, "Add All Variables", callback=self.add_all) self.remove_all_button = gui.button(box2, self, "Remove All", callback=self.remove_all) gui.rubber(box2) boxes = gui.widgetBox(self.controlArea, orientation=QHBoxLayout()) layout = boxes.layout() box_setting = gui.vBox(boxes, addToLayout=False, box=True) self.cb_pa = gui.checkBox(box_setting, self, "purge_attributes", "Remove unused features", callback=self.conditions_changed) gui.separator(box_setting, height=1) self.cb_pc = gui.checkBox(box_setting, self, "purge_classes", "Remove unused classes", callback=self.conditions_changed) layout.addWidget(box_setting, 1) self.report_button.setFixedWidth(120) gui.rubber(self.buttonsArea.layout()) layout.addWidget(self.buttonsArea) acbox = gui.auto_send(None, self, "auto_commit") layout.addWidget(acbox, 1) layout.setAlignment(acbox, Qt.AlignBottom) self.info.set_input_summary(self.info.NoInput) self.info.set_output_summary(self.info.NoOutput) self.set_data(None) self.resize(600, 400) def add_row(self, attr=None, condition_type=None, condition_value=None): model = self.cond_list.model() row = model.rowCount() model.insertRow(row) attr_combo = ComboBoxSearch( minimumContentsLength=12, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon) attr_combo.setModel(self.variable_model) attr_combo.row = row attr_combo.setCurrentIndex( self.variable_model.indexOf(attr) if attr else len(self.AllTypes) + 1) self.cond_list.setCellWidget(row, 0, attr_combo) index = QPersistentModelIndex(model.index(row, 3)) temp_button = QPushButton( '×', self, flat=True, styleSheet='* {font-size: 16pt; color: silver}' '*:hover {color: black}') temp_button.clicked.connect(lambda: self.remove_one(index.row())) self.cond_list.setCellWidget(row, 3, temp_button) self.remove_all_button.setDisabled(False) self.set_new_operators(attr_combo, attr is not None, condition_type, condition_value) attr_combo.currentIndexChanged.connect( lambda _: self.set_new_operators(attr_combo, False)) self.cond_list.resizeRowToContents(row) def add_all(self): if self.cond_list.rowCount(): Mb = QMessageBox if Mb.question( self, "Remove existing filters", "This will replace the existing filters with " "filters for all variables.", Mb.Ok | Mb.Cancel) != Mb.Ok: return self.remove_all() for attr in self.variable_model[len(self.AllTypes) + 1:]: self.add_row(attr) self.conditions_changed() def remove_one(self, rownum): self.remove_one_row(rownum) self.conditions_changed() def remove_all(self): self.remove_all_rows() self.conditions_changed() def remove_one_row(self, rownum): self.cond_list.removeRow(rownum) if self.cond_list.model().rowCount() == 0: self.remove_all_button.setDisabled(True) def remove_all_rows(self): # Disconnect signals to avoid stray emits when changing variable_model for row in range(self.cond_list.rowCount()): for col in (0, 1): widg = self.cond_list.cellWidget(row, col) if widg: widg.currentIndexChanged.disconnect() self.cond_list.clear() self.cond_list.setRowCount(0) self.remove_all_button.setDisabled(True) def set_new_operators(self, attr_combo, adding_all, selected_index=None, selected_values=None): old_combo = self.cond_list.cellWidget(attr_combo.row, 1) prev_text = old_combo.currentText() if old_combo else "" oper_combo = QComboBox() oper_combo.row = attr_combo.row oper_combo.attr_combo = attr_combo attr_name = attr_combo.currentText() if attr_name in self.AllTypes: oper_combo.addItems(self.operator_names[attr_name]) else: var = self.data.domain[attr_name] oper_combo.addItems(self.operator_names[type(var)]) if selected_index is None: selected_index = oper_combo.findText(prev_text) if selected_index == -1: selected_index = 0 oper_combo.setCurrentIndex(selected_index) self.cond_list.setCellWidget(oper_combo.row, 1, oper_combo) self.set_new_values(oper_combo, adding_all, selected_values) oper_combo.currentIndexChanged.connect( lambda _: self.set_new_values(oper_combo, False)) @staticmethod def _get_lineedit_contents(box): contents = [] for child in getattr(box, "controls", [box]): if isinstance(child, QLineEdit): contents.append(child.text()) elif isinstance(child, DateTimeWidget): if child.format == (0, 1): contents.append(child.time()) elif child.format == (1, 0): contents.append(child.date()) elif child.format == (1, 1): contents.append(child.dateTime()) return contents @staticmethod def _get_value_contents(box): cont = [] names = [] for child in getattr(box, "controls", [box]): if isinstance(child, QLineEdit): cont.append(child.text()) elif isinstance(child, QComboBox): cont.append(child.currentIndex()) elif isinstance(child, QToolButton): if child.popup is not None: model = child.popup.list_view.model() for row in range(model.rowCount()): item = model.item(row) if item.checkState(): cont.append(row + 1) names.append(item.text()) child.desc_text = ', '.join(names) child.set_text() elif isinstance(child, DateTimeWidget): if child.format == (0, 1): cont.append(child.time()) elif child.format == (1, 0): cont.append(child.date()) elif child.format == (1, 1): cont.append(child.dateTime()) elif isinstance(child, QLabel) or child is None: pass else: raise TypeError('Type %s not supported.' % type(child)) return tuple(cont) class QDoubleValidatorEmpty(QDoubleValidator): def validate(self, input_, pos): if not input_: return QDoubleValidator.Acceptable, input_, pos if self.locale().groupSeparator() in input_: return QDoubleValidator.Invalid, input_, pos return super().validate(input_, pos) def set_new_values(self, oper_combo, adding_all, selected_values=None): # def remove_children(): # for child in box.children()[1:]: # box.layout().removeWidget(child) # child.setParent(None) def add_textual(contents): le = gui.lineEdit(box, self, None, sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) if contents: le.setText(contents) le.setAlignment(Qt.AlignRight) le.editingFinished.connect(self.conditions_changed) return le def add_numeric(contents): le = add_textual(contents) le.setValidator(OWSelectRows.QDoubleValidatorEmpty()) return le box = self.cond_list.cellWidget(oper_combo.row, 2) lc = ["", ""] oper = oper_combo.currentIndex() attr_name = oper_combo.attr_combo.currentText() if attr_name in self.AllTypes: vtype = self.AllTypes[attr_name] var = None else: var = self.data.domain[attr_name] var_idx = self.data.domain.index(attr_name) vtype = vartype(var) if selected_values is not None: lc = list(selected_values) + ["", ""] lc = [str(x) if vtype != 4 else x for x in lc[:2]] if box and vtype == box.var_type: lc = self._get_lineedit_contents(box) + lc if oper_combo.currentText().endswith(" defined"): label = QLabel() label.var_type = vtype self.cond_list.setCellWidget(oper_combo.row, 2, label) elif var is not None and var.is_discrete: if oper_combo.currentText().endswith(" one of"): if selected_values: lc = list(selected_values) button = DropDownToolButton(self, var, lc) button.var_type = vtype self.cond_list.setCellWidget(oper_combo.row, 2, button) else: combo = ComboBoxSearch() combo.addItems(("", ) + var.values) if lc[0]: combo.setCurrentIndex(int(lc[0])) else: combo.setCurrentIndex(0) combo.var_type = vartype(var) self.cond_list.setCellWidget(oper_combo.row, 2, combo) combo.currentIndexChanged.connect(self.conditions_changed) else: box = gui.hBox(self, addToLayout=False) box.var_type = vtype self.cond_list.setCellWidget(oper_combo.row, 2, box) if vtype == 2: # continuous: box.controls = [add_numeric(lc[0])] if oper > 5: gui.widgetLabel(box, " and ") box.controls.append(add_numeric(lc[1])) elif vtype == 3: # string: box.controls = [add_textual(lc[0])] if oper in [6, 7]: gui.widgetLabel(box, " and ") box.controls.append(add_textual(lc[1])) elif vtype == 4: # time: def invalidate_datetime(): if w_: if w.dateTime() > w_.dateTime(): w_.setDateTime(w.dateTime()) if w.format == (1, 1): w.calendarWidget.timeedit.setTime(w.time()) w_.calendarWidget.timeedit.setTime(w_.time()) elif w.format == (1, 1): w.calendarWidget.timeedit.setTime(w.time()) def datetime_changed(): self.conditions_changed() invalidate_datetime() datetime_format = (var.have_date, var.have_time) column = self.data.get_column_view(var_idx)[0] w = DateTimeWidget(self, column, datetime_format) w.set_datetime(lc[0]) box.controls = [w] box.layout().addWidget(w) w.dateTimeChanged.connect(datetime_changed) if oper > 5: gui.widgetLabel(box, " and ") w_ = DateTimeWidget(self, column, datetime_format) w_.set_datetime(lc[1]) box.layout().addWidget(w_) box.controls.append(w_) invalidate_datetime() w_.dateTimeChanged.connect(datetime_changed) else: w_ = None else: box.controls = [] if not adding_all: self.conditions_changed() @Inputs.data def set_data(self, data): self.closeContext() self.data = data self.cb_pa.setEnabled(not isinstance(data, SqlTable)) self.cb_pc.setEnabled(not isinstance(data, SqlTable)) self.remove_all_rows() self.add_button.setDisabled(data is None) self.add_all_button.setDisabled( data is None or len(data.domain.variables) + len(data.domain.metas) > 100) if not data: self.info.set_input_summary(self.info.NoInput) self.data_desc = None self.variable_model.set_domain(None) self.commit() return self.data_desc = report.describe_data_brief(data) self.variable_model.set_domain(data.domain) self.conditions = [] self.openContext(data) for attr, cond_type, cond_value in self.conditions: if attr in self.variable_model: self.add_row(attr, cond_type, cond_value) if not self.cond_list.model().rowCount(): self.add_row() self.info.set_input_summary(data.approx_len(), format_summary_details(data)) self.unconditional_commit() def conditions_changed(self): try: cells_by_rows = ([ self.cond_list.cellWidget(row, col) for col in range(3) ] for row in range(self.cond_list.rowCount())) self.conditions = [ (var_cell.currentData(gui.TableVariable) or var_cell.currentText(), oper_cell.currentIndex(), self._get_value_contents(val_cell)) for var_cell, oper_cell, val_cell in cells_by_rows ] if self.update_on_change and ( self.last_output_conditions is None or self.last_output_conditions != self.conditions): self.commit() except AttributeError: # Attribute error appears if the signal is triggered when the # controls are being constructed pass @staticmethod def _values_to_floats(attr, values): if len(values) == 0: return values if not all(values): return None if isinstance(attr, TimeVariable): values = (value.toString(format=Qt.ISODate) for value in values) parse = lambda x: (attr.parse(x), True) else: parse = QLocale().toDouble try: floats, ok = zip(*[parse(v) for v in values]) if not all(ok): raise ValueError('Some values could not be parsed as floats' 'in the current locale: {}'.format(values)) except TypeError: floats = values # values already floats assert all(isinstance(v, float) for v in floats) return floats def commit(self): matching_output = self.data non_matching_output = None annotated_output = None self.Error.clear() if self.data: domain = self.data.domain conditions = [] for attr_name, oper_idx, values in self.conditions: if attr_name in self.AllTypes: attr_index = attr = None attr_type = self.AllTypes[attr_name] operators = self.Operators[attr_name] else: attr_index = domain.index(attr_name) attr = domain[attr_index] attr_type = vartype(attr) operators = self.Operators[type(attr)] opertype, _ = operators[oper_idx] if attr_type == 0: filt = data_filter.IsDefined() elif attr_type in (2, 4): # continuous, time try: floats = self._values_to_floats(attr, values) except ValueError as e: self.Error.parsing_error(e.args[0]) return if floats is None: continue filt = data_filter.FilterContinuous( attr_index, opertype, *floats) elif attr_type == 3: # string filt = data_filter.FilterString(attr_index, opertype, *[str(v) for v in values]) else: if opertype == FilterDiscreteType.IsDefined: f_values = None else: if not values or not values[0]: continue values = [attr.values[i - 1] for i in values] if opertype == FilterDiscreteType.Equal: f_values = {values[0]} elif opertype == FilterDiscreteType.NotEqual: f_values = set(attr.values) f_values.remove(values[0]) elif opertype == FilterDiscreteType.In: f_values = set(values) else: raise ValueError("invalid operand") filt = data_filter.FilterDiscrete(attr_index, f_values) conditions.append(filt) if conditions: filters = data_filter.Values(conditions) matching_output = filters(self.data) filters.negate = True non_matching_output = filters(self.data) row_sel = np.in1d(self.data.ids, matching_output.ids) annotated_output = create_annotated_table(self.data, row_sel) # if hasattr(self.data, "name"): # matching_output.name = self.data.name # non_matching_output.name = self.data.name purge_attrs = self.purge_attributes purge_classes = self.purge_classes if (purge_attrs or purge_classes) and \ not isinstance(self.data, SqlTable): attr_flags = sum([ Remove.RemoveConstant * purge_attrs, Remove.RemoveUnusedValues * purge_attrs ]) class_flags = sum([ Remove.RemoveConstant * purge_classes, Remove.RemoveUnusedValues * purge_classes ]) # same settings used for attributes and meta features remover = Remove(attr_flags, class_flags, attr_flags) matching_output = remover(matching_output) non_matching_output = remover(non_matching_output) annotated_output = remover(annotated_output) if not matching_output: matching_output = None if not non_matching_output: non_matching_output = None if not annotated_output: annotated_output = None self.Outputs.matching_data.send(matching_output) self.Outputs.unmatched_data.send(non_matching_output) self.Outputs.annotated_data.send(annotated_output) self.match_desc = report.describe_data_brief(matching_output) self.nonmatch_desc = report.describe_data_brief(non_matching_output) summary = matching_output.approx_len() if matching_output else \ self.info.NoOutput details = format_summary_details( matching_output) if matching_output else "" self.info.set_output_summary(summary, details) def send_report(self): if not self.data: self.report_paragraph("No data.") return pdesc = None describe_domain = False for d in (self.data_desc, self.match_desc, self.nonmatch_desc): if not d or not d["Data instances"]: continue ndesc = d.copy() del ndesc["Data instances"] if pdesc is not None and pdesc != ndesc: describe_domain = True pdesc = ndesc conditions = [] for attr, oper, values in self.conditions: if isinstance(attr, str): attr_name = attr var_type = self.AllTypes[attr] names = self.operator_names[attr_name] else: attr_name = attr.name var_type = vartype(attr) names = self.operator_names[type(attr)] name = names[oper] if oper == len(names) - 1: conditions.append("{} {}".format(attr_name, name)) elif var_type == 1: # discrete if name == "is one of": valnames = [attr.values[v - 1] for v in values] if not valnames: continue if len(valnames) == 1: valstr = valnames[0] else: valstr = f"{', '.join(valnames[:-1])} or {valnames[-1]}" conditions.append(f"{attr} is {valstr}") elif values and values[0]: value = values[0] - 1 conditions.append(f"{attr} {name} {attr.values[value]}") elif var_type == 3: # string variable conditions.append( f"{attr} {name} {' and '.join(map(repr, values))}") elif var_type == 4: # time values = (value.toString(format=Qt.ISODate) for value in values) conditions.append(f"{attr} {name} {' and '.join(values)}") elif all(x for x in values): # numeric variable conditions.append(f"{attr} {name} {' and '.join(values)}") items = OrderedDict() if describe_domain: items.update(self.data_desc) else: items["Instances"] = self.data_desc["Data instances"] items["Condition"] = " AND ".join(conditions) or "no conditions" self.report_items("Data", items) if describe_domain: self.report_items("Matching data", self.match_desc) self.report_items("Non-matching data", self.nonmatch_desc) else: match_inst = \ bool(self.match_desc) and \ self.match_desc["Data instances"] nonmatch_inst = \ bool(self.nonmatch_desc) and \ self.nonmatch_desc["Data instances"] self.report_items( "Output", (("Matching data", "{} instances".format(match_inst) if match_inst else "None"), ("Non-matching data", nonmatch_inst > 0 and "{} instances".format(nonmatch_inst))))
class OWSelectRows(widget.OWWidget): name = "Select Rows" id = "Orange.widgets.data.file" description = "Select rows from the data based on values of variables." icon = "icons/SelectRows.svg" priority = 100 category = "Data" keywords = ["filter"] class Inputs: data = Input("Data", Table) class Outputs: matching_data = Output("Matching Data", Table, default=True) unmatched_data = Output("Unmatched Data", Table) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) want_main_area = False settingsHandler = SelectRowsContextHandler() conditions = ContextSetting([]) update_on_change = Setting(True) purge_attributes = Setting(False, schema_only=True) purge_classes = Setting(False, schema_only=True) auto_commit = Setting(True) Operators = { ContinuousVariable: [ (FilterContinuous.Equal, "equals"), (FilterContinuous.NotEqual, "is not"), (FilterContinuous.Less, "is below"), (FilterContinuous.LessEqual, "is at most"), (FilterContinuous.Greater, "is greater than"), (FilterContinuous.GreaterEqual, "is at least"), (FilterContinuous.Between, "is between"), (FilterContinuous.Outside, "is outside"), (FilterContinuous.IsDefined, "is defined"), ], DiscreteVariable: [ (FilterDiscreteType.Equal, "is"), (FilterDiscreteType.NotEqual, "is not"), (FilterDiscreteType.In, "is one of"), (FilterDiscreteType.IsDefined, "is defined") ], StringVariable: [ (FilterString.Equal, "equals"), (FilterString.NotEqual, "is not"), (FilterString.Less, "is before"), (FilterString.LessEqual, "is equal or before"), (FilterString.Greater, "is after"), (FilterString.GreaterEqual, "is equal or after"), (FilterString.Between, "is between"), (FilterString.Outside, "is outside"), (FilterString.Contains, "contains"), (FilterString.StartsWith, "begins with"), (FilterString.EndsWith, "ends with"), (FilterString.IsDefined, "is defined"), ] } Operators[TimeVariable] = Operators[ContinuousVariable] operator_names = {vtype: [name for _, name in filters] for vtype, filters in Operators.items()} class Error(widget.OWWidget.Error): parsing_error = Msg("{}") def __init__(self): super().__init__() self.old_purge_classes = True self.conditions = [] self.last_output_conditions = None self.data = None self.data_desc = self.match_desc = self.nonmatch_desc = None box = gui.vBox(self.controlArea, 'Conditions', stretch=100) self.cond_list = QTableWidget( box, showGrid=False, selectionMode=QTableWidget.NoSelection) box.layout().addWidget(self.cond_list) self.cond_list.setColumnCount(4) self.cond_list.setRowCount(0) self.cond_list.verticalHeader().hide() self.cond_list.horizontalHeader().hide() for i in range(3): self.cond_list.horizontalHeader().setSectionResizeMode(i, QHeaderView.Stretch) self.cond_list.horizontalHeader().resizeSection(3, 30) self.cond_list.viewport().setBackgroundRole(QPalette.Window) box2 = gui.hBox(box) gui.rubber(box2) self.add_button = gui.button( box2, self, "Add Condition", callback=self.add_row) self.add_all_button = gui.button( box2, self, "Add All Variables", callback=self.add_all) self.remove_all_button = gui.button( box2, self, "Remove All", callback=self.remove_all) gui.rubber(box2) boxes = gui.widgetBox(self.controlArea, orientation=QGridLayout()) layout = boxes.layout() layout.setColumnStretch(0, 1) layout.setColumnStretch(1, 1) box_data = gui.vBox(boxes, 'Data', addToLayout=False) self.data_in_variables = gui.widgetLabel(box_data, " ") self.data_out_rows = gui.widgetLabel(box_data, " ") layout.addWidget(box_data, 0, 0) box_setting = gui.vBox(boxes, 'Purging', addToLayout=False) self.cb_pa = gui.checkBox( box_setting, self, "purge_attributes", "Remove unused features", callback=self.conditions_changed) gui.separator(box_setting, height=1) self.cb_pc = gui.checkBox( box_setting, self, "purge_classes", "Remove unused classes", callback=self.conditions_changed) layout.addWidget(box_setting, 0, 1) self.report_button.setFixedWidth(120) gui.rubber(self.buttonsArea.layout()) layout.addWidget(self.buttonsArea, 1, 0) acbox = gui.auto_commit( None, self, "auto_commit", label="Send", orientation=Qt.Horizontal, checkbox_label="Send automatically") layout.addWidget(acbox, 1, 1) self.set_data(None) self.resize(600, 400) def add_row(self, attr=None, condition_type=None, condition_value=None): model = self.cond_list.model() row = model.rowCount() model.insertRow(row) attr_combo = gui.OrangeComboBox( minimumContentsLength=12, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon) attr_combo.row = row for var in self._visible_variables(self.data.domain): attr_combo.addItem(*gui.attributeItem(var)) attr_combo.setCurrentIndex(attr or 0) self.cond_list.setCellWidget(row, 0, attr_combo) index = QPersistentModelIndex(model.index(row, 3)) temp_button = QPushButton('×', self, flat=True, styleSheet='* {font-size: 16pt; color: silver}' '*:hover {color: black}') temp_button.clicked.connect(lambda: self.remove_one(index.row())) self.cond_list.setCellWidget(row, 3, temp_button) self.remove_all_button.setDisabled(False) self.set_new_operators(attr_combo, attr is not None, condition_type, condition_value) attr_combo.currentIndexChanged.connect( lambda _: self.set_new_operators(attr_combo, False)) self.cond_list.resizeRowToContents(row) @staticmethod def _visible_variables(domain): """Generate variables in order they should be presented in in combos.""" return filter_visible(chain(domain.class_vars, domain.metas, domain.attributes)) def add_all(self): if self.cond_list.rowCount(): Mb = QMessageBox if Mb.question( self, "Remove existing filters", "This will replace the existing filters with " "filters for all variables.", Mb.Ok | Mb.Cancel) != Mb.Ok: return self.remove_all() domain = self.data.domain for i in range(len(domain.variables) + len(domain.metas)): self.add_row(i) def remove_one(self, rownum): self.remove_one_row(rownum) self.conditions_changed() def remove_all(self): self.remove_all_rows() self.conditions_changed() def remove_one_row(self, rownum): self.cond_list.removeRow(rownum) if self.cond_list.model().rowCount() == 0: self.remove_all_button.setDisabled(True) def remove_all_rows(self): self.cond_list.clear() self.cond_list.setRowCount(0) self.remove_all_button.setDisabled(True) def set_new_operators(self, attr_combo, adding_all, selected_index=None, selected_values=None): oper_combo = QComboBox() oper_combo.row = attr_combo.row oper_combo.attr_combo = attr_combo var = self.data.domain[attr_combo.currentText()] oper_combo.addItems(self.operator_names[type(var)]) oper_combo.setCurrentIndex(selected_index or 0) self.cond_list.setCellWidget(oper_combo.row, 1, oper_combo) self.set_new_values(oper_combo, adding_all, selected_values) oper_combo.currentIndexChanged.connect( lambda _: self.set_new_values(oper_combo, False)) @staticmethod def _get_lineedit_contents(box): return [child.text() for child in getattr(box, "controls", [box]) if isinstance(child, QLineEdit)] @staticmethod def _get_value_contents(box): cont = [] names = [] for child in getattr(box, "controls", [box]): if isinstance(child, QLineEdit): cont.append(child.text()) elif isinstance(child, QComboBox): cont.append(child.currentIndex()) elif isinstance(child, QToolButton): if child.popup is not None: model = child.popup.list_view.model() for row in range(model.rowCount()): item = model.item(row) if item.checkState(): cont.append(row + 1) names.append(item.text()) child.desc_text = ', '.join(names) child.set_text() elif isinstance(child, QLabel) or child is None: pass else: raise TypeError('Type %s not supported.' % type(child)) return tuple(cont) class QDoubleValidatorEmpty(QDoubleValidator): def validate(self, input_, pos): if not input_: return QDoubleValidator.Acceptable, input_, pos if self.locale().groupSeparator() in input_: return QDoubleValidator.Invalid, input_, pos return super().validate(input_, pos) def set_new_values(self, oper_combo, adding_all, selected_values=None): # def remove_children(): # for child in box.children()[1:]: # box.layout().removeWidget(child) # child.setParent(None) def add_textual(contents): le = gui.lineEdit(box, self, None, sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) if contents: le.setText(contents) le.setAlignment(Qt.AlignRight) le.editingFinished.connect(self.conditions_changed) return le def add_numeric(contents): le = add_textual(contents) le.setValidator(OWSelectRows.QDoubleValidatorEmpty()) return le def add_datetime(contents): le = add_textual(contents) le.setValidator(QRegExpValidator(QRegExp(TimeVariable.REGEX))) return le var = self.data.domain[oper_combo.attr_combo.currentText()] box = self.cond_list.cellWidget(oper_combo.row, 2) if selected_values is not None: lc = list(selected_values) + ["", ""] lc = [str(x) for x in lc[:2]] else: lc = ["", ""] if box and vartype(var) == box.var_type: lc = self._get_lineedit_contents(box) + lc oper = oper_combo.currentIndex() if oper_combo.currentText() == "is defined": label = QLabel() label.var_type = vartype(var) self.cond_list.setCellWidget(oper_combo.row, 2, label) elif var.is_discrete: if oper_combo.currentText() == "is one of": if selected_values: lc = [x for x in list(selected_values)] button = DropDownToolButton(self, var, lc) button.var_type = vartype(var) self.cond_list.setCellWidget(oper_combo.row, 2, button) else: combo = QComboBox() combo.addItems([""] + var.values) if lc[0]: combo.setCurrentIndex(int(lc[0])) else: combo.setCurrentIndex(0) combo.var_type = vartype(var) self.cond_list.setCellWidget(oper_combo.row, 2, combo) combo.currentIndexChanged.connect(self.conditions_changed) else: box = gui.hBox(self, addToLayout=False) box.var_type = vartype(var) self.cond_list.setCellWidget(oper_combo.row, 2, box) if var.is_continuous: validator = add_datetime if isinstance(var, TimeVariable) else add_numeric box.controls = [validator(lc[0])] if oper > 5: gui.widgetLabel(box, " and ") box.controls.append(validator(lc[1])) elif var.is_string: box.controls = [add_textual(lc[0])] if oper in [6, 7]: gui.widgetLabel(box, " and ") box.controls.append(add_textual(lc[1])) else: box.controls = [] if not adding_all: self.conditions_changed() @Inputs.data def set_data(self, data): self.closeContext() self.data = data self.cb_pa.setEnabled(not isinstance(data, SqlTable)) self.cb_pc.setEnabled(not isinstance(data, SqlTable)) self.remove_all_rows() self.add_button.setDisabled(data is None) self.add_all_button.setDisabled( data is None or len(data.domain.variables) + len(data.domain.metas) > 100) if not data: self.data_desc = None self.commit() return self.data_desc = report.describe_data_brief(data) self.conditions = [] try: self.openContext(data) except Exception: pass variables = list(self._visible_variables(self.data.domain)) varnames = [v.name for v in variables] if self.conditions: for attr, cond_type, cond_value in self.conditions: if attr in varnames: self.add_row(varnames.index(attr), cond_type, cond_value) elif variables: self.add_row() self.update_info(data, self.data_in_variables, "In: ") self.unconditional_commit() def conditions_changed(self): try: self.conditions = [] self.conditions = [ (self.cond_list.cellWidget(row, 0).currentText(), self.cond_list.cellWidget(row, 1).currentIndex(), self._get_value_contents(self.cond_list.cellWidget(row, 2))) for row in range(self.cond_list.rowCount())] if self.update_on_change and ( self.last_output_conditions is None or self.last_output_conditions != self.conditions): self.commit() except AttributeError: # Attribute error appears if the signal is triggered when the # controls are being constructed pass def _values_to_floats(self, attr, values): if not len(values): return values if not all(values): return None if isinstance(attr, TimeVariable): parse = lambda x: (attr.parse(x), True) else: parse = QLocale().toDouble try: floats, ok = zip(*[parse(v) for v in values]) if not all(ok): raise ValueError('Some values could not be parsed as floats' 'in the current locale: {}'.format(values)) except TypeError: floats = values # values already floats assert all(isinstance(v, float) for v in floats) return floats def commit(self): matching_output = self.data non_matching_output = None annotated_output = None self.Error.clear() if self.data: domain = self.data.domain conditions = [] for attr_name, oper_idx, values in self.conditions: attr_index = domain.index(attr_name) attr = domain[attr_index] operators = self.Operators[type(attr)] opertype, _ = operators[oper_idx] if attr.is_continuous: try: floats = self._values_to_floats(attr, values) except ValueError as e: self.Error.parsing_error(e.args[0]) return if floats is None: continue filter = data_filter.FilterContinuous( attr_index, opertype, *floats) elif attr.is_string: filter = data_filter.FilterString( attr_index, opertype, *[str(v) for v in values]) else: if opertype == FilterDiscreteType.IsDefined: f_values = None else: if not values or not values[0]: continue values = [attr.values[i-1] for i in values] if opertype == FilterDiscreteType.Equal: f_values = {values[0]} elif opertype == FilterDiscreteType.NotEqual: f_values = set(attr.values) f_values.remove(values[0]) elif opertype == FilterDiscreteType.In: f_values = set(values) else: raise ValueError("invalid operand") filter = data_filter.FilterDiscrete(attr_index, f_values) conditions.append(filter) if conditions: self.filters = data_filter.Values(conditions) matching_output = self.filters(self.data) self.filters.negate = True non_matching_output = self.filters(self.data) row_sel = np.in1d(self.data.ids, matching_output.ids) annotated_output = create_annotated_table(self.data, row_sel) # if hasattr(self.data, "name"): # matching_output.name = self.data.name # non_matching_output.name = self.data.name purge_attrs = self.purge_attributes purge_classes = self.purge_classes if (purge_attrs or purge_classes) and \ not isinstance(self.data, SqlTable): attr_flags = sum([Remove.RemoveConstant * purge_attrs, Remove.RemoveUnusedValues * purge_attrs]) class_flags = sum([Remove.RemoveConstant * purge_classes, Remove.RemoveUnusedValues * purge_classes]) # same settings used for attributes and meta features remover = Remove(attr_flags, class_flags, attr_flags) matching_output = remover(matching_output) non_matching_output = remover(non_matching_output) annotated_output = remover(annotated_output) if matching_output is not None and not len(matching_output): matching_output = None if non_matching_output is not None and not len(non_matching_output): non_matching_output = None if annotated_output is not None and not len(annotated_output): annotated_output = None self.Outputs.matching_data.send(matching_output) self.Outputs.unmatched_data.send(non_matching_output) self.Outputs.annotated_data.send(annotated_output) self.match_desc = report.describe_data_brief(matching_output) self.nonmatch_desc = report.describe_data_brief(non_matching_output) self.update_info(matching_output, self.data_out_rows, "Out: ") def update_info(self, data, lab1, label): def sp(s, capitalize=True): return s and s or ("No" if capitalize else "no"), "s" * (s != 1) if data is None: lab1.setText("") else: lab1.setText(label + "~%s row%s, %s variable%s" % (sp(data.approx_len()) + sp(len(data.domain.variables) + len(data.domain.metas))) ) def send_report(self): if not self.data: self.report_paragraph("No data.") return pdesc = None describe_domain = False for d in (self.data_desc, self.match_desc, self.nonmatch_desc): if not d or not d["Data instances"]: continue ndesc = d.copy() del ndesc["Data instances"] if pdesc is not None and pdesc != ndesc: describe_domain = True pdesc = ndesc conditions = [] domain = self.data.domain for attr_name, oper, values in self.conditions: attr_index = domain.index(attr_name) attr = domain[attr_index] names = self.operator_names[type(attr)] name = names[oper] if oper == len(names) - 1: conditions.append("{} {}".format(attr, name)) elif attr.is_discrete: if name == "is one of": if len(values) == 1: conditions.append("{} is {}".format( attr, attr.values[values[0] - 1])) elif len(values) > 1: conditions.append("{} is {} or {}".format( attr, ", ".join(attr.values[v - 1] for v in values[:-1]), attr.values[values[-1] - 1])) else: if not (values and values[0]): continue value = values[0] - 1 conditions.append("{} {} {}". format(attr, name, attr.values[value])) else: if len(values) == 1: conditions.append("{} {} {}". format(attr, name, *values)) else: conditions.append("{} {} {} and {}". format(attr, name, *values)) items = OrderedDict() if describe_domain: items.update(self.data_desc) else: items["Instances"] = self.data_desc["Data instances"] items["Condition"] = " AND ".join(conditions) or "no conditions" self.report_items("Data", items) if describe_domain: self.report_items("Matching data", self.match_desc) self.report_items("Non-matching data", self.nonmatch_desc) else: match_inst = \ bool(self.match_desc) and \ self.match_desc["Data instances"] nonmatch_inst = \ bool(self.nonmatch_desc) and \ self.nonmatch_desc["Data instances"] self.report_items( "Output", (("Matching data", "{} instances".format(match_inst) if match_inst else "None"), ("Non-matching data", nonmatch_inst > 0 and "{} instances".format(nonmatch_inst))))
class OWSelectRows(widget.OWWidget): name = "Select Rows" id = "Orange.widgets.data.file" description = "Select rows from the data based on values of variables." icon = "icons/SelectRows.svg" priority = 100 category = "Data" inputs = [("Data", Table, "set_data")] outputs = [("Matching Data", Table, widget.Default), ("Unmatched Data", Table)] want_main_area = False settingsHandler = SelectRowsContextHandler() conditions = ContextSetting([]) update_on_change = Setting(True) purge_attributes = Setting(True) purge_classes = Setting(True) auto_commit = Setting(True) operator_names = { ContinuousVariable: [ "equals", "is not", "is below", "is at most", "is greater than", "is at least", "is between", "is outside", "is defined" ], DiscreteVariable: ["is", "is not", "is one of", "is defined"], StringVariable: [ "equals", "is not", "is before", "is equal or before", "is after", "is equal or after", "is between", "is outside", "contains", "begins with", "ends with", "is defined" ] } operator_names[TimeVariable] = operator_names[ContinuousVariable] def __init__(self): super().__init__() self.old_purge_classes = True self.conditions = [] self.last_output_conditions = None self.data = None self.data_desc = self.match_desc = self.nonmatch_desc = None box = gui.vBox(self.controlArea, 'Conditions', stretch=100) self.cond_list = QTableWidget(box, showGrid=False, selectionMode=QTableWidget.NoSelection) box.layout().addWidget(self.cond_list) self.cond_list.setColumnCount(3) self.cond_list.setRowCount(0) self.cond_list.verticalHeader().hide() self.cond_list.horizontalHeader().hide() self.cond_list.resizeColumnToContents(0) self.cond_list.horizontalHeader().setSectionResizeMode( QHeaderView.Stretch) self.cond_list.viewport().setBackgroundRole(QPalette.Window) box2 = gui.hBox(box) gui.rubber(box2) self.add_button = gui.button(box2, self, "Add Condition", callback=self.add_row) self.add_all_button = gui.button(box2, self, "Add All Variables", callback=self.add_all) self.remove_all_button = gui.button(box2, self, "Remove All", callback=self.remove_all) gui.rubber(box2) boxes = gui.widgetBox(self.controlArea, orientation=QGridLayout()) layout = boxes.layout() layout.setColumnStretch(0, 1) layout.setColumnStretch(1, 1) box_data = gui.vBox(boxes, 'Data', addToLayout=False) self.data_in_variables = gui.widgetLabel(box_data, " ") self.data_out_rows = gui.widgetLabel(box_data, " ") layout.addWidget(box_data, 0, 0) box_setting = gui.vBox(boxes, 'Purging', addToLayout=False) self.cb_pa = gui.checkBox(box_setting, self, "purge_attributes", "Remove unused features", callback=self.conditions_changed) gui.separator(box_setting, height=1) self.cb_pc = gui.checkBox(box_setting, self, "purge_classes", "Remove unused classes", callback=self.conditions_changed) layout.addWidget(box_setting, 0, 1) self.report_button.setFixedWidth(120) gui.rubber(self.buttonsArea.layout()) layout.addWidget(self.buttonsArea, 1, 0) acbox = gui.auto_commit(None, self, "auto_commit", label="Send", orientation=Qt.Horizontal, checkbox_label="Send automatically") layout.addWidget(acbox, 1, 1) self.set_data(None) self.resize(600, 400) def add_row(self, attr=None, condition_type=None, condition_value=None): model = self.cond_list.model() row = model.rowCount() model.insertRow(row) attr_combo = QComboBox( minimumContentsLength=12, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon) attr_combo.row = row for var in filter_visible( chain(self.data.domain.variables, self.data.domain.metas)): attr_combo.addItem(*gui.attributeItem(var)) attr_combo.setCurrentIndex(attr or 0) self.cond_list.setCellWidget(row, 0, attr_combo) self.remove_all_button.setDisabled(False) self.set_new_operators(attr_combo, attr is not None, condition_type, condition_value) attr_combo.currentIndexChanged.connect( lambda _: self.set_new_operators(attr_combo, False)) self.cond_list.resizeRowToContents(row) def add_all(self): if self.cond_list.rowCount(): Mb = QMessageBox if Mb.question( self, "Remove existing filters", "This will replace the existing filters with " "filters for all variables.", Mb.Ok | Mb.Cancel) != Mb.Ok: return self.remove_all() domain = self.data.domain for i in range(len(domain.variables) + len(domain.metas)): self.add_row(i) def remove_all(self): self.remove_all_rows() self.conditions_changed() def remove_all_rows(self): self.cond_list.clear() self.cond_list.setRowCount(0) self.remove_all_button.setDisabled(True) def set_new_operators(self, attr_combo, adding_all, selected_index=None, selected_values=None): oper_combo = QComboBox() oper_combo.row = attr_combo.row oper_combo.attr_combo = attr_combo var = self.data.domain[attr_combo.currentText()] oper_combo.addItems(self.operator_names[type(var)]) oper_combo.setCurrentIndex(selected_index or 0) self.set_new_values(oper_combo, adding_all, selected_values) self.cond_list.setCellWidget(oper_combo.row, 1, oper_combo) oper_combo.currentIndexChanged.connect( lambda _: self.set_new_values(oper_combo, False)) @staticmethod def _get_lineedit_contents(box): return [ child.text() for child in getattr(box, "controls", [box]) if isinstance(child, QLineEdit) ] @staticmethod def _get_value_contents(box): cont = [] names = [] for child in getattr(box, "controls", [box]): if isinstance(child, QLineEdit): cont.append(child.text()) elif isinstance(child, QComboBox): cont.append(child.currentIndex()) elif isinstance(child, QToolButton): if child.popup is not None: model = child.popup.list_view.model() for row in range(model.rowCount()): item = model.item(row) if item.checkState(): cont.append(row + 1) names.append(item.text()) child.desc_text = ', '.join(names) child.set_text() elif child is None: pass else: raise TypeError('Type %s not supported.' % type(child)) return tuple(cont) class QDoubleValidatorEmpty(QDoubleValidator): def validate(self, input_, pos): if not input_: return (QDoubleValidator.Acceptable, input_, pos) else: return super().validate(input_, pos) def set_new_values(self, oper_combo, adding_all, selected_values=None): # def remove_children(): # for child in box.children()[1:]: # box.layout().removeWidget(child) # child.setParent(None) def add_textual(contents): le = gui.lineEdit(box, self, None) if contents: le.setText(contents) le.setAlignment(Qt.AlignRight) le.editingFinished.connect(self.conditions_changed) return le def add_numeric(contents): le = add_textual(contents) le.setValidator(OWSelectRows.QDoubleValidatorEmpty()) return le def add_datetime(contents): le = add_textual(contents) le.setValidator(QRegExpValidator(QRegExp(TimeVariable.REGEX))) return le var = self.data.domain[oper_combo.attr_combo.currentText()] box = self.cond_list.cellWidget(oper_combo.row, 2) if selected_values is not None: lc = list(selected_values) + ["", ""] lc = [str(x) for x in lc[:2]] else: lc = ["", ""] if box and vartype(var) == box.var_type: lc = self._get_lineedit_contents(box) + lc oper = oper_combo.currentIndex() if oper == oper_combo.count() - 1: self.cond_list.removeCellWidget(oper_combo.row, 2) elif var.is_discrete: if oper_combo.currentText() == "is one of": if selected_values: lc = [x for x in list(selected_values)] button = DropDownToolButton(self, var, lc) button.var_type = vartype(var) self.cond_list.setCellWidget(oper_combo.row, 2, button) else: combo = QComboBox() combo.addItems([""] + var.values) if lc[0]: combo.setCurrentIndex(int(lc[0])) else: combo.setCurrentIndex(0) combo.var_type = vartype(var) self.cond_list.setCellWidget(oper_combo.row, 2, combo) combo.currentIndexChanged.connect(self.conditions_changed) else: box = gui.hBox(self, addToLayout=False) box.var_type = vartype(var) self.cond_list.setCellWidget(oper_combo.row, 2, box) if var.is_continuous: validator = add_datetime if isinstance( var, TimeVariable) else add_numeric box.controls = [validator(lc[0])] if oper > 5: gui.widgetLabel(box, " and ") box.controls.append(validator(lc[1])) gui.rubber(box) elif var.is_string: box.controls = [add_textual(lc[0])] if oper in [6, 7]: gui.widgetLabel(box, " and ") box.controls.append(add_textual(lc[1])) else: box.controls = [] if not adding_all: self.conditions_changed() def set_data(self, data): self.closeContext() self.data = data self.cb_pa.setEnabled(not isinstance(data, SqlTable)) self.cb_pc.setEnabled(not isinstance(data, SqlTable)) self.remove_all_rows() self.add_button.setDisabled(data is None) self.add_all_button.setDisabled( data is None or len(data.domain.variables) + len(data.domain.metas) > 100) if not data: self.data_desc = None self.commit() return self.data_desc = report.describe_data_brief(data) self.conditions = [] try: self.openContext(data) except Exception: pass if not self.conditions and len(data.domain.variables): self.add_row() self.update_info(data, self.data_in_variables, "In: ") for attr, cond_type, cond_value in self.conditions: attrs = [ a.name for a in filter_visible( chain(data.domain.variables, data.domain.metas)) ] if attr in attrs: self.add_row(attrs.index(attr), cond_type, cond_value) self.unconditional_commit() def conditions_changed(self): try: self.conditions = [] self.conditions = [ (self.cond_list.cellWidget(row, 0).currentText(), self.cond_list.cellWidget(row, 1).currentIndex(), self._get_value_contents(self.cond_list.cellWidget(row, 2))) for row in range(self.cond_list.rowCount()) ] if self.update_on_change and ( self.last_output_conditions is None or self.last_output_conditions != self.conditions): self.commit() except AttributeError: # Attribute error appears if the signal is triggered when the # controls are being constructed pass def commit(self): matching_output = self.data non_matching_output = None self.error() if self.data: domain = self.data.domain conditions = [] for attr_name, oper, values in self.conditions: attr_index = domain.index(attr_name) attr = domain[attr_index] if attr.is_continuous: if any(not v for v in values): continue # Parse datetime strings into floats if isinstance(attr, TimeVariable): try: values = [attr.parse(v) for v in values] except ValueError as e: self.error(e.args[0]) return filter = data_filter.FilterContinuous( attr_index, oper, *[float(v) for v in values]) elif attr.is_string: filter = data_filter.FilterString( attr_index, oper, *[str(v) for v in values]) else: if oper == 3: f_values = None else: if not values or not values[0]: continue values = [attr.values[i - 1] for i in values] if oper == 0: f_values = {values[0]} elif oper == 1: f_values = set(attr.values) f_values.remove(values[0]) elif oper == 2: f_values = set(values) else: raise ValueError("invalid operand") filter = data_filter.FilterDiscrete(attr_index, f_values) conditions.append(filter) if conditions: filters = data_filter.Values(conditions) matching_output = filters(self.data) filters.negate = True non_matching_output = filters(self.data) # if hasattr(self.data, "name"): # matching_output.name = self.data.name # non_matching_output.name = self.data.name purge_attrs = self.purge_attributes purge_classes = self.purge_classes if (purge_attrs or purge_classes) and \ not isinstance(self.data, SqlTable): attr_flags = sum([ Remove.RemoveConstant * purge_attrs, Remove.RemoveUnusedValues * purge_attrs ]) class_flags = sum([ Remove.RemoveConstant * purge_classes, Remove.RemoveUnusedValues * purge_classes ]) # same settings used for attributes and meta features remover = Remove(attr_flags, class_flags, attr_flags) matching_output = remover(matching_output) non_matching_output = remover(non_matching_output) self.send("Matching Data", matching_output) self.send("Unmatched Data", non_matching_output) self.match_desc = report.describe_data_brief(matching_output) self.nonmatch_desc = report.describe_data_brief(non_matching_output) self.update_info(matching_output, self.data_out_rows, "Out: ") def update_info(self, data, lab1, label): def sp(s, capitalize=True): return s and s or ("No" if capitalize else "no"), "s" * (s != 1) if data is None: lab1.setText("") else: lab1.setText( label + "~%s row%s, %s variable%s" % (sp(data.approx_len()) + sp(len(data.domain.variables) + len(data.domain.metas)))) def send_report(self): if not self.data: self.report_paragraph("No data.") return pdesc = None describe_domain = False for d in (self.data_desc, self.match_desc, self.nonmatch_desc): if not d or not d["Data instances"]: continue ndesc = d.copy() del ndesc["Data instances"] if pdesc is not None and pdesc != ndesc: describe_domain = True pdesc = ndesc conditions = [] domain = self.data.domain for attr_name, oper, values in self.conditions: attr_index = domain.index(attr_name) attr = domain[attr_index] names = self.operator_names[type(attr)] name = names[oper] if oper == len(names) - 1: conditions.append("{} {}".format(attr, name)) elif attr.is_discrete: if name == "is one of": if len(values) == 1: conditions.append("{} is {}".format( attr, attr.values[values[0] - 1])) elif len(values) > 1: conditions.append("{} is {} or {}".format( attr, ", ".join(attr.values[v - 1] for v in values[:-1]), attr.values[values[-1] - 1])) else: if not (values and values[0]): continue value = values[0] - 1 conditions.append("{} {} {}".format( attr, name, attr.values[value])) else: if len(values) == 1: conditions.append("{} {} {}".format(attr, name, *values)) else: conditions.append("{} {} {} and {}".format( attr, name, *values)) items = OrderedDict() if describe_domain: items.update(self.data_desc) else: items["Instances"] = self.data_desc["Data instances"] items["Condition"] = " AND ".join(conditions) or "no conditions" self.report_items("Data", items) if describe_domain: self.report_items("Matching data", self.match_desc) self.report_items("Non-matching data", self.nonmatch_desc) else: match_inst = \ bool(self.match_desc) and \ self.match_desc["Data instances"] nonmatch_inst = \ bool(self.nonmatch_desc) and \ self.nonmatch_desc["Data instances"] self.report_items( "Output", (("Matching data", "{} instances".format(match_inst) if match_inst else "None"), ("Non-matching data", nonmatch_inst > 0 and "{} instances".format(nonmatch_inst))))
class LightConfigUI(QWidget): keyPressed = pyqtSignal(int) def __init__(self, lights, parent): super(LightConfigUI, self).__init__() self.title = 'Lighting Configuration' self.setLayout(QVBoxLayout(self)) self.layout().setAlignment(Qt.AlignCenter) self.parent = parent self.lights = lights # Init logger self.logger = Logger('lightConfig', "UI : LightConfig", level='debug') # Create layout self._plus = QPushButton('+', self) self._plus.clicked.connect(self.__createLight) self._minus = QPushButton('-', self) self._minus.clicked.connect(self.__destroyLight) _panel = QWidget(self) _panel.setLayout(QHBoxLayout(_panel)) _panel.layout().setAlignment(Qt.AlignRight) _panel.layout().addWidget(self._plus) _panel.layout().addWidget(self._minus) self.layout().addWidget(_panel) self._lightsList = QTableWidget(0, 5, self) self._lightsList.setSelectionBehavior(QAbstractItemView.SelectRows) self._lightsList.setHorizontalHeaderLabels( ['Name', 'Output Pin', 'Enabled', 'Icon', 'Strobe']) self._lightsList.horizontalHeader().setSectionResizeMode( QHeaderView.Stretch) self.keyPressed.connect(self.__onKey) self.layout().addWidget(self._lightsList) self._editBtn = QPushButton('Edit', self) self._editBtn.clicked.connect(self.__editLight) self.layout().addWidget(self._editBtn) self._closeBtn = QPushButton('Close', self) self._closeBtn.clicked.connect(self.__closeBtnAction) self.layout().addWidget(self._closeBtn) # Populate table for _light in self.lights: _i = self._lightsList.rowCount() self._lightsList.setRowCount(_i + 1) for _c, _item in enumerate([ _light.name, str(_light.outputPin), str(_light.enabled), _light.icon, str(_light.strobe) ]): _tblItem = QTableWidgetItem(_item) _tblItem.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) self._lightsList.setItem(_i, _c, _tblItem) def keyPressEvent(self, event): super(LightConfigUI, self).keyPressEvent(event) self.keyPressed.emit(event.key()) def __onKey(self, key): if key == Qt.Key_Escape: self._lightsList.clearSelection() def __createLight(self): self.parent.loadUI('create_light') def __destroyLight(self): items = self._lightsList.selectedIndexes() rows = [] for i in items: if i.row() not in rows: rows.append(i.row()) rows.sort(reverse=True) for row in rows: self.parent.lights.rmLight(row) self.parent.lights.save() self.parent.loadUI('config_light') def __editLight(self): sIdx = self._lightsList.currentIndex().row() if sIdx not in [None, -1]: self.parent.loadUI("edit_light", sIdx) self.parent.disableConfigButtons() else: msgBox = QMessageBox() msgBox.setIcon(QMessageBox.Warning) msgBox.setWindowTitle('Unable to Edit Lighting Element') msgBox.setText('Please select a Lighting element before editing.') msgBox.setStandardButtons(QMessageBox.Close) msgBox.exec_() def __closeBtnAction(self): self.parent.loadUI('control_light') self.parent.enableConfigButtons()