UNPKG

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
(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