Skip to content

Commit

Permalink
improved Escher integration (#513)
Browse files Browse the repository at this point in the history
* improved Escher integration

* cleanup

* more signal/slot, cleanup

* fix renaming bug, use wastebaket icon for reaction deletion
  • Loading branch information
axelvonkamp authored Jul 2, 2024
1 parent 1873d28 commit 6385cea
Show file tree
Hide file tree
Showing 4 changed files with 291 additions and 95 deletions.
174 changes: 167 additions & 7 deletions cnapy/data/escher_cnapy.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,51 @@
builder.load_map(JSON.parse(map_and_geometry[0]));
builder.map.zoomContainer.goTo(JSON.parse(map_and_geometry[1]), JSON.parse(map_and_geometry[2]));
}
builder.passPropsSearchBar({display:true});
cnapy_bridge.changeReactionId.connect(changeReacId)
cnapy_bridge.zoomIn.connect(builder.zoom_container.zoom_in)
cnapy_bridge.zoomOut.connect(builder.zoom_container.zoom_out)
cnapy_bridge.changeMetId.connect(changeMetId)
cnapy_bridge.highlightAndFocusReaction.connect(highlightAndFocusReaction)
cnapy_bridge.highlightReaction.connect(highlightReaction)
cnapy_bridge.deleteReaction.connect(deleteReaction)
cnapy_bridge.updateReactionStoichiometry.connect(updateReactionStoichiometry)
cnapy_bridge.addMapToJumpListIfReactionPresent.connect(addMapToJumpListIfReactionPresent)
cnapy_bridge.hideSearchBar.connect(function() {search_container.style.display = 'none';})
cnapy_bridge.displaySearchBarFor.connect(function(value) {
search_container.style.display = '';
search_field.value = value;
search_field.dispatchEvent(new Event('input'));
})
cnapy_bridge.setCobraModel.connect(function (model_data) {
builder.load_model(JSON.parse(model_data))
})
cnapy_bridge.enableEditing.connect(function (enable) {
if (enable) {
tooltip = "[]"
menu = "block"
}
else {
tooltip = "['object','label']"
menu = "none"
}
builder.settings.set('enable_editing', enable);
builder.settings.set('enable_keys', enable);
builder.settings.set('enable_tooltips', tooltip);
document.getElementsByClassName('button-panel')[0].hidden = !enable;
document.getElementsByClassName('menu-bar')[0].style['display'] = menu
})
cnapy_bridge.visualizeCompValues.connect(function (reaction_data, text_only) {
var styles
if (text_only) {
styles = builder.map.settings.get('reaction_styles')
builder.map.settings.set('reaction_styles', 'text')
}
builder.set_reaction_data([reaction_data])
if (text_only)
builder.settings._options.reaction_styles=styles
})
cnapy_bridge.clearReactionData.connect(function() {builder.set_reaction_data(null)})
builder.passPropsSearchBar({display:true}); // create here, will be hidden in finish_setup
setTimeout(() => {cnapy_bridge.finish_setup();}, 50)
})
}
Expand All @@ -42,7 +86,7 @@
const preact = escher.libs.preact;
const h = preact.createElement;

