Skip to content

Commit

Permalink
Network Explorer: Edges with colors and labels
Browse files Browse the repository at this point in the history
  • Loading branch information
janezd committed Jun 10, 2023
1 parent aabf527 commit 773e9a1
Show file tree
Hide file tree
Showing 3 changed files with 168 additions and 65 deletions.
148 changes: 112 additions & 36 deletions orangecontrib/network/widgets/OWNxExplorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@
from AnyQt.QtCore import QTimer, QSize, Qt, Signal, QObject, QThread

import Orange
from Orange.data import Table, Domain, StringVariable
from Orange.data import Table, Domain, StringVariable, ContinuousVariable
from Orange.widgets import gui, widget
from Orange.widgets.settings import Setting, SettingProvider
from Orange.widgets.utils.itemmodels import DomainModel
from Orange.widgets.utils.plot import OWPlotGUI
from Orange.widgets.visualize.utils.widget import OWDataProjectionWidget
from Orange.widgets.widget import Input, Output

from orangecontrib.network.network import compose
from orangecontrib.network.network.base import Network
from orangecontrib.network.network.layout import fruchterman_reingold
from orangecontrib.network.widgets.graphview import GraphView

FR_ALLOWED_TIME = 30

WEIGHTS_COMBO_ITEM = "Weights"

class OWNxExplorer(OWDataProjectionWidget):
name = "Network Explorer"
Expand Down Expand Up @@ -50,6 +52,11 @@ class Outputs(OWDataProjectionWidget.Outputs):
mark_min_conn = Setting(5)
mark_max_conn = Setting(5)
mark_most_conn = Setting(1)
# TODO: Context setting or (better) hints, because there are two separate
# contexts, also from the user perspective
edge_width_variable = Setting(None)
edge_label_variable = Setting(None)
edge_color_variable = Setting(None)

alpha_value = 255 # Override the setting from parent

Expand Down Expand Up @@ -83,6 +90,7 @@ def __init__(self):
self.node_data = None
self.distance_matrix = None
self.edges = None
self.edge_data = None
self.positions = None

self._optimizer = None
Expand All @@ -101,12 +109,24 @@ def sizeHint(self):
def _add_controls(self):
self.gui = OWPlotGUI(self)
self._add_info_box()
self.gui.point_properties_box(self.controlArea)
self._add_effects_box()
self.gui.plot_properties_box(self.controlArea)
self._add_node_box()
self._add_edge_box()
self._add_properties_box()
self._add_mark_box()
self.controls.attr_label.activated.connect(self.on_change_label_attr)

def _add_node_box(self):
sgui = self.gui
box = sgui.create_gridbox(self.controlArea, "Nodes")
sgui.add_widgets([
sgui.Color,
sgui.Shape,
sgui.Label,
sgui.Size,
sgui.PointSize,
], box)
box.layout().itemAtPosition(5, 0).widget().setText("")

def _add_info_box(self):
info = gui.vBox(self.controlArea, box="Layout")
gui.label(
Expand Down Expand Up @@ -136,36 +156,49 @@ def _add_info_box(self):
label="Make edges with large weights shorter",
callback=self.improve)

def _add_effects_box(self):
gbox = self.gui.create_gridbox(self.controlArea, box="Widths and Sizes")
self.gui.add_widget(self.gui.PointSize, gbox)
gbox.layout().itemAtPosition(1, 0).widget().setText("Node Size:")
def _add_edge_box(self):
gbox = self.gui.create_gridbox(self.controlArea, box="Edges")

order = (None, WEIGHTS_COMBO_ITEM, DomainModel.Separator) + DomainModel.SEPARATED
self.edge_label_model = DomainModel(
placeholder="(None)", order=order, separators=True)
self.gui._combo(
gbox, "edge_label_variable", "Label", self.graph.update_edge_labels,
model=self.edge_label_model)
self.edge_color_model = DomainModel(
placeholder="(Same color)",
valid_types=DomainModel.PRIMITIVE)
self.gui._combo(
gbox, "edge_color_variable", "Color", self.graph.update_edge_colors,
model=self.edge_color_model)
self.edge_width_model = DomainModel(
valid_types=ContinuousVariable,
placeholder="(Same width)", order=order, separators=False)
self.gui.add_control(
gbox, gui.hSlider, "Edge width:",
gbox,
gui.comboBox, "Width:",
master=self, value="edge_width_variable",
model=self.edge_width_model,
callback=self.graph.update_edge_widths
)
self.gui.add_control(
gbox, gui.hSlider, "",
master=self, value='graph.edge_width',
minValue=1, maxValue=10, step=1,
callback=self.graph.update_edges)
box = gui.vBox(None)
gbox.layout().addWidget(box, 3, 0, 1, 2)
gui.separator(box)
self.checkbox_relative_edges = gui.checkBox(
box, self, 'graph.relative_edge_widths',
'Scale edge widths to weights',
callback=self.graph.update_edges)
self.checkbox_show_weights = gui.checkBox(
box, self, 'graph.show_edge_weights',
'Show edge weights',
callback=self.graph.update_edge_labels)
self.checkbox_show_weights = gui.checkBox(
box, self, 'graph.label_selected_edges',
'Label only edges of selected nodes',
callback=self.graph.update_edge_labels)
callback=self.graph.update_edge_widths)

