UNPKG

escher-vis

Version:

Escher: A Web Application for Building, Sharing, and Embedding Data-Rich Visualizations of Biological Pathways

1,459 lines (1,351 loc) 51.3 kB
/** * For documentation of this class, see docs/javascript_api.rst */ /* global $ */ var utils = require('./utils') var BuildInput = require('./BuildInput') var ZoomContainer = require('./ZoomContainer') var Map = require('./Map') var CobraModel = require('./CobraModel') var Brush = require('./Brush') var CallbackManager = require('./CallbackManager') var ui = require('./ui') var SearchBar = require('./SearchBar') var Settings = require('./Settings') var SettingsMenu = require('./SettingsMenu') var TextEditInput = require('./TextEditInput') var QuickJump = require('./QuickJump') var data_styles = require('./data_styles') var builder_embed = require('./inline').builder_embed var TooltipContainer = require('./TooltipContainer') var DefaultTooltip = require('./Tooltip').DefaultTooltip var _ = require('underscore') var d3_select = require('d3-selection').select var d3_selection = require('d3-selection').selection var d3_json = require('d3-request').json var Builder = utils.make_class() Builder.prototype = { init: init, load_map: load_map, load_model: load_model, _set_mode: _set_mode, view_mode: view_mode, build_mode: build_mode, brush_mode: brush_mode, zoom_mode: zoom_mode, rotate_mode: rotate_mode, text_mode: text_mode, _reaction_check_add_abs: _reaction_check_add_abs, set_reaction_data: set_reaction_data, set_metabolite_data: set_metabolite_data, set_gene_data: set_gene_data, _update_data: _update_data, _toggle_direction_buttons: _toggle_direction_buttons, _set_up_menu: _set_up_menu, _set_up_button_panel: _set_up_button_panel, _setup_status: _setup_status, _setup_quick_jump: _setup_quick_jump, _setup_modes: _setup_modes, _get_keys: _get_keys, _setup_confirm_before_exit: _setup_confirm_before_exit } module.exports = Builder function init (map_data, model_data, embedded_css, selection, options) { // Defaults if (!selection) { selection = d3_select('body').append('div') } else if (selection instanceof d3_selection) { // D3 V4 selection } else if ('node' in selection) { // If user passes in a selection from an different d3 version/instance, // then reselect. selection = d3_select(selection.node()) } else { // HTML Element selection = d3_select(selection) } if (!options) { options = {} } if (!embedded_css) { embedded_css = builder_embed } this.map_data = map_data this.model_data = model_data this.embedded_css = embedded_css this.selection = selection // apply this object as data for the selection this.selection.datum(this) this.selection.__builder__ = this // Remember if the user provided a custom value for reaction_styles this.has_custom_reaction_styles = Boolean(options.reaction_styles) // set defaults this.options = utils.set_options(options, { // view options menu: 'all', scroll_behavior: 'pan', use_3d_transform: !utils.check_browser('safari'), enable_editing: true, enable_keys: true, enable_search: true, fill_screen: false, zoom_to_element: null, full_screen_button: false, ignore_bootstrap: false, // map, model, and styles starting_reaction: null, never_ask_before_quit: false, unique_map_id: null, primary_metabolite_radius: 20, secondary_metabolite_radius: 10, marker_radius: 5, gene_font_size: 18, hide_secondary_metabolites: false, show_gene_reaction_rules: false, hide_all_labels: false, canvas_size_and_loc: null, // applied data // reaction reaction_data: null, reaction_styles: ['color', 'size', 'text'], reaction_compare_style: 'log2_fold', reaction_scale: [ { type: 'min', color: '#c8c8c8', size: 12 }, { type: 'median', color: '#9696ff', size: 20 }, { type: 'max', color: '#ff0000', size: 25 } ], reaction_no_data_color: '#dcdcdc', reaction_no_data_size: 8, // gene gene_data: null, and_method_in_gene_reaction_rule: 'mean', // metabolite metabolite_data: null, metabolite_styles: ['color', 'size', 'text'], metabolite_compare_style: 'log2_fold', metabolite_scale: [ { type: 'min', color: '#fffaf0', size: 20 }, { type: 'median', color: '#f1c470', size: 30 }, { type: 'max', color: '#800000', size: 40 } ], metabolite_no_data_color: '#ffffff', metabolite_no_data_size: 10, // View and build options identifiers_on_map: 'bigg_id', highlight_missing: false, allow_building_duplicate_reactions: false, cofactors: [ 'atp', 'adp', 'nad', 'nadh', 'nadp', 'nadph', 'gtp', 'gdp', 'h', 'coa', 'ump', 'h20', 'ppi' ], // Extensions tooltip_component: DefaultTooltip, enable_tooltips: true, // Callbacks first_load_callback: null, }, { primary_metabolite_radius: true, secondary_metabolite_radius: true, marker_radius: true, gene_font_size: true, reaction_no_data_size: true, metabolite_no_data_size: true, }) // Check the location if (utils.check_for_parent_tag(this.selection, 'svg')) { throw new Error('Builder cannot be placed within an svg node '+ 'becuase UI elements are html-based.') } // Initialize the settings var set_option = function (option, new_value) { this.options[option] = new_value }.bind(this) var get_option = function (option) { return this.options[option] }.bind(this) // the options that are erased when the settings menu is canceled var conditional = [ 'hide_secondary_metabolites', 'show_gene_reaction_rules', 'hide_all_labels', 'scroll_behavior', 'reaction_styles', 'reaction_compare_style', 'reaction_scale', 'reaction_no_data_color', 'reaction_no_data_size', 'and_method_in_gene_reaction_rule', 'metabolite_styles', 'metabolite_compare_style', 'metabolite_scale', 'metabolite_no_data_color', 'metabolite_no_data_size', 'identifiers_on_map', 'highlight_missing', 'allow_building_duplicate_reactions', 'enable_tooltips' ] this.settings = new Settings(set_option, get_option, conditional) // Check the scales have max and min var scales = [ 'reaction_scale', 'metabolite_scale' ] scales.forEach(function (name) { this.settings.streams[name].onValue(function (val) { var types = [ 'min', 'max' ] types.forEach(function (type) { var has = val.reduce(function (has_found, scale_el) { return has_found || (scale_el.type === type) }, false) if (!has) { val.push({ type: type, color: '#ffffff', size: 10 }) this.settings.set_conditional(name, val) } }.bind(this)) }.bind(this)) }.bind(this)) // TODO warn about repeated types in the scale // Set up this callback manager this.callback_manager = CallbackManager() if (this.options.first_load_callback !== null) { this.callback_manager.set('first_load', this.options.first_load_callback) } // Load the model, map, and update data in both this.load_model(this.model_data, false) this.load_map(this.map_data, false) var message_fn = this._reaction_check_add_abs() this._update_data(true, true) // Setting callbacks. TODO enable atomic updates. Right now, every time the // menu closes, everything is drawn. this.settings.status_bus .onValue(function(x) { if (x === 'accepted') { this._update_data(true, true, [ 'reaction', 'metabolite' ], false) if (this.zoom_container !== null) { var new_behavior = this.settings.get_option('scroll_behavior') this.zoom_container.set_scroll_behavior(new_behavior) } if (this.map !== null) { this.map.draw_all_nodes(false) this.map.draw_all_reactions(true, false) this.map.select_none() } } }.bind(this)) this.callback_manager.run('first_load', this) if (message_fn !== null) setTimeout(message_fn, 500) } /** * For documentation of this function, see docs/javascript_api.rst. */ function load_model (model_data, should_update_data) { if (_.isUndefined(should_update_data)) { should_update_data = true } // Check the cobra model if (_.isNull(model_data)) { this.cobra_model = null } else { this.cobra_model = CobraModel.from_cobra_json(model_data) } if (this.map) { this.map.cobra_model = this.cobra_model if (should_update_data) { this._update_data(true, false) } if (this.settings.get_option('highlight_missing')) { this.map.draw_all_reactions(false, false) } } this.callback_manager.run('load_model', null, model_data, should_update_data) } /** * For documentation of this function, see docs/javascript_api.rst */ function load_map (map_data, should_update_data) { if (_.isUndefined(should_update_data)) should_update_data = true // Begin with some definitions var selectable_mousedown_enabled = true var shift_key_on = false // remove the old builder utils.remove_child_nodes(this.selection) // set up the zoom container this.zoom_container = new ZoomContainer(this.selection, this.options.scroll_behavior, this.options.use_3d_transform, this.options.fill_screen) var zoomed_sel = this.zoom_container.zoomed_sel var svg = this.zoom_container.svg // remove the old map side effects if (this.map) this.map.key_manager.toggle(false) if (map_data !== null) { // import map this.map = Map.from_data(map_data, svg, this.embedded_css, zoomed_sel, this.zoom_container, this.settings, this.cobra_model, this.options.enable_search) } else { // new map this.map = new Map(svg, this.embedded_css, zoomed_sel, this.zoom_container, this.settings, this.cobra_model, this.options.canvas_size_and_loc, this.options.enable_search) } // zoom container status changes this.zoom_container.callback_manager.set('svg_start', function () { this.map.set_status('Drawing ...') }.bind(this)) this.zoom_container.callback_manager.set('svg_finish', function () { this.map.set_status('') }.bind(this)) // Set the data for the map if (should_update_data) this._update_data(false, true) // Set up the reaction input with complete.ly this.build_input = new BuildInput(this.selection, this.map, this.zoom_container, this.settings) // Set up the text edit input this.text_edit_input = new TextEditInput(this.selection, this.map, this.zoom_container) // Set up the tooltip container this.tooltip_container = new TooltipContainer(this.selection, this.map, this.options.tooltip_component, this.zoom_container) // Set up the Brush this.brush = new Brush(zoomed_sel, false, this.map, '.canvas-group') this.map.canvas.callback_manager.set('resize', function() { this.brush.toggle(true) }.bind(this)) // Set up the modes this._setup_modes(this.map, this.brush, this.zoom_container) var s = this.selection .append('div').attr('class', 'search-menu-container') .append('div').attr('class', 'search-menu-container-inline') var menu_div = s.append('div') var search_bar_div = s.append('div') var button_div = this.selection.append('div') // Set up the search bar this.search_bar = new SearchBar(search_bar_div, this.map.search_index, this.map) // Set up the hide callbacks this.search_bar.callback_manager.set('show', function() { this.settings_bar.toggle(false) }.bind(this)) // Set up the settings var settings_div = this.selection.append('div') var settings_cb = function (type, on_off) { // Temporarily set the abs type, for previewing it in the Settings menu var o = this.options[type + '_styles'] if (on_off && o.indexOf('abs') === -1) { o.push('abs') } else if (!on_off) { var i = o.indexOf('abs') if (i !== -1) { this.options[type + '_styles'] = o.slice(0, i).concat(o.slice(i + 1)) } } this._update_data(false, true, type) }.bind(this) this.settings_bar = new SettingsMenu(settings_div, this.settings, this.map, settings_cb) this.settings_bar.callback_manager.set('show', function () { this.search_bar.toggle(false) }.bind(this)) // Set up key manager var keys = this._get_keys(this.map, this.zoom_container, this.search_bar, this.settings_bar, this.options.enable_editing, this.options.full_screen_button) this.map.key_manager.assigned_keys = keys // Tell the key manager about the reaction input and search bar this.map.key_manager.input_list = [this.build_input, this.search_bar, this.settings_bar, this.text_edit_input] // Make sure the key manager remembers all those changes this.map.key_manager.update() // Turn it on/off this.map.key_manager.toggle(this.options.enable_keys) // Set up menu and status bars if (this.options.menu === 'all') { if (this.options.ignore_bootstrap) { console.error('Cannot create the dropdown menus if ignore_bootstrap = true') } else { this._set_up_menu(menu_div, this.map, this.map.key_manager, keys, this.options.enable_editing, this.options.enable_keys, this.options.full_screen_button) } } this._set_up_button_panel(button_div, keys, this.options.enable_editing, this.options.enable_keys, this.options.full_screen_button, this.options.menu, this.options.ignore_bootstrap) // Setup selection box if (this.options.zoom_to_element) { var type = this.options.zoom_to_element.type, element_id = this.options.zoom_to_element.id if (_.isUndefined(type) || [ 'reaction', 'node' ].indexOf(type) === -1) { throw new Error('zoom_to_element type must be "reaction" or "node"') } if (_.isUndefined(element_id)) { throw new Error('zoom_to_element must include id') } if (type === 'reaction') { this.map.zoom_to_reaction(element_id) } else if (type === 'node') { this.map.zoom_to_node(element_id) } } else if (map_data !== null) { this.map.zoom_extent_canvas() } else { if (this.options.starting_reaction !== null && this.cobra_model !== null) { // Draw default reaction if no map is provided var size = this.zoom_container.get_size() var start_coords = { x: size.width / 2, y: size.height / 4 } this.map.new_reaction_from_scratch(this.options.starting_reaction, start_coords, 90) this.map.zoom_extent_nodes() } else { this.map.zoom_extent_canvas() } } // Status in both modes var status = this._setup_status(this.selection, this.map) // Set up quick jump this._setup_quick_jump(this.selection) // Start in zoom mode for builder, view mode for viewer if (this.options.enable_editing) { this.zoom_mode() } else { this.view_mode() } // confirm before leaving the page if (this.options.enable_editing) { this._setup_confirm_before_exit() } // draw this.map.draw_everything() } function _set_mode (mode) { this.search_bar.toggle(false) // input this.build_input.toggle(mode == 'build') this.build_input.direction_arrow.toggle(mode == 'build') if (this.options.menu == 'all' && this.options.enable_editing) { this._toggle_direction_buttons(mode == 'build') } // brush this.brush.toggle(mode == 'brush') // zoom this.zoom_container.toggle_pan_drag(mode == 'zoom' || mode == 'view') // resize canvas this.map.canvas.toggle_resize(mode == 'zoom' || mode == 'brush') // Behavior. Be careful of the order becuase rotation and // toggle_selectable_drag both use Behavior.selectable_drag. if (mode == 'rotate') { this.map.behavior.toggle_selectable_drag(false) // before toggle_rotation_mode this.map.behavior.toggle_rotation_mode(true) } else { this.map.behavior.toggle_rotation_mode(mode == 'rotate') // before toggle_selectable_drag this.map.behavior.toggle_selectable_drag(mode == 'brush') } this.map.behavior.toggle_selectable_click(mode == 'build' || mode == 'brush') this.map.behavior.toggle_label_drag(mode == 'brush') this.map.behavior.toggle_label_mouseover(true) this.map.behavior.toggle_text_label_edit(mode == 'text') this.map.behavior.toggle_bezier_drag(mode == 'brush') // edit selections if (mode == 'view' || mode == 'text') this.map.select_none() if (mode == 'rotate') this.map.deselect_text_labels() this.map.draw_everything() } function view_mode() { /** For documentation of this function, see docs/javascript_api.rst. */ this.callback_manager.run('view_mode') this._set_mode('view') } function build_mode() { /** For documentation of this function, see docs/javascript_api.rst. */ this.callback_manager.run('build_mode') this._set_mode('build') } function brush_mode() { /** For documentation of this function, see docs/javascript_api.rst. */ this.callback_manager.run('brush_mode') this._set_mode('brush') } function zoom_mode() { /** For documentation of this function, see docs/javascript_api.rst. */ this.callback_manager.run('zoom_mode') this._set_mode('zoom') } function rotate_mode() { /** For documentation of this function, see docs/javascript_api.rst. */ this.callback_manager.run('rotate_mode') this._set_mode('rotate') } function text_mode() { /** For documentation of this function, see docs/javascript_api.rst. */ this.callback_manager.run('text_mode') this._set_mode('text') } function _reaction_check_add_abs () { var curr_style = this.options.reaction_styles var did_abs = false if (this.options.reaction_data !== null && !this.has_custom_reaction_styles && !_.contains(curr_style, 'abs')) { this.settings.set_conditional('reaction_styles', curr_style.concat('abs')) return function () { this.map.set_status('Visualizing absolute value of reaction data. ' + 'Change this option in Settings.', 5000) }.bind(this) } return null } /** * For documentation of this function, see docs/javascript_api.rst. */ function set_reaction_data (data) { this.options.reaction_data = data var message_fn = this._reaction_check_add_abs() this._update_data(true, true, 'reaction') if (message_fn) { message_fn() } else { this.map.set_status('') } } /** * For documentation of this function, see docs/javascript_api.rst. */ function set_gene_data (data, clear_gene_reaction_rules) { if (clear_gene_reaction_rules) { // default undefined this.settings.set_conditional('show_gene_reaction_rules', false) } this.options.gene_data = data this._update_data(true, true, 'reaction') this.map.set_status('') } function set_metabolite_data(data) { /** For documentation of this function, see docs/javascript_api.rst. */ this.options.metabolite_data = data this._update_data(true, true, 'metabolite') this.map.set_status('') } /** * Set data and settings for the model. * update_model: (Boolean) Update data for the model. * update_map: (Boolean) Update data for the map. * kind: (Optional, Default: all) An array defining which data is being updated * that can include any of: ['reaction', 'metabolite']. * should_draw: (Optional, Default: true) Whether to redraw the update sections * of the map. */ function _update_data (update_model, update_map, kind, should_draw) { // defaults if (kind === undefined) { kind = [ 'reaction', 'metabolite' ] } if (should_draw === undefined) { should_draw = true } var update_metabolite_data = (kind.indexOf('metabolite') !== -1) var update_reaction_data = (kind.indexOf('reaction') !== -1) var met_data_object var reaction_data_object var gene_data_object // ------------------- // First map, and draw // ------------------- // metabolite data if (update_metabolite_data && update_map && this.map !== null) { met_data_object = data_styles.import_and_check(this.options.metabolite_data, 'metabolite_data') this.map.apply_metabolite_data_to_map(met_data_object) if (should_draw) { this.map.draw_all_nodes(false) } } // reaction data if (update_reaction_data) { if (this.options.reaction_data !== null && update_map && this.map !== null) { reaction_data_object = data_styles.import_and_check(this.options.reaction_data, 'reaction_data') this.map.apply_reaction_data_to_map(reaction_data_object) if (should_draw) this.map.draw_all_reactions(false, false) } else if (this.options.gene_data !== null && update_map && this.map !== null) { gene_data_object = make_gene_data_object(this.options.gene_data, this.cobra_model, this.map) this.map.apply_gene_data_to_map(gene_data_object) if (should_draw) this.map.draw_all_reactions(false, false) } else if (update_map && this.map !== null) { // clear the data this.map.apply_reaction_data_to_map(null) if (should_draw) this.map.draw_all_reactions(false, false) } } // ---------------------------------------------------------------- // Then the model, after drawing. Delay by 5ms so the the map draws // first. // ---------------------------------------------------------------- // If this function runs again, cancel the previous model update if (this.update_model_timer) { clearTimeout(this.update_model_timer) } var delay = 5 this.update_model_timer = setTimeout(function () { // metabolite_data if (update_metabolite_data && update_model && this.cobra_model !== null) { // if we haven't already made this if (!met_data_object) { met_data_object = data_styles.import_and_check(this.options.metabolite_data, 'metabolite_data') } this.cobra_model.apply_metabolite_data(met_data_object, this.options.metabolite_styles, this.options.metabolite_compare_style) } // reaction data if (update_reaction_data) { if (this.options.reaction_data !== null && update_model && this.cobra_model !== null) { // if we haven't already made this if (!reaction_data_object) { reaction_data_object = data_styles.import_and_check(this.options.reaction_data, 'reaction_data') } this.cobra_model.apply_reaction_data(reaction_data_object, this.options.reaction_styles, this.options.reaction_compare_style) } else if (this.options.gene_data !== null && update_model && this.cobra_model !== null) { if (!gene_data_object) { gene_data_object = make_gene_data_object(this.options.gene_data, this.cobra_model, this.map) } this.cobra_model.apply_gene_data(gene_data_object, this.options.reaction_styles, this.options.identifiers_on_map, this.options.reaction_compare_style, this.options.and_method_in_gene_reaction_rule) } else if (update_model && this.cobra_model !== null) { // clear the data this.cobra_model.apply_reaction_data(null, this.options.reaction_styles, this.options.reaction_compare_style) } } // callback this.callback_manager.run('update_data', null, update_model, update_map, kind, should_draw) }.bind(this), delay) // definitions function make_gene_data_object(gene_data, cobra_model, map) { var all_reactions = {} if (cobra_model !== null) utils.extend(all_reactions, cobra_model.reactions) // extend, overwrite if (map !== null) utils.extend(all_reactions, map.reactions, true) // this object has reaction keys and values containing associated genes return data_styles.import_and_check(gene_data, 'gene_data', all_reactions) } } function _set_up_menu (menu_selection, map, key_manager, keys, enable_editing, enable_keys, full_screen_button, ignore_bootstrap) { var menu = menu_selection.attr('id', 'menu') .append('ul') .attr('class', 'nav nav-pills') // map dropdown ui.dropdown_menu(menu, 'Map') .button({ key: keys.save, text: 'Save map JSON', key_text: (enable_keys ? ' (Ctrl+S)' : null) }) .button({ text: 'Load map JSON', key_text: (enable_keys ? ' (Ctrl+O)' : null), input: { assign: key_manager.assigned_keys.load, key: 'fn', fn: load_map_for_file.bind(this), pre_fn: function() { map.set_status('Loading map ...') }, failure_fn: function() { map.set_status('') }} }) .button({ key: keys.save_svg, text: 'Export as SVG', key_text: (enable_keys ? ' (Ctrl+Shift+S)' : null) }) .button({ key: keys.save_png, text: 'Export as PNG', key_text: (enable_keys ? ' (Ctrl+Shift+P)' : null) }) .button({ key: keys.clear_map, text: 'Clear map' }) // model dropdown var model_menu = ui.dropdown_menu(menu, 'Model') .button({ text: 'Load COBRA model JSON', key_text: (enable_keys ? ' (Ctrl+M)' : null), input: { assign: key_manager.assigned_keys.load_model, key: 'fn', fn: load_model_for_file.bind(this), pre_fn: function() { map.set_status('Loading model ...') }, failure_fn: function() { map.set_status('') } } }) .button({ id: 'convert_map', key: keys.convert_map, text: 'Update names and gene reaction rules using model' }) .button({ id: 'clear_model', key: keys.clear_model, text: 'Clear model' }) // disable the clear and convert buttons var disable_model_clear_convert = function() { model_menu.dropdown.selectAll('li') .classed('escher-disabled', function(d) { if ((d.id == 'clear_model' || d.id == 'convert_map') && this.cobra_model === null) return true return null }.bind(this)) }.bind(this) disable_model_clear_convert() this.callback_manager.set('load_model', disable_model_clear_convert) // data dropdown var data_menu = ui.dropdown_menu(menu, 'Data') .button({ input: { assign: key_manager.assigned_keys.load_reaction_data, key: 'fn', fn: load_reaction_data_for_file.bind(this), accept_csv: true, pre_fn: function() { map.set_status('Loading reaction data ...') }, failure_fn: function() { map.set_status('') }}, text: 'Load reaction data' }) .button({ key: keys.clear_reaction_data, text: 'Clear reaction data' }) .divider() .button({ input: { fn: load_gene_data_for_file.bind(this), accept_csv: true, pre_fn: function() { map.set_status('Loading gene data ...') }, failure_fn: function() { map.set_status('') }}, text: 'Load gene data' }) .button({ key: keys.clear_gene_data, text: 'Clear gene data' }) .divider() .button({ input: { fn: load_metabolite_data_for_file.bind(this), accept_csv: true, pre_fn: function() { map.set_status('Loading metabolite data ...') }, failure_fn: function() { map.set_status('') }}, text: 'Load metabolite data' }) .button({ key: keys.clear_metabolite_data, text: 'Clear metabolite data' }) // update the buttons var disable_clears = function() { data_menu.dropdown.selectAll('li') .classed('escher-disabled', function(d) { if (!d) return null if (d.text == 'Clear reaction data' && this.options.reaction_data === null) return true if (d.text == 'Clear gene data' && this.options.gene_data === null) return true if (d.text == 'Clear metabolite data' && this.options.metabolite_data === null) return true return null }.bind(this)) }.bind(this) disable_clears() this.callback_manager.set('update_data', disable_clears) // edit dropdown var edit_menu = ui.dropdown_menu(menu, 'Edit', true) if (enable_editing) { edit_menu .button({ key: keys.zoom_mode, id: 'zoom-mode-menu-button', text: 'Pan mode', key_text: (enable_keys ? ' (Z)' : null) }) .button({ key: keys.brush_mode, id: 'brush-mode-menu-button', text: 'Select mode', key_text: (enable_keys ? ' (V)' : null) }) .button({ key: keys.build_mode, id: 'build-mode-menu-button', text: 'Add reaction mode', key_text: (enable_keys ? ' (N)' : null) }) .button({ key: keys.rotate_mode, id: 'rotate-mode-menu-button', text: 'Rotate mode', key_text: (enable_keys ? ' (R)' : null) }) .button({ key: keys.text_mode, id: 'text-mode-menu-button', text: 'Text mode', key_text: (enable_keys ? ' (T)' : null) }) .divider() .button({ key: keys.delete, text: 'Delete', key_text: (enable_keys ? ' (Del)' : null) }) .button({ key: keys.undo, text: 'Undo', key_text: (enable_keys ? ' (Ctrl+Z)' : null) }) .button({ key: keys.redo, text: 'Redo', key_text: (enable_keys ? ' (Ctrl+Shift+Z)' : null) }) .button({ key: keys.toggle_primary, text: 'Toggle primary/secondary', key_text: (enable_keys ? ' (P)' : null) }) .button({ key: keys.cycle_primary, text: 'Rotate reactant locations', key_text: (enable_keys ? ' (C)' : null) }) .button({ key: keys.select_all, text: 'Select all', key_text: (enable_keys ? ' (Ctrl+A)' : null) }) .button({ key: keys.select_none, text: 'Select none', key_text: (enable_keys ? ' (Ctrl+Shift+A)' : null) }) .button({ key: keys.invert_selection, text: 'Invert selection' }) } else { edit_menu.button({ key: keys.view_mode, id: 'view-mode-menu-button', text: 'View mode' }) } // view dropdown var view_menu = ui.dropdown_menu(menu, 'View', true) .button({ key: keys.zoom_in, text: 'Zoom in', key_text: (enable_keys ? ' (+)' : null) }) .button({ key: keys.zoom_out, text: 'Zoom out', key_text: (enable_keys ? ' (-)' : null) }) .button({ key: keys.extent_nodes, text: 'Zoom to nodes', key_text: (enable_keys ? ' (0)' : null) }) .button({ key: keys.extent_canvas, text: 'Zoom to canvas', key_text: (enable_keys ? ' (1)' : null) }) .button({ key: keys.search, text: 'Find', key_text: (enable_keys ? ' (F)' : null) }) if (full_screen_button) { view_menu.button({ key: keys.full_screen, text: 'Full screen', key_text: (enable_keys ? ' (2)' : null) }) } if (enable_editing) { view_menu.button({ key: keys.toggle_beziers, id: 'bezier-button', text: 'Show control points', key_text: (enable_keys ? ' (B)' : null) }) map.callback_manager .set('toggle_beziers.button', function(on_off) { menu.select('#bezier-button').select('.dropdown-button-text') .text((on_off ? 'Hide' : 'Show') + ' control points' + (enable_keys ? ' (B)' : '')) }) } view_menu.divider() .button({ key: keys.show_settings, text: 'Settings', key_text: (enable_keys ? ' (,)' : null) }) // help menu.append('a') .attr('class', 'help-button') .attr('target', '#') .attr('href', 'https://escher.readthedocs.org') .text('?') // set up mode callbacks var select_button = function (id) { // toggle the button $(this.selection.node()).find('#' + id).button('toggle') // menu buttons var ids = [ 'zoom-mode-menu-button', 'brush-mode-menu-button', 'build-mode-menu-button', 'rotate-mode-menu-button', 'view-mode-menu-button', 'text-mode-menu-button', ] ids.forEach(function(this_id) { var b_id = this_id.replace('-menu', '') this.selection.select('#' + this_id) .select('span') .classed('glyphicon', b_id == id) .classed('glyphicon-ok', b_id == id) }.bind(this)) } this.callback_manager.set('zoom_mode', select_button.bind(this, 'zoom-mode-button')) this.callback_manager.set('brush_mode', select_button.bind(this, 'brush-mode-button')) this.callback_manager.set('build_mode', select_button.bind(this, 'build-mode-button')) this.callback_manager.set('rotate_mode', select_button.bind(this, 'rotate-mode-button')) this.callback_manager.set('view_mode', select_button.bind(this, 'view-mode-button')) this.callback_manager.set('text_mode', select_button.bind(this, 'text-mode-button')) // definitions function load_map_for_file(error, map_data) { /** Load a map. This reloads the whole builder. */ if (error) { console.warn(error) this.map.set_status('Error loading map: ' + error, 2000) return } try { check_map(map_data) this.load_map(map_data) this.map.set_status('Loaded map ' + map_data[0].map_name, 3000) } catch (e) { console.warn(e) this.map.set_status('Error loading map: ' + e, 2000) } // definitions function check_map(data) { /** Perform a quick check to make sure the map is mostly valid. */ if (!('map_id' in data[0] && 'reactions' in data[1] && 'nodes' in data[1] && 'canvas' in data[1])) throw new Error('Bad map data.') } } function load_model_for_file(error, data) { /** Load a cobra model. Redraws the whole map if the highlight_missing option is true. */ if (error) { console.warn(error) this.map.set_status('Error loading model: ' + error, 2000) return } try { this.load_model(data, true) this.build_input.toggle(false) if ('id' in data) this.map.set_status('Loaded model ' + data.id, 3000) else this.map.set_status('Loaded model (no model id)', 3000) } catch (e) { console.warn(e) this.map.set_status('Error loading model: ' + e, 2000) } } function load_reaction_data_for_file(error, data) { if (error) { console.warn(error) this.map.set_status('Could not parse file as JSON or CSV', 2000) return } // turn off gene data if (data !== null) this.set_gene_data(null) this.set_reaction_data(data) } function load_metabolite_data_for_file(error, data) { if (error) { console.warn(error) this.map.set_status('Could not parse file as JSON or CSV', 2000) return } this.set_metabolite_data(data) } function load_gene_data_for_file(error, data) { if (error) { console.warn(error) this.map.set_status('Could not parse file as JSON or CSV', 2000) return } // turn off reaction data if (data !== null) this.set_reaction_data(null) // turn on gene_reaction_rules this.settings.set_conditional('show_gene_reaction_rules', true) this.set_gene_data(data) } } function _set_up_button_panel(button_selection, keys, enable_editing, enable_keys, full_screen_button, menu_option, ignore_bootstrap) { var button_panel = button_selection.append('ul') .attr('class', 'nav nav-pills nav-stacked') .attr('id', 'button-panel') // buttons ui.individual_button(button_panel.append('li'), { key: keys.zoom_in, text: '+', icon: 'glyphicon glyphicon-plus-sign', tooltip: 'Zoom in', key_text: (enable_keys ? ' (+)' : null), ignore_bootstrap: ignore_bootstrap }) ui.individual_button(button_panel.append('li'), { key: keys.zoom_out, text: '–', icon: 'glyphicon glyphicon-minus-sign', tooltip: 'Zoom out', key_text: (enable_keys ? ' (-)' : null), ignore_bootstrap: ignore_bootstrap }) ui.individual_button(button_panel.append('li'), { key: keys.extent_canvas, text: '↔', icon: 'glyphicon glyphicon-resize-full', tooltip: 'Zoom to canvas', key_text: (enable_keys ? ' (1)' : null), ignore_bootstrap: ignore_bootstrap }) if (full_screen_button) { ui.individual_button(button_panel.append('li'), { key: keys.full_screen, text: '▣', icon: 'glyphicon glyphicon-fullscreen', tooltip: 'Full screen', key_text: (enable_keys ? ' (2)' : null), ignore_bootstrap: ignore_bootstrap }) } // mode buttons if (enable_editing && menu_option === 'all') { ui.radio_button_group(button_panel.append('li')) .button({ key: keys.zoom_mode, id: 'zoom-mode-button', text: 'Z', icon: 'glyphicon glyphicon-move', tooltip: 'Pan mode', key_text: (enable_keys ? ' (Z)' : null), ignore_bootstrap: ignore_bootstrap }) .button({ key: keys.brush_mode, text: 'V', id: 'brush-mode-button', icon: 'glyphicon glyphicon-hand-up', tooltip: 'Select mode', key_text: (enable_keys ? ' (V)' : null), ignore_bootstrap: ignore_bootstrap }) .button({ key: keys.build_mode, text: 'N', id: 'build-mode-button', icon: 'glyphicon glyphicon-plus', tooltip: 'Add reaction mode', key_text: (enable_keys ? ' (N)' : null), ignore_bootstrap: ignore_bootstrap }) .button({ key: keys.rotate_mode, text: 'R', id: 'rotate-mode-button', icon: 'glyphicon glyphicon-repeat', tooltip: 'Rotate mode', key_text: (enable_keys ? ' (R)' : null), ignore_bootstrap: ignore_bootstrap }) .button({ key: keys.text_mode, text: 'T', id: 'text-mode-button', icon: 'glyphicon glyphicon-font', tooltip: 'Text mode', key_text: (enable_keys ? ' (T)' : null), ignore_bootstrap: ignore_bootstrap }) // arrow buttons this.direction_buttons = button_panel.append('li') var o = ui.button_group(this.direction_buttons) .button({ key: keys.direction_arrow_left, text: '←', icon: 'glyphicon glyphicon-arrow-left', tooltip: 'Direction arrow (←)', ignore_bootstrap: ignore_bootstrap }) .button({ key: keys.direction_arrow_right, text: '→', icon: 'glyphicon glyphicon-arrow-right', tooltip: 'Direction arrow (→)', ignore_bootstrap: ignore_bootstrap }) .button({ key: keys.direction_arrow_up, text: '↑', icon: 'glyphicon glyphicon-arrow-up', tooltip: 'Direction arrow (↑)', ignore_bootstrap: ignore_bootstrap }) .button({ key: keys.direction_arrow_down, text: '↓', icon: 'glyphicon glyphicon-arrow-down', tooltip: 'Direction arrow (↓)', ignore_bootstrap: ignore_bootstrap }) } } function _toggle_direction_buttons(on_off) { if (_.isUndefined(on_off)) on_off = !this.direction_buttons.style('display') === 'block' this.direction_buttons.style('display', on_off ? 'block' : 'none') } function _setup_status (selection, map) { var status_bar = selection.append('div').attr('id', 'status') map.callback_manager.set('set_status', function (status) { status_bar.html(status) }) return status_bar } function _setup_quick_jump (selection) { // function to load a map var load_fn = function (new_map_name, quick_jump_path, callback) { if (this.options.enable_editing && !this.options.never_ask_before_quit) { if (!(confirm(('You will lose any unsaved changes.\n\n' + 'Are you sure you want to switch maps?')))) { if (callback) callback(false) return } } this.map.set_status('Loading map ' + new_map_name + ' ...') var url = utils.name_to_url(new_map_name, quick_jump_path) d3_json(url, function (error, data) { if (error) { console.warn('Could not load data: ' + error) this.map.set_status('Could not load map', 2000) if (callback) callback(false) return } // run callback before load_map so the new map has the correct // quick_jump menu if (callback) callback(true) // now reload this.load_map(data) this.map.set_status('') }.bind(this)) }.bind(this) // make the quick jump object this.quick_jump = QuickJump(selection, load_fn) } function _setup_modes (map, brush, zoom_container) { // set up zoom+pan and brush modes var was_enabled = {} map.callback_manager.set('start_rotation', function () { was_enabled.brush = brush.enabled brush.toggle(false) was_enabled.zoom = zoom_container.zoom_on zoom_container.toggle_pan_drag(false) was_enabled.selectable_mousedown = map.behavior.selectable_mousedown !== null map.behavior.toggle_selectable_click(false) was_enabled.label_mouseover = map.behavior.label_mouseover !== null map.behavior.toggle_label_mouseover(false) }) map.callback_manager.set('end_rotation', function () { brush.toggle(was_enabled.brush) zoom_container.toggle_pan_drag(was_enabled.zoom) map.behavior.toggle_selectable_click(was_enabled.selectable_mousedown) map.behavior.toggle_label_mouseover(was_enabled.label_mouseover) was_enabled = {} }) } /** * Define keyboard shortcuts */ function _get_keys (map, zoom_container, search_bar, settings_bar, enable_editing, full_screen_button) { var keys = { save: { key: 'ctrl+s', target: map, fn: map.save, }, save_svg: { key: 'ctrl+shift+s', target: map, fn: map.save_svg, }, save_png: { key: 'ctrl+shift+p', target: map, fn: map.save_png, }, load: { key: 'ctrl+o', fn: null, // defined by button }, convert_map: { target: map, fn: map.convert_map, }, clear_map: { target: map, fn: map.clear_map, }, load_model: { key: 'ctrl+m', fn: null, // defined by button }, clear_model: { fn: this.load_model.bind(this, null, true), }, load_reaction_data: { fn: null }, // defined by button clear_reaction_data: { target: this, fn: function () { this.set_reaction_data(null) }, }, load_metabolite_data: { fn: null }, // defined by button clear_metabolite_data: { target: this, fn: function () { this.set_metabolite_data(null) }, }, load_gene_data: { fn: null }, // defined by button clear_gene_data: { target: this, fn: function () { this.set_gene_data(null, true) }, }, zoom_in_ctrl: { key: 'ctrl+=', target: zoom_container, fn: zoom_container.zoom_in, }, zoom_in: { key: '=', target: zoom_container, fn: zoom_container.zoom_in, ignore_with_input: true, }, zoom_out_ctrl: { key: 'ctrl+-', target: zoom_container, fn: zoom_container.zoom_out, }, zoom_out: { key: '-', target: zoom_container, fn: zoom_container.zoom_out, ignore_with_input: true, }, extent_nodes_ctrl: { key: 'ctrl+0', target: map, fn: map.zoom_extent_nodes, }, extent_nodes: { key: '0', target: map, fn: map.zoom_extent_nodes, ignore_with_input: true, }, extent_canvas_ctrl: { key: 'ctrl+1', target: map, fn: map.zoom_extent_canvas, }, extent_canvas: { key: '1', target: map, fn: map.zoom_extent_canvas, ignore_with_input: true, }, search_ctrl: { key: 'ctrl+f', fn: search_bar.toggle.bind(search_bar, true), }, search: { key: 'f', fn: search_bar.toggle.bind(search_bar, true), ignore_with_input: true, }, view_mode: { target: this, fn: this.view_mode, ignore_with_input: true, }, show_settings_ctrl: { key: 'ctrl+,', target: settings_bar, fn: settings_bar.toggle, }, show_settings: { key: ',', target: settings_bar, fn: settings_bar.toggle, ignore_with_input: true, }, } if (full_screen_button) { utils.extend(keys, { full_screen_ctrl: { key: 'ctrl+2', target: map, fn: map.full_screen, }, full_screen: { key: '2', target: map, fn: map.full_screen, ignore_with_input: true, }, }) } if (enable_editing) { utils.extend(keys, { build_mode: { key: 'n', target: this, fn: this.build_mode, ignore_with_input: true, }, zoom_mode: { key: 'z', target: this, fn: this.zoom_mode, ignore_with_input: true, }, brush_mode: { key: 'v', target: this, fn: this.brush_mode, ignore_with_input: true, }, rotate_mode: { key: 'r', target: this, fn: this.rotate_mode, ignore_with_input: true, }, text_mode: { key: 't', target: this, fn: this.text_mode, ignore_with_input: true, }, toggle_beziers: { key: 'b', target: map, fn: map.toggle_beziers, ignore_with_input: true, }, delete_ctrl: { key: 'ctrl+backspace', target: map, fn: map.delete_selected, ignore_with_input: true, }, delete: { key: 'backspace', target: map, fn: map.delete_selected, ignore_with_input: true, }, delete_del: { key: 'del', target: map, fn: map.delete_selected, ignore_with_input: true, }, toggle_primary: { key: 'p', target: map, fn: map.toggle_selected_node_primary, ignore_with_input: true, }, cycle_primary: { key: 'c', target: map, fn: map.cycle_primary_node, ignore_with_input: true, }, direction_arrow_right: { key: 'right', target: this.build_input.direction_arrow, fn: this.build_input.direction_arrow.right, ignore_with_input: true, }, direction_arrow_down: { key: 'down', target: this.build_input.direction_arrow, fn: this.build_input.direction_arrow.down, ignore_with_input: true,