class OWNxExplorer(widget.OWWidget): name = "Network Explorer" description = "Visually explore the network and its properties." icon = "icons/NetworkExplorer.svg" priority = 6420 inputs = [ ("Network", network.Graph, "set_graph", widget.Default), ("Node Subset", Table, 'set_marking_items'), ("Node Data", Table, "set_items"), ("Node Distances", Orange.misc.DistMatrix, "set_items_distance_matrix"), ] outputs = [(Output.SUBGRAPH, network.Graph), (Output.DISTANCE, Orange.misc.DistMatrix), (Output.SELECTED, Table), (Output.HIGHLIGHTED, Table), (Output.REMAINING, Table)] settingsList = ["lastVertexSizeColumn", "lastColorColumn", "lastLabelColumns", "lastTooltipColumns",] # TODO: set settings UserAdviceMessages = [ widget.Message('When selecting nodes on the Marking tab, ' 'press <b><tt>Enter</tt></b> key to add ' '<b><font color="{}">highlighted</font></b> nodes to ' '<b><font color="{}">selection</font></b>.' .format(Node.Pen.HIGHLIGHTED.color().name(), Node.Pen.SELECTED.color().name()), 'marking-info', widget.Message.Information), widget.Message('Left-click to select nodes ' '(hold <b><tt>Shift</tt></b> to append to selection). ' 'Right-click to pan/move the view. Scroll to zoom.', 'mouse-info', widget.Message.Information), ] do_auto_commit = settings.Setting(True) maxNodeSize = settings.Setting(50) minNodeSize = settings.Setting(8) selectionMode = settings.Setting(0) tabIndex = settings.Setting(0) showEdgeWeights = settings.Setting(False) relativeEdgeWidths = settings.Setting(False) invertNodeSize = settings.Setting(False) markDistance = settings.Setting(1) markSearchString = settings.Setting("") markNBest = settings.Setting(1) markNConnections = settings.Setting(2) def __init__(self): super().__init__() #self.contextHandlers = {"": DomainContextHandler("", [ContextField("attributes", selected="node_label_attrs"), ContextField("attributes", selected="tooltipAttributes"), "color"])} self.view = GraphView(self) self.mainArea.layout().addWidget(self.view) self.graph_attrs = [] self.acceptingEnterKeypress = False self.node_label_attrs = [] self.tooltipAttributes = [] self.searchStringTimer = QTimer(self) self.markInputItems = None self.node_color_attr = 0 self.node_size_attr = 0 self.nHighlighted = 0 self.nSelected = 0 self.verticesPerEdge = 0 self.edgesPerVertex = 0 self.lastVertexSizeColumn = '' self.lastColorColumn = '' self.lastLabelColumns = set() self.lastTooltipColumns = set() self.items_matrix = None self.number_of_nodes_label = 0 self.number_of_edges_label = 0 self.graph = None self.setMinimumWidth(600) self.tabs = gui.tabWidget(self.controlArea) self.displayTab = gui.createTabPage(self.tabs, "Display") self.markTab = gui.createTabPage(self.tabs, "Marking") def on_tab_changed(index): self.tabIndex = index self.set_selection_mode() self.tabs.currentChanged.connect(on_tab_changed) self.tabs.setCurrentIndex(self.tabIndex) ib = gui.widgetBox(self.displayTab, "Info") gui.label(ib, self, "Nodes: %(number_of_nodes_label)i (%(verticesPerEdge).2f per edge)") gui.label(ib, self, "Edges: %(number_of_edges_label)i (%(edgesPerVertex).2f per node)") box = gui.widgetBox(self.displayTab, "Nodes") self.relayout_button = gui.button(box, self, 'Re-layout', callback=self.relayout, autoDefault=False) self.view.positionsChanged.connect(lambda _: self.progressbar.advance()) def animationFinished(): self.relayout_button.setEnabled(True) self.progressbar.finish() self.view.animationFinished.connect(animationFinished) self.colorCombo = gui.comboBox( box, self, "node_color_attr", label='Color:', orientation='horizontal', callback=self.set_node_colors) self.invertNodeSizeCheck = self.maxNodeSizeSpin = QWidget() # Forward declaration self.nodeSizeCombo = gui.comboBox( box, self, "node_size_attr", label='Size:', orientation='horizontal', callback=self.set_node_sizes) hb = gui.widgetBox(box, orientation="horizontal") hb.layout().addStretch(1) self.minNodeSizeSpin = gui.spin( hb, self, "minNodeSize", 1, 50, step=1, label="Min:", callback=self.set_node_sizes) self.minNodeSizeSpin.setValue(8) gui.separator(hb) self.maxNodeSizeSpin = gui.spin( hb, self, "maxNodeSize", 10, 200, step=5, label="Max:", callback=self.set_node_sizes) self.maxNodeSizeSpin.setValue(50) gui.separator(hb) self.invertNodeSizeCheck = gui.checkBox( hb, self, "invertNodeSize", "Invert", callback=self.set_node_sizes) hb = gui.widgetBox(self.displayTab, box="Node labels | tooltips", orientation="horizontal", addSpace=False) self.attListBox = gui.listBox( hb, self, "node_label_attrs", "graph_attrs", selectionMode=QListWidget.MultiSelection, sizeHint=QSize(100, 100), callback=self._on_node_label_attrs_changed) self.tooltipListBox = gui.listBox( hb, self, "tooltipAttributes", "graph_attrs", selectionMode=QListWidget.MultiSelection, sizeHint=QSize(100, 100), callback=self._clicked_tooltip_lstbox) eb = gui.widgetBox(self.displayTab, "Edges", orientation="vertical") self.checkbox_relative_edges = gui.checkBox( eb, self, 'relativeEdgeWidths', 'Relative edge widths', callback=self.set_edge_sizes) self.checkbox_show_weights = gui.checkBox( eb, self, 'showEdgeWeights', 'Show edge weights', callback=self.set_edge_labels) ib = gui.widgetBox(self.markTab, "Info", orientation="vertical") gui.label(ib, self, "Nodes: %(number_of_nodes_label)i") gui.label(ib, self, "Selected: %(nSelected)i") gui.label(ib, self, "Highlighted: %(nHighlighted)i") def on_selection_change(): self.nSelected = len(self.view.getSelected()) self.nHighlighted = len(self.view.getHighlighted()) self.set_selection_mode() self.commit() self.view.selectionChanged.connect(on_selection_change) ib = gui.widgetBox(self.markTab, "Highlight nodes ...") ribg = gui.radioButtonsInBox(ib, self, "selectionMode", callback=self.set_selection_mode) gui.appendRadioButton(ribg, "None") gui.appendRadioButton(ribg, "... whose attributes contain:") self.ctrlMarkSearchString = gui.lineEdit(gui.indentedBox(ribg), self, "markSearchString", callback=self._set_search_string_timer, callbackOnType=True) self.searchStringTimer.timeout.connect(self.set_selection_mode) gui.appendRadioButton(ribg, "... neighbours of selected, ≤ N hops away") ib = gui.indentedBox(ribg, orientation=0) self.ctrlMarkDistance = gui.spin(ib, self, "markDistance", 1, 100, 1, label="Hops:", callback=lambda: self.set_selection_mode(SelectionMode.NEIGHBORS)) ib.layout().addStretch(1) gui.appendRadioButton(ribg, "... with at least N connections") gui.appendRadioButton(ribg, "... with at most N connections") ib = gui.indentedBox(ribg, orientation=0) self.ctrlMarkNConnections = gui.spin(ib, self, "markNConnections", 0, 1000000, 1, label="Connections:", callback=lambda: self.set_selection_mode(SelectionMode.AT_MOST_N if self.selectionMode == SelectionMode.AT_MOST_N else SelectionMode.AT_LEAST_N)) ib.layout().addStretch(1) gui.appendRadioButton(ribg, "... with more connections than any neighbor") gui.appendRadioButton(ribg, "... with more connections than average neighbor") gui.appendRadioButton(ribg, "... with most connections") ib = gui.indentedBox(ribg, orientation=0) self.ctrlMarkNumber = gui.spin(ib, self, "markNBest", 1, 1000000, 1, label="Number of nodes:", callback=lambda: self.set_selection_mode(SelectionMode.MOST_CONN)) ib.layout().addStretch(1) self.markInputRadioButton = gui.appendRadioButton(ribg, "... given in the ItemSubset input signal") self.markInput = 0 ib = gui.indentedBox(ribg) self.markInputCombo = gui.comboBox(ib, self, 'markInput', callback=lambda: self.set_selection_mode(SelectionMode.FROM_INPUT)) self.markInputRadioButton.setEnabled(False) gui.auto_commit(ribg, self, 'do_auto_commit', 'Output changes') self.markTab.layout().addStretch(1) self.set_graph(None) self.set_selection_mode() def commit(self): self.send_data() def set_items_distance_matrix(self, matrix): assert matrix is None or isinstance(matrix, Orange.misc.DistMatrix) self.items_matrix = matrix self.relayout() def _set_search_string_timer(self): self.selectionMode = SelectionMode.SEARCH self.searchStringTimer.stop() self.searchStringTimer.start(300) def switchTab(self, index=None): index = index or self.tabs.currentIndex() curTab = self.tabs.widget(index) self.acceptingEnterKeypress = False if curTab == self.markTab and self.selectionMode != SelectionMode.NONE: self.acceptingEnterKeypress = True @non_reentrant def set_selection_mode(self, selectionMode=None): self.searchStringTimer.stop() selectionMode = self.selectionMode = selectionMode or self.selectionMode self.switchTab() if (self.graph is None or self.tabs.widget(self.tabs.currentIndex()) != self.markTab): return if selectionMode == SelectionMode.NONE: self.view.setHighlighted([]) elif selectionMode == SelectionMode.SEARCH: table, txt = self.graph.items(), self.markSearchString.lower() if not table or not txt: return toMark = set(i for i, instance in enumerate(table) if txt in " ".join(map(str, instance.list)).lower()) self.view.setHighlighted(toMark) elif selectionMode == SelectionMode.NEIGHBORS: selected = set(self.view.getSelected()) neighbors = selected.copy() for _ in range(self.markDistance): for neigh in list(neighbors): neighbors |= set(self.graph[neigh].keys()) neighbors -= selected self.view.setHighlighted(neighbors) elif selectionMode == SelectionMode.AT_LEAST_N: self.view.setHighlighted( set(node for node, degree in self.graph.degree().items() if degree >= self.markNConnections)) elif selectionMode == SelectionMode.AT_MOST_N: self.view.setHighlighted( set(node for node, degree in self.graph.degree().items() if degree <= self.markNConnections)) elif selectionMode == SelectionMode.ANY_NEIGH: self.view.setHighlighted( set(node for node, degree in self.graph.degree().items() if degree > max(self.graph.degree(self.graph[node]).values(), default=0))) elif selectionMode == SelectionMode.AVG_NEIGH: self.view.setHighlighted( set(node for node, degree in self.graph.degree().items() if degree > np.nan_to_num(np.mean(list(self.graph.degree(self.graph[node]).values()))))) elif selectionMode == SelectionMode.MOST_CONN: degrees = np.array(sorted(self.graph.degree().items(), key=lambda i: i[1], reverse=True)) cut_ind = max(1, min(self.markNBest, self.graph.number_of_nodes())) cut_degree = degrees[cut_ind - 1, 1] toMark = set(degrees[degrees[:, 1] >= cut_degree, 0]) self.view.setHighlighted(toMark) elif selectionMode == SelectionMode.FROM_INPUT: var = self.markInputCombo.currentText() tomark = {} if self.markInputItems: if var == 'ID': values = {x.id for x in self.markInputItems} tomark = {x for x in self.graph.nodes() if self.graph.items()[x].id in values} else: clean = lambda s: str(s).strip().upper() values = {clean(x[var]) for x in self.markInputItems} tomark = {x for x in self.graph.nodes() if clean(self.graph.items()[x][var]) in values} self.view.setHighlighted(tomark) def keyReleaseEvent(self, ev): """On Enter, expand the selected set with the highlighted""" if (not self.acceptingEnterKeypress or ev.key() not in (Qt.Key_Return, Qt.Key_Enter)): super().keyReleaseEvent(ev) return highlighted = self.view.getHighlighted() self.view.setSelected(highlighted, extend=True) self.view.setHighlighted([]) self.set_selection_mode() def save_network(self): # TODO: this was never reviewed since Orange2 if self.view is None or self.graph is None: return filename = QFileDialog.getSaveFileName( self, 'Save Network', '', 'NetworkX graph as Python pickle (*.gpickle)\n' 'NetworkX edge list (*.edgelist)\n' 'Pajek network (*.net *.pajek)\n' 'GML network (*.gml)') if filename: _, ext = os.path.splitext(filename) if not ext: filename += ".net" items = self.graph.items() for i in range(self.graph.number_of_nodes()): graph_node = self.graph.node[i] plot_node = self.networkCanvas.networkCurve.nodes()[i] if items is not None: ex = items[i] if 'x' in ex.domain: ex['x'] = plot_node.x() if 'y' in ex.domain: ex['y'] = plot_node.y() graph_node['x'] = plot_node.x() graph_node['y'] = plot_node.y() network.readwrite.write(self.graph, filename) def send_data(self): if not self.graph: for output in Output.all: self.send(output, None) return selected = self.view.getSelected() self.send(Output.SUBGRAPH, self.graph.subgraph(selected) if selected else None) self.send(Output.DISTANCE, self.items_matrix.submatrix(sorted(selected)) if self.items_matrix is not None and selected else None) items = self.graph.items() if not items: self.send(Output.SELECTED, None) self.send(Output.HIGHLIGHTED, None) self.send(Output.REMAINING, None) else: highlighted = self.view.getHighlighted() self.send(Output.SELECTED, items[sorted(selected), :] if selected else None) self.send(Output.HIGHLIGHTED, items[sorted(highlighted), :] if highlighted else None) remaining = sorted(set(self.graph) - set(selected) - set(highlighted)) self.send(Output.REMAINING, items[remaining, :] if remaining else None) def _set_combos(self): self._clear_combos() self.graph_attrs = self.graph.items_vars() lastLabelColumns = self.lastLabelColumns lastTooltipColumns = self.lastTooltipColumns for var in self.graph_attrs: if var.is_discrete or var.is_continuous: self.colorCombo.addItem(gui.attributeIconDict[gui.vartype(var)], var.name, var) if var.is_continuous: self.nodeSizeCombo.addItem(gui.attributeIconDict[gui.vartype(var)], var.name, var) elif var.is_string: try: value = self.graph.items()[0][var].value except (IndexError, TypeError): pass else: # can value be a list? if len(value.split(',')) > 1: self.nodeSizeCombo.addItem(gui.attributeIconDict[gui.vartype(var)], var.name, var) self.nodeSizeCombo.setDisabled(not self.graph_attrs) self.colorCombo.setDisabled(not self.graph_attrs) for i in range(self.nodeSizeCombo.count()): if self.lastVertexSizeColumn == \ self.nodeSizeCombo.itemText(i): self.node_size_attr = i self.set_node_sizes() break for i in range(self.colorCombo.count()): if self.lastColorColumn == self.colorCombo.itemText(i): self.node_color_attr = i self.set_node_colors() break for i in range(self.attListBox.count()): if str(self.attListBox.item(i).text()) in lastLabelColumns: self.attListBox.item(i).setSelected(True) self._on_node_label_attrs_changed() for i in range(self.tooltipListBox.count()): if (self.tooltipListBox.item(i).text() in lastTooltipColumns or not lastTooltipColumns): self.tooltipListBox.item(i).setSelected(True) self._clicked_tooltip_lstbox() self.lastLabelColumns = lastLabelColumns self.lastTooltipColumns = lastTooltipColumns def _clear_combos(self): self.graph_attrs = [] self.colorCombo.clear() self.nodeSizeCombo.clear() self.colorCombo.addItem('(none)', None) self.nodeSizeCombo.addItem("(uniform)") def set_graph_none(self): self.graph = None self.graph_base = None self._clear_combos() self.number_of_nodes_label = 0 self.number_of_edges_label = 0 self.verticesPerEdge = 0 self.edgesPerVertex = 0 self._items = None self.view.set_graph(None) def set_graph(self, graph): if not graph: return self.set_graph_none() if graph.number_of_nodes() < 2: self.set_graph_none() self.information('I\'m not really in a mood to visualize just one node. Try again tomorrow.') return self.information() all_edges_equal = bool(1 == len(set(w for u,v,w in graph.edges_iter(data='weight')))) self.checkbox_show_weights.setEnabled(not all_edges_equal) self.checkbox_relative_edges.setEnabled(not all_edges_equal) self.graph_base = graph self.graph = graph.copy() # Set items table from the separate signal if self._items: self.set_items(self._items) self.view.set_graph(self.graph, relayout=False) # Set labels self.number_of_nodes_label = self.graph.number_of_nodes() self.number_of_edges_label = self.graph.number_of_edges() self.verticesPerEdge = self.graph.number_of_nodes() / max(1, self.graph.number_of_edges()) self.edgesPerVertex = self.graph.number_of_edges() / max(1, self.graph.number_of_nodes()) self._set_combos() if self.graph.number_of_nodes() + self.graph.number_of_edges() > 30000: self.set_graph_none() self.error('Network is too large to visualize. Sorry.') return self.error() self.set_selection_mode() self.relayout() def set_items(self, items=None): self._items = items if items is None: return self.set_graph(self.graph_base) if not self.graph: self.warning('No graph found!') return self.warning() if len(items) != self.graph.number_of_nodes(): self.error('Items table must have one instance for each network node.') return self.error() self.graph.set_items(items) self._set_combos() def set_marking_items(self, items): self.markInputCombo.clear() self.markInputRadioButton.setEnabled(False) self.markInputItems = items self.warning() if items is None: return if self.graph is None or self.graph.items() is None: self.warning('No graph provided or no items attached to the graph.') return graph_items = self.graph.items() domain = graph_items.domain if len(items) > 0: commonVars = (set(x.name for x in chain(items.domain.variables, items.domain.metas)) & set(x.name for x in chain(domain.variables, domain.metas))) self.markInputCombo.addItem(gui.attributeIconDict[gui.vartype(DiscreteVariable())], "ID") for var in commonVars: orgVar, mrkVar = domain[var], items.domain[var] if type(orgVar) == type(mrkVar) == StringVariable: self.markInputCombo.addItem(gui.attributeIconDict[gui.vartype(orgVar)], orgVar.name) self.markInputRadioButton.setEnabled(True) def relayout(self): if self.graph is None or self.graph.number_of_nodes() <= 1: return self.progressbar = gui.ProgressBar(self, FR_ITERATIONS) distmatrix = self.items_matrix if distmatrix is not None and distmatrix.shape[0] != self.graph.number_of_nodes(): self.warning(17, "Distance matrix size doesn't match the number of network nodes. Not using it.") distmatrix = None self.warning(17) self.relayout_button.setDisabled(True) self.view.relayout(randomize=False, weight=distmatrix) def _on_node_label_attrs_changed(self): if not self.graph: return attributes = self.lastLabelColumns = [self.graph_attrs[i] for i in self.node_label_attrs] if attributes: table = self.graph.items() if not table: return for i, node in enumerate(self.view.nodes): text = ', '.join(map(str, table[i, attributes][0].list)) node.setText(text) else: for node in self.view.nodes: node.setText('') def _clicked_tooltip_lstbox(self): if not self.graph: return attributes = self.lastTooltipColumns = [self.graph_attrs[i] for i in self.tooltipAttributes] if attributes: table = self.graph.items() if not table: return assert self.view.nodes for i, node in enumerate(self.view.nodes): node.setTooltip(lambda row=i, attributes=attributes, table=table: '<br>'.join('<b>{.name}:</b> {}'.format(i[0], str(i[1]).replace('<', '<')) for i in zip(attributes, table[row, attributes][0].list)) ) else: for node in self.view.nodes: node.setTooltip(None) def set_edge_labels(self): if self.showEdgeWeights: weights = (str(w or '') for u, v, w in self.graph.edges_iter(data='weight')) else: weights = ('' for i in range(self.graph.number_of_edges())) for edge, weight in zip(self.view.edges, weights): edge.setText(weight) def set_node_colors(self): if not self.graph: return self.lastColorColumn = self.colorCombo.currentText() attribute = self.colorCombo.itemData(self.colorCombo.currentIndex()) assert not attribute or isinstance(attribute, Orange.data.Variable) if not attribute: for node in self.view.nodes: node.setColor(None) return table = self.graph.items() if not table: return if attribute in table.domain.class_vars: values = table[:, attribute].Y if values.ndim > 1: values = values.T elif attribute in table.domain.metas: values = table[:, attribute].metas[:, 0] elif attribute in table.domain.attributes: values = table[:, attribute].X[:, 0] else: raise RuntimeError("Shouldn't be able to select this column") if attribute.is_continuous: colors = CONTINUOUS_PALETTE[scale(values)] elif attribute.is_discrete: DISCRETE_PALETTE = ColorPaletteGenerator(len(attribute.values)) colors = DISCRETE_PALETTE[values] for node, color in zip(self.view.nodes, colors): node.setColor(color) def set_node_sizes(self): attribute = self.nodeSizeCombo.itemData(self.nodeSizeCombo.currentIndex()) depending_widgets = (self.invertNodeSizeCheck, self.maxNodeSizeSpin) for w in depending_widgets: w.setDisabled(not attribute) if not self.graph: return table = self.graph.items() if not table: return if attribute in table.domain.class_vars: values = table[:, attribute].Y if values.ndim > 1: values = values.T elif attribute in table.domain.metas: values = table[:, attribute].metas[:, 0] elif attribute in table.domain.attributes: values = table[:, attribute].X[:, 0] else: for node in self.view.nodes: node.setSize(self.minNodeSize) return values = np.array(values) if self.invertNodeSize: values += np.nanmin(values) + 1 values = 1/values nodemin, nodemax = np.nanmin(values), np.nanmax(values) if nodemin == nodemax: # np.polyfit borks on this condition sizes = (self.minNodeSize for i in range(len(self.view.nodes))) else: k, n = np.polyfit([nodemin, nodemax], [self.minNodeSize, self.maxNodeSize], 1) sizes = values * k + n sizes[np.isnan(sizes)] = np.nanmean(sizes) for node, size in zip(self.view.nodes, sizes): node.setSize(size) def set_edge_sizes(self): if not self.graph: return if self.relativeEdgeWidths: widths = [self.graph.edge[u][v].get('weight', 1) for u, v in self.graph.edges()] widths = scale(widths, .7, 8) else: widths = (.7 for i in range(self.graph.number_of_edges())) for edge, width in zip(self.view.edges, widths): edge.setSize(width) def sendReport(self): self.reportSettings("Graph data", [("Number of vertices", self.graph.number_of_nodes()), ("Number of edges", self.graph.number_of_edges()), ("Vertices per edge", "%.3f" % self.verticesPerEdge), ("Edges per vertex", "%.3f" % self.edgesPerVertex), ]) if self.node_color_attr or self.node_size_attr or self.node_label_attrs: self.reportSettings("Visual settings", [self.node_color_attr and ("Vertex color", self.colorCombo.currentText()), self.node_size_attr and ("Vertex size", str(self.nodeSizeCombo.currentText()) + " (inverted)" if self.invertNodeSize else ""), self.node_label_attrs and ("Labels", ", ".join(self.graph_attrs[i].name for i in self.node_label_attrs)), ]) self.reportSection("Graph") self.reportImage(self.view)
class OWNxExplorer(widget.OWWidget): name = "Network Explorer" description = "Visually explore the network and its properties." icon = "icons/NetworkExplorer.svg" priority = 6420 inputs = [ ("Network", network.Graph, "set_graph", widget.Default), ("Node Subset", Table, 'set_marking_items'), ("Node Data", Table, "set_items"), ("Node Distances", Orange.misc.DistMatrix, "set_items_distance_matrix"), ] outputs = [(Output.SUBGRAPH, network.Graph), (Output.UNSELECTED_SUBGRAPH, network.Graph), (Output.DISTANCE, Orange.misc.DistMatrix), (Output.SELECTED, Table), (Output.HIGHLIGHTED, Table), (Output.REMAINING, Table)] settingsList = [ "lastVertexSizeColumn", "lastColorColumn", "lastLabelColumns", "lastTooltipColumns", ] # TODO: set settings UserAdviceMessages = [ widget.Message( 'When selecting nodes on the Marking tab, ' 'press <b><tt>Enter</tt></b> key to add ' '<b><font color="{}">highlighted</font></b> nodes to ' '<b><font color="{}">selection</font></b>.'.format( Node.Pen.HIGHLIGHTED.color().name(), Node.Pen.SELECTED.color().name()), 'marking-info', widget.Message.Information), widget.Message( 'Left-click to select nodes ' '(hold <b><tt>Shift</tt></b> to append to selection). ' 'Right-click to pan/move the view. Scroll to zoom.', 'mouse-info', widget.Message.Information), ] do_auto_commit = settings.Setting(True) maxNodeSize = settings.Setting(50) minNodeSize = settings.Setting(8) selectionMode = settings.Setting(SelectionMode.FROM_INPUT) tabIndex = settings.Setting(0) showEdgeWeights = settings.Setting(False) relativeEdgeWidths = settings.Setting(False) invertNodeSize = settings.Setting(False) markDistance = settings.Setting(1) markSearchString = settings.Setting("") markNBest = settings.Setting(1) markNConnections = settings.Setting(2) graph_name = 'view' def __init__(self): super().__init__() #self.contextHandlers = {"": DomainContextHandler("", [ContextField("attributes", selected="node_label_attrs"), ContextField("attributes", selected="tooltipAttributes"), "color"])} self.view = GraphView(self) self.mainArea.layout().addWidget(self.view) self.graph_attrs = [] self.acceptingEnterKeypress = False self.node_label_attrs = [] self.tooltipAttributes = [] self.searchStringTimer = QTimer(self) self.markInputItems = None self.node_color_attr = 0 self.node_size_attr = 0 self.nHighlighted = 0 self.nSelected = 0 self.verticesPerEdge = 0 self.edgesPerVertex = 0 self.lastVertexSizeColumn = '' self.lastColorColumn = '' self.lastLabelColumns = set() self.lastTooltipColumns = set() self.items_matrix = None self.number_of_nodes_label = 0 self.number_of_edges_label = 0 self.graph = None self.setMinimumWidth(600) self.tabs = gui.tabWidget(self.controlArea) self.displayTab = gui.createTabPage(self.tabs, "Display") self.markTab = gui.createTabPage(self.tabs, "Marking") def on_tab_changed(index): self.tabIndex = index self.set_selection_mode() self.tabs.currentChanged.connect(on_tab_changed) self.tabs.setCurrentIndex(self.tabIndex) ib = gui.widgetBox(self.displayTab, "Info") gui.label( ib, self, "Nodes: %(number_of_nodes_label)i (%(verticesPerEdge).2f per edge)" ) gui.label( ib, self, "Edges: %(number_of_edges_label)i (%(edgesPerVertex).2f per node)") box = gui.widgetBox(self.displayTab, "Nodes") self.relayout_button = gui.button(box, self, 'Re-layout', callback=self.relayout, autoDefault=False) self.view.positionsChanged.connect( lambda _: self.progressbar.advance()) def animationFinished(): self.relayout_button.setEnabled(True) self.progressbar.finish() self.view.animationFinished.connect(animationFinished) self.colorCombo = gui.comboBox(box, self, "node_color_attr", label='Color:', orientation='horizontal', callback=self.set_node_colors) self.invertNodeSizeCheck = self.maxNodeSizeSpin = QWidget( ) # Forward declaration self.nodeSizeCombo = gui.comboBox(box, self, "node_size_attr", label='Size:', orientation='horizontal', callback=self.set_node_sizes) hb = gui.widgetBox(box, orientation="horizontal") hb.layout().addStretch(1) self.minNodeSizeSpin = gui.spin(hb, self, "minNodeSize", 1, 50, step=1, label="Min:", callback=self.set_node_sizes) self.minNodeSizeSpin.setValue(8) gui.separator(hb) self.maxNodeSizeSpin = gui.spin(hb, self, "maxNodeSize", 10, 200, step=5, label="Max:", callback=self.set_node_sizes) self.maxNodeSizeSpin.setValue(50) gui.separator(hb) self.invertNodeSizeCheck = gui.checkBox(hb, self, "invertNodeSize", "Invert", callback=self.set_node_sizes) hb = gui.widgetBox(self.displayTab, box="Node labels | tooltips", orientation="horizontal", addSpace=False) self.attListBox = gui.listBox( hb, self, "node_label_attrs", "graph_attrs", selectionMode=QListWidget.MultiSelection, sizeHint=QSize(100, 100), callback=self._on_node_label_attrs_changed) self.tooltipListBox = gui.listBox( hb, self, "tooltipAttributes", "graph_attrs", selectionMode=QListWidget.MultiSelection, sizeHint=QSize(100, 100), callback=self._clicked_tooltip_lstbox) eb = gui.widgetBox(self.displayTab, "Edges", orientation="vertical") self.checkbox_relative_edges = gui.checkBox( eb, self, 'relativeEdgeWidths', 'Relative edge widths', callback=self.set_edge_sizes) self.checkbox_show_weights = gui.checkBox( eb, self, 'showEdgeWeights', 'Show edge weights', callback=self.set_edge_labels) ib = gui.widgetBox(self.markTab, "Info", orientation="vertical") gui.label(ib, self, "Nodes: %(number_of_nodes_label)i") gui.label(ib, self, "Selected: %(nSelected)i") gui.label(ib, self, "Highlighted: %(nHighlighted)i") def on_selection_change(): self.nSelected = len(self.view.getSelected()) self.nHighlighted = len(self.view.getHighlighted()) self.set_selection_mode() self.commit() self.view.selectionChanged.connect(on_selection_change) ib = gui.widgetBox(self.markTab, "Highlight nodes ...") ribg = gui.radioButtonsInBox(ib, self, "selectionMode", callback=self.set_selection_mode) gui.appendRadioButton(ribg, "None") gui.appendRadioButton(ribg, "... whose attributes contain:") self.ctrlMarkSearchString = gui.lineEdit( gui.indentedBox(ribg), self, "markSearchString", callback=self._set_search_string_timer, callbackOnType=True) self.searchStringTimer.timeout.connect(self.set_selection_mode) gui.appendRadioButton(ribg, "... neighbours of selected, ≤ N hops away") ib = gui.indentedBox(ribg, orientation=0) self.ctrlMarkDistance = gui.spin( ib, self, "markDistance", 1, 100, 1, label="Hops:", callback=lambda: self.set_selection_mode(SelectionMode.NEIGHBORS)) ib.layout().addStretch(1) gui.appendRadioButton(ribg, "... with at least N connections") gui.appendRadioButton(ribg, "... with at most N connections") ib = gui.indentedBox(ribg, orientation=0) self.ctrlMarkNConnections = gui.spin( ib, self, "markNConnections", 0, 1000000, 1, label="Connections:", callback=lambda: self.set_selection_mode( SelectionMode.AT_MOST_N if self.selectionMode == SelectionMode. AT_MOST_N else SelectionMode.AT_LEAST_N)) ib.layout().addStretch(1) gui.appendRadioButton(ribg, "... with more connections than any neighbor") gui.appendRadioButton( ribg, "... with more connections than average neighbor") gui.appendRadioButton(ribg, "... with most connections") ib = gui.indentedBox(ribg, orientation=0) self.ctrlMarkNumber = gui.spin( ib, self, "markNBest", 1, 1000000, 1, label="Number of nodes:", callback=lambda: self.set_selection_mode(SelectionMode.MOST_CONN)) ib.layout().addStretch(1) self.markInputRadioButton = gui.appendRadioButton( ribg, "... from Node Subset input signal") self.markInputRadioButton.setEnabled(True) gui.auto_commit(ribg, self, 'do_auto_commit', 'Output changes') self.markTab.layout().addStretch(1) self.set_graph(None) self.set_selection_mode() def commit(self): self.send_data() def set_items_distance_matrix(self, matrix): assert matrix is None or isinstance(matrix, Orange.misc.DistMatrix) self.items_matrix = matrix self.relayout() def _set_search_string_timer(self): self.selectionMode = SelectionMode.SEARCH self.searchStringTimer.stop() self.searchStringTimer.start(300) def switchTab(self, index=None): index = index or self.tabs.currentIndex() curTab = self.tabs.widget(index) self.acceptingEnterKeypress = False if curTab == self.markTab and self.selectionMode != SelectionMode.NONE: self.acceptingEnterKeypress = True @non_reentrant def set_selection_mode(self, selectionMode=None): self.searchStringTimer.stop() selectionMode = self.selectionMode = selectionMode or self.selectionMode self.switchTab() if (self.graph is None or self.tabs.widget(self.tabs.currentIndex()) != self.markTab and selectionMode != SelectionMode.FROM_INPUT): return if selectionMode == SelectionMode.NONE: self.view.setHighlighted([]) elif selectionMode == SelectionMode.SEARCH: table, txt = self.graph.items(), self.markSearchString.lower() if not table or not txt: return toMark = set(i for i, instance in enumerate(table) if txt in " ".join(map(str, instance.list)).lower()) self.view.setHighlighted(toMark) elif selectionMode == SelectionMode.NEIGHBORS: selected = set(self.view.getSelected()) neighbors = selected.copy() for _ in range(self.markDistance): for neigh in list(neighbors): neighbors |= set(self.graph[neigh].keys()) neighbors -= selected self.view.setHighlighted(neighbors) elif selectionMode == SelectionMode.AT_LEAST_N: self.view.setHighlighted( set(node for node, degree in self.graph.degree().items() if degree >= self.markNConnections)) elif selectionMode == SelectionMode.AT_MOST_N: self.view.setHighlighted( set(node for node, degree in self.graph.degree().items() if degree <= self.markNConnections)) elif selectionMode == SelectionMode.ANY_NEIGH: self.view.setHighlighted( set(node for node, degree in self.graph.degree().items() if degree > max(self.graph.degree(self.graph[node]).values(), default=0))) elif selectionMode == SelectionMode.AVG_NEIGH: self.view.setHighlighted( set(node for node, degree in self.graph.degree().items( ) if degree > np.nan_to_num( np.mean(list(self.graph.degree( self.graph[node]).values()))))) elif selectionMode == SelectionMode.MOST_CONN: degrees = np.array( sorted(self.graph.degree().items(), key=lambda i: i[1], reverse=True)) cut_ind = max(1, min(self.markNBest, self.graph.number_of_nodes())) cut_degree = degrees[cut_ind - 1, 1] toMark = set(degrees[degrees[:, 1] >= cut_degree, 0]) self.view.setHighlighted(toMark) elif selectionMode == SelectionMode.FROM_INPUT: tomark = {} if self.markInputItems: ids = set(self.markInputItems.ids) tomark = { x for x in self.graph.nodes() if self.graph.items()[x].id in ids } self.view.setHighlighted(tomark) def keyReleaseEvent(self, ev): """On Enter, expand the selected set with the highlighted""" if (not self.acceptingEnterKeypress or ev.key() not in (Qt.Key_Return, Qt.Key_Enter)): super().keyReleaseEvent(ev) return highlighted = self.view.getHighlighted() self.view.setSelected(highlighted, extend=True) self.view.setHighlighted([]) self.set_selection_mode() def save_network(self): # TODO: this was never reviewed since Orange2 if self.view is None or self.graph is None: return filename = QFileDialog.getSaveFileName( self, 'Save Network', '', 'NetworkX graph as Python pickle (*.gpickle)\n' 'NetworkX edge list (*.edgelist)\n' 'Pajek network (*.net *.pajek)\n' 'GML network (*.gml)') if filename: _, ext = os.path.splitext(filename) if not ext: filename += ".net" items = self.graph.items() for i in range(self.graph.number_of_nodes()): graph_node = self.graph.node[i] plot_node = self.networkCanvas.networkCurve.nodes()[i] if items is not None: ex = items[i] if 'x' in ex.domain: ex['x'] = plot_node.x() if 'y' in ex.domain: ex['y'] = plot_node.y() graph_node['x'] = plot_node.x() graph_node['y'] = plot_node.y() network.readwrite.write(self.graph, filename) def send_data(self): if not self.graph: for output in Output.all: self.send(output, None) return selected = self.view.getSelected() self.send(Output.SUBGRAPH, self.graph.subgraph(selected) if selected else None) self.send( Output.UNSELECTED_SUBGRAPH, self.graph.subgraph(self.view.getUnselected()) if selected else self.graph) self.send( Output.DISTANCE, self.items_matrix.submatrix(sorted(selected)) if self.items_matrix is not None and selected else None) items = self.graph.items() if not items: self.send(Output.SELECTED, None) self.send(Output.HIGHLIGHTED, None) self.send(Output.REMAINING, None) else: highlighted = self.view.getHighlighted() self.send(Output.SELECTED, items[sorted(selected), :] if selected else None) self.send(Output.HIGHLIGHTED, items[sorted(highlighted), :] if highlighted else None) remaining = sorted( set(self.graph) - set(selected) - set(highlighted)) self.send(Output.REMAINING, items[remaining, :] if remaining else None) def _set_combos(self): self._clear_combos() self.graph_attrs = self.graph.items_vars() lastLabelColumns = self.lastLabelColumns lastTooltipColumns = self.lastTooltipColumns for var in self.graph_attrs: if var.is_discrete or var.is_continuous: self.colorCombo.addItem( gui.attributeIconDict[gui.vartype(var)], var.name, var) if var.is_continuous: self.nodeSizeCombo.addItem( gui.attributeIconDict[gui.vartype(var)], var.name, var) elif var.is_string: try: value = self.graph.items()[0][var].value except (IndexError, TypeError): pass else: # can value be a list? if len(value.split(',')) > 1: self.nodeSizeCombo.addItem( gui.attributeIconDict[gui.vartype(var)], var.name, var) self.nodeSizeCombo.setDisabled(not self.graph_attrs) self.colorCombo.setDisabled(not self.graph_attrs) for i in range(self.nodeSizeCombo.count()): if self.lastVertexSizeColumn == \ self.nodeSizeCombo.itemText(i): self.node_size_attr = i self.set_node_sizes() break for i in range(self.colorCombo.count()): if self.lastColorColumn == self.colorCombo.itemText(i): self.node_color_attr = i self.set_node_colors() break for i in range(self.attListBox.count()): if str(self.attListBox.item(i).text()) in lastLabelColumns: self.attListBox.item(i).setSelected(True) self._on_node_label_attrs_changed() for i in range(self.tooltipListBox.count()): if (self.tooltipListBox.item(i).text() in lastTooltipColumns or not lastTooltipColumns): self.tooltipListBox.item(i).setSelected(True) self._clicked_tooltip_lstbox() self.lastLabelColumns = lastLabelColumns self.lastTooltipColumns = lastTooltipColumns def _clear_combos(self): self.graph_attrs = [] self.colorCombo.clear() self.nodeSizeCombo.clear() self.colorCombo.addItem('(none)', None) self.nodeSizeCombo.addItem("(uniform)") def set_graph_none(self): self.graph = None self.graph_base = None self._clear_combos() self.number_of_nodes_label = 0 self.number_of_edges_label = 0 self.verticesPerEdge = 0 self.edgesPerVertex = 0 self._items = None self.view.set_graph(None) def set_graph(self, graph): if not graph: return self.set_graph_none() if graph.number_of_nodes() < 2: self.set_graph_none() self.information( 'I\'m not really in a mood to visualize just one node. Try again tomorrow.' ) return if graph.number_of_nodes() + graph.number_of_edges() > 30000: self.set_graph_none() self.error('Network is too large to visualize. Sorry.') return self.information() all_edges_equal = bool( 1 == len(set(w for u, v, w in graph.edges_iter(data='weight')))) self.checkbox_show_weights.setEnabled(not all_edges_equal) self.checkbox_relative_edges.setEnabled(not all_edges_equal) self.graph_base = graph self.graph = graph.copy() # Set items table from the separate signal if self._items: self.set_items(self._items) self.view.set_graph(self.graph, relayout=False) # Set labels self.number_of_nodes_label = self.graph.number_of_nodes() self.number_of_edges_label = self.graph.number_of_edges() self.verticesPerEdge = self.graph.number_of_nodes() / max( 1, self.graph.number_of_edges()) self.edgesPerVertex = self.graph.number_of_edges() / max( 1, self.graph.number_of_nodes()) self._set_combos() self.error() self.set_selection_mode() self.relayout() def set_items(self, items=None): self._items = items if items is None: return self.set_graph(self.graph_base) if not self.graph: self.warning('No graph found!') return self.warning() if len(items) != self.graph.number_of_nodes(): self.error( 'Items table must have one instance for each network node.') return self.error() self.graph.set_items(items) self._set_combos() def set_marking_items(self, items): self.markInputRadioButton.setEnabled(False) self.markInputItems = items self.warning() if items is None: self.view.selectionChanged.emit() return if self.graph is None or self.graph.items() is None: self.warning( 'No graph provided or no items attached to the graph.') return graph_items = self.graph.items() domain = graph_items.domain if len(items) > 0: commonVars = ( set(x.name for x in chain(items.domain.variables, items.domain.metas)) & set(x.name for x in chain(domain.variables, domain.metas))) self.markInputRadioButton.setEnabled(True) self.view.selectionChanged.emit() def relayout(self): if self.graph is None or self.graph.number_of_nodes() <= 1: return self.progressbar = gui.ProgressBar(self, FR_ITERATIONS) distmatrix = self.items_matrix if distmatrix is not None and distmatrix.shape[ 0] != self.graph.number_of_nodes(): self.warning( 17, "Distance matrix size doesn't match the number of network nodes. Not using it." ) distmatrix = None self.warning(17) self.relayout_button.setDisabled(True) self.view.relayout(randomize=False, weight=distmatrix) def _on_node_label_attrs_changed(self): if not self.graph: return attributes = self.lastLabelColumns = [ self.graph_attrs[i] for i in self.node_label_attrs ] if attributes: table = self.graph.items() if not table: return for i, node in enumerate(self.view.nodes): text = ', '.join(map(str, table[i, attributes][0].list)) node.setText(text) else: for node in self.view.nodes: node.setText('') def _clicked_tooltip_lstbox(self): if not self.graph: return attributes = self.lastTooltipColumns = [ self.graph_attrs[i] for i in self.tooltipAttributes ] if attributes: table = self.graph.items() if not table: return assert self.view.nodes for i, node in enumerate(self.view.nodes): node.setTooltip( lambda row=i, attributes=attributes, table=table: '<br>'. join('<b>{.name}:</b> {}'.format( i[0], str(i[1]).replace('<', '<')) for i in zip( attributes, table[row, attributes][0].list))) else: for node in self.view.nodes: node.setTooltip(None) def set_edge_labels(self): if self.showEdgeWeights: weights = (str(w or '') for u, v, w in self.graph.edges_iter(data='weight')) else: weights = ('' for i in range(self.graph.number_of_edges())) for edge, weight in zip(self.view.edges, weights): edge.setText(weight) def set_node_colors(self): if not self.graph: return self.lastColorColumn = self.colorCombo.currentText() attribute = self.colorCombo.itemData(self.colorCombo.currentIndex()) assert not attribute or isinstance(attribute, Orange.data.Variable) if not attribute: for node in self.view.nodes: node.setColor(None) return table = self.graph.items() if not table: return if attribute in table.domain.class_vars: values = table[:, attribute].Y if values.ndim > 1: values = values.T elif attribute in table.domain.metas: values = table[:, attribute].metas[:, 0] elif attribute in table.domain.attributes: values = table[:, attribute].X[:, 0] else: raise RuntimeError("Shouldn't be able to select this column") if attribute.is_continuous: colors = CONTINUOUS_PALETTE[scale(values)] elif attribute.is_discrete: DISCRETE_PALETTE = ColorPaletteGenerator(len(attribute.values)) colors = DISCRETE_PALETTE[values] for node, color in zip(self.view.nodes, colors): node.setColor(color) def set_node_sizes(self): attribute = self.nodeSizeCombo.itemData( self.nodeSizeCombo.currentIndex()) depending_widgets = (self.invertNodeSizeCheck, self.maxNodeSizeSpin) for w in depending_widgets: w.setDisabled(not attribute) if not self.graph: return table = self.graph.items() if table is None: return try: values = table.get_column_view(attribute)[0] except Exception: for node in self.view.nodes: node.setSize(self.minNodeSize) return if isinstance(table.domain[attribute], StringVariable): values = np.array([(s.count(',') + 1) if s else np.nan for s in values]) if self.invertNodeSize: values += np.nanmin(values) + 1 values = 1 / values nodemin, nodemax = np.nanmin(values), np.nanmax(values) if nodemin == nodemax: # np.polyfit borks on this condition sizes = (self.minNodeSize for i in range(len(self.view.nodes))) else: k, n = np.polyfit([nodemin, nodemax], [self.minNodeSize, self.maxNodeSize], 1) sizes = values * k + n sizes[np.isnan(sizes)] = np.nanmean(sizes) for node, size in zip(self.view.nodes, sizes): node.setSize(size) def set_edge_sizes(self): if not self.graph: return if self.relativeEdgeWidths: widths = [ self.graph.edge[u][v].get('weight', 1) for u, v in self.graph.edges() ] widths = scale(widths, .7, 8) else: widths = (.7 for i in range(self.graph.number_of_edges())) for edge, width in zip(self.view.edges, widths): edge.setSize(width) def send_report(self): self.report_data("Data", self.graph.items()) self.report_items('Graph info', [ ("Number of vertices", self.graph.number_of_nodes()), ("Number of edges", self.graph.number_of_edges()), ("Vertices per edge", "%.3f" % self.verticesPerEdge), ("Edges per vertex", "%.3f" % self.edgesPerVertex), ]) if self.node_color_attr or self.node_size_attr or self.node_label_attrs: self.report_items("Visual settings", [ ("Vertex color", self.colorCombo.currentText()), ("Vertex size", str(self.nodeSizeCombo.currentText()) + " (inverted)" if self.invertNodeSize else ""), ("Labels", ", ".join(self.graph_attrs[i].name for i in self.node_label_attrs)), ]) self.report_plot("Graph", self.view)
class OWNxExplorer(widget.OWWidget): name = "Network Explorer" description = "Visually explore the network and its properties." icon = "icons/NetworkExplorer.svg" priority = 6420 class Inputs: network = Input("Network", network.Graph, default=True) node_subset = Input("Node Subset", Table) node_data = Input("Node Data", Table) node_distances = Input("Node Distances", Orange.misc.DistMatrix) class Outputs: subgraph = Output("Selected sub-network", network.Graph) unselected_subgraph = Output("Remaining sub-network", network.Graph) distances = Output("Distance matrix", Orange.misc.DistMatrix) selected = Output("Selected items", Table) highlighted = Output("Highlighted items", Table) remaining = Output("Remaining items", Table) UserAdviceMessages = [ widget.Message( 'When selecting nodes on the Marking tab, ' 'press <b><tt>Enter</tt></b> key to add ' '<b><font color="{}">highlighted</font></b> nodes to ' '<b><font color="{}">selection</font></b>.'.format( Node.Pen.HIGHLIGHTED.color().name(), Node.Pen.SELECTED.color().name()), 'marking-info', widget.Message.Information), widget.Message( 'Left-click to select nodes ' '(hold <b><tt>Shift</tt></b> to append to selection). ' 'Right-click to pan/move the view. Scroll to zoom.', 'mouse-info', widget.Message.Information), ] settingsHandler = DomainContextHandler() do_auto_commit = Setting(True) selectionMode = Setting(SelectionMode.FROM_INPUT) tabIndex = Setting(0) showEdgeWeights = Setting(False) relativeEdgeWidths = Setting(False) randomizePositions = Setting(True) invertNodeSize = Setting(False) markDistance = Setting(1) markSearchString = Setting("") markNBest = Setting(1) markNConnections = Setting(2) point_width = Setting(10) edge_width = Setting(1) attr_size = ContextSetting(None) attr_color = ContextSetting(None) attrs_label = ContextSetting({}) attrs_tooltip = ContextSetting({}) graph_name = 'view' class Warning(widget.OWWidget.Warning): distance_matrix_size = widget.Msg( "Distance matrix size doesn't match the number of network nodes. Not using it." ) no_graph_found = widget.Msg('No graph found!') no_graph_or_items = widget.Msg( 'No graph provided or no items attached to the graph.') class Error(widget.OWWidget.Error): instance_for_each_node = widget.Msg( 'Items table must have one instance for each network node.') network_too_large = widget.Msg( 'Network is too large to visualize. Sorry.') def __init__(self): super().__init__() #self.contextHandlers = {"": DomainContextHandler("", [ContextField("attributes", selected="node_label_attrs"), ContextField("attributes", selected="tooltipAttributes"), "color"])} self.view = GraphView(self) self.mainArea.layout().addWidget(self.view) self.graph_attrs = [] self.acceptingEnterKeypress = False self.node_label_attrs = [] self.tooltipAttributes = [] self.searchStringTimer = QTimer(self) self.markInputItems = None self.node_color_attr = 0 self.node_size_attr = 0 self.nHighlighted = 0 self.nSelected = 0 self.verticesPerEdge = 0 self.edgesPerVertex = 0 self.items_matrix = None self.number_of_nodes_label = 0 self.number_of_edges_label = 0 self.graph = None self.setMinimumWidth(600) self.tabs = gui.tabWidget(self.controlArea) self.displayTab = gui.createTabPage(self.tabs, "Display") self.markTab = gui.createTabPage(self.tabs, "Marking") def on_tab_changed(index): self.tabIndex = index self.set_selection_mode() self.tabs.currentChanged.connect(on_tab_changed) self.tabs.setCurrentIndex(self.tabIndex) ib = gui.widgetBox(self.displayTab, "Info") gui.label( ib, self, "Nodes: %(number_of_nodes_label)i (%(verticesPerEdge).2f per edge)" ) gui.label( ib, self, "Edges: %(number_of_edges_label)i (%(edgesPerVertex).2f per node)") box = gui.widgetBox(self.displayTab, "Nodes") self.relayout_button = gui.button(box, self, 'Re-layout', callback=self.relayout, autoDefault=False) self.randomize_cb = gui.checkBox(box, self, "randomizePositions", "Randomize positions") self.view.positionsChanged.connect( lambda positions, progress: self.progressbar.widget.progressBarSet( int(round(100 * progress)))) def animationFinished(): self.relayout_button.setEnabled(True) self.progressbar.finish() self.view.animationFinished.connect(animationFinished) self.color_model = VariableListModel(placeholder="(Same color)") self.color_combo = gui.comboBox(box, self, "attr_color", label='Color:', orientation='horizontal', callback=self.set_node_colors, model=self.color_model) self.size_model = VariableListModel(placeholder="(Same size)") self.size_combo = gui.comboBox(box, self, "attr_size", label='Size:', orientation='horizontal', callback=self.set_node_sizes, model=self.size_model) gui.hSlider(box, self, 'point_width', label="Symbol size: ", minValue=1, maxValue=10, step=1, createLabel=False, callback=self.set_node_sizes) hb = gui.widgetBox(box, orientation="horizontal") hb.layout().addStretch(1) self.invertNodeSizeCheck = gui.checkBox(hb, self, "invertNodeSize", "Invert", callback=self.set_node_sizes) hb = gui.widgetBox(self.displayTab, box="Node labels | tooltips", orientation="horizontal", addSpace=False) self.attListBox = gui.listBox( hb, self, "node_label_attrs", "graph_attrs", selectionMode=QListWidget.MultiSelection, sizeHint=QSize(100, 100), callback=self._on_node_label_attrs_changed) self.tooltipListBox = gui.listBox( hb, self, "tooltipAttributes", "graph_attrs", selectionMode=QListWidget.MultiSelection, sizeHint=QSize(100, 100), callback=self._clicked_tooltip_lstbox) eb = gui.widgetBox(self.displayTab, "Edges", orientation="vertical") self.checkbox_relative_edges = gui.checkBox( eb, self, 'relativeEdgeWidths', 'Relative edge widths', callback=self.set_edge_sizes) gui.hSlider(eb, self, 'edge_width', label="Edge width: ", minValue=1, maxValue=10, step=1, createLabel=False, callback=self.set_edge_sizes) self.checkbox_show_weights = gui.checkBox( eb, self, 'showEdgeWeights', 'Show edge weights', callback=self.set_edge_labels) ib = gui.widgetBox(self.markTab, "Info", orientation="vertical") gui.label(ib, self, "Nodes: %(number_of_nodes_label)i") gui.label(ib, self, "Selected: %(nSelected)i") gui.label(ib, self, "Highlighted: %(nHighlighted)i") def on_selection_change(): self.nSelected = len(self.view.getSelected()) self.nHighlighted = len(self.view.getHighlighted()) self.set_selection_mode() self.commit() self.view.selectionChanged.connect(on_selection_change) ib = gui.widgetBox(self.markTab, "Highlight nodes ...") ribg = gui.radioButtonsInBox(ib, self, "selectionMode", callback=self.set_selection_mode) gui.appendRadioButton(ribg, "None") gui.appendRadioButton(ribg, "... whose attributes contain:") self.ctrlMarkSearchString = gui.lineEdit( gui.indentedBox(ribg), self, "markSearchString", callback=self._set_search_string_timer, callbackOnType=True) self.searchStringTimer.timeout.connect(self.set_selection_mode) gui.appendRadioButton(ribg, "... neighbours of selected, ≤ N hops away") ib = gui.indentedBox(ribg, orientation=0) self.ctrlMarkDistance = gui.spin( ib, self, "markDistance", 1, 100, 1, label="Hops:", callback=lambda: self.set_selection_mode(SelectionMode.NEIGHBORS)) ib.layout().addStretch(1) gui.appendRadioButton(ribg, "... with at least N connections") gui.appendRadioButton(ribg, "... with at most N connections") ib = gui.indentedBox(ribg, orientation=0) self.ctrlMarkNConnections = gui.spin( ib, self, "markNConnections", 0, 1000000, 1, label="Connections:", callback=lambda: self.set_selection_mode( SelectionMode.AT_MOST_N if self.selectionMode == SelectionMode. AT_MOST_N else SelectionMode.AT_LEAST_N)) ib.layout().addStretch(1) gui.appendRadioButton(ribg, "... with more connections than any neighbor") gui.appendRadioButton( ribg, "... with more connections than average neighbor") gui.appendRadioButton(ribg, "... with most connections") ib = gui.indentedBox(ribg, orientation=0) self.ctrlMarkNumber = gui.spin( ib, self, "markNBest", 1, 1000000, 1, label="Number of nodes:", callback=lambda: self.set_selection_mode(SelectionMode.MOST_CONN)) ib.layout().addStretch(1) self.markInputRadioButton = gui.appendRadioButton( ribg, "... from Node Subset input signal") self.markInputRadioButton.setEnabled(True) gui.auto_commit(ribg, self, 'do_auto_commit', 'Output changes') self.markTab.layout().addStretch(1) self.set_graph(None) self.set_selection_mode() def sizeHint(self): return QSize(800, 600) def commit(self): self.send_data() @Inputs.node_distances def set_items_distance_matrix(self, matrix): assert matrix is None or isinstance(matrix, Orange.misc.DistMatrix) self.items_matrix = matrix self.relayout() def _set_search_string_timer(self): self.selectionMode = SelectionMode.SEARCH self.searchStringTimer.stop() self.searchStringTimer.start(300) def switchTab(self, index=None): index = index or self.tabs.currentIndex() curTab = self.tabs.widget(index) self.acceptingEnterKeypress = False if curTab == self.markTab and self.selectionMode != SelectionMode.NONE: self.acceptingEnterKeypress = True @non_reentrant def set_selection_mode(self, selectionMode=None): self.searchStringTimer.stop() selectionMode = self.selectionMode = selectionMode or self.selectionMode self.switchTab() if (self.graph is None or self.tabs.widget(self.tabs.currentIndex()) != self.markTab and selectionMode != SelectionMode.FROM_INPUT): return if selectionMode == SelectionMode.NONE: self.view.setHighlighted([]) elif selectionMode == SelectionMode.SEARCH: table, txt = self.graph.items(), self.markSearchString.lower() if not table or not txt: return toMark = set(i for i, instance in enumerate(table) if txt in " ".join(map(str, instance.list)).lower()) self.view.setHighlighted(toMark) elif selectionMode == SelectionMode.NEIGHBORS: selected = set(self.view.getSelected()) neighbors = selected.copy() for _ in range(self.markDistance): for neigh in list(neighbors): neighbors |= set(self.graph[neigh].keys()) neighbors -= selected self.view.setHighlighted(neighbors) elif selectionMode == SelectionMode.AT_LEAST_N: self.view.setHighlighted( set(node for node, degree in self.graph.degree() if degree >= self.markNConnections)) elif selectionMode == SelectionMode.AT_MOST_N: self.view.setHighlighted( set(node for node, degree in self.graph.degree() if degree <= self.markNConnections)) elif selectionMode == SelectionMode.ANY_NEIGH: self.view.setHighlighted( set(node for node, degree in self.graph.degree() if degree > max(dict(self.graph.degree(self.graph[node])).values(), default=0))) elif selectionMode == SelectionMode.AVG_NEIGH: self.view.setHighlighted( set(node for node, degree in self.graph.degree() if degree > np.nan_to_num( np.mean( list( dict(self.graph.degree( self.graph[node])).values()))))) elif selectionMode == SelectionMode.MOST_CONN: degrees = np.array( sorted(self.graph.degree(), key=lambda i: i[1], reverse=True)) cut_ind = max(1, min(self.markNBest, self.graph.number_of_nodes())) cut_degree = degrees[cut_ind - 1, 1] toMark = set(degrees[degrees[:, 1] >= cut_degree, 0]) self.view.setHighlighted(toMark) elif selectionMode == SelectionMode.FROM_INPUT: tomark = {} if self.markInputItems: ids = set(self.markInputItems.ids) tomark = { x for x in self.graph if self.graph.items()[x].id in ids } self.view.setHighlighted(tomark) def keyReleaseEvent(self, ev): """On Enter, expand the selected set with the highlighted""" if (not self.acceptingEnterKeypress or ev.key() not in (Qt.Key_Return, Qt.Key_Enter)): super().keyReleaseEvent(ev) return highlighted = self.view.getHighlighted() self.view.setSelected(highlighted, extend=True) self.view.setHighlighted([]) self.set_selection_mode() def save_network(self): # TODO: this was never reviewed since Orange2 if self.view is None or self.graph is None: return filename = QFileDialog.getSaveFileName( self, 'Save Network', '', 'NetworkX graph as Python pickle (*.gpickle)\n' 'NetworkX edge list (*.edgelist)\n' 'Pajek network (*.net *.pajek)\n' 'GML network (*.gml)') if filename: _, ext = os.path.splitext(filename) if not ext: filename += ".net" items = self.graph.items() for i in range(self.graph.number_of_nodes()): graph_node = self.graph.node[i] plot_node = self.networkCanvas.networkCurve.nodes()[i] if items is not None: ex = items[i] if 'x' in ex.domain: ex['x'] = plot_node.x() if 'y' in ex.domain: ex['y'] = plot_node.y() graph_node['x'] = plot_node.x() graph_node['y'] = plot_node.y() network.readwrite.write(self.graph, filename) def send_data(self): if not self.graph: for output in dir(self.Outputs): if not output.startswith('__'): getattr(self.Outputs, output).send(None) return selected = self.view.getSelected() self.Outputs.subgraph.send( self.graph.subgraph(selected) if selected else None) self.Outputs.unselected_subgraph.send( self.graph.subgraph(self.view.getUnselected() ) if selected else self.graph) self.Outputs.distances.send( self.items_matrix.submatrix(sorted(selected)) if self.items_matrix is not None and selected else None) items = self.graph.items() if not items: self.Outputs.selected.send(None) self.Outputs.highlighted.send(None) self.Outputs.remaining.send(None) else: highlighted = self.view.getHighlighted() self.Outputs.selected.send(items[ sorted(selected), :] if selected else None) self.Outputs.highlighted.send(items[ sorted(highlighted), :] if highlighted else None) remaining = sorted( set(self.graph) - set(selected) - set(highlighted)) self.Outputs.remaining.send(items[ remaining, :] if remaining else None) def _set_combos(self): self._clear_combos() self.graph_attrs = self.graph.items_vars() self.color_model[:] = [None] + [ v for v in self.graph_attrs if v.is_primitive() ] self.size_model[:] = [None] + [ v for v in self.graph_attrs if v.is_continuous ] self.size_combo.setDisabled(not self.graph_attrs) self.color_combo.setDisabled(not self.graph_attrs) self.set_node_sizes() self.set_node_colors() self.set_edge_sizes() for columns, box in ((self.attrs_label, self.attListBox), (self.attrs_tooltip, self.tooltipListBox)): columns = [var.name for var in columns] if columns: selection = QItemSelection() model = box.model() for i in range(box.count()): if str(box.item(i).text()) in columns: selection.append(QItemSelectionRange(model.index(i, 0))) selmodel = box.selectionModel() selmodel.select(selection, selmodel.Select | selmodel.Clear) else: box.selectionModel().clearSelection() self._on_node_label_attrs_changed() self._clicked_tooltip_lstbox() def _clear_combos(self): self.graph_attrs = [] self.color_combo.clear() self.size_combo.clear() def set_graph_none(self): self.graph = None self.graph_base = None self._clear_combos() self.number_of_nodes_label = 0 self.number_of_edges_label = 0 self.verticesPerEdge = 0 self.edgesPerVertex = 0 self._items = None self.view.set_graph(None) @Inputs.network def set_graph(self, graph): if not graph: return self.set_graph_none() if graph.number_of_nodes() < 2: self.set_graph_none() self.information( 'I\'m not really in a mood to visualize just one node. Try again tomorrow.' ) return if graph.number_of_nodes() + graph.number_of_edges() > 30000: self.set_graph_none() self.Error.network_too_large() return self.information() self.closeContext() all_edges_equal = bool( 1 == len(set(w for u, v, w in graph.edges(data='weight')))) self.checkbox_show_weights.setEnabled(not all_edges_equal) self.checkbox_relative_edges.setEnabled(not all_edges_equal) self.graph_base = graph self.graph = graph.copy() # Set items table from the separate signal if self._items: self.set_items(self._items) self.view.set_graph(self.graph, relayout=False) # Set labels self.number_of_nodes_label = self.graph.number_of_nodes() self.number_of_edges_label = self.graph.number_of_edges() self.verticesPerEdge = self.graph.number_of_nodes() / max( 1, self.graph.number_of_edges()) self.edgesPerVertex = self.graph.number_of_edges() / max( 1, self.graph.number_of_nodes()) self._set_combos() if self.graph.items(): self.openContext(self.graph.items().domain) self.Error.clear() self.set_selection_mode() self.randomizePositions = True self.relayout() @Inputs.node_data def set_items(self, items=None): self._items = items if items is None: return self.set_graph(self.graph_base) if not self.graph: self.Warning.no_graph_found() return self.Warning.clear() if len(items) != self.graph.number_of_nodes(): self.Error.instance_for_each_node() return self.Error.instance_for_each_node.clear() self.graph.set_items(items) self._set_combos() @Inputs.node_subset def set_marking_items(self, items): self.markInputRadioButton.setEnabled(False) self.markInputItems = items self.Warning.clear() if self.selectionMode == SelectionMode.FROM_INPUT and \ (items is None or self.graph is None or self.graph.items() is None): self.selectionMode = SelectionMode.NONE if items is None: self.view.selectionChanged.emit() return if self.graph is None or self.graph.items() is None: self.Warning.no_graph_or_items() return if len(items) > 0: self.markInputRadioButton.setEnabled(True) self.view.selectionChanged.emit() def relayout(self): if self.graph is None or self.graph.number_of_nodes() <= 1: return self.progressbar = gui.ProgressBar(self, FR_ITERATIONS) distmatrix = self.items_matrix if distmatrix is not None and distmatrix.shape[ 0] != self.graph.number_of_nodes(): self.Warning.distance_matrix_size() distmatrix = None self.Warning.distance_matrix_size.clear() self.relayout_button.setDisabled(True) self.view.relayout(randomize=self.randomizePositions, weight=distmatrix) def _on_node_label_attrs_changed(self): if not self.graph: return attributes = self.attrs_label = [ self.graph_attrs[i] for i in self.node_label_attrs ] if attributes: table = self.graph.items() if not table: return for i, node in enumerate(self.view.nodes): text = ', '.join(map(str, table[i, attributes][0].list)) node.setText(text) else: for node in self.view.nodes: node.setText('') def _clicked_tooltip_lstbox(self): if not self.graph: return attributes = self.attrs_tooltip = [ self.graph_attrs[i] for i in self.tooltipAttributes ] if attributes: table = self.graph.items() if not table: return assert self.view.nodes for i, node in enumerate(self.view.nodes): node.setTooltip( lambda row=i, attributes=attributes, table=table: '<br>'. join('<b>{.name}:</b> {}'.format( i[0], str(i[1]).replace('<', '<')) for i in zip( attributes, table[row, attributes][0].list))) else: for node in self.view.nodes: node.setTooltip(None) def set_edge_labels(self): if not self.graph: return if self.showEdgeWeights: weights = (str(w or '') for u, v, w in self.graph.edges(data='weight')) else: weights = ('' for i in range(self.graph.number_of_edges())) for edge, weight in zip(self.view.edges, weights): edge.setText(weight) def set_node_colors(self): if not self.graph: return attribute = self.attr_color assert not attribute or isinstance(attribute, Orange.data.Variable) if self.view.legend is not None: self.view.scene().removeItem(self.view.legend) self.view.legend.clear() else: self.view.legend = LegendItem() self.view.legend.set_parent(self.view) if not attribute: for node in self.view.nodes: node.setColor(None) return table = self.graph.items() if not table: return if attribute in table.domain.class_vars: values = table[:, attribute].Y if values.ndim > 1: values = values.T elif attribute in table.domain.metas: values = table[:, attribute].metas[:, 0] elif attribute in table.domain.attributes: values = table[:, attribute].X[:, 0] else: raise RuntimeError("Shouldn't be able to select this column") if attribute.is_continuous: colors = CONTINUOUS_PALETTE[scale(values)] label = PaletteItemSample( CONTINUOUS_PALETTE, DiscretizedScale(np.nanmin(values), np.nanmax(values))) self.view.legend.addItem(label, "") self.view.legend.setGeometry(label.boundingRect()) elif attribute.is_discrete: DISCRETE_PALETTE = ColorPaletteGenerator(len(attribute.values)) colors = DISCRETE_PALETTE[values] for value, color in zip(attribute.values, DISCRETE_PALETTE): self.view.legend.addItem( ScatterPlotItem(pen=Node.Pen.DEFAULT, brush=QBrush(QColor(color)), size=10, symbol="o"), escape(value)) for node, color in zip(self.view.nodes, colors): node.setColor(color) self.view.scene().addItem(self.view.legend) self.view.legend.geometry_changed() def set_node_sizes(self): self.invertNodeSizeCheck.setDisabled(not self.attr_size) if not self.graph: return table = self.graph.items() if table is None: return try: a = table.get_column_view(self.attr_size)[0] values = a.copy() except Exception: for node in self.view.nodes: node.setSize(MIN_NODE_SIZE * self.point_width) return if self.invertNodeSize: values += np.nanmin(values) + 1 values = 1 / values nodemin, nodemax = np.nanmin(values), np.nanmax(values) if nodemin == nodemax: # np.polyfit borks on this condition sizes = (MIN_NODE_SIZE for _ in range(len(self.view.nodes))) else: k, n = np.polyfit([nodemin, nodemax], [MIN_NODE_SIZE, MAX_NODE_SIZE], 1) sizes = values * k + n sizes[np.isnan(sizes)] = np.nanmean(sizes) for node, size in zip(self.view.nodes, sizes): node.setSize(size * self.point_width) def set_edge_sizes(self): if not self.graph: return if self.relativeEdgeWidths: widths = [ self.graph.adj[u][v].get('weight', 1) for u, v in self.graph.edges() ] widths = scale(widths, .7, 8) * np.log2(self.edge_width / 4 + 1) else: widths = (.7 * self.edge_width for _ in range(self.graph.number_of_edges())) for edge, width in zip(self.view.edges, widths): edge.setSize(width) def send_report(self): self.report_data("Data", self.graph.items()) self.report_items('Graph info', [ ("Number of vertices", self.graph.number_of_nodes()), ("Number of edges", self.graph.number_of_edges()), ("Vertices per edge", "%.3f" % self.verticesPerEdge), ("Edges per vertex", "%.3f" % self.edgesPerVertex), ]) if self.node_color_attr or self.node_size_attr or self.node_label_attrs: self.report_items("Visual settings", [ ("Vertex color", self.colorCombo.currentText()), ("Vertex size", str(self.nodeSizeCombo.currentText()) + " (inverted)" if self.invertNodeSize else ""), ("Labels", ", ".join(self.graph_attrs[i].name for i in self.node_label_attrs)), ]) self.report_plot("Graph", self.view)