# This is ugly: create a slider that controls alpha_value so that
# parent can enable and disable it - although it's never added to any
# layout and visible to the user
gui.hSlider(None, self, "graph.alpha_value")

def _add_properties_box(self):
sgui = self.gui
return sgui.create_box([
sgui.LabelOnlySelected,
sgui.ClassDensity,
sgui.ShowLegend], self.controlArea, None, False)

def _add_mark_box(self):
hbox = gui.hBox(None, box=True)
self.mainArea.layout().addWidget(hbox)
Expand Down Expand Up @@ -469,22 +502,32 @@ def set_actual_data():
self.cb_class_density.setEnabled(self.can_draw_density())

def set_actual_edges():
def set_checkboxes(value):
self.checkbox_show_weights.setEnabled(value)
self.checkbox_relative_edges.setEnabled(value)

self.Warning.distance_matrix_mismatch.clear()

if self.network is None:
self.edges = None
set_checkboxes(False)
self.edge_data = None
return

set_checkboxes(True)
if network.number_of_edges(0):
self.edges = network.edges[0].edges.tocoo()
edges = network.edges[0]
self.edges = edges.edges.tocoo()
self.edge_data = edge_data = edges.edge_data
if isinstance(edge_data, np.ndarray):
if edge_data.dtype == float:
self.edge_data = Table.from_numpy(
Domain([ContinuousVariable("label")]),
np.atleast_2d(edge_data))
else:
self.edge_data = Table.from_numpy(
Domain([], metas=[StringVariable("label")]),
np.empty((len(edge_data), 0)),
metas=edge_data.reshape(len(edge_data), 1))
elif edge_data is not None:
assert isinstance(edges.edge_data, Table)
else:
self.edges = sp.coo_matrix((0, 3))
self.edge_data = None
if self.distance_matrix is not None:
if len(self.distance_matrix) != self.number_of_nodes:
self.Warning.distance_matrix_mismatch()
Expand All @@ -496,9 +539,11 @@ def set_checkboxes(value):
)
if np.allclose(self.edges.data, 0):
self.edges.data[:] = 1
set_checkboxes(False)
elif len(set(self.edges.data)) == 1:
set_checkboxes(False)

domain = None if self.edge_data is None else self.edge_data.domain
self.edge_label_model.set_domain(domain)
self.edge_color_model.set_domain(domain)
self.edge_width_model.set_domain(domain)

self.stop_optimization_and_wait()
set_actual_data()
Expand Down Expand Up @@ -581,6 +626,34 @@ def get_subset_mask(self):
def get_edges(self):
return self.edges

def get_edge_labels(self):
if self.edge_label_variable is None:
return None
if self.edge_label_variable == WEIGHTS_COMBO_ITEM:
weights = self.edges.data
if np.allclose(np.modf(weights)[0], 0):
return np.array([str(x) for x in weights.astype(int)])
else:
return np.array(["{:.02}".format(x) for x in weights])
elw = self.edge_label_variable
tostr = elw.repr_val
return np.array([tostr(x) for x in self.edge_data.get_column(elw)])

def get_edge_widths(self):
if self.edge_width_variable is None:
return None
if self.edge_width_variable == WEIGHTS_COMBO_ITEM:
widths = self.edges.data
return widths if len(set(widths)) > 1 else None
else:
return self.edge_data.get_column(self.edge_width_variable)

def get_edge_colors(self):
var = self.edge_color_variable
if var is None:
return None
return var.palette.values_to_qcolors(self.edge_data.get_column(var))

def is_directed(self):
return self.network is not None and self.network.edges[0].directed

Expand Down Expand Up @@ -719,7 +792,10 @@ def main():
network = read_pajek(join(dirname(dirname(__file__)), 'networks', 'leu_by_genesets.net'))
#network = read_pajek(join(dirname(dirname(__file__)), 'networks', 'davis.net'))
#transform_data_to_orange_table(network)
data = Table("/Users/janez/Downloads/relations.tab")
network = compose.network_from_edge_table(data, *data.domain.metas[:2])
WidgetPreview(OWNxExplorer).run(set_graph=network)


if __name__ == "__main__":
main()
Loading

0 comments on commit 773e9a1

Please sign in to comment.