def get_control_by_type(param): if isinstance(param, tuple): options = param[1:] param = param[0] else: options = () if param == 'str': result = QLineEdit() if 'pwd' in options: result.setEchoMode(QLineEdit.Password) if 'optional' in options: result.setPlaceholderText('(Optional)') elif param == 'big_str': result = QPlainTextEdit() elif param == 'label': result = QLabel() if 'url' in options: result.setTextFormat(Qt.RichText) result.setTextInteractionFlags(Qt.TextBrowserInteraction) result.setOpenExternalLinks(True) elif param == 'checkbox': result = QCheckBox() else: raise RuntimeError() return result
class DocumentEditor(QGroupBox): text_changed = Signal(str, str) remove_clicked = Signal() def __init__(self, title, text, parent=None): super().__init__(parent) self.setLayout(QVBoxLayout()) self.title_le = QLineEdit() self.title_le.setPlaceholderText("Document title") self.title_le.setText(title) self.title_le.editingFinished.connect(self._on_text_changed) self.text_area = CustomQPlainTextEdit() self.text_area.setPlaceholderText("Document text") self.text_area.setPlainText(text) self.text_area.editingFinished.connect(self._on_text_changed) remove_button = QPushButton("x") remove_button.setFixedWidth(35) remove_button.setFocusPolicy(Qt.NoFocus) remove_button.clicked.connect(self._on_remove_clicked) box = gui.hBox(self) box.layout().addWidget(self.title_le) box.layout().addWidget(remove_button) self.layout().addWidget(self.text_area) def _on_text_changed(self): self.text_changed.emit(self.title_le.text(), self.text_area.toPlainText()) def _on_remove_clicked(self): self.remove_clicked.emit()
class OWPCA(widget.OWWidget): name = "PCA" description = "Principal component analysis with a scree-diagram." icon = "icons/PCA.svg" priority = 3050 keywords = ["principal component analysis", "linear transformation"] class Inputs: data = Input("Data", Table) class Outputs: transformed_data = Output("Transformed data", Table) components = Output("Components", Table) pca = Output("PCA", PCA, dynamic=False) preprocessor = Output("Preprocessor", Preprocess) settingsHandler = settings.DomainContextHandler() ncomponents = settings.Setting(2) variance_covered = settings.Setting(100) batch_size = settings.Setting(100) address = settings.Setting('') auto_update = settings.Setting(True) auto_commit = settings.Setting(True) normalize = settings.ContextSetting(True) decomposition_idx = settings.ContextSetting(0) maxp = settings.Setting(20) axis_labels = settings.Setting(10) graph_name = "plot.plotItem" class Warning(widget.OWWidget.Warning): trivial_components = widget.Msg( "All components of the PCA are trivial (explain 0 variance). " "Input data is constant (or near constant).") class Error(widget.OWWidget.Error): no_features = widget.Msg("At least 1 feature is required") no_instances = widget.Msg("At least 1 data instance is required") sparse_data = widget.Msg("Sparse data is not supported") def __init__(self): super().__init__() self.data = None self._pca = None self._transformed = None self._variance_ratio = None self._cumulative = None self._line = False self._init_projector() # Components Selection box = gui.vBox(self.controlArea, "Components Selection") form = QFormLayout() box.layout().addLayout(form) self.components_spin = gui.spin( box, self, "ncomponents", 1, MAX_COMPONENTS, callback=self._update_selection_component_spin, keyboardTracking=False ) self.components_spin.setSpecialValueText("All") self.variance_spin = gui.spin( box, self, "variance_covered", 1, 100, callback=self._update_selection_variance_spin, keyboardTracking=False ) self.variance_spin.setSuffix("%") form.addRow("Components:", self.components_spin) form.addRow("Variance covered:", self.variance_spin) # Incremental learning self.sampling_box = gui.vBox(self.controlArea, "Incremental learning") self.addresstext = QLineEdit(box) self.addresstext.setPlaceholderText('Remote server') if self.address: self.addresstext.setText(self.address) self.sampling_box.layout().addWidget(self.addresstext) form = QFormLayout() self.sampling_box.layout().addLayout(form) self.batch_spin = gui.spin( self.sampling_box, self, "batch_size", 50, 100000, step=50, keyboardTracking=False) form.addRow("Batch size ~ ", self.batch_spin) self.start_button = gui.button( self.sampling_box, self, "Start remote computation", callback=self.start, autoDefault=False, tooltip="Start/abort computation on the server") self.start_button.setEnabled(False) gui.checkBox(self.sampling_box, self, "auto_update", "Periodically fetch model", callback=self.update_model) self.__timer = QTimer(self, interval=2000) self.__timer.timeout.connect(self.get_model) self.sampling_box.setVisible(remotely) # Decomposition self.decomposition_box = gui.radioButtons( self.controlArea, self, "decomposition_idx", [d.name for d in DECOMPOSITIONS], box="Decomposition", callback=self._update_decomposition ) # Options self.options_box = gui.vBox(self.controlArea, "Options") self.normalize_box = gui.checkBox( self.options_box, self, "normalize", "Normalize data", callback=self._update_normalize ) self.maxp_spin = gui.spin( self.options_box, self, "maxp", 1, MAX_COMPONENTS, label="Show only first", callback=self._setup_plot, keyboardTracking=False ) self.controlArea.layout().addStretch() gui.auto_commit(self.controlArea, self, "auto_commit", "Apply", checkbox_label="Apply automatically") self.plot = pg.PlotWidget(background="w") axis = self.plot.getAxis("bottom") axis.setLabel("Principal Components") axis = self.plot.getAxis("left") axis.setLabel("Proportion of variance") self.plot_horlabels = [] self.plot_horlines = [] self.plot.getViewBox().setMenuEnabled(False) self.plot.getViewBox().setMouseEnabled(False, False) self.plot.showGrid(True, True, alpha=0.5) self.plot.setRange(xRange=(0.0, 1.0), yRange=(0.0, 1.0)) self.mainArea.layout().addWidget(self.plot) self._update_normalize() def update_model(self): self.get_model() if self.auto_update and self.rpca and not self.rpca.ready(): self.__timer.start(2000) else: self.__timer.stop() def update_buttons(self, sparse_data=False): if sparse_data: self.normalize = False buttons = self.decomposition_box.buttons for cls, button in zip(DECOMPOSITIONS, buttons): button.setDisabled(sparse_data and not cls.supports_sparse) if not buttons[self.decomposition_idx].isEnabled(): # Set decomposition index to first sparse-enabled decomposition for i, cls in enumerate(DECOMPOSITIONS): if cls.supports_sparse: self.decomposition_idx = i break self._init_projector() def start(self): if 'Abort' in self.start_button.text(): self.rpca.abort() self.__timer.stop() self.start_button.setText("Start remote computation") else: self.address = self.addresstext.text() with remote.server(self.address): from Orange.projection.pca import RemotePCA maxiter = (1e5 + self.data.approx_len()) / self.batch_size * 3 self.rpca = RemotePCA(self.data, self.batch_size, int(maxiter)) self.update_model() self.start_button.setText("Abort remote computation") @Inputs.data def set_data(self, data): self.closeContext() self.clear_messages() self.clear() self.start_button.setEnabled(False) self.information() self.data = None if isinstance(data, SqlTable): if data.approx_len() < AUTO_DL_LIMIT: data = Table(data) elif not remotely: self.information("Data has been sampled") data_sample = data.sample_time(1, no_cache=True) data_sample.download_data(2000, partial=True) data = Table(data_sample) else: # data was big and remote available self.sampling_box.setVisible(True) self.start_button.setText("Start remote computation") self.start_button.setEnabled(True) if not isinstance(data, SqlTable): self.sampling_box.setVisible(False) if isinstance(data, Table): if len(data.domain.attributes) == 0: self.Error.no_features() self.clear_outputs() return if len(data) == 0: self.Error.no_instances() self.clear_outputs() return self.openContext(data) sparse_data = data is not None and data.is_sparse() self.normalize_box.setDisabled(sparse_data) self.update_buttons(sparse_data=sparse_data) self.data = data self.fit() def fit(self): self.clear() self.Warning.trivial_components.clear() if self.data is None: return data = self.data self._pca_projector.preprocessors = \ self._pca_preprocessors + ([Normalize()] if self.normalize else []) if not isinstance(data, SqlTable): pca = self._pca_projector(data) variance_ratio = pca.explained_variance_ratio_ cumulative = numpy.cumsum(variance_ratio) if numpy.isfinite(cumulative[-1]): self.components_spin.setRange(0, len(cumulative)) self._pca = pca self._variance_ratio = variance_ratio self._cumulative = cumulative self._setup_plot() else: self.Warning.trivial_components() self.unconditional_commit() def clear(self): self._pca = None self._transformed = None self._variance_ratio = None self._cumulative = None self._line = None self.plot_horlabels = [] self.plot_horlines = [] self.plot.clear() def clear_outputs(self): self.Outputs.transformed_data.send(None) self.Outputs.components.send(None) self.Outputs.pca.send(self._pca_projector) self.Outputs.preprocessor.send(None) def get_model(self): if self.rpca is None: return if self.rpca.ready(): self.__timer.stop() self.start_button.setText("Restart (finished)") self._pca = self.rpca.get_state() if self._pca is None: return self._variance_ratio = self._pca.explained_variance_ratio_ self._cumulative = numpy.cumsum(self._variance_ratio) self._setup_plot() self._transformed = None self.commit() def _setup_plot(self): self.plot.clear() if self._pca is None: return explained_ratio = self._variance_ratio explained = self._cumulative p = min(len(self._variance_ratio), self.maxp) self.plot.plot(numpy.arange(p), explained_ratio[:p], pen=pg.mkPen(QColor(Qt.red), width=2), antialias=True, name="Variance") self.plot.plot(numpy.arange(p), explained[:p], pen=pg.mkPen(QColor(Qt.darkYellow), width=2), antialias=True, name="Cumulative Variance") cutpos = self._nselected_components() - 1 self._line = pg.InfiniteLine( angle=90, pos=cutpos, movable=True, bounds=(0, p - 1)) self._line.setCursor(Qt.SizeHorCursor) self._line.setPen(pg.mkPen(QColor(Qt.black), width=2)) self._line.sigPositionChanged.connect(self._on_cut_changed) self.plot.addItem(self._line) self.plot_horlines = ( pg.PlotCurveItem(pen=pg.mkPen(QColor(Qt.blue), style=Qt.DashLine)), pg.PlotCurveItem(pen=pg.mkPen(QColor(Qt.blue), style=Qt.DashLine))) self.plot_horlabels = ( pg.TextItem(color=QColor(Qt.black), anchor=(1, 0)), pg.TextItem(color=QColor(Qt.black), anchor=(1, 1))) for item in self.plot_horlabels + self.plot_horlines: self.plot.addItem(item) self._set_horline_pos() self.plot.setRange(xRange=(0.0, p - 1), yRange=(0.0, 1.0)) self._update_axis() def _set_horline_pos(self): cutidx = self.ncomponents - 1 for line, label, curve in zip(self.plot_horlines, self.plot_horlabels, (self._variance_ratio, self._cumulative)): y = curve[cutidx] line.setData([-1, cutidx], 2 * [y]) label.setPos(cutidx, y) label.setPlainText("{:.3f}".format(y)) def _on_cut_changed(self, line): # cut changed by means of a cut line over the scree plot. value = int(round(line.value())) self._line.setValue(value) current = self._nselected_components() components = value + 1 if not (self.ncomponents == 0 and components == len(self._variance_ratio)): self.ncomponents = components self._set_horline_pos() if self._pca is not None: var = self._cumulative[components - 1] if numpy.isfinite(var): self.variance_covered = int(var * 100) if current != self._nselected_components(): self._invalidate_selection() def _update_selection_component_spin(self): # cut changed by "ncomponents" spin. if self._pca is None: self._invalidate_selection() return if self.ncomponents == 0: # Special "All" value cut = len(self._variance_ratio) else: cut = self.ncomponents var = self._cumulative[cut - 1] if numpy.isfinite(var): self.variance_covered = int(var * 100) if numpy.floor(self._line.value()) + 1 != cut: self._line.setValue(cut - 1) self._invalidate_selection() def _update_selection_variance_spin(self): # cut changed by "max variance" spin. if self._pca is None: return cut = numpy.searchsorted(self._cumulative, self.variance_covered / 100.0) + 1 cut = min(cut, len(self._cumulative)) self.ncomponents = cut if numpy.floor(self._line.value()) + 1 != cut: self._line.setValue(cut - 1) self._invalidate_selection() def _update_normalize(self): self.fit() if self.data is None: self._invalidate_selection() def _init_projector(self): cls = DECOMPOSITIONS[self.decomposition_idx] self._pca_projector = cls(n_components=MAX_COMPONENTS) self._pca_projector.component = self.ncomponents self._pca_preprocessors = cls.preprocessors def _update_decomposition(self): self._init_projector() self._update_normalize() def _nselected_components(self): """Return the number of selected components.""" if self._pca is None: return 0 if self.ncomponents == 0: # Special "All" value max_comp = len(self._variance_ratio) else: max_comp = self.ncomponents var_max = self._cumulative[max_comp - 1] if var_max != numpy.floor(self.variance_covered / 100.0): cut = max_comp assert numpy.isfinite(var_max) self.variance_covered = int(var_max * 100) else: self.ncomponents = cut = numpy.searchsorted( self._cumulative, self.variance_covered / 100.0) + 1 return cut def _invalidate_selection(self): self.commit() def _update_axis(self): p = min(len(self._variance_ratio), self.maxp) axis = self.plot.getAxis("bottom") d = max((p-1)//(self.axis_labels-1), 1) axis.setTicks([[(i, str(i+1)) for i in range(0, p, d)]]) def commit(self): transformed = components = pp = None if self._pca is not None: if self._transformed is None: # Compute the full transform (MAX_COMPONENTS components) only once. self._transformed = self._pca(self.data) transformed = self._transformed domain = Domain( transformed.domain.attributes[:self.ncomponents], self.data.domain.class_vars, self.data.domain.metas ) transformed = transformed.from_table(domain, transformed) # prevent caching new features by defining compute_value dom = Domain([ContinuousVariable(a.name, compute_value=lambda _: None) for a in self._pca.orig_domain.attributes], metas=[StringVariable(name='component')]) metas = numpy.array([['PC{}'.format(i + 1) for i in range(self.ncomponents)]], dtype=object).T components = Table(dom, self._pca.components_[:self.ncomponents], metas=metas) components.name = 'components' pp = ApplyDomain(domain, "PCA") self._pca_projector.component = self.ncomponents self.Outputs.transformed_data.send(transformed) self.Outputs.components.send(components) self.Outputs.pca.send(self._pca_projector) self.Outputs.preprocessor.send(pp) def send_report(self): if self.data is None: return self.report_items(( ("Decomposition", DECOMPOSITIONS[self.decomposition_idx].name), ("Normalize data", str(self.normalize)), ("Selected components", self.ncomponents), ("Explained variance", "{:.3f} %".format(self.variance_covered)) )) self.report_plot() @classmethod def migrate_settings(cls, settings, version): if "variance_covered" in settings: # Due to the error in gh-1896 the variance_covered was persisted # as a NaN value, causing a TypeError in the widgets `__init__`. vc = settings["variance_covered"] if isinstance(vc, numbers.Real): if numpy.isfinite(vc): vc = int(vc) else: vc = 100 settings["variance_covered"] = vc if settings.get("ncomponents", 0) > MAX_COMPONENTS: settings["ncomponents"] = MAX_COMPONENTS
def variables_filter(model, parent=None): """ GUI components: ListView with a lineedit which works as a filter. One can write a variable name in a edit box and possible matches are then shown in a listview. """ def update_completer_model(): """ This gets called when the model for available attributes changes through either drag/drop or the left/right button actions. """ nonlocal original_completer_items items = [var.name for var in model] items += [ "%s=%s" % item for v in model for item in v.attributes.items() ] new = sorted(set(items)) if new != original_completer_items: original_completer_items = new completer_model.setStringList(original_completer_items) def update_completer_prefix(): """ Prefixes all items in the completer model with the current already done completion to enable the completion of multiple keywords. """ nonlocal original_completer_items prefix = str(completer.completionPrefix()) if not prefix.endswith(" ") and " " in prefix: prefix, _ = prefix.rsplit(" ", 1) items = [prefix + " " + item for item in original_completer_items] else: items = original_completer_items old = list(map(str, completer_model.stringList())) if set(old) != set(items): completer_model.setStringList(items) original_completer_items = [] filter_edit = QLineEdit() filter_edit.setToolTip("Filter the list of available variables.") filter_edit.setPlaceholderText("Filter") completer_model = QStringListModel() completer = QCompleter(completer_model) completer.setCompletionMode(QCompleter.InlineCompletion) completer.setModelSorting(QCompleter.CaseSensitivelySortedModel) filter_edit.setCompleter(completer) completer_navigator = CompleterNavigator(parent) filter_edit.installEventFilter(completer_navigator) proxy = VariableFilterProxyModel() proxy.setSourceModel(model) view = VariablesListItemView(acceptedType=Orange.data.Variable) view.setModel(proxy) model.dataChanged.connect(update_completer_model) model.rowsInserted.connect(update_completer_model) model.rowsRemoved.connect(update_completer_model) filter_edit.textChanged.connect(update_completer_prefix) filter_edit.textChanged.connect(proxy.set_filter_string) return filter_edit, view
class SignInForm(QDialog): def __init__(self, flags, *args, **kwargs): super().__init__(flags, *args, **kwargs) self.cm: CredentialManager = CredentialManager(CREDENTIAL_MANAGER_SERVICE) self.setWindowTitle('Sign in') self.setFixedSize(400, 250) self.server_cb_label = QLabel('Server *') self.server_cb = QComboBox(self) self.server_cb.addItems(RESOLWE_URLS) self.server_cb.setEditable(True) self.username_label = QLabel('Username *') self.username_line_edit = QLineEdit(self) self.username_line_edit.setPlaceholderText('Enter correct username') self.username_line_edit.returnPressed.connect(self.sign_in) self.username_line_edit.textChanged.connect(self.handle_sign_in_btn) self.password_label = QLabel('Password *') self.password_line_edit = QLineEdit(self) self.password_line_edit.setPlaceholderText('Enter correct password') self.password_line_edit.returnPressed.connect(self.sign_in) self.password_line_edit.textChanged.connect(self.handle_sign_in_btn) self.password_line_edit.setEchoMode(QLineEdit.Password) self.sign_in_btn = QPushButton('Sign in', self) self.sign_in_btn.setDisabled(True) self.sign_in_btn.clicked.connect(self.sign_in) self.error_msg = QLabel('Unable to log in with provided credentials.') self.error_msg.setStyleSheet('color:red') self.error_msg.hide() layout = QVBoxLayout(self) layout.addWidget(self.server_cb_label) layout.addWidget(self.server_cb) layout.addWidget(self.username_label) layout.addWidget(self.username_line_edit) layout.addWidget(self.password_label) layout.addWidget(self.password_line_edit) layout.addWidget(self.error_msg) layout.addStretch() layout.addWidget(self.sign_in_btn) self.resolwe_instance = None def handle_sign_in_btn(self): self.sign_in_btn.setEnabled( True if self.username_line_edit.text() and self.password_line_edit.text() else False ) def sign_in(self): self.server_cb_label.setStyleSheet(None) self.username_label.setStyleSheet(None) self.password_label.setStyleSheet(None) self.error_msg.hide() server = self.server_cb.currentText() username = self.cm.username if self.cm.username else self.username_line_edit.text() password = self.cm.password if self.cm.password else self.password_line_edit.text() if not server: self.server_cb_label.setStyleSheet('color:red') return if not username: self.username_label.setStyleSheet('color:red') return if not password: self.password_label.setStyleSheet('color:red') return try: self.resolwe_instance = connect(username, password, url=server) except ResolweAuthException: self.error_msg.show() return self.cm.username = username self.cm.password = password self.accept()
class OWSql(OWWidget): name = "SQL Table" id = "orange.widgets.data.sql" description = "Load data set from SQL." icon = "icons/SQLTable.svg" priority = 10 category = "Data" keywords = ["data", "file", "load", "read"] class Outputs: data = Output( "Data", Table, doc="Attribute-valued data set read from the input file.") settings_version = 2 want_main_area = False resizing_enabled = False host = Setting(None) port = Setting(None) database = Setting(None) schema = Setting(None) username = "" password = "" table = Setting(None) sql = Setting("") guess_values = Setting(True) download = Setting(False) materialize = Setting(False) materialize_table_name = Setting("") class Information(OWWidget.Information): data_sampled = Msg("Data description was generated from a sample.") class Error(OWWidget.Error): connection = Msg("{}") no_backends = Msg("Please install a backend to use this widget") missing_extension = Msg("Database is missing extension{}: {}") def __init__(self): super().__init__() self.backend = None self.data_desc_table = None self.database_desc = None vbox = gui.vBox(self.controlArea, "Server", addSpace=True) box = gui.vBox(vbox) self.backendmodel = BackendModel(Backend.available_backends()) self.backendcombo = QComboBox(box) if len(self.backendmodel): self.backendcombo.setModel(self.backendmodel) else: self.Error.no_backends() box.setEnabled(False) box.layout().addWidget(self.backendcombo) self.servertext = QLineEdit(box) self.servertext.setPlaceholderText('Server') self.servertext.setToolTip('Server') self.servertext.editingFinished.connect(self._load_credentials) if self.host: self.servertext.setText(self.host if not self.port else '{}:{}'. format(self.host, self.port)) box.layout().addWidget(self.servertext) self.databasetext = QLineEdit(box) self.databasetext.setPlaceholderText('Database[/Schema]') self.databasetext.setToolTip('Database or optionally Database/Schema') if self.database: self.databasetext.setText( self.database if not self.schema else '{}/{}'. format(self.database, self.schema)) box.layout().addWidget(self.databasetext) self.usernametext = QLineEdit(box) self.usernametext.setPlaceholderText('Username') self.usernametext.setToolTip('Username') box.layout().addWidget(self.usernametext) self.passwordtext = QLineEdit(box) self.passwordtext.setPlaceholderText('Password') self.passwordtext.setToolTip('Password') self.passwordtext.setEchoMode(QLineEdit.Password) box.layout().addWidget(self.passwordtext) self._load_credentials() tables = gui.hBox(box) self.tablemodel = TableModel() self.tablecombo = QComboBox( minimumContentsLength=35, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength) self.tablecombo.setModel(self.tablemodel) self.tablecombo.setToolTip('table') tables.layout().addWidget(self.tablecombo) self.tablecombo.activated[int].connect(self.select_table) self.connectbutton = gui.button(tables, self, '↻', callback=self.connect) self.connectbutton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) tables.layout().addWidget(self.connectbutton) self.custom_sql = gui.vBox(box) self.custom_sql.setVisible(False) self.sqltext = QTextEdit(self.custom_sql) self.sqltext.setPlainText(self.sql) self.custom_sql.layout().addWidget(self.sqltext) mt = gui.hBox(self.custom_sql) cb = gui.checkBox(mt, self, 'materialize', 'Materialize to table ') cb.setToolTip('Save results of the query in a table') le = gui.lineEdit(mt, self, 'materialize_table_name') le.setToolTip('Save results of the query in a table') self.executebtn = gui.button(self.custom_sql, self, 'Execute', callback=self.open_table) box.layout().addWidget(self.custom_sql) gui.checkBox(box, self, "guess_values", "Auto-discover discrete variables", callback=self.open_table) gui.checkBox(box, self, "download", "Download data to local memory", callback=self.open_table) gui.rubber(self.buttonsArea) QTimer.singleShot(0, self.connect) def _load_credentials(self): self._parse_host_port() cm = self._credential_manager(self.host, self.port) self.username = cm.username self.password = cm.password if self.username: self.usernametext.setText(self.username) if self.password: self.passwordtext.setText(self.password) def _save_credentials(self): cm = self._credential_manager(self.host, self.port) cm.username = self.username cm.password = self.password def _credential_manager(self, host, port): return CredentialManager("SQL Table: {}:{}".format(host, port)) def error(self, id=0, text=""): super().error(id, text) err_style = 'QLineEdit {border: 2px solid red;}' if 'server' in text or 'host' in text: self.servertext.setStyleSheet(err_style) else: self.servertext.setStyleSheet('') if 'role' in text: self.usernametext.setStyleSheet(err_style) else: self.usernametext.setStyleSheet('') if 'database' in text: self.databasetext.setStyleSheet(err_style) else: self.databasetext.setStyleSheet('') def _parse_host_port(self): hostport = self.servertext.text().split(':') self.host = hostport[0] self.port = hostport[1] if len(hostport) == 2 else None def connect(self): self._parse_host_port() self.database, _, self.schema = self.databasetext.text().partition('/') self.username = self.usernametext.text() or None self.password = self.passwordtext.text() or None try: if self.backendcombo.currentIndex() < 0: return backend = self.backendmodel[self.backendcombo.currentIndex()] self.backend = backend( dict(host=self.host, port=self.port, database=self.database, user=self.username, password=self.password)) self.Error.connection.clear() self._save_credentials() self.database_desc = OrderedDict( (("Host", self.host), ("Port", self.port), ("Database", self.database), ("User name", self.username))) self.refresh_tables() self.select_table() except BackendError as err: error = str(err).split('\n')[0] self.Error.connection(error) self.database_desc = self.data_desc_table = None self.tablecombo.clear() def refresh_tables(self): self.tablemodel.clear() self.Error.missing_extension.clear() if self.backend is None: self.data_desc_table = None return self.tablemodel.append("Select a table") self.tablemodel.extend(self.backend.list_tables(self.schema)) self.tablemodel.append("Custom SQL") def select_table(self): curIdx = self.tablecombo.currentIndex() if self.tablecombo.itemText(curIdx) != "Custom SQL": self.custom_sql.setVisible(False) return self.open_table() else: self.custom_sql.setVisible(True) self.data_desc_table = None self.database_desc["Table"] = "(None)" self.table = None #self.Error.missing_extension( # 's' if len(missing) > 1 else '', # ', '.join(missing), # shown=missing) def open_table(self): table = self.get_table() self.data_desc_table = table self.Outputs.data.send(table) def get_table(self): if self.tablecombo.currentIndex() <= 0: if self.database_desc: self.database_desc["Table"] = "(None)" self.data_desc_table = None return if self.tablecombo.currentIndex() < self.tablecombo.count() - 1: self.table = self.tablemodel[self.tablecombo.currentIndex()] self.database_desc["Table"] = self.table if "Query" in self.database_desc: del self.database_desc["Query"] else: self.sql = self.table = self.sqltext.toPlainText() if self.materialize: import psycopg2 if not self.materialize_table_name: self.Error.connection( "Specify a table name to materialize the query") return try: with self.backend.execute_sql_query( "DROP TABLE IF EXISTS " + self.materialize_table_name): pass with self.backend.execute_sql_query( "CREATE TABLE " + self.materialize_table_name + " AS " + self.table): pass with self.backend.execute_sql_query( "ANALYZE " + self.materialize_table_name): pass self.table = self.materialize_table_name except (psycopg2.ProgrammingError, BackendError) as ex: self.Error.connection(str(ex)) return try: table = SqlTable(dict(host=self.host, port=self.port, database=self.database, user=self.username, password=self.password), self.table, backend=type(self.backend), inspect_values=False) except BackendError as ex: self.Error.connection(str(ex)) return self.Error.connection.clear() sample = False if table.approx_len() > LARGE_TABLE and self.guess_values: confirm = QMessageBox(self) confirm.setIcon(QMessageBox.Warning) confirm.setText("Attribute discovery might take " "a long time on large tables.\n" "Do you want to auto discover attributes?") confirm.addButton("Yes", QMessageBox.YesRole) no_button = confirm.addButton("No", QMessageBox.NoRole) sample_button = confirm.addButton("Yes, on a sample", QMessageBox.YesRole) confirm.exec() if confirm.clickedButton() == no_button: self.guess_values = False elif confirm.clickedButton() == sample_button: sample = True self.Information.clear() if self.guess_values: QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) if sample: s = table.sample_time(1) domain = s.get_domain(inspect_values=True) self.Information.data_sampled() else: domain = table.get_domain(inspect_values=True) QApplication.restoreOverrideCursor() table.domain = domain if self.download: if table.approx_len() > MAX_DL_LIMIT: QMessageBox.warning( self, 'Warning', "Data is too big to download.\n" "Consider using the Data Sampler widget to download " "a sample instead.") self.download = False elif table.approx_len() > AUTO_DL_LIMIT: confirm = QMessageBox.question( self, 'Question', "Data appears to be big. Do you really " "want to download it to local memory?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if confirm == QMessageBox.No: self.download = False if self.download: table.download_data(MAX_DL_LIMIT) table = Table(table) return table def send_report(self): if not self.database_desc: self.report_paragraph("No database connection.") return self.report_items("Database", self.database_desc) if self.data_desc_table: self.report_items("Data", report.describe_data(self.data_desc_table)) @classmethod def migrate_settings(cls, settings, version): if version < 2: # Until Orange version 3.4.4 username and password had been stored # in Settings. cm = cls._credential_manager(settings["host"], settings["port"]) cm.username = settings["username"] cm.password = settings["password"]
class VizRankDialog(QDialog, ProgressBarMixin, WidgetMessagesMixin): """ Base class for VizRank dialogs, providing a GUI with a table and a button, and the skeleton for managing the evaluation of visualizations. Derived classes must provide methods - `iterate_states` for generating combinations (e.g. pairs of attritutes), - `compute_score(state)` for computing the score of a combination, - `row_for_state(state)` that returns a list of items inserted into the table for the given state. and, optionally, - `state_count` that returns the number of combinations (used for progress bar) - `on_selection_changed` that handles event triggered when the user selects a table row. The method should emit signal `VizRankDialog.selectionChanged(object)`. - `bar_length` returns the length of the bar corresponding to the score. The class provides a table and a button. A widget constructs a single instance of this dialog in its `__init__`, like (in Sieve) by using a convenience method :obj:`add_vizrank`:: self.vizrank, self.vizrank_button = SieveRank.add_vizrank( box, self, "Score Combinations", self.set_attr) When the widget receives new data, it must call the VizRankDialog's method :obj:`VizRankDialog.initialize()` to clear the GUI and reset the state. Clicking the Start button calls method `run` (and renames the button to Pause). Run sets up a progress bar by getting the number of combinations from :obj:`VizRankDialog.state_count()`. It restores the paused state (if any) and calls generator :obj:`VizRankDialog.iterate_states()`. For each generated state, it calls :obj:`VizRankDialog.score(state)`, which must return the score (lower is better) for this state. If the returned state is not `None`, the data returned by `row_for_state` is inserted at the appropriate place in the table. Args: master (Orange.widget.OWWidget): widget to which the dialog belongs Attributes: master (Orange.widget.OWWidget): widget to which the dialog belongs captionTitle (str): the caption for the dialog. This can be a class attribute. `captionTitle` is used by the `ProgressBarMixin`. """ captionTitle = "" NEGATIVE_COLOR = QColor(70, 190, 250) POSITIVE_COLOR = QColor(170, 242, 43) processingStateChanged = Signal(int) progressBarValueChanged = Signal(float) messageActivated = Signal(Msg) messageDeactivated = Signal(Msg) selectionChanged = Signal(object) class Information(WidgetMessagesMixin.Information): nothing_to_rank = Msg("There is nothing to rank.") def __init__(self, master): """Initialize the attributes and set up the interface""" QDialog.__init__(self, master, windowTitle=self.captionTitle) WidgetMessagesMixin.__init__(self) self.setLayout(QVBoxLayout()) self.insert_message_bar() self.layout().insertWidget(0, self.message_bar) self.master = master self.keep_running = False self.scheduled_call = None self.saved_state = None self.saved_progress = 0 self.scores = [] self.add_to_model = queue.Queue() self.update_timer = QTimer(self) self.update_timer.timeout.connect(self._update) self.update_timer.setInterval(200) self._thread = None self._worker = None self.filter = QLineEdit() self.filter.setPlaceholderText("Filter ...") self.filter.textChanged.connect(self.filter_changed) self.layout().addWidget(self.filter) # Remove focus from line edit self.setFocus(Qt.ActiveWindowFocusReason) self.rank_model = QStandardItemModel(self) self.model_proxy = QSortFilterProxyModel(self, filterCaseSensitivity=False) self.model_proxy.setSourceModel(self.rank_model) self.rank_table = view = QTableView( selectionBehavior=QTableView.SelectRows, selectionMode=QTableView.SingleSelection, showGrid=False, editTriggers=gui.TableView.NoEditTriggers) if self._has_bars: view.setItemDelegate(TableBarItem()) else: view.setItemDelegate(HorizontalGridDelegate()) view.setModel(self.model_proxy) view.selectionModel().selectionChanged.connect( self.on_selection_changed) view.horizontalHeader().setStretchLastSection(True) view.horizontalHeader().hide() self.layout().addWidget(view) self.button = gui.button(self, self, "Start", callback=self.toggle, default=True) @property def _has_bars(self): return type(self).bar_length is not VizRankDialog.bar_length @classmethod def add_vizrank(cls, widget, master, button_label, set_attr_callback): """ Equip the widget with VizRank button and dialog, and monkey patch the widget's `closeEvent` and `hideEvent` to close/hide the vizrank, too. Args: widget (QWidget): the widget into whose layout to insert the button master (Orange.widgets.widget.OWWidget): the master widget button_label: the label for the button set_attr_callback: the callback for setting the projection chosen in the vizrank Returns: tuple with Vizrank dialog instance and push button """ # Monkey patching could be avoided by mixing-in the class (not # necessarily a good idea since we can make a mess of multiple # defined/derived closeEvent and hideEvent methods). Furthermore, # per-class patching would be better than per-instance, but we don't # want to mess with meta-classes either. vizrank = cls(master) button = gui.button(widget, master, button_label, callback=vizrank.reshow, enabled=False) vizrank.selectionChanged.connect(lambda args: set_attr_callback(*args)) master_close_event = master.closeEvent master_hide_event = master.hideEvent master_delete_event = master.onDeleteWidget def closeEvent(event): vizrank.close() master_close_event(event) def hideEvent(event): vizrank.hide() master_hide_event(event) def deleteEvent(): vizrank.keep_running = False if vizrank._thread is not None and vizrank._thread.isRunning(): vizrank._thread.quit() vizrank._thread.wait() master_delete_event() master.closeEvent = closeEvent master.hideEvent = hideEvent master.onDeleteWidget = deleteEvent return vizrank, button def reshow(self): """Put the widget on top of all windows """ self.show() self.raise_() self.activateWindow() def initialize(self): """ Clear and initialize the dialog. This method must be called by the widget when the data is reset, e.g. from `set_data` handler. """ if self._thread is not None and self._thread.isRunning(): self.keep_running = False self._thread.quit() self._thread.wait() self.keep_running = False self.scheduled_call = None self.saved_state = None self.saved_progress = 0 self.update_timer.stop() self.progressBarFinished() self.scores = [] self._update_model() # empty queue self.rank_model.clear() self.button.setText("Start") self.button.setEnabled(self.check_preconditions()) self._thread = QThread(self) self._worker = Worker(self) self._worker.moveToThread(self._thread) self._worker.stopped.connect(self._thread.quit) self._worker.stopped.connect(self._select_first_if_none) self._worker.stopped.connect(self._stopped) self._worker.done.connect(self._done) self._thread.started.connect(self._worker.do_work) def filter_changed(self, text): self.model_proxy.setFilterFixedString(text) def stop_and_reset(self, reset_method=None): if self.keep_running: self.scheduled_call = reset_method or self.initialize self.keep_running = False else: self.initialize() def check_preconditions(self): """Check whether there is sufficient data for ranking.""" return True def on_selection_changed(self, selected, deselected): """ Set the new visualization in the widget when the user select a row in the table. If derived class does not reimplement this, the table gives the information but the user can't click it to select the visualization. Args: selected: the index of the selected item deselected: the index of the previously selected item """ pass def iterate_states(self, initial_state): """ Generate all possible states (e.g. attribute combinations) for the given data. The content of the generated states is specific to the visualization. This method must be defined in the derived classes. Args: initial_state: initial state; None if this is the first call """ raise NotImplementedError def state_count(self): """ Return the number of states for the progress bar. Derived classes should implement this to ensure the proper behaviour of the progress bar""" return 0 def compute_score(self, state): """ Abstract method for computing the score for the given state. Smaller scores are better. Args: state: the state, e.g. the combination of attributes as generated by :obj:`state_count`. """ raise NotImplementedError def bar_length(self, score): """Compute the bar length (between 0 and 1) corresponding to the score. Return `None` if the score cannot be normalized. """ return None def row_for_state(self, score, state): """ Abstract method that return the items that are inserted into the table. Args: score: score, computed by :obj:`compute_score` state: the state, e.g. combination of attributes """ raise NotImplementedError def _select_first_if_none(self): if not self.rank_table.selectedIndexes(): self.rank_table.selectRow(0) def _done(self): self.button.setText("Finished") self.button.setEnabled(False) self.keep_running = False self.saved_state = None def _stopped(self): self.update_timer.stop() self.progressBarFinished() self._update_model() self.stopped() if self.scheduled_call: self.scheduled_call() def _update(self): self._update_model() self._update_progress() def _update_progress(self): self.progressBarSet( int(self.saved_progress * 100 / max(1, self.state_count()))) def _update_model(self): try: while True: pos, row_items = self.add_to_model.get_nowait() self.rank_model.insertRow(pos, row_items) except queue.Empty: pass def toggle(self): """Start or pause the computation.""" self.keep_running = not self.keep_running if self.keep_running: self.button.setText("Pause") self.progressBarInit() self.update_timer.start() self.before_running() self._thread.start() else: self.button.setText("Continue") self._thread.quit() # Need to sync state (the worker must read the keep_running # state and stop) for reliable restart. self._thread.wait() def before_running(self): """Code that is run before running vizrank in its own thread""" pass def stopped(self): """Code that is run after stopping the vizrank thread""" pass
class OWSelectAttributes(widget.OWWidget): name = "Select Columns" description = "Select columns from the data table and assign them to " \ "data features, classes or meta variables." icon = "icons/SelectColumns.svg" priority = 100 class Inputs: data = Input("Data", Table) class Outputs: data = Output("Data", Table) features = Output("Features", widget.AttributeList, dynamic=False) want_main_area = False want_control_area = True settingsHandler = SelectAttributesDomainContextHandler() domain_role_hints = ContextSetting({}) auto_commit = Setting(True) def __init__(self): super().__init__() self.controlArea = QWidget(self.controlArea) self.layout().addWidget(self.controlArea) layout = QGridLayout() self.controlArea.setLayout(layout) layout.setContentsMargins(4, 4, 4, 4) box = gui.vBox(self.controlArea, "Available Variables", addToLayout=False) self.filter_edit = QLineEdit() self.filter_edit.setToolTip("Filter the list of available variables.") box.layout().addWidget(self.filter_edit) if hasattr(self.filter_edit, "setPlaceholderText"): self.filter_edit.setPlaceholderText("Filter") self.completer = QCompleter() self.completer.setCompletionMode(QCompleter.InlineCompletion) self.completer_model = QStringListModel() self.completer.setModel(self.completer_model) self.completer.setModelSorting(QCompleter.CaseSensitivelySortedModel) self.filter_edit.setCompleter(self.completer) self.completer_navigator = CompleterNavigator(self) self.filter_edit.installEventFilter(self.completer_navigator) def dropcompleted(action): if action == Qt.MoveAction: self.commit() self.available_attrs = VariableListModel(enable_dnd=True) self.available_attrs_proxy = VariableFilterProxyModel() self.available_attrs_proxy.setSourceModel(self.available_attrs) self.available_attrs_view = VariablesListItemView( acceptedType=Orange.data.Variable) self.available_attrs_view.setModel(self.available_attrs_proxy) aa = self.available_attrs aa.dataChanged.connect(self.update_completer_model) aa.rowsInserted.connect(self.update_completer_model) aa.rowsRemoved.connect(self.update_completer_model) self.available_attrs_view.selectionModel().selectionChanged.connect( partial(self.update_interface_state, self.available_attrs_view)) self.available_attrs_view.dragDropActionDidComplete.connect( dropcompleted) self.filter_edit.textChanged.connect(self.update_completer_prefix) self.filter_edit.textChanged.connect( self.available_attrs_proxy.set_filter_string) box.layout().addWidget(self.available_attrs_view) layout.addWidget(box, 0, 0, 3, 1) box = gui.vBox(self.controlArea, "Features", addToLayout=False) self.used_attrs = VariableListModel(enable_dnd=True) self.used_attrs_view = VariablesListItemView( acceptedType=(Orange.data.DiscreteVariable, Orange.data.ContinuousVariable)) self.used_attrs_view.setModel(self.used_attrs) self.used_attrs_view.selectionModel().selectionChanged.connect( partial(self.update_interface_state, self.used_attrs_view)) self.used_attrs_view.dragDropActionDidComplete.connect(dropcompleted) box.layout().addWidget(self.used_attrs_view) layout.addWidget(box, 0, 2, 1, 1) box = gui.vBox(self.controlArea, "Target Variable", addToLayout=False) self.class_attrs = ClassVarListItemModel(enable_dnd=True) self.class_attrs_view = ClassVariableItemView( acceptedType=(Orange.data.DiscreteVariable, Orange.data.ContinuousVariable)) self.class_attrs_view.setModel(self.class_attrs) self.class_attrs_view.selectionModel().selectionChanged.connect( partial(self.update_interface_state, self.class_attrs_view)) self.class_attrs_view.dragDropActionDidComplete.connect(dropcompleted) self.class_attrs_view.setMaximumHeight(72) box.layout().addWidget(self.class_attrs_view) layout.addWidget(box, 1, 2, 1, 1) box = gui.vBox(self.controlArea, "Meta Attributes", addToLayout=False) self.meta_attrs = VariableListModel(enable_dnd=True) self.meta_attrs_view = VariablesListItemView( acceptedType=Orange.data.Variable) self.meta_attrs_view.setModel(self.meta_attrs) self.meta_attrs_view.selectionModel().selectionChanged.connect( partial(self.update_interface_state, self.meta_attrs_view)) self.meta_attrs_view.dragDropActionDidComplete.connect(dropcompleted) box.layout().addWidget(self.meta_attrs_view) layout.addWidget(box, 2, 2, 1, 1) bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0) layout.addWidget(bbox, 0, 1, 1, 1) self.up_attr_button = gui.button(bbox, self, "Up", callback=partial( self.move_up, self.used_attrs_view)) self.move_attr_button = gui.button(bbox, self, ">", callback=partial( self.move_selected, self.used_attrs_view)) self.down_attr_button = gui.button(bbox, self, "Down", callback=partial( self.move_down, self.used_attrs_view)) bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0) layout.addWidget(bbox, 1, 1, 1, 1) self.up_class_button = gui.button(bbox, self, "Up", callback=partial( self.move_up, self.class_attrs_view)) self.move_class_button = gui.button(bbox, self, ">", callback=partial( self.move_selected, self.class_attrs_view, exclusive=False)) self.down_class_button = gui.button(bbox, self, "Down", callback=partial( self.move_down, self.class_attrs_view)) bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0) layout.addWidget(bbox, 2, 1, 1, 1) self.up_meta_button = gui.button(bbox, self, "Up", callback=partial( self.move_up, self.meta_attrs_view)) self.move_meta_button = gui.button(bbox, self, ">", callback=partial( self.move_selected, self.meta_attrs_view)) self.down_meta_button = gui.button(bbox, self, "Down", callback=partial( self.move_down, self.meta_attrs_view)) autobox = gui.auto_commit(None, self, "auto_commit", "Send") layout.addWidget(autobox, 3, 0, 1, 3) reset = gui.button(None, self, "Reset", callback=self.reset) autobox.layout().insertWidget(0, self.report_button) autobox.layout().insertWidget(1, reset) autobox.layout().insertSpacing(2, 10) reset.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.report_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) layout.setRowStretch(0, 4) layout.setRowStretch(1, 0) layout.setRowStretch(2, 2) layout.setHorizontalSpacing(0) self.controlArea.setLayout(layout) self.data = None self.output_data = None self.original_completer_items = [] self.resize(500, 600) @Inputs.data def set_data(self, data=None): self.update_domain_role_hints() self.closeContext() self.data = data if data is not None: self.openContext(data) all_vars = data.domain.variables + data.domain.metas var_sig = lambda attr: (attr.name, vartype(attr)) domain_hints = { var_sig(attr): ("attribute", i) for i, attr in enumerate(data.domain.attributes) } domain_hints.update({ var_sig(attr): ("meta", i) for i, attr in enumerate(data.domain.metas) }) if data.domain.class_vars: domain_hints.update({ var_sig(attr): ("class", i) for i, attr in enumerate(data.domain.class_vars) }) # update the hints from context settings domain_hints.update(self.domain_role_hints) attrs_for_role = lambda role: [ (domain_hints[var_sig(attr)][1], attr) for attr in all_vars if domain_hints[var_sig(attr)][0] == role ] attributes = [ attr for place, attr in sorted(attrs_for_role("attribute"), key=lambda a: a[0]) ] classes = [ attr for place, attr in sorted(attrs_for_role("class"), key=lambda a: a[0]) ] metas = [ attr for place, attr in sorted(attrs_for_role("meta"), key=lambda a: a[0]) ] available = [ attr for place, attr in sorted(attrs_for_role("available"), key=lambda a: a[0]) ] self.used_attrs[:] = attributes self.class_attrs[:] = classes self.meta_attrs[:] = metas self.available_attrs[:] = available else: self.used_attrs[:] = [] self.class_attrs[:] = [] self.meta_attrs[:] = [] self.available_attrs[:] = [] self.unconditional_commit() def update_domain_role_hints(self): """ Update the domain hints to be stored in the widgets settings. """ hints_from_model = lambda role, model: [( (attr.name, vartype(attr)), (role, i)) for i, attr in enumerate(model)] hints = dict(hints_from_model("available", self.available_attrs)) hints.update(hints_from_model("attribute", self.used_attrs)) hints.update(hints_from_model("class", self.class_attrs)) hints.update(hints_from_model("meta", self.meta_attrs)) self.domain_role_hints = hints def selected_rows(self, view): """ Return the selected rows in the view. """ rows = view.selectionModel().selectedRows() model = view.model() if isinstance(model, QSortFilterProxyModel): rows = [model.mapToSource(r) for r in rows] return [r.row() for r in rows] def move_rows(self, view, rows, offset): model = view.model() newrows = [min(max(0, row + offset), len(model) - 1) for row in rows] for row, newrow in sorted(zip(rows, newrows), reverse=offset > 0): model[row], model[newrow] = model[newrow], model[row] selection = QItemSelection() for nrow in newrows: index = model.index(nrow, 0) selection.select(index, index) view.selectionModel().select(selection, QItemSelectionModel.ClearAndSelect) self.commit() def move_up(self, view): selected = self.selected_rows(view) self.move_rows(view, selected, -1) def move_down(self, view): selected = self.selected_rows(view) self.move_rows(view, selected, 1) def move_selected(self, view, exclusive=False): if self.selected_rows(view): self.move_selected_from_to(view, self.available_attrs_view) elif self.selected_rows(self.available_attrs_view): self.move_selected_from_to(self.available_attrs_view, view, exclusive) def move_selected_from_to(self, src, dst, exclusive=False): self.move_from_to(src, dst, self.selected_rows(src), exclusive) def move_from_to(self, src, dst, rows, exclusive=False): src_model = source_model(src) attrs = [src_model[r] for r in rows] for s1, s2 in reversed(list(slices(rows))): del src_model[s1:s2] dst_model = source_model(dst) dst_model.extend(attrs) self.commit() def update_interface_state(self, focus=None, selected=None, deselected=None): for view in [ self.available_attrs_view, self.used_attrs_view, self.class_attrs_view, self.meta_attrs_view ]: if view is not focus and not view.hasFocus( ) and self.selected_rows(view): view.selectionModel().clear() def selected_vars(view): model = source_model(view) return [model[i] for i in self.selected_rows(view)] available_selected = selected_vars(self.available_attrs_view) attrs_selected = selected_vars(self.used_attrs_view) class_selected = selected_vars(self.class_attrs_view) meta_selected = selected_vars(self.meta_attrs_view) available_types = set(map(type, available_selected)) all_primitive = all(var.is_primitive() for var in available_types) move_attr_enabled = (available_selected and all_primitive) or \ attrs_selected self.move_attr_button.setEnabled(bool(move_attr_enabled)) if move_attr_enabled: self.move_attr_button.setText(">" if available_selected else "<") move_class_enabled = (all_primitive and available_selected) or class_selected self.move_class_button.setEnabled(bool(move_class_enabled)) if move_class_enabled: self.move_class_button.setText(">" if available_selected else "<") move_meta_enabled = available_selected or meta_selected self.move_meta_button.setEnabled(bool(move_meta_enabled)) if move_meta_enabled: self.move_meta_button.setText(">" if available_selected else "<") def update_completer_model(self, *_): """ This gets called when the model for available attributes changes through either drag/drop or the left/right button actions. """ vars = list(self.available_attrs) items = [var.name for var in vars] items += [ "%s=%s" % item for v in vars for item in v.attributes.items() ] new = sorted(set(items)) if new != self.original_completer_items: self.original_completer_items = new self.completer_model.setStringList(self.original_completer_items) def update_completer_prefix(self, filter): """ Prefixes all items in the completer model with the current already done completion to enable the completion of multiple keywords. """ prefix = str(self.completer.completionPrefix()) if not prefix.endswith(" ") and " " in prefix: prefix, _ = prefix.rsplit(" ", 1) items = [ prefix + " " + item for item in self.original_completer_items ] else: items = self.original_completer_items old = list(map(str, self.completer_model.stringList())) if set(old) != set(items): self.completer_model.setStringList(items) def commit(self): self.update_domain_role_hints() if self.data is not None: attributes = list(self.used_attrs) class_var = list(self.class_attrs) metas = list(self.meta_attrs) domain = Orange.data.Domain(attributes, class_var, metas) newdata = self.data.transform(domain) self.output_data = newdata self.Outputs.data.send(newdata) self.Outputs.features.send(widget.AttributeList(attributes)) else: self.output_data = None self.Outputs.data.send(None) self.Outputs.features.send(None) def reset(self): if self.data is not None: self.available_attrs[:] = [] self.used_attrs[:] = self.data.domain.attributes self.class_attrs[:] = self.data.domain.class_vars self.meta_attrs[:] = self.data.domain.metas self.update_domain_role_hints() self.commit() def send_report(self): if not self.data or not self.output_data: return in_domain, out_domain = self.data.domain, self.output_data.domain self.report_domain("Input data", self.data.domain) if (in_domain.attributes, in_domain.class_vars, in_domain.metas) == (out_domain.attributes, out_domain.class_vars, out_domain.metas): self.report_paragraph("Output data", "No changes.") else: self.report_domain("Output data", self.output_data.domain) diff = list( set(in_domain.variables + in_domain.metas) - set(out_domain.variables + out_domain.metas)) if diff: text = "%i (%s)" % (len(diff), ", ".join(x.name for x in diff)) self.report_items((("Removed", text), ))
class VizRankDialog(QDialog, ProgressBarMixin, WidgetMessagesMixin): """ Base class for VizRank dialogs, providing a GUI with a table and a button, and the skeleton for managing the evaluation of visualizations. Derived classes must provide methods - `iterate_states` for generating combinations (e.g. pairs of attritutes), - `compute_score(state)` for computing the score of a combination, - `row_for_state(state)` that returns a list of items inserted into the table for the given state. and, optionally, - `state_count` that returns the number of combinations (used for progress bar) - `on_selection_changed` that handles event triggered when the user selects a table row. The method should emit signal `VizRankDialog.selectionChanged(object)`. - `bar_length` returns the length of the bar corresponding to the score. The class provides a table and a button. A widget constructs a single instance of this dialog in its `__init__`, like (in Sieve) by using a convenience method :obj:`add_vizrank`:: self.vizrank, self.vizrank_button = SieveRank.add_vizrank( box, self, "Score Combinations", self.set_attr) When the widget receives new data, it must call the VizRankDialog's method :obj:`VizRankDialog.initialize()` to clear the GUI and reset the state. Clicking the Start button calls method `run` (and renames the button to Pause). Run sets up a progress bar by getting the number of combinations from :obj:`VizRankDialog.state_count()`. It restores the paused state (if any) and calls generator :obj:`VizRankDialog.iterate_states()`. For each generated state, it calls :obj:`VizRankDialog.score(state)`, which must return the score (lower is better) for this state. If the returned state is not `None`, the data returned by `row_for_state` is inserted at the appropriate place in the table. Args: master (Orange.widget.OWWidget): widget to which the dialog belongs Attributes: master (Orange.widget.OWWidget): widget to which the dialog belongs captionTitle (str): the caption for the dialog. This can be a class attribute. `captionTitle` is used by the `ProgressBarMixin`. """ captionTitle = "" processingStateChanged = Signal(int) progressBarValueChanged = Signal(float) messageActivated = Signal(Msg) messageDeactivated = Signal(Msg) selectionChanged = Signal(object) class Information(WidgetMessagesMixin.Information): nothing_to_rank = Msg("There is nothing to rank.") def __init__(self, master): """Initialize the attributes and set up the interface""" QDialog.__init__(self, master, windowTitle=self.captionTitle) WidgetMessagesMixin.__init__(self) self.setLayout(QVBoxLayout()) self.insert_message_bar() self.layout().insertWidget(0, self.message_bar) self.master = master self.keep_running = False self.scheduled_call = None self.saved_state = None self.saved_progress = 0 self.scores = [] self.add_to_model = queue.Queue() self.update_timer = QTimer(self) self.update_timer.timeout.connect(self._update) self.update_timer.setInterval(200) self._thread = None self._worker = None self.filter = QLineEdit() self.filter.setPlaceholderText("Filter ...") self.filter.textChanged.connect(self.filter_changed) self.layout().addWidget(self.filter) # Remove focus from line edit self.setFocus(Qt.ActiveWindowFocusReason) self.rank_model = QStandardItemModel(self) self.model_proxy = QSortFilterProxyModel( self, filterCaseSensitivity=False) self.model_proxy.setSourceModel(self.rank_model) self.rank_table = view = QTableView( selectionBehavior=QTableView.SelectRows, selectionMode=QTableView.SingleSelection, showGrid=False, editTriggers=gui.TableView.NoEditTriggers) if self._has_bars: view.setItemDelegate(TableBarItem()) else: view.setItemDelegate(HorizontalGridDelegate()) view.setModel(self.model_proxy) view.selectionModel().selectionChanged.connect( self.on_selection_changed) view.horizontalHeader().setStretchLastSection(True) view.horizontalHeader().hide() self.layout().addWidget(view) self.button = gui.button( self, self, "Start", callback=self.toggle, default=True) @property def _has_bars(self): return type(self).bar_length is not VizRankDialog.bar_length @classmethod def add_vizrank(cls, widget, master, button_label, set_attr_callback): """ Equip the widget with VizRank button and dialog, and monkey patch the widget's `closeEvent` and `hideEvent` to close/hide the vizrank, too. Args: widget (QWidget): the widget into whose layout to insert the button master (Orange.widgets.widget.OWWidget): the master widget button_label: the label for the button set_attr_callback: the callback for setting the projection chosen in the vizrank Returns: tuple with Vizrank dialog instance and push button """ # Monkey patching could be avoided by mixing-in the class (not # necessarily a good idea since we can make a mess of multiple # defined/derived closeEvent and hideEvent methods). Furthermore, # per-class patching would be better than per-instance, but we don't # want to mess with meta-classes either. vizrank = cls(master) button = gui.button( widget, master, button_label, callback=vizrank.reshow, enabled=False) vizrank.selectionChanged.connect(lambda args: set_attr_callback(*args)) master_close_event = master.closeEvent master_hide_event = master.hideEvent master_delete_event = master.onDeleteWidget def closeEvent(event): vizrank.close() master_close_event(event) def hideEvent(event): vizrank.hide() master_hide_event(event) def deleteEvent(): vizrank.keep_running = False if vizrank._thread is not None and vizrank._thread.isRunning(): vizrank._thread.quit() vizrank._thread.wait() master_delete_event() master.closeEvent = closeEvent master.hideEvent = hideEvent master.onDeleteWidget = deleteEvent return vizrank, button def reshow(self): """Put the widget on top of all windows """ self.show() self.raise_() self.activateWindow() def initialize(self): """ Clear and initialize the dialog. This method must be called by the widget when the data is reset, e.g. from `set_data` handler. """ if self._thread is not None and self._thread.isRunning(): self.keep_running = False self._thread.quit() self._thread.wait() self.keep_running = False self.scheduled_call = None self.saved_state = None self.saved_progress = 0 self.update_timer.stop() self.progressBarFinished() self.scores = [] self._update_model() # empty queue self.rank_model.clear() self.button.setText("Start") self.button.setEnabled(self.check_preconditions()) self._thread = QThread(self) self._worker = Worker(self) self._worker.moveToThread(self._thread) self._worker.stopped.connect(self._thread.quit) self._worker.stopped.connect(self._select_first_if_none) self._worker.stopped.connect(self._stopped) self._worker.done.connect(self._done) self._thread.started.connect(self._worker.do_work) def filter_changed(self, text): self.model_proxy.setFilterFixedString(text) def stop_and_reset(self, reset_method=None): if self.keep_running: self.scheduled_call = reset_method or self.initialize self.keep_running = False else: self.initialize() def check_preconditions(self): """Check whether there is sufficient data for ranking.""" return True def on_selection_changed(self, selected, deselected): """ Set the new visualization in the widget when the user select a row in the table. If derived class does not reimplement this, the table gives the information but the user can't click it to select the visualization. Args: selected: the index of the selected item deselected: the index of the previously selected item """ pass def iterate_states(self, initial_state): """ Generate all possible states (e.g. attribute combinations) for the given data. The content of the generated states is specific to the visualization. This method must be defined in the derived classes. Args: initial_state: initial state; None if this is the first call """ raise NotImplementedError def state_count(self): """ Return the number of states for the progress bar. Derived classes should implement this to ensure the proper behaviour of the progress bar""" return 0 def compute_score(self, state): """ Abstract method for computing the score for the given state. Smaller scores are better. Args: state: the state, e.g. the combination of attributes as generated by :obj:`state_count`. """ raise NotImplementedError def bar_length(self, score): """Compute the bar length (between 0 and 1) corresponding to the score. Return `None` if the score cannot be normalized. """ return None def row_for_state(self, score, state): """ Abstract method that return the items that are inserted into the table. Args: score: score, computed by :obj:`compute_score` state: the state, e.g. combination of attributes """ raise NotImplementedError def _select_first_if_none(self): if not self.rank_table.selectedIndexes(): self.rank_table.selectRow(0) def _done(self): self.button.setText("Finished") self.button.setEnabled(False) self.keep_running = False self.saved_state = None def _stopped(self): self.update_timer.stop() self.progressBarFinished() self._update_model() self.stopped() if self.scheduled_call: self.scheduled_call() def _update(self): self._update_model() self._update_progress() def _update_progress(self): self.progressBarSet(int(self.saved_progress * 100 / max(1, self.state_count()))) def _update_model(self): try: while True: pos, row_items = self.add_to_model.get_nowait() self.rank_model.insertRow(pos, row_items) except queue.Empty: pass def toggle(self): """Start or pause the computation.""" self.keep_running = not self.keep_running if self.keep_running: self.button.setText("Pause") self.progressBarInit() self.update_timer.start() self.before_running() self._thread.start() else: self.button.setText("Continue") self._thread.quit() # Need to sync state (the worker must read the keep_running # state and stop) for reliable restart. self._thread.wait() def before_running(self): """Code that is run before running vizrank in its own thread""" pass def stopped(self): """Code that is run after stopping the vizrank thread""" pass
class OWPCA(widget.OWWidget): name = "PCA" description = "Principal component analysis with a scree-diagram." icon = "icons/PCA.svg" priority = 3050 inputs = [("Data", Table, "set_data")] outputs = [("Transformed data", Table), ("Components", Table), ("PCA", PCA)] ncomponents = settings.Setting(2) variance_covered = settings.Setting(100) batch_size = settings.Setting(100) address = settings.Setting('') auto_update = settings.Setting(True) auto_commit = settings.Setting(True) normalize = settings.Setting(True) maxp = settings.Setting(20) axis_labels = settings.Setting(10) graph_name = "plot.plotItem" def __init__(self): super().__init__() self.data = None self._pca = None self._transformed = None self._variance_ratio = None self._cumulative = None self._line = False self._pca_projector = PCA() self._pca_projector.component = self.ncomponents self._pca_preprocessors = PCA.preprocessors # Components Selection box = gui.vBox(self.controlArea, "Components Selection") form = QFormLayout() box.layout().addLayout(form) self.components_spin = gui.spin( box, self, "ncomponents", 0, 1000, callback=self._update_selection_component_spin, keyboardTracking=False) self.components_spin.setSpecialValueText("All") self.variance_spin = gui.spin( box, self, "variance_covered", 1, 100, callback=self._update_selection_variance_spin, keyboardTracking=False) self.variance_spin.setSuffix("%") form.addRow("Components:", self.components_spin) form.addRow("Variance covered:", self.variance_spin) # Incremental learning self.sampling_box = gui.vBox(self.controlArea, "Incremental learning") self.addresstext = QLineEdit(box) self.addresstext.setPlaceholderText('Remote server') if self.address: self.addresstext.setText(self.address) self.sampling_box.layout().addWidget(self.addresstext) form = QFormLayout() self.sampling_box.layout().addLayout(form) self.batch_spin = gui.spin(self.sampling_box, self, "batch_size", 50, 100000, step=50, keyboardTracking=False) form.addRow("Batch size ~ ", self.batch_spin) self.start_button = gui.button( self.sampling_box, self, "Start remote computation", callback=self.start, autoDefault=False, tooltip="Start/abort computation on the server") self.start_button.setEnabled(False) gui.checkBox(self.sampling_box, self, "auto_update", "Periodically fetch model", callback=self.update_model) self.__timer = QTimer(self, interval=2000) self.__timer.timeout.connect(self.get_model) self.sampling_box.setVisible(remotely) # Options self.options_box = gui.vBox(self.controlArea, "Options") gui.checkBox(self.options_box, self, "normalize", "Normalize data", callback=self._update_normalize) self.maxp_spin = gui.spin(self.options_box, self, "maxp", 1, 100, label="Show only first", callback=self._setup_plot, keyboardTracking=False) self.controlArea.layout().addStretch() gui.auto_commit(self.controlArea, self, "auto_commit", "Apply", checkbox_label="Apply automatically") self.plot = pg.PlotWidget(background="w") axis = self.plot.getAxis("bottom") axis.setLabel("Principal Components") axis = self.plot.getAxis("left") axis.setLabel("Proportion of variance") self.plot_horlabels = [] self.plot_horlines = [] self.plot.getViewBox().setMenuEnabled(False) self.plot.getViewBox().setMouseEnabled(False, False) self.plot.showGrid(True, True, alpha=0.5) self.plot.setRange(xRange=(0.0, 1.0), yRange=(0.0, 1.0)) self.mainArea.layout().addWidget(self.plot) self._update_normalize() def update_model(self): self.get_model() if self.auto_update and self.rpca and not self.rpca.ready(): self.__timer.start(2000) else: self.__timer.stop() def start(self): if 'Abort' in self.start_button.text(): self.rpca.abort() self.__timer.stop() self.start_button.setText("Start remote computation") else: self.address = self.addresstext.text() with remote.server(self.address): from Orange.projection.pca import RemotePCA maxiter = (1e5 + self.data.approx_len()) / self.batch_size * 3 self.rpca = RemotePCA(self.data, self.batch_size, int(maxiter)) self.update_model() self.start_button.setText("Abort remote computation") def set_data(self, data): self.information() if isinstance(data, SqlTable): if data.approx_len() < AUTO_DL_LIMIT: data = Table(data) elif not remotely: self.information("Data has been sampled") data_sample = data.sample_time(1, no_cache=True) data_sample.download_data(2000, partial=True) data = Table(data_sample) self.data = data self.fit() def fit(self): self.clear() self.start_button.setEnabled(False) if self.data is None: return data = self.data self._transformed = None if isinstance(data, SqlTable): # data was big and remote available self.sampling_box.setVisible(True) self.start_button.setText("Start remote computation") self.start_button.setEnabled(True) else: self.sampling_box.setVisible(False) pca = self._pca_projector(data) variance_ratio = pca.explained_variance_ratio_ cumulative = numpy.cumsum(variance_ratio) self.components_spin.setRange(0, len(cumulative)) self._pca = pca self._variance_ratio = variance_ratio self._cumulative = cumulative self._setup_plot() self.unconditional_commit() def clear(self): self._pca = None self._transformed = None self._variance_ratio = None self._cumulative = None self._line = None self.plot_horlabels = [] self.plot_horlines = [] self.plot.clear() def get_model(self): if self.rpca is None: return if self.rpca.ready(): self.__timer.stop() self.start_button.setText("Restart (finished)") self._pca = self.rpca.get_state() if self._pca is None: return self._variance_ratio = self._pca.explained_variance_ratio_ self._cumulative = numpy.cumsum(self._variance_ratio) self._setup_plot() self._transformed = None self.commit() def _setup_plot(self): self.plot.clear() explained_ratio = self._variance_ratio explained = self._cumulative p = min(len(self._variance_ratio), self.maxp) self.plot.plot(numpy.arange(p), explained_ratio[:p], pen=pg.mkPen(QColor(Qt.red), width=2), antialias=True, name="Variance") self.plot.plot(numpy.arange(p), explained[:p], pen=pg.mkPen(QColor(Qt.darkYellow), width=2), antialias=True, name="Cumulative Variance") cutpos = self._nselected_components() - 1 self._line = pg.InfiniteLine(angle=90, pos=cutpos, movable=True, bounds=(0, p - 1)) self._line.setCursor(Qt.SizeHorCursor) self._line.setPen(pg.mkPen(QColor(Qt.black), width=2)) self._line.sigPositionChanged.connect(self._on_cut_changed) self.plot.addItem(self._line) self.plot_horlines = ( pg.PlotCurveItem(pen=pg.mkPen(QColor(Qt.blue), style=Qt.DashLine)), pg.PlotCurveItem(pen=pg.mkPen(QColor(Qt.blue), style=Qt.DashLine))) self.plot_horlabels = (pg.TextItem(color=QColor(Qt.black), anchor=(1, 0)), pg.TextItem(color=QColor(Qt.black), anchor=(1, 1))) for item in self.plot_horlabels + self.plot_horlines: self.plot.addItem(item) self._set_horline_pos() self.plot.setRange(xRange=(0.0, p - 1), yRange=(0.0, 1.0)) self._update_axis() def _set_horline_pos(self): cutidx = self.ncomponents - 1 for line, label, curve in zip( self.plot_horlines, self.plot_horlabels, (self._variance_ratio, self._cumulative)): y = curve[cutidx] line.setData([-1, cutidx], 2 * [y]) label.setPos(cutidx, y) label.setPlainText("{:.3f}".format(y)) def _on_cut_changed(self, line): # cut changed by means of a cut line over the scree plot. value = int(round(line.value())) self._line.setValue(value) current = self._nselected_components() components = value + 1 if not (self.ncomponents == 0 and components == len(self._variance_ratio)): self.ncomponents = components self._set_horline_pos() if self._pca is not None: self.variance_covered = self._cumulative[components - 1] * 100 if current != self._nselected_components(): self._invalidate_selection() def _update_selection_component_spin(self): # cut changed by "ncomponents" spin. if self._pca is None: self._invalidate_selection() return if self.ncomponents == 0: # Special "All" value cut = len(self._variance_ratio) else: cut = self.ncomponents self.variance_covered = self._cumulative[cut - 1] * 100 if numpy.floor(self._line.value()) + 1 != cut: self._line.setValue(cut - 1) self._invalidate_selection() def _update_selection_variance_spin(self): # cut changed by "max variance" spin. if self._pca is None: return cut = numpy.searchsorted(self._cumulative, self.variance_covered / 100.0) + 1 self.ncomponents = cut if numpy.floor(self._line.value()) + 1 != cut: self._line.setValue(cut - 1) self._invalidate_selection() def _update_normalize(self): if self.normalize: pp = self._pca_preprocessors + [Normalize()] else: pp = self._pca_preprocessors self._pca_projector.preprocessors = pp self.fit() if self.data is None: self._invalidate_selection() def _nselected_components(self): """Return the number of selected components.""" if self._pca is None: return 0 if self.ncomponents == 0: # Special "All" value max_comp = len(self._variance_ratio) else: max_comp = self.ncomponents var_max = self._cumulative[max_comp - 1] if var_max != numpy.floor(self.variance_covered / 100.0): cut = max_comp self.variance_covered = var_max * 100 else: self.ncomponents = cut = numpy.searchsorted( self._cumulative, self.variance_covered / 100.0) + 1 return cut def _invalidate_selection(self): self.commit() def _update_axis(self): p = min(len(self._variance_ratio), self.maxp) axis = self.plot.getAxis("bottom") d = max((p - 1) // (self.axis_labels - 1), 1) axis.setTicks([[(i, str(i + 1)) for i in range(0, p, d)]]) def commit(self): transformed = components = None if self._pca is not None: if self._transformed is None: # Compute the full transform (all components) only once. self._transformed = self._pca(self.data) transformed = self._transformed domain = Domain(transformed.domain.attributes[:self.ncomponents], self.data.domain.class_vars, self.data.domain.metas) transformed = transformed.from_table(domain, transformed) dom = Domain(self._pca.orig_domain.attributes, metas=[StringVariable(name='component')]) metas = numpy.array( [['PC{}'.format(i + 1) for i in range(self.ncomponents)]], dtype=object).T components = Table(dom, self._pca.components_[:self.ncomponents], metas=metas) components.name = 'components' self._pca_projector.component = self.ncomponents self.send("Transformed data", transformed) self.send("Components", components) self.send("PCA", self._pca_projector) def send_report(self): if self.data is None: return self.report_items( (("Selected components", self.ncomponents), ("Explained variance", "{:.3f} %".format(self.variance_covered)))) self.report_plot()
class OWMarkerGenes(widget.OWWidget): name = "Marker Genes" icon = 'icons/OWMarkerGenes.svg' priority = 130 replaces = ['orangecontrib.single_cell.widgets.owmarkergenes.OWMarkerGenes'] class Warning(widget.OWWidget.Warning): using_local_files = widget.Msg("Can't connect to serverfiles. Using cached files.") class Outputs: genes = widget.Output("Genes", Table) want_main_area = True want_control_area = True auto_commit = Setting(True) selected_source = Setting("") selected_organism = Setting("") selected_root_attribute = Setting(0) settingsHandler = MarkerGroupContextHandler() # noqa: N815 selected_genes = settings.ContextSetting([]) settings_version = 2 _data = None _available_sources = None def __init__(self) -> None: super().__init__() # define the layout main_area = QWidget(self.mainArea) self.mainArea.layout().addWidget(main_area) layout = QGridLayout() main_area.setLayout(layout) layout.setContentsMargins(4, 4, 4, 4) # filter line edit self.filter_line_edit = QLineEdit() self.filter_line_edit.setPlaceholderText("Filter marker genes") layout.addWidget(self.filter_line_edit, 0, 0, 1, 3) # define available markers view self.available_markers_view = TreeView() box = gui.vBox(self.mainArea, "Available markers", addToLayout=False) box.layout().addWidget(self.available_markers_view) layout.addWidget(box, 1, 0, 2, 1) # create selected markers view self.selected_markers_view = TreeView() box = gui.vBox(self.mainArea, "Selected markers", addToLayout=False) box.layout().addWidget(self.selected_markers_view) layout.addWidget(box, 1, 2, 2, 1) self.available_markers_view.otherView = self.selected_markers_view self.selected_markers_view.otherView = self.available_markers_view # buttons box = gui.vBox(self.mainArea, addToLayout=False, margin=0) layout.addWidget(box, 1, 1, 1, 1) self.move_button = gui.button(box, self, ">", callback=self._move_selected) self._init_description_area(layout) self._init_control_area() self._load_data() def _init_description_area(self, layout: QLayout) -> None: """ Function define an info area with description of the genes and add it to the layout. """ box = gui.widgetBox(self.mainArea, "Description", addToLayout=False) self.descriptionlabel = QTextBrowser( openExternalLinks=True, textInteractionFlags=(Qt.TextSelectableByMouse | Qt.LinksAccessibleByMouse) ) box.setMaximumHeight(self.descriptionlabel.fontMetrics().height() * (NUM_LINES_TEXT + 3)) # description filed self.descriptionlabel.setText("Select a gene to see information.") self.descriptionlabel.setFrameStyle(QTextBrowser.NoFrame) # no (white) text background self.descriptionlabel.viewport().setAutoFillBackground(False) box.layout().addWidget(self.descriptionlabel) layout.addWidget(box, 3, 0, 1, 3) def _init_control_area(self) -> None: """ Function defines dropdowns and the button in the control area. """ box = gui.widgetBox(self.controlArea, 'Database', margin=0) self.source_index = -1 self.db_source_cb = gui.comboBox(box, self, 'source_index') self.db_source_cb.activated[int].connect(self._set_db_source_index) box = gui.widgetBox(self.controlArea, 'Organism', margin=0) self.organism_index = -1 self.group_cb = gui.comboBox(box, self, 'organism_index') self.group_cb.activated[int].connect(self._set_group_index) box = gui.widgetBox(self.controlArea, 'Group by', margin=0) self.group_by_cb = gui.comboBox( box, self, 'selected_root_attribute', items=GROUP_BY_ITEMS, callback=self._setup ) gui.rubber(self.controlArea) gui.auto_commit(self.controlArea, self, "auto_commit", "Commit") def sizeHint(self): return super().sizeHint().expandedTo(QSize(900, 500)) @property def available_sources(self) -> dict: return self._available_sources @available_sources.setter def available_sources(self, value: dict) -> None: """ Set _available_sources variable, add them to dropdown, and select the source that was previously. """ self._available_sources = value items = sorted(list(value.keys()), reverse=True) # panglao first try: idx = items.index(self.selected_source) except ValueError: idx = -1 self.db_source_cb.clear() self.db_source_cb.addItems(items) if idx != -1: self.source_index = idx self.selected_source = items[idx] elif items: self.source_index = min(max(self.source_index, 0), len(items) - 1) self._set_db_source_index(self.source_index) @property def data(self) -> Table: return self._data @data.setter def data(self, value: Table): """ Set the source data. The data is then filtered on the first meta column (group). Select set dropdown with the groups and select the one that was selected previously. """ self._data = value domain = value.domain if domain.metas: group = domain.metas[0] groupcol, _ = value.get_column_view(group) if group.is_string: group_values = list(set(groupcol)) elif group.is_discrete: group_values = group.values else: raise TypeError("Invalid column type") group_values = sorted(group_values) # human first try: idx = group_values.index(self.selected_organism) except ValueError: idx = -1 self.group_cb.clear() self.group_cb.addItems(group_values) if idx != -1: self.organism_index = idx self.selected_organism = group_values[idx] elif group_values: self.organism_index = min(max(self.organism_index, 0), len(group_values) - 1) self._set_group_index(self.organism_index) def _load_data(self) -> None: """ Collect available data sources (marker genes data sets). """ self.Warning.using_local_files.clear() found_sources = {} try: found_sources.update(serverfiles.ServerFiles().allinfo(SERVER_FILES_DOMAIN)) except requests.exceptions.ConnectionError: found_sources.update(serverfiles.allinfo(SERVER_FILES_DOMAIN)) self.Warning.using_local_files() self.available_sources = {item.get('title').split(': ')[-1]: item for item in found_sources.values()} def _source_changed(self) -> None: """ Respond on change of the source and download the data. """ if self.available_sources: file_name = self.available_sources[self.selected_source]['filename'] try: serverfiles.update(SERVER_FILES_DOMAIN, file_name) except requests.exceptions.ConnectionError: # try to update file. Ignore network errors. pass try: file_path = serverfiles.localpath_download(SERVER_FILES_DOMAIN, file_name) except requests.exceptions.ConnectionError as err: # Unexpected error. raise err self.data = Table.from_file(file_path) def _setup(self) -> None: """ Setup the views with data. """ self.closeContext() self.selected_genes = [] self.openContext((self.selected_organism, self.selected_source)) data_not_selected, data_selected = self._filter_data_group(self.data) # add model to available markers view group_by = GROUP_BY_ITEMS[self.selected_root_attribute] tree_model = TreeModel(data_not_selected, group_by) proxy_model = FilterProxyModel(self.filter_line_edit) proxy_model.setSourceModel(tree_model) self.available_markers_view.setModel(proxy_model) self.available_markers_view.selectionModel().selectionChanged.connect( partial(self._on_selection_changed, self.available_markers_view) ) tree_model = TreeModel(data_selected, group_by) proxy_model = FilterProxyModel(self.filter_line_edit) proxy_model.setSourceModel(tree_model) self.selected_markers_view.setModel(proxy_model) self.selected_markers_view.selectionModel().selectionChanged.connect( partial(self._on_selection_changed, self.selected_markers_view) ) self.selected_markers_view.model().sourceModel().data_added.connect(self._selected_markers_changed) self.selected_markers_view.model().sourceModel().data_removed.connect(self._selected_markers_changed) # update output and messages self._selected_markers_changed() def _filter_data_group(self, data: Table) -> Tuple[Table, Tuple]: """ Function filter the table based on the selected group (Mouse, Human) and divide them in two groups based on selected_data variable. Parameters ---------- data Table to be filtered Returns ------- data_not_selected Data that will initially be in available markers view. data_selected Data that will initially be in selected markers view. """ group = data.domain.metas[0] gvec = data.get_column_view(group)[0] if group.is_string: mask = gvec == self.selected_organism else: mask = gvec == self.organism_index data = data[mask] # divide data based on selected_genes variable (context) unique_gene_names = np.core.defchararray.add( data.get_column_view("Entrez ID")[0].astype(str), data.get_column_view("Cell Type")[0].astype(str) ) mask = np.isin(unique_gene_names, self.selected_genes) data_not_selected = data[~mask] data_selected = data[mask] return data_not_selected, data_selected def commit(self) -> None: rows = self.selected_markers_view.model().sourceModel().rootItem.get_data_rows() if len(rows) > 0: metas = [r.metas for r in rows] data = Table.from_numpy(self.data.domain, np.empty((len(metas), 0)), metas=np.array(metas)) # always false for marker genes data tables in single cell data.attributes[GENE_AS_ATTRIBUTE_NAME] = False # set taxonomy id in data.attributes data.attributes[TAX_ID] = MAP_GROUP_TO_TAX_ID.get(self.selected_organism, '') # set column id flag data.attributes[GENE_ID_COLUMN] = "Entrez ID" data.name = 'Marker Genes' else: data = None self.Outputs.genes.send(data) def _update_description(self, view: TreeView) -> None: """ Upate the description about the gene. Only in case when one gene is selected. """ selection = self._selected_rows(view) qmodel = view.model().sourceModel() if len(selection) > 1 or len(selection) == 0 or qmodel.node_from_index(selection[0]).data_row is None: self.descriptionlabel.setText("Select a gene to see information.") else: data_row = qmodel.node_from_index(selection[0]).data_row self.descriptionlabel.setHtml( f"<b>Gene name:</b> {data_row['Name']}<br/>" f"<b>Entrez ID:</b> {data_row['Entrez ID']}<br/>" f"<b>Cell Type:</b> {data_row['Cell Type']}<br/>" f"<b>Function:</b> {data_row['Function']}<br/>" f"<b>Reference:</b> <a href='{data_row['URL']}'>{data_row['Reference']}</a>" ) def _update_data_info(self) -> None: """ Updates output info in the control area. """ sel_model = self.selected_markers_view.model().sourceModel() self.info.set_output_summary(f"Selected: {str(len(sel_model))}") # callback functions def _selected_markers_changed(self) -> None: """ This function is called when markers in the selected view are added or removed. """ rows = self.selected_markers_view.model().sourceModel().rootItem.get_data_rows() self.selected_genes = [row["Entrez ID"].value + row["Cell Type"].value for row in rows] self._update_data_info() self.commit() def _on_selection_changed(self, view: TreeView) -> None: """ When selection in one of the view changes in a view button should change a sign in the correct direction and other view should reset the selection. Also gene description is updated. """ self.move_button.setText(">" if view is self.available_markers_view else "<") if view is self.available_markers_view: self.selected_markers_view.clearSelection() else: self.available_markers_view.clearSelection() self._update_description(view) def _set_db_source_index(self, source_index: int) -> None: """ Set the index of selected database source - index in a dropdown. """ self.source_index = source_index self.selected_source = self.db_source_cb.itemText(source_index) self._source_changed() def _set_group_index(self, group_index: int) -> None: """ Set the index of organism - index in a dropdown. """ self.organism_index = group_index self.selected_organism = self.group_cb.itemText(group_index) self._setup() def _move_selected(self) -> None: """ Move selected genes when button clicked. """ if self._selected_rows(self.selected_markers_view): self._move_selected_from_to(self.selected_markers_view, self.available_markers_view) elif self._selected_rows(self.available_markers_view): self._move_selected_from_to(self.available_markers_view, self.selected_markers_view) # support functions for callbacks def _move_selected_from_to(self, src: TreeView, dst: TreeView) -> None: """ Function moves items from src model to dst model. """ selected_items = self._selected_rows(src) src_model = src.model().sourceModel() dst_model = dst.model().sourceModel() # move data as mimeData from source to destination tree view mime_data = src_model.mimeData(selected_items) # remove nodes from the source view src_model.remove_node_list(selected_items) dst_model.dropMimeData(mime_data, Qt.MoveAction, -1, -1) @staticmethod def _selected_rows(view: TreeView) -> List[QModelIndex]: """ Return the selected rows in the view. """ rows = view.selectionModel().selectedRows() return list(map(view.model().mapToSource, rows)) @classmethod def migrate_settings(cls, settings, version=0): def migrate_to_version_2(): settings["selected_source"] = settings.pop("selected_db_source", "") settings["selected_organism"] = settings.pop("selected_group", "") if "context_settings" in settings: for co in settings["context_settings"]: co.values["selected_genes"] = [g[0] + g[1] for g in co.values["selected_genes"]] if version < 2: migrate_to_version_2()
class ComboBoxSearch(QComboBox): """ A drop down list combo box with filter/search. The popup list view is filtered by text entered in the filter field. Note ---- `popup`, `lineEdit` and `completer` from the base QComboBox class are unused. Setting/modifying them will have no effect. """ # NOTE: Setting editable + QComboBox.NoInsert policy + ... did not achieve # the same results. def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__searchline = QLineEdit(self, visible=False, frame=False) self.__searchline.setAttribute(Qt.WA_MacShowFocusRect, False) self.__searchline.setFocusProxy(self) self.__popup = None # type: Optional[QAbstractItemModel] self.__proxy = None # type: Optional[QSortFilterProxyModel] self.__popupTimer = QElapsedTimer() self.setFocusPolicy(Qt.ClickFocus | Qt.TabFocus) def showPopup(self): # type: () -> None """ Reimplemented from QComboBox.showPopup Popup up a customized view and filter edit line. Note ---- The .popup(), .lineEdit(), .completer() of the base class are not used. """ if self.__popup is not None: # We have user entered state that cannot be disturbed # (entered filter text, scroll offset, ...) return # pragma: no cover if self.count() == 0: return opt = QStyleOptionComboBox() self.initStyleOption(opt) popup = QListView( uniformItemSizes=True, horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff, verticalScrollBarPolicy=Qt.ScrollBarAsNeeded, iconSize=self.iconSize(), ) popup.setFocusProxy(self.__searchline) popup.setParent(self, Qt.Popup | Qt.FramelessWindowHint) popup.setItemDelegate(_ComboBoxListDelegate(popup)) proxy = QSortFilterProxyModel( popup, filterCaseSensitivity=Qt.CaseInsensitive ) proxy.setFilterKeyColumn(self.modelColumn()) proxy.setSourceModel(self.model()) popup.setModel(proxy) root = proxy.mapFromSource(self.rootModelIndex()) popup.setRootIndex(root) self.__popup = popup self.__proxy = proxy self.__searchline.setText("") self.__searchline.setPlaceholderText("Filter...") self.__searchline.setVisible(True) self.__searchline.textEdited.connect(proxy.setFilterFixedString) style = self.style() # type: QStyle popuprect_origin = style.subControlRect( QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxListBoxPopup, self ) # type: QRect popuprect_origin = QRect( self.mapToGlobal(popuprect_origin.topLeft()), popuprect_origin.size() ) editrect = style.subControlRect( QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxEditField, self ) # type: QRect self.__searchline.setGeometry(editrect) desktop = QApplication.desktop() screenrect = desktop.availableGeometry(self) # type: QRect # get the height for the view listrect = QRect() for i in range(min(proxy.rowCount(root), self.maxVisibleItems())): index = proxy.index(i, self.modelColumn(), root) if index.isValid(): listrect = listrect.united(popup.visualRect(index)) if listrect.height() >= screenrect.height(): break window = popup.window() # type: QWidget window.ensurePolished() if window.layout() is not None: window.layout().activate() else: QApplication.sendEvent(window, QEvent(QEvent.LayoutRequest)) margins = qwidget_margin_within(popup.viewport(), window) height = (listrect.height() + 2 * popup.spacing() + margins.top() + margins.bottom()) popup_size = (QSize(popuprect_origin.width(), height) .expandedTo(window.minimumSize()) .boundedTo(window.maximumSize()) .boundedTo(screenrect.size())) popuprect = QRect(popuprect_origin.bottomLeft(), popup_size) popuprect = dropdown_popup_geometry( popuprect, popuprect_origin, screenrect) popup.setGeometry(popuprect) current = proxy.mapFromSource( self.model().index(self.currentIndex(), self.modelColumn(), self.rootModelIndex())) popup.setCurrentIndex(current) popup.scrollTo(current, QAbstractItemView.EnsureVisible) popup.show() popup.setFocus(Qt.PopupFocusReason) popup.installEventFilter(self) popup.viewport().installEventFilter(self) popup.viewport().setMouseTracking(True) self.update() self.__popupTimer.restart() def hidePopup(self): """Reimplemented""" if self.__popup is not None: popup = self.__popup self.__popup = self.__proxy = None popup.setFocusProxy(None) popup.hide() popup.deleteLater() popup.removeEventFilter(self) popup.viewport().removeEventFilter(self) # need to call base hidePopup even though the base showPopup was not # called (update internal state wrt. 'pressed' arrow, ...) super().hidePopup() self.__searchline.hide() self.update() def initStyleOption(self, option): # type: (QStyleOptionComboBox) -> None super().initStyleOption(option) option.editable = True def __updateGeometries(self): opt = QStyleOptionComboBox() self.initStyleOption(opt) editarea = self.style().subControlRect( QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxEditField, self) self.__searchline.setGeometry(editarea) def resizeEvent(self, event): """Reimplemented.""" super().resizeEvent(event) self.__updateGeometries() def paintEvent(self, event): """Reimplemented.""" opt = QStyleOptionComboBox() self.initStyleOption(opt) painter = QStylePainter(self) painter.drawComplexControl(QStyle.CC_ComboBox, opt) if not self.__searchline.isVisibleTo(self): opt.editable = False painter.drawControl(QStyle.CE_ComboBoxLabel, opt) def eventFilter(self, obj, event): # pylint: disable=too-many-branches # type: (QObject, QEvent) -> bool """Reimplemented.""" etype = event.type() if etype == QEvent.FocusOut and self.__popup is not None: self.hidePopup() return True if etype == QEvent.Hide and self.__popup is not None: self.hidePopup() return False if etype == QEvent.KeyPress or etype == QEvent.KeyRelease or \ etype == QEvent.ShortcutOverride and obj is self.__popup: event = event # type: QKeyEvent key, modifiers = event.key(), event.modifiers() if key in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Select): current = self.__popup.currentIndex() if current.isValid(): self.__activateProxyIndex(current) elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown): return False # elif key in (Qt.Key_Tab, Qt.Key_Backtab): pass elif key == Qt.Key_Escape or \ (key == Qt.Key_F4 and modifiers & Qt.AltModifier): self.__popup.hide() return True else: # pass the input events to the filter edit line (no propagation # up the parent chain). self.__searchline.event(event) if event.isAccepted(): return True if etype == QEvent.MouseButtonRelease and self.__popup is not None \ and obj is self.__popup.viewport() \ and self.__popupTimer.elapsed() >= \ QApplication.doubleClickInterval(): event = event # type: QMouseEvent index = self.__popup.indexAt(event.pos()) if index.isValid(): self.__activateProxyIndex(index) if etype == QEvent.MouseMove and self.__popup is not None \ and obj is self.__popup.viewport(): event = event # type: QMouseEvent opt = QStyleOptionComboBox() self.initStyleOption(opt) style = self.style() # type: QStyle if style.styleHint(QStyle.SH_ComboBox_ListMouseTracking, opt, self): index = self.__popup.indexAt(event.pos()) if index.isValid() and \ index.flags() & (Qt.ItemIsEnabled | Qt.ItemIsSelectable): self.__popup.setCurrentIndex(index) if etype == QEvent.MouseButtonPress and self.__popup is obj: # Popup border or out of window mouse button press/release. # At least on windows this needs to be handled. style = self.style() opt = QStyleOptionComboBox() self.initStyleOption(opt) opt.subControls = QStyle.SC_All opt.activeSubControls = QStyle.SC_ComboBoxArrow pos = self.mapFromGlobal(event.globalPos()) sc = style.hitTestComplexControl(QStyle.CC_ComboBox, opt, pos, self) if sc != QStyle.SC_None: self.__popup.setAttribute(Qt.WA_NoMouseReplay) self.hidePopup() return super().eventFilter(obj, event) def __activateProxyIndex(self, index): # type: (QModelIndex) -> None # Set current and activate the source index corresponding to the proxy # index in the popup's model. if self.__popup is not None and index.isValid(): proxy = self.__popup.model() assert index.model() is proxy index = proxy.mapToSource(index) assert index.model() is self.model() if index.isValid() and \ index.flags() & (Qt.ItemIsEnabled | Qt.ItemIsSelectable): self.hidePopup() text = self.itemText(index.row()) self.setCurrentIndex(index.row()) self.activated[int].emit(index.row()) self.activated[str].emit(text)
class OWBaseSql(OWWidget, openclass=True): """Base widget for connecting to a database. Override `get_backend` when subclassing to get corresponding backend. """ class Outputs: data = Output("Data", Table) class Error(OWWidget.Error): connection = Msg("{}") want_main_area = False resizing_enabled = False host = Setting(None) # type: Optional[str] port = Setting(None) # type: Optional[str] database = Setting(None) # type: Optional[str] schema = Setting(None) # type: Optional[str] username = "" password = "" def __init__(self): super().__init__() self.backend = None # type: Optional[Backend] self.data_desc_table = None # type: Optional[Table] self.database_desc = None # type: Optional[OrderedDict] self._setup_gui() self.connect() def _setup_gui(self): self.controlArea.setMinimumWidth(360) vbox = gui.vBox(self.controlArea, "Server", addSpace=True) self.serverbox = gui.vBox(vbox) self.servertext = QLineEdit(self.serverbox) self.servertext.setPlaceholderText("Server") self.servertext.setToolTip("Server") self.servertext.editingFinished.connect(self._load_credentials) if self.host: self.servertext.setText(self.host if not self.port else "{}:{}".format(self.host, self.port)) self.serverbox.layout().addWidget(self.servertext) self.databasetext = QLineEdit(self.serverbox) self.databasetext.setPlaceholderText("Database[/Schema]") self.databasetext.setToolTip("Database or optionally Database/Schema") if self.database: self.databasetext.setText( self.database if not self.schema else "{}/{}".format(self.database, self.schema)) self.serverbox.layout().addWidget(self.databasetext) self.usernametext = QLineEdit(self.serverbox) self.usernametext.setPlaceholderText("Username") self.usernametext.setToolTip("Username") self.serverbox.layout().addWidget(self.usernametext) self.passwordtext = QLineEdit(self.serverbox) self.passwordtext.setPlaceholderText("Password") self.passwordtext.setToolTip("Password") self.passwordtext.setEchoMode(QLineEdit.Password) self.serverbox.layout().addWidget(self.passwordtext) self._load_credentials() self.connectbutton = gui.button(self.serverbox, self, "Connect", callback=self.connect) self.connectbutton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) def _load_credentials(self): self._parse_host_port() cm = self._credential_manager(self.host, self.port) self.username = cm.username self.password = cm.password if self.username: self.usernametext.setText(self.username) if self.password: self.passwordtext.setText(self.password) def _save_credentials(self): cm = self._credential_manager(self.host, self.port) cm.username = self.username or "" cm.password = self.password or "" @staticmethod def _credential_manager(host, port): return CredentialManager("SQL Table: {}:{}".format(host, port)) def _parse_host_port(self): hostport = self.servertext.text().split(":") self.host = hostport[0] self.port = hostport[1] if len(hostport) == 2 else None def _check_db_settings(self): self._parse_host_port() self.database, _, self.schema = self.databasetext.text().partition("/") self.username = self.usernametext.text() or None self.password = self.passwordtext.text() or None def connect(self): self.clear() self._check_db_settings() if not self.host or not self.database: return try: backend = self.get_backend() if backend is None: return self.backend = backend(dict( host=self.host, port=self.port, database=self.database, user=self.username, password=self.password )) self.on_connection_success() except BackendError as err: self.on_connection_error(err) def get_backend(self) -> Type[Backend]: """ Derived widgets should override this to get corresponding backend. Returns ------- backend: Type[Backend] """ raise NotImplementedError def on_connection_success(self): self._save_credentials() self.database_desc = OrderedDict(( ("Host", self.host), ("Port", self.port), ("Database", self.database), ("User name", self.username) )) def on_connection_error(self, err): error = str(err).split("\n")[0] self.Error.connection(error) def open_table(self): data = self.get_table() self.data_desc_table = data self.Outputs.data.send(data) info = str(len(data)) if data else self.info.NoOutput self.info.set_output_summary(info) def get_table(self) -> Table: """ Derived widgets should override this to get corresponding table. Returns ------- table: Table """ raise NotImplementedError def clear(self): self.Error.connection.clear() self.database_desc = None self.data_desc_table = None self.Outputs.data.send(None) self.info.set_output_summary(self.info.NoOutput) def send_report(self): if not self.database_desc: self.report_paragraph("No database connection.") return self.report_items("Database", self.database_desc) if self.data_desc_table: self.report_items("Data", report.describe_data(self.data_desc_table))
class OWPCA(widget.OWWidget): name = "PCA" description = "Principal component analysis with a scree-diagram." icon = "icons/PCA.svg" priority = 3050 inputs = [("Data", Table, "set_data")] outputs = [("Transformed data", Table), ("Components", Table), ("PCA", PCA)] ncomponents = settings.Setting(2) variance_covered = settings.Setting(100) batch_size = settings.Setting(100) address = settings.Setting('') auto_update = settings.Setting(True) auto_commit = settings.Setting(True) normalize = settings.Setting(True) maxp = settings.Setting(20) axis_labels = settings.Setting(10) graph_name = "plot.plotItem" def __init__(self): super().__init__() self.data = None self._pca = None self._transformed = None self._variance_ratio = None self._cumulative = None self._line = False self._pca_projector = PCA() self._pca_projector.component = self.ncomponents self._pca_preprocessors = PCA.preprocessors # Components Selection box = gui.vBox(self.controlArea, "Components Selection") form = QFormLayout() box.layout().addLayout(form) self.components_spin = gui.spin( box, self, "ncomponents", 0, 1000, callback=self._update_selection_component_spin, keyboardTracking=False ) self.components_spin.setSpecialValueText("All") self.variance_spin = gui.spin( box, self, "variance_covered", 1, 100, callback=self._update_selection_variance_spin, keyboardTracking=False ) self.variance_spin.setSuffix("%") form.addRow("Components:", self.components_spin) form.addRow("Variance covered:", self.variance_spin) # Incremental learning self.sampling_box = gui.vBox(self.controlArea, "Incremental learning") self.addresstext = QLineEdit(box) self.addresstext.setPlaceholderText('Remote server') if self.address: self.addresstext.setText(self.address) self.sampling_box.layout().addWidget(self.addresstext) form = QFormLayout() self.sampling_box.layout().addLayout(form) self.batch_spin = gui.spin( self.sampling_box, self, "batch_size", 50, 100000, step=50, keyboardTracking=False) form.addRow("Batch size ~ ", self.batch_spin) self.start_button = gui.button( self.sampling_box, self, "Start remote computation", callback=self.start, autoDefault=False, tooltip="Start/abort computation on the server") self.start_button.setEnabled(False) gui.checkBox(self.sampling_box, self, "auto_update", "Periodically fetch model", callback=self.update_model) self.__timer = QTimer(self, interval=2000) self.__timer.timeout.connect(self.get_model) self.sampling_box.setVisible(remotely) # Options self.options_box = gui.vBox(self.controlArea, "Options") gui.checkBox(self.options_box, self, "normalize", "Normalize data", callback=self._update_normalize) self.maxp_spin = gui.spin( self.options_box, self, "maxp", 1, 100, label="Show only first", callback=self._setup_plot, keyboardTracking=False ) self.controlArea.layout().addStretch() gui.auto_commit(self.controlArea, self, "auto_commit", "Apply", checkbox_label="Apply automatically") self.plot = pg.PlotWidget(background="w") axis = self.plot.getAxis("bottom") axis.setLabel("Principal Components") axis = self.plot.getAxis("left") axis.setLabel("Proportion of variance") self.plot_horlabels = [] self.plot_horlines = [] self.plot.getViewBox().setMenuEnabled(False) self.plot.getViewBox().setMouseEnabled(False, False) self.plot.showGrid(True, True, alpha=0.5) self.plot.setRange(xRange=(0.0, 1.0), yRange=(0.0, 1.0)) self.mainArea.layout().addWidget(self.plot) self._update_normalize() def update_model(self): self.get_model() if self.auto_update and self.rpca and not self.rpca.ready(): self.__timer.start(2000) else: self.__timer.stop() def start(self): if 'Abort' in self.start_button.text(): self.rpca.abort() self.__timer.stop() self.start_button.setText("Start remote computation") else: self.address = self.addresstext.text() with remote.server(self.address): from Orange.projection.pca import RemotePCA maxiter = (1e5 + self.data.approx_len()) / self.batch_size * 3 self.rpca = RemotePCA(self.data, self.batch_size, int(maxiter)) self.update_model() self.start_button.setText("Abort remote computation") def set_data(self, data): self.information() if isinstance(data, SqlTable): if data.approx_len() < AUTO_DL_LIMIT: data = Table(data) elif not remotely: self.information("Data has been sampled") data_sample = data.sample_time(1, no_cache=True) data_sample.download_data(2000, partial=True) data = Table(data_sample) self.data = data self.fit() def fit(self): self.clear() self.start_button.setEnabled(False) if self.data is None: return data = self.data self._transformed = None if isinstance(data, SqlTable): # data was big and remote available self.sampling_box.setVisible(True) self.start_button.setText("Start remote computation") self.start_button.setEnabled(True) else: self.sampling_box.setVisible(False) pca = self._pca_projector(data) variance_ratio = pca.explained_variance_ratio_ cumulative = numpy.cumsum(variance_ratio) self.components_spin.setRange(0, len(cumulative)) self._pca = pca self._variance_ratio = variance_ratio self._cumulative = cumulative self._setup_plot() self.unconditional_commit() def clear(self): self._pca = None self._transformed = None self._variance_ratio = None self._cumulative = None self._line = None self.plot_horlabels = [] self.plot_horlines = [] self.plot.clear() def get_model(self): if self.rpca is None: return if self.rpca.ready(): self.__timer.stop() self.start_button.setText("Restart (finished)") self._pca = self.rpca.get_state() if self._pca is None: return self._variance_ratio = self._pca.explained_variance_ratio_ self._cumulative = numpy.cumsum(self._variance_ratio) self._setup_plot() self._transformed = None self.commit() def _setup_plot(self): self.plot.clear() explained_ratio = self._variance_ratio explained = self._cumulative p = min(len(self._variance_ratio), self.maxp) self.plot.plot(numpy.arange(p), explained_ratio[:p], pen=pg.mkPen(QColor(Qt.red), width=2), antialias=True, name="Variance") self.plot.plot(numpy.arange(p), explained[:p], pen=pg.mkPen(QColor(Qt.darkYellow), width=2), antialias=True, name="Cumulative Variance") cutpos = self._nselected_components() - 1 self._line = pg.InfiniteLine( angle=90, pos=cutpos, movable=True, bounds=(0, p - 1)) self._line.setCursor(Qt.SizeHorCursor) self._line.setPen(pg.mkPen(QColor(Qt.black), width=2)) self._line.sigPositionChanged.connect(self._on_cut_changed) self.plot.addItem(self._line) self.plot_horlines = ( pg.PlotCurveItem(pen=pg.mkPen(QColor(Qt.blue), style=Qt.DashLine)), pg.PlotCurveItem(pen=pg.mkPen(QColor(Qt.blue), style=Qt.DashLine))) self.plot_horlabels = ( pg.TextItem(color=QColor(Qt.black), anchor=(1, 0)), pg.TextItem(color=QColor(Qt.black), anchor=(1, 1))) for item in self.plot_horlabels + self.plot_horlines: self.plot.addItem(item) self._set_horline_pos() self.plot.setRange(xRange=(0.0, p - 1), yRange=(0.0, 1.0)) self._update_axis() def _set_horline_pos(self): cutidx = self.ncomponents - 1 for line, label, curve in zip(self.plot_horlines, self.plot_horlabels, (self._variance_ratio, self._cumulative)): y = curve[cutidx] line.setData([-1, cutidx], 2 * [y]) label.setPos(cutidx, y) label.setPlainText("{:.3f}".format(y)) def _on_cut_changed(self, line): # cut changed by means of a cut line over the scree plot. value = int(round(line.value())) self._line.setValue(value) current = self._nselected_components() components = value + 1 if not (self.ncomponents == 0 and components == len(self._variance_ratio)): self.ncomponents = components self._set_horline_pos() if self._pca is not None: self.variance_covered = self._cumulative[components - 1] * 100 if current != self._nselected_components(): self._invalidate_selection() def _update_selection_component_spin(self): # cut changed by "ncomponents" spin. if self._pca is None: self._invalidate_selection() return if self.ncomponents == 0: # Special "All" value cut = len(self._variance_ratio) else: cut = self.ncomponents self.variance_covered = self._cumulative[cut - 1] * 100 if numpy.floor(self._line.value()) + 1 != cut: self._line.setValue(cut - 1) self._invalidate_selection() def _update_selection_variance_spin(self): # cut changed by "max variance" spin. if self._pca is None: return cut = numpy.searchsorted(self._cumulative, self.variance_covered / 100.0) + 1 cut = min(cut, len(self._cumulative)) self.ncomponents = cut if numpy.floor(self._line.value()) + 1 != cut: self._line.setValue(cut - 1) self._invalidate_selection() def _update_normalize(self): if self.normalize: pp = self._pca_preprocessors + [Normalize()] else: pp = self._pca_preprocessors self._pca_projector.preprocessors = pp self.fit() if self.data is None: self._invalidate_selection() def _nselected_components(self): """Return the number of selected components.""" if self._pca is None: return 0 if self.ncomponents == 0: # Special "All" value max_comp = len(self._variance_ratio) else: max_comp = self.ncomponents var_max = self._cumulative[max_comp - 1] if var_max != numpy.floor(self.variance_covered / 100.0): cut = max_comp self.variance_covered = var_max * 100 else: self.ncomponents = cut = numpy.searchsorted( self._cumulative, self.variance_covered / 100.0) + 1 return cut def _invalidate_selection(self): self.commit() def _update_axis(self): p = min(len(self._variance_ratio), self.maxp) axis = self.plot.getAxis("bottom") d = max((p-1)//(self.axis_labels-1), 1) axis.setTicks([[(i, str(i+1)) for i in range(0, p, d)]]) def commit(self): transformed = components = None if self._pca is not None: if self._transformed is None: # Compute the full transform (all components) only once. self._transformed = self._pca(self.data) transformed = self._transformed domain = Domain( transformed.domain.attributes[:self.ncomponents], self.data.domain.class_vars, self.data.domain.metas ) transformed = transformed.from_table(domain, transformed) dom = Domain(self._pca.orig_domain.attributes, metas=[StringVariable(name='component')]) metas = numpy.array([['PC{}'.format(i + 1) for i in range(self.ncomponents)]], dtype=object).T components = Table(dom, self._pca.components_[:self.ncomponents], metas=metas) components.name = 'components' self._pca_projector.component = self.ncomponents self.send("Transformed data", transformed) self.send("Components", components) self.send("PCA", self._pca_projector) def send_report(self): if self.data is None: return self.report_items(( ("Selected components", self.ncomponents), ("Explained variance", "{:.3f} %".format(self.variance_covered)) )) self.report_plot()
def variables_filter(model, parent=None, accepted_type=Orange.data.Variable, view_type=VariablesListItemView): """ GUI components: ListView with a lineedit which works as a filter. One can write a variable name in a edit box and possible matches are then shown in a listview. """ def update_completer_model(): """ This gets called when the model for available attributes changes through either drag/drop or the left/right button actions. """ nonlocal original_completer_items items = ["%s=%s" % item for v in model for item in v.attributes.items()] new = sorted(set(items)) if new != original_completer_items: original_completer_items = new completer_model.setStringList(original_completer_items) def update_completer_prefix(): """ Prefixes all items in the completer model with the current already done completion to enable the completion of multiple keywords. """ nonlocal original_completer_items prefix = str(completer.completionPrefix()) if not prefix.endswith(" ") and " " in prefix: prefix, _ = prefix.rsplit(" ", 1) items = [prefix + " " + item for item in original_completer_items] else: items = original_completer_items old = list(map(str, completer_model.stringList())) if set(old) != set(items): completer_model.setStringList(items) original_completer_items = [] filter_edit = QLineEdit() filter_edit.setToolTip("Filter the list of available variables.") filter_edit.setPlaceholderText("Filter") completer_model = QStringListModel() completer = QCompleter(completer_model, filter_edit) completer.setCompletionMode(QCompleter.InlineCompletion) completer.setModelSorting(QCompleter.CaseSensitivelySortedModel) filter_edit.setCompleter(completer) completer_navigator = CompleterNavigator(parent) filter_edit.installEventFilter(completer_navigator) proxy = VariableFilterProxyModel() proxy.setSourceModel(model) view = view_type(acceptedType=accepted_type) view.setModel(proxy) model.dataChanged.connect(update_completer_model) model.rowsInserted.connect(update_completer_model) model.rowsRemoved.connect(update_completer_model) filter_edit.textChanged.connect(update_completer_prefix) filter_edit.textChanged.connect(proxy.set_filter_string) return filter_edit, view
class OWPCA(widget.OWWidget): name = "PCA" description = "Principal component analysis with a scree-diagram." icon = "icons/PCA.svg" priority = 3050 class Inputs: data = Input("Data", Table) class Outputs: transformed_data = Output("Transformed data", Table) components = Output("Components", Table) pca = Output("PCA", PCA, dynamic=False) settingsHandler = settings.DomainContextHandler() ncomponents = settings.Setting(2) variance_covered = settings.Setting(100) batch_size = settings.Setting(100) address = settings.Setting('') auto_update = settings.Setting(True) auto_commit = settings.Setting(True) normalize = settings.ContextSetting(True) decomposition_idx = settings.ContextSetting(0) maxp = settings.Setting(20) axis_labels = settings.Setting(10) graph_name = "plot.plotItem" class Warning(widget.OWWidget.Warning): trivial_components = widget.Msg( "All components of the PCA are trivial (explain 0 variance). " "Input data is constant (or near constant).") class Error(widget.OWWidget.Error): no_features = widget.Msg("At least 1 feature is required") no_instances = widget.Msg("At least 1 data instance is required") sparse_data = widget.Msg("Sparse data is not supported") def __init__(self): super().__init__() self.data = None self._pca = None self._transformed = None self._variance_ratio = None self._cumulative = None self._line = False self._init_projector() # Components Selection box = gui.vBox(self.controlArea, "Components Selection") form = QFormLayout() box.layout().addLayout(form) self.components_spin = gui.spin( box, self, "ncomponents", 1, MAX_COMPONENTS, callback=self._update_selection_component_spin, keyboardTracking=False) self.components_spin.setSpecialValueText("All") self.variance_spin = gui.spin( box, self, "variance_covered", 1, 100, callback=self._update_selection_variance_spin, keyboardTracking=False) self.variance_spin.setSuffix("%") form.addRow("Components:", self.components_spin) form.addRow("Variance covered:", self.variance_spin) # Incremental learning self.sampling_box = gui.vBox(self.controlArea, "Incremental learning") self.addresstext = QLineEdit(box) self.addresstext.setPlaceholderText('Remote server') if self.address: self.addresstext.setText(self.address) self.sampling_box.layout().addWidget(self.addresstext) form = QFormLayout() self.sampling_box.layout().addLayout(form) self.batch_spin = gui.spin(self.sampling_box, self, "batch_size", 50, 100000, step=50, keyboardTracking=False) form.addRow("Batch size ~ ", self.batch_spin) self.start_button = gui.button( self.sampling_box, self, "Start remote computation", callback=self.start, autoDefault=False, tooltip="Start/abort computation on the server") self.start_button.setEnabled(False) gui.checkBox(self.sampling_box, self, "auto_update", "Periodically fetch model", callback=self.update_model) self.__timer = QTimer(self, interval=2000) self.__timer.timeout.connect(self.get_model) self.sampling_box.setVisible(remotely) # Decomposition self.decomposition_box = gui.radioButtons( self.controlArea, self, "decomposition_idx", [d.name for d in DECOMPOSITIONS], box="Decomposition", callback=self._update_decomposition) # Options self.options_box = gui.vBox(self.controlArea, "Options") self.normalize_box = gui.checkBox(self.options_box, self, "normalize", "Normalize data", callback=self._update_normalize) self.maxp_spin = gui.spin(self.options_box, self, "maxp", 1, MAX_COMPONENTS, label="Show only first", callback=self._setup_plot, keyboardTracking=False) self.controlArea.layout().addStretch() gui.auto_commit(self.controlArea, self, "auto_commit", "Apply", checkbox_label="Apply automatically") self.plot = pg.PlotWidget(background="w") axis = self.plot.getAxis("bottom") axis.setLabel("Principal Components") axis = self.plot.getAxis("left") axis.setLabel("Proportion of variance") self.plot_horlabels = [] self.plot_horlines = [] self.plot.getViewBox().setMenuEnabled(False) self.plot.getViewBox().setMouseEnabled(False, False) self.plot.showGrid(True, True, alpha=0.5) self.plot.setRange(xRange=(0.0, 1.0), yRange=(0.0, 1.0)) self.mainArea.layout().addWidget(self.plot) self._update_normalize() def update_model(self): self.get_model() if self.auto_update and self.rpca and not self.rpca.ready(): self.__timer.start(2000) else: self.__timer.stop() def update_buttons(self, sparse_data=False): if sparse_data: self.normalize = False buttons = self.decomposition_box.buttons for cls, button in zip(DECOMPOSITIONS, buttons): button.setDisabled(sparse_data and not cls.supports_sparse) if not buttons[self.decomposition_idx].isEnabled(): # Set decomposition index to first sparse-enabled decomposition for i, cls in enumerate(DECOMPOSITIONS): if cls.supports_sparse: self.decomposition_idx = i break self._init_projector() def start(self): if 'Abort' in self.start_button.text(): self.rpca.abort() self.__timer.stop() self.start_button.setText("Start remote computation") else: self.address = self.addresstext.text() with remote.server(self.address): from Orange.projection.pca import RemotePCA maxiter = (1e5 + self.data.approx_len()) / self.batch_size * 3 self.rpca = RemotePCA(self.data, self.batch_size, int(maxiter)) self.update_model() self.start_button.setText("Abort remote computation") @Inputs.data def set_data(self, data): self.closeContext() self.clear_messages() self.clear() self.start_button.setEnabled(False) self.information() self.data = None if isinstance(data, SqlTable): if data.approx_len() < AUTO_DL_LIMIT: data = Table(data) elif not remotely: self.information("Data has been sampled") data_sample = data.sample_time(1, no_cache=True) data_sample.download_data(2000, partial=True) data = Table(data_sample) else: # data was big and remote available self.sampling_box.setVisible(True) self.start_button.setText("Start remote computation") self.start_button.setEnabled(True) if not isinstance(data, SqlTable): self.sampling_box.setVisible(False) if isinstance(data, Table): if len(data.domain.attributes) == 0: self.Error.no_features() self.clear_outputs() return if len(data) == 0: self.Error.no_instances() self.clear_outputs() return self.openContext(data) sparse_data = data is not None and data.is_sparse() self.normalize_box.setDisabled(sparse_data) self.update_buttons(sparse_data=sparse_data) self.data = data self.fit() def fit(self): self.clear() self.Warning.trivial_components.clear() if self.data is None: return data = self.data if not isinstance(data, SqlTable): pca = self._pca_projector(data) variance_ratio = pca.explained_variance_ratio_ cumulative = numpy.cumsum(variance_ratio) if numpy.isfinite(cumulative[-1]): self.components_spin.setRange(0, len(cumulative)) self._pca = pca self._variance_ratio = variance_ratio self._cumulative = cumulative self._setup_plot() else: self.Warning.trivial_components() self.unconditional_commit() def clear(self): self._pca = None self._transformed = None self._variance_ratio = None self._cumulative = None self._line = None self.plot_horlabels = [] self.plot_horlines = [] self.plot.clear() def clear_outputs(self): self.Outputs.transformed_data.send(None) self.Outputs.components.send(None) self.Outputs.pca.send(self._pca_projector) def get_model(self): if self.rpca is None: return if self.rpca.ready(): self.__timer.stop() self.start_button.setText("Restart (finished)") self._pca = self.rpca.get_state() if self._pca is None: return self._variance_ratio = self._pca.explained_variance_ratio_ self._cumulative = numpy.cumsum(self._variance_ratio) self._setup_plot() self._transformed = None self.commit() def _setup_plot(self): self.plot.clear() if self._pca is None: return explained_ratio = self._variance_ratio explained = self._cumulative p = min(len(self._variance_ratio), self.maxp) self.plot.plot(numpy.arange(p), explained_ratio[:p], pen=pg.mkPen(QColor(Qt.red), width=2), antialias=True, name="Variance") self.plot.plot(numpy.arange(p), explained[:p], pen=pg.mkPen(QColor(Qt.darkYellow), width=2), antialias=True, name="Cumulative Variance") cutpos = self._nselected_components() - 1 self._line = pg.InfiniteLine(angle=90, pos=cutpos, movable=True, bounds=(0, p - 1)) self._line.setCursor(Qt.SizeHorCursor) self._line.setPen(pg.mkPen(QColor(Qt.black), width=2)) self._line.sigPositionChanged.connect(self._on_cut_changed) self.plot.addItem(self._line) self.plot_horlines = ( pg.PlotCurveItem(pen=pg.mkPen(QColor(Qt.blue), style=Qt.DashLine)), pg.PlotCurveItem(pen=pg.mkPen(QColor(Qt.blue), style=Qt.DashLine))) self.plot_horlabels = (pg.TextItem(color=QColor(Qt.black), anchor=(1, 0)), pg.TextItem(color=QColor(Qt.black), anchor=(1, 1))) for item in self.plot_horlabels + self.plot_horlines: self.plot.addItem(item) self._set_horline_pos() self.plot.setRange(xRange=(0.0, p - 1), yRange=(0.0, 1.0)) self._update_axis() def _set_horline_pos(self): cutidx = self.ncomponents - 1 for line, label, curve in zip( self.plot_horlines, self.plot_horlabels, (self._variance_ratio, self._cumulative)): y = curve[cutidx] line.setData([-1, cutidx], 2 * [y]) label.setPos(cutidx, y) label.setPlainText("{:.3f}".format(y)) def _on_cut_changed(self, line): # cut changed by means of a cut line over the scree plot. value = int(round(line.value())) self._line.setValue(value) current = self._nselected_components() components = value + 1 if not (self.ncomponents == 0 and components == len(self._variance_ratio)): self.ncomponents = components self._set_horline_pos() if self._pca is not None: var = self._cumulative[components - 1] if numpy.isfinite(var): self.variance_covered = int(var * 100) if current != self._nselected_components(): self._invalidate_selection() def _update_selection_component_spin(self): # cut changed by "ncomponents" spin. if self._pca is None: self._invalidate_selection() return if self.ncomponents == 0: # Special "All" value cut = len(self._variance_ratio) else: cut = self.ncomponents var = self._cumulative[cut - 1] if numpy.isfinite(var): self.variance_covered = int(var * 100) if numpy.floor(self._line.value()) + 1 != cut: self._line.setValue(cut - 1) self._invalidate_selection() def _update_selection_variance_spin(self): # cut changed by "max variance" spin. if self._pca is None: return cut = numpy.searchsorted(self._cumulative, self.variance_covered / 100.0) + 1 cut = min(cut, len(self._cumulative)) self.ncomponents = cut if numpy.floor(self._line.value()) + 1 != cut: self._line.setValue(cut - 1) self._invalidate_selection() def _update_normalize(self): if self.normalize: pp = self._pca_preprocessors + [Normalize()] else: pp = self._pca_preprocessors self._pca_projector.preprocessors = pp self.fit() if self.data is None: self._invalidate_selection() def _init_projector(self): cls = DECOMPOSITIONS[self.decomposition_idx] self._pca_projector = cls(n_components=MAX_COMPONENTS) self._pca_projector.component = self.ncomponents self._pca_preprocessors = cls.preprocessors def _update_decomposition(self): self._init_projector() self._update_normalize() def _nselected_components(self): """Return the number of selected components.""" if self._pca is None: return 0 if self.ncomponents == 0: # Special "All" value max_comp = len(self._variance_ratio) else: max_comp = self.ncomponents var_max = self._cumulative[max_comp - 1] if var_max != numpy.floor(self.variance_covered / 100.0): cut = max_comp assert numpy.isfinite(var_max) self.variance_covered = int(var_max * 100) else: self.ncomponents = cut = numpy.searchsorted( self._cumulative, self.variance_covered / 100.0) + 1 return cut def _invalidate_selection(self): self.commit() def _update_axis(self): p = min(len(self._variance_ratio), self.maxp) axis = self.plot.getAxis("bottom") d = max((p - 1) // (self.axis_labels - 1), 1) axis.setTicks([[(i, str(i + 1)) for i in range(0, p, d)]]) def commit(self): transformed = components = None if self._pca is not None: if self._transformed is None: # Compute the full transform (MAX_COMPONENTS components) only once. self._transformed = self._pca(self.data) transformed = self._transformed domain = Domain(transformed.domain.attributes[:self.ncomponents], self.data.domain.class_vars, self.data.domain.metas) transformed = transformed.from_table(domain, transformed) dom = Domain([ ContinuousVariable(a.name) for a in self._pca.orig_domain.attributes ], metas=[StringVariable(name='component')]) metas = numpy.array( [['PC{}'.format(i + 1) for i in range(self.ncomponents)]], dtype=object).T components = Table(dom, self._pca.components_[:self.ncomponents], metas=metas) components.name = 'components' self._pca_projector.component = self.ncomponents self.Outputs.transformed_data.send(transformed) self.Outputs.components.send(components) self.Outputs.pca.send(self._pca_projector) def send_report(self): if self.data is None: return self.report_items( (("Decomposition", DECOMPOSITIONS[self.decomposition_idx].name), ("Normalize data", str(self.normalize)), ("Selected components", self.ncomponents), ("Explained variance", "{:.3f} %".format(self.variance_covered)))) self.report_plot() @classmethod def migrate_settings(cls, settings, version): if "variance_covered" in settings: # Due to the error in gh-1896 the variance_covered was persisted # as a NaN value, causing a TypeError in the widgets `__init__`. vc = settings["variance_covered"] if isinstance(vc, numbers.Real): if numpy.isfinite(vc): vc = int(vc) else: vc = 100 settings["variance_covered"] = vc if settings["ncomponents"] > MAX_COMPONENTS: settings["ncomponents"] = MAX_COMPONENTS
class ComboBoxSearch(QComboBox): """ A drop down list combo box with filter/search. The popup list view is filtered by text entered in the filter field. Note ---- `popup`, `lineEdit` and `completer` from the base QComboBox class are unused. Setting/modifying them will have no effect. """ # NOTE: Setting editable + QComboBox.NoInsert policy + ... did not achieve # the same results. def __init__(self, parent=None, **kwargs): self.__maximumContentsLength = MAXIMUM_CONTENTS_LENGTH self.__searchline = QLineEdit(visible=False, frame=False) self.__searchline.setAttribute(Qt.WA_MacShowFocusRect, False) self.__popup = None # type: Optional[QAbstractItemModel] self.__proxy = None # type: Optional[QSortFilterProxyModel] self.__popupTimer = QElapsedTimer() super().__init__(parent, **kwargs) self.__searchline.setParent(self) self.__searchline.setFocusProxy(self) self.setFocusPolicy(Qt.ClickFocus | Qt.TabFocus) def setMaximumContentsLength(self, length): # type: (int) -> None """ Set the maximum contents length hint. The hint specifies the upper bound on the `sizeHint` and `minimumSizeHint` width specified in character length. Set to 0 or negative value to disable. Note ---- This property does not affect the widget's `maximumSize`. The widget can still grow depending on its `sizePolicy`. Parameters ---------- length : int Maximum contents length hint. """ if self.__maximumContentsLength != length: self.__maximumContentsLength = length self.updateGeometry() def _get_size_hint(self): sh = super().sizeHint() if self.__maximumContentsLength > 0: width = ( self.fontMetrics().width("X") * self.__maximumContentsLength + self.iconSize().width() + 4 ) sh = sh.boundedTo(QSize(width, sh.height())) return sh def sizeHint(self): # type: () -> QSize # reimplemented return self._get_size_hint() def minimumSizeHint(self): # type: () -> QSize # reimplemented return self._get_size_hint() def showPopup(self): # type: () -> None """ Reimplemented from QComboBox.showPopup Popup up a customized view and filter edit line. Note ---- The .popup(), .lineEdit(), .completer() of the base class are not used. """ if self.__popup is not None: # We have user entered state that cannot be disturbed # (entered filter text, scroll offset, ...) return # pragma: no cover if self.count() == 0: return opt = QStyleOptionComboBox() self.initStyleOption(opt) popup = QListView( uniformItemSizes=True, horizontalScrollBarPolicy=Qt.ScrollBarAlwaysOff, verticalScrollBarPolicy=Qt.ScrollBarAsNeeded, iconSize=self.iconSize(), ) popup.setFocusProxy(self.__searchline) popup.setParent(self, Qt.Popup | Qt.FramelessWindowHint) popup.setItemDelegate(_ComboBoxListDelegate(popup)) proxy = QSortFilterProxyModel( popup, filterCaseSensitivity=Qt.CaseInsensitive ) proxy.setFilterKeyColumn(self.modelColumn()) proxy.setSourceModel(self.model()) popup.setModel(proxy) root = proxy.mapFromSource(self.rootModelIndex()) popup.setRootIndex(root) self.__popup = popup self.__proxy = proxy self.__searchline.setText("") self.__searchline.setPlaceholderText("Filter...") self.__searchline.setVisible(True) self.__searchline.textEdited.connect(proxy.setFilterFixedString) style = self.style() # type: QStyle popuprect_origin = style.subControlRect( QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxListBoxPopup, self ) # type: QRect popuprect_origin = QRect( self.mapToGlobal(popuprect_origin.topLeft()), popuprect_origin.size() ) editrect = style.subControlRect( QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxEditField, self ) # type: QRect self.__searchline.setGeometry(editrect) desktop = QApplication.desktop() screenrect = desktop.availableGeometry(self) # type: QRect # get the height for the view listrect = QRect() for i in range(min(proxy.rowCount(root), self.maxVisibleItems())): index = proxy.index(i, self.modelColumn(), root) if index.isValid(): listrect = listrect.united(popup.visualRect(index)) if listrect.height() >= screenrect.height(): break window = popup.window() # type: QWidget window.ensurePolished() if window.layout() is not None: window.layout().activate() else: QApplication.sendEvent(window, QEvent(QEvent.LayoutRequest)) margins = qwidget_margin_within(popup.viewport(), window) height = (listrect.height() + 2 * popup.spacing() + margins.top() + margins.bottom()) popup_size = (QSize(popuprect_origin.width(), height) .expandedTo(window.minimumSize()) .boundedTo(window.maximumSize()) .boundedTo(screenrect.size())) popuprect = QRect(popuprect_origin.bottomLeft(), popup_size) popuprect = dropdown_popup_geometry( popuprect, popuprect_origin, screenrect) popup.setGeometry(popuprect) current = proxy.mapFromSource( self.model().index(self.currentIndex(), self.modelColumn(), self.rootModelIndex())) popup.setCurrentIndex(current) popup.scrollTo(current, QAbstractItemView.EnsureVisible) popup.show() popup.setFocus(Qt.PopupFocusReason) popup.installEventFilter(self) popup.viewport().installEventFilter(self) popup.viewport().setMouseTracking(True) self.update() self.__popupTimer.restart() def hidePopup(self): """Reimplemented""" if self.__popup is not None: popup = self.__popup self.__popup = self.__proxy = None popup.setFocusProxy(None) popup.hide() popup.deleteLater() popup.removeEventFilter(self) popup.viewport().removeEventFilter(self) # need to call base hidePopup even though the base showPopup was not # called (update internal state wrt. 'pressed' arrow, ...) super().hidePopup() self.__searchline.hide() self.update() def initStyleOption(self, option): # type: (QStyleOptionComboBox) -> None super().initStyleOption(option) option.editable = True def __updateGeometries(self): opt = QStyleOptionComboBox() self.initStyleOption(opt) editarea = self.style().subControlRect( QStyle.CC_ComboBox, opt, QStyle.SC_ComboBoxEditField, self) self.__searchline.setGeometry(editarea) def resizeEvent(self, event): """Reimplemented.""" super().resizeEvent(event) self.__updateGeometries() def paintEvent(self, event): """Reimplemented.""" opt = QStyleOptionComboBox() self.initStyleOption(opt) painter = QStylePainter(self) painter.drawComplexControl(QStyle.CC_ComboBox, opt) if not self.__searchline.isVisibleTo(self): opt.editable = False painter.drawControl(QStyle.CE_ComboBoxLabel, opt) def eventFilter(self, obj, event): # pylint: disable=too-many-branches # type: (QObject, QEvent) -> bool """Reimplemented.""" etype = event.type() if etype == QEvent.FocusOut and self.__popup is not None: self.hidePopup() return True if etype == QEvent.Hide and self.__popup is not None: self.hidePopup() return False if etype == QEvent.KeyPress or etype == QEvent.KeyRelease or \ etype == QEvent.ShortcutOverride and obj is self.__popup: event = event # type: QKeyEvent key, modifiers = event.key(), event.modifiers() if key in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Select): current = self.__popup.currentIndex() if current.isValid(): self.__activateProxyIndex(current) elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown): return False # elif key in (Qt.Key_Tab, Qt.Key_Backtab): pass elif key == Qt.Key_Escape or \ (key == Qt.Key_F4 and modifiers & Qt.AltModifier): self.__popup.hide() return True else: # pass the input events to the filter edit line (no propagation # up the parent chain). self.__searchline.event(event) if event.isAccepted(): return True if etype == QEvent.MouseButtonRelease and self.__popup is not None \ and obj is self.__popup.viewport() \ and self.__popupTimer.elapsed() >= \ QApplication.doubleClickInterval(): event = event # type: QMouseEvent index = self.__popup.indexAt(event.pos()) if index.isValid(): self.__activateProxyIndex(index) if etype == QEvent.MouseMove and self.__popup is not None \ and obj is self.__popup.viewport(): event = event # type: QMouseEvent opt = QStyleOptionComboBox() self.initStyleOption(opt) style = self.style() # type: QStyle if style.styleHint(QStyle.SH_ComboBox_ListMouseTracking, opt, self): index = self.__popup.indexAt(event.pos()) if index.isValid() and \ index.flags() & (Qt.ItemIsEnabled | Qt.ItemIsSelectable): self.__popup.setCurrentIndex(index) if etype == QEvent.MouseButtonPress and self.__popup is obj: # Popup border or out of window mouse button press/release. # At least on windows this needs to be handled. style = self.style() opt = QStyleOptionComboBox() self.initStyleOption(opt) opt.subControls = QStyle.SC_All opt.activeSubControls = QStyle.SC_ComboBoxArrow pos = self.mapFromGlobal(event.globalPos()) sc = style.hitTestComplexControl(QStyle.CC_ComboBox, opt, pos, self) if sc != QStyle.SC_None: self.__popup.setAttribute(Qt.WA_NoMouseReplay) self.hidePopup() return super().eventFilter(obj, event) def __activateProxyIndex(self, index): # type: (QModelIndex) -> None # Set current and activate the source index corresponding to the proxy # index in the popup's model. if self.__popup is not None and index.isValid(): proxy = self.__popup.model() assert index.model() is proxy index = proxy.mapToSource(index) assert index.model() is self.model() if index.isValid() and \ index.flags() & (Qt.ItemIsEnabled | Qt.ItemIsSelectable): self.hidePopup() text = self.itemText(index.row()) self.setCurrentIndex(index.row()) self.activated[int].emit(index.row()) self.activated[str].emit(text)
class OWSql(OWWidget): name = "SQL Table" id = "orange.widgets.data.sql" description = "Load data set from SQL." icon = "icons/SQLTable.svg" priority = 30 category = "Data" keywords = ["data", "file", "load", "read", "SQL"] class Outputs: data = Output("Data", Table, doc="Attribute-valued data set read from the input file.") settings_version = 2 want_main_area = False resizing_enabled = False host = Setting(None) port = Setting(None) database = Setting(None) schema = Setting(None) username = "" password = "" table = Setting(None) sql = Setting("") guess_values = Setting(True) download = Setting(False) materialize = Setting(False) materialize_table_name = Setting("") class Information(OWWidget.Information): data_sampled = Msg("Data description was generated from a sample.") class Error(OWWidget.Error): connection = Msg("{}") no_backends = Msg("Please install a backend to use this widget") missing_extension = Msg("Database is missing extension{}: {}") def __init__(self): super().__init__() self.backend = None self.data_desc_table = None self.database_desc = None vbox = gui.vBox(self.controlArea, "Server", addSpace=True) box = gui.vBox(vbox) self.backends = BackendModel(Backend.available_backends()) self.backendcombo = QComboBox(box) if len(self.backends): self.backendcombo.setModel(self.backends) else: self.Error.no_backends() box.setEnabled(False) box.layout().addWidget(self.backendcombo) self.servertext = QLineEdit(box) self.servertext.setPlaceholderText('Server') self.servertext.setToolTip('Server') self.servertext.editingFinished.connect(self._load_credentials) if self.host: self.servertext.setText(self.host if not self.port else '{}:{}'.format(self.host, self.port)) box.layout().addWidget(self.servertext) self.databasetext = QLineEdit(box) self.databasetext.setPlaceholderText('Database[/Schema]') self.databasetext.setToolTip('Database or optionally Database/Schema') if self.database: self.databasetext.setText( self.database if not self.schema else '{}/{}'.format(self.database, self.schema)) box.layout().addWidget(self.databasetext) self.usernametext = QLineEdit(box) self.usernametext.setPlaceholderText('Username') self.usernametext.setToolTip('Username') box.layout().addWidget(self.usernametext) self.passwordtext = QLineEdit(box) self.passwordtext.setPlaceholderText('Password') self.passwordtext.setToolTip('Password') self.passwordtext.setEchoMode(QLineEdit.Password) box.layout().addWidget(self.passwordtext) self._load_credentials() self.tables = TableModel() tables = gui.hBox(box) self.tablecombo = QComboBox( minimumContentsLength=35, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLength ) self.tablecombo.setModel(self.tables) self.tablecombo.setToolTip('table') tables.layout().addWidget(self.tablecombo) self.connect() index = self.tablecombo.findText(str(self.table)) if index != -1: self.tablecombo.setCurrentIndex(index) # set up the callback to select_table in case of selection change self.tablecombo.activated[int].connect(self.select_table) self.connectbutton = gui.button( tables, self, '↻', callback=self.connect) self.connectbutton.setSizePolicy( QSizePolicy.Fixed, QSizePolicy.Fixed) tables.layout().addWidget(self.connectbutton) self.custom_sql = gui.vBox(box) self.custom_sql.setVisible(False) self.sqltext = QTextEdit(self.custom_sql) self.sqltext.setPlainText(self.sql) self.custom_sql.layout().addWidget(self.sqltext) mt = gui.hBox(self.custom_sql) cb = gui.checkBox(mt, self, 'materialize', 'Materialize to table ') cb.setToolTip('Save results of the query in a table') le = gui.lineEdit(mt, self, 'materialize_table_name') le.setToolTip('Save results of the query in a table') self.executebtn = gui.button( self.custom_sql, self, 'Execute', callback=self.open_table) box.layout().addWidget(self.custom_sql) gui.checkBox(box, self, "guess_values", "Auto-discover categorical variables", callback=self.open_table) gui.checkBox(box, self, "download", "Download data to local memory", callback=self.open_table) gui.rubber(self.buttonsArea) QTimer.singleShot(0, self.select_table) def _load_credentials(self): self._parse_host_port() cm = self._credential_manager(self.host, self.port) self.username = cm.username self.password = cm.password if self.username: self.usernametext.setText(self.username) if self.password: self.passwordtext.setText(self.password) def _save_credentials(self): cm = self._credential_manager(self.host, self.port) cm.username = self.username or '' cm.password = self.password or '' def _credential_manager(self, host, port): return CredentialManager("SQL Table: {}:{}".format(host, port)) def error(self, id=0, text=""): super().error(id, text) err_style = 'QLineEdit {border: 2px solid red;}' if 'server' in text or 'host' in text: self.servertext.setStyleSheet(err_style) else: self.servertext.setStyleSheet('') if 'role' in text: self.usernametext.setStyleSheet(err_style) else: self.usernametext.setStyleSheet('') if 'database' in text: self.databasetext.setStyleSheet(err_style) else: self.databasetext.setStyleSheet('') def _parse_host_port(self): hostport = self.servertext.text().split(':') self.host = hostport[0] self.port = hostport[1] if len(hostport) == 2 else None def connect(self): self._parse_host_port() self.database, _, self.schema = self.databasetext.text().partition('/') self.username = self.usernametext.text() or None self.password = self.passwordtext.text() or None try: if self.backendcombo.currentIndex() < 0: return backend = self.backends[self.backendcombo.currentIndex()] self.backend = backend(dict( host=self.host, port=self.port, database=self.database, user=self.username, password=self.password )) self.Error.connection.clear() self._save_credentials() self.database_desc = OrderedDict(( ("Host", self.host), ("Port", self.port), ("Database", self.database), ("User name", self.username) )) self.refresh_tables() except BackendError as err: error = str(err).split('\n')[0] self.Error.connection(error) self.database_desc = self.data_desc_table = None self.tablecombo.clear() def refresh_tables(self): self.tables.clear() self.Error.missing_extension.clear() if self.backend is None: self.data_desc_table = None return self.tables.append("Select a table") self.tables.append("Custom SQL") self.tables.extend(self.backend.list_tables(self.schema)) # Called on tablecombo selection change: def select_table(self): curIdx = self.tablecombo.currentIndex() if self.tablecombo.itemText(curIdx) != "Custom SQL": self.custom_sql.setVisible(False) return self.open_table() else: self.custom_sql.setVisible(True) self.data_desc_table = None self.database_desc["Table"] = "(None)" self.table = None if len(str(self.sql)) > 14: return self.open_table() #self.Error.missing_extension( # 's' if len(missing) > 1 else '', # ', '.join(missing), # shown=missing) def open_table(self): table = self.get_table() self.data_desc_table = table self.Outputs.data.send(table) def get_table(self): curIdx = self.tablecombo.currentIndex() if curIdx <= 0: if self.database_desc: self.database_desc["Table"] = "(None)" self.data_desc_table = None return if self.tablecombo.itemText(curIdx) != "Custom SQL": self.table = self.tables[self.tablecombo.currentIndex()] self.database_desc["Table"] = self.table if "Query" in self.database_desc: del self.database_desc["Query"] what = self.table else: what = self.sql = self.sqltext.toPlainText() self.table = "Custom SQL" if self.materialize: import psycopg2 if not self.materialize_table_name: self.Error.connection( "Specify a table name to materialize the query") return try: with self.backend.execute_sql_query("DROP TABLE IF EXISTS " + self.materialize_table_name): pass with self.backend.execute_sql_query("CREATE TABLE " + self.materialize_table_name + " AS " + self.sql): pass with self.backend.execute_sql_query("ANALYZE " + self.materialize_table_name): pass except (psycopg2.ProgrammingError, BackendError) as ex: self.Error.connection(str(ex)) return try: table = SqlTable(dict(host=self.host, port=self.port, database=self.database, user=self.username, password=self.password), what, backend=type(self.backend), inspect_values=False) except BackendError as ex: self.Error.connection(str(ex)) return self.Error.connection.clear() sample = False if table.approx_len() > LARGE_TABLE and self.guess_values: confirm = QMessageBox(self) confirm.setIcon(QMessageBox.Warning) confirm.setText("Attribute discovery might take " "a long time on large tables.\n" "Do you want to auto discover attributes?") confirm.addButton("Yes", QMessageBox.YesRole) no_button = confirm.addButton("No", QMessageBox.NoRole) sample_button = confirm.addButton("Yes, on a sample", QMessageBox.YesRole) confirm.exec() if confirm.clickedButton() == no_button: self.guess_values = False elif confirm.clickedButton() == sample_button: sample = True self.Information.clear() if self.guess_values: QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) if sample: s = table.sample_time(1) domain = s.get_domain(inspect_values=True) self.Information.data_sampled() else: domain = table.get_domain(inspect_values=True) QApplication.restoreOverrideCursor() table.domain = domain if self.download: if table.approx_len() > MAX_DL_LIMIT: QMessageBox.warning( self, 'Warning', "Data is too big to download.\n" "Consider using the Data Sampler widget to download " "a sample instead.") self.download = False elif table.approx_len() > AUTO_DL_LIMIT: confirm = QMessageBox.question( self, 'Question', "Data appears to be big. Do you really " "want to download it to local memory?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if confirm == QMessageBox.No: self.download = False if self.download: table.download_data(MAX_DL_LIMIT) table = Table(table) return table def send_report(self): if not self.database_desc: self.report_paragraph("No database connection.") return self.report_items("Database", self.database_desc) if self.data_desc_table: self.report_items("Data", report.describe_data(self.data_desc_table)) @classmethod def migrate_settings(cls, settings, version): if version < 2: # Until Orange version 3.4.4 username and password had been stored # in Settings. cm = cls._credential_manager(settings["host"], settings["port"]) cm.username = settings["username"] cm.password = settings["password"]
class OWSelectAttributes(widget.OWWidget): name = "Select Columns" description = "Select columns from the data table and assign them to " \ "data features, classes or meta variables." icon = "icons/SelectColumns.svg" priority = 100 inputs = [("Data", Table, "set_data")] outputs = [("Data", Table), ("Features", widget.AttributeList)] want_main_area = False want_control_area = True settingsHandler = SelectAttributesDomainContextHandler() domain_role_hints = ContextSetting({}) auto_commit = Setting(False) def __init__(self): super().__init__() self.controlArea = QWidget(self.controlArea) self.layout().addWidget(self.controlArea) layout = QGridLayout() self.controlArea.setLayout(layout) layout.setContentsMargins(4, 4, 4, 4) box = gui.vBox(self.controlArea, "Available Variables", addToLayout=False) self.filter_edit = QLineEdit() self.filter_edit.setToolTip("Filter the list of available variables.") box.layout().addWidget(self.filter_edit) if hasattr(self.filter_edit, "setPlaceholderText"): self.filter_edit.setPlaceholderText("Filter") self.completer = QCompleter() self.completer.setCompletionMode(QCompleter.InlineCompletion) self.completer_model = QStringListModel() self.completer.setModel(self.completer_model) self.completer.setModelSorting( QCompleter.CaseSensitivelySortedModel) self.filter_edit.setCompleter(self.completer) self.completer_navigator = CompleterNavigator(self) self.filter_edit.installEventFilter(self.completer_navigator) def dropcompleted(action): if action == Qt.MoveAction: self.commit() self.available_attrs = VariableListModel(enable_dnd=True) self.available_attrs_proxy = VariableFilterProxyModel() self.available_attrs_proxy.setSourceModel(self.available_attrs) self.available_attrs_view = VariablesListItemView( acceptedType=Orange.data.Variable) self.available_attrs_view.setModel(self.available_attrs_proxy) aa = self.available_attrs aa.dataChanged.connect(self.update_completer_model) aa.rowsInserted.connect(self.update_completer_model) aa.rowsRemoved.connect(self.update_completer_model) self.available_attrs_view.selectionModel().selectionChanged.connect( partial(self.update_interface_state, self.available_attrs_view)) self.available_attrs_view.dragDropActionDidComplete.connect(dropcompleted) self.filter_edit.textChanged.connect(self.update_completer_prefix) self.filter_edit.textChanged.connect( self.available_attrs_proxy.set_filter_string) box.layout().addWidget(self.available_attrs_view) layout.addWidget(box, 0, 0, 3, 1) box = gui.vBox(self.controlArea, "Features", addToLayout=False) self.used_attrs = VariableListModel(enable_dnd=True) self.used_attrs_view = VariablesListItemView( acceptedType=(Orange.data.DiscreteVariable, Orange.data.ContinuousVariable)) self.used_attrs_view.setModel(self.used_attrs) self.used_attrs_view.selectionModel().selectionChanged.connect( partial(self.update_interface_state, self.used_attrs_view)) self.used_attrs_view.dragDropActionDidComplete.connect(dropcompleted) box.layout().addWidget(self.used_attrs_view) layout.addWidget(box, 0, 2, 1, 1) box = gui.vBox(self.controlArea, "Target Variable", addToLayout=False) self.class_attrs = ClassVarListItemModel(enable_dnd=True) self.class_attrs_view = ClassVariableItemView( acceptedType=(Orange.data.DiscreteVariable, Orange.data.ContinuousVariable)) self.class_attrs_view.setModel(self.class_attrs) self.class_attrs_view.selectionModel().selectionChanged.connect( partial(self.update_interface_state, self.class_attrs_view)) self.class_attrs_view.dragDropActionDidComplete.connect(dropcompleted) self.class_attrs_view.setMaximumHeight(24) box.layout().addWidget(self.class_attrs_view) layout.addWidget(box, 1, 2, 1, 1) box = gui.vBox(self.controlArea, "Meta Attributes", addToLayout=False) self.meta_attrs = VariableListModel(enable_dnd=True) self.meta_attrs_view = VariablesListItemView( acceptedType=Orange.data.Variable) self.meta_attrs_view.setModel(self.meta_attrs) self.meta_attrs_view.selectionModel().selectionChanged.connect( partial(self.update_interface_state, self.meta_attrs_view)) self.meta_attrs_view.dragDropActionDidComplete.connect(dropcompleted) box.layout().addWidget(self.meta_attrs_view) layout.addWidget(box, 2, 2, 1, 1) bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0) layout.addWidget(bbox, 0, 1, 1, 1) self.up_attr_button = gui.button(bbox, self, "Up", callback=partial(self.move_up, self.used_attrs_view)) self.move_attr_button = gui.button(bbox, self, ">", callback=partial(self.move_selected, self.used_attrs_view)) self.down_attr_button = gui.button(bbox, self, "Down", callback=partial(self.move_down, self.used_attrs_view)) bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0) layout.addWidget(bbox, 1, 1, 1, 1) self.move_class_button = gui.button(bbox, self, ">", callback=partial(self.move_selected, self.class_attrs_view, exclusive=True)) bbox = gui.vBox(self.controlArea, addToLayout=False, margin=0) layout.addWidget(bbox, 2, 1, 1, 1) self.up_meta_button = gui.button(bbox, self, "Up", callback=partial(self.move_up, self.meta_attrs_view)) self.move_meta_button = gui.button(bbox, self, ">", callback=partial(self.move_selected, self.meta_attrs_view)) self.down_meta_button = gui.button(bbox, self, "Down", callback=partial(self.move_down, self.meta_attrs_view)) autobox = gui.auto_commit(None, self, "auto_commit", "Send") layout.addWidget(autobox, 3, 0, 1, 3) reset = gui.button(None, self, "Reset", callback=self.reset) autobox.layout().insertWidget(0, self.report_button) autobox.layout().insertWidget(1, reset) autobox.layout().insertSpacing(2, 10) reset.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.report_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) layout.setRowStretch(0, 4) layout.setRowStretch(1, 0) layout.setRowStretch(2, 2) layout.setHorizontalSpacing(0) self.controlArea.setLayout(layout) self.data = None self.output_data = None self.original_completer_items = [] self.resize(500, 600) def set_data(self, data=None): self.update_domain_role_hints() self.closeContext() self.data = data if data is not None: self.openContext(data) all_vars = data.domain.variables + data.domain.metas var_sig = lambda attr: (attr.name, vartype(attr)) domain_hints = {var_sig(attr): ("attribute", i) for i, attr in enumerate(data.domain.attributes)} domain_hints.update({var_sig(attr): ("meta", i) for i, attr in enumerate(data.domain.metas)}) if data.domain.class_vars: domain_hints.update( {var_sig(attr): ("class", i) for i, attr in enumerate(data.domain.class_vars)}) # update the hints from context settings domain_hints.update(self.domain_role_hints) attrs_for_role = lambda role: [ (domain_hints[var_sig(attr)][1], attr) for attr in all_vars if domain_hints[var_sig(attr)][0] == role] attributes = [ attr for place, attr in sorted(attrs_for_role("attribute"), key=lambda a: a[0])] classes = [ attr for place, attr in sorted(attrs_for_role("class"), key=lambda a: a[0])] metas = [ attr for place, attr in sorted(attrs_for_role("meta"), key=lambda a: a[0])] available = [ attr for place, attr in sorted(attrs_for_role("available"), key=lambda a: a[0])] self.used_attrs[:] = attributes self.class_attrs[:] = classes self.meta_attrs[:] = metas self.available_attrs[:] = available else: self.used_attrs[:] = [] self.class_attrs[:] = [] self.meta_attrs[:] = [] self.available_attrs[:] = [] self.unconditional_commit() def update_domain_role_hints(self): """ Update the domain hints to be stored in the widgets settings. """ hints_from_model = lambda role, model: [ ((attr.name, vartype(attr)), (role, i)) for i, attr in enumerate(model)] hints = dict(hints_from_model("available", self.available_attrs)) hints.update(hints_from_model("attribute", self.used_attrs)) hints.update(hints_from_model("class", self.class_attrs)) hints.update(hints_from_model("meta", self.meta_attrs)) self.domain_role_hints = hints def selected_rows(self, view): """ Return the selected rows in the view. """ rows = view.selectionModel().selectedRows() model = view.model() if isinstance(model, QSortFilterProxyModel): rows = [model.mapToSource(r) for r in rows] return [r.row() for r in rows] def move_rows(self, view, rows, offset): model = view.model() newrows = [min(max(0, row + offset), len(model) - 1) for row in rows] for row, newrow in sorted(zip(rows, newrows), reverse=offset > 0): model[row], model[newrow] = model[newrow], model[row] selection = QItemSelection() for nrow in newrows: index = model.index(nrow, 0) selection.select(index, index) view.selectionModel().select( selection, QItemSelectionModel.ClearAndSelect) self.commit() def move_up(self, view): selected = self.selected_rows(view) self.move_rows(view, selected, -1) def move_down(self, view): selected = self.selected_rows(view) self.move_rows(view, selected, 1) def move_selected(self, view, exclusive=False): if self.selected_rows(view): self.move_selected_from_to(view, self.available_attrs_view) elif self.selected_rows(self.available_attrs_view): self.move_selected_from_to(self.available_attrs_view, view, exclusive) def move_selected_from_to(self, src, dst, exclusive=False): self.move_from_to(src, dst, self.selected_rows(src), exclusive) def move_from_to(self, src, dst, rows, exclusive=False): src_model = source_model(src) attrs = [src_model[r] for r in rows] if exclusive and len(attrs) != 1: return for s1, s2 in reversed(list(slices(rows))): del src_model[s1:s2] dst_model = source_model(dst) if exclusive and len(dst_model) > 0: src_model.append(dst_model[0]) del dst_model[0] dst_model.extend(attrs) self.commit() def update_interface_state(self, focus=None, selected=None, deselected=None): for view in [self.available_attrs_view, self.used_attrs_view, self.class_attrs_view, self.meta_attrs_view]: if view is not focus and not view.hasFocus() and self.selected_rows(view): view.selectionModel().clear() def selected_vars(view): model = source_model(view) return [model[i] for i in self.selected_rows(view)] available_selected = selected_vars(self.available_attrs_view) attrs_selected = selected_vars(self.used_attrs_view) class_selected = selected_vars(self.class_attrs_view) meta_selected = selected_vars(self.meta_attrs_view) available_types = set(map(type, available_selected)) all_primitive = all(var.is_primitive() for var in available_types) move_attr_enabled = (available_selected and all_primitive) or \ attrs_selected self.move_attr_button.setEnabled(bool(move_attr_enabled)) if move_attr_enabled: self.move_attr_button.setText(">" if available_selected else "<") move_class_enabled = (len(available_selected) == 1 and all_primitive) or \ class_selected self.move_class_button.setEnabled(bool(move_class_enabled)) if move_class_enabled: self.move_class_button.setText(">" if available_selected else "<") move_meta_enabled = available_selected or meta_selected self.move_meta_button.setEnabled(bool(move_meta_enabled)) if move_meta_enabled: self.move_meta_button.setText(">" if available_selected else "<") def update_completer_model(self, *_): """ This gets called when the model for available attributes changes through either drag/drop or the left/right button actions. """ vars = list(self.available_attrs) items = [var.name for var in vars] items += ["%s=%s" % item for v in vars for item in v.attributes.items()] new = sorted(set(items)) if new != self.original_completer_items: self.original_completer_items = new self.completer_model.setStringList(self.original_completer_items) def update_completer_prefix(self, filter): """ Prefixes all items in the completer model with the current already done completion to enable the completion of multiple keywords. """ prefix = str(self.completer.completionPrefix()) if not prefix.endswith(" ") and " " in prefix: prefix, _ = prefix.rsplit(" ", 1) items = [prefix + " " + item for item in self.original_completer_items] else: items = self.original_completer_items old = list(map(str, self.completer_model.stringList())) if set(old) != set(items): self.completer_model.setStringList(items) def commit(self): self.update_domain_role_hints() if self.data is not None: attributes = list(self.used_attrs) class_var = list(self.class_attrs) metas = list(self.meta_attrs) domain = Orange.data.Domain(attributes, class_var, metas) newdata = self.data.from_table(domain, self.data) self.output_data = newdata self.send("Data", newdata) self.send("Features", widget.AttributeList(attributes)) else: self.output_data = None self.send("Data", None) self.send("Features", None) def reset(self): if self.data is not None: self.available_attrs[:] = [] self.used_attrs[:] = self.data.domain.attributes self.class_attrs[:] = self.data.domain.class_vars self.meta_attrs[:] = self.data.domain.metas self.update_domain_role_hints() self.commit() def send_report(self): if not self.data or not self.output_data: return in_domain, out_domain = self.data.domain, self.output_data.domain self.report_domain("Input data", self.data.domain) if (in_domain.attributes, in_domain.class_vars, in_domain.metas) == ( out_domain.attributes, out_domain.class_vars, out_domain.metas): self.report_paragraph("Output data", "No changes.") else: self.report_domain("Output data", self.output_data.domain) diff = list(set(in_domain.variables + in_domain.metas) - set(out_domain.variables + out_domain.metas)) if diff: text = "%i (%s)" % (len(diff), ", ".join(x.name for x in diff)) self.report_items((("Removed", text),))
class ListViewSearch(QListView): """ An QListView with an implicit and transparent row filtering. """ def __init__(self, *a, preferred_size=None, **ak): super().__init__(*a, **ak) self.__search = QLineEdit(self, placeholderText="Filter...") self.__search.textEdited.connect(self.__setFilterString) # Use an QSortFilterProxyModel for filtering. Note that this is # never set on the view, only its rows insertes/removed signals are # connected to observe an update row hidden state. self.__pmodel = QSortFilterProxyModel( self, filterCaseSensitivity=Qt.CaseInsensitive) self.__pmodel.rowsAboutToBeRemoved.connect( self.__filter_rowsAboutToBeRemoved) self.__pmodel.rowsInserted.connect(self.__filter_rowsInserted) self.__layout() self.preferred_size = preferred_size self.setMinimumHeight(100) def setFilterPlaceholderText(self, text: str): self.__search.setPlaceholderText(text) def filterPlaceholderText(self) -> str: return self.__search.placeholderText() def setFilterProxyModel(self, proxy: QSortFilterProxyModel) -> None: """ Set an instance of QSortFilterProxyModel that will be used for filtering the model. The `proxy` must be a filtering proxy only; it MUST not sort the row of the model. The FilterListView takes ownership of the proxy. """ self.__pmodel.rowsAboutToBeRemoved.disconnect( self.__filter_rowsAboutToBeRemoved) self.__pmodel.rowsInserted.disconnect(self.__filter_rowsInserted) self.__pmodel = proxy proxy.setParent(self) self.__pmodel.rowsAboutToBeRemoved.connect( self.__filter_rowsAboutToBeRemoved) self.__pmodel.rowsInserted.connect(self.__filter_rowsInserted) self.__pmodel.setSourceModel(self.model()) self.__filter_reset() def filterProxyModel(self) -> QSortFilterProxyModel: return self.__pmodel def setModel(self, model: QAbstractItemModel) -> None: super().setModel(model) self.__pmodel.setSourceModel(model) self.__filter_reset() self.model().rowsInserted.connect(self.__model_rowInserted) def setRootIndex(self, index: QModelIndex) -> None: super().setRootIndex(index) self.__filter_reset() def __filter_reset(self): root = self.rootIndex() self.__filter(range(self.__pmodel.rowCount(root))) def __setFilterString(self, string: str): self.__pmodel.setFilterFixedString(string) def setFilterString(self, string: str): """Set the filter string.""" self.__search.setText(string) self.__pmodel.setFilterFixedString(string) def filterString(self): """Return the filter string.""" return self.__search.text() def __filter(self, rows: Iterable[int]) -> None: """Set hidden state for rows based on filter string""" root = self.rootIndex() pm = self.__pmodel for r in rows: self.setRowHidden(r, not pm.filterAcceptsRow(r, root)) def __filter_set(self, rows: Iterable[int], state: bool): for r in rows: self.setRowHidden(r, state) def __filter_rowsAboutToBeRemoved(self, parent: QModelIndex, start: int, end: int) -> None: fmodel = self.__pmodel mrange = QItemSelection(fmodel.index(start, 0, parent), fmodel.index(end, 0, parent)) mranges = fmodel.mapSelectionToSource(mrange) for mrange in mranges: self.__filter_set(range(mrange.top(), mrange.bottom() + 1), True) def __filter_rowsInserted(self, parent: QModelIndex, start: int, end: int) -> None: fmodel = self.__pmodel mrange = QItemSelection(fmodel.index(start, 0, parent), fmodel.index(end, 0, parent)) mranges = fmodel.mapSelectionToSource(mrange) for mrange in mranges: self.__filter_set(range(mrange.top(), mrange.bottom() + 1), False) def __model_rowInserted(self, _, start: int, end: int) -> None: """ Filter elements when inserted in list - proxy model's rowsAboutToBeRemoved is not called on elements that are hidden when inserting """ self.__filter(range(start, end + 1)) def resizeEvent(self, event: QResizeEvent) -> None: super().resizeEvent(event) def updateGeometries(self) -> None: super().updateGeometries() self.__layout() def __layout(self): margins = self.viewportMargins() search = self.__search sh = search.sizeHint() size = self.size() margins.setTop(sh.height()) vscroll = self.verticalScrollBar() style = self.style() transient = style.styleHint(QStyle.SH_ScrollBar_Transient, None, vscroll) w = size.width() if vscroll.isVisibleTo(self) and not transient: w = w - vscroll.width() - 1 search.setGeometry(0, 0, w, sh.height()) self.setViewportMargins(margins) def sizeHint(self): return (self.preferred_size if self.preferred_size is not None else super().sizeHint())