var tooltipStyle = {
const tooltipStyle = {
'min-width': '40px',
'min-height': '10px',
'border-radius': '2px',
Expand All @@ -56,6 +100,13 @@
'box-shadow': '4px 6px 20px 0px rgba(0, 0, 0, 0.4)'
};

const buttonStyle = {
position: 'absolute',
top: '2px',
right: '2px',
fontSize: '10px'
};

class CnapyTooltip extends preact.Component {
constructor() {
super()
Expand All @@ -82,8 +133,10 @@
var tip = h('div', {className: 'cnapy-tooltip', style: tooltipStyle},
h('div', {className: 'id', onClick: (event) => this.handleClickOnID(event), style: "font-weight: bold;"}, this.props.biggId),
h('div', {className: 'name'}, this.props.name))

if (this.props.type === 'reaction') {

if (this.props.type === 'reaction') {
tip.children.push(h('button', {style: buttonStyle, 'aria-label': 'Close',
'title': 'Delete reaction from map', onClick: (event) => {deleteReaction(this.props.biggId)}}, '🗑️'))
tip.children.push(h('input', {type: 'text',
id: 'reaction-box-input',
style: 'color: black',
Expand All @@ -108,14 +161,13 @@
builder = escher.Builder(null, null, null, escher.libs.d3_select('#map_container'),
{menu: 'all', fill_screen: true, never_ask_before_quit: true, tooltip_component: CnapyTooltip, scroll_behavior: 'zoom'})

function reactionOnMap(reacId, mapName) {
function addMapToJumpListIfReactionPresent(reacId, mapName) {
var records = builder.map.search_index.find(reacId);
for (i=0; i<records.length; i++) {
var record = records[i];
if (record.type == "reaction" && builder.map.reactions[record.reaction_id].bigg_id == reacId)
return mapName;
cnapy_bridge.add_map_to_jump_list(mapName);
}
return "";
}

function highlightAndFocusReaction(reacId) {
Expand All @@ -140,6 +192,114 @@
}
}

function changeReacId(oldReacId, newReacId) {
var records = builder.map.search_index.find(oldReacId);
var reaction_ids = []
for (i=0; i<records.length; i++) {
var record = records[i];
if (record.type == "reaction") {
var reaction = builder.map.reactions[record.reaction_id]
if (reaction.bigg_id == oldReacId) {
reaction.bigg_id = newReacId
builder.map.search_index.index["r"+record.reaction_id].name = newReacId
reaction_ids.push(record.reaction_id)
}
}
}
builder.map.draw_these_reactions(reaction_ids)
}

function changeMetId(oldMetId, newMetId) {
var records = builder.map.search_index.find(oldMetId);
var node_ids = []
for (i=0; i<records.length; i++) {
var record = records[i];
if (record.type == "metabolite") {
var node = builder.map.nodes[record.node_id]
if (node.bigg_id == oldMetId) {
node.bigg_id = newMetId
builder.map.search_index.index["n"+record.node_id].name = newMetId
node_ids.push(record.node_id)
}
}
}
builder.map.draw_these_nodes(node_ids)
// need to go through all reactions and change metabolite ID if it occurs
Object.values(builder.map.reactions).forEach(reaction => {
reaction.metabolites.forEach(met => {if (met.bigg_id == oldMetId) {met.bigg_id = newMetId}})
})
}

// !! only updates stoichiomteric coefficients of already existing metabolites
// !! does not add new or delete removed metabolites
function updateReactionStoichiometry(reacId, newStoic, reversibility) {
var records = builder.map.search_index.find(reacId)
var reaction_ids = []
for (i=0; i<records.length; i++) {
var reaction = builder.map.reactions[records[i].reaction_id]
if (reaction.bigg_id == reacId) {
reaction_ids.push(records[i].reaction_id)
for (i=0; i<reaction.metabolites.length; i++) {
var met = reaction.metabolites[i];
var new_coeff = newStoic[met.bigg_id];
if (new_coeff)
met.coefficient = new_coeff
}
reaction.reversibility = reversibility
for (var segmentId in reaction.segments) { // from convert_map function of Map.js
const segment = reaction.segments[segmentId]

// propagate reversibility
segment.reversibility = reaction.reversibility

const from_node = builder.map.nodes[segment.from_node_id]
const to_node = builder.map.nodes[segment.to_node_id]

// propagate coefficients
reaction.metabolites.forEach(met => {
if (met.bigg_id === from_node.bigg_id) {
segment.from_node_coefficient = met.coefficient
} else if (met.bigg_id === to_node.bigg_id) {
segment.to_node_coefficient = met.coefficient
}
})
}
}
}
builder.map.draw_these_reactions(reaction_ids)
}

function deleteReaction(reacId) {
builder.map.deselect_nodes()
var records = builder.map.search_index.find(reacId);
var reaction_ids = new Set()
for (i=0; i<records.length; i++) { // multiple records for the same reaction can occur
var record = records[i];
if (record.type == "reaction" && builder.map.reactions[record.reaction_id].bigg_id == reacId)
reaction_ids.add(record.reaction_id)
}
for (rid of reaction_ids) {
reaction = builder.map.reactions[rid];
var node_ids = new Set()
for (let key in reaction.segments) {
seg = reaction.segments[key];
node_ids.add(reaction.segments[key].from_node_id);
node_ids.add(reaction.segments[key].to_node_id);
}
nodes = {}
for (let nid of node_ids) {
node = builder.map.nodes[nid]
if (node.node_type != 'metabolite')
nodes[nid] = node
else // do not delete metabolite nodes connected to other reactions
if (node.connected_segments.length == 1)
nodes[nid] = node
}
builder.map.delete_selectable(nodes, {}, true)
}
cnapy_bridge.unsaved_changes()
}

</script>
</body>
</html>
26 changes: 20 additions & 6 deletions cnapy/gui_elements/central_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,13 +179,16 @@ def show_bottom_of_console(self):
def handle_changed_reaction(self, previous_id: str, reaction: cobra.Reaction):
self.parent.unsaved_changes()
reaction_has_box = False
escher_map_present = False
for mmap in self.appdata.project.maps:
if previous_id in self.appdata.project.maps[mmap]["boxes"].keys():
self.appdata.project.maps[mmap]["boxes"][reaction.id] = self.appdata.project.maps[mmap]["boxes"].pop(
previous_id)
reaction_has_box = True
if reaction_has_box:
self.update_reaction_on_maps(previous_id, reaction.id)
if self.appdata.project.maps[mmap]["view"] == "escher":
escher_map_present = True
if reaction_has_box or escher_map_present:
self.update_reaction_on_maps(previous_id, reaction.id, reaction_has_box, escher_map_present)
self.update_item_in_history(previous_id, reaction.id, reaction.name, ModelItemType.Reaction)

def handle_deleted_reaction(self, reaction: cobra.Reaction):
Expand All @@ -207,8 +210,12 @@ def handle_deleted_reaction(self, reaction: cobra.Reaction):
@Slot(cobra.Metabolite, object, str)
def handle_changed_metabolite(self, metabolite: cobra.Metabolite, affected_reactions, previous_id: str):
self.parent.unsaved_changes()
for reaction in affected_reactions:
for reaction in affected_reactions: # only updates CNApy maps
self.update_reaction_on_maps(reaction.id, reaction.id)
for idx in range(0, self.map_tabs.count()):
m = self.map_tabs.widget(idx)
if isinstance(m, EscherMapView):
m.change_metabolite_id(previous_id, metabolite.id)
self.update_item_in_history(previous_id, metabolite.id, metabolite.name, ModelItemType.Metabolite)

def handle_changed_gene(self, previous_id: str, gene: cobra.Gene):
Expand Down Expand Up @@ -546,18 +553,25 @@ def update_map(self, idx):
m.update()
self.__recolor_map()

def update_reaction_on_maps(self, old_reaction_id: str, new_reaction_id: str):
def update_reaction_on_maps(self, old_reaction_id: str, new_reaction_id: str,
update_cnapy_maps:bool=True, update_escher_maps:bool=False):
for idx in range(0, self.map_tabs.count()):
m = self.map_tabs.widget(idx)
if isinstance(m, MapView): # TODO: what should be done on Escher maps?
if update_cnapy_maps and isinstance(m, MapView):
m.update_reaction(old_reaction_id, new_reaction_id)
elif update_escher_maps and isinstance(m, EscherMapView):
if old_reaction_id != new_reaction_id:
m.change_reaction_id(old_reaction_id, new_reaction_id)
else:
m.update_reaction_stoichiometry(old_reaction_id)

def delete_reaction_on_maps(self, reation_id: str):
for idx in range(0, self.map_tabs.count()):
m = self.map_tabs.widget(idx)
if isinstance(m, MapView):
m.delete_box(reation_id)
#TODO: find out if a reaction can be programatically removed from Escher
else:
m.delete_reaction(reation_id)

def update_maps(self):
for idx in range(0, self.map_tabs.count()):
Expand Down
Loading

0 comments on commit 6385cea

Please sign in to comment.