escher-vis
Version:
Escher: A Web Application for Building, Sharing, and Embedding Data-Rich Visualizations of Biological Pathways
1,497 lines (1,371 loc) • 1.16 MB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.escher = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/**
* Behavior. Defines the set of click and drag behaviors for the map, and keeps
* track of which behaviors are activated.
*
* A Behavior instance has the following attributes:
*
* my_behavior.rotation_drag, my_behavior.text_label_mousedown,
* my_behavior.text_label_click, my_behavior.selectable_mousedown,
* my_behavior.selectable_click, my_behavior.selectable_drag,
* my_behavior.node_mouseover, my_behavior.node_mouseout,
* my_behavior.label_mousedown, my_behavior.label_mouseover,
* my_behavior.label_mouseout, my_behavior.bezier_drag,
* my_behavior.bezier_mouseover, my_behavior.bezier_mouseout,
* my_behavior.reaction_label_drag, my_behavior.node_label_drag,
*
*/
var utils = require('./utils')
var build = require('./build')
var d3_drag = require('d3-drag').drag
var d3_select = require('d3-selection').select
var d3_mouse = require('d3-selection').mouse
var d3_selection = require('d3-selection')
var Behavior = utils.make_class()
// methods
Behavior.prototype = {
init: init,
toggle_rotation_mode: toggle_rotation_mode,
turn_everything_on: turn_everything_on,
turn_everything_off: turn_everything_off,
// toggle
toggle_selectable_click: toggle_selectable_click,
toggle_text_label_edit: toggle_text_label_edit,
toggle_selectable_drag: toggle_selectable_drag,
toggle_label_drag: toggle_label_drag,
toggle_label_mouseover: toggle_label_mouseover,
toggle_bezier_drag: toggle_bezier_drag,
// util
turn_off_drag: turn_off_drag,
// get drag behaviors
_get_selectable_drag: _get_selectable_drag,
_get_bezier_drag: _get_bezier_drag,
_get_reaction_label_drag: _get_reaction_label_drag,
_get_node_label_drag: _get_node_label_drag,
_get_generic_drag: _get_generic_drag,
_get_generic_angular_drag: _get_generic_angular_drag
}
module.exports = Behavior
// definitions
function init (map, undo_stack) {
this.map = map
this.undo_stack = undo_stack
// make an empty function that can be called as a behavior and does nothing
this.empty_behavior = function () {}
// rotation mode operates separately from the rest
this.rotation_mode_enabled = false
this.rotation_drag = d3_drag()
// behaviors to be applied
this.selectable_mousedown = null
this.text_label_mousedown = null
this.text_label_click = null
this.selectable_drag = this.empty_behavior
this.node_mouseover = null
this.node_mouseout = null
this.label_mousedown = null
this.label_mouseover = null
this.label_mouseout = null
this.bezier_drag = this.empty_behavior
this.bezier_mouseover = null
this.bezier_mouseout = null
this.reaction_label_drag = this.empty_behavior
this.node_label_drag = this.empty_behavior
this.dragging = false
this.turn_everything_on()
}
/**
* Toggle everything except rotation mode and text mode.
*/
function turn_everything_on () {
this.toggle_selectable_click(true)
this.toggle_selectable_drag(true)
this.toggle_label_drag(true)
this.toggle_label_mouseover(true)
}
/**
* Toggle everything except rotation mode and text mode.
*/
function turn_everything_off () {
this.toggle_selectable_click(false)
this.toggle_selectable_drag(false)
this.toggle_label_drag(false)
this.toggle_label_mouseover(false)
}
/**
* Listen for rotation, and rotate selected nodes.
*/
function toggle_rotation_mode (on_off) {
if (on_off === undefined) {
this.rotation_mode_enabled = !this.rotation_mode_enabled
} else {
this.rotation_mode_enabled = on_off
}
var selection_node = this.map.sel.selectAll('.node-circle')
var selection_background = this.map.sel.selectAll('#canvas')
if (this.rotation_mode_enabled) {
this.map.callback_manager.run('start_rotation')
var selected_nodes = this.map.get_selected_nodes()
if (Object.keys(selected_nodes).length === 0) {
console.warn('No selected nodes')
return
}
// show center
this.center = average_location(selected_nodes)
show_center.call(this)
// this.set_status('Drag to rotate.')
var map = this.map
var selected_node_ids = Object.keys(selected_nodes)
var reactions = this.map.reactions
var nodes = this.map.nodes
var beziers = this.map.beziers
var start_fn = function (d) {
// silence other listeners
d3_selection.event.sourceEvent.stopPropagation()
}
var drag_fn = function (d, angle, total_angle, center) {
var updated = build.rotate_nodes(selected_nodes, reactions,
beziers, angle, center)
map.draw_these_nodes(updated.node_ids)
map.draw_these_reactions(updated.reaction_ids)
}
var end_fn = function (d) {}
var undo_fn = function (d, total_angle, center) {
// undo
var these_nodes = {}
selected_node_ids.forEach(function (id) {
these_nodes[id] = nodes[id]
})
var updated = build.rotate_nodes(these_nodes, reactions,
beziers, -total_angle,
center)
map.draw_these_nodes(updated.node_ids)
map.draw_these_reactions(updated.reaction_ids)
}
var redo_fn = function (d, total_angle, center) {
// redo
var these_nodes = {}
selected_node_ids.forEach(function (id) {
these_nodes[id] = nodes[id]
})
var updated = build.rotate_nodes(these_nodes, reactions,
beziers, total_angle,
center)
map.draw_these_nodes(updated.node_ids)
map.draw_these_reactions(updated.reaction_ids) }
var center_fn = function () {
return this.center
}.bind(this)
this.rotation_drag = this._get_generic_angular_drag(start_fn, drag_fn,
end_fn, undo_fn,
redo_fn, center_fn,
this.map.sel)
selection_background.call(this.rotation_drag)
this.selectable_drag = this.rotation_drag
} else {
// turn off all listeners
hide_center.call(this)
selection_node.on('mousedown.center', null)
selection_background.on('mousedown.center', null)
selection_background.on('mousedown.drag', null)
selection_background.on('touchstart.drag', null)
this.rotation_drag = null
this.selectable_drag = null
}
// definitions
function show_center () {
var sel = this.map.sel.selectAll('#rotation-center').data([ 0 ])
var enter_sel = sel.enter().append('g').attr('id', 'rotation-center')
enter_sel.append('path').attr('d', 'M-32 0 L32 0')
.attr('class', 'rotation-center-line')
enter_sel.append('path').attr('d', 'M0 -32 L0 32')
.attr('class', 'rotation-center-line')
var update_sel = enter_sel.merge(sel)
update_sel.attr('transform',
'translate(' + this.center.x + ',' + this.center.y + ')')
.attr('visibility', 'visible')
.on('mouseover', function () {
var current = parseFloat(update_sel.selectAll('path').style('stroke-width'))
update_sel.selectAll('path').style('stroke-width', current * 2 + 'px')
})
.on('mouseout', function () {
update_sel.selectAll('path').style('stroke-width', null)
})
.call(d3_drag().on('drag', function () {
var cur = utils.d3_transform_catch(update_sel.attr('transform'))
var new_loc = [
d3_selection.event.dx + cur.translate[0],
d3_selection.event.dy + cur.translate[1]
]
update_sel.attr('transform', 'translate(' + new_loc + ')')
this.center = { x: new_loc[0], y: new_loc[1] }
}.bind(this)))
}
function hide_center(sel) {
this.map.sel.select('#rotation-center')
.attr('visibility', 'hidden')
}
function average_location(nodes) {
var xs = []
var ys = []
for (var node_id in nodes) {
var node = nodes[node_id]
if (node.x !== undefined)
xs.push(node.x)
if (node.y !== undefined)
ys.push(node.y)
}
return { x: utils.mean(xs),
y: utils.mean(ys) }
}
}
/**
* With no argument, toggle the node click on or off. Pass in a boolean argument
* to set the on/off state.
*/
function toggle_selectable_click (on_off) {
if (on_off === undefined) {
on_off = this.selectable_mousedown === null
}
if (on_off) {
var map = this.map
this.selectable_mousedown = function (d) {
// stop propogation for the buildinput to work right
d3_selection.event.stopPropagation()
// this.parentNode.__data__.was_selected = d3_select(this.parentNode).classed('selected')
// d3_select(this.parentNode).classed('selected', true)
}
this.selectable_click = function (d) {
// stop propogation for the buildinput to work right
d3_selection.event.stopPropagation()
// click suppressed. This DOES have en effect.
if (d3_selection.event.defaultPrevented) return
// turn off the temporary selection so select_selectable
// works. This is a bit of a hack.
// if (!this.parentNode.__data__.was_selected)
// d3_select(this.parentNode).classed('selected', false)
map.select_selectable(this, d, d3_selection.event.shiftKey)
// this.parentNode.__data__.was_selected = false
}
this.node_mouseover = function (d) {
d3_select(this).style('stroke-width', null)
var current = parseFloat(d3_select(this).style('stroke-width'))
if (!d3_select(this.parentNode).classed('selected'))
d3_select(this).style('stroke-width', current * 3 + 'px')
}
this.node_mouseout = function (d) {
d3_select(this).style('stroke-width', null)
}
} else {
this.selectable_mousedown = null
this.selectable_click = null
this.node_mouseover = null
this.node_mouseout = null
this.map.sel.select('#nodes')
.selectAll('.node-circle').style('stroke-width', null)
}
}
/**
* With no argument, toggle the text edit on mousedown on/off. Pass in a boolean
* argument to set the on/off state. The backup state is equal to
* selectable_mousedown.
*/
function toggle_text_label_edit (on_off) {
if (on_off === undefined) {
on_off = this.text_edit_mousedown == null
}
if (on_off) {
var map = this.map
var selection = this.selection
this.text_label_mousedown = function () {
if (d3_selection.event.defaultPrevented) {
return // mousedown suppressed
}
// run the callback
var coords_a = utils.d3_transform_catch(d3_select(this).attr('transform'))
.translate
var coords = { x: coords_a[0], y: coords_a[1] }
map.callback_manager.run('edit_text_label', null, d3_select(this), coords)
d3_selection.event.stopPropagation()
}
this.text_label_click = null
this.map.sel.select('#text-labels')
.selectAll('.label')
.classed('edit-text-cursor', true)
// add the new-label listener
this.map.sel.on('mousedown.new_text_label', function (node) {
// silence other listeners
d3_selection.event.preventDefault()
var coords = {
x: d3_mouse(node)[0],
y: d3_mouse(node)[1],
}
this.map.callback_manager.run('new_text_label', null, coords)
}.bind(this, this.map.sel.node()))
} else {
this.text_label_mousedown = this.selectable_mousedown
this.text_label_click = this.selectable_click
this.map.sel.select('#text-labels')
.selectAll('.label')
.classed('edit-text-cursor', false)
// remove the new-label listener
this.map.sel.on('mousedown.new_text_label', null)
this.map.callback_manager.run('hide_text_label_editor')
}
}
/**
* With no argument, toggle the node drag & bezier drag on or off. Pass in a
* boolean argument to set the on/off state.
*/
function toggle_selectable_drag (on_off) {
if (on_off === undefined) {
on_off = this.selectable_drag === this.empty_behavior
}
if (on_off) {
this.selectable_drag = this._get_selectable_drag(this.map, this.undo_stack)
this.bezier_drag = this._get_bezier_drag(this.map, this.undo_stack)
} else {
this.selectable_drag = this.empty_behavior
this.bezier_drag = this.empty_behavior
}
}
/**
* With no argument, toggle the label drag on or off. Pass in a boolean argument
* to set the on/off state.
* @param {Boolean} on_off - The new on/off state.
*/
function toggle_label_drag (on_off) {
if (on_off === undefined) {
on_off = this.label_drag === this.empty_behavior
}
if (on_off) {
this.reaction_label_drag = this._get_reaction_label_drag(this.map)
this.node_label_drag = this._get_node_label_drag(this.map)
} else {
this.reaction_label_drag = this.empty_behavior
this.node_label_drag = this.empty_behavior
}
}
/**
* With no argument, toggle the tooltips on mouseover labels.
* @param {Boolean} on_off - The new on/off state.
*/
function toggle_label_mouseover (on_off) {
if (on_off === undefined) {
on_off = this.label_mouseover === null
}
if (on_off) {
// Show/hide tooltip.
// @param {String} type - 'reaction_label' or 'node_label'
// @param {Object} d - D3 data for DOM element
this.label_mouseover = function (type, d) {
if (!this.dragging) {
this.map.callback_manager.run('show_tooltip', null, type, d)
}
}.bind(this)
this.label_mouseout = function () {
this.map.callback_manager.run('delay_hide_tooltip')
}.bind(this)
} else {
this.label_mouseover = null
}
}
/**
* With no argument, toggle the bezier drag on or off. Pass in a boolean
* argument to set the on/off state.
*/
function toggle_bezier_drag (on_off) {
if (on_off === undefined) {
on_off = this.bezier_drag === this.empty_behavior
}
if (on_off) {
this.bezier_drag = this._get_bezier_drag(this.map)
this.bezier_mouseover = function (d) {
d3_select(this).style('stroke-width', String(3)+'px')
}
this.bezier_mouseout = function (d) {
d3_select(this).style('stroke-width', String(1)+'px')
}
} else {
this.bezier_drag = this.empty_behavior
this.bezier_mouseover = null
this.bezier_mouseout = null
}
}
function turn_off_drag (sel) {
sel.on('mousedown.drag', null)
sel.on('touchstart.drag', null)
}
/**
* Drag the selected nodes and text labels.
* @param {} map -
* @param {} undo_stack -
*/
function _get_selectable_drag (map, undo_stack) {
// define some variables
var behavior = d3_drag()
var the_timeout = null
var total_displacement = null
// for nodes
var node_ids_to_drag = null
var reaction_ids = null
// for text labels
var text_label_ids_to_drag = null
var move_label = function (text_label_id, displacement) {
var text_label = map.text_labels[text_label_id]
text_label.x = text_label.x + displacement.x
text_label.y = text_label.y + displacement.y
}
var set_dragging = function (on_off) {
this.dragging = on_off
}.bind(this)
behavior.on('start', function (d) {
set_dragging(true)
// silence other listeners (e.g. nodes BELOW this one)
d3_selection.event.sourceEvent.stopPropagation()
// remember the total displacement for later
total_displacement = { x: 0, y: 0 }
// If a text label is selected, the rest is not necessary
if (d3_select(this).attr('class').indexOf('label') === -1) {
// Note that drag start is called even for a click event
var data = this.parentNode.__data__,
bigg_id = data.bigg_id,
node_group = this.parentNode
// Move element to back (for the next step to work). Wait 200ms
// before making the move, becuase otherwise the element will be
// deleted before the click event gets called, and selection
// will stop working.
the_timeout = setTimeout(function () {
node_group.parentNode.insertBefore(node_group,
node_group.parentNode.firstChild)
}, 200)
// prepare to combine metabolites
map.sel.selectAll('.metabolite-circle')
.on('mouseover.combine', function (d) {
if (d.bigg_id === bigg_id && d.node_id !== data.node_id) {
d3_select(this).style('stroke-width', String(12) + 'px')
.classed('node-to-combine', true)
}
})
.on('mouseout.combine', function (d) {
if (d.bigg_id === bigg_id) {
map.sel.selectAll('.node-to-combine')
.style('stroke-width', String(2) + 'px')
.classed('node-to-combine', false)
}
})
}
})
behavior.on('drag', function (d) {
// if this node is not already selected, then select this one and
// deselect all other nodes. Otherwise, leave the selection alone.
if (!d3_select(this.parentNode).classed('selected')) {
map.select_selectable(this, d)
}
// get the grabbed id
var grabbed = {}
if (d3_select(this).attr('class').indexOf('label') === -1) {
// if it is a node
grabbed['type'] = 'node'
grabbed['id'] = this.parentNode.__data__.node_id
} else {
// if it is a text label
grabbed['type'] = 'label'
grabbed['id'] = this.__data__.text_label_id
}
var selected_node_ids = map.get_selected_node_ids()
var selected_text_label_ids = map.get_selected_text_label_ids()
node_ids_to_drag = []; text_label_ids_to_drag = []
// choose the nodes and text labels to drag
if (grabbed['type']=='node' &&
selected_node_ids.indexOf(grabbed['id']) === -1) {
node_ids_to_drag.push(grabbed['id'])
} else if (grabbed['type'] === 'label' &&
selected_text_label_ids.indexOf(grabbed['id']) === -1) {
text_label_ids_to_drag.push(grabbed['id'])
} else {
node_ids_to_drag = selected_node_ids
text_label_ids_to_drag = selected_text_label_ids
}
reaction_ids = []
var displacement = {
x: d3_selection.event.dx,
y: d3_selection.event.dy,
}
total_displacement = utils.c_plus_c(total_displacement, displacement)
node_ids_to_drag.forEach(function (node_id) {
// update data
var node = map.nodes[node_id],
updated = build.move_node_and_dependents(node, node_id,
map.reactions,
map.beziers,
displacement)
reaction_ids = utils.unique_concat([ reaction_ids, updated.reaction_ids ])
// remember the displacements
// if (!(node_id in total_displacement)) total_displacement[node_id] = { x: 0, y: 0 }
// total_displacement[node_id] = utils.c_plus_c(total_displacement[node_id], displacement)
})
text_label_ids_to_drag.forEach(function (text_label_id) {
move_label(text_label_id, displacement)
// remember the displacements
// if (!(node_id in total_displacement)) total_displacement[node_id] = { x: 0, y: 0 }
// total_displacement[node_id] = utils.c_plus_c(total_displacement[node_id], displacement)
})
// draw
map.draw_these_nodes(node_ids_to_drag)
map.draw_these_reactions(reaction_ids)
map.draw_these_text_labels(text_label_ids_to_drag)
})
behavior.on('end', function () {
set_dragging(false)
if (node_ids_to_drag === null) {
// Drag end can be called when drag has not been called. In this, case, do
// nothing.
total_displacement = null
node_ids_to_drag = null
text_label_ids_to_drag = null
reaction_ids = null
the_timeout = null
return
}
// look for mets to combine
var node_to_combine_array = []
map.sel.selectAll('.node-to-combine').each(function (d) {
node_to_combine_array.push(d.node_id)
})
if (node_to_combine_array.length === 1) {
// If a node is ready for it, combine nodes
var fixed_node_id = node_to_combine_array[0],
dragged_node_id = this.parentNode.__data__.node_id,
saved_dragged_node = utils.clone(map.nodes[dragged_node_id]),
segment_objs_moved_to_combine = combine_nodes_and_draw(fixed_node_id,
dragged_node_id)
undo_stack.push(function () {
// undo
// put the old node back
map.nodes[dragged_node_id] = saved_dragged_node
var fixed_node = map.nodes[fixed_node_id],
updated_reactions = []
segment_objs_moved_to_combine.forEach(function (segment_obj) {
var segment = map.reactions[segment_obj.reaction_id].segments[segment_obj.segment_id]
if (segment.from_node_id==fixed_node_id) {
segment.from_node_id = dragged_node_id
} else if (segment.to_node_id==fixed_node_id) {
segment.to_node_id = dragged_node_id
} else {
console.error('Segment does not connect to fixed node')
}
// removed this segment_obj from the fixed node
fixed_node.connected_segments = fixed_node.connected_segments.filter(function (x) {
return !(x.reaction_id==segment_obj.reaction_id && x.segment_id==segment_obj.segment_id)
})
if (updated_reactions.indexOf(segment_obj.reaction_id)==-1)
updated_reactions.push(segment_obj.reaction_id)
})
map.draw_these_nodes([dragged_node_id])
map.draw_these_reactions(updated_reactions)
}, function () {
// redo
combine_nodes_and_draw(fixed_node_id, dragged_node_id)
})
} else {
// otherwise, drag node
// add to undo/redo stack
// remember the displacement, dragged nodes, and reactions
var saved_displacement = utils.clone(total_displacement),
// BUG TODO this variable disappears!
// Happens sometimes when you drag a node, then delete it, then undo twice
saved_node_ids = utils.clone(node_ids_to_drag),
saved_text_label_ids = utils.clone(text_label_ids_to_drag),
saved_reaction_ids = utils.clone(reaction_ids)
undo_stack.push(function () {
// undo
saved_node_ids.forEach(function (node_id) {
var node = map.nodes[node_id]
build.move_node_and_dependents(node, node_id, map.reactions,
map.beziers,
utils.c_times_scalar(saved_displacement, -1))
})
saved_text_label_ids.forEach(function (text_label_id) {
move_label(text_label_id,
utils.c_times_scalar(saved_displacement, -1))
})
map.draw_these_nodes(saved_node_ids)
map.draw_these_reactions(saved_reaction_ids)
map.draw_these_text_labels(saved_text_label_ids)
}, function () {
// redo
saved_node_ids.forEach(function (node_id) {
var node = map.nodes[node_id]
build.move_node_and_dependents(node, node_id, map.reactions,
map.beziers,
saved_displacement)
})
saved_text_label_ids.forEach(function (text_label_id) {
move_label(text_label_id, saved_displacement)
})
map.draw_these_nodes(saved_node_ids)
map.draw_these_reactions(saved_reaction_ids)
map.draw_these_text_labels(saved_text_label_ids)
})
}
// stop combining metabolites
map.sel.selectAll('.metabolite-circle')
.on('mouseover.combine', null)
.on('mouseout.combine', null)
// clear the timeout
clearTimeout(the_timeout)
// clear the shared variables
total_displacement = null
node_ids_to_drag = null
text_label_ids_to_drag = null
reaction_ids = null
the_timeout = null
})
return behavior
// definitions
function combine_nodes_and_draw (fixed_node_id, dragged_node_id) {
var dragged_node = map.nodes[dragged_node_id]
var fixed_node = map.nodes[fixed_node_id]
var updated_segment_objs = []
dragged_node.connected_segments.forEach(function (segment_obj) {
// change the segments to reflect
var segment
try {
segment = map.reactions[segment_obj.reaction_id].segments[segment_obj.segment_id]
if (segment === undefined) throw new Error('undefined segment')
} catch (e) {
console.warn('Could not find connected segment ' + segment_obj.segment_id)
return
}
if (segment.from_node_id==dragged_node_id) segment.from_node_id = fixed_node_id
else if (segment.to_node_id==dragged_node_id) segment.to_node_id = fixed_node_id
else {
console.error('Segment does not connect to dragged node')
return
}
// moved segment_obj to fixed_node
fixed_node.connected_segments.push(segment_obj)
updated_segment_objs.push(utils.clone(segment_obj))
return
})
// delete the old node
map.delete_node_data([dragged_node_id])
// turn off the class
map.sel.selectAll('.node-to-combine').classed('node-to-combine', false)
// draw
map.draw_everything()
// return for undo
return updated_segment_objs
}
}
function _get_bezier_drag (map) {
var move_bezier = function (reaction_id, segment_id, bez, bezier_id,
displacement) {
var segment = map.reactions[reaction_id].segments[segment_id]
segment[bez] = utils.c_plus_c(segment[bez], displacement)
map.beziers[bezier_id].x = segment[bez].x
map.beziers[bezier_id].y = segment[bez].y
}
var start_fn = function (d) {
d.dragging = true
}
var drag_fn = function (d, displacement, total_displacement) {
// draw
move_bezier(d.reaction_id, d.segment_id, d.bezier, d.bezier_id,
displacement)
map.draw_these_reactions([d.reaction_id], false)
map.draw_these_beziers([d.bezier_id])
}
var end_fn = function (d) {
d.dragging = false
}
var undo_fn = function (d, displacement) {
move_bezier(d.reaction_id, d.segment_id, d.bezier, d.bezier_id,
utils.c_times_scalar(displacement, -1))
map.draw_these_reactions([d.reaction_id], false)
map.draw_these_beziers([d.bezier_id])
}
var redo_fn = function (d, displacement) {
move_bezier(d.reaction_id, d.segment_id, d.bezier, d.bezier_id,
displacement)
map.draw_these_reactions([d.reaction_id], false)
map.draw_these_beziers([d.bezier_id])
}
return this._get_generic_drag(start_fn, drag_fn, end_fn, undo_fn, redo_fn,
this.map.sel)
}
function _get_reaction_label_drag (map) {
var move_label = function (reaction_id, displacement) {
var reaction = map.reactions[reaction_id]
reaction.label_x = reaction.label_x + displacement.x
reaction.label_y = reaction.label_y + displacement.y
}
var start_fn = function (d) {
// hide tooltips when drag starts
map.callback_manager.run('hide_tooltip')
}
var drag_fn = function (d, displacement, total_displacement) {
// draw
move_label(d.reaction_id, displacement)
map.draw_these_reactions([ d.reaction_id ])
}
var end_fn = function (d) {
}
var undo_fn = function (d, displacement) {
move_label(d.reaction_id, utils.c_times_scalar(displacement, -1))
map.draw_these_reactions([ d.reaction_id ])
}
var redo_fn = function (d, displacement) {
move_label(d.reaction_id, displacement)
map.draw_these_reactions([ d.reaction_id ])
}
return this._get_generic_drag(start_fn, drag_fn, end_fn, undo_fn, redo_fn,
this.map.sel)
}
function _get_node_label_drag (map) {
var move_label = function (node_id, displacement) {
var node = map.nodes[node_id]
node.label_x = node.label_x + displacement.x
node.label_y = node.label_y + displacement.y
}
var start_fn = function (d) {
// hide tooltips when drag starts
map.callback_manager.run('hide_tooltip')
}
var drag_fn = function (d, displacement, total_displacement) {
// draw
move_label(d.node_id, displacement)
map.draw_these_nodes([ d.node_id ])
}
var end_fn = function (d) {
}
var undo_fn = function (d, displacement) {
move_label(d.node_id, utils.c_times_scalar(displacement, -1))
map.draw_these_nodes ([ d.node_id ])
}
var redo_fn = function (d, displacement) {
move_label(d.node_id, displacement)
map.draw_these_nodes([ d.node_id ])
}
return this._get_generic_drag(start_fn, drag_fn, end_fn, undo_fn, redo_fn,
this.map.sel)
}
/**
* Make a generic drag behavior, with undo/redo.
*
* start_fn: function (d) Called at drag start.
*
* drag_fn: function (d, displacement, total_displacement) Called during drag.
*
* end_fn
*
* undo_fn
*
* redo_fn
*
* relative_to_selection: a d3 selection that the locations are calculated
* against.
*
*/
function _get_generic_drag (start_fn, drag_fn, end_fn, undo_fn, redo_fn,
relative_to_selection) {
// define some variables
var behavior = d3_drag()
var total_displacement
var undo_stack = this.undo_stack
var rel = relative_to_selection.node()
behavior.on('start', function (d) {
this.dragging = true
// silence other listeners
d3_selection.event.sourceEvent.stopPropagation()
total_displacement = { x: 0, y: 0 }
start_fn(d)
}.bind(this))
behavior.on('drag', function (d) {
// update data
var displacement = {
x: d3_selection.event.dx,
y: d3_selection.event.dy,
}
var location = {
x: d3_mouse(rel)[0],
y: d3_mouse(rel)[1],
}
// remember the displacement
total_displacement = utils.c_plus_c(total_displacement, displacement)
drag_fn(d, displacement, total_displacement, location)
}.bind(this))
behavior.on('end', function (d) {
this.dragging = false
// add to undo/redo stack
// remember the displacement, dragged nodes, and reactions
var saved_d = utils.clone(d)
var saved_displacement = utils.clone(total_displacement) // BUG TODO this variable disappears!
var saved_location = {
x: d3_mouse(rel)[0],
y: d3_mouse(rel)[1],
}
undo_stack.push(function () {
// undo
undo_fn(saved_d, saved_displacement, saved_location)
}, function () {
// redo
redo_fn(saved_d, saved_displacement, saved_location)
})
end_fn(d)
}.bind(this))
return behavior
}
/** Make a generic drag behavior, with undo/redo. Supplies angles in place of
* displacements.
*
* start_fn: function (d) Called at drag start.
*
* drag_fn: function (d, displacement, total_displacement) Called during drag.
*
* end_fn:
*
* undo_fn:
*
* redo_fn:
*
* get_center:
*
* relative_to_selection: a d3 selection that the locations are calculated
* against.
*
*/
function _get_generic_angular_drag (start_fn, drag_fn, end_fn, undo_fn, redo_fn,
get_center, relative_to_selection) {
// define some variables
var behavior = d3_drag()
var total_angle
var undo_stack = this.undo_stack
var rel = relative_to_selection.node()
behavior.on('start', function (d) {
this.dragging = true
// silence other listeners
d3_selection.event.sourceEvent.stopPropagation()
total_angle = 0
start_fn(d)
}.bind(this))
behavior.on('drag', function (d) {
// update data
var displacement = {
x: d3_selection.event.dx,
y: d3_selection.event.dy,
}
var location = {
x: d3_mouse(rel)[0],
y: d3_mouse(rel)[1],
}
var center = get_center()
var angle = utils.angle_for_event(displacement, location, center)
// remember the displacement
total_angle = total_angle + angle
drag_fn(d, angle, total_angle, center)
}.bind(this))
behavior.on('end', function (d) {
this.dragging = false
// add to undo/redo stack
// remember the displacement, dragged nodes, and reactions
var saved_d = utils.clone(d)
var saved_angle = total_angle
var saved_center = utils.clone(get_center())
undo_stack.push(function () {
// undo
undo_fn(saved_d, saved_angle, saved_center)
}, function () {
// redo
redo_fn(saved_d, saved_angle, saved_center)
})
end_fn(d)
}.bind(this))
return behavior
}
},{"./build":26,"./utils":33,"d3-drag":46,"d3-selection":53}],2:[function(require,module,exports){
/**
* Define a brush to select elements in a map.
* @param {D3 Selection} selection - A d3 selection to place the brush in.
* @param {Boolean} is_enabled - Whether to turn the brush on.
* @param {escher.Map} map - The map where the brush will be active.
* @param {String} insert_after - A d3 selector string to choose the svg element
* that the brush will be inserted after. Often a
* canvas element (e.g. '.canvas-group').
*/
var utils = require('./utils')
var d3_brush = require('d3-brush').brush
var d3_brushSelection = require('d3-brush').brushSelection
var d3_scaleIdentity = require('d3-scale').scaleIdentity
var d3_selection = require('d3-selection')
var d3_select = require('d3-selection').select
var Brush = utils.make_class()
Brush.prototype = {
init: init,
toggle: toggle,
setup_selection_brush: setup_selection_brush,
}
module.exports = Brush
/**
* Initialize the brush.
* @param {D3 Selection} selection - The selection for the brush.
* @param {Boolean} is_enabled - Whether to enable right away.
* @param {escher.Map} map - The Escher Map object.
* @param {Node} insert_after - A node within selection to insert after.
*/
function init (selection, is_enabled, map, insert_after) {
this.brush_sel = selection.append('g').attr('id', 'brush-container')
var node = this.brush_sel.node()
var insert_before_node = selection.select(insert_after).node().nextSibling
if (node !== insert_before_node) {
node.parentNode.insertBefore(node, insert_before_node)
}
this.enabled = is_enabled
this.map = map
}
/**
* Returns a boolean for the on/off status of the brush
* @return {Boolean}
*/
function brush_is_enabled () {
return this.map.sel.select('.brush').empty()
}
/**
* Turn the brush on or off
* @param {Boolean} on_off
*/
function toggle (on_off) {
if (on_off === undefined) {
on_off = !this.enabled
}
if (on_off) {
this.setup_selection_brush()
} else {
this.brush_sel.selectAll('*').remove()
}
}
/**
* Turn off the mouse crosshair
*/
function turn_off_crosshair (sel) {
sel.selectAll('rect').attr('cursor', null)
}
function setup_selection_brush () {
var map = this.map
var selection = this.brush_sel
var selectable_selection = map.sel.selectAll('#nodes,#text-labels')
var size_and_location = map.canvas.size_and_location()
var width = size_and_location.width
var height = size_and_location.height
var x = size_and_location.x
var y = size_and_location.y
// Clear existing brush
selection.selectAll('*').remove()
// Set a flag so we know that the brush is being cleared at the end of a
// successful brush
var clearing_flag = false
var brush = d3_brush()
.extent([ [ x, y ], [ x + width, y + height ] ])
.on('start', function () {
turn_off_crosshair(selection)
// unhide secondary metabolites if they are hidden
if (map.settings.get_option('hide_secondary_metabolites')) {
map.settings.set_conditional('hide_secondary_metabolites', false)
map.draw_everything()
map.set_status('Showing secondary metabolites. You can hide them ' +
'again in Settings.', 2000)
}
})
.on('brush', function () {
var shift_key_on = d3_selection.event.sourceEvent.shiftKey
var rect = d3_brushSelection(this)
// Check for no selection (e.g. after clearing brush)
if (rect !== null) {
// When shift is pressed, ignore the currently selected nodes.
// Otherwise, brush all nodes.
var selection = (
shift_key_on ?
selectable_selection.selectAll('.node:not(.selected),.text-label:not(.selected)') :
selectable_selection.selectAll('.node,.text-label')
)
selection.classed('selected', function (d) {
var sx = d.x
var sy = d.y
return (rect[0][0] <= sx && sx < rect[1][0] &&
rect[0][1] <= sy && sy < rect[1][1])
})
}
})
.on('end', function () {
turn_off_crosshair(selection)
// Clear brush
var rect = d3_brushSelection(this)
if (rect === null) {
if (clearing_flag) {
clearing_flag = false
} else {
// Empty selection, deselect all
map.select_none()
}
} else {
// Not empty, then clear the box
clearing_flag = true
selection.call(brush.move, null)
}
})
selection
// Initialize brush
.call(brush)
// Turn off the pan grab icons
turn_off_crosshair(selection)
}
},{"./utils":33,"d3-brush":42,"d3-scale":52,"d3-selection":53}],3:[function(require,module,exports){
/** BuildInput
Arguments
---------
selection: A d3 selection for the BuildInput.
map: A Map instance.
zoom_container: A ZoomContainer instance.
settings: A Settings instance.
*/
var utils = require('./utils')
var PlacedDiv = require('./PlacedDiv')
var completely = require('./complete.ly')
var DirectionArrow = require('./DirectionArrow')
var CobraModel = require('./CobraModel')
var _ = require('underscore')
var d3_select = require('d3-selection').select
var d3_mouse = require('d3-selection').mouse
var BuildInput = utils.make_class()
BuildInput.prototype = {
init: init,
setup_map_callbacks: setup_map_callbacks,
setup_zoom_callbacks: setup_zoom_callbacks,
is_visible: is_visible,
toggle: toggle,
show_dropdown: show_dropdown,
hide_dropdown: hide_dropdown,
place_at_selected: place_at_selected,
place: place,
reload_at_selected: reload_at_selected,
reload: reload,
toggle_start_reaction_listener: toggle_start_reaction_listener,
hide_target: hide_target,
show_target: show_target
}
module.exports = BuildInput
function init (selection, map, zoom_container, settings) {
// set up container
var new_sel = selection.append('div').attr('id', 'rxn-input')
this.placed_div = PlacedDiv(new_sel, map, { x: 240, y: 0 })
this.placed_div.hide()
// set up complete.ly
var c = completely(new_sel.node(), { backgroundColor: '#eee' })
d3_select(c.input)
this.completely = c
// close button
new_sel.append('button').attr('class', 'button input-close-button')
.text("×")
.on('mousedown', function () { this.hide_dropdown() }.bind(this))
// map
this.map = map
// set up the reaction direction arrow
var default_angle = 90 // degrees
this.direction_arrow = new DirectionArrow(map.sel)
this.direction_arrow.set_rotation(default_angle)
this.setup_map_callbacks(map)
// zoom container
this.zoom_container = zoom_container
this.setup_zoom_callbacks(zoom_container)
// settings
this.settings = settings
// toggle off
this.toggle(false)
this.target_coords = null
}
function setup_map_callbacks (map) {
// input
map.callback_manager.set('select_metabolite_with_id.input', function (selected_node, coords) {
if (this.is_active) {
this.reload(selected_node, coords, false)
this.show_dropdown(coords)
}
this.hide_target()
}.bind(this))
map.callback_manager.set('select_selectable.input', function (count, selected_node, coords) {
this.hide_target()
if (count == 1 && this.is_active && coords) {
this.reload(selected_node, coords, false)
this.show_dropdown(coords)
} else {
this.toggle(false)
}
}.bind(this))
map.callback_manager.set('deselect_nodes', function() {
this.direction_arrow.hide()
this.hide_dropdown()
}.bind(this))
// svg export
map.callback_manager.set('before_svg_export', function() {
this.direction_arrow.hide()
this.hide_target()
}.bind(this))
}
function setup_zoom_callbacks(zoom_container) {
zoom_container.callback_manager.set('zoom.input', function() {
if (this.is_active) {
this.place_at_selected()
}
}.bind(this))
}
function is_visible() {
return this.placed_div.is_visible()
}
function toggle(on_off) {
if (on_off===undefined) this.is_active = !this.is_active
else this.is_active = on_off
if (this.is_active) {
this.toggle_start_reaction_listener(true)
if (_.isNull(this.target_coords))
this.reload_at_selected()
else
this.placed_div.place(this.target_coords)
this.show_dropdown()
this.map.set_status('Click on the canvas or an existing metabolite')
this.direction_arrow.show()
} else {
this.toggle_start_reaction_listener(false)
this.hide_dropdown()
this.map.set_status(null)
this.direction_arrow.hide()
}
}
function show_dropdown (coords) {
// escape key
this.clear_escape = this.map.key_manager
.add_escape_listener(function() {
this.hide_dropdown()
}.bind(this), true)
// dropdown
this.completely.input.blur()
this.completely.repaint()
this.completely.setText('')
this.completely.input.focus()
}
function hide_dropdown () {
// escape key
if (this.clear_escape) this.clear_escape()
this.clear_escape = null
// dropdown
this.placed_div.hide()
this.completely.input.blur()
this.completely.hideDropDown()
}
function place_at_selected() {
/** Place autocomplete box at the first selected node. */
// get the selected node
this.map.deselect_text_labels()
var selected_node = this.map.select_single_node()
if (selected_node==null) return
var coords = { x: selected_node.x, y: selected_node.y }
this.place(coords)
}
function place(coords) {
this.placed_div.place(coords)
this.direction_arrow.set_location(coords)
this.direction_arrow.show()
}
function reload_at_selected() {
/** Reload data for autocomplete box and redraw box at the first selected
node. */
// get the selected node
this.map.deselect_text_labels()
var selected_node = this.map.select_single_node()
if (selected_node==null) return false
var coords = { x: selected_node.x, y: selected_node.y }
// reload the reaction input
this.reload(selected_node, coords, false)
return true
}
/**
* Reload data for autocomplete box and redraw box at the new coordinates.
*/
function reload (selected_node, coords, starting_from_scratch) {
// Try finding the selected node
if (!starting_from_scratch && !selected_node) {
console.error('No selected node, and not starting from scratch')
return
}
this.place(coords)
if (this.map.cobra_model===null) {
this.completely.setText('Cannot add: No model.')
return
}
// settings
var show_names = this.settings.get_option('identifiers_on_map') === 'name'
var allow_duplicates = this.settings.get_option('allow_building_duplicate_reactions')
// Find selected
var options = [],
cobra_reactions = this.map.cobra_model.reactions,
cobra_metabolites = this.map.cobra_model.metabolites,
reactions = this.map.reactions,
has_data_on_reactions = this.map.has_data_on_reactions,
reaction_data = this.map.reaction_data,
reaction_data_styles = this.map.reaction_data_styles,
selected_m_name = (selected_node ? (show_names ? selected_node.name : selected_node.bigg_id) : ''),
bold_mets_in_str = function(str, mets) {
return str.replace(new RegExp('(^| )(' + mets.join('|') + ')($| )', 'g'),
'$1<b>$2</b>$3')
}
// for reactions
var reaction_suggestions = {}
for (var bigg_id in cobra_reactions) {
var reaction = cobra_reactions[bigg_id]
var reaction_name = reaction.name
var show_r_name = (show_names ? reaction_name : bigg_id)
// ignore drawn reactions
if ((!allow_duplicates) && already_drawn(bigg_id, reactions)) {
continue
}
// check segments for match to selected metabolite
for (var met_bigg_id in reaction.metabolites) {
// if starting with a selected metabolite, check for that id
if (starting_from_scratch || met_bigg_id == selected_node.bigg_id) {
// don't add suggestions twice
if (bigg_id in reaction_suggestions) continue
var met_name = cobra_metabolites[met_bigg_id].name
if (has_data_on_reactions) {
options.push({ reaction_data: reaction.data,
html: ('<b>' + show_r_name + '</b>' +
': ' +
reaction.data_string),
matches: [show_r_name],
id: bigg_id })
reaction_suggestions[bigg_id] = true
} else {
// get the metabolite names or IDs
var mets = {}
var show_met_names = []
var met_id
if (show_names) {
for (met_id in reaction.metabolites) {
var name = cobra_metabolites[met_id].name
mets[name] = reaction.metabolites[met_id]
show_met_names.push(name)
}
} else {
mets = utils.clone(reaction.metabolites)
for (met_id in reaction.metabolites) {
show_met_names.push(met_id)
}
}
var show_gene_names = _.flatten(reaction.genes.map(function(g_obj) {
return [ g_obj.name, g_obj.bigg_id ]
}))
// get the reaction string
var reaction_string = CobraModel.build_reaction_string(mets,
reaction.reversibility,
reaction.lower_bound,
reaction.upper_bound)
options.push({
html: ('<b>' + show_r_name + '</b>' + '\t' +
bold_mets_in_str(reaction_string, [selected_m_name])),
matches: [ show_r_name ].concat(show_met_names).concat(show_gene_names),
id: bigg_id
})
reaction_suggestions[bigg_id] = true
}
}
}
}
// Generate the array of reactions to suggest and sort it
var sort_fn
if (has_data_on_reactions) {
sort_fn = function(x, y) {
return Math.abs(y.reaction_data) - Math.abs(x.reaction_data)
}
} else {
sort_fn = function(x, y) {
return (x.html.toLowerCase() < y.html.toLowerCase() ? -1 : 1)
}
}
options = options.sort(sort_fn)
// set up the box with data
var complete = this.completely
complete.options = options
// TODO test this behavior
// if (strings_to_display.length==1) complete.setText(strings_to_display[0])
// else complete.setText("")
complete.setText('')
var direction_arrow = this.direction_arrow,
check_and_build = function(id) {
if (id !== null) {
// make sure the selected node exists, in case changes were made in the meantime
if (starting_from_scratch) {
this.map.new_reaction_from_scratch(id,
coords,
direction_arrow.get_rotation())
} else {
if (!(selected_node.node_id in this.map.nodes)) {
console.error('Selected node no longer exists')
this.hide_dropdown()
return
}
this.map.new_reaction_for_metabolite(id,
selected_node.node_id,
direction_arrow.get_rotation())
}
}
}.bind(this)
complete.onEnter = function(id) {
this.setText('')
this.onChange('')
check_and_build(id)
}
//definitions
function already_drawn (bigg_id, reactions) {
for (var drawn_id in reactions) {
if (reactions[drawn_id].bigg_id === bigg_id)
return true
}
return false
}
}
/**
* Toggle listening for a click to place a new reaction on the canvas.
*/
function toggle_start_reaction_listener (on_off) {
if (on_off === undefined) {
this.start_reaction_listener = !this.start_reaction_listener
} else if (this.start_reaction_listener === on_off) {
return
} else {
this.start_reaction_listener = on_off
}
if (this.start_reaction_listener) {
this.map.sel.on('click.start_reaction', function(node) {
// TODO fix this hack
if (this.direction_arrow.dragging) return
// reload the reaction input
var coords = { x: d3_mouse(node)[0],
y: d3_mouse(node)[1] }
// unselect metabolites
this.map.deselect_nodes()
this.map.deselect_text_labels()
// reload the reaction input
this.reload(null, coords, true)
// generate the target symbol
this.show_target(this.map, coords)
// show the dropdown
this.show_dropdown(coords)
}.bind(this, this.map.sel.node()))
this.map.sel.classed('start-reaction-cursor', true)
} else {
this.map.sel.on('click.start_reaction', null)
this.map.sel.classed('start-reaction-cursor', false)
this.hide_target()
}
}
function hide_target () {
if (this.target_coords) {
thi