diff --git a/epdb/models.py b/epdb/models.py index 16785613..2ed9c5a1 100644 --- a/epdb/models.py +++ b/epdb/models.py @@ -2071,7 +2071,10 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix def empty_due_to_threshold(self): return self.kv.get("empty_due_to_threshold", False) - def d3_json(self): + def merged_d3_json(self): + return self.d3_json(merged=True) + + def d3_json(self, merged=False): # Ideally it would be something like this but # to reduce crossing in edges do a DFS # nodes = [n.d3_json() for n in self.nodes] @@ -2079,15 +2082,68 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix nodes = [] processed = set() + structure_to_combined_nodes = dict() + node_to_combined_node = dict() queue = list() - for n in self.root_nodes: - queue.append(n) - # Add unconnected nodes - for n in self.nodes.order_by("url"): - if len(n.out_edges.all()) == 0: - if n not in queue: - queue.append(n) + class CombinedNode(object): + + def __init__(self, representative: "Node"): + self.representative = representative + self.nodes = set([self.representative]) + + def ids(self): + return [n.id for n in self.nodes] + + def d3_json(self): + d3_json = self.representative.d3_json() + compound = self.representative.default_node_label.compound + d3_json["name"] = compound.get_name() + d3_json["smiles"] = compound.default_structure.smiles + d3_json["node_label_id"] = compound.default_structure.url + d3_json["image"] = f"{compound.default_structure.url}?image=svg", + d3_json["image_svg"] = IndigoUtils.mol_to_svg( + compound.default_structure.molfile + if compound.default_structure.molfile is not None + and compound.default_structure.molfile.strip() + else compound.default_structure.smiles, + width=40, + height=40, + ), + + return d3_json + + def __repr__(self): + return f"CombinedNode({self.nodes})" + + if merged: + for n in self.nodes: + def_struct = n.default_node_label.compound.default_structure + if def_struct in structure_to_combined_nodes: + structure_to_combined_nodes[def_struct].nodes.add(n) + else: + cn = CombinedNode(n) + structure_to_combined_nodes[def_struct] = cn + + # for n in self.nodes.order_by("url"): + # if len(n.out_edges.all()) == 0: + # if n not in queue: + # queue.append(n) + + for cn in structure_to_combined_nodes.values(): + for n in cn.nodes: + node_to_combined_node[n] = cn + + queue = list(structure_to_combined_nodes.values()) + else: + for n in self.nodes: + queue.append(n) + + # Add unconnected nodes + for n in self.nodes.order_by("url"): + if len(n.out_edges.all()) == 0: + if n not in queue: + queue.append(n) for i in queue: processed.add(i) @@ -2097,25 +2153,56 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix processed.add(current) nodes.append(current.d3_json()) - for e in self.edges.filter(start_nodes=current).order_by("url").distinct(): + if merged: + q = Q(start_nodes__in=current.ids()) + else: + q = Q(start_nodes=current) + + for e in self.edges.filter(q).order_by("url").distinct(): for prod in e.end_nodes.all().order_by("url"): - if prod not in queue and prod not in processed: - queue.append(prod) + + if merged: + def_struct = n.default_node_label.compound.default_structure + if def_struct in structure_to_combined_nodes: + structure_to_combined_nodes[def_struct].nodes.add(n) + else: + cn = CombinedNode(n) + structure_to_combined_nodes[def_struct] = cn + if cn not in queue and cn not in processed: + queue.append(cn) + else: + if prod not in queue and prod not in processed: + queue.append(prod) # We shouldn't lose or make up nodes... - if len(nodes) != len(self.nodes): - logger.debug( - f"{self.get_name()}: Num Nodes {len(nodes)} vs. DB Nodes {len(self.nodes)}" - ) + if not merged: + if len(nodes) != len(self.nodes): + logger.debug( + f"{self.get_name()}: Num Nodes {len(nodes)} vs. DB Nodes {len(self.nodes)}" + ) links = [e.d3_json() for e in self.edges] # D3 links Nodes based on indices in nodes array + combined_node_depth_to_idx = dict() node_url_to_idx = dict() for i, n in enumerate(nodes): n["id"] = i node_url_to_idx[n["url"]] = i + # Add potential missing mappings + if merged: + cn_idx_mapping = dict() + for n in nodes: + for cn in processed: + for sub_n in cn.nodes: + if n["url"] == sub_n.url: + cn_idx_mapping[cn] = n["id"] + + for n in processed: + for sub_n in n.nodes: + node_url_to_idx[sub_n.url] = cn_idx_mapping[n] + adjusted_links = [] for link in links: # Check if we'll need pseudo nodes diff --git a/epdb/views.py b/epdb/views.py index b5307d1c..fd363f52 100644 --- a/epdb/views.py +++ b/epdb/views.py @@ -2313,6 +2313,7 @@ def package_pathway(request, package_uuid, pathway_uuid): context["pathway"] = current_pathway context["current_object"] = current_pathway + context["merged"] = request.GET.get("merged", False) == "true" context["breadcrumbs"] = [ {"Home": s.SERVER_URL}, diff --git a/templates/objects/pathway.html b/templates/objects/pathway.html index b386daf6..20102597 100644 --- a/templates/objects/pathway.html +++ b/templates/objects/pathway.html @@ -544,8 +544,11 @@ {% endif %} + {% if merged %} + {{ pathway.merged_d3_json|json_script:"pathway" }} + {% else %} {{ pathway.d3_json|json_script:"pathway" }} - + {% endif %}