def move_rows(self, view: QListView, offset: int, roles=(Qt.EditRole,)): rows = [idx.row() for idx in view.selectionModel().selectedRows()] model = view.model() # type: QAbstractItemModel rowcount = model.rowCount() newrows = [min(max(0, row + offset), rowcount - 1) for row in rows] def itemData(index): return {role: model.data(index, role) for role in roles} for row, newrow in sorted(zip(rows, newrows), reverse=offset > 0): d1 = itemData(model.index(row, 0)) d2 = itemData(model.index(newrow, 0)) model.setItemData(model.index(row, 0), d2) model.setItemData(model.index(newrow, 0), d1) selection = QItemSelection() for nrow in newrows: index = model.index(nrow, 0) selection.select(index, index) view.selectionModel().select( selection, QItemSelectionModel.ClearAndSelect) self.commit()
class OWQualityControl(widget.OWWidget): name = "Quality Control" description = "Experiment quality control" icon = "../widgets/icons/QualityControl.svg" priority = 5000 inputs = [("Experiment Data", Orange.data.Table, "set_data")] outputs = [] DISTANCE_FUNCTIONS = [("Distance from Pearson correlation", dist_pcorr), ("Euclidean distance", dist_eucl), ("Distance from Spearman correlation", dist_spearman)] settingsHandler = SetContextHandler() split_by_labels = settings.ContextSetting({}) sort_by_labels = settings.ContextSetting({}) selected_distance_index = settings.Setting(0) def __init__(self, parent=None): super().__init__(parent) ## Attributes self.data = None self.distances = None self.groups = None self.unique_pos = None self.base_group_index = 0 ## GUI box = gui.widgetBox(self.controlArea, "Info") self.info_box = gui.widgetLabel(box, "\n") ## Separate By box box = gui.widgetBox(self.controlArea, "Separate By") self.split_by_model = itemmodels.PyListModel(parent=self) self.split_by_view = QListView() self.split_by_view.setSelectionMode(QListView.ExtendedSelection) self.split_by_view.setModel(self.split_by_model) box.layout().addWidget(self.split_by_view) self.split_by_view.selectionModel().selectionChanged.connect( self.on_split_key_changed) ## Sort By box box = gui.widgetBox(self.controlArea, "Sort By") self.sort_by_model = itemmodels.PyListModel(parent=self) self.sort_by_view = QListView() self.sort_by_view.setSelectionMode(QListView.ExtendedSelection) self.sort_by_view.setModel(self.sort_by_model) box.layout().addWidget(self.sort_by_view) self.sort_by_view.selectionModel().selectionChanged.connect( self.on_sort_key_changed) ## Distance box box = gui.widgetBox(self.controlArea, "Distance Measure") gui.comboBox(box, self, "selected_distance_index", items=[name for name, _ in self.DISTANCE_FUNCTIONS], callback=self.on_distance_measure_changed) self.scene = QGraphicsScene() self.scene_view = QGraphicsView(self.scene) self.scene_view.setRenderHints(QPainter.Antialiasing) self.scene_view.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) self.mainArea.layout().addWidget(self.scene_view) self.scene_view.installEventFilter(self) self._disable_updates = False self._cached_distances = {} self._base_index_hints = {} self.main_widget = None self.resize(800, 600) def clear(self): """Clear the widget state.""" self.data = None self.distances = None self.groups = None self.unique_pos = None with disable_updates(self): self.split_by_model[:] = [] self.sort_by_model[:] = [] self.main_widget = None self.scene.clear() self.info_box.setText("\n") self._cached_distances = {} def set_data(self, data=None): """Set input experiment data.""" self.closeContext() self.clear() self.error(0) self.warning(0) if data is not None: keys = self.get_suitable_keys(data) if not keys: self.error(0, "Data has no suitable feature labels.") data = None self.data = data if data is not None: self.on_new_data() def update_label_candidates(self): """Update the label candidates selection GUI (Group/Sort By views). """ keys = self.get_suitable_keys(self.data) with disable_updates(self): self.split_by_model[:] = keys self.sort_by_model[:] = keys def get_suitable_keys(self, data): """ Return suitable attr label keys from the data where the key has at least two unique values in the data. """ attrs = [attr.attributes.items() for attr in data.domain.attributes] attrs = reduce(operator.iadd, attrs, []) # in case someone put non string values in attributes dict attrs = [(str(key), str(value)) for key, value in attrs] attrs = set(attrs) values = defaultdict(set) for key, value in attrs: values[key].add(value) keys = [key for key in values if len(values[key]) > 1] return keys def selected_split_by_labels(self): """Return the current selected split labels. """ sel_m = self.split_by_view.selectionModel() indices = [r.row() for r in sel_m.selectedRows()] return [self.sort_by_model[i] for i in indices] def selected_sort_by_labels(self): """Return the current selected sort labels """ sel_m = self.sort_by_view.selectionModel() indices = [r.row() for r in sel_m.selectedRows()] return [self.sort_by_model[i] for i in indices] def selected_distance(self): """Return the selected distance function. """ return self.DISTANCE_FUNCTIONS[self.selected_distance_index][1] def selected_base_group_index(self): """Return the selected base group index """ return self.base_group_index def selected_base_indices(self, base_group_index=None): indices = [] for g, ind in self.groups: if base_group_index is None: label = group_label(self.selected_split_by_labels(), g) ind = [i for i in ind if i is not None] i = self._base_index_hints.get(label, ind[0] if ind else None) else: i = ind[base_group_index] indices.append(i) return indices def on_new_data(self): """We have new data and need to recompute all. """ self.closeContext() self.update_label_candidates() self.info_box.setText( "%s genes \n%s experiments" % (len(self.data), len(self.data.domain.attributes)) ) self.base_group_index = 0 keys = self.get_suitable_keys(self.data) self.openContext(keys) ## Restore saved context settings (split/sort selection) split_by_labels = self.split_by_labels sort_by_labels = self.sort_by_labels def select(model, selection_model, selected_items): """Select items in a Qt item model view """ all_items = list(model) try: indices = [all_items.index(item) for item in selected_items] except: indices = [] for ind in indices: selection_model.select(model.index(ind), QItemSelectionModel.Select) with disable_updates(self): select(self.split_by_view.model(), self.split_by_view.selectionModel(), split_by_labels) select(self.sort_by_view.model(), self.sort_by_view.selectionModel(), sort_by_labels) with widget_disable(self): self.split_and_update() def on_split_key_changed(self, *args): """Split key has changed """ with widget_disable(self): if not self._disable_updates: self.base_group_index = 0 self.split_by_labels = self.selected_split_by_labels() self.split_and_update() def on_sort_key_changed(self, *args): """Sort key has changed """ with widget_disable(self): if not self._disable_updates: self.base_group_index = 0 self.sort_by_labels = self.selected_sort_by_labels() self.split_and_update() def on_distance_measure_changed(self): """Distance measure has changed """ if self.data is not None: with widget_disable(self): self.update_distances() self.replot_experiments() def on_view_resize(self, size): """The view with the quality plot has changed """ if self.main_widget: current = self.main_widget.size() self.main_widget.resize(size.width() - 6, current.height()) self.scene.setSceneRect(self.scene.itemsBoundingRect()) def on_rug_item_clicked(self, item): """An ``item`` in the quality plot has been clicked. """ update = False sort_by_labels = self.selected_sort_by_labels() if sort_by_labels and item.in_group: ## The item is part of the group if item.group_index != self.base_group_index: self.base_group_index = item.group_index update = True else: if sort_by_labels: # If the user clicked on an background item it # invalidates the sorted labels selection with disable_updates(self): self.sort_by_view.selectionModel().clear() update = True index = item.index group = item.group label = group_label(self.selected_split_by_labels(), group) if self._base_index_hints.get(label, 0) != index: self._base_index_hints[label] = index update = True if update: with widget_disable(self): self.split_and_update() def eventFilter(self, obj, event): if obj is self.scene_view and event.type() == QEvent.Resize: self.on_view_resize(event.size()) return super().eventFilter(obj, event) def split_and_update(self): """ Split the data based on the selected sort/split labels and update the quality plot. """ split_labels = self.selected_split_by_labels() sort_labels = self.selected_sort_by_labels() self.warning(0) if not split_labels: self.warning(0, "No separate by label selected.") self.groups, self.unique_pos = \ exp.separate_by(self.data, split_labels, consider=sort_labels, add_empty=True) self.groups = sorted(self.groups.items(), key=lambda t: list(map(float_if_posible, t[0]))) self.unique_pos = sorted(self.unique_pos.items(), key=lambda t: list(map(float_if_posible, t[0]))) if self.groups: if sort_labels: group_base = self.selected_base_group_index() base_indices = self.selected_base_indices(group_base) else: base_indices = self.selected_base_indices() self.update_distances(base_indices) self.replot_experiments() def get_cached_distances(self, measure): if measure not in self._cached_distances: attrs = self.data.domain.attributes mat = numpy.zeros((len(attrs), len(attrs))) self._cached_distances[measure] = \ (mat, set(zip(range(len(attrs)), range(len(attrs))))) return self._cached_distances[measure] def get_cached_distance(self, measure, i, j): matrix, computed = self.get_cached_distances(measure) key = (i, j) if i < j else (j, i) if key in computed: return matrix[i, j] else: return None def get_distance(self, measure, i, j): d = self.get_cached_distance(measure, i, j) if d is None: vec_i = take_columns(self.data, [i]) vec_j = take_columns(self.data, [j]) d = measure(vec_i, vec_j) mat, computed = self.get_cached_distances(measure) mat[i, j] = d key = key = (i, j) if i < j else (j, i) computed.add(key) return d def store_distance(self, measure, i, j, dist): matrix, computed = self.get_cached_distances(measure) key = (i, j) if i < j else (j, i) matrix[j, i] = matrix[i, j] = dist computed.add(key) def update_distances(self, base_indices=()): """Recompute the experiment distances. """ distance = self.selected_distance() if base_indices == (): base_group_index = self.selected_base_group_index() base_indices = [ind[base_group_index] \ for _, ind in self.groups] assert(len(base_indices) == len(self.groups)) base_distances = [] attributes = self.data.domain.attributes pb = gui.ProgressBar(self, len(self.groups) * len(attributes)) for (group, indices), base_index in zip(self.groups, base_indices): # Base column of the group if base_index is not None: base_vec = take_columns(self.data, [base_index]) distances = [] # Compute the distances between base column # and all the rest data columns. for i in range(len(attributes)): if i == base_index: distances.append(0.0) elif self.get_cached_distance(distance, i, base_index) is not None: distances.append(self.get_cached_distance(distance, i, base_index)) else: vec_i = take_columns(self.data, [i]) dist = distance(base_vec, vec_i) self.store_distance(distance, i, base_index, dist) distances.append(dist) pb.advance() base_distances.append(distances) else: base_distances.append(None) pb.finish() self.distances = base_distances def replot_experiments(self): """Replot the whole quality plot. """ self.scene.clear() labels = [] max_dist = numpy.nanmax(list(filter(None, self.distances))) rug_widgets = [] group_pen = QPen(Qt.black) group_pen.setWidth(2) group_pen.setCapStyle(Qt.RoundCap) background_pen = QPen(QColor(0, 0, 250, 150)) background_pen.setWidth(1) background_pen.setCapStyle(Qt.RoundCap) main_widget = QGraphicsWidget() layout = QGraphicsGridLayout() attributes = self.data.domain.attributes if self.data is not None: for (group, indices), dist_vec in zip(self.groups, self.distances): indices_set = set(indices) rug_items = [] if dist_vec is not None: for i, attr in enumerate(attributes): # Is this a within group distance or background in_group = i in indices_set if in_group: rug_item = ClickableRugItem(dist_vec[i] / max_dist, 1.0, self.on_rug_item_clicked) rug_item.setPen(group_pen) tooltip = experiment_description(attr) rug_item.setToolTip(tooltip) rug_item.group_index = indices.index(i) rug_item.setZValue(rug_item.zValue() + 1) else: rug_item = ClickableRugItem(dist_vec[i] / max_dist, 0.85, self.on_rug_item_clicked) rug_item.setPen(background_pen) tooltip = experiment_description(attr) rug_item.setToolTip(tooltip) rug_item.group = group rug_item.index = i rug_item.in_group = in_group rug_items.append(rug_item) rug_widget = RugGraphicsWidget(parent=main_widget) rug_widget.set_rug(rug_items) rug_widgets.append(rug_widget) label = group_label(self.selected_split_by_labels(), group) label_item = QGraphicsSimpleTextItem(label, main_widget) label_item = GraphicsSimpleTextLayoutItem(label_item, parent=layout) label_item.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) labels.append(label_item) for i, (label, rug_w) in enumerate(zip(labels, rug_widgets)): layout.addItem(label, i, 0, Qt.AlignVCenter) layout.addItem(rug_w, i, 1) layout.setRowMaximumHeight(i, 30) main_widget.setLayout(layout) self.scene.addItem(main_widget) self.main_widget = main_widget self.rug_widgets = rug_widgets self.labels = labels self.on_view_resize(self.scene_view.size())
def _set_selection(view: QListView, indices: List[int]): view.clearSelection() sm = view.selectionModel() model = view.model() for ind in indices: sm.select(model.index(ind), QItemSelectionModel.Select)
class SortedListWidget(QWidget): sortingOrderChanged = Signal() class _MyItemDelegate(QStyledItemDelegate): def __init__(self, sortingModel, parent): QStyledItemDelegate.__init__(self, parent) self.sortingModel = sortingModel def sizeHint(self, option, index): size = QStyledItemDelegate.sizeHint(self, option, index) return QSize(size.width(), size.height() + 4) def createEditor(self, parent, option, index): cb = QComboBox(parent) cb.setModel(self.sortingModel) cb.showPopup() return cb def setEditorData(self, editor, index): pass # TODO: sensible default def setModelData(self, editor, model, index): text = editor.currentText() model.setData(index, text) def __init__(self, *args): QWidget.__init__(self, *args) self.setContentsMargins(0, 0, 0, 0) gridLayout = QGridLayout() gridLayout.setContentsMargins(0, 0, 0, 0) gridLayout.setSpacing(1) model = QStandardItemModel(self) model.rowsInserted.connect(self.__changed) model.rowsRemoved.connect(self.__changed) model.dataChanged.connect(self.__changed) self._listView = QListView(self) self._listView.setModel(model) # self._listView.setDragEnabled(True) self._listView.setDropIndicatorShown(True) self._listView.setDragDropMode(QListView.InternalMove) self._listView.viewport().setAcceptDrops(True) self._listView.setMinimumHeight(100) gridLayout.addWidget(self._listView, 0, 0, 2, 2) vButtonLayout = QVBoxLayout() self._upAction = QAction( "\u2191", self, toolTip="Move up") self._upButton = QToolButton(self) self._upButton.setDefaultAction(self._upAction) self._upButton.setSizePolicy( QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) self._downAction = QAction( "\u2193", self, toolTip="Move down") self._downButton = QToolButton(self) self._downButton.setDefaultAction(self._downAction) self._downButton.setSizePolicy( QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) vButtonLayout.addWidget(self._upButton) vButtonLayout.addWidget(self._downButton) gridLayout.addLayout(vButtonLayout, 0, 2, 2, 1) hButtonLayout = QHBoxLayout() self._addAction = QAction("+", self) self._addButton = QToolButton(self) self._addButton.setDefaultAction(self._addAction) self._removeAction = QAction("-", self) self._removeButton = QToolButton(self) self._removeButton.setDefaultAction(self._removeAction) hButtonLayout.addWidget(self._addButton) hButtonLayout.addWidget(self._removeButton) hButtonLayout.addStretch(10) gridLayout.addLayout(hButtonLayout, 2, 0, 1, 2) self.setLayout(gridLayout) self._addAction.triggered.connect(self._onAddAction) self._removeAction.triggered.connect(self._onRemoveAction) self._upAction.triggered.connect(self._onUpAction) self._downAction.triggered.connect(self._onDownAction) def sizeHint(self): size = QWidget.sizeHint(self) return QSize(size.width(), 100) def _onAddAction(self): item = QStandardItem("") item.setFlags(item.flags() ^ Qt.ItemIsDropEnabled) self._listView.model().appendRow(item) self._listView.setCurrentIndex(item.index()) self._listView.edit(item.index()) def _onRemoveAction(self): current = self._listView.currentIndex() self._listView.model().takeRow(current.row()) def _onUpAction(self): row = self._listView.currentIndex().row() model = self._listView.model() if row > 0: items = model.takeRow(row) model.insertRow(row - 1, items) self._listView.setCurrentIndex(model.index(row - 1, 0)) def _onDownAction(self): row = self._listView.currentIndex().row() model = self._listView.model() if row < model.rowCount() and row >= 0: items = model.takeRow(row) if row == model.rowCount(): model.appendRow(items) else: model.insertRow(row + 1, items) self._listView.setCurrentIndex(model.index(row + 1, 0)) def setModel(self, model): """ Set a model to select items from """ self._model = model self._listView.setItemDelegate(self._MyItemDelegate(self._model, self)) def addItem(self, *args): """ Add a new entry in the list """ item = QStandardItem(*args) item.setFlags(item.flags() ^ Qt.ItemIsDropEnabled) self._listView.model().appendRow(item) def setItems(self, items): self._listView.model().clear() for item in items: self.addItem(item) def items(self): order = [] for row in range(self._listView.model().rowCount()): order.append(str(self._listView.model().item(row, 0).text())) return order def __changed(self): self.sortingOrderChanged.emit() sortingOrder = property(items, setItems)
class OWGenotypeDistances(widget.OWWidget): name = "Expression Profile Distances" description = ("Compute distances between expression profiles of " "different experimental factors.") icon = "../widgets/icons/GenotypeDistances.svg" priority = 1050 inputs = [("Data", Orange.data.Table, "set_data")] outputs = [("Distances", Orange.misc.DistMatrix), ("Sorted Data", Orange.data.Table)] settingsHandler = SetContextHandler() separate_keys = settings.ContextSetting({}) relevant_keys = settings.ContextSetting({}) distance_measure = settings.Setting(0) auto_commit = settings.Setting(False) DISTANCE_FUNCTIONS = [ ("Distance from Pearson correlation", dist_pcorr), ("Euclidean distance", dist_eucl), ("Distance from Spearman correlation", dist_spearman) ] def __init__(self, parent=None): super().__init__(self, parent) self.data = None self.partitions = [] self.matrix = None self.split_groups = [] self._disable_updates = False ######## # GUI ######## box = gui.widgetBox(self.controlArea, "Input") self.info_box = gui.widgetLabel(box, "No data on input\n") box = gui.widgetBox(self.controlArea, "Separate By", addSpace=True) self.separate_view = QListView( selectionMode=QListView.MultiSelection ) box.layout().addWidget(self.separate_view) box = gui.widgetBox(self.controlArea, "Sort By", addSpace=True) self.relevant_view = QListView( selectionMode=QListView.MultiSelection) box.layout().addWidget(self.relevant_view) self.distance_view = gui.comboBox( self.controlArea, self, "distance_measure", box="Distance Measure", items=[name for name, _ in self.DISTANCE_FUNCTIONS]) gui.rubber(self.controlArea) gui.auto_commit(self.controlArea, self, "auto_commit", "Commit") self.groups_box = gui.widgetBox(self.mainArea, "Groups") self.groups_scroll_area = QScrollArea() self.groups_box.layout().addWidget(self.groups_scroll_area) def sizeHint(self): return QSize(800, 600) def clear(self): self.data = None self.partitions = [] self.split_groups = [] self.matrix = None def get_suitable_keys(self, data): """Return suitable attr label keys from the data where the key has at least two unique values in the data. """ attrs = [attr.attributes.items() for attr in data.domain.attributes] attrs = reduce(operator.iadd, attrs, []) # in case someone put non string values in attributes dict attrs = [(str(key), str(value)) for key, value in attrs] attrs = set(attrs) values = defaultdict(set) for key, value in attrs: values[key].add(value) keys = [key for key in values if len(values[key]) > 1] return keys def set_data(self, data=None): """Set the input data table. """ self.closeContext() self.clear() self.error(0) self.warning(0) if data and not self.get_suitable_keys(data): self.error(0, "Data has no suitable column labels.") data = None self.data = data if data: self.info_box.setText("{0} genes\n{1} experiments" .format(len(data), len(data.domain))) self.update_control() self.split_data() else: self.separate_view.setModel(itemmodels.PyListModel([])) self.relevant_view.setModel(itemmodels.PyListModel([])) self.groups_scroll_area.setWidget(QWidget()) self.info_box.setText("No data on input.\n") self.commit() def update_control(self): """Update the control area of the widget. Populate the list views with keys from attribute labels. """ keys = self.get_suitable_keys(self.data) model = itemmodels.PyListModel(keys) self.separate_view.setModel(model) self.separate_view.selectionModel().selectionChanged.connect( self.on_separate_key_changed) model = itemmodels.PyListModel(keys) self.relevant_view.setModel(model) self.relevant_view.selectionModel().selectionChanged.connect( self.on_relevant_key_changed) self.openContext(keys) # Get the selected keys from the open context separate_keys = self.separate_keys relevant_keys = self.relevant_keys def select(model, selection_model, selected_items): all_items = list(model) try: indices = [all_items.index(item) for item in selected_items] except: indices = [] selection = QItemSelection() for ind in indices: index = model.index(ind) selection.select(index, index) selection_model.select(selection, QItemSelectionModel.Select) self._disable_updates = True try: select(self.relevant_view.model(), self.relevant_view.selectionModel(), relevant_keys) select(self.separate_view.model(), self.separate_view.selectionModel(), separate_keys) finally: self._disable_updates = False def on_separate_key_changed(self, *args): if not self._disable_updates: self.separate_keys = self.selected_separeate_by_keys() self.split_data() def on_relevant_key_changed(self, *args): if not self._disable_updates: self.relevant_keys = self.selected_relevant_keys() self.split_data() def selected_separeate_by_keys(self): """Return the currently selected separate by keys """ rows = self.separate_view.selectionModel().selectedRows() rows = sorted([idx.row() for idx in rows]) keys = [self.separate_view.model()[row] for row in rows] return keys def selected_relevant_keys(self): """Return the currently selected relevant keys """ rows = self.relevant_view.selectionModel().selectedRows() rows = sorted([idx.row() for idx in rows]) keys = [self.relevant_view.model()[row] for row in rows] return keys def split_data(self): """Split the data and update the Groups widget """ separate_keys = self.selected_separeate_by_keys() relevant_keys = self.selected_relevant_keys() self.warning(0) if not separate_keys: self.warning(0, "No separate by column selected.") partitions, uniquepos = separate_by( self.data, separate_keys, consider=relevant_keys) partitions = partitions.items() all_values = defaultdict(set) for a in [at.attributes for at in self.data.domain.attributes]: for k, v in a.items(): all_values[k].add(v) # sort groups pkeys = [key for key, _ in partitions] types = [data_type([a[i] for a in pkeys]) for i in range(len(pkeys[0]))] partitions = sorted(partitions, key=lambda x: tuple(types[i](v) for i,v in enumerate(x[0]))) split_groups = [] # Collect relevant key value pairs for all columns relevant_items = None for keys, indices in partitions: if relevant_items == None: relevant_items = [defaultdict(set) for _ in indices] for i, ind in enumerate(indices): if ind is not None: attr = self.data.domain[ind] for key in relevant_keys: relevant_items[i][key].add(attr.attributes[key]) #those with different values between rows are not relevant for d in relevant_items: for k, s in list(d.items()): if len(s) > 1: del d[k] else: d[k] = s.pop() def get_attr(attr_index, i): if attr_index is None: attr = Orange.data.ContinuousVariable(next(missing_name_gen), compute_value=lambda x: None) attr.attributes.update(relevant_items[i]) return attr else: return self.data.domain[attr_index] for keys, indices in partitions: attrs = [get_attr(attr_index, i) for i, attr_index in enumerate(indices)] for attr in attrs: attr.attributes.update(zip(separate_keys, keys)) domain = Orange.data.Domain(attrs, [], self.data.domain.metas) split_groups.append((keys, domain)) self.set_groups(separate_keys, split_groups, relevant_keys, relevant_items, all_values, uniquepos) self.partitions = partitions self.split_groups = split_groups self.commit() def set_groups(self, keys, groups, relevant_keys, relevant_items, all_values, uniquepos): """Set the current data groups and update the Group widget """ layout = QVBoxLayout() header_widths = [] header_views = [] palette = self.palette() all_values = all_values.keys() def for_print(rd): attrs = [] for d in rd: attr = Orange.data.ContinuousVariable(next(inactive_name_gen)) attr.attributes.update(d) attrs.append(attr) return Orange.data.Domain(attrs, None) for separatev, domain in [(None, for_print(relevant_items))] + groups: label = None if separatev is not None: ann_vals = " <b>|</b> ".join(["<b>{0}</b> = {1}".format(key,val) \ for key, val in zip(keys, separatev)]) label = QLabel(ann_vals) model = QStandardItemModel() for i, attr in enumerate(domain.attributes): item = QStandardItem() if separatev is not None: isunique = uniquepos[separatev][i] else: isunique = all(a[i] for a in uniquepos.values()) if str(attr.name).startswith("!!missing "): # TODO: Change this to not depend on name header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \ for key in all_values if key not in relevant_items[i]] header_text = "\n".join(header_text) if header_text else "Empty" item.setData(header_text, Qt.DisplayRole) item.setFlags(Qt.NoItemFlags) item.setData(QColor(Qt.red), Qt.ForegroundRole) item.setData(palette.color(QPalette.Disabled, QPalette.Window), Qt.BackgroundRole) item.setData("Missing feature.", Qt.ToolTipRole) elif str(attr.name).startswith("!!inactive "): header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \ for key in all_values if key in relevant_items[i]] header_text = "\n".join(header_text) if header_text else "No descriptor" item.setData(header_text, Qt.DisplayRole) item.setData(palette.color(QPalette.Disabled, QPalette.Window), Qt.BackgroundRole) else: header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \ for key in all_values if key not in relevant_items[i]] header_text = "\n".join(header_text) if header_text else "Empty" item.setData(header_text, Qt.DisplayRole) item.setData(attr.name, Qt.ToolTipRole) if not isunique: item.setData(QColor(Qt.red), Qt.ForegroundRole) model.setHorizontalHeaderItem(i, item) attr_count = len(domain.attributes) view = MyHeaderView(Qt.Horizontal) view.setResizeMode(QHeaderView.Fixed) view.setModel(model) hint = view.sizeHint() view.setMaximumHeight(hint.height()) widths = [view.sectionSizeHint(i) for i in range(attr_count)] header_widths.append(widths) header_views.append(view) if label: layout.addWidget(label) layout.addWidget(view) layout.addSpacing(8) # Make all header sections the same width width_sum = 0 max_header_count = max([h.count() for h in header_views]) for i in range(max_header_count): max_width = max([w[i] for w in header_widths if i < len(w)] or [0]) for view in header_views: if i < view.count(): view.resizeSection(i, max_width) width_sum += max_width + 2 for h in header_views: h.setMinimumWidth(h.length() + 4) widget = QWidget() widget.setLayout(layout) widget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Maximum) layout.activate() max_width = max(h.length() for h in header_views) + 20 left, _, right, _ = self.getContentsMargins() widget.setMinimumWidth(width_sum) widget.setMinimumWidth(max_width + left + right) self.groups_scroll_area.setWidget(widget) def compute_distances(self, separate_keys, partitions, data): """Compute the distances between genotypes. """ if separate_keys and partitions: self.progressBarInit() # matrix = Orange.misc.DistMatrix(len(partitions)) matrix = numpy.zeros((len(partitions), len(partitions))) profiles = [linearize(data, indices) for _, indices in partitions] dist_func = self.DISTANCE_FUNCTIONS[self.distance_measure][1] # from Orange.utils import progress_bar_milestones count = (len(profiles) * len(profiles) - 1) / 2 # milestones = progress_bar_milestones(count) iter_count = 0 for i in range(len(profiles)): for j in range(i + 1, len(profiles)): matrix[i, j] = dist_func(profiles[i], profiles[j]) matrix[j, i] = matrix[i, j] iter_count += 1 # if iter_count in milestones: self.progressBarSet(100.0 * iter_count / count) self.progressBarFinished() items = [["{0}={1}".format(key, value) for key, value in zip(separate_keys, values)] for values, _ in partitions] items = [" | ".join(item) for item in items] # matrix.setattr("items", items) matrix = Orange.misc.DistMatrix(matrix) else: matrix = None self.matrix = matrix def commit(self): separate_keys = self.selected_separeate_by_keys() self.compute_distances(separate_keys, self.partitions, self.data) if self.split_groups: all_attrs = [] for group, domain in self.split_groups: attrs = [] group_name = " | ".join("{0}={1}".format(*item) for item in zip(separate_keys, group)) for attr in domain.attributes: newattr = clone_attr(attr) newattr.attributes["<GENOTYPE GROUP>"] = group_name # Need a better way to pass the groups to downstream widgets. attrs.append(newattr) all_attrs.extend(attrs) domain = Orange.data.Domain(all_attrs, self.data.domain.class_vars, self.data.domain.metas) data = Orange.data.Table(domain, self.data) else: data = None self.send("Sorted Data", data) self.send("Distances", self.matrix) def send_report(self): self.report_items(( ("Separate By", ", ".join(self.selected_separeate_by_keys())), ("Sort By", ", ".join(self.selected_relevant_keys())), ("Distance Measure", self.DISTANCE_FUNCTIONS[self.distance_measure][0]) )) layout = self.groups_scroll_area.widget().layout() html = "<table>" for i in range(layout.count()): item = layout.itemAt(i) if isinstance(item, QSpacerItem): html += "<tr><td></td></tr>" elif isinstance(item, QWidgetItem): hor = item.widget() if isinstance(hor, QLabel): label = hor.text() html += "<tr><td><b>%s</b></td></tr>" % label elif isinstance(hor, QHeaderView): model = hor.model() content = (model.horizontalHeaderItem(col) for col in range(model.columnCount())) content = (item.text().replace('\n', "<br/>") for item in content) html += "<tr>" + ''.join("<td>{}</td>".format(item) for item in content) + "</tr>" html += "</table>" self.report_raw("Groups", html)
class OWGenotypeDistances(widget.OWWidget): name = "Expression Profile Distances" description = ("Compute distances between expression profiles of " "different experimental factors.") icon = "../widgets/icons/GenotypeDistances.svg" priority = 1050 inputs = [("Data", Orange.data.Table, "set_data")] outputs = [("Distances", Orange.misc.DistMatrix), ("Sorted Data", Orange.data.Table)] settingsHandler = SetContextHandler() separate_keys = settings.ContextSetting({}) relevant_keys = settings.ContextSetting({}) distance_measure = settings.Setting(0) auto_commit = settings.Setting(False) DISTANCE_FUNCTIONS = [("Distance from Pearson correlation", dist_pcorr), ("Euclidean distance", dist_eucl), ("Distance from Spearman correlation", dist_spearman) ] def __init__(self, parent=None): super().__init__(self, parent) self.data = None self.partitions = [] self.matrix = None self.split_groups = [] self._disable_updates = False ######## # GUI ######## box = gui.widgetBox(self.controlArea, "Input") self.info_box = gui.widgetLabel(box, "No data on input\n") box = gui.widgetBox(self.controlArea, "Separate By", addSpace=True) self.separate_view = QListView(selectionMode=QListView.MultiSelection) box.layout().addWidget(self.separate_view) box = gui.widgetBox(self.controlArea, "Sort By", addSpace=True) self.relevant_view = QListView(selectionMode=QListView.MultiSelection) box.layout().addWidget(self.relevant_view) self.distance_view = gui.comboBox( self.controlArea, self, "distance_measure", box="Distance Measure", items=[name for name, _ in self.DISTANCE_FUNCTIONS]) gui.rubber(self.controlArea) gui.auto_commit(self.controlArea, self, "auto_commit", "Commit") self.groups_box = gui.widgetBox(self.mainArea, "Groups") self.groups_scroll_area = QScrollArea() self.groups_box.layout().addWidget(self.groups_scroll_area) def sizeHint(self): return QSize(800, 600) def clear(self): self.data = None self.partitions = [] self.split_groups = [] self.matrix = None def get_suitable_keys(self, data): """Return suitable attr label keys from the data where the key has at least two unique values in the data. """ attrs = [attr.attributes.items() for attr in data.domain.attributes] attrs = reduce(operator.iadd, attrs, []) # in case someone put non string values in attributes dict attrs = [(str(key), str(value)) for key, value in attrs] attrs = set(attrs) values = defaultdict(set) for key, value in attrs: values[key].add(value) keys = [key for key in values if len(values[key]) > 1] return keys def set_data(self, data=None): """Set the input data table. """ self.closeContext() self.clear() self.error(0) self.warning(0) if data and not self.get_suitable_keys(data): self.error(0, "Data has no suitable column labels.") data = None self.data = data if data: self.info_box.setText("{0} genes\n{1} experiments".format( len(data), len(data.domain))) self.update_control() self.split_data() else: self.separate_view.setModel(itemmodels.PyListModel([])) self.relevant_view.setModel(itemmodels.PyListModel([])) self.groups_scroll_area.setWidget(QWidget()) self.info_box.setText("No data on input.\n") self.commit() def update_control(self): """Update the control area of the widget. Populate the list views with keys from attribute labels. """ keys = self.get_suitable_keys(self.data) model = itemmodels.PyListModel(keys) self.separate_view.setModel(model) self.separate_view.selectionModel().selectionChanged.connect( self.on_separate_key_changed) model = itemmodels.PyListModel(keys) self.relevant_view.setModel(model) self.relevant_view.selectionModel().selectionChanged.connect( self.on_relevant_key_changed) self.openContext(keys) # Get the selected keys from the open context separate_keys = self.separate_keys relevant_keys = self.relevant_keys def select(model, selection_model, selected_items): all_items = list(model) try: indices = [all_items.index(item) for item in selected_items] except: indices = [] selection = QItemSelection() for ind in indices: index = model.index(ind) selection.select(index, index) selection_model.select(selection, QItemSelectionModel.Select) self._disable_updates = True try: select(self.relevant_view.model(), self.relevant_view.selectionModel(), relevant_keys) select(self.separate_view.model(), self.separate_view.selectionModel(), separate_keys) finally: self._disable_updates = False def on_separate_key_changed(self, *args): if not self._disable_updates: self.separate_keys = self.selected_separeate_by_keys() self.split_data() def on_relevant_key_changed(self, *args): if not self._disable_updates: self.relevant_keys = self.selected_relevant_keys() self.split_data() def selected_separeate_by_keys(self): """Return the currently selected separate by keys """ rows = self.separate_view.selectionModel().selectedRows() rows = sorted([idx.row() for idx in rows]) keys = [self.separate_view.model()[row] for row in rows] return keys def selected_relevant_keys(self): """Return the currently selected relevant keys """ rows = self.relevant_view.selectionModel().selectedRows() rows = sorted([idx.row() for idx in rows]) keys = [self.relevant_view.model()[row] for row in rows] return keys def split_data(self): """Split the data and update the Groups widget """ separate_keys = self.selected_separeate_by_keys() relevant_keys = self.selected_relevant_keys() self.warning(0) if not separate_keys: self.warning(0, "No separate by column selected.") partitions, uniquepos = separate_by(self.data, separate_keys, consider=relevant_keys) partitions = partitions.items() all_values = defaultdict(set) for a in [at.attributes for at in self.data.domain.attributes]: for k, v in a.items(): all_values[k].add(v) # sort groups pkeys = [key for key, _ in partitions] types = [ data_type([a[i] for a in pkeys]) for i in range(len(pkeys[0])) ] partitions = sorted(partitions, key=lambda x: tuple(types[i](v) for i, v in enumerate(x[0]))) split_groups = [] # Collect relevant key value pairs for all columns relevant_items = None for keys, indices in partitions: if relevant_items == None: relevant_items = [defaultdict(set) for _ in indices] for i, ind in enumerate(indices): if ind is not None: attr = self.data.domain[ind] for key in relevant_keys: relevant_items[i][key].add(attr.attributes[key]) #those with different values between rows are not relevant for d in relevant_items: for k, s in list(d.items()): if len(s) > 1: del d[k] else: d[k] = s.pop() def get_attr(attr_index, i): if attr_index is None: attr = Orange.data.ContinuousVariable( next(missing_name_gen), compute_value=lambda x: None) attr.attributes.update(relevant_items[i]) return attr else: return self.data.domain[attr_index] for keys, indices in partitions: attrs = [ get_attr(attr_index, i) for i, attr_index in enumerate(indices) ] for attr in attrs: attr.attributes.update(zip(separate_keys, keys)) domain = Orange.data.Domain(attrs, [], self.data.domain.metas) split_groups.append((keys, domain)) self.set_groups(separate_keys, split_groups, relevant_keys, relevant_items, all_values, uniquepos) self.partitions = partitions self.split_groups = split_groups self.commit() def set_groups(self, keys, groups, relevant_keys, relevant_items, all_values, uniquepos): """Set the current data groups and update the Group widget """ layout = QVBoxLayout() header_widths = [] header_views = [] palette = self.palette() all_values = all_values.keys() def for_print(rd): attrs = [] for d in rd: attr = Orange.data.ContinuousVariable(next(inactive_name_gen)) attr.attributes.update(d) attrs.append(attr) return Orange.data.Domain(attrs, None) for separatev, domain in [(None, for_print(relevant_items))] + groups: label = None if separatev is not None: ann_vals = " <b>|</b> ".join(["<b>{0}</b> = {1}".format(key,val) \ for key, val in zip(keys, separatev)]) label = QLabel(ann_vals) model = QStandardItemModel() for i, attr in enumerate(domain.attributes): item = QStandardItem() if separatev is not None: isunique = uniquepos[separatev][i] else: isunique = all(a[i] for a in uniquepos.values()) if str(attr.name).startswith( "!!missing " ): # TODO: Change this to not depend on name header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \ for key in all_values if key not in relevant_items[i]] header_text = "\n".join( header_text) if header_text else "Empty" item.setData(header_text, Qt.DisplayRole) item.setFlags(Qt.NoItemFlags) item.setData(QColor(Qt.red), Qt.ForegroundRole) item.setData( palette.color(QPalette.Disabled, QPalette.Window), Qt.BackgroundRole) item.setData("Missing feature.", Qt.ToolTipRole) elif str(attr.name).startswith("!!inactive "): header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \ for key in all_values if key in relevant_items[i]] header_text = "\n".join( header_text) if header_text else "No descriptor" item.setData(header_text, Qt.DisplayRole) item.setData( palette.color(QPalette.Disabled, QPalette.Window), Qt.BackgroundRole) else: header_text = ["{0}={1}".format(key, attr.attributes.get(key, "?")) \ for key in all_values if key not in relevant_items[i]] header_text = "\n".join( header_text) if header_text else "Empty" item.setData(header_text, Qt.DisplayRole) item.setData(attr.name, Qt.ToolTipRole) if not isunique: item.setData(QColor(Qt.red), Qt.ForegroundRole) model.setHorizontalHeaderItem(i, item) attr_count = len(domain.attributes) view = MyHeaderView(Qt.Horizontal) view.setResizeMode(QHeaderView.Fixed) view.setModel(model) hint = view.sizeHint() view.setMaximumHeight(hint.height()) widths = [view.sectionSizeHint(i) for i in range(attr_count)] header_widths.append(widths) header_views.append(view) if label: layout.addWidget(label) layout.addWidget(view) layout.addSpacing(8) # Make all header sections the same width width_sum = 0 max_header_count = max([h.count() for h in header_views]) for i in range(max_header_count): max_width = max([w[i] for w in header_widths if i < len(w)] or [0]) for view in header_views: if i < view.count(): view.resizeSection(i, max_width) width_sum += max_width + 2 for h in header_views: h.setMinimumWidth(h.length() + 4) widget = QWidget() widget.setLayout(layout) widget.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Maximum) layout.activate() max_width = max(h.length() for h in header_views) + 20 left, _, right, _ = self.getContentsMargins() widget.setMinimumWidth(width_sum) widget.setMinimumWidth(max_width + left + right) self.groups_scroll_area.setWidget(widget) def compute_distances(self, separate_keys, partitions, data): """Compute the distances between genotypes. """ if separate_keys and partitions: self.progressBarInit() # matrix = Orange.misc.DistMatrix(len(partitions)) matrix = numpy.zeros((len(partitions), len(partitions))) profiles = [linearize(data, indices) for _, indices in partitions] dist_func = self.DISTANCE_FUNCTIONS[self.distance_measure][1] # from Orange.utils import progress_bar_milestones count = (len(profiles) * len(profiles) - 1) / 2 # milestones = progress_bar_milestones(count) iter_count = 0 for i in range(len(profiles)): for j in range(i + 1, len(profiles)): matrix[i, j] = dist_func(profiles[i], profiles[j]) matrix[j, i] = matrix[i, j] iter_count += 1 # if iter_count in milestones: self.progressBarSet(100.0 * iter_count / count) self.progressBarFinished() items = [[ "{0}={1}".format(key, value) for key, value in zip(separate_keys, values) ] for values, _ in partitions] items = [" | ".join(item) for item in items] # matrix.setattr("items", items) matrix = Orange.misc.DistMatrix(matrix) else: matrix = None self.matrix = matrix def commit(self): separate_keys = self.selected_separeate_by_keys() self.compute_distances(separate_keys, self.partitions, self.data) if self.split_groups: all_attrs = [] for group, domain in self.split_groups: attrs = [] group_name = " | ".join("{0}={1}".format(*item) for item in zip(separate_keys, group)) for attr in domain.attributes: newattr = clone_attr(attr) newattr.attributes[ "<GENOTYPE GROUP>"] = group_name # Need a better way to pass the groups to downstream widgets. attrs.append(newattr) all_attrs.extend(attrs) domain = Orange.data.Domain(all_attrs, self.data.domain.class_vars, self.data.domain.metas) data = Orange.data.Table(domain, self.data) else: data = None self.send("Sorted Data", data) self.send("Distances", self.matrix) def send_report(self): self.report_items( (("Separate By", ", ".join(self.selected_separeate_by_keys())), ("Sort By", ", ".join(self.selected_relevant_keys())), ("Distance Measure", self.DISTANCE_FUNCTIONS[self.distance_measure][0]))) layout = self.groups_scroll_area.widget().layout() html = "<table>" for i in range(layout.count()): item = layout.itemAt(i) if isinstance(item, QSpacerItem): html += "<tr><td></td></tr>" elif isinstance(item, QWidgetItem): hor = item.widget() if isinstance(hor, QLabel): label = hor.text() html += "<tr><td><b>%s</b></td></tr>" % label elif isinstance(hor, QHeaderView): model = hor.model() content = (model.horizontalHeaderItem(col) for col in range(model.columnCount())) content = (item.text().replace('\n', "<br/>") for item in content) html += "<tr>" + ''.join("<td>{}</td>".format(item) for item in content) + "</tr>" html += "</table>" self.report_raw("Groups", html)
class SortedListWidget(QWidget): sortingOrderChanged = Signal() class _MyItemDelegate(QStyledItemDelegate): def __init__(self, sortingModel, parent): QStyledItemDelegate.__init__(self, parent) self.sortingModel = sortingModel def sizeHint(self, option, index): size = QStyledItemDelegate.sizeHint(self, option, index) return QSize(size.width(), size.height() + 4) def createEditor(self, parent, option, index): cb = QComboBox(parent) cb.setModel(self.sortingModel) cb.showPopup() return cb def setEditorData(self, editor, index): pass # TODO: sensible default def setModelData(self, editor, model, index): text = editor.currentText() model.setData(index, text) def __init__(self, *args): QWidget.__init__(self, *args) self.setContentsMargins(0, 0, 0, 0) gridLayout = QGridLayout() gridLayout.setContentsMargins(0, 0, 0, 0) gridLayout.setSpacing(1) model = QStandardItemModel(self) model.rowsInserted.connect(self.__changed) model.rowsRemoved.connect(self.__changed) model.dataChanged.connect(self.__changed) self._listView = QListView(self) self._listView.setModel(model) # self._listView.setDragEnabled(True) self._listView.setDropIndicatorShown(True) self._listView.setDragDropMode(QListView.InternalMove) self._listView.viewport().setAcceptDrops(True) self._listView.setMinimumHeight(100) gridLayout.addWidget(self._listView, 0, 0, 2, 2) vButtonLayout = QVBoxLayout() self._upAction = QAction("\u2191", self, toolTip="Move up") self._upButton = QToolButton(self) self._upButton.setDefaultAction(self._upAction) self._upButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) self._downAction = QAction("\u2193", self, toolTip="Move down") self._downButton = QToolButton(self) self._downButton.setDefaultAction(self._downAction) self._downButton.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.MinimumExpanding) vButtonLayout.addWidget(self._upButton) vButtonLayout.addWidget(self._downButton) gridLayout.addLayout(vButtonLayout, 0, 2, 2, 1) hButtonLayout = QHBoxLayout() self._addAction = QAction("+", self) self._addButton = QToolButton(self) self._addButton.setDefaultAction(self._addAction) self._removeAction = QAction("-", self) self._removeButton = QToolButton(self) self._removeButton.setDefaultAction(self._removeAction) hButtonLayout.addWidget(self._addButton) hButtonLayout.addWidget(self._removeButton) hButtonLayout.addStretch(10) gridLayout.addLayout(hButtonLayout, 2, 0, 1, 2) self.setLayout(gridLayout) self._addAction.triggered.connect(self._onAddAction) self._removeAction.triggered.connect(self._onRemoveAction) self._upAction.triggered.connect(self._onUpAction) self._downAction.triggered.connect(self._onDownAction) def sizeHint(self): size = QWidget.sizeHint(self) return QSize(size.width(), 100) def _onAddAction(self): item = QStandardItem("") item.setFlags(item.flags() ^ Qt.ItemIsDropEnabled) self._listView.model().appendRow(item) self._listView.setCurrentIndex(item.index()) self._listView.edit(item.index()) def _onRemoveAction(self): current = self._listView.currentIndex() self._listView.model().takeRow(current.row()) def _onUpAction(self): row = self._listView.currentIndex().row() model = self._listView.model() if row > 0: items = model.takeRow(row) model.insertRow(row - 1, items) self._listView.setCurrentIndex(model.index(row - 1, 0)) def _onDownAction(self): row = self._listView.currentIndex().row() model = self._listView.model() if row < model.rowCount() and row >= 0: items = model.takeRow(row) if row == model.rowCount(): model.appendRow(items) else: model.insertRow(row + 1, items) self._listView.setCurrentIndex(model.index(row + 1, 0)) def setModel(self, model): """ Set a model to select items from """ self._model = model self._listView.setItemDelegate(self._MyItemDelegate(self._model, self)) def addItem(self, *args): """ Add a new entry in the list """ item = QStandardItem(*args) item.setFlags(item.flags() ^ Qt.ItemIsDropEnabled) self._listView.model().appendRow(item) def setItems(self, items): self._listView.model().clear() for item in items: self.addItem(item) def items(self): order = [] for row in range(self._listView.model().rowCount()): order.append(str(self._listView.model().item(row, 0).text())) return order def __changed(self): self.sortingOrderChanged.emit() sortingOrder = property(items, setItems)