class DiscreteFeatureEditor(FeatureEditor): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.valuesedit = QLineEdit() self.valuesedit.textChanged.connect(self._invalidate) layout = self.layout() layout.addRow(self.tr("Values"), self.valuesedit) def setEditorData(self, data, domain): self.valuesedit.setText( ", ".join(v.replace(",", r"\,") for v in data.values)) super().setEditorData(data, domain) def editorData(self): values = self.valuesedit.text() values = re.split(r"(?<!\\),", values) values = tuple(filter(None, [v.replace(r"\,", ",").strip() for v in values])) return DiscreteDescriptor( name=self.nameedit.text(), values=values, base_value=-1, ordered=False, expression=self.expressionedit.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 VariableEditor(QWidget): """ An editor widget for a variable. Can edit the variable name, and its attributes dictionary. """ variable_changed = Signal() def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self.var = None # type: Optional[Variable] layout = QVBoxLayout() self.setLayout(layout) self.form = form = QFormLayout( fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow, objectName="editor-form-layout" ) layout.addLayout(self.form) self.name_edit = QLineEdit(objectName="name-editor") self.name_edit.editingFinished.connect( lambda: self.name_edit.isModified() and self.on_name_changed() ) form.addRow("Name:", self.name_edit) vlayout = QVBoxLayout(margin=0, spacing=1) self.labels_edit = view = QTreeView( objectName="annotation-pairs-edit", rootIsDecorated=False, editTriggers=QTreeView.DoubleClicked | QTreeView.EditKeyPressed, ) self.labels_model = model = DictItemsModel() view.setModel(model) view.selectionModel().selectionChanged.connect( self.on_label_selection_changed) agrp = QActionGroup(view, objectName="annotate-action-group") action_add = QAction( "+", self, objectName="action-add-label", toolTip="Add a new label.", shortcut=QKeySequence(QKeySequence.New), shortcutContext=Qt.WidgetShortcut ) action_delete = QAction( "\N{MINUS SIGN}", self, objectName="action-delete-label", toolTip="Remove selected label.", shortcut=QKeySequence(QKeySequence.Delete), shortcutContext=Qt.WidgetShortcut ) agrp.addAction(action_add) agrp.addAction(action_delete) view.addActions([action_add, action_delete]) def add_label(): row = [QStandardItem(), QStandardItem()] model.appendRow(row) idx = model.index(model.rowCount() - 1, 0) view.setCurrentIndex(idx) view.edit(idx) def remove_label(): rows = view.selectionModel().selectedRows(0) if rows: assert len(rows) == 1 idx = rows[0].row() model.removeRow(idx) action_add.triggered.connect(add_label) action_delete.triggered.connect(remove_label) agrp.setEnabled(False) self.add_label_action = action_add self.remove_label_action = action_delete # Necessary signals to know when the labels change model.dataChanged.connect(self.on_labels_changed) model.rowsInserted.connect(self.on_labels_changed) model.rowsRemoved.connect(self.on_labels_changed) vlayout.addWidget(self.labels_edit) hlayout = QHBoxLayout() hlayout.setContentsMargins(0, 0, 0, 0) button = FixedSizeButton( self, defaultAction=self.add_label_action, accessibleName="Add", ) hlayout.addWidget(button) button = FixedSizeButton( self, defaultAction=self.remove_label_action, accessibleName="Remove", ) hlayout.addWidget(button) hlayout.addStretch(10) vlayout.addLayout(hlayout) form.addRow("Labels:", vlayout) def set_data(self, var, transform=()): # type: (Optional[Variable], Sequence[Transform]) -> None """ Set the variable to edit. """ self.clear() self.var = var if var is not None: name = var.name annotations = var.annotations for tr in transform: if isinstance(tr, Rename): name = tr.name elif isinstance(tr, Annotate): annotations = tr.annotations self.name_edit.setText(name) self.labels_model.set_dict(dict(annotations)) self.add_label_action.actionGroup().setEnabled(True) else: self.add_label_action.actionGroup().setEnabled(False) def get_data(self): """Retrieve the modified variable. """ if self.var is None: return None, [] name = self.name_edit.text().strip() labels = tuple(sorted(self.labels_model.get_dict().items())) tr = [] if self.var.name != name: tr.append(Rename(name)) if self.var.annotations != labels: tr.append(Annotate(labels)) return self.var, tr def clear(self): """Clear the editor state. """ self.var = None self.name_edit.setText("") self.labels_model.setRowCount(0) @Slot() def on_name_changed(self): self.variable_changed.emit() @Slot() def on_labels_changed(self): self.variable_changed.emit() @Slot() def on_label_selection_changed(self): selected = self.labels_edit.selectionModel().selectedRows() self.remove_label_action.setEnabled(bool(len(selected)))
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 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()
class VariableEditor(QWidget): """An editor widget for a variable. Can edit the variable name, and its attributes dictionary. """ variable_changed = Signal() def __init__(self, parent=None): QWidget.__init__(self, parent) self.var = None self.setup_gui() def setup_gui(self): layout = QVBoxLayout() self.setLayout(layout) self.main_form = QFormLayout() self.main_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow) layout.addLayout(self.main_form) self._setup_gui_name() self._setup_gui_labels() def _setup_gui_name(self): self.name_edit = QLineEdit() self.main_form.addRow("Name:", self.name_edit) self.name_edit.editingFinished.connect(self.on_name_changed) def _setup_gui_labels(self): vlayout = QVBoxLayout() vlayout.setContentsMargins(0, 0, 0, 0) vlayout.setSpacing(1) self.labels_edit = QTreeView() self.labels_edit.setEditTriggers(QTreeView.CurrentChanged) self.labels_edit.setRootIsDecorated(False) self.labels_model = DictItemsModel() self.labels_edit.setModel(self.labels_model) self.labels_edit.selectionModel().selectionChanged.connect( self.on_label_selection_changed) # Necessary signals to know when the labels change self.labels_model.dataChanged.connect(self.on_labels_changed) self.labels_model.rowsInserted.connect(self.on_labels_changed) self.labels_model.rowsRemoved.connect(self.on_labels_changed) vlayout.addWidget(self.labels_edit) hlayout = QHBoxLayout() hlayout.setContentsMargins(0, 0, 0, 0) hlayout.setSpacing(1) self.add_label_action = QAction( "+", self, toolTip="Add a new label.", triggered=self.on_add_label, enabled=False, shortcut=QKeySequence(QKeySequence.New)) self.remove_label_action = QAction( unicodedata.lookup("MINUS SIGN"), self, toolTip="Remove selected label.", triggered=self.on_remove_label, enabled=False, shortcut=QKeySequence(QKeySequence.Delete)) button_size = gui.toolButtonSizeHint() button_size = QSize(button_size, button_size) button = QToolButton(self) button.setFixedSize(button_size) button.setDefaultAction(self.add_label_action) hlayout.addWidget(button) button = QToolButton(self) button.setFixedSize(button_size) button.setDefaultAction(self.remove_label_action) hlayout.addWidget(button) hlayout.addStretch(10) vlayout.addLayout(hlayout) self.main_form.addRow("Labels:", vlayout) def set_data(self, var): """Set the variable to edit. """ self.clear() self.var = var if var is not None: self.name_edit.setText(var.name) self.labels_model.set_dict(dict(var.attributes)) self.add_label_action.setEnabled(True) else: self.add_label_action.setEnabled(False) self.remove_label_action.setEnabled(False) def get_data(self): """Retrieve the modified variable. """ name = str(self.name_edit.text()).strip() labels = self.labels_model.get_dict() # Is the variable actually changed. if self.var is not None and not self.is_same(): var = type(self.var)(name) var.attributes.update(labels) self.var = var else: var = self.var return var def is_legal(self): name = str(self.name_edit.text()).strip() return not len(name) == 0 def is_same(self): """Is the current model state the same as the input. """ name = str(self.name_edit.text()).strip() labels = self.labels_model.get_dict() return (self.var is not None and name == self.var.name and labels == self.var.attributes) def clear(self): """Clear the editor state. """ self.var = None self.name_edit.setText("") self.labels_model.set_dict({}) def maybe_commit(self): if not self.is_same() and self.is_legal(): self.commit() def commit(self): """Emit a ``variable_changed()`` signal. """ self.variable_changed.emit() @Slot() def on_name_changed(self): self.maybe_commit() @Slot() def on_labels_changed(self, *args): self.maybe_commit() @Slot() def on_add_label(self): self.labels_model.appendRow([QStandardItem(""), QStandardItem("")]) row = self.labels_model.rowCount() - 1 index = self.labels_model.index(row, 0) self.labels_edit.edit(index) @Slot() def on_remove_label(self): rows = self.labels_edit.selectionModel().selectedRows() if rows: row = rows[0] self.labels_model.removeRow(row.row()) @Slot() def on_label_selection_changed(self): selected = self.labels_edit.selectionModel().selectedRows() self.remove_label_action.setEnabled(bool(len(selected)))
def _(edit: QLineEdit, value: str): edit.setText(value)
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
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") 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) 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) 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))
def setEditorData(editor: QLineEdit, index: QModelIndex): word_list = index.data(Qt.DisplayRole) editor.setText(word_list.name)
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" 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 # max_components limit allows scikit-learn to select a faster method for big data self._pca_projector = PCA(max_components=MAX_COMPONENTS) 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", 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) # 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, 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 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.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 data.is_sparse(): self.Error.sparse_data() self.clear_outputs() return 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.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.send("Transformed data", None) self.send("Components", None) self.send("PCA", 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 _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(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() @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 OWSql(OWWidget): name = "SQL Table" id = "orange.widgets.data.sql" description = "Load dataset 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 dataset 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 VariableEditor(QWidget): """ An editor widget for a variable. Can edit the variable name, and its attributes dictionary. """ variable_changed = Signal() def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self.var = None # type: Optional[Variable] layout = QVBoxLayout() self.setLayout(layout) self.form = form = QFormLayout( fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow, objectName="editor-form-layout" ) layout.addLayout(self.form) self.name_edit = QLineEdit(objectName="name-editor") self.name_edit.editingFinished.connect( lambda: self.name_edit.isModified() and self.on_name_changed() ) form.addRow("名称:", self.name_edit) vlayout = QVBoxLayout(margin=0, spacing=1) self.labels_edit = view = QTreeView( objectName="annotation-pairs-edit", rootIsDecorated=False, editTriggers=QTreeView.DoubleClicked | QTreeView.EditKeyPressed, ) self.labels_model = model = DictItemsModel() view.setModel(model) view.selectionModel().selectionChanged.connect( self.on_label_selection_changed) agrp = QActionGroup(view, objectName="annotate-action-group") action_add = QAction( "+", self, objectName="action-add-label", toolTip="添加新标签。", shortcut=QKeySequence(QKeySequence.New), shortcutContext=Qt.WidgetShortcut ) action_delete = QAction( "\N{MINUS SIGN}", self, objectName="action-delete-label", toolTip="删除选中标签。", shortcut=QKeySequence(QKeySequence.Delete), shortcutContext=Qt.WidgetShortcut ) agrp.addAction(action_add) agrp.addAction(action_delete) view.addActions([action_add, action_delete]) def add_label(): row = [QStandardItem(), QStandardItem()] model.appendRow(row) idx = model.index(model.rowCount() - 1, 0) view.setCurrentIndex(idx) view.edit(idx) def remove_label(): rows = view.selectionModel().selectedRows(0) if rows: assert len(rows) == 1 idx = rows[0].row() model.removeRow(idx) action_add.triggered.connect(add_label) action_delete.triggered.connect(remove_label) agrp.setEnabled(False) self.add_label_action = action_add self.remove_label_action = action_delete # Necessary signals to know when the labels change model.dataChanged.connect(self.on_labels_changed) model.rowsInserted.connect(self.on_labels_changed) model.rowsRemoved.connect(self.on_labels_changed) vlayout.addWidget(self.labels_edit) hlayout = QHBoxLayout() hlayout.setContentsMargins(0, 0, 0, 0) button = FixedSizeButton( self, defaultAction=self.add_label_action, accessibleName="Add", ) hlayout.addWidget(button) button = FixedSizeButton( self, defaultAction=self.remove_label_action, accessibleName="Remove", ) hlayout.addWidget(button) hlayout.addStretch(10) vlayout.addLayout(hlayout) form.addRow("标签:", vlayout) def set_data(self, var, transform=()): # type: (Optional[Variable], Sequence[Transform]) -> None """ Set the variable to edit. """ self.clear() self.var = var if var is not None: name = var.name annotations = var.annotations for tr in transform: if isinstance(tr, Rename): name = tr.name elif isinstance(tr, Annotate): annotations = tr.annotations self.name_edit.setText(name) self.labels_model.set_dict(dict(annotations)) self.add_label_action.actionGroup().setEnabled(True) else: self.add_label_action.actionGroup().setEnabled(False) def get_data(self): """Retrieve the modified variable. """ if self.var is None: return None, [] name = self.name_edit.text().strip() labels = tuple(sorted(self.labels_model.get_dict().items())) tr = [] if self.var.name != name: tr.append(Rename(name)) if self.var.annotations != labels: tr.append(Annotate(labels)) return self.var, tr def clear(self): """Clear the editor state. """ self.var = None self.name_edit.setText("") self.labels_model.setRowCount(0) @Slot() def on_name_changed(self): self.variable_changed.emit() @Slot() def on_labels_changed(self): self.variable_changed.emit() @Slot() def on_label_selection_changed(self): selected = self.labels_edit.selectionModel().selectedRows() self.remove_label_action.setEnabled(bool(len(selected)))
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 if sys.platform == "darwin": slmargin = self.__searchline.style() \ .pixelMetric(QStyle.PM_FocusFrameVMargin) popuprect_origin.adjust(slmargin / 2, 0, -slmargin * 1.5, slmargin) 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 ColorGradientSelection(QWidget): activated = Signal(int) currentIndexChanged = Signal(int) thresholdsChanged = Signal(float, float) centerChanged = Signal(float) def __init__(self, *args, thresholds=(0.0, 1.0), center=None, **kwargs): super().__init__(*args, **kwargs) low = round(clip(thresholds[0], 0., 1.), 2) high = round(clip(thresholds[1], 0., 1.), 2) high = max(low, high) self.__threshold_low, self.__threshold_high = low, high self.__center = center form = QFormLayout(formAlignment=Qt.AlignLeft, labelAlignment=Qt.AlignLeft, fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow) form.setContentsMargins(0, 0, 0, 0) self.gradient_cb = QComboBox( None, objectName="gradient-combo-box", ) self.gradient_cb.setAttribute(Qt.WA_LayoutUsesWidgetRect) icsize = self.style().pixelMetric(QStyle.PM_SmallIconSize, None, self.gradient_cb) self.gradient_cb.setIconSize(QSize(64, icsize)) model = itemmodels.ContinuousPalettesModel() model.setParent(self) self.gradient_cb.setModel(model) self.gradient_cb.activated[int].connect(self.activated) self.gradient_cb.currentIndexChanged.connect(self.currentIndexChanged) if center is not None: def __on_center_changed(): self.__center = float(self.center_edit.text() or "0") self.centerChanged.emit(self.__center) self.center_box = QWidget() center_layout = QHBoxLayout() self.center_box.setLayout(center_layout) width = QFontMetrics(self.font()).boundingRect("9999999").width() self.center_edit = QLineEdit(text=f"{self.__center}", maximumWidth=width, placeholderText="0", alignment=Qt.AlignRight) self.center_edit.setValidator(QDoubleValidator()) self.center_edit.editingFinished.connect(__on_center_changed) center_layout.setContentsMargins(0, 0, 0, 0) center_layout.addStretch(1) center_layout.addWidget(QLabel("Centered at")) center_layout.addWidget(self.center_edit) self.gradient_cb.currentIndexChanged.connect( self.__update_center_visibility) else: self.center_box = None slider_low = Slider(objectName="threshold-low-slider", minimum=0, maximum=100, value=int(low * 100), orientation=Qt.Horizontal, tickPosition=QSlider.TicksBelow, pageStep=10, toolTip=self.tr("Low gradient threshold"), whatsThis=self.tr( "Applying a low threshold will squeeze the " "gradient from the lower end")) slider_high = Slider(objectName="threshold-low-slider", minimum=0, maximum=100, value=int(high * 100), orientation=Qt.Horizontal, tickPosition=QSlider.TicksAbove, pageStep=10, toolTip=self.tr("High gradient threshold"), whatsThis=self.tr( "Applying a high threshold will squeeze the " "gradient from the higher end")) form.setWidget(0, QFormLayout.SpanningRole, self.gradient_cb) if self.center_box: form.setWidget(1, QFormLayout.SpanningRole, self.center_box) form.addRow(self.tr("Low:"), slider_low) form.addRow(self.tr("High:"), slider_high) self.slider_low = slider_low self.slider_high = slider_high self.slider_low.valueChanged.connect(self.__on_slider_low_moved) self.slider_high.valueChanged.connect(self.__on_slider_high_moved) self.setLayout(form) def setModel(self, model: QAbstractItemModel) -> None: self.gradient_cb.setModel(model) def model(self) -> QAbstractItemModel: return self.gradient_cb.model() def findData(self, data: Any, role: Qt.ItemDataRole) -> int: return self.gradient_cb.findData(data, role) def setCurrentIndex(self, index: int) -> None: self.gradient_cb.setCurrentIndex(index) self.__update_center_visibility() def currentIndex(self) -> int: return self.gradient_cb.currentIndex() currentIndex_ = Property(int, currentIndex, setCurrentIndex, notify=currentIndexChanged) def currentData(self, role=Qt.UserRole) -> Any: return self.gradient_cb.currentData(role) def thresholds(self) -> Tuple[float, float]: return self.__threshold_low, self.__threshold_high thresholds_ = Property(object, thresholds, notify=thresholdsChanged) def thresholdLow(self) -> float: return self.__threshold_low def setThresholdLow(self, low: float) -> None: self.setThresholds(low, max(self.__threshold_high, low)) thresholdLow_ = Property(float, thresholdLow, setThresholdLow, notify=thresholdsChanged) def thresholdHigh(self) -> float: return self.__threshold_high def setThresholdHigh(self, high: float) -> None: self.setThresholds(min(self.__threshold_low, high), high) def center(self) -> float: return self.__center def setCenter(self, center: float) -> None: self.__center = center self.center_edit.setText(f"{center}") self.centerChanged.emit(center) thresholdHigh_ = Property(float, thresholdLow, setThresholdLow, notify=thresholdsChanged) def __on_slider_low_moved(self, value: int) -> None: high = self.slider_high old = self.__threshold_low, self.__threshold_high self.__threshold_low = value / 100. if value >= high.value(): self.__threshold_high = value / 100. high.setSliderPosition(value) new = self.__threshold_low, self.__threshold_high if new != old: self.thresholdsChanged.emit(*new) def __on_slider_high_moved(self, value: int) -> None: low = self.slider_low old = self.__threshold_low, self.__threshold_high self.__threshold_high = value / 100. if low.value() >= value: self.__threshold_low = value / 100 low.setSliderPosition(value) new = self.__threshold_low, self.__threshold_high if new != old: self.thresholdsChanged.emit(*new) def setThresholds(self, low: float, high: float) -> None: low = round(clip(low, 0., 1.), 2) high = round(clip(high, 0., 1.), 2) if low > high: high = low if self.__threshold_low != low or self.__threshold_high != high: self.__threshold_high = high self.__threshold_low = low self.slider_low.setSliderPosition(low * 100) self.slider_high.setSliderPosition(high * 100) self.thresholdsChanged.emit(high, low) def __update_center_visibility(self): if self.center_box is None: return palette = self.currentData() self.center_box.setVisible( isinstance(palette, colorpalettes.Palette) and palette.flags & palette.Flags.Diverging != 0)
class FeatureEditor(QFrame): FUNCTIONS = dict(chain([(key, val) for key, val in math.__dict__.items() if not key.startswith("_")], [(key, val) for key, val in builtins.__dict__.items() if key in {"str", "float", "int", "len", "abs", "max", "min"}])) featureChanged = Signal() featureEdited = Signal() modifiedChanged = Signal(bool) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) layout = QFormLayout( fieldGrowthPolicy=QFormLayout.ExpandingFieldsGrow ) layout.setContentsMargins(0, 0, 0, 0) self.nameedit = QLineEdit( placeholderText="Name...", sizePolicy=QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) ) self.expressionedit = QLineEdit( placeholderText="Expression..." ) self.attrs_model = itemmodels.VariableListModel( ["Select Feature"], parent=self) self.attributescb = QComboBox( minimumContentsLength=16, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon, sizePolicy=QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) ) self.attributescb.setModel(self.attrs_model) sorted_funcs = sorted(self.FUNCTIONS) self.funcs_model = itemmodels.PyListModelTooltip() self.funcs_model.setParent(self) self.funcs_model[:] = chain(["Select Function"], sorted_funcs) self.funcs_model.tooltips[:] = chain( [''], [self.FUNCTIONS[func].__doc__ for func in sorted_funcs]) self.functionscb = QComboBox( minimumContentsLength=16, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon, sizePolicy=QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)) self.functionscb.setModel(self.funcs_model) hbox = QHBoxLayout() hbox.addWidget(self.attributescb) hbox.addWidget(self.functionscb) layout.addRow(self.nameedit, self.expressionedit) layout.addRow(self.tr(""), hbox) self.setLayout(layout) self.nameedit.editingFinished.connect(self._invalidate) self.expressionedit.textChanged.connect(self._invalidate) self.attributescb.currentIndexChanged.connect(self.on_attrs_changed) self.functionscb.currentIndexChanged.connect(self.on_funcs_changed) self._modified = False def setModified(self, modified): if not type(modified) is bool: raise TypeError if self._modified != modified: self._modified = modified self.modifiedChanged.emit(modified) def modified(self): return self._modified modified = Property(bool, modified, setModified, notify=modifiedChanged) def setEditorData(self, data, domain): self.nameedit.setText(data.name) self.expressionedit.setText(data.expression) self.setModified(False) self.featureChanged.emit() self.attrs_model[:] = ["Select Feature"] if domain is not None and (domain or domain.metas): self.attrs_model[:] += chain(domain.attributes, domain.class_vars, domain.metas) def editorData(self): return FeatureDescriptor(name=self.nameedit.text(), expression=self.nameedit.text()) def _invalidate(self): self.setModified(True) self.featureEdited.emit() self.featureChanged.emit() def on_attrs_changed(self): index = self.attributescb.currentIndex() if index > 0: attr = sanitized_name(self.attrs_model[index].name) self.insert_into_expression(attr) self.attributescb.setCurrentIndex(0) def on_funcs_changed(self): index = self.functionscb.currentIndex() if index > 0: func = self.funcs_model[index] if func in ["atan2", "fmod", "ldexp", "log", "pow", "copysign", "hypot"]: self.insert_into_expression(func + "(,)") self.expressionedit.cursorBackward(False, 2) elif func in ["e", "pi"]: self.insert_into_expression(func) else: self.insert_into_expression(func + "()") self.expressionedit.cursorBackward(False) self.functionscb.setCurrentIndex(0) def insert_into_expression(self, what): cp = self.expressionedit.cursorPosition() ct = self.expressionedit.text() text = ct[:cp] + what + ct[cp:] self.expressionedit.setText(text) self.expressionedit.setFocus()
class FeatureEditor(QFrame): FUNCTIONS = dict(chain([(key, val) for key, val in math.__dict__.items() if not key.startswith("_")], [(key, val) for key, val in builtins.__dict__.items() if key in {"str", "float", "int", "len", "abs", "max", "min"}])) featureChanged = Signal() featureEdited = Signal() modifiedChanged = Signal(bool) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) layout = QFormLayout( fieldGrowthPolicy=QFormLayout.ExpandingFieldsGrow ) layout.setContentsMargins(0, 0, 0, 0) self.nameedit = QLineEdit( placeholderText="名字...", sizePolicy=QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) ) self.expressionedit = QLineEdit( placeholderText="表达式..." ) self.attrs_model = itemmodels.VariableListModel( ["Select Feature"], parent=self) self.attributescb = gui.OrangeComboBox( minimumContentsLength=16, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon, sizePolicy=QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) ) self.attributescb.setModel(self.attrs_model) sorted_funcs = sorted(self.FUNCTIONS) self.funcs_model = itemmodels.PyListModelTooltip() self.funcs_model.setParent(self) self.funcs_model[:] = chain(["Select Function"], sorted_funcs) self.funcs_model.tooltips[:] = chain( [''], [self.FUNCTIONS[func].__doc__ for func in sorted_funcs]) self.functionscb = gui.OrangeComboBox( minimumContentsLength=16, sizeAdjustPolicy=QComboBox.AdjustToMinimumContentsLengthWithIcon, sizePolicy=QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)) self.functionscb.setModel(self.funcs_model) hbox = QHBoxLayout() hbox.addWidget(self.attributescb) hbox.addWidget(self.functionscb) layout.addRow(self.nameedit, self.expressionedit) layout.addRow(self.tr(""), hbox) self.setLayout(layout) self.nameedit.editingFinished.connect(self._invalidate) self.expressionedit.textChanged.connect(self._invalidate) self.attributescb.currentIndexChanged.connect(self.on_attrs_changed) self.functionscb.currentIndexChanged.connect(self.on_funcs_changed) self._modified = False def setModified(self, modified): if not type(modified) is bool: raise TypeError if self._modified != modified: self._modified = modified self.modifiedChanged.emit(modified) def modified(self): return self._modified modified = Property(bool, modified, setModified, notify=modifiedChanged) def setEditorData(self, data, domain): self.nameedit.setText(data.name) self.expressionedit.setText(data.expression) self.setModified(False) self.featureChanged.emit() self.attrs_model[:] = ["Select Feature"] if domain is not None and not domain.empty(): self.attrs_model[:] += chain(domain.attributes, domain.class_vars, domain.metas) def editorData(self): return FeatureDescriptor(name=self.nameedit.text(), expression=self.nameedit.text()) def _invalidate(self): self.setModified(True) self.featureEdited.emit() self.featureChanged.emit() def on_attrs_changed(self): index = self.attributescb.currentIndex() if index > 0: attr = sanitized_name(self.attrs_model[index].name) self.insert_into_expression(attr) self.attributescb.setCurrentIndex(0) def on_funcs_changed(self): index = self.functionscb.currentIndex() if index > 0: func = self.funcs_model[index] if func in ["atan2", "fmod", "ldexp", "log", "pow", "copysign", "hypot"]: self.insert_into_expression(func + "(,)") self.expressionedit.cursorBackward(False, 2) elif func in ["e", "pi"]: self.insert_into_expression(func) else: self.insert_into_expression(func + "()") self.expressionedit.cursorBackward(False) self.functionscb.setCurrentIndex(0) def insert_into_expression(self, what): cp = self.expressionedit.cursorPosition() ct = self.expressionedit.text() text = ct[:cp] + what + ct[cp:] self.expressionedit.setText(text) self.expressionedit.setFocus()
class VariableEditor(QWidget): """An editor widget for a variable. Can edit the variable name, and its attributes dictionary. """ variable_changed = Signal() def __init__(self, parent=None): QWidget.__init__(self, parent) self.var = None self.setup_gui() def setup_gui(self): layout = QVBoxLayout() self.setLayout(layout) self.main_form = QFormLayout() self.main_form.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow) layout.addLayout(self.main_form) self._setup_gui_name() self._setup_gui_labels() def _setup_gui_name(self): self.name_edit = QLineEdit() self.main_form.addRow("Name:", self.name_edit) self.name_edit.editingFinished.connect(self.on_name_changed) def _setup_gui_labels(self): vlayout = QVBoxLayout() vlayout.setContentsMargins(0, 0, 0, 0) vlayout.setSpacing(1) self.labels_edit = QTreeView() self.labels_edit.setEditTriggers(QTreeView.CurrentChanged) self.labels_edit.setRootIsDecorated(False) self.labels_model = DictItemsModel() self.labels_edit.setModel(self.labels_model) self.labels_edit.selectionModel().selectionChanged.connect( self.on_label_selection_changed) # Necessary signals to know when the labels change self.labels_model.dataChanged.connect(self.on_labels_changed) self.labels_model.rowsInserted.connect(self.on_labels_changed) self.labels_model.rowsRemoved.connect(self.on_labels_changed) vlayout.addWidget(self.labels_edit) hlayout = QHBoxLayout() hlayout.setContentsMargins(0, 0, 0, 0) hlayout.setSpacing(1) self.add_label_action = QAction( "+", self, toolTip="Add a new label.", triggered=self.on_add_label, enabled=False, shortcut=QKeySequence(QKeySequence.New)) self.remove_label_action = QAction( unicodedata.lookup("MINUS SIGN"), self, toolTip="Remove selected label.", triggered=self.on_remove_label, enabled=False, shortcut=QKeySequence(QKeySequence.Delete)) button_size = gui.toolButtonSizeHint() button_size = QSize(button_size, button_size) button = QToolButton(self) button.setFixedSize(button_size) button.setDefaultAction(self.add_label_action) hlayout.addWidget(button) button = QToolButton(self) button.setFixedSize(button_size) button.setDefaultAction(self.remove_label_action) hlayout.addWidget(button) hlayout.addStretch(10) vlayout.addLayout(hlayout) self.main_form.addRow("Labels:", vlayout) def set_data(self, var): """Set the variable to edit. """ self.clear() self.var = var if var is not None: self.name_edit.setText(var.name) self.labels_model.set_dict(dict(var.attributes)) self.add_label_action.setEnabled(True) else: self.add_label_action.setEnabled(False) self.remove_label_action.setEnabled(False) def get_data(self): """Retrieve the modified variable. """ name = str(self.name_edit.text()) labels = self.labels_model.get_dict() # Is the variable actually changed. if self.var is not None and not self.is_same(): var = type(self.var)(name) var.attributes.update(labels) self.var = var else: var = self.var return var def is_same(self): """Is the current model state the same as the input. """ name = str(self.name_edit.text()) labels = self.labels_model.get_dict() return (self.var is not None and name == self.var.name and labels == self.var.attributes) def clear(self): """Clear the editor state. """ self.var = None self.name_edit.setText("") self.labels_model.set_dict({}) def maybe_commit(self): if not self.is_same(): self.commit() def commit(self): """Emit a ``variable_changed()`` signal. """ self.variable_changed.emit() @Slot() def on_name_changed(self): self.maybe_commit() @Slot() def on_labels_changed(self, *args): self.maybe_commit() @Slot() def on_add_label(self): self.labels_model.appendRow([QStandardItem(""), QStandardItem("")]) row = self.labels_model.rowCount() - 1 index = self.labels_model.index(row, 0) self.labels_edit.edit(index) @Slot() def on_remove_label(self): rows = self.labels_edit.selectionModel().selectedRows() if rows: row = rows[0] self.labels_model.removeRow(row.row()) @Slot() def on_label_selection_changed(self): selected = self.labels_edit.selectionModel().selectedRows() self.remove_label_action.setEnabled(bool(len(selected)))
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())